import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommandContext, CommandFacts, CommandForm, CommandReading, CommandRule, CommandWord, CommonAttribute, DeclaratorFacts, Defines, DirectiveFacts, EmbeddedReading, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, HealthFacts, ImportFacts, ImportItem, Inline, InstructionFlag, InterpreterFacts, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, OptionsFacts, ParameterFacts, PositionalParameter, PreviewTable, PropertyItem, PropertyRow, Redirect, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, StageFacts, StartFacts, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract' import { commonIndent, splitLines } from '../editText' /** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */ export interface ContextualWord { /** 没有匹配的语境时用的译法。 */ zh: string /** * 语境 → 译法。语境是关键字节点往上的节点类型链,外层在前、用 ` > ` 连接: * `raise_statement` 指父节点,`for_statement > else_clause` 指父节点 else_clause 且祖父节点 for_statement。 * 同时匹配多条时取最长的那条。 */ byContext: Record } /** * 节点模板的一项。字符串是插入的译词,按关键字着色;另外两种是子节点槽位: * `{ field }` 按 tree-sitter 字段名取子节点,`{ namedChild }` 按具名子节点序号(0 起,不计注释)取,给语法没定义字段名的节点用。 */ export type TemplatePart = string | { field: string } | { namedChild: number } /** 一种语言的中文直译表。引擎替换表里的关键字、内置类型、运算符,按节点模板重排少数语法结构,其余原样照抄。 */ /** * 一种语言的中文词表:只有叫法和语法节点类型,语言知道的结构(词性、遍历)翻译时从 SourceDocument.language 取—— * 未翻译的词、关键字译文按它给的词性着色,它认出的遍历整个换成一个框,框里按读的顺序重排(见 iterationGroup)。 */ export interface ZhTable { id: string label: string languageIds: string[] /** 关键字:按语法树里匿名节点的类型查(Python 的 `not in` 这类组合关键字本身也是一个匿名节点);随所在语法节点换译法的写成 ContextualWord。 */ keywords: Record /** 字面量关键字:具名的叶子节点(true / None …),按节点类型查。 */ literals: Record /** 内置类型:只在 isTypePosition 成立的叶子上按原文查。 */ builtinTypes: Record isTypePosition: (leaf: Node) => boolean /** 翻成文字的运算符:只认父节点在 operatorParents 里的运算符节点(TS 的非空断言 `x!` 不算)。 */ operators: Record operatorParents: string[] /** 空字面量(没有具名子节点)整个换成一个词:空数组 / 空列表 / 空字典 / 空对象。 */ emptyLiterals: Record /** 字典 / 对象字面量(语言插件给了 entries 的值)的叫法,按节点类型查:属性表里的值写成「字典 3 项」,各项展开成子行。 */ mapLiterals: Record /** 整段原样照抄、按字符串着色的节点。 */ stringNodes: string[] /** 注释(或 docstring)节点去掉注释符号后的原文,可以多行;text 是节点的源码去掉代码阅读的记号(SourceDocument.markers)以后剩下的。 */ commentText: (node: Node, text: string) => string /** * 节点模板:节点类型 → 按中文语序重排的译文。命中的节点只输出模板里的译词和槽位,节点自己的关键字、标点不再输出; * 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。 */ nodeTemplates: Record /** * 节点读法:节点类型 → 怎么读成一行译文,给模板排不了的写法(shell 的 `${x:-d}`、`[ -f x ]`、`case`)。 * 命中的节点只输出读法给的,不再照常翻它的子节点;读法返回 undefined 就照常翻。读法里要翻子节点用 ReaderContext.say。 */ readers?: Record /** 并成一块的几条同种语句(TextBlock.group)怎么读:按第一条的节点类型查,读成一行字加一张表;没有的照常翻第一条。 */ groups?: Record /** 解释器开关(SwitchFacts.id)的叫法:errexit → 出错就停;没有的写源码里的写法。 */ switches?: Record /** 解析指令(ParserDirective.name)的叫法和补充说明:syntax → 语法(BuildKit 按这个版本读这份文件);没有的写原名。 */ directives?: Record } /** 读一个节点时能用的(见 ZhTable.readers)。 */ export interface ReaderContext { document: SourceDocument /** 照常翻一个节点(照样认读法、命令),只翻落在要翻的这一段里的部分。 */ say: (node: Node) => Inline[] /** node 整个落在要翻的这一段里:复合语句的块只翻头部,读法按它决定写不写主体里的东西。 */ whole: (node: Node) => boolean /** 源码里一个名字照抄、按 role 着色(不给 role 按语言插件定的词性),名字带源码下标。 */ name: (node: Node, role?: TokenRole) => Segment /** 读一条命令(命令规则读的,没有规则时命令名照抄、各个词照常翻),连重定向;不是命令时没有。 */ command: (node: Node) => CommandReading | undefined } /** 一种节点的读法:一行译文,可以带一张排在这一段下面的表(分情况的「情况 / 做什么」);返回 undefined 就照常翻。 */ export type NodeReader = (node: Node, context: ReaderContext) => Inline[] | { text: Inline[], table?: PreviewTable } | undefined /** 并成一块的几条同种语句的读法(见 ZhTable.groups):照源码顺序的各条。 */ export type GroupReader = (nodes: Node[], context: ReaderContext) => { text: Inline[], table?: PreviewTable } interface Token { /** 注释是空文字:不进译文,只在前后留一个空格。 */ text: string role: TokenRole translated: boolean /** 源码里的原文,用来认出逗号、括号;模板插入的译词、框没有原文。 */ raw: string /** 前面要空一格:源码里它前面有空白,或者模板在它前面插了别的项。 */ gap: boolean /** 照抄的名字、字符串在源码里的起始下标,见 Segment.offset。 */ offset?: number /** 整个换成一个框的节点(写在表达式里的遍历):输出这个框,text 为空。 */ group?: SegmentGroup } /** * 排在这一段译文下面的那张表:跨几行写的调用的实参表(见 emitCall),或读法给的表(分情况的「情况 / 做什么」)。 * 一段只排一张,排了的记在 table 里。 */ interface TableSlot { table?: PreviewTable } /** * 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。 * slot 是这一段下面能不能排一张表(跨几行写的调用、读法给的表);没有就照常翻。 */ interface Collector { document: SourceDocument /** 要翻的那个节点 */ root: Node tokens: Token[] cursor: number gap: boolean slot?: TableSlot } const CONTEXT_SEPARATOR = ' > ' const CLOSING = new Set([')', ']', '}']) /** 跟在一个式子后面、不属于这个式子的标点:右括号、逗号、分号、冒号。 */ const SEPARATORS = new Set([...CLOSING, ',', ';', ':']) const HEADER_OPENERS = new Set(['{', ':']) /** 源码着色里代表名字的词性:着这几种色的具名叶子是标识符、属性名、类型名,能查定义。 */ const NAME_ROLES = new Set(['variable', 'constant', '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) : []) } /** 节点的源码去掉整个落在它里面的记号(中文名记号 `@Comment 当前用户` 不是说明文字)。 */ function withoutMarkers(node: Node, markers: readonly SourceRange[]): string { let text = '' let cursor = node.startIndex for (const marker of markers.filter(m => m.startIndex >= node.startIndex && m.endIndex <= node.endIndex).sort((a, b) => a.startIndex - b.startIndex)) { text += node.text.slice(cursor - node.startIndex, marker.startIndex - node.startIndex) cursor = Math.max(cursor, marker.endIndex) } return text + node.text.slice(cursor - node.startIndex) } function sameSpan(a: Node, b: Node): boolean { return a.startIndex === b.startIndex && a.endIndex === b.endIndex && a.type === b.type } /** * 在要翻的 root 里,node 后面还紧跟着包着它的式子的别的部分(`.model_dump()`、`[0]`、`or x`):从 node 往上找第一个后面有东西的节点, * 那个东西不是右括号、逗号、分号、冒号(语法之外的注释不算)。 */ function continued(node: Node, root: Node): boolean { for (let current: Node | null = node; current && !sameSpan(current, root); current = current.parent) { let next = current.nextSibling while (next?.isExtra) next = next.nextSibling if (next) return !SEPARATORS.has(next.type) } return false } function isComment(token: Token): boolean { return token.text === '' && !token.group } /** 下一个要输出的词的原文(跳过注释)。 */ function nextRaw(tokens: Token[], i: number): string { for (let j = i + 1; j < tokens.length; j++) { if (!isComment(tokens[j])) return tokens[j].raw } return '' } function oneLine(text: string): string { return text.replace(/\s*\n\s*/g, ' ') } /** * 注释的 Markdown 原文:各个注释节点去掉注释符号后(texts)拼起来,去掉行尾空白、首尾空行和共同缩进,保留换行(段落之间的空行也留着)。 * 只有一个节点(块注释、docstring)时第一行紧跟在引号 / 注释符号后面,本来就没有缩进,不参与算共同缩进(同 PEP 257 的做法); * 几条行注释连成的一块每行地位相同,第一行也算,缩进四格的代码块才留得住。 */ 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, '') } /** 模板槽位引用的子节点;注释是语法之外的节点,不占具名子节点序号。 */ function slotChild(node: Node, slot: Exclude): Node | null | undefined { if ('field' in slot) return node.childForFieldName(slot.field) return node.namedChildren.filter(child => child && !child.isExtra)[slot.namedChild] } const ATTRIBUTE_ZH: Record = { kind: '种类', access: '访问级别', async: '异步', abstract: '抽象', typeOnly: '仅类型' } const KIND_ZH: Record = { import: '导入', function: '函数', class: '类', enum: '枚举', exception: '异常', type: '类型', field: '字段', const: '常量', variable: '变量', if: '条件', else: '否则', loop: '循环', try: '尝试', catch: '捕获', finally: '最终', return: '返回', statement: '语句', comment: '注释', interpreter: '解释器', options: '选项', case: '分情况', command: '命令', pipeline: '管道', directive: '解析指令', stage: '阶段', run: '执行', copy: '复制', setting: '设置', label: '标签', health: '健康检查', start: '启动', } const ACCESS_ZH: Record = { public: '公共', private: '私有', protected: '受保护' } const ITERATION_ZH: Record = { generator: '生成器', list: '列表', set: '集合', dict: '字典' } /** 被调用的名字定义成这几种块时,调用就是新建对象。 */ const CONSTRUCTED = new Set(['class', 'exception', 'enum']) const ARGUMENT_COLUMNS = ['参数', '值'] const YES_NO: Record<'true' | 'false', string> = { true: '是', false: '否' } /** 筛选属性和值的中文叫法;属性表里的访问级别、异步、抽象、仅类型这几行,属性名和值也用这一套。 */ const ZH_ATTRIBUTE_NAMES: AttributeNames = { attributes: ATTRIBUTE_ZH, values: { kind: KIND_ZH, access: ACCESS_ZH, async: YES_NO, abstract: YES_NO, typeOnly: YES_NO }, } 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, language: LanguageAdapter): number | undefined { const linkable = stringNodes.has(node.type) || (node.childCount === 0 && node.isNamed && NAME_ROLES.has(language.tokenRole(node))) return linkable ? node.startIndex : undefined } /** 能整个换成中文的节点 → 译文和词性。 */ function translateNode(node: Node, language: LanguageAdapter): { 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: language.tokenRole(firstLeaf(node)) } return undefined } if (node.childCount === 0 && table.literals[node.type]) return { text: table.literals[node.type], role: language.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 之间有空白,下一个词前面就空一格;跨行写的一对圆括号挤成一行时贴着里面的内容 * (`f(\n x\n)` 写成 `f(x)`),方括号、花括号照旧空一格。 */ function advance(out: Collector, index: number): void { const between = out.document.source.slice(out.cursor, index) const hugging = between.includes('\n') && (out.tokens.at(-1)?.raw === '(' || out.document.source[index] === ')') out.gap ||= !hugging && /\s/.test(between) out.cursor = index } function emit(out: Collector, token: Omit): void { out.tokens.push({ ...token, gap: out.gap }) out.gap = false } function emitSource(out: Collector, node: Node, token: Omit): void { advance(out, node.startIndex) emit(out, { ...token, raw: node.text }) out.cursor = node.endIndex } /** 语言插件认出的写在表达式里的遍历;没整个落在要翻的这一段里就不重排、照常翻。 */ function iterationIn(node: Node, span: SourceSpan, language: LanguageAdapter): IterationFacts | undefined { if (!language.iteration || node.startIndex < span.startIndex || node.endIndex > span.endIndex) return undefined return language.iteration(node) } /** * 遍历按读的顺序重排进一个框:「遍历 <来源> 的每个 <变量>」,紧挨着的几节 for 用顿号接着写(异步的写「异步遍历」, * 同步、异步换了就另起一句),条件在它所在的位置写「,仅当 <条件>」,最后「,取 <值>」,字典是「<键>: <值>」。 * 来源、变量、条件、值照常翻,里面的名字带源码下标,套着的遍历是框里的框。 */ function iterationGroup(facts: IterationFacts, span: SourceSpan, document: SourceDocument): SegmentGroup { const segments: Inline[] = [] const plain = (text: string): void => pushSegment(segments, text, 'plain') const word = (text: string): void => pushSegment(segments, text, 'keyword') const say = (node: Node): void => appendInline(segments, joinTokens(tokensIn(node, span, document))) let previous: IterationClause | undefined for (const clause of facts.clauses) { if (clause.kind === 'if') { plain(',') word('仅当') plain(' ') say(clause.condition) } else { if (previous?.kind === 'for' && previous.async === clause.async) { plain('、') } else { if (previous) plain(',') word(clause.async ? '异步遍历' : '遍历') plain(' ') } say(clause.source) plain(' ') word('的每个') plain(' ') say(clause.variables) } previous = clause } plain(',') word('取') plain(' ') if (facts.key) { say(facts.key) plain(': ') } say(facts.value) return { style: 'iteration', label: ITERATION_ZH[facts.result], segments } } /** * 遍历整个换成一个框;和外面的调用共用的那对括号照常写在框外,紧贴着框。 * 只翻这个遍历(它是调用的实参,排在实参表里)时外面的调用不在译文里,这对括号也不写。 */ function emitIteration(out: Collector, node: Node, facts: IterationFacts, span: SourceSpan): void { const alone = node.startIndex === out.root.startIndex && node.endIndex === out.root.endIndex const shared = alone ? undefined : facts.sharedParentheses const bracket = (paren: Node): Omit => ({ text: paren.text, role: out.document.language.tokenRole(paren), translated: false }) if (shared) emitSource(out, shared.open, bracket(shared.open)) else advance(out, node.startIndex) emit(out, { text: '', role: 'plain', translated: true, raw: '', group: iterationGroup(facts, span, out.document) }) if (shared) { out.cursor = shared.close.startIndex emitSource(out, shared.close, bracket(shared.close)) } out.cursor = node.endIndex } /** 要排成一行、实参摊成表的调用:实参列表跨了几行,整个落在要翻的这一段里,这一段还没排过别的调用。 */ function expandedCall(node: Node, span: SourceSpan, slot: TableSlot, { language: { call } }: SourceDocument): CallFacts | undefined { if (slot.table || !call || node.startIndex < span.startIndex || node.endIndex > span.endIndex) return undefined const facts = call(node) return facts && facts.argumentList.startPosition.row !== facts.argumentList.endPosition.row ? facts : undefined } /** * node 落在 span 里的那一段翻成一行译文,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall)。 * 一行译文的块、常量 / 变量的值、实参表里每个值各翻各的,各自排自己里面的那个调用。 */ function expandedText(node: Node, span: SourceSpan, document: SourceDocument): { text: Inline[], table?: PreviewTable } { const slot: TableSlot = {} const text = joinTokens(tokensIn(node, span, document, slot)) return { text, ...(slot.table && { table: slot.table }) } } /** * 实参表:参数 | 值,每个实参一行,照源码顺序。按名字传的写名字,按位置传的写「第 N 个」(只数按位置传的), * 展开的写「展开」(*args、...args)、「展开关键字」(**kwargs)。值照常翻;值里有跨几行写的调用时同样排成一行, * 它的实参表排在值那一格里(PropertyItem.table),一层层往下套。 */ function argumentTable(facts: CallFacts, span: SourceSpan, document: SourceDocument): PreviewTable { let position = 0 const label = (arg: CallArgument): Inline[] => { switch (arg.kind) { case 'keyword': return [segment(arg.name.text, 'variable', offsetOf(arg.name, document.language))] case 'positional': position++ return [hint(`第 ${position} 个`)] case 'spread': return [hint('展开')] case 'keywordSpread': return [hint('展开关键字')] } } const row = (arg: CallArgument): PropertyItem => { const { text, table } = expandedText(arg.value, span, document) return { cells: [label(arg), text], ...(table && { table }) } } return { columns: ARGUMENT_COLUMNS, items: facts.arguments.map(row) } } /** * 调用排成一行「新建 / 调用 被调用的名字 · N 个参数」,实参摊成表记在 slot 里。被调用的名字照常翻、带源码下标, * 最后那个名字新建时按类型、调用时按函数着色,前面几节按变量着色。新建还是调用看写法(CallFacts.construct), * 写法看不出来的问名字的定义:是类(含异常、枚举)就是新建,还没查到、查不到按调用算。 * 调用后面还紧跟着同一个式子的别的部分(见 continued)时整行括起来:「(新建 X · 3 个参数).model_dump()」。 */ function emitCall(out: Collector, slot: TableSlot, node: Node, facts: CallFacts, span: SourceSpan): void { slot.table = argumentTable(facts, span, out.document) const construct = facts.construct ?? CONSTRUCTED.has(out.document.calleeKind?.(facts)) const wrapped = continued(node, out.root) advance(out, node.startIndex) if (wrapped) emit(out, { text: '(', role: 'plain', translated: true, raw: '' }) emit(out, { text: construct ? '新建' : '调用', role: 'storage', translated: true, raw: '' }) out.gap = true out.cursor = facts.callee.startIndex const from = out.tokens.length collect(facts.callee, span, out) for (const token of out.tokens.slice(from)) { if (token.offset !== undefined && NAME_ROLES.has(token.role)) token.role = token.offset !== facts.name?.startIndex ? 'variable' : construct ? 'type' : 'function' } out.gap = true emit(out, { text: `· ${facts.arguments.length} 个参数`, role: 'hint', translated: true, raw: '' }) if (wrapped) emit(out, { text: ')', role: 'plain', translated: true, raw: '' }) out.cursor = node.endIndex } /** 读法用的:照常翻、看节点在不在这一段里、照抄名字、读命令。 */ function readerContext(out: Collector, span: SourceSpan): ReaderContext { const { document } = out return { document, say: node => joinTokens(tokensIn(node, span, document)), whole: node => node.startIndex >= span.startIndex && node.endIndex <= span.endIndex, name: (node, role) => segment(node.text, role ?? document.language.tokenRole(node), node.startIndex), command: (node) => { const facts = document.language.command?.(node) return facts && commandReading(facts, document) }, } } /** 读出来的几段接着前面的词输出;读法给了表、这一段还没排表时表排在这一段下面。 */ function emitReading(out: Collector, node: Node, reading: Inline[] | { text: Inline[], table?: PreviewTable }): void { const { text, table: below } = Array.isArray(reading) ? { text: reading, table: undefined } : reading if (below && out.slot && !out.slot.table) out.slot.table = below emitPieces(out, node, text) } function emitPieces(out: Collector, node: Node, pieces: Inline[]): void { advance(out, node.startIndex) for (const piece of pieces) { if (!('role' in piece)) emit(out, { text: '', role: 'plain', translated: true, raw: '', group: piece }) else if (piece.text) emit(out, { text: piece.text, role: piece.role, translated: true, raw: '', ...(piece.offset !== undefined && { offset: piece.offset }) }) } out.cursor = node.endIndex } /** * 一条命令:交给命令名的命令规则读(SourceDocument.commandRule);没有规则、规则不读时命令名照抄(按函数着色、带源码下标)、 * 各个词照常翻。后面跟着重定向的意思(见 redirectText);规则给了表(选项写得多的命令)就带着。 */ function commandReading(facts: CommandFacts, document: SourceDocument): CommandReading { const say = (node: Node): Inline[] => expression(node, document) const ruleFor = (name: CommandWord): CommandRule | undefined => document.commandRule?.(name.literal ?? name.node.text) const context: CommandContext = { displayLanguage: 'zh', translate: say, command: (name, args) => ruleFor(name)?.read({ name, arguments: args, redirects: [] }, context), } const reading = ruleFor(facts.name)?.read(facts, context) const name = facts.name.node const text = reading?.text ?? [ segment(facts.name.literal ?? name.text, 'function', name.startIndex), ...facts.arguments.flatMap(arg => [segment(' ', 'plain', undefined), ...say(arg.node)]), ] return { text: [...text, ...facts.redirects.flatMap(redirect => redirectText(redirect, say))], ...(reading?.value && { value: true }), ...(reading?.table && { table: reading.table }), } } 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 { language } = out.document const iteration = iterationIn(node, span, language) if (iteration) { emitIteration(out, node, iteration, span) return } const { slot } = out const call = slot && expandedCall(node, span, slot, out.document) if (slot && call) { emitCall(out, slot, node, call, span) return } const reading = table.readers?.[node.type]?.(node, readerContext(out, span)) if (reading) { emitReading(out, node, reading) return } const command = language.command?.(node) if (command) { emitReading(out, node, commandReading(command, out.document)) 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, language) if (translated) { emitSource(out, node, { ...translated, translated: true }) return } if (node.childCount === 0 || stringNodes.has(node.type)) { const role = stringNodes.has(node.type) ? 'string' : language.tokenRole(node) emitSource(out, node, { text: oneLine(node.text), role, translated: false, offset: offsetOf(node, language) }) return } for (const child of node.children) { if (child) collect(child, span, out) } } /** 按译文顺序收集 node 落在 span 里的词;给了 slot,跨几行写的调用排成一行、实参摊成表(见 emitCall)。 */ function tokensIn(node: Node, span: SourceSpan, document: SourceDocument, slot?: TableSlot): Token[] { const out: Collector = { document, root: node, tokens: [], cursor: span.startIndex, gap: false, ...(slot && { slot }) } collect(node, span, out) return out.tokens } /** node 的源码原样照抄:叶子按源码着色(给了 role 就都按它),叶子之间的空白、标点照抄。 */ function verbatim(node: Node, { source, language }: SourceDocument, 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 ?? language.tokenRole(current), offsetOf(current, language)) cursor = current.endIndex } visit(node) pushSegment(segments, source.slice(cursor, node.endIndex), 'plain') return segments } /** 注释节点去掉注释符号和记号后的原文。 */ function commentOf(node: Node, { markers }: SourceDocument): string { return table.commentText(node, withoutMarkers(node, markers)) } function translateComment(block: TextBlock, document: SourceDocument): 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] return { markdown: docText(nodes.map(node => commentOf(node, document))) } } function translate(block: TextBlock, document: SourceDocument): TextTranslation { if (block.kind === 'comment') return { text: translateComment(block, document) } const group = block.group && table.groups?.[block.node.type] if (block.group && group) { const out: Collector = { document, root: block.node, tokens: [], cursor: block.header.startIndex, gap: false } return group(block.group, readerContext(out, block.header)) } const slot: TableSlot = {} const tokens = tokensIn(block.node, block.header, document, slot) 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 { text: joinTokens(tokens), ...(slot.table && { table: slot.table }) } } function expression(node: Node, document: SourceDocument): Inline[] { return joinTokens(tokensIn(node, nodeSpan(node), document)) } function doc(node: Node, document: SourceDocument): Markdown | undefined { const markdown = docText([commentOf(node, document)]) return markdown ? { markdown } : undefined } function describe(block: FactBlock, document: SourceDocument): PropertyRow[] { const { header } = block const { language } = document // 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致 const clipped = (part: SourceSpan): SourceSpan => part.endIndex > header.endIndex ? { ...part, endIndex: header.endIndex, endRow: header.endRow } : part const within = (node: Node, part: SourceSpan): Inline[] => joinTokens(tokensIn(node, clipped(part), document)) const phrases: Phrases = { translated: node => within(node, nodeSpan(node)), within, expanded: node => expandedText(node, clipped(nodeSpan(node)), document), body: node => expression(node, document), returned: (node) => { const [value] = language.command?.(node)?.arguments ?? [] return value ? expression(value.node, document) : [] }, entryCount: (literal, size) => { const name = table.mapLiterals[literal.type] return [hint(name ? `${name} ${size} 项` : `${size} 项`)] }, verbatim: node => verbatim(node, document), // 点分模块名逐节照抄,每一节各自能跳到定义,和 VS Code 里一样 module: node => stringNodes.has(node.type) ? [segment(node.text, 'string', offsetOf(node, language))] : verbatim(node, document, 'type'), name: (node, role) => [segment(node.text, role, offsetOf(node, language))], doc: node => doc(node, document), command: (form) => { if (form.form === 'exec') return expandedText(form.node, clipped(nodeSpan(form.node)), document) const reading = document.embedded?.(form.code.node) return reading ? embeddedText(reading) : { text: verbatim(form.code.node, document) } }, } switch (block.kind) { case 'function': return functionRows(block.facts, phrases) case 'class': return classRows(block.facts, phrases) case 'enum': return enumRows(block.facts, phrases) case 'exception': return exceptionRows(block.facts, phrases) case 'import': return importRows(block.facts, phrases) case 'interpreter': return interpreterRows(block.facts) case 'options': return optionsRows(block.facts, table.switches ?? {}) case 'directive': return directiveRows(block.facts, table.directives ?? {}) case 'stage': return stageRows(block.facts, phrases) case 'health': return healthRows(block.facts, phrases) case 'start': return startRows(block.facts, phrases) default: return block.facts.scope ? scopedVariableRows(block.facts.declarators, block.facts.scope, phrases) : variableRows(block, phrases) } } return { id: table.id, label: table.label, languageIds: table.languageIds, displayLanguage: 'zh', translate, describe, expression, doc, attributeNames: ZH_ATTRIBUTE_NAMES, } } function nodeSpan(node: Node): SourceSpan { return { startIndex: node.startIndex, endIndex: node.endIndex, startRow: node.startPosition.row, endRow: node.endPosition.row } } function segment(text: string, role: TokenRole, offset: number | undefined): Segment { return offset === undefined ? { text, role } : { text, role, offset } } /** 相邻的两段不带下标的普通文字并成一段;框不和别的并。 */ function pushSegment(segments: Inline[], text: string, role: TokenRole, offset?: number): void { if (!text) return const prev = segments.at(-1) if (role === 'plain' && prev && 'role' in prev && prev.role === 'plain') prev.text += text else segments.push(segment(text, role, offset)) } function appendInline(segments: Inline[], pieces: Inline[]): void { for (const piece of pieces) { if ('role' in piece) pushSegment(segments, piece.text, piece.role, piece.offset) else segments.push(piece) } } /** 词连成译文:源码里隔着空白的词之间空一格,注释不进译文,右括号前的尾逗号去掉。 */ function joinTokens(tokens: Token[]): Inline[] { const segments: Inline[] = [] let gap = false tokens.forEach((token, i) => { gap ||= token.gap const trailingComma = token.raw === ',' && !token.translated && CLOSING.has(nextRaw(tokens, i)) if (isComment(token) || trailingComma) { gap ||= token.role === 'comment' return } if (gap && segments.length > 0) pushSegment(segments, ' ', 'plain') if (token.group) segments.push(token.group) else pushSegment(segments, token.text, token.role, token.offset) // 「非」是个词,后面补一个空格,不和操作数粘在一起 gap = token.translated && token.raw === '!' }) return segments } // ─── 属性表 ─────────────────────────────────────────────────────────────────── /** 属性表里的值怎么写。 */ interface Phrases { /** 按表翻译,只翻落在块头部里的那一段。 */ translated: (node: Node) => Inline[] /** 同 translated,只翻 node 落在 span 里的那一段(对象字面量里方法的头部)。 */ within: (node: Node, span: SourceSpan) => Inline[] /** 同 translated,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall)。 */ expanded: (node: Node) => { text: Inline[], table?: PreviewTable } /** 函数主体里的一句(不在块头部里)整句照常翻:函数打印的那几句。 */ body: (node: Node) => Inline[] /** 一句 return 返回的值(语言插件读出的命令的第一个词);只写了 return 时是空的。 */ returned: (node: Node) => Inline[] /** 字典 / 对象字面量的项数:「字典 3 项」,叫法按字面量的节点类型查表。 */ entryCount: (literal: Node, size: number) => Segment[] /** 源码原样照抄、按源码着色(装饰器)。 */ verbatim: (node: Node) => Segment[] /** 导入来源:字符串字面量按字符串着色,模块名按类型着色。 */ module: (node: Node) => Segment[] /** 源码里的一个名字原样照抄、按 role 着色(函数、类的名称)。 */ name: (node: Node, role: TokenRole) => Segment[] /** 函数、类的说明(JSDoc、docstring):去掉注释符号、引号和共同缩进的 Markdown 原文;没有文字时没有。 */ doc: (node: Node) => Markdown | undefined /** 一条要执行的命令:数组写法交给命令规则读,shell 写法是宿主读好的嵌着的代码(SourceDocument.embedded),可以带一张选项表。 */ command: (form: CommandForm) => { text: Inline[], table?: PreviewTable } } /** * 嵌着的一段代码读成一行:整段只有一步时是这一步的译文(连它的表),几步时各步的译文连起来, * 一步失败就停的(shell 的 &&)用「然后」连,别的用分号。 */ export function embeddedText(reading: EmbeddedReading): { text: Inline[], table?: PreviewTable } { const [first] = reading.steps if (reading.whole && first) return first const separator: Inline[] = reading.chained ? [segment(' ', 'plain', undefined), segment('然后', 'keyword', undefined), segment(' ', 'plain', undefined)] : [segment(';', 'plain', undefined)] return { text: reading.steps.flatMap((step, i) => i > 0 ? [...separator, ...step.text] : step.text) } } function hint(text: string): Segment { return { text, role: 'hint' } } const STREAM_ZH: Record<'output' | 'error' | 'both', string> = { output: '输出', error: '错误输出', both: '全部输出' } /** * 重定向的意思,括号里淡色写在命令后面:`>&2` 打印到错误输出,`2>&1` 错误输出并进输出,`2>/dev/null` 丢掉错误输出, * `> f` 写到 f,`>> f` 追加到 f,`< f` 从 f 读。 */ function redirectText(redirect: Redirect, say: (node: Node) => Inline[]): Inline[] { const note = (...parts: Inline[]): Inline[] => [hint('('), ...parts, hint(')')] switch (redirect.kind) { case 'merge': if (redirect.stream === redirect.into) return [] return note(hint(redirect.into === 'error' ? '打印到错误输出' : '错误输出并进输出')) case 'read': return note(hint('从 '), ...say(redirect.source), hint(' 读')) default: { const stream = STREAM_ZH[redirect.stream] if (redirect.target.text === '/dev/null') return note(hint(`丢掉${stream}`)) const where = redirect.kind === 'append' ? '追加到 ' : '写到 ' return note(hint(redirect.stream === 'output' ? where : `${stream}${where}`), ...say(redirect.target)) } } } /** 布尔属性的一行:没有这个概念的语言(facts 里没填)没有这一行。 */ function flagRows(attribute: 'async' | 'abstract' | 'typeOnly', value: boolean | undefined): PropertyRow[] { return value === undefined ? [] : [{ label: ATTRIBUTE_ZH[attribute], value: [{ text: YES_NO[`${value}`], role: 'storage' }] }] } function count(n: number): Segment[] { return [hint(n > 0 ? `${n} 个` : '无')] } function joined(parts: Inline[][], separator: string): Inline[] { return parts.flatMap((part, i) => i > 0 ? [{ text: separator, role: 'plain' as const }, ...part] : part) } function accessRows(access: Access | undefined): PropertyRow[] { return access ? [{ label: ATTRIBUTE_ZH.access, value: [{ text: ACCESS_ZH[access], role: 'keyword' }] }] : [] } /** 这一行 / 子行讲的定义:几个节点(导入项是名称和别名)合起来在源码里的范围;一个节点都没有就不填。 */ function defining(...nodes: (Node | undefined)[]): Defines { const present = nodes.filter((node): node is Node => node !== undefined) if (present.length === 0) return {} return { defines: { startIndex: Math.min(...present.map(node => node.startIndex)), endIndex: Math.max(...present.map(node => node.endIndex)) } } } function importNodes(item: ImportItem): (Node | undefined)[] { switch (item.kind) { case 'named': return [item.name, item.alias] case 'module': return [item.module, item.alias] default: return [item.alias] } } function aliased(item: Inline[], alias: Node | undefined, say: Phrases): Inline[] { 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[] { return [ { label: '名称', value: facts.name ? say.name(facts.name, 'function') : [hint('匿名')] }, ...docRows(facts.doc, say), ...accessRows(facts.access), ...flagRows('async', facts.async), ...decoratorRows(facts.decorators, say), ...(facts.positional ? positionalRows(facts, facts.positional, say) : [ parameterRow('输入参数', facts.parameters, say), { label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')] }, ]), ] } /** * 按位置取参数的函数(shell):输入参数写「N 个,按位置」,下面一张表「位置 | 函数里叫」(没起名的写 $N); * 输出是它打印的那几句(调用处用 $(函数 …) 拿去用时注明);返回是写了的 return 值,没写 return、也不打印时是最后一句成不成功。 */ function positionalRows(facts: FunctionFacts, positional: PositionalParameter[], say: Phrases): PropertyRow[] { const inputs: PropertyRow = positional.length === 0 ? { label: '输入参数', value: [hint('无')] } : { label: '输入参数', value: [hint(`${positional.length} 个,按位置`)], columns: ['位置', '函数里叫'], items: positional.map(param => ({ cells: [[hint(`第 ${param.position} 个`)], param.name ? say.name(param.name, 'variable') : [segment(`$${param.position}`, 'variable', undefined)]], ...defining(param.name), })), } const prints = facts.prints ?? [] const captured = facts.captured && facts.name ? [hint(`(调用处用 $(${facts.name.text} …) 拿到)`)] : [] const output: PropertyRow[] = prints.length > 0 ? [{ label: '输出', value: [...joined(prints.map(say.body), '\n'), ...captured] }] : [] const returns = facts.returns ?? [] const values = returns.map(say.returned).filter(value => value.length > 0) const result: PropertyRow[] = values.length > 0 ? [{ label: '返回', value: joined(values, '、') }] : prints.length === 0 ? [{ label: '返回', value: [hint('最后一句成不成功')] }] : [] return [inputs, ...output, ...result] } /** 解释器:叫什么,怎么找到它(按 PATH 找,或写死的路径)。 */ function interpreterRows(facts: InterpreterFacts): PropertyRow[] { return [ { label: '解释器', value: [segment(facts.program, 'function', undefined)] }, { label: '怎么找', value: [hint(facts.lookup === 'env' ? `按 PATH 去找(${facts.command})` : `写死的路径(${facts.command})`)] }, ] } /** 解释器开关:每个开关一行,属性名是开关的意思(没有叫法的写源码里的写法),值是开没开,后面淡色写源码里的写法。 */ function optionsRows(facts: OptionsFacts, names: Record): PropertyRow[] { return facts.switches.map(item => ({ label: names[item.id] ?? item.flag, value: [{ text: YES_NO[`${item.on}`], role: 'storage' }, { text: ' ', role: 'plain' }, hint(item.flag)], })) } /** 解析指令:一条一行,行名是指令的叫法,值照写,后面淡色写补充说明(语法版本是给 BuildKit 看的)。 */ function directiveRows(facts: DirectiveFacts, names: Record): PropertyRow[] { return facts.directives.map(({ name, value }) => { const known = names[name] return { label: known?.label ?? name, value: [segment(value, 'string', undefined), ...(known?.note ? [hint(known.note)] : [])] } }) } /** 选项的值:只是一个变量(`$BUILDPLATFORM`、`${X}`)时按变量着色、能跳到定义,别的照写成字符串。 */ function flagValue(flag: InstructionFlag): Inline[] { const match = /^\$(\{)?(\w+)\}?$/.exec(flag.value) if (!match) return [segment(flag.value, 'string', flag.valueStart)] const name = match[2] return [segment(name, /^[A-Z][A-Z0-9_]*$/.test(name) ? 'constant' : 'variable', flag.valueStart + (match[1] ? 2 : 1))] } /** * 阶段:名称(没起名的写按序号引用)/ 说明(紧贴在 FROM 上面的注释)/ 基础镜像(镜像名,标签、摘要写了才写; * 接着前面某个阶段建的写「阶段 X」)/ 平台(写了 --platform 才有)。 */ function stageRows(facts: StageFacts, say: Phrases): PropertyRow[] { const doc = (facts.doc ?? []).map(node => say.doc(node)?.markdown).filter(Boolean).join('\n') const platform = facts.flags.find(flag => flag.name === 'platform') const image: Inline[] = facts.stage ? [segment('阶段', 'keyword', undefined), segment(' ', 'plain', undefined), ...say.name(facts.image, 'variable')] : [ ...say.translated(facts.image), ...(facts.tag ? [hint(',标签 '), ...say.translated(facts.tag)] : []), ...(facts.digest ? [hint(',摘要 '), ...say.translated(facts.digest)] : []), ] return [ facts.name ? { label: '名称', value: say.name(facts.name, 'variable'), ...defining(facts.name) } : { label: '名称', value: [hint(`没起名,按序号 ${facts.index} 引用`)] }, ...(doc ? [{ label: '说明', value: { markdown: doc } }] : []), { label: '基础镜像', value: image }, ...(platform ? [{ label: '平台', value: flagValue(platform) }] : []), ] } const DURATION_UNITS: Record = { h: '小时', m: '分', s: '秒', ms: '毫秒', us: '微秒', ns: '纳秒' } /** 时长(`30s`、`1m30s`、`500ms`):数字按数字着色,单位写成中文;认不出的照写。 */ function durationText(flag: InstructionFlag): Inline[] { const parts = [...flag.value.matchAll(/(\d+(?:\.\d+)?)(ms|us|ns|[hms])/g)] if (parts.length === 0 || parts.map(part => part[0]).join('') !== flag.value) return [segment(flag.value, 'string', flag.valueStart)] return parts.flatMap(([, amount, unit], i) => [...(i > 0 ? [hint(' ')] : []), segment(amount, 'number', undefined), hint(` ${DURATION_UNITS[unit]}`)]) } /** 一行命令连着它的表(选项写得多的命令的「选项 | 意思」)。 */ function commandRow(label: string, command: { text: Inline[], table?: PreviewTable }): PropertyRow { return { label, value: command.text, ...(command.table && { columns: command.table.columns, items: command.table.items }) } } /** 健康检查:间隔 / 超时 / 启动后宽限 / 宽限期里的间隔 / 失败几次算不健康(写了才有)/ 检查命令;NONE 只有一行「不做健康检查」。 */ function healthRows(facts: HealthFacts, say: Phrases): PropertyRow[] { if (facts.none) return [{ label: '检查', value: [hint('不做健康检查(基础镜像里设的也关掉)')] }] const flag = (name: string): InstructionFlag | undefined => facts.flags.find(item => item.name === name) const durations: [string, string][] = [['interval', '间隔'], ['timeout', '超时'], ['start-period', '启动后宽限'], ['start-interval', '宽限期里的间隔']] const retries = flag('retries') return [ ...durations.flatMap(([name, label]) => { const found = flag(name) return found ? [{ label, value: durationText(found) }] : [] }), ...(retries ? [{ label: '失败几次算不健康', value: [segment(retries.value, 'number', retries.valueStart), hint(' 次')] }] : []), ...(facts.command ? [commandRow('检查命令', say.command(facts.command))] : []), ] } /** 启动:命令(ENTRYPOINT 叫入口命令)、写法(数组不经过 shell / 经过 shell)。 */ function startRows(facts: StartFacts, say: Phrases): PropertyRow[] { return [ commandRow(facts.instruction === 'cmd' ? '命令' : '入口命令', say.command(facts.command)), { label: '写法', value: [hint(facts.command.form === 'exec' ? '数组,不经过 shell' : '经过 shell')] }, ] } /** 节点所在的那条顶层语句(父节点是根)。 */ function statementOf(node: Node): Node { let current = node while (current.parent?.parent) current = current.parent return current } /** * 分在哪儿有效的变量(Dockerfile 的构建参数、环境变量):一个变量是「构建参数 / 环境变量」和「默认值 / 值」两行(构建参数没写默认值的 * 写「没写,构建时传」);几个变量(一条写了几个,或几条连着写)并成一行,下面一张「名称 | 值」表,每行讲的定义是那个变量名。 */ function scopedVariableRows(declarators: DeclaratorFacts[], scope: 'build' | 'environment', say: Phrases): PropertyRow[] { const label = scope === 'build' ? '构建参数' : '环境变量' const valueLabel = scope === 'build' ? '默认值' : '值' const valueOf = (declarator: DeclaratorFacts): Inline[] => declarator.value ? say.translated(declarator.value) : [hint(scope === 'build' ? '没写,构建时传' : '空')] const [only] = declarators if (declarators.length === 1 && only) return [{ label, value: say.translated(only.name), ...defining(only.name) }, { label: valueLabel, value: valueOf(only) }] const statements = new Set(declarators.map(declarator => statementOf(declarator.name).id)).size return [{ label, value: [hint(statements > 1 ? `${statements} 条连着写` : `${declarators.length} 个`)], columns: ['名称', valueLabel], items: declarators.map(declarator => ({ cells: [say.translated(declarator.name), valueOf(declarator)], ...defining(declarator.name) })), }] } /** 参数表:列名是名称 / 类型 / 默认值,每个参数一行,讲的定义是参数名;没有默认值的格子留空。 */ function parameterRow(label: string, parameters: ParameterFacts[], say: Phrases): PropertyRow { return { label, value: count(parameters.length), columns: ['名称', '类型', '默认值'], items: 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 ? say.translated(param.default) : [], ], ...defining(param.name), })), } } /** * 类:名称 / 说明 / 访问级别 / 抽象(只有分这个的语言才有)/ 装饰器 / 类型参数 / 父类 / 实现的接口, * 说明及以后各行有才显示;类型参数、父类、实现的接口各是一行,多项用逗号隔开。类型参数那一行讲的是这几个泛型参数的定义。 */ function classRows(facts: ClassFacts, say: Phrases): PropertyRow[] { const listRow = (label: string, parts: Inline[][], defines: Defines = {}): PropertyRow[] => parts.length > 0 ? [{ label, value: joined(parts, ', '), ...defines }] : [] return [ { label: '名称', value: facts.name ? say.name(facts.name, 'type') : [hint('匿名')] }, ...docRows(facts.doc, say), ...accessRows(facts.access), ...flagRows('abstract', facts.abstract), ...decoratorRows(facts.decorators, say), ...listRow('类型参数', facts.typeParameters.map(say.translated), defining(...facts.typeParameters)), ...listRow('父类', facts.bases.map(base => baseText(base, say))), ...listRow('实现的接口', facts.implements.map(say.translated)), ] } /** 父类的一项:TypeScript 的泛型实参紧跟在后面。 */ function baseText(base: BaseFacts, say: Phrases): Inline[] { return base.typeArguments ? [...say.translated(base.value), ...say.translated(base.typeArguments)] : say.translated(base.value) } /** * 枚举(TypeScript 的 enum、Python enum 库的类,同一张表):名称 / 说明 / 访问级别 / 装饰器 / 继承(照写的父类),有才显示; * 成员收成一张表:成员 | 值 | 说明。没写值的成员写语言算出的值(自增的 0、1、2……),算不出来写「自动」,都按说明词着色; * 说明是成员的注释、JSDoc、属性 docstring 的原文用空格连起来,一个成员都没有说明时不给这一列。 */ function enumRows(facts: EnumFacts, say: Phrases): PropertyRow[] { const docs = facts.members.map(member => docsText(member.docs, say)) const described = docs.some(Boolean) return [ { label: '名称', value: say.name(facts.name, 'type') }, ...docRows(facts.doc, say), ...accessRows(facts.access), ...decoratorRows(facts.decorators, say), ...baseRows(facts.bases, say), { label: '成员', value: count(facts.members.length), columns: described ? ['成员', '值', '说明'] : ['成员', '值'], items: facts.members.map((member, i) => ({ cells: [ say.name(member.name, 'variable'), member.value ? say.translated(member.value) : [hint(member.computed === undefined ? '自动' : String(member.computed))], ...(described ? [docCell(docs[i])] : []), ], ...defining(member.name), })), }, ] } /** * 异常(Python、TypeScript 同一张表):名称 / 说明 / 访问级别 / 装饰器 / 继承(照写的父类),说明及以后有才显示; * 类属性收成一张表:属性 | 类型 | 值 | 说明,一个属性都没写类型时没有「类型」列、都没有说明时没有「说明」列,没写的格子留空; * 构造参数同函数的输入参数表。没有类属性、没写构造函数时没有这两行。 */ function exceptionRows(facts: ExceptionFacts, say: Phrases): PropertyRow[] { const docs = facts.fields.map(field => docsText(field.docs, say)) const described = docs.some(Boolean) const typed = facts.fields.some(field => field.type) const fields: PropertyRow = { label: '属性', value: count(facts.fields.length), columns: ['属性', ...(typed ? ['类型'] : []), '值', ...(described ? ['说明'] : [])], items: facts.fields.map((field, i) => ({ cells: [ say.name(field.name, 'variable'), ...(typed ? [field.type ? say.translated(field.type) : []] : []), field.value ? say.translated(field.value) : [], ...(described ? [docCell(docs[i])] : []), ], ...defining(field.name), })), } return [ { label: '名称', value: facts.name ? say.name(facts.name, 'type') : [hint('匿名')] }, ...docRows(facts.doc, say), ...accessRows(facts.access), ...decoratorRows(facts.decorators, say), ...baseRows(facts.bases, say), ...(facts.fields.length > 0 ? [fields] : []), ...(facts.parameters ? [parameterRow('构造参数', facts.parameters, say)] : []), ] } /** 继承:照写的父类,逗号隔开;没有父类时没有这一行。 */ function baseRows(bases: BaseFacts[], say: Phrases): PropertyRow[] { return bases.length > 0 ? [{ label: '继承', value: joined(bases.map(base => baseText(base, say)), ', ') }] : [] } /** 表格里一格说明:几段说明文字的原文用空格连起来。 */ function docsText(nodes: Node[], say: Phrases): string { return nodes.map(node => say.doc(node)?.markdown).filter(Boolean).join(' ') } function docCell(text: string): Inline[] { return text ? [segment(text, 'comment', undefined)] : [] } /** 函数、类、常量、变量的说明:有文字才有这一行。 */ function docRows(doc: Node | undefined, say: Phrases): PropertyRow[] { const markdown = doc && say.doc(doc) return markdown ? [{ label: '说明', value: markdown }] : [] } /** 装饰器:源码原样照抄,多个换行。 */ function decoratorRows(decorators: Node[], say: Phrases): PropertyRow[] { return decorators.length > 0 ? [{ label: '装饰器', value: joined(decorators.map(say.verbatim), '\n') }] : [] } /** 导入:来源 / 仅类型(只有分这个的语言才有)/ 导入项(每项一行,有别名写成「名称 作为 别名」)。 */ function importRows(facts: ImportFacts, say: Phrases): PropertyRow[] { const itemText = (item: ImportItem): Inline[] => { 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('无')] }, ...flagRows('typeOnly', facts.typeOnly), { label: '导入项', value: count(facts.items.length), items: facts.items.map(item => ({ cells: [itemText(item)], ...defining(...importNodes(item)) })) }, ] } /** * 常量 / 变量:每个声明的变量依次是名称 / 访问级别 / 类型(有标注才有)/ 值(变量叫初始值,见 declaredValue); * 说明(有才显示)是整条语句的,排在第一个名称后面。 */ function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] { const valueLabel = block.kind === 'const' ? '值' : '初始值' return block.facts.declarators.flatMap((declarator, i) => [ { label: '名称', value: say.translated(declarator.name), ...defining(declarator.name) }, ...(i === 0 ? docRows(block.facts.doc, say) : []), ...accessRows(block.facts.access), ...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []), { label: valueLabel, ...(declarator.value ? declaredValue(declarator.value, declarator.entries, say) : { value: [hint('无')] }) }, ]) } /** * 声明的值:字典 / 对象字面量见 literalValue;别的值是一行译文,里面跨几行写的调用排成「新建 / 调用 …」, * 实参摊成表(参数 | 值)展开在这一行下面。 */ function declaredValue(value: Node, entries: EntryFacts[] | undefined, say: Phrases): Pick { if (entries?.length) return literalValue(value, entries, say) const { text, table } = say.expanded(value) return table ? { value: text, ...table } : { value: text } } /** * 值:字典 / 对象字面量(语言插件给了 entries)写项数,各项展开成下一层子行; * 空字面量和别的值是一行译文(空的译成「空字典」「空对象」)。 */ function literalValue(value: Node, entries: EntryFacts[] | undefined, say: Phrases): { value: Inline[], items?: PropertyItem[] } { if (!entries?.length) return { value: say.translated(value) } return { value: say.entryCount(value, entries.length), items: entries.map(entry => entryItem(entry, say)) } } /** * 字典 / 对象字面量的一项是一个子行,两列:键 | 值。键照源码写法(字符串带引号,计算属性名 `[k]` 照常翻), * `**x`、`...x` 的键写「展开」、值是展开的表达式,方法的值是它的头部。讲的定义是键(对象的属性名)。 */ function entryItem(entry: EntryFacts, say: Phrases): PropertyItem { switch (entry.kind) { case 'pair': { const { value, ...nested } = literalValue(entry.value, entry.entries, say) return { cells: [say.translated(entry.key), value], ...nested, ...defining(entry.key) } } case 'spread': return { cells: [[hint('展开')], say.translated(entry.value)] } case 'method': return { cells: [say.translated(entry.name), say.within(entry.node, entry.header)], ...defining(entry.name) } } }