diff --git a/.changeset/tall-kids-juggle.md b/.changeset/tall-kids-juggle.md new file mode 100644 index 0000000000..48a54b3436 --- /dev/null +++ b/.changeset/tall-kids-juggle.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: speed up parser a little bit diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index fa7b292640..8edd403487 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -10,25 +10,7 @@ import read_options from './read/options.js'; import { is_reserved } from '../../../utils.js'; import { disallow_children } from '../2-analyze/visitors/shared/special-element.js'; import * as state from '../../state.js'; - -/** @param {number} cc */ -function is_whitespace(cc) { - // fast path for common whitespace - if (cc === 32 || (cc <= 13 && cc >= 9)) return true; - // rare whitespace — \u00a0, \u1680, \u2000-\u200a, \u2028, \u2029, \u202f, \u205f, \u3000, \ufeff - if (cc < 160) return false; - return ( - cc === 160 || - cc === 5760 || - (cc >= 8192 && cc <= 8202) || - cc === 8232 || - cc === 8233 || - cc === 8239 || - cc === 8287 || - cc === 12288 || - cc === 65279 - ); -} +import { is_whitespace } from './utils/whitespace.js'; const regex_lang_attribute = /|]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g; @@ -278,8 +260,27 @@ export class Parser { }; } + /** @param {string} delimiter */ + read_until(delimiter) { + if (this.index >= this.template.length) { + if (this.loose) return ''; + e.unexpected_eof(this.template.length); + } + + const start = this.index; + const index = this.template.indexOf(delimiter, start); + + if (index !== -1) { + this.index = index; + return this.template.slice(start, this.index); + } + + this.index = this.template.length; + return this.template.slice(start); + } + /** @param {RegExp} pattern */ - read_until(pattern) { + read_until_regex(pattern) { if (this.index >= this.template.length) { if (this.loose) return ''; e.unexpected_eof(this.template.length); @@ -302,6 +303,7 @@ export class Parser { e.expected_whitespace(this.index); } + this.index++; this.allow_whitespace(); } diff --git a/packages/svelte/src/compiler/phases/1-parse/read/script.js b/packages/svelte/src/compiler/phases/1-parse/read/script.js index 4472ce61c3..48fb540f9c 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/script.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/script.js @@ -22,7 +22,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module']; */ export function read_script(parser, start, attributes) { const script_start = parser.index; - const data = parser.read_until(regex_closing_script_tag); + const data = parser.read_until_regex(regex_closing_script_tag); if (parser.index >= parser.template.length) { e.element_unclosed(parser.template.length, 'script'); } diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index df036d0d33..574c353adb 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -16,8 +16,6 @@ const REGEX_WHITESPACE_OR_COLON = /[\s:]/; const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/y; const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/; const REGEX_UNICODE_SEQUENCE = /\\[0-9a-fA-F]{1,6}(\r\n|\s)?/y; -const REGEX_COMMENT_CLOSE = /\*\//; -const REGEX_HTML_COMMENT_CLOSE = /-->/; /** * @param {Parser} parser @@ -478,7 +476,7 @@ function read_block_item(parser) { function read_declaration(parser) { const start = parser.index; - const property = parser.read_until(REGEX_WHITESPACE_OR_COLON); + const property = parser.read_until_regex(REGEX_WHITESPACE_OR_COLON); parser.allow_whitespace(); parser.eat(':'); let index = parser.index; @@ -661,7 +659,7 @@ function allow_comment_or_whitespace(parser, capture_comments = true) { } if (parser.eat(''); parser.eat('-->', true); } @@ -676,7 +674,7 @@ function allow_comment_or_whitespace(parser, capture_comments = true) { function read_comment(parser) { const start = parser.index; parser.eat('/*', true); - const value = parser.read_until(REGEX_COMMENT_CLOSE); + const value = parser.read_until('*/'); parser.eat('*/', true); const end = parser.index; diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index a14dd167a5..220fe24b9d 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -15,13 +15,10 @@ import { get_attribute_expression, is_expression_attribute } from '../../../util import { closing_tag_omitted } from '../../../../html-tree-validation.js'; import { list } from '../../../utils/string.js'; import { locator } from '../../../state.js'; -import * as b from '#compiler/builders'; +import { is_whitespace } from '../utils/whitespace.js'; const regex_invalid_unquoted_attribute_value = /(\/>|[\s"'=<>`])/y; const regex_closing_textarea_tag = /<\/textarea(\s[^>]*)?>/iy; -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 = /["']/y; const regex_attribute_value = /(?:"([^"]*)"|'([^'])*'|([^>\s]+))/y; const regex_doctype_name = /^![a-zA-Z]+$/; @@ -67,7 +64,7 @@ export default function element(parser) { let parent = parser.current(); if (parser.eat('!--')) { - const data = parser.read_until(regex_closing_comment); + const data = parser.read_until('-->'); parser.eat('-->', true); parser.append({ @@ -81,7 +78,7 @@ export default function element(parser) { } if (parser.eat('/')) { - const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag); + const name = read_tag_name(parser); parser.allow_whitespace(); parser.eat('>', true); @@ -137,7 +134,7 @@ export default function element(parser) { return; } - const tag = read_tag(parser, regex_whitespace_or_slash_or_closing_tag); + const tag = read_tag(parser); if (tag.name.startsWith('svelte:') && !meta_tags.has(tag.name)) { const bounds = { start: start + 1, end: start + 1 + tag.name.length }; @@ -475,7 +472,7 @@ function parent_is_shadowroot_template(stack) { function read_static_attribute(parser) { const start = parser.index; - const tag = read_tag(parser, regex_token_ending_character); + const tag = read_tag(parser, true); if (!tag.name) return null; /** @type {true | Array} */ @@ -607,7 +604,7 @@ function read_attribute(parser) { } } - const tag = read_tag(parser, regex_token_ending_character); + const tag = read_tag(parser, true); if (!tag.name) return null; @@ -731,7 +728,7 @@ function read_comment(parser) { const start = parser.index; if (parser.eat('//')) { - const value = parser.read_until(/\n/); + const value = parser.read_until('\n'); const end = parser.index; return { @@ -747,7 +744,7 @@ function read_comment(parser) { } if (parser.eat('/*')) { - const value = parser.read_until(/\*\//); + const value = parser.read_until('*/'); parser.eat('*/'); const end = parser.index; @@ -847,25 +844,21 @@ function read_attribute_value(parser) { * @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} */ const chunks = []; + let chunk_start = parser.index; /** @param {number} end */ function flush(end) { - if (end > current_chunk.start) { - current_chunk.raw = parser.template.slice(current_chunk.start, end); - current_chunk.data = decode_character_references(current_chunk.raw, true); - current_chunk.end = end; - chunks.push(current_chunk); + if (end > chunk_start) { + const raw = parser.template.slice(chunk_start, end); + chunks.push({ + start: chunk_start, + end, + type: 'Text', + raw, + data: decode_character_references(raw, true) + }); } } @@ -879,12 +872,14 @@ function read_sequence(parser, done, location) { if (parser.match('#')) { const index = parser.index - 1; parser.eat('#'); - const name = parser.read_until(/[^a-z]/); + // const name = parser.read_until_regex(/[^a-z]/); + const name = read_lowercase_name(parser); 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]/); + // const name = parser.read_until_regex(/[^a-z]/); + const name = read_lowercase_name(parser); e.tag_invalid_placement(index, name, location); } @@ -907,14 +902,7 @@ function read_sequence(parser, done, location) { }; chunks.push(chunk); - - current_chunk = { - start: parser.index, - end: -1, - type: 'Text', - raw: '', - data: '' - }; + chunk_start = parser.index; } else { parser.index++; } @@ -929,12 +917,36 @@ function read_sequence(parser, done, location) { /** * @param {Parser} parser - * @param {RegExp} regex + * @param {boolean} [attribute] + */ +function read_tag_name(parser, attribute = false) { + const start = parser.index; + if (start >= parser.template.length && !parser.loose) e.unexpected_eof(parser.template.length); + + while (parser.index < parser.template.length) { + const cc = parser.template.charCodeAt(parser.index); + if ( + is_whitespace(cc) || + cc === 47 || // / + cc === 62 || // > + (attribute && (cc === 34 || cc === 39 || cc === 61)) // " ' = + ) { + break; + } + parser.index += 1; + } + + return parser.template.slice(start, parser.index); +} + +/** + * @param {Parser} parser + * @param {boolean} [attribute] * @returns {Identifier & { start: number, end: number, loc: SourceLocation }} */ -function read_tag(parser, regex) { +function read_tag(parser, attribute = false) { const start = parser.index; - const name = parser.read_until(regex); + const name = read_tag_name(parser, attribute); const end = parser.index; return { @@ -948,3 +960,15 @@ function read_tag(parser, regex) { } }; } + +/** @param {Parser} parser */ +function read_lowercase_name(parser) { + const start = parser.index; + while (parser.index < parser.template.length) { + const cc = parser.template.charCodeAt(parser.index); + // a-z + if (cc < 97 || cc > 122) break; + parser.index += 1; + } + return parser.template.slice(start, parser.index); +} diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/html.js b/packages/svelte/src/compiler/phases/1-parse/utils/html.js index ccb2005054..dd0b0224a6 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/html.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/html.js @@ -38,6 +38,8 @@ const entity_pattern_attr_value = get_entity_pattern(true); * @param {boolean} is_attribute_value */ export function decode_character_references(html, is_attribute_value) { + if (html.indexOf('&') === -1) return html; // fast path + const entity_pattern = is_attribute_value ? entity_pattern_attr_value : entity_pattern_content; return html.replace( entity_pattern, diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/whitespace.js b/packages/svelte/src/compiler/phases/1-parse/utils/whitespace.js new file mode 100644 index 0000000000..b9a7efc9a3 --- /dev/null +++ b/packages/svelte/src/compiler/phases/1-parse/utils/whitespace.js @@ -0,0 +1,18 @@ +/** @param {number} cc */ +export function is_whitespace(cc) { + // fast path for common whitespace + if (cc === 32 || (cc <= 13 && cc >= 9)) return true; + // rare whitespace — \u00a0, \u1680, \u2000-\u200a, \u2028, \u2029, \u202f, \u205f, \u3000, \ufeff + if (cc < 160) return false; + return ( + cc === 160 || + cc === 5760 || + (cc >= 8192 && cc <= 8202) || + cc === 8232 || + cc === 8233 || + cc === 8239 || + cc === 8287 || + cc === 12288 || + cc === 65279 + ); +} diff --git a/packages/svelte/src/compiler/state.js b/packages/svelte/src/compiler/state.js index 5ae001ec50..51a02a61e1 100644 --- a/packages/svelte/src/compiler/state.js +++ b/packages/svelte/src/compiler/state.js @@ -33,10 +33,14 @@ export let component_name = ''; export let source; /** - * The source code split into lines (set by `set_source`) - * @type {string[]} + * The source code split into lines, initialized when a diagnostic needs a code frame + * @type {string[] | undefined} */ -export let source_lines = []; +let source_lines; + +export function get_source_lines() { + return (source_lines ??= source.split('\n')); +} /** * True if compiling with `dev: true` @@ -52,7 +56,7 @@ export let locator; /** @param {string} value */ export function set_source(value) { source = value; - source_lines = source.split('\n'); + source_lines = undefined; const l = getLocator(source, { offsetLine: 1 }); @@ -141,7 +145,7 @@ export function reset(state) { runes = false; component_name = UNKNOWN_FILENAME; source = ''; - source_lines = []; + source_lines = undefined; filename = (state.filename ?? UNKNOWN_FILENAME).replace(/\\/g, '/'); warning_filter = state.warning ?? (() => true); warnings = []; diff --git a/packages/svelte/src/compiler/utils/compile_diagnostic.js b/packages/svelte/src/compiler/utils/compile_diagnostic.js index 95d028ee35..ab0732c958 100644 --- a/packages/svelte/src/compiler/utils/compile_diagnostic.js +++ b/packages/svelte/src/compiler/utils/compile_diagnostic.js @@ -15,7 +15,7 @@ function tabs_to_spaces(str) { * @param {number} column */ function get_code_frame(line, column) { - const lines = state.source_lines; + const lines = state.get_source_lines(); const frame_start = Math.max(0, line - 2); const frame_end = Math.min(line + 3, lines.length); const digits = String(frame_end + 1).length;