Skip to content

Commit

Permalink
feat: 支持根元素为组件,#58
Browse files Browse the repository at this point in the history
遗留问题:

- 编译期检测:根元素使用了 s-if、s-for、fragment 等情况
- 反解:s-data 目前实现为子组件的数据,待确认 baidu/san#498
  • Loading branch information
harttle committed Jun 1, 2020
1 parent 272adce commit 0602a77
Show file tree
Hide file tree
Showing 15 changed files with 167 additions and 79 deletions.
2 changes: 2 additions & 0 deletions src/models/component-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ export class ComponentInfo {
// Raw components
public readonly childComponentClasses: Components
public component: CompiledComponent<{}>
public readonly rootANode: ANode

constructor ({ filters, computed, template, cid, componentClass, children = [], childComponentClasses, component }: ComponentInfoOptions) {
this.component = component
this.rootANode = component.aNode
this.filters = filters
this.computed = computed
this.template = template
Expand Down
68 changes: 35 additions & 33 deletions src/target-js/compilers/anode-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,30 @@ import { getANodeProps, getANodePropByName } from '../../utils/anode-util'
import * as TypeGuards from '../../utils/type-guards'

/**
* ANode 的编译方法集合对象
*/
* ANode 编译
*
* 负责单个 ComponentClass 的编译,每个 ANodeCompiler 对应于一个 ComponentInfo。
*/
export class ANodeCompiler {
private ssrIndex = 0
private elementCompiler: ElementCompiler

constructor (
private owner: ComponentInfo,
private root: ComponentTree,
private tree: ComponentTree,
noTemplateOutput: boolean,
public emitter: JSEmitter
) {
this.elementCompiler = new ElementCompiler(
owner,
root,
tree,
noTemplateOutput,
this,
emitter
)
}

compile (aNode: ANode, parentNode: ANode) {
compile (aNode: ANode, parentNode: ANode | undefined, needOutputData: boolean) {
if (TypeGuards.isATextNode(aNode)) return this.compileText(aNode, parentNode)
if (TypeGuards.isAIfNode(aNode)) return this.compileIf(aNode)
if (TypeGuards.isAForNode(aNode)) return this.compileFor(aNode)
Expand All @@ -40,48 +42,48 @@ export class ANodeCompiler {

const ComponentClass = this.owner.getChildComponentClass(aNode)
if (ComponentClass) {
const info = this.root.addComponentClass(ComponentClass)
return info ? this.compileComponent(aNode, info) : undefined
const info = this.tree.addComponentClass(ComponentClass)
return info ? this.compileComponent(aNode, info, needOutputData) : undefined
}
return this.compileElement(aNode)
return this.compileElement(aNode, needOutputData)
}

compileText (aNode: ATextNode, parentNode: ANode) {
private compileText (aNode: ATextNode, parentNode: ANode | undefined) {
const { emitter } = this
if (TypeGuards.isAFragmentNode(parentNode) && parentNode.children[0] === aNode) {
emitter.bufferHTMLLiteral('<!--s-frag-->')
if (parentNode && TypeGuards.isAFragmentNode(parentNode) && parentNode.children[0] === aNode) {
emitter.writeHTMLLiteral('<!--s-frag-->')
}
if (aNode.textExpr.original) {
emitter.writeIf('!noDataOutput', () => {
emitter.bufferHTMLLiteral('<!--s-text-->')
emitter.writeHTMLLiteral('<!--s-text-->')
})
}

if (aNode.textExpr.value != null) {
emitter.bufferHTMLLiteral((aNode.textExpr.segs[0] as ExprStringNode).literal!)
emitter.writeHTMLLiteral((aNode.textExpr.segs[0] as ExprStringNode).literal!)
} else {
emitter.writeHTML(expr(aNode.textExpr))
emitter.writeHTMLExpression(expr(aNode.textExpr))
}

if (aNode.textExpr.original) {
emitter.writeIf('!noDataOutput', () => {
emitter.bufferHTMLLiteral('<!--/s-text-->')
emitter.writeHTMLLiteral('<!--/s-text-->')
})
}
if (TypeGuards.isAFragmentNode(parentNode) && parentNode.children[parentNode.children.length - 1] === aNode) {
emitter.bufferHTMLLiteral('<!--s-frag-->')
if (parentNode && TypeGuards.isAFragmentNode(parentNode) && parentNode.children[parentNode.children.length - 1] === aNode) {
emitter.writeHTMLLiteral('<!--s-frag-->')
}
}

compileTemplate (aNode: ATemplateNode | AFragmentNode) {
this.elementCompiler.inner(aNode)
}

compileIf (aNode: AIfNode) {
private compileIf (aNode: AIfNode) {
const { emitter } = this
// output if
const ifDirective = aNode.directives['if'] // eslint-disable-line dot-notation
emitter.writeIf(expr(ifDirective.value), () => this.compile(aNode.ifRinsed, aNode))
emitter.writeIf(expr(ifDirective.value), () => this.compile(aNode.ifRinsed, aNode, false))

// output elif and else
for (const elseANode of aNode.elses || []) {
Expand All @@ -92,7 +94,7 @@ export class ANodeCompiler {
emitter.writeLine('else {')
}
emitter.indent()
this.compile(elseANode, aNode)
this.compile(elseANode, aNode, false)
emitter.unindent()
emitter.writeLine('}')
}
Expand Down Expand Up @@ -122,7 +124,7 @@ export class ANodeCompiler {
indexName + '++', () => {
emitter.writeLine('ctx.data.' + indexName + '=' + indexName + ';')
emitter.writeLine('ctx.data.' + itemName + '= ' + listName + '[' + indexName + '];')
this.compile(forElementANode, aNode)
this.compile(forElementANode, aNode, false)
})
})

Expand All @@ -132,7 +134,7 @@ export class ANodeCompiler {
emitter.writeIf(listName + '[' + indexName + '] != null', () => {
emitter.writeLine('ctx.data.' + indexName + '=' + indexName + ';')
emitter.writeLine('ctx.data.' + itemName + '= ' + listName + '[' + indexName + '];')
this.compile(forElementANode, aNode)
this.compile(forElementANode, aNode, false)
})
})
emitter.endIf()
Expand All @@ -153,7 +155,7 @@ export class ANodeCompiler {
emitter.writeFunction('$defaultSlotRender', ['ctx', 'currentCtx'], () => {
emitter.writeLine('var html = "";')
for (const aNodeChild of aNode.children) {
this.compile(aNodeChild, aNode)
this.compile(aNodeChild, aNode, false)
}
emitter.writeLine('return html;')
})
Expand Down Expand Up @@ -207,19 +209,18 @@ export class ANodeCompiler {
emitter.writeLine('ctx.slotRenderers.' + rendererId + '();')
}

compileElement (aNode: ANode, emitData = false) {
private compileElement (aNode: ANode, needOutputData: boolean) {
this.elementCompiler.tagStart(aNode)
if (emitData) {
this.emitter.writeIf(
'!noDataOutput',
() => this.emitter.writeDataComment()
)
}
if (needOutputData) this.outputData()
this.elementCompiler.inner(aNode)
this.elementCompiler.tagEnd(aNode)
}

private compileComponent (aNode: ANode, info: ComponentInfo) {
private outputData () {
this.emitter.writeIf('!noDataOutput', () => this.emitter.writeDataComment())
}

private compileComponent (aNode: ANode, info: ComponentInfo, needOutputData: boolean) {
const { emitter } = this

const defaultSourceSlots: ANode[] = []
Expand Down Expand Up @@ -253,9 +254,10 @@ export class ANodeCompiler {
emitter.writeLine(', ' + expr(sourceSlotCode.prop.expr) + ']);')
}

const ndo = needOutputData ? 'noDataOutput' : 'true'
const funcName = 'sanssrRuntime.renderer' + info.cid
emitter.nextLine(`html += ${funcName}(`)
emitter.write(this.componentDataCode(aNode) + ', true, sanssrRuntime, ctx, currentCtx, ' +
emitter.write(this.componentDataCode(aNode) + `, ${ndo}, sanssrRuntime, ctx, currentCtx, ` +
stringifier.str(aNode.tagName) + ', $sourceSlots);')
emitter.writeLine('$sourceSlots = null;')
}
Expand All @@ -264,7 +266,7 @@ export class ANodeCompiler {
const { emitter } = this
emitter.writeAnonymousFunction(['ctx', 'currentCtx'], () => {
emitter.writeLine('var html = "";')
for (const slot of slots) this.compile(slot, parentANode)
for (const slot of slots) this.compile(slot, parentANode, false)
emitter.writeLine('return html;')
})
}
Expand Down
54 changes: 23 additions & 31 deletions src/target-js/compilers/element-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,11 @@ export class ElementCompiler {
root: ComponentTree,
private noTemplateOutput: boolean,
private aNodeCompiler: ANodeCompiler,
public emitter: JSEmitter = new JSEmitter()
private emitter: JSEmitter = new JSEmitter()
) {}

/**
* 编译元素标签头
*
* @param aNode 抽象节点
* @param tagNameVariable 组件标签为外部动态传入时的标签变量名
*/
tagStart (aNode: ANode) {
const props = aNode.props
Expand All @@ -33,14 +30,13 @@ export class ElementCompiler {
const { emitter } = this

// element start '<'
if (tagName === 'fragment') return
if (tagName) {
emitter.bufferHTMLLiteral('<' + tagName)
emitter.writeHTMLLiteral('<' + tagName)
} else if (this.noTemplateOutput) {
return
} else {
emitter.bufferHTMLLiteral('<')
emitter.writeHTML('tagName || "div"')
emitter.writeHTMLLiteral('<')
emitter.writeHTMLExpression('tagName || "div"')
}

// element properties
Expand All @@ -50,17 +46,17 @@ export class ElementCompiler {
if (bindDirective) this.compileBindProperties(tagName, bindDirective)

// element end '>'
emitter.bufferHTMLLiteral('>')
emitter.writeHTMLLiteral('>')
}

private compileProperty (tagName: string, prop: ANodeProperty, propsIndex: { [key: string]: ANodeProperty }) {
const { name } = prop
const { emitter } = this
if (name === 'slot') return

if (isExprBoolNode(prop.expr)) return emitter.bufferHTMLLiteral(' ' + name)
if (isExprStringNode(prop.expr)) return emitter.bufferHTMLLiteral(` ${name}="${prop.expr.literal}"`)
if (prop.expr.value != null) return emitter.bufferHTMLLiteral(` ${name}="${expr(prop.expr)}"`)
if (isExprBoolNode(prop.expr)) return emitter.writeHTMLLiteral(' ' + name)
if (isExprStringNode(prop.expr)) return emitter.writeHTMLLiteral(` ${name}="${prop.expr.literal}"`)
if (prop.expr.value != null) return emitter.writeHTMLLiteral(` ${name}="${expr(prop.expr)}"`)

if (name === 'value') {
if (tagName === 'textarea') return
Expand All @@ -70,17 +66,17 @@ export class ElementCompiler {
if (tagName === 'option') {
emitter.writeLine(`$optionValue = ${expr(prop.expr)};`)
emitter.writeIf('$optionValue != null', () => {
emitter.writeHTML('" value=\\"" + $optionValue + "\\""') // value attr
emitter.writeHTMLExpression('" value=\\"" + $optionValue + "\\""') // value attr
})
emitter.writeIf('$optionValue === $selectValue', () => {
emitter.bufferHTMLLiteral(' selected') // selected attr
emitter.writeHTMLLiteral(' selected') // selected attr
})
return
}
}

if (name === 'readonly' || name === 'disabled' || name === 'multiple') {
return emitter.writeHTML(`_.boolAttrFilter("${name}", ${expr(prop.expr)})`)
return emitter.writeHTMLExpression(`_.boolAttrFilter("${name}", ${expr(prop.expr)})`)
}

const valueProp = propsIndex['value']
Expand All @@ -89,18 +85,18 @@ export class ElementCompiler {
switch (inputType.raw) {
case 'checkbox':
return emitter.writeIf(`_.includes(${expr(prop.expr)}, ${expr(valueProp.expr)})`, () => {
emitter.bufferHTMLLiteral(' checked')
emitter.writeHTMLLiteral(' checked')
})
case 'radio':
return emitter.writeIf(`${expr(prop.expr)} === ${expr(valueProp.expr)}`, () => {
emitter.bufferHTMLLiteral(' checked')
emitter.writeHTMLLiteral(' checked')
})
}
}

const onlyOneAccessor = prop.expr.type === ExprType.ACCESSOR
const escp = (prop.x || onlyOneAccessor ? ', true' : '')
emitter.writeHTML(`_.attrFilter("${name}", ${expr(prop.expr)}${escp})`)
emitter.writeHTMLExpression(`_.attrFilter("${name}", ${expr(prop.expr)}${escp})`)
}

private compileBindProperties (tagName: string, bindDirective: Directive<any>) {
Expand All @@ -116,11 +112,11 @@ export class ElementCompiler {
emitter.writeCase('"disabled"')
emitter.writeCase('"multiple"')
emitter.writeCase('"checked"', () => {
emitter.writeHTML('_.boolAttrFilter($key, $value)')
emitter.writeHTMLExpression('_.boolAttrFilter($key, $value)')
emitter.writeBreak()
})
emitter.writeDefault(() => {
emitter.writeHTML('_.attrFilter($key, $value, true)')
emitter.writeHTMLExpression('_.attrFilter($key, $value, true)')
})
})
})
Expand All @@ -131,18 +127,14 @@ export class ElementCompiler {

/**
* 编译元素闭合
*
* @param aNode 抽象节点
* @param tagNameVariable 组件标签为外部动态传入时的标签变量名
*/
tagEnd (aNode: ANode) {
const { emitter } = this
const tagName = aNode.tagName

if (tagName === 'fragment') return
if (tagName) {
if (!autoCloseTags.has(tagName)) {
emitter.bufferHTMLLiteral('</' + tagName + '>')
emitter.writeHTMLLiteral('</' + tagName + '>')
}

if (tagName === 'select') {
Expand All @@ -155,9 +147,9 @@ export class ElementCompiler {
} else if (this.noTemplateOutput) {
// noop
} else {
emitter.bufferHTMLLiteral('</')
emitter.writeHTML('tagName || "div"')
emitter.bufferHTMLLiteral('>')
emitter.writeHTMLLiteral('</')
emitter.writeHTMLExpression('tagName || "div"')
emitter.writeHTMLLiteral('>')
}
}

Expand All @@ -169,16 +161,16 @@ export class ElementCompiler {
// inner content
if (aNode.tagName === 'textarea') {
const valueProp = getANodePropByName(aNode, 'value')
if (valueProp) emitter.writeHTML(`_.escapeHTML(${expr(valueProp.expr)})`)
if (valueProp) emitter.writeHTMLExpression(`_.escapeHTML(${expr(valueProp.expr)})`)
return
}

const htmlDirective = aNode.directives.html
if (htmlDirective) {
emitter.writeHTML(expr(htmlDirective.value))
emitter.writeHTMLExpression(expr(htmlDirective.value))
return
}
// only ATextNode#children is not defined, it has been taken over by ANodeCompiler#compileText()
for (const aNodeChild of aNode.children!) this.aNodeCompiler.compile(aNodeChild, aNode)
for (const aNodeChild of aNode.children!) this.aNodeCompiler.compile(aNodeChild, aNode, false)
}
}
2 changes: 1 addition & 1 deletion src/target-js/compilers/renderer-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export class RendererCompiler {
if (ifDirective) emitter.writeLine('if (' + expr(ifDirective.value) + ') {')

const aNodeCompiler = new ANodeCompiler(componentInfo, this.componentTree, this.noTemplateOutput, emitter)
aNodeCompiler.compileElement(component.aNode, true)
aNodeCompiler.compile(component.aNode, undefined, true)

if (ifDirective) emitter.writeLine('}')
emitter.writeLine('return html;')
Expand Down
8 changes: 4 additions & 4 deletions src/target-js/emitters/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@ export class JSEmitter extends Emitter {
return this.defaultWrite(str)
}

public writeHTML (code: string) {
public writeHTMLExpression (code: string) {
this.writeLine(`html += ${code};`)
}

public writeDataComment () {
this.writeHTML('"<!--s-data:" + JSON.stringify(' + dataAccess() + ') + "-->"')
this.writeHTMLExpression(`"<!--s-data:" + JSON.stringify(${dataAccess()}) + "-->"`)
}

public bufferHTMLLiteral (str: string) {
public writeHTMLLiteral (str: string) {
this.buffer += str
}

public clearStringLiteralBuffer () {
if (this.buffer === '') return
const buffer = this.buffer
this.buffer = ''
this.writeHTML(stringLiteralize(buffer))
this.writeHTMLExpression(stringLiteralize(buffer))
}

public writeSwitch (expr: string, body: Function) {
Expand Down
Loading

0 comments on commit 0602a77

Please sign in to comment.