mirror of https://github.com/sveltejs/svelte
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.
296 lines
5.6 KiB
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 };
|
|
}
|