@abraca/cli 1.9.1 → 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 -575
- package/dist/abracadabra-cli.cjs.map +1 -1
- package/dist/abracadabra-cli.esm.js +2515 -576
- 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,247 +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) {
|
|
1289
|
-
if (child.nodeName === "docLink") {
|
|
1290
|
-
const docId = child.getAttribute("docId");
|
|
1291
|
-
if (docId) parts.push(`[[${docId}]]`);
|
|
1292
|
-
continue;
|
|
1293
|
-
}
|
|
1294
|
-
parts.push(elementTextContent(child));
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
return parts.join("");
|
|
1298
|
-
}
|
|
1299
|
-
function serializeElement(el, indent = "") {
|
|
1300
|
-
switch (el.nodeName) {
|
|
1301
|
-
case "documentHeader":
|
|
1302
|
-
case "documentMeta": return "";
|
|
1303
|
-
case "heading": {
|
|
1304
|
-
const level = Number(el.getAttribute("level")) || 1;
|
|
1305
|
-
return `${"#".repeat(level)} ${elementTextContent(el)}`;
|
|
1306
|
-
}
|
|
1307
|
-
case "paragraph": return elementTextContent(el);
|
|
1308
|
-
case "bulletList": return serializeList(el, "bullet", indent);
|
|
1309
|
-
case "orderedList": return serializeList(el, "ordered", indent);
|
|
1310
|
-
case "taskList": return serializeTaskList(el, indent);
|
|
1311
|
-
case "codeBlock": return `\`\`\`${el.getAttribute("language") || ""}\n${elementTextContent(el)}\n\`\`\``;
|
|
1312
|
-
case "blockquote": {
|
|
1313
|
-
const lines = [];
|
|
1314
|
-
for (let i = 0; i < el.length; i++) {
|
|
1315
|
-
const child = el.get(i);
|
|
1316
|
-
if (child instanceof yjs.XmlElement) lines.push(serializeElement(child, indent));
|
|
1317
|
-
}
|
|
1318
|
-
return lines.map((l) => `> ${l}`).join("\n");
|
|
1319
|
-
}
|
|
1320
|
-
case "horizontalRule": return "---";
|
|
1321
|
-
case "table": return serializeTable(el);
|
|
1322
|
-
case "docEmbed": {
|
|
1323
|
-
const docId = el.getAttribute("docId");
|
|
1324
|
-
if (!docId) return "";
|
|
1325
|
-
const seamlessAttr = el.getAttribute("seamless");
|
|
1326
|
-
return seamlessAttr === true || seamlessAttr === "true" ? `![[${docId}]]{seamless}` : `![[${docId}]]`;
|
|
1327
|
-
}
|
|
1328
|
-
case "svgEmbed": {
|
|
1329
|
-
const svg = el.getAttribute("svg") || "";
|
|
1330
|
-
const svgTitle = el.getAttribute("title") || "";
|
|
1331
|
-
if (!svg) return "";
|
|
1332
|
-
return `\`\`\`svg${svgTitle ? ` ${svgTitle}` : ""}\n${svg}\n\`\`\``;
|
|
1333
|
-
}
|
|
1334
|
-
case "image": {
|
|
1335
|
-
const src = el.getAttribute("src") || "";
|
|
1336
|
-
const alt = el.getAttribute("alt") || "";
|
|
1337
|
-
const w = el.getAttribute("width");
|
|
1338
|
-
const h = el.getAttribute("height");
|
|
1339
|
-
let attrs = "";
|
|
1340
|
-
if (w || h) {
|
|
1341
|
-
const parts = [];
|
|
1342
|
-
if (w) parts.push(`width="${w}"`);
|
|
1343
|
-
if (h) parts.push(`height="${h}"`);
|
|
1344
|
-
attrs = `{${parts.join(" ")}}`;
|
|
1345
|
-
}
|
|
1346
|
-
return `${attrs}`;
|
|
1347
|
-
}
|
|
1348
|
-
case "callout": return `::${el.getAttribute("type") || "note"}\n${serializeChildren(el, indent)}\n::`;
|
|
1349
|
-
case "collapsible": {
|
|
1350
|
-
const label = el.getAttribute("label") || "Details";
|
|
1351
|
-
const open = el.getAttribute("open");
|
|
1352
|
-
const props = [`label="${label}"`];
|
|
1353
|
-
if (open === true || open === "true") props.push("open=\"true\"");
|
|
1354
|
-
const inner = serializeChildren(el, indent);
|
|
1355
|
-
return `::collapsible{${props.join(" ")}}\n${inner}\n::`;
|
|
1356
|
-
}
|
|
1357
|
-
case "steps": return `::steps\n${serializeChildren(el, indent)}\n::`;
|
|
1358
|
-
case "card": {
|
|
1359
|
-
const title = el.getAttribute("title") || "";
|
|
1360
|
-
const icon = el.getAttribute("icon") || "";
|
|
1361
|
-
const to = el.getAttribute("to") || "";
|
|
1362
|
-
const props = [];
|
|
1363
|
-
if (title) props.push(`title="${title}"`);
|
|
1364
|
-
if (icon) props.push(`icon="${icon}"`);
|
|
1365
|
-
if (to) props.push(`to="${to}"`);
|
|
1366
|
-
const inner = serializeChildren(el, indent);
|
|
1367
|
-
return `::card{${props.join(" ")}}\n${inner}\n::`;
|
|
1368
|
-
}
|
|
1369
|
-
case "cardGroup": return `::card-group\n${serializeChildren(el, indent)}\n::`;
|
|
1370
|
-
case "codeCollapse": return `::code-collapse\n${serializeChildren(el, indent)}\n::`;
|
|
1371
|
-
case "codeGroup": return `::code-group\n${serializeChildren(el, indent)}\n::`;
|
|
1372
|
-
case "codePreview": return `::code-preview\n${serializeChildren(el, indent)}\n::`;
|
|
1373
|
-
case "codeTree": return `::code-tree{files="${el.getAttribute("files") || "[]"}"}\n::`;
|
|
1374
|
-
case "accordion": return serializeSlottedComponent(el, "accordion", "accordionItem", "item");
|
|
1375
|
-
case "tabs": return serializeSlottedComponent(el, "tabs", "tabsItem", "tab");
|
|
1376
|
-
case "field": {
|
|
1377
|
-
const fieldName = el.getAttribute("name") || "";
|
|
1378
|
-
const fieldType = el.getAttribute("type") || "string";
|
|
1379
|
-
const required = el.getAttribute("required");
|
|
1380
|
-
const props = [];
|
|
1381
|
-
if (fieldName) props.push(`name="${fieldName}"`);
|
|
1382
|
-
props.push(`type="${fieldType}"`);
|
|
1383
|
-
if (required === true || required === "true") props.push("required=\"true\"");
|
|
1384
|
-
const inner = serializeChildren(el, indent);
|
|
1385
|
-
return `::field{${props.join(" ")}}\n${inner}\n::`;
|
|
1386
|
-
}
|
|
1387
|
-
case "fieldGroup": return `::field-group\n${serializeChildren(el, indent)}\n::`;
|
|
1388
|
-
default: return serializeChildren(el, indent);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
function serializeList(el, type, indent) {
|
|
1392
|
-
const lines = [];
|
|
1393
|
-
for (let i = 0; i < el.length; i++) {
|
|
1394
|
-
const item = el.get(i);
|
|
1395
|
-
if (item instanceof yjs.XmlElement && item.nodeName === "listItem") {
|
|
1396
|
-
const prefix = type === "bullet" ? "- " : `${i + 1}. `;
|
|
1397
|
-
const content = elementTextContent(item);
|
|
1398
|
-
lines.push(`${indent}${prefix}${content}`);
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
return lines.join("\n");
|
|
1402
|
-
}
|
|
1403
|
-
function serializeTaskList(el, indent) {
|
|
1404
|
-
const lines = [];
|
|
1405
|
-
for (let i = 0; i < el.length; i++) {
|
|
1406
|
-
const item = el.get(i);
|
|
1407
|
-
if (item instanceof yjs.XmlElement && item.nodeName === "taskItem") {
|
|
1408
|
-
const checked = item.getAttribute("checked");
|
|
1409
|
-
const marker = checked === true || checked === "true" ? "[x]" : "[ ]";
|
|
1410
|
-
const content = elementTextContent(item);
|
|
1411
|
-
lines.push(`${indent}- ${marker} ${content}`);
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
return lines.join("\n");
|
|
1415
|
-
}
|
|
1416
|
-
function serializeTable(el) {
|
|
1417
|
-
const rows = [];
|
|
1418
|
-
for (let i = 0; i < el.length; i++) {
|
|
1419
|
-
const row = el.get(i);
|
|
1420
|
-
if (!(row instanceof yjs.XmlElement) || row.nodeName !== "tableRow") continue;
|
|
1421
|
-
const cells = [];
|
|
1422
|
-
for (let j = 0; j < row.length; j++) {
|
|
1423
|
-
const cell = row.get(j);
|
|
1424
|
-
if (cell instanceof yjs.XmlElement) cells.push(elementTextContent(cell));
|
|
1425
|
-
}
|
|
1426
|
-
rows.push(cells);
|
|
1427
|
-
}
|
|
1428
|
-
if (!rows.length) return "";
|
|
1429
|
-
const headerRow = rows[0];
|
|
1430
|
-
const lines = [`| ${headerRow.join(" | ")} |`];
|
|
1431
|
-
lines.push(`| ${headerRow.map(() => "---").join(" | ")} |`);
|
|
1432
|
-
for (let i = 1; i < rows.length; i++) lines.push(`| ${rows[i].join(" | ")} |`);
|
|
1433
|
-
return lines.join("\n");
|
|
1434
|
-
}
|
|
1435
|
-
function serializeSlottedComponent(el, componentName, childNodeName, slotName) {
|
|
1436
|
-
const parts = [`::${componentName}`];
|
|
1437
|
-
for (let i = 0; i < el.length; i++) {
|
|
1438
|
-
const child = el.get(i);
|
|
1439
|
-
if (!(child instanceof yjs.XmlElement) || child.nodeName !== childNodeName) continue;
|
|
1440
|
-
const label = child.getAttribute("label") || `Item ${i + 1}`;
|
|
1441
|
-
const icon = child.getAttribute("icon") || "";
|
|
1442
|
-
const props = [`label="${label}"`];
|
|
1443
|
-
if (icon) props.push(`icon="${icon}"`);
|
|
1444
|
-
parts.push(`#${slotName}{${props.join(" ")}}`);
|
|
1445
|
-
const inner = serializeChildren(child, "");
|
|
1446
|
-
if (inner) parts.push(inner);
|
|
1447
|
-
}
|
|
1448
|
-
parts.push("::");
|
|
1449
|
-
return parts.join("\n");
|
|
1450
|
-
}
|
|
1451
|
-
function serializeChildren(el, indent) {
|
|
1452
|
-
const parts = [];
|
|
1453
|
-
for (let i = 0; i < el.length; i++) {
|
|
1454
|
-
const child = el.get(i);
|
|
1455
|
-
if (child instanceof yjs.XmlElement) {
|
|
1456
|
-
const serialized = serializeElement(child, indent);
|
|
1457
|
-
if (serialized) parts.push(serialized);
|
|
1458
|
-
} else if (child instanceof yjs.XmlText) {
|
|
1459
|
-
const text = xmlTextToMarkdown(child);
|
|
1460
|
-
if (text) parts.push(text);
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
return parts.join("\n\n");
|
|
1464
|
-
}
|
|
1465
|
-
/**
|
|
1466
|
-
* Converts a Y.XmlFragment (TipTap document) to markdown.
|
|
1467
|
-
* Extracts the title from the documentHeader element.
|
|
1468
|
-
*
|
|
1469
|
-
* @returns `{ title, markdown }` where title is the H1/header text
|
|
1470
|
-
*/
|
|
1471
|
-
function yjsToMarkdown(fragment) {
|
|
1472
|
-
let title = "Untitled";
|
|
1473
|
-
const bodyParts = [];
|
|
1474
|
-
for (let i = 0; i < fragment.length; i++) {
|
|
1475
|
-
const child = fragment.get(i);
|
|
1476
|
-
if (!(child instanceof yjs.XmlElement)) continue;
|
|
1477
|
-
if (child.nodeName === "documentHeader") {
|
|
1478
|
-
title = elementTextContent(child) || "Untitled";
|
|
1479
|
-
continue;
|
|
1480
|
-
}
|
|
1481
|
-
if (child.nodeName === "documentMeta") continue;
|
|
1482
|
-
const serialized = serializeElement(child);
|
|
1483
|
-
if (serialized !== "") bodyParts.push(serialized);
|
|
1484
|
-
}
|
|
1485
|
-
return {
|
|
1486
|
-
title,
|
|
1487
|
-
markdown: bodyParts.join("\n\n")
|
|
1488
|
-
};
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
//#endregion
|
|
1492
|
-
//#region packages/mcp/src/converters/markdownToYjs.ts
|
|
1493
|
-
/**
|
|
1494
|
-
* Markdown → Y.js converter.
|
|
1495
|
-
* Ported from cou-sh/app/utils/markdownToYjs.ts with Vue dependency removed.
|
|
1496
|
-
*/
|
|
1352
|
+
//#region packages/convert/src/markdown-to-yjs.ts
|
|
1497
1353
|
function parseInlineArray(raw) {
|
|
1498
1354
|
return raw.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
1499
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
|
+
}
|
|
1500
1360
|
function parseFrontmatter(markdown) {
|
|
1501
1361
|
const noResult = {
|
|
1502
1362
|
meta: {},
|
|
@@ -1527,8 +1387,8 @@ function parseFrontmatter(markdown) {
|
|
|
1527
1387
|
if (kvMatch) {
|
|
1528
1388
|
const key = kvMatch[1];
|
|
1529
1389
|
const val = kvMatch[2].trim();
|
|
1530
|
-
if (val.startsWith("[") && val.endsWith("]")) raw[key] = parseInlineArray(val);
|
|
1531
|
-
else raw[key] = val;
|
|
1390
|
+
if (val.startsWith("[") && val.endsWith("]")) raw[key] = parseInlineArray(val).map(stripQuotes);
|
|
1391
|
+
else raw[key] = stripQuotes(val);
|
|
1532
1392
|
}
|
|
1533
1393
|
i++;
|
|
1534
1394
|
}
|
|
@@ -1555,54 +1415,27 @@ function parseFrontmatter(markdown) {
|
|
|
1555
1415
|
}[priorityRaw.toLowerCase()] ?? (Number(priorityRaw) || 0);
|
|
1556
1416
|
const checkedRaw = raw["checked"] ?? raw["done"];
|
|
1557
1417
|
if (checkedRaw !== void 0) meta.checked = checkedRaw === "true" || checkedRaw === true;
|
|
1558
|
-
const dateStart = getStr([
|
|
1418
|
+
const dateStart = getStr([
|
|
1419
|
+
"dateStart",
|
|
1420
|
+
"date",
|
|
1421
|
+
"created"
|
|
1422
|
+
]);
|
|
1559
1423
|
if (dateStart) meta.dateStart = dateStart;
|
|
1560
|
-
const dateEnd = getStr(["due"]);
|
|
1424
|
+
const dateEnd = getStr(["dateEnd", "due"]);
|
|
1561
1425
|
if (dateEnd) meta.dateEnd = dateEnd;
|
|
1562
|
-
const subtitle = getStr(["
|
|
1426
|
+
const subtitle = getStr(["subtitle", "description"]);
|
|
1563
1427
|
if (subtitle) meta.subtitle = subtitle;
|
|
1564
1428
|
const url = getStr(["url"]);
|
|
1565
1429
|
if (url) meta.url = url;
|
|
1566
|
-
const email = getStr(["email"]);
|
|
1567
|
-
if (email) meta.email = email;
|
|
1568
|
-
const phone = getStr(["phone"]);
|
|
1569
|
-
if (phone) meta.phone = phone;
|
|
1570
1430
|
const ratingRaw = getStr(["rating"]);
|
|
1571
1431
|
if (ratingRaw !== void 0) {
|
|
1572
1432
|
const n = Number(ratingRaw);
|
|
1573
1433
|
if (!Number.isNaN(n)) meta.rating = Math.min(5, Math.max(0, n));
|
|
1574
1434
|
}
|
|
1575
|
-
const
|
|
1576
|
-
if (datetimeStart) meta.datetimeStart = datetimeStart;
|
|
1577
|
-
const datetimeEnd = getStr(["datetimeEnd"]);
|
|
1578
|
-
if (datetimeEnd) meta.datetimeEnd = datetimeEnd;
|
|
1579
|
-
const allDayRaw = raw["allDay"];
|
|
1580
|
-
if (allDayRaw !== void 0) meta.allDay = allDayRaw === "true" || allDayRaw === true;
|
|
1581
|
-
const geoLatRaw = getStr(["geoLat"]);
|
|
1582
|
-
if (geoLatRaw !== void 0) {
|
|
1583
|
-
const n = Number(geoLatRaw);
|
|
1584
|
-
if (!Number.isNaN(n)) meta.geoLat = n;
|
|
1585
|
-
}
|
|
1586
|
-
const geoLngRaw = getStr(["geoLng"]);
|
|
1587
|
-
if (geoLngRaw !== void 0) {
|
|
1588
|
-
const n = Number(geoLngRaw);
|
|
1589
|
-
if (!Number.isNaN(n)) meta.geoLng = n;
|
|
1590
|
-
}
|
|
1591
|
-
const geoType = getStr(["geoType"]);
|
|
1592
|
-
if (geoType && (geoType === "marker" || geoType === "line" || geoType === "measure")) meta.geoType = geoType;
|
|
1593
|
-
const geoDescription = getStr(["geoDescription"]);
|
|
1594
|
-
if (geoDescription) meta.geoDescription = geoDescription;
|
|
1595
|
-
const numberRaw = getStr(["number"]);
|
|
1596
|
-
if (numberRaw !== void 0) {
|
|
1597
|
-
const n = Number(numberRaw);
|
|
1598
|
-
if (!Number.isNaN(n)) meta.number = n;
|
|
1599
|
-
}
|
|
1600
|
-
const unit = getStr(["unit"]);
|
|
1601
|
-
if (unit) meta.unit = unit;
|
|
1602
|
-
const note = getStr(["note"]);
|
|
1603
|
-
if (note) meta.note = note;
|
|
1435
|
+
const rawTitle = typeof raw["title"] === "string" ? raw["title"] : void 0;
|
|
1604
1436
|
return {
|
|
1605
|
-
title:
|
|
1437
|
+
title: rawTitle !== void 0 ? stripQuotes(rawTitle) : void 0,
|
|
1438
|
+
type: getStr(["type"]),
|
|
1606
1439
|
meta,
|
|
1607
1440
|
body
|
|
1608
1441
|
};
|
|
@@ -1610,66 +1443,79 @@ function parseFrontmatter(markdown) {
|
|
|
1610
1443
|
function parseInline(text) {
|
|
1611
1444
|
const stripped = text.replace(/\{lang="[^"]*"\}/g, "").replace(/:(?!badge|icon|kbd)(\w[\w-]*)\[([^\]]*)\](\{[^}]*\})?/g, "$2").replace(/:(?!badge|icon|kbd)(\w[\w-]*)(\{[^}]*\})/g, "");
|
|
1612
1445
|
const tokens = [];
|
|
1613
|
-
const re =
|
|
1446
|
+
const re = /\$([^$\n]+?)\$|@\[([^\]]+?)\]\(user:([^)]+?)\)|:badge\[([^\]]*)\](\{[^}]*\})?|:icon\{([^}]*)\}|:kbd\{([^}]*)\}|\[\[([0-9a-fA-F-]{36})(?:\|([^\]]+?))?\]\]|~~(.+?)~~|\*\*(.+?)\*\*|\*(.+?)\*|_(.+?)_|`(.+?)`|\[(.+?)\]\((.+?)\)/g;
|
|
1614
1447
|
let lastIndex = 0;
|
|
1615
1448
|
let match;
|
|
1616
1449
|
while ((match = re.exec(stripped)) !== null) {
|
|
1617
1450
|
if (match.index > lastIndex) tokens.push({ text: stripped.slice(lastIndex, match.index) });
|
|
1618
|
-
if (match[1] !== void 0) {
|
|
1619
|
-
|
|
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]);
|
|
1620
1464
|
tokens.push({
|
|
1621
|
-
text: match[
|
|
1465
|
+
text: match[4] || "Badge",
|
|
1622
1466
|
attrs: { badge: {
|
|
1623
|
-
label: match[
|
|
1467
|
+
label: match[4] || "Badge",
|
|
1624
1468
|
color: badgeProps["color"] || "neutral",
|
|
1625
1469
|
variant: badgeProps["variant"] || "subtle"
|
|
1626
1470
|
} }
|
|
1627
1471
|
});
|
|
1628
|
-
} else if (match[
|
|
1629
|
-
const iconProps = parseMdcProps(`{${match[
|
|
1472
|
+
} else if (match[6] !== void 0) {
|
|
1473
|
+
const iconProps = parseMdcProps(`{${match[6]}}`);
|
|
1630
1474
|
tokens.push({
|
|
1631
1475
|
text: "",
|
|
1632
1476
|
attrs: { proseIcon: { name: iconProps["name"] || "i-lucide-star" } }
|
|
1633
1477
|
});
|
|
1634
|
-
} else if (match[
|
|
1635
|
-
const kbdProps = parseMdcProps(`{${match[
|
|
1478
|
+
} else if (match[7] !== void 0) {
|
|
1479
|
+
const kbdProps = parseMdcProps(`{${match[7]}}`);
|
|
1636
1480
|
tokens.push({
|
|
1637
1481
|
text: kbdProps["value"] || "",
|
|
1638
1482
|
attrs: { kbd: { value: kbdProps["value"] || "" } }
|
|
1639
1483
|
});
|
|
1640
|
-
} else if (match[
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1484
|
+
} else if (match[8] !== void 0) {
|
|
1485
|
+
const docId = match[8];
|
|
1486
|
+
const label = match[9] ?? docId;
|
|
1487
|
+
tokens.push({
|
|
1488
|
+
text: label,
|
|
1489
|
+
attrs: { docLink: { docId } }
|
|
1490
|
+
});
|
|
1491
|
+
} else if (match[10] !== void 0) tokens.push({
|
|
1492
|
+
text: match[10],
|
|
1647
1493
|
attrs: { strike: true }
|
|
1648
1494
|
});
|
|
1649
|
-
else if (match[
|
|
1650
|
-
text: match[
|
|
1495
|
+
else if (match[11] !== void 0) tokens.push({
|
|
1496
|
+
text: match[11],
|
|
1651
1497
|
attrs: { bold: true }
|
|
1652
1498
|
});
|
|
1653
|
-
else if (match[
|
|
1654
|
-
text: match[
|
|
1499
|
+
else if (match[12] !== void 0) tokens.push({
|
|
1500
|
+
text: match[12],
|
|
1655
1501
|
attrs: { italic: true }
|
|
1656
1502
|
});
|
|
1657
|
-
else if (match[
|
|
1658
|
-
text: match[
|
|
1503
|
+
else if (match[13] !== void 0) tokens.push({
|
|
1504
|
+
text: match[13],
|
|
1659
1505
|
attrs: { italic: true }
|
|
1660
1506
|
});
|
|
1661
|
-
else if (match[
|
|
1662
|
-
text: match[
|
|
1507
|
+
else if (match[14] !== void 0) tokens.push({
|
|
1508
|
+
text: match[14],
|
|
1663
1509
|
attrs: { code: true }
|
|
1664
1510
|
});
|
|
1665
|
-
else if (match[
|
|
1666
|
-
text: match[
|
|
1667
|
-
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] } }
|
|
1668
1514
|
});
|
|
1669
1515
|
lastIndex = match.index + match[0].length;
|
|
1670
1516
|
}
|
|
1671
1517
|
if (lastIndex < stripped.length) tokens.push({ text: stripped.slice(lastIndex) });
|
|
1672
|
-
return tokens.filter((t) => t.
|
|
1518
|
+
return tokens.filter((t) => t.text.length > 0);
|
|
1673
1519
|
}
|
|
1674
1520
|
function parseTableRow(line) {
|
|
1675
1521
|
const parts = line.split("|");
|
|
@@ -1678,6 +1524,7 @@ function parseTableRow(line) {
|
|
|
1678
1524
|
function isTableSeparator(line) {
|
|
1679
1525
|
return /^\|[\s|:-]+\|$/.test(line.trim());
|
|
1680
1526
|
}
|
|
1527
|
+
/** Extract fenced code blocks from MDC #code slot lines. */
|
|
1681
1528
|
function extractFencedCode(lines) {
|
|
1682
1529
|
const result = [];
|
|
1683
1530
|
let i = 0;
|
|
@@ -1704,14 +1551,23 @@ function extractFencedCode(lines) {
|
|
|
1704
1551
|
}
|
|
1705
1552
|
return result;
|
|
1706
1553
|
}
|
|
1554
|
+
/** Extract key="value" pairs from MDC prop syntax `{key="value" other="x"}` */
|
|
1707
1555
|
function parseMdcProps(propsStr) {
|
|
1708
1556
|
if (!propsStr) return {};
|
|
1709
1557
|
const result = {};
|
|
1710
|
-
|
|
1558
|
+
let s = propsStr.trim();
|
|
1559
|
+
if (s.startsWith("{") && s.endsWith("}")) s = s.slice(1, -1);
|
|
1560
|
+
const re = /(\w[\w-]*)(?:=(?:"([^"]*)"|([^\s"}]+)))?/g;
|
|
1711
1561
|
let m;
|
|
1712
|
-
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
|
+
}
|
|
1713
1568
|
return result;
|
|
1714
1569
|
}
|
|
1570
|
+
/** Parse named child MDC blocks from inner lines (e.g. #item for accordion, #tab for tabs) */
|
|
1715
1571
|
function parseMdcChildren(innerLines, slotPrefix) {
|
|
1716
1572
|
const items = [];
|
|
1717
1573
|
let current = null;
|
|
@@ -1743,6 +1599,95 @@ function parseMdcChildren(innerLines, slotPrefix) {
|
|
|
1743
1599
|
}));
|
|
1744
1600
|
}
|
|
1745
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
|
+
}
|
|
1746
1691
|
function parseBlocks(markdown) {
|
|
1747
1692
|
const rawLines = markdown.split("\n");
|
|
1748
1693
|
let firstContentLine = 0;
|
|
@@ -1768,17 +1713,15 @@ function parseBlocks(markdown) {
|
|
|
1768
1713
|
i++;
|
|
1769
1714
|
}
|
|
1770
1715
|
i++;
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
});
|
|
1778
|
-
} 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({
|
|
1779
1722
|
type: "codeBlock",
|
|
1780
1723
|
lang,
|
|
1781
|
-
code
|
|
1724
|
+
code
|
|
1782
1725
|
});
|
|
1783
1726
|
continue;
|
|
1784
1727
|
}
|
|
@@ -1797,14 +1740,16 @@ function parseBlocks(markdown) {
|
|
|
1797
1740
|
i++;
|
|
1798
1741
|
continue;
|
|
1799
1742
|
}
|
|
1800
|
-
const
|
|
1801
|
-
if (
|
|
1802
|
-
const
|
|
1803
|
-
const
|
|
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]);
|
|
1804
1748
|
blocks.push({
|
|
1805
1749
|
type: "docEmbed",
|
|
1806
|
-
docId
|
|
1807
|
-
|
|
1750
|
+
docId,
|
|
1751
|
+
label,
|
|
1752
|
+
props
|
|
1808
1753
|
});
|
|
1809
1754
|
i++;
|
|
1810
1755
|
continue;
|
|
@@ -1856,6 +1801,23 @@ function parseBlocks(markdown) {
|
|
|
1856
1801
|
});
|
|
1857
1802
|
continue;
|
|
1858
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
|
+
}
|
|
1859
1821
|
const MDC_OPEN = /^\s*(:{2,})(\w[\w-]*)(\{[^}]*\})?\s*$/;
|
|
1860
1822
|
if (MDC_OPEN.test(line)) {
|
|
1861
1823
|
const colons = line.match(/^\s*(:+)/)?.[1]?.length ?? 2;
|
|
@@ -2024,15 +1986,8 @@ function parseBlocks(markdown) {
|
|
|
2024
1986
|
continue;
|
|
2025
1987
|
}
|
|
2026
1988
|
if (TASK_RE.test(line)) {
|
|
2027
|
-
const items =
|
|
2028
|
-
|
|
2029
|
-
const m = lines[i].match(TASK_RE);
|
|
2030
|
-
items.push({
|
|
2031
|
-
checked: m[1].toLowerCase() === "x",
|
|
2032
|
-
text: m[2]
|
|
2033
|
-
});
|
|
2034
|
-
i++;
|
|
2035
|
-
}
|
|
1989
|
+
const { items, next } = consumeList(lines, i, 0, "task");
|
|
1990
|
+
i = next;
|
|
2036
1991
|
blocks.push({
|
|
2037
1992
|
type: "taskList",
|
|
2038
1993
|
items
|
|
@@ -2040,12 +1995,9 @@ function parseBlocks(markdown) {
|
|
|
2040
1995
|
continue;
|
|
2041
1996
|
}
|
|
2042
1997
|
if (/^[-*+]\s+/.test(line)) {
|
|
2043
|
-
const items =
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
i++;
|
|
2047
|
-
}
|
|
2048
|
-
if (items.length) {
|
|
1998
|
+
const { items, next } = consumeList(lines, i, 0, "bullet");
|
|
1999
|
+
if (items.length > 0) {
|
|
2000
|
+
i = next;
|
|
2049
2001
|
blocks.push({
|
|
2050
2002
|
type: "bulletList",
|
|
2051
2003
|
items
|
|
@@ -2054,16 +2006,15 @@ function parseBlocks(markdown) {
|
|
|
2054
2006
|
}
|
|
2055
2007
|
}
|
|
2056
2008
|
if (/^\d+\.\s+/.test(line)) {
|
|
2057
|
-
const items =
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
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;
|
|
2061
2017
|
}
|
|
2062
|
-
blocks.push({
|
|
2063
|
-
type: "orderedList",
|
|
2064
|
-
items
|
|
2065
|
-
});
|
|
2066
|
-
continue;
|
|
2067
2018
|
}
|
|
2068
2019
|
if (line.trim() === "") {
|
|
2069
2020
|
i++;
|
|
@@ -2081,23 +2032,18 @@ function parseBlocks(markdown) {
|
|
|
2081
2032
|
}
|
|
2082
2033
|
return blocks;
|
|
2083
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
|
+
*/
|
|
2084
2039
|
function fillTextInto(el, tokens) {
|
|
2085
|
-
const filtered = tokens.filter((t) => t.
|
|
2040
|
+
const filtered = tokens.filter((t) => t.text.length > 0);
|
|
2086
2041
|
if (!filtered.length) return;
|
|
2087
|
-
const
|
|
2088
|
-
|
|
2089
|
-
const xe = new yjs.XmlElement(tok.node);
|
|
2090
|
-
if (tok.nodeAttrs) for (const [k, v] of Object.entries(tok.nodeAttrs)) xe.setAttribute(k, v);
|
|
2091
|
-
return xe;
|
|
2092
|
-
}
|
|
2093
|
-
return new yjs.XmlText();
|
|
2094
|
-
});
|
|
2095
|
-
el.insert(0, children);
|
|
2042
|
+
const xtNodes = filtered.map(() => new yjs.XmlText());
|
|
2043
|
+
el.insert(0, xtNodes);
|
|
2096
2044
|
filtered.forEach((tok, i) => {
|
|
2097
|
-
if (tok.
|
|
2098
|
-
|
|
2099
|
-
if (tok.attrs) xt.insert(0, tok.text, tok.attrs);
|
|
2100
|
-
else xt.insert(0, tok.text);
|
|
2045
|
+
if (tok.attrs) xtNodes[i].insert(0, tok.text, tok.attrs);
|
|
2046
|
+
else xtNodes[i].insert(0, tok.text);
|
|
2101
2047
|
});
|
|
2102
2048
|
}
|
|
2103
2049
|
function blockElName(b) {
|
|
@@ -2126,9 +2072,19 @@ function blockElName(b) {
|
|
|
2126
2072
|
case "fieldGroup": return "fieldGroup";
|
|
2127
2073
|
case "image": return "image";
|
|
2128
2074
|
case "docEmbed": return "docEmbed";
|
|
2129
|
-
case "
|
|
2075
|
+
case "mathBlock": return "mathBlock";
|
|
2076
|
+
case "fileBlock": return "fileBlock";
|
|
2130
2077
|
}
|
|
2131
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
|
+
}
|
|
2132
2088
|
function fillBlock(el, block) {
|
|
2133
2089
|
switch (block.type) {
|
|
2134
2090
|
case "heading":
|
|
@@ -2142,10 +2098,8 @@ function fillBlock(el, block) {
|
|
|
2142
2098
|
case "orderedList": {
|
|
2143
2099
|
const listItemEls = block.items.map(() => new yjs.XmlElement("listItem"));
|
|
2144
2100
|
el.insert(0, listItemEls);
|
|
2145
|
-
block.items.forEach((
|
|
2146
|
-
|
|
2147
|
-
listItemEls[i].insert(0, [paraEl]);
|
|
2148
|
-
fillTextInto(paraEl, parseInline(text));
|
|
2101
|
+
block.items.forEach((item, i) => {
|
|
2102
|
+
populateListItemChildren(listItemEls[i], item, "listItem");
|
|
2149
2103
|
});
|
|
2150
2104
|
break;
|
|
2151
2105
|
}
|
|
@@ -2153,10 +2107,8 @@ function fillBlock(el, block) {
|
|
|
2153
2107
|
const taskItemEls = block.items.map(() => new yjs.XmlElement("taskItem"));
|
|
2154
2108
|
el.insert(0, taskItemEls);
|
|
2155
2109
|
block.items.forEach((item, i) => {
|
|
2156
|
-
taskItemEls[i].setAttribute("checked", item.checked);
|
|
2157
|
-
|
|
2158
|
-
taskItemEls[i].insert(0, [paraEl]);
|
|
2159
|
-
fillTextInto(paraEl, parseInline(item.text));
|
|
2110
|
+
taskItemEls[i].setAttribute("checked", !!item.checked);
|
|
2111
|
+
populateListItemChildren(taskItemEls[i], item, "taskItem");
|
|
2160
2112
|
});
|
|
2161
2113
|
break;
|
|
2162
2114
|
}
|
|
@@ -2344,32 +2296,55 @@ function fillBlock(el, block) {
|
|
|
2344
2296
|
break;
|
|
2345
2297
|
case "docEmbed":
|
|
2346
2298
|
el.setAttribute("docId", block.docId);
|
|
2347
|
-
|
|
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);
|
|
2348
2304
|
break;
|
|
2349
|
-
case "
|
|
2350
|
-
el.setAttribute("
|
|
2351
|
-
|
|
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);
|
|
2352
2313
|
break;
|
|
2353
2314
|
}
|
|
2354
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
|
+
*/
|
|
2355
2327
|
function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled") {
|
|
2356
2328
|
const ydoc = fragment.doc;
|
|
2357
2329
|
if (!ydoc) {
|
|
2358
2330
|
console.warn("[markdownToYjs] fragment has no doc — skipping population");
|
|
2359
2331
|
return;
|
|
2360
2332
|
}
|
|
2361
|
-
const
|
|
2333
|
+
const fm = parseFrontmatter(markdown);
|
|
2334
|
+
const blocks = parseBlocks(fm.body);
|
|
2362
2335
|
let title = fallbackTitle;
|
|
2336
|
+
let titleSource;
|
|
2337
|
+
if (fm.title !== void 0) {
|
|
2338
|
+
title = fm.title;
|
|
2339
|
+
titleSource = "frontmatter";
|
|
2340
|
+
}
|
|
2363
2341
|
let contentBlocks = blocks;
|
|
2364
2342
|
const h1 = blocks.findIndex((b) => b.type === "heading" && b.level === 1);
|
|
2365
2343
|
if (h1 !== -1) {
|
|
2366
2344
|
title = blocks[h1].text;
|
|
2367
2345
|
contentBlocks = blocks.filter((_, i) => i !== h1);
|
|
2346
|
+
titleSource = "h1";
|
|
2368
2347
|
}
|
|
2369
|
-
if (!contentBlocks.length) contentBlocks = [{
|
|
2370
|
-
type: "paragraph",
|
|
2371
|
-
text: ""
|
|
2372
|
-
}];
|
|
2373
2348
|
ydoc.transact(() => {
|
|
2374
2349
|
const headerEl = new yjs.XmlElement("documentHeader");
|
|
2375
2350
|
const metaEl = new yjs.XmlElement("documentMeta");
|
|
@@ -2399,7 +2374,8 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
|
|
|
2399
2374
|
case "fieldGroup": return new yjs.XmlElement("fieldGroup");
|
|
2400
2375
|
case "image": return new yjs.XmlElement("image");
|
|
2401
2376
|
case "docEmbed": return new yjs.XmlElement("docEmbed");
|
|
2402
|
-
case "
|
|
2377
|
+
case "mathBlock": return new yjs.XmlElement("mathBlock");
|
|
2378
|
+
case "fileBlock": return new yjs.XmlElement("fileBlock");
|
|
2403
2379
|
}
|
|
2404
2380
|
});
|
|
2405
2381
|
fragment.insert(0, [
|
|
@@ -2407,53 +2383,1316 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
|
|
|
2407
2383
|
metaEl,
|
|
2408
2384
|
...bodyEls
|
|
2409
2385
|
]);
|
|
2386
|
+
if (titleSource) headerEl.setAttribute("titleSource", titleSource);
|
|
2410
2387
|
const headerXt = new yjs.XmlText();
|
|
2411
2388
|
headerEl.insert(0, [headerXt]);
|
|
2412
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);
|
|
2413
2396
|
contentBlocks.forEach((block, i) => fillBlock(bodyEls[i], block));
|
|
2414
2397
|
});
|
|
2415
2398
|
}
|
|
2416
2399
|
|
|
2417
2400
|
//#endregion
|
|
2418
|
-
//#region packages/
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
aliases: ["info:doc"],
|
|
2429
|
-
description: "Show document metadata (label, type, meta, dates).",
|
|
2430
|
-
usage: "doc id=<docId> | name=<label> | path=<a/b/c>",
|
|
2431
|
-
async run(conn, args) {
|
|
2432
|
-
if (!conn) return "Not connected";
|
|
2433
|
-
const docId = resolveDocument(conn, args.params, args.positional);
|
|
2434
|
-
if (!docId) return "Document not found. Specify id=, name=, or path= to identify the document.";
|
|
2435
|
-
const treeMap = conn.getTreeMap();
|
|
2436
|
-
if (!treeMap) return "Not connected";
|
|
2437
|
-
const raw = treeMap.get(docId);
|
|
2438
|
-
if (!raw) return `Document ${docId} not found in tree.`;
|
|
2439
|
-
const entry = toPlain$1(raw);
|
|
2440
|
-
const lines = [
|
|
2441
|
-
`id: ${docId}`,
|
|
2442
|
-
`label: ${entry.label || "Untitled"}`,
|
|
2443
|
-
`type: ${entry.type ?? "—"}`,
|
|
2444
|
-
`parent: ${entry.parentId ?? "(root)"}`,
|
|
2445
|
-
`order: ${entry.order ?? 0}`,
|
|
2446
|
-
`created: ${entry.createdAt ? new Date(entry.createdAt).toISOString() : "—"}`,
|
|
2447
|
-
`updated: ${entry.updatedAt ? new Date(entry.updatedAt).toISOString() : "—"} (${relativeTime(entry.updatedAt)})`
|
|
2448
|
-
];
|
|
2449
|
-
if (entry.meta && Object.keys(entry.meta).length > 0) {
|
|
2450
|
-
lines.push(`meta:`);
|
|
2451
|
-
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;
|
|
2452
2411
|
}
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
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
|
+
}
|
|
2457
3696
|
});
|
|
2458
3697
|
registerCommand({
|
|
2459
3698
|
name: "read",
|
|
@@ -3143,23 +4382,24 @@ registerCommand({
|
|
|
3143
4382
|
registerCommand({
|
|
3144
4383
|
name: "chat",
|
|
3145
4384
|
aliases: ["send"],
|
|
3146
|
-
description: "Send a chat message to a channel.",
|
|
3147
|
-
usage: "chat
|
|
4385
|
+
description: "Send a chat message to a channel doc.",
|
|
4386
|
+
usage: "chat channel_doc_id=<docId> text=<message>",
|
|
3148
4387
|
async run(conn, args) {
|
|
3149
4388
|
if (!conn) return "Not connected";
|
|
3150
4389
|
const rootProvider = conn.rootProvider;
|
|
3151
4390
|
if (!rootProvider) return "Not connected";
|
|
3152
|
-
const
|
|
3153
|
-
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;
|
|
3154
4394
|
const text = args.params["text"] || args.params["content"] || args.positional[0];
|
|
3155
4395
|
if (!text) return "Missing required parameter: text=<message>";
|
|
3156
4396
|
rootProvider.sendStateless(JSON.stringify({
|
|
3157
|
-
type: "
|
|
3158
|
-
|
|
4397
|
+
type: "messages:send",
|
|
4398
|
+
channel_doc_id,
|
|
3159
4399
|
content: text,
|
|
3160
|
-
|
|
4400
|
+
mentions: []
|
|
3161
4401
|
}));
|
|
3162
|
-
return `Sent to ${
|
|
4402
|
+
return `Sent to ${channel_doc_id}`;
|
|
3163
4403
|
}
|
|
3164
4404
|
});
|
|
3165
4405
|
|
|
@@ -3329,6 +4569,16 @@ const PAGE_TYPES = {
|
|
|
3329
4569
|
core: true,
|
|
3330
4570
|
supportsChildren: true
|
|
3331
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
|
+
},
|
|
3332
4582
|
kanban: {
|
|
3333
4583
|
key: "kanban",
|
|
3334
4584
|
label: "Kanban",
|
|
@@ -4113,6 +5363,695 @@ registerCommand({
|
|
|
4113
5363
|
}
|
|
4114
5364
|
});
|
|
4115
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
|
+
|
|
4116
6055
|
//#endregion
|
|
4117
6056
|
//#region packages/cli/src/index.ts
|
|
4118
6057
|
/**
|
|
@@ -4136,7 +6075,9 @@ const NO_CONNECT_COMMANDS = new Set([
|
|
|
4136
6075
|
"v",
|
|
4137
6076
|
"page-types",
|
|
4138
6077
|
"types",
|
|
4139
|
-
"doctypes"
|
|
6078
|
+
"doctypes",
|
|
6079
|
+
"wiki",
|
|
6080
|
+
"wikipedia"
|
|
4140
6081
|
]);
|
|
4141
6082
|
async function main() {
|
|
4142
6083
|
const args = parseArgs(process.argv);
|