♻️ refactor(vscode): 代码阅读的翻译插件只经契约拿语言插件的词性和推导式结构

This commit is contained in:
cruldra committed 2026-09-26 16:24:36 +08:00
1 parent 07eaf96a6b
commit 202fa954ba
12 files changed
+185 -76

No files matched your search

+18 -9
View File
@@ -365,7 +365,7 @@ export interface PropertyItem extends Defines { cells: Inline[][], items?: Prope
*/ */
export interface PropertyRow extends Defines { label: string, value: PreviewText, items?: PropertyItem[], columns?: string[] } export interface PropertyRow extends Defines { label: string, value: PreviewText, items?: PropertyItem[], columns?: string[] }
/** 语言插件:语法树 → 代码块树。 */ /** 语言插件:语法树 → 代码块树,外加只有它懂、翻译插件翻表达式时要用的信息(叶子的词性、写在表达式里的遍历的结构)。 */
export interface LanguageAdapter { export interface LanguageAdapter {
id: string id: string
/** 覆盖的 VS Code languageId,例如 ['typescript', 'typescriptreact', 'javascript', 'javascriptreact']。 */ /** 覆盖的 VS Code languageId,例如 ['typescript', 'typescriptreact', 'javascript', 'javascriptreact']。 */
@@ -374,10 +374,15 @@ export interface LanguageAdapter {
grammarPath: (languageId: string) => string grammarPath: (languageId: string) => string
/** languageId:一个插件覆盖几种方言时按它区分(JavaScript 没有 TypeScript 的抽象类、仅类型导入)。 */ /** languageId:一个插件覆盖几种方言时按它区分(JavaScript 没有 TypeScript 的抽象类、仅类型导入)。 */
toBlocks: (root: Node, source: string, languageId: string) => CodeBlock[] toBlocks: (root: Node, source: string, languageId: string) => CodeBlock[]
/** 源码着色:给语法树叶子节点定词性。 */ /** 给叶子节点定词性:源码着色用它,译文里照抄的词也按它着色、按它认出能查定义的名字。 */
tokenRole: (leaf: Node) => TokenRole tokenRole: (leaf: Node) => TokenRole
/** node 是写在表达式里的遍历(推导式、生成器表达式)时给出它的结构,别的节点没有;没有这种写法的语言不填。 */
iteration?: (node: Node) => IterationFacts | undefined
} }
/** 翻译插件翻一块时拿到的这份源码:源码字符串,和解析它的语言插件。 */
export interface SourceDocument { source: string, language: LanguageAdapter }
/** 筛选属性和值的显示名;没给名字的显示 id。 */ /** 筛选属性和值的显示名;没给名字的显示 id。 */
export interface AttributeNames { export interface AttributeNames {
attributes: Record<string, string> // access → 访问级别 attributes: Record<string, string> // access → 访问级别
@@ -389,8 +394,8 @@ export interface Translator {
id: string // 'typescript.zh-template' id: string // 'typescript.zh-template'
label: string // '中文直译' label: string // '中文直译'
languageIds: string[] languageIds: string[]
translate: (block: TextBlock, source: string) => PreviewText // 只翻 header 那一段;注释块给 Markdown 原文 translate: (block: TextBlock, document: SourceDocument) => PreviewText // 只翻 header 那一段;注释块给 Markdown 原文
describe: (block: FactBlock, source: string) => PropertyRow[] // 函数、类、导入、常量、变量块 → 属性表 describe: (block: FactBlock, document: SourceDocument) => PropertyRow[] // 函数、类、导入、常量、变量块 → 属性表
attributeNames?: AttributeNames // 筛选属性和值叫什么,和属性表的叫法一致;筛选栏、种类标签用它 attributeNames?: AttributeNames // 筛选属性和值叫什么,和属性表的叫法一致;筛选栏、种类标签用它
} }
@@ -403,7 +408,10 @@ export interface CodeReaderApi {
- `activate()` 返回 `{ codeReader: CodeReaderApi }`;其他扩展声明 `extensionDependencies` 后用 `extensions.getExtension('clurdra.superpowers-vscode-clurdra')!.exports.codeReader` 注册。 - `activate()` 返回 `{ codeReader: CodeReaderApi }`;其他扩展声明 `extensionDependencies` 后用 `extensions.getExtension('clurdra.superpowers-vscode-clurdra')!.exports.codeReader` 注册。
- 内置的 TypeScript、Python 语言插件和两个中文直译翻译插件在 `activate()` 里通过同一个 API 注册,没有旁路。 - 内置的 TypeScript、Python 语言插件和两个中文直译翻译插件在 `activate()` 里通过同一个 API 注册,没有旁路。
- 注册表变化时更新 `superpowers.codeReader.languageIds` 上下文,并让已打开的预览刷新。 - 注册表变化时更新 `superpowers.codeReader.languageIds` 上下文,并让已打开的预览刷新。
- 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译,也给筛选属性和值起名(`attributeNames`)。筛选属性的通用部分由宿主从 facts 推出(`attributes.ts` 的 `blockAttributes`),语言插件只在 `BlockShape.attributes` 里补自己语言特有的。第三方语言插件给函数、类、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表;自己语言的字典 / 映射字面量填进 `DeclaratorFacts.entries` 就能展开成子行(中文模板直译按字面量的节点类型查叫法,见 `ZhTable.mapLiterals`)。写在表达式里的遍历(推导式这类)由语言插件给一个读结构的函数(节点 → `IterationFacts`,Python 是 `languages/python.ts` 的 `pythonIteration`),中文模板直译通过 `ZhTable.iteration` 接上,排成一个 `SegmentGroup`(样式 `iteration`,框头是结果类型的译名);webview 只认框的样式 id,不认语言。 - 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译,也给筛选属性和值起名(`attributeNames`)。筛选属性的通用部分由宿主从 facts 推出(`attributes.ts` 的 `blockAttributes`),语言插件只在 `BlockShape.attributes` 里补自己语言特有的。第三方语言插件给函数、类、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表;自己语言的字典 / 映射字面量填进 `DeclaratorFacts.entries` 就能展开成子行(中文模板直译按字面量的节点类型查叫法,见 `ZhTable.mapLiterals`)。写在表达式里的遍历(推导式这类)由语言插件的 `iteration` 给结构(节点 → `IterationFacts`),翻译插件排成一个 `SegmentGroup`(中文直译是样式 `iteration`、框头是结果类型的译名);webview 只认框的样式 id,不认语言。
- 语言插件的信息只经契约到翻译插件:宿主翻每一块时把 `{ source, language }`(`SourceDocument`,language 是解析这份源码的语言插件)交给 `translate` / `describe`,翻译插件要词性、遍历结构都从 `language` 取,不 import 任何语言插件的模块(lint 规则挡住 `translators/` 下的非测试文件 import `languages/`)。所以第三方翻译插件(英文、AI 翻译)拿得到和内置翻译插件一样的信息,第三方语言插件也能配上任何翻译插件。
- 第三方语言插件要给的:`id`、`languageIds`、`grammarPath`、`toBlocks`(块的种类、头部、子块;函数、类、导入、常量、变量块带 facts)、`tokenRole`。可以给的:facts 里语言有才填的项(async、abstract、typeOnly、access、doc、`DeclaratorFacts.entries`)、`BlockShape.attributes`(通用词表以外的筛选属性)、`iteration`(有写在表达式里的遍历的语言)。没给的就少那一样:没有 entries 的值不展开,没有 `iteration` 的遍历照源码结构逐词翻。
- 翻译插件拿到的:代码块(kind、node、header、children,属性表块还有 facts)和 `SourceDocument`。按语法节点类型写的叫法(关键字表、按语境换的译法、节点模板、注解位置这类只关乎措辞的判断)属于翻译插件自己,写在它的表里。
## 解析运行时 ## 解析运行时
@@ -478,7 +486,7 @@ export interface CodeReaderApi {
| TypeScript | `ternary_expression`(`c ? a : b`) | 如果 〈condition〉 则 〈consequence〉 否则 〈alternative〉 | | TypeScript | `ternary_expression`(`c ? a : b`) | 如果 〈condition〉 则 〈consequence〉 否则 〈alternative〉 |
| Python | `conditional_expression`(`a if c else b`,没有字段名,具名子节点依次是 a、c、b) | 如果 〈1 号〉 则 〈0 号〉 否则 〈2 号〉 | | Python | `conditional_expression`(`a if c else b`,没有字段名,具名子节点依次是 a、c、b) | 如果 〈1 号〉 则 〈0 号〉 否则 〈2 号〉 |
写在表达式里的遍历(推导式)不走节点模板:它的节数不定、要换成一个框,由表里的 `iteration`(语言插件给的读结构的函数)认出来,引擎按「推导式」一节的规则排进框里;和节点模板一样,没整个落在要翻的这一段里就不重排、照常翻。 写在表达式里的遍历(推导式)不走节点模板:它的节数不定、要换成一个框,由语言插件的 `iteration`(翻译时从 `SourceDocument.language` 拿到)认出来,引擎按「推导式」一节的规则排进框里;和节点模板一样,没整个落在要翻的这一段里就不重排、照常翻。
TypeScript 关键字表(流程 = keyword,声明 = storage): TypeScript 关键字表(流程 = keyword,声明 = storage):
@@ -593,10 +601,10 @@ vscode/src/codeReader/
blockView.ts CodeBlock → BlockView(源码着色、去缩进、调用翻译) blockView.ts CodeBlock → BlockView(源码着色、去缩进、调用翻译)
attributes.ts 块的筛选属性(从 facts 推出通用属性 + 语言自己报的)、文件里出现过的属性和块数 attributes.ts 块的筛选属性(从 facts 推出通用属性 + 语言自己报的)、文件里出现过的属性和块数
languages/typescript.ts languages/typescript.ts
languages/python.ts (pythonIteration:推导式的结构) languages/python.ts (iteration:推导式的结构)
translators/zhTemplate.ts 模板直译引擎(按语言的表驱动;推导式排进框见 iterationGroup) translators/zhTemplate.ts 模板直译引擎(按语言的表驱动,词性、遍历结构从 SourceDocument.language 取;推导式排进框见 iterationGroup)
translators/typescriptZh.ts TS 表 translators/typescriptZh.ts TS 表
translators/pythonZh.ts Python 表 translators/pythonZh.ts Python 表(含注解位置的判断)
explorer/fileTree.ts 侧栏文件树(FileTreeDecorator:给行添信息的接口) explorer/fileTree.ts 侧栏文件树(FileTreeDecorator:给行添信息的接口)
notes/noteMap.ts 注释的键、保存 / 改名 / 删除时怎么改(纯函数) notes/noteMap.ts 注释的键、保存 / 改名 / 删除时怎么改(纯函数)
notes/store.ts .spx/code-notes.json 读写 notes/store.ts .spx/code-notes.json 读写
@@ -621,6 +629,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
- 属性表信息:`envLock.ts`、`sample.py`、`routes.py`(原型里的 FastAPI 路由)加内联片段,覆盖各种导入写法、形参写法、类成员访问修饰符、解构和多变量声明、Python 下划线规则;字典 / 对象字面量的各项:Python 字符串键、名字键、嵌套字典、`**` 展开、空字典、类属性,TypeScript 名字键、字符串键、计算属性名、简写、展开、方法和 getter(头部到主体之前)、嵌套对象、空对象,不是字面量的值没有 entries。 - 属性表信息:`envLock.ts`、`sample.py`、`routes.py`(原型里的 FastAPI 路由)加内联片段,覆盖各种导入写法、形参写法、类成员访问修饰符、解构和多变量声明、Python 下划线规则;字典 / 对象字面量的各项:Python 字符串键、名字键、嵌套字典、`**` 展开、空字典、类属性,TypeScript 名字键、字符串键、计算属性名、简写、展开、方法和 getter(头部到主体之前)、嵌套对象、空对象,不是字面量的值没有 entries。
- 字典 / 对象的属性表:项数、键 | 值两列、嵌套一层层展开、「展开」、方法头部、空的一行;键和值里的名字、字符串带下标(嵌套的也是);每项标的定义是键,展开项不标;跳到定义落在对象属性名上只闪那一项、连外层各层一起展开,只是显示着的名字闪整块、展开到它那一层。 - 字典 / 对象的属性表:项数、键 | 值两列、嵌套一层层展开、「展开」、方法头部、空的一行;键和值里的名字、字符串带下标(嵌套的也是);每项标的定义是键,展开项不标;跳到定义落在对象属性名上只闪那一项、连外层各层一起展开,只是显示着的名字闪整块、展开到它那一层。
- 推导式:Python 语言插件读出的结构(调用唯一参数的生成器带 sharedParentheses、几个参数之一的不带,列表带条件,字典分出键和值,集合的解构变量,几节 for / if 照源码顺序,async for,套着的各自认,字面量和 for 语句不算);中文直译「推导式」一节的样例表逐字一致,框的样式、框头、关键字着色,顿号连写、条件位置、另起一句、异步遍历、集合、框里的框、多个参数旁边的生成器、跨行带注释的生成器,属性表的默认值和字典项的值里也换;框里的名字带下标且指回源码原文,高亮各处出现、跳到定义都认框里的名字(只显示在框里的推导式变量 → 框所在的块);TypeScript 不认。 - 推导式:Python 语言插件读出的结构(调用唯一参数的生成器带 sharedParentheses、几个参数之一的不带,列表带条件,字典分出键和值,集合的解构变量,几节 for / if 照源码顺序,async for,套着的各自认,字面量和 for 语句不算);中文直译「推导式」一节的样例表逐字一致,框的样式、框头、关键字着色,顿号连写、条件位置、另起一句、异步遍历、集合、框里的框、多个参数旁边的生成器、跨行带注释的生成器,属性表的默认值和字典项的值里也换;框里的名字带下标且指回源码原文,高亮各处出现、跳到定义都认框里的名字(只显示在框里的推导式变量 → 框所在的块);TypeScript 不认。
- 只经契约:只照 contract 写的假语言插件(`iteration` 把一个普通调用认成遍历)配中文模板,那个调用换成遍历框;同一个翻译插件配上不给 `iteration` 的语言插件照常翻。测试文件不 import 任何语言插件模块。
- 输入参数表格:函数的「输入参数」行带列名 名称 / 类型 / 默认值(TS、Python 一样),默认值格子只有译文、没有就是空格子,别的行没有列名;webview 按有没有列名选表格还是列表,表格的格子按列数取。 - 输入参数表格:函数的「输入参数」行带列名 名称 / 类型 / 默认值(TS、Python 一样),默认值格子只有译文、没有就是空格子,别的行没有列名;webview 按有没有列名选表格还是列表,表格的格子按列数取。
- 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。 - 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。
- `blockView`:`codeTokens` 拼起来等于 `code`;去缩进正确;函数、类、导入、常量、变量块只带属性表,其余块只带译文。 - `blockView`:`codeTokens` 拼起来等于 `code`;去缩进正确;函数、类、导入、常量、变量块只带属性表,其余块只带译文。
+10
View File
@@ -12,4 +12,14 @@ export default antfu(
// overrides // overrides
}, },
}, },
{
// 翻译插件只通过 contract 拿语言插件给的信息(翻译时传进来的 SourceDocument.language),第三方翻译插件也是这样拿
files: ['src/codeReader/translators/**/*.ts'],
ignores: ['**/*.test.ts'],
rules: {
'no-restricted-imports': ['error', {
patterns: [{ group: ['**/languages/*'], message: '翻译插件不直接依赖语言插件:词性、遍历结构等从 SourceDocument.language 取' }],
}],
},
},
) )
+4 -3
View File
@@ -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 type { TreeSitterRuntime } from './runtime'
import { blockAttributes } from './attributes' import { blockAttributes } from './attributes'
import { commonIndent, splitLines } from './editText' 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[] { export function toBlockViews(blocks: CodeBlock[], source: string, adapter: LanguageAdapter, translator: Translator | undefined): BlockView[] {
const lines = lineRanges(source) const lines = lineRanges(source)
const texts = splitLines(source) const texts = splitLines(source)
const document: SourceDocument = { source, language: adapter }
const build = (block: CodeBlock, id: string): BlockView => { const build = (block: CodeBlock, id: string): BlockView => {
const { startRow, endRow } = block.header const { startRow, endRow } = block.header
const rows = texts.slice(startRow, endRow + 1) const rows = texts.slice(startRow, endRow + 1)
@@ -122,8 +123,8 @@ export function toBlockViews(blocks: CodeBlock[], source: string, adapter: Langu
attributes: blockAttributes(block), attributes: blockAttributes(block),
children: block.children.map((child, i) => build(child, `${id}.${i}`)), children: block.children.map((child, i) => build(child, `${id}.${i}`)),
...('facts' in block ...('facts' in block
? { properties: translator ? translator.describe(block, source) : [] } ? { properties: translator ? translator.describe(block, document) : [] }
: { translation: translator ? translator.translate(block, source) : [] }), : { translation: translator ? translator.translate(block, document) : [] }),
} }
} }
return blocks.map((block, i) => build(block, String(i))) return blocks.map((block, i) => build(block, String(i)))
+17 -6
View File
@@ -101,8 +101,8 @@ export type IterationClause
| { kind: 'if', condition: Node } | { kind: 'if', condition: Node }
/** /**
* 写在表达式里的遍历(Python 的列表 / 集合 / 字典推导式、生成器表达式):语言插件从语法树读出结构,翻译插件按自己的语序重排。 * 写在表达式里的遍历(Python 的列表 / 集合 / 字典推导式、生成器表达式):语言插件从语法树读出结构(LanguageAdapter.iteration),
* 节点都指向源码;各节、每一项取的值里还能再套遍历。 * 翻译插件按自己的语序重排。节点都指向源码;各节、每一项取的值里还能再套遍历。
*/ */
export interface IterationFacts { export interface IterationFacts {
result: IterationResult result: IterationResult
@@ -247,7 +247,10 @@ export interface PropertyRow extends Defines {
columns?: string[] columns?: string[]
} }
/** 语言插件:语法树 → 代码块树。 */ /**
* 语言插件:语法树 → 代码块树,外加只有它懂、翻译插件翻表达式时要用的信息(叶子的词性、写在表达式里的遍历的结构)。
* 翻译插件翻一块时从 SourceDocument.language 拿到解析这份源码的语言插件,不直接依赖某个语言插件。
*/
export interface LanguageAdapter { export interface LanguageAdapter {
id: string id: string
/** 覆盖的 VS Code languageId,例如 ['typescript', 'typescriptreact', 'javascript', 'javascriptreact']。 */ /** 覆盖的 VS Code languageId,例如 ['typescript', 'typescriptreact', 'javascript', 'javascriptreact']。 */
@@ -256,8 +259,16 @@ export interface LanguageAdapter {
grammarPath: (languageId: string) => string grammarPath: (languageId: string) => string
/** languageId 是这份源码的 languageId:一个插件覆盖几种方言时按它区分(JavaScript 没有 TypeScript 的抽象类、仅类型导入)。 */ /** languageId 是这份源码的 languageId:一个插件覆盖几种方言时按它区分(JavaScript 没有 TypeScript 的抽象类、仅类型导入)。 */
toBlocks: (root: Node, source: string, languageId: string) => CodeBlock[] toBlocks: (root: Node, source: string, languageId: string) => CodeBlock[]
/** 源码着色:给语法树叶子节点定词性。 */ /** 给语法树叶子节点定词性:源码着色用它,译文里照抄的词也按它着色、按它认出能查定义的名字。 */
tokenRole: (leaf: Node) => TokenRole tokenRole: (leaf: Node) => TokenRole
/** node 是写在表达式里的遍历(推导式、生成器表达式)时给出它的结构,别的节点没有;没有这种写法的语言不填。 */
iteration?: (node: Node) => IterationFacts | undefined
}
/** 翻译插件翻一块时拿到的这份源码:源码字符串,和解析它的语言插件。 */
export interface SourceDocument {
source: string
language: LanguageAdapter
} }
/** 筛选属性和值的显示名,筛选栏、种类标签用它;没给名字的显示 id。 */ /** 筛选属性和值的显示名,筛选栏、种类标签用它;没给名字的显示 id。 */
@@ -274,9 +285,9 @@ export interface Translator {
label: string // '中文直译' label: string // '中文直译'
languageIds: string[] languageIds: string[]
/** 只翻 header 那一段;注释块给去掉注释符号的 Markdown 原文。 */ /** 只翻 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。 */ /** 筛选属性和值叫什么,和属性表里的属性名、值用同一套叫法;不给就都显示 id。 */
attributeNames?: AttributeNames attributeNames?: AttributeNames
} }
@@ -2,7 +2,7 @@ import type { IterationFacts, Node } from '../contract'
import type { Outline } from '../testSupport' import type { Outline } from '../testSupport'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { factsByLine, nodesToText, outline, readFixture, runtime, WASM_DIR, withBlocks } from '../testSupport' 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) const adapter = createPythonAdapter(WASM_DIR)
@@ -335,7 +335,7 @@ describe('python 写在表达式里的遍历(推导式)', () => {
try { try {
const found: IterationFacts[] = [] const found: IterationFacts[] = []
const visit = (node: Node): void => { const visit = (node: Node): void => {
const facts = pythonIteration(node) const facts = adapter.iteration?.(node)
if (facts) if (facts)
found.push(facts) found.push(facts)
for (const child of node.children) { for (const child of node.children) {
+4 -3
View File
@@ -269,7 +269,7 @@ const ITERATION_RESULTS: Record<string, IterationResult> = {
* 推导式、生成器表达式的结构,别的节点没有。几种读不成遍历的写法也没有,照常翻: * 推导式、生成器表达式的结构,别的节点没有。几种读不成遍历的写法也没有,照常翻:
* `for x in a, b`(语法树认、Python 3 不认的多个来源),以及缺了变量、来源、条件或每一项的值(写到一半的代码)。 * `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 result = ITERATION_RESULTS[node.type]
const body = node.childForFieldName('body') const body = node.childForFieldName('body')
if (!result || !body) if (!result || !body)
@@ -360,7 +360,7 @@ function insideString(leaf: Node): boolean {
} }
/** 注解里的名字:typed_parameter / 返回值 / 带注解赋值的 type 节点之内。 */ /** 注解里的名字:typed_parameter / 返回值 / 带注解赋值的 type 节点之内。 */
export function insideAnnotation(leaf: Node): boolean { function insideAnnotation(leaf: Node): boolean {
for (let node = leaf.parent; node; node = node.parent) { for (let node = leaf.parent; node; node = node.parent) {
if (node.type === 'type') if (node.type === 'type')
return true return true
@@ -395,7 +395,7 @@ function identifierRole(leaf: Node): TokenRole {
return 'variable' return 'variable'
} }
export function pythonTokenRole(leaf: Node): TokenRole { function pythonTokenRole(leaf: Node): TokenRole {
if (leaf.type === 'comment') if (leaf.type === 'comment')
return 'comment' return 'comment'
if (insideString(leaf)) if (insideString(leaf))
@@ -430,5 +430,6 @@ export function createPythonAdapter(wasmDir: string): LanguageAdapter {
grammarPath: () => path.join(wasmDir, 'tree-sitter-python.wasm'), grammarPath: () => path.join(wasmDir, 'tree-sitter-python.wasm'),
toBlocks: root => statements(named(root), 'module', -1, true), toBlocks: root => statements(named(root), 'module', -1, true),
tokenRole: pythonTokenRole, tokenRole: pythonTokenRole,
iteration: pythonIteration,
} }
} }
@@ -479,7 +479,7 @@ function nameRole(leaf: Node): TokenRole {
return 'variable' return 'variable'
} }
export function typescriptTokenRole(leaf: Node): TokenRole { function typescriptTokenRole(leaf: Node): TokenRole {
if (leaf.type === 'comment' || leaf.type === 'hash_bang_line') if (leaf.type === 'comment' || leaf.type === 'hash_bang_line')
return 'comment' return 'comment'
if (insideString(leaf)) 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))')
})
})
+11 -3
View File
@@ -1,7 +1,17 @@
import type { Node, Translator } from '../contract' import type { Node, Translator } from '../contract'
import { insideAnnotation, pythonIteration, pythonTokenRole } from '../languages/python'
import { createZhTemplateTranslator } from './zhTemplate' 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 { function stringContent(node: Node): string {
if (node.type === 'string_content') if (node.type === 'string_content')
return node.text return node.text
@@ -17,7 +27,6 @@ export const pythonZhTranslator: Translator = createZhTemplateTranslator({
id: 'python.zh-template', id: 'python.zh-template',
label: '中文直译', label: '中文直译',
languageIds: ['python'], languageIds: ['python'],
tokenRole: pythonTokenRole,
keywords: { keywords: {
'import': '导入', 'import': '导入',
'from': { zh: '从', byContext: { raise_statement: '源自', yield: '自' } }, 'from': { zh: '从', byContext: { raise_statement: '源自', yield: '自' } },
@@ -87,5 +96,4 @@ export const pythonZhTranslator: Translator = createZhTemplateTranslator({
nodeTemplates: { nodeTemplates: {
conditional_expression: ['如果', { namedChild: 1 }, '则', { namedChild: 0 }, '否则', { namedChild: 2 }], conditional_expression: ['如果', { namedChild: 1 }, '则', { namedChild: 0 }, '否则', { namedChild: 2 }],
}, },
iteration: pythonIteration,
}) })
@@ -1,5 +1,4 @@
import type { Node, Translator } from '../contract' import type { Node, Translator } from '../contract'
import { typescriptTokenRole } from '../languages/typescript'
import { createZhTemplateTranslator } from './zhTemplate' import { createZhTemplateTranslator } from './zhTemplate'
function commentText(node: Node): string { function commentText(node: Node): string {
@@ -18,7 +17,6 @@ export const typescriptZhTranslator: Translator = createZhTemplateTranslator({
id: 'typescript.zh-template', id: 'typescript.zh-template',
label: '中文直译', label: '中文直译',
languageIds: ['typescript', 'typescriptreact', 'javascript', 'javascriptreact'], languageIds: ['typescript', 'typescriptreact', 'javascript', 'javascriptreact'],
tokenRole: typescriptTokenRole,
keywords: { keywords: {
// 流程 // 流程
import: '导入', 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 type { ZhTable } from './zhTemplate'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { createPythonAdapter } from '../languages/python' import { createPythonAdapter } from '../languages/python'
@@ -22,7 +22,7 @@ function translateAll(adapter: LanguageAdapter, languageId: string, translator:
const line = block.header.startRow + 1 const line = block.header.startRow + 1
if (byLine.has(line) || 'facts' in block) if (byLine.has(line) || 'facts' in block)
return return
const translation = translator.translate(block, source) const translation = translator.translate(block, { source, language: adapter })
byLine.set(line, 'markdown' in translation byLine.set(line, 'markdown' in translation
? { kind: block.kind, segments: [], markdown: translation.markdown } ? { kind: block.kind, segments: [], markdown: translation.markdown }
: { kind: block.kind, segments: translation }) : { kind: block.kind, segments: translation })
@@ -38,7 +38,7 @@ function describeAll(adapter: LanguageAdapter, languageId: string, translator: T
walk(blocks, (block) => { walk(blocks, (block) => {
const line = block.header.startRow + 1 const line = block.header.startRow + 1
if (!byLine.has(line) && 'facts' in block) 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 return byLine
}) })
@@ -1034,7 +1034,6 @@ describe('中文模板引擎', () => {
id: 'test.zh-template', id: 'test.zh-template',
label: '测试', label: '测试',
languageIds: ['python'], languageIds: ['python'],
tokenRole: () => 'keyword',
keywords: {}, keywords: {},
literals: {}, literals: {},
builtinTypes: {}, builtinTypes: {},
@@ -1050,6 +1049,11 @@ describe('中文模板引擎', () => {
}) })
} }
/** 按 Python 分块、词性都是 role 的语言插件:引擎着色只看翻译时传进来的语言插件。 */
function pythonColored(role: TokenRole): LanguageAdapter {
return { ...py, tokenRole: () => role }
}
it('关键字按父节点类型选译法,更长的节点类型链优先,都不匹配用默认', async () => { it('关键字按父节点类型选译法,更长的节点类型链优先,都不匹配用默认', async () => {
const translator = testTranslator({ const translator = testTranslator({
keywords: { keywords: {
@@ -1066,7 +1070,7 @@ describe('中文模板引擎', () => {
'else:', 'else:',
' pass', ' pass',
].join('\n') ].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[1]).toBe('for k 乙 items')
expect(map[2]).toBe('if k 甲 seen') expect(map[2]).toBe('if k 甲 seen')
expect(map[4]).toBe('丁') expect(map[4]).toBe('丁')
@@ -1076,7 +1080,6 @@ describe('中文模板引擎', () => {
it('节点模板按节点类型重排:槽位按字段名或具名子节点序号取子节点并递归翻译,译词按关键字着色', async () => { it('节点模板按节点类型重排:槽位按字段名或具名子节点序号取子节点并递归翻译,译词按关键字着色', async () => {
const translator = testTranslator({ const translator = testTranslator({
tokenRole: () => 'variable',
keywords: { in: '在', else: { zh: '丙', byContext: { else_clause: '丁' } } }, keywords: { in: '在', else: { zh: '丙', byContext: { else_clause: '丁' } } },
operators: { '==': '等于' }, operators: { '==': '等于' },
operatorParents: ['comparison_operator'], operatorParents: ['comparison_operator'],
@@ -1091,7 +1094,7 @@ describe('中文模板引擎', () => {
'print((a # 注释不占序号', 'print((a # 注释不占序号',
' if k == 1 else b))', ' if k == 1 else b))',
].join('\n') ].join('\n')
const map = await translateAll(py, 'python', translator, source) const map = await translateAll(pythonColored('variable'), 'python', translator, source)
expect(texts(map)).toEqual({ expect(texts(map)).toEqual({
1: '令 n 加上 若 k 在 seen 则 a 不然 b', 1: '令 n 加上 若 k 在 seen 则 a 不然 b',
2: 'print(若 c 则 a 不然 若 d 则 b 不然 e)', 2: 'print(若 c 则 a 不然 若 d 则 b 不然 e)',
@@ -1103,13 +1106,12 @@ describe('中文模板引擎', () => {
it('属性表里的类型、值用同一张表和同一套模板翻', async () => { it('属性表里的类型、值用同一张表和同一套模板翻', async () => {
const translator = testTranslator({ const translator = testTranslator({
tokenRole: () => 'variable',
keywords: { in: '在' }, keywords: { in: '在' },
builtinTypes: { int: '整型' }, builtinTypes: { int: '整型' },
isTypePosition: leaf => leaf.parent?.type === 'type', isTypePosition: leaf => leaf.parent?.type === 'type',
nodeTemplates: { conditional_expression: ['若', { namedChild: 1 }, '则', { namedChild: 0 }, '不然', { namedChild: 2 }] }, 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']) expect(rowTexts(rows)[1]).toEqual(['名称: n', '访问级别: 公共', '类型: 整型', '初始值: 若 k 在 seen 则 a 不然 b'])
}) })
@@ -1127,7 +1129,7 @@ describe('中文模板引擎', () => {
'else:', 'else:',
' pass', ' pass',
].join('\n') ].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', 1: 'x += a if k 在 seen 丙 b',
2: 'if k 在 seen', 2: 'if k 在 seen',
3: 'pass', 3: 'pass',
+36 -37
View File
@@ -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' import { commonIndent, splitLines } from '../editText'
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */ /** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
@@ -20,12 +20,14 @@ export interface ContextualWord {
export type TemplatePart = string | { field: string } | { namedChild: number } export type TemplatePart = string | { field: string } | { namedChild: number }
/** 一种语言的中文直译表。引擎替换表里的关键字、内置类型、运算符,按节点模板重排少数语法结构,其余原样照抄。 */ /** 一种语言的中文直译表。引擎替换表里的关键字、内置类型、运算符,按节点模板重排少数语法结构,其余原样照抄。 */
/**
* 一种语言的中文词表:只有叫法和语法节点类型,语言知道的结构(词性、遍历)翻译时从 SourceDocument.language 取——
* 未翻译的词、关键字译文按它给的词性着色,它认出的遍历整个换成一个框,框里按读的顺序重排(见 iterationGroup)。
*/
export interface ZhTable { export interface ZhTable {
id: string id: string
label: string label: string
languageIds: string[] languageIds: string[]
/** 未翻译的词按源码着色的词性上色;关键字译文的词性也取它。 */
tokenRole: (leaf: Node) => TokenRole
/** 关键字:按语法树里匿名节点的类型查(Python 的 `not in` 这类组合关键字本身也是一个匿名节点);随所在语法节点换译法的写成 ContextualWord。 */ /** 关键字:按语法树里匿名节点的类型查(Python 的 `not in` 这类组合关键字本身也是一个匿名节点);随所在语法节点换译法的写成 ContextualWord。 */
keywords: Record<string, string | ContextualWord> keywords: Record<string, string | ContextualWord>
/** 字面量关键字:具名的叶子节点(true / None …),按节点类型查。 */ /** 字面量关键字:具名的叶子节点(true / None …),按节点类型查。 */
@@ -49,11 +51,6 @@ export interface ZhTable {
* 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。 * 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。
*/ */
nodeTemplates: Record<string, TemplatePart[]> nodeTemplates: Record<string, TemplatePart[]>
/**
* 写在表达式里的遍历(推导式、生成器表达式)的结构,语言插件给;没有这种写法的语言不填。
* 认出来的节点整个换成一个框,框里按读的顺序重排,见 iterationGroup。
*/
iteration?: (node: Node) => IterationFacts | undefined
} }
interface Token { interface Token {
@@ -73,7 +70,7 @@ interface Token {
/** 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。 */ /** 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。 */
interface Collector { interface Collector {
source: string document: SourceDocument
tokens: Token[] tokens: Token[]
cursor: number cursor: number
gap: boolean gap: boolean
@@ -184,13 +181,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
} }
/** 照抄这个节点时带不带源码下标:名字(按名字着色的具名叶子)和整段字符串带,标点、数字、关键字不带。 */ /** 照抄这个节点时带不带源码下标:名字(按名字着色的具名叶子)和整段字符串带,标点、数字、关键字不带。 */
function offsetOf(node: Node): number | undefined { function offsetOf(node: Node, language: LanguageAdapter): number | undefined {
const linkable = stringNodes.has(node.type) || (node.childCount === 0 && node.isNamed && NAME_ROLES.has(table.tokenRole(node))) const linkable = stringNodes.has(node.type) || (node.childCount === 0 && node.isNamed && NAME_ROLES.has(language.tokenRole(node)))
return linkable ? node.startIndex : undefined 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)) { if (node.childCount === 0 && table.isTypePosition(node)) {
const type = table.builtinTypes[node.text] const type = table.builtinTypes[node.text]
if (type) if (type)
@@ -202,11 +199,11 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return { text: table.operators[node.type], role: 'operator' } return { text: table.operators[node.type], role: 'operator' }
const word = table.keywords[node.type] const word = table.keywords[node.type]
if (word !== undefined) 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 return undefined
} }
if (node.childCount === 0 && table.literals[node.type]) 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) if (table.emptyLiterals[node.type] && node.namedChildCount === 0)
return { text: table.emptyLiterals[node.type], role: 'literal' } return { text: table.emptyLiterals[node.type], role: 'literal' }
return undefined return undefined
@@ -223,7 +220,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
/** 源码里上一个词到 index 之间有空白,下一个词前面就空一格。 */ /** 源码里上一个词到 index 之间有空白,下一个词前面就空一格。 */
function advance(out: Collector, index: number): void { 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 out.cursor = index
} }
@@ -239,10 +236,10 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
} }
/** 语言插件认出的写在表达式里的遍历;没整个落在要翻的这一段里就不重排、照常翻。 */ /** 语言插件认出的写在表达式里的遍历;没整个落在要翻的这一段里就不重排、照常翻。 */
function iterationIn(node: Node, span: SourceSpan): IterationFacts | undefined { function iterationIn(node: Node, span: SourceSpan, language: LanguageAdapter): IterationFacts | undefined {
if (!table.iteration || node.startIndex < span.startIndex || node.endIndex > span.endIndex) if (!language.iteration || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
return undefined 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 segments: Inline[] = []
const plain = (text: string): void => pushSegment(segments, text, 'plain') const plain = (text: string): void => pushSegment(segments, text, 'plain')
const word = (text: string): void => pushSegment(segments, text, 'keyword') 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 let previous: IterationClause | undefined
for (const clause of facts.clauses) { for (const clause of facts.clauses) {
if (clause.kind === 'if') { 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 { function emitIteration(out: Collector, node: Node, facts: IterationFacts, span: SourceSpan): void {
const shared = facts.sharedParentheses 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) if (shared)
emitSource(out, shared.open, bracket(shared.open)) emitSource(out, shared.open, bracket(shared.open))
else else
advance(out, node.startIndex) 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) { if (shared) {
out.cursor = shared.close.startIndex out.cursor = shared.close.startIndex
emitSource(out, shared.close, bracket(shared.close)) 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 }) emitSource(out, node, { text: '', role: 'comment', translated: false })
return return
} }
const iteration = iterationIn(node, span) const { language } = out.document
const iteration = iterationIn(node, span, language)
if (iteration) { if (iteration) {
emitIteration(out, node, iteration, span) emitIteration(out, node, iteration, span)
return return
@@ -335,14 +333,14 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
out.cursor = node.endIndex out.cursor = node.endIndex
return return
} }
const translated = translateNode(node) const translated = translateNode(node, language)
if (translated) { if (translated) {
emitSource(out, node, { ...translated, translated: true }) emitSource(out, node, { ...translated, translated: true })
return return
} }
if (node.childCount === 0 || stringNodes.has(node.type)) { if (node.childCount === 0 || stringNodes.has(node.type)) {
const role = stringNodes.has(node.type) ? 'string' : table.tokenRole(node) const role = stringNodes.has(node.type) ? 'string' : language.tokenRole(node)
emitSource(out, node, { text: oneLine(node.text), role, translated: false, offset: offsetOf(node) }) emitSource(out, node, { text: oneLine(node.text), role, translated: false, offset: offsetOf(node, language) })
return return
} }
for (const child of node.children) { for (const child of node.children) {
@@ -352,14 +350,14 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
} }
/** 按译文顺序收集 node 落在 span 里的词。 */ /** 按译文顺序收集 node 落在 span 里的词。 */
function tokensIn(node: Node, span: SourceSpan, source: string): Token[] { function tokensIn(node: Node, span: SourceSpan, document: SourceDocument): Token[] {
const out: Collector = { source, tokens: [], cursor: span.startIndex, gap: false } const out: Collector = { document, tokens: [], cursor: span.startIndex, gap: false }
collect(node, span, out) collect(node, span, out)
return out.tokens return out.tokens
} }
/** node 的源码原样照抄:叶子按源码着色(给了 role 就都按它),叶子之间的空白、标点照抄。 */ /** node 的源码原样照抄:叶子按源码着色(给了 role 就都按它),叶子之间的空白、标点照抄。 */
function verbatim(node: Node, source: string, role?: TokenRole): Segment[] { function verbatim(node: Node, { source, language }: SourceDocument, role?: TokenRole): Segment[] {
const segments: Segment[] = [] const segments: Segment[] = []
let cursor = node.startIndex let cursor = node.startIndex
const visit = (current: Node): void => { const visit = (current: Node): void => {
@@ -373,7 +371,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return return
} }
pushSegment(segments, source.slice(cursor, current.startIndex), 'plain') 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 cursor = current.endIndex
} }
visit(node) visit(node)
@@ -388,10 +386,10 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return { markdown: docText(nodes.map(table.commentText)) } 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') if (block.kind === 'comment')
return translateComment(block) 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 compound = block.children.length > 0 || block.header.endIndex < block.node.endIndex
const last = tokens.at(-1) const last = tokens.at(-1)
if (compound && last && !last.translated && HEADER_OPENERS.has(last.raw)) if (compound && last && !last.translated && HEADER_OPENERS.has(last.raw))
@@ -399,12 +397,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return joinTokens(tokens) return joinTokens(tokens)
} }
function describe(block: FactBlock, source: string): PropertyRow[] { function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
const { header } = block const { header } = block
const { language } = document
// 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致 // 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致
const within = (node: Node, part: SourceSpan): Inline[] => { const within = (node: Node, part: SourceSpan): Inline[] => {
const span = part.endIndex > header.endIndex ? { ...part, endIndex: header.endIndex, endRow: header.endRow } : part 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 = { const phrases: Phrases = {
translated: node => within(node, { startIndex: node.startIndex, endIndex: node.endIndex, startRow: node.startPosition.row, endRow: node.endPosition.row }), 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] const name = table.mapLiterals[literal.type]
return [hint(name ? `${name} ${size} 项` : `${size} 项`)] return [hint(name ? `${name} ${size} 项` : `${size} 项`)]
}, },
verbatim: node => verbatim(node, source), verbatim: node => verbatim(node, document),
// 点分模块名逐节照抄,每一节各自能跳到定义,和 VS Code 里一样 // 点分模块名逐节照抄,每一节各自能跳到定义,和 VS Code 里一样
module: node => stringNodes.has(node.type) ? [segment(node.text, 'string', offsetOf(node))] : verbatim(node, source, 'type'), 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))], name: (node, role) => [segment(node.text, role, offsetOf(node, language))],
doc: (node) => { doc: (node) => {
const markdown = docText([table.commentText(node)]) const markdown = docText([table.commentText(node)])
return markdown ? { markdown } : undefined return markdown ? { markdown } : undefined