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