@abraca/cli 1.8.0 → 2.3.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/dist/abracadabra-cli.cjs +2516 -555
- package/dist/abracadabra-cli.cjs.map +1 -1
- package/dist/abracadabra-cli.esm.js +2515 -556
- package/dist/abracadabra-cli.esm.js.map +1 -1
- package/package.json +10 -5
- package/src/commands/awareness.ts +11 -8
- package/src/commands/spaces.ts +19 -17
- package/src/commands/tree.ts +3 -3
- package/src/commands/wiki/connect.ts +69 -0
- package/src/commands/wiki/index.ts +471 -0
- package/src/commands/wiki/render.ts +91 -0
- package/src/commands/wiki/snapshot.ts +210 -0
- package/src/commands/wiki/types.ts +45 -0
- package/src/commands/wiki/wikipedia.ts +154 -0
- package/src/connection.ts +15 -50
- package/src/crypto.ts +5 -4
- package/src/index.ts +10 -1
package/dist/abracadabra-cli.cjs
CHANGED
|
@@ -37,6 +37,10 @@ node_fs = __toESM(node_fs);
|
|
|
37
37
|
let node_os = require("node:os");
|
|
38
38
|
let node_path = require("node:path");
|
|
39
39
|
node_path = __toESM(node_path);
|
|
40
|
+
let wtf_wikipedia = require("wtf_wikipedia");
|
|
41
|
+
wtf_wikipedia = __toESM(wtf_wikipedia);
|
|
42
|
+
let wtf_plugin_api = require("wtf-plugin-api");
|
|
43
|
+
wtf_plugin_api = __toESM(wtf_plugin_api);
|
|
40
44
|
|
|
41
45
|
//#region packages/cli/src/parser.ts
|
|
42
46
|
/**
|
|
@@ -83,97 +87,205 @@ function parseArgs(argv) {
|
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
//#endregion
|
|
86
|
-
//#region
|
|
87
|
-
/**
|
|
90
|
+
//#region node_modules/@noble/hashes/utils.js
|
|
91
|
+
/**
|
|
92
|
+
* Checks if something is Uint8Array. Be careful: nodejs Buffer will return true.
|
|
93
|
+
* @param a - value to test
|
|
94
|
+
* @returns `true` when the value is a Uint8Array-compatible view.
|
|
95
|
+
* @example
|
|
96
|
+
* Check whether a value is a Uint8Array-compatible view.
|
|
97
|
+
* ```ts
|
|
98
|
+
* isBytes(new Uint8Array([1, 2, 3]));
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
88
101
|
function isBytes(a) {
|
|
89
|
-
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array";
|
|
102
|
+
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
90
103
|
}
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Asserts something is Uint8Array.
|
|
106
|
+
* @param value - value to validate
|
|
107
|
+
* @param length - optional exact length constraint
|
|
108
|
+
* @param title - label included in thrown errors
|
|
109
|
+
* @returns The validated byte array.
|
|
110
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
111
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
112
|
+
* @example
|
|
113
|
+
* Validate that a value is a byte array.
|
|
114
|
+
* ```ts
|
|
115
|
+
* abytes(new Uint8Array([1, 2, 3]));
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
function abytes(value, length, title = "") {
|
|
119
|
+
const bytes = isBytes(value);
|
|
120
|
+
const len = value?.length;
|
|
121
|
+
const needsLen = length !== void 0;
|
|
122
|
+
if (!bytes || needsLen && len !== length) {
|
|
123
|
+
const prefix = title && `"${title}" `;
|
|
124
|
+
const ofLen = needsLen ? ` of length ${length}` : "";
|
|
125
|
+
const got = bytes ? `length=${len}` : `type=${typeof value}`;
|
|
126
|
+
const message = prefix + "expected Uint8Array" + ofLen + ", got " + got;
|
|
127
|
+
if (!bytes) throw new TypeError(message);
|
|
128
|
+
throw new RangeError(message);
|
|
129
|
+
}
|
|
130
|
+
return value;
|
|
95
131
|
}
|
|
96
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Asserts a hash instance has not been destroyed or finished.
|
|
134
|
+
* @param instance - hash instance to validate
|
|
135
|
+
* @param checkFinished - whether to reject finalized instances
|
|
136
|
+
* @throws If the hash instance has already been destroyed or finalized. {@link Error}
|
|
137
|
+
* @example
|
|
138
|
+
* Validate that a hash instance is still usable.
|
|
139
|
+
* ```ts
|
|
140
|
+
* import { aexists } from '@noble/hashes/utils.js';
|
|
141
|
+
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
142
|
+
* const hash = sha256.create();
|
|
143
|
+
* aexists(hash);
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
97
146
|
function aexists(instance, checkFinished = true) {
|
|
98
147
|
if (instance.destroyed) throw new Error("Hash instance has been destroyed");
|
|
99
148
|
if (checkFinished && instance.finished) throw new Error("Hash#digest() has already been called");
|
|
100
149
|
}
|
|
101
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Asserts output is a sufficiently-sized byte array.
|
|
152
|
+
* @param out - destination buffer
|
|
153
|
+
* @param instance - hash instance providing output length
|
|
154
|
+
* Oversized buffers are allowed; downstream code only promises to fill the first `outputLen` bytes.
|
|
155
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
156
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
157
|
+
* @example
|
|
158
|
+
* Validate a caller-provided digest buffer.
|
|
159
|
+
* ```ts
|
|
160
|
+
* import { aoutput } from '@noble/hashes/utils.js';
|
|
161
|
+
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
162
|
+
* const hash = sha256.create();
|
|
163
|
+
* aoutput(new Uint8Array(hash.outputLen), hash);
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
102
166
|
function aoutput(out, instance) {
|
|
103
|
-
abytes(out);
|
|
167
|
+
abytes(out, void 0, "digestInto() output");
|
|
104
168
|
const min = instance.outputLen;
|
|
105
|
-
if (out.length < min) throw new
|
|
169
|
+
if (out.length < min) throw new RangeError("\"digestInto() output\" expected to be of length >=" + min);
|
|
106
170
|
}
|
|
107
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* Zeroizes typed arrays in place. Warning: JS provides no guarantees.
|
|
173
|
+
* @param arrays - arrays to overwrite with zeros
|
|
174
|
+
* @example
|
|
175
|
+
* Zeroize sensitive buffers in place.
|
|
176
|
+
* ```ts
|
|
177
|
+
* clean(new Uint8Array([1, 2, 3]));
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
108
180
|
function clean(...arrays) {
|
|
109
181
|
for (let i = 0; i < arrays.length; i++) arrays[i].fill(0);
|
|
110
182
|
}
|
|
111
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* Creates a DataView for byte-level manipulation.
|
|
185
|
+
* @param arr - source typed array
|
|
186
|
+
* @returns DataView over the same buffer region.
|
|
187
|
+
* @example
|
|
188
|
+
* Create a DataView over an existing buffer.
|
|
189
|
+
* ```ts
|
|
190
|
+
* createView(new Uint8Array(4));
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
112
193
|
function createView(arr) {
|
|
113
194
|
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
114
195
|
}
|
|
115
|
-
/**
|
|
196
|
+
/** Whether the current platform is little-endian. */
|
|
116
197
|
const isLE = new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68;
|
|
117
198
|
const hasHexBuiltin = typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function";
|
|
118
199
|
/**
|
|
119
|
-
*
|
|
120
|
-
* @
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
200
|
+
* Creates a callable hash function from a stateful class constructor.
|
|
201
|
+
* @param hashCons - hash constructor or factory
|
|
202
|
+
* @param info - optional metadata such as DER OID
|
|
203
|
+
* @returns Frozen callable hash wrapper with `.create()`.
|
|
204
|
+
* Wrapper construction eagerly calls `hashCons(undefined)` once to read
|
|
205
|
+
* `outputLen` / `blockLen`, so constructor side effects happen at module
|
|
206
|
+
* init time.
|
|
207
|
+
* @example
|
|
208
|
+
* Wrap a stateful hash constructor into a callable helper.
|
|
209
|
+
* ```ts
|
|
210
|
+
* import { createHasher } from '@noble/hashes/utils.js';
|
|
211
|
+
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
212
|
+
* const wrapped = createHasher(sha256.create, { oid: sha256.oid });
|
|
213
|
+
* wrapped(new Uint8Array([1]));
|
|
214
|
+
* ```
|
|
130
215
|
*/
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return data;
|
|
135
|
-
}
|
|
136
|
-
/** For runtime check if class implements interface */
|
|
137
|
-
var Hash = class {};
|
|
138
|
-
/** Wraps hash function, creating an interface on top of it */
|
|
139
|
-
function createHasher(hashCons) {
|
|
140
|
-
const hashC = (msg) => hashCons().update(toBytes(msg)).digest();
|
|
141
|
-
const tmp = hashCons();
|
|
216
|
+
function createHasher(hashCons, info = {}) {
|
|
217
|
+
const hashC = (msg, opts) => hashCons(opts).update(msg).digest();
|
|
218
|
+
const tmp = hashCons(void 0);
|
|
142
219
|
hashC.outputLen = tmp.outputLen;
|
|
143
220
|
hashC.blockLen = tmp.blockLen;
|
|
144
|
-
hashC.
|
|
145
|
-
|
|
221
|
+
hashC.canXOF = tmp.canXOF;
|
|
222
|
+
hashC.create = (opts) => hashCons(opts);
|
|
223
|
+
Object.assign(hashC, info);
|
|
224
|
+
return Object.freeze(hashC);
|
|
146
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Creates OID metadata for NIST hashes with prefix `06 09 60 86 48 01 65 03 04 02`.
|
|
228
|
+
* @param suffix - final OID byte for the selected hash.
|
|
229
|
+
* The helper accepts any byte even though only the documented NIST hash
|
|
230
|
+
* suffixes are meaningful downstream.
|
|
231
|
+
* @returns Object containing the DER-encoded OID.
|
|
232
|
+
* @example
|
|
233
|
+
* Build OID metadata for a NIST hash.
|
|
234
|
+
* ```ts
|
|
235
|
+
* oidNist(0x01);
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
const oidNist = (suffix) => ({ oid: Uint8Array.from([
|
|
239
|
+
6,
|
|
240
|
+
9,
|
|
241
|
+
96,
|
|
242
|
+
134,
|
|
243
|
+
72,
|
|
244
|
+
1,
|
|
245
|
+
101,
|
|
246
|
+
3,
|
|
247
|
+
4,
|
|
248
|
+
2,
|
|
249
|
+
suffix
|
|
250
|
+
]) });
|
|
147
251
|
|
|
148
252
|
//#endregion
|
|
149
|
-
//#region
|
|
253
|
+
//#region node_modules/@noble/hashes/_md.js
|
|
150
254
|
/**
|
|
151
255
|
* Internal Merkle-Damgard hash utils.
|
|
152
256
|
* @module
|
|
153
257
|
*/
|
|
154
|
-
/** Polyfill for Safari 14. https://caniuse.com/mdn-javascript_builtins_dataview_setbiguint64 */
|
|
155
|
-
function setBigUint64(view, byteOffset, value, isLE) {
|
|
156
|
-
if (typeof view.setBigUint64 === "function") return view.setBigUint64(byteOffset, value, isLE);
|
|
157
|
-
const _32n = BigInt(32);
|
|
158
|
-
const _u32_max = BigInt(4294967295);
|
|
159
|
-
const wh = Number(value >> _32n & _u32_max);
|
|
160
|
-
const wl = Number(value & _u32_max);
|
|
161
|
-
const h = isLE ? 4 : 0;
|
|
162
|
-
const l = isLE ? 0 : 4;
|
|
163
|
-
view.setUint32(byteOffset + h, wh, isLE);
|
|
164
|
-
view.setUint32(byteOffset + l, wl, isLE);
|
|
165
|
-
}
|
|
166
258
|
/**
|
|
167
259
|
* Merkle-Damgard hash construction base class.
|
|
168
260
|
* Could be used to create MD5, RIPEMD, SHA1, SHA2.
|
|
261
|
+
* Accepts only byte-aligned `Uint8Array` input, even when the underlying spec describes bit
|
|
262
|
+
* strings with partial-byte tails.
|
|
263
|
+
* @param blockLen - internal block size in bytes
|
|
264
|
+
* @param outputLen - digest size in bytes
|
|
265
|
+
* @param padOffset - trailing length field size in bytes
|
|
266
|
+
* @param isLE - whether length and state words are encoded in little-endian
|
|
267
|
+
* @example
|
|
268
|
+
* Use a concrete subclass to get the shared Merkle-Damgard update/digest flow.
|
|
269
|
+
* ```ts
|
|
270
|
+
* import { _SHA1 } from '@noble/hashes/legacy.js';
|
|
271
|
+
* const hash = new _SHA1();
|
|
272
|
+
* hash.update(new Uint8Array([97, 98, 99]));
|
|
273
|
+
* hash.digest();
|
|
274
|
+
* ```
|
|
169
275
|
*/
|
|
170
|
-
var HashMD = class
|
|
276
|
+
var HashMD = class {
|
|
277
|
+
blockLen;
|
|
278
|
+
outputLen;
|
|
279
|
+
canXOF = false;
|
|
280
|
+
padOffset;
|
|
281
|
+
isLE;
|
|
282
|
+
buffer;
|
|
283
|
+
view;
|
|
284
|
+
finished = false;
|
|
285
|
+
length = 0;
|
|
286
|
+
pos = 0;
|
|
287
|
+
destroyed = false;
|
|
171
288
|
constructor(blockLen, outputLen, padOffset, isLE) {
|
|
172
|
-
super();
|
|
173
|
-
this.finished = false;
|
|
174
|
-
this.length = 0;
|
|
175
|
-
this.pos = 0;
|
|
176
|
-
this.destroyed = false;
|
|
177
289
|
this.blockLen = blockLen;
|
|
178
290
|
this.outputLen = outputLen;
|
|
179
291
|
this.padOffset = padOffset;
|
|
@@ -183,7 +295,6 @@ var HashMD = class extends Hash {
|
|
|
183
295
|
}
|
|
184
296
|
update(data) {
|
|
185
297
|
aexists(this);
|
|
186
|
-
data = toBytes(data);
|
|
187
298
|
abytes(data);
|
|
188
299
|
const { view, buffer, blockLen } = this;
|
|
189
300
|
const len = data.length;
|
|
@@ -219,11 +330,11 @@ var HashMD = class extends Hash {
|
|
|
219
330
|
pos = 0;
|
|
220
331
|
}
|
|
221
332
|
for (let i = pos; i < blockLen; i++) buffer[i] = 0;
|
|
222
|
-
setBigUint64(
|
|
333
|
+
view.setBigUint64(blockLen - 8, BigInt(this.length * 8), isLE);
|
|
223
334
|
this.process(view, 0);
|
|
224
335
|
const oview = createView(out);
|
|
225
336
|
const len = this.outputLen;
|
|
226
|
-
if (len % 4) throw new Error("_sha2: outputLen
|
|
337
|
+
if (len % 4) throw new Error("_sha2: outputLen must be aligned to 32bit");
|
|
227
338
|
const outLen = len / 4;
|
|
228
339
|
const state = this.get();
|
|
229
340
|
if (outLen > state.length) throw new Error("_sha2: outputLen bigger than state");
|
|
@@ -237,7 +348,7 @@ var HashMD = class extends Hash {
|
|
|
237
348
|
return res;
|
|
238
349
|
}
|
|
239
350
|
_cloneInto(to) {
|
|
240
|
-
to
|
|
351
|
+
to ||= new this.constructor();
|
|
241
352
|
to.set(...this.get());
|
|
242
353
|
const { blockLen, buffer, length, finished, destroyed, pos } = this;
|
|
243
354
|
to.destroyed = destroyed;
|
|
@@ -251,7 +362,10 @@ var HashMD = class extends Hash {
|
|
|
251
362
|
return this._cloneInto();
|
|
252
363
|
}
|
|
253
364
|
};
|
|
254
|
-
/** Initial SHA512 state
|
|
365
|
+
/** Initial SHA512 state from RFC 6234 §6.3: eight RFC 64-bit `H(0)` words stored as sixteen
|
|
366
|
+
* big-endian 32-bit halves. Derived from the fractional parts of the square roots of the first
|
|
367
|
+
* eight prime numbers. Exported as a shared table; callers must treat it as read-only because
|
|
368
|
+
* constructors copy halves from it by index. */
|
|
255
369
|
const SHA512_IV = /* @__PURE__ */ Uint32Array.from([
|
|
256
370
|
1779033703,
|
|
257
371
|
4089235720,
|
|
@@ -272,12 +386,7 @@ const SHA512_IV = /* @__PURE__ */ Uint32Array.from([
|
|
|
272
386
|
]);
|
|
273
387
|
|
|
274
388
|
//#endregion
|
|
275
|
-
//#region
|
|
276
|
-
/**
|
|
277
|
-
* Internal helpers for u64. BigUint64Array is too slow as per 2025, so we implement it using Uint32Array.
|
|
278
|
-
* @todo re-check https://issues.chromium.org/issues/42212588
|
|
279
|
-
* @module
|
|
280
|
-
*/
|
|
389
|
+
//#region node_modules/@noble/hashes/_u64.js
|
|
281
390
|
const U32_MASK64 = /* @__PURE__ */ BigInt(2 ** 32 - 1);
|
|
282
391
|
const _32n = /* @__PURE__ */ BigInt(32);
|
|
283
392
|
function fromBig(n, le = false) {
|
|
@@ -321,12 +430,12 @@ const add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl
|
|
|
321
430
|
const add5H = (low, Ah, Bh, Ch, Dh, Eh) => Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0;
|
|
322
431
|
|
|
323
432
|
//#endregion
|
|
324
|
-
//#region
|
|
433
|
+
//#region node_modules/@noble/hashes/sha2.js
|
|
325
434
|
/**
|
|
326
435
|
* SHA2 hash function. A.k.a. sha256, sha384, sha512, sha512_224, sha512_256.
|
|
327
436
|
* SHA256 is the fastest hash implementable in JS, even faster than Blake3.
|
|
328
|
-
* Check out
|
|
329
|
-
*
|
|
437
|
+
* Check out {@link https://www.rfc-editor.org/rfc/rfc4634 | RFC 4634} and
|
|
438
|
+
* {@link https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf | FIPS 180-4}.
|
|
330
439
|
* @module
|
|
331
440
|
*/
|
|
332
441
|
const K512 = split([
|
|
@@ -415,25 +524,10 @@ const SHA512_Kh = K512[0];
|
|
|
415
524
|
const SHA512_Kl = K512[1];
|
|
416
525
|
const SHA512_W_H = /* @__PURE__ */ new Uint32Array(80);
|
|
417
526
|
const SHA512_W_L = /* @__PURE__ */ new Uint32Array(80);
|
|
418
|
-
|
|
419
|
-
|
|
527
|
+
/** Internal SHA-384 / SHA-512 compression engine from RFC 6234 §6.4. */
|
|
528
|
+
var SHA2_64B = class extends HashMD {
|
|
529
|
+
constructor(outputLen) {
|
|
420
530
|
super(128, outputLen, 16, false);
|
|
421
|
-
this.Ah = SHA512_IV[0] | 0;
|
|
422
|
-
this.Al = SHA512_IV[1] | 0;
|
|
423
|
-
this.Bh = SHA512_IV[2] | 0;
|
|
424
|
-
this.Bl = SHA512_IV[3] | 0;
|
|
425
|
-
this.Ch = SHA512_IV[4] | 0;
|
|
426
|
-
this.Cl = SHA512_IV[5] | 0;
|
|
427
|
-
this.Dh = SHA512_IV[6] | 0;
|
|
428
|
-
this.Dl = SHA512_IV[7] | 0;
|
|
429
|
-
this.Eh = SHA512_IV[8] | 0;
|
|
430
|
-
this.El = SHA512_IV[9] | 0;
|
|
431
|
-
this.Fh = SHA512_IV[10] | 0;
|
|
432
|
-
this.Fl = SHA512_IV[11] | 0;
|
|
433
|
-
this.Gh = SHA512_IV[12] | 0;
|
|
434
|
-
this.Gl = SHA512_IV[13] | 0;
|
|
435
|
-
this.Hh = SHA512_IV[14] | 0;
|
|
436
|
-
this.Hl = SHA512_IV[15] | 0;
|
|
437
531
|
}
|
|
438
532
|
get() {
|
|
439
533
|
const { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
|
|
@@ -536,12 +630,44 @@ var SHA512 = class extends HashMD {
|
|
|
536
630
|
clean(SHA512_W_H, SHA512_W_L);
|
|
537
631
|
}
|
|
538
632
|
destroy() {
|
|
633
|
+
this.destroyed = true;
|
|
539
634
|
clean(this.buffer);
|
|
540
635
|
this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
541
636
|
}
|
|
542
637
|
};
|
|
543
|
-
/**
|
|
544
|
-
|
|
638
|
+
/** Internal SHA-512 hash class grounded in RFC 6234 §6.3 and §6.4. */
|
|
639
|
+
var _SHA512 = class extends SHA2_64B {
|
|
640
|
+
Ah = SHA512_IV[0] | 0;
|
|
641
|
+
Al = SHA512_IV[1] | 0;
|
|
642
|
+
Bh = SHA512_IV[2] | 0;
|
|
643
|
+
Bl = SHA512_IV[3] | 0;
|
|
644
|
+
Ch = SHA512_IV[4] | 0;
|
|
645
|
+
Cl = SHA512_IV[5] | 0;
|
|
646
|
+
Dh = SHA512_IV[6] | 0;
|
|
647
|
+
Dl = SHA512_IV[7] | 0;
|
|
648
|
+
Eh = SHA512_IV[8] | 0;
|
|
649
|
+
El = SHA512_IV[9] | 0;
|
|
650
|
+
Fh = SHA512_IV[10] | 0;
|
|
651
|
+
Fl = SHA512_IV[11] | 0;
|
|
652
|
+
Gh = SHA512_IV[12] | 0;
|
|
653
|
+
Gl = SHA512_IV[13] | 0;
|
|
654
|
+
Hh = SHA512_IV[14] | 0;
|
|
655
|
+
Hl = SHA512_IV[15] | 0;
|
|
656
|
+
constructor() {
|
|
657
|
+
super(64);
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
/**
|
|
661
|
+
* SHA2-512 hash function from RFC 4634.
|
|
662
|
+
* @param msg - message bytes to hash
|
|
663
|
+
* @returns Digest bytes.
|
|
664
|
+
* @example
|
|
665
|
+
* Hash a message with SHA2-512.
|
|
666
|
+
* ```ts
|
|
667
|
+
* sha512(new Uint8Array([97, 98, 99]));
|
|
668
|
+
* ```
|
|
669
|
+
*/
|
|
670
|
+
const sha512 = /* @__PURE__ */ createHasher(() => new _SHA512(), /* @__PURE__ */ oidNist(3));
|
|
545
671
|
|
|
546
672
|
//#endregion
|
|
547
673
|
//#region packages/cli/src/crypto.ts
|
|
@@ -549,7 +675,8 @@ const sha512 = /* @__PURE__ */ createHasher(() => new SHA512());
|
|
|
549
675
|
* Ed25519 key generation, persistence, and challenge signing for CLI auth.
|
|
550
676
|
* Mirrors @abraca/mcp/src/crypto.ts — standalone to avoid MCP SDK dependency.
|
|
551
677
|
*/
|
|
552
|
-
_noble_ed25519.
|
|
678
|
+
_noble_ed25519.hashes.sha512 = sha512;
|
|
679
|
+
_noble_ed25519.hashes.sha512Async = (m) => Promise.resolve(sha512(m));
|
|
553
680
|
const DEFAULT_KEY_PATH = (0, node_path.join)((0, node_os.homedir)(), ".abracadabra", "cli.key");
|
|
554
681
|
function toBase64url(bytes) {
|
|
555
682
|
return Buffer.from(bytes).toString("base64url");
|
|
@@ -572,7 +699,7 @@ async function loadOrCreateKeypair(keyPath) {
|
|
|
572
699
|
publicKeyB64: toBase64url(_noble_ed25519.getPublicKey(privateKey))
|
|
573
700
|
};
|
|
574
701
|
}
|
|
575
|
-
const privateKey = _noble_ed25519.utils.
|
|
702
|
+
const privateKey = _noble_ed25519.utils.randomSecretKey();
|
|
576
703
|
const publicKey = _noble_ed25519.getPublicKey(privateKey);
|
|
577
704
|
const dir = (0, node_path.dirname)(path);
|
|
578
705
|
if (!(0, node_fs.existsSync)(dir)) await (0, node_fs_promises.mkdir)(dir, {
|
|
@@ -614,24 +741,6 @@ function waitForSync(provider, timeoutMs = 15e3) {
|
|
|
614
741
|
provider.on("synced", handler);
|
|
615
742
|
});
|
|
616
743
|
}
|
|
617
|
-
/** Map a DocumentMeta to SpaceMeta shape for display compatibility. */
|
|
618
|
-
function docToSpaceMeta(doc) {
|
|
619
|
-
const publicAccess = doc.public_access;
|
|
620
|
-
let visibility = "private";
|
|
621
|
-
if (publicAccess && publicAccess !== "none") visibility = "public";
|
|
622
|
-
return {
|
|
623
|
-
id: doc.id,
|
|
624
|
-
doc_id: doc.id,
|
|
625
|
-
name: doc.label ?? doc.id,
|
|
626
|
-
description: doc.description ?? null,
|
|
627
|
-
visibility,
|
|
628
|
-
is_hub: doc.is_hub ?? false,
|
|
629
|
-
owner_id: doc.owner_id ?? null,
|
|
630
|
-
created_at: 0,
|
|
631
|
-
updated_at: doc.updated_at ?? 0,
|
|
632
|
-
public_access: publicAccess ?? null
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
744
|
var CLIConnection = class {
|
|
636
745
|
config;
|
|
637
746
|
client;
|
|
@@ -701,29 +810,12 @@ var CLIConnection = class {
|
|
|
701
810
|
}
|
|
702
811
|
this.log(`Authenticated as ${this.displayName} (${keypair.publicKeyB64.slice(0, 12)}...)`);
|
|
703
812
|
this._serverInfo = await this.client.serverInfo();
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
initialDocId = hub.id;
|
|
711
|
-
this.log(`Hub document: ${hub.label ?? hub.id} (${hub.id})`);
|
|
712
|
-
} else if (roots.length > 0) {
|
|
713
|
-
initialDocId = roots[0].id;
|
|
714
|
-
this.log(`No hub, using first root doc: ${roots[0].label ?? roots[0].id}`);
|
|
715
|
-
}
|
|
716
|
-
} catch {
|
|
717
|
-
try {
|
|
718
|
-
this._spaces = await this.client.listSpaces();
|
|
719
|
-
const hub = this._spaces.find((s) => s.is_hub);
|
|
720
|
-
if (hub) initialDocId = hub.doc_id;
|
|
721
|
-
else if (this._spaces.length > 0) initialDocId = this._spaces[0].doc_id;
|
|
722
|
-
} catch {
|
|
723
|
-
this.log("Neither /docs?root=true nor /spaces available, using index_doc_id");
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
if (!initialDocId) throw new Error("No entry point found: server has neither spaces nor index_doc_id configured.");
|
|
813
|
+
const roots = await this.client.listChildren();
|
|
814
|
+
this._spaces = roots.filter((d) => d.kind === _abraca_dabra.Kind.Space);
|
|
815
|
+
const first = this._spaces[0] ?? roots[0];
|
|
816
|
+
const initialDocId = first?.id ?? null;
|
|
817
|
+
if (first) this.log(`Entry document: ${first.label ?? first.id} (${first.id})`);
|
|
818
|
+
if (!initialDocId) throw new Error("No entry point found: server has no top-level documents. Create a Space first.");
|
|
727
819
|
this._rootDocId = initialDocId;
|
|
728
820
|
const doc = new yjs.Doc({ guid: initialDocId });
|
|
729
821
|
const provider = new _abraca_dabra.AbracadabraProvider({
|
|
@@ -1098,7 +1190,7 @@ registerCommand({
|
|
|
1098
1190
|
`URL: ${conn.config.url}`,
|
|
1099
1191
|
`Version: ${si.version ?? "—"}`,
|
|
1100
1192
|
`Protocol: ${si.protocol_version ?? "—"}`,
|
|
1101
|
-
`
|
|
1193
|
+
`Active Doc: ${conn.rootDocId ?? "—"}`,
|
|
1102
1194
|
`Auth: ${si.auth_methods?.join(", ") ?? "—"}`,
|
|
1103
1195
|
`Registration:${si.registration_allowed ? " open" : " closed"}${si.invite_only ? " (invite only)" : ""}`,
|
|
1104
1196
|
`Documents: ${docCount}`,
|
|
@@ -1108,7 +1200,7 @@ registerCommand({
|
|
|
1108
1200
|
});
|
|
1109
1201
|
registerCommand({
|
|
1110
1202
|
name: "spaces",
|
|
1111
|
-
description: "List available spaces
|
|
1203
|
+
description: "List available spaces (top-level documents).",
|
|
1112
1204
|
usage: "spaces [--format=json|tsv]",
|
|
1113
1205
|
async run(conn, args) {
|
|
1114
1206
|
if (!conn) return "Not connected";
|
|
@@ -1118,19 +1210,20 @@ registerCommand({
|
|
|
1118
1210
|
const active = conn.rootDocId;
|
|
1119
1211
|
if (format === "json") return printJson(spaces.map((s) => ({
|
|
1120
1212
|
...s,
|
|
1121
|
-
active: s.
|
|
1213
|
+
active: s.id === active
|
|
1122
1214
|
})));
|
|
1123
|
-
return printTable(spaces.map((s) =>
|
|
1124
|
-
s.
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1215
|
+
return printTable(spaces.map((s) => {
|
|
1216
|
+
const visibility = s.public_access === "observer" ? "public" : "private";
|
|
1217
|
+
return [
|
|
1218
|
+
s.id === active ? "▸" : " ",
|
|
1219
|
+
s.id.slice(0, 8) + "…",
|
|
1220
|
+
s.label ?? s.id,
|
|
1221
|
+
visibility
|
|
1222
|
+
];
|
|
1223
|
+
}), [
|
|
1130
1224
|
"",
|
|
1131
1225
|
"ID",
|
|
1132
1226
|
"NAME",
|
|
1133
|
-
"HUB",
|
|
1134
1227
|
"VISIBILITY"
|
|
1135
1228
|
]);
|
|
1136
1229
|
}
|
|
@@ -1148,12 +1241,12 @@ registerCommand({
|
|
|
1148
1241
|
if (targetId) docId = targetId;
|
|
1149
1242
|
else if (targetName) {
|
|
1150
1243
|
const lower = targetName.toLowerCase();
|
|
1151
|
-
const space = conn.spaces.find((s) => s.
|
|
1152
|
-
if (space) docId = space.
|
|
1244
|
+
const space = conn.spaces.find((s) => (s.label ?? s.id).toLowerCase() === lower || s.id === targetName);
|
|
1245
|
+
if (space) docId = space.id;
|
|
1153
1246
|
}
|
|
1154
1247
|
if (!docId) return "Space not found. Use \"abracadabra spaces\" to list available spaces.";
|
|
1155
1248
|
await conn.switchSpace(docId);
|
|
1156
|
-
return `Switched to space "${conn.spaces.find((s) => s.
|
|
1249
|
+
return `Switched to space "${conn.spaces.find((s) => s.id === docId)?.label ?? docId}"`;
|
|
1157
1250
|
}
|
|
1158
1251
|
});
|
|
1159
1252
|
|
|
@@ -1184,7 +1277,7 @@ registerCommand({
|
|
|
1184
1277
|
const tree = buildTree(readEntries(treeMap), rootId, maxDepth);
|
|
1185
1278
|
if (format === "json") return printJson(tree);
|
|
1186
1279
|
if (tree.length === 0) return "(empty tree)";
|
|
1187
|
-
return (conn.spaces.find((s) => s.
|
|
1280
|
+
return (conn.spaces.find((s) => s.id === conn.rootDocId)?.label ?? conn.rootDocId ?? "Workspace") + "\n" + printTree(toTreeNodes(tree));
|
|
1188
1281
|
}
|
|
1189
1282
|
});
|
|
1190
1283
|
registerCommand({
|
|
@@ -1256,238 +1349,14 @@ registerCommand({
|
|
|
1256
1349
|
});
|
|
1257
1350
|
|
|
1258
1351
|
//#endregion
|
|
1259
|
-
//#region packages/
|
|
1260
|
-
/**
|
|
1261
|
-
* Y.XmlFragment → Markdown serializer.
|
|
1262
|
-
* Walks the TipTap document structure and produces markdown text.
|
|
1263
|
-
*/
|
|
1264
|
-
function deltaToMarkdown(delta) {
|
|
1265
|
-
return delta.map((op) => {
|
|
1266
|
-
let text = op.insert;
|
|
1267
|
-
if (!op.attributes) return text;
|
|
1268
|
-
const a = op.attributes;
|
|
1269
|
-
if (a.code) text = `\`${text}\``;
|
|
1270
|
-
if (a.bold) text = `**${text}**`;
|
|
1271
|
-
if (a.italic) text = `*${text}*`;
|
|
1272
|
-
if (a.strike) text = `~~${text}~~`;
|
|
1273
|
-
if (a.link?.href) text = `[${text}](${a.link.href})`;
|
|
1274
|
-
if (a.badge) text = `:badge[${a.badge.label || text}]`;
|
|
1275
|
-
if (a.kbd) text = `:kbd{value="${a.kbd.value || text}"}`;
|
|
1276
|
-
if (a.proseIcon) text = `:icon{name="${a.proseIcon.name}"}`;
|
|
1277
|
-
return text;
|
|
1278
|
-
}).join("");
|
|
1279
|
-
}
|
|
1280
|
-
function xmlTextToMarkdown(xmlText) {
|
|
1281
|
-
return deltaToMarkdown(xmlText.toDelta());
|
|
1282
|
-
}
|
|
1283
|
-
function elementTextContent(el) {
|
|
1284
|
-
const parts = [];
|
|
1285
|
-
for (let i = 0; i < el.length; i++) {
|
|
1286
|
-
const child = el.get(i);
|
|
1287
|
-
if (child instanceof yjs.XmlText) parts.push(xmlTextToMarkdown(child));
|
|
1288
|
-
else if (child instanceof yjs.XmlElement) parts.push(elementTextContent(child));
|
|
1289
|
-
}
|
|
1290
|
-
return parts.join("");
|
|
1291
|
-
}
|
|
1292
|
-
function serializeElement(el, indent = "") {
|
|
1293
|
-
switch (el.nodeName) {
|
|
1294
|
-
case "documentHeader":
|
|
1295
|
-
case "documentMeta": return "";
|
|
1296
|
-
case "heading": {
|
|
1297
|
-
const level = Number(el.getAttribute("level")) || 1;
|
|
1298
|
-
return `${"#".repeat(level)} ${elementTextContent(el)}`;
|
|
1299
|
-
}
|
|
1300
|
-
case "paragraph": return elementTextContent(el);
|
|
1301
|
-
case "bulletList": return serializeList(el, "bullet", indent);
|
|
1302
|
-
case "orderedList": return serializeList(el, "ordered", indent);
|
|
1303
|
-
case "taskList": return serializeTaskList(el, indent);
|
|
1304
|
-
case "codeBlock": return `\`\`\`${el.getAttribute("language") || ""}\n${elementTextContent(el)}\n\`\`\``;
|
|
1305
|
-
case "blockquote": {
|
|
1306
|
-
const lines = [];
|
|
1307
|
-
for (let i = 0; i < el.length; i++) {
|
|
1308
|
-
const child = el.get(i);
|
|
1309
|
-
if (child instanceof yjs.XmlElement) lines.push(serializeElement(child, indent));
|
|
1310
|
-
}
|
|
1311
|
-
return lines.map((l) => `> ${l}`).join("\n");
|
|
1312
|
-
}
|
|
1313
|
-
case "horizontalRule": return "---";
|
|
1314
|
-
case "table": return serializeTable(el);
|
|
1315
|
-
case "docEmbed": {
|
|
1316
|
-
const docId = el.getAttribute("docId");
|
|
1317
|
-
return docId ? `![[${docId}]]` : "";
|
|
1318
|
-
}
|
|
1319
|
-
case "svgEmbed": {
|
|
1320
|
-
const svg = el.getAttribute("svg") || "";
|
|
1321
|
-
const svgTitle = el.getAttribute("title") || "";
|
|
1322
|
-
if (!svg) return "";
|
|
1323
|
-
return `\`\`\`svg${svgTitle ? ` ${svgTitle}` : ""}\n${svg}\n\`\`\``;
|
|
1324
|
-
}
|
|
1325
|
-
case "image": {
|
|
1326
|
-
const src = el.getAttribute("src") || "";
|
|
1327
|
-
const alt = el.getAttribute("alt") || "";
|
|
1328
|
-
const w = el.getAttribute("width");
|
|
1329
|
-
const h = el.getAttribute("height");
|
|
1330
|
-
let attrs = "";
|
|
1331
|
-
if (w || h) {
|
|
1332
|
-
const parts = [];
|
|
1333
|
-
if (w) parts.push(`width="${w}"`);
|
|
1334
|
-
if (h) parts.push(`height="${h}"`);
|
|
1335
|
-
attrs = `{${parts.join(" ")}}`;
|
|
1336
|
-
}
|
|
1337
|
-
return `${attrs}`;
|
|
1338
|
-
}
|
|
1339
|
-
case "callout": return `::${el.getAttribute("type") || "note"}\n${serializeChildren(el, indent)}\n::`;
|
|
1340
|
-
case "collapsible": {
|
|
1341
|
-
const label = el.getAttribute("label") || "Details";
|
|
1342
|
-
const open = el.getAttribute("open");
|
|
1343
|
-
const props = [`label="${label}"`];
|
|
1344
|
-
if (open === true || open === "true") props.push("open=\"true\"");
|
|
1345
|
-
const inner = serializeChildren(el, indent);
|
|
1346
|
-
return `::collapsible{${props.join(" ")}}\n${inner}\n::`;
|
|
1347
|
-
}
|
|
1348
|
-
case "steps": return `::steps\n${serializeChildren(el, indent)}\n::`;
|
|
1349
|
-
case "card": {
|
|
1350
|
-
const title = el.getAttribute("title") || "";
|
|
1351
|
-
const icon = el.getAttribute("icon") || "";
|
|
1352
|
-
const to = el.getAttribute("to") || "";
|
|
1353
|
-
const props = [];
|
|
1354
|
-
if (title) props.push(`title="${title}"`);
|
|
1355
|
-
if (icon) props.push(`icon="${icon}"`);
|
|
1356
|
-
if (to) props.push(`to="${to}"`);
|
|
1357
|
-
const inner = serializeChildren(el, indent);
|
|
1358
|
-
return `::card{${props.join(" ")}}\n${inner}\n::`;
|
|
1359
|
-
}
|
|
1360
|
-
case "cardGroup": return `::card-group\n${serializeChildren(el, indent)}\n::`;
|
|
1361
|
-
case "codeCollapse": return `::code-collapse\n${serializeChildren(el, indent)}\n::`;
|
|
1362
|
-
case "codeGroup": return `::code-group\n${serializeChildren(el, indent)}\n::`;
|
|
1363
|
-
case "codePreview": return `::code-preview\n${serializeChildren(el, indent)}\n::`;
|
|
1364
|
-
case "codeTree": return `::code-tree{files="${el.getAttribute("files") || "[]"}"}\n::`;
|
|
1365
|
-
case "accordion": return serializeSlottedComponent(el, "accordion", "accordionItem", "item");
|
|
1366
|
-
case "tabs": return serializeSlottedComponent(el, "tabs", "tabsItem", "tab");
|
|
1367
|
-
case "field": {
|
|
1368
|
-
const fieldName = el.getAttribute("name") || "";
|
|
1369
|
-
const fieldType = el.getAttribute("type") || "string";
|
|
1370
|
-
const required = el.getAttribute("required");
|
|
1371
|
-
const props = [];
|
|
1372
|
-
if (fieldName) props.push(`name="${fieldName}"`);
|
|
1373
|
-
props.push(`type="${fieldType}"`);
|
|
1374
|
-
if (required === true || required === "true") props.push("required=\"true\"");
|
|
1375
|
-
const inner = serializeChildren(el, indent);
|
|
1376
|
-
return `::field{${props.join(" ")}}\n${inner}\n::`;
|
|
1377
|
-
}
|
|
1378
|
-
case "fieldGroup": return `::field-group\n${serializeChildren(el, indent)}\n::`;
|
|
1379
|
-
default: return serializeChildren(el, indent);
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
function serializeList(el, type, indent) {
|
|
1383
|
-
const lines = [];
|
|
1384
|
-
for (let i = 0; i < el.length; i++) {
|
|
1385
|
-
const item = el.get(i);
|
|
1386
|
-
if (item instanceof yjs.XmlElement && item.nodeName === "listItem") {
|
|
1387
|
-
const prefix = type === "bullet" ? "- " : `${i + 1}. `;
|
|
1388
|
-
const content = elementTextContent(item);
|
|
1389
|
-
lines.push(`${indent}${prefix}${content}`);
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
return lines.join("\n");
|
|
1393
|
-
}
|
|
1394
|
-
function serializeTaskList(el, indent) {
|
|
1395
|
-
const lines = [];
|
|
1396
|
-
for (let i = 0; i < el.length; i++) {
|
|
1397
|
-
const item = el.get(i);
|
|
1398
|
-
if (item instanceof yjs.XmlElement && item.nodeName === "taskItem") {
|
|
1399
|
-
const checked = item.getAttribute("checked");
|
|
1400
|
-
const marker = checked === true || checked === "true" ? "[x]" : "[ ]";
|
|
1401
|
-
const content = elementTextContent(item);
|
|
1402
|
-
lines.push(`${indent}- ${marker} ${content}`);
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
return lines.join("\n");
|
|
1406
|
-
}
|
|
1407
|
-
function serializeTable(el) {
|
|
1408
|
-
const rows = [];
|
|
1409
|
-
for (let i = 0; i < el.length; i++) {
|
|
1410
|
-
const row = el.get(i);
|
|
1411
|
-
if (!(row instanceof yjs.XmlElement) || row.nodeName !== "tableRow") continue;
|
|
1412
|
-
const cells = [];
|
|
1413
|
-
for (let j = 0; j < row.length; j++) {
|
|
1414
|
-
const cell = row.get(j);
|
|
1415
|
-
if (cell instanceof yjs.XmlElement) cells.push(elementTextContent(cell));
|
|
1416
|
-
}
|
|
1417
|
-
rows.push(cells);
|
|
1418
|
-
}
|
|
1419
|
-
if (!rows.length) return "";
|
|
1420
|
-
const headerRow = rows[0];
|
|
1421
|
-
const lines = [`| ${headerRow.join(" | ")} |`];
|
|
1422
|
-
lines.push(`| ${headerRow.map(() => "---").join(" | ")} |`);
|
|
1423
|
-
for (let i = 1; i < rows.length; i++) lines.push(`| ${rows[i].join(" | ")} |`);
|
|
1424
|
-
return lines.join("\n");
|
|
1425
|
-
}
|
|
1426
|
-
function serializeSlottedComponent(el, componentName, childNodeName, slotName) {
|
|
1427
|
-
const parts = [`::${componentName}`];
|
|
1428
|
-
for (let i = 0; i < el.length; i++) {
|
|
1429
|
-
const child = el.get(i);
|
|
1430
|
-
if (!(child instanceof yjs.XmlElement) || child.nodeName !== childNodeName) continue;
|
|
1431
|
-
const label = child.getAttribute("label") || `Item ${i + 1}`;
|
|
1432
|
-
const icon = child.getAttribute("icon") || "";
|
|
1433
|
-
const props = [`label="${label}"`];
|
|
1434
|
-
if (icon) props.push(`icon="${icon}"`);
|
|
1435
|
-
parts.push(`#${slotName}{${props.join(" ")}}`);
|
|
1436
|
-
const inner = serializeChildren(child, "");
|
|
1437
|
-
if (inner) parts.push(inner);
|
|
1438
|
-
}
|
|
1439
|
-
parts.push("::");
|
|
1440
|
-
return parts.join("\n");
|
|
1441
|
-
}
|
|
1442
|
-
function serializeChildren(el, indent) {
|
|
1443
|
-
const parts = [];
|
|
1444
|
-
for (let i = 0; i < el.length; i++) {
|
|
1445
|
-
const child = el.get(i);
|
|
1446
|
-
if (child instanceof yjs.XmlElement) {
|
|
1447
|
-
const serialized = serializeElement(child, indent);
|
|
1448
|
-
if (serialized) parts.push(serialized);
|
|
1449
|
-
} else if (child instanceof yjs.XmlText) {
|
|
1450
|
-
const text = xmlTextToMarkdown(child);
|
|
1451
|
-
if (text) parts.push(text);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
return parts.join("\n\n");
|
|
1455
|
-
}
|
|
1456
|
-
/**
|
|
1457
|
-
* Converts a Y.XmlFragment (TipTap document) to markdown.
|
|
1458
|
-
* Extracts the title from the documentHeader element.
|
|
1459
|
-
*
|
|
1460
|
-
* @returns `{ title, markdown }` where title is the H1/header text
|
|
1461
|
-
*/
|
|
1462
|
-
function yjsToMarkdown(fragment) {
|
|
1463
|
-
let title = "Untitled";
|
|
1464
|
-
const bodyParts = [];
|
|
1465
|
-
for (let i = 0; i < fragment.length; i++) {
|
|
1466
|
-
const child = fragment.get(i);
|
|
1467
|
-
if (!(child instanceof yjs.XmlElement)) continue;
|
|
1468
|
-
if (child.nodeName === "documentHeader") {
|
|
1469
|
-
title = elementTextContent(child) || "Untitled";
|
|
1470
|
-
continue;
|
|
1471
|
-
}
|
|
1472
|
-
if (child.nodeName === "documentMeta") continue;
|
|
1473
|
-
const serialized = serializeElement(child);
|
|
1474
|
-
if (serialized !== "") bodyParts.push(serialized);
|
|
1475
|
-
}
|
|
1476
|
-
return {
|
|
1477
|
-
title,
|
|
1478
|
-
markdown: bodyParts.join("\n\n")
|
|
1479
|
-
};
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
//#endregion
|
|
1483
|
-
//#region packages/mcp/src/converters/markdownToYjs.ts
|
|
1484
|
-
/**
|
|
1485
|
-
* Markdown → Y.js converter.
|
|
1486
|
-
* Ported from cou-sh/app/utils/markdownToYjs.ts with Vue dependency removed.
|
|
1487
|
-
*/
|
|
1352
|
+
//#region packages/convert/src/markdown-to-yjs.ts
|
|
1488
1353
|
function parseInlineArray(raw) {
|
|
1489
1354
|
return raw.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
1490
1355
|
}
|
|
1356
|
+
function stripQuotes(s) {
|
|
1357
|
+
if (s.length >= 2 && (s.startsWith("\"") && s.endsWith("\"") || s.startsWith("'") && s.endsWith("'"))) return s.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
1358
|
+
return s;
|
|
1359
|
+
}
|
|
1491
1360
|
function parseFrontmatter(markdown) {
|
|
1492
1361
|
const noResult = {
|
|
1493
1362
|
meta: {},
|
|
@@ -1518,8 +1387,8 @@ function parseFrontmatter(markdown) {
|
|
|
1518
1387
|
if (kvMatch) {
|
|
1519
1388
|
const key = kvMatch[1];
|
|
1520
1389
|
const val = kvMatch[2].trim();
|
|
1521
|
-
if (val.startsWith("[") && val.endsWith("]")) raw[key] = parseInlineArray(val);
|
|
1522
|
-
else raw[key] = val;
|
|
1390
|
+
if (val.startsWith("[") && val.endsWith("]")) raw[key] = parseInlineArray(val).map(stripQuotes);
|
|
1391
|
+
else raw[key] = stripQuotes(val);
|
|
1523
1392
|
}
|
|
1524
1393
|
i++;
|
|
1525
1394
|
}
|
|
@@ -1546,54 +1415,27 @@ function parseFrontmatter(markdown) {
|
|
|
1546
1415
|
}[priorityRaw.toLowerCase()] ?? (Number(priorityRaw) || 0);
|
|
1547
1416
|
const checkedRaw = raw["checked"] ?? raw["done"];
|
|
1548
1417
|
if (checkedRaw !== void 0) meta.checked = checkedRaw === "true" || checkedRaw === true;
|
|
1549
|
-
const dateStart = getStr([
|
|
1418
|
+
const dateStart = getStr([
|
|
1419
|
+
"dateStart",
|
|
1420
|
+
"date",
|
|
1421
|
+
"created"
|
|
1422
|
+
]);
|
|
1550
1423
|
if (dateStart) meta.dateStart = dateStart;
|
|
1551
|
-
const dateEnd = getStr(["due"]);
|
|
1424
|
+
const dateEnd = getStr(["dateEnd", "due"]);
|
|
1552
1425
|
if (dateEnd) meta.dateEnd = dateEnd;
|
|
1553
|
-
const subtitle = getStr(["
|
|
1426
|
+
const subtitle = getStr(["subtitle", "description"]);
|
|
1554
1427
|
if (subtitle) meta.subtitle = subtitle;
|
|
1555
1428
|
const url = getStr(["url"]);
|
|
1556
1429
|
if (url) meta.url = url;
|
|
1557
|
-
const email = getStr(["email"]);
|
|
1558
|
-
if (email) meta.email = email;
|
|
1559
|
-
const phone = getStr(["phone"]);
|
|
1560
|
-
if (phone) meta.phone = phone;
|
|
1561
1430
|
const ratingRaw = getStr(["rating"]);
|
|
1562
1431
|
if (ratingRaw !== void 0) {
|
|
1563
1432
|
const n = Number(ratingRaw);
|
|
1564
1433
|
if (!Number.isNaN(n)) meta.rating = Math.min(5, Math.max(0, n));
|
|
1565
1434
|
}
|
|
1566
|
-
const
|
|
1567
|
-
if (datetimeStart) meta.datetimeStart = datetimeStart;
|
|
1568
|
-
const datetimeEnd = getStr(["datetimeEnd"]);
|
|
1569
|
-
if (datetimeEnd) meta.datetimeEnd = datetimeEnd;
|
|
1570
|
-
const allDayRaw = raw["allDay"];
|
|
1571
|
-
if (allDayRaw !== void 0) meta.allDay = allDayRaw === "true" || allDayRaw === true;
|
|
1572
|
-
const geoLatRaw = getStr(["geoLat"]);
|
|
1573
|
-
if (geoLatRaw !== void 0) {
|
|
1574
|
-
const n = Number(geoLatRaw);
|
|
1575
|
-
if (!Number.isNaN(n)) meta.geoLat = n;
|
|
1576
|
-
}
|
|
1577
|
-
const geoLngRaw = getStr(["geoLng"]);
|
|
1578
|
-
if (geoLngRaw !== void 0) {
|
|
1579
|
-
const n = Number(geoLngRaw);
|
|
1580
|
-
if (!Number.isNaN(n)) meta.geoLng = n;
|
|
1581
|
-
}
|
|
1582
|
-
const geoType = getStr(["geoType"]);
|
|
1583
|
-
if (geoType && (geoType === "marker" || geoType === "line" || geoType === "measure")) meta.geoType = geoType;
|
|
1584
|
-
const geoDescription = getStr(["geoDescription"]);
|
|
1585
|
-
if (geoDescription) meta.geoDescription = geoDescription;
|
|
1586
|
-
const numberRaw = getStr(["number"]);
|
|
1587
|
-
if (numberRaw !== void 0) {
|
|
1588
|
-
const n = Number(numberRaw);
|
|
1589
|
-
if (!Number.isNaN(n)) meta.number = n;
|
|
1590
|
-
}
|
|
1591
|
-
const unit = getStr(["unit"]);
|
|
1592
|
-
if (unit) meta.unit = unit;
|
|
1593
|
-
const note = getStr(["note"]);
|
|
1594
|
-
if (note) meta.note = note;
|
|
1435
|
+
const rawTitle = typeof raw["title"] === "string" ? raw["title"] : void 0;
|
|
1595
1436
|
return {
|
|
1596
|
-
title:
|
|
1437
|
+
title: rawTitle !== void 0 ? stripQuotes(rawTitle) : void 0,
|
|
1438
|
+
type: getStr(["type"]),
|
|
1597
1439
|
meta,
|
|
1598
1440
|
body
|
|
1599
1441
|
};
|
|
@@ -1601,63 +1443,74 @@ function parseFrontmatter(markdown) {
|
|
|
1601
1443
|
function parseInline(text) {
|
|
1602
1444
|
const stripped = text.replace(/\{lang="[^"]*"\}/g, "").replace(/:(?!badge|icon|kbd)(\w[\w-]*)\[([^\]]*)\](\{[^}]*\})?/g, "$2").replace(/:(?!badge|icon|kbd)(\w[\w-]*)(\{[^}]*\})/g, "");
|
|
1603
1445
|
const tokens = [];
|
|
1604
|
-
const re =
|
|
1446
|
+
const re = /\$([^$\n]+?)\$|@\[([^\]]+?)\]\(user:([^)]+?)\)|:badge\[([^\]]*)\](\{[^}]*\})?|:icon\{([^}]*)\}|:kbd\{([^}]*)\}|\[\[([0-9a-fA-F-]{36})(?:\|([^\]]+?))?\]\]|~~(.+?)~~|\*\*(.+?)\*\*|\*(.+?)\*|_(.+?)_|`(.+?)`|\[(.+?)\]\((.+?)\)/g;
|
|
1605
1447
|
let lastIndex = 0;
|
|
1606
1448
|
let match;
|
|
1607
1449
|
while ((match = re.exec(stripped)) !== null) {
|
|
1608
1450
|
if (match.index > lastIndex) tokens.push({ text: stripped.slice(lastIndex, match.index) });
|
|
1609
|
-
if (match[1] !== void 0) {
|
|
1610
|
-
|
|
1451
|
+
if (match[1] !== void 0) tokens.push({
|
|
1452
|
+
text: match[1],
|
|
1453
|
+
attrs: { mathInline: { expression: match[1] } }
|
|
1454
|
+
});
|
|
1455
|
+
else if (match[2] !== void 0 && match[3] !== void 0) tokens.push({
|
|
1456
|
+
text: match[2],
|
|
1457
|
+
attrs: { mention: {
|
|
1458
|
+
userId: match[3],
|
|
1459
|
+
label: match[2]
|
|
1460
|
+
} }
|
|
1461
|
+
});
|
|
1462
|
+
else if (match[4] !== void 0) {
|
|
1463
|
+
const badgeProps = parseMdcProps(match[5]);
|
|
1611
1464
|
tokens.push({
|
|
1612
|
-
text: match[
|
|
1465
|
+
text: match[4] || "Badge",
|
|
1613
1466
|
attrs: { badge: {
|
|
1614
|
-
label: match[
|
|
1467
|
+
label: match[4] || "Badge",
|
|
1615
1468
|
color: badgeProps["color"] || "neutral",
|
|
1616
1469
|
variant: badgeProps["variant"] || "subtle"
|
|
1617
1470
|
} }
|
|
1618
1471
|
});
|
|
1619
|
-
} else if (match[
|
|
1620
|
-
const iconProps = parseMdcProps(`{${match[
|
|
1472
|
+
} else if (match[6] !== void 0) {
|
|
1473
|
+
const iconProps = parseMdcProps(`{${match[6]}}`);
|
|
1621
1474
|
tokens.push({
|
|
1622
1475
|
text: "",
|
|
1623
1476
|
attrs: { proseIcon: { name: iconProps["name"] || "i-lucide-star" } }
|
|
1624
1477
|
});
|
|
1625
|
-
} else if (match[
|
|
1626
|
-
const kbdProps = parseMdcProps(`{${match[
|
|
1478
|
+
} else if (match[7] !== void 0) {
|
|
1479
|
+
const kbdProps = parseMdcProps(`{${match[7]}}`);
|
|
1627
1480
|
tokens.push({
|
|
1628
1481
|
text: kbdProps["value"] || "",
|
|
1629
1482
|
attrs: { kbd: { value: kbdProps["value"] || "" } }
|
|
1630
1483
|
});
|
|
1631
|
-
} else if (match[
|
|
1632
|
-
const docId = match[
|
|
1633
|
-
const
|
|
1484
|
+
} else if (match[8] !== void 0) {
|
|
1485
|
+
const docId = match[8];
|
|
1486
|
+
const label = match[9] ?? docId;
|
|
1634
1487
|
tokens.push({
|
|
1635
|
-
text:
|
|
1636
|
-
attrs: {
|
|
1488
|
+
text: label,
|
|
1489
|
+
attrs: { docLink: { docId } }
|
|
1637
1490
|
});
|
|
1638
|
-
} else if (match[
|
|
1639
|
-
text: match[
|
|
1491
|
+
} else if (match[10] !== void 0) tokens.push({
|
|
1492
|
+
text: match[10],
|
|
1640
1493
|
attrs: { strike: true }
|
|
1641
1494
|
});
|
|
1642
|
-
else if (match[
|
|
1643
|
-
text: match[
|
|
1495
|
+
else if (match[11] !== void 0) tokens.push({
|
|
1496
|
+
text: match[11],
|
|
1644
1497
|
attrs: { bold: true }
|
|
1645
1498
|
});
|
|
1646
|
-
else if (match[
|
|
1647
|
-
text: match[
|
|
1499
|
+
else if (match[12] !== void 0) tokens.push({
|
|
1500
|
+
text: match[12],
|
|
1648
1501
|
attrs: { italic: true }
|
|
1649
1502
|
});
|
|
1650
|
-
else if (match[
|
|
1651
|
-
text: match[
|
|
1503
|
+
else if (match[13] !== void 0) tokens.push({
|
|
1504
|
+
text: match[13],
|
|
1652
1505
|
attrs: { italic: true }
|
|
1653
1506
|
});
|
|
1654
|
-
else if (match[
|
|
1655
|
-
text: match[
|
|
1507
|
+
else if (match[14] !== void 0) tokens.push({
|
|
1508
|
+
text: match[14],
|
|
1656
1509
|
attrs: { code: true }
|
|
1657
1510
|
});
|
|
1658
|
-
else if (match[
|
|
1659
|
-
text: match[
|
|
1660
|
-
attrs: { link: { href: match[
|
|
1511
|
+
else if (match[15] !== void 0 && match[16] !== void 0) tokens.push({
|
|
1512
|
+
text: match[15],
|
|
1513
|
+
attrs: { link: { href: match[16] } }
|
|
1661
1514
|
});
|
|
1662
1515
|
lastIndex = match.index + match[0].length;
|
|
1663
1516
|
}
|
|
@@ -1671,6 +1524,7 @@ function parseTableRow(line) {
|
|
|
1671
1524
|
function isTableSeparator(line) {
|
|
1672
1525
|
return /^\|[\s|:-]+\|$/.test(line.trim());
|
|
1673
1526
|
}
|
|
1527
|
+
/** Extract fenced code blocks from MDC #code slot lines. */
|
|
1674
1528
|
function extractFencedCode(lines) {
|
|
1675
1529
|
const result = [];
|
|
1676
1530
|
let i = 0;
|
|
@@ -1697,14 +1551,23 @@ function extractFencedCode(lines) {
|
|
|
1697
1551
|
}
|
|
1698
1552
|
return result;
|
|
1699
1553
|
}
|
|
1554
|
+
/** Extract key="value" pairs from MDC prop syntax `{key="value" other="x"}` */
|
|
1700
1555
|
function parseMdcProps(propsStr) {
|
|
1701
1556
|
if (!propsStr) return {};
|
|
1702
1557
|
const result = {};
|
|
1703
|
-
|
|
1558
|
+
let s = propsStr.trim();
|
|
1559
|
+
if (s.startsWith("{") && s.endsWith("}")) s = s.slice(1, -1);
|
|
1560
|
+
const re = /(\w[\w-]*)(?:=(?:"([^"]*)"|([^\s"}]+)))?/g;
|
|
1704
1561
|
let m;
|
|
1705
|
-
while ((m = re.exec(
|
|
1562
|
+
while ((m = re.exec(s)) !== null) {
|
|
1563
|
+
const key = m[1];
|
|
1564
|
+
if (m[2] !== void 0) result[key] = m[2];
|
|
1565
|
+
else if (m[3] !== void 0) result[key] = m[3];
|
|
1566
|
+
else result[key] = "true";
|
|
1567
|
+
}
|
|
1706
1568
|
return result;
|
|
1707
1569
|
}
|
|
1570
|
+
/** Parse named child MDC blocks from inner lines (e.g. #item for accordion, #tab for tabs) */
|
|
1708
1571
|
function parseMdcChildren(innerLines, slotPrefix) {
|
|
1709
1572
|
const items = [];
|
|
1710
1573
|
let current = null;
|
|
@@ -1736,6 +1599,95 @@ function parseMdcChildren(innerLines, slotPrefix) {
|
|
|
1736
1599
|
}));
|
|
1737
1600
|
}
|
|
1738
1601
|
const TASK_RE = /^[-*+]\s+\[([ xX])\]\s+(.*)/;
|
|
1602
|
+
/**
|
|
1603
|
+
* Consume a list (bullet / ordered / task) starting at `start`. Indented
|
|
1604
|
+
* continuation lines and nested lists are captured into each item's
|
|
1605
|
+
* `innerBlocks` so the parse → serialise → parse cycle preserves tree
|
|
1606
|
+
* structure instead of flattening nested lists onto a single line.
|
|
1607
|
+
*
|
|
1608
|
+
* `indent` is the column of the item marker for the current list. A
|
|
1609
|
+
* nested list starts ≥2 columns deeper. Lines with less indent than
|
|
1610
|
+
* `indent` belong to the outer block and stop consumption.
|
|
1611
|
+
*/
|
|
1612
|
+
function consumeList(lines, start, indent, kind) {
|
|
1613
|
+
const items = [];
|
|
1614
|
+
let i = start;
|
|
1615
|
+
while (i < lines.length) {
|
|
1616
|
+
const line = lines[i];
|
|
1617
|
+
if (line.trim() === "") {
|
|
1618
|
+
let j = i + 1;
|
|
1619
|
+
while (j < lines.length && lines[j].trim() === "") j++;
|
|
1620
|
+
if (j >= lines.length) break;
|
|
1621
|
+
const lookahead = lines[j];
|
|
1622
|
+
if (leadingSpaces(lookahead) < indent) break;
|
|
1623
|
+
if (!matchMarker(lookahead.slice(indent), kind)) break;
|
|
1624
|
+
i = j;
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
const leading = leadingSpaces(line);
|
|
1628
|
+
if (leading < indent) break;
|
|
1629
|
+
if (leading > indent) break;
|
|
1630
|
+
const m = matchMarker(line.slice(indent), kind);
|
|
1631
|
+
if (!m) break;
|
|
1632
|
+
const item = { text: m.text };
|
|
1633
|
+
if (kind === "task") item.checked = m.checked;
|
|
1634
|
+
i++;
|
|
1635
|
+
const contLines = [];
|
|
1636
|
+
while (i < lines.length) {
|
|
1637
|
+
const next = lines[i];
|
|
1638
|
+
if (next.trim() === "") {
|
|
1639
|
+
let k = i + 1;
|
|
1640
|
+
while (k < lines.length && lines[k].trim() === "") k++;
|
|
1641
|
+
if (k >= lines.length) break;
|
|
1642
|
+
if (leadingSpaces(lines[k]) <= indent) break;
|
|
1643
|
+
contLines.push("");
|
|
1644
|
+
i++;
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
const nextIndent = leadingSpaces(next);
|
|
1648
|
+
if (nextIndent <= indent) break;
|
|
1649
|
+
const deindentBy = Math.min(nextIndent, indent + 2);
|
|
1650
|
+
contLines.push(next.slice(deindentBy));
|
|
1651
|
+
i++;
|
|
1652
|
+
}
|
|
1653
|
+
if (contLines.length > 0) item.innerBlocks = parseBlocks(contLines.join("\n"));
|
|
1654
|
+
items.push(item);
|
|
1655
|
+
}
|
|
1656
|
+
return {
|
|
1657
|
+
items,
|
|
1658
|
+
next: i
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
function leadingSpaces(s) {
|
|
1662
|
+
let n = 0;
|
|
1663
|
+
while (n < s.length && s[n] === " ") n++;
|
|
1664
|
+
return n;
|
|
1665
|
+
}
|
|
1666
|
+
function matchMarker(s, kind) {
|
|
1667
|
+
if (kind === "task") {
|
|
1668
|
+
const m = s.match(TASK_RE);
|
|
1669
|
+
if (!m) return null;
|
|
1670
|
+
return {
|
|
1671
|
+
text: m[2],
|
|
1672
|
+
checked: m[1].toLowerCase() === "x"
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
if (kind === "bullet") {
|
|
1676
|
+
if (TASK_RE.test(s)) return null;
|
|
1677
|
+
const m = s.match(/^[-*+]\s+(.*)$/);
|
|
1678
|
+
if (!m) return null;
|
|
1679
|
+
return {
|
|
1680
|
+
text: m[1],
|
|
1681
|
+
checked: false
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
const m = s.match(/^\d+\.\s+(.*)$/);
|
|
1685
|
+
if (!m) return null;
|
|
1686
|
+
return {
|
|
1687
|
+
text: m[1],
|
|
1688
|
+
checked: false
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1739
1691
|
function parseBlocks(markdown) {
|
|
1740
1692
|
const rawLines = markdown.split("\n");
|
|
1741
1693
|
let firstContentLine = 0;
|
|
@@ -1761,17 +1713,15 @@ function parseBlocks(markdown) {
|
|
|
1761
1713
|
i++;
|
|
1762
1714
|
}
|
|
1763
1715
|
i++;
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
});
|
|
1771
|
-
} else blocks.push({
|
|
1716
|
+
const code = codeLines.join("\n");
|
|
1717
|
+
if (lang === "math") blocks.push({
|
|
1718
|
+
type: "mathBlock",
|
|
1719
|
+
expression: code
|
|
1720
|
+
});
|
|
1721
|
+
else blocks.push({
|
|
1772
1722
|
type: "codeBlock",
|
|
1773
1723
|
lang,
|
|
1774
|
-
code
|
|
1724
|
+
code
|
|
1775
1725
|
});
|
|
1776
1726
|
continue;
|
|
1777
1727
|
}
|
|
@@ -1790,11 +1740,16 @@ function parseBlocks(markdown) {
|
|
|
1790
1740
|
i++;
|
|
1791
1741
|
continue;
|
|
1792
1742
|
}
|
|
1793
|
-
const
|
|
1794
|
-
if (
|
|
1743
|
+
const embedMatch = line.match(/^!\[\[([0-9a-fA-F-]{36})(?:\|([^\]]+))?\]\](\{[^}]*\})?\s*$/);
|
|
1744
|
+
if (embedMatch) {
|
|
1745
|
+
const docId = embedMatch[1];
|
|
1746
|
+
const label = embedMatch[2] ?? "";
|
|
1747
|
+
const props = parseMdcProps(embedMatch[3]);
|
|
1795
1748
|
blocks.push({
|
|
1796
1749
|
type: "docEmbed",
|
|
1797
|
-
docId
|
|
1750
|
+
docId,
|
|
1751
|
+
label,
|
|
1752
|
+
props
|
|
1798
1753
|
});
|
|
1799
1754
|
i++;
|
|
1800
1755
|
continue;
|
|
@@ -1846,6 +1801,23 @@ function parseBlocks(markdown) {
|
|
|
1846
1801
|
});
|
|
1847
1802
|
continue;
|
|
1848
1803
|
}
|
|
1804
|
+
const atomMatch = line.match(/^:(\w[\w-]*)(\{[^}]*\})?\s*$/);
|
|
1805
|
+
if (atomMatch && atomMatch[1] === "file") {
|
|
1806
|
+
const props = parseMdcProps(atomMatch[2]);
|
|
1807
|
+
const uploadId = props["upload-id"] ?? props["uploadId"] ?? "";
|
|
1808
|
+
const filename = props["filename"] ?? "";
|
|
1809
|
+
const mime = props["mime"] ?? "";
|
|
1810
|
+
const src = props["src"] ?? (uploadId && filename ? `.abracadabra/files/${uploadId}-${filename}` : "");
|
|
1811
|
+
blocks.push({
|
|
1812
|
+
type: "fileBlock",
|
|
1813
|
+
src,
|
|
1814
|
+
mime,
|
|
1815
|
+
uploadId,
|
|
1816
|
+
filename
|
|
1817
|
+
});
|
|
1818
|
+
i++;
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1849
1821
|
const MDC_OPEN = /^\s*(:{2,})(\w[\w-]*)(\{[^}]*\})?\s*$/;
|
|
1850
1822
|
if (MDC_OPEN.test(line)) {
|
|
1851
1823
|
const colons = line.match(/^\s*(:+)/)?.[1]?.length ?? 2;
|
|
@@ -2014,15 +1986,8 @@ function parseBlocks(markdown) {
|
|
|
2014
1986
|
continue;
|
|
2015
1987
|
}
|
|
2016
1988
|
if (TASK_RE.test(line)) {
|
|
2017
|
-
const items =
|
|
2018
|
-
|
|
2019
|
-
const m = lines[i].match(TASK_RE);
|
|
2020
|
-
items.push({
|
|
2021
|
-
checked: m[1].toLowerCase() === "x",
|
|
2022
|
-
text: m[2]
|
|
2023
|
-
});
|
|
2024
|
-
i++;
|
|
2025
|
-
}
|
|
1989
|
+
const { items, next } = consumeList(lines, i, 0, "task");
|
|
1990
|
+
i = next;
|
|
2026
1991
|
blocks.push({
|
|
2027
1992
|
type: "taskList",
|
|
2028
1993
|
items
|
|
@@ -2030,12 +1995,9 @@ function parseBlocks(markdown) {
|
|
|
2030
1995
|
continue;
|
|
2031
1996
|
}
|
|
2032
1997
|
if (/^[-*+]\s+/.test(line)) {
|
|
2033
|
-
const items =
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
i++;
|
|
2037
|
-
}
|
|
2038
|
-
if (items.length) {
|
|
1998
|
+
const { items, next } = consumeList(lines, i, 0, "bullet");
|
|
1999
|
+
if (items.length > 0) {
|
|
2000
|
+
i = next;
|
|
2039
2001
|
blocks.push({
|
|
2040
2002
|
type: "bulletList",
|
|
2041
2003
|
items
|
|
@@ -2044,16 +2006,15 @@ function parseBlocks(markdown) {
|
|
|
2044
2006
|
}
|
|
2045
2007
|
}
|
|
2046
2008
|
if (/^\d+\.\s+/.test(line)) {
|
|
2047
|
-
const items =
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2009
|
+
const { items, next } = consumeList(lines, i, 0, "ordered");
|
|
2010
|
+
if (items.length > 0) {
|
|
2011
|
+
i = next;
|
|
2012
|
+
blocks.push({
|
|
2013
|
+
type: "orderedList",
|
|
2014
|
+
items
|
|
2015
|
+
});
|
|
2016
|
+
continue;
|
|
2051
2017
|
}
|
|
2052
|
-
blocks.push({
|
|
2053
|
-
type: "orderedList",
|
|
2054
|
-
items
|
|
2055
|
-
});
|
|
2056
|
-
continue;
|
|
2057
2018
|
}
|
|
2058
2019
|
if (line.trim() === "") {
|
|
2059
2020
|
i++;
|
|
@@ -2071,6 +2032,10 @@ function parseBlocks(markdown) {
|
|
|
2071
2032
|
}
|
|
2072
2033
|
return blocks;
|
|
2073
2034
|
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Insert formatted inline tokens into an already-attached Y.XmlElement.
|
|
2037
|
+
* Creates one Y.XmlText per token (attach first, fill second).
|
|
2038
|
+
*/
|
|
2074
2039
|
function fillTextInto(el, tokens) {
|
|
2075
2040
|
const filtered = tokens.filter((t) => t.text.length > 0);
|
|
2076
2041
|
if (!filtered.length) return;
|
|
@@ -2107,9 +2072,19 @@ function blockElName(b) {
|
|
|
2107
2072
|
case "fieldGroup": return "fieldGroup";
|
|
2108
2073
|
case "image": return "image";
|
|
2109
2074
|
case "docEmbed": return "docEmbed";
|
|
2110
|
-
case "
|
|
2075
|
+
case "mathBlock": return "mathBlock";
|
|
2076
|
+
case "fileBlock": return "fileBlock";
|
|
2111
2077
|
}
|
|
2112
2078
|
}
|
|
2079
|
+
function populateListItemChildren(itemEl, item, _itemKind) {
|
|
2080
|
+
const paraEl = new yjs.XmlElement("paragraph");
|
|
2081
|
+
itemEl.insert(itemEl.length, [paraEl]);
|
|
2082
|
+
fillTextInto(paraEl, parseInline(item.text));
|
|
2083
|
+
if (!item.innerBlocks?.length) return;
|
|
2084
|
+
const innerEls = item.innerBlocks.map((b) => new yjs.XmlElement(blockElName(b)));
|
|
2085
|
+
itemEl.insert(itemEl.length, innerEls);
|
|
2086
|
+
item.innerBlocks.forEach((b, i) => fillBlock(innerEls[i], b));
|
|
2087
|
+
}
|
|
2113
2088
|
function fillBlock(el, block) {
|
|
2114
2089
|
switch (block.type) {
|
|
2115
2090
|
case "heading":
|
|
@@ -2123,10 +2098,8 @@ function fillBlock(el, block) {
|
|
|
2123
2098
|
case "orderedList": {
|
|
2124
2099
|
const listItemEls = block.items.map(() => new yjs.XmlElement("listItem"));
|
|
2125
2100
|
el.insert(0, listItemEls);
|
|
2126
|
-
block.items.forEach((
|
|
2127
|
-
|
|
2128
|
-
listItemEls[i].insert(0, [paraEl]);
|
|
2129
|
-
fillTextInto(paraEl, parseInline(text));
|
|
2101
|
+
block.items.forEach((item, i) => {
|
|
2102
|
+
populateListItemChildren(listItemEls[i], item, "listItem");
|
|
2130
2103
|
});
|
|
2131
2104
|
break;
|
|
2132
2105
|
}
|
|
@@ -2134,10 +2107,8 @@ function fillBlock(el, block) {
|
|
|
2134
2107
|
const taskItemEls = block.items.map(() => new yjs.XmlElement("taskItem"));
|
|
2135
2108
|
el.insert(0, taskItemEls);
|
|
2136
2109
|
block.items.forEach((item, i) => {
|
|
2137
|
-
taskItemEls[i].setAttribute("checked", item.checked);
|
|
2138
|
-
|
|
2139
|
-
taskItemEls[i].insert(0, [paraEl]);
|
|
2140
|
-
fillTextInto(paraEl, parseInline(item.text));
|
|
2110
|
+
taskItemEls[i].setAttribute("checked", !!item.checked);
|
|
2111
|
+
populateListItemChildren(taskItemEls[i], item, "taskItem");
|
|
2141
2112
|
});
|
|
2142
2113
|
break;
|
|
2143
2114
|
}
|
|
@@ -2325,31 +2296,55 @@ function fillBlock(el, block) {
|
|
|
2325
2296
|
break;
|
|
2326
2297
|
case "docEmbed":
|
|
2327
2298
|
el.setAttribute("docId", block.docId);
|
|
2299
|
+
for (const flag of [
|
|
2300
|
+
"collapsed",
|
|
2301
|
+
"tall",
|
|
2302
|
+
"seamless"
|
|
2303
|
+
]) if (block.props[flag] === "true" || block.props[flag] === "1") el.setAttribute(flag, true);
|
|
2328
2304
|
break;
|
|
2329
|
-
case "
|
|
2330
|
-
el.setAttribute("
|
|
2331
|
-
|
|
2305
|
+
case "mathBlock":
|
|
2306
|
+
el.setAttribute("expression", block.expression);
|
|
2307
|
+
break;
|
|
2308
|
+
case "fileBlock":
|
|
2309
|
+
if (block.src) el.setAttribute("src", block.src);
|
|
2310
|
+
if (block.mime) el.setAttribute("mime", block.mime);
|
|
2311
|
+
if (block.uploadId) el.setAttribute("uploadId", block.uploadId);
|
|
2312
|
+
if (block.filename) el.setAttribute("filename", block.filename);
|
|
2332
2313
|
break;
|
|
2333
2314
|
}
|
|
2334
2315
|
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Parses markdown text and writes the result into a Y.XmlFragment that
|
|
2318
|
+
* TipTap's Collaboration extension can read.
|
|
2319
|
+
*
|
|
2320
|
+
* Requires `fragment.doc` to be set (i.e. the fragment must already be
|
|
2321
|
+
* obtained from a live Y.Doc via `ydoc.getXmlFragment('default')`).
|
|
2322
|
+
*
|
|
2323
|
+
* @param fragment The target `Y.Doc.getXmlFragment('default')`
|
|
2324
|
+
* @param markdown Raw markdown string
|
|
2325
|
+
* @param fallbackTitle Used as the title when the markdown has no H1
|
|
2326
|
+
*/
|
|
2335
2327
|
function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled") {
|
|
2336
2328
|
const ydoc = fragment.doc;
|
|
2337
2329
|
if (!ydoc) {
|
|
2338
2330
|
console.warn("[markdownToYjs] fragment has no doc — skipping population");
|
|
2339
2331
|
return;
|
|
2340
2332
|
}
|
|
2341
|
-
const
|
|
2333
|
+
const fm = parseFrontmatter(markdown);
|
|
2334
|
+
const blocks = parseBlocks(fm.body);
|
|
2342
2335
|
let title = fallbackTitle;
|
|
2336
|
+
let titleSource;
|
|
2337
|
+
if (fm.title !== void 0) {
|
|
2338
|
+
title = fm.title;
|
|
2339
|
+
titleSource = "frontmatter";
|
|
2340
|
+
}
|
|
2343
2341
|
let contentBlocks = blocks;
|
|
2344
2342
|
const h1 = blocks.findIndex((b) => b.type === "heading" && b.level === 1);
|
|
2345
2343
|
if (h1 !== -1) {
|
|
2346
2344
|
title = blocks[h1].text;
|
|
2347
2345
|
contentBlocks = blocks.filter((_, i) => i !== h1);
|
|
2346
|
+
titleSource = "h1";
|
|
2348
2347
|
}
|
|
2349
|
-
if (!contentBlocks.length) contentBlocks = [{
|
|
2350
|
-
type: "paragraph",
|
|
2351
|
-
text: ""
|
|
2352
|
-
}];
|
|
2353
2348
|
ydoc.transact(() => {
|
|
2354
2349
|
const headerEl = new yjs.XmlElement("documentHeader");
|
|
2355
2350
|
const metaEl = new yjs.XmlElement("documentMeta");
|
|
@@ -2379,7 +2374,8 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
|
|
|
2379
2374
|
case "fieldGroup": return new yjs.XmlElement("fieldGroup");
|
|
2380
2375
|
case "image": return new yjs.XmlElement("image");
|
|
2381
2376
|
case "docEmbed": return new yjs.XmlElement("docEmbed");
|
|
2382
|
-
case "
|
|
2377
|
+
case "mathBlock": return new yjs.XmlElement("mathBlock");
|
|
2378
|
+
case "fileBlock": return new yjs.XmlElement("fileBlock");
|
|
2383
2379
|
}
|
|
2384
2380
|
});
|
|
2385
2381
|
fragment.insert(0, [
|
|
@@ -2387,61 +2383,1324 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
|
|
|
2387
2383
|
metaEl,
|
|
2388
2384
|
...bodyEls
|
|
2389
2385
|
]);
|
|
2386
|
+
if (titleSource) headerEl.setAttribute("titleSource", titleSource);
|
|
2390
2387
|
const headerXt = new yjs.XmlText();
|
|
2391
2388
|
headerEl.insert(0, [headerXt]);
|
|
2392
2389
|
headerXt.insert(0, title);
|
|
2390
|
+
for (const k of Object.keys(fm.meta)) {
|
|
2391
|
+
const v = fm.meta[k];
|
|
2392
|
+
if (v === void 0 || v === null) continue;
|
|
2393
|
+
metaEl.setAttribute(k, v);
|
|
2394
|
+
}
|
|
2395
|
+
if (fm.type) metaEl.setAttribute("type", fm.type);
|
|
2393
2396
|
contentBlocks.forEach((block, i) => fillBlock(bodyEls[i], block));
|
|
2394
2397
|
});
|
|
2395
2398
|
}
|
|
2396
2399
|
|
|
2397
2400
|
//#endregion
|
|
2398
|
-
//#region packages/
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
aliases: ["info:doc"],
|
|
2409
|
-
description: "Show document metadata (label, type, meta, dates).",
|
|
2410
|
-
usage: "doc id=<docId> | name=<label> | path=<a/b/c>",
|
|
2411
|
-
async run(conn, args) {
|
|
2412
|
-
if (!conn) return "Not connected";
|
|
2413
|
-
const docId = resolveDocument(conn, args.params, args.positional);
|
|
2414
|
-
if (!docId) return "Document not found. Specify id=, name=, or path= to identify the document.";
|
|
2415
|
-
const treeMap = conn.getTreeMap();
|
|
2416
|
-
if (!treeMap) return "Not connected";
|
|
2417
|
-
const raw = treeMap.get(docId);
|
|
2418
|
-
if (!raw) return `Document ${docId} not found in tree.`;
|
|
2419
|
-
const entry = toPlain$1(raw);
|
|
2420
|
-
const lines = [
|
|
2421
|
-
`id: ${docId}`,
|
|
2422
|
-
`label: ${entry.label || "Untitled"}`,
|
|
2423
|
-
`type: ${entry.type ?? "—"}`,
|
|
2424
|
-
`parent: ${entry.parentId ?? "(root)"}`,
|
|
2425
|
-
`order: ${entry.order ?? 0}`,
|
|
2426
|
-
`created: ${entry.createdAt ? new Date(entry.createdAt).toISOString() : "—"}`,
|
|
2427
|
-
`updated: ${entry.updatedAt ? new Date(entry.updatedAt).toISOString() : "—"} (${relativeTime(entry.updatedAt)})`
|
|
2428
|
-
];
|
|
2429
|
-
if (entry.meta && Object.keys(entry.meta).length > 0) {
|
|
2430
|
-
lines.push(`meta:`);
|
|
2431
|
-
for (const [k, v] of Object.entries(entry.meta)) lines.push(` ${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`);
|
|
2401
|
+
//#region packages/convert/src/yjs-to-markdown.ts
|
|
2402
|
+
function serializeDelta(delta) {
|
|
2403
|
+
let result = "";
|
|
2404
|
+
for (const op of delta) {
|
|
2405
|
+
if (typeof op.insert !== "string") continue;
|
|
2406
|
+
let text = op.insert;
|
|
2407
|
+
const attrs = op.attributes ?? {};
|
|
2408
|
+
if (attrs.code) {
|
|
2409
|
+
result += `\`${text}\``;
|
|
2410
|
+
continue;
|
|
2432
2411
|
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
});
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2412
|
+
if (attrs.badge) {
|
|
2413
|
+
const b = attrs.badge;
|
|
2414
|
+
const props = [];
|
|
2415
|
+
if (b.color && b.color !== "neutral") props.push(`color="${b.color}"`);
|
|
2416
|
+
if (b.variant && b.variant !== "subtle") props.push(`variant="${b.variant}"`);
|
|
2417
|
+
result += `:badge[${b.label || text}]${props.length ? `{${props.join(" ")}}` : ""}`;
|
|
2418
|
+
continue;
|
|
2419
|
+
}
|
|
2420
|
+
if (attrs.proseIcon) {
|
|
2421
|
+
const icon = attrs.proseIcon.name || "i-lucide-star";
|
|
2422
|
+
result += `:icon{name="${icon}"}`;
|
|
2423
|
+
continue;
|
|
2424
|
+
}
|
|
2425
|
+
if (attrs.kbd) {
|
|
2426
|
+
const value = attrs.kbd.value || text;
|
|
2427
|
+
result += `:kbd{value="${value}"}`;
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
if (attrs.docLink) {
|
|
2431
|
+
const docId = attrs.docLink.docId;
|
|
2432
|
+
if (docId) {
|
|
2433
|
+
result += text === docId ? `[[${docId}]]` : `[[${docId}|${text}]]`;
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
if (attrs.mention) {
|
|
2438
|
+
const { userId, label } = attrs.mention;
|
|
2439
|
+
if (userId) {
|
|
2440
|
+
result += `@[${label || text}](user:${userId})`;
|
|
2441
|
+
continue;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
if (attrs.mathInline) {
|
|
2445
|
+
const expr = attrs.mathInline.expression ?? text;
|
|
2446
|
+
result += `$${expr}$`;
|
|
2447
|
+
continue;
|
|
2448
|
+
}
|
|
2449
|
+
if (attrs.bold) text = `**${text}**`;
|
|
2450
|
+
if (attrs.italic) text = `*${text}*`;
|
|
2451
|
+
if (attrs.strike) text = `~~${text}~~`;
|
|
2452
|
+
if (attrs.link) {
|
|
2453
|
+
const href = attrs.link.href ?? "";
|
|
2454
|
+
text = `[${text}](${href})`;
|
|
2455
|
+
}
|
|
2456
|
+
result += text;
|
|
2457
|
+
}
|
|
2458
|
+
return result;
|
|
2459
|
+
}
|
|
2460
|
+
function serializeInline(el) {
|
|
2461
|
+
const parts = [];
|
|
2462
|
+
for (const child of el.toArray()) if (child instanceof yjs.XmlText) parts.push(serializeDelta(child.toDelta()));
|
|
2463
|
+
else if (child instanceof yjs.XmlElement) parts.push(serializeInline(child));
|
|
2464
|
+
return parts.join("");
|
|
2465
|
+
}
|
|
2466
|
+
function serializeBlock(el, indent = "") {
|
|
2467
|
+
if (el instanceof yjs.XmlText) return serializeDelta(el.toDelta());
|
|
2468
|
+
switch (el.nodeName) {
|
|
2469
|
+
case "documentHeader":
|
|
2470
|
+
case "documentMeta": return "";
|
|
2471
|
+
case "heading": {
|
|
2472
|
+
const level = Number(el.getAttribute("level") ?? 2);
|
|
2473
|
+
return `${"#".repeat(level)} ${serializeInline(el)}`;
|
|
2474
|
+
}
|
|
2475
|
+
case "paragraph": return serializeInline(el);
|
|
2476
|
+
case "bulletList": return serializeListItems(el, "bullet", indent);
|
|
2477
|
+
case "orderedList": return serializeListItems(el, "ordered", indent);
|
|
2478
|
+
case "taskList": return serializeTaskList(el, indent);
|
|
2479
|
+
case "codeBlock": {
|
|
2480
|
+
const lang = el.getAttribute("language") ?? "";
|
|
2481
|
+
const code = getCodeBlockText(el);
|
|
2482
|
+
if (code === "") return `\`\`\`${lang}\n\`\`\``;
|
|
2483
|
+
return `\`\`\`${lang}\n${code}\n\`\`\``;
|
|
2484
|
+
}
|
|
2485
|
+
case "blockquote": {
|
|
2486
|
+
const lines = [];
|
|
2487
|
+
for (const child of el.toArray()) if (child instanceof yjs.XmlElement) {
|
|
2488
|
+
const text = serializeBlock(child);
|
|
2489
|
+
for (const line of text.split("\n")) lines.push(`> ${line}`);
|
|
2490
|
+
}
|
|
2491
|
+
return lines.join("\n");
|
|
2492
|
+
}
|
|
2493
|
+
case "table": return serializeTable(el);
|
|
2494
|
+
case "horizontalRule": return "---";
|
|
2495
|
+
case "image": {
|
|
2496
|
+
const src = el.getAttribute("src") ?? "";
|
|
2497
|
+
const alt = el.getAttribute("alt") ?? "";
|
|
2498
|
+
const width = el.getAttribute("width");
|
|
2499
|
+
const height = el.getAttribute("height");
|
|
2500
|
+
const attrs = [];
|
|
2501
|
+
if (width) attrs.push(`width=${width}`);
|
|
2502
|
+
if (height) attrs.push(`height=${height}`);
|
|
2503
|
+
return `${attrs.length ? `{${attrs.join(" ")}}` : ""}`;
|
|
2504
|
+
}
|
|
2505
|
+
case "docEmbed": {
|
|
2506
|
+
const docId = el.getAttribute("docId") ?? "";
|
|
2507
|
+
const collapsed = el.getAttribute("collapsed");
|
|
2508
|
+
const tall = el.getAttribute("tall");
|
|
2509
|
+
const seamless = el.getAttribute("seamless");
|
|
2510
|
+
const flags = [];
|
|
2511
|
+
if (collapsed === true || collapsed === "true") flags.push("collapsed");
|
|
2512
|
+
if (tall === true || tall === "true") flags.push("tall");
|
|
2513
|
+
if (seamless === true || seamless === "true") flags.push("seamless");
|
|
2514
|
+
return `![[${docId}]]${flags.length ? `{${flags.join(" ")}}` : ""}`;
|
|
2515
|
+
}
|
|
2516
|
+
case "mathBlock": return `\`\`\`math\n${el.getAttribute("expression") ?? ""}\n\`\`\``;
|
|
2517
|
+
case "fileBlock": {
|
|
2518
|
+
const uploadId = el.getAttribute("uploadId") ?? "";
|
|
2519
|
+
const filename = el.getAttribute("filename") ?? "";
|
|
2520
|
+
const mime = el.getAttribute("mime") ?? "";
|
|
2521
|
+
const src = el.getAttribute("src") ?? (uploadId && filename ? `.abracadabra/files/${uploadId}-${filename}` : "");
|
|
2522
|
+
const props = [];
|
|
2523
|
+
if (src) props.push(`src="${src}"`);
|
|
2524
|
+
if (mime) props.push(`mime="${mime}"`);
|
|
2525
|
+
if (uploadId) props.push(`upload-id="${uploadId}"`);
|
|
2526
|
+
if (filename) props.push(`filename="${filename}"`);
|
|
2527
|
+
return `:file{${props.join(" ")}}`;
|
|
2528
|
+
}
|
|
2529
|
+
case "callout": return `::${el.getAttribute("type") ?? "note"}\n${serializeChildren(el)}\n::`;
|
|
2530
|
+
case "collapsible": {
|
|
2531
|
+
const label = el.getAttribute("label") ?? "Details";
|
|
2532
|
+
const open = el.getAttribute("open");
|
|
2533
|
+
const props = [`label="${label}"`];
|
|
2534
|
+
if (open === true || open === "true") props.push("open=\"true\"");
|
|
2535
|
+
return `::collapsible{${props.join(" ")}}\n${serializeChildren(el)}\n::`;
|
|
2536
|
+
}
|
|
2537
|
+
case "steps": return `::steps\n${serializeChildren(el)}\n::`;
|
|
2538
|
+
case "card": {
|
|
2539
|
+
const props = [];
|
|
2540
|
+
const title = el.getAttribute("title");
|
|
2541
|
+
const icon = el.getAttribute("icon");
|
|
2542
|
+
const to = el.getAttribute("to");
|
|
2543
|
+
if (title) props.push(`title="${title}"`);
|
|
2544
|
+
if (icon) props.push(`icon="${icon}"`);
|
|
2545
|
+
if (to) props.push(`to="${to}"`);
|
|
2546
|
+
return `::card${props.length ? `{${props.join(" ")}}` : ""}\n${serializeChildren(el)}\n::`;
|
|
2547
|
+
}
|
|
2548
|
+
case "cardGroup": return `::card-group\n${el.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
|
|
2549
|
+
case "codeCollapse": return `::code-collapse\n${el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
|
|
2550
|
+
case "codeGroup": return `::code-group\n${el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
|
|
2551
|
+
case "codePreview": {
|
|
2552
|
+
const children = el.toArray().filter((c) => c instanceof yjs.XmlElement);
|
|
2553
|
+
const nonCode = children.filter((c) => c.nodeName !== "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
|
|
2554
|
+
const code = children.filter((c) => c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
|
|
2555
|
+
const parts = [nonCode];
|
|
2556
|
+
if (code) parts.push(`#code\n${code}`);
|
|
2557
|
+
return `::code-preview\n${parts.filter(Boolean).join("\n\n")}\n::`;
|
|
2558
|
+
}
|
|
2559
|
+
case "codeTree": return `::code-tree{files="${el.getAttribute("files") ?? "[]"}"}\n::`;
|
|
2560
|
+
case "accordion": return serializeSlottedContainer(el, "accordion", "accordionItem", "item");
|
|
2561
|
+
case "tabs": return serializeSlottedContainer(el, "tabs", "tabsItem", "tab");
|
|
2562
|
+
case "field": {
|
|
2563
|
+
const fieldName = el.getAttribute("name") ?? "";
|
|
2564
|
+
const fieldType = el.getAttribute("type") ?? "string";
|
|
2565
|
+
const required = el.getAttribute("required");
|
|
2566
|
+
const props = [`name="${fieldName}"`, `type="${fieldType}"`];
|
|
2567
|
+
if (required === true || required === "true") props.push("required=\"true\"");
|
|
2568
|
+
return `::field{${props.join(" ")}}\n${serializeChildren(el)}\n::`;
|
|
2569
|
+
}
|
|
2570
|
+
case "fieldGroup": return `::field-group\n${el.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
|
|
2571
|
+
default: return serializeChildren(el);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
function serializeChildren(el) {
|
|
2575
|
+
const blocks = [];
|
|
2576
|
+
for (const child of el.toArray()) if (child instanceof yjs.XmlElement) {
|
|
2577
|
+
const text = serializeBlock(child);
|
|
2578
|
+
if (text) blocks.push(text);
|
|
2579
|
+
} else if (child instanceof yjs.XmlText) {
|
|
2580
|
+
const text = serializeDelta(child.toDelta());
|
|
2581
|
+
if (text) blocks.push(text);
|
|
2582
|
+
}
|
|
2583
|
+
return blocks.join("\n\n");
|
|
2584
|
+
}
|
|
2585
|
+
function serializeListItems(el, type, indent) {
|
|
2586
|
+
const lines = [];
|
|
2587
|
+
let counter = 1;
|
|
2588
|
+
for (const child of el.toArray()) {
|
|
2589
|
+
if (!(child instanceof yjs.XmlElement) || child.nodeName !== "listItem") continue;
|
|
2590
|
+
const prefix = type === "bullet" ? "- " : `${counter++}. `;
|
|
2591
|
+
const subParts = [];
|
|
2592
|
+
for (const sub of child.toArray()) {
|
|
2593
|
+
if (!(sub instanceof yjs.XmlElement)) continue;
|
|
2594
|
+
if (sub.nodeName === "bulletList") subParts.push(serializeListItems(sub, "bullet", indent + " "));
|
|
2595
|
+
else if (sub.nodeName === "orderedList") subParts.push(serializeListItems(sub, "ordered", indent + " "));
|
|
2596
|
+
else subParts.push(serializeInline(sub));
|
|
2597
|
+
}
|
|
2598
|
+
if (subParts.length <= 1) lines.push(`${indent}${prefix}${subParts[0] ?? ""}`);
|
|
2599
|
+
else {
|
|
2600
|
+
lines.push(`${indent}${prefix}${subParts[0] ?? ""}`);
|
|
2601
|
+
for (let i = 1; i < subParts.length; i++) lines.push(subParts[i]);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
return lines.join("\n");
|
|
2605
|
+
}
|
|
2606
|
+
function serializeTaskList(el, indent) {
|
|
2607
|
+
const lines = [];
|
|
2608
|
+
for (const child of el.toArray()) {
|
|
2609
|
+
if (!(child instanceof yjs.XmlElement) || child.nodeName !== "taskItem") continue;
|
|
2610
|
+
const checked = child.getAttribute("checked");
|
|
2611
|
+
const marker = checked === true || checked === "true" ? "[x]" : "[ ]";
|
|
2612
|
+
let header = "";
|
|
2613
|
+
const nestedParts = [];
|
|
2614
|
+
for (const sub of child.toArray()) {
|
|
2615
|
+
if (!(sub instanceof yjs.XmlElement)) continue;
|
|
2616
|
+
if (sub.nodeName === "paragraph" && header === "") header = serializeInline(sub);
|
|
2617
|
+
else if (sub.nodeName === "bulletList") nestedParts.push(serializeListItems(sub, "bullet", indent + " "));
|
|
2618
|
+
else if (sub.nodeName === "orderedList") nestedParts.push(serializeListItems(sub, "ordered", indent + " "));
|
|
2619
|
+
else if (sub.nodeName === "taskList") nestedParts.push(serializeTaskList(sub, indent + " "));
|
|
2620
|
+
else nestedParts.push(indent + " " + serializeBlock(sub, indent + " "));
|
|
2621
|
+
}
|
|
2622
|
+
lines.push(`${indent}- ${marker} ${header}`);
|
|
2623
|
+
for (const part of nestedParts) lines.push(part);
|
|
2624
|
+
}
|
|
2625
|
+
return lines.join("\n");
|
|
2626
|
+
}
|
|
2627
|
+
function getCodeBlockText(el) {
|
|
2628
|
+
for (const child of el.toArray()) if (child instanceof yjs.XmlText) return child.toString();
|
|
2629
|
+
return "";
|
|
2630
|
+
}
|
|
2631
|
+
function serializeTable(el) {
|
|
2632
|
+
const rows = el.toArray().filter((c) => c instanceof yjs.XmlElement);
|
|
2633
|
+
if (!rows.length) return "";
|
|
2634
|
+
const serializedRows = [];
|
|
2635
|
+
for (const row of rows) {
|
|
2636
|
+
const cells = row.toArray().filter((c) => c instanceof yjs.XmlElement).map((cell) => {
|
|
2637
|
+
return cell.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeInline(c)).join(" ");
|
|
2638
|
+
});
|
|
2639
|
+
serializedRows.push(cells);
|
|
2640
|
+
}
|
|
2641
|
+
if (!serializedRows.length) return "";
|
|
2642
|
+
const colCount = Math.max(...serializedRows.map((r) => r.length));
|
|
2643
|
+
const headerRow = serializedRows[0];
|
|
2644
|
+
const separator = Array(colCount).fill("---");
|
|
2645
|
+
const dataRows = serializedRows.slice(1);
|
|
2646
|
+
const formatRow = (cells) => {
|
|
2647
|
+
return `| ${Array(colCount).fill("").map((_, i) => cells[i] ?? "").join(" | ")} |`;
|
|
2648
|
+
};
|
|
2649
|
+
return [
|
|
2650
|
+
formatRow(headerRow),
|
|
2651
|
+
formatRow(separator),
|
|
2652
|
+
...dataRows.map(formatRow)
|
|
2653
|
+
].join("\n");
|
|
2654
|
+
}
|
|
2655
|
+
function serializeSlottedContainer(el, containerName, childName, slotPrefix) {
|
|
2656
|
+
return `::${containerName}\n${el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === childName).map((item) => {
|
|
2657
|
+
const label = item.getAttribute("label") ?? "";
|
|
2658
|
+
const icon = item.getAttribute("icon") ?? "";
|
|
2659
|
+
const props = [];
|
|
2660
|
+
if (label) props.push(`label="${label}"`);
|
|
2661
|
+
if (icon) props.push(`icon="${icon}"`);
|
|
2662
|
+
const content = serializeChildren(item);
|
|
2663
|
+
return `#${slotPrefix}{${props.join(" ")}}\n${content}`;
|
|
2664
|
+
}).join("\n\n")}\n::`;
|
|
2665
|
+
}
|
|
2666
|
+
function generateFrontmatter(label, meta, type) {
|
|
2667
|
+
const lines = [];
|
|
2668
|
+
if (label !== void 0) lines.push(`title: "${escapeYaml(label)}"`);
|
|
2669
|
+
if (type && type !== "doc") lines.push(`type: ${type}`);
|
|
2670
|
+
if (!meta) return `---\n${lines.join("\n")}\n---`;
|
|
2671
|
+
if (meta.tags?.length) lines.push(`tags: [${meta.tags.join(", ")}]`);
|
|
2672
|
+
if (meta.color) lines.push(`color: ${yamlScalar(meta.color)}`);
|
|
2673
|
+
if (meta.icon) lines.push(`icon: ${yamlScalar(meta.icon)}`);
|
|
2674
|
+
if (meta.status) lines.push(`status: ${yamlScalar(meta.status)}`);
|
|
2675
|
+
if (meta.priority !== void 0 && meta.priority !== 0) lines.push(`priority: ${{
|
|
2676
|
+
1: "low",
|
|
2677
|
+
2: "medium",
|
|
2678
|
+
3: "high",
|
|
2679
|
+
4: "urgent"
|
|
2680
|
+
}[meta.priority] ?? meta.priority}`);
|
|
2681
|
+
if (meta.checked !== void 0) lines.push(`checked: ${meta.checked}`);
|
|
2682
|
+
if (meta.dateStart) lines.push(`dateStart: "${escapeYaml(meta.dateStart)}"`);
|
|
2683
|
+
if (meta.dateEnd) lines.push(`dateEnd: "${escapeYaml(meta.dateEnd)}"`);
|
|
2684
|
+
if (meta.subtitle) lines.push(`subtitle: "${escapeYaml(meta.subtitle)}"`);
|
|
2685
|
+
if (meta.url) lines.push(`url: ${meta.url}`);
|
|
2686
|
+
if (meta.rating !== void 0 && meta.rating !== 0) lines.push(`rating: ${meta.rating}`);
|
|
2687
|
+
return `---\n${lines.join("\n")}\n---`;
|
|
2688
|
+
}
|
|
2689
|
+
/**
|
|
2690
|
+
* Render a YAML scalar — bare when safe, double-quoted when the value
|
|
2691
|
+
* needs escaping. YAML treats `#`, `:`, leading whitespace, and a few
|
|
2692
|
+
* other characters as syntactically significant, so anything starting
|
|
2693
|
+
* with one of those gets quoted to stay round-trip safe.
|
|
2694
|
+
*/
|
|
2695
|
+
function yamlScalar(s) {
|
|
2696
|
+
if (s === "") return "\"\"";
|
|
2697
|
+
if (/^[#&*!|>%@`]/.test(s)) return `"${escapeYaml(s)}"`;
|
|
2698
|
+
if (/[:"]/.test(s)) return `"${escapeYaml(s)}"`;
|
|
2699
|
+
if (/^\s|\s$/.test(s)) return `"${escapeYaml(s)}"`;
|
|
2700
|
+
return s;
|
|
2701
|
+
}
|
|
2702
|
+
function escapeYaml(s) {
|
|
2703
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
2704
|
+
}
|
|
2705
|
+
function yjsToMarkdown(fragment, label, meta, type) {
|
|
2706
|
+
const { text: headerText, source: titleSource } = readDocumentHeader(fragment);
|
|
2707
|
+
const effectiveTitle = headerText || label;
|
|
2708
|
+
const docMeta = readDocumentMeta(fragment);
|
|
2709
|
+
const effectiveMeta = meta ?? docMeta.meta;
|
|
2710
|
+
const effectiveType = type ?? docMeta.type;
|
|
2711
|
+
const metaIsEmpty = isMetaEmpty(effectiveMeta);
|
|
2712
|
+
const typeIsDefault = !effectiveType || effectiveType === "doc";
|
|
2713
|
+
const bodyBlocks = collectBodyBlocks(fragment);
|
|
2714
|
+
let body;
|
|
2715
|
+
if (titleSource === "h1" && effectiveTitle) {
|
|
2716
|
+
const tail = serializeBlocksClean(bodyBlocks);
|
|
2717
|
+
body = tail === "" ? `# ${effectiveTitle}` : `# ${effectiveTitle}\n\n${tail}`;
|
|
2718
|
+
} else body = serializeBlocksClean(bodyBlocks);
|
|
2719
|
+
const wantFrontmatterTitle = titleSource === "frontmatter";
|
|
2720
|
+
if (!wantFrontmatterTitle && !(!metaIsEmpty || !typeIsDefault)) return body === "" ? "" : `${body}\n`;
|
|
2721
|
+
const frontmatter = generateFrontmatter(wantFrontmatterTitle ? effectiveTitle : void 0, effectiveMeta, effectiveType);
|
|
2722
|
+
if (body === "") return `${frontmatter}\n`;
|
|
2723
|
+
return `${frontmatter}\n\n${body}\n`;
|
|
2724
|
+
}
|
|
2725
|
+
function readDocumentMeta(fragment) {
|
|
2726
|
+
const meta = {};
|
|
2727
|
+
let type;
|
|
2728
|
+
for (const child of fragment.toArray()) {
|
|
2729
|
+
if (!(child instanceof yjs.XmlElement) || child.nodeName !== "documentMeta") continue;
|
|
2730
|
+
const attrs = child.getAttributes();
|
|
2731
|
+
for (const k of Object.keys(attrs)) {
|
|
2732
|
+
const v = attrs[k];
|
|
2733
|
+
if (v === void 0 || v === null) continue;
|
|
2734
|
+
if (k === "type" && typeof v === "string") {
|
|
2735
|
+
type = v;
|
|
2736
|
+
continue;
|
|
2737
|
+
}
|
|
2738
|
+
meta[k] = v;
|
|
2739
|
+
}
|
|
2740
|
+
break;
|
|
2741
|
+
}
|
|
2742
|
+
return {
|
|
2743
|
+
meta,
|
|
2744
|
+
type
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
function readDocumentHeader(fragment) {
|
|
2748
|
+
for (const child of fragment.toArray()) {
|
|
2749
|
+
if (!(child instanceof yjs.XmlElement) || child.nodeName !== "documentHeader") continue;
|
|
2750
|
+
const text = child.toArray().find((c) => c instanceof yjs.XmlText);
|
|
2751
|
+
const src = child.getAttribute("titleSource");
|
|
2752
|
+
const source = src === "h1" || src === "frontmatter" ? src : void 0;
|
|
2753
|
+
return {
|
|
2754
|
+
text: text ? text.toString() : "",
|
|
2755
|
+
source
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
return { text: "" };
|
|
2759
|
+
}
|
|
2760
|
+
function collectBodyBlocks(fragment) {
|
|
2761
|
+
const out = [];
|
|
2762
|
+
for (const child of fragment.toArray()) {
|
|
2763
|
+
if (!(child instanceof yjs.XmlElement)) continue;
|
|
2764
|
+
if (child.nodeName === "documentHeader" || child.nodeName === "documentMeta") continue;
|
|
2765
|
+
out.push(child);
|
|
2766
|
+
}
|
|
2767
|
+
return out;
|
|
2768
|
+
}
|
|
2769
|
+
function serializeBlocksClean(blocks) {
|
|
2770
|
+
const parts = [];
|
|
2771
|
+
for (const block of blocks) {
|
|
2772
|
+
if (block.nodeName === "paragraph" && block.length === 0) {
|
|
2773
|
+
parts.push("");
|
|
2774
|
+
continue;
|
|
2775
|
+
}
|
|
2776
|
+
parts.push(serializeBlock(block));
|
|
2777
|
+
}
|
|
2778
|
+
while (parts.length && parts[parts.length - 1] === "") parts.pop();
|
|
2779
|
+
return parts.join("\n\n");
|
|
2780
|
+
}
|
|
2781
|
+
function isMetaEmpty(meta) {
|
|
2782
|
+
if (!meta) return true;
|
|
2783
|
+
for (const key of Object.keys(meta)) {
|
|
2784
|
+
const v = meta[key];
|
|
2785
|
+
if (v === void 0 || v === null) continue;
|
|
2786
|
+
if (typeof v === "string" && v === "") continue;
|
|
2787
|
+
if (Array.isArray(v) && v.length === 0) continue;
|
|
2788
|
+
return false;
|
|
2789
|
+
}
|
|
2790
|
+
return true;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
//#endregion
|
|
2794
|
+
//#region packages/convert/src/spec/nodes.ts
|
|
2795
|
+
const BOOL_FALSE_DEFAULT = {
|
|
2796
|
+
key: "",
|
|
2797
|
+
type: "boolean",
|
|
2798
|
+
default: false,
|
|
2799
|
+
optional: true
|
|
2800
|
+
};
|
|
2801
|
+
const bool = (key) => ({
|
|
2802
|
+
...BOOL_FALSE_DEFAULT,
|
|
2803
|
+
key
|
|
2804
|
+
});
|
|
2805
|
+
const str = (key, def) => ({
|
|
2806
|
+
key,
|
|
2807
|
+
type: "string",
|
|
2808
|
+
default: def,
|
|
2809
|
+
optional: true
|
|
2810
|
+
});
|
|
2811
|
+
const num = (key) => ({
|
|
2812
|
+
key,
|
|
2813
|
+
type: "number",
|
|
2814
|
+
optional: true
|
|
2815
|
+
});
|
|
2816
|
+
const int = (key) => ({
|
|
2817
|
+
key,
|
|
2818
|
+
type: "integer",
|
|
2819
|
+
optional: true
|
|
2820
|
+
});
|
|
2821
|
+
const VANILLA_BLOCKS = [
|
|
2822
|
+
{
|
|
2823
|
+
name: "documentHeader",
|
|
2824
|
+
group: "block",
|
|
2825
|
+
wire: "special",
|
|
2826
|
+
doc: "Holds the title; hoisted to frontmatter on serialise."
|
|
2827
|
+
},
|
|
2828
|
+
{
|
|
2829
|
+
name: "documentMeta",
|
|
2830
|
+
group: "block",
|
|
2831
|
+
wire: "special",
|
|
2832
|
+
doc: "Holds page-level meta; serialised into frontmatter."
|
|
2833
|
+
},
|
|
2834
|
+
{
|
|
2835
|
+
name: "paragraph",
|
|
2836
|
+
group: "block",
|
|
2837
|
+
wire: "vanilla",
|
|
2838
|
+
contentBearing: true
|
|
2839
|
+
},
|
|
2840
|
+
{
|
|
2841
|
+
name: "heading",
|
|
2842
|
+
group: "block",
|
|
2843
|
+
wire: "vanilla",
|
|
2844
|
+
attrs: [int("level")],
|
|
2845
|
+
contentBearing: true
|
|
2846
|
+
},
|
|
2847
|
+
{
|
|
2848
|
+
name: "blockquote",
|
|
2849
|
+
group: "block",
|
|
2850
|
+
wire: "vanilla",
|
|
2851
|
+
contentBearing: true
|
|
2852
|
+
},
|
|
2853
|
+
{
|
|
2854
|
+
name: "codeBlock",
|
|
2855
|
+
group: "block",
|
|
2856
|
+
wire: "fence",
|
|
2857
|
+
attrs: [str("language", "")]
|
|
2858
|
+
},
|
|
2859
|
+
{
|
|
2860
|
+
name: "bulletList",
|
|
2861
|
+
group: "block",
|
|
2862
|
+
wire: "vanilla"
|
|
2863
|
+
},
|
|
2864
|
+
{
|
|
2865
|
+
name: "orderedList",
|
|
2866
|
+
group: "block",
|
|
2867
|
+
wire: "vanilla"
|
|
2868
|
+
},
|
|
2869
|
+
{
|
|
2870
|
+
name: "listItem",
|
|
2871
|
+
group: "block",
|
|
2872
|
+
wire: "vanilla",
|
|
2873
|
+
contentBearing: true
|
|
2874
|
+
},
|
|
2875
|
+
{
|
|
2876
|
+
name: "taskList",
|
|
2877
|
+
group: "block",
|
|
2878
|
+
wire: "vanilla"
|
|
2879
|
+
},
|
|
2880
|
+
{
|
|
2881
|
+
name: "taskItem",
|
|
2882
|
+
group: "block",
|
|
2883
|
+
wire: "vanilla",
|
|
2884
|
+
attrs: [bool("checked")],
|
|
2885
|
+
contentBearing: true
|
|
2886
|
+
},
|
|
2887
|
+
{
|
|
2888
|
+
name: "table",
|
|
2889
|
+
group: "block",
|
|
2890
|
+
wire: "vanilla"
|
|
2891
|
+
},
|
|
2892
|
+
{
|
|
2893
|
+
name: "tableRow",
|
|
2894
|
+
group: "block",
|
|
2895
|
+
wire: "vanilla"
|
|
2896
|
+
},
|
|
2897
|
+
{
|
|
2898
|
+
name: "tableHeader",
|
|
2899
|
+
group: "block",
|
|
2900
|
+
wire: "vanilla",
|
|
2901
|
+
contentBearing: true
|
|
2902
|
+
},
|
|
2903
|
+
{
|
|
2904
|
+
name: "tableCell",
|
|
2905
|
+
group: "block",
|
|
2906
|
+
wire: "vanilla",
|
|
2907
|
+
contentBearing: true
|
|
2908
|
+
},
|
|
2909
|
+
{
|
|
2910
|
+
name: "horizontalRule",
|
|
2911
|
+
group: "block",
|
|
2912
|
+
wire: "vanilla"
|
|
2913
|
+
},
|
|
2914
|
+
{
|
|
2915
|
+
name: "image",
|
|
2916
|
+
group: "block",
|
|
2917
|
+
wire: "special",
|
|
2918
|
+
attrs: [
|
|
2919
|
+
str("src"),
|
|
2920
|
+
str("alt", ""),
|
|
2921
|
+
int("width"),
|
|
2922
|
+
int("height")
|
|
2923
|
+
]
|
|
2924
|
+
},
|
|
2925
|
+
{
|
|
2926
|
+
name: "hardBreak",
|
|
2927
|
+
group: "inline",
|
|
2928
|
+
wire: "vanilla"
|
|
2929
|
+
}
|
|
2930
|
+
];
|
|
2931
|
+
const MDC_CONTAINERS = [
|
|
2932
|
+
{
|
|
2933
|
+
name: "callout",
|
|
2934
|
+
group: "block",
|
|
2935
|
+
wire: "mdc-container",
|
|
2936
|
+
attrs: [
|
|
2937
|
+
{
|
|
2938
|
+
key: "type",
|
|
2939
|
+
type: "string",
|
|
2940
|
+
default: "note",
|
|
2941
|
+
optional: true,
|
|
2942
|
+
values: [
|
|
2943
|
+
"note",
|
|
2944
|
+
"tip",
|
|
2945
|
+
"warning",
|
|
2946
|
+
"danger",
|
|
2947
|
+
"info",
|
|
2948
|
+
"caution",
|
|
2949
|
+
"alert",
|
|
2950
|
+
"success",
|
|
2951
|
+
"error"
|
|
2952
|
+
]
|
|
2953
|
+
},
|
|
2954
|
+
str("title"),
|
|
2955
|
+
str("icon")
|
|
2956
|
+
]
|
|
2957
|
+
},
|
|
2958
|
+
{
|
|
2959
|
+
name: "collapsible",
|
|
2960
|
+
group: "block",
|
|
2961
|
+
wire: "mdc-container",
|
|
2962
|
+
attrs: [str("label", "Details"), bool("open")]
|
|
2963
|
+
},
|
|
2964
|
+
{
|
|
2965
|
+
name: "accordion",
|
|
2966
|
+
group: "block",
|
|
2967
|
+
wire: "mdc-slotted",
|
|
2968
|
+
slotChild: "accordionItem"
|
|
2969
|
+
},
|
|
2970
|
+
{
|
|
2971
|
+
name: "accordionItem",
|
|
2972
|
+
group: "block",
|
|
2973
|
+
wire: "mdc-container",
|
|
2974
|
+
mdcTag: "accordion-item",
|
|
2975
|
+
attrs: [str("label", "Item"), str("icon")]
|
|
2976
|
+
},
|
|
2977
|
+
{
|
|
2978
|
+
name: "tabs",
|
|
2979
|
+
group: "block",
|
|
2980
|
+
wire: "mdc-slotted",
|
|
2981
|
+
slotChild: "tabsItem"
|
|
2982
|
+
},
|
|
2983
|
+
{
|
|
2984
|
+
name: "tabsItem",
|
|
2985
|
+
group: "block",
|
|
2986
|
+
wire: "mdc-container",
|
|
2987
|
+
mdcTag: "tabs-item",
|
|
2988
|
+
attrs: [str("label"), str("icon")]
|
|
2989
|
+
},
|
|
2990
|
+
{
|
|
2991
|
+
name: "steps",
|
|
2992
|
+
group: "block",
|
|
2993
|
+
wire: "mdc-container"
|
|
2994
|
+
},
|
|
2995
|
+
{
|
|
2996
|
+
name: "card",
|
|
2997
|
+
group: "block",
|
|
2998
|
+
wire: "mdc-container",
|
|
2999
|
+
attrs: [
|
|
3000
|
+
str("title"),
|
|
3001
|
+
str("icon"),
|
|
3002
|
+
str("to")
|
|
3003
|
+
]
|
|
3004
|
+
},
|
|
3005
|
+
{
|
|
3006
|
+
name: "cardGroup",
|
|
3007
|
+
group: "block",
|
|
3008
|
+
wire: "mdc-slotted",
|
|
3009
|
+
mdcTag: "card-group",
|
|
3010
|
+
slotChild: "card"
|
|
3011
|
+
},
|
|
3012
|
+
{
|
|
3013
|
+
name: "field",
|
|
3014
|
+
group: "block",
|
|
3015
|
+
wire: "mdc-container",
|
|
3016
|
+
attrs: [
|
|
3017
|
+
str("name"),
|
|
3018
|
+
str("type", "string"),
|
|
3019
|
+
bool("required")
|
|
3020
|
+
]
|
|
3021
|
+
},
|
|
3022
|
+
{
|
|
3023
|
+
name: "fieldGroup",
|
|
3024
|
+
group: "block",
|
|
3025
|
+
wire: "mdc-slotted",
|
|
3026
|
+
mdcTag: "field-group",
|
|
3027
|
+
slotChild: "field"
|
|
3028
|
+
},
|
|
3029
|
+
{
|
|
3030
|
+
name: "codeGroup",
|
|
3031
|
+
group: "block",
|
|
3032
|
+
wire: "mdc-slotted",
|
|
3033
|
+
mdcTag: "code-group",
|
|
3034
|
+
slotChild: "codeBlock"
|
|
3035
|
+
},
|
|
3036
|
+
{
|
|
3037
|
+
name: "codeCollapse",
|
|
3038
|
+
group: "block",
|
|
3039
|
+
wire: "mdc-container",
|
|
3040
|
+
mdcTag: "code-collapse"
|
|
3041
|
+
},
|
|
3042
|
+
{
|
|
3043
|
+
name: "codePreview",
|
|
3044
|
+
group: "block",
|
|
3045
|
+
wire: "mdc-container",
|
|
3046
|
+
mdcTag: "code-preview"
|
|
3047
|
+
},
|
|
3048
|
+
{
|
|
3049
|
+
name: "codeTree",
|
|
3050
|
+
group: "block",
|
|
3051
|
+
wire: "mdc-atom-block",
|
|
3052
|
+
mdcTag: "code-tree",
|
|
3053
|
+
attrs: [{
|
|
3054
|
+
key: "files",
|
|
3055
|
+
type: "json"
|
|
3056
|
+
}]
|
|
3057
|
+
},
|
|
3058
|
+
{
|
|
3059
|
+
name: "figure",
|
|
3060
|
+
group: "block",
|
|
3061
|
+
wire: "mdc-container",
|
|
3062
|
+
attrs: [
|
|
3063
|
+
str("src"),
|
|
3064
|
+
str("alt", ""),
|
|
3065
|
+
str("caption")
|
|
3066
|
+
]
|
|
3067
|
+
},
|
|
3068
|
+
{
|
|
3069
|
+
name: "video",
|
|
3070
|
+
group: "block",
|
|
3071
|
+
wire: "mdc-atom-block",
|
|
3072
|
+
attrs: [
|
|
3073
|
+
str("src"),
|
|
3074
|
+
str("poster"),
|
|
3075
|
+
bool("autoplay"),
|
|
3076
|
+
bool("loop"),
|
|
3077
|
+
bool("controls")
|
|
3078
|
+
]
|
|
3079
|
+
},
|
|
3080
|
+
{
|
|
3081
|
+
name: "embed",
|
|
3082
|
+
group: "block",
|
|
3083
|
+
wire: "mdc-atom-block",
|
|
3084
|
+
attrs: [str("src"), str("title")]
|
|
3085
|
+
},
|
|
3086
|
+
{
|
|
3087
|
+
name: "svgEmbed",
|
|
3088
|
+
group: "block",
|
|
3089
|
+
wire: "fence",
|
|
3090
|
+
attrs: [str("title")],
|
|
3091
|
+
mdcTag: "svg",
|
|
3092
|
+
doc: "Serialised as a ```svg fenced block; the SVG markup is the body."
|
|
3093
|
+
},
|
|
3094
|
+
{
|
|
3095
|
+
name: "divider",
|
|
3096
|
+
group: "block",
|
|
3097
|
+
wire: "mdc-atom-block",
|
|
3098
|
+
attrs: [str("label"), str("icon")]
|
|
3099
|
+
},
|
|
3100
|
+
{
|
|
3101
|
+
name: "quote",
|
|
3102
|
+
group: "block",
|
|
3103
|
+
wire: "mdc-container",
|
|
3104
|
+
attrs: [str("cite")]
|
|
3105
|
+
},
|
|
3106
|
+
{
|
|
3107
|
+
name: "progress",
|
|
3108
|
+
group: "block",
|
|
3109
|
+
wire: "mdc-atom-block",
|
|
3110
|
+
attrs: [
|
|
3111
|
+
num("value"),
|
|
3112
|
+
num("max"),
|
|
3113
|
+
str("label")
|
|
3114
|
+
]
|
|
3115
|
+
},
|
|
3116
|
+
{
|
|
3117
|
+
name: "spoiler",
|
|
3118
|
+
group: "block",
|
|
3119
|
+
wire: "mdc-container",
|
|
3120
|
+
attrs: [str("label")]
|
|
3121
|
+
},
|
|
3122
|
+
{
|
|
3123
|
+
name: "colorSwatch",
|
|
3124
|
+
group: "block",
|
|
3125
|
+
wire: "mdc-atom-block",
|
|
3126
|
+
mdcTag: "color-swatch",
|
|
3127
|
+
attrs: [str("color"), str("label")]
|
|
3128
|
+
},
|
|
3129
|
+
{
|
|
3130
|
+
name: "stat",
|
|
3131
|
+
group: "block",
|
|
3132
|
+
wire: "mdc-container",
|
|
3133
|
+
attrs: [
|
|
3134
|
+
str("label"),
|
|
3135
|
+
str("value"),
|
|
3136
|
+
str("icon")
|
|
3137
|
+
]
|
|
3138
|
+
},
|
|
3139
|
+
{
|
|
3140
|
+
name: "statGroup",
|
|
3141
|
+
group: "block",
|
|
3142
|
+
wire: "mdc-slotted",
|
|
3143
|
+
mdcTag: "stat-group",
|
|
3144
|
+
slotChild: "stat"
|
|
3145
|
+
},
|
|
3146
|
+
{
|
|
3147
|
+
name: "button",
|
|
3148
|
+
group: "block",
|
|
3149
|
+
wire: "mdc-atom-block",
|
|
3150
|
+
attrs: [
|
|
3151
|
+
str("label"),
|
|
3152
|
+
str("to"),
|
|
3153
|
+
str("icon"),
|
|
3154
|
+
str("variant")
|
|
3155
|
+
]
|
|
3156
|
+
},
|
|
3157
|
+
{
|
|
3158
|
+
name: "buttonGroup",
|
|
3159
|
+
group: "block",
|
|
3160
|
+
wire: "mdc-slotted",
|
|
3161
|
+
mdcTag: "button-group",
|
|
3162
|
+
slotChild: "button"
|
|
3163
|
+
},
|
|
3164
|
+
{
|
|
3165
|
+
name: "timeline",
|
|
3166
|
+
group: "block",
|
|
3167
|
+
wire: "mdc-slotted",
|
|
3168
|
+
slotChild: "timelineItem"
|
|
3169
|
+
},
|
|
3170
|
+
{
|
|
3171
|
+
name: "timelineItem",
|
|
3172
|
+
group: "block",
|
|
3173
|
+
wire: "mdc-container",
|
|
3174
|
+
mdcTag: "timeline-item",
|
|
3175
|
+
attrs: [
|
|
3176
|
+
str("label"),
|
|
3177
|
+
str("icon"),
|
|
3178
|
+
str("date")
|
|
3179
|
+
]
|
|
3180
|
+
},
|
|
3181
|
+
{
|
|
3182
|
+
name: "diff",
|
|
3183
|
+
group: "block",
|
|
3184
|
+
wire: "mdc-atom-block",
|
|
3185
|
+
attrs: [str("language", ""), {
|
|
3186
|
+
key: "value",
|
|
3187
|
+
type: "string"
|
|
3188
|
+
}]
|
|
3189
|
+
}
|
|
3190
|
+
];
|
|
3191
|
+
const INLINE_AND_SPECIAL = [
|
|
3192
|
+
{
|
|
3193
|
+
name: "docLink",
|
|
3194
|
+
group: "inline",
|
|
3195
|
+
wire: "special",
|
|
3196
|
+
attrs: [str("docId")],
|
|
3197
|
+
doc: "Wire form `[[uuid|label]]`; label regenerated on export."
|
|
3198
|
+
},
|
|
3199
|
+
{
|
|
3200
|
+
name: "docEmbed",
|
|
3201
|
+
group: "block",
|
|
3202
|
+
wire: "special",
|
|
3203
|
+
attrs: [
|
|
3204
|
+
str("docId"),
|
|
3205
|
+
bool("collapsed"),
|
|
3206
|
+
bool("tall"),
|
|
3207
|
+
bool("seamless")
|
|
3208
|
+
],
|
|
3209
|
+
doc: "Wire form `![[uuid|label]]{collapsed tall seamless}`."
|
|
3210
|
+
},
|
|
3211
|
+
{
|
|
3212
|
+
name: "mention",
|
|
3213
|
+
group: "inline",
|
|
3214
|
+
wire: "special",
|
|
3215
|
+
attrs: [str("userId")],
|
|
3216
|
+
doc: "Wire form `@[label](user:uuid)`; label regenerated on export."
|
|
3217
|
+
},
|
|
3218
|
+
{
|
|
3219
|
+
name: "mathInline",
|
|
3220
|
+
group: "inline",
|
|
3221
|
+
wire: "special",
|
|
3222
|
+
attrs: [{
|
|
3223
|
+
key: "expression",
|
|
3224
|
+
type: "string"
|
|
3225
|
+
}],
|
|
3226
|
+
doc: "Wire form `$expression$`."
|
|
3227
|
+
},
|
|
3228
|
+
{
|
|
3229
|
+
name: "mathBlock",
|
|
3230
|
+
group: "block",
|
|
3231
|
+
wire: "fence",
|
|
3232
|
+
attrs: [{
|
|
3233
|
+
key: "expression",
|
|
3234
|
+
type: "string"
|
|
3235
|
+
}],
|
|
3236
|
+
mdcTag: "math",
|
|
3237
|
+
doc: "Wire form ``` ```math\\nexpression\\n``` ```."
|
|
3238
|
+
},
|
|
3239
|
+
{
|
|
3240
|
+
name: "fileBlock",
|
|
3241
|
+
group: "block",
|
|
3242
|
+
wire: "mdc-atom-block",
|
|
3243
|
+
mdcTag: "file",
|
|
3244
|
+
attrs: [
|
|
3245
|
+
str("src"),
|
|
3246
|
+
str("mime"),
|
|
3247
|
+
str("uploadId"),
|
|
3248
|
+
str("filename")
|
|
3249
|
+
],
|
|
3250
|
+
doc: "Wire form `:file{src=… mime=… upload-id=… filename=…}`; binary in sidecar."
|
|
3251
|
+
},
|
|
3252
|
+
{
|
|
3253
|
+
name: "badge",
|
|
3254
|
+
group: "inline",
|
|
3255
|
+
wire: "mdc-atom-inl",
|
|
3256
|
+
attrs: [
|
|
3257
|
+
str("label"),
|
|
3258
|
+
str("color"),
|
|
3259
|
+
str("variant", "subtle")
|
|
3260
|
+
],
|
|
3261
|
+
doc: "Wire form `:badge[Label]{color=… variant=…}`."
|
|
3262
|
+
},
|
|
3263
|
+
{
|
|
3264
|
+
name: "proseIcon",
|
|
3265
|
+
group: "inline",
|
|
3266
|
+
wire: "mdc-atom-inl",
|
|
3267
|
+
mdcTag: "icon",
|
|
3268
|
+
attrs: [str("name")],
|
|
3269
|
+
doc: "Wire form `:icon{name=…}`."
|
|
3270
|
+
},
|
|
3271
|
+
{
|
|
3272
|
+
name: "kbd",
|
|
3273
|
+
group: "inline",
|
|
3274
|
+
wire: "mdc-atom-inl",
|
|
3275
|
+
attrs: [str("value")],
|
|
3276
|
+
doc: "Wire form `:kbd{value=…}`."
|
|
3277
|
+
}
|
|
3278
|
+
];
|
|
3279
|
+
const NODE_SPECS = [
|
|
3280
|
+
...VANILLA_BLOCKS,
|
|
3281
|
+
...MDC_CONTAINERS,
|
|
3282
|
+
...INLINE_AND_SPECIAL
|
|
3283
|
+
];
|
|
3284
|
+
const NODE_SPEC_BY_NAME = new Map(NODE_SPECS.map((spec) => [spec.name, spec]));
|
|
3285
|
+
|
|
3286
|
+
//#endregion
|
|
3287
|
+
//#region packages/convert/src/spec/marks.ts
|
|
3288
|
+
const MARK_SPECS = [
|
|
3289
|
+
{
|
|
3290
|
+
name: "bold",
|
|
3291
|
+
wire: "delimited",
|
|
3292
|
+
delim: "**"
|
|
3293
|
+
},
|
|
3294
|
+
{
|
|
3295
|
+
name: "italic",
|
|
3296
|
+
wire: "delimited",
|
|
3297
|
+
delim: "*"
|
|
3298
|
+
},
|
|
3299
|
+
{
|
|
3300
|
+
name: "strike",
|
|
3301
|
+
wire: "delimited",
|
|
3302
|
+
delim: "~~"
|
|
3303
|
+
},
|
|
3304
|
+
{
|
|
3305
|
+
name: "code",
|
|
3306
|
+
wire: "delimited",
|
|
3307
|
+
delim: "`"
|
|
3308
|
+
},
|
|
3309
|
+
{
|
|
3310
|
+
name: "link",
|
|
3311
|
+
wire: "link",
|
|
3312
|
+
attrs: [{
|
|
3313
|
+
key: "href",
|
|
3314
|
+
type: "string"
|
|
3315
|
+
}, {
|
|
3316
|
+
key: "title",
|
|
3317
|
+
type: "string",
|
|
3318
|
+
optional: true
|
|
3319
|
+
}]
|
|
3320
|
+
},
|
|
3321
|
+
{
|
|
3322
|
+
name: "underline",
|
|
3323
|
+
wire: "delimited",
|
|
3324
|
+
delim: "__",
|
|
3325
|
+
doc: "Disambiguated from bold by delimiter character. Two underscores = underline; two asterisks = bold."
|
|
3326
|
+
},
|
|
3327
|
+
{
|
|
3328
|
+
name: "highlight",
|
|
3329
|
+
wire: "delimited",
|
|
3330
|
+
delim: "==",
|
|
3331
|
+
doc: "Pandoc-style."
|
|
3332
|
+
},
|
|
3333
|
+
{
|
|
3334
|
+
name: "subscript",
|
|
3335
|
+
wire: "delimited",
|
|
3336
|
+
delim: "~",
|
|
3337
|
+
doc: "Single tilde; double tilde is strike."
|
|
3338
|
+
},
|
|
3339
|
+
{
|
|
3340
|
+
name: "superscript",
|
|
3341
|
+
wire: "delimited",
|
|
3342
|
+
delim: "^"
|
|
3343
|
+
},
|
|
3344
|
+
{
|
|
3345
|
+
name: "textStyle",
|
|
3346
|
+
wire: "mdc-span",
|
|
3347
|
+
attrs: [
|
|
3348
|
+
{
|
|
3349
|
+
key: "color",
|
|
3350
|
+
type: "string",
|
|
3351
|
+
optional: true
|
|
3352
|
+
},
|
|
3353
|
+
{
|
|
3354
|
+
key: "backgroundColor",
|
|
3355
|
+
type: "string",
|
|
3356
|
+
optional: true
|
|
3357
|
+
},
|
|
3358
|
+
{
|
|
3359
|
+
key: "fontSize",
|
|
3360
|
+
type: "string",
|
|
3361
|
+
optional: true
|
|
3362
|
+
},
|
|
3363
|
+
{
|
|
3364
|
+
key: "fontFamily",
|
|
3365
|
+
type: "string",
|
|
3366
|
+
optional: true
|
|
3367
|
+
}
|
|
3368
|
+
],
|
|
3369
|
+
doc: "Wire form `:span[text]{color=\"…\" font-size=\"…\"}`. Any of the attrs may be set."
|
|
3370
|
+
}
|
|
3371
|
+
];
|
|
3372
|
+
const MARK_SPEC_BY_NAME = new Map(MARK_SPECS.map((spec) => [spec.name, spec]));
|
|
3373
|
+
const MARK_SPEC_BY_DELIM = new Map(MARK_SPECS.filter((spec) => spec.wire === "delimited" && !!spec.delim).map((spec) => [spec.delim, spec]));
|
|
3374
|
+
|
|
3375
|
+
//#endregion
|
|
3376
|
+
//#region packages/convert/src/spec/universal-meta.ts
|
|
3377
|
+
const UNIVERSAL_META_KEYS = [
|
|
3378
|
+
{
|
|
3379
|
+
key: "title",
|
|
3380
|
+
type: "string",
|
|
3381
|
+
doc: "Display title; the first H1 is hoisted into this field on import."
|
|
3382
|
+
},
|
|
3383
|
+
{
|
|
3384
|
+
key: "type",
|
|
3385
|
+
type: "string",
|
|
3386
|
+
doc: "Page type (doc, kanban, table, …). Omitted on serialise when \"doc\"."
|
|
3387
|
+
},
|
|
3388
|
+
{
|
|
3389
|
+
key: "color",
|
|
3390
|
+
type: "string",
|
|
3391
|
+
doc: "Hex or CSS color name."
|
|
3392
|
+
},
|
|
3393
|
+
{
|
|
3394
|
+
key: "icon",
|
|
3395
|
+
type: "string",
|
|
3396
|
+
doc: "Lucide icon name in kebab-case."
|
|
3397
|
+
},
|
|
3398
|
+
{
|
|
3399
|
+
key: "datetimeStart",
|
|
3400
|
+
type: "iso-datetime"
|
|
3401
|
+
},
|
|
3402
|
+
{
|
|
3403
|
+
key: "datetimeEnd",
|
|
3404
|
+
type: "iso-datetime"
|
|
3405
|
+
},
|
|
3406
|
+
{
|
|
3407
|
+
key: "allDay",
|
|
3408
|
+
type: "boolean"
|
|
3409
|
+
},
|
|
3410
|
+
{
|
|
3411
|
+
key: "dateTaken",
|
|
3412
|
+
type: "iso-datetime"
|
|
3413
|
+
},
|
|
3414
|
+
{
|
|
3415
|
+
key: "dateStart",
|
|
3416
|
+
type: "iso-date",
|
|
3417
|
+
parseAliases: ["date", "created"]
|
|
3418
|
+
},
|
|
3419
|
+
{
|
|
3420
|
+
key: "dateEnd",
|
|
3421
|
+
type: "iso-date",
|
|
3422
|
+
parseAliases: ["due"]
|
|
3423
|
+
},
|
|
3424
|
+
{
|
|
3425
|
+
key: "timeStart",
|
|
3426
|
+
type: "hh-mm"
|
|
3427
|
+
},
|
|
3428
|
+
{
|
|
3429
|
+
key: "timeEnd",
|
|
3430
|
+
type: "hh-mm"
|
|
3431
|
+
},
|
|
3432
|
+
{
|
|
3433
|
+
key: "tags",
|
|
3434
|
+
type: "string[]"
|
|
3435
|
+
},
|
|
3436
|
+
{
|
|
3437
|
+
key: "checked",
|
|
3438
|
+
type: "boolean",
|
|
3439
|
+
parseAliases: ["done"]
|
|
3440
|
+
},
|
|
3441
|
+
{
|
|
3442
|
+
key: "priority",
|
|
3443
|
+
type: "integer",
|
|
3444
|
+
min: 0,
|
|
3445
|
+
max: 4,
|
|
3446
|
+
doc: "Numeric or named (low/medium/high/urgent → 1/2/3/4)."
|
|
3447
|
+
},
|
|
3448
|
+
{
|
|
3449
|
+
key: "status",
|
|
3450
|
+
type: "string"
|
|
3451
|
+
},
|
|
3452
|
+
{
|
|
3453
|
+
key: "rating",
|
|
3454
|
+
type: "number",
|
|
3455
|
+
min: 0,
|
|
3456
|
+
max: 5
|
|
3457
|
+
},
|
|
3458
|
+
{
|
|
3459
|
+
key: "url",
|
|
3460
|
+
type: "string"
|
|
3461
|
+
},
|
|
3462
|
+
{
|
|
3463
|
+
key: "email",
|
|
3464
|
+
type: "string"
|
|
3465
|
+
},
|
|
3466
|
+
{
|
|
3467
|
+
key: "phone",
|
|
3468
|
+
type: "string"
|
|
3469
|
+
},
|
|
3470
|
+
{
|
|
3471
|
+
key: "number",
|
|
3472
|
+
type: "number"
|
|
3473
|
+
},
|
|
3474
|
+
{
|
|
3475
|
+
key: "unit",
|
|
3476
|
+
type: "string"
|
|
3477
|
+
},
|
|
3478
|
+
{
|
|
3479
|
+
key: "subtitle",
|
|
3480
|
+
type: "string",
|
|
3481
|
+
parseAliases: ["description"]
|
|
3482
|
+
},
|
|
3483
|
+
{
|
|
3484
|
+
key: "note",
|
|
3485
|
+
type: "string"
|
|
3486
|
+
},
|
|
3487
|
+
{
|
|
3488
|
+
key: "taskProgress",
|
|
3489
|
+
type: "integer",
|
|
3490
|
+
min: 0,
|
|
3491
|
+
max: 100
|
|
3492
|
+
},
|
|
3493
|
+
{
|
|
3494
|
+
key: "members",
|
|
3495
|
+
type: "members"
|
|
3496
|
+
},
|
|
3497
|
+
{
|
|
3498
|
+
key: "coverUploadId",
|
|
3499
|
+
type: "string"
|
|
3500
|
+
},
|
|
3501
|
+
{
|
|
3502
|
+
key: "coverDocId",
|
|
3503
|
+
type: "string"
|
|
3504
|
+
},
|
|
3505
|
+
{
|
|
3506
|
+
key: "coverMimeType",
|
|
3507
|
+
type: "string"
|
|
3508
|
+
},
|
|
3509
|
+
{
|
|
3510
|
+
key: "geoType",
|
|
3511
|
+
type: "string-enum",
|
|
3512
|
+
values: [
|
|
3513
|
+
"marker",
|
|
3514
|
+
"line",
|
|
3515
|
+
"measure"
|
|
3516
|
+
]
|
|
3517
|
+
},
|
|
3518
|
+
{
|
|
3519
|
+
key: "geoLat",
|
|
3520
|
+
type: "number"
|
|
3521
|
+
},
|
|
3522
|
+
{
|
|
3523
|
+
key: "geoLng",
|
|
3524
|
+
type: "number"
|
|
3525
|
+
},
|
|
3526
|
+
{
|
|
3527
|
+
key: "geoDescription",
|
|
3528
|
+
type: "string"
|
|
3529
|
+
},
|
|
3530
|
+
{
|
|
3531
|
+
key: "deskX",
|
|
3532
|
+
type: "number"
|
|
3533
|
+
},
|
|
3534
|
+
{
|
|
3535
|
+
key: "deskY",
|
|
3536
|
+
type: "number"
|
|
3537
|
+
},
|
|
3538
|
+
{
|
|
3539
|
+
key: "deskZ",
|
|
3540
|
+
type: "number"
|
|
3541
|
+
},
|
|
3542
|
+
{
|
|
3543
|
+
key: "deskMode",
|
|
3544
|
+
type: "string-enum",
|
|
3545
|
+
values: [
|
|
3546
|
+
"icon",
|
|
3547
|
+
"widget-sm",
|
|
3548
|
+
"widget-lg"
|
|
3549
|
+
]
|
|
3550
|
+
},
|
|
3551
|
+
{
|
|
3552
|
+
key: "mmX",
|
|
3553
|
+
type: "number"
|
|
3554
|
+
},
|
|
3555
|
+
{
|
|
3556
|
+
key: "mmY",
|
|
3557
|
+
type: "number"
|
|
3558
|
+
},
|
|
3559
|
+
{
|
|
3560
|
+
key: "graphX",
|
|
3561
|
+
type: "number"
|
|
3562
|
+
},
|
|
3563
|
+
{
|
|
3564
|
+
key: "graphY",
|
|
3565
|
+
type: "number"
|
|
3566
|
+
},
|
|
3567
|
+
{
|
|
3568
|
+
key: "graphPinned",
|
|
3569
|
+
type: "boolean"
|
|
3570
|
+
},
|
|
3571
|
+
{
|
|
3572
|
+
key: "spX",
|
|
3573
|
+
type: "number"
|
|
3574
|
+
},
|
|
3575
|
+
{
|
|
3576
|
+
key: "spY",
|
|
3577
|
+
type: "number"
|
|
3578
|
+
},
|
|
3579
|
+
{
|
|
3580
|
+
key: "spZ",
|
|
3581
|
+
type: "number"
|
|
3582
|
+
},
|
|
3583
|
+
{
|
|
3584
|
+
key: "spRX",
|
|
3585
|
+
type: "number"
|
|
3586
|
+
},
|
|
3587
|
+
{
|
|
3588
|
+
key: "spRY",
|
|
3589
|
+
type: "number"
|
|
3590
|
+
},
|
|
3591
|
+
{
|
|
3592
|
+
key: "spRZ",
|
|
3593
|
+
type: "number"
|
|
3594
|
+
},
|
|
3595
|
+
{
|
|
3596
|
+
key: "spSX",
|
|
3597
|
+
type: "number"
|
|
3598
|
+
},
|
|
3599
|
+
{
|
|
3600
|
+
key: "spSY",
|
|
3601
|
+
type: "number"
|
|
3602
|
+
},
|
|
3603
|
+
{
|
|
3604
|
+
key: "spSZ",
|
|
3605
|
+
type: "number"
|
|
3606
|
+
},
|
|
3607
|
+
{
|
|
3608
|
+
key: "spShape",
|
|
3609
|
+
type: "string-enum",
|
|
3610
|
+
values: [
|
|
3611
|
+
"box",
|
|
3612
|
+
"sphere",
|
|
3613
|
+
"cylinder",
|
|
3614
|
+
"cone",
|
|
3615
|
+
"plane",
|
|
3616
|
+
"torus",
|
|
3617
|
+
"glb"
|
|
3618
|
+
]
|
|
3619
|
+
},
|
|
3620
|
+
{
|
|
3621
|
+
key: "spOpacity",
|
|
3622
|
+
type: "integer",
|
|
3623
|
+
min: 0,
|
|
3624
|
+
max: 100
|
|
3625
|
+
},
|
|
3626
|
+
{
|
|
3627
|
+
key: "spModelUploadId",
|
|
3628
|
+
type: "string"
|
|
3629
|
+
},
|
|
3630
|
+
{
|
|
3631
|
+
key: "spModelDocId",
|
|
3632
|
+
type: "string"
|
|
3633
|
+
},
|
|
3634
|
+
{
|
|
3635
|
+
key: "slidesTransition",
|
|
3636
|
+
type: "string-enum",
|
|
3637
|
+
values: [
|
|
3638
|
+
"none",
|
|
3639
|
+
"fade",
|
|
3640
|
+
"slide"
|
|
3641
|
+
]
|
|
3642
|
+
},
|
|
3643
|
+
{
|
|
3644
|
+
key: "slidesTheme",
|
|
3645
|
+
type: "string-enum",
|
|
3646
|
+
values: ["dark", "light"]
|
|
3647
|
+
},
|
|
3648
|
+
{
|
|
3649
|
+
key: "__schemaVersion",
|
|
3650
|
+
type: "integer",
|
|
3651
|
+
min: 0
|
|
3652
|
+
}
|
|
3653
|
+
];
|
|
3654
|
+
const UNIVERSAL_META_KEY_NAMES = new Set(UNIVERSAL_META_KEYS.map((k) => k.key));
|
|
3655
|
+
|
|
3656
|
+
//#endregion
|
|
3657
|
+
//#region packages/cli/src/commands/documents.ts
|
|
3658
|
+
/**
|
|
3659
|
+
* Document CRUD commands: read, create, rename, move, delete, type, doc.
|
|
3660
|
+
*/
|
|
3661
|
+
/** Safely read a tree map value, converting Y.Map to plain object if needed. */
|
|
3662
|
+
function toPlain$1(val) {
|
|
3663
|
+
return val instanceof yjs.Map ? val.toJSON() : val;
|
|
3664
|
+
}
|
|
3665
|
+
registerCommand({
|
|
3666
|
+
name: "doc",
|
|
3667
|
+
aliases: ["info:doc"],
|
|
3668
|
+
description: "Show document metadata (label, type, meta, dates).",
|
|
3669
|
+
usage: "doc id=<docId> | name=<label> | path=<a/b/c>",
|
|
3670
|
+
async run(conn, args) {
|
|
3671
|
+
if (!conn) return "Not connected";
|
|
3672
|
+
const docId = resolveDocument(conn, args.params, args.positional);
|
|
3673
|
+
if (!docId) return "Document not found. Specify id=, name=, or path= to identify the document.";
|
|
3674
|
+
const treeMap = conn.getTreeMap();
|
|
3675
|
+
if (!treeMap) return "Not connected";
|
|
3676
|
+
const raw = treeMap.get(docId);
|
|
3677
|
+
if (!raw) return `Document ${docId} not found in tree.`;
|
|
3678
|
+
const entry = toPlain$1(raw);
|
|
3679
|
+
const lines = [
|
|
3680
|
+
`id: ${docId}`,
|
|
3681
|
+
`label: ${entry.label || "Untitled"}`,
|
|
3682
|
+
`type: ${entry.type ?? "—"}`,
|
|
3683
|
+
`parent: ${entry.parentId ?? "(root)"}`,
|
|
3684
|
+
`order: ${entry.order ?? 0}`,
|
|
3685
|
+
`created: ${entry.createdAt ? new Date(entry.createdAt).toISOString() : "—"}`,
|
|
3686
|
+
`updated: ${entry.updatedAt ? new Date(entry.updatedAt).toISOString() : "—"} (${relativeTime(entry.updatedAt)})`
|
|
3687
|
+
];
|
|
3688
|
+
if (entry.meta && Object.keys(entry.meta).length > 0) {
|
|
3689
|
+
lines.push(`meta:`);
|
|
3690
|
+
for (const [k, v] of Object.entries(entry.meta)) lines.push(` ${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`);
|
|
3691
|
+
}
|
|
3692
|
+
const children = childrenOf(readEntries(treeMap), docId);
|
|
3693
|
+
lines.push(`children: ${children.length}`);
|
|
3694
|
+
return lines.join("\n");
|
|
3695
|
+
}
|
|
3696
|
+
});
|
|
3697
|
+
registerCommand({
|
|
3698
|
+
name: "read",
|
|
3699
|
+
aliases: ["cat", "r"],
|
|
3700
|
+
description: "Read document content as markdown.",
|
|
3701
|
+
usage: "read id=<docId> | name=<label> | path=<a/b/c> [--format=json|md]",
|
|
3702
|
+
async run(conn, args) {
|
|
3703
|
+
if (!conn) return "Not connected";
|
|
2445
3704
|
const docId = resolveDocument(conn, args.params, args.positional);
|
|
2446
3705
|
if (!docId) return "Document not found. Specify id=, name=, or path= to identify the document.";
|
|
2447
3706
|
try {
|
|
@@ -3123,23 +4382,24 @@ registerCommand({
|
|
|
3123
4382
|
registerCommand({
|
|
3124
4383
|
name: "chat",
|
|
3125
4384
|
aliases: ["send"],
|
|
3126
|
-
description: "Send a chat message to a channel.",
|
|
3127
|
-
usage: "chat
|
|
4385
|
+
description: "Send a chat message to a channel doc.",
|
|
4386
|
+
usage: "chat channel_doc_id=<docId> text=<message>",
|
|
3128
4387
|
async run(conn, args) {
|
|
3129
4388
|
if (!conn) return "Not connected";
|
|
3130
4389
|
const rootProvider = conn.rootProvider;
|
|
3131
4390
|
if (!rootProvider) return "Not connected";
|
|
3132
|
-
const
|
|
3133
|
-
if (!
|
|
4391
|
+
const raw = args.params["channel_doc_id"] || args.params["channel"];
|
|
4392
|
+
if (!raw) return "Missing required parameter: channel_doc_id=<docId>";
|
|
4393
|
+
const channel_doc_id = raw.startsWith("group:") ? raw.slice(6) : raw;
|
|
3134
4394
|
const text = args.params["text"] || args.params["content"] || args.positional[0];
|
|
3135
4395
|
if (!text) return "Missing required parameter: text=<message>";
|
|
3136
4396
|
rootProvider.sendStateless(JSON.stringify({
|
|
3137
|
-
type: "
|
|
3138
|
-
|
|
4397
|
+
type: "messages:send",
|
|
4398
|
+
channel_doc_id,
|
|
3139
4399
|
content: text,
|
|
3140
|
-
|
|
4400
|
+
mentions: []
|
|
3141
4401
|
}));
|
|
3142
|
-
return `Sent to ${
|
|
4402
|
+
return `Sent to ${channel_doc_id}`;
|
|
3143
4403
|
}
|
|
3144
4404
|
});
|
|
3145
4405
|
|
|
@@ -3309,6 +4569,16 @@ const PAGE_TYPES = {
|
|
|
3309
4569
|
core: true,
|
|
3310
4570
|
supportsChildren: true
|
|
3311
4571
|
},
|
|
4572
|
+
prose: {
|
|
4573
|
+
key: "prose",
|
|
4574
|
+
label: "Prose",
|
|
4575
|
+
icon: "pen-tool",
|
|
4576
|
+
description: "Long-form prose with serif typography and a narrow readable measure",
|
|
4577
|
+
core: true,
|
|
4578
|
+
supportsChildren: true,
|
|
4579
|
+
childLabel: "Item",
|
|
4580
|
+
defaultDepth: -1
|
|
4581
|
+
},
|
|
3312
4582
|
kanban: {
|
|
3313
4583
|
key: "kanban",
|
|
3314
4584
|
label: "Kanban",
|
|
@@ -4093,6 +5363,695 @@ registerCommand({
|
|
|
4093
5363
|
}
|
|
4094
5364
|
});
|
|
4095
5365
|
|
|
5366
|
+
//#endregion
|
|
5367
|
+
//#region packages/cli/src/commands/wiki/wikipedia.ts
|
|
5368
|
+
/**
|
|
5369
|
+
* Rate-limited wrapper around wtf_wikipedia + wtf-plugin-api.
|
|
5370
|
+
*
|
|
5371
|
+
* Responsibilities:
|
|
5372
|
+
* - Throttle requests to respect Wikimedia API etiquette
|
|
5373
|
+
* - Cache parsed Documents by canonical title
|
|
5374
|
+
* - Resolve redirects so callers always see the redirect target
|
|
5375
|
+
* - Expose getCategoryPages via wtf-plugin-api
|
|
5376
|
+
*/
|
|
5377
|
+
let pluginExtended = false;
|
|
5378
|
+
function ensurePlugin() {
|
|
5379
|
+
if (pluginExtended) return;
|
|
5380
|
+
wtf_wikipedia.default.extend(wtf_plugin_api.default);
|
|
5381
|
+
pluginExtended = true;
|
|
5382
|
+
}
|
|
5383
|
+
/** A token-bucket-ish throttle: at most `rate` calls per second, FIFO. */
|
|
5384
|
+
var RateLimiter = class {
|
|
5385
|
+
lastTickMs = 0;
|
|
5386
|
+
constructor(intervalMs) {
|
|
5387
|
+
this.intervalMs = intervalMs;
|
|
5388
|
+
}
|
|
5389
|
+
async wait() {
|
|
5390
|
+
const now = Date.now();
|
|
5391
|
+
const earliest = this.lastTickMs + this.intervalMs;
|
|
5392
|
+
if (now < earliest) await new Promise((r) => setTimeout(r, earliest - now));
|
|
5393
|
+
this.lastTickMs = Math.max(now, earliest);
|
|
5394
|
+
}
|
|
5395
|
+
};
|
|
5396
|
+
var WikipediaClient = class {
|
|
5397
|
+
cache = /* @__PURE__ */ new Map();
|
|
5398
|
+
redirects = /* @__PURE__ */ new Map();
|
|
5399
|
+
limiter;
|
|
5400
|
+
fetchOpts;
|
|
5401
|
+
constructor(config) {
|
|
5402
|
+
this.config = config;
|
|
5403
|
+
ensurePlugin();
|
|
5404
|
+
this.limiter = new RateLimiter(Math.max(50, Math.floor(1e3 / Math.max(.1, config.rate))));
|
|
5405
|
+
this.fetchOpts = {
|
|
5406
|
+
lang: config.lang,
|
|
5407
|
+
"Api-User-Agent": config.userAgent,
|
|
5408
|
+
follow_redirects: true
|
|
5409
|
+
};
|
|
5410
|
+
if (config.domain) this.fetchOpts.domain = config.domain;
|
|
5411
|
+
}
|
|
5412
|
+
/**
|
|
5413
|
+
* Fetch and parse a Wikipedia article.
|
|
5414
|
+
* - Returns the cached Document if we've seen this title before.
|
|
5415
|
+
* - Follows redirects and caches under both source and target titles.
|
|
5416
|
+
* - Returns null when the page does not exist.
|
|
5417
|
+
*/
|
|
5418
|
+
async fetchArticle(rawTitle) {
|
|
5419
|
+
const title = canonicalTitle(rawTitle);
|
|
5420
|
+
if (this.cache.has(title)) return this.cache.get(title);
|
|
5421
|
+
if (this.redirects.has(title)) {
|
|
5422
|
+
const target = this.redirects.get(title);
|
|
5423
|
+
return this.cache.get(target) ?? null;
|
|
5424
|
+
}
|
|
5425
|
+
await this.limiter.wait();
|
|
5426
|
+
let doc;
|
|
5427
|
+
try {
|
|
5428
|
+
doc = await wtf_wikipedia.default.fetch(title, this.fetchOpts);
|
|
5429
|
+
} catch (err) {
|
|
5430
|
+
throw new Error(`Wikipedia fetch failed for "${title}": ${err?.message ?? err}`);
|
|
5431
|
+
}
|
|
5432
|
+
if (!doc) return null;
|
|
5433
|
+
if (typeof doc.isRedirect === "function" && doc.isRedirect()) {
|
|
5434
|
+
const target = doc.redirectTo?.()?.page;
|
|
5435
|
+
if (typeof target === "string") {
|
|
5436
|
+
this.redirects.set(title, canonicalTitle(target));
|
|
5437
|
+
return await this.fetchArticle(target);
|
|
5438
|
+
}
|
|
5439
|
+
}
|
|
5440
|
+
const resolvedTitle = canonicalTitle(doc.title?.() ?? title);
|
|
5441
|
+
this.cache.set(resolvedTitle, doc);
|
|
5442
|
+
if (resolvedTitle !== title) this.redirects.set(title, resolvedTitle);
|
|
5443
|
+
return doc;
|
|
5444
|
+
}
|
|
5445
|
+
/**
|
|
5446
|
+
* Fetch the member pages of a category (and optionally sub-categories).
|
|
5447
|
+
* @param category Category title (with or without "Category:" prefix).
|
|
5448
|
+
* @param recursive Whether to traverse sub-categories.
|
|
5449
|
+
* @param maxDepth Recursion depth when recursive=true.
|
|
5450
|
+
*/
|
|
5451
|
+
async fetchCategoryPages(category, recursive, maxDepth) {
|
|
5452
|
+
await this.limiter.wait();
|
|
5453
|
+
const opts = {
|
|
5454
|
+
lang: this.config.lang,
|
|
5455
|
+
"Api-User-Agent": this.config.userAgent,
|
|
5456
|
+
recursive,
|
|
5457
|
+
maxDepth
|
|
5458
|
+
};
|
|
5459
|
+
if (this.config.domain) opts.domain = this.config.domain;
|
|
5460
|
+
return (await wtf_wikipedia.default.getCategoryPages(category, opts) ?? []).map((m) => ({
|
|
5461
|
+
title: canonicalTitle(m.title),
|
|
5462
|
+
type: m.type === "subcat" ? "subcat" : "page"
|
|
5463
|
+
}));
|
|
5464
|
+
}
|
|
5465
|
+
};
|
|
5466
|
+
/** Normalize a Wikipedia title — trim, collapse spaces, strip leading/trailing colons. */
|
|
5467
|
+
function canonicalTitle(s) {
|
|
5468
|
+
return (s ?? "").toString().replace(/_/g, " ").replace(/\s+/g, " ").trim();
|
|
5469
|
+
}
|
|
5470
|
+
/** Detect a category-namespaced title. */
|
|
5471
|
+
const CATEGORY_PREFIX = /^(Category|Catégorie|Kategorie|Categoría|Categoria|Categorie|Kategoria):/i;
|
|
5472
|
+
function isCategoryTitle(title) {
|
|
5473
|
+
return CATEGORY_PREFIX.test(title);
|
|
5474
|
+
}
|
|
5475
|
+
/** Strip the "Category:" prefix for display. */
|
|
5476
|
+
function stripCategoryPrefix(title) {
|
|
5477
|
+
return title.replace(CATEGORY_PREFIX, "").trim();
|
|
5478
|
+
}
|
|
5479
|
+
|
|
5480
|
+
//#endregion
|
|
5481
|
+
//#region packages/cli/src/commands/wiki/snapshot.ts
|
|
5482
|
+
function snapshotArticle(doc, title) {
|
|
5483
|
+
return {
|
|
5484
|
+
title,
|
|
5485
|
+
linkTitles: collectLinkTitles(doc),
|
|
5486
|
+
categories: collectCategories(doc),
|
|
5487
|
+
sections: snapshotSections(doc.sections?.() ?? []),
|
|
5488
|
+
infobox: snapshotInfobox(doc.infobox?.()),
|
|
5489
|
+
lead: leadParagraph(doc),
|
|
5490
|
+
url: typeof doc.url === "function" ? doc.url() : null
|
|
5491
|
+
};
|
|
5492
|
+
}
|
|
5493
|
+
function prettyCategoryLabel(catTitle) {
|
|
5494
|
+
return stripCategoryPrefix(catTitle);
|
|
5495
|
+
}
|
|
5496
|
+
function collectLinkTitles(doc) {
|
|
5497
|
+
const links = doc.links?.() ?? [];
|
|
5498
|
+
const out = /* @__PURE__ */ new Set();
|
|
5499
|
+
for (const l of links) {
|
|
5500
|
+
if (!l) continue;
|
|
5501
|
+
const page = typeof l.page === "function" ? l.page() : null;
|
|
5502
|
+
if (typeof page !== "string" || page.length === 0) continue;
|
|
5503
|
+
if (isCategoryTitle(page)) continue;
|
|
5504
|
+
out.add(canonicalTitle(page));
|
|
5505
|
+
}
|
|
5506
|
+
return [...out];
|
|
5507
|
+
}
|
|
5508
|
+
function collectCategories(doc) {
|
|
5509
|
+
const out = [];
|
|
5510
|
+
for (const c of doc.categories?.() ?? []) {
|
|
5511
|
+
const norm = canonicalTitle(c);
|
|
5512
|
+
if (norm) out.push(norm);
|
|
5513
|
+
}
|
|
5514
|
+
return out;
|
|
5515
|
+
}
|
|
5516
|
+
function snapshotSections(rawSections) {
|
|
5517
|
+
const all = rawSections.map((s) => ({
|
|
5518
|
+
raw: s,
|
|
5519
|
+
title: s.title?.() || "",
|
|
5520
|
+
parentRef: typeof s.parent === "function" ? s.parent() : null,
|
|
5521
|
+
children: []
|
|
5522
|
+
}));
|
|
5523
|
+
const byRaw = /* @__PURE__ */ new Map();
|
|
5524
|
+
for (const s of all) byRaw.set(s.raw, s);
|
|
5525
|
+
const roots = [];
|
|
5526
|
+
for (const s of all) if (s.parentRef && byRaw.has(s.parentRef)) byRaw.get(s.parentRef).children.push(materialize(s));
|
|
5527
|
+
else roots.push(s);
|
|
5528
|
+
return roots.map(materialize);
|
|
5529
|
+
}
|
|
5530
|
+
function materialize(node) {
|
|
5531
|
+
const lists = node.raw.lists?.() ?? [];
|
|
5532
|
+
const paragraphs = node.raw.paragraphs?.() ?? [];
|
|
5533
|
+
let listLength = 0;
|
|
5534
|
+
for (const l of lists) {
|
|
5535
|
+
const lines = l.lines?.() ?? [];
|
|
5536
|
+
listLength += lines.length;
|
|
5537
|
+
}
|
|
5538
|
+
const isList = lists.length > 0 && (paragraphs.length === 0 || listLength >= paragraphs.length * 2);
|
|
5539
|
+
const bodyParts = [];
|
|
5540
|
+
for (const p of paragraphs) {
|
|
5541
|
+
const md = paragraphMarkdown(p);
|
|
5542
|
+
if (md) bodyParts.push(md);
|
|
5543
|
+
}
|
|
5544
|
+
for (const l of lists) {
|
|
5545
|
+
const lines = l.lines?.() ?? [];
|
|
5546
|
+
for (const line of lines) {
|
|
5547
|
+
const text = lineText(line);
|
|
5548
|
+
if (text) bodyParts.push(`- ${text}`);
|
|
5549
|
+
}
|
|
5550
|
+
}
|
|
5551
|
+
return {
|
|
5552
|
+
title: node.title,
|
|
5553
|
+
body: bodyParts.join("\n\n"),
|
|
5554
|
+
isList,
|
|
5555
|
+
listLength,
|
|
5556
|
+
children: node.children
|
|
5557
|
+
};
|
|
5558
|
+
}
|
|
5559
|
+
function snapshotInfobox(box) {
|
|
5560
|
+
if (!box) return void 0;
|
|
5561
|
+
const data = typeof box.json === "function" ? box.json() : null;
|
|
5562
|
+
if (!data || typeof data !== "object") return void 0;
|
|
5563
|
+
const rows = [];
|
|
5564
|
+
for (const [key, val] of Object.entries(data)) {
|
|
5565
|
+
const value = stringifyInfoboxValue(val);
|
|
5566
|
+
if (!value) continue;
|
|
5567
|
+
rows.push({
|
|
5568
|
+
key: humanKey(key),
|
|
5569
|
+
value
|
|
5570
|
+
});
|
|
5571
|
+
}
|
|
5572
|
+
return rows.length > 0 ? rows : void 0;
|
|
5573
|
+
}
|
|
5574
|
+
function stringifyInfoboxValue(val) {
|
|
5575
|
+
if (val == null) return "";
|
|
5576
|
+
if (typeof val === "string") return val;
|
|
5577
|
+
if (typeof val === "number" || typeof val === "boolean") return String(val);
|
|
5578
|
+
if (Array.isArray(val)) return val.map(stringifyInfoboxValue).filter(Boolean).join(", ");
|
|
5579
|
+
if (typeof val === "object") {
|
|
5580
|
+
const o = val;
|
|
5581
|
+
if (typeof o.text === "string") return o.text;
|
|
5582
|
+
if (typeof o.number === "number") return String(o.number);
|
|
5583
|
+
}
|
|
5584
|
+
return "";
|
|
5585
|
+
}
|
|
5586
|
+
function humanKey(k) {
|
|
5587
|
+
return k.replace(/_/g, " ").replace(/^./, (m) => m.toUpperCase());
|
|
5588
|
+
}
|
|
5589
|
+
function leadParagraph(doc) {
|
|
5590
|
+
const first = (doc.paragraphs?.() ?? [])[0];
|
|
5591
|
+
if (!first) return "";
|
|
5592
|
+
return paragraphMarkdown(first);
|
|
5593
|
+
}
|
|
5594
|
+
/**
|
|
5595
|
+
* Render a paragraph as markdown, replacing internal links with `[[Title]]`.
|
|
5596
|
+
* The streaming orchestrator's link rewriter later swaps `[[Title]]` →
|
|
5597
|
+
* `[[docId|label]]` once IDs are known.
|
|
5598
|
+
*/
|
|
5599
|
+
function paragraphMarkdown(paragraph) {
|
|
5600
|
+
const sentences = paragraph.sentences?.() ?? [];
|
|
5601
|
+
const out = [];
|
|
5602
|
+
for (const s of sentences) out.push(sentenceWithWikilinks(s));
|
|
5603
|
+
return out.join(" ").trim();
|
|
5604
|
+
}
|
|
5605
|
+
function sentenceWithWikilinks(sentence) {
|
|
5606
|
+
const text = (sentence.text?.() ?? "").toString();
|
|
5607
|
+
const links = sentence.links?.() ?? [];
|
|
5608
|
+
if (links.length === 0) return text;
|
|
5609
|
+
let result = text;
|
|
5610
|
+
const replacements = links.map((l) => {
|
|
5611
|
+
const page = typeof l.page === "function" ? l.page() : null;
|
|
5612
|
+
const display = typeof l.text === "function" ? l.text() : null;
|
|
5613
|
+
if (typeof page !== "string" || page.length === 0) return null;
|
|
5614
|
+
if (isCategoryTitle(page)) return null;
|
|
5615
|
+
const shown = display && display.length > 0 ? display : page;
|
|
5616
|
+
return {
|
|
5617
|
+
page: canonicalTitle(page),
|
|
5618
|
+
shown
|
|
5619
|
+
};
|
|
5620
|
+
}).filter((x) => x !== null).sort((a, b) => b.shown.length - a.shown.length);
|
|
5621
|
+
for (const { page, shown } of replacements) {
|
|
5622
|
+
if (!result.includes(shown)) continue;
|
|
5623
|
+
const replacement = shown === page ? `[[${page}]]` : `[[${page}|${shown}]]`;
|
|
5624
|
+
result = result.replace(shown, replacement);
|
|
5625
|
+
}
|
|
5626
|
+
return result;
|
|
5627
|
+
}
|
|
5628
|
+
function lineText(line) {
|
|
5629
|
+
if (!line) return "";
|
|
5630
|
+
if (typeof line === "string") return line;
|
|
5631
|
+
if (typeof line.text === "string") return line.text;
|
|
5632
|
+
if (typeof line.text === "function") return line.text();
|
|
5633
|
+
return "";
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5636
|
+
//#endregion
|
|
5637
|
+
//#region packages/cli/src/commands/wiki/render.ts
|
|
5638
|
+
const ICONS = {
|
|
5639
|
+
graph: "git-fork",
|
|
5640
|
+
article: "book-open",
|
|
5641
|
+
category: "tag",
|
|
5642
|
+
infobox: "info",
|
|
5643
|
+
outline: "list",
|
|
5644
|
+
gallery: "images",
|
|
5645
|
+
section: "pilcrow",
|
|
5646
|
+
categories: "tags"
|
|
5647
|
+
};
|
|
5648
|
+
/** Decide a page type for a section based on its shape. */
|
|
5649
|
+
function pickSectionType(section) {
|
|
5650
|
+
if (section.children.length > 0) return {
|
|
5651
|
+
type: "outline",
|
|
5652
|
+
icon: ICONS.outline
|
|
5653
|
+
};
|
|
5654
|
+
if (section.isList && section.listLength >= 5) return {
|
|
5655
|
+
type: "outline",
|
|
5656
|
+
icon: ICONS.outline
|
|
5657
|
+
};
|
|
5658
|
+
return {
|
|
5659
|
+
type: "doc",
|
|
5660
|
+
icon: ICONS.section
|
|
5661
|
+
};
|
|
5662
|
+
}
|
|
5663
|
+
/** Render the lead paragraph as the article-doc body. */
|
|
5664
|
+
function renderArticleLead(article) {
|
|
5665
|
+
return article.lead ?? "";
|
|
5666
|
+
}
|
|
5667
|
+
/** Render the article as a single doc, sections + infobox inlined. */
|
|
5668
|
+
function renderArticleSingleDoc(article) {
|
|
5669
|
+
const parts = [];
|
|
5670
|
+
if (article.lead) parts.push(article.lead);
|
|
5671
|
+
if (article.infobox && article.infobox.length > 0) parts.push("## Infobox", renderInfoboxBody(article.infobox));
|
|
5672
|
+
for (const section of article.sections) parts.push(...renderSectionInline(section, 2));
|
|
5673
|
+
return parts.join("\n\n");
|
|
5674
|
+
}
|
|
5675
|
+
function renderSectionInline(section, level) {
|
|
5676
|
+
const out = [];
|
|
5677
|
+
const prefix = "#".repeat(Math.min(6, level));
|
|
5678
|
+
if (section.title) out.push(`${prefix} ${section.title}`);
|
|
5679
|
+
if (section.body.trim()) out.push(section.body);
|
|
5680
|
+
for (const child of section.children) out.push(...renderSectionInline(child, level + 1));
|
|
5681
|
+
return out;
|
|
5682
|
+
}
|
|
5683
|
+
function renderInfoboxBody(rows) {
|
|
5684
|
+
return rows.map((r) => `- **${r.key}:** ${r.value}`).join("\n");
|
|
5685
|
+
}
|
|
5686
|
+
function renderCategoryBody(members, subcategories) {
|
|
5687
|
+
const parts = [];
|
|
5688
|
+
if (members.length > 0) {
|
|
5689
|
+
parts.push("## Pages");
|
|
5690
|
+
parts.push(members.map((m) => `- [[${m}]]`).join("\n"));
|
|
5691
|
+
}
|
|
5692
|
+
if (subcategories.length > 0) {
|
|
5693
|
+
parts.push("## Sub-categories");
|
|
5694
|
+
parts.push(subcategories.map((s) => `- ${s}`).join("\n"));
|
|
5695
|
+
}
|
|
5696
|
+
return parts.join("\n\n");
|
|
5697
|
+
}
|
|
5698
|
+
/**
|
|
5699
|
+
* Replace `[[Title]]` / `[[Title|Alias]]` in markdown with
|
|
5700
|
+
* `[[docId|label]]` using the title→docId map. Unresolved titles fall
|
|
5701
|
+
* back to plain text (their alias or original title).
|
|
5702
|
+
*/
|
|
5703
|
+
function rewriteLinks(markdown, titleToDocId) {
|
|
5704
|
+
return markdown.replace(/\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]/g, (_match, target, alias) => {
|
|
5705
|
+
const title = target.trim();
|
|
5706
|
+
const docId = titleToDocId.get(title);
|
|
5707
|
+
const display = (alias && alias.trim().length > 0 ? alias : title).trim();
|
|
5708
|
+
if (!docId) return display;
|
|
5709
|
+
return `[[${docId}|${display}]]`;
|
|
5710
|
+
});
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5713
|
+
//#endregion
|
|
5714
|
+
//#region packages/cli/src/commands/wiki/connect.ts
|
|
5715
|
+
/**
|
|
5716
|
+
* Open a DocumentManager session for the wiki command, mirroring the
|
|
5717
|
+
* auth/register flow that CLIConnection uses but using the modern public API.
|
|
5718
|
+
*
|
|
5719
|
+
* Reuses the CLI's Ed25519 keypair handling (loadOrCreateKeypair, signChallenge)
|
|
5720
|
+
* so the wiki command authenticates with the same identity as every other
|
|
5721
|
+
* subcommand.
|
|
5722
|
+
*/
|
|
5723
|
+
async function openSession(config) {
|
|
5724
|
+
const keypair = await loadOrCreateKeypair(config.keyFile);
|
|
5725
|
+
const sign = (challenge) => Promise.resolve(signChallenge(challenge, keypair.privateKey));
|
|
5726
|
+
const dm = new _abraca_dabra.DocumentManager({
|
|
5727
|
+
url: config.url,
|
|
5728
|
+
name: config.name ?? "Wiki Extractor",
|
|
5729
|
+
color: config.color,
|
|
5730
|
+
quiet: config.quiet
|
|
5731
|
+
});
|
|
5732
|
+
try {
|
|
5733
|
+
await dm.client.loginWithKey(keypair.publicKeyB64, sign);
|
|
5734
|
+
} catch (err) {
|
|
5735
|
+
const status = err?.status ?? err?.response?.status;
|
|
5736
|
+
if (status === 404 || status === 422) {
|
|
5737
|
+
if (!config.quiet) console.error("[abracadabra] Key not registered, creating new account...");
|
|
5738
|
+
await dm.client.registerWithKey({
|
|
5739
|
+
publicKey: keypair.publicKeyB64,
|
|
5740
|
+
username: (config.name ?? "wiki-extractor").replace(/\s+/g, "-").toLowerCase(),
|
|
5741
|
+
displayName: config.name ?? "Wiki Extractor",
|
|
5742
|
+
deviceName: "CLI Wiki",
|
|
5743
|
+
inviteCode: config.inviteCode
|
|
5744
|
+
});
|
|
5745
|
+
await dm.client.loginWithKey(keypair.publicKeyB64, sign);
|
|
5746
|
+
} else throw err;
|
|
5747
|
+
}
|
|
5748
|
+
await dm.connect();
|
|
5749
|
+
const rootDocId = dm.rootDocId;
|
|
5750
|
+
if (!rootDocId) throw new Error("Connected but no rootDocId — server has no spaces.");
|
|
5751
|
+
return {
|
|
5752
|
+
dm,
|
|
5753
|
+
rootDocId
|
|
5754
|
+
};
|
|
5755
|
+
}
|
|
5756
|
+
|
|
5757
|
+
//#endregion
|
|
5758
|
+
//#region packages/cli/src/commands/wiki/index.ts
|
|
5759
|
+
registerCommand({
|
|
5760
|
+
name: "wiki",
|
|
5761
|
+
aliases: ["wikipedia"],
|
|
5762
|
+
description: "Fetch Wikipedia articles into a graph of docs (streaming).",
|
|
5763
|
+
usage: [
|
|
5764
|
+
"wiki \"<Article Title>\"",
|
|
5765
|
+
" mode=single|split single doc per article OR split into sections+infobox [split]",
|
|
5766
|
+
" depth=<n> follow internal links to depth N [1]",
|
|
5767
|
+
" category-depth=<n> recurse into sub-categories [1]",
|
|
5768
|
+
" lang=<code> wiki language [en]",
|
|
5769
|
+
" domain=<host> 3rd-party MediaWiki host (overrides lang)",
|
|
5770
|
+
" parent=<docId> parent doc for the new graph [active space root]",
|
|
5771
|
+
" user-agent=<str> Api-User-Agent header (REQUIRED by Wikimedia etiquette)",
|
|
5772
|
+
" rate=<rps> max wikipedia requests per second [3]",
|
|
5773
|
+
" --include-categories expand each article's categories into nested graphs",
|
|
5774
|
+
" --dry-run fetch only the entry article, print outline, no writes"
|
|
5775
|
+
].join("\n"),
|
|
5776
|
+
async run(_conn, args) {
|
|
5777
|
+
const opts = parseOptions(args);
|
|
5778
|
+
if (typeof opts === "string") return opts;
|
|
5779
|
+
const log = (msg) => {
|
|
5780
|
+
if (!args.flags.has("quiet") && !args.flags.has("q")) console.error(`[wiki] ${msg}`);
|
|
5781
|
+
};
|
|
5782
|
+
const wp = new WikipediaClient({
|
|
5783
|
+
lang: opts.lang,
|
|
5784
|
+
domain: opts.domain,
|
|
5785
|
+
userAgent: opts.userAgent,
|
|
5786
|
+
rate: opts.rate
|
|
5787
|
+
});
|
|
5788
|
+
if (opts.dryRun) {
|
|
5789
|
+
log(`fetch ${opts.title}`);
|
|
5790
|
+
const doc = await wp.fetchArticle(opts.title);
|
|
5791
|
+
if (!doc) return `Article not found: "${opts.title}"`;
|
|
5792
|
+
const snap = snapshotArticle(doc, canonicalTitle(doc.title?.() ?? opts.title));
|
|
5793
|
+
return [
|
|
5794
|
+
`Entry: ${snap.title}`,
|
|
5795
|
+
`URL: ${snap.url ?? "(none)"}`,
|
|
5796
|
+
`Internal links: ${snap.linkTitles.length}`,
|
|
5797
|
+
`Categories: ${snap.categories.length}`,
|
|
5798
|
+
`Sections: ${snap.sections.length}`,
|
|
5799
|
+
`Has infobox: ${snap.infobox && snap.infobox.length > 0 ? "yes" : "no"}`,
|
|
5800
|
+
"",
|
|
5801
|
+
"── Sections ──",
|
|
5802
|
+
printSections(snap.sections, "")
|
|
5803
|
+
].join("\n");
|
|
5804
|
+
}
|
|
5805
|
+
const env = globalThis.process?.env ?? {};
|
|
5806
|
+
const url = env["ABRA_URL"];
|
|
5807
|
+
if (!url) return "ABRA_URL is required to write to the server. Set it or pass --dry-run.";
|
|
5808
|
+
const { dm } = await openSession({
|
|
5809
|
+
url,
|
|
5810
|
+
name: env["ABRA_NAME"],
|
|
5811
|
+
color: env["ABRA_COLOR"],
|
|
5812
|
+
inviteCode: env["ABRA_INVITE_CODE"],
|
|
5813
|
+
keyFile: env["ABRA_KEY_FILE"],
|
|
5814
|
+
quiet: args.flags.has("quiet") || args.flags.has("q")
|
|
5815
|
+
});
|
|
5816
|
+
try {
|
|
5817
|
+
const result = await runStreaming(dm, wp, opts, log);
|
|
5818
|
+
return [`Done. Created ${result.articleCount} articles${result.categoryCount > 0 ? ` + ${result.categoryCount} categories` : ""}.`, `Root: ${result.rootDocId}`].join("\n");
|
|
5819
|
+
} finally {
|
|
5820
|
+
await dm.destroy().catch(() => {});
|
|
5821
|
+
}
|
|
5822
|
+
}
|
|
5823
|
+
});
|
|
5824
|
+
async function runStreaming(dm, wp, opts, log) {
|
|
5825
|
+
const titleToDocId = /* @__PURE__ */ new Map();
|
|
5826
|
+
const fetched = /* @__PURE__ */ new Map();
|
|
5827
|
+
const childrenCreated = /* @__PURE__ */ new Set();
|
|
5828
|
+
const categoryToDocId = /* @__PURE__ */ new Map();
|
|
5829
|
+
let categoriesContainerId = null;
|
|
5830
|
+
log(`fetch ${opts.title}`);
|
|
5831
|
+
const entryDoc = await wp.fetchArticle(opts.title);
|
|
5832
|
+
if (!entryDoc) throw new Error(`Article not found: "${opts.title}"`);
|
|
5833
|
+
const entryTitle = canonicalTitle(entryDoc.title?.() ?? opts.title);
|
|
5834
|
+
const entrySnap = snapshotArticle(entryDoc, entryTitle);
|
|
5835
|
+
fetched.set(entryTitle, entrySnap);
|
|
5836
|
+
const rootEntry = dm.tree.create({
|
|
5837
|
+
parentId: opts.parentDocId ?? null,
|
|
5838
|
+
label: entryTitle,
|
|
5839
|
+
type: "graph",
|
|
5840
|
+
meta: { icon: ICONS.graph }
|
|
5841
|
+
});
|
|
5842
|
+
log(`+ ${rootEntry.id.slice(0, 8)}… ${entryTitle} (graph)`);
|
|
5843
|
+
const entryArticleId = createArticleShell(dm, entrySnap, rootEntry.id, log);
|
|
5844
|
+
titleToDocId.set(entryTitle, entryArticleId);
|
|
5845
|
+
const queue = [{
|
|
5846
|
+
title: entryTitle,
|
|
5847
|
+
depth: 0
|
|
5848
|
+
}];
|
|
5849
|
+
let articleCount = 0;
|
|
5850
|
+
while (queue.length > 0) {
|
|
5851
|
+
const { title, depth } = queue.shift();
|
|
5852
|
+
const articleDocId = titleToDocId.get(title);
|
|
5853
|
+
let snap = fetched.get(title);
|
|
5854
|
+
if (!snap) {
|
|
5855
|
+
log(`fetch [d${depth}] ${title}`);
|
|
5856
|
+
try {
|
|
5857
|
+
const doc = await wp.fetchArticle(title);
|
|
5858
|
+
if (!doc) {
|
|
5859
|
+
log(` not found — leaving stub`);
|
|
5860
|
+
continue;
|
|
5861
|
+
}
|
|
5862
|
+
snap = snapshotArticle(doc, canonicalTitle(doc.title?.() ?? title));
|
|
5863
|
+
fetched.set(title, snap);
|
|
5864
|
+
} catch (err) {
|
|
5865
|
+
log(`! fetch failed: ${err?.message ?? err}`);
|
|
5866
|
+
continue;
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
5869
|
+
if (opts.mode === "split" && !childrenCreated.has(title)) {
|
|
5870
|
+
createArticleChildren(dm, snap, articleDocId, log);
|
|
5871
|
+
childrenCreated.add(title);
|
|
5872
|
+
}
|
|
5873
|
+
if (depth < opts.depth) for (const linkTitle of snap.linkTitles) {
|
|
5874
|
+
if (titleToDocId.has(linkTitle)) continue;
|
|
5875
|
+
const shell = dm.tree.create({
|
|
5876
|
+
parentId: rootEntry.id,
|
|
5877
|
+
label: linkTitle,
|
|
5878
|
+
type: "doc",
|
|
5879
|
+
meta: { icon: ICONS.article }
|
|
5880
|
+
});
|
|
5881
|
+
titleToDocId.set(linkTitle, shell.id);
|
|
5882
|
+
queue.push({
|
|
5883
|
+
title: linkTitle,
|
|
5884
|
+
depth: depth + 1
|
|
5885
|
+
});
|
|
5886
|
+
log(`+ ${shell.id.slice(0, 8)}… ${linkTitle} (doc, shell)`);
|
|
5887
|
+
}
|
|
5888
|
+
if (opts.includeCategories && snap.categories.length > 0) {
|
|
5889
|
+
if (!categoriesContainerId) {
|
|
5890
|
+
const c = dm.tree.create({
|
|
5891
|
+
parentId: rootEntry.id,
|
|
5892
|
+
label: "Categories",
|
|
5893
|
+
type: "graph",
|
|
5894
|
+
meta: { icon: ICONS.categories }
|
|
5895
|
+
});
|
|
5896
|
+
categoriesContainerId = c.id;
|
|
5897
|
+
log(`+ ${c.id.slice(0, 8)}… Categories (graph)`);
|
|
5898
|
+
}
|
|
5899
|
+
for (const catTitle of snap.categories) {
|
|
5900
|
+
if (categoryToDocId.has(catTitle)) continue;
|
|
5901
|
+
const cat = dm.tree.create({
|
|
5902
|
+
parentId: categoriesContainerId,
|
|
5903
|
+
label: prettyCategoryLabel(catTitle),
|
|
5904
|
+
type: "graph",
|
|
5905
|
+
meta: { icon: ICONS.category }
|
|
5906
|
+
});
|
|
5907
|
+
categoryToDocId.set(catTitle, cat.id);
|
|
5908
|
+
log(`+ ${cat.id.slice(0, 8)}… ${prettyCategoryLabel(catTitle)} (graph, cat)`);
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5911
|
+
const body = opts.mode === "split" ? renderArticleLead(snap) : renderArticleSingleDoc(snap);
|
|
5912
|
+
if (body.trim().length > 0) {
|
|
5913
|
+
const rewritten = rewriteLinks(body, titleToDocId);
|
|
5914
|
+
try {
|
|
5915
|
+
await dm.content.write(articleDocId, rewritten);
|
|
5916
|
+
log(`✓ body ${title}`);
|
|
5917
|
+
} catch (err) {
|
|
5918
|
+
log(`! body write failed for ${title}: ${err?.message ?? err}`);
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
if (opts.mode === "split") await writeChildrenBodies(dm, snap, articleDocId, titleToDocId, log);
|
|
5922
|
+
articleCount++;
|
|
5923
|
+
}
|
|
5924
|
+
let categoryCount = 0;
|
|
5925
|
+
if (opts.includeCategories && categoryToDocId.size > 0) for (const [catTitle, catDocId] of categoryToDocId) {
|
|
5926
|
+
log(`category ${catTitle}`);
|
|
5927
|
+
try {
|
|
5928
|
+
const members = await wp.fetchCategoryPages(catTitle, opts.categoryDepth > 0, Math.max(0, opts.categoryDepth));
|
|
5929
|
+
const memberArticles = [];
|
|
5930
|
+
const subcats = [];
|
|
5931
|
+
for (const m of members) if (m.type === "subcat") subcats.push(prettyCategoryLabel(m.title));
|
|
5932
|
+
else memberArticles.push(m.title);
|
|
5933
|
+
const rewritten = rewriteLinks(renderCategoryBody(memberArticles, subcats), titleToDocId);
|
|
5934
|
+
if (rewritten.trim().length > 0) {
|
|
5935
|
+
await dm.content.write(catDocId, rewritten);
|
|
5936
|
+
log(`✓ body category ${catTitle}`);
|
|
5937
|
+
}
|
|
5938
|
+
categoryCount++;
|
|
5939
|
+
} catch (err) {
|
|
5940
|
+
log(`! category ${catTitle}: ${err?.message ?? err}`);
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
return {
|
|
5944
|
+
rootDocId: rootEntry.id,
|
|
5945
|
+
articleCount,
|
|
5946
|
+
categoryCount
|
|
5947
|
+
};
|
|
5948
|
+
}
|
|
5949
|
+
function createArticleShell(dm, article, parentId, log) {
|
|
5950
|
+
const meta = { icon: ICONS.article };
|
|
5951
|
+
if (article.url) meta.url = article.url;
|
|
5952
|
+
const entry = dm.tree.create({
|
|
5953
|
+
parentId,
|
|
5954
|
+
label: article.title,
|
|
5955
|
+
type: "doc",
|
|
5956
|
+
meta
|
|
5957
|
+
});
|
|
5958
|
+
log(`+ ${entry.id.slice(0, 8)}… ${article.title} (doc)`);
|
|
5959
|
+
return entry.id;
|
|
5960
|
+
}
|
|
5961
|
+
/**
|
|
5962
|
+
* Create section + infobox child docs for a split-mode article. Returns nothing
|
|
5963
|
+
* — children get bodies written later in writeChildrenBodies.
|
|
5964
|
+
*/
|
|
5965
|
+
function createArticleChildren(dm, article, articleDocId, log) {
|
|
5966
|
+
if (article.infobox && article.infobox.length > 0) {
|
|
5967
|
+
const ib = dm.tree.create({
|
|
5968
|
+
parentId: articleDocId,
|
|
5969
|
+
label: "Infobox",
|
|
5970
|
+
type: "outline",
|
|
5971
|
+
meta: { icon: ICONS.infobox }
|
|
5972
|
+
});
|
|
5973
|
+
log(` + ${ib.id.slice(0, 8)}… Infobox (outline)`);
|
|
5974
|
+
article._infoboxDocId = ib.id;
|
|
5975
|
+
}
|
|
5976
|
+
for (const section of article.sections) createSectionShell(dm, section, articleDocId, log);
|
|
5977
|
+
}
|
|
5978
|
+
function createSectionShell(dm, section, parentDocId, log) {
|
|
5979
|
+
const hasChildren = section.children.length > 0;
|
|
5980
|
+
if (!section.body.trim() && !hasChildren) return;
|
|
5981
|
+
const { type, icon } = pickSectionType(section);
|
|
5982
|
+
const entry = dm.tree.create({
|
|
5983
|
+
parentId: parentDocId,
|
|
5984
|
+
label: section.title || "Untitled section",
|
|
5985
|
+
type,
|
|
5986
|
+
meta: { icon }
|
|
5987
|
+
});
|
|
5988
|
+
log(` + ${entry.id.slice(0, 8)}… ${entry.label} (${type})`);
|
|
5989
|
+
section._docId = entry.id;
|
|
5990
|
+
for (const child of section.children) createSectionShell(dm, child, entry.id, log);
|
|
5991
|
+
}
|
|
5992
|
+
async function writeChildrenBodies(dm, article, _articleDocId, titleToDocId, log) {
|
|
5993
|
+
const infoboxDocId = article._infoboxDocId;
|
|
5994
|
+
if (infoboxDocId && article.infobox && article.infobox.length > 0) try {
|
|
5995
|
+
await dm.content.write(infoboxDocId, renderInfoboxBody(article.infobox));
|
|
5996
|
+
} catch (err) {
|
|
5997
|
+
log(`! infobox body write failed: ${err?.message ?? err}`);
|
|
5998
|
+
}
|
|
5999
|
+
for (const section of article.sections) await writeSectionBody(dm, section, titleToDocId, log);
|
|
6000
|
+
}
|
|
6001
|
+
async function writeSectionBody(dm, section, titleToDocId, log) {
|
|
6002
|
+
const docId = section._docId;
|
|
6003
|
+
if (docId && section.body.trim().length > 0) try {
|
|
6004
|
+
await dm.content.write(docId, rewriteLinks(section.body, titleToDocId));
|
|
6005
|
+
} catch (err) {
|
|
6006
|
+
log(`! section body write failed for ${section.title}: ${err?.message ?? err}`);
|
|
6007
|
+
}
|
|
6008
|
+
for (const child of section.children) await writeSectionBody(dm, child, titleToDocId, log);
|
|
6009
|
+
}
|
|
6010
|
+
function parseOptions(args) {
|
|
6011
|
+
const title = args.positional[0]?.trim() || args.params["title"];
|
|
6012
|
+
if (!title) return "Missing required positional argument: <title>. Example: abracadabra wiki \"Toronto Raptors\"";
|
|
6013
|
+
const env = globalThis.process?.env ?? {};
|
|
6014
|
+
const userAgent = args.params["user-agent"] || args.params["userAgent"] || env["ABRA_WIKI_USER_AGENT"];
|
|
6015
|
+
if (!userAgent) return ["Missing required parameter: user-agent=\"your-name (you@example.com)\"", "(Wikimedia etiquette requires an Api-User-Agent header. Pass user-agent=... or set ABRA_WIKI_USER_AGENT.)"].join("\n");
|
|
6016
|
+
const mode = args.params["mode"] ?? "split";
|
|
6017
|
+
if (mode !== "single" && mode !== "split") return `Invalid mode "${mode}". Use mode=single or mode=split.`;
|
|
6018
|
+
const depth = parseIntOr(args.params["depth"], 1);
|
|
6019
|
+
const categoryDepth = parseIntOr(args.params["category-depth"] ?? args.params["categoryDepth"], 1);
|
|
6020
|
+
const rate = parseFloatOr(args.params["rate"], 3);
|
|
6021
|
+
return {
|
|
6022
|
+
title,
|
|
6023
|
+
mode,
|
|
6024
|
+
depth,
|
|
6025
|
+
categoryDepth,
|
|
6026
|
+
includeCategories: args.flags.has("include-categories") || args.flags.has("includeCategories"),
|
|
6027
|
+
lang: args.params["lang"] ?? "en",
|
|
6028
|
+
domain: args.params["domain"],
|
|
6029
|
+
parentDocId: args.params["parent"],
|
|
6030
|
+
userAgent,
|
|
6031
|
+
rate,
|
|
6032
|
+
dryRun: args.flags.has("dry-run") || args.flags.has("dryRun")
|
|
6033
|
+
};
|
|
6034
|
+
}
|
|
6035
|
+
function parseIntOr(s, fallback) {
|
|
6036
|
+
if (!s) return fallback;
|
|
6037
|
+
const n = Number.parseInt(s, 10);
|
|
6038
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
6039
|
+
}
|
|
6040
|
+
function parseFloatOr(s, fallback) {
|
|
6041
|
+
if (!s) return fallback;
|
|
6042
|
+
const n = Number.parseFloat(s);
|
|
6043
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
6044
|
+
}
|
|
6045
|
+
function printSections(sections, indent) {
|
|
6046
|
+
const lines = [];
|
|
6047
|
+
for (const s of sections) {
|
|
6048
|
+
const hint = s.body ? ` (${s.body.length}b)` : "";
|
|
6049
|
+
lines.push(`${indent}- ${s.title}${hint}${s.children.length > 0 ? ` [${s.children.length} sub]` : ""}`);
|
|
6050
|
+
if (s.children.length > 0) lines.push(printSections(s.children, indent + " "));
|
|
6051
|
+
}
|
|
6052
|
+
return lines.join("\n");
|
|
6053
|
+
}
|
|
6054
|
+
|
|
4096
6055
|
//#endregion
|
|
4097
6056
|
//#region packages/cli/src/index.ts
|
|
4098
6057
|
/**
|
|
@@ -4116,7 +6075,9 @@ const NO_CONNECT_COMMANDS = new Set([
|
|
|
4116
6075
|
"v",
|
|
4117
6076
|
"page-types",
|
|
4118
6077
|
"types",
|
|
4119
|
-
"doctypes"
|
|
6078
|
+
"doctypes",
|
|
6079
|
+
"wiki",
|
|
6080
|
+
"wikipedia"
|
|
4120
6081
|
]);
|
|
4121
6082
|
async function main() {
|
|
4122
6083
|
const args = parseArgs(process.argv);
|