buninu 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/apps/jsgotty/README.en.md +4 -0
- package/apps/jsgotty/README.md +4 -0
- package/apps/jsgotty/package-lock.json +2 -2
- package/apps/jsgotty/package.json +1 -1
- package/apps/jsgotty/static/css/index.css +7 -0
- package/apps/jsgotty/static/js/gotty.js +12 -4
- package/apps/jsgotty/static/js/kitty.js +115 -7
- package/apps/jsgotty/tests/manual/zoom-collector.cjs +55 -0
- package/apps/jsgotty/tests/manual/zoom-debug.js +39 -0
- package/apps/jsgotty/tests/touch-drag-browser.test.js +106 -0
- package/apps/jsgotty/tests/wheel-browser.test.js +60 -0
- package/apps/jsmdcui/CHANGELOG.md +44 -0
- package/apps/jsmdcui/package.json +1 -1
- package/apps/jsmdcui/src/highlight/go-regexp.js +83 -0
- package/apps/jsmdcui/src/highlight/highlighter.js +49 -62
- package/apps/jsmdcui/src/highlight/parser.js +8 -5
- package/apps/jsmdcui/tests/go-regexp.test.js +98 -0
- package/apps/jsmdcui/tests/heading-ids.test.js +21 -16
- package/apps/jsmdcui/tests/highlight-regions.test.js +73 -0
- package/apps/jsmdcui/tests/id-collision.test.js +32 -9
- package/apps/jsmdcui/tests/markdown-behavior.mjs +9 -0
- package/apps/jsmdcui/tests/syntax-parity/NORMAL-HTML.md +46 -0
- package/apps/jsmdcui/tests/syntax-parity/README.md +85 -0
- package/apps/jsmdcui/tests/syntax-parity/REPORT.md +328 -0
- package/apps/jsmdcui/tests/syntax-parity/compare.mjs +75 -0
- package/apps/jsmdcui/tests/syntax-parity/go.mod +5 -0
- package/apps/jsmdcui/tests/syntax-parity/normal-html-report.json +619 -0
- package/apps/jsmdcui/tests/syntax-parity/normal-html.html +9 -0
- package/apps/jsmdcui/tests/syntax-parity/oracle.go +49 -0
- package/apps/jsmdcui/tests/syntax-parity/report.json +2174 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/PowerShell.txt +39 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ada.txt +25 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/apacheconf.txt +49 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/arduino.txt +38 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/asciidoc.txt +33 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/asm.txt +77 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ats.txt +48 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/awk.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/b.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/bat.txt +39 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/c.txt +34 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/caddyfile.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cake.txt +7 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/clojure.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cmake.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/coffeescript.txt +32 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/colortest.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/conky.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cpp.txt +46 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/crontab.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/crystal.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/csharp.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/css.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/csx.txt +7 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cuda.txt +37 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cython.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/d.txt +52 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/dart.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/default.txt +8 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/dockerfile.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/dot.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/elixir.txt +23 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/elm.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/erb.txt +32 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/erlang.txt +23 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/fish.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/forth.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/fortran.txt +40 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/freebsd-kernel.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/fsharp.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gdscript.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gemini.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gentoo-ebuild.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gentoo-etc-portage.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/git-commit.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/git-config.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/git-rebase-todo.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gleam.txt +44 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/glsl.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gnuplot.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/go.txt +33 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/godoc.txt +9 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/golo.txt +50 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gomod.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/graphql.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/groff.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/groovy.txt +42 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/haml.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/hare.txt +31 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/haskell.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/hc.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/html.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/html4.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/html5.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ini.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/inputrc.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/java.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/javascript.txt +41 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/jinja2.txt +13 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/json.txt +21 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/jsonnet.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/julia.txt +24 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/justfile.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/keymap.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/kickstart.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/kotlin.txt +30 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/kvlang.txt +37 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ledger.txt +13 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/lfe.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/lilypond.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/lisp.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/log.txt +45 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/lua.txt +48 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/mail.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/makefile.txt +25 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/man.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/markdown.txt +21 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/mc.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/meson.txt +29 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/micro.txt +25 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/mpdconf.txt +13 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/msbuild.txt +7 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nanorc.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nftables.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nginx.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nim.txt +23 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nix.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nu.txt +92 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/objc.txt +36 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ocaml.txt +25 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/octave.txt +35 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/odin.txt +35 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/pascal.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/patch.txt +13 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/peg.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/perl.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/php.txt +44 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/pkg-config.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/po.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/pony.txt +29 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/pov.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/privoxy-action.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/privoxy-config.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/privoxy-filter.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/proto.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/prql.txt +42 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/puppet.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/python2.txt +24 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/python3.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/r.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/raku.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/reST.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/renpy.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/rpmspec.txt +31 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ruby.txt +34 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/rust.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sage.txt +24 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/scad.txt +23 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/scala.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sed.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sh.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sls.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/smalltalk.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/solidity.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sql.txt +35 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/stata.txt +32 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/svelte.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/swift.txt +53 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/systemd.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/tcl.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/terraform.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/tex.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/toml.txt +31 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/twig.txt +29 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/typescript.txt +32 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/v.txt +47 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/vala.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/verilog.txt +37 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/vhdl.txt +37 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/vi.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/vue.txt +29 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/xml.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/xresources.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/yaml.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/yum.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/zig.txt +24 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/zscript.txt +31 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/zsh.txt +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Adapt Go regexp syntax without changing the upstream syntax definitions.
|
|
2
|
+
// Scoped JS modifiers preserve flag changes across alternatives, but not out
|
|
3
|
+
// of their enclosing group. They add no capturing groups. Modifiers are only
|
|
4
|
+
// emitted where flags differ from those in effect, so flag-free patterns
|
|
5
|
+
// do not depend on engine support for them.
|
|
6
|
+
export function convertGoRegexp(pattern) {
|
|
7
|
+
const source = String(pattern);
|
|
8
|
+
if (!/\(\?[ims-]|\[\^?\]|\(\?P<|\[:[a-z]+:\]/.test(source)) return source;
|
|
9
|
+
let i = 0;
|
|
10
|
+
function expression(inherited, active, nested = false, depth = 0) {
|
|
11
|
+
// Bound syntax-rule recursion, not the length of text being highlighted.
|
|
12
|
+
if (depth > 256) throw new RangeError('Go regexp nesting exceeds 256 groups');
|
|
13
|
+
const flags = new Set(inherited);
|
|
14
|
+
let output = '', wrapped = false;
|
|
15
|
+
function open() {
|
|
16
|
+
const on = 'ims'.split('').filter(f => flags.has(f) && !active.has(f)).join('');
|
|
17
|
+
const off = 'ims'.split('').filter(f => !flags.has(f) && active.has(f)).join('');
|
|
18
|
+
if (!on && !off) return;
|
|
19
|
+
output += `(?${on}${off ? '-' + off : ''}:`;
|
|
20
|
+
wrapped = true;
|
|
21
|
+
}
|
|
22
|
+
function close() { if (wrapped) output += ')'; wrapped = false; }
|
|
23
|
+
while (i < source.length) {
|
|
24
|
+
const ch = source[i];
|
|
25
|
+
if (ch === ')' && nested) { i++; close(); return output; }
|
|
26
|
+
if (ch === '|') { close(); output += '|'; i++; continue; }
|
|
27
|
+
const directive = ch === '(' && source.slice(i).match(/^\(\?([ims]*)(?:-([ims]+))?\)/);
|
|
28
|
+
if (directive && (directive[1] || directive[2])) {
|
|
29
|
+
close();
|
|
30
|
+
for (const f of directive[1]) flags.add(f);
|
|
31
|
+
for (const f of directive[2] || '') flags.delete(f);
|
|
32
|
+
i += directive[0].length;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!wrapped) open();
|
|
36
|
+
if (ch === '\\') { output += source.slice(i, i + 2); i += 2; continue; }
|
|
37
|
+
if (ch === '[') {
|
|
38
|
+
output += source[i++];
|
|
39
|
+
if (source[i] === '^') output += source[i++];
|
|
40
|
+
// Go permits a literal ] as the first member of a character class.
|
|
41
|
+
if (source[i] === ']') { output += '\\]'; i++; }
|
|
42
|
+
while (i < source.length) {
|
|
43
|
+
const posix = source.slice(i).match(/^\[:([a-z]+):\]/);
|
|
44
|
+
if (posix && POSIX_CLASSES[posix[1]] !== undefined) {
|
|
45
|
+
output += POSIX_CLASSES[posix[1]]; i += posix[0].length; continue;
|
|
46
|
+
}
|
|
47
|
+
const c = source[i++]; output += c;
|
|
48
|
+
if (c === '\\' && i < source.length) output += source[i++];
|
|
49
|
+
else if (c === ']') break;
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === '(') {
|
|
54
|
+
const scoped = source.slice(i).match(/^\(\?([ims]*)(?:-([ims]+))?:/);
|
|
55
|
+
const childFlags = new Set(flags);
|
|
56
|
+
if (scoped) {
|
|
57
|
+
for (const f of scoped[1]) childFlags.add(f);
|
|
58
|
+
for (const f of scoped[2] || '') childFlags.delete(f);
|
|
59
|
+
output += '(?:'; i += scoped[0].length;
|
|
60
|
+
} else {
|
|
61
|
+
const prefix = source.slice(i).match(/^\(\?(?:[:=!]|<[=!]|<[^>]+>|P<[^>]+>)/);
|
|
62
|
+
output += prefix ? prefix[0].replace('(?P<', '(?<') : '(';
|
|
63
|
+
i += prefix ? prefix[0].length : 1;
|
|
64
|
+
}
|
|
65
|
+
output += expression(childFlags, flags, true, depth + 1) + ')';
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
output += ch; i++;
|
|
69
|
+
}
|
|
70
|
+
close();
|
|
71
|
+
return output;
|
|
72
|
+
}
|
|
73
|
+
return expression(new Set(), new Set());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Class contents, not nested [...] expressions: e.g. [[:alnum:]._-].
|
|
77
|
+
const POSIX_CLASSES = {
|
|
78
|
+
alnum: 'A-Za-z0-9', alpha: 'A-Za-z', ascii: '\\x00-\\x7F',
|
|
79
|
+
blank: ' \\t', cntrl: '\\x00-\\x1F\\x7F', digit: '0-9',
|
|
80
|
+
graph: '!-~', lower: 'a-z', print: ' -~',
|
|
81
|
+
punct: '!-/:-@\\[-`{-~', space: '\\t\\n\\v\\f\\r ',
|
|
82
|
+
upper: 'A-Z', word: 'A-Za-z0-9_', xdigit: 'A-Fa-f0-9',
|
|
83
|
+
};
|
|
@@ -8,9 +8,7 @@ export class Highlighter {
|
|
|
8
8
|
highlightLine(line, state = null, progress = null) {
|
|
9
9
|
const text = String(line);
|
|
10
10
|
const changes = new Map([[0, state?.group ?? "default"]]);
|
|
11
|
-
const nextState = state
|
|
12
|
-
? scanRegion(changes, text, 0, state, this.rules, progress)
|
|
13
|
-
: scanTop(changes, text, 0, this.rules, progress);
|
|
11
|
+
const nextState = scanLine(changes, text, state, this.rules, progress);
|
|
14
12
|
return {
|
|
15
13
|
changes: normalizeChanges(changes, text.length),
|
|
16
14
|
state: nextState,
|
|
@@ -29,16 +27,18 @@ export class Highlighter {
|
|
|
29
27
|
}
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
export function resolveRules(rules, definitions, seen) {
|
|
30
|
+
export function resolveRules(rules, definitions, seen, ancestors = new Set()) {
|
|
31
|
+
if (ancestors.has(rules) || ancestors.size >= 128) return [];
|
|
32
|
+
const path = new Set(ancestors); path.add(rules);
|
|
33
33
|
const out = [];
|
|
34
34
|
for (const rule of rules) {
|
|
35
35
|
if (rule.type === "include") {
|
|
36
36
|
if (seen.has(rule.include)) continue;
|
|
37
37
|
seen.add(rule.include);
|
|
38
38
|
const def = definitions.find((candidate) => candidate.filetype === rule.include);
|
|
39
|
-
if (def) out.push(...resolveRules(def.rules, definitions, seen));
|
|
39
|
+
if (def) out.push(...resolveRules(def.rules, definitions, seen, path));
|
|
40
40
|
} else if (rule.type === "region") {
|
|
41
|
-
out.push({ ...rule, rules: resolveRules(rule.rules ?? [], definitions, new Set(seen)) });
|
|
41
|
+
out.push({ ...rule, rules: resolveRules(rule.rules ?? [], definitions, new Set(seen), path) });
|
|
42
42
|
} else {
|
|
43
43
|
out.push(rule);
|
|
44
44
|
}
|
|
@@ -53,67 +53,67 @@ export function flattenRules(rules, definitions, seen) {
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// Iterative state machine: adjacent tokens on a minified line must not consume
|
|
57
|
+
// call-stack depth. A state repeated at an unchanged position steps forward.
|
|
58
|
+
function scanLine(changes, line, region, rootRules, progress) {
|
|
59
|
+
let cursor = 0, previous = -1;
|
|
60
|
+
const visited = new Set();
|
|
61
|
+
const parents = new Set();
|
|
62
|
+
for (let r = region; r; r = r.parent) {
|
|
63
|
+
if (parents.has(r) || parents.size >= 128) { region = null; changes.set(0, "default"); break; }
|
|
64
|
+
parents.add(r);
|
|
65
|
+
}
|
|
58
66
|
while (cursor <= line.length) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
if (cursor !== previous) { visited.clear(); previous = cursor; }
|
|
68
|
+
if (visited.has(region)) {
|
|
69
|
+
// A zero-width child can end and reopen forever at the same position.
|
|
70
|
+
// Skip one character in the current region, like a zero-width match.
|
|
71
|
+
cursor = advanceIndex(line, cursor);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (visited.size >= 256) {
|
|
75
|
+
// Fresh object states from malformed externally supplied trees.
|
|
76
|
+
// Discard this line's unfinished region; do not poison subsequent lines.
|
|
77
|
+
changes.set(cursor, "default"); changes.set(line.length, "default");
|
|
65
78
|
return null;
|
|
66
79
|
}
|
|
67
|
-
|
|
68
|
-
applyPatterns(changes, line, cursor, regionStart.start, rules, "default", progress);
|
|
69
|
-
changes.set(regionStart.start, regionStart.region.limitGroup ?? "default");
|
|
70
|
-
changes.set(regionStart.end, regionStart.region.group);
|
|
71
|
-
progress?.(regionStart.end);
|
|
72
|
-
|
|
73
|
-
const state = scanRegion(changes, line, regionStart.end, regionStart.region, rules, progress);
|
|
74
|
-
if (state) return state;
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function scanRegion(changes, line, pos, region, rootRules, progress = null) {
|
|
81
|
-
let cursor = pos;
|
|
82
|
-
const rules = region.rules ?? [];
|
|
83
|
-
while (cursor <= line.length) {
|
|
80
|
+
visited.add(region);
|
|
84
81
|
progress?.(cursor);
|
|
85
|
-
const
|
|
82
|
+
const rules = region?.rules ?? rootRules;
|
|
83
|
+
const end = region ? findMatch(region.end, line, cursor, region.skip, progress) : null;
|
|
86
84
|
const nested = findFirstRegion(rules, line, cursor, progress);
|
|
87
|
-
|
|
88
85
|
if (end && (!nested || end.start <= nested.start)) {
|
|
89
86
|
applyPatterns(changes, line, cursor, end.start, rules, region.group, progress);
|
|
90
87
|
changes.set(end.start, region.limitGroup ?? region.group);
|
|
91
88
|
changes.set(end.end, parentGroup(region));
|
|
92
89
|
progress?.(end.end);
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
const parent = region.parent ?? null;
|
|
91
|
+
cursor = parent ? end.end : Math.max(end.end, advanceIndex(line, end.start));
|
|
92
|
+
region = parent;
|
|
93
|
+
continue;
|
|
95
94
|
}
|
|
96
|
-
|
|
97
95
|
if (nested) {
|
|
98
|
-
applyPatterns(changes, line, cursor, nested.start, rules, region
|
|
99
|
-
changes.set(nested.start, nested.region.limitGroup ?? region.group);
|
|
100
|
-
|
|
101
|
-
changes.set(nested.end,
|
|
96
|
+
applyPatterns(changes, line, cursor, nested.start, rules, region?.group ?? "default", progress);
|
|
97
|
+
changes.set(nested.start, nested.region.limitGroup ?? nested.region.group);
|
|
98
|
+
region = region ? { ...nested.region, parent: region } : nested.region;
|
|
99
|
+
changes.set(nested.end, region.group);
|
|
102
100
|
progress?.(nested.end);
|
|
103
|
-
|
|
104
|
-
if (state) return state;
|
|
105
|
-
cursor = Math.max(nested.end + 1, nextChangeAfter(changes, nested.end));
|
|
101
|
+
cursor = nested.end;
|
|
106
102
|
continue;
|
|
107
103
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
changes.set(line.length, region.group);
|
|
104
|
+
applyPatterns(changes, line, cursor, line.length, rules, region?.group ?? "default", progress);
|
|
105
|
+
changes.set(line.length, region?.group ?? "default");
|
|
111
106
|
progress?.(line.length);
|
|
112
107
|
return region;
|
|
113
108
|
}
|
|
114
109
|
return region;
|
|
115
110
|
}
|
|
116
111
|
|
|
112
|
+
// AdvanceStringIndex for Unicode regexps, including isolated surrogate units.
|
|
113
|
+
function advanceIndex(text, index) {
|
|
114
|
+
return index + (text.codePointAt(index) > 0xFFFF ? 2 : 1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
117
|
function applyPatterns(changes, line, start, end, rules, fallbackGroup, progress = null) {
|
|
118
118
|
if (end < start) return;
|
|
119
119
|
const length = end - start;
|
|
@@ -161,18 +161,12 @@ function findMatch(regex, line, pos, skip, progress = null) {
|
|
|
161
161
|
const global = globalize(regex);
|
|
162
162
|
global.lastIndex = pos;
|
|
163
163
|
let match;
|
|
164
|
-
if (!progress) {
|
|
165
|
-
match = global.exec(masked);
|
|
166
|
-
if (!match) return null;
|
|
167
|
-
const start = match.index ?? 0;
|
|
168
|
-
return { start, end: start + match[0].length };
|
|
169
|
-
}
|
|
170
164
|
while ((match = global.exec(masked)) !== null) {
|
|
171
165
|
const start = match.index ?? 0;
|
|
172
166
|
const end = start + match[0].length;
|
|
173
167
|
progress?.(start);
|
|
174
168
|
if (start >= pos) return { start, end };
|
|
175
|
-
|
|
169
|
+
global.lastIndex = Math.max(global.lastIndex, advanceIndex(masked, start));
|
|
176
170
|
}
|
|
177
171
|
progress?.(line.length);
|
|
178
172
|
return null;
|
|
@@ -189,7 +183,7 @@ function findAllMatches(regex, line, start, end, progress = null) {
|
|
|
189
183
|
if (matchStart >= end) break;
|
|
190
184
|
const matchEnd = matchStart + match[0].length;
|
|
191
185
|
if (match[0].length === 0) {
|
|
192
|
-
global.lastIndex
|
|
186
|
+
global.lastIndex = advanceIndex(line, matchStart);
|
|
193
187
|
continue;
|
|
194
188
|
}
|
|
195
189
|
if (matchEnd > start) out.push({ start: Math.max(matchStart, start), end: Math.min(matchEnd, end) });
|
|
@@ -202,7 +196,7 @@ function findAllMatches(regex, line, start, end, progress = null) {
|
|
|
202
196
|
if (matchStart >= end) break;
|
|
203
197
|
progress?.(matchStart);
|
|
204
198
|
if (match[0].length === 0) {
|
|
205
|
-
global.lastIndex
|
|
199
|
+
global.lastIndex = advanceIndex(line, matchStart);
|
|
206
200
|
continue;
|
|
207
201
|
}
|
|
208
202
|
if (matchEnd <= start || matchStart >= end) continue;
|
|
@@ -229,13 +223,6 @@ function parentGroup(region) {
|
|
|
229
223
|
return region.parent?.group ?? "default";
|
|
230
224
|
}
|
|
231
225
|
|
|
232
|
-
function nextChangeAfter(changes, index) {
|
|
233
|
-
for (const key of [...changes.keys()].sort((a, b) => a - b)) {
|
|
234
|
-
if (key > index) return key;
|
|
235
|
-
}
|
|
236
|
-
return index + 1;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
226
|
function globalize(regex) {
|
|
240
227
|
const flags = new Set(regex.flags.split(""));
|
|
241
228
|
flags.add("g");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
+
import { convertGoRegexp } from "./go-regexp.js";
|
|
2
3
|
|
|
3
4
|
export class SyntaxHeader {
|
|
4
5
|
constructor({ filetype = "", filename = "", header = "", signature = "" } = {}) {
|
|
@@ -195,15 +196,17 @@ function parseRule(rule) {
|
|
|
195
196
|
|
|
196
197
|
function compileRegex(pattern) {
|
|
197
198
|
if (!pattern) return null;
|
|
198
|
-
const translated = translateGoRegexp(pattern);
|
|
199
199
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
// Conversion may reject an excessively nested rule. Keep that failure
|
|
201
|
+
// local to this regexp rather than dropping the entire syntax definition.
|
|
202
|
+
const translated = convertGoRegexp(translateGoRegexp(pattern));
|
|
202
203
|
try {
|
|
203
|
-
return new RegExp(translated);
|
|
204
|
+
return new RegExp(translated, "u");
|
|
204
205
|
} catch {
|
|
205
|
-
return
|
|
206
|
+
return new RegExp(translated);
|
|
206
207
|
}
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
207
210
|
}
|
|
208
211
|
}
|
|
209
212
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {test, expect} from 'bun:test';
|
|
2
|
+
import {convertGoRegexp} from '../src/highlight/go-regexp.js';
|
|
3
|
+
import {SyntaxDefinition,parseHeaderYaml} from '../src/highlight/parser.js';
|
|
4
|
+
const regex = s => new RegExp(convertGoRegexp(s), 'u');
|
|
5
|
+
test('Go flags apply to the remaining alternatives, preserving precedence', () => {
|
|
6
|
+
const r = regex('^a(?i)b$|^c$');
|
|
7
|
+
expect(r.test('aB')).toBe(true);
|
|
8
|
+
expect(r.test('AB')).toBe(false);
|
|
9
|
+
expect(r.test('C')).toBe(true);
|
|
10
|
+
expect(r.test('aC')).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
test('flag changes stay inside groups and can be disabled', () => {
|
|
13
|
+
const r = regex('^(a(?i)b|c)d$');
|
|
14
|
+
expect(r.test('aBd')).toBe(true);
|
|
15
|
+
expect(r.test('Cd')).toBe(true);
|
|
16
|
+
expect(r.test('CD')).toBe(false);
|
|
17
|
+
expect(regex('^(?i)a(?-i)b$').test('Ab')).toBe(true);
|
|
18
|
+
expect(regex('^(?i)a(?-i)b$').test('AB')).toBe(false);
|
|
19
|
+
expect(regex('^(?i:a(?-i:b))c$').test('Abc')).toBe(true);
|
|
20
|
+
expect(regex('^(?i:a(?-i:b))c$').test('ABC')).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
test('capture numbers, scoped flags, escaped literals and class brackets survive', () => {
|
|
23
|
+
expect(regex('^(?i)(a)(b)$').exec('AB').slice(1)).toEqual(['A','B']);
|
|
24
|
+
expect(regex('(?i:a)+').test('AAA')).toBe(true);
|
|
25
|
+
expect(regex('\\(\\?i\\)').test('(?i)')).toBe(true);
|
|
26
|
+
const url = regex('(https?|ftp|ssh)://\\S*[^])>\\s,.]');
|
|
27
|
+
expect(url.exec('https://example.com)')[0]).toBe('https://example.com');
|
|
28
|
+
expect(regex('(?s)^a.b$').test('a\nb')).toBe(true);
|
|
29
|
+
expect(regex('(?m)^b$').test('a\nb\nc')).toBe(true);
|
|
30
|
+
expect(convertGoRegexp('\\bword\\b')).toBe('\\bword\\b');
|
|
31
|
+
});
|
|
32
|
+
test('all bundled syntax regexes compile, and Dockerfile keywords match', async () => {
|
|
33
|
+
const root = new URL('../runtime/syntax/', import.meta.url).pathname;
|
|
34
|
+
const failures = [];
|
|
35
|
+
function walk(rules, file) {
|
|
36
|
+
for (const r of rules) {
|
|
37
|
+
if (r.type === 'pattern' && !r.regex) failures.push(file+': '+r.source);
|
|
38
|
+
if (r.type === 'region') {
|
|
39
|
+
for (const k of ['start','end','skip']) if (r.source[k] && !r[k]) failures.push(file+': '+r.source[k]);
|
|
40
|
+
walk(r.rules,file);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for await (const file of new Bun.Glob('*.yaml').scan(root)) {
|
|
45
|
+
const source = Bun.YAML.parse(await Bun.file(root+'/'+file).text());
|
|
46
|
+
const header = parseHeaderYaml(source);
|
|
47
|
+
for (const [key,re] of [['filename','fileNameRegex'],['header','headerRegex'],['signature','signatureRegex']])
|
|
48
|
+
if (header[key] && !header[re]) failures.push(file+': '+key);
|
|
49
|
+
const def = new SyntaxDefinition(header,source);
|
|
50
|
+
walk(def.rules,file);
|
|
51
|
+
if (file === 'dockerfile.yaml') {
|
|
52
|
+
expect(header.matchFileName('/tmp/Dockerfile')).toBe(true);
|
|
53
|
+
const keyword = def.rules.find(r=>r.group==='type.keyword').regex;
|
|
54
|
+
for (const line of ['FROM debian','run echo hi','COPY . /app']) expect(keyword.test(line)).toBe(true);
|
|
55
|
+
expect(keyword.test('FROMAGE debian')).toBe(false);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
expect(failures).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
test('mixed and negated POSIX classes retain ASCII Go semantics',()=>{
|
|
61
|
+
expect(regex('^[[:alnum:].%_+-]+@[[:alnum:].-]+$').test('a.b+tag@example.com')).toBe(true);
|
|
62
|
+
expect(regex('^[^[:space:]]+:$').test('Subject:')).toBe(true);
|
|
63
|
+
expect(regex('^[^[:space:]]+:$').test('Bad Header:')).toBe(false);
|
|
64
|
+
expect(regex('^[{[:space:]]-block([[:space:]{}]|$)').test(' -block ')).toBe(true);
|
|
65
|
+
expect(regex('^[x[:space:]]$').test('\t')).toBe(true);
|
|
66
|
+
expect(regex('^[x[:space:]]$').test('\u00a0')).toBe(false);
|
|
67
|
+
expect(regex('^[x[:punct:]]$').test('[')).toBe(true);
|
|
68
|
+
expect(regex('^[x[:punct:]]$').test('A')).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
test('modifiers are emitted only where flags change',()=>{
|
|
71
|
+
expect(convertGoRegexp('[]a]')).toBe('[\\]a]');
|
|
72
|
+
expect(convertGoRegexp('(?P<x>a)')).toBe('(?<x>a)');
|
|
73
|
+
expect(convertGoRegexp('[[:alpha:]_]')).toBe('[A-Za-z_]');
|
|
74
|
+
expect(convertGoRegexp('(?i)\\b(select|from)\\b')).toBe('(?i:\\b(select|from)\\b)');
|
|
75
|
+
expect(regex('^(?i)a(?-i:b)$').test('Ab')).toBe(true);
|
|
76
|
+
expect(regex('^(?i)a(?-i:b)$').test('AB')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('converter bounds nested groups without limiting adjacent groups',()=>{
|
|
80
|
+
expect(()=>convertGoRegexp('(?i)'+'('.repeat(50000)+'x'+')'.repeat(50000))).toThrow('nesting exceeds');
|
|
81
|
+
expect(regex('(?i)'+'('.repeat(256)+'x'+')'.repeat(256)).test('X')).toBe(true);
|
|
82
|
+
expect(()=>convertGoRegexp('(?i)'+'(x)'.repeat(50000))).not.toThrow();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('conversion failure disables only its rule and preserves the loaded syntax',async()=>{
|
|
86
|
+
const {loadSyntaxDefinitions}=await import('../src/highlight/parser.js');
|
|
87
|
+
const {Highlighter}=await import('../src/highlight/highlighter.js');
|
|
88
|
+
const deep='(?i)'+'('.repeat(50000)+'x'+')'.repeat(50000);
|
|
89
|
+
const source={filetype:'guarded',detect:{filename:'\\.guarded$'},rules:[{bad:deep},{keyword:'(?i)hello'},{invalid:'['}]};
|
|
90
|
+
const def=new SyntaxDefinition(parseHeaderYaml(source),source);
|
|
91
|
+
expect(def.rules[0].regex).toBeNull();expect(def.rules[2].regex).toBeNull();
|
|
92
|
+
expect(def.rules[1].regex.test('HELLO')).toBe(true);
|
|
93
|
+
const runtime={list:type=>type===1?[{name:'guarded',text:async()=>JSON.stringify(source)}]:[]};
|
|
94
|
+
const defs=await loadSyntaxDefinitions(runtime);
|
|
95
|
+
expect(defs.map(d=>d.filetype)).toEqual(['guarded']);
|
|
96
|
+
expect(defs[0].header.matchFileName('example.guarded')).toBe(true);
|
|
97
|
+
expect(new Highlighter(defs[0],defs).highlightLine('HELLO').changes.get(0)).toBe('keyword');
|
|
98
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
|
+
import { escapesSlashTags } from "./markdown-behavior.mjs";
|
|
2
3
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -347,23 +348,27 @@ test("raw heading markers do not change quoted attribute parsing", () => {
|
|
|
347
348
|
expect(rendered.html).toContain('<h1 id="中文-1">中文</h1>');
|
|
348
349
|
});
|
|
349
350
|
|
|
350
|
-
test("raw heading markers
|
|
351
|
-
const markdown =
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
`;
|
|
355
|
-
expect(collectRawHtmlHeadingDeclarations(markdown)).toEqual([
|
|
356
|
-
{
|
|
357
|
-
id: "中文",
|
|
358
|
-
kind: "raw HTML heading",
|
|
359
|
-
line: 1,
|
|
360
|
-
source: "<h1/id=中文>Raw</h1>",
|
|
361
|
-
},
|
|
351
|
+
test("raw heading markers follow Bun's slash-separated HTML behavior", () => {
|
|
352
|
+
const markdown = "<h1/id=中文>Raw</h1>\n\n# 中文\n";
|
|
353
|
+
expect(collectRawHtmlHeadingDeclarations(markdown)).toEqual(escapesSlashTags ? [] : [
|
|
354
|
+
{ id: "中文", kind: "raw HTML heading", line: 1, source: "<h1/id=中文>Raw</h1>" },
|
|
362
355
|
]);
|
|
363
356
|
const rendered = renderMarkdownWithHeadingIds(markdown);
|
|
364
|
-
|
|
365
|
-
expect(rendered.
|
|
366
|
-
expect(rendered.
|
|
357
|
+
const id = escapesSlashTags ? "中文" : "中文-1";
|
|
358
|
+
expect(rendered.normalized).toBe(true);
|
|
359
|
+
expect(rendered.headings.map(heading => heading.id)).toEqual([id]);
|
|
360
|
+
expect(rendered.html).toContain(escapesSlashTags
|
|
361
|
+
? "<p><h1/id=中文>Raw</h1></p>"
|
|
362
|
+
: "<h1/id=中文>Raw</h1>");
|
|
363
|
+
expect(rendered.html).toContain(`<h1 id="${id}">中文</h1>`);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("slash-tag ambiguity follows Bun's actual Markdown output", () => {
|
|
367
|
+
const rendered = renderMarkdownWithHeadingIds('<div/a< x=">TAIL\n\n# 中文\n');
|
|
368
|
+
expect(rendered.normalized).toBe(escapesSlashTags);
|
|
369
|
+
expect(rendered.headings.map(heading => heading.id)).toEqual(escapesSlashTags ? ["中文"] : []);
|
|
370
|
+
expect(rendered.html).toContain(escapesSlashTags
|
|
371
|
+
? '<h1 id="中文">中文</h1>' : '<h1 id="">中文</h1>');
|
|
367
372
|
});
|
|
368
373
|
|
|
369
374
|
test("self-closing inline headings cannot suppress other raw collisions", () => {
|
|
@@ -511,7 +516,7 @@ test("quoted tag delimiters preserve raw IDs and Markdown heading metadata", ()
|
|
|
511
516
|
});
|
|
512
517
|
|
|
513
518
|
test("browser alignment failures never fall back to lexical heading rewrites", () => {
|
|
514
|
-
const markdown = '<div
|
|
519
|
+
const markdown = '<div x=">TAIL\n\n# 中文\n';
|
|
515
520
|
const rendered = renderMarkdownWithHeadingIds(markdown);
|
|
516
521
|
expect(rendered.normalized).toBe(false);
|
|
517
522
|
expect(rendered.headings).toEqual([]);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {SyntaxDefinition,parseHeaderYaml} from '../src/highlight/parser.js';
|
|
3
|
+
import {Highlighter} from '../src/highlight/highlighter.js';
|
|
4
|
+
async function highlighter(name){
|
|
5
|
+
const source=Bun.YAML.parse(await Bun.file(new URL('../runtime/syntax/'+name+'.yaml',import.meta.url)).text());
|
|
6
|
+
return new Highlighter(new SyntaxDefinition(parseHeaderYaml(source),source));
|
|
7
|
+
}
|
|
8
|
+
function groupAt(result,index){let group='default';for(const [pos,value]of result.changes){if(pos>index)break;group=value;}return group;}
|
|
9
|
+
for(const [syntax,first]of [['html','<div class="hello">Hello</div>'],['xml','<Project Sdk="Microsoft.NET.Sdk">']]){
|
|
10
|
+
test(syntax+' nested attribute returns to outer tag and does not leak to next line',async()=>{
|
|
11
|
+
const h=await highlighter(syntax),r=h.highlightLine(first);
|
|
12
|
+
expect(r.state).toBeNull();
|
|
13
|
+
expect(groupAt(r,first.indexOf('>'))).toBe('symbol.tag');
|
|
14
|
+
const next=h.highlightLine('<p>Next line</p>',r.state);
|
|
15
|
+
expect(groupAt(next,3)).toBe('default');
|
|
16
|
+
expect(next.state).toBeNull();
|
|
17
|
+
expect([...r.changes.keys()].every(p=>p<=first.length)).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
test(syntax+' retains an unfinished attribute across lines then exits it',async()=>{
|
|
20
|
+
const h=await highlighter(syntax);
|
|
21
|
+
const a=h.highlightLine('<div title="first');expect(a.state?.group).toBe('constant.string');
|
|
22
|
+
const b=h.highlightLine('second">Text</div>',a.state);
|
|
23
|
+
expect(groupAt(b,0)).toBe('constant.string');
|
|
24
|
+
expect(groupAt(b,8)).toBe('default');expect(b.state).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
test('HTML multiple attributes, adjacent tags and nested escaped quotes',async()=>{
|
|
28
|
+
const h=await highlighter('html');
|
|
29
|
+
const line='<a href="/path" title="say \\"hi\\"">Link</a><b>next</b>';
|
|
30
|
+
const r=h.highlightLine(line);
|
|
31
|
+
expect(r.state).toBeNull();expect(groupAt(r,line.indexOf('Link'))).toBe('default');
|
|
32
|
+
expect(groupAt(r,line.indexOf('next'))).toBe('default');
|
|
33
|
+
});
|
|
34
|
+
test('zero-width child/parent loop steps forward and keeps highlighting the line',()=>{
|
|
35
|
+
const h=new Highlighter({rules:[{type:'region',group:'outer',start:/</u,end:/>/u,rules:[{type:'region',group:'inner',start:/(?=x)/u,end:/(?=x)/u,rules:[]}]},{type:'pattern',group:'keyword',regex:/END/u}]});
|
|
36
|
+
let calls=0;const line='<x>END';const r=h.highlightLine(line,null,()=>{if(++calls>100)throw Error('loop');});
|
|
37
|
+
expect(r.state).toBeNull();expect(calls).toBeLessThan(100);
|
|
38
|
+
expect(groupAt(r,1)).toBe('outer');expect(groupAt(r,line.indexOf('END'))).toBe('keyword');
|
|
39
|
+
});
|
|
40
|
+
test('empty pattern matches advance past full Unicode characters, with or without progress',()=>{
|
|
41
|
+
const h=new Highlighter({rules:[{type:'pattern',group:'empty',regex:/(?=.)/u},{type:'pattern',group:'number',regex:/42/u}]});
|
|
42
|
+
for(const progress of [null,()=>{}]){
|
|
43
|
+
const r=h.highlightLine('😀\0\ud80042',null,progress);
|
|
44
|
+
expect(groupAt(r,4)).toBe('number');expect(r.state).toBeNull();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
test('NUL and isolated surrogate units inside HTML do not leak state',async()=>{
|
|
48
|
+
const h=await highlighter('html');
|
|
49
|
+
const r=h.highlightLine('<a title="\0😀\ud800">hello\0\udc00</a>');
|
|
50
|
+
expect(r.state).toBeNull();
|
|
51
|
+
expect(h.highlightLine('\0'.repeat(10000)).state).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
test('long lines finish at their final token without recursion or truncation',()=>{
|
|
54
|
+
const h=new Highlighter({rules:[{type:'region',group:'string',start:/"/u,end:/"/u,rules:[]},{type:'pattern',group:'keyword',regex:/END/u}]});
|
|
55
|
+
const line='"x";'.repeat(50000)+'END';
|
|
56
|
+
const r=h.highlightLine(line);
|
|
57
|
+
expect(r.state).toBeNull();expect(groupAt(r,line.length-1)).toBe('keyword');
|
|
58
|
+
expect([...r.changes.keys()].at(-1)).toBe(line.length);
|
|
59
|
+
},30000);
|
|
60
|
+
test('cyclic incoming state and cyclic syntax trees cannot recurse forever',()=>{
|
|
61
|
+
const region={type:'region',group:'cycle',start:/</u,end:/>/u,rules:[]};region.rules.push(region);
|
|
62
|
+
const h=new Highlighter({rules:[region]});
|
|
63
|
+
expect(h.highlightLine('plain').state).toBeNull();
|
|
64
|
+
const state={group:'cycle',rules:[]};state.parent=state;
|
|
65
|
+
expect(h.highlightLine('plain',state).state).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
test('externally supplied zero-width recursive state has a no-progress bound',()=>{
|
|
68
|
+
const child={type:'region',group:'inner',start:/(?=x)/u,end:/$/u,rules:[]};child.rules.push(child);
|
|
69
|
+
const state={group:'outer',end:/$/u,rules:[child]};
|
|
70
|
+
const h=new Highlighter({rules:[]});let calls=0;
|
|
71
|
+
const r=h.highlightLine('x',state,()=>{if(++calls>5000)throw Error('loop');});
|
|
72
|
+
expect(r.state).toBeNull();expect(calls).toBeLessThan(5000);
|
|
73
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
|
+
import { escapesSlashTags } from "./markdown-behavior.mjs";
|
|
2
3
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -39,7 +40,7 @@ test("--check exits 0 and reports unique IDs", async () => {
|
|
|
39
40
|
});
|
|
40
41
|
|
|
41
42
|
test("--check fails closed when browser heading parsing is ambiguous", async () => {
|
|
42
|
-
const result = await runCheck('<div
|
|
43
|
+
const result = await runCheck('<div x=">TAIL\n\n# 中文\n');
|
|
43
44
|
expect(result.exitCode).toBe(2);
|
|
44
45
|
expect(result.stderr.toString()).toContain(
|
|
45
46
|
"Markdown headings could not be classified safely",
|
|
@@ -359,15 +360,37 @@ test("--check ignores heading-looking text inside raw attributes", async () => {
|
|
|
359
360
|
expect(output).toContain("FAILED");
|
|
360
361
|
});
|
|
361
362
|
|
|
362
|
-
test("--check
|
|
363
|
+
test("--check follows Bun's slash-separated raw heading behavior", async () => {
|
|
363
364
|
const result = await runCheck("<h1/id=中文>Raw</h1>\n\n# 中文\n");
|
|
364
365
|
const output = Bun.stripANSI(result.stdout.toString());
|
|
365
|
-
expect(result.exitCode).toBe(1);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
366
|
+
expect(result.exitCode).toBe(escapesSlashTags ? 0 : 1);
|
|
367
|
+
if (escapesSlashTags) {
|
|
368
|
+
expect(output).toContain("Selectable IDs: 1");
|
|
369
|
+
expect(output).toContain("PASSED");
|
|
370
|
+
expect(output).not.toContain("Type: raw HTML heading");
|
|
371
|
+
} else {
|
|
372
|
+
expect(output).toContain("ID #中文");
|
|
373
|
+
expect(output).toContain("Type: raw HTML heading");
|
|
374
|
+
expect(output).toContain("Type: heading");
|
|
375
|
+
expect(output).toContain("FAILED");
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
for (const [command, run] of [["--check", runCheck], ["--outline", runOutline]]) {
|
|
380
|
+
test(`${command} follows Bun's slash-tag ambiguity behavior`, async () => {
|
|
381
|
+
const result = await run('<div/a< x=">TAIL\n\n# 中文\n');
|
|
382
|
+
expect(result.exitCode).toBe(escapesSlashTags ? 0 : 2);
|
|
383
|
+
if (escapesSlashTags) {
|
|
384
|
+
expect(result.stderr.toString()).toBe("");
|
|
385
|
+
if (command === "--check") expect(result.stdout.toString()).toContain("PASSED");
|
|
386
|
+
else expect(result.stdout.toString()).toContain("中文");
|
|
387
|
+
} else {
|
|
388
|
+
expect(result.stderr.toString()).toContain("Markdown headings could not be classified safely");
|
|
389
|
+
expect(result.stdout.toString()).not.toContain("PASSED");
|
|
390
|
+
if (command === "--outline") expect(result.stdout.toString()).toBe("");
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
371
394
|
|
|
372
395
|
test("--check ignores heading-like custom tag names", async () => {
|
|
373
396
|
const result = await runCheck(
|
|
@@ -494,7 +517,7 @@ value
|
|
|
494
517
|
});
|
|
495
518
|
|
|
496
519
|
test("--outline fails closed when browser heading parsing is ambiguous", async () => {
|
|
497
|
-
const result = await runOutline('<div
|
|
520
|
+
const result = await runOutline('<div x=">TAIL\n\n# 中文\n');
|
|
498
521
|
expect(result.exitCode).toBe(2);
|
|
499
522
|
expect(result.stderr.toString()).toContain(
|
|
500
523
|
"Markdown headings could not be classified safely",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Probe the parser, not Bun.version: canaries and backports can differ.
|
|
2
|
+
// CommonMark allows `/` after a block tag name only as part of `/>`.
|
|
3
|
+
const probe = Bun.markdown.html("<h1/id=x>Raw</h1>\n");
|
|
4
|
+
const raw = "<h1/id=x>Raw</h1>\n";
|
|
5
|
+
const escaped = "<p><h1/id=x>Raw</h1></p>\n";
|
|
6
|
+
if (probe !== raw && probe !== escaped) {
|
|
7
|
+
throw new Error(`Unexpected Bun Markdown slash-tag behavior: ${JSON.stringify(probe)}`);
|
|
8
|
+
}
|
|
9
|
+
export const escapesSlashTags = probe === escaped;
|