@otto-code/highlight 0.5.7 → 0.5.8
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/dist/detect.d.ts +2 -0
- package/dist/detect.js +159 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/parsers.js +11 -0
- package/package.json +1 -1
package/dist/detect.d.ts
ADDED
package/dist/detect.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Lightweight, dependency-free language guesser for code fences that arrive
|
|
2
|
+
// without an info string. Agents emit bare ``` blocks constantly, so this
|
|
3
|
+
// recovers highlighting for them — but it is deliberately conservative: when
|
|
4
|
+
// the signals are weak or two languages tie, it returns null and the caller
|
|
5
|
+
// renders plain monospace rather than confidently mis-coloring the block.
|
|
6
|
+
//
|
|
7
|
+
// Every returned extension is a key in parsers.ts's table, so the result plugs
|
|
8
|
+
// straight into highlightCode(`x.${ext}`).
|
|
9
|
+
// Only look at the head of large blocks — the language is obvious well before
|
|
10
|
+
// 4k chars, and detection runs on every streamed chunk.
|
|
11
|
+
const SAMPLE_LIMIT = 4000;
|
|
12
|
+
// A guess only wins if it clears this score AND beats the runner-up by the
|
|
13
|
+
// margin below. Both guard against near-ties on shared signals (import,
|
|
14
|
+
// braces, colons) turning into coin-flip highlighting.
|
|
15
|
+
const MIN_SCORE = 4;
|
|
16
|
+
const MIN_MARGIN = 2;
|
|
17
|
+
// Ordered loosely by signal strength. `m` (multiline) anchors keep matches to
|
|
18
|
+
// line starts where it matters; `i` is used only where a language's keywords
|
|
19
|
+
// are conventionally case-insensitive (SQL).
|
|
20
|
+
const RULES = [
|
|
21
|
+
// Shell
|
|
22
|
+
{
|
|
23
|
+
ext: "sh",
|
|
24
|
+
re: /^\s*(sudo|apt|apt-get|yum|dnf|brew|npm|npx|yarn|pnpm|pip3?|git|cd|ls|cat|echo|grep|sed|awk|curl|wget|chmod|chown|mkdir|rm|cp|mv|tar|ssh|docker|kubectl|make)\b/m,
|
|
25
|
+
weight: 3,
|
|
26
|
+
},
|
|
27
|
+
{ ext: "sh", re: /\$\{?\w+\}?/, weight: 1 },
|
|
28
|
+
{ ext: "sh", re: /^\s*(if|then|fi|for|do|done|while|case|esac|function)\b.*;?\s*$/m, weight: 1 },
|
|
29
|
+
{ ext: "sh", re: /\|\s*(grep|awk|sed|xargs|head|tail|sort|uniq|wc)\b/, weight: 2 },
|
|
30
|
+
{ ext: "sh", re: /^\s*export\s+\w+=/m, weight: 2 },
|
|
31
|
+
// SQL — keyword-insensitive; combos are what make it unambiguous.
|
|
32
|
+
{ ext: "sql", re: /\bSELECT\b[\s\S]*\bFROM\b/i, weight: 4 },
|
|
33
|
+
{ ext: "sql", re: /\bINSERT\s+INTO\b/i, weight: 4 },
|
|
34
|
+
{ ext: "sql", re: /\bUPDATE\b[\s\S]*\bSET\b/i, weight: 4 },
|
|
35
|
+
{ ext: "sql", re: /\bDELETE\s+FROM\b/i, weight: 4 },
|
|
36
|
+
{ ext: "sql", re: /\bCREATE\s+(TABLE|INDEX|VIEW|DATABASE)\b/i, weight: 4 },
|
|
37
|
+
{ ext: "sql", re: /\b(ALTER|DROP)\s+TABLE\b/i, weight: 3 },
|
|
38
|
+
{ ext: "sql", re: /\b(INNER|LEFT|RIGHT|OUTER)\s+JOIN\b/i, weight: 2 },
|
|
39
|
+
// Python
|
|
40
|
+
{ ext: "py", re: /^\s*def\s+\w+\s*\(/m, weight: 3 },
|
|
41
|
+
{ ext: "py", re: /^\s*(from\s+[\w.]+\s+)?import\s+\w/m, weight: 2 },
|
|
42
|
+
{ ext: "py", re: /^\s*class\s+\w+[\w(),\s]*:\s*$/m, weight: 3 },
|
|
43
|
+
{ ext: "py", re: /\belif\b/, weight: 3 },
|
|
44
|
+
{ ext: "py", re: /\bself\b/, weight: 1 },
|
|
45
|
+
{ ext: "py", re: /\bprint\s*\(/, weight: 1 },
|
|
46
|
+
{ ext: "py", re: /\b(True|False|None)\b/, weight: 1 },
|
|
47
|
+
{ ext: "py", re: /^\s*@\w+/m, weight: 1 },
|
|
48
|
+
// JavaScript / TypeScript (folded to ts — its parser handles plain JS)
|
|
49
|
+
{ ext: "ts", re: /\b(const|let)\s+\w+\s*=/, weight: 2 },
|
|
50
|
+
{ ext: "ts", re: /\bfunction\s*\*?\s*\w*\s*\(/, weight: 2 },
|
|
51
|
+
{ ext: "ts", re: /=>\s*[{([]/, weight: 2 },
|
|
52
|
+
{ ext: "ts", re: /\bimport\s+[\s\S]*?\bfrom\s+['"]/, weight: 3 },
|
|
53
|
+
{ ext: "ts", re: /\bexport\s+(default|const|function|class|async)\b/, weight: 2 },
|
|
54
|
+
{ ext: "ts", re: /\bconsole\.(log|error|warn|info)\b/, weight: 2 },
|
|
55
|
+
{ ext: "ts", re: /\binterface\s+\w+\s*\{/, weight: 3 },
|
|
56
|
+
{ ext: "ts", re: /:\s*(string|number|boolean|void|any|unknown|Promise<)/, weight: 2 },
|
|
57
|
+
// Go
|
|
58
|
+
{ ext: "go", re: /^\s*package\s+\w+/m, weight: 4 },
|
|
59
|
+
{ ext: "go", re: /^\s*func\s+(\(\w+\s+\*?\w+\)\s+)?\w+\s*\(/m, weight: 3 },
|
|
60
|
+
{ ext: "go", re: /:=/, weight: 2 },
|
|
61
|
+
{ ext: "go", re: /\bfmt\.\w+\(/, weight: 3 },
|
|
62
|
+
// Rust
|
|
63
|
+
{ ext: "rs", re: /^\s*fn\s+\w+/m, weight: 3 },
|
|
64
|
+
{ ext: "rs", re: /\blet\s+mut\b/, weight: 3 },
|
|
65
|
+
{ ext: "rs", re: /\bprintln!\s*\(/, weight: 3 },
|
|
66
|
+
{ ext: "rs", re: /\b(impl|pub\s+fn|use\s+\w+::|match\s+\w+\s*\{)/, weight: 2 },
|
|
67
|
+
{ ext: "rs", re: /->\s*[\w<&]/, weight: 1 },
|
|
68
|
+
// Java
|
|
69
|
+
{ ext: "java", re: /\bpublic\s+static\s+void\s+main\b/, weight: 5 },
|
|
70
|
+
{ ext: "java", re: /\bSystem\.out\.print(ln)?\b/, weight: 4 },
|
|
71
|
+
{
|
|
72
|
+
ext: "java",
|
|
73
|
+
re: /\b(public|private|protected)\s+(static\s+|final\s+)*(class|void|int|String|boolean)\b/,
|
|
74
|
+
weight: 3,
|
|
75
|
+
},
|
|
76
|
+
{ ext: "java", re: /^\s*import\s+[\w.]+;\s*$/m, weight: 2 },
|
|
77
|
+
// C / C++
|
|
78
|
+
{ ext: "cpp", re: /^\s*#include\s*[<"]/m, weight: 4 },
|
|
79
|
+
{ ext: "cpp", re: /\bstd::\w+/, weight: 3 },
|
|
80
|
+
{ ext: "cpp", re: /\bint\s+main\s*\(/, weight: 2 },
|
|
81
|
+
{ ext: "cpp", re: /\bprintf\s*\(/, weight: 1 },
|
|
82
|
+
// PHP
|
|
83
|
+
{ ext: "php", re: /<\?php/, weight: 6 },
|
|
84
|
+
{ ext: "php", re: /\$\w+\s*=/, weight: 1 },
|
|
85
|
+
{ ext: "php", re: /\becho\s+['"$]/, weight: 1 },
|
|
86
|
+
// HTML
|
|
87
|
+
{
|
|
88
|
+
ext: "html",
|
|
89
|
+
re: /<(!DOCTYPE|html|head|body|div|span|section|article|nav|header|footer|main|script|style|img|input|button|form|table|ul|ol|li|h[1-6])\b/i,
|
|
90
|
+
weight: 3,
|
|
91
|
+
},
|
|
92
|
+
{ ext: "html", re: /<\/(div|span|body|html|p|a|section|ul|li|table)>/i, weight: 2 },
|
|
93
|
+
// CSS
|
|
94
|
+
{ ext: "css", re: /[.#]?[\w-]+\s*\{[^}]*[\w-]+\s*:[^}]*;/, weight: 3 },
|
|
95
|
+
{ ext: "css", re: /@(media|import|keyframes|font-face|supports)\b/, weight: 3 },
|
|
96
|
+
{ ext: "css", re: /^\s*[\w-]+\s*:\s*[^;{]+;\s*$/m, weight: 1 },
|
|
97
|
+
// YAML
|
|
98
|
+
{ ext: "yaml", re: /^---\s*$/m, weight: 3 },
|
|
99
|
+
{ ext: "yaml", re: /^\s*-\s+\w/m, weight: 1 },
|
|
100
|
+
{ ext: "yaml", re: /^\s*[\w-]+:\s+\S/m, weight: 1 },
|
|
101
|
+
];
|
|
102
|
+
function scoreRules(sample) {
|
|
103
|
+
const scores = new Map();
|
|
104
|
+
for (const { ext, re, weight } of RULES) {
|
|
105
|
+
if (re.test(sample))
|
|
106
|
+
scores.set(ext, (scores.get(ext) ?? 0) + weight);
|
|
107
|
+
}
|
|
108
|
+
return scores;
|
|
109
|
+
}
|
|
110
|
+
// Best-effort guess of the language extension for a code block, or null when
|
|
111
|
+
// there isn't enough signal to be confident.
|
|
112
|
+
export function detectLanguage(code) {
|
|
113
|
+
const sample = code.length > SAMPLE_LIMIT ? code.slice(0, SAMPLE_LIMIT) : code;
|
|
114
|
+
const trimmed = sample.trim();
|
|
115
|
+
if (trimmed.length < 3)
|
|
116
|
+
return null;
|
|
117
|
+
// Shebang — the single strongest signal, short-circuit on it.
|
|
118
|
+
if (trimmed.startsWith("#!")) {
|
|
119
|
+
const firstLine = trimmed.slice(0, trimmed.indexOf("\n") === -1 ? undefined : trimmed.indexOf("\n"));
|
|
120
|
+
if (/\b(bash|zsh|ksh|sh)\b/.test(firstLine))
|
|
121
|
+
return "sh";
|
|
122
|
+
if (/\bpython[0-9.]*\b/.test(firstLine))
|
|
123
|
+
return "py";
|
|
124
|
+
if (/\bnode\b/.test(firstLine))
|
|
125
|
+
return "ts";
|
|
126
|
+
}
|
|
127
|
+
// JSON — structural and cheaply verifiable, so a successful parse is decisive.
|
|
128
|
+
if (/^[[{]/.test(trimmed) && /[\]}]$/.test(trimmed)) {
|
|
129
|
+
try {
|
|
130
|
+
JSON.parse(trimmed);
|
|
131
|
+
return "json";
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Not valid JSON — likely a JS object literal or a fragment; keep scoring.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const scores = scoreRules(sample);
|
|
138
|
+
if (scores.size === 0)
|
|
139
|
+
return null;
|
|
140
|
+
let bestExt = null;
|
|
141
|
+
let bestScore = 0;
|
|
142
|
+
let runnerUp = 0;
|
|
143
|
+
for (const [ext, score] of scores) {
|
|
144
|
+
if (score > bestScore) {
|
|
145
|
+
runnerUp = bestScore;
|
|
146
|
+
bestScore = score;
|
|
147
|
+
bestExt = ext;
|
|
148
|
+
}
|
|
149
|
+
else if (score > runnerUp) {
|
|
150
|
+
runnerUp = score;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (bestScore < MIN_SCORE)
|
|
154
|
+
return null;
|
|
155
|
+
if (bestScore - runnerUp < MIN_MARGIN)
|
|
156
|
+
return null;
|
|
157
|
+
return bestExt;
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=detect.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type { HighlightStyle, HighlightToken } from "./types.js";
|
|
2
2
|
export { getParserForFile, isLanguageSupported, getSupportedExtensions } from "./parsers.js";
|
|
3
3
|
export { highlightCode, highlightLine } from "./highlighter.js";
|
|
4
|
+
export { detectLanguage } from "./detect.js";
|
|
4
5
|
export { extractSymbols } from "./symbols.js";
|
|
5
6
|
export type { CodeSymbol, SymbolKind } from "./symbols.js";
|
|
6
7
|
export { darkHighlightColors, lightHighlightColors } from "./colors.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { getParserForFile, isLanguageSupported, getSupportedExtensions } from "./parsers.js";
|
|
2
2
|
export { highlightCode, highlightLine } from "./highlighter.js";
|
|
3
|
+
export { detectLanguage } from "./detect.js";
|
|
3
4
|
export { extractSymbols } from "./symbols.js";
|
|
4
5
|
export { darkHighlightColors, lightHighlightColors } from "./colors.js";
|
|
5
6
|
export { SYNTAX_THEME_IDS, SYNTAX_THEME_OPTIONS, isSyntaxThemeId, resolveSyntaxColors, } from "./themes.js";
|
package/dist/parsers.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { StreamLanguage } from "@codemirror/language";
|
|
2
2
|
import { dart } from "@codemirror/legacy-modes/mode/clike";
|
|
3
3
|
import { swift } from "@codemirror/legacy-modes/mode/swift";
|
|
4
|
+
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
|
5
|
+
import { standardSQL } from "@codemirror/legacy-modes/mode/sql";
|
|
4
6
|
import { parser as jsParser } from "@lezer/javascript";
|
|
5
7
|
import { parser as jsonParser } from "@lezer/json";
|
|
6
8
|
import { parser as cssParser } from "@lezer/css";
|
|
@@ -16,6 +18,8 @@ import { parser as xmlParser } from "@lezer/xml";
|
|
|
16
18
|
import { parser as yamlParser } from "@lezer/yaml";
|
|
17
19
|
import { csharpLanguage } from "@replit/codemirror-lang-csharp";
|
|
18
20
|
import { parser as elixirParser } from "lezer-elixir";
|
|
21
|
+
// Shared instance so the four shell fence aliases don't each build a grammar.
|
|
22
|
+
const shellParser = StreamLanguage.define(shell).parser;
|
|
19
23
|
const parsersByExtension = {
|
|
20
24
|
// JavaScript/TypeScript
|
|
21
25
|
js: jsParser,
|
|
@@ -61,6 +65,13 @@ const parsersByExtension = {
|
|
|
61
65
|
swift: StreamLanguage.define(swift).parser,
|
|
62
66
|
// Dart
|
|
63
67
|
dart: StreamLanguage.define(dart).parser,
|
|
68
|
+
// Shell (fence tags bash/sh/zsh/shell all map here)
|
|
69
|
+
sh: shellParser,
|
|
70
|
+
bash: shellParser,
|
|
71
|
+
zsh: shellParser,
|
|
72
|
+
shell: shellParser,
|
|
73
|
+
// SQL
|
|
74
|
+
sql: StreamLanguage.define(standardSQL).parser,
|
|
64
75
|
// C#
|
|
65
76
|
cs: csharpLanguage.parser,
|
|
66
77
|
// Elixir
|