♻️ refactor(vscode): 代码阅读的翻译插件只经契约拿语言插件的词性和推导式结构
This commit is contained in:
1 parent
07eaf96a6b
commit
202fa954ba
12 files changed
+185
-76
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import type { BlockAttributes, BlockKind, CodeBlock, LanguageAdapter, Node, PreviewText, PropertyRow, Segment, SourceRange, TokenRole, Translator } from './contract'
|
||||
import type { BlockAttributes, BlockKind, CodeBlock, LanguageAdapter, Node, PreviewText, PropertyRow, Segment, SourceDocument, SourceRange, TokenRole, Translator } from './contract'
|
||||
import type { TreeSitterRuntime } from './runtime'
|
||||
import { blockAttributes } from './attributes'
|
||||
import { commonIndent, splitLines } from './editText'
|
||||
@@ -106,6 +106,7 @@ function ownName(block: CodeBlock): Node | undefined {
|
||||
export function toBlockViews(blocks: CodeBlock[], source: string, adapter: LanguageAdapter, translator: Translator | undefined): BlockView[] {
|
||||
const lines = lineRanges(source)
|
||||
const texts = splitLines(source)
|
||||
const document: SourceDocument = { source, language: adapter }
|
||||
const build = (block: CodeBlock, id: string): BlockView => {
|
||||
const { startRow, endRow } = block.header
|
||||
const rows = texts.slice(startRow, endRow + 1)
|
||||
@@ -122,8 +123,8 @@ export function toBlockViews(blocks: CodeBlock[], source: string, adapter: Langu
|
||||
attributes: blockAttributes(block),
|
||||
children: block.children.map((child, i) => build(child, `${id}.${i}`)),
|
||||
...('facts' in block
|
||||
? { properties: translator ? translator.describe(block, source) : [] }
|
||||
: { translation: translator ? translator.translate(block, source) : [] }),
|
||||
? { properties: translator ? translator.describe(block, document) : [] }
|
||||
: { translation: translator ? translator.translate(block, document) : [] }),
|
||||
}
|
||||
}
|
||||
return blocks.map((block, i) => build(block, String(i)))
|
||||
|
||||
@@ -101,8 +101,8 @@ export type IterationClause
|
||||
| { kind: 'if', condition: Node }
|
||||
|
||||
/**
|
||||
* 写在表达式里的遍历(Python 的列表 / 集合 / 字典推导式、生成器表达式):语言插件从语法树读出结构,翻译插件按自己的语序重排。
|
||||
* 节点都指向源码;各节、每一项取的值里还能再套遍历。
|
||||
* 写在表达式里的遍历(Python 的列表 / 集合 / 字典推导式、生成器表达式):语言插件从语法树读出结构(LanguageAdapter.iteration),
|
||||
* 翻译插件按自己的语序重排。节点都指向源码;各节、每一项取的值里还能再套遍历。
|
||||
*/
|
||||
export interface IterationFacts {
|
||||
result: IterationResult
|
||||
@@ -247,7 +247,10 @@ export interface PropertyRow extends Defines {
|
||||
columns?: string[]
|
||||
}
|
||||
|
||||
/** 语言插件:语法树 → 代码块树。 */
|
||||
/**
|
||||
* 语言插件:语法树 → 代码块树,外加只有它懂、翻译插件翻表达式时要用的信息(叶子的词性、写在表达式里的遍历的结构)。
|
||||
* 翻译插件翻一块时从 SourceDocument.language 拿到解析这份源码的语言插件,不直接依赖某个语言插件。
|
||||
*/
|
||||
export interface LanguageAdapter {
|
||||
id: string
|
||||
/** 覆盖的 VS Code languageId,例如 ['typescript', 'typescriptreact', 'javascript', 'javascriptreact']。 */
|
||||
@@ -256,8 +259,16 @@ export interface LanguageAdapter {
|
||||
grammarPath: (languageId: string) => string
|
||||
/** languageId 是这份源码的 languageId:一个插件覆盖几种方言时按它区分(JavaScript 没有 TypeScript 的抽象类、仅类型导入)。 */
|
||||
toBlocks: (root: Node, source: string, languageId: string) => CodeBlock[]
|
||||
/** 源码着色:给语法树叶子节点定词性。 */
|
||||
/** 给语法树叶子节点定词性:源码着色用它,译文里照抄的词也按它着色、按它认出能查定义的名字。 */
|
||||
tokenRole: (leaf: Node) => TokenRole
|
||||
/** node 是写在表达式里的遍历(推导式、生成器表达式)时给出它的结构,别的节点没有;没有这种写法的语言不填。 */
|
||||
iteration?: (node: Node) => IterationFacts | undefined
|
||||
}
|
||||
|
||||
/** 翻译插件翻一块时拿到的这份源码:源码字符串,和解析它的语言插件。 */
|
||||
export interface SourceDocument {
|
||||
source: string
|
||||
language: LanguageAdapter
|
||||
}
|
||||
|
||||
/** 筛选属性和值的显示名,筛选栏、种类标签用它;没给名字的显示 id。 */
|
||||
@@ -274,9 +285,9 @@ export interface Translator {
|
||||
label: string // '中文直译'
|
||||
languageIds: string[]
|
||||
/** 只翻 header 那一段;注释块给去掉注释符号的 Markdown 原文。 */
|
||||
translate: (block: TextBlock, source: string) => PreviewText
|
||||
translate: (block: TextBlock, document: SourceDocument) => PreviewText
|
||||
/** 函数、类、导入、常量、变量块 → 属性表。 */
|
||||
describe: (block: FactBlock, source: string) => PropertyRow[]
|
||||
describe: (block: FactBlock, document: SourceDocument) => PropertyRow[]
|
||||
/** 筛选属性和值叫什么,和属性表里的属性名、值用同一套叫法;不给就都显示 id。 */
|
||||
attributeNames?: AttributeNames
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { IterationFacts, Node } from '../contract'
|
||||
import type { Outline } from '../testSupport'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { factsByLine, nodesToText, outline, readFixture, runtime, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { createPythonAdapter, pythonIteration } from './python'
|
||||
import { createPythonAdapter } from './python'
|
||||
|
||||
const adapter = createPythonAdapter(WASM_DIR)
|
||||
|
||||
@@ -335,7 +335,7 @@ describe('python 写在表达式里的遍历(推导式)', () => {
|
||||
try {
|
||||
const found: IterationFacts[] = []
|
||||
const visit = (node: Node): void => {
|
||||
const facts = pythonIteration(node)
|
||||
const facts = adapter.iteration?.(node)
|
||||
if (facts)
|
||||
found.push(facts)
|
||||
for (const child of node.children) {
|
||||
|
||||
@@ -269,7 +269,7 @@ const ITERATION_RESULTS: Record<string, IterationResult> = {
|
||||
* 推导式、生成器表达式的结构,别的节点没有。几种读不成遍历的写法也没有,照常翻:
|
||||
* `for x in a, b`(语法树认、Python 3 不认的多个来源),以及缺了变量、来源、条件或每一项的值(写到一半的代码)。
|
||||
*/
|
||||
export function pythonIteration(node: Node): IterationFacts | undefined {
|
||||
function pythonIteration(node: Node): IterationFacts | undefined {
|
||||
const result = ITERATION_RESULTS[node.type]
|
||||
const body = node.childForFieldName('body')
|
||||
if (!result || !body)
|
||||
@@ -360,7 +360,7 @@ function insideString(leaf: Node): boolean {
|
||||
}
|
||||
|
||||
/** 注解里的名字:typed_parameter / 返回值 / 带注解赋值的 type 节点之内。 */
|
||||
export function insideAnnotation(leaf: Node): boolean {
|
||||
function insideAnnotation(leaf: Node): boolean {
|
||||
for (let node = leaf.parent; node; node = node.parent) {
|
||||
if (node.type === 'type')
|
||||
return true
|
||||
@@ -395,7 +395,7 @@ function identifierRole(leaf: Node): TokenRole {
|
||||
return 'variable'
|
||||
}
|
||||
|
||||
export function pythonTokenRole(leaf: Node): TokenRole {
|
||||
function pythonTokenRole(leaf: Node): TokenRole {
|
||||
if (leaf.type === 'comment')
|
||||
return 'comment'
|
||||
if (insideString(leaf))
|
||||
@@ -430,5 +430,6 @@ export function createPythonAdapter(wasmDir: string): LanguageAdapter {
|
||||
grammarPath: () => path.join(wasmDir, 'tree-sitter-python.wasm'),
|
||||
toBlocks: root => statements(named(root), 'module', -1, true),
|
||||
tokenRole: pythonTokenRole,
|
||||
iteration: pythonIteration,
|
||||
}
|
||||
}
|
||||
@@ -479,7 +479,7 @@ function nameRole(leaf: Node): TokenRole {
|
||||
return 'variable'
|
||||
}
|
||||
|
||||
export function typescriptTokenRole(leaf: Node): TokenRole {
|
||||
function typescriptTokenRole(leaf: Node): TokenRole {
|
||||
if (leaf.type === 'comment' || leaf.type === 'hash_bang_line')
|
||||
return 'comment'
|
||||
if (insideString(leaf))
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { IterationFacts, LanguageAdapter, Node, TextBlock } from '../contract'
|
||||
import * as path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { segmentText, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { createZhTemplateTranslator } from './zhTemplate'
|
||||
|
||||
/** 每条顶层语句一块一行译文。 */
|
||||
function statement(node: Node): TextBlock {
|
||||
return {
|
||||
kind: 'statement',
|
||||
node,
|
||||
header: { startIndex: node.startIndex, endIndex: node.endIndex, startRow: node.startPosition.row, endRow: node.endPosition.row },
|
||||
children: [],
|
||||
}
|
||||
}
|
||||
|
||||
/** 假装 `keep(变量, 来源, 条件)` 是这门语言写在表达式里的遍历:结构只由语言插件给,翻译插件不认识这个写法。 */
|
||||
function keep(node: Node): IterationFacts | undefined {
|
||||
const [variables, source, condition] = node.childForFieldName('arguments')?.namedChildren ?? []
|
||||
if (node.type !== 'call' || node.childForFieldName('function')?.text !== 'keep' || !variables || !source || !condition)
|
||||
return undefined
|
||||
return { result: 'list', clauses: [{ kind: 'for', variables, source, async: false }, { kind: 'if', condition }], value: variables }
|
||||
}
|
||||
|
||||
/** 只照 contract 写的第三方语言插件:借 Python 的语法包解析,词性、遍历结构都自己给。 */
|
||||
function fakeLanguage(iteration?: LanguageAdapter['iteration']): LanguageAdapter {
|
||||
return {
|
||||
id: 'fake',
|
||||
languageIds: ['fake'],
|
||||
grammarPath: () => path.join(WASM_DIR, 'tree-sitter-python.wasm'),
|
||||
toBlocks: root => root.namedChildren.flatMap(node => node ? [statement(node)] : []),
|
||||
tokenRole: leaf => leaf.type === 'identifier' ? 'variable' : 'plain',
|
||||
...(iteration && { iteration }),
|
||||
}
|
||||
}
|
||||
|
||||
const translator = createZhTemplateTranslator({
|
||||
id: 'fake.zh-template',
|
||||
label: '中文直译',
|
||||
languageIds: ['fake'],
|
||||
keywords: {},
|
||||
literals: {},
|
||||
builtinTypes: {},
|
||||
isTypePosition: () => false,
|
||||
operators: {},
|
||||
operatorParents: [],
|
||||
emptyLiterals: {},
|
||||
mapLiterals: {},
|
||||
stringNodes: [],
|
||||
commentText: node => node.text,
|
||||
nodeTemplates: {},
|
||||
})
|
||||
|
||||
function translateFirst(language: LanguageAdapter, source: string): Promise<string> {
|
||||
return withBlocks(language, 'fake', source, ([block]) => {
|
||||
const translation = translator.translate(block as TextBlock, { source, language })
|
||||
return 'markdown' in translation ? translation.markdown : segmentText(translation)
|
||||
})
|
||||
}
|
||||
|
||||
describe('只照 contract 写的语言插件 + 中文模板', () => {
|
||||
it('语言插件的 iteration 认出的节点整个换成遍历框,框里按读的顺序重排', async () => {
|
||||
expect(await translateFirst(fakeLanguage(keep), 'print(keep(v, vs, v > 0))'))
|
||||
.toBe('print(⟦列表|遍历 vs 的每个 v,仅当 v > 0,取 v⟧)')
|
||||
})
|
||||
|
||||
it('同一个翻译插件配上不给 iteration 的语言插件就照常翻', async () => {
|
||||
expect(await translateFirst(fakeLanguage(), 'print(keep(v, vs, v > 0))')).toBe('print(keep(v, vs, v > 0))')
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,17 @@
|
||||
import type { Node, Translator } from '../contract'
|
||||
import { insideAnnotation, pythonIteration, pythonTokenRole } from '../languages/python'
|
||||
import { createZhTemplateTranslator } from './zhTemplate'
|
||||
|
||||
/** 写在类型注解里(参数、返回值、带注解赋值的 type 节点之内):内置类型名只在这里翻成中文。 */
|
||||
function insideAnnotation(leaf: Node): boolean {
|
||||
for (let node = leaf.parent; node; node = node.parent) {
|
||||
if (node.type === 'type')
|
||||
return true
|
||||
if (node.type === 'block' || node.type === 'module')
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function stringContent(node: Node): string {
|
||||
if (node.type === 'string_content')
|
||||
return node.text
|
||||
@@ -17,7 +27,6 @@ export const pythonZhTranslator: Translator = createZhTemplateTranslator({
|
||||
id: 'python.zh-template',
|
||||
label: '中文直译',
|
||||
languageIds: ['python'],
|
||||
tokenRole: pythonTokenRole,
|
||||
keywords: {
|
||||
'import': '导入',
|
||||
'from': { zh: '从', byContext: { raise_statement: '源自', yield: '自' } },
|
||||
@@ -87,5 +96,4 @@ export const pythonZhTranslator: Translator = createZhTemplateTranslator({
|
||||
nodeTemplates: {
|
||||
conditional_expression: ['如果', { namedChild: 1 }, '则', { namedChild: 0 }, '否则', { namedChild: 2 }],
|
||||
},
|
||||
iteration: pythonIteration,
|
||||
})
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Node, Translator } from '../contract'
|
||||
import { typescriptTokenRole } from '../languages/typescript'
|
||||
import { createZhTemplateTranslator } from './zhTemplate'
|
||||
|
||||
function commentText(node: Node): string {
|
||||
@@ -18,7 +17,6 @@ export const typescriptZhTranslator: Translator = createZhTemplateTranslator({
|
||||
id: 'typescript.zh-template',
|
||||
label: '中文直译',
|
||||
languageIds: ['typescript', 'typescriptreact', 'javascript', 'javascriptreact'],
|
||||
tokenRole: typescriptTokenRole,
|
||||
keywords: {
|
||||
// 流程
|
||||
import: '导入',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Inline, LanguageAdapter, PropertyItem, PropertyRow, Segment, Translator } from '../contract'
|
||||
import type { Inline, LanguageAdapter, PropertyItem, PropertyRow, Segment, TokenRole, Translator } from '../contract'
|
||||
import type { ZhTable } from './zhTemplate'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createPythonAdapter } from '../languages/python'
|
||||
@@ -22,7 +22,7 @@ function translateAll(adapter: LanguageAdapter, languageId: string, translator:
|
||||
const line = block.header.startRow + 1
|
||||
if (byLine.has(line) || 'facts' in block)
|
||||
return
|
||||
const translation = translator.translate(block, source)
|
||||
const translation = translator.translate(block, { source, language: adapter })
|
||||
byLine.set(line, 'markdown' in translation
|
||||
? { kind: block.kind, segments: [], markdown: translation.markdown }
|
||||
: { kind: block.kind, segments: translation })
|
||||
@@ -38,7 +38,7 @@ function describeAll(adapter: LanguageAdapter, languageId: string, translator: T
|
||||
walk(blocks, (block) => {
|
||||
const line = block.header.startRow + 1
|
||||
if (!byLine.has(line) && 'facts' in block)
|
||||
byLine.set(line, translator.describe(block, source))
|
||||
byLine.set(line, translator.describe(block, { source, language: adapter }))
|
||||
})
|
||||
return byLine
|
||||
})
|
||||
@@ -1034,7 +1034,6 @@ describe('中文模板引擎', () => {
|
||||
id: 'test.zh-template',
|
||||
label: '测试',
|
||||
languageIds: ['python'],
|
||||
tokenRole: () => 'keyword',
|
||||
keywords: {},
|
||||
literals: {},
|
||||
builtinTypes: {},
|
||||
@@ -1050,6 +1049,11 @@ describe('中文模板引擎', () => {
|
||||
})
|
||||
}
|
||||
|
||||
/** 按 Python 分块、词性都是 role 的语言插件:引擎着色只看翻译时传进来的语言插件。 */
|
||||
function pythonColored(role: TokenRole): LanguageAdapter {
|
||||
return { ...py, tokenRole: () => role }
|
||||
}
|
||||
|
||||
it('关键字按父节点类型选译法,更长的节点类型链优先,都不匹配用默认', async () => {
|
||||
const translator = testTranslator({
|
||||
keywords: {
|
||||
@@ -1066,7 +1070,7 @@ describe('中文模板引擎', () => {
|
||||
'else:',
|
||||
' pass',
|
||||
].join('\n')
|
||||
const map = texts(await translateAll(py, 'python', translator, source))
|
||||
const map = texts(await translateAll(pythonColored('keyword'), 'python', translator, source))
|
||||
expect(map[1]).toBe('for k 乙 items')
|
||||
expect(map[2]).toBe('if k 甲 seen')
|
||||
expect(map[4]).toBe('丁')
|
||||
@@ -1076,7 +1080,6 @@ describe('中文模板引擎', () => {
|
||||
|
||||
it('节点模板按节点类型重排:槽位按字段名或具名子节点序号取子节点并递归翻译,译词按关键字着色', async () => {
|
||||
const translator = testTranslator({
|
||||
tokenRole: () => 'variable',
|
||||
keywords: { in: '在', else: { zh: '丙', byContext: { else_clause: '丁' } } },
|
||||
operators: { '==': '等于' },
|
||||
operatorParents: ['comparison_operator'],
|
||||
@@ -1091,7 +1094,7 @@ describe('中文模板引擎', () => {
|
||||
'print((a # 注释不占序号',
|
||||
' if k == 1 else b))',
|
||||
].join('\n')
|
||||
const map = await translateAll(py, 'python', translator, source)
|
||||
const map = await translateAll(pythonColored('variable'), 'python', translator, source)
|
||||
expect(texts(map)).toEqual({
|
||||
1: '令 n 加上 若 k 在 seen 则 a 不然 b',
|
||||
2: 'print(若 c 则 a 不然 若 d 则 b 不然 e)',
|
||||
@@ -1103,13 +1106,12 @@ describe('中文模板引擎', () => {
|
||||
|
||||
it('属性表里的类型、值用同一张表和同一套模板翻', async () => {
|
||||
const translator = testTranslator({
|
||||
tokenRole: () => 'variable',
|
||||
keywords: { in: '在' },
|
||||
builtinTypes: { int: '整型' },
|
||||
isTypePosition: leaf => leaf.parent?.type === 'type',
|
||||
nodeTemplates: { conditional_expression: ['若', { namedChild: 1 }, '则', { namedChild: 0 }, '不然', { namedChild: 2 }] },
|
||||
})
|
||||
const rows = await describeAll(py, 'python', translator, 'n: int = a if k in seen else b')
|
||||
const rows = await describeAll(pythonColored('variable'), 'python', translator, 'n: int = a if k in seen else b')
|
||||
expect(rowTexts(rows)[1]).toEqual(['名称: n', '访问级别: 公共', '类型: 整型', '初始值: 若 k 在 seen 则 a 不然 b'])
|
||||
})
|
||||
|
||||
@@ -1127,7 +1129,7 @@ describe('中文模板引擎', () => {
|
||||
'else:',
|
||||
' pass',
|
||||
].join('\n')
|
||||
expect(texts(await translateAll(py, 'python', translator, source))).toEqual({
|
||||
expect(texts(await translateAll(pythonColored('keyword'), 'python', translator, source))).toEqual({
|
||||
1: 'x += a if k 在 seen 丙 b',
|
||||
2: 'if k 在 seen',
|
||||
3: 'pass',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, AttributeNames, BlockKind, ClassFacts, CommonAttribute, Defines, EntryFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, IterationClause, IterationFacts, IterationResult, Markdown, Node, PreviewText, PropertyItem, PropertyRow, Segment, SegmentGroup, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
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 后「未中断时」。 */
|
||||
@@ -20,12 +20,14 @@ export interface ContextualWord {
|
||||
export type TemplatePart = string | { field: string } | { namedChild: number }
|
||||
|
||||
/** 一种语言的中文直译表。引擎替换表里的关键字、内置类型、运算符,按节点模板重排少数语法结构,其余原样照抄。 */
|
||||
/**
|
||||
* 一种语言的中文词表:只有叫法和语法节点类型,语言知道的结构(词性、遍历)翻译时从 SourceDocument.language 取——
|
||||
* 未翻译的词、关键字译文按它给的词性着色,它认出的遍历整个换成一个框,框里按读的顺序重排(见 iterationGroup)。
|
||||
*/
|
||||
export interface ZhTable {
|
||||
id: string
|
||||
label: string
|
||||
languageIds: string[]
|
||||
/** 未翻译的词按源码着色的词性上色;关键字译文的词性也取它。 */
|
||||
tokenRole: (leaf: Node) => TokenRole
|
||||
/** 关键字:按语法树里匿名节点的类型查(Python 的 `not in` 这类组合关键字本身也是一个匿名节点);随所在语法节点换译法的写成 ContextualWord。 */
|
||||
keywords: Record<string, string | ContextualWord>
|
||||
/** 字面量关键字:具名的叶子节点(true / None …),按节点类型查。 */
|
||||
@@ -49,11 +51,6 @@ export interface ZhTable {
|
||||
* 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。
|
||||
*/
|
||||
nodeTemplates: Record<string, TemplatePart[]>
|
||||
/**
|
||||
* 写在表达式里的遍历(推导式、生成器表达式)的结构,语言插件给;没有这种写法的语言不填。
|
||||
* 认出来的节点整个换成一个框,框里按读的顺序重排,见 iterationGroup。
|
||||
*/
|
||||
iteration?: (node: Node) => IterationFacts | undefined
|
||||
}
|
||||
|
||||
interface Token {
|
||||
@@ -73,7 +70,7 @@ interface Token {
|
||||
|
||||
/** 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。 */
|
||||
interface Collector {
|
||||
source: string
|
||||
document: SourceDocument
|
||||
tokens: Token[]
|
||||
cursor: number
|
||||
gap: boolean
|
||||
@@ -184,13 +181,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
}
|
||||
|
||||
/** 照抄这个节点时带不带源码下标:名字(按名字着色的具名叶子)和整段字符串带,标点、数字、关键字不带。 */
|
||||
function offsetOf(node: Node): number | undefined {
|
||||
const linkable = stringNodes.has(node.type) || (node.childCount === 0 && node.isNamed && NAME_ROLES.has(table.tokenRole(node)))
|
||||
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): { text: string, role: TokenRole } | 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)
|
||||
@@ -202,11 +199,11 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return { text: table.operators[node.type], role: 'operator' }
|
||||
const word = table.keywords[node.type]
|
||||
if (word !== undefined)
|
||||
return { text: keywordText(word, node), role: table.tokenRole(firstLeaf(node)) }
|
||||
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: table.tokenRole(node) }
|
||||
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
|
||||
@@ -223,7 +220,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
|
||||
/** 源码里上一个词到 index 之间有空白,下一个词前面就空一格。 */
|
||||
function advance(out: Collector, index: number): void {
|
||||
out.gap ||= /\s/.test(out.source.slice(out.cursor, index))
|
||||
out.gap ||= /\s/.test(out.document.source.slice(out.cursor, index))
|
||||
out.cursor = index
|
||||
}
|
||||
|
||||
@@ -239,10 +236,10 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
}
|
||||
|
||||
/** 语言插件认出的写在表达式里的遍历;没整个落在要翻的这一段里就不重排、照常翻。 */
|
||||
function iterationIn(node: Node, span: SourceSpan): IterationFacts | undefined {
|
||||
if (!table.iteration || node.startIndex < span.startIndex || node.endIndex > span.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 table.iteration(node)
|
||||
return language.iteration(node)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,11 +247,11 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
* 同步、异步换了就另起一句),条件在它所在的位置写「,仅当 <条件>」,最后「,取 <值>」,字典是「<键>: <值>」。
|
||||
* 来源、变量、条件、值照常翻,里面的名字带源码下标,套着的遍历是框里的框。
|
||||
*/
|
||||
function iterationGroup(facts: IterationFacts, span: SourceSpan, source: string): SegmentGroup {
|
||||
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, source)))
|
||||
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') {
|
||||
@@ -295,12 +292,12 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
/** 遍历整个换成一个框;和外面的调用共用的那对括号照常写在框外,紧贴着框。 */
|
||||
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: table.tokenRole(paren), translated: false })
|
||||
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.source) })
|
||||
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))
|
||||
@@ -315,7 +312,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
emitSource(out, node, { text: '', role: 'comment', translated: false })
|
||||
return
|
||||
}
|
||||
const iteration = iterationIn(node, span)
|
||||
const { language } = out.document
|
||||
const iteration = iterationIn(node, span, language)
|
||||
if (iteration) {
|
||||
emitIteration(out, node, iteration, span)
|
||||
return
|
||||
@@ -335,14 +333,14 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
out.cursor = node.endIndex
|
||||
return
|
||||
}
|
||||
const translated = translateNode(node)
|
||||
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' : table.tokenRole(node)
|
||||
emitSource(out, node, { text: oneLine(node.text), role, translated: false, offset: offsetOf(node) })
|
||||
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) {
|
||||
@@ -352,14 +350,14 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
}
|
||||
|
||||
/** 按译文顺序收集 node 落在 span 里的词。 */
|
||||
function tokensIn(node: Node, span: SourceSpan, source: string): Token[] {
|
||||
const out: Collector = { source, tokens: [], cursor: span.startIndex, gap: false }
|
||||
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: string, role?: TokenRole): Segment[] {
|
||||
function verbatim(node: Node, { source, language }: SourceDocument, role?: TokenRole): Segment[] {
|
||||
const segments: Segment[] = []
|
||||
let cursor = node.startIndex
|
||||
const visit = (current: Node): void => {
|
||||
@@ -373,7 +371,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return
|
||||
}
|
||||
pushSegment(segments, source.slice(cursor, current.startIndex), 'plain')
|
||||
pushSegment(segments, current.text, role ?? table.tokenRole(current), offsetOf(current))
|
||||
pushSegment(segments, current.text, role ?? language.tokenRole(current), offsetOf(current, language))
|
||||
cursor = current.endIndex
|
||||
}
|
||||
visit(node)
|
||||
@@ -388,10 +386,10 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return { markdown: docText(nodes.map(table.commentText)) }
|
||||
}
|
||||
|
||||
function translate(block: TextBlock, source: string): PreviewText {
|
||||
function translate(block: TextBlock, document: SourceDocument): PreviewText {
|
||||
if (block.kind === 'comment')
|
||||
return translateComment(block)
|
||||
const tokens = tokensIn(block.node, block.header, source)
|
||||
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))
|
||||
@@ -399,12 +397,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return joinTokens(tokens)
|
||||
}
|
||||
|
||||
function describe(block: FactBlock, source: string): PropertyRow[] {
|
||||
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, source))
|
||||
return joinTokens(tokensIn(node, span, document))
|
||||
}
|
||||
const phrases: Phrases = {
|
||||
translated: node => within(node, { startIndex: node.startIndex, endIndex: node.endIndex, startRow: node.startPosition.row, endRow: node.endPosition.row }),
|
||||
@@ -413,10 +412,10 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
const name = table.mapLiterals[literal.type]
|
||||
return [hint(name ? `${name} ${size} 项` : `${size} 项`)]
|
||||
},
|
||||
verbatim: node => verbatim(node, source),
|
||||
verbatim: node => verbatim(node, document),
|
||||
// 点分模块名逐节照抄,每一节各自能跳到定义,和 VS Code 里一样
|
||||
module: node => stringNodes.has(node.type) ? [segment(node.text, 'string', offsetOf(node))] : verbatim(node, source, 'type'),
|
||||
name: (node, role) => [segment(node.text, role, offsetOf(node))],
|
||||
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) => {
|
||||
const markdown = docText([table.commentText(node)])
|
||||
return markdown ? { markdown } : undefined
|
||||
|
||||
Reference in new issue
Block a user