Skip to content

Commit

Permalink
feat(element): support {#each}
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Dec 2, 2023
1 parent cb7cbec commit 53a7e20
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 68 deletions.
115 changes: 56 additions & 59 deletions packages/element/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ namespace Element {
position: Position
source: string
extra: string
children?: Dict<(string | Token)[]>
}

export function parse(source: string, context?: any) {
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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<S>(element: Element, rules: Visitor<S>, session: S) {
Expand Down
24 changes: 15 additions & 9 deletions packages/element/tests/segment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,21 @@ describe('Element API', () => {
})

it('mismatched tags', () => {
expect(Element.parse('1<foo>2<bar attr>3').join('')).to.equal('1&lt;foo&gt;2&lt;bar attr&gt;3')
expect(Element.parse('1<foo>2<bar>3</foo>4').join('')).to.equal('1<foo>2&lt;bar&gt;3</foo>4')
expect(Element.parse('1<foo>2<bar attr>3').join('')).to.equal('1<foo>2<bar attr>3</bar></foo>')
expect(Element.parse('1<foo/>4').join('')).to.equal('1<foo/>4')
expect(Element.parse('1</foo>4').join('')).to.equal('1&lt;/foo&gt;4')
expect(Element.parse('1</foo>4').join('')).to.equal('14')
})

it('whitespace', () => {
expect(Element.parse(`<>
<foo> 1 </foo>
<!-- comment -->
2
</>`).join('')).to.equal('<template><foo> 1 </foo>2</template>')
})
})

describe('Interpolation', () => {
it('interpolate', () => {
expect(Element.parse('<tag bar={bar}>1{foo}1</tag>', { foo: 233, bar: 666 }))
.to.deep.equal([Element('tag', { bar: 666 }, '1', '233', '1')])
Expand All @@ -48,12 +57,9 @@ describe('Element API', () => {
.to.deep.equal([Element('p', 'negative')])
})

it('whitespace', () => {
expect(Element.parse(`<>
<foo> 1 </foo>
<!-- comment -->
2
</>`).join('')).to.equal('<template><foo> 1 </foo>2</template>')
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')])
})
})

Expand Down

0 comments on commit 53a7e20

Please sign in to comment.