@jarenjs/josl 0.34.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/FORMAT.md +235 -0
- package/README.md +494 -0
- package/dist/types/cst.d.ts +78 -0
- package/dist/types/csv-machine.d.ts +104 -0
- package/dist/types/csv-stream.d.ts +102 -0
- package/dist/types/csv.d.ts +141 -0
- package/dist/types/errors.d.ts +79 -0
- package/dist/types/gbnf.d.ts +20 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/jsonx-scalar.d.ts +92 -0
- package/dist/types/jsonx-stream.d.ts +163 -0
- package/dist/types/jsonx.d.ts +31 -0
- package/dist/types/machine.d.ts +97 -0
- package/dist/types/parse.d.ts +24 -0
- package/dist/types/stream.d.ts +32 -0
- package/dist/types/stringify.d.ts +63 -0
- package/dist/types/util.d.ts +56 -0
- package/dist/types/values.d.ts +60 -0
- package/dist/types/write.d.ts +92 -0
- package/package.json +104 -0
- package/schemas/jaren-josl-data.schema.json +21 -0
- package/src/cst.js +256 -0
- package/src/csv-machine.js +908 -0
- package/src/csv-stream.js +196 -0
- package/src/csv.js +363 -0
- package/src/errors.js +103 -0
- package/src/gbnf.js +179 -0
- package/src/index.js +50 -0
- package/src/jsonx-scalar.js +326 -0
- package/src/jsonx-stream.js +806 -0
- package/src/jsonx.js +342 -0
- package/src/machine.js +1252 -0
- package/src/parse.js +37 -0
- package/src/stream.js +57 -0
- package/src/stringify.js +341 -0
- package/src/util.js +96 -0
- package/src/values.js +104 -0
- package/src/write.js +226 -0
package/src/write.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
//#region JOSL streaming writer
|
|
2
|
+
// The write-side mirror of the streaming reader: an event API that emits
|
|
3
|
+
// JOSL/TOML text chunk by chunk, so a document can be streamed onward
|
|
4
|
+
// (over HTTP, into an LLM prompt, to disk) while it is still being
|
|
5
|
+
// produced. Serialization is shared with stringify.js, so a value round-
|
|
6
|
+
// trips identically whether it went through `stringifyJosl` or a writer.
|
|
7
|
+
//
|
|
8
|
+
// The writer validates lightly - enough to prevent emitting a document
|
|
9
|
+
// the reader would reject (duplicate keys in a section, duplicate
|
|
10
|
+
// headers, root table/array mixing, mode downleveling) - but it does not
|
|
11
|
+
// re-simulate the full table machinery; it trusts the caller's ordering.
|
|
12
|
+
|
|
13
|
+
import { JoslStringifyError } from './errors.js';
|
|
14
|
+
import {
|
|
15
|
+
formatKeyPath,
|
|
16
|
+
formatValue,
|
|
17
|
+
formatSection,
|
|
18
|
+
isPlainTable,
|
|
19
|
+
} from './stringify.js';
|
|
20
|
+
|
|
21
|
+
class JoslStreamWriter {
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.options = {
|
|
24
|
+
mode: options.mode === 'toml' ? 'toml' : 'josl',
|
|
25
|
+
onNull: options.onNull,
|
|
26
|
+
onRegExp: options.onRegExp,
|
|
27
|
+
};
|
|
28
|
+
this.onChunk = options.onChunk ?? null;
|
|
29
|
+
this.chunks = [];
|
|
30
|
+
this.ended = false;
|
|
31
|
+
this.rootIsArray = false;
|
|
32
|
+
this.rootHasPairs = false;
|
|
33
|
+
this.sawSection = false;
|
|
34
|
+
this.sectionKeys = new Set(); // first-level keys of the current section
|
|
35
|
+
this.headers = new Set(); // emitted header paths (scoped per root item)
|
|
36
|
+
this.headerScope = 0; // root items reset header/key tracking
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
emit(chunk) {
|
|
40
|
+
this.chunks.push(chunk);
|
|
41
|
+
if (this.onChunk !== null)
|
|
42
|
+
this.onChunk(chunk);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
guard() {
|
|
46
|
+
if (this.ended)
|
|
47
|
+
throw new JoslStringifyError('cannot write after end()');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
blank() {
|
|
51
|
+
if (this.chunks.length !== 0)
|
|
52
|
+
this.emit('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
toPath(path) {
|
|
56
|
+
const keys = typeof path === 'string' ? [path] : path;
|
|
57
|
+
if (!Array.isArray(keys) || keys.length === 0
|
|
58
|
+
|| keys.some((k) => typeof k !== 'string'))
|
|
59
|
+
throw new JoslStringifyError('a path must be a key or a non-empty array of keys');
|
|
60
|
+
return keys;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
openSection(keys, wrap) {
|
|
64
|
+
this.guard();
|
|
65
|
+
if (this.rootIsArray && this.headerScope === 0)
|
|
66
|
+
throw new JoslStringifyError('expected rootItem() before sections in a root array');
|
|
67
|
+
const header = formatKeyPath(keys);
|
|
68
|
+
const id = `${this.headerScope}:${wrap}${header}`;
|
|
69
|
+
if (this.headers.has(id))
|
|
70
|
+
throw new JoslStringifyError(`section [${header}] was already emitted`);
|
|
71
|
+
if (wrap === '[')
|
|
72
|
+
this.headers.add(id);
|
|
73
|
+
this.sawSection = true;
|
|
74
|
+
this.blank();
|
|
75
|
+
this.emit(wrap === '[' ? `[${header}]\n` : `[[${header}]]\n`);
|
|
76
|
+
this.sectionKeys = new Set();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Emit a key-value pair into the current section.
|
|
81
|
+
* @param {string|string[]} key - A key, or key segments for dotted keys
|
|
82
|
+
* @param {*} value - The value
|
|
83
|
+
* @returns {this} The writer, for chaining
|
|
84
|
+
*/
|
|
85
|
+
pair(key, value) {
|
|
86
|
+
this.guard();
|
|
87
|
+
const keys = this.toPath(key);
|
|
88
|
+
if (this.rootIsArray && this.headerScope === 0)
|
|
89
|
+
throw new JoslStringifyError('expected rootItem() before pairs in a root array');
|
|
90
|
+
if (this.sectionKeys.has(keys[0]))
|
|
91
|
+
throw new JoslStringifyError(`duplicate key '${keys[0]}' in this section`);
|
|
92
|
+
this.sectionKeys.add(keys[0]);
|
|
93
|
+
if (!this.rootIsArray && !this.sawSection)
|
|
94
|
+
this.rootHasPairs = true;
|
|
95
|
+
this.emit(`${formatKeyPath(keys)} = ${formatValue(value, this.options, keys)}\n`);
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Open a `[table]` section.
|
|
101
|
+
* @param {string|string[]} path - Header path
|
|
102
|
+
* @returns {this} The writer, for chaining
|
|
103
|
+
*/
|
|
104
|
+
table(path) {
|
|
105
|
+
this.openSection(this.toPath(path), '[');
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Append a `[[table-array]]` element.
|
|
111
|
+
* @param {string|string[]} path - Header path
|
|
112
|
+
* @returns {this} The writer, for chaining
|
|
113
|
+
*/
|
|
114
|
+
tableArray(path) {
|
|
115
|
+
this.openSection(this.toPath(path), '[[');
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Start a `[[]]` root-array element (JOSL mode only). With a record
|
|
121
|
+
* argument the whole table body is emitted at once - the natural
|
|
122
|
+
* unit for streaming one record per completed result.
|
|
123
|
+
* @param {object} [record] - Optional complete record to emit
|
|
124
|
+
* @returns {this} The writer, for chaining
|
|
125
|
+
*/
|
|
126
|
+
rootItem(record = undefined) {
|
|
127
|
+
this.guard();
|
|
128
|
+
if (this.options.mode === 'toml')
|
|
129
|
+
throw new JoslStringifyError('root arrays ([[]]) are a JOSL extension');
|
|
130
|
+
if (this.rootHasPairs || (!this.rootIsArray && this.sawSection))
|
|
131
|
+
throw new JoslStringifyError('cannot mix a root table and a root array');
|
|
132
|
+
this.rootIsArray = true;
|
|
133
|
+
this.headerScope++;
|
|
134
|
+
this.sectionKeys = new Set();
|
|
135
|
+
this.blank();
|
|
136
|
+
this.emit('[[]]\n');
|
|
137
|
+
if (record !== undefined) {
|
|
138
|
+
if (!isPlainTable(record))
|
|
139
|
+
throw new JoslStringifyError('a root array element must be a table');
|
|
140
|
+
const body = formatSection(record, this.options);
|
|
141
|
+
if (body.length !== 0)
|
|
142
|
+
this.emit(body);
|
|
143
|
+
for (const k of Object.keys(record))
|
|
144
|
+
this.sectionKeys.add(k);
|
|
145
|
+
}
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Emit a comment line (multi-line text becomes multiple comments).
|
|
151
|
+
* @param {string} text - Comment text
|
|
152
|
+
* @returns {this} The writer, for chaining
|
|
153
|
+
*/
|
|
154
|
+
comment(text) {
|
|
155
|
+
this.guard();
|
|
156
|
+
for (const line of String(text).split('\n'))
|
|
157
|
+
this.emit(`# ${line}\n`);
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The document text emitted so far.
|
|
163
|
+
* @returns {string} Concatenated chunks
|
|
164
|
+
*/
|
|
165
|
+
text() {
|
|
166
|
+
return this.chunks.join('');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Finish the document.
|
|
171
|
+
* @returns {string} The complete document text
|
|
172
|
+
*/
|
|
173
|
+
end() {
|
|
174
|
+
this.ended = true;
|
|
175
|
+
return this.text();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Create a streaming JOSL/TOML writer - the write-side mirror of
|
|
181
|
+
* `createStreamReader`. Chunks are delivered through `onChunk` as they
|
|
182
|
+
* are produced and also accumulate for `text()` / `end()`.
|
|
183
|
+
* @param {object} [options] - Writer options
|
|
184
|
+
* @param {'josl'|'toml'} [options.mode] - 'toml' emits strict TOML 1.0
|
|
185
|
+
* @param {'error'|'omit'} [options.onNull] - See `stringifyJosl`
|
|
186
|
+
* @param {'error'|'string'} [options.onRegExp] - See `stringifyJosl`
|
|
187
|
+
* @param {(chunk: string) => void} [options.onChunk] - Chunk sink
|
|
188
|
+
* @returns {JoslStreamWriter} The writer
|
|
189
|
+
*/
|
|
190
|
+
export function createStreamWriter(options = undefined) {
|
|
191
|
+
return new JoslStreamWriter(options ?? {});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Serialize a value as an iterable of text chunks: one chunk per record
|
|
196
|
+
* for a root array, a single chunk for a table root. Useful for piping
|
|
197
|
+
* record streams onward without building the full string.
|
|
198
|
+
* @param {object|Array} value - Same roots as `stringifyJosl`
|
|
199
|
+
* @param {object} [options] - Writer options; see `stringifyJosl`
|
|
200
|
+
* @yields {string} Document chunks, in order
|
|
201
|
+
*/
|
|
202
|
+
export function* stringifyJoslChunks(value, options = {}) {
|
|
203
|
+
if (Array.isArray(value)) {
|
|
204
|
+
if (options.mode === 'toml')
|
|
205
|
+
throw new JoslStringifyError('a TOML root must be a table; root arrays are a JOSL extension');
|
|
206
|
+
for (let i = 0; i < value.length; ++i) {
|
|
207
|
+
if (!isPlainTable(value[i]))
|
|
208
|
+
throw new JoslStringifyError('root array elements must be tables', [i]);
|
|
209
|
+
const body = formatSection(value[i], options);
|
|
210
|
+
// keep byte-identical with stringifyJosl: a body that opens with a
|
|
211
|
+
// section header gets a blank line after the [[]] header
|
|
212
|
+
yield (i === 0 ? '' : '\n') + '[[]]\n'
|
|
213
|
+
+ (body.startsWith('[') ? '\n' : '') + body;
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (!isPlainTable(value))
|
|
218
|
+
throw new JoslStringifyError('a JOSL root must be a table or an array of tables');
|
|
219
|
+
const text = formatSection(value, options);
|
|
220
|
+
if (text.length !== 0)
|
|
221
|
+
yield text;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export { JoslStringifyError } from './errors.js';
|
|
225
|
+
|
|
226
|
+
//#endregion
|