fix(highlight): vendor pure-Lezer Svelte parser (#3534)

* fix(highlight): vendor pure-Lezer Svelte parser

Replace @replit/codemirror-lang-svelte with a vendored, pure-Lezer Svelte parser so the daemon and desktop app stop pulling the interactive CodeMirror editor runtime.

@replit/codemirror-lang-svelte executes EditorView.inputHandler and HTML autocompletion at module load and depends on @codemirror/view, @codemirror/state, and @codemirror/lang-* as peers. Those peers were not declared by @getpaseo/highlight, so electron-builder omitted them from app.asar and the daemon crashed on startup with ERR_MODULE_NOT_FOUND: Cannot find package '@codemirror/lang-css'.

The vendored parser uses only @lezer/lr, @lezer/common, @lezer/highlight, @lezer/javascript, and @lezer/css, with @codemirror/language kept for the existing Language wrapper. Drops the now-unused @codemirror/autocomplete dependency and adds @lezer/lr as a direct dependency.

* fix(nix): update npm-deps hash for vendored Svelte parser
This commit is contained in:
Mohamed Boudra
2026-08-19 07:42:07 +02:00
committed by GitHub
parent 60706feb34
commit d10edbc5f8
9 changed files with 1004 additions and 111 deletions
+1 -1
View File
@@ -1 +1 @@
sha256-2TmrD5JqI4AQSbngbTOfOnR+Z6Gmk3bxC7Bif+FN7jM=
sha256-NyHZR53Y2lZP0eRe068qED1PhB6qPYnSOblFO9NRAKM=
+53 -106
View File
@@ -2554,6 +2554,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@@ -2573,54 +2574,6 @@
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-css": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.2",
"@lezer/css": "^1.1.7"
}
},
"node_modules/@codemirror/lang-html": {
"version": "6.4.12",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.12.tgz",
"integrity": "sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/css": "^1.1.0",
"@lezer/html": "^1.3.12"
}
},
"node_modules/@codemirror/lang-javascript": {
"version": "6.2.5",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/javascript": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.12.4",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
@@ -2644,18 +2597,6 @@
"@codemirror/language": "^6.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
"integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.42.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz",
@@ -11229,25 +11170,6 @@
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@replit/codemirror-lang-svelte": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@replit/codemirror-lang-svelte/-/codemirror-lang-svelte-6.0.0.tgz",
"integrity": "sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==",
"license": "MIT",
"peerDependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.1",
"@codemirror/lang-html": "^6.2.0",
"@codemirror/lang-javascript": "^6.1.1",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0",
"@lezer/javascript": "^1.2.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@replit/codemirror-vim": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.3.0.tgz",
@@ -11302,7 +11224,8 @@
"optional": true,
"os": [
"android"
]
],
"peer": true
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.59.0",
@@ -11315,7 +11238,8 @@
"optional": true,
"os": [
"android"
]
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.59.0",
@@ -11328,7 +11252,8 @@
"optional": true,
"os": [
"darwin"
]
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.59.0",
@@ -11341,7 +11266,8 @@
"optional": true,
"os": [
"darwin"
]
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.59.0",
@@ -11354,7 +11280,8 @@
"optional": true,
"os": [
"freebsd"
]
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.59.0",
@@ -11367,7 +11294,8 @@
"optional": true,
"os": [
"freebsd"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.59.0",
@@ -11380,7 +11308,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.59.0",
@@ -11393,7 +11322,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.59.0",
@@ -11406,7 +11336,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.59.0",
@@ -11419,7 +11350,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.59.0",
@@ -11432,7 +11364,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.59.0",
@@ -11445,7 +11378,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.59.0",
@@ -11458,7 +11392,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.59.0",
@@ -11471,7 +11406,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.59.0",
@@ -11484,7 +11420,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.59.0",
@@ -11497,7 +11434,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.59.0",
@@ -11510,7 +11448,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.59.0",
@@ -11523,7 +11462,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.59.0",
@@ -11536,7 +11476,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.59.0",
@@ -11549,7 +11490,8 @@
"optional": true,
"os": [
"openbsd"
]
],
"peer": true
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.59.0",
@@ -11562,7 +11504,8 @@
"optional": true,
"os": [
"openharmony"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.59.0",
@@ -11575,7 +11518,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.59.0",
@@ -11588,7 +11532,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.59.0",
@@ -11601,7 +11546,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.59.0",
@@ -11614,7 +11560,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
@@ -19208,6 +19155,7 @@
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -38694,7 +38642,6 @@
"name": "@getpaseo/highlight",
"version": "0.4.0",
"dependencies": {
"@codemirror/autocomplete": "6.20.3",
"@codemirror/language": "6.12.4",
"@codemirror/legacy-modes": "^6.5.3",
"@lezer/common": "^1.5.0",
@@ -38706,6 +38653,7 @@
"@lezer/java": "^1.1.3",
"@lezer/javascript": "^1.5.4",
"@lezer/json": "^1.0.3",
"@lezer/lr": "^1.4.8",
"@lezer/markdown": "^1.6.2",
"@lezer/php": "^1.0.5",
"@lezer/python": "^1.1.18",
@@ -38714,7 +38662,6 @@
"@lezer/yaml": "^1.0.4",
"@replit/codemirror-lang-csharp": "^6.2.0",
"@replit/codemirror-lang-nix": "^6.0.1",
"@replit/codemirror-lang-svelte": "^6.0.0",
"lezer-elixir": "^1.1.2"
},
"devDependencies": {
+1 -2
View File
@@ -27,7 +27,6 @@
"typecheck": "tsgo --noEmit"
},
"dependencies": {
"@codemirror/autocomplete": "6.20.3",
"@codemirror/language": "6.12.4",
"@codemirror/legacy-modes": "^6.5.3",
"@lezer/common": "^1.5.0",
@@ -39,6 +38,7 @@
"@lezer/java": "^1.1.3",
"@lezer/javascript": "^1.5.4",
"@lezer/json": "^1.0.3",
"@lezer/lr": "^1.4.8",
"@lezer/markdown": "^1.6.2",
"@lezer/php": "^1.0.5",
"@lezer/python": "^1.1.18",
@@ -47,7 +47,6 @@
"@lezer/yaml": "^1.0.4",
"@replit/codemirror-lang-csharp": "^6.2.0",
"@replit/codemirror-lang-nix": "^6.0.1",
"@replit/codemirror-lang-svelte": "^6.0.0",
"lezer-elixir": "^1.1.2"
},
"devDependencies": {
+3 -2
View File
@@ -16,9 +16,10 @@ import { parser as xmlParser } from "@lezer/xml";
import { parser as yamlParser } from "@lezer/yaml";
import { csharpLanguage } from "@replit/codemirror-lang-csharp";
import { nixLanguage } from "@replit/codemirror-lang-nix";
import { svelteLanguage } from "@replit/codemirror-lang-svelte";
import { parser as elixirParser } from "lezer-elixir";
import type { Parser } from "@lezer/common";
import { parser as svelteBaseParser } from "./svelte/parser.js";
import { configureNesting, defaultNesting } from "./svelte/nesting.js";
function language(parser: Parser): Language {
return new Language(defineLanguageFacet(), parser);
@@ -51,7 +52,7 @@ const languagesByExtension: Record<string, Language> = {
html: language(htmlParser),
htm: language(htmlParser),
// Svelte
svelte: svelteLanguage,
svelte: language(svelteBaseParser.configure({ wrap: configureNesting(defaultNesting) })),
// XML
xml: language(xmlParser),
// Java
@@ -0,0 +1,54 @@
import { styleTags, tags as t } from "@lezer/highlight";
export const svelteHighlighting = styleTags({
"Text RawText": t.content,
"StartTag StartCloseTag SelfClosingEndTag EndTag": t.angleBracket,
TagName: t.tagName,
"MismatchedCloseTag/TagName": [t.tagName, t.invalid],
AttributeName: t.attributeName,
UnquotedAttributeValue: t.attributeValue,
"DoubleQuote SingleQuote AttributeValueContent": t.attributeValue,
Is: t.definitionOperator,
"EntityReference CharacterReference": t.character,
Comment: t.blockComment,
ProcessingInst: t.processingInstruction,
DoctypeDecl: t.documentMeta,
"{ }": t.bracket,
"[ ]": t.squareBracket,
"( )": t.paren,
"| , :": t.punctuation,
"...": t.derefOperator,
ComponentName: t.className,
SvelteElementNamespace: t.namespace,
SvelteElementType: t.tagName,
StyleAttributeName: t.propertyName,
BlockType: t.controlKeyword,
BlockPrefix: t.typeOperator,
"UnknownBlock/BlockType": t.invalid,
UnknownBlockContent: t.invalid,
"if then catch": t.controlKeyword,
as: t.definitionOperator,
Variable: t.variableName,
Modifier: t.modifier,
DirectlyInterpolatedAttributeValue: t.attributeValue,
"DirectiveOn/DirectiveName": t.controlKeyword,
"DirectiveOn/DirectiveTarget": t.typeName,
"DirectiveUse/DirectiveName": t.controlKeyword,
"DirectiveUse/DirectiveTarget": t.function(t.variableName),
"DirectiveBind/DirectiveName": t.controlKeyword,
"DirectiveBind/DirectiveTarget": t.variableName,
"DirectiveLet/DirectiveName": t.definitionKeyword,
"DirectiveLet/DirectiveTarget": t.definition(t.variableName),
"DirectiveTransition/DirectiveName": t.operatorKeyword,
"DirectiveTransition/DirectiveTarget": t.function(t.variableName),
"DirectiveIn/DirectiveName": t.operatorKeyword,
"DirectiveIn/DirectiveTarget": t.function(t.variableName),
"DirectiveOut/DirectiveName": t.operatorKeyword,
"DirectiveOut/DirectiveTarget": t.function(t.variableName),
"DirectiveAnimate/DirectiveName": t.operatorKeyword,
"DirectiveAnimate/DirectiveTarget": t.function(t.variableName),
"DirectiveClass/DirectiveName": t.attributeName,
"DirectiveClass/DirectiveTarget": t.variableName,
"DirectiveStyle/DirectiveName": t.attributeName,
"DirectiveStyle/DirectiveTarget": t.propertyName,
});
+106
View File
@@ -0,0 +1,106 @@
import {
parseMixed,
type Input,
type NestedParse,
type Parser,
type SyntaxNodeRef,
} from "@lezer/common";
import { parser as cssParser } from "@lezer/css";
import { parser as jsParser } from "@lezer/javascript";
import { LongExpression, ScriptText, ShortExpression, StyleText, TextareaText } from "./terms.js";
export interface NestedLang {
tag: "script" | "style" | "textarea";
attrs?: (attrs: Record<string, string>) => boolean;
parser: Parser;
}
const expressionParser = jsParser.configure({ top: "SingleExpression" });
export const defaultNesting: NestedLang[] = [
{
tag: "script",
attrs: (attrs) => attrs.type === "text/typescript" || attrs.lang === "ts",
parser: jsParser.configure({ dialect: "ts" }),
},
{
tag: "script",
attrs(attrs) {
return (
!attrs.type ||
/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i.test(attrs.type)
);
},
parser: jsParser,
},
{
tag: "style",
attrs(attrs) {
return (
(!attrs.lang || attrs.lang === "css" || attrs.lang === "scss") &&
(!attrs.type || /^(text\/)?(x-)?(stylesheet|css|scss)$/i.test(attrs.type))
);
},
parser: cssParser,
},
];
function getAttrs(element: SyntaxNodeRef, input: Input): Record<string, string> {
const attrs: Record<string, string> = Object.create(null);
const firstChild = element.node.firstChild;
if (!firstChild) return attrs;
for (const att of firstChild.getChildren("Attribute")) {
const name = att.getChild("AttributeName");
const value = att.getChild("AttributeValue") || att.getChild("UnquotedAttributeValue");
if (name) {
const attrName = input.read(name.from, name.to);
if (!value) {
attrs[attrName] = "";
} else if (value.name === "AttributeValue") {
attrs[attrName] = input.read(value.from + 1, value.to - 1);
} else {
attrs[attrName] = input.read(value.from, value.to);
}
}
}
return attrs;
}
function maybeNest(node: SyntaxNodeRef, input: Input, tags: NestedLang[]): NestedParse | null {
let attrs: Record<string, string> | undefined;
const parent = node.node.parent;
if (!parent) return null;
for (const tag of tags) {
if (!tag.attrs || tag.attrs(attrs || (attrs = getAttrs(parent, input)))) {
return { parser: tag.parser };
}
}
return null;
}
export function configureNesting(tags: NestedLang[]) {
const script: NestedLang[] = [];
const style: NestedLang[] = [];
const textarea: NestedLang[] = [];
for (const tag of tags) {
let array: NestedLang[];
if (tag.tag === "script") {
array = script;
} else if (tag.tag === "style") {
array = style;
} else if (tag.tag === "textarea") {
array = textarea;
} else {
throw new RangeError("Only script, style, and textarea tags can host nested parsers");
}
array.push(tag);
}
return parseMixed((node, input) => {
const id = node.type.id;
if (id === LongExpression || id === ShortExpression) return { parser: expressionParser };
if (id === ScriptText) return maybeNest(node, input, script);
if (id === StyleText) return maybeNest(node, input, style);
if (id === TextareaText) return maybeNest(node, input, textarea);
return null;
});
}
File diff suppressed because one or more lines are too long
+23
View File
@@ -0,0 +1,23 @@
export const scriptText = 145;
export const StartCloseScriptTag = 1;
export const styleText = 146;
export const StartCloseStyleTag = 2;
export const textareaText = 147;
export const StartCloseTextareaTag = 3;
export const StartTag = 4;
export const StartScriptTag = 5;
export const StartStyleTag = 6;
export const StartTextareaTag = 7;
export const StartSelfClosingTag = 8;
export const StartCloseTag = 9;
export const MismatchedStartCloseTag = 11;
export const missingCloseTag = 148;
export const IncompleteCloseTag = 12;
export const commentContent$1 = 149;
export const LongExpression = 13;
export const ShortExpression = 14;
export const Element = 67;
export const ScriptText = 110;
export const StyleText = 113;
export const TextareaText = 116;
export const OpenTag = 118;
+545
View File
@@ -0,0 +1,545 @@
import { ContextTracker, ExternalTokenizer, type InputStream, type Stack } from "@lezer/lr";
import {
commentContent$1,
Element,
IncompleteCloseTag,
LongExpression,
MismatchedStartCloseTag,
missingCloseTag,
OpenTag,
scriptText,
ShortExpression,
StartCloseScriptTag,
StartCloseStyleTag,
StartCloseTag,
StartCloseTextareaTag,
StartScriptTag,
StartSelfClosingTag,
StartStyleTag,
StartTag,
StartTextareaTag,
styleText,
textareaText,
} from "./terms.js";
const selfClosers: Record<string, boolean> = {
area: true,
base: true,
br: true,
col: true,
command: true,
embed: true,
frame: true,
hr: true,
img: true,
input: true,
keygen: true,
link: true,
meta: true,
param: true,
source: true,
track: true,
wbr: true,
menuitem: true,
};
const implicitlyClosed: Record<string, boolean> = {
dd: true,
li: true,
optgroup: true,
option: true,
p: true,
rp: true,
rt: true,
tbody: true,
td: true,
tfoot: true,
th: true,
tr: true,
};
const closeOnOpen: Record<string, Record<string, boolean>> = {
dd: { dd: true, dt: true },
dt: { dd: true, dt: true },
li: { li: true },
option: { option: true, optgroup: true },
optgroup: { optgroup: true },
p: {
address: true,
article: true,
aside: true,
blockquote: true,
dir: true,
div: true,
dl: true,
fieldset: true,
footer: true,
form: true,
h1: true,
h2: true,
h3: true,
h4: true,
h5: true,
h6: true,
header: true,
hgroup: true,
hr: true,
menu: true,
nav: true,
ol: true,
p: true,
pre: true,
section: true,
table: true,
ul: true,
},
rp: { rp: true, rt: true },
rt: { rp: true, rt: true },
tbody: { tbody: true, tfoot: true },
td: { td: true, th: true },
tfoot: { tbody: true },
th: { td: true, th: true },
thead: { tbody: true, tfoot: true },
tr: { tr: true },
};
function nameChar(ch: number): boolean {
return (
ch === 45 ||
ch === 46 ||
ch === 58 ||
(ch >= 65 && ch <= 90) ||
ch === 95 ||
(ch >= 97 && ch <= 122) ||
ch >= 161
);
}
function isSpace(ch: number): boolean {
return ch === 9 || ch === 10 || ch === 13 || ch === 32;
}
const lessThan = 60;
const greaterThan$1 = 62;
const slash$1 = 47;
const question = 63;
const bang = 33;
let cachedName: string | null | undefined = null;
let cachedInput: InputStream | null = null;
let cachedPos = 0;
function tagNameAfter(input: InputStream, offset: number): string | null | undefined {
const pos = input.pos + offset;
if (cachedPos === pos && cachedInput === input) return cachedName;
let next = input.peek(offset);
let curOffset = offset;
while (isSpace(next)) {
curOffset++;
next = input.peek(curOffset);
}
let name = "";
for (;;) {
if (!nameChar(next)) break;
name += String.fromCharCode(next);
curOffset++;
next = input.peek(curOffset);
}
cachedInput = input;
cachedPos = pos;
let result: string | null | undefined;
if (name) {
result = name.toLowerCase();
} else if (next === question || next === bang) {
result = undefined;
} else {
result = null;
}
cachedName = result;
return result;
}
export class ElementContext {
name: string;
parent: ElementContext | null;
hash: number;
constructor(name: string, parent: ElementContext | null) {
this.name = name;
this.parent = parent;
this.hash = parent ? parent.hash : 0;
for (let i = 0; i < name.length; i++) {
this.hash += (this.hash << 4) + name.charCodeAt(i) + (name.charCodeAt(i) << 8);
}
}
}
const startTagTerms = new Set([
StartTag,
StartSelfClosingTag,
StartScriptTag,
StartStyleTag,
StartTextareaTag,
]);
export const elementContext = new ContextTracker<ElementContext | null>({
start: null,
shift(context, term, _stack, input) {
return startTagTerms.has(term)
? new ElementContext(tagNameAfter(input, 1) || "", context)
: context;
},
reduce(context, term) {
return term === Element && context ? context.parent : context;
},
reuse(context, node, _stack, input) {
const type = node.type.id;
return type === StartTag || type === OpenTag
? new ElementContext(tagNameAfter(input, 1) || "", context)
: context;
},
hash(context) {
return context ? context.hash : 0;
},
strict: false,
});
export const tagStart = new ExternalTokenizer(
// oxlint-disable-next-line complexity -- vendored Lezer tokenizer
(input: InputStream, stack: Stack) => {
if (input.next !== lessThan) {
if (input.next < 0 && stack.context) input.acceptToken(missingCloseTag);
return;
}
input.advance();
const close = (input.next as number) === slash$1;
if (close) input.advance();
const name = tagNameAfter(input, 0);
if (name === undefined) return;
if (!name) {
input.acceptToken(close ? IncompleteCloseTag : StartTag);
return;
}
const parent = stack.context ? (stack.context as ElementContext).name : null;
if (close) {
if (name === parent) {
input.acceptToken(StartCloseTag);
return;
}
if (parent && implicitlyClosed[parent]) {
input.acceptToken(missingCloseTag, -2);
return;
}
for (
let cx: ElementContext | null = stack.context as ElementContext | null;
cx;
cx = cx.parent
) {
if (cx.name === name) return;
}
input.acceptToken(MismatchedStartCloseTag);
} else {
if (name === "script") {
input.acceptToken(StartScriptTag);
return;
}
if (name === "style") {
input.acceptToken(StartStyleTag);
return;
}
if (name === "textarea") {
input.acceptToken(StartTextareaTag);
return;
}
if (Object.prototype.hasOwnProperty.call(selfClosers, name)) {
input.acceptToken(StartSelfClosingTag);
return;
}
if (parent && closeOnOpen[parent]?.[name]) {
input.acceptToken(missingCloseTag, -1);
} else {
input.acceptToken(StartTag);
}
}
},
{ contextual: true },
);
function contentTokenizer(tag: string, textToken: number, endToken: number): ExternalTokenizer {
const lastState = 2 + tag.length;
return new ExternalTokenizer((input) => {
for (let state = 0, matchedLen = 0, i = 0; ; i++) {
if (input.next < 0) {
if (i) input.acceptToken(textToken);
break;
}
if (
(state === 0 && input.next === lessThan) ||
(state === 1 && input.next === slash$1) ||
(state >= 2 && state < lastState && input.next === tag.charCodeAt(state - 2))
) {
state++;
matchedLen++;
} else if ((state === 2 || state === lastState) && isSpace(input.next)) {
matchedLen++;
} else if (state === lastState && input.next === greaterThan$1) {
if (i > matchedLen) input.acceptToken(textToken, -matchedLen);
else input.acceptToken(endToken, -(matchedLen - 2));
break;
} else if ((input.next === 10 || input.next === 13) && i) {
input.acceptToken(textToken, 1);
break;
} else {
state = 0;
matchedLen = 0;
}
input.advance();
}
});
}
export const scriptTokens = contentTokenizer("script", scriptText, StartCloseScriptTag);
export const styleTokens = contentTokenizer("style", styleText, StartCloseStyleTag);
export const textareaTokens = contentTokenizer("textarea", textareaText, StartCloseTextareaTag);
const space = new Set([
9, 10, 11, 12, 13, 32, 133, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201,
8202, 8232, 8233, 8239, 8287, 12288,
]);
const parenOpen = 40;
const parenClose = 41;
const squareOpen = 91;
const squareClose = 93;
const curlyOpen = 123;
const curlyClose = 125;
const comma = 44;
const colon = 58;
const hash = 35;
const at = 64;
const slash = 47;
const greaterThan = 62;
const dash = 45;
const quoteDouble = 34;
const quoteSingle = 39;
const backslash = 92;
const newline = 10;
const asterisk = 42;
const tick = 96;
const prefixes = new Set([colon, hash, at, slash]);
export const commentContent = new ExternalTokenizer((input) => {
for (let dashes = 0, i = 0; ; i++) {
if (input.next < 0) {
if (i) input.acceptToken(commentContent$1);
break;
}
if (input.next === dash) {
dashes++;
} else if (input.next === greaterThan && dashes >= 2) {
if (i > 3) input.acceptToken(commentContent$1, -2);
break;
} else {
dashes = 0;
}
input.advance();
}
});
function createStringHandler(input: InputStream) {
let inString = false;
let inStringType: "double" | "single" | "template" | null = null;
let inStringIgnoreNext = false;
return () => {
if (inString) {
if (inStringIgnoreNext) {
inStringIgnoreNext = false;
return true;
}
if (input.next === backslash) {
inStringIgnoreNext = true;
return true;
}
if (inStringType === "double" && input.next === quoteDouble) {
inString = false;
inStringType = null;
return true;
}
if (inStringType === "single" && input.next === quoteSingle) {
inString = false;
inStringType = null;
return true;
}
if (inStringType === "template" && input.next === tick) {
inString = false;
inStringType = null;
return true;
}
return true;
}
if (input.next === quoteDouble) {
inString = true;
inStringType = "double";
return true;
}
if (input.next === quoteSingle) {
inString = true;
inStringType = "single";
return true;
}
if (input.next === tick) {
inString = true;
inStringType = "template";
return true;
}
return false;
};
}
function createCommentHandler(input: InputStream) {
let inLineComment = false;
let inBlockComment = false;
return () => {
if (inLineComment) {
if (input.next === newline) {
inLineComment = false;
return true;
}
return true;
}
if (inBlockComment) {
if (input.next === asterisk && input.peek(1) === slash) {
inBlockComment = false;
return true;
}
return true;
}
if (input.next === slash && input.peek(1) === slash) {
inLineComment = true;
return true;
}
if (input.next === slash && input.peek(1) === asterisk) {
inBlockComment = true;
return true;
}
return false;
};
}
export const longExpression = new ExternalTokenizer((input) => {
if (prefixes.has(input.next)) {
return;
}
const commentHandler = createCommentHandler(input);
const stringHandler = createStringHandler(input);
const stack: ("(" | "{" | "[")[] = [];
const popIfMatch = (match: "(" | "{" | "[") => {
const idx = stack.lastIndexOf(match);
if (idx !== -1) {
while (stack.length > idx) {
stack.pop();
}
}
};
for (let pos = 0; ; pos++) {
if (input.next < 0) {
if (pos > 0) input.acceptToken(LongExpression);
break;
}
if (commentHandler() || stringHandler()) {
input.advance();
continue;
}
if (
stack.length === 0 &&
(input.next === curlyClose || input.next === parenClose || input.next === squareClose)
) {
input.acceptToken(LongExpression);
break;
}
switch (input.next) {
case parenOpen:
stack.push("(");
break;
case parenClose:
popIfMatch("(");
break;
case squareOpen:
stack.push("[");
break;
case squareClose:
popIfMatch("[");
break;
case curlyOpen:
stack.push("{");
break;
case curlyClose:
popIfMatch("{");
break;
}
input.advance();
}
});
// oxlint-disable-next-line complexity -- vendored Lezer tokenizer
export const shortExpression = new ExternalTokenizer((input) => {
if (prefixes.has(input.peek(0))) {
return;
}
const commentHandler = createCommentHandler(input);
const stringHandler = createStringHandler(input);
const stack: ("(" | "{" | "[")[] = [];
const popIfMatch = (match: "(" | "{" | "[") => {
const idx = stack.lastIndexOf(match);
if (idx !== -1) {
while (stack.length > idx) {
stack.pop();
}
}
};
for (let pos = 0; ; pos++) {
if (input.next < 0) {
if (pos > 0) input.acceptToken(ShortExpression);
break;
}
if (commentHandler() || stringHandler()) {
input.advance();
continue;
}
if (
stack.length === 0 &&
(input.next === curlyClose ||
input.next === parenClose ||
input.next === squareClose ||
input.next === comma)
) {
input.acceptToken(ShortExpression);
break;
}
switch (input.next) {
case parenOpen:
stack.push("(");
break;
case parenClose:
popIfMatch("(");
break;
case squareOpen:
stack.push("[");
break;
case squareClose:
popIfMatch("[");
break;
case curlyOpen:
stack.push("{");
break;
case curlyClose:
popIfMatch("{");
break;
}
if (pos !== 0 && stack.length === 0 && space.has(input.next)) {
input.acceptToken(ShortExpression);
break;
}
input.advance();
}
});