@coldsmirk/inkstone-codemirror 0.8.2
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/README.md +54 -0
- package/dist/context-BDibyUwD.cjs +270 -0
- package/dist/context-Dx6bPnKq.js +235 -0
- package/dist/index.cjs +166 -0
- package/dist/index.d.cts +362 -0
- package/dist/index.d.ts +362 -0
- package/dist/index.js +156 -0
- package/dist/minijinja-DN9iLNbL.cjs +357 -0
- package/dist/minijinja-HpehSBbW.js +355 -0
- package/package.json +95 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @coldsmirk/inkstone-codemirror
|
|
2
|
+
|
|
3
|
+
Framework-agnostic [CodeMirror 6](https://codemirror.net/) plumbing: a controlled editor host (create-once, external-value reconcile, in-place theme swap), the standard document extension bundle, lazy per-language grammar loading, and precise [MiniJinja](https://github.com/mitsuhiko/minijinja) template support with schema-driven context completion.
|
|
4
|
+
|
|
5
|
+
Part of [inkstone](https://github.com/coldsmirk/inkstone). For a drop-in React component use [`@coldsmirk/inkstone-react`](https://www.npmjs.com/package/@coldsmirk/inkstone-react)'s `<CodeMirrorEditor>`, which is built on this package; reach for this layer to wire the same plumbing into your own components (any framework).
|
|
6
|
+
|
|
7
|
+
## What you get
|
|
8
|
+
|
|
9
|
+
- **`ControlledEditorHost`** — the controlled-editor lifecycle solved once: the view is created once, external value changes reconcile into the document without resetting cursor/undo, and theme/extension swaps happen in place.
|
|
10
|
+
- **`documentExtensions(options)`** — the standard document editing bundle (history, brackets, completion UI, optional line numbers / wrapping / search), so hosts start from working defaults instead of a bare `EditorState`.
|
|
11
|
+
- **`codeMirrorLanguages` / `loadLanguage(language)`** — the supported `CodeMirrorLanguage` names and their lazy loaders: one dynamic `import()` per grammar, so only the selected language is fetched, never the whole catalog.
|
|
12
|
+
- **MiniJinja** — `language: "minijinja"` for standalone templates, or `minijinja-<host>` mixed variants (`minijinja-html`, `-xml`, `-json`, `-yaml`, `-css`, `-scss`, `-sass`, `-less`, `-markdown`, `-sql`, `-mysql`, `-pgsql`, `-sqlite`) that overlay MiniJinja tooling on the host grammar. A linter flags non-MiniJinja tags; autocomplete is restricted to MiniJinja's real tags / filters / tests / functions.
|
|
13
|
+
- **Context completion** — `normalizeContext({ schema })` (JSON Schema) or `normalizeContext({ sample })` (representative value) builds an `EditorContext`; dispatch it with `setMinijinjaContext` to make `{{ }}` / `{% if %}` / `{% for %}` complete the render context's variables and walk member access (`user.address.city`). `minijinjaContextField`, `resolveMembers`, and `typeAtPath` expose the underlying pieces.
|
|
14
|
+
- **`searchPhrasesZhCn`** — an opt-in `EditorState.phrases` table localizing the search panel to Simplified Chinese.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add @coldsmirk/inkstone-codemirror \
|
|
20
|
+
@codemirror/state @codemirror/view @codemirror/language @codemirror/autocomplete @codemirror/commands @codemirror/search
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The `@codemirror/*` packages are **peer dependencies** on purpose: `@codemirror/state` breaks at runtime if two copies load, so your app owns the single copy and every extension is guaranteed to be built against it. Node.js >= 22 for build / SSR hosts.
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { ControlledEditorHost, documentExtensions, loadLanguage } from "@coldsmirk/inkstone-codemirror";
|
|
29
|
+
|
|
30
|
+
const host = new ControlledEditorHost({
|
|
31
|
+
parent: document.querySelector("#editor")!,
|
|
32
|
+
doc: "{\n}\n",
|
|
33
|
+
extensions: [documentExtensions({ showLineNumbers: true }), await loadLanguage("json")],
|
|
34
|
+
onChange: next => save(next)
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Later: external value changes reconcile without resetting cursor/undo.
|
|
38
|
+
host.setValue(remoteValue);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For MiniJinja with context completion:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { loadLanguage, normalizeContext, setMinijinjaContext } from "@coldsmirk/inkstone-codemirror";
|
|
45
|
+
|
|
46
|
+
// include `await loadLanguage("minijinja")` in the host's extensions, then:
|
|
47
|
+
view.dispatch({ effects: setMinijinjaContext.of(normalizeContext({ schema })) });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The completion is **structural** — it walks the declared shape (member access along a literal path), not types produced by filters or arithmetic. For a Rust backend the schema is nearly free: derive it from the same `serde` struct you render with via [`schemars`](https://docs.rs/schemars), and the completion can't drift from the real context.
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
UNLICENSED — proprietary. All rights reserved; no use, copying, or redistribution without the author's permission.
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
let _codemirror_state = require("@codemirror/state");
|
|
2
|
+
//#region src/context.ts
|
|
3
|
+
const ANY = { kind: "any" };
|
|
4
|
+
const NULL = { kind: "null" };
|
|
5
|
+
const MAX_DEREF = 100;
|
|
6
|
+
const setMinijinjaContext = _codemirror_state.StateEffect.define();
|
|
7
|
+
const minijinjaContextField = _codemirror_state.StateField.define({
|
|
8
|
+
create() {
|
|
9
|
+
return null;
|
|
10
|
+
},
|
|
11
|
+
update(value, transaction) {
|
|
12
|
+
let next = value;
|
|
13
|
+
for (const effect of transaction.effects) if (effect.is(setMinijinjaContext)) next = effect.value;
|
|
14
|
+
return next;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
function normalizeContext(input) {
|
|
18
|
+
if ("schema" in input) return fromJsonSchema(input.schema);
|
|
19
|
+
return {
|
|
20
|
+
root: fromSample(input.sample),
|
|
21
|
+
defs: {}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function resolveMembers(ir, path, scope) {
|
|
25
|
+
const [first, ...rest] = path;
|
|
26
|
+
if (first === void 0) {
|
|
27
|
+
const top = membersOf(ir, ir.root);
|
|
28
|
+
if (!scope || scope.size === 0) return top;
|
|
29
|
+
return [...[...scope].map(([name, type]) => {
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
type,
|
|
33
|
+
required: true,
|
|
34
|
+
nullable: false
|
|
35
|
+
};
|
|
36
|
+
}), ...top];
|
|
37
|
+
}
|
|
38
|
+
let type = scope?.get(first) ?? stepInto(ir, ir.root, first);
|
|
39
|
+
for (const key of rest) {
|
|
40
|
+
if (!type) break;
|
|
41
|
+
type = stepInto(ir, type, key);
|
|
42
|
+
}
|
|
43
|
+
return type ? membersOf(ir, type) : [];
|
|
44
|
+
}
|
|
45
|
+
function typeAtPath(ir, path) {
|
|
46
|
+
let type = ir.root;
|
|
47
|
+
for (const key of path) {
|
|
48
|
+
const next = stepInto(ir, type, key);
|
|
49
|
+
if (next === null) return null;
|
|
50
|
+
type = next;
|
|
51
|
+
}
|
|
52
|
+
return deref(ir, type);
|
|
53
|
+
}
|
|
54
|
+
function elementType(ir, type) {
|
|
55
|
+
const resolved = deref(ir, type);
|
|
56
|
+
return resolved.kind === "array" ? deref(ir, resolved.element) : null;
|
|
57
|
+
}
|
|
58
|
+
function deref(ir, type) {
|
|
59
|
+
let current = type;
|
|
60
|
+
for (let i = 0; i < MAX_DEREF && current.kind === "ref"; i++) {
|
|
61
|
+
const next = ir.defs[current.name];
|
|
62
|
+
if (!next) return ANY;
|
|
63
|
+
current = next;
|
|
64
|
+
}
|
|
65
|
+
return current;
|
|
66
|
+
}
|
|
67
|
+
function stepInto(ir, type, key) {
|
|
68
|
+
const resolved = deref(ir, type);
|
|
69
|
+
if (resolved.kind !== "object") return null;
|
|
70
|
+
const field = resolved.fields[key];
|
|
71
|
+
return field ? field.type : null;
|
|
72
|
+
}
|
|
73
|
+
function membersOf(ir, type) {
|
|
74
|
+
const resolved = deref(ir, type);
|
|
75
|
+
if (resolved.kind !== "object") return [];
|
|
76
|
+
return Object.entries(resolved.fields).map(([name, field]) => {
|
|
77
|
+
return {
|
|
78
|
+
name,
|
|
79
|
+
type: field.type,
|
|
80
|
+
required: field.required,
|
|
81
|
+
nullable: field.nullable,
|
|
82
|
+
doc: field.doc
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const REF_PREFIXES = ["#/$defs/", "#/definitions/"];
|
|
87
|
+
function fromJsonSchema(schema) {
|
|
88
|
+
if (!isObject(schema)) return {
|
|
89
|
+
root: ANY,
|
|
90
|
+
defs: {}
|
|
91
|
+
};
|
|
92
|
+
const defsSource = {
|
|
93
|
+
...asObject(schema.definitions),
|
|
94
|
+
...asObject(schema.$defs)
|
|
95
|
+
};
|
|
96
|
+
const defs = {};
|
|
97
|
+
for (const [name, sub] of Object.entries(defsSource)) defs[name] = convert(sub);
|
|
98
|
+
return {
|
|
99
|
+
root: convert(schema),
|
|
100
|
+
defs
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function convert(node) {
|
|
104
|
+
if (typeof node === "boolean" || !isObject(node)) return ANY;
|
|
105
|
+
const ref = refName(node.$ref);
|
|
106
|
+
if (ref !== null) return {
|
|
107
|
+
kind: "ref",
|
|
108
|
+
name: ref
|
|
109
|
+
};
|
|
110
|
+
if (Array.isArray(node.allOf)) return mergeAll(node.allOf.map((entry) => convert(entry)));
|
|
111
|
+
const union = node.anyOf ?? node.oneOf;
|
|
112
|
+
if (Array.isArray(union)) {
|
|
113
|
+
const nonNull = union.filter((branch) => !isNullBranch(branch));
|
|
114
|
+
if (nonNull.length === 0) return NULL;
|
|
115
|
+
return nonNull.length === 1 ? convert(nonNull[0]) : mergeAll(nonNull.map((entry) => convert(entry)));
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(node.enum)) return {
|
|
118
|
+
kind: "enum",
|
|
119
|
+
values: node.enum.filter(isLiteral)
|
|
120
|
+
};
|
|
121
|
+
if ("const" in node && isLiteral(node.const)) return {
|
|
122
|
+
kind: "enum",
|
|
123
|
+
values: [node.const]
|
|
124
|
+
};
|
|
125
|
+
const type = primaryType(node);
|
|
126
|
+
if (type === "object" || isObject(node.properties)) return objectType(node);
|
|
127
|
+
if (type === "array") return {
|
|
128
|
+
kind: "array",
|
|
129
|
+
element: convert(Array.isArray(node.items) ? node.items[0] : node.items)
|
|
130
|
+
};
|
|
131
|
+
if (type === "string") return { kind: "string" };
|
|
132
|
+
if (type === "integer" || type === "number") return { kind: "number" };
|
|
133
|
+
if (type === "boolean") return { kind: "boolean" };
|
|
134
|
+
if (type === "null") return NULL;
|
|
135
|
+
return ANY;
|
|
136
|
+
}
|
|
137
|
+
function objectType(node) {
|
|
138
|
+
const properties = asObject(node.properties);
|
|
139
|
+
const required = new Set(Array.isArray(node.required) ? node.required.filter((name) => typeof name === "string") : []);
|
|
140
|
+
const fields = {};
|
|
141
|
+
for (const [name, sub] of Object.entries(properties)) fields[name] = {
|
|
142
|
+
type: convert(sub),
|
|
143
|
+
required: required.has(name),
|
|
144
|
+
nullable: isNullable(sub),
|
|
145
|
+
doc: description(sub)
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
kind: "object",
|
|
149
|
+
fields
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function mergeAll(types) {
|
|
153
|
+
const objects = types.filter((type) => type.kind === "object");
|
|
154
|
+
if (objects.length > 0) {
|
|
155
|
+
const fields = {};
|
|
156
|
+
for (const object of objects) Object.assign(fields, object.fields);
|
|
157
|
+
return {
|
|
158
|
+
kind: "object",
|
|
159
|
+
fields
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return types.find((type) => type.kind !== "any") ?? ANY;
|
|
163
|
+
}
|
|
164
|
+
function fromSample(value, ancestors = /* @__PURE__ */ new WeakSet()) {
|
|
165
|
+
if (value === null || value === void 0) return ANY;
|
|
166
|
+
if (typeof value === "object") {
|
|
167
|
+
if (ancestors.has(value)) return ANY;
|
|
168
|
+
ancestors.add(value);
|
|
169
|
+
const type = Array.isArray(value) ? sampleArrayType(value, ancestors) : sampleObjectType(value, ancestors);
|
|
170
|
+
ancestors.delete(value);
|
|
171
|
+
return type;
|
|
172
|
+
}
|
|
173
|
+
if (typeof value === "string") return { kind: "string" };
|
|
174
|
+
if (typeof value === "number") return { kind: "number" };
|
|
175
|
+
if (typeof value === "boolean") return { kind: "boolean" };
|
|
176
|
+
return ANY;
|
|
177
|
+
}
|
|
178
|
+
function sampleArrayType(value, ancestors) {
|
|
179
|
+
return {
|
|
180
|
+
kind: "array",
|
|
181
|
+
element: value.length > 0 ? fromSample(value[0], ancestors) : ANY
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function sampleObjectType(value, ancestors) {
|
|
185
|
+
const fields = {};
|
|
186
|
+
for (const [name, member] of Object.entries(value)) fields[name] = {
|
|
187
|
+
type: fromSample(member, ancestors),
|
|
188
|
+
required: true,
|
|
189
|
+
nullable: member === null
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
kind: "object",
|
|
193
|
+
fields
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function refName(ref) {
|
|
197
|
+
if (typeof ref !== "string") return null;
|
|
198
|
+
for (const prefix of REF_PREFIXES) if (ref.startsWith(prefix)) return decodeURIComponent(ref.slice(prefix.length));
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
function primaryType(node) {
|
|
202
|
+
const { type } = node;
|
|
203
|
+
if (typeof type === "string") return type;
|
|
204
|
+
if (Array.isArray(type)) {
|
|
205
|
+
const nonNull = type.find((entry) => typeof entry === "string" && entry !== "null");
|
|
206
|
+
if (typeof nonNull === "string") return nonNull;
|
|
207
|
+
return type.includes("null") ? "null" : null;
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
function isNullable(node) {
|
|
212
|
+
if (!isObject(node)) return false;
|
|
213
|
+
const { type } = node;
|
|
214
|
+
if (type === "null" || Array.isArray(type) && type.includes("null")) return true;
|
|
215
|
+
const union = node.anyOf ?? node.oneOf;
|
|
216
|
+
return Array.isArray(union) && union.some((branch) => isNullBranch(branch));
|
|
217
|
+
}
|
|
218
|
+
function isNullBranch(branch) {
|
|
219
|
+
if (!isObject(branch)) return false;
|
|
220
|
+
return branch.type === "null" || Array.isArray(branch.type) && branch.type.length === 1 && branch.type[0] === "null";
|
|
221
|
+
}
|
|
222
|
+
function description(node) {
|
|
223
|
+
return isObject(node) && typeof node.description === "string" ? node.description : void 0;
|
|
224
|
+
}
|
|
225
|
+
function isObject(value) {
|
|
226
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
227
|
+
}
|
|
228
|
+
function asObject(value) {
|
|
229
|
+
return isObject(value) ? value : {};
|
|
230
|
+
}
|
|
231
|
+
function isLiteral(value) {
|
|
232
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
Object.defineProperty(exports, "elementType", {
|
|
236
|
+
enumerable: true,
|
|
237
|
+
get: function() {
|
|
238
|
+
return elementType;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
Object.defineProperty(exports, "minijinjaContextField", {
|
|
242
|
+
enumerable: true,
|
|
243
|
+
get: function() {
|
|
244
|
+
return minijinjaContextField;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
Object.defineProperty(exports, "normalizeContext", {
|
|
248
|
+
enumerable: true,
|
|
249
|
+
get: function() {
|
|
250
|
+
return normalizeContext;
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
Object.defineProperty(exports, "resolveMembers", {
|
|
254
|
+
enumerable: true,
|
|
255
|
+
get: function() {
|
|
256
|
+
return resolveMembers;
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
Object.defineProperty(exports, "setMinijinjaContext", {
|
|
260
|
+
enumerable: true,
|
|
261
|
+
get: function() {
|
|
262
|
+
return setMinijinjaContext;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
Object.defineProperty(exports, "typeAtPath", {
|
|
266
|
+
enumerable: true,
|
|
267
|
+
get: function() {
|
|
268
|
+
return typeAtPath;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { StateEffect, StateField } from "@codemirror/state";
|
|
2
|
+
//#region src/context.ts
|
|
3
|
+
const ANY = { kind: "any" };
|
|
4
|
+
const NULL = { kind: "null" };
|
|
5
|
+
const MAX_DEREF = 100;
|
|
6
|
+
const setMinijinjaContext = StateEffect.define();
|
|
7
|
+
const minijinjaContextField = StateField.define({
|
|
8
|
+
create() {
|
|
9
|
+
return null;
|
|
10
|
+
},
|
|
11
|
+
update(value, transaction) {
|
|
12
|
+
let next = value;
|
|
13
|
+
for (const effect of transaction.effects) if (effect.is(setMinijinjaContext)) next = effect.value;
|
|
14
|
+
return next;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
function normalizeContext(input) {
|
|
18
|
+
if ("schema" in input) return fromJsonSchema(input.schema);
|
|
19
|
+
return {
|
|
20
|
+
root: fromSample(input.sample),
|
|
21
|
+
defs: {}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function resolveMembers(ir, path, scope) {
|
|
25
|
+
const [first, ...rest] = path;
|
|
26
|
+
if (first === void 0) {
|
|
27
|
+
const top = membersOf(ir, ir.root);
|
|
28
|
+
if (!scope || scope.size === 0) return top;
|
|
29
|
+
return [...[...scope].map(([name, type]) => {
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
type,
|
|
33
|
+
required: true,
|
|
34
|
+
nullable: false
|
|
35
|
+
};
|
|
36
|
+
}), ...top];
|
|
37
|
+
}
|
|
38
|
+
let type = scope?.get(first) ?? stepInto(ir, ir.root, first);
|
|
39
|
+
for (const key of rest) {
|
|
40
|
+
if (!type) break;
|
|
41
|
+
type = stepInto(ir, type, key);
|
|
42
|
+
}
|
|
43
|
+
return type ? membersOf(ir, type) : [];
|
|
44
|
+
}
|
|
45
|
+
function typeAtPath(ir, path) {
|
|
46
|
+
let type = ir.root;
|
|
47
|
+
for (const key of path) {
|
|
48
|
+
const next = stepInto(ir, type, key);
|
|
49
|
+
if (next === null) return null;
|
|
50
|
+
type = next;
|
|
51
|
+
}
|
|
52
|
+
return deref(ir, type);
|
|
53
|
+
}
|
|
54
|
+
function elementType(ir, type) {
|
|
55
|
+
const resolved = deref(ir, type);
|
|
56
|
+
return resolved.kind === "array" ? deref(ir, resolved.element) : null;
|
|
57
|
+
}
|
|
58
|
+
function deref(ir, type) {
|
|
59
|
+
let current = type;
|
|
60
|
+
for (let i = 0; i < MAX_DEREF && current.kind === "ref"; i++) {
|
|
61
|
+
const next = ir.defs[current.name];
|
|
62
|
+
if (!next) return ANY;
|
|
63
|
+
current = next;
|
|
64
|
+
}
|
|
65
|
+
return current;
|
|
66
|
+
}
|
|
67
|
+
function stepInto(ir, type, key) {
|
|
68
|
+
const resolved = deref(ir, type);
|
|
69
|
+
if (resolved.kind !== "object") return null;
|
|
70
|
+
const field = resolved.fields[key];
|
|
71
|
+
return field ? field.type : null;
|
|
72
|
+
}
|
|
73
|
+
function membersOf(ir, type) {
|
|
74
|
+
const resolved = deref(ir, type);
|
|
75
|
+
if (resolved.kind !== "object") return [];
|
|
76
|
+
return Object.entries(resolved.fields).map(([name, field]) => {
|
|
77
|
+
return {
|
|
78
|
+
name,
|
|
79
|
+
type: field.type,
|
|
80
|
+
required: field.required,
|
|
81
|
+
nullable: field.nullable,
|
|
82
|
+
doc: field.doc
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const REF_PREFIXES = ["#/$defs/", "#/definitions/"];
|
|
87
|
+
function fromJsonSchema(schema) {
|
|
88
|
+
if (!isObject(schema)) return {
|
|
89
|
+
root: ANY,
|
|
90
|
+
defs: {}
|
|
91
|
+
};
|
|
92
|
+
const defsSource = {
|
|
93
|
+
...asObject(schema.definitions),
|
|
94
|
+
...asObject(schema.$defs)
|
|
95
|
+
};
|
|
96
|
+
const defs = {};
|
|
97
|
+
for (const [name, sub] of Object.entries(defsSource)) defs[name] = convert(sub);
|
|
98
|
+
return {
|
|
99
|
+
root: convert(schema),
|
|
100
|
+
defs
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function convert(node) {
|
|
104
|
+
if (typeof node === "boolean" || !isObject(node)) return ANY;
|
|
105
|
+
const ref = refName(node.$ref);
|
|
106
|
+
if (ref !== null) return {
|
|
107
|
+
kind: "ref",
|
|
108
|
+
name: ref
|
|
109
|
+
};
|
|
110
|
+
if (Array.isArray(node.allOf)) return mergeAll(node.allOf.map((entry) => convert(entry)));
|
|
111
|
+
const union = node.anyOf ?? node.oneOf;
|
|
112
|
+
if (Array.isArray(union)) {
|
|
113
|
+
const nonNull = union.filter((branch) => !isNullBranch(branch));
|
|
114
|
+
if (nonNull.length === 0) return NULL;
|
|
115
|
+
return nonNull.length === 1 ? convert(nonNull[0]) : mergeAll(nonNull.map((entry) => convert(entry)));
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(node.enum)) return {
|
|
118
|
+
kind: "enum",
|
|
119
|
+
values: node.enum.filter(isLiteral)
|
|
120
|
+
};
|
|
121
|
+
if ("const" in node && isLiteral(node.const)) return {
|
|
122
|
+
kind: "enum",
|
|
123
|
+
values: [node.const]
|
|
124
|
+
};
|
|
125
|
+
const type = primaryType(node);
|
|
126
|
+
if (type === "object" || isObject(node.properties)) return objectType(node);
|
|
127
|
+
if (type === "array") return {
|
|
128
|
+
kind: "array",
|
|
129
|
+
element: convert(Array.isArray(node.items) ? node.items[0] : node.items)
|
|
130
|
+
};
|
|
131
|
+
if (type === "string") return { kind: "string" };
|
|
132
|
+
if (type === "integer" || type === "number") return { kind: "number" };
|
|
133
|
+
if (type === "boolean") return { kind: "boolean" };
|
|
134
|
+
if (type === "null") return NULL;
|
|
135
|
+
return ANY;
|
|
136
|
+
}
|
|
137
|
+
function objectType(node) {
|
|
138
|
+
const properties = asObject(node.properties);
|
|
139
|
+
const required = new Set(Array.isArray(node.required) ? node.required.filter((name) => typeof name === "string") : []);
|
|
140
|
+
const fields = {};
|
|
141
|
+
for (const [name, sub] of Object.entries(properties)) fields[name] = {
|
|
142
|
+
type: convert(sub),
|
|
143
|
+
required: required.has(name),
|
|
144
|
+
nullable: isNullable(sub),
|
|
145
|
+
doc: description(sub)
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
kind: "object",
|
|
149
|
+
fields
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function mergeAll(types) {
|
|
153
|
+
const objects = types.filter((type) => type.kind === "object");
|
|
154
|
+
if (objects.length > 0) {
|
|
155
|
+
const fields = {};
|
|
156
|
+
for (const object of objects) Object.assign(fields, object.fields);
|
|
157
|
+
return {
|
|
158
|
+
kind: "object",
|
|
159
|
+
fields
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return types.find((type) => type.kind !== "any") ?? ANY;
|
|
163
|
+
}
|
|
164
|
+
function fromSample(value, ancestors = /* @__PURE__ */ new WeakSet()) {
|
|
165
|
+
if (value === null || value === void 0) return ANY;
|
|
166
|
+
if (typeof value === "object") {
|
|
167
|
+
if (ancestors.has(value)) return ANY;
|
|
168
|
+
ancestors.add(value);
|
|
169
|
+
const type = Array.isArray(value) ? sampleArrayType(value, ancestors) : sampleObjectType(value, ancestors);
|
|
170
|
+
ancestors.delete(value);
|
|
171
|
+
return type;
|
|
172
|
+
}
|
|
173
|
+
if (typeof value === "string") return { kind: "string" };
|
|
174
|
+
if (typeof value === "number") return { kind: "number" };
|
|
175
|
+
if (typeof value === "boolean") return { kind: "boolean" };
|
|
176
|
+
return ANY;
|
|
177
|
+
}
|
|
178
|
+
function sampleArrayType(value, ancestors) {
|
|
179
|
+
return {
|
|
180
|
+
kind: "array",
|
|
181
|
+
element: value.length > 0 ? fromSample(value[0], ancestors) : ANY
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function sampleObjectType(value, ancestors) {
|
|
185
|
+
const fields = {};
|
|
186
|
+
for (const [name, member] of Object.entries(value)) fields[name] = {
|
|
187
|
+
type: fromSample(member, ancestors),
|
|
188
|
+
required: true,
|
|
189
|
+
nullable: member === null
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
kind: "object",
|
|
193
|
+
fields
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function refName(ref) {
|
|
197
|
+
if (typeof ref !== "string") return null;
|
|
198
|
+
for (const prefix of REF_PREFIXES) if (ref.startsWith(prefix)) return decodeURIComponent(ref.slice(prefix.length));
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
function primaryType(node) {
|
|
202
|
+
const { type } = node;
|
|
203
|
+
if (typeof type === "string") return type;
|
|
204
|
+
if (Array.isArray(type)) {
|
|
205
|
+
const nonNull = type.find((entry) => typeof entry === "string" && entry !== "null");
|
|
206
|
+
if (typeof nonNull === "string") return nonNull;
|
|
207
|
+
return type.includes("null") ? "null" : null;
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
function isNullable(node) {
|
|
212
|
+
if (!isObject(node)) return false;
|
|
213
|
+
const { type } = node;
|
|
214
|
+
if (type === "null" || Array.isArray(type) && type.includes("null")) return true;
|
|
215
|
+
const union = node.anyOf ?? node.oneOf;
|
|
216
|
+
return Array.isArray(union) && union.some((branch) => isNullBranch(branch));
|
|
217
|
+
}
|
|
218
|
+
function isNullBranch(branch) {
|
|
219
|
+
if (!isObject(branch)) return false;
|
|
220
|
+
return branch.type === "null" || Array.isArray(branch.type) && branch.type.length === 1 && branch.type[0] === "null";
|
|
221
|
+
}
|
|
222
|
+
function description(node) {
|
|
223
|
+
return isObject(node) && typeof node.description === "string" ? node.description : void 0;
|
|
224
|
+
}
|
|
225
|
+
function isObject(value) {
|
|
226
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
227
|
+
}
|
|
228
|
+
function asObject(value) {
|
|
229
|
+
return isObject(value) ? value : {};
|
|
230
|
+
}
|
|
231
|
+
function isLiteral(value) {
|
|
232
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
export { setMinijinjaContext as a, resolveMembers as i, minijinjaContextField as n, typeAtPath as o, normalizeContext as r, elementType as t };
|