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