✨ feat(vscode): 代码阅读能读嵌在一种语言里的另一种语言的代码:语言插件报出是哪几段,宿主交给那种语言的插件只解析这几段、分步、翻成子块并叠着上色,外层翻译插件只问读出来的样子;契约加构建文件的几种块(解析指令、阶段、执行、复制、设置、标签、健康检查、启动)、分作用范围的变量、并成一块的几条语句,引擎排这些块的属性表
This commit is contained in:
1 parent
834673391e
commit
3353cf870c
11 files changed
+603
-103
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommandContext, CommandFacts, CommandReading, CommandRule, CommandWord, CommonAttribute, Defines, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, InterpreterFacts, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, OptionsFacts, ParameterFacts, PositionalParameter, PreviewTable, PropertyItem, PropertyRow, Redirect, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommandContext, CommandFacts, CommandForm, CommandReading, CommandRule, CommandWord, CommonAttribute, DeclaratorFacts, Defines, DirectiveFacts, EmbeddedReading, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, HealthFacts, ImportFacts, ImportItem, Inline, InstructionFlag, InterpreterFacts, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, OptionsFacts, ParameterFacts, PositionalParameter, PreviewTable, PropertyItem, PropertyRow, Redirect, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, StageFacts, StartFacts, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import { commonIndent, splitLines } from '../editText'
|
||||
|
||||
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
|
||||
@@ -56,8 +56,12 @@ export interface ZhTable {
|
||||
* 命中的节点只输出读法给的,不再照常翻它的子节点;读法返回 undefined 就照常翻。读法里要翻子节点用 ReaderContext.say。
|
||||
*/
|
||||
readers?: Record<string, NodeReader>
|
||||
/** 并成一块的几条同种语句(TextBlock.group)怎么读:按第一条的节点类型查,读成一行字加一张表;没有的照常翻第一条。 */
|
||||
groups?: Record<string, GroupReader>
|
||||
/** 解释器开关(SwitchFacts.id)的叫法:errexit → 出错就停;没有的写源码里的写法。 */
|
||||
switches?: Record<string, string>
|
||||
/** 解析指令(ParserDirective.name)的叫法和补充说明:syntax → 语法(BuildKit 按这个版本读这份文件);没有的写原名。 */
|
||||
directives?: Record<string, { label: string, note?: string }>
|
||||
}
|
||||
|
||||
/** 读一个节点时能用的(见 ZhTable.readers)。 */
|
||||
@@ -76,6 +80,9 @@ export interface ReaderContext {
|
||||
/** 一种节点的读法:一行译文,可以带一张排在这一段下面的表(分情况的「情况 / 做什么」);返回 undefined 就照常翻。 */
|
||||
export type NodeReader = (node: Node, context: ReaderContext) => Inline[] | { text: Inline[], table?: PreviewTable } | undefined
|
||||
|
||||
/** 并成一块的几条同种语句的读法(见 ZhTable.groups):照源码顺序的各条。 */
|
||||
export type GroupReader = (nodes: Node[], context: ReaderContext) => { text: Inline[], table?: PreviewTable }
|
||||
|
||||
interface Token {
|
||||
/** 注释是空文字:不进译文,只在前后留一个空格。 */
|
||||
text: string
|
||||
@@ -225,6 +232,14 @@ const KIND_ZH: Record<BlockKind, string> = {
|
||||
case: '分情况',
|
||||
command: '命令',
|
||||
pipeline: '管道',
|
||||
directive: '解析指令',
|
||||
stage: '阶段',
|
||||
run: '执行',
|
||||
copy: '复制',
|
||||
setting: '设置',
|
||||
label: '标签',
|
||||
health: '健康检查',
|
||||
start: '启动',
|
||||
}
|
||||
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
|
||||
const ITERATION_ZH: Record<IterationResult, string> = { generator: '生成器', list: '列表', set: '集合', dict: '字典' }
|
||||
@@ -502,8 +517,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
}
|
||||
|
||||
/**
|
||||
* 一条命令:交给命令名的命令规则读(SourceDocument.commandRule);没有规则、规则不读时命令名照抄(带源码下标)、各个词照常翻。
|
||||
* 后面跟着重定向的意思(见 redirectText)。
|
||||
* 一条命令:交给命令名的命令规则读(SourceDocument.commandRule);没有规则、规则不读时命令名照抄(按函数着色、带源码下标)、
|
||||
* 各个词照常翻。后面跟着重定向的意思(见 redirectText);规则给了表(选项写得多的命令)就带着。
|
||||
*/
|
||||
function commandReading(facts: CommandFacts, document: SourceDocument): CommandReading {
|
||||
const say = (node: Node): Inline[] => expression(node, document)
|
||||
@@ -516,10 +531,14 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
const reading = ruleFor(facts.name)?.read(facts, context)
|
||||
const name = facts.name.node
|
||||
const text = reading?.text ?? [
|
||||
segment(name.text, document.language.tokenRole(firstLeaf(name)), name.startIndex),
|
||||
segment(facts.name.literal ?? name.text, 'function', name.startIndex),
|
||||
...facts.arguments.flatMap(arg => [segment(' ', 'plain', undefined), ...say(arg.node)]),
|
||||
]
|
||||
return { text: [...text, ...facts.redirects.flatMap(redirect => redirectText(redirect, say))], ...(reading?.value && { value: true }) }
|
||||
return {
|
||||
text: [...text, ...facts.redirects.flatMap(redirect => redirectText(redirect, say))],
|
||||
...(reading?.value && { value: true }),
|
||||
...(reading?.table && { table: reading.table }),
|
||||
}
|
||||
}
|
||||
|
||||
function collect(node: Node, span: SourceSpan, out: Collector): void {
|
||||
@@ -548,7 +567,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
}
|
||||
const command = language.command?.(node)
|
||||
if (command) {
|
||||
emitPieces(out, node, commandReading(command, out.document).text)
|
||||
emitReading(out, node, commandReading(command, out.document))
|
||||
return
|
||||
}
|
||||
const parts = templateParts(node, span)
|
||||
@@ -627,6 +646,11 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
function translate(block: TextBlock, document: SourceDocument): TextTranslation {
|
||||
if (block.kind === 'comment')
|
||||
return { text: translateComment(block, document) }
|
||||
const group = block.group && table.groups?.[block.node.type]
|
||||
if (block.group && group) {
|
||||
const out: Collector = { document, root: block.node, tokens: [], cursor: block.header.startIndex, gap: false }
|
||||
return group(block.group, readerContext(out, block.header))
|
||||
}
|
||||
const slot: TableSlot = {}
|
||||
const tokens = tokensIn(block.node, block.header, document, slot)
|
||||
const compound = block.children.length > 0 || block.header.endIndex < block.node.endIndex
|
||||
@@ -669,6 +693,12 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
module: node => stringNodes.has(node.type) ? [segment(node.text, 'string', offsetOf(node, language))] : verbatim(node, document, 'type'),
|
||||
name: (node, role) => [segment(node.text, role, offsetOf(node, language))],
|
||||
doc: node => doc(node, document),
|
||||
command: (form) => {
|
||||
if (form.form === 'exec')
|
||||
return expandedText(form.node, clipped(nodeSpan(form.node)), document)
|
||||
const reading = document.embedded?.(form.code.node)
|
||||
return reading ? embeddedText(reading) : { text: verbatim(form.code.node, document) }
|
||||
},
|
||||
}
|
||||
switch (block.kind) {
|
||||
case 'function':
|
||||
@@ -685,8 +715,16 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return interpreterRows(block.facts)
|
||||
case 'options':
|
||||
return optionsRows(block.facts, table.switches ?? {})
|
||||
case 'directive':
|
||||
return directiveRows(block.facts, table.directives ?? {})
|
||||
case 'stage':
|
||||
return stageRows(block.facts, phrases)
|
||||
case 'health':
|
||||
return healthRows(block.facts, phrases)
|
||||
case 'start':
|
||||
return startRows(block.facts, phrases)
|
||||
default:
|
||||
return variableRows(block, phrases)
|
||||
return block.facts.scope ? scopedVariableRows(block.facts.declarators, block.facts.scope, phrases) : variableRows(block, phrases)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,6 +816,20 @@ interface Phrases {
|
||||
name: (node: Node, role: TokenRole) => Segment[]
|
||||
/** 函数、类的说明(JSDoc、docstring):去掉注释符号、引号和共同缩进的 Markdown 原文;没有文字时没有。 */
|
||||
doc: (node: Node) => Markdown | undefined
|
||||
/** 一条要执行的命令:数组写法交给命令规则读,shell 写法是宿主读好的嵌着的代码(SourceDocument.embedded),可以带一张选项表。 */
|
||||
command: (form: CommandForm) => { text: Inline[], table?: PreviewTable }
|
||||
}
|
||||
|
||||
/**
|
||||
* 嵌着的一段代码读成一行:整段只有一步时是这一步的译文(连它的表),几步时各步的译文连起来,
|
||||
* 一步失败就停的(shell 的 &&)用「然后」连,别的用分号。
|
||||
*/
|
||||
export function embeddedText(reading: EmbeddedReading): { text: Inline[], table?: PreviewTable } {
|
||||
const [first] = reading.steps
|
||||
if (reading.whole && first)
|
||||
return first
|
||||
const separator: Inline[] = reading.chained ? [segment(' ', 'plain', undefined), segment('然后', 'keyword', undefined), segment(' ', 'plain', undefined)] : [segment(';', 'plain', undefined)]
|
||||
return { text: reading.steps.flatMap((step, i) => i > 0 ? [...separator, ...step.text] : step.text) }
|
||||
}
|
||||
|
||||
function hint(text: string): Segment {
|
||||
@@ -914,6 +966,115 @@ function optionsRows(facts: OptionsFacts, names: Record<string, string>): Proper
|
||||
}))
|
||||
}
|
||||
|
||||
/** 解析指令:一条一行,行名是指令的叫法,值照写,后面淡色写补充说明(语法版本是给 BuildKit 看的)。 */
|
||||
function directiveRows(facts: DirectiveFacts, names: Record<string, { label: string, note?: string }>): PropertyRow[] {
|
||||
return facts.directives.map(({ name, value }) => {
|
||||
const known = names[name]
|
||||
return { label: known?.label ?? name, value: [segment(value, 'string', undefined), ...(known?.note ? [hint(known.note)] : [])] }
|
||||
})
|
||||
}
|
||||
|
||||
/** 选项的值:只是一个变量(`$BUILDPLATFORM`、`${X}`)时按变量着色、能跳到定义,别的照写成字符串。 */
|
||||
function flagValue(flag: InstructionFlag): Inline[] {
|
||||
const match = /^\$(\{)?(\w+)\}?$/.exec(flag.value)
|
||||
if (!match)
|
||||
return [segment(flag.value, 'string', flag.valueStart)]
|
||||
const name = match[2]
|
||||
return [segment(name, /^[A-Z][A-Z0-9_]*$/.test(name) ? 'constant' : 'variable', flag.valueStart + (match[1] ? 2 : 1))]
|
||||
}
|
||||
|
||||
/**
|
||||
* 阶段:名称(没起名的写按序号引用)/ 说明(紧贴在 FROM 上面的注释)/ 基础镜像(镜像名,标签、摘要写了才写;
|
||||
* 接着前面某个阶段建的写「阶段 X」)/ 平台(写了 --platform 才有)。
|
||||
*/
|
||||
function stageRows(facts: StageFacts, say: Phrases): PropertyRow[] {
|
||||
const doc = (facts.doc ?? []).map(node => say.doc(node)?.markdown).filter(Boolean).join('\n')
|
||||
const platform = facts.flags.find(flag => flag.name === 'platform')
|
||||
const image: Inline[] = facts.stage
|
||||
? [segment('阶段', 'keyword', undefined), segment(' ', 'plain', undefined), ...say.name(facts.image, 'variable')]
|
||||
: [
|
||||
...say.translated(facts.image),
|
||||
...(facts.tag ? [hint(',标签 '), ...say.translated(facts.tag)] : []),
|
||||
...(facts.digest ? [hint(',摘要 '), ...say.translated(facts.digest)] : []),
|
||||
]
|
||||
return [
|
||||
facts.name
|
||||
? { label: '名称', value: say.name(facts.name, 'variable'), ...defining(facts.name) }
|
||||
: { label: '名称', value: [hint(`没起名,按序号 ${facts.index} 引用`)] },
|
||||
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
|
||||
{ label: '基础镜像', value: image },
|
||||
...(platform ? [{ label: '平台', value: flagValue(platform) }] : []),
|
||||
]
|
||||
}
|
||||
|
||||
const DURATION_UNITS: Record<string, string> = { h: '小时', m: '分', s: '秒', ms: '毫秒', us: '微秒', ns: '纳秒' }
|
||||
|
||||
/** 时长(`30s`、`1m30s`、`500ms`):数字按数字着色,单位写成中文;认不出的照写。 */
|
||||
function durationText(flag: InstructionFlag): Inline[] {
|
||||
const parts = [...flag.value.matchAll(/(\d+(?:\.\d+)?)(ms|us|ns|[hms])/g)]
|
||||
if (parts.length === 0 || parts.map(part => part[0]).join('') !== flag.value)
|
||||
return [segment(flag.value, 'string', flag.valueStart)]
|
||||
return parts.flatMap(([, amount, unit], i) => [...(i > 0 ? [hint(' ')] : []), segment(amount, 'number', undefined), hint(` ${DURATION_UNITS[unit]}`)])
|
||||
}
|
||||
|
||||
/** 一行命令连着它的表(选项写得多的命令的「选项 | 意思」)。 */
|
||||
function commandRow(label: string, command: { text: Inline[], table?: PreviewTable }): PropertyRow {
|
||||
return { label, value: command.text, ...(command.table && { columns: command.table.columns, items: command.table.items }) }
|
||||
}
|
||||
|
||||
/** 健康检查:间隔 / 超时 / 启动后宽限 / 宽限期里的间隔 / 失败几次算不健康(写了才有)/ 检查命令;NONE 只有一行「不做健康检查」。 */
|
||||
function healthRows(facts: HealthFacts, say: Phrases): PropertyRow[] {
|
||||
if (facts.none)
|
||||
return [{ label: '检查', value: [hint('不做健康检查(基础镜像里设的也关掉)')] }]
|
||||
const flag = (name: string): InstructionFlag | undefined => facts.flags.find(item => item.name === name)
|
||||
const durations: [string, string][] = [['interval', '间隔'], ['timeout', '超时'], ['start-period', '启动后宽限'], ['start-interval', '宽限期里的间隔']]
|
||||
const retries = flag('retries')
|
||||
return [
|
||||
...durations.flatMap(([name, label]) => {
|
||||
const found = flag(name)
|
||||
return found ? [{ label, value: durationText(found) }] : []
|
||||
}),
|
||||
...(retries ? [{ label: '失败几次算不健康', value: [segment(retries.value, 'number', retries.valueStart), hint(' 次')] }] : []),
|
||||
...(facts.command ? [commandRow('检查命令', say.command(facts.command))] : []),
|
||||
]
|
||||
}
|
||||
|
||||
/** 启动:命令(ENTRYPOINT 叫入口命令)、写法(数组不经过 shell / 经过 shell)。 */
|
||||
function startRows(facts: StartFacts, say: Phrases): PropertyRow[] {
|
||||
return [
|
||||
commandRow(facts.instruction === 'cmd' ? '命令' : '入口命令', say.command(facts.command)),
|
||||
{ label: '写法', value: [hint(facts.command.form === 'exec' ? '数组,不经过 shell' : '经过 shell')] },
|
||||
]
|
||||
}
|
||||
|
||||
/** 节点所在的那条顶层语句(父节点是根)。 */
|
||||
function statementOf(node: Node): Node {
|
||||
let current = node
|
||||
while (current.parent?.parent)
|
||||
current = current.parent
|
||||
return current
|
||||
}
|
||||
|
||||
/**
|
||||
* 分在哪儿有效的变量(Dockerfile 的构建参数、环境变量):一个变量是「构建参数 / 环境变量」和「默认值 / 值」两行(构建参数没写默认值的
|
||||
* 写「没写,构建时传」);几个变量(一条写了几个,或几条连着写)并成一行,下面一张「名称 | 值」表,每行讲的定义是那个变量名。
|
||||
*/
|
||||
function scopedVariableRows(declarators: DeclaratorFacts[], scope: 'build' | 'environment', say: Phrases): PropertyRow[] {
|
||||
const label = scope === 'build' ? '构建参数' : '环境变量'
|
||||
const valueLabel = scope === 'build' ? '默认值' : '值'
|
||||
const valueOf = (declarator: DeclaratorFacts): Inline[] => declarator.value ? say.translated(declarator.value) : [hint(scope === 'build' ? '没写,构建时传' : '空')]
|
||||
const [only] = declarators
|
||||
if (declarators.length === 1 && only)
|
||||
return [{ label, value: say.translated(only.name), ...defining(only.name) }, { label: valueLabel, value: valueOf(only) }]
|
||||
const statements = new Set(declarators.map(declarator => statementOf(declarator.name).id)).size
|
||||
return [{
|
||||
label,
|
||||
value: [hint(statements > 1 ? `${statements} 条连着写` : `${declarators.length} 个`)],
|
||||
columns: ['名称', valueLabel],
|
||||
items: declarators.map(declarator => ({ cells: [say.translated(declarator.name), valueOf(declarator)], ...defining(declarator.name) })),
|
||||
}]
|
||||
}
|
||||
|
||||
/** 参数表:列名是名称 / 类型 / 默认值,每个参数一行,讲的定义是参数名;没有默认值的格子留空。 */
|
||||
function parameterRow(label: string, parameters: ParameterFacts[], say: Phrases): PropertyRow {
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user