✨ feat(vscode): 代码阅读的解析与模板直译核心
This commit is contained in:
1 parent
af56985d9a
commit
4d34b8c715
24 files changed
+2294
No files matched your search
@@ -0,0 +1,77 @@
|
||||
import type { Node, Translator } from '../contract'
|
||||
import { insideAnnotation, pythonTokenRole } from '../languages/python'
|
||||
import { createZhTemplateTranslator } from './zhTemplate'
|
||||
|
||||
function stringContent(node: Node): string {
|
||||
if (node.type === 'string_content')
|
||||
return node.text
|
||||
return node.children.map(child => child ? stringContent(child) : '').join('')
|
||||
}
|
||||
|
||||
/** `#` 注释去掉井号;docstring 只取引号里的内容。 */
|
||||
function commentText(node: Node): string {
|
||||
return node.type === 'comment' ? node.text.replace(/^#+\s?/, '') : stringContent(node)
|
||||
}
|
||||
|
||||
export const pythonZhTranslator: Translator = createZhTemplateTranslator({
|
||||
id: 'python.zh-template',
|
||||
label: '中文直译',
|
||||
languageIds: ['python'],
|
||||
tokenRole: pythonTokenRole,
|
||||
keywords: {
|
||||
'import': '导入',
|
||||
'from': '来自',
|
||||
'as': '作为',
|
||||
'def': '函数',
|
||||
'async': '异步',
|
||||
'await': '等待',
|
||||
'class': '类',
|
||||
'return': '返回',
|
||||
'if': '如果',
|
||||
'elif': '否则如果',
|
||||
'else': '否则',
|
||||
'for': '遍历',
|
||||
'in': { zh: '在', byParent: { for_statement: '取自', for_in_clause: '取自' } },
|
||||
'while': '当',
|
||||
'try': '尝试',
|
||||
'except': '捕获',
|
||||
'finally': '最终',
|
||||
'raise': '抛出',
|
||||
'with': '使用',
|
||||
'yield': '产出',
|
||||
'lambda': '匿名函数',
|
||||
'pass': '跳过',
|
||||
'break': '跳出',
|
||||
'continue': '继续',
|
||||
'global': '全局',
|
||||
'nonlocal': '非局部',
|
||||
'del': '删除',
|
||||
'assert': '断言',
|
||||
'not': '非',
|
||||
'and': '与',
|
||||
'or': '或',
|
||||
'is': '是',
|
||||
'not in': '不在',
|
||||
'is not': '不是',
|
||||
'match': '匹配',
|
||||
'case': '情况',
|
||||
},
|
||||
literals: { none: '空', true: '真', false: '假' },
|
||||
builtinTypes: {
|
||||
str: '字符串',
|
||||
int: '整数',
|
||||
float: '浮点数',
|
||||
bool: '布尔',
|
||||
list: '列表',
|
||||
dict: '字典',
|
||||
set: '集合',
|
||||
tuple: '元组',
|
||||
bytes: '字节串',
|
||||
},
|
||||
isTypePosition: leaf => leaf.type === 'identifier' && leaf.parent?.type !== 'attribute' && insideAnnotation(leaf),
|
||||
operators: { '==': '等于', '!=': '不等于' },
|
||||
operatorParents: ['comparison_operator'],
|
||||
emptyLiterals: { list: '空列表', dictionary: '空字典' },
|
||||
stringNodes: ['string'],
|
||||
commentText,
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { Node, Translator } from '../contract'
|
||||
import { typescriptTokenRole } from '../languages/typescript'
|
||||
import { createZhTemplateTranslator } from './zhTemplate'
|
||||
|
||||
function commentText(node: Node): string {
|
||||
const text = node.text
|
||||
if (text.startsWith('//'))
|
||||
return text.replace(/^\/{2,}\s?/, '')
|
||||
return text
|
||||
.replace(/^\/\*+/, '')
|
||||
.replace(/\*+\/$/, '')
|
||||
.split('\n')
|
||||
.map(line => line.replace(/^\s*\*(?!\/)\s?/, ''))
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
export const typescriptZhTranslator: Translator = createZhTemplateTranslator({
|
||||
id: 'typescript.zh-template',
|
||||
label: '中文直译',
|
||||
languageIds: ['typescript', 'typescriptreact', 'javascript', 'javascriptreact'],
|
||||
tokenRole: typescriptTokenRole,
|
||||
keywords: {
|
||||
// 流程
|
||||
import: '导入',
|
||||
from: '来自',
|
||||
as: '作为',
|
||||
export: '公共',
|
||||
default: '默认',
|
||||
if: '如果',
|
||||
else: '否则',
|
||||
for: '遍历',
|
||||
of: '取自',
|
||||
in: '在',
|
||||
while: '当',
|
||||
do: '执行',
|
||||
switch: '分支',
|
||||
case: '情况',
|
||||
break: '跳出',
|
||||
continue: '继续',
|
||||
return: '返回',
|
||||
throw: '抛出',
|
||||
try: '尝试',
|
||||
catch: '捕获',
|
||||
finally: '最终',
|
||||
await: '等待',
|
||||
yield: '产出',
|
||||
// 声明
|
||||
const: '常量',
|
||||
let: '变量',
|
||||
var: '变量',
|
||||
function: '函数',
|
||||
async: '异步',
|
||||
class: '类',
|
||||
extends: '继承',
|
||||
implements: '实现',
|
||||
interface: '接口',
|
||||
type: '类型',
|
||||
enum: '枚举',
|
||||
new: '新建',
|
||||
instanceof: '属于',
|
||||
typeof: '类型为',
|
||||
keyof: '键名',
|
||||
readonly: '只读',
|
||||
public: '公共',
|
||||
private: '私有',
|
||||
protected: '受保护',
|
||||
static: '静态',
|
||||
abstract: '抽象',
|
||||
delete: '删除',
|
||||
},
|
||||
literals: { true: '真', false: '假', null: '空', undefined: '未定义' },
|
||||
builtinTypes: {
|
||||
string: '字符串',
|
||||
number: '数值',
|
||||
boolean: '布尔',
|
||||
any: '任意',
|
||||
unknown: '未知',
|
||||
never: '永不',
|
||||
void: '无',
|
||||
object: '对象',
|
||||
bigint: '大整数',
|
||||
symbol: '符号',
|
||||
},
|
||||
isTypePosition: leaf => leaf.parent?.type === 'predefined_type',
|
||||
operators: { '===': '全等', '!==': '不全等', '==': '等于', '!=': '不等于', '&&': '与', '||': '或', '!': '非' },
|
||||
operatorParents: ['binary_expression', 'unary_expression'],
|
||||
emptyLiterals: { array: '空数组' },
|
||||
stringNodes: ['string', 'template_string', 'regex'],
|
||||
commentText,
|
||||
})
|
||||
@@ -0,0 +1,179 @@
|
||||
import type { LanguageAdapter, Segment, Translator } from '../contract'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createPythonAdapter } from '../languages/python'
|
||||
import { createTypeScriptAdapter } from '../languages/typescript'
|
||||
import { readFixture, walk, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { pythonZhTranslator } from './pythonZh'
|
||||
import { typescriptZhTranslator } from './typescriptZh'
|
||||
|
||||
const ts = createTypeScriptAdapter(WASM_DIR)
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
|
||||
interface Translated { kind: string, segments: Segment[] }
|
||||
|
||||
/** 每块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。 */
|
||||
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))
|
||||
byLine.set(line, { kind: block.kind, segments: translator.translate(block, source) })
|
||||
})
|
||||
return byLine
|
||||
})
|
||||
}
|
||||
|
||||
function text(segments: Segment[] | undefined): string {
|
||||
return (segments ?? []).map(segment => segment.text).join('')
|
||||
}
|
||||
|
||||
function texts(map: Map<number, Translated>): Record<number, string> {
|
||||
return Object.fromEntries([...map].map(([line, { segments }]) => [line, text(segments)]))
|
||||
}
|
||||
|
||||
const CJK = /\p{Script=Han}/u
|
||||
const CJK_ONLY = /^\p{Script=Han}+$/u
|
||||
const WORD_CHAR = /[\p{Script=Han}\w]/u
|
||||
|
||||
/** 被翻的词单独成段,而且不和前后的字母、数字、汉字粘在一起。 */
|
||||
function expectWordsApart(map: Map<number, Translated>): void {
|
||||
for (const [line, { kind, segments }] of map) {
|
||||
if (kind === 'comment')
|
||||
continue
|
||||
const full = text(segments)
|
||||
let at = 0
|
||||
for (const segment of segments) {
|
||||
if (segment.role !== 'string' && CJK.test(segment.text)) {
|
||||
expect(segment.text, `L${line} ${full}`).toMatch(CJK_ONLY)
|
||||
for (const neighbor of [full[at - 1], full[at + segment.text.length]])
|
||||
expect(neighbor ?? ' ', `L${line} "${full}" 里的「${segment.text}」`).not.toMatch(WORD_CHAR)
|
||||
}
|
||||
at += segment.text.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('typeScript 中文直译', () => {
|
||||
it('spec 样例逐字一致', async () => {
|
||||
const env = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
|
||||
const issue = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt'))
|
||||
expect(text(env.get(23)?.segments)).toBe('公共 异步 函数 findEnvFiles(workspaceRoot: 字符串): Promise<字符串[]>')
|
||||
expect(text(env.get(38)?.segments)).toBe('异步 函数 chmodAll(workspaceRoot: 字符串, mode: 数值): Promise<EnvLockResult>')
|
||||
expect(text(env.get(12)?.segments)).toBe('导入 { promises 作为 fsp } 来自 \'node:fs\'')
|
||||
expect(text(env.get(25)?.segments)).toBe('如果 (非 folder)')
|
||||
expect(text(env.get(26)?.segments)).toBe('返回 空数组')
|
||||
expect(text(env.get(41)?.segments)).toBe('遍历 (常量 f 取自 files)')
|
||||
expect(text(env.get(35)?.segments)).toBe('failed: { path: 字符串, reason: 字符串 }[]')
|
||||
expect(text(env.get(53)?.segments)).toBe('Set every `.env*` file in the workspace to 0o444 (read-only).')
|
||||
expect(text(issue.get(14)?.segments)).toBe('常量 issue = issues.find(i => i.number 全等 issueNumber)')
|
||||
expect(text(issue.get(20)?.segments)).toBe('返回 { locked: 真, prerequisiteNumber: prereq.number, prerequisiteColumn: prereq.column }')
|
||||
})
|
||||
|
||||
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[24]).toBe('常量 folder = workspace.workspaceFolders?.find(f => f.uri.fsPath 全等 workspaceRoot)')
|
||||
expect(env[28]).toBe('常量 uris = 等待 workspace.findFiles(include, EXCLUDE_PATTERN)')
|
||||
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(issue[1]).toBe('导入 类型 { Issue } 来自 \'../gitea/types\'')
|
||||
expect(issue[5]).toBe('prerequisiteNumber?: 数值')
|
||||
expect(issue[9]).toBe('纯判定:工单被未完成的前置挡住才算锁定。快照里找不到工单或前置一律放行, 与 webview 的 `isIssueLocked` 保持同一语义。')
|
||||
expect(issue[15]).toBe('如果 (非 issue 或 issue.prerequisite 全等 未定义)')
|
||||
})
|
||||
|
||||
it('每个被翻的词单独成段,着色取语法树词性', async () => {
|
||||
const env = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
|
||||
const issue = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt'))
|
||||
expect(env.get(23)?.segments.filter(segment => segment.role !== 'plain').slice(0, 4)).toEqual([
|
||||
{ text: '公共', role: 'keyword' },
|
||||
{ text: '异步', role: 'storage' },
|
||||
{ text: '函数', role: 'storage' },
|
||||
{ text: 'findEnvFiles', role: 'function' },
|
||||
])
|
||||
expect(issue.get(14)?.segments).toContainEqual({ text: '全等', role: 'operator' })
|
||||
expect(env.get(26)?.segments).toEqual([{ text: '返回', role: 'keyword' }, { text: ' ', role: 'plain' }, { text: '空数组', role: 'literal' }])
|
||||
expectWordsApart(env)
|
||||
expectWordsApart(issue)
|
||||
})
|
||||
|
||||
it('属性名、对象键、非空断言不被误翻', async () => {
|
||||
const source = [
|
||||
'const o = { if: 1, return: i.number, string: x!.type, new: a.default }',
|
||||
'let n = a !== b && !c || d == e != f',
|
||||
// eslint-disable-next-line no-template-curly-in-string -- 被翻译的源码文本里本来就有模板字符串
|
||||
'const s: string | void = typeof v === \'string\' ? `${v}` : \'number\'',
|
||||
].join('\n')
|
||||
const map = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))
|
||||
expect(map[1]).toBe('常量 o = { if: 1, return: i.number, string: x!.type, new: a.default }')
|
||||
expect(map[2]).toBe('变量 n = a 不全等 b 与 非 c 或 d 等于 e 不等于 f')
|
||||
// eslint-disable-next-line no-template-curly-in-string -- 模板字符串原样照抄进译文
|
||||
expect(map[3]).toBe('常量 s: 字符串 | 无 = 类型为 v 全等 \'string\' ? `${v}` : \'number\'')
|
||||
})
|
||||
|
||||
it('多行合成一行、去掉右括号前的尾逗号、行中注释不进译文', async () => {
|
||||
const source = [
|
||||
'call(',
|
||||
' first, // 第一个',
|
||||
' [],',
|
||||
')',
|
||||
].join('\n')
|
||||
expect(texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))[1]).toBe('call( first, 空数组 )')
|
||||
})
|
||||
})
|
||||
|
||||
describe('python 中文直译', () => {
|
||||
it('样例全文逐块一致', async () => {
|
||||
const map = await translateAll(py, 'python', pythonZhTranslator, readFixture('sample.py'))
|
||||
expect(texts(map)).toEqual({
|
||||
1: 'Scan a workspace for .env files.',
|
||||
3: '导入 os',
|
||||
4: '来自 pathlib 导入 Path 作为 P',
|
||||
6: 'MAX_DEPTH = 8',
|
||||
7: 'skipped = 空列表',
|
||||
10: '函数 find_env_files(root: 字符串) -> 列表[字符串]',
|
||||
11: 'Return every .env file under root.',
|
||||
12: '跳过备份文件',
|
||||
13: 'found = [p 遍历 p 取自 os.listdir(root) 如果 p.startswith(\'.env\') 与 非 p.endswith(\'.bak\')]',
|
||||
14: '如果 非 found',
|
||||
15: '返回 空列表',
|
||||
16: '否则如果 len(found) > MAX_DEPTH',
|
||||
17: 'found = found[:MAX_DEPTH]',
|
||||
18: '否则',
|
||||
19: '跳过',
|
||||
20: '遍历 name 取自 found',
|
||||
21: '如果 name 在 skipped',
|
||||
22: '继续',
|
||||
23: '尝试',
|
||||
24: '使用 open(os.path.join(root, found[0])) 作为 handle',
|
||||
25: 'handle.read()',
|
||||
26: '捕获 OSError 作为 err',
|
||||
27: '抛出 RuntimeError(str(err)) 来自 err',
|
||||
28: '最终',
|
||||
29: 'skipped.clear()',
|
||||
30: '返回 found',
|
||||
33: '类 EnvLockResult',
|
||||
34: 'total: 整数',
|
||||
35: 'ok: 列表[字符串] = 空列表',
|
||||
36: 'failed: 字典[字符串, 字符串] = 空字典',
|
||||
37: 'label = \'env\'',
|
||||
39: '函数 summary(self, verbose: 布尔 = 假) -> 字符串',
|
||||
40: '返回 f\'{self.total} files\' 如果 verbose 不是 空 否则 \'\'',
|
||||
})
|
||||
expectWordsApart(map)
|
||||
})
|
||||
|
||||
it('内置类型只在注解里翻,in 按位置区分取自 / 在', async () => {
|
||||
const source = [
|
||||
'def pick(items: dict[str, int], list: set) -> tuple:',
|
||||
' return [str(k) for k in items if k not in list]',
|
||||
].join('\n')
|
||||
const map = texts(await translateAll(py, 'python', pythonZhTranslator, source))
|
||||
expect(map[1]).toBe('函数 pick(items: 字典[字符串, 整数], list: 集合) -> 元组')
|
||||
expect(map[2]).toBe('返回 [str(k) 遍历 k 取自 items 如果 k 不在 list]')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,169 @@
|
||||
import type { CodeBlock, Node, Segment, SourceSpan, TokenRole, Translator } from '../contract'
|
||||
|
||||
/** 按父节点类型区分译法的关键字,例如 Python 的 in:for 头部里「取自」,比较里「在」。 */
|
||||
export interface ContextualWord { zh: string, byParent: Record<string, string> }
|
||||
|
||||
/** 一种语言的中文直译表。引擎只替换表里的三类东西,其余原样照抄。 */
|
||||
export interface ZhTable {
|
||||
id: string
|
||||
label: string
|
||||
languageIds: string[]
|
||||
/** 未翻译的词按源码着色的词性上色;关键字译文的词性也取它。 */
|
||||
tokenRole: (leaf: Node) => TokenRole
|
||||
/** 关键字:按语法树里匿名节点的类型查(Python 的 `not in` 这类组合关键字本身也是一个匿名节点)。 */
|
||||
keywords: Record<string, string | ContextualWord>
|
||||
/** 字面量关键字:具名的叶子节点(true / None …),按节点类型查。 */
|
||||
literals: Record<string, string>
|
||||
/** 内置类型:只在 isTypePosition 成立的叶子上按原文查。 */
|
||||
builtinTypes: Record<string, string>
|
||||
isTypePosition: (leaf: Node) => boolean
|
||||
/** 翻成文字的运算符:只认父节点在 operatorParents 里的运算符节点(TS 的非空断言 `x!` 不算)。 */
|
||||
operators: Record<string, string>
|
||||
operatorParents: string[]
|
||||
/** 空字面量(没有具名子节点)整个换成一个词:空数组 / 空列表 / 空字典。 */
|
||||
emptyLiterals: Record<string, string>
|
||||
/** 整段原样照抄、按字符串着色的节点。 */
|
||||
stringNodes: string[]
|
||||
/** 注释(或 docstring)节点去掉注释符号后的原文,可以多行。 */
|
||||
commentText: (node: Node) => string
|
||||
}
|
||||
|
||||
interface Token {
|
||||
start: number
|
||||
end: number
|
||||
text: string
|
||||
role: TokenRole
|
||||
translated: boolean
|
||||
/** 源码里的原文,用来认出逗号、括号。 */
|
||||
raw: string
|
||||
}
|
||||
|
||||
const CLOSING = new Set([')', ']', '}'])
|
||||
const HEADER_OPENERS = new Set(['{', ':'])
|
||||
|
||||
function firstLeaf(node: Node): Node {
|
||||
const first = node.child(0)
|
||||
return first ? firstLeaf(first) : node
|
||||
}
|
||||
|
||||
function commentsWithin(node: Node, span: SourceSpan): Node[] {
|
||||
if (node.endIndex <= span.startIndex || node.startIndex >= span.endIndex)
|
||||
return []
|
||||
if (node.type === 'comment')
|
||||
return [node]
|
||||
return node.children.flatMap(child => child ? commentsWithin(child, span) : [])
|
||||
}
|
||||
|
||||
/** 下一个要输出的词的原文(跳过注释)。 */
|
||||
function nextRaw(tokens: Token[], i: number): string {
|
||||
for (let j = i + 1; j < tokens.length; j++) {
|
||||
if (tokens[j].text !== '')
|
||||
return tokens[j].raw
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function oneLine(text: string): string {
|
||||
return text.replace(/\s*\n\s*/g, ' ')
|
||||
}
|
||||
|
||||
export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
const operatorParents = new Set(table.operatorParents)
|
||||
const stringNodes = new Set(table.stringNodes)
|
||||
|
||||
/** 能整个换成中文的节点 → 译文和词性。 */
|
||||
function translateNode(node: Node): { text: string, role: TokenRole } | undefined {
|
||||
if (node.childCount === 0 && table.isTypePosition(node)) {
|
||||
const type = table.builtinTypes[node.text]
|
||||
if (type)
|
||||
return { text: type, role: 'type' }
|
||||
}
|
||||
const parent = node.parent
|
||||
if (!node.isNamed) {
|
||||
if (parent && operatorParents.has(parent.type) && table.operators[node.type])
|
||||
return { text: table.operators[node.type], role: 'operator' }
|
||||
const word = table.keywords[node.type]
|
||||
if (word !== undefined) {
|
||||
const text = typeof word === 'string' ? word : (parent && word.byParent[parent.type]) || word.zh
|
||||
return { text, role: table.tokenRole(firstLeaf(node)) }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
if (node.childCount === 0 && table.literals[node.type])
|
||||
return { text: table.literals[node.type], role: table.tokenRole(node) }
|
||||
if (table.emptyLiterals[node.type] && node.namedChildCount === 0)
|
||||
return { text: table.emptyLiterals[node.type], role: 'literal' }
|
||||
return undefined
|
||||
}
|
||||
|
||||
function collect(node: Node, span: SourceSpan, out: Token[]): void {
|
||||
if (node.endIndex <= span.startIndex || node.startIndex >= span.endIndex || node.startIndex === node.endIndex)
|
||||
return
|
||||
const base = { start: node.startIndex, end: node.endIndex }
|
||||
if (node.type === 'comment') {
|
||||
out.push({ ...base, raw: node.text, text: '', role: 'comment', translated: false })
|
||||
return
|
||||
}
|
||||
const translated = translateNode(node)
|
||||
if (translated) {
|
||||
out.push({ ...base, raw: node.text, ...translated, translated: true })
|
||||
return
|
||||
}
|
||||
if (node.childCount === 0 || stringNodes.has(node.type)) {
|
||||
const role = stringNodes.has(node.type) ? 'string' : table.tokenRole(node)
|
||||
out.push({ ...base, raw: node.text, text: oneLine(node.text), role, translated: false })
|
||||
return
|
||||
}
|
||||
for (const child of node.children) {
|
||||
if (child)
|
||||
collect(child, span, out)
|
||||
}
|
||||
}
|
||||
|
||||
function translateComment(block: CodeBlock): Segment[] {
|
||||
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' }] : []
|
||||
}
|
||||
|
||||
function translate(block: CodeBlock, source: string): Segment[] {
|
||||
if (block.kind === 'comment')
|
||||
return translateComment(block)
|
||||
const tokens: Token[] = []
|
||||
collect(block.node, block.header, tokens)
|
||||
const compound = block.children.length > 0 || block.header.endIndex < block.node.endIndex
|
||||
const last = tokens.at(-1)
|
||||
if (compound && last && !last.translated && HEADER_OPENERS.has(last.raw))
|
||||
tokens.pop()
|
||||
|
||||
const segments: Segment[] = []
|
||||
const push = (text: string, role: TokenRole): void => {
|
||||
const prev = segments.at(-1)
|
||||
if (role === 'plain' && prev?.role === 'plain')
|
||||
prev.text += text
|
||||
else
|
||||
segments.push({ text, role })
|
||||
}
|
||||
let gap = false
|
||||
let prevEnd = block.header.startIndex
|
||||
tokens.forEach((token, i) => {
|
||||
gap ||= /\s/.test(source.slice(prevEnd, token.start))
|
||||
prevEnd = token.end
|
||||
const trailingComma = token.raw === ',' && !token.translated && CLOSING.has(nextRaw(tokens, i))
|
||||
if (token.text === '' || trailingComma) {
|
||||
gap ||= token.role === 'comment'
|
||||
return
|
||||
}
|
||||
if (gap && segments.length > 0)
|
||||
push(' ', 'plain')
|
||||
push(token.text, token.role)
|
||||
// 「非」是个词,后面补一个空格,不和操作数粘在一起
|
||||
gap = token.translated && token.raw === '!'
|
||||
})
|
||||
return segments
|
||||
}
|
||||
|
||||
return { id: table.id, label: table.label, languageIds: table.languageIds, translate }
|
||||
}
|
||||
Reference in new issue
Block a user