From 9636b00670b06c34623e3141fffd887bed0e16eb Mon Sep 17 00:00:00 2001 From: urain39 Date: Sat, 23 Nov 2019 17:13:46 +0800 Subject: [PATCH 1/6] design: keep END token --- lib/parser.ts | 15 ++++++++++++--- lib/renderer.ts | 23 ++++++++++++++--------- lib/tokenizer.ts | 2 +- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/lib/parser.ts b/lib/parser.ts index 93f56dd..a28331e 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -5,13 +5,13 @@ import { Renderer } from './renderer'; function buildTree(tokens: Token[]): Token[] { let type_: TokenType, - value: string, + value: string | undefined, section: Token | undefined, sections: Token[] = [], treeRoot: Token[] = [], collector: Token[] = treeRoot; - for (let token of tokens) { + for (const token of tokens) { type_ = token[TokenMember.TYPE]; value = token[TokenMember.VALUE]; @@ -27,6 +27,7 @@ function buildTree(tokens: Token[]): Token[] { // Initialize section block collector = section[TokenMember.BLOCK] = []; break; + // Switch section block case TokenType.ELSE: section = sections[sections.length - 1]; @@ -34,9 +35,13 @@ function buildTree(tokens: Token[]): Token[] { if (!section || section[TokenMember.TYPE] !== TokenType.IF || value !== section[TokenMember.VALUE]) throw new SyntaxError(`Unexpected token ''`); + // Mark section ended(for context.pop) + collector.push([TokenType.END, undefined, undefined, undefined]); + // Switch the block to else block collector = section[TokenMember.ELSE_BLOCK] = []; break; + // Leave a section case TokenType.END: section = sections.pop(); @@ -48,6 +53,9 @@ function buildTree(tokens: Token[]): Token[] { if ((section as Token)[TokenMember.ELSE_BLOCK] instanceof Array && (section[TokenMember.ELSE_BLOCK] as Token[]).length > 0) section[TokenMember.TYPE] = TokenType.ELSE; + // Mark section ended(for context.pop) + collector.push([TokenType.END, undefined, undefined, undefined]); + // Re-bind block to parent block if (sections.length > 0) collector = ((section = (sections[sections.length - 1] as Token), section[TokenMember.ELSE_BLOCK] instanceof Array) ? @@ -55,13 +63,14 @@ function buildTree(tokens: Token[]): Token[] { else collector = treeRoot; break; + // Text or Formatter default: collector.push(token); } } if (sections.length > 0) { - section = (sections.pop() as Token); + section = sections.pop() as Token; type_ = section[TokenMember.TYPE]; value = section[TokenMember.VALUE]; diff --git a/lib/renderer.ts b/lib/renderer.ts index 4147fd3..1f64fcb 100644 --- a/lib/renderer.ts +++ b/lib/renderer.ts @@ -35,23 +35,23 @@ export class Context { // No cached record found if (!found) { - let key: string, + let name_: string, keys: string[] = name.split('.'); - key = keys[0]; + name_ = keys[0]; keys = keys.slice(1); // Try to look up the name in data for (let context: Context | undefined = this; context; context = context.parent) { - // Find out which context contains key - if (context.data.hasOwnProperty(key)) { - value = context.data[key]; + // Find out which context contains name + if (context.data.hasOwnProperty(name_)) { + value = context.data[name_]; break; } } // Resolve properties - for (key of keys) { + for (const key of keys) { if (value instanceof Object && value.hasOwnProperty(key)) { value = value[key]; } else { @@ -70,6 +70,10 @@ export class Context { public push(data: Map): Context { return new Context(data, this); } + + public pop(): Context | undefined { + return this.parent; + } } export class Renderer { @@ -80,9 +84,10 @@ export class Renderer { } public render(data: Map): any { - let value: any = null, - context = new Context(data); + let value: any, + token: Token, + buffer: string[], + context: Context = new Context(data); - value = context.resolve(''); } } diff --git a/lib/tokenizer.ts b/lib/tokenizer.ts index b513aff..ee7909d 100644 --- a/lib/tokenizer.ts +++ b/lib/tokenizer.ts @@ -27,7 +27,7 @@ let TokenTypeMap = { // See https://github.com/microsoft/TypeScript/pull/33050 // https://stackoverflow.com/questions/47842266/recursive-types-in-typescript -type TokenTuple = [TokenType, string, T[] | undefined, T[] | undefined]; +type TokenTuple = [TokenType, string | undefined, T[] | undefined, T[] | undefined]; export interface Token extends TokenTuple {} export function tokenize(source: string, prefix: string, suffix: string): Token[] { From 84e328e8c7781caba6d312ceed89cca5fec3eaec Mon Sep 17 00:00:00 2001 From: urain39 Date: Sat, 23 Nov 2019 19:37:47 +0800 Subject: [PATCH 2/6] design: remove push & pop method of Context --- lib/parser.ts | 6 ------ lib/renderer.ts | 26 +++++++------------------- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/lib/parser.ts b/lib/parser.ts index a28331e..827f5a0 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -35,9 +35,6 @@ function buildTree(tokens: Token[]): Token[] { if (!section || section[TokenMember.TYPE] !== TokenType.IF || value !== section[TokenMember.VALUE]) throw new SyntaxError(`Unexpected token ''`); - // Mark section ended(for context.pop) - collector.push([TokenType.END, undefined, undefined, undefined]); - // Switch the block to else block collector = section[TokenMember.ELSE_BLOCK] = []; break; @@ -53,9 +50,6 @@ function buildTree(tokens: Token[]): Token[] { if ((section as Token)[TokenMember.ELSE_BLOCK] instanceof Array && (section[TokenMember.ELSE_BLOCK] as Token[]).length > 0) section[TokenMember.TYPE] = TokenType.ELSE; - // Mark section ended(for context.pop) - collector.push([TokenType.END, undefined, undefined, undefined]); - // Re-bind block to parent block if (sections.length > 0) collector = ((section = (sections[sections.length - 1] as Token), section[TokenMember.ELSE_BLOCK] instanceof Array) ? diff --git a/lib/renderer.ts b/lib/renderer.ts index 1f64fcb..33d6d67 100644 --- a/lib/renderer.ts +++ b/lib/renderer.ts @@ -36,10 +36,10 @@ export class Context { // No cached record found if (!found) { let name_: string, - keys: string[] = name.split('.'); + properties: string[] = name.split('.'); - name_ = keys[0]; - keys = keys.slice(1); + name_ = properties[0]; + properties = properties.slice(1); // Try to look up the name in data for (let context: Context | undefined = this; context; context = context.parent) { @@ -51,9 +51,9 @@ export class Context { } // Resolve properties - for (const key of keys) { - if (value instanceof Object && value.hasOwnProperty(key)) { - value = value[key]; + for (const property of properties) { + if (value instanceof Object && value.hasOwnProperty(property)) { + value = value[property]; } else { value = null // Reset value break; @@ -66,14 +66,6 @@ export class Context { return value; } - - public push(data: Map): Context { - return new Context(data, this); - } - - public pop(): Context | undefined { - return this.parent; - } } export class Renderer { @@ -84,10 +76,6 @@ export class Renderer { } public render(data: Map): any { - let value: any, - token: Token, - buffer: string[], - context: Context = new Context(data); - + // TODO: } } From c34bb62bb578ac2d7759c32299e40790763a6709 Mon Sep 17 00:00:00 2001 From: urain39 Date: Sat, 23 Nov 2019 19:51:08 +0800 Subject: [PATCH 3/6] annotation: remove undefined for VALUE member of Token --- lib/parser.ts | 2 +- lib/tokenizer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/parser.ts b/lib/parser.ts index 827f5a0..266349f 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -5,7 +5,7 @@ import { Renderer } from './renderer'; function buildTree(tokens: Token[]): Token[] { let type_: TokenType, - value: string | undefined, + value: string, section: Token | undefined, sections: Token[] = [], treeRoot: Token[] = [], diff --git a/lib/tokenizer.ts b/lib/tokenizer.ts index ee7909d..b513aff 100644 --- a/lib/tokenizer.ts +++ b/lib/tokenizer.ts @@ -27,7 +27,7 @@ let TokenTypeMap = { // See https://github.com/microsoft/TypeScript/pull/33050 // https://stackoverflow.com/questions/47842266/recursive-types-in-typescript -type TokenTuple = [TokenType, string | undefined, T[] | undefined, T[] | undefined]; +type TokenTuple = [TokenType, string, T[] | undefined, T[] | undefined]; export interface Token extends TokenTuple {} export function tokenize(source: string, prefix: string, suffix: string): Token[] { From ffaeb3903b68fb1a8a4b5853ee9057620172dfda Mon Sep 17 00:00:00 2001 From: urain39 Date: Sat, 23 Nov 2019 23:47:22 +0800 Subject: [PATCH 4/6] lib/renderer.ts: implement dirty renderer --- lib/parser.ts | 28 ++++++++-------- lib/renderer.ts | 86 ++++++++++++++++++++++++++++++++++++++++++++++-- lib/tokenizer.ts | 10 +++--- 3 files changed, 102 insertions(+), 22 deletions(-) diff --git a/lib/parser.ts b/lib/parser.ts index 266349f..aa455af 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -1,21 +1,21 @@ // Copyright (c) 2018-2019 urain39 -import { Token, TokenMember, TokenType, tokenize } from './tokenizer'; +import { + Token, + TokenType, + TokenMember, + tokenize +} from './tokenizer'; import { Renderer } from './renderer'; function buildTree(tokens: Token[]): Token[] { - let type_: TokenType, - value: string, - section: Token | undefined, + let section: Token | undefined, sections: Token[] = [], treeRoot: Token[] = [], collector: Token[] = treeRoot; for (const token of tokens) { - type_ = token[TokenMember.TYPE]; - value = token[TokenMember.VALUE]; - - switch (type_) { + switch (token[TokenMember.TYPE]) { // Enter a section case TokenType.IF: case TokenType.NOT: @@ -32,8 +32,8 @@ function buildTree(tokens: Token[]): Token[] { section = sections[sections.length - 1]; // Check current(top) section is valid? - if (!section || section[TokenMember.TYPE] !== TokenType.IF || value !== section[TokenMember.VALUE]) - throw new SyntaxError(`Unexpected token ''`); + if (!section || section[TokenMember.TYPE] !== TokenType.IF || token[TokenMember.VALUE] !== section[TokenMember.VALUE]) + throw new SyntaxError(`Unexpected token ''`); // Switch the block to else block collector = section[TokenMember.ELSE_BLOCK] = []; @@ -43,8 +43,8 @@ function buildTree(tokens: Token[]): Token[] { section = sections.pop(); // Check if section is not match - if (!section || value !== section[TokenMember.VALUE]) - throw new SyntaxError(`Unexpected token ''`); + if (!section || token[TokenMember.VALUE] !== section[TokenMember.VALUE]) + throw new SyntaxError(`Unexpected token ''`); // Change type of which section contains else block if ((section as Token)[TokenMember.ELSE_BLOCK] instanceof Array && (section[TokenMember.ELSE_BLOCK] as Token[]).length > 0) @@ -65,10 +65,8 @@ function buildTree(tokens: Token[]): Token[] { if (sections.length > 0) { section = sections.pop() as Token; - type_ = section[TokenMember.TYPE]; - value = section[TokenMember.VALUE]; - throw new SyntaxError(`No match section ''`); + throw new SyntaxError(`No match section ''`); } return treeRoot; diff --git a/lib/renderer.ts b/lib/renderer.ts index 33d6d67..b8ba9a6 100644 --- a/lib/renderer.ts +++ b/lib/renderer.ts @@ -75,7 +75,89 @@ export class Renderer { this.treeRoot = treeRoot; } - public render(data: Map): any { - // TODO: + public renderTree(treeRoot: Token[], context: Context): string[] { + let value: any, + buffer: string[] = []; + + for (const token of treeRoot) { + switch (token[TokenMember.TYPE]) { + case TokenType.IF: + value = context.resolve(token[TokenMember.VALUE]); + + if (!value) + continue; + + if (value instanceof Array) + for (const v of value) + buffer.push(this.renderTree( + token[TokenMember.BLOCK] as Token[], + new Context(v, context) + ).join('')); + else + buffer.push(this.renderTree( + token[TokenMember.BLOCK] as Token[], + context + ).join('')); + break; + case TokenType.NOT: + value = context.resolve(token[TokenMember.VALUE]); + + if (value) + continue; + + buffer.push(this.renderTree( + token[TokenMember.BLOCK] as Token[], + context + ).join('')); + break; + case TokenType.ELSE: + value = context.resolve(token[TokenMember.VALUE]); + + if (value) { + if (value instanceof Array) + for (const v of value) + buffer.push(this.renderTree( + token[TokenMember.BLOCK] as Token[], + new Context(v, context) + ).join('')); + else + buffer.push(this.renderTree( + token[TokenMember.BLOCK] as Token[], + context + ).join('')); + } else { + buffer.push(this.renderTree( + token[TokenMember.ELSE_BLOCK] as Token[], + context + ).join('')); + } + break; + case TokenType.TEXT: + buffer.push( + token[TokenMember.VALUE] + ); + break; + case TokenType.FORMAT: + buffer.push(context.resolve( + token[TokenMember.VALUE] + )); + break; + // TODO: escapeHTML + case TokenType.FORMAT_ESCAPE: + buffer.push(context.resolve( + token[TokenMember.VALUE] + )); + break; + } + } + + return buffer; + } + + public render(data: Map): string { + return this.renderTree( + this.treeRoot, + new Context(data) + ).join(''); } } diff --git a/lib/tokenizer.ts b/lib/tokenizer.ts index b513aff..b4d3238 100644 --- a/lib/tokenizer.ts +++ b/lib/tokenizer.ts @@ -17,6 +17,11 @@ export const enum TokenType { FORMAT_ESCAPE } +// See https://github.com/microsoft/TypeScript/pull/33050 +// https://stackoverflow.com/questions/47842266/recursive-types-in-typescript +type TokenTuple = [TokenType, string, T[] | undefined, T[] | undefined]; +export interface Token extends TokenTuple {} + let TokenTypeMap = { '?': TokenType.IF, '!': TokenType.NOT, @@ -25,11 +30,6 @@ let TokenTypeMap = { '#': TokenType.FORMAT }; -// See https://github.com/microsoft/TypeScript/pull/33050 -// https://stackoverflow.com/questions/47842266/recursive-types-in-typescript -type TokenTuple = [TokenType, string, T[] | undefined, T[] | undefined]; -export interface Token extends TokenTuple {} - export function tokenize(source: string, prefix: string, suffix: string): Token[] { let type_: string, value: string, From 0a29d1ffde49213b8734a2dd010c119ca95284c1 Mon Sep 17 00:00:00 2001 From: urain39 Date: Sun, 24 Nov 2019 01:25:03 +0800 Subject: [PATCH 5/6] lib/renderer.ts: fix Context crash when data is null or undefined --- lib/renderer.ts | 13 ++++++------- tests/.gitkeep | 0 2 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 tests/.gitkeep diff --git a/lib/renderer.ts b/lib/renderer.ts index b8ba9a6..2ab3920 100644 --- a/lib/renderer.ts +++ b/lib/renderer.ts @@ -44,7 +44,7 @@ export class Context { // Try to look up the name in data for (let context: Context | undefined = this; context; context = context.parent) { // Find out which context contains name - if (context.data.hasOwnProperty(name_)) { + if (context.data && context.data.hasOwnProperty && context.data.hasOwnProperty(name_)) { value = context.data[name_]; break; } @@ -88,10 +88,10 @@ export class Renderer { continue; if (value instanceof Array) - for (const v of value) + for (const value_ of value) buffer.push(this.renderTree( token[TokenMember.BLOCK] as Token[], - new Context(v, context) + new Context(value_, context) ).join('')); else buffer.push(this.renderTree( @@ -115,10 +115,10 @@ export class Renderer { if (value) { if (value instanceof Array) - for (const v of value) + for (const value_ of value) buffer.push(this.renderTree( token[TokenMember.BLOCK] as Token[], - new Context(v, context) + new Context(value_, context) ).join('')); else buffer.push(this.renderTree( @@ -156,8 +156,7 @@ export class Renderer { public render(data: Map): string { return this.renderTree( - this.treeRoot, - new Context(data) + this.treeRoot, new Context(data) ).join(''); } } diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29 From 60836dc53eb6c3c437714795e1e8928f624884d2 Mon Sep 17 00:00:00 2001 From: urain39 Date: Sun, 24 Nov 2019 04:52:33 +0800 Subject: [PATCH 6/6] lib/renderer.ts: use instanceof to check context.data --- lib/renderer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/renderer.ts b/lib/renderer.ts index 2ab3920..ad8d1ac 100644 --- a/lib/renderer.ts +++ b/lib/renderer.ts @@ -44,13 +44,14 @@ export class Context { // Try to look up the name in data for (let context: Context | undefined = this; context; context = context.parent) { // Find out which context contains name - if (context.data && context.data.hasOwnProperty && context.data.hasOwnProperty(name_)) { - value = context.data[name_]; + if (context.data instanceof Object && context.data.hasOwnProperty(name_)) { + value = (context.data as Map)[name_]; break; } } // Resolve properties + // XXX: Should we check value valid at first? for (const property of properties) { if (value instanceof Object && value.hasOwnProperty(property)) { value = value[property];