Files
superwork/vscode/src/codeReader/translators/zhTemplate.ts
T

435 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Access, FactBlock, FunctionFacts, ImportFacts, ImportItem, Node, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
export interface ContextualWord {
/** 没有匹配的语境时用的译法。 */
zh: string
/**
* 语境 → 译法。语境是关键字节点往上的节点类型链,外层在前、用 ` > ` 连接:
* `raise_statement` 指父节点,`for_statement > else_clause` 指父节点 else_clause 且祖父节点 for_statement。
* 同时匹配多条时取最长的那条。
*/
byContext: Record<string, string>
}
/**
* 节点模板的一项。字符串是插入的译词,按关键字着色;另外两种是子节点槽位:
* `{ field }` 按 tree-sitter 字段名取子节点,`{ namedChild }` 按具名子节点序号(0 起,不计注释)取,给语法没定义字段名的节点用。
*/
export type TemplatePart = string | { field: string } | { namedChild: number }
/** 一种语言的中文直译表。引擎替换表里的关键字、内置类型、运算符,按节点模板重排少数语法结构,其余原样照抄。 */
export interface ZhTable {
id: string
label: string
languageIds: string[]
/** 未翻译的词按源码着色的词性上色;关键字译文的词性也取它。 */
tokenRole: (leaf: Node) => TokenRole
/** 关键字:按语法树里匿名节点的类型查(Python 的 `not in` 这类组合关键字本身也是一个匿名节点);随所在语法节点换译法的写成 ContextualWord。 */
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
/**
* 节点模板:节点类型 → 按中文语序重排的译文。命中的节点只输出模板里的译词和槽位,节点自己的关键字、标点不再输出;
* 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。
*/
nodeTemplates: Record<string, TemplatePart[]>
}
interface Token {
text: string
role: TokenRole
translated: boolean
/** 源码里的原文,用来认出逗号、括号;模板插入的译词没有原文。 */
raw: string
/** 前面要空一格:源码里它前面有空白,或者模板在它前面插了别的项。 */
gap: boolean
/** 照抄的名字、字符串在源码里的起始下标,见 Segment.offset。 */
offset?: number
}
/** 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。 */
interface Collector {
source: string
tokens: Token[]
cursor: number
gap: boolean
}
const CONTEXT_SEPARATOR = ' > '
const CLOSING = new Set([')', ']', '}'])
const HEADER_OPENERS = new Set(['{', ':'])
/** 源码着色里代表名字的词性:着这几种色的具名叶子是标识符、属性名、类型名,能查定义。 */
const NAME_ROLES = new Set<TokenRole>(['variable', 'function', 'type'])
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, ' ')
}
/** 模板槽位引用的子节点;注释是语法之外的节点,不占具名子节点序号。 */
function slotChild(node: Node, slot: Exclude<TemplatePart, string>): Node | null | undefined {
if ('field' in slot)
return node.childForFieldName(slot.field)
return node.namedChildren.filter(child => child && !child.isExtra)[slot.namedChild]
}
export function createZhTemplateTranslator(table: ZhTable): Translator {
const operatorParents = new Set(table.operatorParents)
const stringNodes = new Set(table.stringNodes)
const contextDepth = Math.max(0, ...Object.values(table.keywords).flatMap(word =>
typeof word === 'string' ? [] : Object.keys(word.byContext).map(context => context.split(CONTEXT_SEPARATOR).length)))
/** 从父节点往上逐层拼出语境去查,更长(更具体)的匹配覆盖更短的。 */
function keywordText(word: string | ContextualWord, node: Node): string {
if (typeof word === 'string')
return word
let text = word.zh
let context = ''
let ancestor = node.parent
for (let level = 0; ancestor && level < contextDepth; level++, ancestor = ancestor.parent) {
context = context ? `${ancestor.type}${CONTEXT_SEPARATOR}${context}` : ancestor.type
text = word.byContext[context] ?? text
}
return text
}
/** 照抄这个节点时带不带源码下标:名字(按名字着色的具名叶子)和整段字符串带,标点、数字、关键字不带。 */
function offsetOf(node: Node): number | undefined {
const linkable = stringNodes.has(node.type) || (node.childCount === 0 && node.isNamed && NAME_ROLES.has(table.tokenRole(node)))
return linkable ? node.startIndex : undefined
}
/** 能整个换成中文的节点 → 译文和词性。 */
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)
return { text: keywordText(word, node), 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
}
/** 节点命中模板时,模板各项换成译词或子节点;不该重排时返回 undefined。 */
function templateParts(node: Node, span: SourceSpan): (string | Node)[] | undefined {
const template = table.nodeTemplates[node.type]
if (!template || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
return undefined
const parts = template.map(part => typeof part === 'string' ? part : slotChild(node, part))
return parts.every((part): part is string | Node => part != null) ? parts : undefined
}
/** 源码里上一个词到 index 之间有空白,下一个词前面就空一格。 */
function advance(out: Collector, index: number): void {
out.gap ||= /\s/.test(out.source.slice(out.cursor, index))
out.cursor = index
}
function emit(out: Collector, token: Omit<Token, 'gap'>): void {
out.tokens.push({ ...token, gap: out.gap })
out.gap = false
}
function emitSource(out: Collector, node: Node, token: Omit<Token, 'gap' | 'raw'>): void {
advance(out, node.startIndex)
emit(out, { ...token, raw: node.text })
out.cursor = node.endIndex
}
function collect(node: Node, span: SourceSpan, out: Collector): void {
if (node.endIndex <= span.startIndex || node.startIndex >= span.endIndex || node.startIndex === node.endIndex)
return
if (node.type === 'comment') {
emitSource(out, node, { text: '', role: 'comment', translated: false })
return
}
const parts = templateParts(node, span)
if (parts) {
advance(out, node.startIndex)
parts.forEach((part, i) => {
out.gap ||= i > 0
if (typeof part === 'string') {
emit(out, { text: part, role: 'keyword', translated: true, raw: '' })
return
}
out.cursor = part.startIndex
collect(part, span, out)
})
out.cursor = node.endIndex
return
}
const translated = translateNode(node)
if (translated) {
emitSource(out, node, { ...translated, translated: true })
return
}
if (node.childCount === 0 || stringNodes.has(node.type)) {
const role = stringNodes.has(node.type) ? 'string' : table.tokenRole(node)
emitSource(out, node, { text: oneLine(node.text), role, translated: false, offset: offsetOf(node) })
return
}
for (const child of node.children) {
if (child)
collect(child, span, out)
}
}
/** 按译文顺序收集 node 落在 span 里的词。 */
function tokensIn(node: Node, span: SourceSpan, source: string): Token[] {
const out: Collector = { source, tokens: [], cursor: span.startIndex, gap: false }
collect(node, span, out)
return out.tokens
}
/** node 的源码原样照抄:叶子按源码着色(给了 role 就都按它),叶子之间的空白、标点照抄。 */
function verbatim(node: Node, source: string, role?: TokenRole): Segment[] {
const segments: Segment[] = []
let cursor = node.startIndex
const visit = (current: Node): void => {
if (current.startIndex === current.endIndex)
return
if (current.childCount > 0) {
for (const child of current.children) {
if (child)
visit(child)
}
return
}
pushSegment(segments, source.slice(cursor, current.startIndex), 'plain')
pushSegment(segments, current.text, role ?? table.tokenRole(current), offsetOf(current))
cursor = current.endIndex
}
visit(node)
pushSegment(segments, source.slice(cursor, node.endIndex), 'plain')
return segments
}
function translateComment(block: TextBlock): 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: TextBlock, source: string): Segment[] {
if (block.kind === 'comment')
return translateComment(block)
const tokens = tokensIn(block.node, block.header, source)
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()
return joinTokens(tokens)
}
function describe(block: FactBlock, source: string): PropertyRow[] {
const { header } = block
const phrases: Phrases = {
// 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致
translated: (node) => {
const clipped = node.endIndex > header.endIndex
const span = {
startIndex: node.startIndex,
endIndex: clipped ? header.endIndex : node.endIndex,
startRow: node.startPosition.row,
endRow: clipped ? header.endRow : node.endPosition.row,
}
return joinTokens(tokensIn(node, span, source))
},
verbatim: node => verbatim(node, source),
// 点分模块名逐节照抄,每一节各自能跳到定义,和 VS Code 里一样
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))],
}
switch (block.kind) {
case 'function':
return functionRows(block.facts, phrases)
case 'import':
return importRows(block.facts, phrases)
default:
return variableRows(block, phrases)
}
}
return { id: table.id, label: table.label, languageIds: table.languageIds, translate, describe }
}
function segment(text: string, role: TokenRole, offset: number | undefined): Segment {
return offset === undefined ? { text, role } : { text, role, offset }
}
function pushSegment(segments: Segment[], text: string, role: TokenRole, offset?: number): void {
if (!text)
return
const prev = segments.at(-1)
if (role === 'plain' && prev?.role === 'plain')
prev.text += text
else
segments.push(segment(text, role, offset))
}
/** 词连成译文:源码里隔着空白的词之间空一格,注释不进译文,右括号前的尾逗号去掉。 */
function joinTokens(tokens: Token[]): Segment[] {
const segments: Segment[] = []
let gap = false
tokens.forEach((token, i) => {
gap ||= token.gap
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)
pushSegment(segments, ' ', 'plain')
pushSegment(segments, token.text, token.role, token.offset)
// 「非」是个词,后面补一个空格,不和操作数粘在一起
gap = token.translated && token.raw === '!'
})
return segments
}
// ─── 属性表 ───────────────────────────────────────────────────────────────────
/** 属性表里值的三种来源。 */
interface Phrases {
/** 按表翻译,只翻落在块头部里的那一段。 */
translated: (node: Node) => Segment[]
/** 源码原样照抄、按源码着色(装饰器)。 */
verbatim: (node: Node) => Segment[]
/** 导入来源:字符串字面量按字符串着色,模块名按类型着色。 */
module: (node: Node) => Segment[]
/** 源码里的一个名字原样照抄、按 role 着色(函数名称)。 */
name: (node: Node, role: TokenRole) => Segment[]
}
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
function hint(text: string): Segment {
return { text, role: 'hint' }
}
function yesNo(value: boolean): Segment[] {
return [{ text: value ? '是' : '否', role: 'storage' }]
}
function count(n: number): Segment[] {
return [hint(n > 0 ? `${n} 个` : '无')]
}
function joined(parts: Segment[][], separator: string): Segment[] {
return parts.flatMap((part, i) => i > 0 ? [{ text: separator, role: 'plain' as const }, ...part] : part)
}
function accessRows(access: Access | undefined): PropertyRow[] {
return access ? [{ label: '访问级别', value: [{ text: ACCESS_ZH[access], role: 'keyword' }] }] : []
}
function aliased(item: Segment[], alias: Node | undefined, say: Phrases): Segment[] {
if (!alias)
return item
return [...item, { text: ' ', role: 'plain' }, { text: '作为', role: 'keyword' }, { text: ' ', role: 'plain' }, ...say.translated(alias)]
}
/** 函数:名称 / 访问级别 / 异步 / 装饰器 / 输入参数(每个参数一行:名称、类型、默认值)/ 输出参数。 */
function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
const decorators = facts.decorators.map(say.verbatim)
return [
{ label: '名称', value: facts.name ? say.name(facts.name, 'function') : [hint('匿名')] },
...accessRows(facts.access),
{ label: '异步', value: yesNo(facts.async) },
...(decorators.length > 0 ? [{ label: '装饰器', value: joined(decorators, '\n') }] : []),
{
label: '输入参数',
value: count(facts.parameters.length),
items: facts.parameters.map(param => ({
cells: [
param.optional ? [...say.translated(param.name), { text: '?', role: 'plain' }] : say.translated(param.name),
param.type ? say.translated(param.type) : [hint('未标注')],
param.default ? [hint('默认 '), ...say.translated(param.default)] : [],
],
})),
},
{ label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')] },
]
}
/** 导入:来源 / 仅类型(只有分这个的语言才有)/ 导入项(每项一行,有别名写成「名称 作为 别名」)。 */
function importRows(facts: ImportFacts, say: Phrases): PropertyRow[] {
const itemText = (item: ImportItem): Segment[] => {
switch (item.kind) {
case 'named':
return aliased(say.translated(item.name), item.alias, say)
case 'default':
return aliased([hint('默认导出')], item.alias, say)
case 'namespace':
return aliased([hint('全部')], item.alias, say)
case 'module':
// 一条语句导入多个模块时(Python 的 import a, b)要写出是哪个模块
return aliased(facts.sources.length > 1 ? say.module(item.module) : [hint('整个模块')], item.alias, say)
}
}
return [
{ label: '来源', value: facts.sources.length > 0 ? joined(facts.sources.map(say.module), ', ') : [hint('无')] },
...(facts.typeOnly === undefined ? [] : [{ label: '仅类型', value: yesNo(facts.typeOnly) }]),
{ label: '导入项', value: count(facts.items.length), items: facts.items.map(item => ({ cells: [itemText(item)] })) },
]
}
/** 常量 / 变量:每个声明的变量依次是名称 / 访问级别 / 类型(有标注才有)/ 值(变量叫初始值)。 */
function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
const valueLabel = block.kind === 'const' ? '值' : '初始值'
return block.facts.declarators.flatMap(declarator => [
{ label: '名称', value: say.translated(declarator.name) },
...accessRows(block.facts.access),
...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []),
{ label: valueLabel, value: declarator.value ? say.translated(declarator.value) : [hint('无')] },
])
}