From e236f80fa039621fd18e7bb2b10a6c4f6dee9e49 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 7 Jun 2016 21:36:00 -0400 Subject: [PATCH] parser for single-file components --- build/build.js | 2 +- flow/compiler.js | 36 ++++++ package.json | 1 + src/compiler/error-detector.js | 6 +- src/compiler/index.js | 6 +- src/compiler/parser/html-parser.js | 7 +- src/compiler/parser/sfc-parser.js | 70 +++++++++++ src/entries/web-compiler.js | 109 ++---------------- src/entries/web-runtime-with-compiler.js | 2 +- src/platforms/web/compiler/index.js | 94 +++++++++++++++ test/unit/modules/compiler/codegen.spec.js | 2 +- test/unit/modules/compiler/optimizer.spec.js | 2 +- test/unit/modules/compiler/parser.spec.js | 2 +- test/unit/modules/compiler/sfc-parser.spec.js | 28 +++++ 14 files changed, 255 insertions(+), 112 deletions(-) create mode 100644 src/compiler/parser/sfc-parser.js create mode 100644 src/platforms/web/compiler/index.js create mode 100644 test/unit/modules/compiler/sfc-parser.spec.js diff --git a/build/build.js b/build/build.js index 997d0c37..6bd032ce 100644 --- a/build/build.js +++ b/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). diff --git a/flow/compiler.js b/flow/compiler.js index 0037446f..9b4c0479 100644 --- a/flow/compiler.js +++ b/flow/compiler.js @@ -16,6 +16,18 @@ declare type CompilerOptions = { delimiters?: [string, string] // template delimiters } +declare type CompiledResult = { + ast: ?ASTElement, + render: string, + staticRenderFns: Array, + errors?: Array +} + +declare type CompiledFunctionResult = { + render: Function, + staticRenderFns: Array +} + 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 +} + +declare type SFCBlock = { + type: "template" | "script" | "style", + content: string, + lang?: string, + scoped?: boolean, + src?: boolean, + map?: Object +} diff --git a/package.json b/package.json index 334e1060..57177b82 100644 --- a/package.json +++ b/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", diff --git a/src/compiler/error-detector.js b/src/compiler/error-detector.js index d73582c5..c516f764 100644 --- a/src/compiler/error-detector.js +++ b/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 { +export function detectErrors (ast: ?ASTNode): Array { const errors: Array = [] - checkNode(ast, errors) + if (ast) { + checkNode(ast, errors) + } return errors } diff --git a/src/compiler/index.js b/src/compiler/index.js index d8ab1889..fcecd8b0 100644 --- a/src/compiler/index.js +++ b/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 -} { +): CompiledResult { const ast = parse(template.trim(), options) optimize(ast, options) const code = generate(ast, options) diff --git a/src/compiler/parser/html-parser.js b/src/compiler/parser/html-parser.js index b79dad44..ffd091e1 100644 --- a/src/compiler/parser/html-parser.js +++ b/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: diff --git a/src/compiler/parser/sfc-parser.js b/src/compiler/parser/sfc-parser.js new file mode 100644 index 00000000..d3e911d0 --- /dev/null +++ b/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 +} diff --git a/src/entries/web-compiler.js b/src/entries/web-compiler.js index 47d75dcc..1c1b8f0d 100644 --- a/src/entries/web-compiler.js +++ b/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 -} - -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 -} { - 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 } diff --git a/src/entries/web-runtime-with-compiler.js b/src/entries/web-runtime-with-compiler.js index 0e5e65ad..34ce6688 100644 --- a/src/entries/web-runtime-with-compiler.js +++ b/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) diff --git a/src/platforms/web/compiler/index.js b/src/platforms/web/compiler/index.js new file mode 100644 index 00000000..c893efca --- /dev/null +++ b/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 + } +} diff --git a/test/unit/modules/compiler/codegen.spec.js b/test/unit/modules/compiler/codegen.spec.js index 868f29c4..6294ae66 100644 --- a/test/unit/modules/compiler/codegen.spec.js +++ b/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 = [] diff --git a/test/unit/modules/compiler/optimizer.spec.js b/test/unit/modules/compiler/optimizer.spec.js index 7ff2b375..1b3a8e1b 100644 --- a/test/unit/modules/compiler/optimizer.spec.js +++ b/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', () => { diff --git a/test/unit/modules/compiler/parser.spec.js b/test/unit/modules/compiler/parser.spec.js index c888f6e3..1704c3ef 100644 --- a/test/unit/modules/compiler/parser.spec.js +++ b/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', () => { diff --git a/test/unit/modules/compiler/sfc-parser.spec.js b/test/unit/modules/compiler/sfc-parser.spec.js new file mode 100644 index 00000000..3f199889 --- /dev/null +++ b/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(` + + + + + `) + expect(res.template.content.trim()).toBe('
hi
') + 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 {}') + }) +})