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

1282 lines
62 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, 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<string, string>
}
/**
* 节点模板的一项。字符串是插入的译词,按关键字着色;另外两种是子节点槽位:
* `{ 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<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>
/** 字典 / 对象字面量(语言插件给了 entries 的值)的叫法,按节点类型查:属性表里的值写成「字典 3 项」,各项展开成子行。 */
mapLiterals: Record<string, string>
/** 整段原样照抄、按字符串着色的节点。 */
stringNodes: string[]
/** 注释(或 docstring)节点去掉注释符号后的原文,可以多行;text 是节点的源码去掉代码阅读的记号(SourceDocument.markers)以后剩下的。 */
commentText: (node: Node, text: string) => string
/**
* 节点模板:节点类型 → 按中文语序重排的译文。命中的节点只输出模板里的译词和槽位,节点自己的关键字、标点不再输出;
* 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。
*/
nodeTemplates: Record<string, TemplatePart[]>
/**
* 节点读法:节点类型 → 怎么读成一行译文,给模板排不了的写法(shell 的 `${x:-d}`、`[ -f x ]`、`case`)。
* 命中的节点只输出读法给的,不再照常翻它的子节点;读法返回 undefined 就照常翻。读法里要翻子节点用 ReaderContext.say。
*/
readers?: Record<string, NodeReader>
/** 并成一块的几条同种语句(TextBlock.group)怎么读:按第一条的节点类型查,读成一行字加一张表;没有的照常翻第一条。 */
groups?: Record<string, GroupReader>
/** 解释器开关(SwitchFacts.id)的叫法:errexit → 出错就停;没有的写源码里的写法。 */
switches?: Record<string, string>
/** 解析指令(ParserDirective.name)的叫法和补充说明:syntax → 语法(BuildKit 按这个版本读这份文件);没有的写原名。 */
directives?: Record<string, { label: string, note?: string }>
}
/** 读一个节点时能用的(见 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<TokenRole>(['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<TemplatePart, string>): 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<CommonAttribute, string> = { kind: '种类', access: '访问级别', async: '异步', abstract: '抽象', typeOnly: '仅类型' }
const KIND_ZH: Record<BlockKind, string> = {
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<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
const ITERATION_ZH: Record<IterationResult, string> = { generator: '生成器', list: '列表', set: '集合', dict: '字典' }
/** 被调用的名字定义成这几种块时,调用就是新建对象。 */
const CONSTRUCTED = new Set<BlockKind | undefined>(['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<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 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<Token, 'gap' | 'raw'> => ({ 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<string, string>): 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<string, { label: string, note?: string }>): 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<string, string> = { 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<PropertyRow, 'value' | 'items' | 'columns'> {
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) }
}
}