689 lines
30 KiB
TypeScript
689 lines
30 KiB
TypeScript
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) }
|
||
}
|
||
}
|