import { locate } from 'locate-character'; import fragment from './state/fragment.js'; import { whitespace } from './patterns.js'; import { trimStart, trimEnd } from './utils/trim.js'; import spaces from './utils/spaces.js'; function tabsToSpaces ( str ) { return str.replace( /^\t+/, match => match.split( '\t' ).join( ' ' ) ); } function ParseError ( message, template, index ) { const { line, column } = locate( template, index ); const lines = template.split( '\n' ); const frameStart = Math.max( 0, line - 2 ); const frameEnd = Math.min( line + 3, lines.length ); const digits = String( frameEnd + 1 ).length; const frame = lines .slice( frameStart, frameEnd ) .map( ( str, i ) => { const isErrorLine = frameStart + i === line; let lineNum = String( i + frameStart + 1 ); while ( lineNum.length < digits ) lineNum = ` ${lineNum}`; if ( isErrorLine ) { const indicator = spaces( digits + 2 + tabsToSpaces( str.slice( 0, column ) ).length ) + '^'; return `${lineNum}: ${tabsToSpaces( str )}\n${indicator}`; } return `${lineNum}: ${tabsToSpaces( str )}`; }) .join( '\n' ); this.message = `${message} (${line + 1}:${column})\n${frame}`; this.loc = { line, column }; this.shortMessage = message; } export default function parse ( template ) { const parser = { index: 0, template, stack: [], current () { return this.stack[ this.stack.length - 1 ]; }, acornError ( err ) { parser.error( err.message.replace( /\(\d+:\d+\)$/, '' ), err.pos ); }, error ( message, index = this.index ) { throw new ParseError( message, this.template, index ); }, eat ( str, required ) { if ( this.match( str ) ) { this.index += str.length; return true; } if ( required ) { this.error( `Expected ${str}` ); } }, 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++; } }, read ( pattern ) { const match = pattern.exec( this.template.slice( this.index ) ); if ( !match || match.index !== 0 ) return null; parser.index += match[0].length; return match[0]; }, 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 ); }, requireWhitespace () { if ( !whitespace.test( this.template[ this.index ] ) ) { this.error( `Expected whitespace` ); } this.allowWhitespace(); }, html: { start: null, end: null, type: 'Fragment', children: [] }, css: null, js: null }; parser.stack.push( parser.html ); let state = fragment; while ( parser.index < parser.template.length ) { state = state( parser ) || fragment; } // trim unnecessary whitespace while ( parser.html.children.length ) { const firstChild = parser.html.children[0]; parser.html.start = firstChild.start; if ( firstChild.type !== 'Text' ) break; const length = firstChild.data.length; firstChild.data = trimStart( firstChild.data ); if ( firstChild.data === '' ) { parser.html.children.shift(); } else { parser.html.start += length - firstChild.data.length; break; } } while ( parser.html.children.length ) { const lastChild = parser.html.children[ parser.html.children.length - 1 ]; parser.html.end = lastChild.end; if ( lastChild.type !== 'Text' ) break; const length = lastChild.data.length; lastChild.data = trimEnd( lastChild.data ); if ( lastChild.data === '' ) { parser.html.children.pop(); } else { parser.html.end -= length - lastChild.data.length; break; } } return { html: parser.html, css: parser.css, js: parser.js }; }