139 lines
6.8 KiB
TypeScript
139 lines
6.8 KiB
TypeScript
import type { BlockView } from './blockView'
|
||
import type { LanguageAdapter, Translator } from './contract'
|
||
import { describe, expect, it } from 'vitest'
|
||
import { readBlockViews, toBlockViews } from './blockView'
|
||
import { createPythonAdapter } from './languages/python'
|
||
import { createTypeScriptAdapter } from './languages/typescript'
|
||
import { propertyLines, readFixture, runtime, segmentText, WASM_DIR, withBlocks } from './testSupport'
|
||
import { pythonZhTranslator } from './translators/pythonZh'
|
||
import { typescriptZhTranslator } from './translators/typescriptZh'
|
||
|
||
const ts = createTypeScriptAdapter(WASM_DIR)
|
||
const py = createPythonAdapter(WASM_DIR)
|
||
|
||
function views(adapter: LanguageAdapter, languageId: string, translator: Translator | undefined, source: string): Promise<BlockView[]> {
|
||
return withBlocks(adapter, languageId, source, blocks => toBlockViews(blocks, source, adapter, translator))
|
||
}
|
||
|
||
function flatten(list: BlockView[]): BlockView[] {
|
||
return list.flatMap(view => [view, ...flatten(view.children)])
|
||
}
|
||
|
||
function byId(list: BlockView[], id: string): BlockView {
|
||
const found = flatten(list).find(view => view.id === id)
|
||
if (!found)
|
||
throw new Error(`没有块 ${id}`)
|
||
return found
|
||
}
|
||
|
||
const SAMPLES: [LanguageAdapter, string, Translator, string][] = [
|
||
[ts, 'typescript', typescriptZhTranslator, 'envLock.ts.txt'],
|
||
[ts, 'typescript', typescriptZhTranslator, 'issueLock.ts.txt'],
|
||
[py, 'python', pythonZhTranslator, 'sample.py'],
|
||
[py, 'python', pythonZhTranslator, 'routes.py'],
|
||
]
|
||
|
||
const PROPERTY_KINDS = new Set(['function', 'import', 'const', 'variable'])
|
||
|
||
function translationText(view: BlockView): string {
|
||
if (!('translation' in view))
|
||
throw new Error(`块 ${view.id} 是属性表`)
|
||
return segmentText(view.translation)
|
||
}
|
||
|
||
describe('blockView', () => {
|
||
it.each(SAMPLES)('%#: codeTokens 拼起来等于 code,code 是 header 整行去共同缩进', async (adapter, languageId, translator, fixture) => {
|
||
const source = readFixture(fixture)
|
||
const lines = source.split('\n')
|
||
for (const view of flatten(await views(adapter, languageId, translator, source))) {
|
||
expect(view.codeTokens.map(token => token.text).join(''), view.id).toBe(view.code)
|
||
const rows = lines.slice(view.startLine, view.endLine + 1)
|
||
const indent = Math.min(...rows.filter(row => row.trim()).map(row => row.length - row.trimStart().length))
|
||
expect(view.code, view.id).toBe(rows.map(row => row.slice(indent)).join('\n'))
|
||
if ('properties' in view)
|
||
expect(view.properties.length, view.id).toBeGreaterThan(0)
|
||
else
|
||
expect(view.translation.length, view.id).toBeGreaterThan(0)
|
||
}
|
||
})
|
||
|
||
it.each(SAMPLES)('%#: 函数、导入、常量、变量块只带属性表,其余块只带一行译文', async (adapter, languageId, translator, fixture) => {
|
||
for (const view of flatten(await views(adapter, languageId, translator, readFixture(fixture)))) {
|
||
expect('properties' in view, view.id).toBe(PROPERTY_KINDS.has(view.kind))
|
||
expect('translation' in view, view.id).toBe(!PROPERTY_KINDS.has(view.kind))
|
||
}
|
||
})
|
||
|
||
it('id 是块在树里的位置,行号 0 起,多行块去掉共同缩进', async () => {
|
||
const env = await views(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
|
||
expect(byId(env, '6.1.0')).toMatchObject({ kind: 'return', startLine: 25, endLine: 25, code: 'return []' })
|
||
expect(byId(env, '6.1')).toMatchObject({ kind: 'if', startLine: 24, code: 'if (!folder)' })
|
||
expect(byId(env, '8.2.1')).toMatchObject({ kind: 'catch', startLine: 45, code: 'catch (err) {' })
|
||
|
||
const issue = await views(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt'))
|
||
expect(byId(issue, '3.4')).toMatchObject({
|
||
kind: 'return',
|
||
startLine: 19,
|
||
endLine: 23,
|
||
code: 'return {\n locked: true,\n prerequisiteNumber: prereq.number,\n prerequisiteColumn: prereq.column,\n}',
|
||
})
|
||
})
|
||
|
||
it('源码着色来自语法树:每个叶子一段,空白和标点归 plain', async () => {
|
||
const env = await views(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
|
||
const tokens = byId(env, '6.3').codeTokens
|
||
expect(byId(env, '6.3').code).toBe('const uris = await workspace.findFiles(include, EXCLUDE_PATTERN)')
|
||
expect(tokens.slice(0, 7)).toEqual([
|
||
{ text: 'const', role: 'storage' },
|
||
{ text: ' ', role: 'plain' },
|
||
{ text: 'uris', role: 'variable' },
|
||
{ text: ' = ', role: 'plain' },
|
||
{ text: 'await', role: 'keyword' },
|
||
{ text: ' ', role: 'plain' },
|
||
{ text: 'workspace', role: 'variable' },
|
||
])
|
||
expect(tokens).toContainEqual({ text: 'findFiles', role: 'function' })
|
||
expect(byId(env, '0').codeTokens).toEqual([{ text: byId(env, '0').code, role: 'comment' }])
|
||
|
||
// 中文注释之后的行:语法树下标按字符串下标算,不按 UTF-8 字节
|
||
const issue = await views(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt'))
|
||
expect(byId(issue, '3').codeTokens.slice(0, 5)).toEqual([
|
||
{ text: 'export', role: 'keyword' },
|
||
{ text: ' ', role: 'plain' },
|
||
{ text: 'function', role: 'storage' },
|
||
{ text: ' ', role: 'plain' },
|
||
{ text: 'evaluateLockedReason', role: 'function' },
|
||
])
|
||
expect(byId(issue, '2').codeTokens).toEqual([{ text: byId(issue, '2').code, role: 'comment' }])
|
||
|
||
const sample = await views(py, 'python', pythonZhTranslator, readFixture('sample.py'))
|
||
const signature = byId(sample, '5').codeTokens
|
||
expect(signature).toContainEqual({ text: 'def', role: 'storage' })
|
||
expect(signature).toContainEqual({ text: 'find_env_files', role: 'function' })
|
||
expect(signature.filter(token => token.text === 'str')).toEqual([{ text: 'str', role: 'type' }, { text: 'str', role: 'type' }])
|
||
})
|
||
|
||
it('\\r\\n 和制表符缩进:code 用 \\n 连接,去掉共同的制表符', async () => {
|
||
const source = 'function f() {\r\n\tif (a &&\r\n\t\tb) {\r\n\t\treturn 1\r\n\t}\r\n}\r\n'
|
||
const list = await views(ts, 'typescript', undefined, source)
|
||
const cond = byId(list, '0.0')
|
||
expect(cond).toMatchObject({ kind: 'if', startLine: 1, endLine: 2, code: 'if (a &&\n\tb) {', translation: [] })
|
||
expect(cond.codeTokens.map(token => token.text).join('')).toBe(cond.code)
|
||
expect(byId(list, '0')).toMatchObject({ kind: 'function', properties: [] })
|
||
})
|
||
|
||
it('readBlockViews:解析、转视图、释放语法树', async () => {
|
||
const list = await readBlockViews(runtime, py, pythonZhTranslator, 'python', 'def f(x: int) -> bool:\n return not x\n')
|
||
expect(list).toHaveLength(1)
|
||
expect('properties' in list[0] && propertyLines(list[0].properties)).toEqual([
|
||
'名称: f',
|
||
'访问级别: 公共',
|
||
'异步: 否',
|
||
'输入参数: 1 个',
|
||
' x | 整数 | ',
|
||
'输出参数: 布尔',
|
||
])
|
||
expect(translationText(list[0].children[0])).toBe('返回 非 x')
|
||
})
|
||
})
|