mirror of
https://github.com/withastro/astro.git
synced 2024-12-16 21:46:22 -05:00
Bring compiler into Astro (#4)
* include source compiler * Import from JS * Conditionally use the instance contents Co-authored-by: Fred K. Schott <fkschott@gmail.com>
This commit is contained in:
parent
174fc1d669
commit
588b086a4d
37 changed files with 4585 additions and 21481 deletions
38
LICENSE
Normal file
38
LICENSE
Normal file
|
@ -0,0 +1,38 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Fred K. Schott
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
This license applies to parts of the `src/compiler` subdirectory originating from the
|
||||
https://github.com/sveltejs/svelte repository:
|
||||
|
||||
|
||||
Copyright (c) 2016-21 [these people](https://github.com/sveltejs/svelte/graphs/contributors)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
"""
|
13
package-lock.json
generated
13
package-lock.json
generated
|
@ -162,6 +162,11 @@
|
|||
"defer-to-connect": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@types/estree": {
|
||||
"version": "0.0.46",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.46.tgz",
|
||||
"integrity": "sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg=="
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
|
||||
|
@ -382,8 +387,7 @@
|
|||
"acorn-jsx": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
|
||||
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
|
||||
"dev": true
|
||||
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng=="
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.12.6",
|
||||
|
@ -1960,6 +1964,11 @@
|
|||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
|
||||
"dev": true
|
||||
},
|
||||
"locate-character": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-2.0.5.tgz",
|
||||
"integrity": "sha512-n2GmejDXtOPBAZdIiEFy5dJ5N38xBCXLNOtw2WpB9kGh6pnrEuKlwYI+Tkpofc4wDtVXHtoAOJaMRlYG/oYaxg=="
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
|
|
|
@ -24,9 +24,11 @@
|
|||
"copy-js:watch": "nodemon -w src --ext js --exec 'npm run copy-js'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/estree": "0.0.46",
|
||||
"@types/node": "^14.14.31",
|
||||
"@vue/server-renderer": "^3.0.7",
|
||||
"acorn": "^7.4.0",
|
||||
"acorn-jsx": "^5.3.1",
|
||||
"astring": "^1.7.0",
|
||||
"cheerio": "^0.22.0",
|
||||
"css-tree": "^1.1.2",
|
||||
|
@ -35,6 +37,7 @@
|
|||
"gray-matter": "^4.0.2",
|
||||
"htmlparser2": "^6.0.0",
|
||||
"kleur": "^4.1.4",
|
||||
"locate-character": "^2.0.5",
|
||||
"magic-string": "^0.25.3",
|
||||
"micromark": "^2.11.4",
|
||||
"micromark-extension-gfm": "^0.3.3",
|
||||
|
|
1
src/compiler.d.ts
vendored
1
src/compiler.d.ts
vendored
|
@ -1 +0,0 @@
|
|||
export { compile, parse, preprocess, walk, VERSION } from './@types/compiler/index';
|
21135
src/compiler.js
21135
src/compiler.js
File diff suppressed because it is too large
Load diff
3
src/compiler/README.md
Normal file
3
src/compiler/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# `hmx/compiler`
|
||||
|
||||
This directory is a fork of `svelte/compiler`. It is meant to stay as close to the original source as possible, so that upstream changes are easy to integrate. Everything svelte-specific and unrelated to parsing (compiler, preprocess, etc) has been removed.
|
75
src/compiler/Stats.ts
Normal file
75
src/compiler/Stats.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
// @ts-nocheck
|
||||
|
||||
const now = (typeof process !== 'undefined' && process.hrtime)
|
||||
? () => {
|
||||
const t = process.hrtime();
|
||||
return t[0] * 1e3 + t[1] / 1e6;
|
||||
}
|
||||
: () => self.performance.now();
|
||||
|
||||
interface Timing {
|
||||
label: string;
|
||||
start: number;
|
||||
end: number;
|
||||
children: Timing[];
|
||||
}
|
||||
|
||||
function collapse_timings(timings) {
|
||||
const result = {};
|
||||
timings.forEach(timing => {
|
||||
result[timing.label] = Object.assign({
|
||||
total: timing.end - timing.start
|
||||
}, timing.children && collapse_timings(timing.children));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export default class Stats {
|
||||
start_time: number;
|
||||
current_timing: Timing;
|
||||
current_children: Timing[];
|
||||
timings: Timing[];
|
||||
stack: Timing[];
|
||||
|
||||
constructor() {
|
||||
this.start_time = now();
|
||||
this.stack = [];
|
||||
this.current_children = this.timings = [];
|
||||
}
|
||||
|
||||
start(label) {
|
||||
const timing = {
|
||||
label,
|
||||
start: now(),
|
||||
end: null,
|
||||
children: []
|
||||
};
|
||||
|
||||
this.current_children.push(timing);
|
||||
this.stack.push(timing);
|
||||
|
||||
this.current_timing = timing;
|
||||
this.current_children = timing.children;
|
||||
}
|
||||
|
||||
stop(label) {
|
||||
if (label !== this.current_timing.label) {
|
||||
throw new Error(`Mismatched timing labels (expected ${this.current_timing.label}, got ${label})`);
|
||||
}
|
||||
|
||||
this.current_timing.end = now();
|
||||
this.stack.pop();
|
||||
this.current_timing = this.stack[this.stack.length - 1];
|
||||
this.current_children = this.current_timing ? this.current_timing.children : this.timings;
|
||||
}
|
||||
|
||||
render() {
|
||||
const timings = Object.assign({
|
||||
total: now() - this.start_time
|
||||
}, collapse_timings(this.timings));
|
||||
|
||||
return {
|
||||
timings
|
||||
};
|
||||
}
|
||||
}
|
1
src/compiler/config.ts
Normal file
1
src/compiler/config.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const test = typeof process !== 'undefined' && process.env.TEST;
|
1
src/compiler/index.ts
Normal file
1
src/compiler/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default as parse } from './parse/index.js';
|
179
src/compiler/interfaces.ts
Normal file
179
src/compiler/interfaces.ts
Normal file
|
@ -0,0 +1,179 @@
|
|||
import { Node, Program } from 'estree';
|
||||
import { SourceMap } from 'magic-string';
|
||||
|
||||
interface BaseNode {
|
||||
start: number;
|
||||
end: number;
|
||||
type: string;
|
||||
children?: TemplateNode[];
|
||||
[prop_name: string]: any;
|
||||
}
|
||||
|
||||
export interface Fragment extends BaseNode {
|
||||
type: 'Fragment';
|
||||
children: TemplateNode[];
|
||||
}
|
||||
|
||||
export interface Text extends BaseNode {
|
||||
type: 'Text';
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface MustacheTag extends BaseNode {
|
||||
type: 'MustacheTag';
|
||||
expression: string;
|
||||
}
|
||||
|
||||
export type DirectiveType = 'Action'
|
||||
| 'Animation'
|
||||
| 'Binding'
|
||||
| 'Class'
|
||||
| 'EventHandler'
|
||||
| 'Let'
|
||||
| 'Ref'
|
||||
| 'Transition';
|
||||
|
||||
interface BaseDirective extends BaseNode {
|
||||
type: DirectiveType;
|
||||
expression: null | Node;
|
||||
name: string;
|
||||
modifiers: string[];
|
||||
}
|
||||
|
||||
export interface Transition extends BaseDirective{
|
||||
type: 'Transition';
|
||||
intro: boolean;
|
||||
outro: boolean;
|
||||
}
|
||||
|
||||
export type Directive = BaseDirective | Transition;
|
||||
|
||||
export type TemplateNode = Text
|
||||
| MustacheTag
|
||||
| BaseNode
|
||||
| Directive
|
||||
| Transition;
|
||||
|
||||
export interface Parser {
|
||||
readonly template: string;
|
||||
readonly filename?: string;
|
||||
|
||||
index: number;
|
||||
stack: Node[];
|
||||
|
||||
html: Node;
|
||||
css: Node;
|
||||
js: Node;
|
||||
meta_tags: {};
|
||||
}
|
||||
|
||||
export interface Script extends BaseNode {
|
||||
type: 'Script';
|
||||
context: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface Style extends BaseNode {
|
||||
type: 'Style';
|
||||
attributes: any[]; // TODO
|
||||
content: {
|
||||
start: number;
|
||||
end: number;
|
||||
styles: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Ast {
|
||||
html: TemplateNode;
|
||||
css: Style;
|
||||
instance: Script;
|
||||
module: Script;
|
||||
}
|
||||
|
||||
export interface Warning {
|
||||
start?: { line: number; column: number; pos?: number };
|
||||
end?: { line: number; column: number };
|
||||
pos?: number;
|
||||
code: string;
|
||||
message: string;
|
||||
filename?: string;
|
||||
frame?: string;
|
||||
toString: () => string;
|
||||
}
|
||||
|
||||
export type ModuleFormat = 'esm' | 'cjs';
|
||||
|
||||
export type CssHashGetter = (args: {
|
||||
name: string;
|
||||
filename: string | undefined;
|
||||
css: string;
|
||||
hash: (input: string) => string;
|
||||
}) => string;
|
||||
|
||||
export interface CompileOptions {
|
||||
format?: ModuleFormat;
|
||||
name?: string;
|
||||
filename?: string;
|
||||
generate?: 'dom' | 'ssr' | false;
|
||||
|
||||
sourcemap?: object | string;
|
||||
outputFilename?: string;
|
||||
cssOutputFilename?: string;
|
||||
sveltePath?: string;
|
||||
|
||||
dev?: boolean;
|
||||
accessors?: boolean;
|
||||
immutable?: boolean;
|
||||
hydratable?: boolean;
|
||||
legacy?: boolean;
|
||||
customElement?: boolean;
|
||||
tag?: string;
|
||||
css?: boolean;
|
||||
loopGuardTimeout?: number;
|
||||
namespace?: string;
|
||||
cssHash?: CssHashGetter;
|
||||
|
||||
preserveComments?: boolean;
|
||||
preserveWhitespace?: boolean;
|
||||
}
|
||||
|
||||
export interface ParserOptions {
|
||||
filename?: string;
|
||||
customElement?: boolean;
|
||||
}
|
||||
|
||||
export interface Visitor {
|
||||
enter: (node: Node) => void;
|
||||
leave?: (node: Node) => void;
|
||||
}
|
||||
|
||||
export interface AppendTarget {
|
||||
slots: Record<string, string>;
|
||||
slot_stack: string[];
|
||||
}
|
||||
|
||||
export interface Var {
|
||||
name: string;
|
||||
export_name?: string; // the `bar` in `export { foo as bar }`
|
||||
injected?: boolean;
|
||||
module?: boolean;
|
||||
mutated?: boolean;
|
||||
reassigned?: boolean;
|
||||
referenced?: boolean; // referenced from template scope
|
||||
referenced_from_script?: boolean; // referenced from script
|
||||
writable?: boolean;
|
||||
|
||||
// used internally, but not exposed
|
||||
global?: boolean;
|
||||
internal?: boolean; // event handlers, bindings
|
||||
initialised?: boolean;
|
||||
hoistable?: boolean;
|
||||
subscribable?: boolean;
|
||||
is_reactive_dependency?: boolean;
|
||||
imported?: boolean;
|
||||
}
|
||||
|
||||
export interface CssResult {
|
||||
code: string;
|
||||
map: SourceMap;
|
||||
}
|
18
src/compiler/parse/acorn.ts
Normal file
18
src/compiler/parse/acorn.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Node } from 'acorn';
|
||||
import acorn from 'acorn';
|
||||
// @ts-ignore
|
||||
import jsx from 'acorn-jsx';
|
||||
|
||||
const acornJsx = acorn.Parser.extend(jsx());
|
||||
|
||||
export const parse = (source: string): Node => acorn.parse(source, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
locations: true
|
||||
});
|
||||
|
||||
export const parse_expression_at = (source: string, index: number): Node => acornJsx.parseExpressionAt(source, index, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
locations: true
|
||||
});
|
252
src/compiler/parse/index.ts
Normal file
252
src/compiler/parse/index.ts
Normal file
|
@ -0,0 +1,252 @@
|
|||
// @ts-nocheck
|
||||
|
||||
import { isIdentifierStart, isIdentifierChar } from 'acorn';
|
||||
import fragment from './state/fragment.js';
|
||||
import { whitespace } from '../utils/patterns.js';
|
||||
import { reserved } from '../utils/names.js';
|
||||
import full_char_code_at from '../utils/full_char_code_at.js';
|
||||
import { TemplateNode, Ast, ParserOptions, Fragment, Style, Script } from '../interfaces.js';
|
||||
import error from '../utils/error.js';
|
||||
|
||||
type ParserState = (parser: Parser) => (ParserState | void);
|
||||
|
||||
interface LastAutoClosedTag {
|
||||
tag: string;
|
||||
reason: string;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
export class Parser {
|
||||
readonly template: string;
|
||||
readonly filename?: string;
|
||||
readonly customElement: boolean;
|
||||
|
||||
index = 0;
|
||||
stack: TemplateNode[] = [];
|
||||
|
||||
html: Fragment;
|
||||
css: Style[] = [];
|
||||
js: Script[] = [];
|
||||
meta_tags = {};
|
||||
last_auto_closed_tag?: LastAutoClosedTag;
|
||||
|
||||
constructor(template: string, options: ParserOptions) {
|
||||
if (typeof template !== 'string') {
|
||||
throw new TypeError('Template must be a string');
|
||||
}
|
||||
|
||||
this.template = template.replace(/\s+$/, '');
|
||||
this.filename = options.filename;
|
||||
this.customElement = options.customElement;
|
||||
|
||||
this.html = {
|
||||
start: null,
|
||||
end: null,
|
||||
type: 'Fragment',
|
||||
children: []
|
||||
};
|
||||
|
||||
this.stack.push(this.html);
|
||||
|
||||
let state: ParserState = fragment;
|
||||
|
||||
while (this.index < this.template.length) {
|
||||
state = state(this) || fragment;
|
||||
}
|
||||
|
||||
if (this.stack.length > 1) {
|
||||
const current = this.current();
|
||||
|
||||
const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
|
||||
const slug = current.type === 'Element' ? 'element' : 'block';
|
||||
|
||||
this.error({
|
||||
code: `unclosed-${slug}`,
|
||||
message: `${type} was left open`
|
||||
}, current.start);
|
||||
}
|
||||
|
||||
if (state !== fragment) {
|
||||
this.error({
|
||||
code: 'unexpected-eof',
|
||||
message: 'Unexpected end of input'
|
||||
});
|
||||
}
|
||||
|
||||
if (this.html.children.length) {
|
||||
let start = this.html.children[0].start;
|
||||
while (whitespace.test(template[start])) start += 1;
|
||||
|
||||
let end = this.html.children[this.html.children.length - 1].end;
|
||||
while (whitespace.test(template[end - 1])) end -= 1;
|
||||
|
||||
this.html.start = start;
|
||||
this.html.end = end;
|
||||
} else {
|
||||
this.html.start = this.html.end = null;
|
||||
}
|
||||
}
|
||||
|
||||
current() {
|
||||
return this.stack[this.stack.length - 1];
|
||||
}
|
||||
|
||||
acorn_error(err: any) {
|
||||
this.error({
|
||||
code: 'parse-error',
|
||||
message: err.message.replace(/ \(\d+:\d+\)$/, '')
|
||||
}, err.pos);
|
||||
}
|
||||
|
||||
error({ code, message }: { code: string; message: string }, index = this.index) {
|
||||
error(message, {
|
||||
name: 'ParseError',
|
||||
code,
|
||||
source: this.template,
|
||||
start: index,
|
||||
filename: this.filename
|
||||
});
|
||||
}
|
||||
|
||||
eat(str: string, required?: boolean, message?: string) {
|
||||
if (this.match(str)) {
|
||||
this.index += str.length;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (required) {
|
||||
this.error({
|
||||
code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`,
|
||||
message: message || `Expected ${str}`
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
match(str: string) {
|
||||
return this.template.slice(this.index, this.index + str.length) === str;
|
||||
}
|
||||
|
||||
match_regex(pattern: RegExp) {
|
||||
const match = pattern.exec(this.template.slice(this.index));
|
||||
if (!match || match.index !== 0) return null;
|
||||
|
||||
return match[0];
|
||||
}
|
||||
|
||||
allow_whitespace() {
|
||||
while (
|
||||
this.index < this.template.length &&
|
||||
whitespace.test(this.template[this.index])
|
||||
) {
|
||||
this.index++;
|
||||
}
|
||||
}
|
||||
|
||||
read(pattern: RegExp) {
|
||||
const result = this.match_regex(pattern);
|
||||
if (result) this.index += result.length;
|
||||
return result;
|
||||
}
|
||||
|
||||
read_identifier(allow_reserved = false) {
|
||||
const start = this.index;
|
||||
|
||||
let i = this.index;
|
||||
|
||||
const code = full_char_code_at(this.template, i);
|
||||
if (!isIdentifierStart(code, true)) return null;
|
||||
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
|
||||
while (i < this.template.length) {
|
||||
const code = full_char_code_at(this.template, i);
|
||||
|
||||
if (!isIdentifierChar(code, true)) break;
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
}
|
||||
|
||||
const identifier = this.template.slice(this.index, this.index = i);
|
||||
|
||||
if (!allow_reserved && reserved.has(identifier)) {
|
||||
this.error({
|
||||
code: 'unexpected-reserved-word',
|
||||
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`
|
||||
}, start);
|
||||
}
|
||||
|
||||
return identifier;
|
||||
}
|
||||
|
||||
read_until(pattern: RegExp) {
|
||||
if (this.index >= this.template.length) {
|
||||
this.error({
|
||||
code: 'unexpected-eof',
|
||||
message: 'Unexpected end of input'
|
||||
});
|
||||
}
|
||||
|
||||
const start = this.index;
|
||||
const match = pattern.exec(this.template.slice(start));
|
||||
|
||||
if (match) {
|
||||
this.index = start + match.index;
|
||||
return this.template.slice(start, this.index);
|
||||
}
|
||||
|
||||
this.index = this.template.length;
|
||||
return this.template.slice(start);
|
||||
}
|
||||
|
||||
require_whitespace() {
|
||||
if (!whitespace.test(this.template[this.index])) {
|
||||
this.error({
|
||||
code: 'missing-whitespace',
|
||||
message: 'Expected whitespace'
|
||||
});
|
||||
}
|
||||
|
||||
this.allow_whitespace();
|
||||
}
|
||||
}
|
||||
|
||||
export default function parse(
|
||||
template: string,
|
||||
options: ParserOptions = {}
|
||||
): Ast {
|
||||
const parser = new Parser(template, options);
|
||||
|
||||
// TODO we may want to allow multiple <style> tags —
|
||||
// one scoped, one global. for now, only allow one
|
||||
if (parser.css.length > 1) {
|
||||
parser.error({
|
||||
code: 'duplicate-style',
|
||||
message: 'You can only have one top-level <style> tag per component'
|
||||
}, parser.css[1].start);
|
||||
}
|
||||
|
||||
const instance_scripts = parser.js.filter(script => script.context === 'default');
|
||||
const module_scripts = parser.js.filter(script => script.context === 'module');
|
||||
|
||||
if (instance_scripts.length > 1) {
|
||||
parser.error({
|
||||
code: 'invalid-script',
|
||||
message: 'A component can only have one instance-level <script> element'
|
||||
}, instance_scripts[1].start);
|
||||
}
|
||||
|
||||
if (module_scripts.length > 1) {
|
||||
parser.error({
|
||||
code: 'invalid-script',
|
||||
message: 'A component can only have one <script context="module"> element'
|
||||
}, module_scripts[1].start);
|
||||
}
|
||||
|
||||
return {
|
||||
html: parser.html,
|
||||
css: parser.css[0],
|
||||
instance: instance_scripts[0],
|
||||
module: module_scripts[0]
|
||||
};
|
||||
}
|
84
src/compiler/parse/read/context.ts
Normal file
84
src/compiler/parse/read/context.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
// @ts-nocheck
|
||||
|
||||
import { Parser } from '../index.js';
|
||||
import { isIdentifierStart } from 'acorn';
|
||||
import full_char_code_at from '../../utils/full_char_code_at.js';
|
||||
import {
|
||||
is_bracket_open,
|
||||
is_bracket_close,
|
||||
is_bracket_pair,
|
||||
get_bracket_close
|
||||
} from '../utils/bracket.js';
|
||||
import { parse_expression_at } from '../acorn.js';
|
||||
import { Pattern } from 'estree';
|
||||
|
||||
export default function read_context(
|
||||
parser: Parser
|
||||
): Pattern & { start: number; end: number } {
|
||||
const start = parser.index;
|
||||
let i = parser.index;
|
||||
|
||||
const code = full_char_code_at(parser.template, i);
|
||||
if (isIdentifierStart(code, true)) {
|
||||
return {
|
||||
type: 'Identifier',
|
||||
name: parser.read_identifier(),
|
||||
start,
|
||||
end: parser.index
|
||||
};
|
||||
}
|
||||
|
||||
if (!is_bracket_open(code)) {
|
||||
parser.error({
|
||||
code: 'unexpected-token',
|
||||
message: 'Expected identifier or destructure pattern'
|
||||
});
|
||||
}
|
||||
|
||||
const bracket_stack = [code];
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
|
||||
while (i < parser.template.length) {
|
||||
const code = full_char_code_at(parser.template, i);
|
||||
if (is_bracket_open(code)) {
|
||||
bracket_stack.push(code);
|
||||
} else if (is_bracket_close(code)) {
|
||||
if (!is_bracket_pair(bracket_stack[bracket_stack.length - 1], code)) {
|
||||
parser.error({
|
||||
code: 'unexpected-token',
|
||||
message: `Expected ${String.fromCharCode(
|
||||
get_bracket_close(bracket_stack[bracket_stack.length - 1])
|
||||
)}`
|
||||
});
|
||||
}
|
||||
bracket_stack.pop();
|
||||
if (bracket_stack.length === 0) {
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
}
|
||||
|
||||
parser.index = i;
|
||||
|
||||
const pattern_string = parser.template.slice(start, i);
|
||||
try {
|
||||
// the length of the `space_with_newline` has to be start - 1
|
||||
// because we added a `(` in front of the pattern_string,
|
||||
// which shifted the entire string to right by 1
|
||||
// so we offset it by removing 1 character in the `space_with_newline`
|
||||
// to achieve that, we remove the 1st space encountered,
|
||||
// so it will not affect the `column` of the node
|
||||
let space_with_newline = parser.template.slice(0, start).replace(/[^\n]/g, ' ');
|
||||
const first_space = space_with_newline.indexOf(' ');
|
||||
space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
|
||||
|
||||
return (parse_expression_at(
|
||||
`${space_with_newline}(${pattern_string} = 1)`,
|
||||
start - 1
|
||||
) as any).left;
|
||||
} catch (error) {
|
||||
parser.acorn_error(error);
|
||||
}
|
||||
}
|
41
src/compiler/parse/read/expression.ts
Normal file
41
src/compiler/parse/read/expression.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
// @ts-nocheck
|
||||
|
||||
import { parse_expression_at } from '../acorn.js';
|
||||
import { Parser } from '../index.js';
|
||||
import { whitespace } from '../../utils/patterns.js';
|
||||
// import { Node } from 'estree';
|
||||
|
||||
export default function read_expression(parser: Parser): string {
|
||||
try {
|
||||
const node = parse_expression_at(parser.template, parser.index);
|
||||
|
||||
let num_parens = 0;
|
||||
|
||||
for (let i = parser.index; i < node.start; i += 1) {
|
||||
if (parser.template[i] === '(') num_parens += 1;
|
||||
}
|
||||
|
||||
let index = node.end;
|
||||
while (num_parens > 0) {
|
||||
const char = parser.template[index];
|
||||
|
||||
if (char === ')') {
|
||||
num_parens -= 1;
|
||||
} else if (!whitespace.test(char)) {
|
||||
parser.error({
|
||||
code: 'unexpected-token',
|
||||
message: 'Expected )'
|
||||
}, index);
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
parser.index = index;
|
||||
|
||||
return parser.template.substring(node.start, node.end);
|
||||
// return node as Node;
|
||||
} catch (err) {
|
||||
parser.acorn_error(err);
|
||||
}
|
||||
}
|
55
src/compiler/parse/read/script.ts
Normal file
55
src/compiler/parse/read/script.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
// @ts-nocheck
|
||||
|
||||
import * as acorn from '../acorn';
|
||||
import { Parser } from '../index.js';
|
||||
import { Script } from '../../interfaces.js';
|
||||
import { Node, Program } from 'estree';
|
||||
|
||||
const script_closing_tag = '</script>';
|
||||
|
||||
function get_context(parser: Parser, attributes: any[], start: number): string {
|
||||
const context = attributes.find(attribute => attribute.name === 'context');
|
||||
if (!context) return 'default';
|
||||
|
||||
if (context.value.length !== 1 || context.value[0].type !== 'Text') {
|
||||
parser.error({
|
||||
code: 'invalid-script',
|
||||
message: 'context attribute must be static'
|
||||
}, start);
|
||||
}
|
||||
|
||||
const value = context.value[0].data;
|
||||
|
||||
if (value !== 'module') {
|
||||
parser.error({
|
||||
code: 'invalid-script',
|
||||
message: 'If the context attribute is supplied, its value must be "module"'
|
||||
}, context.start);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export default function read_script(parser: Parser, start: number, attributes: Node[]): Script {
|
||||
const script_start = parser.index;
|
||||
const script_end = parser.template.indexOf(script_closing_tag, script_start);
|
||||
|
||||
if (script_end === -1) {
|
||||
parser.error({
|
||||
code: 'unclosed-script',
|
||||
message: '<script> must have a closing tag'
|
||||
});
|
||||
}
|
||||
|
||||
const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') +
|
||||
parser.template.slice(script_start, script_end);
|
||||
parser.index = script_end + script_closing_tag.length;
|
||||
|
||||
return {
|
||||
type: 'Script',
|
||||
start,
|
||||
end: parser.index,
|
||||
context: get_context(parser, attributes, start),
|
||||
content: source
|
||||
};
|
||||
}
|
33
src/compiler/parse/read/style.ts
Normal file
33
src/compiler/parse/read/style.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { Parser } from '../index.js';
|
||||
import { Node } from 'estree';
|
||||
import { Style } from '../../interfaces.js';
|
||||
|
||||
export default function read_style(parser: Parser, start: number, attributes: Node[]): Style {
|
||||
const content_start = parser.index;
|
||||
const styles = parser.read_until(/<\/style>/);
|
||||
const content_end = parser.index;
|
||||
parser.eat('</style>', true);
|
||||
const end = parser.index;
|
||||
|
||||
return {
|
||||
type: 'Style',
|
||||
start,
|
||||
end,
|
||||
attributes,
|
||||
content: {
|
||||
start: content_start,
|
||||
end: content_end,
|
||||
styles
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function is_ref_selector(a: any, b: any) { // TODO add CSS node types
|
||||
if (!b) return false;
|
||||
|
||||
return (
|
||||
a.type === 'TypeSelector' &&
|
||||
a.name === 'ref' &&
|
||||
b.type === 'PseudoClassSelector'
|
||||
);
|
||||
}
|
16
src/compiler/parse/state/fragment.ts
Normal file
16
src/compiler/parse/state/fragment.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import tag from './tag.js';
|
||||
import mustache from './mustache.js';
|
||||
import text from './text.js';
|
||||
import { Parser } from '../index.js';
|
||||
|
||||
export default function fragment(parser: Parser) {
|
||||
if (parser.match('<')) {
|
||||
return tag;
|
||||
}
|
||||
|
||||
if (parser.match('{')) {
|
||||
return mustache;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
412
src/compiler/parse/state/mustache.ts
Normal file
412
src/compiler/parse/state/mustache.ts
Normal file
|
@ -0,0 +1,412 @@
|
|||
import read_context from '../read/context.js';
|
||||
import read_expression from '../read/expression.js';
|
||||
import { closing_tag_omitted } from '../utils/html.js';
|
||||
import { whitespace } from '../../utils/patterns.js';
|
||||
import { trim_start, trim_end } from '../../utils/trim.js';
|
||||
import { to_string } from '../utils/node.js';
|
||||
import { Parser } from '../index.js';
|
||||
import { TemplateNode } from '../../interfaces.js';
|
||||
|
||||
type TODO = any;
|
||||
|
||||
function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after: boolean) {
|
||||
if (!block.children || block.children.length === 0) return; // AwaitBlock
|
||||
|
||||
const first_child = block.children[0];
|
||||
const last_child = block.children[block.children.length - 1];
|
||||
|
||||
if (first_child.type === 'Text' && trim_before) {
|
||||
first_child.data = trim_start(first_child.data);
|
||||
if (!first_child.data) block.children.shift();
|
||||
}
|
||||
|
||||
if (last_child.type === 'Text' && trim_after) {
|
||||
last_child.data = trim_end(last_child.data);
|
||||
if (!last_child.data) block.children.pop();
|
||||
}
|
||||
|
||||
if (block.else) {
|
||||
trim_whitespace(block.else, trim_before, trim_after);
|
||||
}
|
||||
|
||||
if (first_child.elseif) {
|
||||
trim_whitespace(first_child, trim_before, trim_after);
|
||||
}
|
||||
}
|
||||
|
||||
export default function mustache(parser: Parser) {
|
||||
const start = parser.index;
|
||||
parser.index += 1;
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
// {/if}, {/each}, {/await} or {/key}
|
||||
if (parser.eat('/')) {
|
||||
let block = parser.current();
|
||||
let expected: TODO;
|
||||
|
||||
if (closing_tag_omitted(block.name)) {
|
||||
block.end = start;
|
||||
parser.stack.pop();
|
||||
block = parser.current();
|
||||
}
|
||||
|
||||
if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') {
|
||||
block.end = start;
|
||||
parser.stack.pop();
|
||||
block = parser.current();
|
||||
|
||||
expected = 'await';
|
||||
}
|
||||
|
||||
if (block.type === 'IfBlock') {
|
||||
expected = 'if';
|
||||
} else if (block.type === 'EachBlock') {
|
||||
expected = 'each';
|
||||
} else if (block.type === 'AwaitBlock') {
|
||||
expected = 'await';
|
||||
} else if (block.type === 'KeyBlock') {
|
||||
expected = 'key';
|
||||
} else {
|
||||
parser.error({
|
||||
code: 'unexpected-block-close',
|
||||
message: 'Unexpected block closing tag'
|
||||
});
|
||||
}
|
||||
|
||||
parser.eat(expected, true);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
while (block.elseif) {
|
||||
block.end = parser.index;
|
||||
parser.stack.pop();
|
||||
block = parser.current();
|
||||
|
||||
if (block.else) {
|
||||
block.else.end = start;
|
||||
}
|
||||
}
|
||||
|
||||
// strip leading/trailing whitespace as necessary
|
||||
const char_before = parser.template[block.start - 1];
|
||||
const char_after = parser.template[parser.index];
|
||||
const trim_before = !char_before || whitespace.test(char_before);
|
||||
const trim_after = !char_after || whitespace.test(char_after);
|
||||
|
||||
trim_whitespace(block, trim_before, trim_after);
|
||||
|
||||
block.end = parser.index;
|
||||
parser.stack.pop();
|
||||
} else if (parser.eat(':else')) {
|
||||
if (parser.eat('if')) {
|
||||
parser.error({
|
||||
code: 'invalid-elseif',
|
||||
message: "'elseif' should be 'else if'"
|
||||
});
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
// :else if
|
||||
if (parser.eat('if')) {
|
||||
const block = parser.current();
|
||||
if (block.type !== 'IfBlock') {
|
||||
parser.error({
|
||||
code: 'invalid-elseif-placement',
|
||||
message: parser.stack.some(block => block.type === 'IfBlock')
|
||||
? `Expected to close ${to_string(block)} before seeing {:else if ...} block`
|
||||
: 'Cannot have an {:else if ...} block outside an {#if ...} block'
|
||||
});
|
||||
}
|
||||
|
||||
parser.require_whitespace();
|
||||
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
block.else = {
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'ElseBlock',
|
||||
children: [
|
||||
{
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'IfBlock',
|
||||
elseif: true,
|
||||
expression,
|
||||
children: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
parser.stack.push(block.else.children[0]);
|
||||
} else {
|
||||
// :else
|
||||
const block = parser.current();
|
||||
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
|
||||
parser.error({
|
||||
code: 'invalid-else-placement',
|
||||
message: parser.stack.some(block => block.type === 'IfBlock' || block.type === 'EachBlock')
|
||||
? `Expected to close ${to_string(block)} before seeing {:else} block`
|
||||
: 'Cannot have an {:else} block outside an {#if ...} or {#each ...} block'
|
||||
});
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
block.else = {
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'ElseBlock',
|
||||
children: []
|
||||
};
|
||||
|
||||
parser.stack.push(block.else);
|
||||
}
|
||||
} else if (parser.match(':then') || parser.match(':catch')) {
|
||||
const block = parser.current();
|
||||
const is_then = parser.eat(':then') || !parser.eat(':catch');
|
||||
|
||||
if (is_then) {
|
||||
if (block.type !== 'PendingBlock') {
|
||||
parser.error({
|
||||
code: 'invalid-then-placement',
|
||||
message: parser.stack.some(block => block.type === 'PendingBlock')
|
||||
? `Expected to close ${to_string(block)} before seeing {:then} block`
|
||||
: 'Cannot have an {:then} block outside an {#await ...} block'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (block.type !== 'ThenBlock' && block.type !== 'PendingBlock') {
|
||||
parser.error({
|
||||
code: 'invalid-catch-placement',
|
||||
message: parser.stack.some(block => block.type === 'ThenBlock' || block.type === 'PendingBlock')
|
||||
? `Expected to close ${to_string(block)} before seeing {:catch} block`
|
||||
: 'Cannot have an {:catch} block outside an {#await ...} block'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
block.end = start;
|
||||
parser.stack.pop();
|
||||
const await_block = parser.current();
|
||||
|
||||
if (!parser.eat('}')) {
|
||||
parser.require_whitespace();
|
||||
await_block[is_then ? 'value' : 'error'] = read_context(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
}
|
||||
|
||||
const new_block: TemplateNode = {
|
||||
start,
|
||||
// @ts-ignore
|
||||
end: null,
|
||||
type: is_then ? 'ThenBlock' : 'CatchBlock',
|
||||
children: [],
|
||||
skip: false
|
||||
};
|
||||
|
||||
await_block[is_then ? 'then' : 'catch'] = new_block;
|
||||
parser.stack.push(new_block);
|
||||
} else if (parser.eat('#')) {
|
||||
// {#if foo}, {#each foo} or {#await foo}
|
||||
let type;
|
||||
|
||||
if (parser.eat('if')) {
|
||||
type = 'IfBlock';
|
||||
} else if (parser.eat('each')) {
|
||||
type = 'EachBlock';
|
||||
} else if (parser.eat('await')) {
|
||||
type = 'AwaitBlock';
|
||||
} else if (parser.eat('key')) {
|
||||
type = 'KeyBlock';
|
||||
} else {
|
||||
parser.error({
|
||||
code: 'expected-block-type',
|
||||
message: 'Expected if, each, await or key'
|
||||
});
|
||||
}
|
||||
|
||||
parser.require_whitespace();
|
||||
|
||||
const expression = read_expression(parser);
|
||||
|
||||
// @ts-ignore
|
||||
const block: TemplateNode = type === 'AwaitBlock' ?
|
||||
{
|
||||
start,
|
||||
end: null,
|
||||
type,
|
||||
expression,
|
||||
value: null,
|
||||
error: null,
|
||||
pending: {
|
||||
start: null,
|
||||
end: null,
|
||||
type: 'PendingBlock',
|
||||
children: [],
|
||||
skip: true
|
||||
},
|
||||
then: {
|
||||
start: null,
|
||||
end: null,
|
||||
type: 'ThenBlock',
|
||||
children: [],
|
||||
skip: true
|
||||
},
|
||||
catch: {
|
||||
start: null,
|
||||
end: null,
|
||||
type: 'CatchBlock',
|
||||
children: [],
|
||||
skip: true
|
||||
}
|
||||
} :
|
||||
{
|
||||
start,
|
||||
end: null,
|
||||
type,
|
||||
expression,
|
||||
children: []
|
||||
};
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
// {#each} blocks must declare a context – {#each list as item}
|
||||
if (type === 'EachBlock') {
|
||||
parser.eat('as', true);
|
||||
parser.require_whitespace();
|
||||
|
||||
block.context = read_context(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.eat(',')) {
|
||||
parser.allow_whitespace();
|
||||
block.index = parser.read_identifier();
|
||||
if (!block.index) {
|
||||
parser.error({
|
||||
code: 'expected-name',
|
||||
message: 'Expected name'
|
||||
});
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
if (parser.eat('(')) {
|
||||
parser.allow_whitespace();
|
||||
|
||||
block.key = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat(')', true);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
}
|
||||
|
||||
const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then');
|
||||
if (await_block_shorthand) {
|
||||
parser.require_whitespace();
|
||||
block.value = read_context(parser);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
const await_block_catch_shorthand = !await_block_shorthand && type === 'AwaitBlock' && parser.eat('catch');
|
||||
if (await_block_catch_shorthand) {
|
||||
parser.require_whitespace();
|
||||
block.error = read_context(parser);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
parser.eat('}', true);
|
||||
|
||||
// @ts-ignore
|
||||
parser.current().children.push(block);
|
||||
parser.stack.push(block);
|
||||
|
||||
if (type === 'AwaitBlock') {
|
||||
let child_block;
|
||||
if (await_block_shorthand) {
|
||||
block.then.skip = false;
|
||||
child_block = block.then;
|
||||
} else if (await_block_catch_shorthand) {
|
||||
block.catch.skip = false;
|
||||
child_block = block.catch;
|
||||
} else {
|
||||
block.pending.skip = false;
|
||||
child_block = block.pending;
|
||||
}
|
||||
|
||||
child_block.start = parser.index;
|
||||
parser.stack.push(child_block);
|
||||
}
|
||||
} else if (parser.eat('@html')) {
|
||||
// {@html content} tag
|
||||
parser.require_whitespace();
|
||||
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
// @ts-ignore
|
||||
parser.current().children.push({
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'RawMustacheTag',
|
||||
expression
|
||||
});
|
||||
} else if (parser.eat('@debug')) {
|
||||
// let identifiers;
|
||||
|
||||
// // Implies {@debug} which indicates "debug all"
|
||||
// if (parser.read(/\s*}/)) {
|
||||
// identifiers = [];
|
||||
// } else {
|
||||
// const expression = read_expression(parser);
|
||||
|
||||
// identifiers = expression.type === 'SequenceExpression'
|
||||
// ? expression.expressions
|
||||
// : [expression];
|
||||
|
||||
// identifiers.forEach(node => {
|
||||
// if (node.type !== 'Identifier') {
|
||||
// parser.error({
|
||||
// code: 'invalid-debug-args',
|
||||
// message: '{@debug ...} arguments must be identifiers, not arbitrary expressions'
|
||||
// }, node.start);
|
||||
// }
|
||||
// });
|
||||
|
||||
// parser.allow_whitespace();
|
||||
// parser.eat('}', true);
|
||||
// }
|
||||
|
||||
// parser.current().children.push({
|
||||
// start,
|
||||
// end: parser.index,
|
||||
// type: 'DebugTag',
|
||||
// identifiers
|
||||
// });
|
||||
throw new Error('@debug not yet supported');
|
||||
} else {
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
// @ts-ignore
|
||||
parser.current().children.push({
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'MustacheTag',
|
||||
expression
|
||||
});
|
||||
}
|
||||
}
|
532
src/compiler/parse/state/tag.ts
Normal file
532
src/compiler/parse/state/tag.ts
Normal file
|
@ -0,0 +1,532 @@
|
|||
// @ts-nocheck
|
||||
|
||||
import read_expression from '../read/expression.js';
|
||||
import read_script from '../read/script.js';
|
||||
import read_style from '../read/style.js';
|
||||
import { decode_character_references, closing_tag_omitted } from '../utils/html.js';
|
||||
import { is_void } from '../../utils/names.js';
|
||||
import { Parser } from '../index.js';
|
||||
import { Directive, DirectiveType, TemplateNode, Text } from '../../interfaces.js';
|
||||
import fuzzymatch from '../../utils/fuzzymatch.js';
|
||||
import list from '../../utils/list.js';
|
||||
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
|
||||
|
||||
const meta_tags = new Map([
|
||||
['svelte:head', 'Head'],
|
||||
['svelte:options', 'Options'],
|
||||
['svelte:window', 'Window'],
|
||||
['svelte:body', 'Body']
|
||||
]);
|
||||
|
||||
const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:fragment');
|
||||
|
||||
const specials = new Map([
|
||||
[
|
||||
'script',
|
||||
{
|
||||
read: read_script,
|
||||
property: 'js'
|
||||
}
|
||||
],
|
||||
[
|
||||
'style',
|
||||
{
|
||||
read: read_style,
|
||||
property: 'css'
|
||||
}
|
||||
]
|
||||
]);
|
||||
|
||||
const SELF = /^svelte:self(?=[\s/>])/;
|
||||
const COMPONENT = /^svelte:component(?=[\s/>])/;
|
||||
const SLOT = /^svelte:fragment(?=[\s/>])/;
|
||||
|
||||
function parent_is_head(stack) {
|
||||
let i = stack.length;
|
||||
while (i--) {
|
||||
const { type } = stack[i];
|
||||
if (type === 'Head') return true;
|
||||
if (type === 'Element' || type === 'InlineComponent') return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default function tag(parser: Parser) {
|
||||
const start = parser.index++;
|
||||
|
||||
let parent = parser.current();
|
||||
|
||||
if (parser.eat('!--')) {
|
||||
const data = parser.read_until(/-->/);
|
||||
parser.eat('-->', true, 'comment was left open, expected -->');
|
||||
|
||||
parser.current().children.push({
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'Comment',
|
||||
data
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const is_closing_tag = parser.eat('/');
|
||||
|
||||
const name = read_tag_name(parser);
|
||||
|
||||
if (meta_tags.has(name)) {
|
||||
const slug = meta_tags.get(name).toLowerCase();
|
||||
if (is_closing_tag) {
|
||||
if (
|
||||
(name === 'svelte:window' || name === 'svelte:body') &&
|
||||
parser.current().children.length
|
||||
) {
|
||||
parser.error({
|
||||
code: `invalid-${slug}-content`,
|
||||
message: `<${name}> cannot have children`
|
||||
}, parser.current().children[0].start);
|
||||
}
|
||||
} else {
|
||||
if (name in parser.meta_tags) {
|
||||
parser.error({
|
||||
code: `duplicate-${slug}`,
|
||||
message: `A component can only have one <${name}> tag`
|
||||
}, start);
|
||||
}
|
||||
|
||||
if (parser.stack.length > 1) {
|
||||
parser.error({
|
||||
code: `invalid-${slug}-placement`,
|
||||
message: `<${name}> tags cannot be inside elements or blocks`
|
||||
}, start);
|
||||
}
|
||||
|
||||
parser.meta_tags[name] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const type = meta_tags.has(name)
|
||||
? meta_tags.get(name)
|
||||
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
|
||||
: name === 'svelte:fragment' ? 'SlotTemplate'
|
||||
: name === 'title' && parent_is_head(parser.stack) ? 'Title'
|
||||
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
|
||||
|
||||
const element: TemplateNode = {
|
||||
start,
|
||||
end: null, // filled in later
|
||||
type,
|
||||
name,
|
||||
attributes: [],
|
||||
children: []
|
||||
};
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (is_closing_tag) {
|
||||
if (is_void(name)) {
|
||||
parser.error({
|
||||
code: 'invalid-void-content',
|
||||
message: `<${name}> is a void element and cannot have children, or a closing tag`
|
||||
}, start);
|
||||
}
|
||||
|
||||
parser.eat('>', true);
|
||||
|
||||
// close any elements that don't have their own closing tags, e.g. <div><p></div>
|
||||
while (parent.name !== name) {
|
||||
if (parent.type !== 'Element') {
|
||||
const message = parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name
|
||||
? `</${name}> attempted to close <${name}> that was already automatically closed by <${parser.last_auto_closed_tag.reason}>`
|
||||
: `</${name}> attempted to close an element that was not open`;
|
||||
parser.error({
|
||||
code: 'invalid-closing-tag',
|
||||
message
|
||||
}, start);
|
||||
}
|
||||
|
||||
parent.end = start;
|
||||
parser.stack.pop();
|
||||
|
||||
parent = parser.current();
|
||||
}
|
||||
|
||||
parent.end = parser.index;
|
||||
parser.stack.pop();
|
||||
|
||||
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
|
||||
parser.last_auto_closed_tag = null;
|
||||
}
|
||||
|
||||
return;
|
||||
} else if (closing_tag_omitted(parent.name, name)) {
|
||||
parent.end = start;
|
||||
parser.stack.pop();
|
||||
parser.last_auto_closed_tag = {
|
||||
tag: parent.name,
|
||||
reason: name,
|
||||
depth: parser.stack.length
|
||||
};
|
||||
}
|
||||
|
||||
const unique_names: Set<string> = new Set();
|
||||
|
||||
let attribute;
|
||||
while ((attribute = read_attribute(parser, unique_names))) {
|
||||
element.attributes.push(attribute);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
if (name === 'svelte:component') {
|
||||
const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this');
|
||||
if (!~index) {
|
||||
parser.error({
|
||||
code: 'missing-component-definition',
|
||||
message: "<svelte:component> must have a 'this' attribute"
|
||||
}, start);
|
||||
}
|
||||
|
||||
const definition = element.attributes.splice(index, 1)[0];
|
||||
if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
|
||||
parser.error({
|
||||
code: 'invalid-component-definition',
|
||||
message: 'invalid component definition'
|
||||
}, definition.start);
|
||||
}
|
||||
|
||||
element.expression = definition.value[0].expression;
|
||||
}
|
||||
|
||||
// special cases – top-level <script> and <style>
|
||||
if (specials.has(name) && parser.stack.length === 1) {
|
||||
const special = specials.get(name);
|
||||
|
||||
parser.eat('>', true);
|
||||
const content = special.read(parser, start, element.attributes);
|
||||
if (content) parser[special.property].push(content);
|
||||
return;
|
||||
}
|
||||
|
||||
parser.current().children.push(element);
|
||||
|
||||
const self_closing = parser.eat('/') || is_void(name);
|
||||
|
||||
parser.eat('>', true);
|
||||
|
||||
if (self_closing) {
|
||||
// don't push self-closing elements onto the stack
|
||||
element.end = parser.index;
|
||||
} else if (name === 'textarea') {
|
||||
// special case
|
||||
element.children = read_sequence(
|
||||
parser,
|
||||
() =>
|
||||
parser.template.slice(parser.index, parser.index + 11) === '</textarea>'
|
||||
);
|
||||
parser.read(/<\/textarea>/);
|
||||
element.end = parser.index;
|
||||
} else if (name === 'script' || name === 'style') {
|
||||
// special case
|
||||
const start = parser.index;
|
||||
const data = parser.read_until(new RegExp(`</${name}>`));
|
||||
const end = parser.index;
|
||||
element.children.push({ start, end, type: 'Text', data });
|
||||
parser.eat(`</${name}>`, true);
|
||||
element.end = parser.index;
|
||||
} else {
|
||||
parser.stack.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
function read_tag_name(parser: Parser) {
|
||||
const start = parser.index;
|
||||
|
||||
if (parser.read(SELF)) {
|
||||
// check we're inside a block, otherwise this
|
||||
// will cause infinite recursion
|
||||
let i = parser.stack.length;
|
||||
let legal = false;
|
||||
|
||||
while (i--) {
|
||||
const fragment = parser.stack[i];
|
||||
if (fragment.type === 'IfBlock' || fragment.type === 'EachBlock' || fragment.type === 'InlineComponent') {
|
||||
legal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!legal) {
|
||||
parser.error({
|
||||
code: 'invalid-self-placement',
|
||||
message: '<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, or slots passed to components'
|
||||
}, start);
|
||||
}
|
||||
|
||||
return 'svelte:self';
|
||||
}
|
||||
|
||||
if (parser.read(COMPONENT)) return 'svelte:component';
|
||||
|
||||
if (parser.read(SLOT)) return 'svelte:fragment';
|
||||
|
||||
const name = parser.read_until(/(\s|\/|>)/);
|
||||
|
||||
if (meta_tags.has(name)) return name;
|
||||
|
||||
if (name.startsWith('svelte:')) {
|
||||
const match = fuzzymatch(name.slice(7), valid_meta_tags);
|
||||
|
||||
let message = `Valid <svelte:...> tag names are ${list(valid_meta_tags)}`;
|
||||
if (match) message += ` (did you mean '${match}'?)`;
|
||||
|
||||
parser.error({
|
||||
code: 'invalid-tag-name',
|
||||
message
|
||||
}, start);
|
||||
}
|
||||
|
||||
if (!valid_tag_name.test(name)) {
|
||||
parser.error({
|
||||
code: 'invalid-tag-name',
|
||||
message: 'Expected valid tag name'
|
||||
}, start);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
function read_attribute(parser: Parser, unique_names: Set<string>) {
|
||||
const start = parser.index;
|
||||
|
||||
function check_unique(name: string) {
|
||||
if (unique_names.has(name)) {
|
||||
parser.error({
|
||||
code: 'duplicate-attribute',
|
||||
message: 'Attributes need to be unique'
|
||||
}, start);
|
||||
}
|
||||
unique_names.add(name);
|
||||
}
|
||||
|
||||
if (parser.eat('{')) {
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.eat('...')) {
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
return {
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'Spread',
|
||||
expression
|
||||
};
|
||||
} else {
|
||||
const value_start = parser.index;
|
||||
|
||||
const name = parser.read_identifier();
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
check_unique(name);
|
||||
|
||||
return {
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'Attribute',
|
||||
name,
|
||||
value: [{
|
||||
start: value_start,
|
||||
end: value_start + name.length,
|
||||
type: 'AttributeShorthand',
|
||||
expression: {
|
||||
start: value_start,
|
||||
end: value_start + name.length,
|
||||
type: 'Identifier',
|
||||
name
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const name = parser.read_until(/[\s=\/>"']/);
|
||||
if (!name) return null;
|
||||
|
||||
let end = parser.index;
|
||||
|
||||
parser.allow_whitespace();
|
||||
|
||||
const colon_index = name.indexOf(':');
|
||||
const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));
|
||||
|
||||
let value: any[] | true = true;
|
||||
if (parser.eat('=')) {
|
||||
parser.allow_whitespace();
|
||||
value = read_attribute_value(parser);
|
||||
end = parser.index;
|
||||
} else if (parser.match_regex(/["']/)) {
|
||||
parser.error({
|
||||
code: 'unexpected-token',
|
||||
message: 'Expected ='
|
||||
}, parser.index);
|
||||
}
|
||||
|
||||
if (type) {
|
||||
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
|
||||
|
||||
if (type === 'Binding' && directive_name !== 'this') {
|
||||
check_unique(directive_name);
|
||||
} else if (type !== 'EventHandler' && type !== 'Action') {
|
||||
check_unique(name);
|
||||
}
|
||||
|
||||
if (type === 'Ref') {
|
||||
parser.error({
|
||||
code: 'invalid-ref-directive',
|
||||
message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead`
|
||||
}, start);
|
||||
}
|
||||
|
||||
if (type === 'Class' && directive_name === '') {
|
||||
parser.error({
|
||||
code: 'invalid-class-directive',
|
||||
message: 'Class binding name cannot be empty'
|
||||
}, start + colon_index + 1);
|
||||
}
|
||||
|
||||
if (value[0]) {
|
||||
if ((value as any[]).length > 1 || value[0].type === 'Text') {
|
||||
parser.error({
|
||||
code: 'invalid-directive-value',
|
||||
message: 'Directive value must be a JavaScript expression enclosed in curly braces'
|
||||
}, value[0].start);
|
||||
}
|
||||
}
|
||||
|
||||
const directive: Directive = {
|
||||
start,
|
||||
end,
|
||||
type,
|
||||
name: directive_name,
|
||||
modifiers,
|
||||
expression: (value[0] && value[0].expression) || null
|
||||
};
|
||||
|
||||
if (type === 'Transition') {
|
||||
const direction = name.slice(0, colon_index);
|
||||
directive.intro = direction === 'in' || direction === 'transition';
|
||||
directive.outro = direction === 'out' || direction === 'transition';
|
||||
}
|
||||
|
||||
if (!directive.expression && (type === 'Binding' || type === 'Class')) {
|
||||
directive.expression = {
|
||||
start: directive.start + colon_index + 1,
|
||||
end: directive.end,
|
||||
type: 'Identifier',
|
||||
name: directive.name
|
||||
} as any;
|
||||
}
|
||||
|
||||
return directive;
|
||||
}
|
||||
|
||||
check_unique(name);
|
||||
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
type: 'Attribute',
|
||||
name,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
function get_directive_type(name: string): DirectiveType {
|
||||
if (name === 'use') return 'Action';
|
||||
if (name === 'animate') return 'Animation';
|
||||
if (name === 'bind') return 'Binding';
|
||||
if (name === 'class') return 'Class';
|
||||
if (name === 'on') return 'EventHandler';
|
||||
if (name === 'let') return 'Let';
|
||||
if (name === 'ref') return 'Ref';
|
||||
if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
|
||||
}
|
||||
|
||||
function read_attribute_value(parser: Parser) {
|
||||
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
|
||||
|
||||
const regex = (
|
||||
quote_mark === "'" ? /'/ :
|
||||
quote_mark === '"' ? /"/ :
|
||||
/(\/>|[\s"'=<>`])/
|
||||
);
|
||||
|
||||
const value = read_sequence(parser, () => !!parser.match_regex(regex));
|
||||
|
||||
if (quote_mark) parser.index += 1;
|
||||
return value;
|
||||
}
|
||||
|
||||
function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
|
||||
let current_chunk: Text = {
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: null
|
||||
};
|
||||
|
||||
function flush() {
|
||||
if (current_chunk.raw) {
|
||||
current_chunk.data = decode_character_references(current_chunk.raw);
|
||||
current_chunk.end = parser.index;
|
||||
chunks.push(current_chunk);
|
||||
}
|
||||
}
|
||||
|
||||
const chunks: TemplateNode[] = [];
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
const index = parser.index;
|
||||
|
||||
if (done()) {
|
||||
flush();
|
||||
return chunks;
|
||||
} else if (parser.eat('{')) {
|
||||
flush();
|
||||
|
||||
parser.allow_whitespace();
|
||||
const expression = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
chunks.push({
|
||||
start: index,
|
||||
end: parser.index,
|
||||
type: 'MustacheTag',
|
||||
expression
|
||||
});
|
||||
|
||||
current_chunk = {
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: null
|
||||
};
|
||||
} else {
|
||||
current_chunk.raw += parser.template[parser.index++];
|
||||
}
|
||||
}
|
||||
|
||||
parser.error({
|
||||
code: 'unexpected-eof',
|
||||
message: 'Unexpected end of input'
|
||||
});
|
||||
}
|
28
src/compiler/parse/state/text.ts
Normal file
28
src/compiler/parse/state/text.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
// @ts-nocheck
|
||||
|
||||
import { decode_character_references } from '../utils/html.js';
|
||||
import { Parser } from '../index.js';
|
||||
|
||||
export default function text(parser: Parser) {
|
||||
const start = parser.index;
|
||||
|
||||
let data = '';
|
||||
|
||||
while (
|
||||
parser.index < parser.template.length &&
|
||||
!parser.match('<') &&
|
||||
!parser.match('{')
|
||||
) {
|
||||
data += parser.template[parser.index++];
|
||||
}
|
||||
|
||||
const node = {
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'Text',
|
||||
raw: data,
|
||||
data: decode_character_references(data)
|
||||
};
|
||||
|
||||
parser.current().children.push(node);
|
||||
}
|
30
src/compiler/parse/utils/bracket.ts
Normal file
30
src/compiler/parse/utils/bracket.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
// @ts-nocheck
|
||||
|
||||
const SQUARE_BRACKET_OPEN = '['.charCodeAt(0);
|
||||
const SQUARE_BRACKET_CLOSE = ']'.charCodeAt(0);
|
||||
const CURLY_BRACKET_OPEN = '{'.charCodeAt(0);
|
||||
const CURLY_BRACKET_CLOSE = '}'.charCodeAt(0);
|
||||
|
||||
export function is_bracket_open(code) {
|
||||
return code === SQUARE_BRACKET_OPEN || code === CURLY_BRACKET_OPEN;
|
||||
}
|
||||
|
||||
export function is_bracket_close(code) {
|
||||
return code === SQUARE_BRACKET_CLOSE || code === CURLY_BRACKET_CLOSE;
|
||||
}
|
||||
|
||||
export function is_bracket_pair(open, close) {
|
||||
return (
|
||||
(open === SQUARE_BRACKET_OPEN && close === SQUARE_BRACKET_CLOSE) ||
|
||||
(open === CURLY_BRACKET_OPEN && close === CURLY_BRACKET_CLOSE)
|
||||
);
|
||||
}
|
||||
|
||||
export function get_bracket_close(open) {
|
||||
if (open === SQUARE_BRACKET_OPEN) {
|
||||
return SQUARE_BRACKET_CLOSE;
|
||||
}
|
||||
if (open === CURLY_BRACKET_OPEN) {
|
||||
return CURLY_BRACKET_CLOSE;
|
||||
}
|
||||
}
|
2034
src/compiler/parse/utils/entities.ts
Normal file
2034
src/compiler/parse/utils/entities.ts
Normal file
File diff suppressed because it is too large
Load diff
153
src/compiler/parse/utils/html.ts
Normal file
153
src/compiler/parse/utils/html.ts
Normal file
|
@ -0,0 +1,153 @@
|
|||
// @ts-nocheck
|
||||
|
||||
import entities from './entities.js';
|
||||
|
||||
const windows_1252 = [
|
||||
8364,
|
||||
129,
|
||||
8218,
|
||||
402,
|
||||
8222,
|
||||
8230,
|
||||
8224,
|
||||
8225,
|
||||
710,
|
||||
8240,
|
||||
352,
|
||||
8249,
|
||||
338,
|
||||
141,
|
||||
381,
|
||||
143,
|
||||
144,
|
||||
8216,
|
||||
8217,
|
||||
8220,
|
||||
8221,
|
||||
8226,
|
||||
8211,
|
||||
8212,
|
||||
732,
|
||||
8482,
|
||||
353,
|
||||
8250,
|
||||
339,
|
||||
157,
|
||||
382,
|
||||
376
|
||||
];
|
||||
|
||||
const entity_pattern = new RegExp(
|
||||
`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(entities).join('|')}))(?:;|\\b)`,
|
||||
'g'
|
||||
);
|
||||
|
||||
export function decode_character_references(html: string) {
|
||||
return html.replace(entity_pattern, (match, entity) => {
|
||||
let code;
|
||||
|
||||
// Handle named entities
|
||||
if (entity[0] !== '#') {
|
||||
code = entities[entity];
|
||||
} else if (entity[1] === 'x') {
|
||||
code = parseInt(entity.substring(2), 16);
|
||||
} else {
|
||||
code = parseInt(entity.substring(1), 10);
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
return match;
|
||||
}
|
||||
|
||||
return String.fromCodePoint(validate_code(code));
|
||||
});
|
||||
}
|
||||
|
||||
const NUL = 0;
|
||||
|
||||
// some code points are verboten. If we were inserting HTML, the browser would replace the illegal
|
||||
// code points with alternatives in some cases - since we're bypassing that mechanism, we need
|
||||
// to replace them ourselves
|
||||
//
|
||||
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
|
||||
function validate_code(code: number) {
|
||||
// line feed becomes generic whitespace
|
||||
if (code === 10) {
|
||||
return 32;
|
||||
}
|
||||
|
||||
// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
|
||||
if (code < 128) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need
|
||||
// to correct the mistake or we'll end up with missing € signs and so on
|
||||
if (code <= 159) {
|
||||
return windows_1252[code - 128];
|
||||
}
|
||||
|
||||
// basic multilingual plane
|
||||
if (code < 55296) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// UTF-16 surrogate halves
|
||||
if (code <= 57343) {
|
||||
return NUL;
|
||||
}
|
||||
|
||||
// rest of the basic multilingual plane
|
||||
if (code <= 65535) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// supplementary multilingual plane 0x10000 - 0x1ffff
|
||||
if (code >= 65536 && code <= 131071) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// supplementary ideographic plane 0x20000 - 0x2ffff
|
||||
if (code >= 131072 && code <= 196607) {
|
||||
return code;
|
||||
}
|
||||
|
||||
return NUL;
|
||||
}
|
||||
|
||||
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
|
||||
const disallowed_contents = new Map([
|
||||
['li', new Set(['li'])],
|
||||
['dt', new Set(['dt', 'dd'])],
|
||||
['dd', new Set(['dt', 'dd'])],
|
||||
[
|
||||
'p',
|
||||
new Set(
|
||||
'address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul'.split(
|
||||
' '
|
||||
)
|
||||
)
|
||||
],
|
||||
['rt', new Set(['rt', 'rp'])],
|
||||
['rp', new Set(['rt', 'rp'])],
|
||||
['optgroup', new Set(['optgroup'])],
|
||||
['option', new Set(['option', 'optgroup'])],
|
||||
['thead', new Set(['tbody', 'tfoot'])],
|
||||
['tbody', new Set(['tbody', 'tfoot'])],
|
||||
['tfoot', new Set(['tbody'])],
|
||||
['tr', new Set(['tr', 'tbody'])],
|
||||
['td', new Set(['td', 'th', 'tr'])],
|
||||
['th', new Set(['td', 'th', 'tr'])]
|
||||
]);
|
||||
|
||||
// can this be a child of the parent element, or does it implicitly
|
||||
// close it, like `<li>one<li>two`?
|
||||
export function closing_tag_omitted(current: string, next?: string) {
|
||||
if (disallowed_contents.has(current)) {
|
||||
if (!next || disallowed_contents.get(current).has(next)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
30
src/compiler/parse/utils/node.ts
Normal file
30
src/compiler/parse/utils/node.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { TemplateNode } from '../../interfaces.js';
|
||||
|
||||
export function to_string(node: TemplateNode) {
|
||||
switch (node.type) {
|
||||
case 'IfBlock':
|
||||
return '{#if} block';
|
||||
case 'ThenBlock':
|
||||
return '{:then} block';
|
||||
case 'ElseBlock':
|
||||
return '{:else} block';
|
||||
case 'PendingBlock':
|
||||
case 'AwaitBlock':
|
||||
return '{#await} block';
|
||||
case 'CatchBlock':
|
||||
return '{:catch} block';
|
||||
case 'EachBlock':
|
||||
return '{#each} block';
|
||||
case 'RawMustacheTag':
|
||||
return '{@html} block';
|
||||
case 'DebugTag':
|
||||
return '{@debug} block';
|
||||
case 'Element':
|
||||
case 'InlineComponent':
|
||||
case 'Slot':
|
||||
case 'Title':
|
||||
return `<${node.name}> tag`;
|
||||
default:
|
||||
return node.type;
|
||||
}
|
||||
}
|
42
src/compiler/utils/error.ts
Normal file
42
src/compiler/utils/error.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
// @ts-nocheck
|
||||
|
||||
import { locate } from 'locate-character';
|
||||
import get_code_frame from './get_code_frame.js';
|
||||
|
||||
class CompileError extends Error {
|
||||
code: string;
|
||||
start: { line: number; column: number };
|
||||
end: { line: number; column: number };
|
||||
pos: number;
|
||||
filename: string;
|
||||
frame: string;
|
||||
|
||||
toString() {
|
||||
return `${this.message} (${this.start.line}:${this.start.column})\n${this.frame}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default function error(message: string, props: {
|
||||
name: string;
|
||||
code: string;
|
||||
source: string;
|
||||
filename: string;
|
||||
start: number;
|
||||
end?: number;
|
||||
}): never {
|
||||
const error = new CompileError(message);
|
||||
error.name = props.name;
|
||||
|
||||
const start = locate(props.source, props.start, { offsetLine: 1 });
|
||||
const end = locate(props.source, props.end || props.start, { offsetLine: 1 });
|
||||
|
||||
error.code = props.code;
|
||||
error.start = start;
|
||||
error.end = end;
|
||||
error.pos = props.start;
|
||||
error.filename = props.filename;
|
||||
|
||||
error.frame = get_code_frame(props.source, start.line - 1, start.column);
|
||||
|
||||
throw error;
|
||||
}
|
10
src/compiler/utils/full_char_code_at.ts
Normal file
10
src/compiler/utils/full_char_code_at.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
// Adapted from https://github.com/acornjs/acorn/blob/6584815dca7440e00de841d1dad152302fdd7ca5/src/tokenize.js
|
||||
// Reproduced under MIT License https://github.com/acornjs/acorn/blob/master/LICENSE
|
||||
|
||||
export default function full_char_code_at(str: string, i: number): number {
|
||||
const code = str.charCodeAt(i);
|
||||
if (code <= 0xd7ff || code >= 0xe000) return code;
|
||||
|
||||
const next = str.charCodeAt(i + 1);
|
||||
return (code << 10) + next - 0x35fdc00;
|
||||
}
|
239
src/compiler/utils/fuzzymatch.ts
Normal file
239
src/compiler/utils/fuzzymatch.ts
Normal file
|
@ -0,0 +1,239 @@
|
|||
// @ts-nocheck
|
||||
|
||||
export default function fuzzymatch(name: string, names: string[]) {
|
||||
const set = new FuzzySet(names);
|
||||
const matches = set.get(name);
|
||||
|
||||
return matches && matches[0] && matches[0][0] > 0.7 ? matches[0][1] : null;
|
||||
}
|
||||
|
||||
// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js
|
||||
// BSD Licensed
|
||||
|
||||
const GRAM_SIZE_LOWER = 2;
|
||||
const GRAM_SIZE_UPPER = 3;
|
||||
|
||||
// return an edit distance from 0 to 1
|
||||
function _distance(str1: string, str2: string) {
|
||||
if (str1 === null && str2 === null) {
|
||||
throw 'Trying to compare two null values';
|
||||
}
|
||||
if (str1 === null || str2 === null) return 0;
|
||||
str1 = String(str1);
|
||||
str2 = String(str2);
|
||||
|
||||
const distance = levenshtein(str1, str2);
|
||||
if (str1.length > str2.length) {
|
||||
return 1 - distance / str1.length;
|
||||
} else {
|
||||
return 1 - distance / str2.length;
|
||||
}
|
||||
}
|
||||
|
||||
// helper functions
|
||||
function levenshtein(str1: string, str2: string) {
|
||||
const current: number[] = [];
|
||||
let prev;
|
||||
let value;
|
||||
|
||||
for (let i = 0; i <= str2.length; i++) {
|
||||
for (let j = 0; j <= str1.length; j++) {
|
||||
if (i && j) {
|
||||
if (str1.charAt(j - 1) === str2.charAt(i - 1)) {
|
||||
value = prev;
|
||||
} else {
|
||||
value = Math.min(current[j], current[j - 1], prev) + 1;
|
||||
}
|
||||
} else {
|
||||
value = i + j;
|
||||
}
|
||||
|
||||
prev = current[j];
|
||||
current[j] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return current.pop();
|
||||
}
|
||||
|
||||
const non_word_regex = /[^\w, ]+/;
|
||||
|
||||
function iterate_grams(value: string, gram_size = 2) {
|
||||
const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-';
|
||||
const len_diff = gram_size - simplified.length;
|
||||
const results = [];
|
||||
|
||||
if (len_diff > 0) {
|
||||
for (let i = 0; i < len_diff; ++i) {
|
||||
value += '-';
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < simplified.length - gram_size + 1; ++i) {
|
||||
results.push(simplified.slice(i, i + gram_size));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function gram_counter(value: string, gram_size = 2) {
|
||||
// return an object where key=gram, value=number of occurrences
|
||||
const result = {};
|
||||
const grams = iterate_grams(value, gram_size);
|
||||
let i = 0;
|
||||
|
||||
for (i; i < grams.length; ++i) {
|
||||
if (grams[i] in result) {
|
||||
result[grams[i]] += 1;
|
||||
} else {
|
||||
result[grams[i]] = 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function sort_descending(a, b) {
|
||||
return b[0] - a[0];
|
||||
}
|
||||
|
||||
class FuzzySet {
|
||||
exact_set = {};
|
||||
match_dict = {};
|
||||
items = {};
|
||||
|
||||
constructor(arr: string[]) {
|
||||
// initialization
|
||||
for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) {
|
||||
this.items[i] = [];
|
||||
}
|
||||
|
||||
// add all the items to the set
|
||||
for (let i = 0; i < arr.length; ++i) {
|
||||
this.add(arr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
add(value: string) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
if (normalized_value in this.exact_set) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let i = GRAM_SIZE_LOWER;
|
||||
for (i; i < GRAM_SIZE_UPPER + 1; ++i) {
|
||||
this._add(value, i);
|
||||
}
|
||||
}
|
||||
|
||||
_add(value: string, gram_size: number) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
const items = this.items[gram_size] || [];
|
||||
const index = items.length;
|
||||
|
||||
items.push(0);
|
||||
const gram_counts = gram_counter(normalized_value, gram_size);
|
||||
let sum_of_square_gram_counts = 0;
|
||||
let gram;
|
||||
let gram_count;
|
||||
|
||||
for (gram in gram_counts) {
|
||||
gram_count = gram_counts[gram];
|
||||
sum_of_square_gram_counts += Math.pow(gram_count, 2);
|
||||
if (gram in this.match_dict) {
|
||||
this.match_dict[gram].push([index, gram_count]);
|
||||
} else {
|
||||
this.match_dict[gram] = [[index, gram_count]];
|
||||
}
|
||||
}
|
||||
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
|
||||
items[index] = [vector_normal, normalized_value];
|
||||
this.items[gram_size] = items;
|
||||
this.exact_set[normalized_value] = value;
|
||||
}
|
||||
|
||||
get(value: string) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
const result = this.exact_set[normalized_value];
|
||||
|
||||
if (result) {
|
||||
return [[1, result]];
|
||||
}
|
||||
|
||||
let results = [];
|
||||
// start with high gram size and if there are no results, go to lower gram sizes
|
||||
for (
|
||||
let gram_size = GRAM_SIZE_UPPER;
|
||||
gram_size >= GRAM_SIZE_LOWER;
|
||||
--gram_size
|
||||
) {
|
||||
results = this.__get(value, gram_size);
|
||||
if (results) {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
__get(value: string, gram_size: number) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
const matches = {};
|
||||
const gram_counts = gram_counter(normalized_value, gram_size);
|
||||
const items = this.items[gram_size];
|
||||
let sum_of_square_gram_counts = 0;
|
||||
let gram;
|
||||
let gram_count;
|
||||
let i;
|
||||
let index;
|
||||
let other_gram_count;
|
||||
|
||||
for (gram in gram_counts) {
|
||||
gram_count = gram_counts[gram];
|
||||
sum_of_square_gram_counts += Math.pow(gram_count, 2);
|
||||
if (gram in this.match_dict) {
|
||||
for (i = 0; i < this.match_dict[gram].length; ++i) {
|
||||
index = this.match_dict[gram][i][0];
|
||||
other_gram_count = this.match_dict[gram][i][1];
|
||||
if (index in matches) {
|
||||
matches[index] += gram_count * other_gram_count;
|
||||
} else {
|
||||
matches[index] = gram_count * other_gram_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
|
||||
let results = [];
|
||||
let match_score;
|
||||
|
||||
// build a results list of [score, str]
|
||||
for (const match_index in matches) {
|
||||
match_score = matches[match_index];
|
||||
results.push([
|
||||
match_score / (vector_normal * items[match_index][0]),
|
||||
items[match_index][1]
|
||||
]);
|
||||
}
|
||||
|
||||
results.sort(sort_descending);
|
||||
|
||||
let new_results = [];
|
||||
const end_index = Math.min(50, results.length);
|
||||
// truncate somewhat arbitrarily to 50
|
||||
for (let i = 0; i < end_index; ++i) {
|
||||
new_results.push([
|
||||
_distance(results[i][1], normalized_value),
|
||||
results[i][1]
|
||||
]);
|
||||
}
|
||||
results = new_results;
|
||||
results.sort(sort_descending);
|
||||
|
||||
new_results = [];
|
||||
for (let i = 0; i < results.length; ++i) {
|
||||
if (results[i][0] == results[0][0]) {
|
||||
new_results.push([results[i][0], this.exact_set[results[i][1]]]);
|
||||
}
|
||||
}
|
||||
|
||||
return new_results;
|
||||
}
|
||||
}
|
31
src/compiler/utils/get_code_frame.ts
Normal file
31
src/compiler/utils/get_code_frame.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
function tabs_to_spaces(str: string) {
|
||||
return str.replace(/^\t+/, match => match.split('\t').join(' '));
|
||||
}
|
||||
|
||||
export default function get_code_frame(
|
||||
source: string,
|
||||
line: number,
|
||||
column: number
|
||||
) {
|
||||
const lines = source.split('\n');
|
||||
|
||||
const frame_start = Math.max(0, line - 2);
|
||||
const frame_end = Math.min(line + 3, lines.length);
|
||||
|
||||
const digits = String(frame_end + 1).length;
|
||||
|
||||
return lines
|
||||
.slice(frame_start, frame_end)
|
||||
.map((str, i) => {
|
||||
const isErrorLine = frame_start + i === line;
|
||||
const line_num = String(i + frame_start + 1).padStart(digits, ' ');
|
||||
|
||||
if (isErrorLine) {
|
||||
const indicator = ' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^';
|
||||
return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`;
|
||||
}
|
||||
|
||||
return `${line_num}: ${tabs_to_spaces(str)}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
4
src/compiler/utils/link.ts
Normal file
4
src/compiler/utils/link.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export function link<T extends { next?: T; prev?: T }>(next: T, prev: T) {
|
||||
prev.next = next;
|
||||
if (next) next.prev = prev;
|
||||
}
|
6
src/compiler/utils/list.ts
Normal file
6
src/compiler/utils/list.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default function list(items: string[], conjunction = 'or') {
|
||||
if (items.length === 1) return items[0];
|
||||
return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[
|
||||
items.length - 1
|
||||
]}`;
|
||||
}
|
139
src/compiler/utils/names.ts
Normal file
139
src/compiler/utils/names.ts
Normal file
|
@ -0,0 +1,139 @@
|
|||
import { isIdentifierStart, isIdentifierChar } from 'acorn';
|
||||
import full_char_code_at from './full_char_code_at.js';
|
||||
|
||||
export const globals = new Set([
|
||||
'alert',
|
||||
'Array',
|
||||
'Boolean',
|
||||
'clearInterval',
|
||||
'clearTimeout',
|
||||
'confirm',
|
||||
'console',
|
||||
'Date',
|
||||
'decodeURI',
|
||||
'decodeURIComponent',
|
||||
'document',
|
||||
'Element',
|
||||
'encodeURI',
|
||||
'encodeURIComponent',
|
||||
'Error',
|
||||
'EvalError',
|
||||
'Event',
|
||||
'EventSource',
|
||||
'fetch',
|
||||
'global',
|
||||
'globalThis',
|
||||
'history',
|
||||
'Infinity',
|
||||
'InternalError',
|
||||
'Intl',
|
||||
'isFinite',
|
||||
'isNaN',
|
||||
'JSON',
|
||||
'localStorage',
|
||||
'location',
|
||||
'Map',
|
||||
'Math',
|
||||
'NaN',
|
||||
'navigator',
|
||||
'Number',
|
||||
'Node',
|
||||
'Object',
|
||||
'parseFloat',
|
||||
'parseInt',
|
||||
'process',
|
||||
'Promise',
|
||||
'prompt',
|
||||
'RangeError',
|
||||
'ReferenceError',
|
||||
'RegExp',
|
||||
'sessionStorage',
|
||||
'Set',
|
||||
'setInterval',
|
||||
'setTimeout',
|
||||
'String',
|
||||
'SyntaxError',
|
||||
'TypeError',
|
||||
'undefined',
|
||||
'URIError',
|
||||
'URL',
|
||||
'window'
|
||||
]);
|
||||
|
||||
export const reserved = new Set([
|
||||
'arguments',
|
||||
'await',
|
||||
'break',
|
||||
'case',
|
||||
'catch',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'default',
|
||||
'delete',
|
||||
'do',
|
||||
'else',
|
||||
'enum',
|
||||
'eval',
|
||||
'export',
|
||||
'extends',
|
||||
'false',
|
||||
'finally',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'implements',
|
||||
'import',
|
||||
'in',
|
||||
'instanceof',
|
||||
'interface',
|
||||
'let',
|
||||
'new',
|
||||
'null',
|
||||
'package',
|
||||
'private',
|
||||
'protected',
|
||||
'public',
|
||||
'return',
|
||||
'static',
|
||||
'super',
|
||||
'switch',
|
||||
'this',
|
||||
'throw',
|
||||
'true',
|
||||
'try',
|
||||
'typeof',
|
||||
'var',
|
||||
'void',
|
||||
'while',
|
||||
'with',
|
||||
'yield'
|
||||
]);
|
||||
|
||||
const void_element_names = /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/;
|
||||
|
||||
export function is_void(name: string) {
|
||||
return void_element_names.test(name) || name.toLowerCase() === '!doctype';
|
||||
}
|
||||
|
||||
export function is_valid(str: string): boolean {
|
||||
let i = 0;
|
||||
|
||||
while (i < str.length) {
|
||||
const code = full_char_code_at(str, i);
|
||||
if (!(i === 0 ? isIdentifierStart : isIdentifierChar)(code, true)) return false;
|
||||
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function sanitize(name: string) {
|
||||
return name
|
||||
.replace(/[^a-zA-Z0-9_]+/g, '_')
|
||||
.replace(/^_/, '')
|
||||
.replace(/_$/, '')
|
||||
.replace(/^[0-9]/, '_$&');
|
||||
}
|
28
src/compiler/utils/namespaces.ts
Normal file
28
src/compiler/utils/namespaces.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
// The `foreign` namespace covers all DOM implementations that aren't HTML5.
|
||||
// It opts out of HTML5-specific a11y checks and case-insensitive attribute names.
|
||||
export const foreign = 'https://svelte.dev/docs#svelte_options';
|
||||
export const html = 'http://www.w3.org/1999/xhtml';
|
||||
export const mathml = 'http://www.w3.org/1998/Math/MathML';
|
||||
export const svg = 'http://www.w3.org/2000/svg';
|
||||
export const xlink = 'http://www.w3.org/1999/xlink';
|
||||
export const xml = 'http://www.w3.org/XML/1998/namespace';
|
||||
export const xmlns = 'http://www.w3.org/2000/xmlns';
|
||||
|
||||
export const valid_namespaces = [
|
||||
'foreign',
|
||||
'html',
|
||||
'mathml',
|
||||
'svg',
|
||||
'xlink',
|
||||
'xml',
|
||||
'xmlns',
|
||||
foreign,
|
||||
html,
|
||||
mathml,
|
||||
svg,
|
||||
xlink,
|
||||
xml,
|
||||
xmlns
|
||||
];
|
||||
|
||||
export const namespaces: Record<string, string> = { foreign, html, mathml, svg, xlink, xml, xmlns };
|
34
src/compiler/utils/nodes_match.ts
Normal file
34
src/compiler/utils/nodes_match.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
// @ts-nocheck
|
||||
|
||||
export function nodes_match(a, b) {
|
||||
if (!!a !== !!b) return false;
|
||||
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
||||
|
||||
if (a && typeof a === 'object') {
|
||||
if (Array.isArray(a)) {
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((child, i) => nodes_match(child, b[i]));
|
||||
}
|
||||
|
||||
const a_keys = Object.keys(a).sort();
|
||||
const b_keys = Object.keys(b).sort();
|
||||
|
||||
if (a_keys.length !== b_keys.length) return false;
|
||||
|
||||
let i = a_keys.length;
|
||||
while (i--) {
|
||||
const key = a_keys[i];
|
||||
if (b_keys[i] !== key) return false;
|
||||
|
||||
if (key === 'start' || key === 'end') continue;
|
||||
|
||||
if (!nodes_match(a[key], b[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return a === b;
|
||||
}
|
3
src/compiler/utils/patterns.ts
Normal file
3
src/compiler/utils/patterns.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const whitespace = /[ \t\r\n]/;
|
||||
|
||||
export const dimensions = /^(?:offset|client)(?:Width|Height)$/;
|
15
src/compiler/utils/trim.ts
Normal file
15
src/compiler/utils/trim.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { whitespace } from './patterns.js';
|
||||
|
||||
export function trim_start(str: string) {
|
||||
let i = 0;
|
||||
while (whitespace.test(str[i])) i += 1;
|
||||
|
||||
return str.slice(i);
|
||||
}
|
||||
|
||||
export function trim_end(str: string) {
|
||||
let i = str.length;
|
||||
while (whitespace.test(str[i - 1])) i -= 1;
|
||||
|
||||
return str.slice(0, i);
|
||||
}
|
296
src/parser.ts
296
src/parser.ts
|
@ -1,296 +0,0 @@
|
|||
const [CHARS, TAG_START, TAG_END, END_TAG_START, EQ, EOF, UNKNOWN] = Array.from(new Array(20), (x, i) => i + 1);
|
||||
|
||||
const voidTags = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
||||
|
||||
type Visitor = (tag: Tag) => Tag;
|
||||
|
||||
interface State {
|
||||
code: string;
|
||||
index: number;
|
||||
visitor: Visitor;
|
||||
tagName?: string;
|
||||
}
|
||||
|
||||
interface Attribute {
|
||||
name: string;
|
||||
value?: string;
|
||||
boolean: boolean;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
interface Text {
|
||||
type: 0;
|
||||
data: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
type: 1;
|
||||
tagName: string;
|
||||
attributes: Array<Attribute>;
|
||||
children: Array<Tag | Text>;
|
||||
void: boolean;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
interface Document {
|
||||
children: Array<Tag | Text>;
|
||||
}
|
||||
|
||||
function stateChar(state: State) {
|
||||
return state.code[state.index];
|
||||
}
|
||||
|
||||
function stateNext(state: State) {
|
||||
state.index++;
|
||||
return stateChar(state);
|
||||
}
|
||||
|
||||
function stateRewind(state: State) {
|
||||
state.index--;
|
||||
return stateChar(state);
|
||||
}
|
||||
|
||||
function stateInBounds(state: State) {
|
||||
return state.index < state.code.length;
|
||||
}
|
||||
|
||||
function createState(code: string, visitor: Visitor): State {
|
||||
return {
|
||||
code,
|
||||
index: 0,
|
||||
visitor,
|
||||
};
|
||||
}
|
||||
|
||||
function* _stringify(tag: Tag): Generator<string, void, unknown> {
|
||||
yield '<';
|
||||
yield tag.tagName;
|
||||
for (let attr of tag.attributes) {
|
||||
yield ' ';
|
||||
yield `"${attr.name}"`;
|
||||
if (!attr.boolean) {
|
||||
yield '=';
|
||||
yield `"${attr.value}"`;
|
||||
}
|
||||
}
|
||||
if (!tag.void) {
|
||||
for (let child of tag.children) {
|
||||
if (child.type === 0) {
|
||||
yield child.data;
|
||||
} else {
|
||||
yield* _stringify(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stringify(tag: Tag) {
|
||||
let out = '';
|
||||
for (let chunk of _stringify(tag)) {
|
||||
out += chunk;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function spliceSlice(str: string, index: number, count: number, add: string) {
|
||||
// We cannot pass negative indexes directly to the 2nd slicing operation.
|
||||
if (index < 0) {
|
||||
index = str.length + index;
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return str.slice(0, index) + (add || '') + str.slice(index + count);
|
||||
}
|
||||
|
||||
function replaceTag(state: State, tag: Tag) {
|
||||
const origLen = tag.end - tag.start;
|
||||
const html = stringify(tag);
|
||||
const newLen = html.length;
|
||||
const newCurIndex = tag.start + newLen;
|
||||
|
||||
state.code = spliceSlice(state.code, tag.start, origLen, html);
|
||||
state.index = newCurIndex;
|
||||
}
|
||||
|
||||
function consumeToken(state: State) {
|
||||
do {
|
||||
const c = stateNext(state);
|
||||
|
||||
if (/\s/.test(c)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c === '<') {
|
||||
return TAG_START;
|
||||
}
|
||||
|
||||
if (c === '>') {
|
||||
return TAG_END;
|
||||
}
|
||||
|
||||
if (c === '/') {
|
||||
return END_TAG_START;
|
||||
}
|
||||
|
||||
if (/[a-zA-Z]/.test(c)) {
|
||||
return CHARS;
|
||||
}
|
||||
|
||||
return UNKNOWN;
|
||||
} while (stateInBounds(state));
|
||||
|
||||
return EOF;
|
||||
}
|
||||
|
||||
function consumeText(state: State): Text {
|
||||
let start = state.index;
|
||||
let data = '';
|
||||
let c = stateNext(state);
|
||||
while (stateInBounds(state) && c !== '<') {
|
||||
data += c;
|
||||
c = stateNext(state);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 0,
|
||||
data,
|
||||
start,
|
||||
end: state.index - 1,
|
||||
};
|
||||
}
|
||||
|
||||
function consumeTagName(state: State): string {
|
||||
let name = '';
|
||||
let token = consumeToken(state);
|
||||
while (token === CHARS) {
|
||||
name += stateChar(state);
|
||||
token = consumeToken(state);
|
||||
}
|
||||
return name.toLowerCase();
|
||||
}
|
||||
|
||||
function consumeAttribute(state: State): Attribute {
|
||||
let start = state.index;
|
||||
let name = '',
|
||||
token;
|
||||
do {
|
||||
name += stateChar(state).toLowerCase();
|
||||
token = consumeToken(state);
|
||||
} while (token === CHARS);
|
||||
|
||||
if (token !== EQ) {
|
||||
stateRewind(state);
|
||||
return {
|
||||
name,
|
||||
boolean: true,
|
||||
start,
|
||||
end: state.index - 1,
|
||||
};
|
||||
}
|
||||
|
||||
let value = '';
|
||||
do {
|
||||
value += stateChar(state).toLowerCase();
|
||||
token = consumeToken(state);
|
||||
} while (token === CHARS);
|
||||
|
||||
return {
|
||||
name,
|
||||
value,
|
||||
boolean: false,
|
||||
start,
|
||||
end: state.index - 1,
|
||||
};
|
||||
}
|
||||
|
||||
function consumeChildren(state: State): Array<Tag | Text> {
|
||||
const children: Array<Tag | Text> = [];
|
||||
|
||||
childLoop: while (stateInBounds(state)) {
|
||||
const token = consumeToken(state);
|
||||
switch (token) {
|
||||
case TAG_START: {
|
||||
const next = consumeToken(state);
|
||||
if (next === END_TAG_START) {
|
||||
consumeTagName(state);
|
||||
consumeToken(state); // >
|
||||
break childLoop;
|
||||
} else {
|
||||
stateRewind(state);
|
||||
consumeTag(state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CHARS: {
|
||||
children.push(consumeText(state));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function consumeTag(state: State): Tag {
|
||||
const start = state.index - 1;
|
||||
const tagName = consumeTagName(state);
|
||||
const attributes: Array<Attribute> = [];
|
||||
|
||||
let token = consumeToken(state);
|
||||
|
||||
// Collect attributes
|
||||
attrLoop: while (token !== TAG_END) {
|
||||
switch (token) {
|
||||
case CHARS: {
|
||||
attributes.push(consumeAttribute(state));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break attrLoop;
|
||||
}
|
||||
}
|
||||
|
||||
token = consumeToken(state);
|
||||
}
|
||||
|
||||
const children: Array<Tag | Text> = consumeChildren(state);
|
||||
|
||||
const node: Tag = {
|
||||
type: 1,
|
||||
tagName,
|
||||
attributes,
|
||||
children,
|
||||
void: voidTags.has(tagName),
|
||||
start,
|
||||
end: state.index - 1,
|
||||
};
|
||||
|
||||
const replacement = state.visitor(node);
|
||||
if (replacement !== node) {
|
||||
replaceTag(state, node);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
function consumeDocument(state: State): Document {
|
||||
const children: Array<Tag | Text> = consumeChildren(state);
|
||||
|
||||
return {
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
export function preparse(code: string, visitor: Visitor) {
|
||||
const state = createState(code, visitor);
|
||||
consumeDocument(state);
|
||||
}
|
|
@ -8,9 +8,9 @@ import micromark from 'micromark';
|
|||
import gfmSyntax from 'micromark-extension-gfm';
|
||||
import matter from 'gray-matter';
|
||||
import gfmHtml from 'micromark-extension-gfm/html.js';
|
||||
import { walk, parse } from './compiler.js';
|
||||
import { walk } from 'estree-walker';
|
||||
import { parse } from './compiler/index.js';
|
||||
import markdownEncode from './markdown-encode.js';
|
||||
import { preparse } from './parser.js';
|
||||
|
||||
const { transformSync } = esbuild;
|
||||
|
||||
|
@ -153,56 +153,12 @@ function getComponentWrapper(_name: string, { type, url }: { type: string; url:
|
|||
throw new Error('Unknown Component Type: ' + name);
|
||||
}
|
||||
|
||||
function runPreparser(template: string): string {
|
||||
const doc = preparse(template, (tag) => {
|
||||
if (tag.tagName === 'script') {
|
||||
let isSetup = false;
|
||||
for (let attr of tag.attributes) {
|
||||
if (attr.name === 'hmx' && attr.value === 'setup') {
|
||||
isSetup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isSetup && typeof tag.children[0] === 'string') {
|
||||
debugger;
|
||||
|
||||
const content = tag.children[0];
|
||||
let { code } = transformSync(content, {
|
||||
loader: 'tsx',
|
||||
jsxFactory: 'h',
|
||||
jsxFragment: 'Fragment',
|
||||
charset: 'utf8',
|
||||
});
|
||||
return {
|
||||
...tag,
|
||||
children: [
|
||||
{
|
||||
type: 0,
|
||||
data: code,
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return tag;
|
||||
});
|
||||
|
||||
// TODO codegen
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
async function convertHmxToJsx(template: string, compileOptions: CompileOptions) {
|
||||
await eslexer.init;
|
||||
|
||||
//template = runPreparser(template);
|
||||
|
||||
const ast = parse(template, {});
|
||||
const script = ast.instance ? ast.instance.content : "";
|
||||
// Todo: Validate that `h` and `Fragment` aren't defined in the script
|
||||
const script = ast.instance ? astring.generate(ast.instance.content) : '';
|
||||
|
||||
const [scriptImports] = eslexer.parse(script, 'optional-sourcename');
|
||||
const components = Object.fromEntries(
|
||||
|
@ -221,6 +177,7 @@ async function convertHmxToJsx(template: string, compileOptions: CompileOptions)
|
|||
let currentDepth = 0;
|
||||
|
||||
walk(ast.html as any, {
|
||||
// @ts-ignore
|
||||
enter(node: TemplateNode, parent, prop, index) {
|
||||
// console.log("enter", node.type);
|
||||
switch (node.type) {
|
||||
|
@ -329,6 +286,7 @@ async function convertHmxToJsx(template: string, compileOptions: CompileOptions)
|
|||
throw new Error('Unexpected node type: ' + node.type);
|
||||
}
|
||||
},
|
||||
// @ts-ignore
|
||||
leave(node: TemplateNode, parent, prop, index) {
|
||||
// console.log("leave", node.type);
|
||||
switch (node.type) {
|
||||
|
|
Loading…
Reference in a new issue