Просмотр исходного кода

parser for single-file components

Evan You 10 лет назад
Родитель
Сommit
e236f80fa0

+ 1 - 1
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 - 0
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 - 0
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",

+ 4 - 2
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
 }
 

+ 1 - 5
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)

+ 4 - 3
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 - 0
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
+}

+ 12 - 97
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
 }

+ 1 - 1
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 - 0
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
+  }
+}

+ 1 - 1
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 = []

+ 1 - 1
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', () => {

+ 1 - 1
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 - 0
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 {}')
+  })
+})