diff --git a/.changeset/dark-birds-go.md b/.changeset/dark-birds-go.md new file mode 100644 index 00000000..6d1b86f2 --- /dev/null +++ b/.changeset/dark-birds-go.md @@ -0,0 +1,12 @@ +--- +'@fuzdev/fuz_code': minor +--- + +perf: rewrite to lexer architecture dropping regexp grammars + +- the regex-grammar engine is removed: `tokenize_syntax.ts`, the + `grammar_*.ts` modules, `SyntaxToken`, `tokenize()`, and the hooks system +- `SyntaxStyler` is now a lexer registry plus `lex`/`stylize` facade; `lex()` + returns the flat event stream (`LexedSyntax`); languages are `SyntaxLang` + lexers registered with `add_lang` +- `Code.svelte` no longer has a `grammar` prop diff --git a/CLAUDE.md b/CLAUDE.md index b4648d77..f1fba96a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,10 +1,12 @@ # fuz_code -> Syntax highlighting - a modernized PrismJS fork +> Syntax highlighting - hand-written single-pass lexers fuz_code (`@fuzdev/fuz_code`) is a runtime syntax highlighting library optimized -for HTML generation with CSS classes. It's a PrismJS fork with TypeScript types -and modern module support. +for HTML generation with CSS classes. It originated as a PrismJS fork and keeps +its `.token_*` class vocabulary, but the tokenizer is a full rewrite — one +hand-written single-pass lexer per language emitting a flat token event stream, +with zero regular expressions. For coding conventions, see Skill(fuz-stack). @@ -42,9 +44,9 @@ dev server. fuz_code is a **syntax highlighting library**: - Runtime HTML generation with CSS classes -- PrismJS-compatible grammar definitions +- Hand-written single-pass lexers (zero regex), one per language - 8 built-in languages (TS, JS, CSS, HTML, JSON, Svelte, Markdown, Bash) -- Extensible grammar system with hooks +- Extensible by writing a lexer (`SyntaxLang`) - Optional Svelte component (`Code.svelte`) ### What fuz_code does NOT include @@ -65,11 +67,10 @@ benchmark/ # performance testing └── compare/ # Prism/Shiki comparison src/ ├── lib/ # exportable library code -│ ├── syntax_styler.ts # SyntaxStyler class, hook system +│ ├── syntax_styler.ts # SyntaxStyler class: registry + lex/stylize facade │ ├── syntax_styler_global.ts # pre-configured global instance -│ ├── tokenize_syntax.ts # tokenize_syntax() function -│ ├── syntax_token.ts # SyntaxToken class, type definitions -│ ├── grammar_*.ts # language definitions (8 files) +│ ├── lexer.ts # lexer substrate: Lexer, TokenTypeRegistry, flat events, HTML render +│ ├── lexer_*.ts # hand-written lexers (json, ts, css, bash, markup, svelte, md) │ ├── Code.svelte # main Svelte component │ ├── CodeHighlight.svelte # experimental CSS Highlight API │ ├── highlight_manager.ts # CSS Highlight API manager @@ -78,8 +79,10 @@ src/ │ ├── theme_variables.css # CSS variable fallbacks │ └── theme_highlight.css # CSS Highlight API theme ├── test/ # test files and fixtures -│ ├── syntax_styler.test.ts │ ├── highlight_manager.test.ts +│ ├── lexer*.test.ts # lexer-engine suites (substrate + per language) +│ ├── lexer.pathological.test.ts # linearity + validity on adversarial inputs +│ ├── pathological.ts # pathological input generators (tests + benchmark) │ └── fixtures/ │ ├── samples/ # source of truth sample files │ ├── generated/ # generated fixture outputs @@ -93,50 +96,54 @@ src/ ### Core system -**SyntaxStyler** - The main class for tokenization and HTML generation. Uses -regex-based tokenization inherited from PrismJS, maintaining compatibility with -existing language definitions. +**Lexer engine** (`lexer.ts` + `lexer_*.ts`) - hand-written single-pass +lexers emitting flat token events (`Int32Array`) rendered to HTML in one +forward pass. Token types intern into a `TokenTypeRegistry` +(`token_types_global` by default, injectable via `SyntaxStylerOptions`). -**syntax_styler_global** - Pre-configured instance with all built-in grammars -registered. Import this for typical usage. +**SyntaxStyler** - The main class: a language registry and a `lex`/`stylize` +facade over the lexer engine. `lex(text, lang)` returns the flat event stream +(`LexedSyntax`); `stylize(text, lang)` renders it to HTML. -**tokenize_syntax()** - Core tokenization function that processes text through -grammar patterns and returns a token stream. +**syntax_styler_global** - Pre-configured instance with all built-in +languages registered. Import this for typical usage. ### Token structure -Tokens form a hierarchical tree where tokens can contain nested tokens: - -- `type` - token type (e.g., 'keyword', 'string') -- `content` - text or nested `SyntaxTokenStream` -- `alias` - CSS class aliases -- `length` - token text length - -Generated HTML uses classes like `.token_keyword`, `.token_string`, styled by -`theme.css`. +The lexer emits a flat event stream (`LexedSyntax`) — leaf/open/close records +in one `Int32Array` with interned type ids; plain text is implicit between +events (recovered from offsets). `render_syntax_html` streams HTML from it in +one forward pass, wrapping spans with classes like `.token_keyword`, +`.token_string` (styled by `theme.css`); `syntax_events_to_tokens` flattens it +to `{type, start, end}` for tests and fixtures. ### Language definitions -- `grammar_clike.ts` - base for C-like languages -- `grammar_js.ts` - JavaScript -- `grammar_ts.ts` - TypeScript (extends JS) -- `grammar_css.ts` - CSS stylesheets -- `grammar_markup.ts` - HTML/XML -- `grammar_json.ts` - JSON -- `grammar_svelte.ts` - Svelte components (extends markup) -- `grammar_bash.ts` - Bash/shell -- `grammar_markdown.ts` - Markdown - -### Hook system - -SyntaxStyler provides hooks for customizing tokenization and rendering: - -- **before_tokenize** - modify code/grammar before tokenization -- **after_tokenize** - modify token stream after tokenization -- **wrap** - customize HTML output per token (add attributes, custom wrapping) - -Register with `add_hook_before_tokenize()`, `add_hook_after_tokenize()`, -`add_hook_wrap()`. +One `SyntaxLang` lexer per language, registered via `add_lang`: + +- `lexer_json.ts` - JSON (with comments) +- `lexer_ts.ts` - TypeScript; also registers the `js`/`javascript` aliases + (TS is a syntactic superset — there is no separate JS lexer) +- `lexer_css.ts` - CSS (including native nesting) +- `lexer_bash.ts` - Bash; also registers the `sh`/`shell` aliases (POSIX sh + is a syntactic subset for highlighting — bash-family only, no fish etc.) +- `lexer_markup.ts` - HTML (`markup`/`html`/`mathml`/`svg`: rawtext + script/style/textarea/title, `style=`/`on*=` attribute embedding) and XML + (`xml`/`ssml`/`atom`/`rss`: plain tag scanning), one shared scanner + parameterized by `MarkupLexMode` +- `lexer_svelte.ts` - Svelte: the markup scanner in svelte mode (script→ts, + no special attrs) plus the `{…}` expression lexer (blocks, each/await + splits, at-directives, directive modifiers) +- `lexer_md.ts` - Markdown: line-oriented block scan (fences with exact-word + info matching and any-length closers, headings, blockquotes, lists, hr) + with a per-block inline scan (emphasis, inline code, links, entities, raw + markup via the markup scanner); fences embed their languages + +Embedded languages resolve lazily by name through the registry (markdown +fences → any language, markup ` diff --git a/src/lib/CodeHighlight.svelte b/src/lib/CodeHighlight.svelte index 73ddabe1..9ebb5289 100644 --- a/src/lib/CodeHighlight.svelte +++ b/src/lib/CodeHighlight.svelte @@ -10,7 +10,7 @@ import type {SvelteHTMLElements} from 'svelte/elements'; import {syntax_styler_global} from './syntax_styler_global.ts'; - import type {SyntaxStyler, SyntaxGrammar} from './syntax_styler.ts'; + import type {SyntaxStyler} from './syntax_styler.ts'; import {supports_css_highlight_api, type HighlightMode} from './highlight_manager.ts'; import {create_range_highlighting} from './range_highlighting.svelte.ts'; @@ -18,7 +18,6 @@ content, lang = 'svelte', mode = 'auto', - grammar, inline = false, wrap = false, syntax_styler = syntax_styler_global, @@ -31,17 +30,13 @@ * Language identifier (e.g., 'ts', 'css', 'html', 'json', 'svelte', 'md'). * * **Purpose:** - * - When `grammar` is not provided, used to look up the grammar via `syntax_styler.get_lang(lang)` - * - Used for metadata: sets the `data-lang` attribute and determines `language_supported` + * - Selects the registered lexer used to highlight `content` + * - Sets the `data-lang` attribute and determines `language_supported` * * **Special values:** * - `null` - Explicitly disables syntax highlighting (content rendered as plain text) * - `undefined` - Falls back to default ('svelte') * - * **Relationship with `grammar`:** - * - If both `lang` and `grammar` are provided, `grammar` takes precedence for tokenization - * - However, `lang` is still used for the `data-lang` attribute and language detection - * * @default 'svelte' */ lang?: string | null; @@ -59,23 +54,6 @@ * @default 'auto' */ mode?: HighlightMode; - /** - * Optional custom `SyntaxGrammar` object for syntax tokenization. - * - * **When to use:** - * - To provide a custom language definition not registered in `syntax_styler.langs` - * - To use a modified/extended version of an existing grammar - * - For one-off grammar variations without registering globally - * - * **Behavior:** - * - When provided, this grammar is used for tokenization instead of looking up via `lang` - * - Enables highlighting even if `lang` is not in the registry (useful for custom languages) - * - The `lang` parameter is still used for metadata (data-lang attribute) - * - When undefined, the grammar is automatically looked up via `syntax_styler.get_lang(lang)` - * - * @default undefined (uses grammar from `syntax_styler.langs[lang]`) - */ - grammar?: SyntaxGrammar | undefined; /** * Whether to render as inline code or block code. * Controls display via CSS classes. @@ -99,7 +77,7 @@ wrap?: boolean; /** * Custom `SyntaxStyler` instance to use for highlighting. - * Allows using a different styler with custom grammars or configuration. + * Allows using a different styler with custom languages or configuration. * * @default syntax_styler_global */ @@ -123,7 +101,6 @@ text: () => content, enabled: () => use_ranges, lang: () => lang, - grammar: () => grammar, syntax_styler: () => syntax_styler, dev_label: 'CodeHighlight', }); @@ -134,7 +111,7 @@ return ''; } - return syntax_styler.stylize(content, lang!, grammar); // ! is safe bc of the `highlighting_disabled` calculation + return syntax_styler.stylize(content, lang!); // ! is safe bc of the `highlighting_disabled` calculation }); // TODO do syntax styling at compile-time in the normal case, and don't import these at runtime diff --git a/src/lib/CodeTextarea.svelte b/src/lib/CodeTextarea.svelte index 222a4f07..ed54d47c 100644 --- a/src/lib/CodeTextarea.svelte +++ b/src/lib/CodeTextarea.svelte @@ -18,13 +18,12 @@ import type {SvelteHTMLElements} from 'svelte/elements'; import {syntax_styler_global} from './syntax_styler_global.ts'; - import type {SyntaxStyler, SyntaxGrammar} from './syntax_styler.ts'; + import type {SyntaxStyler} from './syntax_styler.ts'; import {create_range_highlighting} from './range_highlighting.svelte.ts'; let { value = $bindable(''), lang = 'svelte', - grammar, syntax_styler = syntax_styler_global, wrapper_attrs, ...rest @@ -36,8 +35,6 @@ * highlighting; `undefined` falls back to the default ('svelte'). */ lang?: string | null; - /** Optional custom grammar; takes precedence over `lang` for tokenization. */ - grammar?: SyntaxGrammar | undefined; /** Custom `SyntaxStyler` instance (defaults to the global one). */ syntax_styler?: SyntaxStyler; /** @@ -66,7 +63,6 @@ element: () => backdrop, text: () => display_text, lang: () => lang, - grammar: () => grammar, syntax_styler: () => syntax_styler, dev_label: 'CodeTextarea', }); diff --git a/src/lib/grammar_bash.ts b/src/lib/grammar_bash.ts deleted file mode 100644 index e4ad1187..00000000 --- a/src/lib/grammar_bash.ts +++ /dev/null @@ -1,175 +0,0 @@ -import type {AddSyntaxGrammar, SyntaxGrammarRaw} from './syntax_styler.ts'; - -/** - * Bash/shell grammar for syntax highlighting. - * - * Standalone grammar (no base extension). Covers core bash syntax: - * comments, strings, variables, functions, keywords, builtins, - * operators, and redirections. - * - * Based on Prism (https://github.com/PrismJS/prism) - * by Lea Verou (https://lea.verou.me/) - * - * MIT license - * - * @see LICENSE - */ -export const add_grammar_bash: AddSyntaxGrammar = (syntax_styler) => { - // Shared inside grammar for command substitution — `rest` wired after construction - const command_sub_inside: SyntaxGrammarRaw = { - punctuation: /^\$\(|\)$/, - }; - - // Reusable balanced-paren pattern for $(...) — handles 2 levels of inner () nesting, - // which supports up to 3 levels of $() command substitution (4+ is vanishingly rare) - const command_sub_pattern = /\$\((?:[^()]*|\((?:[^()]*|\([^()]*\))*\))*\)/; - - const grammar_bash = { - // Shebang at file start — matched before general comments - shebang: { - pattern: /^#!.*/, - alias: 'comment', - }, - - // Line comments — require whitespace or start-of-string before # - comment: { - pattern: /(^|\s)#.*/, - lookbehind: true, - greedy: true, - }, - - // Here-documents — must precede string to avoid delimiter consumption - heredoc: [ - // Quoted delimiter (<<'DELIM' or <<"DELIM") — no expansion - { - pattern: /(^|[^<])<<-?\s*(?:['"])(\w+)(?:['"])[\t ]*\n[\s\S]*?\n[\t ]*\2(?=\s*$)/m, - lookbehind: true, - greedy: true, - alias: 'string', - inside: { - // No `m` flag — `^` matches start-of-string (opening) and `$` matches - // end-of-string (closing), so single-word content lines can't false-positive - heredoc_delimiter: [ - { - pattern: /^<<-?\s*(?:['"])\w+(?:['"])/, - alias: 'punctuation', - }, - { - pattern: /\w+$/, - alias: 'punctuation', - }, - ], - } as SyntaxGrammarRaw, - }, - // Unquoted delimiter (<>?|<)/, - alias: 'important', - }, - - // Numbers: hex, octal, base-N, decimal - number: /\b(?:0x[\da-fA-F]+|0[0-7]+|\d+#[\da-zA-Z]+|\d+)\b/, - - // Operators — longest first - operator: /\|\||&&|;;|&>>?|<<>?|=~|[!=]=|[<>]|[|&!]/, - - // Punctuation - punctuation: /[{}[\]();,]/, - } satisfies SyntaxGrammarRaw; - - // Wire circular reference so command substitutions get full bash highlighting - command_sub_inside.rest = grammar_bash; - - syntax_styler.add_lang('bash', grammar_bash, ['sh', 'shell']); -}; diff --git a/src/lib/grammar_clike.ts b/src/lib/grammar_clike.ts deleted file mode 100644 index 1ab09963..00000000 --- a/src/lib/grammar_clike.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type {AddSyntaxGrammar, SyntaxGrammarRaw} from './syntax_styler.ts'; - -export const class_keywords = 'class|extends|implements|instanceof|interface|new'; - -/** - * Based on Prism (https://github.com/PrismJS/prism) - * by Lea Verou (https://lea.verou.me/) - * - * MIT license - * - * @see LICENSE - */ -export const add_grammar_clike: AddSyntaxGrammar = (syntax_styler) => { - const grammar_clike = { - comment: [ - { - pattern: /(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/, - lookbehind: true, - greedy: true, - }, - { - pattern: /(^|[^\\:])\/\/.*/, - lookbehind: true, - greedy: true, - }, - ], - string: { - pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/, - greedy: true, - }, - class_name: { - pattern: new RegExp(`(\\b(?:${class_keywords}|trait)\\s+|\\bcatch\\s+\\()[\\w.\\\\]+`, 'i'), - lookbehind: true, - inside: { - punctuation: /[.\\]/, - }, - }, - keyword: - /\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/, - boolean: /\b(?:false|true)\b/, - function: /\b\w+(?=\()/, - number: /\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i, - operator: /[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/, - punctuation: /[{}[\];(),.:]/, - } satisfies SyntaxGrammarRaw; - - syntax_styler.add_lang('clike', grammar_clike); -}; diff --git a/src/lib/grammar_css.ts b/src/lib/grammar_css.ts deleted file mode 100644 index 837a9f4d..00000000 --- a/src/lib/grammar_css.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type {AddSyntaxGrammar, SyntaxGrammarRaw} from './syntax_styler.ts'; -import {grammar_markup_add_attribute, grammar_markup_add_inlined} from './grammar_markup.ts'; - -var string = /(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/; - -/** - * Based on Prism (https://github.com/PrismJS/prism) - * by Lea Verou (https://lea.verou.me/) - * - * MIT license - * - * @see LICENSE - */ -export const add_grammar_css: AddSyntaxGrammar = (syntax_styler) => { - const grammar_css = { - comment: /\/\*[\s\S]*?\*\//, - atrule: { - pattern: RegExp( - '@[\\w-](?:' + - /[^;{\s"']|\s+(?!\s)/.source + - '|' + - string.source + - ')*?' + - /(?:;|(?=\s*\{))/.source, - ), - inside: { - rule: /^@[\w-]+/, - selector_function_argument: { - pattern: - /(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/, - lookbehind: true, - alias: 'selector', - }, - keyword: { - pattern: /(^|[^\w-])(?:and|not|only|or)(?![\w-])/, - lookbehind: true, - }, - } as SyntaxGrammarRaw, // see `rest` below - }, - url: { - // https://drafts.csswg.org/css-values-3/#urls - pattern: RegExp( - '\\burl\\((?:' + string.source + '|' + /(?:[^\\\r\n()"']|\\[\s\S])*/.source + ')\\)', - 'i', - ), - greedy: true, - inside: { - function: /^url/i, - punctuation: /^\(|\)$/, - string: { - pattern: RegExp('^' + string.source + '$'), - alias: 'url', - }, - }, - }, - selector: { - pattern: RegExp( - '(^|[{}\\s])[^{}\\s](?:[^{};"\'\\s]|\\s+(?![\\s{])|' + string.source + ')*(?=\\s*\\{)', - ), - lookbehind: true, - }, - string: { - pattern: string, - greedy: true, - }, - property: { - pattern: /(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i, - lookbehind: true, - }, - important: /!important\b/i, - function: { - pattern: /(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i, - lookbehind: true, - }, - punctuation: /[(){};:,]/, - } satisfies SyntaxGrammarRaw; - - grammar_css.atrule.inside.rest = grammar_css; - - syntax_styler.add_lang('css', grammar_css); - - grammar_markup_add_inlined(syntax_styler, 'style', 'css'); - grammar_markup_add_attribute(syntax_styler, 'style', 'css'); -}; diff --git a/src/lib/grammar_js.ts b/src/lib/grammar_js.ts deleted file mode 100644 index 509baa79..00000000 --- a/src/lib/grammar_js.ts +++ /dev/null @@ -1,215 +0,0 @@ -import type {AddSyntaxGrammar} from './syntax_styler.ts'; -import {grammar_markup_add_attribute, grammar_markup_add_inlined} from './grammar_markup.ts'; -import {class_keywords} from './grammar_clike.ts'; - -/** - * Based on Prism (https://github.com/PrismJS/prism) - * by Lea Verou (https://lea.verou.me/) - * - * MIT license - * - * @see LICENSE - */ -export const add_grammar_js: AddSyntaxGrammar = (syntax_styler) => { - const grammar_clike = syntax_styler.get_lang('clike'); - - // Main JS keywords (from keyword pattern, excluding those with special lookaheads) - const main_keywords = - 'class|const|debugger|delete|enum|extends|function|implements|in|instanceof|interface|let|new|null|of|package|private|protected|public|static|super|this|typeof|undefined|var|void|with'; - - // Keywords that are treated specially (inserted before 'function') - const special_keywords = - 'as|await|break|case|catch|continue|default|do|else|export|finally|for|from|if|import|return|switch|throw|try|while|yield'; - - // All JS keywords (for negative lookahead in parameter pattern) - // Note: 'assert', 'async', 'get', 'set' have special lookahead requirements in the main keyword pattern - const all_js_keywords = `assert|async|${main_keywords}|get|set|${special_keywords}`; - - const grammar_js = syntax_styler.add_extended_lang( - 'clike', - 'js', - { - class_name: [ - ...grammar_clike.class_name!, - { - pattern: - /(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/, - lookbehind: true, - }, - ], - keyword: [ - { - pattern: new RegExp( - `(^|[^.]|\\.\\.\\.\\s*)\\b(?:assert(?=\\s*\\{)|async(?=\\s*(?:function\\b|\\*|\\(|[$\\w\\xA0-\\uFFFF]|$))|${main_keywords}|(?:get|set)(?=\\s*(?:[#[$\\w\\xA0-\\uFFFF]|$)))\\b`, - ), - lookbehind: true, - }, - ], - // Allow for all non-ASCII characters (See http://stackoverflow.com/a/2008444) - function: - /#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/, - number: { - pattern: RegExp( - /(^|[^\w$])/.source + - '(?:' + - // constant - (/NaN|Infinity/.source + - '|' + - // binary integer - /0[bB][01]+(?:_[01]+)*n?/.source + - '|' + - // octal integer - /0[oO][0-7]+(?:_[0-7]+)*n?/.source + - '|' + - // hexadecimal integer - /0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source + - '|' + - // decimal bigint - /\d+(?:_\d+)*n/.source + - '|' + - // decimal number (integer or float) but no bigint - /(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/ - .source) + - ')' + - /(?![\w$])/.source, - ), - lookbehind: true, - }, - operator: - /--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/, - }, - ['javascript'], - ); - - (grammar_js as any).class_name[0].pattern = new RegExp( - `(\\b(?:${class_keywords})\\s+)[\\w.\\\\]+`, - ); - - syntax_styler.grammar_insert_before('js', 'function', { - special_keyword: new RegExp(`\\b(?:${special_keywords})\\b`), - }); - - syntax_styler.grammar_insert_before('js', 'keyword', { - regex: { - pattern: RegExp( - // lookbehind - /((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)/.source + - // Regex pattern: - // There are 2 regex patterns here. The RegExp set notation proposal added support for nested character - // classes if the `v` flag is present. Unfortunately, nested CCs are both context-free and incompatible - // with the only syntax, so we have to define 2 different regex patterns. - /\//.source + - '(?:' + - /(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\[\r\n])+\/[dgimyus]{0,7}/.source + - '|' + - // `v` flag syntax. This supports 3 levels of nested character classes. - /(?:\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.)*\])*\])*\]|\\.|[^/\\[\r\n])+\/[dgimyus]{0,7}v[dgimyus]{0,7}/ - .source + - ')' + - // lookahead - /(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/.source, - ), - lookbehind: true, - greedy: true, - inside: { - regex_source: { - pattern: /^(\/)[\s\S]+(?=\/[a-z]*$)/, - lookbehind: true, - alias: 'lang_regex', - inside: syntax_styler.langs.regex, // TODO use `get_lang` after adding `regex` support - }, - regex_delimiter: /^\/|\/$/, - regex_flags: /^[a-z]+$/, - }, - }, - // Arrow function and function expression variable names - // This must be declared before keyword because we use "function" inside the look-forward - function_variable: { - pattern: - /#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/, - alias: 'function', - }, - parameter: [ - { - pattern: - /(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/, - lookbehind: true, - inside: grammar_js, - }, - { - pattern: - /(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i, - lookbehind: true, - inside: grammar_js, - }, - { - pattern: /(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/, - lookbehind: true, - inside: grammar_js, - }, - { - pattern: new RegExp( - `((?:\\b|\\s|^)(?!(?:${all_js_keywords})(?![$\\w\\xA0-\\uFFFF]))(?:(?!\\s)[_$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*\\s*)\\(\\s*|\\]\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*\\{)`, - ), - lookbehind: true, - inside: grammar_js, - }, - ], - constant: /\b[A-Z](?:[A-Z_]|\dx?)*\b/, - // Heuristic: treat capitalized identifiers as class names when not already matched - capitalized_identifier: { - pattern: /\b[A-Z][\w]*\b/, - alias: 'class_name', - }, - }); - - syntax_styler.grammar_insert_before('js', 'string', { - hashbang: { - pattern: /^#!.*/, - greedy: true, - alias: 'comment', - }, - template_string: { - pattern: /`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/, - greedy: true, - inside: { - template_punctuation: { - pattern: /^`|`$/, - alias: 'string', - }, - interpolation: { - pattern: /((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/, - lookbehind: true, - inside: { - interpolation_punctuation: { - pattern: /^\$\{|\}$/, - alias: 'punctuation', - }, - rest: grammar_js as any, // TODO try to fix this type - }, - }, - string: /[\s\S]+/, - }, - }, - string_property: { - pattern: /((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m, - lookbehind: true, - greedy: true, - alias: 'property', - }, - }); - - syntax_styler.grammar_insert_before('js', 'operator', { - literal_property: { - pattern: /((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m, - lookbehind: true, - alias: 'property', - }, - }); - - grammar_markup_add_inlined(syntax_styler, 'script', 'js'); - - // add attribute support for all DOM events (on* attributes) - // https://developer.mozilla.org/en-US/docs/Web/Events#Standard_events - grammar_markup_add_attribute(syntax_styler, 'on\\w+', 'js'); -}; diff --git a/src/lib/grammar_json.ts b/src/lib/grammar_json.ts deleted file mode 100644 index 99683098..00000000 --- a/src/lib/grammar_json.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type {AddSyntaxGrammar, SyntaxGrammarRaw} from './syntax_styler.ts'; - -/** - * Based on Prism (https://github.com/PrismJS/prism) - * by Lea Verou (https://lea.verou.me/) - * - * MIT license - * - * @see LICENSE - */ -export const add_grammar_json: AddSyntaxGrammar = (syntax_styler) => { - const grammar_json = { - property: { - pattern: /(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/, - lookbehind: true, - greedy: true, - }, - string: { - pattern: /(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/, - lookbehind: true, - greedy: true, - }, - comment: { - pattern: /\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/, - greedy: true, - }, - number: /-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i, - punctuation: /[{}[\],]/, - operator: /:/, - boolean: /\b(?:false|true)\b/, - null: { - pattern: /\bnull\b/, - alias: 'keyword', - }, - } satisfies SyntaxGrammarRaw; - - syntax_styler.add_lang('json', grammar_json); -}; diff --git a/src/lib/grammar_markdown.ts b/src/lib/grammar_markdown.ts deleted file mode 100644 index 6a4e4ca2..00000000 --- a/src/lib/grammar_markdown.ts +++ /dev/null @@ -1,290 +0,0 @@ -import type { - AddSyntaxGrammar, - SyntaxGrammarTokenRaw, - SyntaxGrammarValueRaw, - SyntaxStyler, -} from './syntax_styler.ts'; - -interface LangDefinition { - aliases: Array; - id: string; -} - -interface FenceType { - backticks: string; - suffix: string; -} - -/** - * Helper to create fenced code block pattern for a language. - */ -const create_fence_pattern = ( - backticks: string, - aliases: Array, - lang_id: string, - syntax_styler: SyntaxStyler, -): SyntaxGrammarTokenRaw => { - const aliases_pattern = aliases.join('|'); - const pattern = new RegExp( - `^${backticks}(?:${aliases_pattern})[^\\n\\r]*(?:\\r?\\n|\\r)[\\s\\S]*?^${backticks}$`, - 'm', - ); - const code_fence_pattern = new RegExp(`^${backticks}[^\\n\\r]*|^${backticks}$`, 'm'); - - return { - pattern, - greedy: true, - inside: { - code_fence: { - pattern: code_fence_pattern, - alias: 'punctuation', - }, - [`lang_${lang_id}`]: { - pattern: /[\s\S]+/, - inside: syntax_styler.get_lang(lang_id), - }, - }, - }; -}; - -/** - * Helper to create catch-all fence pattern (unknown languages). - */ -const create_catchall_fence = (backticks: string): SyntaxGrammarTokenRaw => { - const pattern = new RegExp(`^${backticks}[^\\n\\r]*(?:\\r?\\n|\\r)[\\s\\S]*?^${backticks}$`, 'm'); - const code_fence_pattern = new RegExp(`^${backticks}[^\\n\\r]*|^${backticks}$`, 'm'); - - return { - pattern, - greedy: true, - inside: { - code_fence: { - pattern: code_fence_pattern, - alias: 'punctuation', - }, - }, - }; -}; - -/** - * Helper to create md self-reference placeholder pattern. - */ -const create_md_placeholder = (backticks: string): SyntaxGrammarTokenRaw => { - const pattern = new RegExp( - `^${backticks}(?:md|markdown)[^\\n\\r]*(?:\\r?\\n|\\r)[\\s\\S]*?^${backticks}$`, - 'm', - ); - const code_fence_pattern = new RegExp(`^${backticks}[^\\n\\r]*|^${backticks}$`, 'm'); - - return { - pattern, - greedy: true, - inside: { - code_fence: { - pattern: code_fence_pattern, - alias: 'punctuation', - }, - // lang_md will be added after registration - }, - }; -}; - -/** - * Markdown grammar extending markup. - * Supports: headings, fenced code blocks (3/4/5 backticks with nesting), lists, blockquotes, - * bold, italic, strikethrough, inline code, and links. - */ -export const add_grammar_markdown: AddSyntaxGrammar = (syntax_styler) => { - // Language definitions with aliases - const langs: Array = [ - {aliases: ['ts', 'typescript'], id: 'ts'}, - {aliases: ['js', 'javascript'], id: 'js'}, - {aliases: ['css'], id: 'css'}, - {aliases: ['html', 'markup'], id: 'markup'}, - {aliases: ['json'], id: 'json'}, - {aliases: ['svelte'], id: 'svelte'}, - {aliases: ['bash', 'sh', 'shell'], id: 'bash'}, - ]; - - // Fence types: higher counts first (for proper precedence in tokenization) - const fence_types: Array = [ - {backticks: '`````', suffix: '5tick'}, - {backticks: '````', suffix: '4tick'}, - {backticks: '```', suffix: ''}, - ]; - - // Build grammar dynamically - const grammar: Record = {}; - const md_self_refs: Array = []; // Track md patterns for later self-reference - - // Generate fence patterns for each type - for (const {backticks, suffix} of fence_types) { - const token_suffix = suffix ? `_${suffix}` : ''; - - // md pattern (self-reference added after registration) - const md_key = `fenced_code${token_suffix}_md`; - grammar[md_key] = create_md_placeholder(backticks); - md_self_refs.push(md_key); - - // Other language patterns (use first alias as token name for backward compatibility) - for (const {aliases, id} of langs) { - const token_name = aliases[0]; // Use first alias for token name - grammar[`fenced_code${token_suffix}_${token_name}`] = create_fence_pattern( - backticks, - aliases, - id, - syntax_styler, - ); - } - - // Catch-all fence - grammar[`fenced_code${token_suffix}`] = create_catchall_fence(backticks); - } - - // Register markdown grammar first, then add self-references for md fences - const grammar_md = syntax_styler.add_extended_lang( - 'markup', - 'md', - { - ...grammar, - - // Headings (# through ######) - heading: { - pattern: /^#{1,6}\s+.+$/m, - inside: { - punctuation: { - pattern: /^#{1,6}/, - alias: 'heading_punctuation', - }, - }, - }, - - // Blockquotes (> at line start) - blockquote: { - pattern: /^>\s*.+$/m, - inside: { - punctuation: { - pattern: /^>/, - alias: 'blockquote_punctuation', - }, - }, - }, - - // Lists (* or - with any preceding whitespace) - list: { - pattern: /^\s*[-*]\s+.+$/m, - inside: { - punctuation: /^\s*[-*]/, - }, - }, - - // Inline elements - - // Links [text](url) - link: { - pattern: /\[[^[\]\n\r]+\]\([^)\n\r]+\)/, - inside: { - link_text_wrapper: { - pattern: /^\[[^\]]+\]/, - inside: { - punctuation: { - pattern: /^\[|\]$/, - alias: 'link_punctuation', - }, - link_text: /[^[\]]+/, - }, - }, - url_wrapper: { - pattern: /\([^)]+\)$/, - inside: { - punctuation: { - pattern: /^\(|\)$/, - alias: 'link_punctuation', - }, - url: /[^()]+/, - }, - }, - }, - }, - - // Inline code `text` - inline_code: { - pattern: /`[^`\n\r]+`/, - alias: 'code', - inside: { - punctuation: { - pattern: /^`|`$/, - alias: 'code_punctuation', - }, - content: /[^`]+/, - }, - }, - - // Bold **text** or __text__ - bold: [ - { - // **text** - no asterisks inside - pattern: /\*\*[^\s*][^*]*[^\s*]\*\*|\*\*[^\s*]\*\*/, - inside: { - punctuation: /^\*\*|\*\*$/, - }, - }, - { - // __text__ - at word boundaries, no underscores inside - pattern: /\b__[^\s_][^_]*[^\s_]__\b|\b__[^\s_]__\b/, - inside: { - punctuation: /^__|__$/, - }, - }, - ], - - // Italic *text* or _text_ - italic: [ - { - // *text* - no asterisks inside - pattern: /\*[^\s*][^*]*[^\s*]\*|\*[^\s*]\*/, - inside: { - punctuation: /^\*|\*$/, - }, - }, - { - // _text_ - at word boundaries, no underscores inside - pattern: /\b_[^\s_][^_]*[^\s_]_\b|\b_[^\s_]_\b/, - inside: { - punctuation: /^_|_$/, - }, - }, - ], - - // Strikethrough ~~text~~ - strikethrough: { - pattern: /~~[^\s~][^~]*[^\s~]~~|~~[^\s~]~~/, - inside: { - punctuation: /^~~|~~$/, - }, - }, - }, - ['markdown'], - ); - - // Add self-reference for markdown-in-markdown (all fence types) - // This must be done after registration to avoid circular dependency - const lang_md_inside = { - pattern: /[\s\S]+/, - inside: syntax_styler.get_lang('md'), - }; - for (const key of md_self_refs) { - // After normalization, grammar values are arrays of SyntaxGrammarToken - // We need to add lang_md as a normalized array - const patterns = grammar_md[key]!; - // Manually normalize the lang_md pattern we're adding - const lang_md_token = { - pattern: lang_md_inside.pattern, - lookbehind: false, - greedy: false, - alias: [], - inside: lang_md_inside.inside, - }; - patterns[0]!.inside!.lang_md = [lang_md_token]; - } -}; diff --git a/src/lib/grammar_markup.ts b/src/lib/grammar_markup.ts deleted file mode 100644 index 54ec37e8..00000000 --- a/src/lib/grammar_markup.ts +++ /dev/null @@ -1,226 +0,0 @@ -import type { - SyntaxStyler, - AddSyntaxGrammar, - SyntaxGrammarRaw, - SyntaxGrammarToken, - SyntaxGrammar, -} from './syntax_styler.ts'; - -/** - * Based on Prism (https://github.com/PrismJS/prism) - * by Lea Verou (https://lea.verou.me/) - * - * MIT license - * - * @see LICENSE - */ -export const add_grammar_markup: AddSyntaxGrammar = (syntax_styler) => { - const grammar_markup = { - comment: { - pattern: //, - greedy: true, - }, - processing_instruction: { - pattern: /<\?[\s\S]+?\?>/, - greedy: true, - }, - // https://www.w3.org/TR/xml/#NT-doctypedecl - doctype: { - pattern: /]*>/i, - greedy: true, - }, - cdata: { - pattern: //i, - greedy: true, - }, - tag: { - pattern: - /<\/?(?!\d)[^\s>/=$<%]+(?:\s(?:\s*[^\s>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/, - greedy: true, - inside: { - tag: { - pattern: /^<\/?[^\s>/]+/, - inside: { - punctuation: { - pattern: /^<\/?/, - alias: 'tag_punctuation', - }, - namespace: /^[^\s>/:]+:/, - }, - }, - special_attr: [], - attr_value: { - pattern: /=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/, - inside: { - punctuation: [ - { - pattern: /^=/, - alias: 'attr_equals', - }, - { - pattern: /^(\s*)["']|["']$/, - lookbehind: true, - alias: 'attr_quote', - }, - ], - entity: undefined as any, // see below - }, - }, - punctuation: { - pattern: /\/?>/, - alias: 'tag_punctuation', - }, - attr_name: { - pattern: /[^\s>/]+/, - inside: { - namespace: /^[^\s>/:]+:/, - }, - }, - }, - }, - entity: [ - { - pattern: /&[\da-z]{1,8};/i, - alias: 'named_entity', - }, - /&#x?[\da-f]{1,8};/i, - ], - } satisfies SyntaxGrammarRaw; - - grammar_markup.tag.inside.attr_value.inside.entity = grammar_markup.entity; - - syntax_styler.add_lang('markup', grammar_markup, ['html', 'mathml', 'svg']); - syntax_styler.add_extended_lang('markup', 'xml', {}, ['ssml', 'atom', 'rss']); -}; - -/** - * Adds an inlined language to markup. - * - * An example of an inlined language is CSS with `\n\n@media*/ @@ -66,8 +65,7 @@ Token types (9 unique): 296-306 function light-dark 306-307 punctuation ( 316-317 punctuation , - 326-327 punctuation ) - 327-328 punctuation ; + 326-328 punctuation ); 330-331 punctuation } 332-333 punctuation } 335-351 selector .content::before diff --git a/src/test/fixtures/generated/html/html_complex.html b/src/test/fixtures/generated/html/html_complex.html index f2869291..4b874f34 100644 --- a/src/test/fixtures/generated/html/html_complex.html +++ b/src/test/fixtures/generated/html/html_complex.html @@ -12,7 +12,7 @@ <br /> -<hr onclick="console.log('hi')" /> +<hr onclick="console.log('hi')" /> <img src="image.jpg" alt="access" /> diff --git a/src/test/fixtures/generated/html/html_complex.txt b/src/test/fixtures/generated/html/html_complex.txt index 1e30930b..7c7a45b3 100644 --- a/src/test/fixtures/generated/html/html_complex.txt +++ b/src/test/fixtures/generated/html/html_complex.txt @@ -1,8 +1,8 @@ === STATS === Sample length: 733 characters -Total tokens: 229 +Total tokens: 230 -Token types (19 unique): +Token types (20 unique): punctuation: 117 tag: 64 attr_name: 16 @@ -12,6 +12,7 @@ Token types (19 unique): comment: 1 special_attr: 1 value: 1 + builtin: 1 function: 1 script: 1 lang_js: 1 @@ -95,6 +96,7 @@ Token types (19 unique): 232-233 punctuation = 233-234 punctuation " 234-251 value console.log('hi') + 234-241 builtin console 241-242 punctuation . 242-245 function log 245-246 punctuation ( diff --git a/src/test/fixtures/generated/json/json_complex.html b/src/test/fixtures/generated/json/json_complex.html index e69a58f0..d5d0aa5d 100644 --- a/src/test/fixtures/generated/json/json_complex.html +++ b/src/test/fixtures/generated/json/json_complex.html @@ -6,9 +6,9 @@ "empty": "", "escaped": "quote: \"test\" and backslash: \\", "object": { - "array": [1, "b", false], - "strings": ["1", "2", "3"], - "mixed": ["start", 123, true, "middle", null, "end"], - "nested": [["a", "str", ""], {"key": "nested value"}] + "array": [1, "b", false], + "strings": ["1", "2", "3"], + "mixed": ["start", 123, true, "middle", null, "end"], + "nested": [["a", "str", ""], {"key": "nested value"}] } } diff --git a/src/test/fixtures/generated/json/json_complex.txt b/src/test/fixtures/generated/json/json_complex.txt index e32ee2b3..65b2c771 100644 --- a/src/test/fixtures/generated/json/json_complex.txt +++ b/src/test/fixtures/generated/json/json_complex.txt @@ -1,9 +1,9 @@ === STATS === Sample length: 327 characters -Total tokens: 83 +Total tokens: 77 Token types (7 unique): - punctuation: 37 + punctuation: 31 string: 14 property: 12 operator: 12 @@ -48,8 +48,7 @@ Token types (7 unique): 167-170 string "b" 170-171 punctuation , 172-177 boolean false - 177-178 punctuation ] - 178-179 punctuation , + 177-179 punctuation ], 182-191 property "strings" 191-192 operator : 193-194 punctuation [ @@ -58,8 +57,7 @@ Token types (7 unique): 199-202 string "2" 202-203 punctuation , 204-207 string "3" - 207-208 punctuation ] - 208-209 punctuation , + 207-209 punctuation ], 212-219 property "mixed" 219-220 operator : 221-222 punctuation [ @@ -74,24 +72,20 @@ Token types (7 unique): 252-256 null null 256-257 punctuation , 258-263 string "end" - 263-264 punctuation ] - 264-265 punctuation , + 263-265 punctuation ], 268-276 property "nested" 276-277 operator : - 278-279 punctuation [ - 279-280 punctuation [ + 278-280 punctuation [[ 280-283 string "a" 283-284 punctuation , 285-290 string "str" 290-291 punctuation , 292-294 string "" - 294-295 punctuation ] - 295-296 punctuation , + 294-296 punctuation ], 297-298 punctuation { 298-303 property "key" 303-304 operator : 305-319 string "nested value" - 319-320 punctuation } - 320-321 punctuation ] + 319-321 punctuation }] 323-324 punctuation } 325-326 punctuation } diff --git a/src/test/fixtures/generated/md/md_complex.html b/src/test/fixtures/generated/md/md_complex.html index 4f20f861..a07d7fea 100644 --- a/src/test/fixtures/generated/md/md_complex.html +++ b/src/test/fixtures/generated/md/md_complex.html @@ -22,32 +22,32 @@ This is a [link to example](https://example.com) in text. -## Lists +## Lists -- list item 1 -- list item 2 -- list item 3 +- list item 1 +- list item 2 +- list item 3 -* alternative list item 1 -* alternative list item 2 - - indented list item 1 - - indented list item 2 - - Deeply nested item +* alternative list item 1 +* alternative list item 2 + - indented list item 1 + - indented list item 2 + - Deeply nested item - - after a newline + - after a newline ## Blockquotes > This is a blockquote. > It can span multiple lines. -> Another blockquote with **bold** and _italic_. +> Another blockquote with **bold** and _italic_. ## Fenced Code Blocks ### TypeScript -```ts +```ts function add(a: number, b: number): number { return a + b; } @@ -55,32 +55,32 @@ #### TypeScript with alias 'typescript' -```typescript -interface User { +```typescript +interface User { name: string; age: number; } -const user: User = {name: 'Alice', age: 30}; +const user: User = {name: 'Alice', age: 30}; ``` ### JS -```js -function add(a, b) { +```js +function add(a, b) { return a + b; } ``` #### JS with alias 'javascript' -```javascript -const fifteen = multiply(5, 3); +```javascript +const fifteen = multiply(5, 3); ``` ### CSS -```css +```css .container { display: flex; font-size: 14px; @@ -89,7 +89,7 @@ ### HTML -```html +```html <!doctype html> <div class="test"> <p>hello world!</p> @@ -102,7 +102,7 @@ #### HTML with alias 'markup' -```markup +```markup <ul> <li>a</li> <li>b</li> @@ -111,22 +111,22 @@ ### JSON -```json +```json { - "key": "value", - "array": [1, 2, 3, true, false, null] + "key": "value", + "array": [1, 2, 3, true, false, null] } ``` ### Svelte -```svelte +```svelte <script lang="ts"> - let count: number = $state(0); - const increment = () => count++; + let count: number = $state(0); + const increment = () => count++; </script> -<button type="button" onclick={increment}> +<button type="button" onclick={increment}> Count: {count} </button> @@ -139,8 +139,8 @@ ### Bash -```bash -if [[ $name =~ ^[a-z]+$ ]]; then +```bash +if [[ $name =~ ^[a-z]+$ ]]; then echo "hello $(whoami)" fi ``` @@ -175,18 +175,18 @@ Empty fence above. -```unknownlang +```unknownlang Plain text for unknown language. const variable = 2; ``` Nested fences (3 to 5): -`````md +`````md with `five` fences -````md +````md with `four` fences -```html +```html <style> body { color: red; } </style> @@ -194,7 +194,7 @@ ending `four` ```` another `four` -````ts +````ts const a = '5'; ```` ending `five` @@ -212,7 +212,7 @@ <div> -```ts +```ts const example = 'embedded code'; ``` diff --git a/src/test/fixtures/generated/md/md_complex.txt b/src/test/fixtures/generated/md/md_complex.txt index 0239de59..4e9a2209 100644 --- a/src/test/fixtures/generated/md/md_complex.txt +++ b/src/test/fixtures/generated/md/md_complex.txt @@ -1,34 +1,31 @@ === STATS === Sample length: 2799 characters -Total tokens: 527 +Total tokens: 525 -Token types (60 unique): - punctuation: 225 +Token types (51 unique): + punctuation: 223 tag: 56 - code_fence: 35 + code_fence: 34 heading: 23 operator: 20 - keyword: 13 + fenced_code: 17 + keyword: 12 list: 9 + bold: 7 + italic: 7 inline_code: 7 content: 7 lang_ts: 7 builtin: 7 number: 7 attr_name: 7 - bold: 6 - italic: 6 + property: 6 + attr_value: 6 string: 5 - attr_value: 5 function: 4 - property: 4 blockquote: 3 - fenced_code_ts: 3 - fenced_code_js: 3 - lang_js: 3 lang_css: 3 selector: 3 - fenced_code_html: 3 lang_markup: 3 strikethrough: 2 link: 2 @@ -37,32 +34,26 @@ Token types (60 unique): url_wrapper: 2 url: 2 special_keyword: 2 - capitalized_identifier: 2 type_annotation: 2 :: 2 type: 2 - string_property: 2 + lang_js: 2 boolean: 2 svelte_expression: 2 style: 2 - fenced_code: 2 lang_md: 2 class_name: 1 - parameter: 1 - fenced_code_css: 1 + type_name: 1 doctype: 1 comment: 1 - fenced_code_svelte: 1 + lang_json: 1 + null: 1 lang_svelte: 1 script: 1 function_variable: 1 - fenced_code_bash: 1 lang_bash: 1 variable: 1 command_substitution: 1 - fenced_code_5tick_md: 1 - fenced_code_4tick_md: 1 - fenced_code_4tick_ts: 1 === TOKENS === 0-11 heading # Heading 1 @@ -109,24 +100,24 @@ Token types (60 unique): 344-345 punctuation ) 356-364 heading ## Lists 356-358 punctuation ## - 364-379 list \n\n- list item 1 - 364-367 punctuation \n\n- - 379-393 list \n- list item 2 - 379-381 punctuation \n- - 393-407 list \n- list item 3 - 393-395 punctuation \n- - 407-434 list \n\n* alternative list item 1 - 407-410 punctuation \n\n* - 434-460 list \n* alternative list item 2 - 434-436 punctuation \n* - 460-485 list \n - indented list item 1 - 460-464 punctuation \n - - 485-510 list \n - indented list item 2 - 485-489 punctuation \n - - 510-535 list \n - Deeply nested item - 510-516 punctuation \n - - 535-556 list \n\n - after a newline - 535-540 punctuation \n\n - + 366-379 list - list item 1 + 366-367 punctuation - + 380-393 list - list item 2 + 380-381 punctuation - + 394-407 list - list item 3 + 394-395 punctuation - + 409-434 list * alternative list item 1 + 409-410 punctuation * + 435-460 list * alternative list item 2 + 435-436 punctuation * + 461-485 list - indented list item 1 + 461-464 punctuation - + 486-510 list - indented list item 2 + 486-489 punctuation - + 511-535 list - Deeply nested item + 511-516 punctuation - + 537-556 list - after a newline + 537-540 punctuation - 558-572 heading ## Blockquotes 558-560 punctuation ## 574-597 blockquote > This is a blockquote. @@ -135,11 +126,17 @@ Token types (60 unique): 598-599 punctuation > 629-677 blockquote > Another blockquote with **bold** and _italic_. 629-630 punctuation > + 655-663 bold **bold** + 655-657 punctuation ** + 661-663 punctuation ** + 668-676 italic _italic_ + 668-669 punctuation _ + 675-676 punctuation _ 679-700 heading ## Fenced Code Blocks 679-681 punctuation ## 702-716 heading ### TypeScript 702-705 punctuation ### - 718-789 fenced_code_ts ```ts\nfunction add(a: number, b: number): number {\n\treturn a + b;\n}\n``` + 718-789 fenced_code ```ts\nfunction add(a: number, b: number): number {\n\treturn a + b;\n}\n``` 718-723 code_fence ```ts 723-786 lang_ts \nfunction add(a: number, b: number): number {\n\treturn a + b;\n}\n 724-732 keyword function @@ -161,12 +158,11 @@ Token types (60 unique): 786-789 code_fence ``` 791-830 heading #### TypeScript with alias 'typescript' 791-795 punctuation #### - 832-943 fenced_code_ts ```typescript\ninterface User {\n\tname: string;\n\tage: number;\n}\n\nconst user: User = {name: 'Alice', age: 30};\n``` + 832-943 fenced_code ```typescript\ninterface User {\n\tname: string;\n\tage: number;\n}\n\nconst user: User = {name: 'Alice', age: 30};\n``` 832-845 code_fence ```typescript 845-940 lang_ts \ninterface User {\n\tname: string;\n\tage: number;\n}\n\nconst user: User = {name: 'Alice', age: 30};\n 846-855 keyword interface 856-860 class_name User - 856-860 capitalized_identifier User 861-862 punctuation { 868-869 operator : 870-876 builtin string @@ -176,10 +172,10 @@ Token types (60 unique): 890-891 punctuation ; 892-893 punctuation } 895-900 keyword const - 905-912 type_annotation : User + 905-911 type_annotation : User 905-906 : : - 906-912 type User - 907-911 capitalized_identifier User + 907-911 type User + 907-911 type_name User 912-913 operator = 914-915 punctuation { 919-920 operator : @@ -187,18 +183,16 @@ Token types (60 unique): 928-929 punctuation , 933-934 operator : 935-937 number 30 - 937-938 punctuation } - 938-939 punctuation ; + 937-939 punctuation }; 940-943 code_fence ``` 945-951 heading ### JS 945-948 punctuation ### - 953-1000 fenced_code_js ```js\nfunction add(a, b) {\n\treturn a + b;\n}\n``` + 953-1000 fenced_code ```js\nfunction add(a, b) {\n\treturn a + b;\n}\n``` 953-958 code_fence ```js 958-997 lang_js \nfunction add(a, b) {\n\treturn a + b;\n}\n 959-967 keyword function 968-971 function add 971-972 punctuation ( - 972-976 parameter a, b 973-974 punctuation , 976-977 punctuation ) 978-979 punctuation { @@ -209,7 +203,7 @@ Token types (60 unique): 997-1000 code_fence ``` 1002-1033 heading #### JS with alias 'javascript' 1002-1006 punctuation #### -1035-1084 fenced_code_js ```javascript\nconst fifteen = multiply(5, 3);\n``` +1035-1084 fenced_code ```javascript\nconst fifteen = multiply(5, 3);\n``` 1035-1048 code_fence ```javascript 1048-1081 lang_js \nconst fifteen = multiply(5, 3);\n 1049-1054 keyword const @@ -219,12 +213,11 @@ Token types (60 unique): 1074-1075 number 5 1075-1076 punctuation , 1077-1078 number 3 -1078-1079 punctuation ) -1079-1080 punctuation ; +1078-1080 punctuation ); 1081-1084 code_fence ``` 1086-1093 heading ### CSS 1086-1089 punctuation ### -1095-1154 fenced_code_css ```css\n.container {\n\tdisplay: flex;\n\tfont-size: 14px;\n}\n``` +1095-1154 fenced_code ```css\n.container {\n\tdisplay: flex;\n\tfont-size: 14px;\n}\n``` 1095-1101 code_fence ```css 1101-1151 lang_css \n.container {\n\tdisplay: flex;\n\tfont-size: 14px;\n}\n 1102-1112 selector .container @@ -239,7 +232,7 @@ Token types (60 unique): 1151-1154 code_fence ``` 1156-1164 heading ### HTML 1156-1159 punctuation ### -1166-1308 fenced_code_html ```html\n\n
\n\t

hello world!

\n
\n\n\n\n\n``` +1166-1308 fenced_code ```html\n\n
\n\t

hello world!

\n
\n\n\n\n\n``` 1166-1173 code_fence ```html 1173-1305 lang_markup \n\n
\n\t

hello world!

\n
\n\n\n\n\n 1174-1189 doctype @@ -282,7 +275,7 @@ Token types (60 unique): 1305-1308 code_fence ``` 1310-1339 heading #### HTML with alias 'markup' 1310-1314 punctuation #### -1341-1389 fenced_code_html ```markup\n
    \n\t
  • a
  • \n\t
  • b
  • \n
\n``` +1341-1389 fenced_code ```markup\n
    \n\t
  • a
  • \n\t
  • b
  • \n
\n``` 1341-1350 code_fence ```markup 1350-1386 lang_markup \n
    \n\t
  • a
  • \n\t
  • b
  • \n
\n 1351-1355 tag
    @@ -312,15 +305,15 @@ Token types (60 unique): 1386-1389 code_fence ``` 1391-1399 heading ### JSON 1391-1394 punctuation ### -1401-1472 fenced_code_js ```json\n{\n\t"key": "value",\n\t"array": [1, 2, 3, true, false, null]\n}\n``` +1401-1472 fenced_code ```json\n{\n\t"key": "value",\n\t"array": [1, 2, 3, true, false, null]\n}\n``` 1401-1408 code_fence ```json -1408-1469 lang_js \n{\n\t"key": "value",\n\t"array": [1, 2, 3, true, false, null]\n}\n +1408-1469 lang_json \n{\n\t"key": "value",\n\t"array": [1, 2, 3, true, false, null]\n}\n 1409-1410 punctuation { -1412-1417 string_property "key" +1412-1417 property "key" 1417-1418 operator : 1419-1426 string "value" 1426-1427 punctuation , -1429-1436 string_property "array" +1429-1436 property "array" 1436-1437 operator : 1438-1439 punctuation [ 1439-1440 number 1 @@ -333,13 +326,13 @@ Token types (60 unique): 1452-1453 punctuation , 1454-1459 boolean false 1459-1460 punctuation , -1461-1465 keyword null +1461-1465 null null 1465-1466 punctuation ] 1467-1468 punctuation } 1469-1472 code_fence ``` 1474-1484 heading ### Svelte 1474-1477 punctuation ### -1486-1716 fenced_code_svelte ```svelte\n\n\n\n\n\n``` +1486-1716 fenced_code ```svelte\n\n\n\n\n\n``` 1486-1495 code_fence ```svelte 1495-1713 lang_svelte \n\n\n\n\n\n 1496-1514 tag ', ['script', 'lang_js', 'keyword']), [ + ['script', '\nlet x = 1;\n'], + ['lang_js', '\nlet x = 1;\n'], + ['keyword', 'let'], + ]); + }); + + test('markup constructs inside block containers clamp to the line', () => { + const text = '> quote { + // the markup probe cache is shared across the whole document scan + // (`MdScanCache.markup`) — entity-less attribute values on earlier lines + // must not lose an entity in a later one + assert.deepEqual(picked('x\n\ny', ['entity']), [ + ['entity', '&'], + ]); + }); +}); + +describe('lexer_md sample', () => { + const content = readFileSync('src/test/fixtures/samples/sample_complex.md', 'utf8'); + + test('lexes the sample with valid invariants', () => { + assert.deepEqual(validate_syntax_events(syntax_styler_global.lex(content, 'md')), []); + }); + + test('sample produces its characteristic token types', () => { + const types = new Set( + syntax_events_to_tokens(syntax_styler_global.lex(content, 'md')).map((t) => t.type), + ); + for (const t of ['heading', 'fenced_code', 'code_fence', 'list', 'bold', 'italic']) { + assert.ok(types.has(t), `expected a ${t} token in the sample`); + } + }); + + test('every prefix lexes without throwing, with valid invariants', () => { + for (let len = 0; len <= content.length; len += 13) { + const prefix = content.slice(0, len); + const lexed = syntax_styler_global.lex(prefix, 'md'); + assert.deepEqual(validate_syntax_events(lexed), [], `prefix of length ${len}`); + } + }); + + test('lexing is deterministic', () => { + const a = syntax_events_to_tokens(syntax_styler_global.lex(content, 'md')); + const b = syntax_events_to_tokens(syntax_styler_global.lex(content, 'md')); + assert.deepEqual(a, b); + }); +}); diff --git a/src/test/lexer_svelte.test.ts b/src/test/lexer_svelte.test.ts new file mode 100644 index 00000000..17f3d5ff --- /dev/null +++ b/src/test/lexer_svelte.test.ts @@ -0,0 +1,314 @@ +import {describe, test, assert} from 'vitest'; +import {readFileSync} from 'node:fs'; + +import {syntax_styler_global} from '$lib/syntax_styler_global.ts'; +import {syntax_events_to_tokens, validate_syntax_events} from '$lib/lexer.ts'; + +const tokens_of = (text: string): Array<[string, string]> => + syntax_events_to_tokens(syntax_styler_global.lex(text, 'svelte')).map((t) => [ + t.type, + text.slice(t.start, t.end), + ]); + +const picked = (text: string, types: Array): Array<[string, string]> => + tokens_of(text).filter(([type]) => types.includes(type)); + +describe('lexer_svelte expressions', () => { + test('a plain expression', () => { + assert.deepEqual(tokens_of('{x}'), [ + ['svelte_expression', '{x}'], + ['punctuation', '{'], + ['lang_ts', 'x'], + ['punctuation', '}'], + ]); + }); + + test('an empty expression coalesces its braces', () => { + assert.deepEqual(tokens_of('{}'), [ + ['svelte_expression', '{}'], + ['punctuation', '{}'], + ]); + }); + + test('braces inside strings do not close the expression', () => { + assert.deepEqual(tokens_of("{'}'}"), [ + ['svelte_expression', "{'}'}"], + ['punctuation', '{'], + ['lang_ts', "'}'"], + ['string', "'}'"], + ['punctuation', '}'], + ]); + }); + + test('nested braces balance to any depth', () => { + const text = '{f({a: {b: {c: 1}}})}'; + const tokens = tokens_of(text); + assert.deepEqual(tokens[0], ['svelte_expression', text]); + assert.deepEqual(tokens.at(-1), ['punctuation', '}']); + }); + + test('an unterminated expression extends to the end', () => { + const text = '{#if x'; + assert.deepEqual(validate_syntax_events(syntax_styler_global.lex(text, 'svelte')), []); + assert.deepEqual(picked(text, ['special_keyword']), [['special_keyword', '#if']]); + }); + + test('expressions and tags lex across probe-starved prefixes', () => { + // the window's `<`/`{` probes are cached (`MarkupProbeCache`) — a run of + // constructs without one probe char must not lose the construct after it + assert.deepEqual(picked('aa{x}', ['svelte_expression']), [ + ['svelte_expression', '{x}'], + ]); + assert.deepEqual(picked('{x}{y}a', ['tag']), [ + ['tag', ''], + ['tag', ''], + ['tag', ' { + test('open, else, and close blocks', () => { + assert.deepEqual(tokens_of('{#if x}'), [ + ['svelte_expression', '{#if x}'], + ['block', '{#if x}'], + ['punctuation', '{'], + ['special_keyword', '#if'], + ['lang_ts', 'x'], + ['punctuation', '}'], + ]); + assert.deepEqual(picked('{:else}', ['special_keyword']), [['special_keyword', ':else']]); + assert.deepEqual(picked('{:else if y}', ['special_keyword', 'lang_ts']), [ + ['special_keyword', ':else if'], + ['lang_ts', 'y'], + ]); + assert.deepEqual(picked('{/if}', ['block', 'special_keyword']), [ + ['block', '{/if}'], + ['special_keyword', '/if'], + ]); + }); + + test('snippet blocks lex their signature as ts', () => { + assert.deepEqual(picked('{#snippet greet(name: string)}', ['special_keyword', 'function']), [ + ['special_keyword', '#snippet'], + ['function', 'greet'], + ]); + }); + + test('an unknown sigil word stays a plain expression', () => { + assert.deepEqual(picked('{#nope x}', ['block', 'each', 'special_keyword']), []); + assert.deepEqual(tokens_of('{#nope x}')[0], ['svelte_expression', '{#nope x}']); + }); +}); + +describe('lexer_svelte each', () => { + test('the expression before `as` is lexed as ts', () => { + assert.deepEqual(tokens_of('{#each thing_keys as [k, v] (key(k))}').slice(0, 6), [ + ['svelte_expression', '{#each thing_keys as [k, v] (key(k))}'], + ['each', '{#each thing_keys as [k, v] (key(k))}'], + ['punctuation', '{'], + ['special_keyword', '#each'], + ['lang_ts', 'thing_keys'], + ['keyword', 'as'], + ]); + }); + + test('`as` inside brackets or identifiers does not split', () => { + assert.deepEqual(picked('{#each has_as as base}', ['keyword']), [['keyword', 'as']]); + assert.deepEqual(picked('{#each f([x as y]) as z}', ['keyword']).length, 1); + }); + + test('a close-each is a block', () => { + assert.deepEqual(picked('{/each}', ['block', 'each']), [['block', '{/each}']]); + }); +}); + +describe('lexer_svelte await', () => { + test('`then` splits two ts interiors', () => { + assert.deepEqual( + picked('{#await promise then value}', ['special_keyword', 'keyword', 'lang_ts']), + [ + ['special_keyword', '#await'], + ['lang_ts', 'promise'], + ['keyword', 'then'], + ['lang_ts', 'value'], + ], + ); + }); + + test(':then and :catch are special keywords', () => { + assert.deepEqual(picked('{:then v}', ['special_keyword']), [['special_keyword', ':then']]); + assert.deepEqual(picked('{:catch e}', ['special_keyword']), [['special_keyword', ':catch']]); + }); +}); + +describe('lexer_svelte at-directives', () => { + test('directive structure', () => { + assert.deepEqual(tokens_of('{@render children()}').slice(0, 5), [ + ['svelte_expression', '{@render children()}'], + ['at_directive', '{@render children()}'], + ['punctuation', '{'], + ['at_keyword', '@render'], + ['lang_ts', 'children()'], + ]); + }); + + test('nested braces and comments inside directives', () => { + const text = '{@attach f(() => {\n\t// arg\n})}'; + assert.deepEqual(validate_syntax_events(syntax_styler_global.lex(text, 'svelte')), []); + assert.deepEqual(picked(text, ['at_keyword', 'comment']), [ + ['at_keyword', '@attach'], + ['comment', '// arg'], + ]); + }); +}); + +describe('lexer_svelte tags', () => { + test('an expression-valued attribute', () => { + assert.deepEqual(tokens_of('').slice(3, 9), [ + ['attr_name', 'b'], + ['attr_value', '={x}'], + ['punctuation', '='], + ['svelte_expression', '{x}'], + ['punctuation', '{'], + ['lang_ts', 'x'], + ]); + }); + + test('shorthand and spread expressions in attribute position', () => { + assert.deepEqual(picked('