more parsing

pull/31/head
Rich-Harris 8 years ago
parent f2f4a04ce1
commit 7f42cc98b3

@ -0,0 +1,3 @@
# svelte
Coming soon...

@ -1,7 +1,133 @@
import deindent from 'deindent';
import walkHtml from './walkHtml.js';
export default function generate ( parsed ) {
return `
const counters = {
element: 0,
text: 0,
anchor: 0
};
const renderBlocks = [];
const updateBlocks = [];
const teardownBlocks = [];
const codeBlocks = [];
// TODO add contents of <script> tag, with `export default` replaced with `var template =`
// TODO css
// create component
parsed.html.children.forEach( child => {
let currentElement = 'options.target';
const elementStack = [ currentElement ];
walkHtml( child, {
enter ( node ) {
if ( node.type === 'Element' ) {
currentElement = `element_${counters.element++}`;
const renderBlock = deindent`
var ${currentElement} = document.createElement( '${node.name}' );
options.target.appendChild( ${currentElement} );
`;
renderBlocks.push( renderBlock );
// updateBlocks.push( deindent`
//
// ` );
teardownBlocks.push( deindent`
${currentElement}.parentNode.removeChild( ${currentElement} );
` );
}
else if ( node.type === 'Text' ) {
renderBlocks.push( deindent`
${currentElement}.appendChild( document.createTextNode( ${JSON.stringify( node.data )} ) );
` );
}
else {
throw new Error( `Not implemented: ${node.type}` );
}
},
leave ( node ) {
if ( node.type === 'Element' ) {
elementStack.pop();
currentElement = elementStack[ elementStack.length - 1 ];
}
}
});
});
const code = deindent`
export default function createComponent ( options ) {
console.log( 'TODO' );
var component = {};
var state = {};
var observers = {
immediate: Object.create( null ),
deferred: Object.create( null )
};
// universal methods
function dispatchObservers ( group, state, oldState ) {
for ( const key in group ) {
const newValue = state[ key ];
const oldValue = oldState[ key ];
if ( newValue === oldValue && typeof newValue !== 'object' ) continue;
const callbacks = group[ key ];
if ( !callbacks ) continue;
for ( let i = 0; i < callbacks.length; i += 1 ) {
callbacks[i].call( component, newValue, oldValue );
}
}
}
component.get = function get ( key ) {
return state[ key ];
};
component.set = function set ( newState ) {
const oldState = state;
state = Object.assign( {}, oldState, newState );
${updateBlocks.join( '\n\n' )}
};
component.observe = function ( key, callback, options = {} ) {
const group = options.defer ? observers.deferred : observers.immediate;
( group[ key ] || ( group[ key ] = [] ) ).push( callback );
if ( options.init !== false ) callback( state[ key ] );
return {
cancel () {
const index = group[ key ].indexOf( callback );
if ( ~index ) group[ key ].splice( index, 1 );
}
};
};
component.teardown = function teardown () {
${teardownBlocks.join( '\n\n' )}
state = {};
};
${renderBlocks.join( '\n\n' )}
component.set( options.data );
return component;
}
`;
`.trim();
return { code };
}

@ -0,0 +1,15 @@
export default function walkHtml ( html, { enter, leave } ) {
function visit ( node ) {
enter( node );
if ( node.children ) {
node.children.forEach( child => {
visit( child );
});
}
leave( node );
}
visit( html );
}

@ -6,30 +6,106 @@ describe( 'parse', () => {
assert.equal( typeof parse, 'function' );
});
it( 'parses a single element', () => {
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 ), {
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'
}
]
}
]
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
});
});
});

@ -1,41 +1,72 @@
import { parseExpressionAt } from 'acorn';
import { locate } from 'locate-character';
const validNameChar = /[a-zA-Z0-9_$]/;
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 whitespace = /\s/;
export default function parse ( template ) {
let i = 0;
const parser = {
index: 0,
template,
stack: [],
current () {
return this.stack[ this.stack.length - 1 ];
},
error ( message ) {
const { line, column } = locate( this.template, this.index );
throw new Error( `${message} (${line}:${column})` );
},
eat ( str ) {
if ( this.match( str ) ) {
this.index += str.length;
return true;
}
},
match ( str ) {
return this.template.slice( this.index, this.index + str.length ) === str;
},
allowWhitespace () {
while ( this.index < this.template.length && whitespace.test( this.template[ this.index ] ) ) {
this.index++;
}
},
readUntil ( pattern ) {
const match = pattern.exec( this.template.slice( this.index ) );
return this.template.slice( this.index, match ? ( this.index += match.index ) : this.template.length );
},
remaining () {
return this.template.slice( this.index );
}
};
const root = {
const html = {
start: 0,
end: template.length,
type: 'Fragment',
children: []
};
const stack = [ root ];
let current = root;
let css = null;
let js = null;
function error ( message ) {
const { line, column } = locate( template, i );
throw new Error( `${message} (${line}:${column})` );
}
function match ( str ) {
return template.slice( i, i + str.length ) === str;
}
parser.stack.push( html );
function fragment () {
const char = template[i];
parser.allowWhitespace();
while ( char === ' ' ) {
i += 1;
}
if ( char === '<' ) {
if ( parser.match( '<' ) ) {
return tag;
}
if ( match( '{{' ) ) {
if ( parser.match( '{{' ) ) {
return mustache;
}
@ -43,72 +74,210 @@ export default function parse ( template ) {
}
function tag () {
const start = i++;
let char = template[ i ];
const start = parser.index++;
const isClosingTag = char === '/';
if ( isClosingTag ) {
// this is a closing tag
i += 1;
char = template[ i ];
}
const isClosingTag = parser.eat( '/' );
// TODO handle cases like <li>one<li>two
let name = '';
while ( validNameChar.test( char ) ) {
name += char;
i += 1;
char = template[i];
}
const name = readTagName();
if ( isClosingTag ) {
if ( char !== '>' ) error( `Expected '>'` );
if ( !parser.eat( '>' ) ) parser.error( `Expected '>'` );
i += 1;
current.end = i;
stack.pop();
current = stack[ stack.length - 1 ];
parser.current().end = parser.index;
parser.stack.pop();
return fragment;
}
const attributes = [];
let attribute;
while ( attribute = readAttribute() ) {
attributes.push( attribute );
}
parser.allowWhitespace();
const element = {
start,
end: null, // filled in later
type: 'Element',
name,
attributes: {},
attributes,
children: []
};
current.children.push( element );
stack.push( element );
parser.current().children.push( element );
current = element;
const selfClosing = parser.eat( '/' ) || voidElementNames.test( name );
if ( char === '>' ) {
i += 1;
return fragment;
if ( !parser.eat( '>' ) ) {
parser.error( `Expected >` );
}
return attributes;
if ( selfClosing ) {
element.end = parser.index;
} else {
// don't push self-closing elements onto the stack
parser.stack.push( element );
}
return fragment;
}
function readTagName () {
const start = parser.index;
const name = parser.readUntil( /(\s|\/|>)/ );
if ( !validTagName.test( name ) ) {
parser.error( `Expected valid tag name`, start );
}
return name;
}
function readAttribute () {
const name = parser.readUntil( /(\s|=|\/|>)/ );
if ( !name ) return null;
parser.allowWhitespace();
const value = parser.eat( '=' ) ? readAttributeValue() : true;
return { name, value };
}
function readAttributeValue () {
if ( parser.eat( `'` ) ) return readQuotedAttributeValue( `'` );
if ( parser.eat( `"` ) ) return readQuotedAttributeValue( `"` );
parser.error( `TODO unquoted attribute values` );
}
function readQuotedAttributeValue ( quoteMark ) {
let currentChunk = {
start: parser.index,
end: null,
type: 'AttributeText',
data: ''
};
let escaped = false;
const chunks = [];
while ( parser.index < parser.template.length ) {
if ( escaped ) {
currentChunk.data += parser.template[ parser.index++ ];
}
else {
if ( parser.match( '{{' ) ) {
const index = parser.index;
currentChunk.end = index;
if ( currentChunk.data ) {
chunks.push( currentChunk );
}
const expression = readExpression();
parser.allowWhitespace();
if ( !parser.eat( '}}' ) ) {
parser.error( `Expected }}` );
}
chunks.push({
start: index,
end: parser.index,
type: 'MustacheTag',
expression
});
currentChunk = {
start: parser.index,
end: null,
type: 'AttributeText',
data: ''
};
}
else if ( parser.match( '\\' ) ) {
escaped = true;
}
else if ( parser.match( quoteMark ) ) {
if ( currentChunk.data ) {
chunks.push( currentChunk );
return chunks;
}
}
}
}
parser.error( `Unexpected end of input` );
}
function mustache () {
const start = parser.index;
parser.index += 2;
parser.allowWhitespace();
if ( parser.match( '#if' ) ) {
return ifBlock;
}
if ( parser.match( '#each' ) ) {
return eachBlock;
}
const expression = readExpression( template, parser.index );
parser.allowWhitespace();
if ( !parser.eat( '}}' ) ) parser.error( `Expected }}` );
parser.current().children.push({
start,
end: parser.index,
type: 'MustacheTag',
expression
});
return fragment;
}
function ifBlock () {
throw new Error( 'TODO' );
}
function eachBlock () {
throw new Error( 'TODO' );
}
function readExpression () {
const node = parseExpressionAt( parser.template, parser.index );
parser.index = node.end;
// TODO check it's a valid expression. probably shouldn't have
// [arrow] function expressions, etc
return node;
}
function text () {
const start = i;
const start = parser.index;
let data = '';
while ( i < template.length && template[i] !== '<' && !match( '{{' ) ) {
data += template[ i++ ];
while ( !parser.match( '<' ) && !parser.match( '{{' ) ) {
data += template[ parser.index++ ];
}
current.children.push({
parser.current().children.push({
start,
end: i,
end: parser.index,
type: 'Text',
data
});
@ -116,19 +285,11 @@ export default function parse ( template ) {
return fragment;
}
function attributes () {
const char = template[i];
if ( char === '>' ) {
i += 1;
return fragment;
}
}
let state = fragment;
while ( i < template.length ) {
while ( parser.index < parser.template.length ) {
state = state();
}
return root;
return { html, css, js };
}

@ -1,6 +1,6 @@
{
"name": "svelte",
"version": "1.0.0",
"version": "0.0.1",
"description": "The frameworkless UI framework",
"main": "dist/svelte-compiler.js",
"scripts": {
@ -31,6 +31,8 @@
"reify": "^0.4.0"
},
"dependencies": {
"acorn": "^4.0.3",
"deindent": "^0.1.0",
"locate-character": "^2.0.0"
}
}

@ -5,9 +5,9 @@ import jsdom from 'jsdom';
require.extensions[ '.svelte' ] = function ( module, filename ) {
const source = fs.readFileSync( filename, 'utf-8' );
const compiled = compile( source );
const { code } = compile( source );
return module._compile( compiled, filename );
return module._compile( code, filename );
};
describe( 'svelte', () => {
@ -29,6 +29,7 @@ describe( 'svelte', () => {
if ( err ) {
reject( err );
} else {
global.document = window.document;
fulfil( window );
}
});

Loading…
Cancel
Save