import {
genIf,
genFor,
genData,
genText,
genElement,
genChildren,
CodegenState
} from 'compiler/codegen/index'
import {
genAttrSegments,
genDOMPropSegments,
genClassSegments,
genStyleSegments,
applyModelTransform
} from './modules'
import { escape } from '../util'
import { optimizability } from './optimizer'
import type { CodegenResult } from 'compiler/codegen/index'
import { ASTElement, ASTNode, CompilerOptions } from 'types/compiler'
export type StringSegment = {
type: number
value: string
}
export const RAW = 0
export const INTERPOLATION = 1
export const EXPRESSION = 2
export function generate(
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genSSRElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
function genSSRElement(el: ASTElement, state: CodegenState): string {
if (el.for && !el.forProcessed) {
return genFor(el, state, genSSRElement)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state, genSSRElement)
} else if (el.tag === 'template' && !el.slotTarget) {
return el.ssrOptimizability === optimizability.FULL
? genChildrenAsStringNode(el, state)
: genSSRChildren(el, state) || 'void 0'
}
switch (el.ssrOptimizability) {
case optimizability.FULL:
return genStringElement(el, state)
case optimizability.SELF:
return genStringElementWithChildren(el, state)
case optimizability.CHILDREN:
return genNormalElement(el, state, true)
case optimizability.PARTIAL:
return genNormalElement(el, state, false)
default:
return genElement(el, state)
}
}
function genNormalElement(el, state, stringifyChildren) {
const data = el.plain ? undefined : genData(el, state)
const children = stringifyChildren
? `[${genChildrenAsStringNode(el, state)}]`
: genSSRChildren(el, state, true)
return `_c('${el.tag}'${data ? `,${data}` : ''}${
children ? `,${children}` : ''
})`
}
function genSSRChildren(el, state, checkSkip?: boolean) {
return genChildren(el, state, checkSkip, genSSRElement, genSSRNode)
}
function genSSRNode(el, state) {
return el.type === 1 ? genSSRElement(el, state) : genText(el)
}
function genChildrenAsStringNode(el, state) {
return el.children.length
? `_ssrNode(${flattenSegments(childrenToSegments(el, state))})`
: ''
}
function genStringElement(el, state) {
return `_ssrNode(${elementToString(el, state)})`
}
function genStringElementWithChildren(el, state) {
const children = genSSRChildren(el, state, true)
return `_ssrNode(${flattenSegments(elementToOpenTagSegments(el, state))},"</${
el.tag
}>"${children ? `,${children}` : ''})`
}
function elementToString(el, state) {
return `(${flattenSegments(elementToSegments(el, state))})`
}
function elementToSegments(el, state): Array<StringSegment> {
if (el.for && !el.forProcessed) {
el.forProcessed = true
return [
{
type: EXPRESSION,
value: genFor(el, state, elementToString, '_ssrList')
}
]
} else if (el.if && !el.ifProcessed) {
el.ifProcessed = true
return [
{
type: EXPRESSION,
value: genIf(el, state, elementToString, '"<!---->"')
}
]
} else if (el.tag === 'template') {
return childrenToSegments(el, state)
}
const openSegments = elementToOpenTagSegments(el, state)
const childrenSegments = childrenToSegments(el, state)
const { isUnaryTag } = state.options
const close =
isUnaryTag && isUnaryTag(el.tag)
? []
: [{ type: RAW, value: `</${el.tag}>` }]
return openSegments.concat(childrenSegments, close)
}
function elementToOpenTagSegments(el, state): Array<StringSegment> {
applyModelTransform(el, state)
let binding
const segments = [{ type: RAW, value: `<${el.tag}` }]
if (el.attrs) {
segments.push.apply(segments, genAttrSegments(el.attrs))
}
if (el.props) {
segments.push.apply(segments, genDOMPropSegments(el.props, el.attrs))
}
if ((binding = el.attrsMap['v-bind'])) {
segments.push({ type: EXPRESSION, value: `_ssrAttrs(${binding})` })
}
if ((binding = el.attrsMap['v-bind.prop'])) {
segments.push({ type: EXPRESSION, value: `_ssrDOMProps(${binding})` })
}
if (el.staticClass || el.classBinding) {
segments.push.apply(
segments,
genClassSegments(el.staticClass, el.classBinding)
)
}
if (el.staticStyle || el.styleBinding || el.attrsMap['v-show']) {
segments.push.apply(
segments,
genStyleSegments(
el.attrsMap.style,
el.staticStyle,
el.styleBinding,
el.attrsMap['v-show']
)
)
}
if (state.options.scopeId) {
segments.push({ type: RAW, value: ` ${state.options.scopeId}` })
}
segments.push({ type: RAW, value: `>` })
return segments
}
function childrenToSegments(el, state): Array<StringSegment> {
let binding
if ((binding = el.attrsMap['v-html'])) {
return [{ type: EXPRESSION, value: `_s(${binding})` }]
}
if ((binding = el.attrsMap['v-text'])) {
return [{ type: INTERPOLATION, value: `_s(${binding})` }]
}
if (el.tag === 'textarea' && (binding = el.attrsMap['v-model'])) {
return [{ type: INTERPOLATION, value: `_s(${binding})` }]
}
return el.children ? nodesToSegments(el.children, state) : []
}
function nodesToSegments(
children: Array<ASTNode>,
state: CodegenState
): Array<StringSegment> {
const segments: StringSegment[] = []
for (let i = 0; i < children.length; i++) {
const c = children[i]
if (c.type === 1) {
segments.push.apply(segments, elementToSegments(c, state))
} else if (c.type === 2) {
segments.push({ type: INTERPOLATION, value: c.expression })
} else if (c.type === 3) {
let text = escape(c.text)
if (c.isComment) {
text = '<!--' + text + '-->'
}
segments.push({ type: RAW, value: text })
}
}
return segments
}
function flattenSegments(segments: Array<StringSegment>): string {
const mergedSegments: string[] = []
let textBuffer = ''
const pushBuffer = () => {
if (textBuffer) {
mergedSegments.push(JSON.stringify(textBuffer))
textBuffer = ''
}
}
for (let i = 0; i < segments.length; i++) {
const s = segments[i]
if (s.type === RAW) {
textBuffer += s.value
} else if (s.type === INTERPOLATION) {
pushBuffer()
mergedSegments.push(`_ssrEscape(${s.value})`)
} else if (s.type === EXPRESSION) {
pushBuffer()
mergedSegments.push(`(${s.value})`)
}
}
pushBuffer()
return mergedSegments.join('+')
}