@lingfan-engine/editor 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/package.json +39 -3
- package/src/chapters/build.ts +180 -0
- package/src/chapters/index.ts +34 -0
- package/src/chapters/types.ts +71 -0
- package/src/contracts/diagnostics.ts +62 -0
- package/src/contracts/editing.ts +26 -0
- package/src/contracts/forms.ts +69 -0
- package/src/contracts/index.ts +22 -0
- package/src/contracts/texts.ts +204 -0
- package/src/diagnostics/analyze.ts +161 -0
- package/src/diagnostics/grouping.ts +164 -0
- package/src/diagnostics/index.ts +35 -0
- package/src/diagnostics/message.ts +60 -0
- package/src/diagnostics/refs.ts +123 -0
- package/src/diagnostics/symbols.ts +221 -0
- package/src/editing/columnGrouping.ts +331 -0
- package/src/editing/graphConnect.ts +88 -0
- package/src/editing/index.ts +72 -0
- package/src/editing/pointers.ts +291 -0
- package/src/editing/saveNormalization.ts +58 -0
- package/src/editing/session.ts +134 -0
- package/src/editing/storyOps.ts +268 -0
- package/src/editing/tsSourceGuard.ts +40 -0
- package/src/element/drag.ts +22 -0
- package/src/element/dragCreate.ts +60 -0
- package/src/element/index.ts +11 -0
- package/src/i18n/coverage.ts +191 -0
- package/src/i18n/extract.ts +38 -0
- package/src/i18n/index.ts +47 -0
- package/src/i18n/reconcile.ts +71 -0
- package/src/i18n/skeleton.ts +112 -0
- package/src/i18n/storyKeys.ts +27 -0
- package/src/i18n/surfaces.ts +54 -0
- package/src/i18n/table.ts +235 -0
- package/src/index.ts +342 -0
- package/src/layout/index.ts +14 -0
- package/src/layout/steps.ts +378 -0
- package/src/schema/catalog.ts +300 -0
- package/src/schema/elementForms.ts +307 -0
- package/src/schema/elementPalette.ts +100 -0
- package/src/schema/forms.ts +196 -0
- package/src/schema/index.ts +55 -0
- package/src/schema/opSchemas/commands.ts +374 -0
- package/src/schema/opSchemas/fragments.ts +24 -0
- package/src/schema/opSchemas/index.ts +16 -0
- package/src/schema/opSchemas/map.ts +134 -0
- package/src/schema/opSchemas/types.ts +42 -0
- package/src/schema/opSchemas/validate.ts +184 -0
- package/src/schema/surface.ts +101 -0
- package/src/schema/validation.ts +92 -0
- package/src/schema/walk.ts +167 -0
- package/src/script/expr/audit/index.ts +8 -0
- package/src/script/expr/audit/parser.ts +370 -0
- package/src/script/expr/audit/token.ts +23 -0
- package/src/script/expr/build.ts +203 -0
- package/src/script/expr/index.ts +17 -0
- package/src/script/expr/registry.ts +130 -0
- package/src/script/index.ts +168 -0
- package/src/script/vocabulary-gen.ts +202 -0
- package/src/script/vocabulary.g.ts +175 -0
- package/src/script/words/audio.ts +114 -0
- package/src/script/words/cells.ts +78 -0
- package/src/script/words/compile-time.ts +27 -0
- package/src/script/words/composite.ts +125 -0
- package/src/script/words/elements.ts +202 -0
- package/src/script/words/extension.ts +25 -0
- package/src/script/words/flow.ts +391 -0
- package/src/script/words/index.ts +23 -0
- package/src/script/words/interaction.ts +44 -0
- package/src/script/words/minigame.ts +66 -0
- package/src/script/words/narrative.ts +261 -0
- package/src/script/words/save.ts +54 -0
- package/src/script/words/vars.ts +155 -0
- package/src/script/words/video.ts +80 -0
- package/src/shared/guards.ts +19 -0
- package/src/shared/index.ts +6 -0
- package/src/shared/pointer.ts +23 -0
- package/README.md +0 -3
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 诊断文案 · **主句 / 说明分层**(纯函数,可测)。
|
|
3
|
+
*
|
|
4
|
+
* 存在理由:诊断 `message` 把「状态」和「怎么处置」塞进同一句——
|
|
5
|
+
* 例:`未使用的译文键:第三章 · 相遇(say / menu / input / notify 四个翻译面均未命中原文——该键运行期不会生效;若这段文字只用于元素文本或宿主界面,本条可忽略)`。
|
|
6
|
+
* 括号里那段**明说「可忽略」**却占主句大半篇幅 ⇒ 窄栏里每条竖排多行,
|
|
7
|
+
* **扫读成本极高**。
|
|
8
|
+
*
|
|
9
|
+
* 分层后:主句只说状态(进列表、可扫读),说明进 `title`/次要行(按需展开)。
|
|
10
|
+
*
|
|
11
|
+
* **不改 `Diagnostic.message` 的既有形状**(契约字段、诊断码、pointer 都不动)——
|
|
12
|
+
* 拆的是**渲染分层**:`detailOf(diagnostic)` 从 message 里切出「主句 / 说明」,
|
|
13
|
+
* 这样**既有测试与消费方全部不受影响**(不制造第二份文案事实源)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 主句 / 说明两段文案 */
|
|
17
|
+
export interface DiagnosticMessageParts {
|
|
18
|
+
/** 主句:状态本身(列表里显示的就是它) */
|
|
19
|
+
readonly brief: string;
|
|
20
|
+
/** 说明:怎么处置 / 为什么(默认收起,悬停或展开可见);无说明时为空串 */
|
|
21
|
+
readonly detail: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 从 `message` 切出主句与说明。
|
|
26
|
+
*
|
|
27
|
+
* 判据:**首个全角括号及其后内容**算说明(现有 4 条诊断的说明都在括号里,
|
|
28
|
+
* 且括号内不含右括号以外的结构)。没括号 ⇒ 整句是主句、无说明。
|
|
29
|
+
*
|
|
30
|
+
* **不切 `pointer`**:pointer 是另一个字段,不参与文案分层。
|
|
31
|
+
*/
|
|
32
|
+
export function splitDiagnosticMessage(
|
|
33
|
+
message: string,
|
|
34
|
+
): DiagnosticMessageParts {
|
|
35
|
+
const start = message.indexOf("(");
|
|
36
|
+
if (start <= 0) return { brief: message, detail: "" };
|
|
37
|
+
// 说明本身若含「(…)」嵌套,只取到**配对**的右括号
|
|
38
|
+
let depth = 0;
|
|
39
|
+
let end = -1;
|
|
40
|
+
for (let i = start; i < message.length; i += 1) {
|
|
41
|
+
if (message[i] === "(") depth += 1;
|
|
42
|
+
else if (message[i] === ")") {
|
|
43
|
+
depth -= 1;
|
|
44
|
+
if (depth === 0) {
|
|
45
|
+
end = i;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (end < 0) return { brief: message, detail: "" };
|
|
51
|
+
return {
|
|
52
|
+
brief: message.slice(0, start).trimEnd(),
|
|
53
|
+
detail: message.slice(start + 1, end).trim(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 列表里显示的主句(便捷出口) */
|
|
58
|
+
export function diagnosticBrief(message: string): string {
|
|
59
|
+
return splitDiagnosticMessage(message).brief;
|
|
60
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表达式引用抽取(纯函数,零依赖):
|
|
3
|
+
* 从故事文本 / 字段值里剥出会被求值的表达式源,并抽出其中的变量键引用。
|
|
4
|
+
* 供符号索引(./symbols)按字段形态调用;extractExpressionRefs 经
|
|
5
|
+
* ../diagnostics/index.ts 对外转发。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 行内富文本标记(镜像 packages/core/src/runtime/expr/interpolate.ts;行为互锁见 tests/editor/schema.test.ts) */
|
|
9
|
+
const INLINE_SHORT_TAGS = new Set([
|
|
10
|
+
"b",
|
|
11
|
+
"/b",
|
|
12
|
+
"i",
|
|
13
|
+
"/i",
|
|
14
|
+
"u",
|
|
15
|
+
"/u",
|
|
16
|
+
"w",
|
|
17
|
+
"fast",
|
|
18
|
+
"p",
|
|
19
|
+
"color",
|
|
20
|
+
"font",
|
|
21
|
+
"size",
|
|
22
|
+
]);
|
|
23
|
+
const INLINE_PREFIXED_TAGS = [
|
|
24
|
+
"color=",
|
|
25
|
+
"/color",
|
|
26
|
+
"font=",
|
|
27
|
+
"/font",
|
|
28
|
+
"size=",
|
|
29
|
+
"/size",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function isInlineTag(content: string): boolean {
|
|
33
|
+
if (INLINE_SHORT_TAGS.has(content)) return true;
|
|
34
|
+
return INLINE_PREFIXED_TAGS.some((prefix) => content.startsWith(prefix));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 表达式内置名与字面量(与引擎 runtime/expr/builtins.ts 全集互锁:新增内建须同步) */
|
|
38
|
+
const EXPR_BUILTINS = new Set([
|
|
39
|
+
"random",
|
|
40
|
+
"min",
|
|
41
|
+
"max",
|
|
42
|
+
"abs",
|
|
43
|
+
"clamp",
|
|
44
|
+
"arrayGet",
|
|
45
|
+
"dictGet",
|
|
46
|
+
"optGet",
|
|
47
|
+
"len",
|
|
48
|
+
"true",
|
|
49
|
+
"false",
|
|
50
|
+
]);
|
|
51
|
+
const IDENT_PATH_RE =
|
|
52
|
+
/[A-Za-z_\u4e00-\u9fa5][A-Za-z0-9_\u4e00-\u9fa5]*(?:\.[A-Za-z_][A-Za-z0-9_\u4e00-\u9fa5]*)*/g;
|
|
53
|
+
|
|
54
|
+
/** 表达式中的变量键引用(点路径整键 = 全局键路径/字典下钻;跳过字符串字面量/数字尾巴/内置名) */
|
|
55
|
+
export function extractExpressionRefs(expr: string): string[] {
|
|
56
|
+
let stripped = "";
|
|
57
|
+
for (let i = 0; i < expr.length; i += 1) {
|
|
58
|
+
const ch = expr[i];
|
|
59
|
+
if (ch === '"') {
|
|
60
|
+
i += 1;
|
|
61
|
+
while (i < expr.length) {
|
|
62
|
+
if (expr[i] === "\\") {
|
|
63
|
+
i += 2;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (expr[i] === '"') break;
|
|
67
|
+
i += 1;
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
stripped += ch;
|
|
72
|
+
}
|
|
73
|
+
const refs: string[] = [];
|
|
74
|
+
for (const match of stripped.matchAll(IDENT_PATH_RE)) {
|
|
75
|
+
const name = match[0];
|
|
76
|
+
const prev = stripped.slice(0, match.index ?? 0).trimEnd();
|
|
77
|
+
if (/\d$/.test(prev)) continue;
|
|
78
|
+
if (EXPR_BUILTINS.has(name)) continue;
|
|
79
|
+
if (!refs.includes(name)) refs.push(name);
|
|
80
|
+
}
|
|
81
|
+
return refs;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 引号字符串字面量剔除后的 {…} 段求值源提取(插值面:先判行内标记,再剥格式后缀) */
|
|
85
|
+
export function exprSourcesInText(text: string): string[] {
|
|
86
|
+
const sources: string[] = [];
|
|
87
|
+
let i = 0;
|
|
88
|
+
while (i < text.length) {
|
|
89
|
+
if (text[i] !== "{") {
|
|
90
|
+
i += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const end = text.indexOf("}", i);
|
|
94
|
+
if (end < 0) break;
|
|
95
|
+
const content = text.slice(i + 1, end).trim();
|
|
96
|
+
if (!isInlineTag(content)) {
|
|
97
|
+
let exprSrc = content;
|
|
98
|
+
if (!content.includes("?")) {
|
|
99
|
+
const colon = content.indexOf(":");
|
|
100
|
+
if (colon > 0) exprSrc = content.slice(0, colon).trim();
|
|
101
|
+
}
|
|
102
|
+
sources.push(exprSrc);
|
|
103
|
+
}
|
|
104
|
+
i = end + 1;
|
|
105
|
+
}
|
|
106
|
+
return sources;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 表达式面源提取(cond/on/in:{…} 包装剥壳,与执行器 evalCond 同构) */
|
|
110
|
+
export function exprSourceInExpressionField(raw: string): string {
|
|
111
|
+
const t = raw.trim();
|
|
112
|
+
return t.startsWith("{") && t.endsWith("}") ? t.slice(1, -1) : t;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const COMPOUND_PREFIX = /^\s*(\+=|-=|\*=|\/=|%=)\s*([\s\S]+)$/;
|
|
116
|
+
|
|
117
|
+
/** value 字段源提取({expr} 包装或复合赋值右部;其余 = 字面量无引用),与执行器 evalValue/execAssign 同构 */
|
|
118
|
+
export function exprSourceInValueField(raw: string): string | null {
|
|
119
|
+
const compound = COMPOUND_PREFIX.exec(raw);
|
|
120
|
+
if (compound !== null) return compound[2];
|
|
121
|
+
const t = raw.trim();
|
|
122
|
+
return t.startsWith("{") && t.endsWith("}") ? t.slice(1, -1) : null;
|
|
123
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 符号索引:把故事里的定义键 / 列 / 函数 / 跳转目标 / 变量引用与可翻译原文
|
|
3
|
+
* 收进一张 SymbolIndex(单次遍历)。消费方是分析器(./analyze)与编辑器功能。
|
|
4
|
+
*/
|
|
5
|
+
import type { Story } from "@lingfan-engine/core";
|
|
6
|
+
import type { FieldDescriptor, SymbolIndex } from "../contracts/index.ts";
|
|
7
|
+
import { joinPointer } from "../contracts/index.ts";
|
|
8
|
+
import { TRANSLATE_SURFACES, valuesAtPath } from "../i18n/index.ts";
|
|
9
|
+
import { walkStoryCommands, walkStoryElements } from "../schema/index.ts";
|
|
10
|
+
import { isPlainObject } from "../shared/index.ts";
|
|
11
|
+
import {
|
|
12
|
+
extractExpressionRefs,
|
|
13
|
+
exprSourceInExpressionField,
|
|
14
|
+
exprSourceInValueField,
|
|
15
|
+
exprSourcesInText,
|
|
16
|
+
} from "./refs.ts";
|
|
17
|
+
|
|
18
|
+
/** 变量定义字段(op → 相对字段路径);键 = 符号索引 definedKeys 的来源 */
|
|
19
|
+
const VAR_DEF_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
20
|
+
set: ["key"],
|
|
21
|
+
define: ["key"],
|
|
22
|
+
let: ["key"],
|
|
23
|
+
local: ["key"],
|
|
24
|
+
array: ["key"],
|
|
25
|
+
dict: ["key"],
|
|
26
|
+
random: ["var"],
|
|
27
|
+
for: ["var"],
|
|
28
|
+
foreach: ["var"],
|
|
29
|
+
input: ["store"],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** 跳转目标字段(op → [字段路径, 目标种类]) */
|
|
33
|
+
const TARGET_FIELDS: Readonly<
|
|
34
|
+
Record<
|
|
35
|
+
string,
|
|
36
|
+
readonly (readonly [string, "column" | "function" | "callable"])[]
|
|
37
|
+
>
|
|
38
|
+
> = {
|
|
39
|
+
jump: [["target", "column"]],
|
|
40
|
+
// `call` 的目标**可以是列(label)或 func**(「调用子过程(func 或 label)」)
|
|
41
|
+
// ⇒ `callable` = 任一存在即可。
|
|
42
|
+
call: [["target", "callable"]],
|
|
43
|
+
menu: [["options[].target", "column"]],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// 可翻译原文面与路径取值 = i18n 工具链模块的单一事实源:
|
|
47
|
+
// 诊断的 originals 收集与 extractStoryKeys 消费同一张表(互锁: i18n-key-extract-parity)
|
|
48
|
+
|
|
49
|
+
/** 符号索引:一次遍历收集全部符号与引用(畸形输入 fail-closed 返回空索引) */
|
|
50
|
+
export function indexStory(story: Story): SymbolIndex {
|
|
51
|
+
const index: SymbolIndex = {
|
|
52
|
+
columnPointers: new Map(),
|
|
53
|
+
duplicateColumns: [],
|
|
54
|
+
entry: story?.entry,
|
|
55
|
+
definedKeys: new Map(),
|
|
56
|
+
functions: new Map(),
|
|
57
|
+
targets: [],
|
|
58
|
+
resources: [],
|
|
59
|
+
variableRefs: [],
|
|
60
|
+
originals: new Set(),
|
|
61
|
+
};
|
|
62
|
+
if (story === null || typeof story !== "object") return index;
|
|
63
|
+
if (Array.isArray(story.columns)) {
|
|
64
|
+
story.columns.forEach((column, columnIndex) => {
|
|
65
|
+
if (!isPlainObject(column) || typeof column.id !== "string") return;
|
|
66
|
+
const columnPointer = joinPointer("columns", columnIndex);
|
|
67
|
+
if (index.columnPointers.has(column.id)) {
|
|
68
|
+
index.duplicateColumns.push({ id: column.id, pointer: columnPointer });
|
|
69
|
+
} else {
|
|
70
|
+
index.columnPointers.set(column.id, columnPointer);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
for (const key of Object.keys(story.defines ?? {})) {
|
|
75
|
+
if (!index.definedKeys.has(key)) {
|
|
76
|
+
index.definedKeys.set(key, joinPointer("defines", key));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
walkStoryCommands(story, (cmd, pointer, fields) => {
|
|
80
|
+
if (!isPlainObject(cmd) || typeof cmd.op !== "string") return;
|
|
81
|
+
const op = cmd.op;
|
|
82
|
+
if (op === "func" && typeof cmd.name === "string") {
|
|
83
|
+
if (!index.functions.has(cmd.name)) {
|
|
84
|
+
index.functions.set(cmd.name, {
|
|
85
|
+
params: Array.isArray(cmd.params)
|
|
86
|
+
? cmd.params.filter((p): p is string => typeof p === "string")
|
|
87
|
+
: [],
|
|
88
|
+
pointer,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (op === "navigate") {
|
|
93
|
+
const scene =
|
|
94
|
+
typeof cmd.scene === "string" && cmd.scene !== "" ? cmd.scene : null;
|
|
95
|
+
const target = scene ?? (typeof cmd.path === "string" ? cmd.path : "");
|
|
96
|
+
if (target !== "") {
|
|
97
|
+
index.targets.push({ pointer, target, kind: "column" });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const [path, targetKind] of TARGET_FIELDS[op] ?? []) {
|
|
101
|
+
for (const { value, pointer: fieldPointer } of valuesAtPath(cmd, path)) {
|
|
102
|
+
if (typeof value === "string" && value !== "") {
|
|
103
|
+
index.targets.push({
|
|
104
|
+
pointer: `${pointer}${fieldPointer}`,
|
|
105
|
+
target: value,
|
|
106
|
+
kind: targetKind,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const path of VAR_DEF_FIELDS[op] ?? []) {
|
|
112
|
+
for (const { value, pointer: fieldPointer } of valuesAtPath(cmd, path)) {
|
|
113
|
+
if (
|
|
114
|
+
typeof value === "string" &&
|
|
115
|
+
value !== "" &&
|
|
116
|
+
!index.definedKeys.has(value)
|
|
117
|
+
) {
|
|
118
|
+
index.definedKeys.set(value, `${pointer}${fieldPointer}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const path of TRANSLATE_SURFACES[op] ?? []) {
|
|
123
|
+
for (const { value } of valuesAtPath(cmd, path)) {
|
|
124
|
+
if (typeof value === "string" && value !== "")
|
|
125
|
+
index.originals.add(value);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
scanFields(cmd, pointer, fields ?? [], index);
|
|
129
|
+
});
|
|
130
|
+
// 翻译面:元素展示文字(`text`,运行期装载时 Translate)也计入原文集合
|
|
131
|
+
walkStoryElements(story, (node) => {
|
|
132
|
+
if (!isPlainObject(node)) return;
|
|
133
|
+
const text = node.text;
|
|
134
|
+
if (typeof text === "string" && text !== "") index.originals.add(text);
|
|
135
|
+
});
|
|
136
|
+
return index;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function scanFields(
|
|
140
|
+
cmd: Record<string, unknown>,
|
|
141
|
+
pointer: string,
|
|
142
|
+
fields: readonly FieldDescriptor[],
|
|
143
|
+
index: SymbolIndex,
|
|
144
|
+
): void {
|
|
145
|
+
for (const field of fields) {
|
|
146
|
+
const value = cmd[field.key];
|
|
147
|
+
const fieldPointer = `${pointer}/${field.key}`;
|
|
148
|
+
switch (field.kind) {
|
|
149
|
+
case "expression":
|
|
150
|
+
if (typeof value === "string" && value !== "") {
|
|
151
|
+
for (const name of extractExpressionRefs(
|
|
152
|
+
exprSourceInExpressionField(value),
|
|
153
|
+
)) {
|
|
154
|
+
index.variableRefs.push({
|
|
155
|
+
pointer: fieldPointer,
|
|
156
|
+
name,
|
|
157
|
+
kind: "expression",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
case "value":
|
|
163
|
+
if (typeof value === "string" && value !== "") {
|
|
164
|
+
const source = exprSourceInValueField(value);
|
|
165
|
+
if (source !== null) {
|
|
166
|
+
for (const name of extractExpressionRefs(source)) {
|
|
167
|
+
index.variableRefs.push({
|
|
168
|
+
pointer: fieldPointer,
|
|
169
|
+
name,
|
|
170
|
+
kind: "expression",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
case "text":
|
|
177
|
+
if (typeof value === "string" && value !== "") {
|
|
178
|
+
for (const source of exprSourcesInText(value)) {
|
|
179
|
+
for (const name of extractExpressionRefs(source)) {
|
|
180
|
+
index.variableRefs.push({
|
|
181
|
+
pointer: fieldPointer,
|
|
182
|
+
name,
|
|
183
|
+
kind: "interpolation",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
case "resource":
|
|
190
|
+
if (typeof value === "string" && value !== "") {
|
|
191
|
+
index.resources.push({ pointer: fieldPointer, path: value });
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
case "array":
|
|
195
|
+
case "body":
|
|
196
|
+
case "object":
|
|
197
|
+
if (isPlainObject(value)) {
|
|
198
|
+
scanFields(
|
|
199
|
+
value as Record<string, unknown>,
|
|
200
|
+
fieldPointer,
|
|
201
|
+
field.properties ?? [],
|
|
202
|
+
index,
|
|
203
|
+
);
|
|
204
|
+
} else if (Array.isArray(value) && field.item !== undefined) {
|
|
205
|
+
value.forEach((element, itemIndex) => {
|
|
206
|
+
if (isPlainObject(element)) {
|
|
207
|
+
scanFields(
|
|
208
|
+
element as Record<string, unknown>,
|
|
209
|
+
`${fieldPointer}/${itemIndex}`,
|
|
210
|
+
field.item!.properties ?? [],
|
|
211
|
+
index,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
default:
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|