pull/31/head
Rich Harris 9 years ago
parent 8f892bf65b
commit a44398b0f1

@ -1,196 +0,0 @@
import * as assert from 'assert';
import parse from './index.js';
describe( 'parse', () => {
it( 'is a function', () => {
assert.equal( typeof parse, 'function' );
});
it( 'parses a self-closing element', () => {
const template = '<div/>';
assert.deepEqual( parse( template ), {
html: {
start: 0,
end: 6,
type: 'Fragment',
children: [
{
start: 0,
end: 6,
type: 'Element',
name: 'div',
attributes: [],
children: []
}
]
},
css: null,
js: null
});
});
it( 'parses an element with text', () => {
const template = `<span>test</span>`;
assert.deepEqual( parse( template ), {
html: {
start: 0,
end: 17,
type: 'Fragment',
children: [
{
start: 0,
end: 17,
type: 'Element',
name: 'span',
attributes: [],
children: [
{
start: 6,
end: 10,
type: 'Text',
data: 'test'
}
]
}
]
},
css: null,
js: null
});
});
it( 'parses an element with a mustache tag', () => {
const template = `<h1>hello {{name}}!</h1>`;
assert.deepEqual( parse( template ), {
html: {
start: 0,
end: 24,
type: 'Fragment',
children: [
{
start: 0,
end: 24,
type: 'Element',
name: 'h1',
attributes: [],
children: [
{
start: 4,
end: 10,
type: 'Text',
data: 'hello '
},
{
start: 10,
end: 18,
type: 'MustacheTag',
expression: {
start: 12,
end: 16,
type: 'Identifier',
name: 'name'
}
},
{
start: 18,
end: 19,
type: 'Text',
data: '!'
}
]
}
]
},
css: null,
js: null
});
});
it( 'parses an {{#if}}...{{/if}} block', () => {
const template = `{{#if foo}}bar{{/if}}`;
assert.deepEqual( parse( template ), {
html: {
start: 0,
end: 21,
type: 'Fragment',
children: [
{
start: 0,
end: 21,
type: 'IfBlock',
expression: {
start: 6,
end: 9,
type: 'Identifier',
name: 'foo'
},
children: [
{
start: 11,
end: 14,
type: 'Text',
data: 'bar'
}
]
}
]
},
css: null,
js: null
});
});
it( 'parses an {{#each}}...{{/each}} block', () => {
const template = `{{#each animals as animal}}<p>{{animal}}</p>{{/each}}`;
assert.deepEqual( parse( template ), {
html: {
start: 0,
end: 53,
type: 'Fragment',
children: [
{
start: 0,
end: 53,
type: 'EachBlock',
expression: {
start: 8,
end: 15,
type: 'Identifier',
name: 'animals'
},
context: 'animal',
children: [
{
start: 27,
end: 44,
type: 'Element',
name: 'p',
attributes: [],
children: [
{
start: 30,
end: 40,
type: 'MustacheTag',
expression: {
start: 32,
end: 38,
type: 'Identifier',
name: 'animal'
}
}
]
}
]
}
]
},
css: null,
js: null
});
});
});

@ -63,20 +63,21 @@ export default function parse ( template ) {
}
this.allowWhitespace();
}
};
},
const html = {
start: 0,
end: template.length,
type: 'Fragment',
children: []
};
html: {
start: 0,
end: template.length,
type: 'Fragment',
children: []
},
let css = null;
let js = null;
css: null,
parser.stack.push( html );
js: null
};
parser.stack.push( parser.html );
let state = fragment;
@ -84,5 +85,9 @@ export default function parse ( template ) {
state = state( parser ) || fragment;
}
return { html, css, js };
return {
html: parser.html,
css: parser.css,
js: parser.js
};
}

@ -0,0 +1,25 @@
import { parse, tokenizer, tokTypes } from 'acorn';
export default function readScript ( parser, start, attributes ) {
const scriptStart = parser.index;
let scriptEnd = null;
const js = {
start,
end: null,
attributes,
content: null
};
const endPattern = /\s*<\/script\>/g;
for ( const token of tokenizer( parser.remaining() ) ) {
endPattern.lastIndex = scriptStart + token.end;
if ( endPattern.test( parser.template ) ) {
scriptEnd = scriptStart + token.end;
break;
}
}
js.content = parse( )
}

@ -1,8 +1,22 @@
import readExpression from '../read/expression.js';
import readScript from '../read/script.js';
import readStyle from '../read/style.js';
const validTagName = /^[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
const voidElementNames = /^(?:area|base|br|col|command|doctype|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
const specials = {
script: {
read: readScript,
property: 'js'
},
style: {
read: readStyle,
property: 'css'
}
};
export default function tag ( parser ) {
const start = parser.index++;
@ -30,6 +44,20 @@ export default function tag ( parser ) {
parser.allowWhitespace();
// special cases <script> and <style>
if ( name in specials ) {
const special = specials[ name ];
if ( parser[ special.id ] ) {
parser.index = start;
parser.error( `You can only have one <${name}> tag per component` );
}
parser.eat( '>', true );
parser[ special.id ] = special.read( parser, start, attributes );
return;
}
const element = {
start,
end: null, // filled in later
@ -43,9 +71,7 @@ export default function tag ( parser ) {
const selfClosing = parser.eat( '/' ) || voidElementNames.test( name );
if ( !parser.eat( '>' ) ) {
parser.error( `Expected >` );
}
parser.eat( '>', true );
if ( selfClosing ) {
element.end = parser.index;

@ -0,0 +1,6 @@
import * as assert from 'assert';
export default {
description: 'hello world',
html: '<h1>Hello world!</h1>'
};

@ -0,0 +1,9 @@
<h1>Hello {{name}}!</h1>
<script>
export default {
data: () => ({
name: 'world'
})
};
</script>

@ -0,0 +1 @@
{{#each animals as animal}}<p>{{animal}}</p>{{/each}}

@ -0,0 +1,45 @@
{
"html": {
"start": 0,
"end": 53,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 53,
"type": "EachBlock",
"expression": {
"start": 8,
"end": 15,
"type": "Identifier",
"name": "animals"
},
"context": "animal",
"children": [
{
"start": 27,
"end": 44,
"type": "Element",
"name": "p",
"attributes": [],
"children": [
{
"start": 30,
"end": 40,
"type": "MustacheTag",
"expression": {
"start": 32,
"end": 38,
"type": "Identifier",
"name": "animal"
}
}
]
}
]
}
]
},
"css": null,
"js": null
}

@ -0,0 +1,43 @@
{
"html": {
"start": 0,
"end": 24,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 24,
"type": "Element",
"name": "h1",
"attributes": [],
"children": [
{
"start": 4,
"end": 10,
"type": "Text",
"data": "hello "
},
{
"start": 10,
"end": 18,
"type": "MustacheTag",
"expression": {
"start": 12,
"end": 16,
"type": "Identifier",
"name": "name"
}
},
{
"start": 18,
"end": 19,
"type": "Text",
"data": "!"
}
]
}
]
},
"css": null,
"js": null
}

@ -0,0 +1,26 @@
{
"html": {
"start": 0,
"end": 17,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 17,
"type": "Element",
"name": "span",
"attributes": [],
"children": [
{
"start": 6,
"end": 10,
"type": "Text",
"data": "test"
}
]
}
]
},
"css": null,
"js": null
}

@ -0,0 +1 @@
{{#if foo}}bar{{/if}}

@ -0,0 +1,30 @@
{
"html": {
"start": 0,
"end": 21,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 21,
"type": "IfBlock",
"expression": {
"start": 6,
"end": 9,
"type": "Identifier",
"name": "foo"
},
"children": [
{
"start": 11,
"end": 14,
"type": "Text",
"data": "bar"
}
]
}
]
},
"css": null,
"js": null
}

@ -0,0 +1,9 @@
<h1>Hello {{name}}!</h1>
<script>
export default {
data: () => ({
name: 'world'
})
};
</script>

@ -0,0 +1,43 @@
{
"html": {
"start": 0,
"end": 24,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 24,
"type": "Element",
"name": "h1",
"attributes": [],
"children": [
{
"start": 4,
"end": 10,
"type": "Text",
"data": "Hello "
},
{
"start": 10,
"end": 18,
"type": "MustacheTag",
"expression": {
"start": 12,
"end": 16,
"type": "Identifier",
"name": "name"
}
},
{
"start": 18,
"end": 19,
"type": "Text",
"data": "!"
}
]
}
]
},
"css": null,
"js": null
}

@ -0,0 +1,19 @@
{
"html": {
"start": 0,
"end": 6,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 6,
"type": "Element",
"name": "div",
"attributes": [],
"children": []
}
]
},
"css": null,
"js": null
}

@ -1,4 +1,5 @@
import { compile } from '../compiler/index.js';
import parse from '../compiler/parse/index.js';
import * as assert from 'assert';
import * as path from 'path';
import * as fs from 'fs';
@ -13,99 +14,126 @@ require.extensions[ '.svelte' ] = function ( module, filename ) {
return module._compile( code, filename );
};
describe( 'svelte', () => {
function loadConfig ( dir ) {
try {
return require( `./samples/${dir}/_config.js` ).default;
} catch ( err ) {
if ( err.code === 'E_NOT_FOUND' ) {
return {};
}
throw err;
}
function exists ( path ) {
try {
fs.statSync( path );
return true;
} catch ( err ) {
return false;
}
}
function env () {
return new Promise( ( fulfil, reject ) => {
jsdom.env( '<main></main>', ( err, window ) => {
if ( err ) {
reject( err );
} else {
global.document = window.document;
fulfil( window );
}
});
});
}
describe( 'svelte', () => {
describe( 'parser', () => {
fs.readdirSync( 'test/parser' ).forEach( dir => {
if ( dir[0] === '.' ) return;
fs.readdirSync( 'test/samples' ).forEach( dir => {
if ( dir[0] === '.' ) return;
const solo = exists( `test/parser/${dir}/solo` );
const config = loadConfig( dir );
( solo ? it.only : it )( dir, () => {
const input = fs.readFileSync( `test/parser/${dir}/input.svelte`, 'utf-8' ).trim();
const actual = parse( input );
const expected = require( `./parser/${dir}/output.json` );
( config.solo ? it.only : it )( dir, () => {
let compiled;
assert.deepEqual( actual, expected );
});
});
});
describe( 'compiler', () => {
function loadConfig ( dir ) {
try {
const source = fs.readFileSync( `test/samples/${dir}/main.svelte`, 'utf-8' );
compiled = compile( source );
return require( `./compiler/${dir}/_config.js` ).default;
} catch ( err ) {
if ( config.compileError ) {
config.compileError( err );
return;
} else {
throw err;
if ( err.code === 'E_NOT_FOUND' ) {
return {};
}
throw err;
}
}
const { code } = compiled;
const withLineNumbers = code.split( '\n' ).map( ( line, i ) => {
i = String( i + 1 );
while ( i.length < 3 ) i = ` ${i}`;
function env () {
return new Promise( ( fulfil, reject ) => {
jsdom.env( '<main></main>', ( err, window ) => {
if ( err ) {
reject( err );
} else {
global.document = window.document;
fulfil( window );
}
});
});
}
return `${i}: ${line.replace( /^\t+/, match => match.split( '\t' ).join( ' ' ) )}`;
}).join( '\n' );
fs.readdirSync( 'test/compiler' ).forEach( dir => {
if ( dir[0] === '.' ) return;
cache[ path.resolve( `test/samples/${dir}/main.svelte` ) ] = code;
const config = loadConfig( dir );
let factory;
( config.solo ? it.only : it )( dir, () => {
let compiled;
try {
factory = require( `./samples/${dir}/main.svelte` ).default;
} catch ( err ) {
console.log( withLineNumbers ); // eslint-disable-line no-console
throw err;
}
try {
const source = fs.readFileSync( `test/compiler/${dir}/main.svelte`, 'utf-8' );
compiled = compile( source );
} catch ( err ) {
if ( config.compileError ) {
config.compileError( err );
return;
} else {
throw err;
}
}
if ( config.show ) {
console.log( withLineNumbers ); // eslint-disable-line no-console
}
const { code } = compiled;
const withLineNumbers = code.split( '\n' ).map( ( line, i ) => {
i = String( i + 1 );
while ( i.length < 3 ) i = ` ${i}`;
return env()
.then( window => {
const target = window.document.querySelector( 'main' );
return `${i}: ${line.replace( /^\t+/, match => match.split( '\t' ).join( ' ' ) )}`;
}).join( '\n' );
const component = factory({
target,
data: config.data
});
cache[ path.resolve( `test/compiler/${dir}/main.svelte` ) ] = code;
if ( config.html ) {
assert.equal( target.innerHTML, config.html );
}
let factory;
if ( config.test ) {
config.test( component, target );
} else {
component.teardown();
assert.equal( target.innerHTML, '' );
}
})
.catch( err => {
if ( !config.show ) console.log( withLineNumbers ); // eslint-disable-line no-console
try {
factory = require( `./compiler/${dir}/main.svelte` ).default;
} catch ( err ) {
console.log( withLineNumbers ); // eslint-disable-line no-console
throw err;
});
}
if ( config.show ) {
console.log( withLineNumbers ); // eslint-disable-line no-console
}
return env()
.then( window => {
const target = window.document.querySelector( 'main' );
const component = factory({
target,
data: config.data
});
if ( config.html ) {
assert.equal( target.innerHTML, config.html );
}
if ( config.test ) {
config.test( component, target );
} else {
component.teardown();
assert.equal( target.innerHTML, '' );
}
})
.catch( err => {
if ( !config.show ) console.log( withLineNumbers ); // eslint-disable-line no-console
throw err;
});
});
});
});
});

Loading…
Cancel
Save