Repository: vuejs/vue
Pull request: Support custom blocks in SFC parser

The approach is simple and experimental. Worth discussing

This allow to use other block appart from `template`, `script` or
`style` in the SFC parser. This allows such things as writing tests or
examples directly into the SFC file. Those are meant to be handled by
programs others than vue-loader like vue-play.

an usage example could be:

```vue
<example name="simple">
  <my-button some-prop="red">Hello</my-button>
</example>
```

custom playground. The content wouldn't be parsed by the SFC parser. Its third parties' programs responsibility to do so
```vue
<play name="simple">
  <template>
    <my-button @click.native="increment">Hello</my-button>
  </template>
  <script>
    export default {
      data () { return { count: 0 }},
      methods: {
        increment () { this.count++ }
      }
    }
  </script>
</play>

<!-- src hasn't be added yet -->
<play src="./MyButton.play"></play>
```

Changed files:
filename: flow/compiler.js
patch: @@ -145,6 +145,16 @@ declare type SFCDescriptor = {
   template: ?SFCBlock;
   script: ?SFCBlock;
   styles: Array<SFCBlock>;
+  customBlocks: Array<SFCCustomBlock>;
+}
+
+declare type SFCCustomBlock = {
+  type: string;
+  content: string;
+  start?: number;
+  end?: number;
+  src?: string;
+  attrs: {[attribute:string]: string};
 }
 
 declare type SFCBlock = {

filename: src/sfc/parser.js
patch: @@ -22,10 +22,11 @@ export function parseComponent (
   const sfc: SFCDescriptor = {
     template: null,
     script: null,
-    styles: []
+    styles: [],
+    customBlocks: []
   }
   let depth = 0
-  let currentBlock: ?SFCBlock = null
+  let currentBlock: ?(SFCBlock | SFCCustomBlock) = null
 
   function start (
     tag: string,
@@ -34,17 +35,30 @@ export function parseComponent (
     start: number,
     end: number
   ) {
-    if (isSpecialTag(tag) && depth === 0) {
-      currentBlock = {
-        type: tag,
-        content: '',
-        start: end
-      }
-      checkAttrs(currentBlock, attrs)
-      if (tag === 'style') {
-        sfc.styles.push(currentBlock)
-      } else {
-        sfc[tag] = currentBlock
+    if (depth === 0) {
+      if (isSpecialTag(tag)) {
+        currentBlock = {
+          type: tag,
+          content: '',
+          start: end
+        }
+        checkAttrs(currentBlock, attrs)
+        if (tag === 'style') {
+          sfc.styles.push(currentBlock)
+        } else {
+          sfc[tag] = currentBlock
+        }
+      } else { // custom blocks
+        currentBlock = {
+          type: tag,
+          content: '',
+          start: end,
+          attrs: attrs.reduce((cumulated, { name, value }) => {
+            cumulated[name] = value
+            return cumulated
+          }, Object.create(null))
+        }
+        sfc.customBlocks.push(currentBlock)
       }
     }
     if (!unary) {
@@ -71,7 +85,7 @@ export function parseComponent (
   }
 
   function end (tag: string, start: number, end: number) {
-    if (isSpecialTag(tag) && depth === 1 && currentBlock) {
+    if (depth === 1 && currentBlock) {
       currentBlock.end = start
       let text = deindent(content.slice(currentBlock.start, currentBlock.end))
       // pad content so that linters and pre-processors can output correct
@@ -85,7 +99,7 @@ export function parseComponent (
     depth--
   }
 
-  function padContent (block: SFCBlock) {
+  function padContent (block: SFCBlock | SFCCustomBlock) {
     const offset = content.slice(0, block.start).split(splitRE).length
     const padChar = block.type === 'script' && !block.lang
       ? '//\n'

filename: test/unit/modules/sfc/sfc-parser.spec.js
patch: @@ -77,4 +77,50 @@ describe('Single File Component parser', () => {
     `)
     expect(res.template.content.trim()).toBe(`div\n  h1(v-if='1 < 2') hello`)
   })
+
+  it('should handle custom blocks without without parsing them', () => {
+    const res = parseComponent(`
+      <template>
+        <div></div>
+      </template>
+      <example name="simple">
+        <my-button ref="button">Hello</my-button>
+      </example>
+      <example name="with props">
+        <my-button color="red">Hello</my-button>
+      </example>
+      <test name="simple" foo="bar">
+      export default function simple (vm) {
+        describe('Hello', () => {
+          it('should display Hello', () => {
+            this.vm.$refs.button.$el.innerText.should.equal('Hello')
+          }))
+        }))
+      }
+      </test>
+    `)
+    expect(res.customBlocks.length).toBe(3)
+
+    const simpleExample = res.customBlocks[0]
+    expect(simpleExample.type).toBe('example')
+    expect(simpleExample.content.trim()).toBe('<my-button ref="button">Hello</my-button>')
+    expect(simpleExample.attrs.name).toBe('simple')
+
+    const withProps = res.customBlocks[1]
+    expect(withProps.type).toBe('example')
+    expect(withProps.content.trim()).toBe('<my-button color="red">Hello</my-button>')
+    expect(withProps.attrs.name).toBe('with props')
+
+    const simpleTest = res.customBlocks[2]
+    expect(simpleTest.type).toBe('test')
+    expect(simpleTest.content.trim()).toBe(`export default function simple (vm) {
+  describe('Hello', () => {
+    it('should display Hello', () => {
+      this.vm.$refs.button.$el.innerText.should.equal('Hello')
+    }))
+  }))
+}`)
+    expect(simpleTest.attrs.name).toBe('simple')
+    expect(simpleTest.attrs.foo).toBe('bar')
+  })
 })