♻️ refactor(vscode): 代码阅读的翻译插件只经契约拿语言插件的词性和推导式结构
This commit is contained in:
1 parent
07eaf96a6b
commit
202fa954ba
12 files changed
+185
-76
No files matched your search
@@ -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`;去缩进正确;函数、类、导入、常量、变量块只带属性表,其余块只带译文。
|
||||||
|
|||||||
@@ -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 取' }],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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))')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user