From 53a7e200e959a35ff1c254ac423f0136161f7ab6 Mon Sep 17 00:00:00 2001 From: Shigma Date: Sun, 3 Dec 2023 02:44:42 +0800 Subject: [PATCH] feat(element): support {#each} --- packages/element/src/index.ts | 115 ++++++++++++------------- packages/element/tests/segment.spec.ts | 24 ++++-- 2 files changed, 71 insertions(+), 68 deletions(-) diff --git a/packages/element/src/index.ts b/packages/element/src/index.ts index 09194355..34e5b7f3 100644 --- a/packages/element/src/index.ts +++ b/packages/element/src/index.ts @@ -259,6 +259,7 @@ namespace Element { position: Position source: string extra: string + children?: Dict<(string | Token)[]> } export function parse(source: string, context?: any) { @@ -290,7 +291,7 @@ namespace Element { name, position, source: tagCap.groups.curly, - extra: tagCap.groups.curly.slice(name.length + 2, -1), + extra: tagCap.groups.curly.slice(1 + (tagCap.groups.derivative ?? '').length, -1), }) continue } @@ -310,58 +311,55 @@ namespace Element { .replace(/\s*\n\s*$/, ''))) } - return parseTokens(tokens, context) + return parseTokens(foldTokens(tokens), context) } - export function parseTokens(tokens: (string | Token)[], context?: any) { - const stack = [Element(Fragment)] - - function pushElement(...element: Element[]) { - if (!stack[0].attrs.continuation) { - stack[0].children.push(...element) - } else { - stack[0].attrs[stack[0].attrs.continuation].push(...element) - } - } - - function rollback(index: number) { - for (; index > 0; index--) { - const { children } = stack.shift() - const { source } = stack[0].children.pop() - pushElement(Element('text', { content: source })) - pushElement(...children) - } + function foldTokens(tokens: (string | Token)[]) { + const stack: [Token, string][] = [[{ + type: 'angle', + name: Fragment, + position: Position.OPEN, + source: '', + extra: '', + children: { default: [] }, + }, 'default']] + + function pushToken(...tokens: (string | Token)[]) { + const [token, slot] = stack[0] + token.children[slot].push(...tokens) } for (const token of tokens) { if (typeof token === 'string') { - pushElement(Element('text', { content: token })) + pushToken(token) continue } - if (token.position === Position.CONTINUE) { - stack[0].attrs.continuation = token.name - stack[0].attrs[token.name] = [] - } else if (token.position === Position.CLOSE) { - let index = 0 - while (index < stack.length && stack[index].type !== token.name) index++ - if (index === stack.length) { - // no matching open tag - pushElement(Element('text', { content: token.source })) - } else { - rollback(index) - const element = stack.shift() - delete element.source - if (element.type === 'if') { - const { expr } = element.attrs - if (evaluate(expr, context)) { - pushElement(...element.children) - } else { - pushElement(...element.attrs.else ?? []) - } - } + const { name, position } = token + if (position === Position.CLOSE) { + if (stack[0][0].name === name) { + stack.shift() } + } else if (position === Position.CONTINUE) { + stack[0][0].children[name] = [] + stack[0][1] = name + } else if (position === Position.OPEN) { + pushToken(token) + token.children = { default: [] } + stack.unshift([token, 'default']) } else { - // OPEN | EMPTY + pushToken(token) + } + } + + return stack[stack.length - 1][0].children.default + } + + function parseTokens(tokens: (string | Token)[], context?: any) { + const result: Element[] = [] + for (const token of tokens) { + if (typeof token === 'string') { + result.push(Element('text', { content: token })) + } else if (token.type === 'angle') { const attrs = {} const attrRegExp = context ? attrRegExp2 : attrRegExp1 let attrCap: RegExpExecArray @@ -377,26 +375,25 @@ namespace Element { attrs[key] = true } } - if (token.type === 'angle') { - const element = Element(token.name, attrs) - pushElement(element) - if (token.position === Position.OPEN) { - element.source = token.source - stack.unshift(element) - } + result.push(Element(token.name, attrs, token.children && parseTokens(token.children.default, context))) + } else if (!token.name) { + result.push(...toElementArray(interpolate(token.extra, context))) + } else if (token.name === 'if') { + if (evaluate(token.extra, context)) { + result.push(...parseTokens(token.children.default, context)) } else { - if (token.name === '') { - pushElement(...Element.toElementArray(interpolate(token.source.slice(1, -1), context))) - } else if (token.name === 'if') { - const element = Element('if', { expr: token.extra }) - element.source = token.source - stack.unshift(element) - } + result.push(...parseTokens(token.children.else || [], context)) + } + } else if (token.name === 'each') { + const [expr, ident] = token.extra.split(/\s+as\s+/) + const items = interpolate(expr, context) + if (!items || !items[Symbol.iterator]) continue + for (const item of items) { + result.push(...parseTokens(token.children.default, { ...context, [ident]: item })) } } } - rollback(stack.length - 1) - return stack[0].children + return result } function visit(element: Element, rules: Visitor, session: S) { diff --git a/packages/element/tests/segment.spec.ts b/packages/element/tests/segment.spec.ts index 6b01e320..b9958e98 100644 --- a/packages/element/tests/segment.spec.ts +++ b/packages/element/tests/segment.spec.ts @@ -26,12 +26,21 @@ describe('Element API', () => { }) it('mismatched tags', () => { - expect(Element.parse('123').join('')).to.equal('1<foo>2<bar attr>3') - expect(Element.parse('1234').join('')).to.equal('12<bar>34') + expect(Element.parse('123').join('')).to.equal('123') expect(Element.parse('14').join('')).to.equal('14') - expect(Element.parse('14').join('')).to.equal('1</foo>4') + expect(Element.parse('14').join('')).to.equal('14') }) + it('whitespace', () => { + expect(Element.parse(`<> + 1 + + 2 + `).join('')).to.equal('') + }) + }) + + describe('Interpolation', () => { it('interpolate', () => { expect(Element.parse('1{foo}1', { foo: 233, bar: 666 })) .to.deep.equal([Element('tag', { bar: 666 }, '1', '233', '1')]) @@ -48,12 +57,9 @@ describe('Element API', () => { .to.deep.equal([Element('p', 'negative')]) }) - it('whitespace', () => { - expect(Element.parse(`<> - 1 - - 2 - `).join('')).to.equal('') + it('#each', () => { + expect(Element.parse('{#each arr as i}{i ** 2}{/each}', { arr: [1, 2, 3] })) + .to.deep.equal([Element.text('1'), Element.text('4'), Element.text('9')]) }) })