✨ feat(vscode): 代码阅读的注释和函数说明按 Markdown 渲染
This commit is contained in:
1 parent
aae9012f14
commit
a53c2a6aac
18 files changed
+394
-66
No files matched your search
Generated
+44
@@ -89,6 +89,9 @@ importers:
|
||||
lucide-react:
|
||||
specifier: ^1.16.0
|
||||
version: 1.16.0(react@19.2.6)
|
||||
markdown-it:
|
||||
specifier: ^15.0.2
|
||||
version: 15.0.2
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.6
|
||||
@@ -1515,6 +1518,9 @@ packages:
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
argparse@3.0.2:
|
||||
resolution: {integrity: sha512-mFdDM6WqWKraGLsVb+C9CahPnzTXOefAOLq3jYcca2YZ8bEWpr++Tzj+zSaKW9+X9L5uSxcm1AZ3Y6aZJ09OhQ==}
|
||||
|
||||
args-tokenizer@0.3.0:
|
||||
resolution: {integrity: sha512-xXAd7G2Mll5W8uo37GETpQ2VrE84M181Z7ugHFGQnJZ50M2mbOv0osSZ9VsSgPfJQ+LVG0prSi0th+ELMsno7Q==}
|
||||
|
||||
@@ -1871,6 +1877,10 @@ packages:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
entities@8.1.0:
|
||||
resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
environment@1.1.0:
|
||||
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2576,6 +2586,9 @@ packages:
|
||||
linkify-it@5.0.0:
|
||||
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||
|
||||
linkify-it@6.1.0:
|
||||
resolution: {integrity: sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==}
|
||||
|
||||
local-pkg@1.1.2:
|
||||
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -2646,6 +2659,10 @@ packages:
|
||||
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||
hasBin: true
|
||||
|
||||
markdown-it@15.0.2:
|
||||
resolution: {integrity: sha512-q4IGxMv56jCqT4OCRCADBoDP3LO4MhmTXjFbphHPXs4g3j9Xg5RDnxqN8IF/3vIWEU+VCnUq+7JUg/cfy2E6Qw==}
|
||||
hasBin: true
|
||||
|
||||
markdown-table@3.0.4:
|
||||
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||
|
||||
@@ -2688,6 +2705,9 @@ packages:
|
||||
mdurl@2.0.0:
|
||||
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
||||
|
||||
mdurl@2.1.0:
|
||||
resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==}
|
||||
|
||||
merge2@1.4.1:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -3416,6 +3436,9 @@ packages:
|
||||
uc.micro@2.1.0:
|
||||
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||
|
||||
uc.micro@3.0.0:
|
||||
resolution: {integrity: sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==}
|
||||
|
||||
ufo@1.6.1:
|
||||
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
||||
|
||||
@@ -5012,6 +5035,8 @@ snapshots:
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
argparse@3.0.2: {}
|
||||
|
||||
args-tokenizer@0.3.0: {}
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
@@ -5362,6 +5387,8 @@ snapshots:
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
entities@8.1.0: {}
|
||||
|
||||
environment@1.1.0: {}
|
||||
|
||||
es-module-lexer@1.7.0: {}
|
||||
@@ -6149,6 +6176,10 @@ snapshots:
|
||||
dependencies:
|
||||
uc.micro: 2.1.0
|
||||
|
||||
linkify-it@6.1.0:
|
||||
dependencies:
|
||||
uc.micro: 3.0.0
|
||||
|
||||
local-pkg@1.1.2:
|
||||
dependencies:
|
||||
mlly: 1.7.4
|
||||
@@ -6216,6 +6247,15 @@ snapshots:
|
||||
punycode.js: 2.3.1
|
||||
uc.micro: 2.1.0
|
||||
|
||||
markdown-it@15.0.2:
|
||||
dependencies:
|
||||
argparse: 3.0.2
|
||||
entities: 8.1.0
|
||||
linkify-it: 6.1.0
|
||||
mdurl: 2.1.0
|
||||
punycode.js: 2.3.1
|
||||
uc.micro: 3.0.0
|
||||
|
||||
markdown-table@3.0.4: {}
|
||||
|
||||
mdast-util-find-and-replace@3.0.2:
|
||||
@@ -6333,6 +6373,8 @@ snapshots:
|
||||
|
||||
mdurl@2.0.0: {}
|
||||
|
||||
mdurl@2.1.0: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
|
||||
micromark-core-commonmark@2.0.2:
|
||||
@@ -7202,6 +7244,8 @@ snapshots:
|
||||
|
||||
uc.micro@2.1.0: {}
|
||||
|
||||
uc.micro@3.0.0: {}
|
||||
|
||||
ufo@1.6.1: {}
|
||||
|
||||
uglify-js@3.19.3:
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { readBlockViews, toBlockViews } from './blockView'
|
||||
import { createPythonAdapter } from './languages/python'
|
||||
import { createTypeScriptAdapter } from './languages/typescript'
|
||||
import { propertyLines, readFixture, runtime, segmentText, WASM_DIR, withBlocks } from './testSupport'
|
||||
import { previewText, propertyLines, readFixture, runtime, WASM_DIR, withBlocks } from './testSupport'
|
||||
import { pythonZhTranslator } from './translators/pythonZh'
|
||||
import { typescriptZhTranslator } from './translators/typescriptZh'
|
||||
|
||||
@@ -38,7 +38,7 @@ const PROPERTY_KINDS = new Set(['function', 'import', 'const', 'variable'])
|
||||
function translationText(view: BlockView): string {
|
||||
if (!('translation' in view))
|
||||
throw new Error(`块 ${view.id} 是属性表`)
|
||||
return segmentText(view.translation)
|
||||
return previewText(view.translation)
|
||||
}
|
||||
|
||||
describe('blockView', () => {
|
||||
@@ -53,7 +53,7 @@ describe('blockView', () => {
|
||||
if ('properties' in view)
|
||||
expect(view.properties.length, view.id).toBeGreaterThan(0)
|
||||
else
|
||||
expect(view.translation.length, view.id).toBeGreaterThan(0)
|
||||
expect(previewText(view.translation), view.id).not.toBe('')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -100,6 +100,13 @@ describe('blockView', () => {
|
||||
expect(getUser?.children[0]).toMatchObject({ kind: 'comment', startLine: 38, endLine: 38 })
|
||||
})
|
||||
|
||||
it('注释块的译文、函数的「说明」是 Markdown 原文,webview 按它渲染', async () => {
|
||||
const env = await views(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
|
||||
expect(byId(env, '0')).toMatchObject({ kind: 'comment', translation: { markdown: expect.stringMatching(/^\.env file lock \/ unlock helpers\.\n\nRecursively/) } })
|
||||
const findEnvFiles = byId(env, '5')
|
||||
expect('properties' in findEnvFiles && findEnvFiles.properties[1]).toEqual({ label: '说明', value: { markdown: expect.stringMatching(/^Scan the workspace folder/) } })
|
||||
})
|
||||
|
||||
it('id 是块在树里的位置,行号 0 起,多行块去掉共同缩进', async () => {
|
||||
const env = await views(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
|
||||
expect(byId(env, '5.1.0')).toMatchObject({ kind: 'return', startLine: 25, endLine: 25, code: 'return []' })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BlockKind, CodeBlock, LanguageAdapter, Node, PropertyRow, Segment, SourceRange, TokenRole, Translator } from './contract'
|
||||
import type { BlockKind, CodeBlock, LanguageAdapter, Node, PreviewText, PropertyRow, Segment, SourceRange, TokenRole, Translator } from './contract'
|
||||
import type { TreeSitterRuntime } from './runtime'
|
||||
import { commonIndent, splitLines } from './editText'
|
||||
|
||||
@@ -18,8 +18,8 @@ interface BlockViewBase {
|
||||
children: BlockView[]
|
||||
}
|
||||
|
||||
/** 发给 webview 的一块(可序列化):函数、导入、常量、变量块预览时是属性表,其余是一行译文。 */
|
||||
export type BlockView = BlockViewBase & ({ translation: Segment[] } | { properties: PropertyRow[] })
|
||||
/** 发给 webview 的一块(可序列化):函数、导入、常量、变量块预览时是属性表,注释块是 Markdown 说明,其余是一行译文。 */
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: PropertyRow[] })
|
||||
|
||||
interface Line { start: number, end: number }
|
||||
|
||||
|
||||
@@ -110,6 +110,15 @@ export type TokenRole
|
||||
*/
|
||||
export interface Segment { text: string, role: TokenRole, offset?: number }
|
||||
|
||||
/**
|
||||
* 一段 Markdown 说明文字:注释、JSDoc、docstring 去掉注释符号、引号和共同缩进后的原文,不做别的转换
|
||||
* (reStructuredText 的 `:func:`、JSDoc 的 `@param` 都照抄)。预览按 Markdown 渲染成注释色的段落、列表、代码块,源码里的 HTML 当文字显示。
|
||||
*/
|
||||
export interface Markdown { markdown: string }
|
||||
|
||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数的说明)。 */
|
||||
export type PreviewText = Segment[] | Markdown
|
||||
|
||||
/**
|
||||
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts,例如参数名、导入项的名称和别名、
|
||||
* 声明的变量名)在源码里的范围。跳到定义时,定义处落在这个范围里就只闪这一行;不填(第三方翻译插件可以不填)就闪整块。
|
||||
@@ -119,10 +128,10 @@ export interface Defines { defines?: SourceRange }
|
||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值。 */
|
||||
export interface PropertyItem extends Defines { cells: Segment[][] }
|
||||
|
||||
/** 属性表的一行:左列属性名,右列着色的值;items 是展开在这一行下面的子行(输入参数、导入项)。 */
|
||||
/** 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项)。 */
|
||||
export interface PropertyRow extends Defines {
|
||||
label: string
|
||||
value: Segment[]
|
||||
value: PreviewText
|
||||
items?: PropertyItem[]
|
||||
}
|
||||
|
||||
@@ -143,8 +152,8 @@ export interface Translator {
|
||||
id: string // 'typescript.zh-template'
|
||||
label: string // '中文直译'
|
||||
languageIds: string[]
|
||||
/** 只翻 header 那一段。 */
|
||||
translate: (block: TextBlock, source: string) => Segment[]
|
||||
/** 只翻 header 那一段;注释块给去掉注释符号的 Markdown 原文。 */
|
||||
translate: (block: TextBlock, source: string) => PreviewText
|
||||
/** 函数、导入、常量、变量块 → 属性表。 */
|
||||
describe: (block: FactBlock, source: string) => PropertyRow[]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BlockKind, CodeBlock, LanguageAdapter, PropertyRow, Segment } from './contract'
|
||||
import type { BlockKind, CodeBlock, LanguageAdapter, PreviewText, PropertyRow, Segment } from './contract'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import { Node } from '@vscode/tree-sitter-wasm'
|
||||
@@ -49,10 +49,15 @@ export function segmentText(segments: Segment[]): string {
|
||||
return segments.map(segment => segment.text).join('')
|
||||
}
|
||||
|
||||
/** 预览文字写成文字:着色的词拼起来,Markdown 取原文。 */
|
||||
export function previewText(text: PreviewText): string {
|
||||
return 'markdown' in text ? text.markdown : segmentText(text)
|
||||
}
|
||||
|
||||
/** 属性表写成文字:每行 `名称: 值`,展开的子行缩进两格、各列用 ` | ` 隔开。 */
|
||||
export function propertyLines(rows: PropertyRow[]): string[] {
|
||||
return rows.flatMap(row => [
|
||||
`${row.label}: ${segmentText(row.value)}`,
|
||||
`${row.label}: ${previewText(row.value)}`,
|
||||
...(row.items ?? []).map(item => ` ${item.cells.map(segmentText).join(' | ')}`),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -11,16 +11,21 @@ import { createZhTemplateTranslator } from './zhTemplate'
|
||||
const ts = createTypeScriptAdapter(WASM_DIR)
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
|
||||
interface Translated { kind: string, segments: Segment[] }
|
||||
/** 一块的译文:一行译文在 segments 里;注释块是 Markdown,原文在 markdown 里,segments 为空。 */
|
||||
interface Translated { kind: string, segments: Segment[], markdown?: string }
|
||||
|
||||
/** 一行译文块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。属性表块见 describeAll。 */
|
||||
/** 一行译文块、注释块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。属性表块见 describeAll。 */
|
||||
function translateAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<Map<number, Translated>> {
|
||||
return withBlocks(adapter, languageId, source, (blocks) => {
|
||||
const byLine = new Map<number, Translated>()
|
||||
walk(blocks, (block) => {
|
||||
const line = block.header.startRow + 1
|
||||
if (!byLine.has(line) && !('facts' in block))
|
||||
byLine.set(line, { kind: block.kind, segments: translator.translate(block, source) })
|
||||
if (byLine.has(line) || 'facts' in block)
|
||||
return
|
||||
const translation = translator.translate(block, source)
|
||||
byLine.set(line, 'markdown' in translation
|
||||
? { kind: block.kind, segments: [], markdown: translation.markdown }
|
||||
: { kind: block.kind, segments: translation })
|
||||
})
|
||||
return byLine
|
||||
})
|
||||
@@ -44,7 +49,10 @@ function rowTexts(map: Map<number, PropertyRow[]>): Record<number, string[]> {
|
||||
}
|
||||
|
||||
function valueOf(rows: PropertyRow[] | undefined, label: string): Segment[] {
|
||||
return rows?.find(row => row.label === label)?.value ?? []
|
||||
const value = rows?.find(row => row.label === label)?.value ?? []
|
||||
if ('markdown' in value)
|
||||
throw new Error(`「${label}」是 Markdown`)
|
||||
return value
|
||||
}
|
||||
|
||||
function text(segments: Segment[] | undefined): string {
|
||||
@@ -52,7 +60,7 @@ function text(segments: Segment[] | undefined): string {
|
||||
}
|
||||
|
||||
function texts(map: Map<number, Translated>): Record<number, string> {
|
||||
return Object.fromEntries([...map].map(([line, { segments }]) => [line, text(segments)]))
|
||||
return Object.fromEntries([...map].map(([line, { segments, markdown }]) => [line, markdown ?? text(segments)]))
|
||||
}
|
||||
|
||||
/** 只看文字和着色:去掉各段的源码下标、属性行讲的定义(这两样另有用例)。 */
|
||||
@@ -93,14 +101,23 @@ describe('typeScript 中文直译', () => {
|
||||
expect(text(issue.get(20)?.segments)).toBe('返回 { locked: 真, prerequisiteNumber: prereq.number, prerequisiteColumn: prereq.column }')
|
||||
})
|
||||
|
||||
it('其余块:结构照抄,复合语句去掉结尾的 {,注释去掉注释符号', async () => {
|
||||
it('其余块:结构照抄,复合语句去掉结尾的 {,注释去掉注释符号、保留换行', async () => {
|
||||
const env = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt')))
|
||||
const issue = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt')))
|
||||
expect(env[32]).toBe('公共 接口 EnvLockResult')
|
||||
expect(env[42]).toBe('尝试')
|
||||
expect(env[46]).toBe('捕获 (err)')
|
||||
expect(env[47]).toBe('result.failed.push({ path: f, reason: 如果 err 属于 Error 则 err.message 否则 String(err) })')
|
||||
expect(env[1]).toBe('.env file lock / unlock helpers. Recursively scans the main workspace folder for `.env*` files (skipping `node_modules`, `.git`, `.claude`) and chmods them to 0o444 (locked) or 0o644 (unlocked) in bulk. Backs the toolbar Lock toggle in `PanelHeader`. No persistent watcher — re-scan happens on every toggle, so newly created `.env` files are only picked up the next time the user clicks the button.')
|
||||
expect(env[1]).toBe([
|
||||
'.env file lock / unlock helpers.',
|
||||
'',
|
||||
'Recursively scans the main workspace folder for `.env*` files (skipping',
|
||||
'`node_modules`, `.git`, `.claude`) and chmods them to 0o444 (locked) or',
|
||||
'0o644 (unlocked) in bulk. Backs the toolbar Lock toggle in `PanelHeader`.',
|
||||
'',
|
||||
'No persistent watcher — re-scan happens on every toggle, so newly created',
|
||||
'`.env` files are only picked up the next time the user clicks the button.',
|
||||
].join('\n'))
|
||||
expect(issue[5]).toBe('prerequisiteNumber?: 数值')
|
||||
expect(issue[15]).toBe('如果 (非 issue 或者 issue.prerequisite 全等 未定义)')
|
||||
})
|
||||
@@ -382,7 +399,7 @@ describe('属性表', () => {
|
||||
const rows = colors(env.get(18) ?? [])
|
||||
expect(rows.map(row => row.label)).toEqual(['名称', '说明', '访问级别', '异步', '输入参数', '输出参数'])
|
||||
expect(rows[0].value).toEqual([{ text: 'findEnvFiles', role: 'function' }])
|
||||
expect(rows[1].value).toEqual([{ text: valueOf(env.get(18), '说明')[0].text, role: 'comment' }])
|
||||
expect(rows[1].value).toEqual({ markdown: expect.stringMatching(/^Scan the workspace folder/) })
|
||||
expect(rows[2].value).toEqual([{ text: '公共', role: 'keyword' }])
|
||||
expect(rows[3].value).toEqual([{ text: '是', role: 'storage' }])
|
||||
expect(rows[4].value).toEqual([{ text: '1 个', role: 'hint' }])
|
||||
@@ -447,6 +464,57 @@ describe('属性表', () => {
|
||||
expect(pyRows[8].slice(0, 2)).toEqual(['名称: g', '访问级别: 公共'])
|
||||
})
|
||||
|
||||
it('注释块和函数的说明是 Markdown 原文:只去注释符号、引号和共同缩进,reST 角色、双反引号、JSDoc 标签、HTML 照抄', async () => {
|
||||
const pySource = [
|
||||
'"""手机号规范化 / 脱敏工具.',
|
||||
'',
|
||||
'凡是入库、比对的手机号都先过 :func:`normalize_phone`,日志里打印 ``user`` 时用 <b>mask</b>。',
|
||||
'"""',
|
||||
'# 用法:',
|
||||
'#',
|
||||
'# mask_phone("13800138000")',
|
||||
'def mask_phone(phone: str) -> str:',
|
||||
' """把中间四位换成 ``*``.',
|
||||
'',
|
||||
' - 不足 7 位原样返回',
|
||||
' - 见 https://example.com/phone',
|
||||
' """',
|
||||
].join('\n')
|
||||
const pyText = await translateAll(py, 'python', pythonZhTranslator, pySource)
|
||||
expect(pyText.get(1)).toEqual({
|
||||
kind: 'comment',
|
||||
segments: [],
|
||||
markdown: '手机号规范化 / 脱敏工具.\n\n凡是入库、比对的手机号都先过 :func:`normalize_phone`,日志里打印 ``user`` 时用 <b>mask</b>。',
|
||||
})
|
||||
expect(pyText.get(5)?.markdown).toBe('用法:\n\n mask_phone("13800138000")')
|
||||
const pyRows = await describeAll(py, 'python', pythonZhTranslator, pySource)
|
||||
expect(pyRows.get(8)?.find(row => row.label === '说明')?.value).toEqual({
|
||||
markdown: '把中间四位换成 ``*``.\n\n- 不足 7 位原样返回\n- 见 https://example.com/phone',
|
||||
})
|
||||
|
||||
const tsSource = [
|
||||
'// 第一行',
|
||||
'// - 缩进的列表',
|
||||
'/* 单行块注释 */',
|
||||
'/**',
|
||||
' * 读配置,见 {@link load}。',
|
||||
' *',
|
||||
' * ```ts',
|
||||
' * load(\'a\')',
|
||||
' * ```',
|
||||
' * @param path 文件路径',
|
||||
' */',
|
||||
'function load(path: string) {}',
|
||||
].join('\n')
|
||||
const tsText = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, tsSource))
|
||||
expect(tsText[1]).toBe('第一行\n - 缩进的列表')
|
||||
expect(tsText[3]).toBe('单行块注释')
|
||||
const tsRows = await describeAll(ts, 'typescript', typescriptZhTranslator, tsSource)
|
||||
expect(tsRows.get(4)?.find(row => row.label === '说明')?.value).toEqual({
|
||||
markdown: '读配置,见 {@link load}。\n\n```ts\nload(\'a\')\n```\n@param path 文件路径',
|
||||
})
|
||||
})
|
||||
|
||||
it('导入:默认导出、全部、整个模块、副作用导入;Python 没有「仅类型」', async () => {
|
||||
const tsSource = [
|
||||
'import d, { b as c } from \'y\'',
|
||||
@@ -517,7 +585,7 @@ describe('源码下标(跳到定义用)', () => {
|
||||
}
|
||||
|
||||
function propertySegments(rows: PropertyRow[]): Segment[] {
|
||||
return rows.flatMap(row => [...row.value, ...(row.items ?? []).flatMap(item => item.cells.flat())])
|
||||
return rows.flatMap(row => [...('markdown' in row.value ? [] : row.value), ...(row.items ?? []).flatMap(item => item.cells.flat())])
|
||||
}
|
||||
|
||||
it('python:点分模块名每一节、导入项、别名各自带下标,点号不带;前面有中文注释也对得上', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, Defines, FactBlock, FunctionFacts, ImportFacts, ImportItem, Node, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import type { Access, Defines, FactBlock, FunctionFacts, ImportFacts, ImportItem, Markdown, Node, PreviewText, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import { commonIndent, splitLines } from '../editText'
|
||||
|
||||
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
|
||||
@@ -102,13 +102,15 @@ function oneLine(text: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 说明文字排版:去掉行尾空白、首尾空行和共同缩进,保留换行(段落之间的空行也留着)。
|
||||
* 第一行紧跟在引号 / 注释符号后面,本来就没有缩进,不参与算共同缩进(同 PEP 257 的做法)。
|
||||
* 注释的 Markdown 原文:各个注释节点去掉注释符号后(texts)拼起来,去掉行尾空白、首尾空行和共同缩进,保留换行(段落之间的空行也留着)。
|
||||
* 只有一个节点(块注释、docstring)时第一行紧跟在引号 / 注释符号后面,本来就没有缩进,不参与算共同缩进(同 PEP 257 的做法);
|
||||
* 几条行注释连成的一块每行地位相同,第一行也算,缩进四格的代码块才留得住。
|
||||
*/
|
||||
function docText(raw: string): string {
|
||||
const [first = '', ...rest] = splitLines(raw).map(line => line.trimEnd())
|
||||
const indent = commonIndent(rest).length
|
||||
return [first.trim(), ...rest.map(line => line.slice(indent))].join('\n').replace(/^\n+|\n+$/g, '')
|
||||
function docText(texts: string[]): string {
|
||||
const lines = splitLines(texts.join('\n')).map(line => line.trimEnd())
|
||||
const quoted = texts.length === 1
|
||||
const indent = commonIndent(quoted ? lines.slice(1) : lines).length
|
||||
return lines.map((line, i) => quoted && i === 0 ? line.trim() : line.slice(indent)).join('\n').replace(/^\n+|\n+$/g, '')
|
||||
}
|
||||
|
||||
/** 模板槽位引用的子节点;注释是语法之外的节点,不占具名子节点序号。 */
|
||||
@@ -261,15 +263,14 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return segments
|
||||
}
|
||||
|
||||
function translateComment(block: TextBlock): Segment[] {
|
||||
function translateComment(block: TextBlock): Markdown {
|
||||
const { startIndex, endIndex } = block.header
|
||||
const root = block.node.tree.rootNode
|
||||
const nodes = block.node.type === 'comment' ? commentsWithin(root.descendantForIndex(startIndex, endIndex) ?? root, block.header) : [block.node]
|
||||
const text = nodes.map(table.commentText).join('\n').split('\n').map(line => line.trim()).filter(Boolean).join(' ')
|
||||
return text ? [{ text, role: 'comment' }] : []
|
||||
return { markdown: docText(nodes.map(table.commentText)) }
|
||||
}
|
||||
|
||||
function translate(block: TextBlock, source: string): Segment[] {
|
||||
function translate(block: TextBlock, source: string): PreviewText {
|
||||
if (block.kind === 'comment')
|
||||
return translateComment(block)
|
||||
const tokens = tokensIn(block.node, block.header, source)
|
||||
@@ -299,8 +300,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
module: node => stringNodes.has(node.type) ? [segment(node.text, 'string', offsetOf(node))] : verbatim(node, source, 'type'),
|
||||
name: (node, role) => [segment(node.text, role, offsetOf(node))],
|
||||
doc: (node) => {
|
||||
const text = docText(table.commentText(node))
|
||||
return text ? [{ text, role: 'comment' }] : []
|
||||
const markdown = docText([table.commentText(node)])
|
||||
return markdown ? { markdown } : undefined
|
||||
},
|
||||
}
|
||||
switch (block.kind) {
|
||||
@@ -362,8 +363,8 @@ interface Phrases {
|
||||
module: (node: Node) => Segment[]
|
||||
/** 源码里的一个名字原样照抄、按 role 着色(函数名称)。 */
|
||||
name: (node: Node, role: TokenRole) => Segment[]
|
||||
/** 函数的说明(JSDoc、docstring):去掉注释符号、引号和共同缩进,保留换行,按注释着色;没有文字时是空的。 */
|
||||
doc: (node: Node) => Segment[]
|
||||
/** 函数的说明(JSDoc、docstring):去掉注释符号、引号和共同缩进的 Markdown 原文;没有文字时没有。 */
|
||||
doc: (node: Node) => Markdown | undefined
|
||||
}
|
||||
|
||||
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
|
||||
@@ -416,10 +417,10 @@ function aliased(item: Segment[], alias: Node | undefined, say: Phrases): Segmen
|
||||
/** 函数:名称 / 访问级别 / 异步 / 装饰器 / 输入参数(每个参数一行:名称、类型、默认值)/ 输出参数。 */
|
||||
function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
|
||||
const decorators = facts.decorators.map(say.verbatim)
|
||||
const doc = facts.doc ? say.doc(facts.doc) : []
|
||||
const doc = facts.doc && say.doc(facts.doc)
|
||||
return [
|
||||
{ label: '名称', value: facts.name ? say.name(facts.name, 'function') : [hint('匿名')] },
|
||||
...(doc.length > 0 ? [{ label: '说明', value: doc }] : []),
|
||||
...(doc ? [{ label: '说明', value: doc }] : []),
|
||||
...accessRows(facts.access),
|
||||
{ label: '异步', value: yesNo(facts.async) },
|
||||
...(decorators.length > 0 ? [{ label: '装饰器', value: joined(decorators, '\n') }] : []),
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.16.0",
|
||||
"markdown-it": "^15.0.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.6.0"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Fragment } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CodeEditor } from './CodeEditor'
|
||||
import { PropertyGrid } from './PropertyGrid'
|
||||
import { SegmentText } from './SegmentText'
|
||||
import { TextPreview } from './TextPreview'
|
||||
|
||||
const KIND_LABEL: Record<BlockKind, string> = {
|
||||
import: '导入',
|
||||
@@ -66,11 +66,7 @@ export function BlockLevel({ blocks, kind, tree }: { blocks: BlockView[], kind?:
|
||||
function BlockPreview({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement {
|
||||
if ('properties' in block)
|
||||
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} />
|
||||
return (
|
||||
<p className="cr-code">
|
||||
<SegmentText segments={block.translation} />
|
||||
</p>
|
||||
)
|
||||
return <TextPreview text={block.translation} />
|
||||
}
|
||||
|
||||
/** 一块:折叠箭头、种类标签、预览内容或源码、右边的「编辑 / 预览」切换。 */
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PropertyItem, PropertyRow } from './types'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { Fragment } from 'react'
|
||||
import { SegmentText } from './SegmentText'
|
||||
import { TextPreview } from './TextPreview'
|
||||
import { propertyItemId, propertyRowId } from './viewState'
|
||||
|
||||
/** 子行里除最后一列以外各列的宽度(参数的名称、类型),最后一列占满剩下的;编辑区窄时按比例收,不把最后一列挤没 */
|
||||
@@ -69,9 +70,7 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
|
||||
</button>
|
||||
)
|
||||
: <span className="cr-prop-label flex h-5 w-[76px] shrink-0 items-center">{row.label}</span>}
|
||||
<p className="cr-code min-w-0 flex-1">
|
||||
<SegmentText segments={row.value} />
|
||||
</p>
|
||||
<TextPreview text={row.value} className="min-w-0 flex-1" />
|
||||
</div>
|
||||
{items.length > 0 && !folded && <PropertyItems rowId={id} items={items} />}
|
||||
</Fragment>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { PreviewText } from './types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { renderMarkdown } from './markdown'
|
||||
import { SegmentText } from './SegmentText'
|
||||
|
||||
/** 预览里的一段文字:着色的词按源码排版;Markdown 说明渲染成段落、列表、代码块。 */
|
||||
export function TextPreview({ text, className }: { text: PreviewText, className?: string }): ReactElement {
|
||||
if ('markdown' in text)
|
||||
return <div className={cn('cr-markdown', className)} dangerouslySetInnerHTML={{ __html: renderMarkdown(text.markdown) }} />
|
||||
return (
|
||||
<p className={cn('cr-code', className)}>
|
||||
<SegmentText segments={text} />
|
||||
</p>
|
||||
)
|
||||
}
|
||||
@@ -163,6 +163,85 @@ body.vscode-high-contrast-light {
|
||||
.cr-role-comment { color: var(--cr-comment); }
|
||||
.cr-role-hint { color: var(--vscode-descriptionForeground); }
|
||||
|
||||
/* 注释块、函数说明的 Markdown:正文界面字体、注释色,和属性名一样的行高;代码用编辑器字体、垫代码块底色。
|
||||
间距压紧:一行注释和一行译文一样高,标题只比正文稍大 */
|
||||
.cr-markdown {
|
||||
font-family: var(--vscode-font-family, system-ui, sans-serif), "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
|
||||
font-size: var(--vscode-font-size, 13px);
|
||||
line-height: 20px;
|
||||
color: var(--cr-comment);
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.cr-markdown :is(p, ul, ol, pre, blockquote, table, h1, h2, h3, h4, h5, h6, hr) {
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
.cr-markdown :is(li, blockquote) > :last-child,
|
||||
.cr-markdown > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.cr-markdown :is(ul, ol) {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.cr-markdown ul { list-style: disc; }
|
||||
.cr-markdown ol { list-style: decimal; }
|
||||
.cr-markdown :is(ul, ol) ul { list-style: circle; }
|
||||
.cr-markdown :is(h1, h2, h3, h4, h5, h6) {
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cr-markdown h1 { font-size: 1.15em; }
|
||||
.cr-markdown h2 { font-size: 1.08em; }
|
||||
/* 行内代码换了字体,行高压成 1 才不把这一行撑高 */
|
||||
.cr-markdown code {
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--vscode-editor-font-family, monospace), "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", monospace;
|
||||
font-size: 1em;
|
||||
line-height: 1;
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
}
|
||||
.cr-markdown pre {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
white-space: pre-wrap;
|
||||
tab-size: 4;
|
||||
}
|
||||
.cr-markdown pre code {
|
||||
padding: 0;
|
||||
font-size: var(--vscode-editor-font-size, 12.5px);
|
||||
line-height: inherit;
|
||||
background: none;
|
||||
}
|
||||
.cr-markdown a {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
text-decoration: none;
|
||||
}
|
||||
.cr-markdown a:hover {
|
||||
color: var(--vscode-textLink-activeForeground);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.cr-markdown blockquote {
|
||||
padding: 0 8px;
|
||||
border-left: 3px solid var(--vscode-textBlockQuote-border);
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
}
|
||||
.cr-markdown table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.cr-markdown :is(th, td) {
|
||||
padding: 0 8px;
|
||||
border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
|
||||
}
|
||||
.cr-markdown th {
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
}
|
||||
.cr-markdown hr {
|
||||
border: 0;
|
||||
border-top: 1px solid color-mix(in srgb, currentColor 30%, transparent);
|
||||
}
|
||||
|
||||
/* 按住 ⌘ / Ctrl 停在查得到定义的名字上:和编辑器里的链接一样 */
|
||||
.cr-code [data-link] {
|
||||
color: var(--vscode-editorLink-activeForeground);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { renderMarkdown } from './markdown'
|
||||
|
||||
describe('renderMarkdown', () => {
|
||||
it('一行文字是一个段落;段内换行是软换行,空行分段', () => {
|
||||
expect(renderMarkdown('读配置。')).toBe('<p>读配置。</p>\n')
|
||||
expect(renderMarkdown('第一行\n接着写\n\n第二段')).toBe('<p>第一行\n接着写</p>\n<p>第二段</p>\n')
|
||||
})
|
||||
|
||||
it('行内代码(含双反引号)、强调、列表、代码块、标题、引用、表格', () => {
|
||||
expect(renderMarkdown('先过 :func:`normalize_phone`,打印 ``user`` 时 **脱敏**')).toBe(
|
||||
'<p>先过 :func:<code>normalize_phone</code>,打印 <code>user</code> 时 <strong>脱敏</strong></p>\n',
|
||||
)
|
||||
expect(renderMarkdown('- 甲\n- 乙')).toBe('<ul>\n<li>甲</li>\n<li>乙</li>\n</ul>\n')
|
||||
expect(renderMarkdown('```ts\nload(\'a\')\n```')).toBe('<pre><code class="language-ts">load(\'a\')\n</code></pre>\n')
|
||||
expect(renderMarkdown('例子:\n\n f(1)')).toBe('<p>例子:</p>\n<pre><code>f(1)\n</code></pre>\n')
|
||||
expect(renderMarkdown('# 标题')).toBe('<h1>标题</h1>\n')
|
||||
expect(renderMarkdown('> 引用')).toBe('<blockquote>\n<p>引用</p>\n</blockquote>\n')
|
||||
expect(renderMarkdown('| a | b |\n| - | - |\n| 1 | 2 |')).toContain('<table>')
|
||||
})
|
||||
|
||||
it('源码里的 HTML 当文字显示', () => {
|
||||
expect(renderMarkdown('用 <b>mask</b> <script>alert(1)</script>')).toBe(
|
||||
'<p>用 <b>mask</b> <script>alert(1)</script></p>\n',
|
||||
)
|
||||
expect(renderMarkdown('<div onclick="x()">块</div>')).toBe('<p><div onclick="x()">块</div></p>\n')
|
||||
})
|
||||
|
||||
it('带协议的网址、邮箱自动成链接;不带协议的文件名不算', () => {
|
||||
expect(renderMarkdown('见 https://example.com/phone')).toBe('<p>见 <a href="https://example.com/phone">https://example.com/phone</a></p>\n')
|
||||
expect(renderMarkdown('[文档](http://example.com)')).toBe('<p><a href="http://example.com">文档</a></p>\n')
|
||||
expect(renderMarkdown('找 dev@example.com')).toContain('<a href="mailto:dev@example.com">')
|
||||
expect(renderMarkdown('改 sample.py 和 README.md')).toBe('<p>改 sample.py 和 README.md</p>\n')
|
||||
})
|
||||
|
||||
it('链接只认 http / https / mailto,别的照原文显示;图片不渲染', () => {
|
||||
expect(renderMarkdown('[点](javascript:alert(1))')).not.toContain('<a')
|
||||
expect(renderMarkdown('[命令](command:workbench.action.reloadWindow)')).not.toContain('<a')
|
||||
expect(renderMarkdown('[相对](./x.md)')).not.toContain('<a')
|
||||
expect(renderMarkdown('')).not.toContain('<img')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
||||
/**
|
||||
* 注释、函数说明的 Markdown 排版:CommonMark 加 GFM 的表格、删除线,带 http(s):// 的网址和邮箱自动成链接。
|
||||
* 源码里的 HTML 当文字显示;链接只认 http / https / mailto,别的写法照原文显示——
|
||||
* webview 会拦下 <a> 的点击交给 VS Code 在外部打开。图片不渲染:webview 的 CSP 不放行外部图片。
|
||||
*/
|
||||
const md = new MarkdownIt({ html: false, linkify: true }).disable('image')
|
||||
md.validateLink = url => /^(?:https?|mailto):/i.test(url)
|
||||
|
||||
/** 最近渲染过的文字 → HTML,最多留 CACHE_SIZE 段,最久没用的先丢。 */
|
||||
const CACHE_SIZE = 500
|
||||
const cache = new Map<string, string>()
|
||||
|
||||
/** Markdown 原文 → HTML;同一段文字只渲染一次。 */
|
||||
export function renderMarkdown(text: string): string {
|
||||
const html = cache.get(text) ?? md.render(text)
|
||||
cache.delete(text)
|
||||
cache.set(text, html)
|
||||
if (cache.size > CACHE_SIZE)
|
||||
cache.delete(cache.keys().next().value!)
|
||||
return html
|
||||
}
|
||||
@@ -56,6 +56,17 @@ describe('跳到定义:滚到哪块、展开哪些', () => {
|
||||
expect(revealPlan(BLOCKS, 4, 102)).toEqual({ id: '1.0.0', expand: ['1', '1.0'] })
|
||||
})
|
||||
|
||||
it('注释块、函数「说明」的 Markdown 里的名字不带下标,不算显示着定义', () => {
|
||||
const blocks: BlockView[] = [
|
||||
{ id: '0', kind: 'comment', startLine: 0, endLine: 0, code: '', codeTokens: [], children: [], translation: { markdown: '见 `get_db`' } },
|
||||
factBlock('1', 'function', 1, [
|
||||
{ label: '说明', value: { markdown: '同 `get_db`' } },
|
||||
{ label: '输入参数', value: [{ text: '1 个', role: 'hint' }], items: [{ cells: [[name('get_db', 30)]] }] },
|
||||
]),
|
||||
]
|
||||
expect(revealPlan(blocks, 0, 30)).toEqual({ id: '1', expand: ['1/p1'] })
|
||||
})
|
||||
|
||||
it('翻译插件没标 defines、名字在属性行展开的子行里(参数):闪整块,连那一行一起展开', () => {
|
||||
expect(revealPlan(BLOCKS, 2, 57)).toEqual({ id: '1', expand: ['1/p2'] })
|
||||
expect(revealPlan(BLOCKS, 0, 34)).toEqual({ id: '0', expand: ['0/p1'] })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BlockView, Segment, SourceRange } from './types'
|
||||
import type { BlockView, PreviewText, SourceRange } from './types'
|
||||
import { propertyItemId, propertyRowId } from './viewState'
|
||||
|
||||
/** 跳到定义时要滚到的块,就是这处定义的属性行 / 子行(只闪它),以及要展开的块和属性行:它们折叠着,目标就不在页面上。 */
|
||||
@@ -30,8 +30,9 @@ function covers(range: SourceRange | undefined, offset: number): boolean {
|
||||
return range !== undefined && range.startIndex <= offset && offset < range.endIndex
|
||||
}
|
||||
|
||||
function shows(segments: Segment[], offset: number): boolean {
|
||||
return segments.some(segment => segment.offset === offset)
|
||||
/** 文字里显示着 offset 处的名字;Markdown 说明里的名字不带下标。 */
|
||||
function shows(text: PreviewText, offset: number): boolean {
|
||||
return !('markdown' in text) && text.some(segment => segment.offset === offset)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,12 @@ export type TokenRole
|
||||
/** offset:照抄自源码里的名字、字符串时它在源码里的起始下标,⌘ / Ctrl 悬停、点击时拿它查定义;译词、提示词没有 */
|
||||
export interface Segment { text: string, role: TokenRole, offset?: number }
|
||||
|
||||
/** 一段 Markdown 说明文字:注释、JSDoc、docstring 去掉注释符号、引号和共同缩进后的原文,按 Markdown 渲染成注释色的段落、列表、代码块 */
|
||||
export interface Markdown { markdown: string }
|
||||
|
||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数的说明) */
|
||||
export type PreviewText = Segment[] | Markdown
|
||||
|
||||
/** 源码里一段的下标,end 为开区间 */
|
||||
export interface SourceRange { startIndex: number, endIndex: number }
|
||||
|
||||
@@ -29,10 +35,10 @@ export interface Defines { defines?: SourceRange }
|
||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值 */
|
||||
export interface PropertyItem extends Defines { cells: Segment[][] }
|
||||
|
||||
/** 属性表的一行:左列属性名,右列着色的值;items 是展开在这一行下面的子行(输入参数、导入项) */
|
||||
/** 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项) */
|
||||
export interface PropertyRow extends Defines {
|
||||
label: string
|
||||
value: Segment[]
|
||||
value: PreviewText
|
||||
items?: PropertyItem[]
|
||||
}
|
||||
|
||||
@@ -52,8 +58,8 @@ interface BlockViewBase {
|
||||
children: BlockView[]
|
||||
}
|
||||
|
||||
/** 函数、导入、常量、变量块预览时是属性表,其余是一行译文 */
|
||||
export type BlockView = BlockViewBase & ({ translation: Segment[] } | { properties: PropertyRow[] })
|
||||
/** 函数、导入、常量、变量块预览时是属性表,注释块是 Markdown 说明,其余是一行译文 */
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: PropertyRow[] })
|
||||
|
||||
export interface TranslatorOption { id: string, label: string }
|
||||
|
||||
|
||||
Reference in new issue
Block a user