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