Browse Source

parser for single-file components

dev
Evan You 9 years ago
parent
commit
e236f80fa0
  1. 2
      build/build.js
  2. 36
      flow/compiler.js
  3. 1
      package.json
  4. 6
      src/compiler/error-detector.js
  5. 6
      src/compiler/index.js
  6. 7
      src/compiler/parser/html-parser.js
  7. 70
      src/compiler/parser/sfc-parser.js
  8. 109
      src/entries/web-compiler.js
  9. 2
      src/entries/web-runtime-with-compiler.js
  10. 94
      src/platforms/web/compiler/index.js
  11. 2
      test/unit/modules/compiler/codegen.spec.js
  12. 2
      test/unit/modules/compiler/optimizer.spec.js
  13. 2
      test/unit/modules/compiler/parser.spec.js
  14. 28
      test/unit/modules/compiler/sfc-parser.spec.js

2
build/build.js

@ -65,7 +65,7 @@ var builds = [
{
entry: 'src/entries/web-compiler.js',
format: 'cjs',
external: ['entities'],
external: ['entities', 'de-indent'],
out: 'dist/compiler.js'
},
// Web server renderer (CommonJS).

36
flow/compiler.js

@ -16,6 +16,18 @@ declare type CompilerOptions = {
delimiters?: [string, string] // template delimiters
}
declare type CompiledResult = {
ast: ?ASTElement,
render: string,
staticRenderFns: Array<string>,
errors?: Array<string>
}
declare type CompiledFunctionResult = {
render: Function,
staticRenderFns: Array<Function>
}
declare type ModuleOptions = {
transformNode: (el: ASTElement) => void, // transform an element's AST node
genData: (el: ASTElement) => string, // generate extra data string for an element
@ -112,3 +124,27 @@ declare type ASTText = {
text: string,
static?: boolean
}
// SFC-parser related declarations
declare module 'de-indent' {
declare var exports: {
(str: string): string;
}
}
// an object format describing a single-file component.
declare type SFCDescriptor = {
template: ?SFCBlock,
script: ?SFCBlock,
styles: Array<SFCBlock>
}
declare type SFCBlock = {
type: "template" | "script" | "style",
content: string,
lang?: string,
scoped?: boolean,
src?: boolean,
map?: Object
}

1
package.json

@ -51,6 +51,7 @@
"chromedriver": "^2.21.2",
"codecov.io": "^0.1.6",
"cross-spawn": "^4.0.0",
"de-indent": "^1.0.2",
"entities": "^1.1.1",
"eslint": "^2.11.0",
"eslint-config-vue": "^1.0.3",

6
src/compiler/error-detector.js

@ -3,9 +3,11 @@
import { dirRE } from './parser/index'
// detect problematic expressions in a template
export function detectErrors (ast: ASTNode): Array<string> {
export function detectErrors (ast: ?ASTNode): Array<string> {
const errors: Array<string> = []
checkNode(ast, errors)
if (ast) {
checkNode(ast, errors)
}
return errors
}

6
src/compiler/index.js

@ -10,11 +10,7 @@ import { generate } from './codegen'
export function compile (
template: string,
options: CompilerOptions
): {
ast: ?ASTElement,
render: string,
staticRenderFns: Array<string>
} {
): CompiledResult {
const ast = parse(template.trim(), options)
optimize(ast, options)
const code = generate(ast, options)

7
src/compiler/parser/html-parser.js

@ -10,7 +10,7 @@
*/
import { decodeHTML } from 'entities'
import { makeMap } from 'shared/util'
import { makeMap, no } from 'shared/util'
import { isNonPhrasingTag, canBeLeftOpenTag } from 'web/util/index'
// Regular Expressions for parsing tags and attributes
@ -61,12 +61,13 @@ export function parseHTML (html, handler) {
const stack = []
const attribute = attrForHandler(handler)
const expectHTML = handler.expectHTML
const isUnaryTag = handler.isUnaryTag || (() => false)
const isUnaryTag = handler.isUnaryTag || no
const isSpecialTag = handler.isSpecialTag || special
let last, prevTag, nextTag, lastTag
while (html) {
last = html
// Make sure we're not in a script or style element
if (!lastTag || !special(lastTag)) {
if (!lastTag || !isSpecialTag(lastTag)) {
const textEnd = html.indexOf('<')
if (textEnd === 0) {
// Comment:

70
src/compiler/parser/sfc-parser.js

@ -0,0 +1,70 @@
/* @flow */
import { parseHTML } from './html-parser'
import { makeMap } from 'shared/util'
import deindent from 'de-indent'
const isSpecialTag = makeMap('script,style,template', true)
/**
* Parse a single-file component (*.vue) file into an SFC Descriptor Object.
*/
export function parseSFC (content: string): SFCDescriptor {
const sfc: SFCDescriptor = {
template: null,
script: null,
styles: []
}
let depth = 0
let currentBlock
function start (tag, attrs) {
depth++
if (depth > 1) {
return
}
if (isSpecialTag(tag)) {
const block: SFCBlock = currentBlock = {
type: tag,
content: ''
}
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i]
if (attr.name === 'lang') {
block.lang = attr.value
}
if (attr.name === 'scoped') {
block.scoped = true
}
if (attr.name === 'src') {
block.src = attr.value
}
}
if (tag === 'style') {
sfc.styles.push(block)
} else {
sfc[tag] = block
}
}
}
function end () {
depth--
currentBlock = null
}
function chars (text) {
if (currentBlock) {
currentBlock.content = deindent(text)
}
}
parseHTML(content, {
isSpecialTag,
start,
end,
chars
})
return sfc
}

109
src/entries/web-compiler.js

@ -1,104 +1,19 @@
/* @flow */
import { extend, genStaticKeys, noop } from 'shared/util'
import { warn } from 'core/util/debug'
import { compile as baseCompile } from 'compiler/index'
import { compile as baseCompile } from 'web/compiler/index'
import { detectErrors } from 'compiler/error-detector'
import modules from 'web/compiler/modules/index'
import directives from 'web/compiler/directives/index'
import { isIE, isReservedTag, isUnaryTag, mustUseProp, getTagNamespace } from 'web/util/index'
// detect possible CSP restriction
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
try {
new Function('return 1')
} catch (e) {
if (e.toString().match(/unsafe-eval|CSP/)) {
warn(
'It seems you are using the standalone build of Vue.js in an ' +
'environment with Content Security Policy that prohibits unsafe-eval. ' +
'The template compiler cannot work in this environment. Consider ' +
'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
'templates into render functions.'
)
}
}
}
type CompiledFunctions = {
render: Function,
staticRenderFns: Array<Function>
}
const cache1: { [key: string]: CompiledFunctions } = Object.create(null)
const cache2: { [key: string]: CompiledFunctions } = Object.create(null)
export const baseOptions: CompilerOptions = {
isIE,
expectHTML: true,
preserveWhitespace: true,
modules,
staticKeys: genStaticKeys(modules),
directives,
isReservedTag,
isUnaryTag,
mustUseProp,
getTagNamespace
}
export { parseSFC as parseComponent } from 'compiler/parser/sfc-parser'
export { compileToFunctions } from 'web/compiler/index'
export function compile (
template: string,
options?: CompilerOptions
): {
ast: ?ASTElement,
render: string,
staticRenderFns: Array<string>
} {
options = options
? extend(extend({}, baseOptions), options)
: baseOptions
return baseCompile(template, options)
}
export function compileToFunctions (
template: string,
options?: CompilerOptions,
vm: Component
): CompiledFunctions {
const cache = options && options.preserveWhitespace === false ? cache1 : cache2
const key = options && options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
const res = {}
const compiled = compile(template, options)
res.render = makeFunction(compiled.render)
const l = compiled.staticRenderFns.length
res.staticRenderFns = new Array(l)
for (let i = 0; i < l; i++) {
res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i])
}
if (process.env.NODE_ENV !== 'production') {
if (res.render === noop || res.staticRenderFns.some(fn => fn === noop)) {
const errors = compiled.ast ? detectErrors(compiled.ast, warn) : []
warn(
`failed to compile template:\n\n${template}\n\n` +
errors.join('\n') +
'\n\n',
vm
)
options?: Object
): CompiledResult {
const errors = []
const compiled = baseCompile(template, {
warn: msg => {
errors.push(msg)
}
}
return (cache[key] = res)
}
function makeFunction (code) {
try {
return new Function(code)
} catch (e) {
return noop
}
})
compiled.errors = errors.concat(detectErrors(compiled.ast))
return compiled
}

2
src/entries/web-runtime-with-compiler.js

@ -4,7 +4,7 @@ import Vue from './web-runtime'
import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { query } from 'web/util/index'
import { compileToFunctions } from './web-compiler'
import { compileToFunctions } from 'web/compiler/index'
const idToTemplate = cached(id => {
const el = query(id)

94
src/platforms/web/compiler/index.js

@ -0,0 +1,94 @@
/* @flow */
import { extend, genStaticKeys, noop } from 'shared/util'
import { warn } from 'core/util/debug'
import { compile as baseCompile } from 'compiler/index'
import { detectErrors } from 'compiler/error-detector'
import modules from './modules/index'
import directives from './directives/index'
import { isIE, isReservedTag, isUnaryTag, mustUseProp, getTagNamespace } from '../util/index'
const cache1: { [key: string]: CompiledFunctionResult } = Object.create(null)
const cache2: { [key: string]: CompiledFunctionResult } = Object.create(null)
export const baseOptions: CompilerOptions = {
isIE,
expectHTML: true,
preserveWhitespace: true,
modules,
staticKeys: genStaticKeys(modules),
directives,
isReservedTag,
isUnaryTag,
mustUseProp,
getTagNamespace
}
export function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
options = options
? extend(extend({}, baseOptions), options)
: baseOptions
return baseCompile(template, options)
}
export function compileToFunctions (
template: string,
options?: CompilerOptions,
vm: Component
): CompiledFunctionResult {
const _warn = (options && options.warn) || warn
// detect possible CSP restriction
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
try {
new Function('return 1')
} catch (e) {
if (e.toString().match(/unsafe-eval|CSP/)) {
_warn(
'It seems you are using the standalone build of Vue.js in an ' +
'environment with Content Security Policy that prohibits unsafe-eval. ' +
'The template compiler cannot work in this environment. Consider ' +
'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
'templates into render functions.'
)
}
}
}
const cache = options && options.preserveWhitespace === false ? cache1 : cache2
const key = options && options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
const res = {}
const compiled = compile(template, options)
res.render = makeFunction(compiled.render)
const l = compiled.staticRenderFns.length
res.staticRenderFns = new Array(l)
for (let i = 0; i < l; i++) {
res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i])
}
if (process.env.NODE_ENV !== 'production') {
if (res.render === noop || res.staticRenderFns.some(fn => fn === noop)) {
_warn(
`failed to compile template:\n\n${template}\n\n` +
detectErrors(compiled.ast).join('\n') +
'\n\n',
vm
)
}
}
return (cache[key] = res)
}
function makeFunction (code) {
try {
return new Function(code)
} catch (e) {
return noop
}
}

2
test/unit/modules/compiler/codegen.spec.js

@ -4,7 +4,7 @@ import { generate } from 'compiler/codegen'
import { isObject } from 'shared/util'
import directives from 'web/compiler/directives/index'
import { isReservedTag } from 'web/util/index'
import { baseOptions } from 'entries/web-compiler'
import { baseOptions } from 'web/compiler/index'
function assertCodegen (template, generatedCode, ...args) {
let staticRenderFnCodes = []

2
test/unit/modules/compiler/optimizer.spec.js

@ -1,6 +1,6 @@
import { parse } from 'compiler/parser/index'
import { optimize } from 'compiler/optimizer'
import { baseOptions } from 'entries/web-compiler'
import { baseOptions } from 'web/compiler/index'
describe('optimizer', () => {
it('simple', () => {

2
test/unit/modules/compiler/parser.spec.js

@ -1,6 +1,6 @@
import { parse } from 'compiler/parser/index'
import { extend } from 'shared/util'
import { baseOptions } from 'entries/web-compiler'
import { baseOptions } from 'web/compiler/index'
describe('parser', () => {
it('simple element', () => {

28
test/unit/modules/compiler/sfc-parser.spec.js

@ -0,0 +1,28 @@
import { parseSFC } from 'compiler/parser/sfc-parser'
describe('SFC parser', () => {
it('should parse', () => {
const res = parseSFC(`
<template>
<div>hi</div>
</template>
<style src="./test.css"></style>
<style lang="stylus" scoped>
h1
color red
h2
color green
</style>
<script>
export default {}
</script>
`)
expect(res.template.content.trim()).toBe('<div>hi</div>')
expect(res.styles.length).toBe(2)
expect(res.styles[0].src).toBe('./test.css')
expect(res.styles[1].lang).toBe('stylus')
expect(res.styles[1].scoped).toBe(true)
expect(res.styles[1].content.trim()).toBe('h1\n color red\nh2\n color green')
expect(res.script.content.trim()).toBe('export default {}')
})
})
Loading…
Cancel
Save