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:
Matthew Phillips 2021-03-16 16:08:11 -04:00 committed by GitHub
parent 174fc1d669
commit 588b086a4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 4585 additions and 21481 deletions

38
LICENSE Normal file
View 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
View file

@ -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",

View file

@ -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
View file

@ -1 +0,0 @@
export { compile, parse, preprocess, walk, VERSION } from './@types/compiler/index';

File diff suppressed because it is too large Load diff

3
src/compiler/README.md Normal file
View 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
View 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
View file

@ -0,0 +1 @@
export const test = typeof process !== 'undefined' && process.env.TEST;

1
src/compiler/index.ts Normal file
View file

@ -0,0 +1 @@
export { default as parse } from './parse/index.js';

179
src/compiler/interfaces.ts Normal file
View 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;
}

View 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
View 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]
};
}

View 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);
}
}

View 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);
}
}

View 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
};
}

View 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'
);
}

View 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;
}

View 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
});
}
}

View 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'
});
}

View 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);
}

View 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;
}
}

File diff suppressed because it is too large Load diff

View 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;
}

View 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;
}
}

View 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;
}

View 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;
}

View 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;
}
}

View 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');
}

View 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;
}

View 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
View 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]/, '_$&');
}

View 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 };

View 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;
}

View file

@ -0,0 +1,3 @@
export const whitespace = /[ \t\r\n]/;
export const dimensions = /^(?:offset|client)(?:Width|Height)$/;

View 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);
}

View file

@ -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);
}

View file

@ -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) {