fix: vite

This commit is contained in:
2025-03-12 03:23:38 -04:00
parent 58b08839cc
commit c442635d6b
1630 changed files with 888315 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
/** @import { Comment, Program } from 'estree' */
/** @import { Node } from 'acorn' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from 'acorn-typescript';
import { locator } from '../../state.js';
const ParserWithTS = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }));
/**
* @param {string} source
* @param {boolean} typescript
* @param {boolean} [is_script]
*/
export function parse(source, typescript, is_script) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
// @ts-ignore
const parse_statement = parser.prototype.parseStatement;
// If we're dealing with a <script> then it might contain an export
// for something that doesn't exist directly inside but is inside the
// component instead, so we need to ensure that Acorn doesn't throw
// an error in these cases
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = function (...args) {
const v = parse_statement.call(this, ...args);
// @ts-ignore
this.undefinedExports = {};
return v;
};
}
let ast;
try {
ast = parser.parse(source, {
onComment,
sourceType: 'module',
ecmaVersion: 13,
locations: true
});
} finally {
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = parse_statement;
}
}
if (typescript) amend(source, ast);
add_comments(ast);
return /** @type {Program} */ (ast);
}
/**
* @param {string} source
* @param {boolean} typescript
* @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/
export function parse_expression_at(source, typescript, index) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
const ast = parser.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 13,
locations: true
});
if (typescript) amend(source, ast);
add_comments(ast);
return ast;
}
/**
* Acorn doesn't add comments to the AST by itself. This factory returns the capabilities
* to add them after the fact. They are needed in order to support `svelte-ignore` comments
* in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting.
* @param {string} source
*/
function get_comment_handlers(source) {
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/** @type {CommentWithLocation[]} */
const comments = [];
return {
/**
* @param {boolean} block
* @param {string} value
* @param {number} start
* @param {number} end
*/
onComment: (block, value, start, end) => {
if (block && /\n/.test(value)) {
let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;
let b = a;
while (/[ \t]/.test(source[b])) b += 1;
const indentation = source.slice(a, b);
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
comments.push({ type: block ? 'Block' : 'Line', value, start, end });
},
/** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
add_comments(ast) {
if (comments.length === 0) return;
walk(ast, null, {
_(node, { next, path }) {
let comment;
while (comments[0] && comments[0].start < node.start) {
comment = /** @type {CommentWithLocation} */ (comments.shift());
(node.leadingComments ||= []).push(comment);
}
next();
if (comments[0]) {
const parent = /** @type {any} */ (path.at(-1));
if (parent === undefined || node.end !== parent.end) {
const slice = source.slice(node.end, comments[0].start);
const is_last_in_body =
((parent?.type === 'BlockStatement' || parent?.type === 'Program') &&
parent.body.indexOf(node) === parent.body.length - 1) ||
(parent?.type === 'ArrayExpression' &&
parent.elements.indexOf(node) === parent.elements.length - 1) ||
(parent?.type === 'ObjectExpression' &&
parent.properties.indexOf(node) === parent.properties.length - 1);
if (is_last_in_body) {
// Special case: There can be multiple trailing comments after the last node in a block,
// and they can be separated by newlines
let end = node.end;
while (comments.length) {
const comment = comments[0];
if (parent && comment.start >= parent.end) break;
(node.trailingComments ||= []).push(comment);
comments.shift();
end = comment.end;
}
} else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
}
}
}
}
});
// Special case: Trailing comments after the root node (which can only happen for expression tags or for Program nodes).
// Adding them ensures that we can later detect the end of the expression tag correctly.
if (comments.length > 0 && (comments[0].start >= ast.end || ast.type === 'Program')) {
(ast.trailingComments ||= []).push(...comments.splice(0));
}
}
};
}
/**
* Tidy up some stuff left behind by acorn-typescript
* @param {string} source
* @param {Node} node
*/
function amend(source, node) {
return walk(node, null, {
_(node, context) {
// @ts-expect-error
delete node.loc.start.index;
// @ts-expect-error
delete node.loc.end.index;
if (typeof node.loc?.end === 'number') {
const loc = locator(node.loc.end);
if (loc) {
node.loc.end = {
line: loc.line,
column: loc.column
};
}
}
if (
/** @type {any} */ (node).typeAnnotation &&
(node.end === undefined || node.end < node.start)
) {
// i think there might be a bug in acorn-typescript that prevents
// `end` from being assigned when there's a type annotation
let end = /** @type {any} */ (node).typeAnnotation.start;
while (/\s/.test(source[end - 1])) end -= 1;
node.end = end;
}
context.next();
}
});
}

View File

@@ -0,0 +1,3 @@
// Silence the acorn typescript errors through this ambient type definition + tsconfig.json path alias
// That way we can omit `"skipLibCheck": true` and catch other errors in our d.ts files
declare module 'acorn-typescript';

View File

@@ -0,0 +1,312 @@
/** @import { AST } from '#compiler' */
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';
import { regex_whitespace } from '../patterns.js';
import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js';
import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
const regex_position_indicator = / \(\d+:\d+\)$/;
const regex_lang_attribute =
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
export class Parser {
/**
* @readonly
* @type {string}
*/
template;
/**
* @readonly
* @type {string}
*/
template_untrimmed;
/**
* Whether or not we're in loose parsing mode, in which
* case we try to continue parsing as much as possible
* @type {boolean}
*/
loose;
/** */
index = 0;
/** Whether we're parsing in TypeScript mode */
ts = false;
/** @type {AST.TemplateNode[]} */
stack = [];
/** @type {AST.Fragment[]} */
fragments = [];
/** @type {AST.Root} */
root;
/** @type {Record<string, boolean>} */
meta_tags = {};
/** @type {LastAutoClosedTag | undefined} */
last_auto_closed_tag;
/**
* @param {string} template
* @param {boolean} loose
*/
constructor(template, loose) {
if (typeof template !== 'string') {
throw new TypeError('Template must be a string');
}
this.loose = loose;
this.template_untrimmed = template;
this.template = template.trimEnd();
let match_lang;
do match_lang = regex_lang_attribute.exec(template);
while (match_lang && match_lang[0][1] !== 's'); // ensure it starts with '<s' to match script tags
regex_lang_attribute.lastIndex = 0; // reset matched index to pass tests - otherwise declare the regex inside the constructor
this.ts = match_lang?.[2] === 'ts';
this.root = {
css: null,
js: [],
// @ts-ignore
start: null,
// @ts-ignore
end: null,
type: 'Root',
fragment: create_fragment(),
options: null,
metadata: {
ts: this.ts
}
};
this.stack.push(this.root);
this.fragments.push(this.root.fragment);
/** @type {ParserState} */
let state = fragment;
while (this.index < this.template.length) {
state = state(this) || fragment;
}
if (this.stack.length > 1) {
const current = this.current();
if (this.loose) {
current.end = this.template.length;
} else if (current.type === 'RegularElement') {
current.end = current.start + 1;
e.element_unclosed(current, current.name);
} else {
current.end = current.start + 1;
e.block_unclosed(current);
}
}
if (state !== fragment) {
e.unexpected_eof(this.index);
}
if (this.root.fragment.nodes.length) {
let start = /** @type {number} */ (this.root.fragment.nodes[0].start);
while (regex_whitespace.test(template[start])) start += 1;
let end = /** @type {number} */ (
this.root.fragment.nodes[this.root.fragment.nodes.length - 1].end
);
while (regex_whitespace.test(template[end - 1])) end -= 1;
this.root.start = start;
this.root.end = end;
} else {
// @ts-ignore
this.root.start = this.root.end = null;
}
const options_index = this.root.fragment.nodes.findIndex(
/** @param {any} thing */
(thing) => thing.type === 'SvelteOptions'
);
if (options_index !== -1) {
const options = /** @type {AST.SvelteOptionsRaw} */ (this.root.fragment.nodes[options_index]);
this.root.fragment.nodes.splice(options_index, 1);
this.root.options = read_options(options);
disallow_children(options);
// We need this for the old AST format
Object.defineProperty(this.root.options, '__raw__', {
value: options,
enumerable: false
});
}
}
current() {
return this.stack[this.stack.length - 1];
}
/**
* @param {any} err
* @returns {never}
*/
acorn_error(err) {
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
}
/**
* @param {string} str
* @param {boolean} required
* @param {boolean} required_in_loose
*/
eat(str, required = false, required_in_loose = true) {
if (this.match(str)) {
this.index += str.length;
return true;
}
if (required && (!this.loose || required_in_loose)) {
e.expected_token(this.index, str);
}
return false;
}
/** @param {string} str */
match(str) {
const length = str.length;
if (length === 1) {
// more performant than slicing
return this.template[this.index] === str;
}
return this.template.slice(this.index, this.index + length) === str;
}
/**
* Match a regex at the current index
* @param {RegExp} pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
*/
match_regex(pattern) {
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 && regex_whitespace.test(this.template[this.index])) {
this.index++;
}
}
/**
* Search for a regex starting at the current index and return the result if it matches
* @param {RegExp} pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
*/
read(pattern) {
const result = this.match_regex(pattern);
if (result) this.index += result.length;
return result;
}
/** @param {any} allow_reserved */
read_identifier(allow_reserved = false) {
const start = this.index;
let i = this.index;
const code = /** @type {number} */ (this.template.codePointAt(i));
if (!isIdentifierStart(code, true)) return null;
i += code <= 0xffff ? 1 : 2;
while (i < this.template.length) {
const code = /** @type {number} */ (this.template.codePointAt(i));
if (!isIdentifierChar(code, true)) break;
i += code <= 0xffff ? 1 : 2;
}
const identifier = this.template.slice(this.index, (this.index = i));
if (!allow_reserved && is_reserved(identifier)) {
e.unexpected_reserved_word(start, identifier);
}
return identifier;
}
/** @param {RegExp} pattern */
read_until(pattern) {
if (this.index >= this.template.length) {
if (this.loose) return '';
e.unexpected_eof(this.template.length);
}
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 (!regex_whitespace.test(this.template[this.index])) {
e.expected_whitespace(this.index);
}
this.allow_whitespace();
}
pop() {
this.fragments.pop();
return this.stack.pop();
}
/**
* @template {AST.Fragment['nodes'][number]} T
* @param {T} node
* @returns {T}
*/
append(node) {
this.fragments.at(-1)?.nodes.push(node);
return node;
}
}
/**
* @param {string} template
* @param {boolean} [loose]
* @returns {AST.Root}
*/
export function parse(template, loose = false) {
const parser = new Parser(template, loose);
return parser.root;
}
/** @typedef {(parser: Parser) => ParserState | void} ParserState */
/** @typedef {Object} LastAutoClosedTag
* @property {string} tag
* @property {string} reason
* @property {number} depth
*/

View File

@@ -0,0 +1,187 @@
/** @import { Location } from 'locate-character' */
/** @import { Pattern } from 'estree' */
/** @import { Parser } from '../index.js' */
import { is_bracket_open, is_bracket_close, get_bracket_close } from '../utils/bracket.js';
import { parse_expression_at } from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
import { locator } from '../../../state.js';
/**
* @param {Parser} parser
* @returns {Pattern}
*/
export default function read_pattern(parser) {
const start = parser.index;
let i = parser.index;
const name = parser.read_identifier();
if (name !== null) {
const annotation = read_type_annotation(parser);
return {
type: 'Identifier',
name,
start,
loc: {
start: /** @type {Location} */ (locator(start)),
end: /** @type {Location} */ (locator(parser.index))
},
end: parser.index,
typeAnnotation: annotation
};
}
if (!is_bracket_open(parser.template[i])) {
e.expected_pattern(i);
}
i = match_bracket(parser, start);
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(regex_not_newline_characters, ' ');
const first_space = space_with_newline.indexOf(' ');
space_with_newline =
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
const expression = /** @type {any} */ (
parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, parser.ts, start - 1)
).left;
expression.typeAnnotation = read_type_annotation(parser);
if (expression.typeAnnotation) {
expression.end = expression.typeAnnotation.end;
}
return expression;
} catch (error) {
parser.acorn_error(error);
}
}
/**
* @param {Parser} parser
* @param {number} start
*/
function match_bracket(parser, start) {
const bracket_stack = [];
let i = start;
while (i < parser.template.length) {
let char = parser.template[i++];
if (char === "'" || char === '"' || char === '`') {
i = match_quote(parser, i, char);
continue;
}
if (is_bracket_open(char)) {
bracket_stack.push(char);
} else if (is_bracket_close(char)) {
const popped = /** @type {string} */ (bracket_stack.pop());
const expected = /** @type {string} */ (get_bracket_close(popped));
if (char !== expected) {
e.expected_token(i - 1, expected);
}
if (bracket_stack.length === 0) {
return i;
}
}
}
e.unexpected_eof(parser.template.length);
}
/**
* @param {Parser} parser
* @param {number} start
* @param {string} quote
*/
function match_quote(parser, start, quote) {
let is_escaped = false;
let i = start;
while (i < parser.template.length) {
const char = parser.template[i++];
if (is_escaped) {
is_escaped = false;
continue;
}
if (char === quote) {
return i;
}
if (char === '\\') {
is_escaped = true;
}
if (quote === '`' && char === '$' && parser.template[i] === '{') {
i = match_bracket(parser, i);
}
}
e.unterminated_string_constant(start);
}
/**
* @param {Parser} parser
* @returns {any}
*/
function read_type_annotation(parser) {
const start = parser.index;
parser.allow_whitespace();
if (!parser.eat(':')) {
parser.index = start;
return undefined;
}
// we need to trick Acorn into parsing the type annotation
const insert = '_ as ';
let a = parser.index - insert.length;
const template =
parser.template.slice(0, a).replace(/[^\n]/g, ' ') +
insert +
// If this is a type annotation for a function parameter, Acorn-TS will treat subsequent
// parameters as part of a sequence expression instead, and will then error on optional
// parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
let expression = parse_expression_at(template, parser.ts, a);
// `foo: bar = baz` gets mangled — fix it
if (expression.type === 'AssignmentExpression') {
let b = expression.right.start;
while (template[b] !== '=') b -= 1;
expression = parse_expression_at(template.slice(0, b), parser.ts, a);
}
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that
if (expression.type === 'SequenceExpression') {
expression = expression.expressions[0];
}
parser.index = /** @type {number} */ (expression.end);
return {
type: 'TSTypeAnnotation',
start,
end: parser.index,
typeAnnotation: /** @type {any} */ (expression).typeAnnotation
};
}

View File

@@ -0,0 +1,81 @@
/** @import { Expression } from 'estree' */
/** @import { Parser } from '../index.js' */
import { parse_expression_at } from '../acorn.js';
import { regex_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
import { find_matching_bracket } from '../utils/bracket.js';
/**
* @param {Parser} parser
* @param {string} [opening_token]
* @returns {Expression | undefined}
*/
export function get_loose_identifier(parser, opening_token) {
// Find the next } and treat it as the end of the expression
const end = find_matching_bracket(parser.template, parser.index, opening_token ?? '{');
if (end) {
const start = parser.index;
parser.index = end;
// We don't know what the expression is and signal this by returning an empty identifier
return {
type: 'Identifier',
start,
end,
name: ''
};
}
}
/**
* @param {Parser} parser
* @param {string} [opening_token]
* @param {boolean} [disallow_loose]
* @returns {Expression}
*/
export default function read_expression(parser, opening_token, disallow_loose) {
try {
const node = parse_expression_at(parser.template, parser.ts, parser.index);
let num_parens = 0;
if (node.leadingComments !== undefined && node.leadingComments.length > 0) {
parser.index = node.leadingComments.at(-1).end;
}
for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) {
if (parser.template[i] === '(') num_parens += 1;
}
let index = /** @type {number} */ (node.end);
if (node.trailingComments !== undefined && node.trailingComments.length > 0) {
index = node.trailingComments.at(-1).end;
}
while (num_parens > 0) {
const char = parser.template[index];
if (char === ')') {
num_parens -= 1;
} else if (!regex_whitespace.test(char)) {
e.expected_token(index, ')');
}
index += 1;
}
parser.index = index;
return /** @type {Expression} */ (node);
} catch (err) {
// If we are in an each loop we need the error to be thrown in cases like
// `as { y = z }` so we still throw and handle the error there
if (parser.loose && !disallow_loose) {
const expression = get_loose_identifier(parser, opening_token);
if (expression) {
return expression;
}
}
parser.acorn_error(err);
}
}

View File

@@ -0,0 +1,261 @@
/** @import { ObjectExpression } from 'estree' */
/** @import { AST } from '#compiler' */
import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js';
import * as e from '../../../errors.js';
/**
* @param {AST.SvelteOptionsRaw} node
* @returns {AST.Root['options']}
*/
export default function read_options(node) {
/** @type {AST.SvelteOptions} */
const component_options = {
start: node.start,
end: node.end,
// @ts-ignore
attributes: node.attributes
};
if (!node) {
return component_options;
}
for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute') {
e.svelte_options_invalid_attribute(attribute);
}
const { name } = attribute;
switch (name) {
case 'runes': {
component_options.runes = get_boolean_value(attribute);
break;
}
case 'tag': {
e.svelte_options_deprecated_tag(attribute);
break; // eslint doesn't know this is unnecessary
}
case 'customElement': {
/** @type {AST.SvelteOptions['customElement']} */
const ce = {};
const { value: v } = attribute;
const value = v === true || Array.isArray(v) ? v : [v];
if (value === true) {
e.svelte_options_invalid_customelement(attribute);
} else if (value[0].type === 'Text') {
const tag = get_static_value(attribute);
validate_tag(attribute, tag);
ce.tag = tag;
component_options.customElement = ce;
break;
} else if (value[0].expression.type !== 'ObjectExpression') {
// Before Svelte 4 it was necessary to explicitly set customElement to null or else you'd get a warning.
// This is no longer necessary, but for backwards compat just skip in this case now.
if (value[0].expression.type === 'Literal' && value[0].expression.value === null) {
break;
}
e.svelte_options_invalid_customelement(attribute);
}
/** @type {Array<[string, any]>} */
const properties = [];
for (const property of value[0].expression.properties) {
if (
property.type !== 'Property' ||
property.computed ||
property.key.type !== 'Identifier'
) {
e.svelte_options_invalid_customelement(attribute);
}
properties.push([property.key.name, property.value]);
}
const tag = properties.find(([name]) => name === 'tag');
if (tag) {
const tag_value = tag[1]?.value;
validate_tag(tag, tag_value);
ce.tag = tag_value;
}
const props = properties.find(([name]) => name === 'props')?.[1];
if (props) {
if (props.type !== 'ObjectExpression') {
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props = {};
for (const property of /** @type {ObjectExpression} */ (props).properties) {
if (
property.type !== 'Property' ||
property.computed ||
property.key.type !== 'Identifier' ||
property.value.type !== 'ObjectExpression'
) {
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props[property.key.name] = {};
for (const prop of property.value.properties) {
if (
prop.type !== 'Property' ||
prop.computed ||
prop.key.type !== 'Identifier' ||
prop.value.type !== 'Literal'
) {
e.svelte_options_invalid_customelement_props(attribute);
}
if (prop.key.name === 'type') {
if (
['String', 'Number', 'Boolean', 'Array', 'Object'].indexOf(
/** @type {string} */ (prop.value.value)
) === -1
) {
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props[property.key.name].type = /** @type {any} */ (prop.value.value);
} else if (prop.key.name === 'reflect') {
if (typeof prop.value.value !== 'boolean') {
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props[property.key.name].reflect = prop.value.value;
} else if (prop.key.name === 'attribute') {
if (typeof prop.value.value !== 'string') {
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props[property.key.name].attribute = prop.value.value;
} else {
e.svelte_options_invalid_customelement_props(attribute);
}
}
}
}
const shadow = properties.find(([name]) => name === 'shadow')?.[1];
if (shadow) {
const shadowdom = shadow?.value;
if (shadowdom !== 'open' && shadowdom !== 'none') {
e.svelte_options_invalid_customelement_shadow(shadow);
}
ce.shadow = shadowdom;
}
const extend = properties.find(([name]) => name === 'extend')?.[1];
if (extend) {
ce.extend = extend;
}
component_options.customElement = ce;
break;
}
case 'namespace': {
const value = get_static_value(attribute);
if (value === NAMESPACE_SVG) {
component_options.namespace = 'svg';
} else if (value === NAMESPACE_MATHML) {
component_options.namespace = 'mathml';
} else if (value === 'html' || value === 'mathml' || value === 'svg') {
component_options.namespace = value;
} else {
e.svelte_options_invalid_attribute_value(attribute, `"html", "mathml" or "svg"`);
}
break;
}
case 'css': {
const value = get_static_value(attribute);
if (value === 'injected') {
component_options.css = value;
} else {
e.svelte_options_invalid_attribute_value(attribute, `"injected"`);
}
break;
}
case 'immutable': {
component_options.immutable = get_boolean_value(attribute);
break;
}
case 'preserveWhitespace': {
component_options.preserveWhitespace = get_boolean_value(attribute);
break;
}
case 'accessors': {
component_options.accessors = get_boolean_value(attribute);
break;
}
default:
e.svelte_options_unknown_attribute(attribute, name);
}
}
return component_options;
}
/**
* @param {any} attribute
*/
function get_static_value(attribute) {
const { value } = attribute;
if (value === true) return true;
const chunk = Array.isArray(value) ? value[0] : value;
if (!chunk) return true;
if (value.length > 1) {
return null;
}
if (chunk.type === 'Text') return chunk.data;
if (chunk.expression.type !== 'Literal') {
return null;
}
return chunk.expression.value;
}
/**
* @param {any} attribute
*/
function get_boolean_value(attribute) {
const value = get_static_value(attribute);
if (typeof value !== 'boolean') {
e.svelte_options_invalid_attribute_value(attribute, 'true or false');
}
return value;
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
const tag_name_char =
'[a-z0-9_.\xB7\xC0-\xD6\xD8-\xF6\xF8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}-]';
const regex_valid_tag_name = new RegExp(`^[a-z]${tag_name_char}*-${tag_name_char}*$`, 'u');
const reserved_tag_names = [
'annotation-xml',
'color-profile',
'font-face',
'font-face-src',
'font-face-uri',
'font-face-format',
'font-face-name',
'missing-glyph'
];
/**
* @param {any} attribute
* @param {string | null} tag
* @returns {asserts tag is string}
*/
function validate_tag(attribute, tag) {
if (typeof tag !== 'string') {
e.svelte_options_invalid_tagname(attribute);
}
if (tag) {
if (!regex_valid_tag_name.test(tag)) {
e.svelte_options_invalid_tagname(attribute);
} else if (reserved_tag_names.includes(tag)) {
e.svelte_options_reserved_tagname(attribute);
}
}
}

View File

@@ -0,0 +1,90 @@
/** @import { Program } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import * as acorn from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { is_text_attribute } from '../../../utils/ast.js';
const regex_closing_script_tag = /<\/script\s*>/;
const regex_starts_with_closing_script_tag = /^<\/script\s*>/;
const RESERVED_ATTRIBUTES = ['server', 'client', 'worker', 'test', 'default'];
const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive>} attributes
* @returns {AST.Script}
*/
export function read_script(parser, start, attributes) {
const script_start = parser.index;
const data = parser.read_until(regex_closing_script_tag);
if (parser.index >= parser.template.length) {
e.element_unclosed(parser.template.length, 'script');
}
const source =
parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data;
parser.read(regex_starts_with_closing_script_tag);
/** @type {Program} */
let ast;
try {
ast = acorn.parse(source, parser.ts, true);
} catch (err) {
parser.acorn_error(err);
}
// TODO is this necessary?
ast.start = script_start;
/** @type {'default' | 'module'} */
let context = 'default';
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (RESERVED_ATTRIBUTES.includes(attribute.name)) {
e.script_reserved_attribute(attribute, attribute.name);
}
if (!ALLOWED_ATTRIBUTES.includes(attribute.name)) {
w.script_unknown_attribute(attribute);
}
if (attribute.name === 'module') {
if (attribute.value !== true) {
// Deliberately a generic code to future-proof for potential other attributes
e.script_invalid_attribute_value(attribute, attribute.name);
}
context = 'module';
}
if (attribute.name === 'context') {
if (attribute.value === true || !is_text_attribute(attribute)) {
e.script_invalid_context(attribute);
}
const value = attribute.value[0].data;
if (value !== 'module') {
e.script_invalid_context(attribute);
}
context = 'module';
}
}
return {
type: 'Script',
start,
end: parser.index,
context,
content: ast,
// @ts-ignore
attributes
};
}

View File

@@ -0,0 +1,625 @@
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import * as e from '../../../errors.js';
const REGEX_MATCHER = /^[~^$*|]?=/;
const REGEX_CLOSING_BRACKET = /[\s\]]/;
const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/; // only `i` and `s` are valid today, but make it future-proof
const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/;
const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/;
const REGEX_NTH_OF =
/^(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of\s+)/;
const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/;
const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/;
const REGEX_COMMENT_CLOSE = /\*\//;
const REGEX_HTML_COMMENT_CLOSE = /-->/;
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive>} attributes
* @returns {AST.CSS.StyleSheet}
*/
export default function read_style(parser, start, attributes) {
const content_start = parser.index;
const children = read_body(parser, '</style');
const content_end = parser.index;
parser.read(/^<\/style\s*>/);
return {
type: 'StyleSheet',
start,
end: parser.index,
attributes,
children,
content: {
start: content_start,
end: content_end,
styles: parser.template.slice(content_start, content_end),
comment: null
}
};
}
/**
* @param {Parser} parser
* @param {string} close
* @returns {any[]}
*/
function read_body(parser, close) {
/** @type {Array<AST.CSS.Rule | AST.CSS.Atrule>} */
const children = [];
while (parser.index < parser.template.length) {
allow_comment_or_whitespace(parser);
if (parser.match(close)) {
return children;
}
if (parser.match('@')) {
children.push(read_at_rule(parser));
} else {
children.push(read_rule(parser));
}
}
e.expected_token(parser.template.length, close);
}
/**
* @param {Parser} parser
* @returns {AST.CSS.Atrule}
*/
function read_at_rule(parser) {
const start = parser.index;
parser.eat('@', true);
const name = read_identifier(parser);
const prelude = read_value(parser);
/** @type {AST.CSS.Block | null} */
let block = null;
if (parser.match('{')) {
// e.g. `@media (...) {...}`
block = read_block(parser);
} else {
// e.g. `@import '...'`
parser.eat(';', true);
}
return {
type: 'Atrule',
start,
end: parser.index,
name,
prelude,
block
};
}
/**
* @param {Parser} parser
* @returns {AST.CSS.Rule}
*/
function read_rule(parser) {
const start = parser.index;
return {
type: 'Rule',
prelude: read_selector_list(parser),
block: read_block(parser),
start,
end: parser.index,
metadata: {
parent_rule: null,
has_local_selectors: false,
is_global_block: false
}
};
}
/**
* @param {Parser} parser
* @param {boolean} [inside_pseudo_class]
* @returns {AST.CSS.SelectorList}
*/
function read_selector_list(parser, inside_pseudo_class = false) {
/** @type {AST.CSS.ComplexSelector[]} */
const children = [];
allow_comment_or_whitespace(parser);
const start = parser.index;
while (parser.index < parser.template.length) {
children.push(read_selector(parser, inside_pseudo_class));
const end = parser.index;
allow_comment_or_whitespace(parser);
if (inside_pseudo_class ? parser.match(')') : parser.match('{')) {
return {
type: 'SelectorList',
start,
end,
children
};
} else {
parser.eat(',', true);
allow_comment_or_whitespace(parser);
}
}
e.unexpected_eof(parser.template.length);
}
/**
* @param {Parser} parser
* @param {boolean} [inside_pseudo_class]
* @returns {AST.CSS.ComplexSelector}
*/
function read_selector(parser, inside_pseudo_class = false) {
const list_start = parser.index;
/** @type {AST.CSS.RelativeSelector[]} */
const children = [];
/**
* @param {AST.CSS.Combinator | null} combinator
* @param {number} start
* @returns {AST.CSS.RelativeSelector}
*/
function create_selector(combinator, start) {
return {
type: 'RelativeSelector',
combinator,
selectors: [],
start,
end: -1,
metadata: {
is_global: false,
is_global_like: false,
scoped: false
}
};
}
/** @type {AST.CSS.RelativeSelector} */
let relative_selector = create_selector(null, parser.index);
while (parser.index < parser.template.length) {
let start = parser.index;
if (parser.eat('&')) {
relative_selector.selectors.push({
type: 'NestingSelector',
name: '&',
start,
end: parser.index
});
} else if (parser.eat('*')) {
let name = '*';
if (parser.eat('|')) {
// * is the namespace (which we ignore)
name = read_identifier(parser);
}
relative_selector.selectors.push({
type: 'TypeSelector',
name,
start,
end: parser.index
});
} else if (parser.eat('#')) {
relative_selector.selectors.push({
type: 'IdSelector',
name: read_identifier(parser),
start,
end: parser.index
});
} else if (parser.eat('.')) {
relative_selector.selectors.push({
type: 'ClassSelector',
name: read_identifier(parser),
start,
end: parser.index
});
} else if (parser.eat('::')) {
relative_selector.selectors.push({
type: 'PseudoElementSelector',
name: read_identifier(parser),
start,
end: parser.index
});
// We read the inner selectors of a pseudo element to ensure it parses correctly,
// but we don't do anything with the result.
if (parser.eat('(')) {
read_selector_list(parser, true);
parser.eat(')', true);
}
} else if (parser.eat(':')) {
const name = read_identifier(parser);
/** @type {null | AST.CSS.SelectorList} */
let args = null;
if (parser.eat('(')) {
args = read_selector_list(parser, true);
parser.eat(')', true);
}
relative_selector.selectors.push({
type: 'PseudoClassSelector',
name,
args,
start,
end: parser.index
});
} else if (parser.eat('[')) {
parser.allow_whitespace();
const name = read_identifier(parser);
parser.allow_whitespace();
/** @type {string | null} */
let value = null;
const matcher = parser.read(REGEX_MATCHER);
if (matcher) {
parser.allow_whitespace();
value = read_attribute_value(parser);
}
parser.allow_whitespace();
const flags = parser.read(REGEX_ATTRIBUTE_FLAGS);
parser.allow_whitespace();
parser.eat(']', true);
relative_selector.selectors.push({
type: 'AttributeSelector',
start,
end: parser.index,
name,
matcher,
value,
flags
});
} else if (inside_pseudo_class && parser.match_regex(REGEX_NTH_OF)) {
// nth of matcher must come before combinator matcher to prevent collision else the '+' in '+2n-1' would be parsed as a combinator
relative_selector.selectors.push({
type: 'Nth',
value: /**@type {string} */ (parser.read(REGEX_NTH_OF)),
start,
end: parser.index
});
} else if (parser.match_regex(REGEX_PERCENTAGE)) {
relative_selector.selectors.push({
type: 'Percentage',
value: /** @type {string} */ (parser.read(REGEX_PERCENTAGE)),
start,
end: parser.index
});
} else if (!parser.match_regex(REGEX_COMBINATOR)) {
let name = read_identifier(parser);
if (parser.eat('|')) {
// we ignore the namespace when trying to find matching element classes
name = read_identifier(parser);
}
relative_selector.selectors.push({
type: 'TypeSelector',
name,
start,
end: parser.index
});
}
const index = parser.index;
allow_comment_or_whitespace(parser);
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
// rewind, so we know whether to continue building the selector list
parser.index = index;
relative_selector.end = index;
children.push(relative_selector);
return {
type: 'ComplexSelector',
start: list_start,
end: index,
children,
metadata: {
rule: null,
used: false
}
};
}
parser.index = index;
const combinator = read_combinator(parser);
if (combinator) {
if (relative_selector.selectors.length > 0) {
relative_selector.end = index;
children.push(relative_selector);
}
// ...and start a new one
relative_selector = create_selector(combinator, combinator.start);
parser.allow_whitespace();
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
e.css_selector_invalid(parser.index);
}
}
}
e.unexpected_eof(parser.template.length);
}
/**
* @param {Parser} parser
* @returns {AST.CSS.Combinator | null}
*/
function read_combinator(parser) {
const start = parser.index;
parser.allow_whitespace();
const index = parser.index;
const name = parser.read(REGEX_COMBINATOR);
if (name) {
const end = parser.index;
parser.allow_whitespace();
return {
type: 'Combinator',
name,
start: index,
end
};
}
if (parser.index !== start) {
return {
type: 'Combinator',
name: ' ',
start,
end: parser.index
};
}
return null;
}
/**
* @param {Parser} parser
* @returns {AST.CSS.Block}
*/
function read_block(parser) {
const start = parser.index;
parser.eat('{', true);
/** @type {Array<AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule>} */
const children = [];
while (parser.index < parser.template.length) {
allow_comment_or_whitespace(parser);
if (parser.match('}')) {
break;
} else {
children.push(read_block_item(parser));
}
}
parser.eat('}', true);
return {
type: 'Block',
start,
end: parser.index,
children
};
}
/**
* Reads a declaration, rule or at-rule
*
* @param {Parser} parser
* @returns {AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule}
*/
function read_block_item(parser) {
if (parser.match('@')) {
return read_at_rule(parser);
}
// read ahead to understand whether we're dealing with a declaration or a nested rule.
// this involves some duplicated work, but avoids a try-catch that would disguise errors
const start = parser.index;
read_value(parser);
const char = parser.template[parser.index];
parser.index = start;
return char === '{' ? read_rule(parser) : read_declaration(parser);
}
/**
* @param {Parser} parser
* @returns {AST.CSS.Declaration}
*/
function read_declaration(parser) {
const start = parser.index;
const property = parser.read_until(REGEX_WHITESPACE_OR_COLON);
parser.allow_whitespace();
parser.eat(':');
let index = parser.index;
parser.allow_whitespace();
const value = read_value(parser);
if (!value && !property.startsWith('--')) {
e.css_empty_declaration({ start, end: index });
}
const end = parser.index;
if (!parser.match('}')) {
parser.eat(';', true);
}
return {
type: 'Declaration',
start,
end,
property,
value
};
}
/**
* @param {Parser} parser
* @returns {string}
*/
function read_value(parser) {
let value = '';
let escaped = false;
let in_url = false;
/** @type {null | '"' | "'"} */
let quote_mark = null;
while (parser.index < parser.template.length) {
const char = parser.template[parser.index];
if (escaped) {
value += '\\' + char;
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (char === quote_mark) {
quote_mark = null;
} else if (char === ')') {
in_url = false;
} else if (quote_mark === null && (char === '"' || char === "'")) {
quote_mark = char;
} else if (char === '(' && value.slice(-3) === 'url') {
in_url = true;
} else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) {
return value.trim();
}
value += char;
parser.index++;
}
e.unexpected_eof(parser.template.length);
}
/**
* Read a property that may or may not be quoted, e.g.
* `foo` or `'foo bar'` or `"foo bar"`
* @param {Parser} parser
*/
function read_attribute_value(parser) {
let value = '';
let escaped = false;
const quote_mark = parser.eat('"') ? '"' : parser.eat("'") ? "'" : null;
while (parser.index < parser.template.length) {
const char = parser.template[parser.index];
if (escaped) {
value += '\\' + char;
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (quote_mark ? char === quote_mark : REGEX_CLOSING_BRACKET.test(char)) {
if (quote_mark) {
parser.eat(quote_mark, true);
}
return value.trim();
} else {
value += char;
}
parser.index++;
}
e.unexpected_eof(parser.template.length);
}
/**
* https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
* @param {Parser} parser
*/
function read_identifier(parser) {
const start = parser.index;
let identifier = '';
if (parser.match('--') || parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) {
e.css_expected_identifier(start);
}
let escaped = false;
while (parser.index < parser.template.length) {
const char = parser.template[parser.index];
if (escaped) {
identifier += '\\' + char;
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (
/** @type {number} */ (char.codePointAt(0)) >= 160 ||
REGEX_VALID_IDENTIFIER_CHAR.test(char)
) {
identifier += char;
} else {
break;
}
parser.index++;
}
if (identifier === '') {
e.css_expected_identifier(start);
}
return identifier;
}
/** @param {Parser} parser */
function allow_comment_or_whitespace(parser) {
parser.allow_whitespace();
while (parser.match('/*') || parser.match('<!--')) {
if (parser.eat('/*')) {
parser.read_until(REGEX_COMMENT_CLOSE);
parser.eat('*/', true);
}
if (parser.eat('<!--')) {
parser.read_until(REGEX_HTML_COMMENT_CLOSE);
parser.eat('-->', true);
}
parser.allow_whitespace();
}
}

View File

@@ -0,0 +1,147 @@
/** @import { Context, Visitors } from 'zimmerframe' */
/** @import { FunctionExpression, FunctionDeclaration } from 'estree' */
import { walk } from 'zimmerframe';
import * as b from '../../utils/builders.js';
import * as e from '../../errors.js';
/**
* @param {FunctionExpression | FunctionDeclaration} node
* @param {Context<any, any>} context
*/
function remove_this_param(node, context) {
if (node.params[0]?.type === 'Identifier' && node.params[0].name === 'this') {
node.params.shift();
}
return context.next();
}
/** @type {Visitors<any, null>} */
const visitors = {
_(node, context) {
const n = context.next() ?? node;
// TODO there may come a time when we decide to preserve type annotations.
// until that day comes, we just delete them so they don't confuse esrap
delete n.typeAnnotation;
delete n.typeParameters;
delete n.returnType;
delete n.accessibility;
},
Decorator(node) {
e.typescript_invalid_feature(node, 'decorators (related TSC proposal is not stage 4 yet)');
},
ImportDeclaration(node) {
if (node.importKind === 'type') return b.empty;
if (node.specifiers?.length > 0) {
const specifiers = node.specifiers.filter((/** @type {any} */ s) => s.importKind !== 'type');
if (specifiers.length === 0) return b.empty;
return { ...node, specifiers };
}
return node;
},
ExportNamedDeclaration(node, context) {
if (node.exportKind === 'type') return b.empty;
if (node.declaration) {
const result = context.next();
if (result?.declaration?.type === 'EmptyStatement') {
return b.empty;
}
return result;
}
if (node.specifiers) {
const specifiers = node.specifiers.filter((/** @type {any} */ s) => s.exportKind !== 'type');
if (specifiers.length === 0) return b.empty;
return { ...node, specifiers };
}
return node;
},
ExportDefaultDeclaration(node) {
if (node.exportKind === 'type') return b.empty;
return node;
},
ExportAllDeclaration(node) {
if (node.exportKind === 'type') return b.empty;
return node;
},
PropertyDefinition(node, { next }) {
if (node.accessor) {
e.typescript_invalid_feature(
node,
'accessor fields (related TSC proposal is not stage 4 yet)'
);
}
return next();
},
TSAsExpression(node, context) {
return context.visit(node.expression);
},
TSSatisfiesExpression(node, context) {
return context.visit(node.expression);
},
TSNonNullExpression(node, context) {
return context.visit(node.expression);
},
TSInterfaceDeclaration() {
return b.empty;
},
TSTypeAliasDeclaration() {
return b.empty;
},
TSEnumDeclaration(node) {
e.typescript_invalid_feature(node, 'enums');
},
TSParameterProperty(node, context) {
if ((node.readonly || node.accessibility) && context.path.at(-2)?.kind === 'constructor') {
e.typescript_invalid_feature(node, 'accessibility modifiers on constructor parameters');
}
return context.visit(node.parameter);
},
TSInstantiationExpression(node, context) {
return context.visit(node.expression);
},
FunctionExpression: remove_this_param,
FunctionDeclaration: remove_this_param,
TSDeclareFunction() {
return b.empty;
},
ClassDeclaration(node, context) {
if (node.declare) {
return b.empty;
}
delete node.implements;
return context.next();
},
VariableDeclaration(node, context) {
if (node.declare) {
return b.empty;
}
return context.next();
},
TSModuleDeclaration(node, context) {
if (!node.body) return b.empty;
// namespaces can contain non-type nodes
const cleaned = /** @type {any[]} */ (node.body.body).map((entry) => context.visit(entry));
if (cleaned.some((entry) => entry !== b.empty)) {
e.typescript_invalid_feature(node, 'namespaces with non-type nodes');
}
return b.empty;
}
};
/**
* @template T
* @param {T} ast
* @returns {T}
*/
export function remove_typescript_nodes(ast) {
return walk(ast, null, visitors);
}

View File

@@ -0,0 +1,823 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { is_void } from '../../../../utils.js';
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 } from '../utils/html.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { create_fragment } from '../utils/create.js';
import { create_attribute, create_expression_metadata, is_element_node } from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';
import { regex_whitespace } from '../../patterns.js';
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
const regex_closing_comment = /-->/;
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
const regex_token_ending_character = /[\s=/>"']/;
const regex_starts_with_quote_characters = /^["']/;
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
const regex_valid_element_name =
/^(?:![a-zA-Z]+|[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])$/;
export const regex_valid_component_name =
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers adjusted for our needs
// (must start with uppercase letter if no dots, can contain dots)
/^(?:\p{Lu}[$\u200c\u200d\p{ID_Continue}.]*|\p{ID_Start}[$\u200c\u200d\p{ID_Continue}]*(?:\.[$\u200c\u200d\p{ID_Continue}]+)+)$/u;
/** @type {Map<string, AST.ElementLike['type']>} */
const root_only_meta_tags = new Map([
['svelte:head', 'SvelteHead'],
['svelte:options', 'SvelteOptions'],
['svelte:window', 'SvelteWindow'],
['svelte:document', 'SvelteDocument'],
['svelte:body', 'SvelteBody']
]);
/** @type {Map<string, AST.ElementLike['type']>} */
const meta_tags = new Map([
...root_only_meta_tags,
['svelte:element', 'SvelteElement'],
['svelte:component', 'SvelteComponent'],
['svelte:self', 'SvelteSelf'],
['svelte:fragment', 'SvelteFragment'],
['svelte:boundary', 'SvelteBoundary']
]);
/** @param {Parser} parser */
export default function element(parser) {
const start = parser.index++;
let parent = parser.current();
if (parser.eat('!--')) {
const data = parser.read_until(regex_closing_comment);
parser.eat('-->', true);
parser.append({
type: 'Comment',
start,
end: parser.index,
data
});
return;
}
const is_closing_tag = parser.eat('/');
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
if (is_closing_tag) {
parser.allow_whitespace();
parser.eat('>', true);
if (is_void(name)) {
e.void_element_invalid_content(start);
}
// close any elements that don't have their own closing tags, e.g. <div><p></div>
while (/** @type {AST.RegularElement} */ (parent).name !== name) {
if (parser.loose) {
// If the previous element did interpret the next opening tag as an attribute, backtrack
if (is_element_node(parent)) {
const last = parent.attributes.at(-1);
if (last?.type === 'Attribute' && last.name === `<${name}`) {
parser.index = last.start;
parent.attributes.pop();
break;
}
}
}
if (parent.type !== 'RegularElement' && !parser.loose) {
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
} else {
e.element_invalid_closing_tag(start, name);
}
}
parent.end = start;
parser.pop();
parent = parser.current();
}
parent.end = parser.index;
parser.pop();
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
parser.last_auto_closed_tag = undefined;
}
return;
}
if (name.startsWith('svelte:') && !meta_tags.has(name)) {
const bounds = { start: start + 1, end: start + 1 + name.length };
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
}
if (!regex_valid_element_name.test(name) && !regex_valid_component_name.test(name)) {
// <div. -> in the middle of typing -> allow in loose mode
if (!parser.loose || !name.endsWith('.')) {
const bounds = { start: start + 1, end: start + 1 + name.length };
e.tag_invalid_name(bounds);
}
}
if (root_only_meta_tags.has(name)) {
if (name in parser.meta_tags) {
e.svelte_meta_duplicate(start, name);
}
if (parent.type !== 'Root') {
e.svelte_meta_invalid_placement(start, name);
}
parser.meta_tags[name] = true;
}
const type = meta_tags.has(name)
? meta_tags.get(name)
: regex_valid_component_name.test(name) || (parser.loose && name.endsWith('.'))
? 'Component'
: name === 'title' && parent_is_head(parser.stack)
? 'TitleElement'
: // TODO Svelte 6/7: once slots are removed in favor of snippets, always keep slot as a regular element
name === 'slot' && !parent_is_shadowroot_template(parser.stack)
? 'SlotElement'
: 'RegularElement';
/** @type {AST.ElementLike} */
const element =
type === 'RegularElement'
? {
type,
start,
end: -1,
name,
attributes: [],
fragment: create_fragment(true),
metadata: {
svg: false,
mathml: false,
scoped: false,
has_spread: false,
path: []
}
}
: /** @type {AST.ElementLike} */ ({
type,
start,
end: -1,
name,
attributes: [],
fragment: create_fragment(true),
metadata: {
// unpopulated at first, differs between types
}
});
parser.allow_whitespace();
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
parent.end = start;
parser.pop();
parser.last_auto_closed_tag = {
tag: parent.name,
reason: name,
depth: parser.stack.length
};
}
/** @type {string[]} */
const unique_names = [];
const current = parser.current();
const is_top_level_script_or_style =
(name === 'script' || name === 'style') && current.type === 'Root';
const read = is_top_level_script_or_style ? read_static_attribute : read_attribute;
let attribute;
while ((attribute = read(parser))) {
// animate and transition can only be specified once per element so no need
// to check here, use can be used multiple times, same for the on directive
// finally let already has error handling in case of duplicate variable names
if (
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'StyleDirective' ||
attribute.type === 'ClassDirective'
) {
// `bind:attribute` and `attribute` are just the same but `class:attribute`,
// `style:attribute` and `attribute` are different and should be allowed together
// so we concatenate the type while normalizing the type for BindDirective
const type = attribute.type === 'BindDirective' ? 'Attribute' : attribute.type;
if (unique_names.includes(type + attribute.name)) {
e.attribute_duplicate(attribute);
// <svelte:element bind:this this=..> is allowed
} else if (attribute.name !== 'this') {
unique_names.push(type + attribute.name);
}
}
element.attributes.push(attribute);
parser.allow_whitespace();
}
if (element.type === 'SvelteComponent') {
const index = element.attributes.findIndex(
/** @param {any} attr */
(attr) => attr.type === 'Attribute' && attr.name === 'this'
);
if (index === -1) {
e.svelte_component_missing_this(start);
}
const definition = /** @type {AST.Attribute} */ (element.attributes.splice(index, 1)[0]);
if (!is_expression_attribute(definition)) {
e.svelte_component_invalid_this(definition.start);
}
element.expression = get_attribute_expression(definition);
}
if (element.type === 'SvelteElement') {
const index = element.attributes.findIndex(
/** @param {any} attr */
(attr) => attr.type === 'Attribute' && attr.name === 'this'
);
if (index === -1) {
e.svelte_element_missing_this(start);
}
const definition = /** @type {AST.Attribute} */ (element.attributes.splice(index, 1)[0]);
if (definition.value === true) {
e.svelte_element_missing_this(definition);
}
if (!is_expression_attribute(definition)) {
w.svelte_element_invalid_this(definition);
// note that this is wrong, in the case of e.g. `this="h{n}"` — it will result in `<h>`.
// it would be much better to just error here, but we are preserving the existing buggy
// Svelte 4 behaviour out of an overabundance of caution regarding breaking changes.
// TODO in 6.0, error
const chunk = /** @type {Array<AST.ExpressionTag | AST.Text>} */ (definition.value)[0];
element.tag =
chunk.type === 'Text'
? {
type: 'Literal',
value: chunk.data,
raw: `'${chunk.raw}'`,
start: chunk.start,
end: chunk.end
}
: chunk.expression;
} else {
element.tag = get_attribute_expression(definition);
}
}
if (is_top_level_script_or_style) {
parser.eat('>', true);
/** @type {AST.Comment | null} */
let prev_comment = null;
for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
const node = current.fragment.nodes[i];
if (i === current.fragment.nodes.length - 1 && node.end !== start) {
break;
}
if (node.type === 'Comment') {
prev_comment = node;
break;
} else if (node.type !== 'Text' || node.data.trim()) {
break;
}
}
if (name === 'script') {
const content = read_script(parser, start, element.attributes);
if (prev_comment) {
// We take advantage of the fact that the root will never have leadingComments set,
// and set the previous comment to it so that the warning mechanism can later
// inspect the root and see if there was a html comment before it silencing specific warnings.
content.content.leadingComments = [{ type: 'Line', value: prev_comment.data }];
}
if (content.context === 'module') {
if (current.module) e.script_duplicate(start);
current.module = content;
} else {
if (current.instance) e.script_duplicate(start);
current.instance = content;
}
} else {
const content = read_style(parser, start, element.attributes);
content.content.comment = prev_comment;
if (current.css) e.style_duplicate(start);
current.css = content;
}
return;
}
parser.append(element);
const self_closing = parser.eat('/') || is_void(name);
const closed = parser.eat('>', true, false);
// Loose parsing mode
if (!closed) {
// We may have eaten an opening `<` of the next element and treated it as an attribute...
const last = element.attributes.at(-1);
if (last?.type === 'Attribute' && last.name === '<') {
parser.index = last.start;
element.attributes.pop();
} else {
// ... or we may have eaten part of a following block ...
const prev_1 = parser.template[parser.index - 1];
const prev_2 = parser.template[parser.index - 2];
const current = parser.template[parser.index];
if (prev_2 === '{' && prev_1 === '/') {
parser.index -= 2;
} else if (prev_1 === '{' && (current === '#' || current === '@' || current === ':')) {
parser.index -= 1;
} else {
// ... or we're followed by whitespace, for example near the end of the template,
// which we want to take in so that language tools has more room to work with
parser.allow_whitespace();
if (parser.index === parser.template.length) {
while (
parser.index < parser.template_untrimmed.length &&
regex_whitespace.test(parser.template_untrimmed[parser.index])
) {
parser.index++;
}
}
}
}
}
if (self_closing || !closed) {
// don't push self-closing elements onto the stack
element.end = parser.index;
} else if (name === 'textarea') {
// special case
element.fragment.nodes = read_sequence(
parser,
() => regex_closing_textarea_tag.test(parser.template.slice(parser.index)),
'inside <textarea>'
);
parser.read(regex_closing_textarea_tag);
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;
/** @type {AST.Text} */
const node = {
start,
end,
type: 'Text',
data,
raw: data
};
element.fragment.nodes.push(node);
parser.eat(`</${name}>`, true);
element.end = parser.index;
} else {
parser.stack.push(element);
parser.fragments.push(element.fragment);
}
}
/** @param {AST.TemplateNode[]} stack */
function parent_is_head(stack) {
let i = stack.length;
while (i--) {
const { type } = stack[i];
if (type === 'SvelteHead') return true;
if (type === 'RegularElement' || type === 'Component') return false;
}
return false;
}
/** @param {AST.TemplateNode[]} stack */
function parent_is_shadowroot_template(stack) {
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root
let i = stack.length;
while (i--) {
if (
stack[i].type === 'RegularElement' &&
/** @type {AST.RegularElement} */ (stack[i]).attributes.some(
(a) => a.type === 'Attribute' && a.name === 'shadowrootmode'
)
) {
return true;
}
}
return false;
}
/**
* @param {Parser} parser
* @returns {AST.Attribute | null}
*/
function read_static_attribute(parser) {
const start = parser.index;
const name = parser.read_until(regex_token_ending_character);
if (!name) return null;
/** @type {true | Array<AST.Text | AST.ExpressionTag>} */
let value = true;
if (parser.eat('=')) {
parser.allow_whitespace();
let raw = parser.match_regex(regex_attribute_value);
if (!raw) {
e.expected_attribute_value(parser.index);
}
parser.index += raw.length;
const quoted = raw[0] === '"' || raw[0] === "'";
if (quoted) {
raw = raw.slice(1, -1);
}
value = [
{
start: parser.index - raw.length - (quoted ? 1 : 0),
end: quoted ? parser.index - 1 : parser.index,
type: 'Text',
raw: raw,
data: decode_character_references(raw, true)
}
];
}
if (parser.match_regex(regex_starts_with_quote_characters)) {
e.expected_token(parser.index, '=');
}
return create_attribute(name, start, parser.index, value);
}
/**
* @param {Parser} parser
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null}
*/
function read_attribute(parser) {
const start = parser.index;
if (parser.eat('{')) {
parser.allow_whitespace();
if (parser.eat('...')) {
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.SpreadAttribute} */
const spread = {
type: 'SpreadAttribute',
start,
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
}
};
return spread;
} else {
const value_start = parser.index;
let name = parser.read_identifier();
if (name === null) {
if (
parser.loose &&
(parser.match('#') || parser.match('/') || parser.match('@') || parser.match(':'))
) {
// We're likely in an unclosed opening tag and did read part of a block.
// Return null to not crash the parser so it can continue with closing the tag.
return null;
} else if (parser.loose && parser.match('}')) {
// Likely in the middle of typing, just created the shorthand
name = '';
} else {
e.attribute_empty_shorthand(start);
}
}
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.ExpressionTag} */
const expression = {
type: 'ExpressionTag',
start: value_start,
end: value_start + name.length,
expression: {
start: value_start,
end: value_start + name.length,
type: 'Identifier',
name
},
metadata: {
expression: create_expression_metadata()
}
};
return create_attribute(name, start, parser.index, expression);
}
}
const name = parser.read_until(regex_token_ending_character);
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));
/** @type {true | AST.ExpressionTag | Array<AST.Text | AST.ExpressionTag>} */
let value = true;
if (parser.eat('=')) {
parser.allow_whitespace();
if (parser.template[parser.index] === '/' && parser.template[parser.index + 1] === '>') {
const char_start = parser.index;
parser.index++; // consume '/'
value = [
{
start: char_start,
end: char_start + 1,
type: 'Text',
raw: '/',
data: '/'
}
];
end = parser.index;
} else {
value = read_attribute_value(parser);
end = parser.index;
}
} else if (parser.match_regex(regex_starts_with_quote_characters)) {
e.expected_token(parser.index, '=');
}
if (type) {
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
if (directive_name === '') {
e.directive_missing_name({ start, end: start + colon_index + 1 }, name);
}
if (type === 'StyleDirective') {
return {
start,
end,
type,
name: directive_name,
modifiers: /** @type {Array<'important'>} */ (modifiers),
value,
metadata: {
expression: create_expression_metadata()
}
};
}
const first_value = value === true ? undefined : Array.isArray(value) ? value[0] : value;
/** @type {Expression | null} */
let expression = null;
if (first_value) {
const attribute_contains_text =
/** @type {any[]} */ (value).length > 1 || first_value.type === 'Text';
if (attribute_contains_text) {
e.directive_invalid_value(/** @type {number} */ (first_value.start));
} else {
// TODO throw a parser error in a future version here if this `[ExpressionTag]` instead of `ExpressionTag`,
// which means stringified value, which isn't allowed for some directives?
expression = first_value.expression;
}
}
/** @type {AST.Directive} */
const directive = {
start,
end,
type,
name: directive_name,
expression,
metadata: {
expression: create_expression_metadata()
}
};
// @ts-expect-error we do this separately from the declaration to avoid upsetting typescript
directive.modifiers = modifiers;
if (directive.type === 'TransitionDirective') {
const direction = name.slice(0, colon_index);
directive.intro = direction === 'in' || direction === 'transition';
directive.outro = direction === 'out' || direction === 'transition';
}
// Directive name is expression, e.g. <p class:isRed />
if (
(directive.type === 'BindDirective' || directive.type === 'ClassDirective') &&
!directive.expression
) {
directive.expression = /** @type {any} */ ({
start: start + colon_index + 1,
end,
type: 'Identifier',
name: directive.name
});
}
return directive;
}
return create_attribute(name, start, end, value);
}
/**
* @param {string} name
* @returns {any}
*/
function get_directive_type(name) {
if (name === 'use') return 'UseDirective';
if (name === 'animate') return 'AnimateDirective';
if (name === 'bind') return 'BindDirective';
if (name === 'class') return 'ClassDirective';
if (name === 'style') return 'StyleDirective';
if (name === 'on') return 'OnDirective';
if (name === 'let') return 'LetDirective';
if (name === 'in' || name === 'out' || name === 'transition') return 'TransitionDirective';
return false;
}
/**
* @param {Parser} parser
* @return {AST.ExpressionTag | Array<AST.ExpressionTag | AST.Text>}
*/
function read_attribute_value(parser) {
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
if (quote_mark && parser.eat(quote_mark)) {
return [
{
start: parser.index - 1,
end: parser.index - 1,
type: 'Text',
raw: '',
data: ''
}
];
}
/** @type {Array<AST.ExpressionTag | AST.Text>} */
let value;
try {
value = read_sequence(
parser,
() => {
// handle common case of quote marks existing outside of regex for performance reasons
if (quote_mark) return parser.match(quote_mark);
return !!parser.match_regex(regex_invalid_unquoted_attribute_value);
},
'in attribute value'
);
} catch (/** @type {any} */ error) {
if (error.code === 'js_parse_error') {
// if the attribute value didn't close + self-closing tag
// eg: `<Component test={{a:1} />`
// acorn may throw a `Unterminated regular expression` because of `/>`
const pos = error.position?.[0];
if (pos !== undefined && parser.template.slice(pos - 1, pos + 1) === '/>') {
parser.index = pos;
e.expected_token(pos, quote_mark || '}');
}
}
throw error;
}
if (value.length === 0 && !quote_mark) {
e.expected_attribute_value(parser.index);
}
if (quote_mark) parser.index += 1;
if (quote_mark || value.length > 1 || value[0].type === 'Text') {
return value;
} else {
return value[0];
}
}
/**
* @param {Parser} parser
* @param {() => boolean} done
* @param {string} location
* @returns {any[]}
*/
function read_sequence(parser, done, location) {
/** @type {AST.Text} */
let current_chunk = {
start: parser.index,
end: -1,
type: 'Text',
raw: '',
data: ''
};
/** @type {Array<AST.Text | AST.ExpressionTag>} */
const chunks = [];
/** @param {number} end */
function flush(end) {
if (current_chunk.raw) {
current_chunk.data = decode_character_references(current_chunk.raw, true);
current_chunk.end = end;
chunks.push(current_chunk);
}
}
while (parser.index < parser.template.length) {
const index = parser.index;
if (done()) {
flush(parser.index);
return chunks;
} else if (parser.eat('{')) {
if (parser.match('#')) {
const index = parser.index - 1;
parser.eat('#');
const name = parser.read_until(/[^a-z]/);
e.block_invalid_placement(index, name, location);
} else if (parser.match('@')) {
const index = parser.index - 1;
parser.eat('@');
const name = parser.read_until(/[^a-z]/);
e.tag_invalid_placement(index, name, location);
}
flush(parser.index - 1);
parser.allow_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.ExpressionTag} */
const chunk = {
type: 'ExpressionTag',
start: index,
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
}
};
chunks.push(chunk);
current_chunk = {
start: parser.index,
end: -1,
type: 'Text',
raw: '',
data: ''
};
} else {
current_chunk.raw += parser.template[parser.index++];
}
}
if (parser.loose) {
return chunks;
} else {
e.unexpected_eof(parser.template.length);
}
}

View File

@@ -0,0 +1,17 @@
/** @import { Parser } from '../index.js' */
import element from './element.js';
import tag from './tag.js';
import text from './text.js';
/** @param {Parser} parser */
export default function fragment(parser) {
if (parser.match('<')) {
return element;
}
if (parser.match('{')) {
return tag;
}
return text;
}

View File

@@ -0,0 +1,715 @@
/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
import { create_expression_metadata } from '../../nodes.js';
import { parse_expression_at } from '../acorn.js';
import read_pattern from '../read/context.js';
import read_expression, { get_loose_identifier } from '../read/expression.js';
import { create_fragment } from '../utils/create.js';
const regex_whitespace_with_closing_curly_brace = /^\s*}/;
/** @param {Parser} parser */
export default function tag(parser) {
const start = parser.index;
parser.index += 1;
parser.allow_whitespace();
if (parser.eat('#')) return open(parser);
if (parser.eat(':')) return next(parser);
if (parser.eat('@')) return special(parser);
if (parser.match('/')) {
if (!parser.match('/*') && !parser.match('//')) {
parser.eat('/');
return close(parser);
}
}
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'ExpressionTag',
start,
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
}
});
}
/** @param {Parser} parser */
function open(parser) {
let start = parser.index - 2;
while (parser.template[start] !== '{') start -= 1;
if (parser.eat('if')) {
parser.require_whitespace();
/** @type {AST.IfBlock} */
const block = parser.append({
type: 'IfBlock',
elseif: false,
start,
end: -1,
test: read_expression(parser),
consequent: create_fragment(),
alternate: null
});
parser.allow_whitespace();
parser.eat('}', true);
parser.stack.push(block);
parser.fragments.push(block.consequent);
return;
}
if (parser.eat('each')) {
parser.require_whitespace();
const template = parser.template;
let end = parser.template.length;
/** @type {Expression | undefined} */
let expression;
// we have to do this loop because `{#each x as { y = z }}` fails to parse —
// the `as { y = z }` is treated as an Expression but it's actually a Pattern.
// the 'fix' is to backtrack and hide everything from the `as` onwards, until
// we get a valid expression
while (!expression) {
try {
expression = read_expression(parser, undefined, true);
} catch (err) {
end = /** @type {any} */ (err).position[0] - 2;
while (end > start && parser.template.slice(end, end + 2) !== 'as') {
end -= 1;
}
if (end <= start) {
if (parser.loose) {
expression = get_loose_identifier(parser);
if (expression) {
break;
}
}
throw err;
}
// @ts-expect-error parser.template is meant to be readonly, this is a special case
parser.template = template.slice(0, end);
}
}
// @ts-expect-error
parser.template = template;
parser.allow_whitespace();
// {#each} blocks must declare a context {#each list as item}
if (!parser.match('as')) {
// this could be a TypeScript assertion that was erroneously eaten.
if (expression.type === 'SequenceExpression') {
expression = expression.expressions[0];
}
let assertion = null;
let end = expression.end;
expression = walk(expression, null, {
// @ts-expect-error
TSAsExpression(node, context) {
if (node.end === /** @type {Expression} */ (expression).end) {
assertion = node;
end = node.expression.end;
return node.expression;
}
context.next();
}
});
expression.end = end;
if (assertion) {
// we can't reset `parser.index` to `expression.expression.end` because
// it will ignore any parentheses — we need to jump through this hoop
let end = /** @type {any} */ (/** @type {any} */ (assertion).typeAnnotation).start - 2;
while (parser.template.slice(end, end + 2) !== 'as') end -= 1;
parser.index = end;
}
}
/** @type {Pattern | null} */
let context = null;
let index;
let key;
if (parser.eat('as')) {
parser.require_whitespace();
context = read_pattern(parser);
} else {
// {#each Array.from({ length: 10 }), i} is read as a sequence expression,
// which is set back above - we now gotta reset the index as a consequence
// to properly read the , i part
parser.index = /** @type {number} */ (expression.end);
}
parser.allow_whitespace();
if (parser.eat(',')) {
parser.allow_whitespace();
index = parser.read_identifier();
if (!index) {
e.expected_identifier(parser.index);
}
parser.allow_whitespace();
}
if (parser.eat('(')) {
parser.allow_whitespace();
key = read_expression(parser, '(');
parser.allow_whitespace();
parser.eat(')', true);
parser.allow_whitespace();
}
const matches = parser.eat('}', true, false);
if (!matches) {
// Parser may have read the `as` as part of the expression (e.g. in `{#each foo. as x}`)
if (parser.template.slice(parser.index - 4, parser.index) === ' as ') {
const prev_index = parser.index;
context = read_pattern(parser);
parser.eat('}', true);
expression = {
type: 'Identifier',
name: '',
start: expression.start,
end: prev_index - 4
};
} else {
parser.eat('}', true); // rerun to produce the parser error
}
}
/** @type {AST.EachBlock} */
const block = parser.append({
type: 'EachBlock',
start,
end: -1,
expression,
body: create_fragment(),
context,
index,
key,
metadata: /** @type {any} */ (null) // filled in later
});
parser.stack.push(block);
parser.fragments.push(block.body);
return;
}
if (parser.eat('await')) {
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
/** @type {AST.AwaitBlock} */
const block = parser.append({
type: 'AwaitBlock',
start,
end: -1,
expression,
value: null,
error: null,
pending: null,
then: null,
catch: null
});
if (parser.eat('then')) {
if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
parser.allow_whitespace();
} else {
parser.require_whitespace();
block.value = read_pattern(parser);
parser.allow_whitespace();
}
block.then = create_fragment();
parser.fragments.push(block.then);
} else if (parser.eat('catch')) {
if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
parser.allow_whitespace();
} else {
parser.require_whitespace();
block.error = read_pattern(parser);
parser.allow_whitespace();
}
block.catch = create_fragment();
parser.fragments.push(block.catch);
} else {
block.pending = create_fragment();
parser.fragments.push(block.pending);
}
const matches = parser.eat('}', true, false);
// Parser may have read the `then/catch` as part of the expression (e.g. in `{#await foo. then x}`)
if (!matches) {
if (parser.template.slice(parser.index - 6, parser.index) === ' then ') {
const prev_index = parser.index;
block.value = read_pattern(parser);
parser.eat('}', true);
block.expression = {
type: 'Identifier',
name: '',
start: expression.start,
end: prev_index - 6
};
block.then = block.pending;
block.pending = null;
} else if (parser.template.slice(parser.index - 7, parser.index) === ' catch ') {
const prev_index = parser.index;
block.error = read_pattern(parser);
parser.eat('}', true);
block.expression = {
type: 'Identifier',
name: '',
start: expression.start,
end: prev_index - 7
};
block.catch = block.pending;
block.pending = null;
} else {
parser.eat('}', true); // rerun to produce the parser error
}
}
parser.stack.push(block);
return;
}
if (parser.eat('key')) {
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.KeyBlock} */
const block = parser.append({
type: 'KeyBlock',
start,
end: -1,
expression,
fragment: create_fragment()
});
parser.stack.push(block);
parser.fragments.push(block.fragment);
return;
}
if (parser.eat('snippet')) {
parser.require_whitespace();
const name_start = parser.index;
let name = parser.read_identifier();
const name_end = parser.index;
if (name === null) {
if (parser.loose) {
name = '';
} else {
e.expected_identifier(parser.index);
}
}
parser.allow_whitespace();
const params_start = parser.index;
const matched = parser.eat('(', true, false);
if (matched) {
let parentheses = 1;
while (parser.index < parser.template.length && (!parser.match(')') || parentheses !== 1)) {
if (parser.match('(')) parentheses++;
if (parser.match(')')) parentheses--;
parser.index += 1;
}
parser.eat(')', true);
}
const prelude = parser.template.slice(0, params_start).replace(/\S/g, ' ');
const params = parser.template.slice(params_start, parser.index);
let function_expression = matched
? /** @type {ArrowFunctionExpression} */ (
parse_expression_at(prelude + `${params} => {}`, parser.ts, params_start)
)
: { params: [] };
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.SnippetBlock} */
const block = parser.append({
type: 'SnippetBlock',
start,
end: -1,
expression: {
type: 'Identifier',
start: name_start,
end: name_end,
name
},
parameters: function_expression.params,
body: create_fragment(),
metadata: {
can_hoist: false,
sites: new Set()
}
});
parser.stack.push(block);
parser.fragments.push(block.body);
return;
}
e.expected_block_type(parser.index);
}
/** @param {Parser} parser */
function next(parser) {
const start = parser.index - 1;
const block = parser.current(); // TODO type should not be TemplateNode, that's much too broad
if (block.type === 'IfBlock') {
if (!parser.eat('else')) e.expected_token(start, '{:else} or {:else if}');
if (parser.eat('if')) e.block_invalid_elseif(start);
parser.allow_whitespace();
parser.fragments.pop();
block.alternate = create_fragment();
parser.fragments.push(block.alternate);
// :else if
if (parser.eat('if')) {
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
let elseif_start = start - 1;
while (parser.template[elseif_start] !== '{') elseif_start -= 1;
/** @type {AST.IfBlock} */
const child = parser.append({
start: elseif_start,
end: -1,
type: 'IfBlock',
elseif: true,
test: expression,
consequent: create_fragment(),
alternate: null
});
parser.stack.push(child);
parser.fragments.pop();
parser.fragments.push(child.consequent);
} else {
// :else
parser.allow_whitespace();
parser.eat('}', true);
}
return;
}
if (block.type === 'EachBlock') {
if (!parser.eat('else')) e.expected_token(start, '{:else}');
parser.allow_whitespace();
parser.eat('}', true);
block.fallback = create_fragment();
parser.fragments.pop();
parser.fragments.push(block.fallback);
return;
}
if (block.type === 'AwaitBlock') {
if (parser.eat('then')) {
if (block.then) {
e.block_duplicate_clause(start, '{:then}');
}
if (!parser.eat('}')) {
parser.require_whitespace();
block.value = read_pattern(parser);
parser.allow_whitespace();
parser.eat('}', true);
}
block.then = create_fragment();
parser.fragments.pop();
parser.fragments.push(block.then);
return;
}
if (parser.eat('catch')) {
if (block.catch) {
e.block_duplicate_clause(start, '{:catch}');
}
if (!parser.eat('}')) {
parser.require_whitespace();
block.error = read_pattern(parser);
parser.allow_whitespace();
parser.eat('}', true);
}
block.catch = create_fragment();
parser.fragments.pop();
parser.fragments.push(block.catch);
return;
}
e.expected_token(start, '{:then ...} or {:catch ...}');
}
e.block_invalid_continuation_placement(start);
}
/** @param {Parser} parser */
function close(parser) {
const start = parser.index - 1;
let block = parser.current();
/** Only relevant/reached for loose parsing mode */
let matched;
switch (block.type) {
case 'IfBlock':
matched = parser.eat('if', true, false);
if (!matched) {
block.end = start - 1;
parser.pop();
close(parser);
return;
}
parser.allow_whitespace();
parser.eat('}', true);
while (block.elseif) {
block.end = parser.index;
parser.stack.pop();
block = /** @type {AST.IfBlock} */ (parser.current());
}
block.end = parser.index;
parser.pop();
return;
case 'EachBlock':
matched = parser.eat('each', true, false);
break;
case 'KeyBlock':
matched = parser.eat('key', true, false);
break;
case 'AwaitBlock':
matched = parser.eat('await', true, false);
break;
case 'SnippetBlock':
matched = parser.eat('snippet', true, false);
break;
case 'RegularElement':
if (parser.loose) {
matched = false;
} else {
// TODO handle implicitly closed elements
e.block_unexpected_close(start);
}
break;
default:
e.block_unexpected_close(start);
}
if (!matched) {
block.end = start - 1;
parser.pop();
close(parser);
return;
}
parser.allow_whitespace();
parser.eat('}', true);
block.end = parser.index;
parser.pop();
}
/** @param {Parser} parser */
function special(parser) {
let start = parser.index;
while (parser.template[start] !== '{') start -= 1;
if (parser.eat('html')) {
// {@html content} tag
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'HtmlTag',
start,
end: parser.index,
expression
});
return;
}
if (parser.eat('debug')) {
/** @type {Identifier[]} */
let identifiers;
// Implies {@debug} which indicates "debug all"
if (parser.read(regex_whitespace_with_closing_curly_brace)) {
identifiers = [];
} else {
const expression = read_expression(parser);
identifiers =
expression.type === 'SequenceExpression'
? /** @type {Identifier[]} */ (expression.expressions)
: [/** @type {Identifier} */ (expression)];
identifiers.forEach(
/** @param {any} node */ (node) => {
if (node.type !== 'Identifier') {
e.debug_tag_invalid_arguments(/** @type {number} */ (node.start));
}
}
);
parser.allow_whitespace();
parser.eat('}', true);
}
parser.append({
type: 'DebugTag',
start,
end: parser.index,
identifiers
});
return;
}
if (parser.eat('const')) {
parser.require_whitespace();
const id = read_pattern(parser);
parser.allow_whitespace();
parser.eat('=', true);
parser.allow_whitespace();
const expression_start = parser.index;
const init = read_expression(parser);
if (
init.type === 'SequenceExpression' &&
!parser.template.substring(expression_start, init.start).includes('(')
) {
// const a = (b, c) is allowed but a = b, c = d is not;
e.const_tag_invalid_expression(init);
}
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'ConstTag',
start,
end: parser.index,
declaration: {
type: 'VariableDeclaration',
kind: 'const',
declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }],
start: start + 2, // start at const, not at @const
end: parser.index - 1
}
});
}
if (parser.eat('render')) {
// {@render foo(...)}
parser.require_whitespace();
const expression = read_expression(parser);
if (
expression.type !== 'CallExpression' &&
(expression.type !== 'ChainExpression' || expression.expression.type !== 'CallExpression')
) {
e.render_tag_invalid_expression(expression);
}
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'RenderTag',
start,
end: parser.index,
expression: /** @type {AST.RenderTag['expression']} */ (expression),
metadata: {
dynamic: false,
arguments: [],
path: [],
snippets: new Set()
}
});
}
}

View File

@@ -0,0 +1,23 @@
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { decode_character_references } from '../utils/html.js';
/** @param {Parser} parser */
export default function text(parser) {
const start = parser.index;
let data = '';
while (parser.index < parser.template.length && !parser.match('<') && !parser.match('{')) {
data += parser.template[parser.index++];
}
/** @type {AST.Text} */
parser.append({
type: 'Text',
start,
end: parser.index,
raw: data,
data: decode_character_references(data, false)
});
}

View File

@@ -0,0 +1,164 @@
const SQUARE_BRACKET_OPEN = '[';
const SQUARE_BRACKET_CLOSE = ']';
const CURLY_BRACKET_OPEN = '{';
const CURLY_BRACKET_CLOSE = '}';
const PARENTHESES_OPEN = '(';
const PARENTHESES_CLOSE = ')';
/** @param {string} char */
export function is_bracket_open(char) {
return char === SQUARE_BRACKET_OPEN || char === CURLY_BRACKET_OPEN;
}
/** @param {string} char */
export function is_bracket_close(char) {
return char === SQUARE_BRACKET_CLOSE || char === CURLY_BRACKET_CLOSE;
}
/** @param {string} open */
export function get_bracket_close(open) {
if (open === SQUARE_BRACKET_OPEN) {
return SQUARE_BRACKET_CLOSE;
}
if (open === CURLY_BRACKET_OPEN) {
return CURLY_BRACKET_CLOSE;
}
if (open === PARENTHESES_OPEN) {
return PARENTHESES_CLOSE;
}
}
/**
* @param {number} num
* @returns {number} Infinity if {@link num} is negative, else {@link num}.
*/
function infinity_if_negative(num) {
if (num < 0) {
return Infinity;
}
return num;
}
/**
* @param {string} string The string to search.
* @param {number} search_start_index The index to start searching at.
* @param {"'" | '"' | '`'} string_start_char The character that started this string.
* @returns {number} The index of the end of this string expression, or `Infinity` if not found.
*/
function find_string_end(string, search_start_index, string_start_char) {
let string_to_search;
if (string_start_char === '`') {
string_to_search = string;
} else {
// we could slice at the search start index, but this way the index remains valid
string_to_search = string.slice(
0,
infinity_if_negative(string.indexOf('\n', search_start_index))
);
}
return find_unescaped_char(string_to_search, search_start_index, string_start_char);
}
/**
* @param {string} string The string to search.
* @param {number} search_start_index The index to start searching at.
* @returns {number} The index of the end of this regex expression, or `Infinity` if not found.
*/
function find_regex_end(string, search_start_index) {
return find_unescaped_char(string, search_start_index, '/');
}
/**
*
* @param {string} string The string to search.
* @param {number} search_start_index The index to begin the search at.
* @param {string} char The character to search for.
* @returns {number} The index of the first unescaped instance of {@link char}, or `Infinity` if not found.
*/
function find_unescaped_char(string, search_start_index, char) {
let i = search_start_index;
while (true) {
const found_index = string.indexOf(char, i);
if (found_index === -1) {
return Infinity;
}
if (count_leading_backslashes(string, found_index - 1) % 2 === 0) {
return found_index;
}
i = found_index + 1;
}
}
/**
* Count consecutive leading backslashes before {@link search_start_index}.
*
* @example
* ```js
* count_leading_backslashes('\\\\\\foo', 2); // 3 (the backslashes have to be escaped in the string literal, there are three in reality)
* ```
*
* @param {string} string The string to search.
* @param {number} search_start_index The index to begin the search at.
*/
function count_leading_backslashes(string, search_start_index) {
let i = search_start_index;
let count = 0;
while (string[i] === '\\') {
count++;
i--;
}
return count;
}
/**
* Finds the corresponding closing bracket, ignoring brackets found inside comments, strings, or regex expressions.
* @param {string} template The string to search.
* @param {number} index The index to begin the search at.
* @param {string} open The opening bracket (ex: `'{'` will search for `'}'`).
* @returns {number | undefined} The index of the closing bracket, or undefined if not found.
*/
export function find_matching_bracket(template, index, open) {
const close = get_bracket_close(open);
let brackets = 1;
let i = index;
while (brackets > 0 && i < template.length) {
const char = template[i];
switch (char) {
case "'":
case '"':
case '`':
i = find_string_end(template, i + 1, char) + 1;
continue;
case '/': {
const next_char = template[i + 1];
if (!next_char) continue;
if (next_char === '/') {
i = infinity_if_negative(template.indexOf('\n', i + 1)) + '\n'.length;
continue;
}
if (next_char === '*') {
i = infinity_if_negative(template.indexOf('*/', i + 1)) + '*/'.length;
continue;
}
i = find_regex_end(template, i + 1) + '/'.length;
continue;
}
default: {
const char = template[i];
if (char === open) {
brackets++;
} else if (char === close) {
brackets--;
}
if (brackets === 0) {
return i;
}
i++;
}
}
}
return undefined;
}

View File

@@ -0,0 +1,16 @@
/** @import { AST } from '#compiler' */
/**
* @param {any} transparent
* @returns {AST.Fragment}
*/
export function create_fragment(transparent = false) {
return {
type: 'Fragment',
nodes: [],
metadata: {
transparent,
dynamic: false
}
};
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,280 @@
/**
* @param {string} name
* @param {string[]} names
* @returns {string | null}
*/
export default function fuzzymatch(name, names) {
if (names.length === 0) return null;
const set = new FuzzySet(names);
const matches = set.get(name);
return matches && 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
/**
* @param {string} str1
* @param {string} str2
*/
function _distance(str1, str2) {
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);
return 1 - distance / Math.max(str1.length, str2.length);
}
// helper functions
/**
* @param {string} str1
* @param {string} str2
*/
function levenshtein(str1, str2) {
/** @type {number[]} */
const current = [];
let prev = 0;
let value = 0;
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 /** @type {number} */ (current.pop());
}
const non_word_regex = /[^\w, ]+/;
/**
* @param {string} value
* @param {any} gram_size
*/
function iterate_grams(value, 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;
}
/**
* @param {string} value
* @param {any} gram_size
*/
function gram_counter(value, gram_size = 2) {
// return an object where key=gram, value=number of occurrences
/** @type {Record<string, number>} */
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;
}
/**
* @param {MatchTuple} a
* @param {MatchTuple} b
*/
function sort_descending(a, b) {
return b[0] - a[0];
}
class FuzzySet {
/** @type {Record<string, string>} */
exact_set = {};
/** @type {Record<string, [number, number][]>} */
match_dict = {};
/** @type {Record<string, number[]>} */
items = {};
/** @param {string[]} arr */
constructor(arr) {
// initialisation
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]);
}
}
/** @param {string} value */
add(value) {
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);
}
}
/**
* @param {string} value
* @param {number} gram_size
*/
_add(value, gram_size) {
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);
// @ts-ignore no idea what this code is doing
items[index] = [vector_normal, normalized_value];
this.items[gram_size] = items;
this.exact_set[normalized_value] = value;
}
/** @param {string} value */
get(value) {
const normalized_value = value.toLowerCase();
const result = this.exact_set[normalized_value];
if (result) {
return /** @type {MatchTuple[]} */ ([[1, result]]);
}
// 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) {
const results = this.__get(value, gram_size);
if (results.length > 0) return results;
}
return null;
}
/**
* @param {string} value
* @param {number} gram_size
* @returns {MatchTuple[]}
*/
__get(value, gram_size) {
const normalized_value = value.toLowerCase();
/** @type {Record<string, number>} */
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);
/** @type {MatchTuple[]} */
let results = [];
let match_score;
// build a results list of [score, str]
for (const match_index in matches) {
match_score = matches[match_index];
// @ts-ignore no idea what this code is doing
results.push([match_score / (vector_normal * items[match_index][0]), items[match_index][1]]);
}
results.sort(sort_descending);
/** @type {MatchTuple[]} */
let new_results = [];
const end_index = Math.min(50, results.length);
// truncate somewhat arbitrarily to 50
for (let i = 0; i < end_index; ++i) {
// @ts-ignore no idea what this code is doing
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]) {
// @ts-ignore no idea what this code is doing
new_results.push([results[i][0], this.exact_set[results[i][1]]]);
}
}
return new_results;
}
}
/** @typedef {[score: number, match: string]} MatchTuple */

View File

@@ -0,0 +1,120 @@
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
];
/**
* @param {string} entity_name
* @param {boolean} is_attribute_value
*/
function reg_exp_entity(entity_name, is_attribute_value) {
// https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
// doesn't decode the html entity which not ends with ; and next character is =, number or alphabet in attribute value.
if (is_attribute_value && !entity_name.endsWith(';')) {
return `${entity_name}\\b(?!=)`;
}
return entity_name;
}
/** @param {boolean} is_attribute_value */
function get_entity_pattern(is_attribute_value) {
const reg_exp_num = '#(?:x[a-fA-F\\d]+|\\d+)(?:;)?';
const reg_exp_entities = Object.keys(entities).map(
/** @param {any} entity_name */ (entity_name) => reg_exp_entity(entity_name, is_attribute_value)
);
const entity_pattern = new RegExp(`&(${reg_exp_num}|${reg_exp_entities.join('|')})`, 'g');
return entity_pattern;
}
const entity_pattern_content = get_entity_pattern(false);
const entity_pattern_attr_value = get_entity_pattern(true);
/**
* @param {string} html
* @param {boolean} is_attribute_value
*/
export function decode_character_references(html, is_attribute_value) {
const entity_pattern = is_attribute_value ? entity_pattern_attr_value : entity_pattern_content;
return html.replace(
entity_pattern,
/**
* @param {any} match
* @param {keyof typeof entities} entity
*/ (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
/** @param {number} code */
function validate_code(code) {
// 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;
}