@abraca/cli 1.9.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abracadabra-cli.cjs +2516 -575
- package/dist/abracadabra-cli.cjs.map +1 -1
- package/dist/abracadabra-cli.esm.js +2515 -576
- package/dist/abracadabra-cli.esm.js.map +1 -1
- package/package.json +10 -5
- package/src/commands/awareness.ts +11 -8
- package/src/commands/spaces.ts +19 -17
- package/src/commands/tree.ts +3 -3
- package/src/commands/wiki/connect.ts +69 -0
- package/src/commands/wiki/index.ts +471 -0
- package/src/commands/wiki/render.ts +91 -0
- package/src/commands/wiki/snapshot.ts +210 -0
- package/src/commands/wiki/types.ts +45 -0
- package/src/commands/wiki/wikipedia.ts +154 -0
- package/src/connection.ts +15 -50
- package/src/crypto.ts +5 -4
- package/src/index.ts +10 -1
|
@@ -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,247 +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) {
|
|
1260
|
-
if (child.nodeName === "docLink") {
|
|
1261
|
-
const docId = child.getAttribute("docId");
|
|
1262
|
-
if (docId) parts.push(`[[${docId}]]`);
|
|
1263
|
-
continue;
|
|
1264
|
-
}
|
|
1265
|
-
parts.push(elementTextContent(child));
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
return parts.join("");
|
|
1269
|
-
}
|
|
1270
|
-
function serializeElement(el, indent = "") {
|
|
1271
|
-
switch (el.nodeName) {
|
|
1272
|
-
case "documentHeader":
|
|
1273
|
-
case "documentMeta": return "";
|
|
1274
|
-
case "heading": {
|
|
1275
|
-
const level = Number(el.getAttribute("level")) || 1;
|
|
1276
|
-
return `${"#".repeat(level)} ${elementTextContent(el)}`;
|
|
1277
|
-
}
|
|
1278
|
-
case "paragraph": return elementTextContent(el);
|
|
1279
|
-
case "bulletList": return serializeList(el, "bullet", indent);
|
|
1280
|
-
case "orderedList": return serializeList(el, "ordered", indent);
|
|
1281
|
-
case "taskList": return serializeTaskList(el, indent);
|
|
1282
|
-
case "codeBlock": return `\`\`\`${el.getAttribute("language") || ""}\n${elementTextContent(el)}\n\`\`\``;
|
|
1283
|
-
case "blockquote": {
|
|
1284
|
-
const lines = [];
|
|
1285
|
-
for (let i = 0; i < el.length; i++) {
|
|
1286
|
-
const child = el.get(i);
|
|
1287
|
-
if (child instanceof Y.XmlElement) lines.push(serializeElement(child, indent));
|
|
1288
|
-
}
|
|
1289
|
-
return lines.map((l) => `> ${l}`).join("\n");
|
|
1290
|
-
}
|
|
1291
|
-
case "horizontalRule": return "---";
|
|
1292
|
-
case "table": return serializeTable(el);
|
|
1293
|
-
case "docEmbed": {
|
|
1294
|
-
const docId = el.getAttribute("docId");
|
|
1295
|
-
if (!docId) return "";
|
|
1296
|
-
const seamlessAttr = el.getAttribute("seamless");
|
|
1297
|
-
return seamlessAttr === true || seamlessAttr === "true" ? `![[${docId}]]{seamless}` : `![[${docId}]]`;
|
|
1298
|
-
}
|
|
1299
|
-
case "svgEmbed": {
|
|
1300
|
-
const svg = el.getAttribute("svg") || "";
|
|
1301
|
-
const svgTitle = el.getAttribute("title") || "";
|
|
1302
|
-
if (!svg) return "";
|
|
1303
|
-
return `\`\`\`svg${svgTitle ? ` ${svgTitle}` : ""}\n${svg}\n\`\`\``;
|
|
1304
|
-
}
|
|
1305
|
-
case "image": {
|
|
1306
|
-
const src = el.getAttribute("src") || "";
|
|
1307
|
-
const alt = el.getAttribute("alt") || "";
|
|
1308
|
-
const w = el.getAttribute("width");
|
|
1309
|
-
const h = el.getAttribute("height");
|
|
1310
|
-
let attrs = "";
|
|
1311
|
-
if (w || h) {
|
|
1312
|
-
const parts = [];
|
|
1313
|
-
if (w) parts.push(`width="${w}"`);
|
|
1314
|
-
if (h) parts.push(`height="${h}"`);
|
|
1315
|
-
attrs = `{${parts.join(" ")}}`;
|
|
1316
|
-
}
|
|
1317
|
-
return `${attrs}`;
|
|
1318
|
-
}
|
|
1319
|
-
case "callout": return `::${el.getAttribute("type") || "note"}\n${serializeChildren(el, indent)}\n::`;
|
|
1320
|
-
case "collapsible": {
|
|
1321
|
-
const label = el.getAttribute("label") || "Details";
|
|
1322
|
-
const open = el.getAttribute("open");
|
|
1323
|
-
const props = [`label="${label}"`];
|
|
1324
|
-
if (open === true || open === "true") props.push("open=\"true\"");
|
|
1325
|
-
const inner = serializeChildren(el, indent);
|
|
1326
|
-
return `::collapsible{${props.join(" ")}}\n${inner}\n::`;
|
|
1327
|
-
}
|
|
1328
|
-
case "steps": return `::steps\n${serializeChildren(el, indent)}\n::`;
|
|
1329
|
-
case "card": {
|
|
1330
|
-
const title = el.getAttribute("title") || "";
|
|
1331
|
-
const icon = el.getAttribute("icon") || "";
|
|
1332
|
-
const to = el.getAttribute("to") || "";
|
|
1333
|
-
const props = [];
|
|
1334
|
-
if (title) props.push(`title="${title}"`);
|
|
1335
|
-
if (icon) props.push(`icon="${icon}"`);
|
|
1336
|
-
if (to) props.push(`to="${to}"`);
|
|
1337
|
-
const inner = serializeChildren(el, indent);
|
|
1338
|
-
return `::card{${props.join(" ")}}\n${inner}\n::`;
|
|
1339
|
-
}
|
|
1340
|
-
case "cardGroup": return `::card-group\n${serializeChildren(el, indent)}\n::`;
|
|
1341
|
-
case "codeCollapse": return `::code-collapse\n${serializeChildren(el, indent)}\n::`;
|
|
1342
|
-
case "codeGroup": return `::code-group\n${serializeChildren(el, indent)}\n::`;
|
|
1343
|
-
case "codePreview": return `::code-preview\n${serializeChildren(el, indent)}\n::`;
|
|
1344
|
-
case "codeTree": return `::code-tree{files="${el.getAttribute("files") || "[]"}"}\n::`;
|
|
1345
|
-
case "accordion": return serializeSlottedComponent(el, "accordion", "accordionItem", "item");
|
|
1346
|
-
case "tabs": return serializeSlottedComponent(el, "tabs", "tabsItem", "tab");
|
|
1347
|
-
case "field": {
|
|
1348
|
-
const fieldName = el.getAttribute("name") || "";
|
|
1349
|
-
const fieldType = el.getAttribute("type") || "string";
|
|
1350
|
-
const required = el.getAttribute("required");
|
|
1351
|
-
const props = [];
|
|
1352
|
-
if (fieldName) props.push(`name="${fieldName}"`);
|
|
1353
|
-
props.push(`type="${fieldType}"`);
|
|
1354
|
-
if (required === true || required === "true") props.push("required=\"true\"");
|
|
1355
|
-
const inner = serializeChildren(el, indent);
|
|
1356
|
-
return `::field{${props.join(" ")}}\n${inner}\n::`;
|
|
1357
|
-
}
|
|
1358
|
-
case "fieldGroup": return `::field-group\n${serializeChildren(el, indent)}\n::`;
|
|
1359
|
-
default: return serializeChildren(el, indent);
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
function serializeList(el, type, indent) {
|
|
1363
|
-
const lines = [];
|
|
1364
|
-
for (let i = 0; i < el.length; i++) {
|
|
1365
|
-
const item = el.get(i);
|
|
1366
|
-
if (item instanceof Y.XmlElement && item.nodeName === "listItem") {
|
|
1367
|
-
const prefix = type === "bullet" ? "- " : `${i + 1}. `;
|
|
1368
|
-
const content = elementTextContent(item);
|
|
1369
|
-
lines.push(`${indent}${prefix}${content}`);
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
return lines.join("\n");
|
|
1373
|
-
}
|
|
1374
|
-
function serializeTaskList(el, indent) {
|
|
1375
|
-
const lines = [];
|
|
1376
|
-
for (let i = 0; i < el.length; i++) {
|
|
1377
|
-
const item = el.get(i);
|
|
1378
|
-
if (item instanceof Y.XmlElement && item.nodeName === "taskItem") {
|
|
1379
|
-
const checked = item.getAttribute("checked");
|
|
1380
|
-
const marker = checked === true || checked === "true" ? "[x]" : "[ ]";
|
|
1381
|
-
const content = elementTextContent(item);
|
|
1382
|
-
lines.push(`${indent}- ${marker} ${content}`);
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
return lines.join("\n");
|
|
1386
|
-
}
|
|
1387
|
-
function serializeTable(el) {
|
|
1388
|
-
const rows = [];
|
|
1389
|
-
for (let i = 0; i < el.length; i++) {
|
|
1390
|
-
const row = el.get(i);
|
|
1391
|
-
if (!(row instanceof Y.XmlElement) || row.nodeName !== "tableRow") continue;
|
|
1392
|
-
const cells = [];
|
|
1393
|
-
for (let j = 0; j < row.length; j++) {
|
|
1394
|
-
const cell = row.get(j);
|
|
1395
|
-
if (cell instanceof Y.XmlElement) cells.push(elementTextContent(cell));
|
|
1396
|
-
}
|
|
1397
|
-
rows.push(cells);
|
|
1398
|
-
}
|
|
1399
|
-
if (!rows.length) return "";
|
|
1400
|
-
const headerRow = rows[0];
|
|
1401
|
-
const lines = [`| ${headerRow.join(" | ")} |`];
|
|
1402
|
-
lines.push(`| ${headerRow.map(() => "---").join(" | ")} |`);
|
|
1403
|
-
for (let i = 1; i < rows.length; i++) lines.push(`| ${rows[i].join(" | ")} |`);
|
|
1404
|
-
return lines.join("\n");
|
|
1405
|
-
}
|
|
1406
|
-
function serializeSlottedComponent(el, componentName, childNodeName, slotName) {
|
|
1407
|
-
const parts = [`::${componentName}`];
|
|
1408
|
-
for (let i = 0; i < el.length; i++) {
|
|
1409
|
-
const child = el.get(i);
|
|
1410
|
-
if (!(child instanceof Y.XmlElement) || child.nodeName !== childNodeName) continue;
|
|
1411
|
-
const label = child.getAttribute("label") || `Item ${i + 1}`;
|
|
1412
|
-
const icon = child.getAttribute("icon") || "";
|
|
1413
|
-
const props = [`label="${label}"`];
|
|
1414
|
-
if (icon) props.push(`icon="${icon}"`);
|
|
1415
|
-
parts.push(`#${slotName}{${props.join(" ")}}`);
|
|
1416
|
-
const inner = serializeChildren(child, "");
|
|
1417
|
-
if (inner) parts.push(inner);
|
|
1418
|
-
}
|
|
1419
|
-
parts.push("::");
|
|
1420
|
-
return parts.join("\n");
|
|
1421
|
-
}
|
|
1422
|
-
function serializeChildren(el, indent) {
|
|
1423
|
-
const parts = [];
|
|
1424
|
-
for (let i = 0; i < el.length; i++) {
|
|
1425
|
-
const child = el.get(i);
|
|
1426
|
-
if (child instanceof Y.XmlElement) {
|
|
1427
|
-
const serialized = serializeElement(child, indent);
|
|
1428
|
-
if (serialized) parts.push(serialized);
|
|
1429
|
-
} else if (child instanceof Y.XmlText) {
|
|
1430
|
-
const text = xmlTextToMarkdown(child);
|
|
1431
|
-
if (text) parts.push(text);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
return parts.join("\n\n");
|
|
1435
|
-
}
|
|
1436
|
-
/**
|
|
1437
|
-
* Converts a Y.XmlFragment (TipTap document) to markdown.
|
|
1438
|
-
* Extracts the title from the documentHeader element.
|
|
1439
|
-
*
|
|
1440
|
-
* @returns `{ title, markdown }` where title is the H1/header text
|
|
1441
|
-
*/
|
|
1442
|
-
function yjsToMarkdown(fragment) {
|
|
1443
|
-
let title = "Untitled";
|
|
1444
|
-
const bodyParts = [];
|
|
1445
|
-
for (let i = 0; i < fragment.length; i++) {
|
|
1446
|
-
const child = fragment.get(i);
|
|
1447
|
-
if (!(child instanceof Y.XmlElement)) continue;
|
|
1448
|
-
if (child.nodeName === "documentHeader") {
|
|
1449
|
-
title = elementTextContent(child) || "Untitled";
|
|
1450
|
-
continue;
|
|
1451
|
-
}
|
|
1452
|
-
if (child.nodeName === "documentMeta") continue;
|
|
1453
|
-
const serialized = serializeElement(child);
|
|
1454
|
-
if (serialized !== "") bodyParts.push(serialized);
|
|
1455
|
-
}
|
|
1456
|
-
return {
|
|
1457
|
-
title,
|
|
1458
|
-
markdown: bodyParts.join("\n\n")
|
|
1459
|
-
};
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
//#endregion
|
|
1463
|
-
//#region packages/mcp/src/converters/markdownToYjs.ts
|
|
1464
|
-
/**
|
|
1465
|
-
* Markdown → Y.js converter.
|
|
1466
|
-
* Ported from cou-sh/app/utils/markdownToYjs.ts with Vue dependency removed.
|
|
1467
|
-
*/
|
|
1321
|
+
//#region packages/convert/src/markdown-to-yjs.ts
|
|
1468
1322
|
function parseInlineArray(raw) {
|
|
1469
1323
|
return raw.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
1470
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
|
+
}
|
|
1471
1329
|
function parseFrontmatter(markdown) {
|
|
1472
1330
|
const noResult = {
|
|
1473
1331
|
meta: {},
|
|
@@ -1498,8 +1356,8 @@ function parseFrontmatter(markdown) {
|
|
|
1498
1356
|
if (kvMatch) {
|
|
1499
1357
|
const key = kvMatch[1];
|
|
1500
1358
|
const val = kvMatch[2].trim();
|
|
1501
|
-
if (val.startsWith("[") && val.endsWith("]")) raw[key] = parseInlineArray(val);
|
|
1502
|
-
else raw[key] = val;
|
|
1359
|
+
if (val.startsWith("[") && val.endsWith("]")) raw[key] = parseInlineArray(val).map(stripQuotes);
|
|
1360
|
+
else raw[key] = stripQuotes(val);
|
|
1503
1361
|
}
|
|
1504
1362
|
i++;
|
|
1505
1363
|
}
|
|
@@ -1526,54 +1384,27 @@ function parseFrontmatter(markdown) {
|
|
|
1526
1384
|
}[priorityRaw.toLowerCase()] ?? (Number(priorityRaw) || 0);
|
|
1527
1385
|
const checkedRaw = raw["checked"] ?? raw["done"];
|
|
1528
1386
|
if (checkedRaw !== void 0) meta.checked = checkedRaw === "true" || checkedRaw === true;
|
|
1529
|
-
const dateStart = getStr([
|
|
1387
|
+
const dateStart = getStr([
|
|
1388
|
+
"dateStart",
|
|
1389
|
+
"date",
|
|
1390
|
+
"created"
|
|
1391
|
+
]);
|
|
1530
1392
|
if (dateStart) meta.dateStart = dateStart;
|
|
1531
|
-
const dateEnd = getStr(["due"]);
|
|
1393
|
+
const dateEnd = getStr(["dateEnd", "due"]);
|
|
1532
1394
|
if (dateEnd) meta.dateEnd = dateEnd;
|
|
1533
|
-
const subtitle = getStr(["
|
|
1395
|
+
const subtitle = getStr(["subtitle", "description"]);
|
|
1534
1396
|
if (subtitle) meta.subtitle = subtitle;
|
|
1535
1397
|
const url = getStr(["url"]);
|
|
1536
1398
|
if (url) meta.url = url;
|
|
1537
|
-
const email = getStr(["email"]);
|
|
1538
|
-
if (email) meta.email = email;
|
|
1539
|
-
const phone = getStr(["phone"]);
|
|
1540
|
-
if (phone) meta.phone = phone;
|
|
1541
1399
|
const ratingRaw = getStr(["rating"]);
|
|
1542
1400
|
if (ratingRaw !== void 0) {
|
|
1543
1401
|
const n = Number(ratingRaw);
|
|
1544
1402
|
if (!Number.isNaN(n)) meta.rating = Math.min(5, Math.max(0, n));
|
|
1545
1403
|
}
|
|
1546
|
-
const
|
|
1547
|
-
if (datetimeStart) meta.datetimeStart = datetimeStart;
|
|
1548
|
-
const datetimeEnd = getStr(["datetimeEnd"]);
|
|
1549
|
-
if (datetimeEnd) meta.datetimeEnd = datetimeEnd;
|
|
1550
|
-
const allDayRaw = raw["allDay"];
|
|
1551
|
-
if (allDayRaw !== void 0) meta.allDay = allDayRaw === "true" || allDayRaw === true;
|
|
1552
|
-
const geoLatRaw = getStr(["geoLat"]);
|
|
1553
|
-
if (geoLatRaw !== void 0) {
|
|
1554
|
-
const n = Number(geoLatRaw);
|
|
1555
|
-
if (!Number.isNaN(n)) meta.geoLat = n;
|
|
1556
|
-
}
|
|
1557
|
-
const geoLngRaw = getStr(["geoLng"]);
|
|
1558
|
-
if (geoLngRaw !== void 0) {
|
|
1559
|
-
const n = Number(geoLngRaw);
|
|
1560
|
-
if (!Number.isNaN(n)) meta.geoLng = n;
|
|
1561
|
-
}
|
|
1562
|
-
const geoType = getStr(["geoType"]);
|
|
1563
|
-
if (geoType && (geoType === "marker" || geoType === "line" || geoType === "measure")) meta.geoType = geoType;
|
|
1564
|
-
const geoDescription = getStr(["geoDescription"]);
|
|
1565
|
-
if (geoDescription) meta.geoDescription = geoDescription;
|
|
1566
|
-
const numberRaw = getStr(["number"]);
|
|
1567
|
-
if (numberRaw !== void 0) {
|
|
1568
|
-
const n = Number(numberRaw);
|
|
1569
|
-
if (!Number.isNaN(n)) meta.number = n;
|
|
1570
|
-
}
|
|
1571
|
-
const unit = getStr(["unit"]);
|
|
1572
|
-
if (unit) meta.unit = unit;
|
|
1573
|
-
const note = getStr(["note"]);
|
|
1574
|
-
if (note) meta.note = note;
|
|
1404
|
+
const rawTitle = typeof raw["title"] === "string" ? raw["title"] : void 0;
|
|
1575
1405
|
return {
|
|
1576
|
-
title:
|
|
1406
|
+
title: rawTitle !== void 0 ? stripQuotes(rawTitle) : void 0,
|
|
1407
|
+
type: getStr(["type"]),
|
|
1577
1408
|
meta,
|
|
1578
1409
|
body
|
|
1579
1410
|
};
|
|
@@ -1581,66 +1412,79 @@ function parseFrontmatter(markdown) {
|
|
|
1581
1412
|
function parseInline(text) {
|
|
1582
1413
|
const stripped = text.replace(/\{lang="[^"]*"\}/g, "").replace(/:(?!badge|icon|kbd)(\w[\w-]*)\[([^\]]*)\](\{[^}]*\})?/g, "$2").replace(/:(?!badge|icon|kbd)(\w[\w-]*)(\{[^}]*\})/g, "");
|
|
1583
1414
|
const tokens = [];
|
|
1584
|
-
const re =
|
|
1415
|
+
const re = /\$([^$\n]+?)\$|@\[([^\]]+?)\]\(user:([^)]+?)\)|:badge\[([^\]]*)\](\{[^}]*\})?|:icon\{([^}]*)\}|:kbd\{([^}]*)\}|\[\[([0-9a-fA-F-]{36})(?:\|([^\]]+?))?\]\]|~~(.+?)~~|\*\*(.+?)\*\*|\*(.+?)\*|_(.+?)_|`(.+?)`|\[(.+?)\]\((.+?)\)/g;
|
|
1585
1416
|
let lastIndex = 0;
|
|
1586
1417
|
let match;
|
|
1587
1418
|
while ((match = re.exec(stripped)) !== null) {
|
|
1588
1419
|
if (match.index > lastIndex) tokens.push({ text: stripped.slice(lastIndex, match.index) });
|
|
1589
|
-
if (match[1] !== void 0) {
|
|
1590
|
-
|
|
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]);
|
|
1591
1433
|
tokens.push({
|
|
1592
|
-
text: match[
|
|
1434
|
+
text: match[4] || "Badge",
|
|
1593
1435
|
attrs: { badge: {
|
|
1594
|
-
label: match[
|
|
1436
|
+
label: match[4] || "Badge",
|
|
1595
1437
|
color: badgeProps["color"] || "neutral",
|
|
1596
1438
|
variant: badgeProps["variant"] || "subtle"
|
|
1597
1439
|
} }
|
|
1598
1440
|
});
|
|
1599
|
-
} else if (match[
|
|
1600
|
-
const iconProps = parseMdcProps(`{${match[
|
|
1441
|
+
} else if (match[6] !== void 0) {
|
|
1442
|
+
const iconProps = parseMdcProps(`{${match[6]}}`);
|
|
1601
1443
|
tokens.push({
|
|
1602
1444
|
text: "",
|
|
1603
1445
|
attrs: { proseIcon: { name: iconProps["name"] || "i-lucide-star" } }
|
|
1604
1446
|
});
|
|
1605
|
-
} else if (match[
|
|
1606
|
-
const kbdProps = parseMdcProps(`{${match[
|
|
1447
|
+
} else if (match[7] !== void 0) {
|
|
1448
|
+
const kbdProps = parseMdcProps(`{${match[7]}}`);
|
|
1607
1449
|
tokens.push({
|
|
1608
1450
|
text: kbdProps["value"] || "",
|
|
1609
1451
|
attrs: { kbd: { value: kbdProps["value"] || "" } }
|
|
1610
1452
|
});
|
|
1611
|
-
} else if (match[
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1453
|
+
} else if (match[8] !== void 0) {
|
|
1454
|
+
const docId = match[8];
|
|
1455
|
+
const label = match[9] ?? docId;
|
|
1456
|
+
tokens.push({
|
|
1457
|
+
text: label,
|
|
1458
|
+
attrs: { docLink: { docId } }
|
|
1459
|
+
});
|
|
1460
|
+
} else if (match[10] !== void 0) tokens.push({
|
|
1461
|
+
text: match[10],
|
|
1618
1462
|
attrs: { strike: true }
|
|
1619
1463
|
});
|
|
1620
|
-
else if (match[
|
|
1621
|
-
text: match[
|
|
1464
|
+
else if (match[11] !== void 0) tokens.push({
|
|
1465
|
+
text: match[11],
|
|
1622
1466
|
attrs: { bold: true }
|
|
1623
1467
|
});
|
|
1624
|
-
else if (match[
|
|
1625
|
-
text: match[
|
|
1468
|
+
else if (match[12] !== void 0) tokens.push({
|
|
1469
|
+
text: match[12],
|
|
1626
1470
|
attrs: { italic: true }
|
|
1627
1471
|
});
|
|
1628
|
-
else if (match[
|
|
1629
|
-
text: match[
|
|
1472
|
+
else if (match[13] !== void 0) tokens.push({
|
|
1473
|
+
text: match[13],
|
|
1630
1474
|
attrs: { italic: true }
|
|
1631
1475
|
});
|
|
1632
|
-
else if (match[
|
|
1633
|
-
text: match[
|
|
1476
|
+
else if (match[14] !== void 0) tokens.push({
|
|
1477
|
+
text: match[14],
|
|
1634
1478
|
attrs: { code: true }
|
|
1635
1479
|
});
|
|
1636
|
-
else if (match[
|
|
1637
|
-
text: match[
|
|
1638
|
-
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] } }
|
|
1639
1483
|
});
|
|
1640
1484
|
lastIndex = match.index + match[0].length;
|
|
1641
1485
|
}
|
|
1642
1486
|
if (lastIndex < stripped.length) tokens.push({ text: stripped.slice(lastIndex) });
|
|
1643
|
-
return tokens.filter((t) => t.
|
|
1487
|
+
return tokens.filter((t) => t.text.length > 0);
|
|
1644
1488
|
}
|
|
1645
1489
|
function parseTableRow(line) {
|
|
1646
1490
|
const parts = line.split("|");
|
|
@@ -1649,6 +1493,7 @@ function parseTableRow(line) {
|
|
|
1649
1493
|
function isTableSeparator(line) {
|
|
1650
1494
|
return /^\|[\s|:-]+\|$/.test(line.trim());
|
|
1651
1495
|
}
|
|
1496
|
+
/** Extract fenced code blocks from MDC #code slot lines. */
|
|
1652
1497
|
function extractFencedCode(lines) {
|
|
1653
1498
|
const result = [];
|
|
1654
1499
|
let i = 0;
|
|
@@ -1675,14 +1520,23 @@ function extractFencedCode(lines) {
|
|
|
1675
1520
|
}
|
|
1676
1521
|
return result;
|
|
1677
1522
|
}
|
|
1523
|
+
/** Extract key="value" pairs from MDC prop syntax `{key="value" other="x"}` */
|
|
1678
1524
|
function parseMdcProps(propsStr) {
|
|
1679
1525
|
if (!propsStr) return {};
|
|
1680
1526
|
const result = {};
|
|
1681
|
-
|
|
1527
|
+
let s = propsStr.trim();
|
|
1528
|
+
if (s.startsWith("{") && s.endsWith("}")) s = s.slice(1, -1);
|
|
1529
|
+
const re = /(\w[\w-]*)(?:=(?:"([^"]*)"|([^\s"}]+)))?/g;
|
|
1682
1530
|
let m;
|
|
1683
|
-
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
|
+
}
|
|
1684
1537
|
return result;
|
|
1685
1538
|
}
|
|
1539
|
+
/** Parse named child MDC blocks from inner lines (e.g. #item for accordion, #tab for tabs) */
|
|
1686
1540
|
function parseMdcChildren(innerLines, slotPrefix) {
|
|
1687
1541
|
const items = [];
|
|
1688
1542
|
let current = null;
|
|
@@ -1714,6 +1568,95 @@ function parseMdcChildren(innerLines, slotPrefix) {
|
|
|
1714
1568
|
}));
|
|
1715
1569
|
}
|
|
1716
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
|
+
}
|
|
1717
1660
|
function parseBlocks(markdown) {
|
|
1718
1661
|
const rawLines = markdown.split("\n");
|
|
1719
1662
|
let firstContentLine = 0;
|
|
@@ -1739,17 +1682,15 @@ function parseBlocks(markdown) {
|
|
|
1739
1682
|
i++;
|
|
1740
1683
|
}
|
|
1741
1684
|
i++;
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
});
|
|
1749
|
-
} 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({
|
|
1750
1691
|
type: "codeBlock",
|
|
1751
1692
|
lang,
|
|
1752
|
-
code
|
|
1693
|
+
code
|
|
1753
1694
|
});
|
|
1754
1695
|
continue;
|
|
1755
1696
|
}
|
|
@@ -1768,14 +1709,16 @@ function parseBlocks(markdown) {
|
|
|
1768
1709
|
i++;
|
|
1769
1710
|
continue;
|
|
1770
1711
|
}
|
|
1771
|
-
const
|
|
1772
|
-
if (
|
|
1773
|
-
const
|
|
1774
|
-
const
|
|
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]);
|
|
1775
1717
|
blocks.push({
|
|
1776
1718
|
type: "docEmbed",
|
|
1777
|
-
docId
|
|
1778
|
-
|
|
1719
|
+
docId,
|
|
1720
|
+
label,
|
|
1721
|
+
props
|
|
1779
1722
|
});
|
|
1780
1723
|
i++;
|
|
1781
1724
|
continue;
|
|
@@ -1827,6 +1770,23 @@ function parseBlocks(markdown) {
|
|
|
1827
1770
|
});
|
|
1828
1771
|
continue;
|
|
1829
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
|
+
}
|
|
1830
1790
|
const MDC_OPEN = /^\s*(:{2,})(\w[\w-]*)(\{[^}]*\})?\s*$/;
|
|
1831
1791
|
if (MDC_OPEN.test(line)) {
|
|
1832
1792
|
const colons = line.match(/^\s*(:+)/)?.[1]?.length ?? 2;
|
|
@@ -1995,15 +1955,8 @@ function parseBlocks(markdown) {
|
|
|
1995
1955
|
continue;
|
|
1996
1956
|
}
|
|
1997
1957
|
if (TASK_RE.test(line)) {
|
|
1998
|
-
const items =
|
|
1999
|
-
|
|
2000
|
-
const m = lines[i].match(TASK_RE);
|
|
2001
|
-
items.push({
|
|
2002
|
-
checked: m[1].toLowerCase() === "x",
|
|
2003
|
-
text: m[2]
|
|
2004
|
-
});
|
|
2005
|
-
i++;
|
|
2006
|
-
}
|
|
1958
|
+
const { items, next } = consumeList(lines, i, 0, "task");
|
|
1959
|
+
i = next;
|
|
2007
1960
|
blocks.push({
|
|
2008
1961
|
type: "taskList",
|
|
2009
1962
|
items
|
|
@@ -2011,12 +1964,9 @@ function parseBlocks(markdown) {
|
|
|
2011
1964
|
continue;
|
|
2012
1965
|
}
|
|
2013
1966
|
if (/^[-*+]\s+/.test(line)) {
|
|
2014
|
-
const items =
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
i++;
|
|
2018
|
-
}
|
|
2019
|
-
if (items.length) {
|
|
1967
|
+
const { items, next } = consumeList(lines, i, 0, "bullet");
|
|
1968
|
+
if (items.length > 0) {
|
|
1969
|
+
i = next;
|
|
2020
1970
|
blocks.push({
|
|
2021
1971
|
type: "bulletList",
|
|
2022
1972
|
items
|
|
@@ -2025,16 +1975,15 @@ function parseBlocks(markdown) {
|
|
|
2025
1975
|
}
|
|
2026
1976
|
}
|
|
2027
1977
|
if (/^\d+\.\s+/.test(line)) {
|
|
2028
|
-
const items =
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
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;
|
|
2032
1986
|
}
|
|
2033
|
-
blocks.push({
|
|
2034
|
-
type: "orderedList",
|
|
2035
|
-
items
|
|
2036
|
-
});
|
|
2037
|
-
continue;
|
|
2038
1987
|
}
|
|
2039
1988
|
if (line.trim() === "") {
|
|
2040
1989
|
i++;
|
|
@@ -2052,23 +2001,18 @@ function parseBlocks(markdown) {
|
|
|
2052
2001
|
}
|
|
2053
2002
|
return blocks;
|
|
2054
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
|
+
*/
|
|
2055
2008
|
function fillTextInto(el, tokens) {
|
|
2056
|
-
const filtered = tokens.filter((t) => t.
|
|
2009
|
+
const filtered = tokens.filter((t) => t.text.length > 0);
|
|
2057
2010
|
if (!filtered.length) return;
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2060
|
-
const xe = new Y.XmlElement(tok.node);
|
|
2061
|
-
if (tok.nodeAttrs) for (const [k, v] of Object.entries(tok.nodeAttrs)) xe.setAttribute(k, v);
|
|
2062
|
-
return xe;
|
|
2063
|
-
}
|
|
2064
|
-
return new Y.XmlText();
|
|
2065
|
-
});
|
|
2066
|
-
el.insert(0, children);
|
|
2011
|
+
const xtNodes = filtered.map(() => new Y.XmlText());
|
|
2012
|
+
el.insert(0, xtNodes);
|
|
2067
2013
|
filtered.forEach((tok, i) => {
|
|
2068
|
-
if (tok.
|
|
2069
|
-
|
|
2070
|
-
if (tok.attrs) xt.insert(0, tok.text, tok.attrs);
|
|
2071
|
-
else xt.insert(0, tok.text);
|
|
2014
|
+
if (tok.attrs) xtNodes[i].insert(0, tok.text, tok.attrs);
|
|
2015
|
+
else xtNodes[i].insert(0, tok.text);
|
|
2072
2016
|
});
|
|
2073
2017
|
}
|
|
2074
2018
|
function blockElName(b) {
|
|
@@ -2097,9 +2041,19 @@ function blockElName(b) {
|
|
|
2097
2041
|
case "fieldGroup": return "fieldGroup";
|
|
2098
2042
|
case "image": return "image";
|
|
2099
2043
|
case "docEmbed": return "docEmbed";
|
|
2100
|
-
case "
|
|
2044
|
+
case "mathBlock": return "mathBlock";
|
|
2045
|
+
case "fileBlock": return "fileBlock";
|
|
2101
2046
|
}
|
|
2102
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
|
+
}
|
|
2103
2057
|
function fillBlock(el, block) {
|
|
2104
2058
|
switch (block.type) {
|
|
2105
2059
|
case "heading":
|
|
@@ -2113,10 +2067,8 @@ function fillBlock(el, block) {
|
|
|
2113
2067
|
case "orderedList": {
|
|
2114
2068
|
const listItemEls = block.items.map(() => new Y.XmlElement("listItem"));
|
|
2115
2069
|
el.insert(0, listItemEls);
|
|
2116
|
-
block.items.forEach((
|
|
2117
|
-
|
|
2118
|
-
listItemEls[i].insert(0, [paraEl]);
|
|
2119
|
-
fillTextInto(paraEl, parseInline(text));
|
|
2070
|
+
block.items.forEach((item, i) => {
|
|
2071
|
+
populateListItemChildren(listItemEls[i], item, "listItem");
|
|
2120
2072
|
});
|
|
2121
2073
|
break;
|
|
2122
2074
|
}
|
|
@@ -2124,10 +2076,8 @@ function fillBlock(el, block) {
|
|
|
2124
2076
|
const taskItemEls = block.items.map(() => new Y.XmlElement("taskItem"));
|
|
2125
2077
|
el.insert(0, taskItemEls);
|
|
2126
2078
|
block.items.forEach((item, i) => {
|
|
2127
|
-
taskItemEls[i].setAttribute("checked", item.checked);
|
|
2128
|
-
|
|
2129
|
-
taskItemEls[i].insert(0, [paraEl]);
|
|
2130
|
-
fillTextInto(paraEl, parseInline(item.text));
|
|
2079
|
+
taskItemEls[i].setAttribute("checked", !!item.checked);
|
|
2080
|
+
populateListItemChildren(taskItemEls[i], item, "taskItem");
|
|
2131
2081
|
});
|
|
2132
2082
|
break;
|
|
2133
2083
|
}
|
|
@@ -2315,32 +2265,55 @@ function fillBlock(el, block) {
|
|
|
2315
2265
|
break;
|
|
2316
2266
|
case "docEmbed":
|
|
2317
2267
|
el.setAttribute("docId", block.docId);
|
|
2318
|
-
|
|
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);
|
|
2319
2273
|
break;
|
|
2320
|
-
case "
|
|
2321
|
-
el.setAttribute("
|
|
2322
|
-
|
|
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);
|
|
2323
2282
|
break;
|
|
2324
2283
|
}
|
|
2325
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
|
+
*/
|
|
2326
2296
|
function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled") {
|
|
2327
2297
|
const ydoc = fragment.doc;
|
|
2328
2298
|
if (!ydoc) {
|
|
2329
2299
|
console.warn("[markdownToYjs] fragment has no doc — skipping population");
|
|
2330
2300
|
return;
|
|
2331
2301
|
}
|
|
2332
|
-
const
|
|
2302
|
+
const fm = parseFrontmatter(markdown);
|
|
2303
|
+
const blocks = parseBlocks(fm.body);
|
|
2333
2304
|
let title = fallbackTitle;
|
|
2305
|
+
let titleSource;
|
|
2306
|
+
if (fm.title !== void 0) {
|
|
2307
|
+
title = fm.title;
|
|
2308
|
+
titleSource = "frontmatter";
|
|
2309
|
+
}
|
|
2334
2310
|
let contentBlocks = blocks;
|
|
2335
2311
|
const h1 = blocks.findIndex((b) => b.type === "heading" && b.level === 1);
|
|
2336
2312
|
if (h1 !== -1) {
|
|
2337
2313
|
title = blocks[h1].text;
|
|
2338
2314
|
contentBlocks = blocks.filter((_, i) => i !== h1);
|
|
2315
|
+
titleSource = "h1";
|
|
2339
2316
|
}
|
|
2340
|
-
if (!contentBlocks.length) contentBlocks = [{
|
|
2341
|
-
type: "paragraph",
|
|
2342
|
-
text: ""
|
|
2343
|
-
}];
|
|
2344
2317
|
ydoc.transact(() => {
|
|
2345
2318
|
const headerEl = new Y.XmlElement("documentHeader");
|
|
2346
2319
|
const metaEl = new Y.XmlElement("documentMeta");
|
|
@@ -2370,7 +2343,8 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
|
|
|
2370
2343
|
case "fieldGroup": return new Y.XmlElement("fieldGroup");
|
|
2371
2344
|
case "image": return new Y.XmlElement("image");
|
|
2372
2345
|
case "docEmbed": return new Y.XmlElement("docEmbed");
|
|
2373
|
-
case "
|
|
2346
|
+
case "mathBlock": return new Y.XmlElement("mathBlock");
|
|
2347
|
+
case "fileBlock": return new Y.XmlElement("fileBlock");
|
|
2374
2348
|
}
|
|
2375
2349
|
});
|
|
2376
2350
|
fragment.insert(0, [
|
|
@@ -2378,53 +2352,1316 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
|
|
|
2378
2352
|
metaEl,
|
|
2379
2353
|
...bodyEls
|
|
2380
2354
|
]);
|
|
2355
|
+
if (titleSource) headerEl.setAttribute("titleSource", titleSource);
|
|
2381
2356
|
const headerXt = new Y.XmlText();
|
|
2382
2357
|
headerEl.insert(0, [headerXt]);
|
|
2383
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);
|
|
2384
2365
|
contentBlocks.forEach((block, i) => fillBlock(bodyEls[i], block));
|
|
2385
2366
|
});
|
|
2386
2367
|
}
|
|
2387
2368
|
|
|
2388
2369
|
//#endregion
|
|
2389
|
-
//#region packages/
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
aliases: ["info:doc"],
|
|
2400
|
-
description: "Show document metadata (label, type, meta, dates).",
|
|
2401
|
-
usage: "doc id=<docId> | name=<label> | path=<a/b/c>",
|
|
2402
|
-
async run(conn, args) {
|
|
2403
|
-
if (!conn) return "Not connected";
|
|
2404
|
-
const docId = resolveDocument(conn, args.params, args.positional);
|
|
2405
|
-
if (!docId) return "Document not found. Specify id=, name=, or path= to identify the document.";
|
|
2406
|
-
const treeMap = conn.getTreeMap();
|
|
2407
|
-
if (!treeMap) return "Not connected";
|
|
2408
|
-
const raw = treeMap.get(docId);
|
|
2409
|
-
if (!raw) return `Document ${docId} not found in tree.`;
|
|
2410
|
-
const entry = toPlain$1(raw);
|
|
2411
|
-
const lines = [
|
|
2412
|
-
`id: ${docId}`,
|
|
2413
|
-
`label: ${entry.label || "Untitled"}`,
|
|
2414
|
-
`type: ${entry.type ?? "—"}`,
|
|
2415
|
-
`parent: ${entry.parentId ?? "(root)"}`,
|
|
2416
|
-
`order: ${entry.order ?? 0}`,
|
|
2417
|
-
`created: ${entry.createdAt ? new Date(entry.createdAt).toISOString() : "—"}`,
|
|
2418
|
-
`updated: ${entry.updatedAt ? new Date(entry.updatedAt).toISOString() : "—"} (${relativeTime(entry.updatedAt)})`
|
|
2419
|
-
];
|
|
2420
|
-
if (entry.meta && Object.keys(entry.meta).length > 0) {
|
|
2421
|
-
lines.push(`meta:`);
|
|
2422
|
-
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;
|
|
2423
2380
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
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
|
+
}
|
|
2428
3665
|
});
|
|
2429
3666
|
registerCommand({
|
|
2430
3667
|
name: "read",
|
|
@@ -3114,23 +4351,24 @@ registerCommand({
|
|
|
3114
4351
|
registerCommand({
|
|
3115
4352
|
name: "chat",
|
|
3116
4353
|
aliases: ["send"],
|
|
3117
|
-
description: "Send a chat message to a channel.",
|
|
3118
|
-
usage: "chat
|
|
4354
|
+
description: "Send a chat message to a channel doc.",
|
|
4355
|
+
usage: "chat channel_doc_id=<docId> text=<message>",
|
|
3119
4356
|
async run(conn, args) {
|
|
3120
4357
|
if (!conn) return "Not connected";
|
|
3121
4358
|
const rootProvider = conn.rootProvider;
|
|
3122
4359
|
if (!rootProvider) return "Not connected";
|
|
3123
|
-
const
|
|
3124
|
-
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;
|
|
3125
4363
|
const text = args.params["text"] || args.params["content"] || args.positional[0];
|
|
3126
4364
|
if (!text) return "Missing required parameter: text=<message>";
|
|
3127
4365
|
rootProvider.sendStateless(JSON.stringify({
|
|
3128
|
-
type: "
|
|
3129
|
-
|
|
4366
|
+
type: "messages:send",
|
|
4367
|
+
channel_doc_id,
|
|
3130
4368
|
content: text,
|
|
3131
|
-
|
|
4369
|
+
mentions: []
|
|
3132
4370
|
}));
|
|
3133
|
-
return `Sent to ${
|
|
4371
|
+
return `Sent to ${channel_doc_id}`;
|
|
3134
4372
|
}
|
|
3135
4373
|
});
|
|
3136
4374
|
|
|
@@ -3300,6 +4538,16 @@ const PAGE_TYPES = {
|
|
|
3300
4538
|
core: true,
|
|
3301
4539
|
supportsChildren: true
|
|
3302
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
|
+
},
|
|
3303
4551
|
kanban: {
|
|
3304
4552
|
key: "kanban",
|
|
3305
4553
|
label: "Kanban",
|
|
@@ -4084,6 +5332,695 @@ registerCommand({
|
|
|
4084
5332
|
}
|
|
4085
5333
|
});
|
|
4086
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
|
+
|
|
4087
6024
|
//#endregion
|
|
4088
6025
|
//#region packages/cli/src/index.ts
|
|
4089
6026
|
/**
|
|
@@ -4107,7 +6044,9 @@ const NO_CONNECT_COMMANDS = new Set([
|
|
|
4107
6044
|
"v",
|
|
4108
6045
|
"page-types",
|
|
4109
6046
|
"types",
|
|
4110
|
-
"doctypes"
|
|
6047
|
+
"doctypes",
|
|
6048
|
+
"wiki",
|
|
6049
|
+
"wikipedia"
|
|
4111
6050
|
]);
|
|
4112
6051
|
async function main() {
|
|
4113
6052
|
const args = parseArgs(process.argv);
|