✨ feat(vscode): 代码阅读的解析与模板直译核心
This commit is contained in:
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 }
|
||||
}
|
||||
Reference in new issue
Block a user