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