blob: 46e017d6d1f7dcd8dad74024feae2370be2df0e5 [file]
/**
* Syntax colouring for the file-diff preview.
*
* The diff body used to paint every line one flat colour — green for an
* addition, red for a deletion, grey for context — so a fifty-line hunk read
* as three blocks of tint with no structure inside them. Astryx's `CodeBlock`
* cannot supply the missing half: it highlights a whole buffer in one
* language, and a diff is neither (its marker column is not code, and its
* lines are two files interleaved).
*
* What Astryx does ship is the tokenizer underneath that component, and it
* carries no state between lines: `tokenize` returns one token array per line,
* each starting from that line's own offset with nothing inherited from the
* one above. That is what a diff can use — the rows tokenize as one buffer of
* stripped code and the results still line up row for row, with no pretence
* that the old and new sides are each a compilable file.
*
* Stateless is not the same as line-bounded, and the difference is visible.
* `tokenizeLine` bounds where a match may *start*, not how far it may run, and
* several patterns span newlines — the JS block comment `/\*[\s\S]*?\*\/`, the
* HTML `<!-- -->`, Python's triple-quoted strings. A row that opens one gets a
* token reaching into the rows below (clamped where it is rendered), and those
* rows, having inherited nothing, colour as ordinary code. So the interior of a
* multi-line comment reads as code rather than as comment. That is a colour
* being wrong, never a character: it is strictly better than the flat tint it
* replaced, and fixing it means carrying an "inside a construct" flag across
* rows here — a real feature, not a clamp, and not worth it until someone
* misreads a diff because of it.
*
* The `astryx-token-*` classes the spans carry are Astryx's own span-mode
* fallback classes, injected by `ensureHighlightStyles()`; the product's
* `--color-syntax-*` theme tokens are what they resolve against.
*/
import { tokenize, type TokenLine } from '@astryxdesign/core';
/**
* File extension → the language identifier Astryx's tokenizer knows. The table
* is bounded by what `buildLanguagePatterns` implements, not by what Maka
* happens to edit: anything else tokenizes to nothing and renders as plain
* text, which is the result the diff had before and never a wrong colouring.
*/
const LANGUAGE_BY_EXTENSION: Readonly<Record<string, string>> = {
ts: 'typescript',
tsx: 'tsx',
mts: 'typescript',
cts: 'typescript',
js: 'javascript',
jsx: 'jsx',
mjs: 'javascript',
cjs: 'javascript',
json: 'json',
jsonc: 'json',
html: 'html',
htm: 'html',
xml: 'xml',
svg: 'svg',
css: 'css',
scss: 'scss',
less: 'less',
py: 'python',
pyi: 'python',
sh: 'bash',
bash: 'bash',
zsh: 'zsh',
php: 'php',
hh: 'hack',
yaml: 'yaml',
yml: 'yaml',
md: 'markdown',
markdown: 'markdown',
};
/**
* Per-row tokens for a diff's stripped code, in row order — empty when the
* paths do not agree on one language the tokenizer knows.
*
* Every producer of a `file_diff` result names exactly one path, so the
* disagreement case is a guard rather than a feature: a diff spanning two
* languages renders plain instead of taking whichever file came first.
*
* Joining the rows into one buffer is a way to make a single `tokenize` call,
* not a claim that the buffer is valid source — the tokenizer returns one
* entry per line and inherits no state from the line above (see the note on
* newline-spanning patterns at the top of this file).
*/
export function diffSyntaxTokens(paths: readonly string[], code: readonly string[]): TokenLine[] {
if (code.length === 0) return [];
let language: string | undefined;
for (const path of paths) {
const candidate = syntaxLanguageForPath(path);
if (candidate === undefined) return [];
if (language !== undefined && language !== candidate) return [];
language = candidate;
}
return language === undefined ? [] : tokenize(code.join('\n'), language);
}
export function syntaxLanguageForPath(path: string): string | undefined {
const extension = /\.([A-Za-z0-9]+)$/.exec(path)?.[1]?.toLowerCase();
return extension ? LANGUAGE_BY_EXTENSION[extension] : undefined;
}