契约加 LanguageAdapter.chain、Translator.chain、RuleContext.chain、PropertyRow.chain、TextTranslation.rows;跨行写或实参里有闭包的链排成卡片,连着同名调用合成一步列出每次实参,注释挂在步骤上,闭包收起、子块挂到那一步(BlockView.step);Rust 插件读链、标准库常见方法写中文名;挤成一行时另起一行的 . 贴着前面
1643 lines
82 KiB
TypeScript
1643 lines
82 KiB
TypeScript
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ChainArgument, ChainFacts, ChainGroup, ChainStep, ChainStepFacts, ChainView, ClassFacts, CollectionFacts, CommandContext, CommandFacts, CommandForm, CommandReading, CommandRule, CommandWord, CommonAttribute, DeclaratorFacts, DeferredFacts, Defines, DirectiveFacts, Doc, ElementFacts, 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 type { ParamDocs } from '../rules/docRows'
|
||
import { readDoc } from '../docTags'
|
||
import { commonIndent, splitLines } from '../editText'
|
||
import { docBodyRows, paramDocCell, paramDocs, returnNote, throwsRows, unmatchedParamRows, writtenParamRows } from '../rules/docRows'
|
||
|
||
/** 按所在语法节点区分译法的关键字,例如 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` 里的运算符节点,同一个符号写在别处的照抄。 */
|
||
operators: Record<string, string>
|
||
operatorParents: string[]
|
||
/** 空字面量(没有具名子节点)整个换成一个词:空数组 / 空列表 / 空字典 / 空对象。 */
|
||
emptyLiterals: Record<string, string>
|
||
/** 写出各项的字面量(语言插件给了各项,见 LanguageAdapter.collection)的叫法,按节点类型查:属性表里拆开的值写成「字典 3 项」「列表 2 项」。 */
|
||
collections: Record<string, string>
|
||
/** 链式调用卡片里常见方法的中文名,按方法名查(`iter` → 遍历):方法那一列写它,悬停看原名;没有的写 `.方法名`。 */
|
||
methods?: Record<string, string>
|
||
/** 整段原样照抄、按字符串着色的节点。 */
|
||
stringNodes: string[]
|
||
/** 注释(或 docstring)节点去掉注释符号后的原文,可以多行;text 是节点的源码去掉代码阅读的记号(SourceDocument.markers)以后剩下的。 */
|
||
commentText: (node: Node, text: string) => string
|
||
/**
|
||
* 说明是连着几条行注释写的(Rust 的 `///`)时,从第一条(facts.doc)起这一段的各条注释节点,照源码顺序;
|
||
* 不填就是那一个节点(JSDoc、docstring 本来就是一个节点)。
|
||
*/
|
||
docNodes?: (first: Node) => Node[]
|
||
/**
|
||
* 节点模板:节点类型 → 按中文语序重排的译文。命中的节点只输出模板里的译词和槽位,节点自己的关键字、标点不再输出;
|
||
* 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。
|
||
*/
|
||
nodeTemplates: Record<string, TemplatePart[]>
|
||
/**
|
||
* 节点读法:节点类型 → 怎么读成一行译文,给模板排不了的写法(shell 的 `${x:-d}`、`[ -f x ]`、`case`,TypeScript 一元运算里只有 `!x` 换语序)。
|
||
* 命中的节点只输出读法给的,不再照常翻它的子节点;读法返回 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)、读法给的表(分情况的「情况 / 做什么」),
|
||
* 或跨几行写的字面量拆开的各项(见 emitCollection)。一段只排一样,排了的记在这里。
|
||
*/
|
||
interface Slot {
|
||
items?: PropertyItem[]
|
||
table?: PreviewTable
|
||
}
|
||
|
||
/**
|
||
* 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。
|
||
* slot 是这一段下面能不能排一样东西(跨几行写的调用、字面量,读法给的表);没有就照常翻。
|
||
*/
|
||
interface Collector {
|
||
document: SourceDocument
|
||
/** 要翻的那个节点 */
|
||
root: Node
|
||
tokens: Token[]
|
||
cursor: number
|
||
gap: boolean
|
||
slot?: Slot
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
/** 节点的源码去掉整个落在它里面的记号(中文名记号 `@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
|
||
}
|
||
|
||
/** 这一段下面已经排了一样东西(见 Slot)。 */
|
||
function taken(slot: Slot): boolean {
|
||
return slot.items !== undefined || slot.table !== undefined
|
||
}
|
||
|
||
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: '仅类型' }
|
||
export const KIND_ZH: Record<BlockKind, string> = {
|
||
import: '导入',
|
||
function: '函数',
|
||
class: '类',
|
||
trait: '特质',
|
||
impl: '实现',
|
||
module: '模块',
|
||
attribute: '编译属性',
|
||
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: '否' }
|
||
|
||
/** 筛选属性和值的中文叫法;属性表里的访问级别、异步、抽象、仅类型这几行,属性名和值也用这一套。 */
|
||
export 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 || !inside(node, span))
|
||
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 之间有空白,下一个词前面就空一格;跨行写的挤成一行时换行处该贴着写的贴着写(见 [hugs])。 */
|
||
function advance(out: Collector, index: number): void {
|
||
const between = out.document.source.slice(out.cursor, index)
|
||
out.gap ||= !hugs(between, out.tokens.at(-1)?.raw, out.document.source.slice(index, index + 1)) && /\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 || !inside(node, span))
|
||
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: Slot, { language: { call } }: SourceDocument): CallFacts | undefined {
|
||
if (taken(slot) || !call || !inside(node, span))
|
||
return undefined
|
||
const facts = call(node)
|
||
return facts && facts.argumentList.startPosition.row !== facts.argumentList.endPosition.row ? facts : undefined
|
||
}
|
||
|
||
/** 要按项拆开的字面量:语言插件给了各项(LanguageAdapter.collection)、不是空的,跨了几行,整个落在要翻的这一段里,这一段还没排过别的。 */
|
||
function expandedCollection(node: Node, span: SourceSpan, slot: Slot, { language: { collection } }: SourceDocument): CollectionFacts | undefined {
|
||
if (taken(slot) || !collection || !inside(node, span))
|
||
return undefined
|
||
const facts = collection(node)
|
||
if (!facts || (facts.kind === 'map' ? facts.entries : facts.elements).length === 0)
|
||
return undefined
|
||
return node.startPosition.row !== node.endPosition.row ? facts : undefined
|
||
}
|
||
|
||
/**
|
||
* node 落在 span 里的那一段翻成一行译文,里面先遇到的(先外后里)跨几行写的调用排成一行「新建 / 调用 …」、实参摊成表(见 emitCall),
|
||
* 或跨几行写的字面量排成一行「列表 2 项」、各项拆成子行(见 emitCollection);同一段里后面再有的照常一行。
|
||
* 给了 call(node 整个就是这次调用)时不管写成几行都这样排。一行译文的块、属性表里每一格的值各是一段,各自排自己里面的那一个。
|
||
*/
|
||
function expandedText(node: Node, span: SourceSpan, document: SourceDocument, call?: CallFacts): CellValue {
|
||
const slot: Slot = {}
|
||
const out = collector(node, span, document, slot)
|
||
if (call)
|
||
emitCall(out, slot, node, call, span)
|
||
else
|
||
collect(node, span, out)
|
||
return { text: joinTokens(out.tokens), ...slot }
|
||
}
|
||
|
||
/**
|
||
* 属性表里一格的值(常量 / 变量的值、字典一项的值、列表的一项、实参的值),只翻落在 span 里的部分,见 expandedText;
|
||
* maps 为真时(实参的值以外)值整个是字典 / 对象字面量的写在一行里也拆,空字面量照旧一行(「空字典」)。
|
||
*/
|
||
function cellValue(node: Node, span: SourceSpan, document: SourceDocument, maps = true): CellValue {
|
||
const collection = maps ? document.language.collection?.(node) : undefined
|
||
if (collection?.kind === 'map' && collection.entries.length > 0)
|
||
return collectionValue(node, collection, span, document)
|
||
return expandedText(node, span, document)
|
||
}
|
||
|
||
/**
|
||
* 拆开的字面量:写叫法和项数(展开的也算一项),叫法按字面量的节点类型查表(ZhTable.collections):「字典 3 项」;
|
||
* 各项是下一层子行,字典 / 对象见 entryItem,列表 / 元组 / 集合 / 数组见 elementItems。
|
||
*/
|
||
function collectionValue(literal: Node, facts: CollectionFacts, span: SourceSpan, document: SourceDocument): { text: Segment[], items: PropertyItem[] } {
|
||
const items = facts.kind === 'map' ? facts.entries.map(entry => entryItem(entry, span, document)) : elementItems(facts.elements, span, document)
|
||
const name = table.collections[literal.type]
|
||
return { text: [hint(name ? `${name} ${items.length} 项` : `${items.length} 项`)], items }
|
||
}
|
||
|
||
/**
|
||
* 字典 / 对象的一项是一个子行,两列:键 | 值。键照源码写法(字符串带引号,计算属性名 `[k]` 照常翻),值见 cellValue;
|
||
* `**x`、`...x` 的键写「展开」、值是展开的表达式,方法的值是它的头部。讲的定义是键(对象的属性名)。
|
||
*/
|
||
function entryItem(entry: EntryFacts, span: SourceSpan, document: SourceDocument): PropertyItem {
|
||
const say = (node: Node, part = span): Inline[] => joinTokens(tokensIn(node, part, document))
|
||
switch (entry.kind) {
|
||
case 'pair': {
|
||
const { text, ...nested } = cellValue(entry.value, span, document)
|
||
return { cells: [say(entry.key), text], ...nested, ...defining(entry.key) }
|
||
}
|
||
case 'spread':
|
||
return { cells: [[hint('展开')], say(entry.value)] }
|
||
case 'method':
|
||
return { cells: [say(entry.name), say(entry.node, clip(entry.header, span))], ...defining(entry.name) }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 列表的各项各是一个子行,两列:第 N 项 | 值(只数不是展开的项),`*x`、`...x` 写「展开」、值是展开的表达式。
|
||
* 整项是一次调用的不管写成几行都排成「新建 / 调用 …」,实参表跟着这一项,同一个列表里样子一致;别的值见 cellValue。
|
||
*/
|
||
function elementItems(elements: ElementFacts[], span: SourceSpan, document: SourceDocument): PropertyItem[] {
|
||
let position = 0
|
||
return elements.map((element): PropertyItem => {
|
||
if (element.kind === 'spread')
|
||
return { cells: [[hint('展开')], joinTokens(tokensIn(element.value, span, document))] }
|
||
position++
|
||
const { value } = element
|
||
const call = inside(value, span) ? document.language.call?.(value) : undefined
|
||
const { text, ...nested } = call ? expandedText(value, span, document, call) : cellValue(value, span, document)
|
||
return { cells: [[hint(`第 ${position} 项`)], text], ...nested }
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 实参表:参数 | 值,每个实参一行,照源码顺序。按名字传的写名字,按位置传的写「第 N 个」(只数按位置传的),
|
||
* 展开的写「展开」(*args、...args)、「展开关键字」(**kwargs)。值见 cellValue(写在一行里的字典 / 对象照旧一行):值里有跨几行写的调用时
|
||
* 同样排成一行,它的实参表排在值那一格里(PropertyItem.table);值里有跨几行写的字典 / 列表时各项拆在那一格里(PropertyItem.items)。一层层往下套。
|
||
*/
|
||
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, ...nested } = cellValue(arg.value, span, document, false)
|
||
return { cells: [label(arg), text], ...nested }
|
||
}
|
||
return { columns: ARGUMENT_COLUMNS, items: facts.arguments.map(row) }
|
||
}
|
||
|
||
/**
|
||
* 调用排成一行「新建 / 调用 被调用的名字 · N 个参数」,实参摊成表记在 slot 里。被调用的名字照常翻、带源码下标,
|
||
* 最后那个名字新建时按类型、调用时按函数着色,前面几节按变量着色。新建还是调用看写法(CallFacts.construct),
|
||
* 写法看不出来的问名字的定义:是类(含异常、枚举)就是新建,还没查到、查不到按调用算。
|
||
* 调用后面还紧跟着同一个式子的别的部分(见 continued)时整行括起来:「(新建 X · 3 个参数).model_dump()」。
|
||
*/
|
||
function emitCall(out: Collector, slot: Slot, 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
|
||
}
|
||
|
||
/**
|
||
* 跨几行写的字面量排成一行「列表 2 项」「字典 3 项」,各项拆成子行记在 slot 里(见 collectionValue)。
|
||
* 后面还紧跟着同一个式子的别的部分(见 continued)时整个括起来:「(列表 2 项) + rest」。
|
||
*/
|
||
function emitCollection(out: Collector, slot: Slot, node: Node, facts: CollectionFacts, span: SourceSpan): void {
|
||
const { text, items } = collectionValue(node, facts, span, out.document)
|
||
slot.items = items
|
||
const wrapped = continued(node, out.root)
|
||
advance(out, node.startIndex)
|
||
if (wrapped)
|
||
emit(out, { text: '(', role: 'plain', translated: true, raw: '' })
|
||
for (const piece of text)
|
||
emit(out, { text: piece.text, role: piece.role, 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 => inside(node, span),
|
||
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 && !taken(out.slot))
|
||
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 collection = slot && expandedCollection(node, span, slot, out.document)
|
||
if (slot && collection) {
|
||
emitCollection(out, slot, node, collection, 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,先遇到的跨几行写的调用、字面量排开(见 expandedText)。 */
|
||
function collector(node: Node, span: SourceSpan, document: SourceDocument, slot?: Slot): Collector {
|
||
return { document, root: node, tokens: [], cursor: span.startIndex, gap: false, ...(slot && { slot }) }
|
||
}
|
||
|
||
/** 按译文顺序收集 node 落在 span 里的词;slot 见 collector。 */
|
||
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument, slot?: Slot): Token[] {
|
||
const out = collector(node, span, document, slot)
|
||
collect(node, span, out)
|
||
return out.tokens
|
||
}
|
||
|
||
/**
|
||
* `node` 的源码原样照抄:叶子按源码着色(给了 `role` 就都按它),名字带源码下标,叶子之间的空白、标点照抄;
|
||
* `flat` 为真时压成一行:连着的空白(含换行)收成一个空格,换行处该贴着写的贴着写(见 [hugs]),注释不抄,右括号前的尾逗号去掉。
|
||
*/
|
||
function verbatim(node: Node, { source, language }: SourceDocument, role?: TokenRole, flat = false): Segment[] {
|
||
const leaves: Node[] = []
|
||
const gather = (current: Node): void => {
|
||
if (current.startIndex === current.endIndex)
|
||
return
|
||
if (current.childCount === 0) {
|
||
leaves.push(current)
|
||
return
|
||
}
|
||
for (const child of current.children) {
|
||
if (child)
|
||
gather(child)
|
||
}
|
||
}
|
||
gather(node)
|
||
const segments: Segment[] = []
|
||
let cursor = node.startIndex
|
||
// 压成一行时不抄的注释、尾逗号连同它前面的空白,记到下一个词前面
|
||
let pending = ''
|
||
let previous: string | undefined
|
||
leaves.forEach((leaf, i) => {
|
||
const between = pending + source.slice(cursor, leaf.startIndex)
|
||
cursor = leaf.endIndex
|
||
const own = language.tokenRole(leaf)
|
||
if (flat && (own === 'comment' || (leaf.type === ',' && CLOSING.has(leaves[i + 1]?.type ?? '')))) {
|
||
pending = own === 'comment' ? `${between} ` : between
|
||
return
|
||
}
|
||
pending = ''
|
||
pushSegment(segments, !flat ? between : hugs(between, previous, leaf.text) ? '' : between.replace(/\s+/g, ' '), 'plain')
|
||
pushSegment(segments, flat ? leaf.text.replace(/\s+/g, ' ') : leaf.text, role ?? own, offsetOf(leaf, language))
|
||
previous = leaf.text
|
||
})
|
||
const tail = source.slice(cursor, node.endIndex)
|
||
pushSegment(segments, flat ? tail.replace(/\s+/g, ' ') : tail, 'plain')
|
||
return segments
|
||
}
|
||
|
||
/** 注释节点去掉注释符号和记号后的原文。 */
|
||
function commentOf(node: Node, { markers }: SourceDocument): string {
|
||
return table.commentText(node, withoutMarkers(node, markers))
|
||
}
|
||
|
||
/** 注释块的原文:几条行注释连成的一块是从 block.node 起、开头落在头部里的各个兄弟节点。 */
|
||
function translateComment(block: TextBlock, document: SourceDocument): Markdown {
|
||
const nodes: Node[] = []
|
||
for (let node: Node | null = block.node; node && node.startIndex < block.header.endIndex; node = node.nextSibling)
|
||
nodes.push(node)
|
||
return { markdown: docText(nodes.map(node => commentOf(node, document))) }
|
||
}
|
||
|
||
// ─── 链式调用 ─────────────────────────────────────────────────────────────
|
||
|
||
/** 一条链值不值得排成卡片:跨行写(某一步的 `.` 另起一行)或实参里有闭包时是它的结构;不值得、语言插件读不出链时没有。 */
|
||
function cardChain(node: Node, document: SourceDocument): ChainFacts | undefined {
|
||
const facts = document.language.chain?.(node)
|
||
if (!facts)
|
||
return undefined
|
||
const broken = facts.steps.some((step, i) => step.dot.startPosition.row > (i === 0 ? facts.start : stepEnd(facts.steps[i - 1])).endPosition.row)
|
||
return broken || facts.steps.some(step => step.arguments.some(arg => arg.closure)) ? facts : undefined
|
||
}
|
||
|
||
/**
|
||
* 链排成卡片的样子:起点照抄一行,各步照源码顺序。规则给的读法(groups)先合;别的连着调用同一个方法的合成一步(见 [repeatedStep]),
|
||
* 剩下的一步一项(见 [singleStep])。
|
||
*/
|
||
function chainView(facts: ChainFacts, document: SourceDocument, groups: readonly ChainGroup[] = []): ChainView {
|
||
const { steps } = facts
|
||
const out: ChainStep[] = []
|
||
for (let i = 0; i < steps.length;) {
|
||
const from = i
|
||
const group = groups.find(item => item.from === from && item.to >= from && item.to < steps.length)
|
||
if (group) {
|
||
out.push(groupStep(steps.slice(from, group.to + 1), group, document))
|
||
i = group.to + 1
|
||
continue
|
||
}
|
||
let end = from + 1
|
||
while (end < steps.length && steps[end].method.text === steps[from].method.text && !groups.some(item => item.from === end))
|
||
end++
|
||
out.push(end - from > 1 ? repeatedStep(steps.slice(from, end), document) : singleStep(steps[from], document))
|
||
i = end
|
||
}
|
||
return { start: verbatim(facts.start, document, undefined, true), steps: out }
|
||
}
|
||
|
||
/** 一步一项:实参本身是值得排成卡片的链时排成小链,写在这一步上面的注释是「注释」一行。 */
|
||
function singleStep(step: ChainStepFacts, document: SourceDocument): ChainStep {
|
||
const lines = miniLines(step, document)
|
||
const rows = noteRows([step], document)
|
||
return {
|
||
name: stepName([step], document),
|
||
value: stepValue(step, document, lines !== undefined),
|
||
...(lines && { lines }),
|
||
...(rows && { rows }),
|
||
range: stepRange([step]),
|
||
}
|
||
}
|
||
|
||
/** 连着几次调用同一个方法合成一步:值是「N 次」,下面一张表一行一次,列是各个实参(每次最多一个时只有一列「实参」)。 */
|
||
function repeatedStep(steps: ChainStepFacts[], document: SourceDocument): ChainStep {
|
||
const most = Math.max(...steps.map(step => step.arguments.length))
|
||
const columns = most <= 1 ? ['实参'] : Array.from({ length: most }, (_, i) => `第 ${i + 1} 个`)
|
||
const rows = noteRows(steps, document)
|
||
return {
|
||
name: stepName(steps, document),
|
||
value: [hint(`${steps.length} 次`)],
|
||
...(most > 0 && { table: { columns, items: steps.map(step => ({ cells: step.arguments.map(arg => argumentText(arg, document)) })) } }),
|
||
...(rows && { rows }),
|
||
range: stepRange(steps),
|
||
}
|
||
}
|
||
|
||
/** 规则合成的一步(见 [ChainGroup]):叫法是规则的;规则没换值、没给表时一步的照常,几步的每步一行排成小链。 */
|
||
function groupStep(steps: ChainStepFacts[], group: ChainGroup, document: SourceDocument): ChainStep {
|
||
const [first] = steps
|
||
const single = steps.length === 1 ? first : undefined
|
||
const replaced = group.value !== undefined || group.table !== undefined
|
||
const mini = single && !replaced ? miniLines(single, document) : undefined
|
||
const lines = replaced ? undefined : single ? mini : steps.map(step => callLine(step, document))
|
||
const rows = noteRows(steps, document)
|
||
return {
|
||
name: stepName(steps, document, group.label),
|
||
value: group.value ?? (single ? stepValue(single, document, mini !== undefined) : []),
|
||
...(lines && { lines }),
|
||
...(rows && { rows }),
|
||
...(group.table && { table: group.table }),
|
||
range: stepRange(steps),
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 方法那一列:起了叫法(规则给的 label,或词表里这个方法的中文名,见 [ZhTable] 的 methods)写叫法,几步都是同一个方法时悬停看原名、
|
||
* ⌘ / Ctrl + 点击跳到第一步的方法名;几步不同的方法合成的没有原名可讲。没有叫法时写 `.名字`。
|
||
*/
|
||
function stepName(steps: ChainStepFacts[], document: SourceDocument, label?: string): Inline[] {
|
||
const [first] = steps
|
||
const same = steps.every(step => step.method.text === first.method.text)
|
||
const word = label ?? (same ? table.methods?.[first.method.text] : undefined)
|
||
if (word === undefined)
|
||
return methodText(first, document)
|
||
if (!same)
|
||
return [{ text: word, role: 'function' }]
|
||
return [{ text: word, role: 'function', refers: { startIndex: first.method.startIndex, endIndex: first.method.endIndex }, tooltip: `.${first.method.text}` }]
|
||
}
|
||
|
||
/** `.名字`,名字按函数着色、带源码下标;写了泛型实参的跟着 `::<…>`。 */
|
||
function methodText(step: ChainStepFacts, document: SourceDocument): Inline[] {
|
||
return [
|
||
{ text: '.', role: 'plain' },
|
||
segment(step.method.text, 'function', step.method.startIndex),
|
||
...(step.typeArguments ? [{ text: '::', role: 'plain' as const }, ...verbatim(step.typeArguments, document, undefined, true)] : []),
|
||
]
|
||
}
|
||
|
||
/** 值那一列:实参(见 [argumentText])用逗号隔开,`.await`、`?` 跟在后面(见 [afterText]);实参排成了小链时只剩后面这截。 */
|
||
function stepValue(step: ChainStepFacts, document: SourceDocument, mini: boolean): Inline[] {
|
||
const args = mini ? [] : joined(step.arguments.map(arg => argumentText(arg, document)), ', ')
|
||
const after = afterText(step, document)
|
||
return args.length > 0 && after.length > 0 ? [...args, { text: ' ', role: 'plain' }, ...after] : [...args, ...after]
|
||
}
|
||
|
||
/** 一个实参:收起来的闭包、async 块是一个 fold 框(见 [foldGroup]),别的照抄成一行。 */
|
||
function argumentText(arg: ChainArgument, document: SourceDocument): Inline[] {
|
||
return arg.deferred ? [foldGroup(arg.deferred, document)] : verbatim(arg.node, document, undefined, true)
|
||
}
|
||
|
||
/** 收起来的闭包、async 块:「闭包 |参数| · N 句」,主体只有一个 match 时是「闭包 |参数| · match 值 · N 支」;参数、match 照抄。 */
|
||
function foldGroup(deferred: DeferredFacts, document: SourceDocument): SegmentGroup {
|
||
const say = (node: Node): Inline[] => verbatim(node, document, undefined, true)
|
||
const parts: Inline[][] = [
|
||
...(deferred.parameters ? [say(deferred.parameters)] : []),
|
||
...(deferred.match
|
||
? [[...say(firstLeaf(deferred.match.node)), { text: ' ', role: 'plain' as const }, ...say(deferred.match.value)], [hint(`${deferred.match.arms} 支`)]]
|
||
: [[hint(`${deferred.statements} 句`)]]),
|
||
]
|
||
return { style: 'fold', label: deferred.kind === 'closure' ? '闭包' : '异步块', segments: parts.flatMap((part, i) => i > 0 ? [hint(' · '), ...part] : part) }
|
||
}
|
||
|
||
/** 跟在调用后面的 `.await`、`?` 照常翻(「等待」),连接的 `.` 不写。 */
|
||
function afterText(step: ChainStepFacts, document: SourceDocument): Inline[] {
|
||
const out: Inline[] = []
|
||
let from = step.call.endIndex
|
||
for (const layer of step.after) {
|
||
const mark = layer.children.find(child => child !== null && child.startIndex >= from && child.type !== '.')
|
||
if (mark)
|
||
appendInline(out, joinTokens(tokensIn(layer, { ...nodeSpan(layer), startIndex: mark.startIndex }, document)))
|
||
from = layer.endIndex
|
||
}
|
||
return out
|
||
}
|
||
|
||
/** 只有一个实参、它本身是值得排成卡片的链时排成小链:第一行是它的起点,后面一步一行(见 [callLine]);别的没有。 */
|
||
function miniLines(step: ChainStepFacts, document: SourceDocument): Inline[][] | undefined {
|
||
const [only] = step.arguments
|
||
const inner = step.arguments.length === 1 && only && !only.deferred ? cardChain(only.node, document) : undefined
|
||
return inner && [verbatim(inner.start, document, undefined, true), ...inner.steps.map(item => callLine(item, document))]
|
||
}
|
||
|
||
/** 小链的一节:`.名字(实参)`,`.await`、`?` 跟在后面。 */
|
||
function callLine(step: ChainStepFacts, document: SourceDocument): Inline[] {
|
||
const after = afterText(step, document)
|
||
return [
|
||
...methodText(step, document),
|
||
{ text: '(', role: 'plain' },
|
||
...joined(step.arguments.map(arg => argumentText(arg, document)), ', '),
|
||
{ text: ')', role: 'plain' },
|
||
...(after.length > 0 ? [{ text: ' ', role: 'plain' as const }, ...after] : []),
|
||
]
|
||
}
|
||
|
||
/** 写在这几步上面的注释读成「注释」一行(Markdown 原文),每一步的一段,空行隔开;都没有时没有。 */
|
||
function noteRows(steps: ChainStepFacts[], document: SourceDocument): PropertyRow[] | undefined {
|
||
const parts = steps.filter(step => step.comments.length > 0).map(step => docText(step.comments.map(comment => commentOf(comment, document))))
|
||
return parts.length > 0 ? [{ label: KIND_ZH.comment, value: { markdown: parts.join('\n\n') } }] : undefined
|
||
}
|
||
|
||
/** 块整句就是一条值得排成卡片的链(外面只套着 return 这类关键字和分号):从块的节点往下,沿着只有一个具名子节点的那几层找。 */
|
||
function wholeChain(root: Node, span: SourceSpan, document: SourceDocument): { node: Node, facts: ChainFacts } | undefined {
|
||
for (let node: Node | undefined = root; node && inside(node, span);) {
|
||
const facts = cardChain(node, document)
|
||
if (facts)
|
||
return { node, facts }
|
||
const kids: Node[] = node.namedChildren.filter((child): child is Node => child !== null && !child.isExtra)
|
||
node = kids.length === 1 ? kids[0] : undefined
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
/** 整句是链的块:译文只剩链外面的词(`return` 的「返回」,分号不写),下面是「起点」「步骤」两行,链的各步在「步骤」那一行上。 */
|
||
function chainTranslation(block: TextBlock, chain: Node, facts: ChainFacts, document: SourceDocument): TextTranslation {
|
||
const { header } = block
|
||
const tokens = [
|
||
...tokensIn(block.node, { ...header, endIndex: chain.startIndex }, document),
|
||
...tokensIn(block.node, { ...header, startIndex: chain.endIndex }, document),
|
||
]
|
||
while (tokens.length > 0 && SEPARATORS.has(tokens[tokens.length - 1].raw))
|
||
tokens.pop()
|
||
const view = chainView(facts, document)
|
||
return { text: joinTokens(tokens), rows: [{ label: '起点', value: view.start }, { label: '步骤', value: count(view.steps.length), chain: view.steps }] }
|
||
}
|
||
|
||
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)
|
||
return group(block.group, readerContext(collector(block.node, block.header, document), block.header))
|
||
const chained = wholeChain(block.node, block.header, document)
|
||
if (chained)
|
||
return chainTranslation(block, chained.node, chained.facts, document)
|
||
const slot: Slot = {}
|
||
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 }
|
||
}
|
||
|
||
function expression(node: Node, document: SourceDocument): Inline[] {
|
||
return joinTokens(tokensIn(node, nodeSpan(node), document))
|
||
}
|
||
|
||
function doc(node: Node, document: SourceDocument): Doc | undefined {
|
||
const markdown = docText((table.docNodes?.(node) ?? [node]).map(part => commentOf(part, document)))
|
||
return markdown ? readDoc(markdown) : undefined
|
||
}
|
||
|
||
function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
|
||
const { header } = block
|
||
const { language } = document
|
||
const clipped = (part: SourceSpan): SourceSpan => clip(part, header)
|
||
const phrases: Phrases = {
|
||
translated: node => joinTokens(tokensIn(node, clipped(nodeSpan(node)), document)),
|
||
value: node => cellValue(node, clipped(nodeSpan(node)), document),
|
||
chain: (node) => {
|
||
const facts = inside(node, header) ? cardChain(node, document) : undefined
|
||
return facts && chainView(facts, document)
|
||
},
|
||
body: node => expression(node, document),
|
||
returned: (node) => {
|
||
const [value] = language.command?.(node)?.arguments ?? []
|
||
return value ? expression(value.node, document) : []
|
||
},
|
||
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':
|
||
case 'trait':
|
||
case 'impl':
|
||
case 'module':
|
||
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)
|
||
// Rust 的编译属性由 Rust 的直译排(translators/rustZh.ts)
|
||
case 'attribute':
|
||
return []
|
||
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,
|
||
verbatim: (node, document, role) => verbatim(node, document, role, true),
|
||
doc,
|
||
chain: (node, document, groups) => {
|
||
const facts = document.language.chain?.(node)
|
||
return facts && chainView(facts, document, groups)
|
||
},
|
||
attributeNames: ZH_ATTRIBUTE_NAMES,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 跨行写的挤成一行时,两个词中间的换行处贴着写:圆括号贴着里面的内容(`f(\n x\n)` 写成 `f(x)`),另起一行的 `.` 贴着前面
|
||
* (链式调用 `a\n .b()` 写成 `a.b()`);方括号、花括号照旧空一格。
|
||
* @param between 两个词中间的源码
|
||
* @param previous 前一个词
|
||
* @param next 后一个词(或从它开头的源码)
|
||
*/
|
||
/** 链式调用一步的结尾:调用,套着 `.await`、`?` 时是最外面那层。 */
|
||
function stepEnd(step: ChainStepFacts): Node {
|
||
return step.after[step.after.length - 1] ?? step.call
|
||
}
|
||
|
||
/** 几步合起来在源码里的范围:第一步的 `.` 到最后一步的结尾(见 ChainStep.range)。 */
|
||
function stepRange(steps: readonly ChainStepFacts[]): SourceRange {
|
||
const last = steps[steps.length - 1]
|
||
return { startIndex: steps[0].dot.startIndex, endIndex: stepEnd(last).endIndex }
|
||
}
|
||
|
||
function hugs(between: string, previous: string | undefined, next: string): boolean {
|
||
return between.includes('\n') && (previous === '(' || next.startsWith(')') || next.startsWith('.'))
|
||
}
|
||
|
||
function nodeSpan(node: Node): SourceSpan {
|
||
return { startIndex: node.startIndex, endIndex: node.endIndex, startRow: node.startPosition.row, endRow: node.endPosition.row }
|
||
}
|
||
|
||
/** node 整个落在 span 里。 */
|
||
function inside(node: Node, span: SourceSpan): boolean {
|
||
return node.startIndex >= span.startIndex && node.endIndex <= span.endIndex
|
||
}
|
||
|
||
/** part 超出 limit 结尾的部分截掉:值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致。 */
|
||
function clip(part: SourceSpan, limit: SourceSpan): SourceSpan {
|
||
return part.endIndex > limit.endIndex ? { ...part, endIndex: limit.endIndex, endRow: limit.endRow } : part
|
||
}
|
||
|
||
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 = false
|
||
})
|
||
return segments
|
||
}
|
||
|
||
// ─── 属性表 ───────────────────────────────────────────────────────────────────
|
||
|
||
/** 属性表里一格的值:一行译文,带下一层子行(拆开的字面量的各项)或那次调用的实参表。 */
|
||
interface CellValue {
|
||
text: Inline[]
|
||
items?: PropertyItem[]
|
||
table?: PreviewTable
|
||
}
|
||
|
||
/** 属性表里的值怎么写。 */
|
||
interface Phrases {
|
||
/** 按表翻译,只翻落在块头部里的那一段。 */
|
||
translated: (node: Node) => Inline[]
|
||
/** 一格里的值,只翻落在块头部里的那一段:字面量的各项拆成下一层子行,跨几行写的调用排成「新建 / 调用 …」带实参表(见 cellValue)。 */
|
||
value: (node: Node) => CellValue
|
||
/** 值是一条值得排成卡片的链式调用、整个落在块头部里时它排成的样子(见 ChainView);别的值没有。 */
|
||
chain: (node: Node) => ChainView | undefined
|
||
/** 函数主体里的一句(不在块头部里)整句照常翻:函数打印的那几句。 */
|
||
body: (node: Node) => Inline[]
|
||
/** 一句 return 返回的值(语言插件读出的命令的第一个词);只写了 return 时是空的。 */
|
||
returned: (node: Node) => Inline[]
|
||
/** 源码原样照抄、按源码着色(装饰器)。 */
|
||
verbatim: (node: Node) => Segment[]
|
||
/** 导入来源:字符串字面量按字符串着色,模块名按类型着色。 */
|
||
module: (node: Node) => Segment[]
|
||
/** 源码里的一个名字原样照抄、按 role 着色(函数、类的名称)。 */
|
||
name: (node: Node, role: TokenRole) => Segment[]
|
||
/** 函数、类的说明(JSDoc、docstring):去掉注释符号、引号和共同缩进的 Markdown 原文拆成正文和块标签;没有文字时没有。 */
|
||
doc: (node: Node) => Doc | 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)]
|
||
}
|
||
|
||
/**
|
||
* 函数:名称 / 说明 / 对不上的参数 / 访问级别 / 异步(只有分这个的语言才有)/ 装饰器 / 输入参数 / 输出参数 / 异常。
|
||
* 输入参数是一张表,列名是名称 / 类型 / 默认值,每个参数一行;没有默认值的格子留空。说明里的块标签拆开放:
|
||
* `@param` 进输入参数表多出的「说明」列(有参数对上才有这一列),名字对不上的排在说明下面;`@return` 跟在输出参数的类型后面;
|
||
* `@throws` 是「异常」一行。
|
||
*/
|
||
function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
|
||
const doc = facts.doc && say.doc(facts.doc)
|
||
const names = facts.positional ? facts.positional.flatMap(param => param.name ? [param.name.text] : []) : facts.parameters.map(param => param.name.text)
|
||
const docs = paramDocs(doc, names)
|
||
return [
|
||
{ label: '名称', value: facts.name ? say.name(facts.name, 'function') : [hint('匿名')] },
|
||
...docBodyRows(doc),
|
||
...unmatchedParamRows(docs.unmatched),
|
||
...accessRows(facts.access),
|
||
...flagRows('async', facts.async),
|
||
...decoratorRows(facts.decorators, say),
|
||
...(facts.positional
|
||
? positionalRows(facts, facts.positional, say, docs, doc)
|
||
: [
|
||
parameterRow('输入参数', facts.parameters, say, docs),
|
||
{ label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')], ...returnNote(doc) },
|
||
]),
|
||
...throwsRows(doc),
|
||
]
|
||
}
|
||
|
||
/**
|
||
* 按位置取参数的函数(shell):输入参数写「N 个,按位置」,下面一张表「位置 | 函数里叫」(没起名的写 $N;`@param` 按起的名字对上时多一列说明);
|
||
* 输出是它打印的那几句(调用处用 $(函数 …) 拿去用时注明);返回是写了的 return 值,没写 return、也不打印时是最后一句成不成功。
|
||
*/
|
||
function positionalRows(facts: FunctionFacts, positional: PositionalParameter[], say: Phrases, docs: ParamDocs, doc: Doc | undefined): PropertyRow[] {
|
||
const described = positional.some(param => param.name && docs.of(param.name.text))
|
||
const inputs: PropertyRow = positional.length === 0
|
||
? { label: '输入参数', value: [hint('无')] }
|
||
: {
|
||
label: '输入参数',
|
||
value: [hint(`${positional.length} 个,按位置`)],
|
||
columns: described ? ['位置', '函数里叫', '说明'] : ['位置', '函数里叫'],
|
||
items: positional.map(param => ({
|
||
cells: [
|
||
[hint(`第 ${param.position} 个`)],
|
||
param.name ? say.name(param.name, 'variable') : [segment(`$${param.position}`, 'variable', undefined)],
|
||
...(described ? [param.name ? paramDocCell(docs, param.name.text) : []] : []),
|
||
],
|
||
...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` 跟在返回那一行后面;只有打印、没写 return 时跟在输出那一行后面
|
||
const [noted] = [...result, ...output]
|
||
return [inputs, ...[...output, ...result].map(row => row === noted ? { ...row, ...returnNote(doc) } : row)]
|
||
}
|
||
|
||
/** 解释器:叫什么,怎么找到它(按 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)?.body?.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) })),
|
||
}]
|
||
}
|
||
|
||
/**
|
||
* 参数表:列名是名称 / 类型 / 默认值,每个参数一行,讲的定义是参数名;没有默认值的格子留空。
|
||
* 说明里有参数对上了(docs)时多一列「说明」,没对上的格子留空。
|
||
*/
|
||
function parameterRow(label: string, parameters: ParameterFacts[], say: Phrases, docs: ParamDocs): PropertyRow {
|
||
const described = parameters.some(param => docs.of(param.name.text))
|
||
return {
|
||
label,
|
||
value: count(parameters.length),
|
||
columns: described ? ['名称', '类型', '默认值', '说明'] : ['名称', '类型', '默认值'],
|
||
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) : [],
|
||
...(described ? [paramDocCell(docs, param.name.text)] : []),
|
||
],
|
||
...defining(param.name),
|
||
})),
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 类:名称 / 说明 / 访问级别 / 抽象(只有分这个的语言才有)/ 装饰器 / 类型参数 / 父类 / 实现的接口,
|
||
* 说明及以后各行有才显示;类型参数、父类、实现的接口各是一行,多项用逗号隔开。类型参数那一行讲的是这几个泛型参数的定义。
|
||
* 类没有构造参数表可对,说明里的 `@param` 照写成「构造参数」一张「名称 | 说明」表;`@throws` 是「异常」一行。
|
||
*/
|
||
function classRows(facts: ClassFacts, say: Phrases): PropertyRow[] {
|
||
const listRow = (label: string, parts: Inline[][], defines: Defines = {}): PropertyRow[] =>
|
||
parts.length > 0 ? [{ label, value: joined(parts, ', '), ...defines }] : []
|
||
const doc = facts.doc && say.doc(facts.doc)
|
||
return [
|
||
{ label: '名称', value: facts.name ? say.name(facts.name, 'type') : [hint('匿名')] },
|
||
...docBodyRows(doc),
|
||
...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)),
|
||
...writtenParamRows(doc?.params ?? [], '构造参数'),
|
||
...throwsRows(doc),
|
||
]
|
||
}
|
||
|
||
/** 父类的一项: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') },
|
||
...docBodyRows(facts.doc && say.doc(facts.doc)),
|
||
...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 同一张表):名称 / 说明 / 访问级别 / 装饰器 / 继承(照写的父类),说明及以后有才显示;
|
||
* 类属性收成一张表:属性 | 类型 | 值 | 说明,一个属性都没写类型时没有「类型」列、都没有说明时没有「说明」列,没写的格子留空;
|
||
* 构造参数同函数的输入参数表。没有类属性、没写构造函数时没有这两行。
|
||
* 说明里的 `@param` 对到构造参数表(同函数的输入参数,对不上的排在说明下面;没写构造函数时同类,照写成一张表),`@throws` 是「异常」一行。
|
||
*/
|
||
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),
|
||
})),
|
||
}
|
||
const doc = facts.doc && say.doc(facts.doc)
|
||
const params = facts.parameters && paramDocs(doc, facts.parameters.map(param => param.name.text))
|
||
return [
|
||
{ label: '名称', value: facts.name ? say.name(facts.name, 'type') : [hint('匿名')] },
|
||
...docBodyRows(doc),
|
||
...unmatchedParamRows(params?.unmatched ?? []),
|
||
...accessRows(facts.access),
|
||
...decoratorRows(facts.decorators, say),
|
||
...baseRows(facts.bases, say),
|
||
...(facts.fields.length > 0 ? [fields] : []),
|
||
...(facts.parameters && params ? [parameterRow('构造参数', facts.parameters, say, params)] : writtenParamRows(doc?.params ?? [], '构造参数')),
|
||
...throwsRows(doc),
|
||
]
|
||
}
|
||
|
||
/** 继承:照写的父类,逗号隔开;没有父类时没有这一行。 */
|
||
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)?.body?.markdown).filter(Boolean).join(' ')
|
||
}
|
||
|
||
function docCell(text: string): Inline[] {
|
||
return text ? [segment(text, 'comment', undefined)] : []
|
||
}
|
||
|
||
/** 装饰器:源码原样照抄,多个换行。 */
|
||
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 ? docBodyRows(block.facts.doc && say.doc(block.facts.doc)) : []),
|
||
...accessRows(block.facts.access),
|
||
...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []),
|
||
{ label: valueLabel, ...(declarator.value ? declaredValue(declarator.value, say) : { value: [hint('无')] }) },
|
||
])
|
||
}
|
||
|
||
/**
|
||
* 声明的值(见 Phrases.value):拆开的字面量的各项、跨几行写的调用的实参表(参数 | 值,带列名)都展开在这一行下面;
|
||
* 值是一条值得排成卡片的链式调用时,这一行写起点,链的各步接在下面(见 Phrases.chain)。
|
||
*/
|
||
function declaredValue(value: Node, say: Phrases): Pick<PropertyRow, 'value' | 'items' | 'columns' | 'chain'> {
|
||
const chain = say.chain(value)
|
||
if (chain)
|
||
return { value: chain.start, chain: chain.steps }
|
||
const { text, items, table } = say.value(value)
|
||
if (table)
|
||
return { value: text, ...table }
|
||
return items ? { value: text, items } : { value: text }
|
||
}
|