feat(editor): 新增 lowlight 实例与 extractLang 语言提取
common 集(~35 种语言)注册到 lowlight;extractLang 从完整 info string
(如 'python runnable {...}')提取首 token,使 runnable 块按正确语言高亮。
This commit is contained in:
parent
0b4346ecd4
commit
e79e0d9ee2
63
libs/pnpm-lock.yaml
generated
63
libs/pnpm-lock.yaml
generated
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
libs/tiptap-editor/src/__tests__/highlight.test.ts
Normal file
50
libs/tiptap-editor/src/__tests__/highlight.test.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { extractLang, lowlight } from '../highlight';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* extractLang 测试:从完整 fence info string 提取语言名(首个 token)。
|
||||||
|
* 与后端 parse_fence_info(src/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();
|
||||||
|
});
|
||||||
|
});
|
||||||
29
libs/tiptap-editor/src/highlight.ts
Normal file
29
libs/tiptap-editor/src/highlight.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { createLowlight, common } from 'lowlight';
|
||||||
|
|
||||||
|
const base = createLowlight(common);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从完整 fence info string 提取语言名(首个 token)。
|
||||||
|
*
|
||||||
|
* 与后端 parse_fence_info(src/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 block(language='python')也兼容:extractLang('python') → 'python'。
|
||||||
|
*/
|
||||||
|
export const lowlight = {
|
||||||
|
...base,
|
||||||
|
highlight(language: string, value: string) {
|
||||||
|
return base.highlight(extractLang(language), value);
|
||||||
|
},
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user