Skip to content

Commit b46ed3e

Browse files
committed
fix(langs): generate syntax highlighting from file extensions.
1 parent 98acfd8 commit b46ed3e

File tree

10 files changed

+552
-216
lines changed

10 files changed

+552
-216
lines changed

extensions/langs/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ loadLanguage('tsx');
5555

5656
langs.tsx();
5757

58-
console.log('langNames:', langNames); // => "jsx" | "typescript" | "javascript" | "tsx"
58+
console.log('langNames:', langNames); // => "jsx" | "ts" | "js" | "tsx"
5959
```
6060

6161
## Usage
@@ -120,6 +120,7 @@ export default function App() {
120120
- ~~`@codemirror/legacy-modes/mode/cpp`~~ => [`@codemirror/lang-cpp`](https://www.npmjs.com/package/@codemirror/lang-cpp)
121121
- ~~`@codemirror/legacy-modes/mode/html`~~ => [`@codemirror/lang-html`](https://www.npmjs.com/package/@codemirror/lang-html)
122122
- ~~`@codemirror/legacy-modes/mode/java`~~ => [`@codemirror/lang-java`](https://www.npmjs.com/package/@codemirror/lang-java)
123+
- ~~`@codemirror/legacy-modes/mode/go`~~ => [`@codemirror/lang-go`](https://www.npmjs.com/package/@codemirror/lang-go)
123124
- ~~`@codemirror/legacy-modes/mode/javascript`~~ => [`@codemirror/lang-javascript`](https://www.npmjs.com/package/@codemirror/lang-javascript)
124125
- ~~`@codemirror/legacy-modes/mode/json`~~ => [`@codemirror/lang-json`](https://www.npmjs.com/package/@codemirror/lang-json)
125126
- ~~`@codemirror/legacy-modes/mode/lezer`~~ => [`@codemirror/lang-lezer`](https://www.npmjs.com/package/@codemirror/lang-lezer)

extensions/langs/gen-langs-map.cjs

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
// gen-langs-map.cjs
2+
// 将 CodeMirror 的 languages 配置(含动态 import/legacy/sql 包装)
3+
// 生成一个静态打包用的映射:扩展名 -> 对应加载函数
4+
// 运行:node gen-langs-map.cjs input.js > langs.generated.ts
5+
6+
const fs = require('fs');
7+
const path = require('path');
8+
const parser = require('@babel/parser');
9+
const traverse = require('@babel/traverse').default;
10+
const generate = require('@babel/generator').default;
11+
12+
const inputPath = process.argv[2];
13+
if (!inputPath) {
14+
console.error('Usage: node gen-langs-map.cjs <input.js>');
15+
process.exit(1);
16+
}
17+
const src = fs.readFileSync(inputPath, 'utf8');
18+
19+
const ast = parser.parse(src, {
20+
sourceType: 'unambiguous',
21+
plugins: ['dynamicImport']
22+
});
23+
24+
function getProp(node, name) {
25+
if (!node || node.type !== 'ObjectExpression') return null;
26+
for (const p of node.properties) {
27+
if ((p.key?.name || p.key?.value) === name) return p;
28+
}
29+
return null;
30+
}
31+
32+
function litStr(node) {
33+
return node && node.type === 'StringLiteral' ? node.value : null;
34+
}
35+
36+
function arrStrs(propNode) {
37+
if (!propNode) return [];
38+
const val = propNode.value || propNode; // ObjectProperty vs direct
39+
const arr = val.type === 'ArrayExpression' ? val : val.value;
40+
if (!arr || arr.type !== 'ArrayExpression') return [];
41+
return arr.elements
42+
.map(e => (e && e.type === 'StringLiteral' ? e.value : null))
43+
.filter(Boolean);
44+
}
45+
46+
function findReturnArg(fn) {
47+
// ObjectMethod / ObjectProperty(FunctionExpression/ArrowFunctionExpression)
48+
if (!fn) return null;
49+
if (fn.type === 'ObjectMethod') {
50+
const ret = (fn.body.body || []).find(s => s.type === 'ReturnStatement');
51+
return ret?.argument || null;
52+
}
53+
if (fn.type === 'ObjectProperty') {
54+
const v = fn.value;
55+
if (v.type === 'ArrowFunctionExpression' || v.type === 'FunctionExpression') {
56+
if (v.body.type === 'BlockStatement') {
57+
const ret = (v.body.body || []).find(s => s.type === 'ReturnStatement');
58+
return ret?.argument || null;
59+
}
60+
return v.body; // concise arrow body
61+
}
62+
}
63+
return null;
64+
}
65+
66+
// 记录导入:按模块聚合命名导入;以及通配(* as)导入
67+
const namedImports = new Map(); // module -> Set(symbol)
68+
const starImports = new Map(); // module -> alias
69+
let needsStreamLanguage = false;
70+
71+
function addNamed(module, sym) {
72+
if (!namedImports.has(module)) namedImports.set(module, new Set());
73+
namedImports.get(module).add(sym);
74+
}
75+
function addStar(module, alias) {
76+
starImports.set(module, alias);
77+
}
78+
79+
function codeFrom(node) {
80+
return generate(node, { concise: true }).code;
81+
}
82+
83+
function parseLoadSpec(loadNode) {
84+
// 可能是: import('pkg').then(m => m.xxx(...))
85+
// 或 import('pkg').then(m => legacy(m.xxx(...)))
86+
// 或 sql("Dialect")
87+
// 或 直接 return legacy(m.xyz)(几乎不会,但兼容)
88+
if (!loadNode) return null;
89+
90+
// sql("DialectName")
91+
if (loadNode.type === 'CallExpression' && loadNode.callee?.name === 'sql') {
92+
const dialect = litStr(loadNode.arguments[0]);
93+
if (!dialect) return null;
94+
addStar('@codemirror/lang-sql', 'SQL');
95+
return { kind: 'sql', dialect };
96+
}
97+
98+
// import('pkg').then(arrow)
99+
if (
100+
loadNode.type === 'CallExpression' &&
101+
loadNode.callee?.type === 'MemberExpression' &&
102+
loadNode.callee.property?.name === 'then'
103+
) {
104+
const importCall = loadNode.callee.object;
105+
if (
106+
importCall?.type === 'CallExpression' &&
107+
importCall.callee?.type === 'Import'
108+
) {
109+
const modArg = importCall.arguments[0];
110+
const modulePath = litStr(modArg);
111+
const thenArg = loadNode.arguments[0];
112+
// then(m => <expr>)
113+
let body = thenArg?.type === 'ArrowFunctionExpression' ? thenArg.body : null;
114+
if (!body) return null;
115+
116+
// legacy(m.xxx(...)) or legacy(m.xxx)
117+
if (body.type === 'CallExpression' && body.callee?.name === 'legacy') {
118+
needsStreamLanguage = true;
119+
const inner = body.arguments[0];
120+
if (inner?.type === 'MemberExpression') {
121+
const symbol = inner.property.name || inner.property.value;
122+
addNamed(modulePath, symbol);
123+
return { kind: 'legacy', modulePath, symbol, call: false, args: [] };
124+
}
125+
if (inner?.type === 'CallExpression' && inner.callee.type === 'MemberExpression') {
126+
const symbol = inner.callee.property.name || inner.callee.property.value;
127+
const args = inner.arguments.map(a => codeFrom(a));
128+
addNamed(modulePath, symbol);
129+
return { kind: 'legacy', modulePath, symbol, call: true, args };
130+
}
131+
return null;
132+
}
133+
134+
// m.xxx(...) 或 m.xxx
135+
if (body.type === 'CallExpression' && body.callee?.type === 'MemberExpression') {
136+
const symbol = body.callee.property.name || body.callee.property.value;
137+
const args = body.arguments.map(a => codeFrom(a));
138+
addNamed(modulePath, symbol);
139+
return { kind: 'modern', modulePath, symbol, args };
140+
}
141+
if (body.type === 'MemberExpression') {
142+
const symbol = body.property.name || body.property.value;
143+
addNamed(modulePath, symbol);
144+
return { kind: 'modern', modulePath, symbol, args: [] };
145+
}
146+
}
147+
}
148+
149+
// 兜底:可能直接 legacy(m.xxx)
150+
if (loadNode.type === 'CallExpression' && loadNode.callee?.name === 'legacy') {
151+
needsStreamLanguage = true;
152+
const inner = loadNode.arguments[0];
153+
if (inner?.type === 'MemberExpression') {
154+
const symbol = inner.property.name || inner.property.value;
155+
// 模块不详,无法导入
156+
return { kind: 'legacy', modulePath: null, symbol, call: false, args: [] };
157+
}
158+
}
159+
160+
return null;
161+
}
162+
163+
let languagesArray = null;
164+
165+
traverse(ast, {
166+
VariableDeclarator(path) {
167+
const id = path.node.id;
168+
if (id.type === 'Identifier' && id.name === 'languages') {
169+
if (path.node.init && path.node.init.type === 'ArrayExpression') {
170+
languagesArray = path.node.init;
171+
}
172+
}
173+
}
174+
});
175+
176+
if (!languagesArray) {
177+
console.error('未找到变量 `languages` 的数组定义。');
178+
process.exit(1);
179+
}
180+
181+
const entries = []; // [{ext, expr}]
182+
for (const el of languagesArray.elements) {
183+
if (!el) continue;
184+
// 期望是 language.LanguageDescription.of({ ... })
185+
if (el.type !== 'CallExpression') continue;
186+
187+
// 取对象参数
188+
const obj = el.arguments?.[0];
189+
if (!obj || obj.type !== 'ObjectExpression') continue;
190+
191+
const exts = arrStrs(getProp(obj, 'extensions')); // 只用 extensions 做键
192+
if (!exts.length) continue; // 没有扩展名就无法生成键(例如 Dockerfile/Nginx 等)
193+
194+
// load 方法
195+
const loadProp = getProp(obj, 'load');
196+
const retArg = findReturnArg(loadProp);
197+
const spec = parseLoadSpec(retArg);
198+
199+
if (!spec) continue;
200+
201+
// 生成每个扩展名对应的表达式字符串
202+
for (const ext of exts) {
203+
let expr;
204+
if (spec.kind === 'sql') {
205+
// 例如: () => SQL.sql({ dialect: SQL.SQLite })
206+
expr = `() => SQL.sql({ dialect: SQL.${spec.dialect} })`;
207+
} else if (spec.kind === 'modern') {
208+
// 例如: () => javascript({ jsx: true })
209+
const callArgs = spec.args.length ? `(${spec.args.join(',')})` : '()';
210+
expr = `() => ${spec.symbol}${callArgs}`;
211+
} else if (spec.kind === 'legacy') {
212+
// 例如: () => StreamLanguage.define(perl) 或 define(asn1({}))
213+
const inner = spec.call
214+
? `${spec.symbol}(${spec.args.join(',')})`
215+
: spec.symbol;
216+
expr = `() => StreamLanguage.define(${inner})`;
217+
if (spec.modulePath) {
218+
addNamed(spec.modulePath, spec.symbol);
219+
}
220+
} else {
221+
continue;
222+
}
223+
224+
entries.push([ext, expr]);
225+
}
226+
}
227+
228+
entries.push(['solidity', `() => solidity`]);
229+
entries.push(['nix', `() => nix()`]);
230+
entries.push(['svelte', `() => svelte()`]);
231+
232+
// 组装 import 语句
233+
const importLines = [];
234+
if (needsStreamLanguage) {
235+
importLines.push(`import { StreamLanguage, LanguageSupport } from '@codemirror/language';`);
236+
}
237+
238+
for (const [mod, set] of namedImports) {
239+
if (mod === '@codemirror/lang-sql') continue; // 由星号导入覆盖
240+
const names = Array.from(set).sort();
241+
importLines.push(`import { ${names.join(', ')} } from '${mod}';`);
242+
}
243+
for (const [mod, alias] of starImports) {
244+
importLines.push(`import * as ${alias} from '${mod}';`);
245+
}
246+
247+
importLines.push(`import { nix } from '@replit/codemirror-lang-nix';`);
248+
importLines.push(`import { svelte } from '@replit/codemirror-lang-svelte';`);
249+
importLines.push(`import { solidity } from '@replit/codemirror-lang-solidity';`);
250+
251+
// 去重 & 排序键
252+
const mapObj = new Map();
253+
for (const [k, v] of entries) mapObj.set(k, v); // 后者覆盖前者
254+
const sorted = Array.from(mapObj.entries()).sort((a, b) => a[0].localeCompare(b[0]));
255+
256+
// 输出文件内容
257+
let out = '';
258+
out += `// auto-generated by gen-langs-map.cjs – DO NOT EDIT\n`;
259+
out += importLines.join('\n') + (importLines.length ? '\n\n' : '');
260+
out += `export const langs: Record<string, () => LanguageSupport | StreamLanguage<unknown>> = {\n`;
261+
for (const [k, v] of sorted) {
262+
const key = /^[a-z0-9_+-]+$/i.test(k) ? k : JSON.stringify(k);
263+
out += ` ${JSON.stringify(k)}: ${v},\n`;
264+
}
265+
out += `};\n\n`;
266+
267+
out += `export const langNames = Object.keys(langs) as LanguageName[];\n\n`;
268+
out += `export type LanguageName = keyof typeof langs;\n`;
269+
out += `export function loadLanguage(name: LanguageName) {\n`;
270+
out += ` return langs[name] ? langs[name]() : null;\n`;
271+
out += `}\n`;
272+
273+
process.stdout.write(out);

extensions/langs/package.json

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
},
1818
"scripts": {
1919
"watch": "tsbb watch src/*.ts --use-babel",
20-
"build": "tsbb build src/*.ts --use-babel"
20+
"build": "tsbb build src/*.ts --use-babel",
21+
"gen": "node gen-langs-map.cjs ../../node_modules/@codemirror/language-data/dist/index.cjs > src/index.ts"
2122
},
2223
"repository": {
2324
"type": "git",
@@ -29,33 +30,12 @@
2930
"cjs"
3031
],
3132
"peerDependencies": {
32-
"@codemirror/language-data": ">=6.0.0",
33+
"@codemirror/language": ">=6.0.0",
3334
"@codemirror/legacy-modes": ">=6.0.0"
3435
},
3536
"dependencies": {
36-
"@codemirror/lang-angular": "^0.1.0",
37-
"@codemirror/lang-cpp": "^6.0.0",
38-
"@codemirror/lang-css": "^6.2.0",
39-
"@codemirror/lang-html": "^6.4.0",
40-
"@codemirror/lang-java": "^6.0.0",
41-
"@codemirror/lang-javascript": "^6.1.0",
42-
"@codemirror/lang-json": "^6.0.0",
43-
"@codemirror/lang-less": "^6.0.1",
44-
"@codemirror/lang-lezer": "^6.0.0",
45-
"@codemirror/lang-liquid": "^6.0.1",
46-
"@codemirror/lang-markdown": "^6.1.0",
47-
"@codemirror/lang-php": "^6.0.0",
48-
"@codemirror/lang-python": "^6.1.0",
49-
"@codemirror/lang-rust": "^6.0.0",
50-
"@codemirror/lang-sass": "^6.0.1",
51-
"@codemirror/lang-sql": "^6.4.0",
52-
"@codemirror/lang-vue": "^0.1.1",
53-
"@codemirror/lang-wast": "^6.0.0",
54-
"@codemirror/lang-xml": "^6.0.0",
55-
"@codemirror/language-data": ">=6.0.0",
56-
"@codemirror/legacy-modes": ">=6.0.0",
57-
"@nextjournal/lang-clojure": "^1.0.0",
58-
"@replit/codemirror-lang-csharp": "^6.1.0",
37+
"@codemirror/language": ">=6.0.0",
38+
"@codemirror/language-data": "^6.5.1",
5939
"@replit/codemirror-lang-nix": "^6.0.1",
6040
"@replit/codemirror-lang-solidity": "^6.0.1",
6141
"@replit/codemirror-lang-svelte": "^6.0.0",
@@ -74,5 +54,10 @@
7454
"lcov",
7555
"json-summary"
7656
]
57+
},
58+
"devDependencies": {
59+
"@babel/generator": "^7.28.0",
60+
"@babel/parser": "^7.28.0",
61+
"@babel/traverse": "^7.28.0"
7762
}
7863
}

0 commit comments

Comments
 (0)