You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte/compiler/parse/index.js

296 lines
5.6 KiB

import { parseExpressionAt } from 'acorn';
import { locate } from 'locate-character';
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 ) {
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 html = {
start: 0,
end: template.length,
type: 'Fragment',
children: []
};
let css = null;
let js = null;
parser.stack.push( html );
function fragment () {
parser.allowWhitespace();
if ( parser.match( '<' ) ) {
return tag;
}
if ( parser.match( '{{' ) ) {
return mustache;
}
return text;
}
function tag () {
const start = parser.index++;
const isClosingTag = parser.eat( '/' );
// TODO handle cases like <li>one<li>two
const name = readTagName();
if ( isClosingTag ) {
if ( !parser.eat( '>' ) ) parser.error( `Expected '>'` );
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,
children: []
};
parser.current().children.push( element );
const selfClosing = parser.eat( '/' ) || voidElementNames.test( name );
if ( !parser.eat( '>' ) ) {
parser.error( `Expected >` );
}
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 = parser.index;
let data = '';
while ( !parser.match( '<' ) && !parser.match( '{{' ) ) {
data += template[ parser.index++ ];
}
parser.current().children.push({
start,
end: parser.index,
type: 'Text',
data
});
return fragment;
}
let state = fragment;
while ( parser.index < parser.template.length ) {
state = state();
}
return { html, css, js };
}