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

689 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Access, AttributeNames, BlockKind, ClassFacts, CommonAttribute, Defines, EntryFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, PreviewText, PropertyItem, PropertyRow, Segment, SegmentGroup, SourceDocument, SourceSpan, TextBlock, 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)节点去掉注释符号后的原文,可以多行。 */
commentText: (node: Node) => string
/**
* 节点模板:节点类型 → 按中文语序重排的译文。命中的节点只输出模板里的译词和槽位,节点自己的关键字、标点不再输出;
* 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。
*/
nodeTemplates: Record<string, TemplatePart[]>
}
interface Token {
/** 注释是空文字:不进译文,只在前后留一个空格。 */
text: string
role: TokenRole
translated: boolean
/** 源码里的原文,用来认出逗号、括号;模板插入的译词、框没有原文。 */
raw: string
/** 前面要空一格:源码里它前面有空白,或者模板在它前面插了别的项。 */
gap: boolean
/** 照抄的名字、字符串在源码里的起始下标,见 Segment.offset。 */
offset?: number
/** 整个换成一个框的节点(写在表达式里的遍历):输出这个框,text 为空。 */
group?: SegmentGroup
}
/** 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。 */
interface Collector {
document: SourceDocument
tokens: Token[]
cursor: number
gap: boolean
}
const CONTEXT_SEPARATOR = ' > '
const CLOSING = new Set([')', ']', '}'])
const HEADER_OPENERS = new Set(['{', ':'])
/** 源码着色里代表名字的词性:着这几种色的具名叶子是标识符、属性名、类型名,能查定义。 */
const NAME_ROLES = new Set<TokenRole>(['variable', 'function', 'type'])
function firstLeaf(node: Node): Node {
const first = node.child(0)
return first ? firstLeaf(first) : node
}
function commentsWithin(node: Node, span: SourceSpan): Node[] {
if (node.endIndex <= span.startIndex || node.startIndex >= span.endIndex)
return []
if (node.type === 'comment')
return [node]
return node.children.flatMap(child => child ? commentsWithin(child, span) : [])
}
function 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: '类',
type: '类型',
field: '字段',
const: '常量',
variable: '变量',
if: '条件',
else: '否则',
loop: '循环',
try: '尝试',
catch: '捕获',
finally: '最终',
return: '返回',
statement: '语句',
comment: '注释',
}
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
const ITERATION_ZH: Record<IterationResult, string> = { generator: '生成器', list: '列表', set: '集合', dict: '字典' }
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 之间有空白,下一个词前面就空一格。 */
function advance(out: Collector, index: number): void {
out.gap ||= /\s/.test(out.document.source.slice(out.cursor, index))
out.cursor = index
}
function emit(out: Collector, token: Omit<Token, 'gap'>): void {
out.tokens.push({ ...token, gap: out.gap })
out.gap = false
}
function emitSource(out: Collector, node: Node, token: Omit<Token, 'gap' | 'raw'>): void {
advance(out, node.startIndex)
emit(out, { ...token, raw: node.text })
out.cursor = node.endIndex
}
/** 语言插件认出的写在表达式里的遍历;没整个落在要翻的这一段里就不重排、照常翻。 */
function 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 shared = 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 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 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 里的词。 */
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument): Token[] {
const out: Collector = { document, tokens: [], cursor: span.startIndex, gap: false }
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 translateComment(block: TextBlock): 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(table.commentText)) }
}
function translate(block: TextBlock, document: SourceDocument): PreviewText {
if (block.kind === 'comment')
return translateComment(block)
const tokens = tokensIn(block.node, block.header, document)
const compound = block.children.length > 0 || block.header.endIndex < block.node.endIndex
const last = tokens.at(-1)
if (compound && last && !last.translated && HEADER_OPENERS.has(last.raw))
tokens.pop()
return joinTokens(tokens)
}
function expression(node: Node, document: SourceDocument): Inline[] {
return joinTokens(tokensIn(node, nodeSpan(node), document))
}
function doc(node: Node): Markdown | undefined {
const markdown = docText([table.commentText(node)])
return markdown ? { markdown } : undefined
}
function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
const { header } = block
const { language } = document
// 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致
const within = (node: Node, part: SourceSpan): Inline[] => {
const span = part.endIndex > header.endIndex ? { ...part, endIndex: header.endIndex, endRow: header.endRow } : part
return joinTokens(tokensIn(node, span, document))
}
const phrases: Phrases = {
translated: node => within(node, nodeSpan(node)),
within,
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,
}
switch (block.kind) {
case 'function':
return functionRows(block.facts, phrases)
case 'class':
return classRows(block.facts, phrases)
case 'import':
return importRows(block.facts, phrases)
default:
return variableRows(block, phrases)
}
}
return {
id: table.id,
label: table.label,
languageIds: table.languageIds,
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[]
/** 字典 / 对象字面量的项数:「字典 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
}
function hint(text: string): Segment {
return { text, role: 'hint' }
}
/** 布尔属性的一行:没有这个概念的语言(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),
{
label: '输入参数',
value: count(facts.parameters.length),
columns: ['名称', '类型', '默认值'],
items: facts.parameters.map(param => ({
cells: [
param.optional ? [...say.translated(param.name), { text: '?', role: 'plain' }] : say.translated(param.name),
param.type ? say.translated(param.type) : [hint('未标注')],
param.default ? say.translated(param.default) : [],
],
...defining(param.name),
})),
},
{ label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')] },
]
}
/**
* 类:名称 / 说明 / 访问级别 / 抽象(只有分这个的语言才有)/ 装饰器 / 类型参数 / 父类 / 实现的接口,
* 说明及以后各行有才显示;类型参数、父类、实现的接口各是一行,多项用逗号隔开。类型参数那一行讲的是这几个泛型参数的定义。
*/
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 => base.typeArguments ? [...say.translated(base.value), ...say.translated(base.typeArguments)] : say.translated(base.value))),
...listRow('实现的接口', facts.implements.map(say.translated)),
]
}
/** 函数、类的说明:有文字才有这一行。 */
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)) })) },
]
}
/** 常量 / 变量:每个声明的变量依次是名称 / 访问级别 / 类型(有标注才有)/ 值(变量叫初始值,字典 / 对象字面量见 literalValue)。 */
function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
const valueLabel = block.kind === 'const' ? '值' : '初始值'
return block.facts.declarators.flatMap(declarator => [
{ label: '名称', value: say.translated(declarator.name), ...defining(declarator.name) },
...accessRows(block.facts.access),
...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []),
{ label: valueLabel, ...(declarator.value ? literalValue(declarator.value, declarator.entries, say) : { value: [hint('无')] }) },
])
}
/**
* 值:字典 / 对象字面量(语言插件给了 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) }
}
}