feat(editor): 新增 lowlight 实例与 extractLang 语言提取

common 集(~35 种语言)注册到 lowlight;extractLang 从完整 info string
(如 'python runnable {...}')提取首 token,使 runnable 块按正确语言高亮。
This commit is contained in:
xfy 2026-07-06 15:40:38 +08:00
parent 0b4346ecd4
commit e79e0d9ee2
4 changed files with 145 additions and 1 deletions

63
libs/pnpm-lock.yaml generated
View File

@ -67,6 +67,9 @@ importers:
'@tiptap/core': '@tiptap/core':
specifier: ^3.27.1 specifier: ^3.27.1
version: 3.27.1(@tiptap/pm@3.27.1) version: 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/extension-code-block-lowlight':
specifier: ^3.27.1
version: 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/extension-code-block@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(highlight.js@11.11.1)(lowlight@3.3.0)
'@tiptap/extension-file-handler': '@tiptap/extension-file-handler':
specifier: ^3.27.1 specifier: ^3.27.1
version: 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/extension-text-style@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1)))(@tiptap/pm@3.27.1) version: 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/extension-text-style@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1)))(@tiptap/pm@3.27.1)
@ -94,6 +97,9 @@ importers:
'@tiptap/suggestion': '@tiptap/suggestion':
specifier: ^3.27.1 specifier: ^3.27.1
version: 3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1) version: 3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
lowlight:
specifier: ^3.3.0
version: 3.3.0
yggdrasil-core: {} yggdrasil-core: {}
@ -370,6 +376,15 @@ packages:
peerDependencies: peerDependencies:
'@tiptap/extension-list': 3.27.1 '@tiptap/extension-list': 3.27.1
'@tiptap/extension-code-block-lowlight@3.27.1':
resolution: {integrity: sha512-rDZYDwKpV3Uw8xjI5pzSQTLJ7pEyJkDrVhnzdh9mBY3sHVwP1maYh+0kRUrHi5N3EhiNp/GXG1kPjysPIQ4nqQ==}
peerDependencies:
'@tiptap/core': 3.27.1
'@tiptap/extension-code-block': 3.27.1
'@tiptap/pm': 3.27.1
highlight.js: ^11
lowlight: ^2 || ^3
'@tiptap/extension-code-block@3.27.1': '@tiptap/extension-code-block@3.27.1':
resolution: {integrity: sha512-pHlzmZx2OlHfyQ0yRlT5UL4mGokz947DthZuYefN1OleVqOkHpWBG+2JQwqoNq6bmzMne92zbH32rhcJUEYSjA==} resolution: {integrity: sha512-pHlzmZx2OlHfyQ0yRlT5UL4mGokz947DthZuYefN1OleVqOkHpWBG+2JQwqoNq6bmzMne92zbH32rhcJUEYSjA==}
peerDependencies: peerDependencies:
@ -524,9 +539,15 @@ packages:
'@types/estree@1.0.9': '@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/node@26.0.1': '@types/node@26.0.1':
resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==} resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==}
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@types/whatwg-mimetype@3.0.2': '@types/whatwg-mimetype@3.0.2':
resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
@ -583,10 +604,17 @@ packages:
crelt@1.0.7: crelt@1.0.7:
resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
detect-libc@2.1.2: detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
entities@7.0.1: entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
@ -619,6 +647,10 @@ packages:
resolution: {integrity: sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==} resolution: {integrity: sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
highlight.js@11.11.1:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
lightningcss-android-arm64@1.32.0: lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
@ -696,6 +728,9 @@ packages:
linkifyjs@4.3.3: linkifyjs@4.3.3:
resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==} resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==}
lowlight@3.3.0:
resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==}
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@ -1186,6 +1221,14 @@ snapshots:
dependencies: dependencies:
'@tiptap/extension-list': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1) '@tiptap/extension-list': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
'@tiptap/extension-code-block-lowlight@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/extension-code-block@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(highlight.js@11.11.1)(lowlight@3.3.0)':
dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/extension-code-block': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
'@tiptap/pm': 3.27.1
highlight.js: 11.11.1
lowlight: 3.3.0
'@tiptap/extension-code-block@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)': '@tiptap/extension-code-block@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)':
dependencies: dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1) '@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
@ -1356,10 +1399,16 @@ snapshots:
'@types/estree@1.0.9': {} '@types/estree@1.0.9': {}
'@types/hast@3.0.4':
dependencies:
'@types/unist': 3.0.3
'@types/node@26.0.1': '@types/node@26.0.1':
dependencies: dependencies:
undici-types: 8.3.0 undici-types: 8.3.0
'@types/unist@3.0.3': {}
'@types/whatwg-mimetype@3.0.2': {} '@types/whatwg-mimetype@3.0.2': {}
'@types/ws@8.18.1': '@types/ws@8.18.1':
@ -1429,8 +1478,14 @@ snapshots:
crelt@1.0.7: {} crelt@1.0.7: {}
dequal@2.0.3: {}
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
devlop@1.1.0:
dependencies:
dequal: 2.0.3
entities@7.0.1: {} entities@7.0.1: {}
es-module-lexer@2.2.0: {} es-module-lexer@2.2.0: {}
@ -1461,6 +1516,8 @@ snapshots:
- bufferutil - bufferutil
- utf-8-validate - utf-8-validate
highlight.js@11.11.1: {}
lightningcss-android-arm64@1.32.0: lightningcss-android-arm64@1.32.0:
optional: true optional: true
@ -1512,6 +1569,12 @@ snapshots:
linkifyjs@4.3.3: {} linkifyjs@4.3.3: {}
lowlight@3.3.0:
dependencies:
'@types/hast': 3.0.4
devlop: 1.1.0
highlight.js: 11.11.1
magic-string@0.30.21: magic-string@0.30.21:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5

View File

@ -12,6 +12,7 @@
}, },
"dependencies": { "dependencies": {
"@tiptap/core": "^3.27.1", "@tiptap/core": "^3.27.1",
"@tiptap/extension-code-block-lowlight": "^3.27.1",
"@tiptap/extension-file-handler": "^3.27.1", "@tiptap/extension-file-handler": "^3.27.1",
"@tiptap/extension-image": "^3.27.1", "@tiptap/extension-image": "^3.27.1",
"@tiptap/extension-link": "^3.27.1", "@tiptap/extension-link": "^3.27.1",
@ -20,6 +21,7 @@
"@tiptap/markdown": "^3.27.1", "@tiptap/markdown": "^3.27.1",
"@tiptap/pm": "^3.27.1", "@tiptap/pm": "^3.27.1",
"@tiptap/starter-kit": "^3.27.1", "@tiptap/starter-kit": "^3.27.1",
"@tiptap/suggestion": "^3.27.1" "@tiptap/suggestion": "^3.27.1",
"lowlight": "^3.3.0"
} }
} }

View File

@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest';
import { extractLang, lowlight } from '../highlight';
/**
* extractLang fence info string token
* parse_fence_infosrc/api/code_runner/languages.rs
*/
describe('extractLang', () => {
it.each([
['python', 'python', '纯语言名'],
['python runnable', 'python', 'runnable 标记'],
['python runnable {"timeout_secs":10}', 'python', 'runnable + overrides JSON'],
['node runnable', 'node', 'node runnable'],
[' python ', 'python', '前后空格(trim)'],
['', '', '空字符串'],
[' ', '', '纯空格'],
])('%s → %s (%s)', (input, expected) => {
expect(extractLang(input)).toBe(expected);
});
});
/**
* lowlight wrapper highlight extractLang
* runnable info string
*/
describe('lowlight wrapper', () => {
it('runnable info string 按 python 高亮(不抛错)', () => {
// 未包装的 lowlight 遇到 'python runnable {...}' 会回退 highlightAuto 或抛错;
// wrapper 提取 'python' 后正常高亮。
const result = lowlight.highlight('python runnable {"timeout_secs":10}', 'def f():\n pass');
expect(result).toBeDefined();
});
it('python 代码高亮输出含 hljs-keyword class', () => {
const result = lowlight.highlight('python', 'def f():\n pass');
const html = JSON.stringify(result);
expect(html).toContain('hljs-keyword');
});
it('普通语言名(无 runnable正常工作', () => {
const result = lowlight.highlight('javascript', 'const x = 1;');
expect(JSON.stringify(result)).toContain('hljs-');
});
it('未注册语言不抛错(回退处理)', () => {
// extractLang 返回未注册语言名时lowlight 内部会抛错,
// CodeBlockLowlight 源码用 try/catch 兜底,这里只验 wrapper 不改变此行为。
expect(() => lowlight.highlight('totally-unknown-lang', 'code')).toThrow();
});
});

View File

@ -0,0 +1,29 @@
import { createLowlight, common } from 'lowlight';
const base = createLowlight(common);
/**
* fence info string token
*
* parse_fence_infosrc/api/code_runner/languages.rs:80
* info string `python runnable {"timeout_secs":10}` token
*/
export function extractLang(info: string): string {
return info.trim().split(/\s+/)[0] || '';
}
/**
* lowlight highlight info string
*
* CodeBlockLowlight `block.node.attrs.language` `python runnable {...}`
* `lowlight.highlight(language, code)` lowlight `python` token
* wrapper extractLang token使 runnable
*
* code blocklanguage='python'extractLang('python') 'python'
*/
export const lowlight = {
...base,
highlight(language: string, value: string) {
return base.highlight(extractLang(language), value);
},
};