@yuneta/gobj-ui 6.0.0 → 6.1.1

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.
@@ -0,0 +1,258 @@
1
+ /***********************************************************************
2
+ * schema_model.test.js
3
+ *
4
+ * The three flat topics turned back into a schema, pinned.
5
+ *
6
+ * Most of these are about the two ways the grouping can go wrong
7
+ * quietly: a fkey shape the backend answers with and the parser
8
+ * does not know (the record disappears from its topic), and an id
9
+ * split on '.' standing in for the link (the record lands under
10
+ * the wrong topic the day a name carries a dot).
11
+ ***********************************************************************/
12
+ import { describe, test, expect } from "vitest";
13
+ import {
14
+ DEFAULT_ORDER,
15
+ parse_fkey_ref,
16
+ record_name,
17
+ record_order,
18
+ build_schema_model,
19
+ find_treedb,
20
+ find_topic,
21
+ find_col,
22
+ fkey_ref,
23
+ next_order,
24
+ moved_orders,
25
+ is_empty_value,
26
+ } from "./schema_model.js";
27
+
28
+
29
+ function col(treedb, topic, name, order, extra)
30
+ {
31
+ return Object.assign({
32
+ id: `${treedb}.${topic}.${name}`,
33
+ value: name,
34
+ order: order,
35
+ topics: [`topics^${treedb}.${topic}^cols`],
36
+ }, extra || {});
37
+ }
38
+
39
+ function topic(treedb, name, order, extra)
40
+ {
41
+ return Object.assign({
42
+ id: `${treedb}.${name}`,
43
+ value: name,
44
+ order: order,
45
+ treedbs: [`treedbs^${treedb}^topics`],
46
+ }, extra || {});
47
+ }
48
+
49
+
50
+ describe("parse_fkey_ref — the four shapes of one fact", () => {
51
+ test("the bare ref string", () => {
52
+ expect(parse_fkey_ref("topics^db.users^cols")).toEqual([
53
+ {topic_name: "topics", id: "db.users", hook_name: "cols"}
54
+ ]);
55
+ });
56
+
57
+ test("a list of ref strings", () => {
58
+ expect(parse_fkey_ref(["topics^db.users^cols"])[0].id).toBe("db.users");
59
+ });
60
+
61
+ test("the expanded object", () => {
62
+ expect(parse_fkey_ref([{topic_name: "topics", id: "db.users", hook_name: "cols"}])).toEqual([
63
+ {topic_name: "topics", id: "db.users", hook_name: "cols"}
64
+ ]);
65
+ });
66
+
67
+ test("a dict keyed BY the ref", () => {
68
+ expect(parse_fkey_ref({"topics^db.users^cols": true})[0].id).toBe("db.users");
69
+ });
70
+
71
+ test("a dict whose value is the expanded node reads the value, not the key", () => {
72
+ const refs = parse_fkey_ref({
73
+ "topics^stale^cols": {topic_name: "topics", id: "db.users", hook_name: "cols"}
74
+ });
75
+ expect(refs[0].id).toBe("db.users");
76
+ });
77
+
78
+ test("nothing, and rubbish, contribute nothing", () => {
79
+ expect(parse_fkey_ref(null)).toEqual([]);
80
+ expect(parse_fkey_ref(undefined)).toEqual([]);
81
+ expect(parse_fkey_ref([])).toEqual([]);
82
+ expect(parse_fkey_ref("not-a-ref")).toEqual([]);
83
+ expect(parse_fkey_ref(["a^b"])).toEqual([]);
84
+ expect(parse_fkey_ref([{topic_name: "topics"}])).toEqual([]);
85
+ });
86
+ });
87
+
88
+ describe("record_name / record_order", () => {
89
+ test("the name is the pkey2, not the qualified id", () => {
90
+ expect(record_name({id: "db.users.name", value: "name"})).toBe("name");
91
+ });
92
+
93
+ test("no pkey2 falls back to the id — an older store keyed by rowid", () => {
94
+ expect(record_name({id: "181"})).toBe("181");
95
+ expect(record_name({id: "181", value: ""})).toBe("181");
96
+ });
97
+
98
+ test("an order arriving as a STRING still sorts as a number", () => {
99
+ expect(record_order({order: "10"})).toBe(10);
100
+ });
101
+
102
+ test("no order at all goes last", () => {
103
+ expect(record_order({})).toBe(DEFAULT_ORDER);
104
+ expect(record_order({order: "not a number"})).toBe(DEFAULT_ORDER);
105
+ });
106
+ });
107
+
108
+ describe("build_schema_model", () => {
109
+ const records = {
110
+ treedbs: [
111
+ {id: "treedb_b", schema_version: 3, c_schema_version: 3},
112
+ {id: "treedb_a", schema_version: 24, c_schema_version: 23},
113
+ ],
114
+ topics: [
115
+ topic("treedb_a", "users", 2, {pkey: "id", topic_version: 7}),
116
+ topic("treedb_a", "departments", 1, {pkey: "id", topic_version: 4}),
117
+ topic("treedb_b", "logs", 1),
118
+ ],
119
+ cols: [
120
+ col("treedb_a", "users", "name", 2),
121
+ col("treedb_a", "users", "id", 1),
122
+ col("treedb_a", "departments", "id", 1),
123
+ ],
124
+ };
125
+
126
+ test("treedbs carry their topics, topics their columns", () => {
127
+ const model = build_schema_model(records);
128
+ expect(model.treedbs.map(d => d.id)).toEqual(["treedb_a", "treedb_b"]);
129
+ const a = find_treedb(model, "treedb_a");
130
+ expect(a.topics.map(x => x.name)).toEqual(["departments", "users"]);
131
+ expect(find_topic(model, "treedb_a", "users").cols.map(c => c.name)).toEqual(["id", "name"]);
132
+ });
133
+
134
+ test("the schema keeps its DECLARED order, not the store's", () => {
135
+ /* `users` is order 2 and comes first in the record list; the model
136
+ puts `departments` (order 1) ahead of it. */
137
+ const model = build_schema_model(records);
138
+ expect(find_treedb(model, "treedb_a").topics[0].name).toBe("departments");
139
+ });
140
+
141
+ test("versions and topic metadata travel with the entry", () => {
142
+ const model = build_schema_model(records);
143
+ expect(find_treedb(model, "treedb_a").schema_version).toBe(24);
144
+ expect(find_treedb(model, "treedb_a").c_schema_version).toBe(23);
145
+ expect(find_topic(model, "treedb_a", "users").topic_version).toBe(7);
146
+ expect(find_topic(model, "treedb_a", "users").pkey).toBe("id");
147
+ });
148
+
149
+ test("grouping follows the FKEY, so a name carrying a dot still lands right", () => {
150
+ /* Splitting the qualified id on '.' would file this column under a
151
+ treedb called "my" and a topic called "db". */
152
+ const model = build_schema_model({
153
+ treedbs: [{id: "my.db"}],
154
+ topics: [{id: "my.db.users", value: "users", treedbs: ["treedbs^my.db^topics"]}],
155
+ cols: [{id: "my.db.users.name", value: "name",
156
+ topics: ["topics^my.db.users^cols"]}],
157
+ });
158
+ expect(find_col(model, "my.db", "users", "name")).toBeTruthy();
159
+ expect(model.orphan_cols).toEqual([]);
160
+ });
161
+
162
+ test("a record whose parent is gone is an ORPHAN, not a silent drop", () => {
163
+ const model = build_schema_model({
164
+ treedbs: [{id: "treedb_a"}],
165
+ topics: [topic("treedb_gone", "users", 1)],
166
+ cols: [col("treedb_a", "vanished", "name", 1)],
167
+ });
168
+ expect(model.orphan_topics.map(x => x.name)).toEqual(["users"]);
169
+ expect(model.orphan_cols.map(x => x.name)).toEqual(["name"]);
170
+ expect(find_treedb(model, "treedb_a").topics).toEqual([]);
171
+ });
172
+
173
+ test("equal orders break the tie by name, so two runs draw the same schema", () => {
174
+ const model = build_schema_model({
175
+ treedbs: [{id: "db"}],
176
+ topics: [topic("db", "zeta"), topic("db", "alpha")],
177
+ cols: [],
178
+ });
179
+ expect(find_treedb(model, "db").topics.map(x => x.name)).toEqual(["alpha", "zeta"]);
180
+ });
181
+
182
+ test("empty and absent inputs give an empty model, never a throw", () => {
183
+ expect(build_schema_model().treedbs).toEqual([]);
184
+ expect(build_schema_model({}).orphan_cols).toEqual([]);
185
+ expect(build_schema_model({treedbs: null, topics: 7}).treedbs).toEqual([]);
186
+ });
187
+
188
+ test("a record with no id is not a record", () => {
189
+ const model = build_schema_model({treedbs: [{}, {id: "db"}], topics: [], cols: []});
190
+ expect(model.treedbs.map(d => d.id)).toEqual(["db"]);
191
+ });
192
+ });
193
+
194
+ describe("writing helpers", () => {
195
+ test("fkey_ref composes what a new child must carry", () => {
196
+ expect(fkey_ref("topics", "db.users", "cols")).toBe("topics^db.users^cols");
197
+ });
198
+
199
+ test("a new sibling goes AFTER the last one, not into the 9999 crowd", () => {
200
+ expect(next_order([{order: 1}, {order: 3}, {order: 2}])).toBe(4);
201
+ });
202
+
203
+ test("siblings that all defaulted to 9999 still start a real numbering", () => {
204
+ expect(next_order([{order: DEFAULT_ORDER}, {order: DEFAULT_ORDER}])).toBe(1);
205
+ expect(next_order([])).toBe(1);
206
+ expect(next_order(null)).toBe(1);
207
+ });
208
+
209
+ test("moved_orders writes only the rows whose place actually changed", () => {
210
+ const list = [
211
+ {id: "a", order: 2, record: {order: 2}},
212
+ {id: "b", order: 1, record: {order: 1}},
213
+ {id: "c", order: 3, record: {order: 3}},
214
+ ];
215
+ /* `a` moved to the front: only a and b change, c keeps order 3. */
216
+ expect(moved_orders(list)).toEqual([{id: "a", order: 1}, {id: "b", order: 2}]);
217
+ });
218
+
219
+ test("a list already in order is no writes at all", () => {
220
+ const list = [
221
+ {id: "a", order: 1, record: {order: 1}},
222
+ {id: "b", order: 2, record: {order: 2}},
223
+ ];
224
+ expect(moved_orders(list)).toEqual([]);
225
+ });
226
+
227
+ test("a list of 9999s is renumbered from 1", () => {
228
+ const list = [
229
+ {id: "a", order: DEFAULT_ORDER, record: {}},
230
+ {id: "b", order: DEFAULT_ORDER, record: {}},
231
+ ];
232
+ expect(moved_orders(list)).toEqual([{id: "a", order: 1}, {id: "b", order: 2}]);
233
+ });
234
+ });
235
+
236
+ describe("is_empty_value — what the store answers when a blob was never set", () => {
237
+ test("nothing is nothing", () => {
238
+ expect(is_empty_value(null)).toBe(true);
239
+ expect(is_empty_value(undefined)).toBe(true);
240
+ expect(is_empty_value("")).toBe(true);
241
+ });
242
+
243
+ test("an EMPTY COLLECTION is nothing too — this is the whole point", () => {
244
+ /* A `blob` column that was never set comes back as `{}`, not as
245
+ nothing. Read as a value it puts `'hook': {}` into an exported
246
+ literal, which is a hook with no mapping. */
247
+ expect(is_empty_value({})).toBe(true);
248
+ expect(is_empty_value([])).toBe(true);
249
+ });
250
+
251
+ test("anything with something in it is something", () => {
252
+ expect(is_empty_value({a: 1})).toBe(false);
253
+ expect(is_empty_value(["a"])).toBe(false);
254
+ expect(is_empty_value("x")).toBe(false);
255
+ expect(is_empty_value(0)).toBe(false);
256
+ expect(is_empty_value(false)).toBe(false);
257
+ });
258
+ });
@@ -0,0 +1,381 @@
1
+ /***********************************************************************
2
+ * schema_to_c.js
3
+ *
4
+ * The stored schema written back as the C literal it came from.
5
+ *
6
+ * A treedb's schema has two homes. It is DECLARED in C, as a
7
+ * `treedb_schema_*.c` string literal compiled into the yuno, and
8
+ * it is STORED in `treedb_system_schema`, which is what the
9
+ * operator edits here. The stored one wins at run time, so an
10
+ * edit made in this console works — and lives nowhere the next
11
+ * build knows about. Rebuild that yuno on a machine with an empty
12
+ * store and the column is gone.
13
+ *
14
+ * `diff-schema` says the two halves have drifted. This is the
15
+ * other half of that answer: the edit, in the form that can be
16
+ * pasted into the source, so the declaration catches up with what
17
+ * the node is actually running.
18
+ *
19
+ * Two outputs, because they answer different questions:
20
+ * schema_to_json() — the schema as a value: to diff, to keep, to
21
+ * feed back in.
22
+ * schema_to_c() — the same value as the literal: single
23
+ * quotes, `\n\` continuations, padded, ready
24
+ * to paste.
25
+ *
26
+ * Copyright (c) 2026, ArtGins.
27
+ * All Rights Reserved.
28
+ ***********************************************************************/
29
+ import {
30
+ col_flags,
31
+ col_hook,
32
+ col_enum,
33
+ topic_pkey2s,
34
+ as_json,
35
+ is_empty_value,
36
+ } from "./schema_model.js";
37
+
38
+
39
+ /* Column where the `\n\` continuation sits in the .c literals. Not a
40
+ * law — the files in the tree do not agree on it (68 in the app
41
+ * schemas, 56 in the meta one) — so it is an option with the most
42
+ * common value as its default. */
43
+ const DEFAULT_PAD = 68;
44
+
45
+ /* The order a topic declares itself in. The store has no order of its
46
+ * own (a JSON object), so without this the literal comes out shuffled
47
+ * and every export diffs against the last one. */
48
+ const TOPIC_KEY_ORDER = [
49
+ "id", "pkey", "pkey2s", "system_flag", "tkey",
50
+ "topic_version", "system_topic"
51
+ ];
52
+
53
+ /* Same for a column, in the order the .c literals write it. */
54
+ const COL_KEY_ORDER = [
55
+ "header", "fillspace", "type", "enum", "flag", "hook",
56
+ "default", "placeholder", "description", "template", "properties"
57
+ ];
58
+
59
+ /* Written as quoted strings in the literals even though they are
60
+ * integer columns. Kept that way so an export can be pasted next to a
61
+ * hand-written schema without looking foreign. */
62
+ const VERSION_KEYS = ["schema_version", "topic_version"];
63
+
64
+ /* Storage-only: they say where the record lives, not what it declares.
65
+ * `_geometry` is here as a FIELD — where the graph editor put this
66
+ * record's box — and it is not the same thing as a column NAMED
67
+ * `_geometry`, which the .c literals do declare and which stays. */
68
+ const COL_SKIP = ["id", "value", "topics", "order", "_geometry", "__md_treedb__"];
69
+ const TOPIC_SKIP = ["id", "value", "treedbs", "cols", "order", "_geometry", "__md_treedb__"];
70
+
71
+ /* Fields whose stored form may be the JSON text of the value. */
72
+ const JSON_FIELDS = ["enum", "hook", "default", "template", "properties", "pkey2s"];
73
+
74
+
75
+ /* Escaping a value crosses TWO layers, and the second one is not
76
+ * JSON's.
77
+ *
78
+ * The literal is a C string, so a backslash is `\\` and a quote is
79
+ * `\"` in the source. Then the yuno calls helper_quote2doublequote()
80
+ * on the whole block before parsing it (c_controlcenter.c and every
81
+ * other schema loader): EVERY single quote becomes a double one. That
82
+ * is what makes `'id'` legal JSON — and it means a single quote inside
83
+ * a VALUE cannot survive as itself, whatever it is delimited with.
84
+ *
85
+ * So a quote is written as the JSON escape `\u0027`, which that pass
86
+ * cannot see. Spelled out as constants because counting backslashes
87
+ * inside a regexp replacement is how the wrong number gets committed. */
88
+ const BACKSLASH = "\\";
89
+ const DQUOTE = "\"";
90
+ const C_BACKSLASH = BACKSLASH + BACKSLASH; /* \\ -> \ */
91
+ const JSON_BACKSLASH = C_BACKSLASH + C_BACKSLASH; /* \\\\ -> \\ -> \ */
92
+ const JSON_DQUOTE = C_BACKSLASH + BACKSLASH + DQUOTE; /* \\\" -> \" -> " */
93
+ const JSON_QUOTE = C_BACKSLASH + "u0027"; /* \\u0027 -> \u0027 -> ' */
94
+
95
+
96
+ /***************************************************************
97
+ * Order the keys of a plain object: the known ones first, in
98
+ * the declared order, the rest after in a stable one.
99
+ ***************************************************************/
100
+ function ordered_entries(obj, order)
101
+ {
102
+ let entries = [];
103
+ let seen = {};
104
+
105
+ for(let key of order) {
106
+ if(Object.prototype.hasOwnProperty.call(obj, key)) {
107
+ entries.push([key, obj[key]]);
108
+ seen[key] = true;
109
+ }
110
+ }
111
+ let rest = Object.keys(obj).filter(k => !seen[k]);
112
+ rest.sort();
113
+ for(let key of rest) {
114
+ entries.push([key, obj[key]]);
115
+ }
116
+ return entries;
117
+ }
118
+
119
+ /***************************************************************
120
+ * schema_to_json(treedb) -> the schema as the .c literal holds it
121
+ *
122
+ * {id, schema_version, topics: [{id, pkey, ..., cols: {...}}]}
123
+ *
124
+ * `topics` is a LIST (the order is the schema's) and `cols` is a
125
+ * DICT keyed by column name (its order is the insertion order,
126
+ * which is the schema's too).
127
+ ***************************************************************/
128
+ function schema_to_json(treedb)
129
+ {
130
+ if(!treedb) {
131
+ return null;
132
+ }
133
+
134
+ /* A version is an INTEGER column of the store and a QUOTED STRING
135
+ * in every .c literal in the tree. This output is the literal's
136
+ * shape, so it is the literal's spelling too — otherwise an export
137
+ * re-read is not equal to the export. */
138
+ let version = (v) => {
139
+ return (v === null || v === undefined) ? v : String(v);
140
+ };
141
+
142
+ let out = {
143
+ id: treedb.id,
144
+ schema_version: version(treedb.schema_version),
145
+ topics: []
146
+ };
147
+
148
+ for(let topic of (treedb.topics || [])) {
149
+ let record = topic.record || {};
150
+ let jn_topic = {id: topic.name};
151
+
152
+ for(let [key, value] of Object.entries(record)) {
153
+ if(TOPIC_SKIP.indexOf(key) >= 0) {
154
+ continue;
155
+ }
156
+ if(is_empty_value(value)) {
157
+ continue;
158
+ }
159
+ let read = JSON_FIELDS.indexOf(key) >= 0 ? as_json(value) : value;
160
+ if(is_empty_value(read)) {
161
+ continue;
162
+ }
163
+ jn_topic[key] = read;
164
+ }
165
+ if(jn_topic.topic_version !== undefined) {
166
+ jn_topic.topic_version = version(jn_topic.topic_version);
167
+ }
168
+
169
+ let pkey2s = topic_pkey2s(record);
170
+ if(pkey2s.length === 1) {
171
+ jn_topic.pkey2s = pkey2s[0]; /* the literals write the single one bare */
172
+ } else if(pkey2s.length > 1) {
173
+ jn_topic.pkey2s = pkey2s;
174
+ } else {
175
+ delete jn_topic.pkey2s;
176
+ }
177
+
178
+ let cols = {};
179
+ for(let col of (topic.cols || [])) {
180
+ let col_record = col.record || {};
181
+ let jn_col = {};
182
+ for(let [key, value] of Object.entries(col_record)) {
183
+ if(COL_SKIP.indexOf(key) >= 0) {
184
+ continue;
185
+ }
186
+ if(is_empty_value(value)) {
187
+ continue;
188
+ }
189
+ let read = JSON_FIELDS.indexOf(key) >= 0 ? as_json(value) : value;
190
+ if(is_empty_value(read)) {
191
+ continue;
192
+ }
193
+ jn_col[key] = read;
194
+ }
195
+ let flags = col_flags(col_record);
196
+ if(flags.length > 0) {
197
+ jn_col.flag = flags;
198
+ } else {
199
+ delete jn_col.flag;
200
+ }
201
+ let hook = col_hook(col_record);
202
+ if(hook && !is_empty_value(hook)) {
203
+ jn_col.hook = hook;
204
+ } else {
205
+ delete jn_col.hook;
206
+ }
207
+ let e = col_enum(col_record);
208
+ if(e.length > 0) {
209
+ jn_col.enum = e;
210
+ }
211
+ let ordered_col = {};
212
+ for(let [key, value] of ordered_entries(jn_col, COL_KEY_ORDER)) {
213
+ ordered_col[key] = value;
214
+ }
215
+ cols[col.name] = ordered_col;
216
+ }
217
+ jn_topic.cols = cols;
218
+
219
+ /* Re-created in the declared order: an object built by
220
+ * assignment keeps its insertion order, and the loop above ran
221
+ * in whatever order the record's keys happened to be in. */
222
+ let ordered = {};
223
+ for(let [key, value] of ordered_entries(jn_topic, TOPIC_KEY_ORDER)) {
224
+ if(key === "cols") {
225
+ continue;
226
+ }
227
+ ordered[key] = value;
228
+ }
229
+ ordered.cols = jn_topic.cols;
230
+ out.topics.push(ordered);
231
+ }
232
+
233
+ return out;
234
+ }
235
+
236
+ /***************************************************************
237
+ * A scalar as the literal writes it. Single quotes, because the
238
+ * whole thing lives inside a C string delimited by double ones —
239
+ * a value that carries a single quote has to swap, and escape.
240
+ ***************************************************************/
241
+ function c_scalar(value, key)
242
+ {
243
+ if(typeof value === "boolean") {
244
+ return value ? "true" : "false";
245
+ }
246
+ if(typeof value === "number") {
247
+ if(VERSION_KEYS.indexOf(key) >= 0) {
248
+ return `'${value}'`;
249
+ }
250
+ return String(value);
251
+ }
252
+ return `'${c_text(String(value))}'`;
253
+ }
254
+
255
+ /***************************************************************
256
+ * The characters of a string that cannot be written as
257
+ * themselves.
258
+ ***************************************************************/
259
+ function c_text(text)
260
+ {
261
+ return text
262
+ .replace(/\\/g, JSON_BACKSLASH)
263
+ .replace(/"/g, JSON_DQUOTE)
264
+ .replace(/'/g, JSON_QUOTE);
265
+ }
266
+
267
+ /***************************************************************
268
+ * Render a value at `indent`, appending its lines to `lines`.
269
+ * Collections always go multiline: that is how every schema in
270
+ * the tree is written, and a one-line array in the middle of
271
+ * them reads as a different file.
272
+ ***************************************************************/
273
+ function c_value(lines, value, indent, key, trailing)
274
+ {
275
+ let pad = " ".repeat(indent);
276
+ let end = trailing ? "," : "";
277
+
278
+ if(Array.isArray(value)) {
279
+ lines.push(`${pad}[`);
280
+ for(let i = 0; i < value.length; i++) {
281
+ c_value(lines, value[i], indent + 4, "", i < value.length - 1);
282
+ }
283
+ lines.push(`${pad}]${end}`);
284
+ return;
285
+ }
286
+ if(value && typeof value === "object") {
287
+ lines.push(`${pad}{`);
288
+ let entries = Object.entries(value);
289
+ for(let i = 0; i < entries.length; i++) {
290
+ c_pair(lines, entries[i][0], entries[i][1], indent + 4, i < entries.length - 1);
291
+ }
292
+ lines.push(`${pad}}${end}`);
293
+ return;
294
+ }
295
+ lines.push(`${pad}${c_scalar(value, key)}${end}`);
296
+ }
297
+
298
+ /***************************************************************
299
+ * Render `'key': value` at `indent`.
300
+ ***************************************************************/
301
+ function c_pair(lines, key, value, indent, trailing)
302
+ {
303
+ let pad = " ".repeat(indent);
304
+ let end = trailing ? "," : "";
305
+
306
+ if(Array.isArray(value)) {
307
+ lines.push(`${pad}'${c_text(key)}': [`);
308
+ for(let i = 0; i < value.length; i++) {
309
+ c_value(lines, value[i], indent + 4, key, i < value.length - 1);
310
+ }
311
+ lines.push(`${pad}]${end}`);
312
+ return;
313
+ }
314
+ if(value && typeof value === "object") {
315
+ lines.push(`${pad}'${c_text(key)}': {`);
316
+ let entries = Object.entries(value);
317
+ for(let i = 0; i < entries.length; i++) {
318
+ c_pair(lines, entries[i][0], entries[i][1], indent + 4, i < entries.length - 1);
319
+ }
320
+ lines.push(`${pad}}${end}`);
321
+ return;
322
+ }
323
+ lines.push(`${pad}'${c_text(key)}': ${c_scalar(value, key)}${end}`);
324
+ }
325
+
326
+ /***************************************************************
327
+ * schema_to_c(treedb, options) -> the C source text
328
+ *
329
+ * options {var_name, pad}
330
+ * var_name the array's name; defaults to
331
+ * `treedb_schema_<treedb id without its prefix>`
332
+ * pad column of the `\n\` continuation
333
+ ***************************************************************/
334
+ function schema_to_c(treedb, options)
335
+ {
336
+ let opts = options || {};
337
+ let json = schema_to_json(treedb);
338
+
339
+ if(!json) {
340
+ return "";
341
+ }
342
+
343
+ let name = opts.var_name;
344
+ if(!name) {
345
+ let id = String(json.id || "schema");
346
+ name = id.indexOf("treedb_") === 0 ? `treedb_schema_${id.slice("treedb_".length)}`
347
+ : `treedb_schema_${id}`;
348
+ }
349
+ let pad = (typeof opts.pad === "number" && opts.pad > 0) ? opts.pad : DEFAULT_PAD;
350
+
351
+ let lines = [];
352
+ lines.push("{");
353
+ c_pair(lines, "id", json.id, 4, true);
354
+ c_pair(lines, "schema_version", json.schema_version, 4, true);
355
+ lines.push(" 'topics': [");
356
+ for(let i = 0; i < json.topics.length; i++) {
357
+ c_value(lines, json.topics[i], 8, "", i < json.topics.length - 1);
358
+ }
359
+ lines.push(" ]");
360
+ lines.push("}");
361
+
362
+ /* The continuation is what makes it a C literal: every line ends
363
+ * with `\n\`, padded to the same column so the block reads as a
364
+ * block. A line longer than the pad keeps one space — losing the
365
+ * separator would glue the `\n\` to the value. */
366
+ let body = lines.map((line) => {
367
+ let padding = line.length < pad ? " ".repeat(pad - line.length) : " ";
368
+ return `${line}${padding}\\n\\`;
369
+ });
370
+
371
+ return `static char ${name}[]= "\\\n${body.join("\n")}\n";\n`;
372
+ }
373
+
374
+
375
+ export {
376
+ DEFAULT_PAD,
377
+ TOPIC_KEY_ORDER,
378
+ COL_KEY_ORDER,
379
+ schema_to_json,
380
+ schema_to_c,
381
+ };