✨ feat(vscode): 代码阅读的解析与模板直译核心

This commit is contained in:
cruldra committed 2026-09-24 21:16:21 +08:00
1 parent af56985d9a
commit 4d34b8c715
24 files changed
+2294

No files matched your search

@@ -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 }
}