isorite 0.12.0 → 0.13.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/README.md +6 -5
- package/dist/main.js +5688 -8903
- package/docs/setup-for-agents.md +44 -102
- package/docs/setup.md +23 -87
- package/docs/terminal.md +54 -55
- package/guide/AGENTS.md +4 -0
- package/guide/atlas/find-what-it-rests-on.md +9 -0
- package/guide/atlas/the-atlas.md +21 -0
- package/guide/atlas/the-switches/satellites.svg +17 -0
- package/guide/atlas/the-switches.md +27 -0
- package/guide/iso/agents-md.md +13 -0
- package/guide/iso/artifacts/the-guide.csv +8 -0
- package/guide/iso/artifacts/the-guide.html +35 -0
- package/guide/iso/artifacts.md +15 -0
- package/guide/iso/ask-iso.md +7 -0
- package/guide/iso/give-it-notes.md +9 -0
- package/guide/iso/in-hand.md +18 -0
- package/guide/iso/iso-writes-marked.md +15 -0
- package/guide/iso/iso.md +17 -0
- package/guide/iso/let-it-write.md +11 -0
- package/guide/iso/write-agents-md.md +11 -0
- package/guide/notes/a-note-is-markdown.md +17 -0
- package/guide/notes/a-notes-details.md +23 -0
- package/guide/notes/diagrams-and-code.md +43 -0
- package/guide/notes/every-change-is-kept.md +14 -0
- package/guide/notes/folders.md +11 -0
- package/guide/notes/go-back-a-version.md +9 -0
- package/guide/notes/link-your-notes.md +9 -0
- package/guide/notes/links-and-their-kinds.md +23 -0
- package/guide/notes/make-move-delete.md +9 -0
- package/guide/notes/write-in-your-notes.md +9 -0
- package/guide/out-of-the-base/web-and-code-links.md +13 -0
- package/guide/questions-and-problems/ask-a-question.md +9 -0
- package/guide/questions-and-problems/how-iso-reads-you.md +9 -0
- package/guide/questions-and-problems/problems.md +15 -0
- package/guide/questions-and-problems/questions.md +27 -0
- package/guide/questions-and-problems/resolve-a-question.md +9 -0
- package/guide/questions-and-problems/why-markdown-links.md +7 -0
- package/guide/solve-this-one.md +11 -0
- package/guide/start-here.md +39 -0
- package/guide/terminal/let-the-agent-remember.md +9 -0
- package/guide/terminal/memory.md +13 -0
- package/guide/terminal/your-own-agent.md +24 -0
- package/guide/your-notes.md +6 -0
- package/guide/your-own-base/keeping-a-base-well.md +16 -0
- package/guide/your-own-base/starting-a-base.md +21 -0
- package/package.json +4 -7
- package/web/_app/immutable/assets/0.BEjn1Bfl.css +1 -0
- package/web/_app/immutable/assets/4.fOHsAKBF.css +1 -0
- package/web/_app/immutable/assets/drawn.D6lJzDcc.css +1 -0
- package/web/_app/immutable/chunks/{BK6pLgdi.js → 3fCuxe18.js} +1 -1
- package/web/_app/immutable/chunks/{D5XlJBy8.js → B-DzK7Gb.js} +1 -1
- package/web/_app/immutable/chunks/{BuUzVkG2.js → B35sQTcK.js} +1 -1
- package/web/_app/immutable/chunks/{D4YEq54N.js → B6uHgLdT.js} +1 -1
- package/web/_app/immutable/chunks/{Dh1lBxfx.js → BA78_vo5.js} +1 -1
- package/web/_app/immutable/chunks/{tUSDgbpy.js → BCGaqnao.js} +1 -1
- package/web/_app/immutable/chunks/BCyLp7pS.js +1 -0
- package/web/_app/immutable/chunks/{EMo1IeTW.js → BK31dpt6.js} +1 -1
- package/web/_app/immutable/chunks/BOxykeIc.js +1 -0
- package/web/_app/immutable/chunks/{DlXrOzlh.js → BT_btfh0.js} +1 -1
- package/web/_app/immutable/chunks/{Xm0YTnxV.js → BWE_HS0c.js} +1 -1
- package/web/_app/immutable/chunks/{BLmF1B2i.js → BXBhaEho.js} +1 -1
- package/web/_app/immutable/chunks/{BAclEN0O.js → Bb_N98K9.js} +1 -1
- package/web/_app/immutable/chunks/{gPtCGHiy.js → Bd-WxALw.js} +1 -1
- package/web/_app/immutable/chunks/{BA8Hpxhg.js → BhEqJU15.js} +1 -1
- package/web/_app/immutable/chunks/{DkxkvOjx.js → BiATTH-R.js} +1 -1
- package/web/_app/immutable/chunks/{D5vocsTl.js → Bj-5n5Pb.js} +1 -1
- package/web/_app/immutable/chunks/{DRX-TQzj.js → BqipEJXw.js} +1 -1
- package/web/_app/immutable/chunks/{CiMp12cC.js → Bt-fHApV.js} +1 -1
- package/web/_app/immutable/chunks/{DXTK-eyo.js → BvPxdlBc.js} +1 -1
- package/web/_app/immutable/chunks/{DfCZPwp1.js → C2npZzkn.js} +1 -1
- package/web/_app/immutable/chunks/{DF89zyH-.js → C5Z-Y7Lu.js} +1 -1
- package/web/_app/immutable/chunks/{CD6xPodA.js → C8h1oeAZ.js} +1 -1
- package/web/_app/immutable/chunks/{DDCg82xL.js → C8nK3Jvr.js} +1 -1
- package/web/_app/immutable/chunks/{CFEI5lV9.js → CAlIuau0.js} +1 -1
- package/web/_app/immutable/chunks/{9z49435t.js → CFf_dAzX.js} +1 -1
- package/web/_app/immutable/chunks/{D3-rqfsM.js → CMIt1Ma8.js} +1 -1
- package/web/_app/immutable/chunks/{-2Meesge.js → CMikI1NP.js} +3 -3
- package/web/_app/immutable/chunks/{Czz9qOh8.js → CT4_O9X_.js} +1 -1
- package/web/_app/immutable/chunks/{DX4bJJO0.js → CTDYUZPz.js} +1 -1
- package/web/_app/immutable/chunks/{By4S_gy5.js → CdY1G_wk.js} +1 -1
- package/web/_app/immutable/chunks/Cd_SqIWq.js +523 -0
- package/web/_app/immutable/chunks/{DBC2xjOo.js → CdmC_l9F.js} +1 -1
- package/web/_app/immutable/chunks/{DbNETcPs.js → Cf-5I5_6.js} +1 -1
- package/web/_app/immutable/chunks/Cl5gQm2e.js +1 -0
- package/web/_app/immutable/chunks/{BWVMfAMh.js → CryuQgNh.js} +1 -1
- package/web/_app/immutable/chunks/{CT_Ry4q8.js → Ct_EK1xu.js} +1 -1
- package/web/_app/immutable/chunks/{BymwHy7o.js → CtkUy_15.js} +1 -1
- package/web/_app/immutable/chunks/{Dpa4Syyy.js → CzYPMYd4.js} +1 -1
- package/web/_app/immutable/chunks/{RlApGeew.js → D1LK3xRu.js} +1 -1
- package/web/_app/immutable/chunks/{DXK7PB4g.js → D6sVhEz3.js} +1 -1
- package/web/_app/immutable/chunks/{jTXVas70.js → D9JwUycj.js} +1 -1
- package/web/_app/immutable/chunks/{rghE0CVz.js → DCZYeMlH.js} +1 -1
- package/web/_app/immutable/chunks/{RFxekmX-.js → DDAG55ce.js} +1 -1
- package/web/_app/immutable/chunks/{CyFLqlvP.js → DFuPQaNa.js} +1 -1
- package/web/_app/immutable/chunks/{BXAOBcPb.js → DHJ8FcFD.js} +1 -1
- package/web/_app/immutable/chunks/{Bt7VOT5H.js → DKqMzyyE.js} +1 -1
- package/web/_app/immutable/chunks/{Bq2GBAJ0.js → DRcTlHZP.js} +1 -1
- package/web/_app/immutable/chunks/{C_amgxV1.js → DjUnonHY.js} +1 -1
- package/web/_app/immutable/chunks/{whhiiiga.js → Djr0h_v1.js} +1 -1
- package/web/_app/immutable/chunks/{vfDvz3qP.js → DrVtdHUW.js} +1 -1
- package/web/_app/immutable/chunks/{Uo6irDQW.js → O98JkczU.js} +74 -74
- package/web/_app/immutable/chunks/{oWM_kgYq.js → PsfIQCJ1.js} +1 -1
- package/web/_app/immutable/chunks/{Cl5zRP_n.js → TanDZqdf.js} +1 -1
- package/web/_app/immutable/chunks/{D95RKsvN.js → UDPiRmKc.js} +1 -1
- package/web/_app/immutable/chunks/WLe0MT1c.js +1 -0
- package/web/_app/immutable/chunks/{BTclWEfx.js → ZwM5k8r0.js} +1 -1
- package/web/_app/immutable/chunks/e2LGvGEb.js +3 -0
- package/web/_app/immutable/chunks/ej_0XxR7.js +1 -0
- package/web/_app/immutable/chunks/{CjtKPNlv.js → kG4HctVa.js} +1 -1
- package/web/_app/immutable/chunks/{CQd9D8Nc.js → l1LsK1sB.js} +1 -1
- package/web/_app/immutable/chunks/{D_jbSWJl.js → s_hW4oA1.js} +1 -1
- package/web/_app/immutable/entry/app.DM_AWBgH.js +2 -0
- package/web/_app/immutable/entry/start.BLhIKDcR.js +1 -0
- package/web/_app/immutable/nodes/0.xSbkmYPn.js +2 -0
- package/web/_app/immutable/nodes/1.BD2wJdoH.js +1 -0
- package/web/_app/immutable/nodes/2.shPqwtS_.js +1 -0
- package/web/_app/immutable/nodes/3.e_5kWaf_.js +1 -0
- package/web/_app/immutable/nodes/4.BEMEU6Jz.js +15 -0
- package/web/_app/immutable/nodes/5.DxaO-e4O.js +1 -0
- package/web/_app/immutable/nodes/6.C9ia8cDm.js +1 -0
- package/web/_app/version.json +1 -1
- package/web/index.html +16 -19
- package/dist/core.js +0 -2464
- package/plugin/assets/mark-lapis.png +0 -0
- package/plugin/assets/mark-quartz.png +0 -0
- package/plugin/assets/wordmark-lapis.png +0 -0
- package/plugin/assets/wordmark-quartz.png +0 -0
- package/plugin/isorite.ts +0 -65
- package/plugin/logo.ts +0 -99
- package/plugin/notepage.tsx +0 -165
- package/plugin/notes.ts +0 -10
- package/plugin/noteview.ts +0 -60
- package/plugin/oneatlas.ts +0 -83
- package/plugin/palette.ts +0 -21
- package/plugin/play.ts +0 -104
- package/plugin/server.ts +0 -76
- package/plugin/sources.ts +0 -18
- package/plugin/tui.tsx +0 -556
- package/plugin/wordmark.ts +0 -180
- package/web/_app/immutable/assets/0.BXPxx6Q0.css +0 -1
- package/web/_app/immutable/assets/4.CUdclsPi.css +0 -1
- package/web/_app/immutable/assets/5.rBvyRuST.css +0 -1
- package/web/_app/immutable/assets/Drawing.Q7J9MyIo.css +0 -1
- package/web/_app/immutable/assets/NoteEditor.S3q5lmKq.css +0 -1
- package/web/_app/immutable/assets/Timeline.LsRXYIBv.css +0 -1
- package/web/_app/immutable/chunks/4kgFUGOH.js +0 -1
- package/web/_app/immutable/chunks/8SS1q0nk.js +0 -7
- package/web/_app/immutable/chunks/Bk_JinGf.js +0 -1
- package/web/_app/immutable/chunks/BqrMZ9MX.js +0 -1
- package/web/_app/immutable/chunks/DThNJvmp.js +0 -1
- package/web/_app/immutable/chunks/DbeMVOH4.js +0 -1
- package/web/_app/immutable/chunks/DsQj0LyB.js +0 -523
- package/web/_app/immutable/chunks/DshHzxgH.js +0 -1
- package/web/_app/immutable/chunks/Iqr2CudW.js +0 -3
- package/web/_app/immutable/entry/app.CvK8zypr.js +0 -2
- package/web/_app/immutable/entry/start.Dcgm6sQd.js +0 -1
- package/web/_app/immutable/nodes/0.0TdRBEoR.js +0 -26
- package/web/_app/immutable/nodes/1.Dchqgt4j.js +0 -1
- package/web/_app/immutable/nodes/2.C0CMUdSu.js +0 -1
- package/web/_app/immutable/nodes/3.MjtWDMWd.js +0 -1
- package/web/_app/immutable/nodes/4.hhS0SQta.js +0 -2
- package/web/_app/immutable/nodes/5.C33_cw2J.js +0 -9
- package/web/_app/immutable/nodes/6.owNfwwRl.js +0 -1
- package/web/_app/immutable/nodes/7.BpxHVHoY.js +0 -1
- /package/web/_app/immutable/assets/{7.C00vyVPo.css → 6.C00vyVPo.css} +0 -0
package/dist/core.js
DELETED
|
@@ -1,2464 +0,0 @@
|
|
|
1
|
-
import { Document, Scalar, isMap, isScalar, isSeq, parseDocument, stringify } from "yaml";
|
|
2
|
-
import MarkdownIt from "markdown-it";
|
|
3
|
-
import { parse, stringify as stringify$1 } from "smol-toml";
|
|
4
|
-
//#region packages/core/src/text.ts
|
|
5
|
-
const WS = "\\t\\n\\x0b\\x0c\\r\\x1c-\\x1f \\x85\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000";
|
|
6
|
-
const LEAD = new RegExp(`^[${WS}]+`, "u");
|
|
7
|
-
const TRAIL = new RegExp(`[${WS}]+$`, "u");
|
|
8
|
-
const strip = (s) => s.replace(LEAD, "").replace(TRAIL, "");
|
|
9
|
-
const rstrip = (s) => s.replace(TRAIL, "");
|
|
10
|
-
/** Compare by code point, as Python sorts strings. */
|
|
11
|
-
function cmp(a, b) {
|
|
12
|
-
if (a === b) return 0;
|
|
13
|
-
const x = [...a], y = [...b];
|
|
14
|
-
for (let i = 0; i < Math.min(x.length, y.length); i++) {
|
|
15
|
-
const d = x[i].codePointAt(0) - y[i].codePointAt(0);
|
|
16
|
-
if (d) return d;
|
|
17
|
-
}
|
|
18
|
-
return x.length - y.length;
|
|
19
|
-
}
|
|
20
|
-
/** Compare arrays of strings (or numbers) element by element, as Python compares tuples. */
|
|
21
|
-
function cmpTuple(a, b) {
|
|
22
|
-
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
23
|
-
const x = a[i], y = b[i];
|
|
24
|
-
const d = typeof x === "number" && typeof y === "number" ? x - y : cmp(String(x), String(y));
|
|
25
|
-
if (d) return d;
|
|
26
|
-
}
|
|
27
|
-
return a.length - b.length;
|
|
28
|
-
}
|
|
29
|
-
/** A frontmatter value as text: true/false, whole-number floats without ".0",
|
|
30
|
-
* nothing for null. */
|
|
31
|
-
function text(value) {
|
|
32
|
-
if (value === null || value === void 0) return "";
|
|
33
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
34
|
-
return String(value);
|
|
35
|
-
}
|
|
36
|
-
const DATETIME = /^(\d{4})-(\d{2})-(\d{2})(?:[Tt ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(?:([Zz])|([+-])(\d{2}):?(\d{2}))?)?$/;
|
|
37
|
-
/** Milliseconds since the epoch for a frontmatter time, or null when it is not one. */
|
|
38
|
-
function toTime(value) {
|
|
39
|
-
if (value instanceof Date) return value.getTime();
|
|
40
|
-
if (typeof value !== "string") return null;
|
|
41
|
-
const m = DATETIME.exec(strip(value));
|
|
42
|
-
if (!m) return null;
|
|
43
|
-
const [, y, mo, d, hh = "0", mm = "0", ss = "0", frac = "0", , sign, oh = "0", om = "0"] = m;
|
|
44
|
-
const n = (s) => Number(s);
|
|
45
|
-
const [Y, M, D, H, Mi, S] = [
|
|
46
|
-
n(y),
|
|
47
|
-
n(mo),
|
|
48
|
-
n(d),
|
|
49
|
-
n(hh),
|
|
50
|
-
n(mm),
|
|
51
|
-
n(ss)
|
|
52
|
-
];
|
|
53
|
-
const probe = new Date(Date.UTC(Y, M - 1, D, H, Mi, S));
|
|
54
|
-
if (Y < 1 || probe.getUTCFullYear() !== Y || probe.getUTCMonth() !== M - 1 || probe.getUTCDate() !== D || probe.getUTCHours() !== H || probe.getUTCMinutes() !== Mi || probe.getUTCSeconds() !== S) return null;
|
|
55
|
-
const micro = Number(frac.slice(0, 6).padEnd(6, "0"));
|
|
56
|
-
let t = probe.getTime() + Math.floor(micro / 1e3);
|
|
57
|
-
if (sign) {
|
|
58
|
-
if (n(oh) > 23 || n(om) > 59) return null;
|
|
59
|
-
const offset = (n(oh) * 60 + n(om)) * 6e4;
|
|
60
|
-
t += sign === "+" ? -offset : offset;
|
|
61
|
-
}
|
|
62
|
-
return t;
|
|
63
|
-
}
|
|
64
|
-
/** ISO 8601 in UTC to the second, as the Python core writes times. */
|
|
65
|
-
function iso(time) {
|
|
66
|
-
return (/* @__PURE__ */ new Date(Math.floor(time / 1e3) * 1e3)).toISOString().replace(".000Z", "Z");
|
|
67
|
-
}
|
|
68
|
-
/** Decode %XX escapes as UTF-8, replacing invalid sequences (Python's unquote). */
|
|
69
|
-
function unquote(s) {
|
|
70
|
-
if (!s.includes("%")) return s;
|
|
71
|
-
const bytes = [];
|
|
72
|
-
const enc = new TextEncoder();
|
|
73
|
-
for (let i = 0; i < s.length;) if (s[i] === "%" && /^[0-9a-fA-F]{2}$/.test(s.slice(i + 1, i + 3))) {
|
|
74
|
-
bytes.push(parseInt(s.slice(i + 1, i + 3), 16));
|
|
75
|
-
i += 3;
|
|
76
|
-
} else {
|
|
77
|
-
const cp = s.codePointAt(i);
|
|
78
|
-
const ch = String.fromCodePoint(cp);
|
|
79
|
-
bytes.push(...enc.encode(ch));
|
|
80
|
-
i += ch.length;
|
|
81
|
-
}
|
|
82
|
-
return new TextDecoder("utf-8").decode(new Uint8Array(bytes));
|
|
83
|
-
}
|
|
84
|
-
/** Python's posixpath.dirname. */
|
|
85
|
-
function dirname(p) {
|
|
86
|
-
let head = p.slice(0, p.lastIndexOf("/") + 1);
|
|
87
|
-
if (head && !/^\/+$/.test(head)) head = head.replace(/\/+$/, "");
|
|
88
|
-
return head;
|
|
89
|
-
}
|
|
90
|
-
function basename(p) {
|
|
91
|
-
return p.slice(p.lastIndexOf("/") + 1);
|
|
92
|
-
}
|
|
93
|
-
function join(a, b) {
|
|
94
|
-
if (b.startsWith("/") || !a) return b;
|
|
95
|
-
return a.endsWith("/") ? a + b : `${a}/${b}`;
|
|
96
|
-
}
|
|
97
|
-
/** Python's posixpath.normpath. */
|
|
98
|
-
function normpath(p) {
|
|
99
|
-
if (!p) return ".";
|
|
100
|
-
const initial = p.startsWith("/") ? p.startsWith("//") && !p.startsWith("///") ? 2 : 1 : 0;
|
|
101
|
-
const out = [];
|
|
102
|
-
for (const part of p.split("/")) {
|
|
103
|
-
if (!part || part === ".") continue;
|
|
104
|
-
if (part !== ".." || !initial && !out.length || out[out.length - 1] === "..") out.push(part);
|
|
105
|
-
else if (out.length) out.pop();
|
|
106
|
-
}
|
|
107
|
-
return "/".repeat(initial) + out.join("/") || ".";
|
|
108
|
-
}
|
|
109
|
-
/** SHA-256 as hex, synchronous and dependency-free, so the core runs anywhere: of text (as UTF-8), or of bytes. */
|
|
110
|
-
function sha256(message) {
|
|
111
|
-
const K = new Uint32Array([
|
|
112
|
-
1116352408,
|
|
113
|
-
1899447441,
|
|
114
|
-
3049323471,
|
|
115
|
-
3921009573,
|
|
116
|
-
961987163,
|
|
117
|
-
1508970993,
|
|
118
|
-
2453635748,
|
|
119
|
-
2870763221,
|
|
120
|
-
3624381080,
|
|
121
|
-
310598401,
|
|
122
|
-
607225278,
|
|
123
|
-
1426881987,
|
|
124
|
-
1925078388,
|
|
125
|
-
2162078206,
|
|
126
|
-
2614888103,
|
|
127
|
-
3248222580,
|
|
128
|
-
3835390401,
|
|
129
|
-
4022224774,
|
|
130
|
-
264347078,
|
|
131
|
-
604807628,
|
|
132
|
-
770255983,
|
|
133
|
-
1249150122,
|
|
134
|
-
1555081692,
|
|
135
|
-
1996064986,
|
|
136
|
-
2554220882,
|
|
137
|
-
2821834349,
|
|
138
|
-
2952996808,
|
|
139
|
-
3210313671,
|
|
140
|
-
3336571891,
|
|
141
|
-
3584528711,
|
|
142
|
-
113926993,
|
|
143
|
-
338241895,
|
|
144
|
-
666307205,
|
|
145
|
-
773529912,
|
|
146
|
-
1294757372,
|
|
147
|
-
1396182291,
|
|
148
|
-
1695183700,
|
|
149
|
-
1986661051,
|
|
150
|
-
2177026350,
|
|
151
|
-
2456956037,
|
|
152
|
-
2730485921,
|
|
153
|
-
2820302411,
|
|
154
|
-
3259730800,
|
|
155
|
-
3345764771,
|
|
156
|
-
3516065817,
|
|
157
|
-
3600352804,
|
|
158
|
-
4094571909,
|
|
159
|
-
275423344,
|
|
160
|
-
430227734,
|
|
161
|
-
506948616,
|
|
162
|
-
659060556,
|
|
163
|
-
883997877,
|
|
164
|
-
958139571,
|
|
165
|
-
1322822218,
|
|
166
|
-
1537002063,
|
|
167
|
-
1747873779,
|
|
168
|
-
1955562222,
|
|
169
|
-
2024104815,
|
|
170
|
-
2227730452,
|
|
171
|
-
2361852424,
|
|
172
|
-
2428436474,
|
|
173
|
-
2756734187,
|
|
174
|
-
3204031479,
|
|
175
|
-
3329325298
|
|
176
|
-
]);
|
|
177
|
-
const data = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
178
|
-
const len = data.length;
|
|
179
|
-
const padded = new Uint8Array(len + 9 + 63 >> 6 << 6);
|
|
180
|
-
padded.set(data);
|
|
181
|
-
padded[len] = 128;
|
|
182
|
-
const view = new DataView(padded.buffer);
|
|
183
|
-
view.setUint32(padded.length - 8, Math.floor(len / 536870912));
|
|
184
|
-
view.setUint32(padded.length - 4, len << 3 >>> 0);
|
|
185
|
-
const h = new Uint32Array([
|
|
186
|
-
1779033703,
|
|
187
|
-
3144134277,
|
|
188
|
-
1013904242,
|
|
189
|
-
2773480762,
|
|
190
|
-
1359893119,
|
|
191
|
-
2600822924,
|
|
192
|
-
528734635,
|
|
193
|
-
1541459225
|
|
194
|
-
]);
|
|
195
|
-
const w = /* @__PURE__ */ new Uint32Array(64);
|
|
196
|
-
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
197
|
-
for (let off = 0; off < padded.length; off += 64) {
|
|
198
|
-
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4);
|
|
199
|
-
for (let i = 16; i < 64; i++) {
|
|
200
|
-
const a = w[i - 15], b = w[i - 2];
|
|
201
|
-
const s0 = rotr(a, 7) ^ rotr(a, 18) ^ a >>> 3;
|
|
202
|
-
const s1 = rotr(b, 17) ^ rotr(b, 19) ^ b >>> 10;
|
|
203
|
-
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
204
|
-
}
|
|
205
|
-
let [a, b, c, d, e, f, g, hh] = h;
|
|
206
|
-
for (let i = 0; i < 64; i++) {
|
|
207
|
-
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
208
|
-
const ch = e & f ^ ~e & g;
|
|
209
|
-
const t1 = hh + S1 + ch + K[i] + w[i] >>> 0;
|
|
210
|
-
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
211
|
-
hh = g;
|
|
212
|
-
g = f;
|
|
213
|
-
f = e;
|
|
214
|
-
e = d + t1 >>> 0;
|
|
215
|
-
d = c;
|
|
216
|
-
c = b;
|
|
217
|
-
b = a;
|
|
218
|
-
a = t1 + t2 >>> 0;
|
|
219
|
-
}
|
|
220
|
-
h[0] += a;
|
|
221
|
-
h[1] += b;
|
|
222
|
-
h[2] += c;
|
|
223
|
-
h[3] += d;
|
|
224
|
-
h[4] += e;
|
|
225
|
-
h[5] += f;
|
|
226
|
-
h[6] += g;
|
|
227
|
-
h[7] += hh;
|
|
228
|
-
}
|
|
229
|
-
return [...h].map((x) => x.toString(16).padStart(8, "0")).join("");
|
|
230
|
-
}
|
|
231
|
-
/** The version of a note that learner-record events refer to. */
|
|
232
|
-
function contentHash(body) {
|
|
233
|
-
return sha256(strip(body)).slice(0, 12);
|
|
234
|
-
}
|
|
235
|
-
/** Python's repr() of a float. */
|
|
236
|
-
function floatRepr(x) {
|
|
237
|
-
if (!Number.isFinite(x)) return Number.isNaN(x) ? "NaN" : x > 0 ? "Infinity" : "-Infinity";
|
|
238
|
-
const a = Math.abs(x);
|
|
239
|
-
if (x !== 0 && (a >= 0x2386f26fc10000 || a < 1e-4)) return x.toExponential().replace(/e([+-])(\d)$/, "e$10$2");
|
|
240
|
-
return Number.isInteger(x) ? `${x.toFixed(0)}.0` : String(x);
|
|
241
|
-
}
|
|
242
|
-
/** Python's repr(): what Python prints for a value inside a list or dict. */
|
|
243
|
-
function pyRepr(v) {
|
|
244
|
-
if (v === null || v === void 0) return "None";
|
|
245
|
-
if (typeof v === "boolean") return v ? "True" : "False";
|
|
246
|
-
if (typeof v === "number") return Number.isInteger(v) ? String(v) : floatRepr(v);
|
|
247
|
-
if (typeof v === "string") {
|
|
248
|
-
const quote = v.includes("'") && !v.includes("\"") ? "\"" : "'";
|
|
249
|
-
return quote + [...v].map((c) => {
|
|
250
|
-
if (c === "\\" || c === quote) return "\\" + c;
|
|
251
|
-
if (c === "\n") return "\\n";
|
|
252
|
-
if (c === "\r") return "\\r";
|
|
253
|
-
if (c === " ") return "\\t";
|
|
254
|
-
const cp = c.codePointAt(0);
|
|
255
|
-
return cp < 32 || cp === 127 ? "\\x" + cp.toString(16).padStart(2, "0") : c;
|
|
256
|
-
}).join("") + quote;
|
|
257
|
-
}
|
|
258
|
-
if (Array.isArray(v)) return "[" + v.map(pyRepr).join(", ") + "]";
|
|
259
|
-
if (typeof v === "object") return "{" + Object.entries(v).map(([k, x]) => `${pyRepr(k)}: ${pyRepr(x)}`).join(", ") + "}";
|
|
260
|
-
return String(v);
|
|
261
|
-
}
|
|
262
|
-
//#endregion
|
|
263
|
-
//#region packages/core/src/frontmatter.ts
|
|
264
|
-
var FrontmatterError = class extends Error {};
|
|
265
|
-
const BOM$1 = "";
|
|
266
|
-
/** The frontmatter's YAML text (with its last line break, as in the file) and
|
|
267
|
-
* the body; the YAML is null when the note has none. */
|
|
268
|
-
function frontmatterText(source) {
|
|
269
|
-
const text = source.startsWith(BOM$1) ? source.slice(1) : source;
|
|
270
|
-
if (!text.startsWith("---")) return [null, text];
|
|
271
|
-
const lines = text.split("\n");
|
|
272
|
-
if (strip(lines[0]) !== "---") return [null, text];
|
|
273
|
-
for (let i = 1; i < lines.length; i++) if (rstrip(lines[i]) === "---") return [lines.slice(1, i).join("\n") + "\n", lines.slice(i + 1).join("\n")];
|
|
274
|
-
throw new FrontmatterError("frontmatter block is not closed with '---'");
|
|
275
|
-
}
|
|
276
|
-
/** Parse frontmatter YAML as the core reads it. */
|
|
277
|
-
function parseYaml(raw) {
|
|
278
|
-
return parseDocument(raw.replace(/\r\n?/g, "\n"), {
|
|
279
|
-
version: "1.2",
|
|
280
|
-
schema: "core",
|
|
281
|
-
uniqueKeys: false
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
/** `[frontmatter, body]`; frontmatter is null when the note has none. */
|
|
285
|
-
function splitFrontmatter(source) {
|
|
286
|
-
const [raw, body] = frontmatterText(source);
|
|
287
|
-
if (raw === null) return [null, body];
|
|
288
|
-
let meta = {};
|
|
289
|
-
if (strip(raw)) {
|
|
290
|
-
const doc = parseYaml(raw);
|
|
291
|
-
const problem = doc.errors[0] ?? doc.warnings.find((w) => w.code === "TAG_RESOLVE_FAILED");
|
|
292
|
-
if (problem) throw new FrontmatterError(`unparseable YAML frontmatter: ${problem.message}`);
|
|
293
|
-
meta = doc.toJS({ maxAliasCount: 1e3 });
|
|
294
|
-
}
|
|
295
|
-
if (meta === null || meta === void 0) meta = {};
|
|
296
|
-
if (typeof meta !== "object" || Array.isArray(meta)) throw new FrontmatterError("frontmatter is not a mapping");
|
|
297
|
-
return [meta, body.replace(/^\n+/, "")];
|
|
298
|
-
}
|
|
299
|
-
/** Serialise frontmatter, keeping key order. */
|
|
300
|
-
function dumpFrontmatter(meta) {
|
|
301
|
-
return stringify(meta, {
|
|
302
|
-
version: "1.2",
|
|
303
|
-
lineWidth: 100
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
//#endregion
|
|
307
|
-
//#region packages/core/src/markdown.ts
|
|
308
|
-
const md = new MarkdownIt("commonmark");
|
|
309
|
-
const cache = /* @__PURE__ */ new Map();
|
|
310
|
-
function parse$1(body) {
|
|
311
|
-
let hit = cache.get(body);
|
|
312
|
-
if (!hit) {
|
|
313
|
-
const env = {};
|
|
314
|
-
hit = {
|
|
315
|
-
tokens: md.parse(body, env),
|
|
316
|
-
defs: Object.values(env.references ?? {}).map((d) => [d.href ?? "", d.title ?? null])
|
|
317
|
-
};
|
|
318
|
-
if (cache.size > 8192) cache.clear();
|
|
319
|
-
cache.set(body, hit);
|
|
320
|
-
}
|
|
321
|
-
return hit;
|
|
322
|
-
}
|
|
323
|
-
const RATINGS = {
|
|
324
|
-
requires: 3,
|
|
325
|
-
uses: 2,
|
|
326
|
-
"see also": 1
|
|
327
|
-
};
|
|
328
|
-
/** The rating a link title names, or null (unrated, or an ordinary title). */
|
|
329
|
-
function rating(title) {
|
|
330
|
-
let key = strip((title ?? "").toLowerCase().replaceAll("-", " ").replaceAll("_", " ")).split(/\s+/).filter(Boolean).join(" ");
|
|
331
|
-
if (key === "seealso") key = "see also";
|
|
332
|
-
return key in RATINGS ? key : null;
|
|
333
|
-
}
|
|
334
|
-
/** Link targets with their ratings: every link in order of appearance, then
|
|
335
|
-
* each reference definition no link used. */
|
|
336
|
-
function linkRefs(body) {
|
|
337
|
-
const { tokens, defs } = parse$1(body);
|
|
338
|
-
const refs = [];
|
|
339
|
-
for (const t of tokens) {
|
|
340
|
-
if (t.type !== "inline" || !t.children) continue;
|
|
341
|
-
for (const c of t.children) if (c.type === "link_open") refs.push([c.attrGet("href") ?? "", rating(c.attrGet("title"))]);
|
|
342
|
-
}
|
|
343
|
-
const used = new Set(refs.map(([href]) => href));
|
|
344
|
-
for (const [href, title] of defs) if (!used.has(href)) refs.push([href, rating(title)]);
|
|
345
|
-
return refs;
|
|
346
|
-
}
|
|
347
|
-
/** Every link in a body, in order, with its text as written (marks dropped). */
|
|
348
|
-
function linkTexts(body) {
|
|
349
|
-
const { tokens } = parse$1(body);
|
|
350
|
-
const out = [];
|
|
351
|
-
for (const t of tokens) {
|
|
352
|
-
if (t.type !== "inline" || !t.children) continue;
|
|
353
|
-
let open = null;
|
|
354
|
-
for (const c of t.children) if (c.type === "link_open") open = {
|
|
355
|
-
href: c.attrGet("href") ?? "",
|
|
356
|
-
text: ""
|
|
357
|
-
};
|
|
358
|
-
else if (c.type === "link_close" && open) {
|
|
359
|
-
out.push({
|
|
360
|
-
href: open.href,
|
|
361
|
-
text: open.text.trim()
|
|
362
|
-
});
|
|
363
|
-
open = null;
|
|
364
|
-
} else if (open && (c.type === "text" || c.type === "code_inline")) open.text += c.content;
|
|
365
|
-
}
|
|
366
|
-
return out;
|
|
367
|
-
}
|
|
368
|
-
/** Every link and image in a body, in order: where it points, its title, and whether it is an image
|
|
369
|
-
* (``, which for an artifact or a picture means "shown here", an embed), with an image's alt text. */
|
|
370
|
-
function citeRefs(body) {
|
|
371
|
-
const { tokens } = parse$1(body);
|
|
372
|
-
const out = [];
|
|
373
|
-
for (const t of tokens) {
|
|
374
|
-
if (t.type !== "inline" || !t.children) continue;
|
|
375
|
-
for (const c of t.children) if (c.type === "link_open") out.push({
|
|
376
|
-
href: c.attrGet("href") ?? "",
|
|
377
|
-
title: c.attrGet("title"),
|
|
378
|
-
embed: false
|
|
379
|
-
});
|
|
380
|
-
else if (c.type === "image") out.push({
|
|
381
|
-
href: c.attrGet("src") ?? "",
|
|
382
|
-
title: c.attrGet("title"),
|
|
383
|
-
embed: true,
|
|
384
|
-
alt: c.content
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
return out;
|
|
388
|
-
}
|
|
389
|
-
const NOT_SLUG = /[^\p{L}\p{N}_\s-]/gu;
|
|
390
|
-
const SPACES = /[\s_]+/gu;
|
|
391
|
-
function slugify(text) {
|
|
392
|
-
return strip(text.toLowerCase().replace(NOT_SLUG, "")).replace(SPACES, "-");
|
|
393
|
-
}
|
|
394
|
-
function headings(body) {
|
|
395
|
-
const { tokens } = parse$1(body);
|
|
396
|
-
const out = [];
|
|
397
|
-
tokens.forEach((t, i) => {
|
|
398
|
-
if (t.type !== "heading_open") return;
|
|
399
|
-
const text = strip(tokens[i + 1]?.content ?? "");
|
|
400
|
-
out.push({
|
|
401
|
-
level: Number(t.tag.slice(1)),
|
|
402
|
-
text,
|
|
403
|
-
slug: slugify(text),
|
|
404
|
-
line: t.map?.[0] ?? 0
|
|
405
|
-
});
|
|
406
|
-
});
|
|
407
|
-
return out;
|
|
408
|
-
}
|
|
409
|
-
/** The text under `heading` (matched by text or slug) up to the next heading of
|
|
410
|
-
* the same or higher level, including the heading line itself. */
|
|
411
|
-
function section(body, heading) {
|
|
412
|
-
const wanted = strip(strip(heading).replace(/^#+/, "")).toLowerCase();
|
|
413
|
-
const wantedSlug = slugify(wanted);
|
|
414
|
-
const hs = headings(body);
|
|
415
|
-
const lines = body.split("\n");
|
|
416
|
-
for (const [idx, h] of hs.entries()) {
|
|
417
|
-
if (h.text.toLowerCase() !== wanted && h.slug !== wantedSlug) continue;
|
|
418
|
-
let end = lines.length;
|
|
419
|
-
for (const later of hs.slice(idx + 1)) if (later.level <= h.level) {
|
|
420
|
-
end = later.line;
|
|
421
|
-
break;
|
|
422
|
-
}
|
|
423
|
-
return strip(lines.slice(h.line, end).join("\n"));
|
|
424
|
-
}
|
|
425
|
-
return null;
|
|
426
|
-
}
|
|
427
|
-
//#endregion
|
|
428
|
-
//#region packages/core/src/picture.ts
|
|
429
|
-
const TYPES = {
|
|
430
|
-
png: "image/png",
|
|
431
|
-
jpg: "image/jpeg",
|
|
432
|
-
jpeg: "image/jpeg",
|
|
433
|
-
gif: "image/gif",
|
|
434
|
-
svg: "image/svg+xml",
|
|
435
|
-
webp: "image/webp",
|
|
436
|
-
avif: "image/avif"
|
|
437
|
-
};
|
|
438
|
-
/** A picture's media type, by its extension, or null when the path isn't a picture's (M22). */
|
|
439
|
-
const pictureType = (path) => {
|
|
440
|
-
const dot = path.lastIndexOf(".");
|
|
441
|
-
return dot < 0 || path.lastIndexOf("/") > dot ? null : TYPES[path.slice(dot + 1).toLowerCase()] ?? null;
|
|
442
|
-
};
|
|
443
|
-
const OTHER = {
|
|
444
|
-
html: "text/html",
|
|
445
|
-
htm: "text/html",
|
|
446
|
-
csv: "text/csv",
|
|
447
|
-
tsv: "text/tab-separated-values",
|
|
448
|
-
json: "application/json",
|
|
449
|
-
jsonl: "application/jsonl",
|
|
450
|
-
ndjson: "application/x-ndjson",
|
|
451
|
-
txt: "text/plain",
|
|
452
|
-
tex: "text/plain",
|
|
453
|
-
pdf: "application/pdf",
|
|
454
|
-
glb: "model/gltf-binary",
|
|
455
|
-
gltf: "model/gltf+json",
|
|
456
|
-
stl: "model/stl",
|
|
457
|
-
obj: "model/obj",
|
|
458
|
-
zip: "application/zip",
|
|
459
|
-
parquet: "application/vnd.apache.parquet",
|
|
460
|
-
arrow: "application/vnd.apache.arrow.file",
|
|
461
|
-
npy: "application/octet-stream",
|
|
462
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
463
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
464
|
-
epub: "application/epub+zip",
|
|
465
|
-
odt: "application/vnd.oasis.opendocument.text"
|
|
466
|
-
};
|
|
467
|
-
/** Any file's media type in the notes (M23), by its extension: a picture's, one listed, or octet-stream. Null for a
|
|
468
|
-
* note (Markdown is read as a note, never as a file) and for a path with no extension. */
|
|
469
|
-
const fileType = (path) => {
|
|
470
|
-
const dot = path.lastIndexOf(".");
|
|
471
|
-
if (dot < 0 || path.lastIndexOf("/") > dot) return null;
|
|
472
|
-
const ext = path.slice(dot + 1).toLowerCase();
|
|
473
|
-
return ext === "md" ? null : pictureType(path) ?? OTHER[ext] ?? "application/octet-stream";
|
|
474
|
-
};
|
|
475
|
-
/** Width and height in pixels, or null. */
|
|
476
|
-
function pictureSize(bytes, type) {
|
|
477
|
-
const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength), n = bytes.length;
|
|
478
|
-
const at = (i, s) => [...s].every((c, k) => bytes[i + k] === c.charCodeAt(0));
|
|
479
|
-
const ok = (w, h) => w > 0 && h > 0 ? {
|
|
480
|
-
width: w,
|
|
481
|
-
height: h
|
|
482
|
-
} : null;
|
|
483
|
-
try {
|
|
484
|
-
if (type === "image/png" && n >= 24 && at(12, "IHDR")) return ok(v.getUint32(16), v.getUint32(20));
|
|
485
|
-
if (type === "image/gif" && n >= 10 && at(0, "GIF")) return ok(v.getUint16(6, true), v.getUint16(8, true));
|
|
486
|
-
if (type === "image/jpeg") {
|
|
487
|
-
for (let i = 2; i + 9 < n;) {
|
|
488
|
-
if (bytes[i] !== 255) return null;
|
|
489
|
-
const m = bytes[i + 1];
|
|
490
|
-
if (m >= 192 && m <= 207 && m !== 196 && m !== 200 && m !== 204) return ok(v.getUint16(i + 7), v.getUint16(i + 5));
|
|
491
|
-
i += 2 + v.getUint16(i + 2);
|
|
492
|
-
}
|
|
493
|
-
return null;
|
|
494
|
-
}
|
|
495
|
-
if (type === "image/webp" && n >= 30 && at(0, "RIFF") && at(8, "WEBP")) {
|
|
496
|
-
if (at(12, "VP8 ")) return ok(v.getUint16(26, true) & 16383, v.getUint16(28, true) & 16383);
|
|
497
|
-
if (at(12, "VP8L")) {
|
|
498
|
-
const b = v.getUint32(21, true);
|
|
499
|
-
return ok((b & 16383) + 1, (b >> 14 & 16383) + 1);
|
|
500
|
-
}
|
|
501
|
-
if (at(12, "VP8X")) return ok(1 + (bytes[24] | bytes[25] << 8 | bytes[26] << 16), 1 + (bytes[27] | bytes[28] << 8 | bytes[29] << 16));
|
|
502
|
-
return null;
|
|
503
|
-
}
|
|
504
|
-
if (type === "image/svg+xml") {
|
|
505
|
-
const head = new TextDecoder().decode(bytes.subarray(0, 4096)), tag = /<svg\b[^>]*>/i.exec(head)?.[0] ?? "";
|
|
506
|
-
const attr = (name) => new RegExp(`\\s${name}\\s*=\\s*["']\\s*([\\d.]+)\\s*(px)?\\s*["']`, "i").exec(tag);
|
|
507
|
-
const w = attr("width"), h = attr("height");
|
|
508
|
-
if (w && h) return ok(Math.round(Number(w[1])), Math.round(Number(h[1])));
|
|
509
|
-
const box = /\sviewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(tag);
|
|
510
|
-
return box ? ok(Math.round(Number(box[1])), Math.round(Number(box[2]))) : null;
|
|
511
|
-
}
|
|
512
|
-
} catch {}
|
|
513
|
-
return null;
|
|
514
|
-
}
|
|
515
|
-
//#endregion
|
|
516
|
-
//#region packages/core/src/kinds.ts
|
|
517
|
-
function kindOf(type) {
|
|
518
|
-
const t = type.trim().toLowerCase();
|
|
519
|
-
return t === "question" ? "question" : t === "problem" ? "problem" : "note";
|
|
520
|
-
}
|
|
521
|
-
/** The field naming what a kind is about, and the one saying it is done. */
|
|
522
|
-
const ABOUT = {
|
|
523
|
-
question: "about",
|
|
524
|
-
problem: "uses"
|
|
525
|
-
};
|
|
526
|
-
const DONE = {
|
|
527
|
-
question: "resolved",
|
|
528
|
-
problem: "solved"
|
|
529
|
-
};
|
|
530
|
-
/** Note ids from frontmatter: a list, or one; `/a/b.md` and `a/b` alike. */
|
|
531
|
-
function idsOf(raw) {
|
|
532
|
-
return (typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw : []).map((t) => text(t).trim().replace(/^\/+/, "").replace(/\.md$/, "")).filter(Boolean);
|
|
533
|
-
}
|
|
534
|
-
/** When it was resolved or solved, as written (a date), or null while it is open. */
|
|
535
|
-
function doneOf(meta, kind) {
|
|
536
|
-
if (kind === "note") return null;
|
|
537
|
-
const v = meta[DONE[kind]];
|
|
538
|
-
if (v === null || v === void 0 || v === false || v === "") return null;
|
|
539
|
-
return v instanceof Date ? v.toISOString().slice(0, 10) : text(v);
|
|
540
|
-
}
|
|
541
|
-
/** An embed: `[[!name]]` alone on a line, outside code. */
|
|
542
|
-
const EMBED = /^[ \t]*\[\[!([^\]\n]+)\]\][ \t]*$/;
|
|
543
|
-
/** The embeds in a note's body, in order, as written (a name, or a path). */
|
|
544
|
-
function embedRefs(body) {
|
|
545
|
-
const out = [];
|
|
546
|
-
let fenced = false;
|
|
547
|
-
for (const line of body.split("\n")) {
|
|
548
|
-
if (/^\s*(```|~~~)/.test(line)) {
|
|
549
|
-
fenced = !fenced;
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
if (fenced) continue;
|
|
553
|
-
const m = EMBED.exec(line);
|
|
554
|
-
if (m) out.push(m[1].trim());
|
|
555
|
-
}
|
|
556
|
-
return out;
|
|
557
|
-
}
|
|
558
|
-
/** The embed that names a note, in its shortest form: its file name, or its path where the name is shared. */
|
|
559
|
-
function embedFor(id, ids) {
|
|
560
|
-
const name = id.split("/").at(-1);
|
|
561
|
-
let shared = 0;
|
|
562
|
-
for (const other of ids) if (other.split("/").at(-1) === name) shared++;
|
|
563
|
-
return `[[!${shared > 1 ? id : name}]]`;
|
|
564
|
-
}
|
|
565
|
-
/** A code link's file and lines, or null when it is not one, or names a path outside the repository. */
|
|
566
|
-
function codeTarget(href) {
|
|
567
|
-
const m = /^code:([^#?]+)(?:#L(\d+)(?:-L?(\d+))?)?$/.exec(href.trim());
|
|
568
|
-
if (!m) return null;
|
|
569
|
-
const path = decodeURIComponent(m[1]).replace(/^\.?\/+/, "");
|
|
570
|
-
if (!path || path.split("/").some((p) => p === ".." || !p)) return null;
|
|
571
|
-
return {
|
|
572
|
-
path,
|
|
573
|
-
...m[2] ? { line: Number(m[2]) } : {},
|
|
574
|
-
...m[3] ? { end: Number(m[3]) } : {}
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
/** A note's links out of the base, in order, each once. */
|
|
578
|
-
function outwardLinks(body) {
|
|
579
|
-
const out = [], seen = /* @__PURE__ */ new Set();
|
|
580
|
-
for (const { href, text: words } of linkTexts(body)) {
|
|
581
|
-
if (seen.has(href)) continue;
|
|
582
|
-
if (/^https?:\/\//i.test(href)) out.push({
|
|
583
|
-
kind: "web",
|
|
584
|
-
href,
|
|
585
|
-
text: words || href.replace(/^https?:\/\/(www\.)?/i, "")
|
|
586
|
-
});
|
|
587
|
-
else {
|
|
588
|
-
const c = codeTarget(href);
|
|
589
|
-
if (!c) continue;
|
|
590
|
-
out.push({
|
|
591
|
-
kind: "code",
|
|
592
|
-
href,
|
|
593
|
-
text: words || c.path.split("/").at(-1),
|
|
594
|
-
...c
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
seen.add(href);
|
|
598
|
-
}
|
|
599
|
-
for (const { href, embed, alt } of citeRefs(body)) {
|
|
600
|
-
if (!embed || seen.has(href)) continue;
|
|
601
|
-
const c = codeTarget(href);
|
|
602
|
-
if (!c || !pictureType(c.path)) continue;
|
|
603
|
-
out.push({
|
|
604
|
-
kind: "code",
|
|
605
|
-
href,
|
|
606
|
-
text: alt || c.path.split("/").at(-1),
|
|
607
|
-
path: c.path,
|
|
608
|
-
picture: true
|
|
609
|
-
});
|
|
610
|
-
seen.add(href);
|
|
611
|
-
}
|
|
612
|
-
return out;
|
|
613
|
-
}
|
|
614
|
-
//#endregion
|
|
615
|
-
//#region packages/core/src/bundle.ts
|
|
616
|
-
/** The base's own instructions, for Iso and any other agent, at the top of the notes (M15). Not a note. */
|
|
617
|
-
const AGENTS_FILE = "AGENTS.md";
|
|
618
|
-
const OKF_VERSION = "0.2";
|
|
619
|
-
const KINDS$2 = [
|
|
620
|
-
["artifact", /\.html?$/i],
|
|
621
|
-
["image", /\.(png|jpe?g|gif|svg|webp|avif)$/i],
|
|
622
|
-
["data", /\.(csv|tsv|json|jsonl|ndjson|parquet|arrow|feather|npy|npz|xlsx|xls|h5|hdf5|nc)$/i],
|
|
623
|
-
["document", /\.(pdf|docx?|odt|rtf|epub|pptx?|odp|txt|tex)$/i]
|
|
624
|
-
];
|
|
625
|
-
/** What kind of file a path is: null for a note, or a path with no extension (a note or a folder named without one);
|
|
626
|
-
* anything not listed is misc rather than dropped. */
|
|
627
|
-
const citeKind = (path) => {
|
|
628
|
-
const name = basename(path);
|
|
629
|
-
if (/\.md$/i.test(name) || !/\.[^.]+$/.test(name.replace(/^\./, ""))) return null;
|
|
630
|
-
return KINDS$2.find(([, re]) => re.test(name))?.[0] ?? "misc";
|
|
631
|
-
};
|
|
632
|
-
const isEntry = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
633
|
-
var Concept = class {
|
|
634
|
-
id;
|
|
635
|
-
path;
|
|
636
|
-
meta;
|
|
637
|
-
body;
|
|
638
|
-
mtime;
|
|
639
|
-
links = [];
|
|
640
|
-
cites = [];
|
|
641
|
-
/** The notes it embeds (`[[!name]]`, M12), resolved to ids, in order. */
|
|
642
|
-
embeds = [];
|
|
643
|
-
/** Its links out of the base: to the web, and to code (M12). */
|
|
644
|
-
outward = [];
|
|
645
|
-
/** For a question or a problem: the notes it is about (or uses), from its frontmatter and from the notes that
|
|
646
|
-
* embed it. */
|
|
647
|
-
attached = [];
|
|
648
|
-
constructor(id, path, meta, body, mtime = 0) {
|
|
649
|
-
this.id = id;
|
|
650
|
-
this.path = path;
|
|
651
|
-
this.meta = meta;
|
|
652
|
-
this.body = body;
|
|
653
|
-
this.mtime = mtime;
|
|
654
|
-
}
|
|
655
|
-
get directory() {
|
|
656
|
-
return dirname(this.id);
|
|
657
|
-
}
|
|
658
|
-
get type() {
|
|
659
|
-
const t = this.meta.type;
|
|
660
|
-
return t === null || t === void 0 || t === "" ? "" : text(t);
|
|
661
|
-
}
|
|
662
|
-
get title() {
|
|
663
|
-
const t = this.meta.title;
|
|
664
|
-
if (t) return text(t);
|
|
665
|
-
const stem = basename(this.id);
|
|
666
|
-
const words = strip(stem.replaceAll("-", " ").replaceAll("_", " "));
|
|
667
|
-
return words ? words[0].toUpperCase() + words.slice(1).toLowerCase() : stem;
|
|
668
|
-
}
|
|
669
|
-
get description() {
|
|
670
|
-
return text(this.meta.description || "");
|
|
671
|
-
}
|
|
672
|
-
/** A note, an open question, or a problem (M12). */
|
|
673
|
-
get kind() {
|
|
674
|
-
return kindOf(this.type);
|
|
675
|
-
}
|
|
676
|
-
/** When a question was resolved or a problem solved, as written; null while open, and for a note. */
|
|
677
|
-
get done() {
|
|
678
|
-
return doneOf(this.meta, this.kind);
|
|
679
|
-
}
|
|
680
|
-
get tags() {
|
|
681
|
-
let tags = this.meta.tags || [];
|
|
682
|
-
if (typeof tags === "string") tags = [tags];
|
|
683
|
-
return Array.isArray(tags) ? tags.filter((t) => t !== null && t !== void 0).map(text) : [];
|
|
684
|
-
}
|
|
685
|
-
get status() {
|
|
686
|
-
return text(this.meta.status || "stable");
|
|
687
|
-
}
|
|
688
|
-
get generated() {
|
|
689
|
-
return isEntry(this.meta.generated) ? this.meta.generated : {};
|
|
690
|
-
}
|
|
691
|
-
get generatedAt() {
|
|
692
|
-
return toTime(this.generated.at);
|
|
693
|
-
}
|
|
694
|
-
get verified() {
|
|
695
|
-
const v = this.meta.verified;
|
|
696
|
-
if (isEntry(v)) return [v];
|
|
697
|
-
return Array.isArray(v) ? v.filter(isEntry) : [];
|
|
698
|
-
}
|
|
699
|
-
/** OKF §5.3. */
|
|
700
|
-
get trust() {
|
|
701
|
-
const events = this.verified;
|
|
702
|
-
if (!events.length) return "unverified";
|
|
703
|
-
return events.some((e) => text(e.by).startsWith("human:")) ? "human-reviewed" : "machine-confirmed";
|
|
704
|
-
}
|
|
705
|
-
get lastHumanVerification() {
|
|
706
|
-
const times = this.verified.filter((e) => text(e.by).startsWith("human:")).map((e) => toTime(e.at)).filter((t) => t !== null);
|
|
707
|
-
return times.length ? Math.max(...times) : null;
|
|
708
|
-
}
|
|
709
|
-
/** Human-reviewed, but meaningfully changed since the latest human check. */
|
|
710
|
-
get verificationStale() {
|
|
711
|
-
const verified = this.lastHumanVerification, generated = this.generatedAt;
|
|
712
|
-
return verified !== null && generated !== null && generated > verified;
|
|
713
|
-
}
|
|
714
|
-
/** `stale_after` has passed (OKF §5.5). */
|
|
715
|
-
contentStale(now = Date.now()) {
|
|
716
|
-
const at = toTime(this.meta.stale_after);
|
|
717
|
-
return at !== null && Math.floor(now / 1e3) * 1e3 >= at;
|
|
718
|
-
}
|
|
719
|
-
};
|
|
720
|
-
var Directory = class {
|
|
721
|
-
concepts = [];
|
|
722
|
-
children = [];
|
|
723
|
-
hasIndex = false;
|
|
724
|
-
hasLog = false;
|
|
725
|
-
id;
|
|
726
|
-
constructor(id) {
|
|
727
|
-
this.id = id;
|
|
728
|
-
}
|
|
729
|
-
get name() {
|
|
730
|
-
return this.id ? basename(this.id) : "";
|
|
731
|
-
}
|
|
732
|
-
};
|
|
733
|
-
const SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
734
|
-
const LOG_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
735
|
-
/** Sort paths part by part, as Python sorts Path objects. */
|
|
736
|
-
function comparePaths(a, b) {
|
|
737
|
-
return cmpTuple(a.split("/"), b.split("/"));
|
|
738
|
-
}
|
|
739
|
-
var Bundle = class Bundle {
|
|
740
|
-
concepts = /* @__PURE__ */ new Map();
|
|
741
|
-
directories = /* @__PURE__ */ new Map();
|
|
742
|
-
issues = [];
|
|
743
|
-
/** Every file in the bundle that is not Markdown, by path: what a note may cite (artifacts, pictures, data). */
|
|
744
|
-
files = /* @__PURE__ */ new Set();
|
|
745
|
-
rootMeta = {};
|
|
746
|
-
cache = /* @__PURE__ */ new Map();
|
|
747
|
-
/** Load a bundle from its files (paths relative to the bundle root). */
|
|
748
|
-
static fromFiles(files) {
|
|
749
|
-
const b = new Bundle();
|
|
750
|
-
b.scan([...files]);
|
|
751
|
-
return b;
|
|
752
|
-
}
|
|
753
|
-
memo(key, make) {
|
|
754
|
-
if (!this.cache.has(key)) this.cache.set(key, make());
|
|
755
|
-
return this.cache.get(key);
|
|
756
|
-
}
|
|
757
|
-
scan(files) {
|
|
758
|
-
this.directories.set("", new Directory(""));
|
|
759
|
-
const entries = /* @__PURE__ */ new Map();
|
|
760
|
-
for (const f of files) {
|
|
761
|
-
const path = f.path.replace(/^\/+|\/+$/g, "");
|
|
762
|
-
if (!path) continue;
|
|
763
|
-
entries.set(path, {
|
|
764
|
-
...f,
|
|
765
|
-
path
|
|
766
|
-
});
|
|
767
|
-
const parts = path.split("/");
|
|
768
|
-
for (let i = 1; i < parts.length; i++) {
|
|
769
|
-
const d = parts.slice(0, i).join("/");
|
|
770
|
-
if (!entries.has(d)) entries.set(d, {
|
|
771
|
-
path: d,
|
|
772
|
-
dir: true
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
for (const f of [...entries.values()].sort((a, b) => comparePaths(a.path, b.path))) {
|
|
777
|
-
const rel = f.path;
|
|
778
|
-
if (f.text !== void 0) f.text = f.text.replace(/\r\n?/g, "\n");
|
|
779
|
-
if (rel.split("/").some((part) => part.startsWith("."))) continue;
|
|
780
|
-
if (f.dir) {
|
|
781
|
-
this.ensureDir(rel);
|
|
782
|
-
continue;
|
|
783
|
-
}
|
|
784
|
-
if (!rel.endsWith(".md") || rel === "AGENTS.md") {
|
|
785
|
-
this.files.add(rel);
|
|
786
|
-
continue;
|
|
787
|
-
}
|
|
788
|
-
const directory = dirname(rel);
|
|
789
|
-
this.ensureDir(directory);
|
|
790
|
-
const name = basename(rel);
|
|
791
|
-
if (name === "index.md") {
|
|
792
|
-
this.directories.get(directory).hasIndex = true;
|
|
793
|
-
this.checkIndex(f.text ?? "", rel, directory);
|
|
794
|
-
} else if (name === "log.md") {
|
|
795
|
-
this.directories.get(directory).hasLog = true;
|
|
796
|
-
this.checkLog(f.text ?? "", rel);
|
|
797
|
-
} else this.loadConcept(f.text ?? "", rel, f.mtime ?? 0);
|
|
798
|
-
}
|
|
799
|
-
for (const concept of this.concepts.values()) {
|
|
800
|
-
const links = [];
|
|
801
|
-
for (const [target, rel] of linkRefs(concept.body)) {
|
|
802
|
-
const link = this.resolve(concept, target);
|
|
803
|
-
if (link) {
|
|
804
|
-
link.rel = rel;
|
|
805
|
-
links.push(link);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
concept.links = links;
|
|
809
|
-
concept.outward = outwardLinks(concept.body);
|
|
810
|
-
for (const link of links) if (link.broken) this.issue(concept.path, "warning", "broken-link", `broken link to ${link.target}`);
|
|
811
|
-
for (const { href, title, embed, alt } of citeRefs(concept.body)) {
|
|
812
|
-
const path = this.filePath(concept, href), kind = path === null ? null : citeKind(path);
|
|
813
|
-
if (path === null || kind === null) continue;
|
|
814
|
-
const t = strip((title ?? "").toLowerCase());
|
|
815
|
-
const cite = {
|
|
816
|
-
target: path,
|
|
817
|
-
kind,
|
|
818
|
-
embed,
|
|
819
|
-
broken: !this.files.has(path),
|
|
820
|
-
rel: rating(title),
|
|
821
|
-
align: t === "left" ? "left" : t === "center" || t === "centre" ? "center" : null,
|
|
822
|
-
...embed && alt ? { alt } : {}
|
|
823
|
-
};
|
|
824
|
-
concept.cites.push(cite);
|
|
825
|
-
if (cite.broken) this.issue(concept.path, "warning", "broken-link", kind === "image" ? `picture not in the notes: ${path}` : `broken link to ${path}`);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
const byName = /* @__PURE__ */ new Map();
|
|
829
|
-
for (const id of this.concepts.keys()) {
|
|
830
|
-
const n = basename(id);
|
|
831
|
-
byName.set(n, [...byName.get(n) ?? [], id]);
|
|
832
|
-
}
|
|
833
|
-
for (const concept of this.concepts.values()) for (const ref of embedRefs(concept.body)) {
|
|
834
|
-
const path = ref.replace(/^\/+/, "").replace(/\.md$/, "");
|
|
835
|
-
const found = path.includes("/") ? this.concepts.has(path) ? [path] : [] : byName.get(path) ?? [];
|
|
836
|
-
if (found.length === 1) concept.embeds.push(found[0]);
|
|
837
|
-
else this.issue(concept.path, "warning", "embed-broken", found.length ? `two notes are named ${path}: embed one by its path` : `no note to embed: ${ref}`);
|
|
838
|
-
}
|
|
839
|
-
for (const concept of this.concepts.values()) {
|
|
840
|
-
if (concept.kind === "note") continue;
|
|
841
|
-
concept.attached = idsOf(concept.meta[ABOUT[concept.kind]]).filter((id) => this.concepts.has(id) && id !== concept.id);
|
|
842
|
-
}
|
|
843
|
-
for (const concept of this.concepts.values()) for (const id of concept.embeds) {
|
|
844
|
-
const embedded = this.concepts.get(id);
|
|
845
|
-
if (embedded.kind !== "note" && id !== concept.id && !embedded.attached.includes(concept.id)) embedded.attached.push(concept.id);
|
|
846
|
-
}
|
|
847
|
-
for (const group of this.requiresCycles()) this.issue(this.concepts.get(group[0]).path, "warning", "requires-cycle", `requires cycle: ${group.join(", ")} require each other`);
|
|
848
|
-
}
|
|
849
|
-
issue(path, level, code, message) {
|
|
850
|
-
this.issues.push({
|
|
851
|
-
path,
|
|
852
|
-
level,
|
|
853
|
-
code,
|
|
854
|
-
message
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
ensureDir(directory) {
|
|
858
|
-
if (this.directories.has(directory)) return;
|
|
859
|
-
const parent = dirname(directory);
|
|
860
|
-
this.ensureDir(parent);
|
|
861
|
-
this.directories.set(directory, new Directory(directory));
|
|
862
|
-
this.directories.get(parent).children.push(directory);
|
|
863
|
-
}
|
|
864
|
-
loadConcept(source, rel, mtime) {
|
|
865
|
-
const cid = rel.slice(0, -3);
|
|
866
|
-
let meta, body;
|
|
867
|
-
try {
|
|
868
|
-
[meta, body] = splitFrontmatter(source);
|
|
869
|
-
} catch (err) {
|
|
870
|
-
if (!(err instanceof FrontmatterError)) throw err;
|
|
871
|
-
this.issue(rel, "error", "frontmatter-invalid", err.message);
|
|
872
|
-
[meta, body] = [{}, source];
|
|
873
|
-
}
|
|
874
|
-
const concept = new Concept(cid, rel, meta ?? {}, body, mtime);
|
|
875
|
-
if (meta === null) this.issue(rel, "error", "frontmatter-missing", "missing YAML frontmatter");
|
|
876
|
-
else if (!concept.type) this.issue(rel, "error", "type-missing", "frontmatter has no non-empty 'type'");
|
|
877
|
-
if ("generated" in concept.meta && !("by" in concept.generated)) this.issue(rel, "warning", "generated-without-by", "'generated' should record 'by'");
|
|
878
|
-
for (const event of concept.verified) if (!("by" in event)) this.issue(rel, "warning", "verified-without-by", "'verified' entry without 'by'");
|
|
879
|
-
this.concepts.set(cid, concept);
|
|
880
|
-
this.directories.get(dirname(rel)).concepts.push(cid);
|
|
881
|
-
}
|
|
882
|
-
checkIndex(source, rel, directory) {
|
|
883
|
-
let meta;
|
|
884
|
-
try {
|
|
885
|
-
[meta] = splitFrontmatter(source);
|
|
886
|
-
} catch (err) {
|
|
887
|
-
if (!(err instanceof FrontmatterError)) throw err;
|
|
888
|
-
this.issue(rel, "error", "frontmatter-invalid", err.message);
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
if (meta === null) return;
|
|
892
|
-
if (directory !== "") this.issue(rel, "error", "index-frontmatter", "index.md below the root must not have frontmatter");
|
|
893
|
-
else if (Object.keys(meta).some((k) => k !== "okf_version")) this.issue(rel, "error", "root-index-frontmatter", "root index.md frontmatter may only carry okf_version");
|
|
894
|
-
else this.rootMeta = meta;
|
|
895
|
-
}
|
|
896
|
-
checkLog(source, rel) {
|
|
897
|
-
for (const line of source.split("\n")) if (line.startsWith("## ") && !LOG_DATE.test(strip(line.slice(3)))) this.issue(rel, "warning", "log-heading-date", `log heading is not YYYY-MM-DD: ${strip(line)}`);
|
|
898
|
-
}
|
|
899
|
-
/** A link's target as a path in the bundle (not checked to exist), or null for an address or a way out of it. */
|
|
900
|
-
filePath(concept, raw) {
|
|
901
|
-
if (!raw || raw.startsWith("#") || SCHEME.test(raw)) return null;
|
|
902
|
-
const target = unquote(raw.split("#", 1)[0].split("?", 1)[0]);
|
|
903
|
-
if (!target || target.endsWith("/")) return null;
|
|
904
|
-
const norm = normpath(target.startsWith("/") ? target.replace(/^\/+/, "") : join(concept.directory, target));
|
|
905
|
-
return norm.startsWith("..") || norm === "." ? null : norm;
|
|
906
|
-
}
|
|
907
|
-
resolve(concept, raw) {
|
|
908
|
-
if (!raw || raw.startsWith("#") || SCHEME.test(raw)) return null;
|
|
909
|
-
const target = unquote(raw.split("#", 1)[0].split("?", 1)[0]);
|
|
910
|
-
if (!target) return null;
|
|
911
|
-
const joined = target.startsWith("/") ? target.replace(/^\/+/, "") : join(concept.directory, target);
|
|
912
|
-
let norm = joined ? normpath(joined) : ".";
|
|
913
|
-
if (norm.startsWith("..")) return null;
|
|
914
|
-
if (norm === ".") norm = "";
|
|
915
|
-
const link = (t, kind, broken) => ({
|
|
916
|
-
target: t,
|
|
917
|
-
kind,
|
|
918
|
-
broken,
|
|
919
|
-
rel: null
|
|
920
|
-
});
|
|
921
|
-
if (target.endsWith("/") || this.directories.has(norm)) return link(norm + "/", "directory", !this.directories.has(norm));
|
|
922
|
-
if (norm.endsWith("/index.md") || norm === "index.md") {
|
|
923
|
-
const d = dirname(norm);
|
|
924
|
-
return link(d + "/", "directory", !this.directories.has(d));
|
|
925
|
-
}
|
|
926
|
-
if (norm.endsWith(".md")) {
|
|
927
|
-
const cid = norm.slice(0, -3);
|
|
928
|
-
return link(cid, "concept", !this.concepts.has(cid));
|
|
929
|
-
}
|
|
930
|
-
return null;
|
|
931
|
-
}
|
|
932
|
-
/** Each concept's direct prerequisites: the concepts it links to as `requires`. */
|
|
933
|
-
requiresGraph() {
|
|
934
|
-
return this.memo("requires", () => {
|
|
935
|
-
const graph = /* @__PURE__ */ new Map();
|
|
936
|
-
for (const [cid, c] of this.concepts) {
|
|
937
|
-
const targets = new Set(c.links.filter((l) => l.rel === "requires" && l.kind === "concept" && !l.broken && l.target !== cid && this.concepts.has(l.target)).map((l) => l.target));
|
|
938
|
-
graph.set(cid, [...targets].sort(cmp));
|
|
939
|
-
}
|
|
940
|
-
return graph;
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
/** Strongly connected components of the requires graph (Tarjan), singletons
|
|
944
|
-
* included; a component is a group that require each other. */
|
|
945
|
-
components() {
|
|
946
|
-
return this.memo("components", () => {
|
|
947
|
-
const graph = this.requiresGraph();
|
|
948
|
-
const index = /* @__PURE__ */ new Map(), low = /* @__PURE__ */ new Map();
|
|
949
|
-
const stack = [], onStack = /* @__PURE__ */ new Set(), groups = [];
|
|
950
|
-
let counter = 0;
|
|
951
|
-
const visit = (v) => {
|
|
952
|
-
index.set(v, counter);
|
|
953
|
-
low.set(v, counter);
|
|
954
|
-
counter++;
|
|
955
|
-
stack.push(v);
|
|
956
|
-
onStack.add(v);
|
|
957
|
-
for (const w of graph.get(v)) if (!index.has(w)) {
|
|
958
|
-
visit(w);
|
|
959
|
-
low.set(v, Math.min(low.get(v), low.get(w)));
|
|
960
|
-
} else if (onStack.has(w)) low.set(v, Math.min(low.get(v), index.get(w)));
|
|
961
|
-
if (low.get(v) === index.get(v)) {
|
|
962
|
-
const group = [];
|
|
963
|
-
for (;;) {
|
|
964
|
-
const w = stack.pop();
|
|
965
|
-
onStack.delete(w);
|
|
966
|
-
group.push(w);
|
|
967
|
-
if (w === v) break;
|
|
968
|
-
}
|
|
969
|
-
groups.push(group.sort(cmp));
|
|
970
|
-
}
|
|
971
|
-
};
|
|
972
|
-
for (const v of [...graph.keys()].sort(cmp)) if (!index.has(v)) visit(v);
|
|
973
|
-
return groups;
|
|
974
|
-
});
|
|
975
|
-
}
|
|
976
|
-
/** Groups of concepts that require each other, directly or through a chain. */
|
|
977
|
-
requiresCycles() {
|
|
978
|
-
return this.components().filter((g) => g.length > 1).sort(cmpTuple);
|
|
979
|
-
}
|
|
980
|
-
/** Every concept's place in a reading order and its depth (the longest chain
|
|
981
|
-
* of prerequisites below it). A topological sort of the requires graph with
|
|
982
|
-
* cycles as one step; among the notes ready, it prefers staying in the
|
|
983
|
-
* folder just read, then shallower notes, then folder and title. */
|
|
984
|
-
prerequisiteOrder() {
|
|
985
|
-
return this.memo("order", () => {
|
|
986
|
-
const graph = this.requiresGraph();
|
|
987
|
-
const comps = this.components();
|
|
988
|
-
const compOf = /* @__PURE__ */ new Map();
|
|
989
|
-
comps.forEach((g, i) => g.forEach((cid) => compOf.set(cid, i)));
|
|
990
|
-
const needs = comps.map((g, i) => [...new Set(g.flatMap((v) => graph.get(v).map((w) => compOf.get(w))))].filter((j) => j !== i).sort((a, b) => a - b));
|
|
991
|
-
const depth = comps.map(() => -1);
|
|
992
|
-
comps.forEach((_, i) => {
|
|
993
|
-
depth[i] = 1 + Math.max(-1, ...needs[i].map((j) => depth[j]));
|
|
994
|
-
});
|
|
995
|
-
const waiting = needs.map((n) => n.length);
|
|
996
|
-
const unlocks = comps.map(() => []);
|
|
997
|
-
needs.forEach((n, i) => n.forEach((j) => unlocks[j].push(i)));
|
|
998
|
-
const ready = new Set(waiting.flatMap((w, i) => w === 0 ? [i] : []));
|
|
999
|
-
const lower = (cid) => this.concepts.get(cid).title.toLowerCase();
|
|
1000
|
-
const title = comps.map((g) => g.map(lower).sort(cmp)[0]);
|
|
1001
|
-
const folder = comps.map((g) => this.concepts.get(g[0]).directory);
|
|
1002
|
-
const parts = folder.map((f) => f ? f.split("/") : []);
|
|
1003
|
-
const shared = (a, b) => {
|
|
1004
|
-
let n = 0;
|
|
1005
|
-
while (n < Math.min(a.length, b.length) && a[n] === b[n]) n++;
|
|
1006
|
-
return n;
|
|
1007
|
-
};
|
|
1008
|
-
const out = /* @__PURE__ */ new Map();
|
|
1009
|
-
let last = null;
|
|
1010
|
-
while (ready.size) {
|
|
1011
|
-
const near = last === null ? [] : parts[last];
|
|
1012
|
-
const key = (k) => [
|
|
1013
|
-
-shared(parts[k], near),
|
|
1014
|
-
depth[k],
|
|
1015
|
-
folder[k],
|
|
1016
|
-
title[k]
|
|
1017
|
-
];
|
|
1018
|
-
let best = -1;
|
|
1019
|
-
for (const k of [...ready].sort((a, b) => a - b)) if (best < 0 || cmpTuple(key(k), key(best)) < 0) best = k;
|
|
1020
|
-
ready.delete(best);
|
|
1021
|
-
for (const cid of [...comps[best]].sort((a, b) => cmp(lower(a), lower(b)))) out.set(cid, {
|
|
1022
|
-
order: out.size,
|
|
1023
|
-
depth: depth[best]
|
|
1024
|
-
});
|
|
1025
|
-
last = best;
|
|
1026
|
-
for (const k of unlocks[best]) if (--waiting[k] === 0) ready.add(k);
|
|
1027
|
-
}
|
|
1028
|
-
return out;
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
/** Everything `cid` requires, directly or through a chain, in reading order. */
|
|
1032
|
-
prerequisites(cid) {
|
|
1033
|
-
const graph = this.requiresGraph();
|
|
1034
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1035
|
-
const todo = [...graph.get(cid) ?? []];
|
|
1036
|
-
while (todo.length) {
|
|
1037
|
-
const v = todo.pop();
|
|
1038
|
-
if (!seen.has(v)) {
|
|
1039
|
-
seen.add(v);
|
|
1040
|
-
todo.push(...graph.get(v));
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
seen.delete(cid);
|
|
1044
|
-
const order = this.prerequisiteOrder();
|
|
1045
|
-
return [...seen].sort((a, b) => order.get(a).order - order.get(b).order);
|
|
1046
|
-
}
|
|
1047
|
-
/** The concepts that link to `cid`. */
|
|
1048
|
-
backlinks(cid) {
|
|
1049
|
-
return this.memo("backlinks", () => {
|
|
1050
|
-
const m = /* @__PURE__ */ new Map();
|
|
1051
|
-
for (const c of this.concepts.values()) for (const l of c.links) {
|
|
1052
|
-
if (!m.has(l.target)) m.set(l.target, /* @__PURE__ */ new Set());
|
|
1053
|
-
m.get(l.target).add(c.id);
|
|
1054
|
-
}
|
|
1055
|
-
return new Map([...m].map(([t, ids]) => [t, [...ids].sort(cmp)]));
|
|
1056
|
-
}).get(cid) ?? [];
|
|
1057
|
-
}
|
|
1058
|
-
/** Accept an id, a bundle path, or a bundle-absolute link. */
|
|
1059
|
-
resolveId(ref) {
|
|
1060
|
-
let id = strip(ref).replace(/^\/+/, "");
|
|
1061
|
-
if (id.endsWith(".md")) id = id.slice(0, -3);
|
|
1062
|
-
return this.concepts.has(id) ? id : null;
|
|
1063
|
-
}
|
|
1064
|
-
/** Every issue found in the bundle. */
|
|
1065
|
-
lint() {
|
|
1066
|
-
return [...this.issues];
|
|
1067
|
-
}
|
|
1068
|
-
overviewFor(directory) {
|
|
1069
|
-
for (const cid of this.directories.get(directory).concepts) {
|
|
1070
|
-
const c = this.concepts.get(cid);
|
|
1071
|
-
if (c.type.toLowerCase() === "overview") return c;
|
|
1072
|
-
}
|
|
1073
|
-
return null;
|
|
1074
|
-
}
|
|
1075
|
-
/** The generated OKF §8 index of a folder. */
|
|
1076
|
-
renderIndex(directory) {
|
|
1077
|
-
const d = this.directories.get(directory);
|
|
1078
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1079
|
-
for (const cid of d.concepts) {
|
|
1080
|
-
const c = this.concepts.get(cid);
|
|
1081
|
-
const t = c.type || "Concept";
|
|
1082
|
-
if (!groups.has(t)) groups.set(t, []);
|
|
1083
|
-
groups.get(t).push(c);
|
|
1084
|
-
}
|
|
1085
|
-
const parts = [];
|
|
1086
|
-
if (directory === "") parts.push(`---\nokf_version: "0.2"\n---\n`);
|
|
1087
|
-
const typeKey = (t) => [t.toLowerCase() !== "overview" ? 1 : 0, t.toLowerCase()];
|
|
1088
|
-
for (const typeName of [...groups.keys()].sort((a, b) => cmpTuple(typeKey(a), typeKey(b)))) {
|
|
1089
|
-
const items = [...groups.get(typeName)].sort((a, b) => cmp(a.title.toLowerCase(), b.title.toLowerCase()));
|
|
1090
|
-
const lines = [`# ${typeName}`, ""];
|
|
1091
|
-
for (const c of items) {
|
|
1092
|
-
const desc = c.description ? ` - ${oneLine(c.description)}` : "";
|
|
1093
|
-
lines.push(`* [${escape(c.title)}](${basename(c.path)})${desc}`);
|
|
1094
|
-
}
|
|
1095
|
-
parts.push(lines.join("\n") + "\n");
|
|
1096
|
-
}
|
|
1097
|
-
if (d.children.length) {
|
|
1098
|
-
const lines = ["# Directories", ""];
|
|
1099
|
-
for (const child of [...d.children].sort(cmp)) {
|
|
1100
|
-
const overview = this.overviewFor(child);
|
|
1101
|
-
const desc = overview?.description ? ` - ${oneLine(overview.description)}` : "";
|
|
1102
|
-
const name = basename(child);
|
|
1103
|
-
lines.push(`* [${name}](${name}/)${desc}`);
|
|
1104
|
-
}
|
|
1105
|
-
parts.push(lines.join("\n") + "\n");
|
|
1106
|
-
}
|
|
1107
|
-
if (!groups.size && !d.children.length) parts.push("# Contents\n");
|
|
1108
|
-
return parts.join("\n").replace(/\s+$/u, "") + "\n";
|
|
1109
|
-
}
|
|
1110
|
-
};
|
|
1111
|
-
const oneLine = (s) => strip(s).split(/\s+/u).filter(Boolean).join(" ");
|
|
1112
|
-
const escape = (s) => s.replaceAll("[", "\\[").replaceAll("]", "\\]");
|
|
1113
|
-
//#endregion
|
|
1114
|
-
//#region packages/core/src/source.ts
|
|
1115
|
-
const BOM = "";
|
|
1116
|
-
/** The file as `[prefix, frontmatter, body]`: `prefix + body` is exactly the text. The prefix is the frontmatter
|
|
1117
|
-
* block with its fences and the blank lines under it; `frontmatter` is the YAML between the fences, or null. */
|
|
1118
|
-
function splitSource(text) {
|
|
1119
|
-
const [raw] = frontmatterText(text);
|
|
1120
|
-
if (raw === null) return [
|
|
1121
|
-
"",
|
|
1122
|
-
null,
|
|
1123
|
-
text
|
|
1124
|
-
];
|
|
1125
|
-
const open = text.indexOf("\n") + 1;
|
|
1126
|
-
let at = open;
|
|
1127
|
-
for (const line of text.slice(open).split("\n")) {
|
|
1128
|
-
at += line.length + 1;
|
|
1129
|
-
if (line.replace(/\s+$/u, "") === "---") break;
|
|
1130
|
-
}
|
|
1131
|
-
at = Math.min(at, text.length);
|
|
1132
|
-
while (text[at] === "\n" || text[at] === "\r" && text[at + 1] === "\n") at += text[at] === "\r" ? 2 : 1;
|
|
1133
|
-
return [
|
|
1134
|
-
text.slice(0, at),
|
|
1135
|
-
raw,
|
|
1136
|
-
text.slice(at)
|
|
1137
|
-
];
|
|
1138
|
-
}
|
|
1139
|
-
const YAML_STYLE = {
|
|
1140
|
-
lineWidth: 100,
|
|
1141
|
-
flowCollectionPadding: false,
|
|
1142
|
-
indentSeq: false
|
|
1143
|
-
};
|
|
1144
|
-
function node(doc, value) {
|
|
1145
|
-
const n = doc.createNode(value);
|
|
1146
|
-
const style = (x) => {
|
|
1147
|
-
if (isSeq(x)) {
|
|
1148
|
-
if (x.items.length && x.items.every((i) => isScalar(i) && !String(i.value).includes("\n"))) x.flow = true;
|
|
1149
|
-
else x.items.forEach(style);
|
|
1150
|
-
} else if (isMap(x)) x.items.forEach((p) => style(p.value));
|
|
1151
|
-
else if (isScalar(x) && typeof x.value === "string" && x.value.includes("\n")) x.type = Scalar.BLOCK_LITERAL;
|
|
1152
|
-
};
|
|
1153
|
-
style(n);
|
|
1154
|
-
return n;
|
|
1155
|
-
}
|
|
1156
|
-
/** One top-level field as it is written, ending in a line break. */
|
|
1157
|
-
function fieldText(key, value) {
|
|
1158
|
-
const doc = new Document({}, { version: "1.2" });
|
|
1159
|
-
const n = node(doc, value);
|
|
1160
|
-
if (isMap(n) && n.items.every((p) => isScalar(p.value) && !String(p.value.value).includes("\n"))) n.flow = true;
|
|
1161
|
-
doc.set(key, n);
|
|
1162
|
-
return doc.toString(YAML_STYLE);
|
|
1163
|
-
}
|
|
1164
|
-
const lineStart = (text, at) => text.lastIndexOf("\n", at - 1) + 1;
|
|
1165
|
-
const lineEnd = (text, at) => {
|
|
1166
|
-
const nl = text.indexOf("\n", at);
|
|
1167
|
-
return nl < 0 ? text.length : nl + 1;
|
|
1168
|
-
};
|
|
1169
|
-
/** Change top-level frontmatter fields in `raw` (null removes one), touching only their lines; new fields go at
|
|
1170
|
-
* the end, or `type` at the start. */
|
|
1171
|
-
function editFrontmatter(raw, changes) {
|
|
1172
|
-
let text = raw && !raw.endsWith("\n") ? raw + "\n" : raw;
|
|
1173
|
-
for (const [key, value] of Object.entries(changes)) {
|
|
1174
|
-
const doc = parseYaml(text);
|
|
1175
|
-
if (doc.errors.length) throw new FrontmatterError(`unparseable YAML frontmatter: ${doc.errors[0].message}`);
|
|
1176
|
-
if (text.trim() && !isMap(doc.contents)) throw new FrontmatterError("frontmatter is not a mapping");
|
|
1177
|
-
const pair = (isMap(doc.contents) ? doc.contents.items : []).find((p) => (isScalar(p.key) ? p.key.value : p.key) === key);
|
|
1178
|
-
const replacement = value === null || value === void 0 ? "" : fieldText(key, value);
|
|
1179
|
-
if (pair) {
|
|
1180
|
-
const keyRange = pair.key.range;
|
|
1181
|
-
const valueRange = pair.value?.range;
|
|
1182
|
-
const start = lineStart(text, keyRange[0]);
|
|
1183
|
-
const end = lineEnd(text, Math.max(keyRange[1], (valueRange?.[1] ?? keyRange[1]) - 1));
|
|
1184
|
-
text = text.slice(0, start) + replacement + text.slice(end);
|
|
1185
|
-
} else if (replacement) text = key === "type" ? replacement + text : text + replacement;
|
|
1186
|
-
}
|
|
1187
|
-
return text;
|
|
1188
|
-
}
|
|
1189
|
-
/** The file `before` with frontmatter fields changed and, when `body` is not null, its body replaced. What was
|
|
1190
|
-
* not changed is kept to the character, the file's line endings and byte-order mark included. */
|
|
1191
|
-
function spliceText(before, changes, body) {
|
|
1192
|
-
const bom = before.startsWith(BOM) ? BOM : "";
|
|
1193
|
-
const [prefix, raw, oldBody] = splitSource(bom ? before.slice(1) : before);
|
|
1194
|
-
let head = prefix;
|
|
1195
|
-
if (Object.keys(changes).length) {
|
|
1196
|
-
head = `---\n${editFrontmatter((raw ?? "").replace(/\r\n/g, "\n"), changes)}---\n${raw === null ? "\n" : prefix.slice(prefix.lastIndexOf("---") + 3).replace(/^[^\n]*\n/, "")}`;
|
|
1197
|
-
if (before.includes("\r\n")) head = head.replace(/\r?\n/g, "\r\n");
|
|
1198
|
-
}
|
|
1199
|
-
let newBody = oldBody;
|
|
1200
|
-
if (body !== null) newBody = before.includes("\r\n") ? body.replace(/\r?\n/g, "\r\n") : body;
|
|
1201
|
-
return bom + head + newBody;
|
|
1202
|
-
}
|
|
1203
|
-
//#endregion
|
|
1204
|
-
//#region packages/core/src/unwrap.ts
|
|
1205
|
-
const ITEM = /^\s*([-*+]|\d+[.)])\s+/;
|
|
1206
|
-
const APART = /^\s*(#{1,6}\s|\||<|```|~~~|\$\$|>|---\s*$|\*\*\*\s*$|___\s*$|\[[^\]]+\]:\s)/;
|
|
1207
|
-
const QUOTED = /^\s*>\s?\S/;
|
|
1208
|
-
function unwrap(text) {
|
|
1209
|
-
const lines = text.split("\n"), out = [];
|
|
1210
|
-
let i = 0, fence = "", maths = false;
|
|
1211
|
-
/** What the last line written may be continued by: prose, a quoted line, or nothing. */
|
|
1212
|
-
let open = "";
|
|
1213
|
-
if (lines[0] === "---") {
|
|
1214
|
-
const end = lines.indexOf("---", 1);
|
|
1215
|
-
if (end > 0) {
|
|
1216
|
-
out.push(...lines.slice(0, end + 1));
|
|
1217
|
-
i = end + 1;
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
for (; i < lines.length; i++) {
|
|
1221
|
-
const l = lines[i], t = l.trim();
|
|
1222
|
-
if (fence) {
|
|
1223
|
-
out.push(l);
|
|
1224
|
-
if (t.startsWith(fence)) fence = "";
|
|
1225
|
-
continue;
|
|
1226
|
-
}
|
|
1227
|
-
if (maths) {
|
|
1228
|
-
out.push(l);
|
|
1229
|
-
if (t.endsWith("$$")) maths = false;
|
|
1230
|
-
continue;
|
|
1231
|
-
}
|
|
1232
|
-
if (t.startsWith("```") || t.startsWith("~~~")) {
|
|
1233
|
-
fence = t.slice(0, 3);
|
|
1234
|
-
out.push(l);
|
|
1235
|
-
open = "";
|
|
1236
|
-
continue;
|
|
1237
|
-
}
|
|
1238
|
-
if (t.startsWith("$$") && !t.slice(2).includes("$$")) {
|
|
1239
|
-
maths = true;
|
|
1240
|
-
out.push(l);
|
|
1241
|
-
open = "";
|
|
1242
|
-
continue;
|
|
1243
|
-
}
|
|
1244
|
-
if (!t) {
|
|
1245
|
-
out.push(l);
|
|
1246
|
-
open = "";
|
|
1247
|
-
continue;
|
|
1248
|
-
}
|
|
1249
|
-
const hard = /( {2}|\\)$/.test(out.at(-1) ?? "");
|
|
1250
|
-
if (APART.test(l)) {
|
|
1251
|
-
if (open === "quote" && QUOTED.test(l) && !/^\s*>\s*([-*+]|\d+[.)]|#)/.test(l) && !hard) {
|
|
1252
|
-
out[out.length - 1] += " " + l.replace(/^\s*>\s?/, "").trim();
|
|
1253
|
-
continue;
|
|
1254
|
-
}
|
|
1255
|
-
out.push(l);
|
|
1256
|
-
open = QUOTED.test(l) ? "quote" : "";
|
|
1257
|
-
continue;
|
|
1258
|
-
}
|
|
1259
|
-
if (ITEM.test(l)) {
|
|
1260
|
-
out.push(l);
|
|
1261
|
-
open = "prose";
|
|
1262
|
-
continue;
|
|
1263
|
-
}
|
|
1264
|
-
if (open === "prose" && !hard) {
|
|
1265
|
-
out[out.length - 1] += " " + t;
|
|
1266
|
-
continue;
|
|
1267
|
-
}
|
|
1268
|
-
out.push(l);
|
|
1269
|
-
open = open === "" && /^( {4}|\t)/.test(l) ? "" : "prose";
|
|
1270
|
-
}
|
|
1271
|
-
return out.join("\n");
|
|
1272
|
-
}
|
|
1273
|
-
//#endregion
|
|
1274
|
-
//#region packages/core/src/search.ts
|
|
1275
|
-
const TOKEN = /[a-z0-9]+(?:['’][a-z]+)?/g;
|
|
1276
|
-
const STOP = new Set("a an and are as at be by for from has have in is it its of on or that the this to was were will with not but if then than so do does did can could should would into about".split(" "));
|
|
1277
|
-
const FIELD_WEIGHTS = {
|
|
1278
|
-
title: 4,
|
|
1279
|
-
tags: 3,
|
|
1280
|
-
description: 2,
|
|
1281
|
-
headings: 2,
|
|
1282
|
-
type: 1,
|
|
1283
|
-
body: 1
|
|
1284
|
-
};
|
|
1285
|
-
const K1 = 1.4;
|
|
1286
|
-
const B = .75;
|
|
1287
|
-
function stem(token) {
|
|
1288
|
-
for (const suffix of ["ness", "ing"]) if (token.endsWith(suffix) && token.length - suffix.length >= 4) return token.slice(0, -suffix.length);
|
|
1289
|
-
if (token.length > 4 && token.endsWith("ies")) return token.slice(0, -3) + "y";
|
|
1290
|
-
if (token.length > 3 && token.endsWith("s") && ![
|
|
1291
|
-
"ss",
|
|
1292
|
-
"us",
|
|
1293
|
-
"is"
|
|
1294
|
-
].some((e) => token.endsWith(e))) return token.slice(0, -1);
|
|
1295
|
-
return token;
|
|
1296
|
-
}
|
|
1297
|
-
function tokenize(text) {
|
|
1298
|
-
return (text.toLowerCase().replaceAll("’", "'").match(TOKEN) ?? []).filter((t) => !STOP.has(t)).map(stem);
|
|
1299
|
-
}
|
|
1300
|
-
/** Rounded as Python's round(x, 3) prints it (they differ only on exact ties). */
|
|
1301
|
-
const round3 = (x) => Number(x.toFixed(3));
|
|
1302
|
-
var SearchIndex = class {
|
|
1303
|
-
bundle;
|
|
1304
|
-
docs = /* @__PURE__ */ new Map();
|
|
1305
|
-
lengths = /* @__PURE__ */ new Map();
|
|
1306
|
-
df = /* @__PURE__ */ new Map();
|
|
1307
|
-
avgLen;
|
|
1308
|
-
constructor(bundle) {
|
|
1309
|
-
this.bundle = bundle;
|
|
1310
|
-
for (const [cid, c] of bundle.concepts) {
|
|
1311
|
-
const tf = /* @__PURE__ */ new Map();
|
|
1312
|
-
const fields = {
|
|
1313
|
-
title: c.title,
|
|
1314
|
-
tags: c.tags.join(" "),
|
|
1315
|
-
description: c.description,
|
|
1316
|
-
headings: headings(c.body).map((h) => h.text).join(" "),
|
|
1317
|
-
type: c.type,
|
|
1318
|
-
body: c.body
|
|
1319
|
-
};
|
|
1320
|
-
for (const [name, text] of Object.entries(fields)) for (const token of tokenize(text)) tf.set(token, (tf.get(token) ?? 0) + FIELD_WEIGHTS[name]);
|
|
1321
|
-
this.docs.set(cid, tf);
|
|
1322
|
-
this.lengths.set(cid, [...tf.values()].reduce((s, v) => s + v, 0));
|
|
1323
|
-
for (const token of tf.keys()) this.df.set(token, (this.df.get(token) ?? 0) + 1);
|
|
1324
|
-
}
|
|
1325
|
-
const total = [...this.lengths.values()].reduce((s, v) => s + v, 0);
|
|
1326
|
-
this.avgLen = this.lengths.size ? total / this.lengths.size : 0;
|
|
1327
|
-
}
|
|
1328
|
-
idf(token) {
|
|
1329
|
-
const n = this.docs.size, df = this.df.get(token) ?? 0;
|
|
1330
|
-
return Math.log(1 + (n - df + .5) / (df + .5));
|
|
1331
|
-
}
|
|
1332
|
-
search(query, { limit = 8, type, tags, trust, status, under } = {}) {
|
|
1333
|
-
const terms = tokenize(query);
|
|
1334
|
-
const wanted = new Set([...tags ?? []].map((t) => t.toLowerCase()));
|
|
1335
|
-
const hits = [];
|
|
1336
|
-
for (const [cid, tf] of this.docs) {
|
|
1337
|
-
const c = this.bundle.concepts.get(cid);
|
|
1338
|
-
if (type && c.type.toLowerCase() !== type.toLowerCase()) continue;
|
|
1339
|
-
if (wanted.size) {
|
|
1340
|
-
const have = new Set(c.tags.map((t) => t.toLowerCase()));
|
|
1341
|
-
if (![...wanted].every((t) => have.has(t))) continue;
|
|
1342
|
-
}
|
|
1343
|
-
if (trust && c.trust !== trust) continue;
|
|
1344
|
-
if (status && c.status !== status) continue;
|
|
1345
|
-
if (under && !(cid + "/").startsWith(under.replace(/^\/+|\/+$/g, "") + "/")) continue;
|
|
1346
|
-
let score = 0;
|
|
1347
|
-
const norm = this.avgLen ? K1 * (.25 + B * this.lengths.get(cid) / this.avgLen) : K1;
|
|
1348
|
-
for (const term of terms) {
|
|
1349
|
-
const f = tf.get(term) ?? 0;
|
|
1350
|
-
if (f) score += this.idf(term) * f * 2.4 / (f + norm);
|
|
1351
|
-
}
|
|
1352
|
-
if (terms.length && score <= 0) continue;
|
|
1353
|
-
hits.push({
|
|
1354
|
-
concept: c,
|
|
1355
|
-
score,
|
|
1356
|
-
snippet: snippet(c, terms)
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
hits.sort((a, b) => b.score - a.score || cmp(a.concept.id, b.concept.id));
|
|
1360
|
-
return hits.slice(0, limit);
|
|
1361
|
-
}
|
|
1362
|
-
};
|
|
1363
|
-
/** The first body line mentioning a query term, trimmed around the match. */
|
|
1364
|
-
function snippet(concept, terms, width = 160) {
|
|
1365
|
-
const termSet = new Set(terms);
|
|
1366
|
-
let inFence = false;
|
|
1367
|
-
for (const raw of concept.body.split("\n")) {
|
|
1368
|
-
const line = strip(raw);
|
|
1369
|
-
if (line.startsWith("```") || line.startsWith("~~~")) {
|
|
1370
|
-
inFence = !inFence;
|
|
1371
|
-
continue;
|
|
1372
|
-
}
|
|
1373
|
-
if (inFence || !line) continue;
|
|
1374
|
-
if (!tokenize(line).some((t) => termSet.has(t))) continue;
|
|
1375
|
-
const chars = [...line];
|
|
1376
|
-
const lower = [...line.toLowerCase()];
|
|
1377
|
-
const find = (t, from = 0) => {
|
|
1378
|
-
const tt = [...t];
|
|
1379
|
-
outer: for (let i = from; i + tt.length <= lower.length; i++) {
|
|
1380
|
-
for (let j = 0; j < tt.length; j++) if (lower[i + j] !== tt[j]) continue outer;
|
|
1381
|
-
return i;
|
|
1382
|
-
}
|
|
1383
|
-
return -1;
|
|
1384
|
-
};
|
|
1385
|
-
const positions = [...termSet].map((t) => find(t)).filter((p) => p >= 0);
|
|
1386
|
-
const pos = positions.length ? Math.min(...positions) : 0;
|
|
1387
|
-
let start = Math.max(0, pos - Math.floor(width / 3));
|
|
1388
|
-
if (start) {
|
|
1389
|
-
const space = chars.indexOf(" ", start);
|
|
1390
|
-
start = space >= 0 && space < pos ? space + 1 : start;
|
|
1391
|
-
}
|
|
1392
|
-
const text = chars.slice(start, start + width).join("");
|
|
1393
|
-
return (start ? "…" : "") + text + (start + width < chars.length ? "…" : "");
|
|
1394
|
-
}
|
|
1395
|
-
return [...concept.description].slice(0, width).join("");
|
|
1396
|
-
}
|
|
1397
|
-
//#endregion
|
|
1398
|
-
//#region packages/core/src/record.ts
|
|
1399
|
-
const KINDS = [
|
|
1400
|
-
"autodidactic",
|
|
1401
|
-
"interactive",
|
|
1402
|
-
"ai"
|
|
1403
|
-
];
|
|
1404
|
-
const MAX_EVENT_BYTES = 65536;
|
|
1405
|
-
const ID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
1406
|
-
const DEVICE_RE = /^[a-z0-9]{4,16}$/;
|
|
1407
|
-
const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
1408
|
-
const LINE_BREAKS = /* @__PURE__ */ new RegExp("\\r\\n|[\\n\\r\\v\\f\\x1c\\x1d\\x1e\\x85\\u2028\\u2029]");
|
|
1409
|
-
var LearnerError = class extends Error {};
|
|
1410
|
-
function encode(n, length) {
|
|
1411
|
-
let out = "";
|
|
1412
|
-
for (let i = length - 1; i >= 0; i--) out += CROCKFORD[Number(n >> BigInt(5 * i) & 31n)];
|
|
1413
|
-
return out;
|
|
1414
|
-
}
|
|
1415
|
-
const RANDOM_MASK = (1n << 80n) - 1n;
|
|
1416
|
-
let last = [0, 0n];
|
|
1417
|
-
function random80() {
|
|
1418
|
-
return crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(10)).reduce((acc, b) => acc << 8n | BigInt(b), 0n);
|
|
1419
|
-
}
|
|
1420
|
-
/** A ULID. Within one millisecond the random part counts up, so ids from one
|
|
1421
|
-
* device sort in the order they were made. */
|
|
1422
|
-
function newId(ms = Date.now()) {
|
|
1423
|
-
const rand = ms === last[0] ? last[1] + 1n : random80();
|
|
1424
|
-
last = [ms, rand];
|
|
1425
|
-
return encode(BigInt(ms), 10) + encode(rand & RANDOM_MASK, 16);
|
|
1426
|
-
}
|
|
1427
|
-
/** A fixed id for an event written before events had ids: its time, and a
|
|
1428
|
-
* hash of the line in place of randomness, so every device derives the same one. */
|
|
1429
|
-
function legacyId(line, at) {
|
|
1430
|
-
const ms = toTime(typeof at === "string" ? at : null) ?? 0;
|
|
1431
|
-
return encode(BigInt(ms), 10) + encode(BigInt("0x" + sha256(line).slice(0, 20)), 16);
|
|
1432
|
-
}
|
|
1433
|
-
/** The events in one record's text, in the order written, each once (a retried
|
|
1434
|
-
* write can repeat one); lines that do not parse are skipped. */
|
|
1435
|
-
function readRecord(text) {
|
|
1436
|
-
const out = [];
|
|
1437
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1438
|
-
for (const line of text.split(LINE_BREAKS)) {
|
|
1439
|
-
let e;
|
|
1440
|
-
try {
|
|
1441
|
-
e = JSON.parse(line);
|
|
1442
|
-
} catch {
|
|
1443
|
-
continue;
|
|
1444
|
-
}
|
|
1445
|
-
if (typeof e !== "object" || e === null || Array.isArray(e)) continue;
|
|
1446
|
-
const event = e;
|
|
1447
|
-
const withId = typeof event.id === "string" ? event : {
|
|
1448
|
-
id: legacyId(line, event.at),
|
|
1449
|
-
...event
|
|
1450
|
-
};
|
|
1451
|
-
if (!seen.has(withId.id)) {
|
|
1452
|
-
seen.add(withId.id);
|
|
1453
|
-
out.push(withId);
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
return out;
|
|
1457
|
-
}
|
|
1458
|
-
/** The union of several devices' records, by id, in time order. The same in
|
|
1459
|
-
* any order and however often it is repeated. */
|
|
1460
|
-
function mergeRecords(...records) {
|
|
1461
|
-
const byId = /* @__PURE__ */ new Map();
|
|
1462
|
-
for (const record of records) for (const e of record) if (!byId.has(e.id)) byId.set(e.id, e);
|
|
1463
|
-
return [...byId.keys()].sort().map((k) => byId.get(k));
|
|
1464
|
-
}
|
|
1465
|
-
/** Check an event from a page or agent, and stamp it (id, time, device) for
|
|
1466
|
-
* appending. Callers supply the device id and the time. */
|
|
1467
|
-
function cleanEvent(event, { device, now = Date.now() }) {
|
|
1468
|
-
if (typeof event !== "object" || event === null || Array.isArray(event)) throw new LearnerError("an event is a JSON object");
|
|
1469
|
-
const e = event;
|
|
1470
|
-
if (typeof e.event !== "string" || !e.event || e.event.length > 40) throw new LearnerError("an event needs an 'event' name");
|
|
1471
|
-
if (e.kind !== void 0 && e.kind !== null && !KINDS.includes(e.kind)) throw new LearnerError(`'kind' is one of ${KINDS.join(", ")}`);
|
|
1472
|
-
if (e.id !== void 0 && e.id !== null && !(typeof e.id === "string" && ID_RE.test(e.id))) throw new LearnerError("'id' is a ULID (26 characters)");
|
|
1473
|
-
if (e.device !== void 0 && e.device !== null && !(typeof e.device === "string" && DEVICE_RE.test(e.device))) throw new LearnerError("'device' is 4 to 16 lowercase letters and digits");
|
|
1474
|
-
const { at: _at, id, device: dev, ...rest } = e;
|
|
1475
|
-
const out = {
|
|
1476
|
-
id: id || newId(now),
|
|
1477
|
-
at: iso(now),
|
|
1478
|
-
device: dev || device,
|
|
1479
|
-
...rest
|
|
1480
|
-
};
|
|
1481
|
-
if (new TextEncoder().encode(JSON.stringify(out)).length > 65536) throw new LearnerError("event too large");
|
|
1482
|
-
return out;
|
|
1483
|
-
}
|
|
1484
|
-
//#endregion
|
|
1485
|
-
//#region packages/core/src/graph.ts
|
|
1486
|
-
const STRENGTH = {
|
|
1487
|
-
requires: 3,
|
|
1488
|
-
uses: 2,
|
|
1489
|
-
"see also": 1
|
|
1490
|
-
};
|
|
1491
|
-
/** Note ids in the order the dashboard lists them (sorted, as the build writes them). */
|
|
1492
|
-
function noteIds(bundle) {
|
|
1493
|
-
return [...bundle.concepts.keys()].sort(cmp);
|
|
1494
|
-
}
|
|
1495
|
-
/** Directed links between notes, each with its strongest rating. */
|
|
1496
|
-
function strengthEdges(bundle, ids = noteIds(bundle)) {
|
|
1497
|
-
const strongest = /* @__PURE__ */ new Map();
|
|
1498
|
-
for (const id of ids) for (const l of bundle.concepts.get(id).links) {
|
|
1499
|
-
if (l.broken || l.kind !== "concept" || !bundle.concepts.has(l.target) || l.target === id) continue;
|
|
1500
|
-
const key = id + "\n" + l.target;
|
|
1501
|
-
strongest.set(key, Math.max(strongest.get(key) ?? 0, l.rel ? STRENGTH[l.rel] : 2));
|
|
1502
|
-
}
|
|
1503
|
-
return [...strongest].map(([key, s]) => {
|
|
1504
|
-
const [a, b] = key.split("\n");
|
|
1505
|
-
return [
|
|
1506
|
-
a,
|
|
1507
|
-
b,
|
|
1508
|
-
s
|
|
1509
|
-
];
|
|
1510
|
-
});
|
|
1511
|
-
}
|
|
1512
|
-
/** PageRank over the links: how central each note is. */
|
|
1513
|
-
function pagerank(ids, edges, damping = .85, rounds = 40) {
|
|
1514
|
-
const n = ids.length;
|
|
1515
|
-
const out = new Map(ids.map((id) => [id, []]));
|
|
1516
|
-
for (const [a, b] of edges) out.get(a).push(b);
|
|
1517
|
-
let rank = new Map(ids.map((id) => [id, 1 / n]));
|
|
1518
|
-
for (let i = 0; i < rounds; i++) {
|
|
1519
|
-
const next = new Map(ids.map((id) => [id, (1 - damping) / n]));
|
|
1520
|
-
let dangling = 0;
|
|
1521
|
-
for (const id of ids) {
|
|
1522
|
-
const targets = out.get(id);
|
|
1523
|
-
if (!targets.length) {
|
|
1524
|
-
dangling += rank.get(id);
|
|
1525
|
-
continue;
|
|
1526
|
-
}
|
|
1527
|
-
const share = damping * rank.get(id) / targets.length;
|
|
1528
|
-
for (const t of targets) next.set(t, next.get(t) + share);
|
|
1529
|
-
}
|
|
1530
|
-
for (const id of ids) next.set(id, next.get(id) + damping * dangling / n);
|
|
1531
|
-
rank = next;
|
|
1532
|
-
}
|
|
1533
|
-
return rank;
|
|
1534
|
-
}
|
|
1535
|
-
/** Tarjan's strongly connected components: node → component number. */
|
|
1536
|
-
function stronglyConnected(ids, next) {
|
|
1537
|
-
const index = /* @__PURE__ */ new Map(), low = /* @__PURE__ */ new Map(), onStack = /* @__PURE__ */ new Set();
|
|
1538
|
-
const stack = [], component = /* @__PURE__ */ new Map();
|
|
1539
|
-
let counter = 0, groups = 0;
|
|
1540
|
-
const visit = (v) => {
|
|
1541
|
-
index.set(v, counter);
|
|
1542
|
-
low.set(v, counter);
|
|
1543
|
-
counter++;
|
|
1544
|
-
stack.push(v);
|
|
1545
|
-
onStack.add(v);
|
|
1546
|
-
for (const w of next(v)) if (!index.has(w)) {
|
|
1547
|
-
visit(w);
|
|
1548
|
-
low.set(v, Math.min(low.get(v), low.get(w)));
|
|
1549
|
-
} else if (onStack.has(w)) low.set(v, Math.min(low.get(v), index.get(w)));
|
|
1550
|
-
if (low.get(v) === index.get(v)) {
|
|
1551
|
-
let w;
|
|
1552
|
-
do {
|
|
1553
|
-
w = stack.pop();
|
|
1554
|
-
onStack.delete(w);
|
|
1555
|
-
component.set(w, groups);
|
|
1556
|
-
} while (w !== v);
|
|
1557
|
-
groups++;
|
|
1558
|
-
}
|
|
1559
|
-
};
|
|
1560
|
-
for (const v of ids) if (!index.has(v)) visit(v);
|
|
1561
|
-
return component;
|
|
1562
|
-
}
|
|
1563
|
-
/** Links implied by others, as "from\nto" keys: a → c is implied when c can also
|
|
1564
|
-
* be reached from a through a chain of links at least as strong (never through
|
|
1565
|
-
* "see also"), as in a transitive reduction. Links inside a group of notes that
|
|
1566
|
-
* require each other (a cycle) are left alone. Only the map hides them. */
|
|
1567
|
-
function impliedLinks(ids, edges) {
|
|
1568
|
-
const out = new Map(ids.map((id) => [id, []]));
|
|
1569
|
-
for (const [a, b, s] of edges) out.get(a).push([b, s]);
|
|
1570
|
-
const component = stronglyConnected(ids, (v) => out.get(v).filter(([, s]) => s >= 2).map(([w]) => w));
|
|
1571
|
-
const implied = /* @__PURE__ */ new Set();
|
|
1572
|
-
for (const [a, c, s] of edges) {
|
|
1573
|
-
if (component.get(a) === component.get(c)) continue;
|
|
1574
|
-
const need = Math.max(s, 2);
|
|
1575
|
-
const seen = /* @__PURE__ */ new Set([a]);
|
|
1576
|
-
const stack = [];
|
|
1577
|
-
for (const [b, sb] of out.get(a)) if (b !== c && sb >= need && !seen.has(b)) {
|
|
1578
|
-
seen.add(b);
|
|
1579
|
-
stack.push(b);
|
|
1580
|
-
}
|
|
1581
|
-
while (stack.length) {
|
|
1582
|
-
const v = stack.pop();
|
|
1583
|
-
if (v === c) {
|
|
1584
|
-
implied.add(a + "\n" + c);
|
|
1585
|
-
break;
|
|
1586
|
-
}
|
|
1587
|
-
for (const [w, sw] of out.get(v)) if (sw >= need && !seen.has(w)) {
|
|
1588
|
-
seen.add(w);
|
|
1589
|
-
stack.push(w);
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
return implied;
|
|
1594
|
-
}
|
|
1595
|
-
//#endregion
|
|
1596
|
-
//#region packages/core/src/blocks.ts
|
|
1597
|
-
const FENCE = /^\s{0,3}(`{3,}|~{3,})/;
|
|
1598
|
-
function blocks(source) {
|
|
1599
|
-
const lines = source.split("\n"), out = [];
|
|
1600
|
-
let i = 0;
|
|
1601
|
-
if (lines[0]?.trimEnd() === "---") {
|
|
1602
|
-
const close = lines.findIndex((l, k) => k > 0 && l.trimEnd() === "---");
|
|
1603
|
-
if (close > 0) i = close + 1;
|
|
1604
|
-
}
|
|
1605
|
-
while (i < lines.length) {
|
|
1606
|
-
if (!lines[i].trim()) {
|
|
1607
|
-
i++;
|
|
1608
|
-
continue;
|
|
1609
|
-
}
|
|
1610
|
-
const from = i;
|
|
1611
|
-
let fence = null, maths = false;
|
|
1612
|
-
for (; i < lines.length; i++) {
|
|
1613
|
-
const line = lines[i], t = line.trim();
|
|
1614
|
-
if (fence) {
|
|
1615
|
-
if (t.startsWith(fence) && !t.slice(fence.length).trim().replaceAll(fence[0], "")) fence = null;
|
|
1616
|
-
continue;
|
|
1617
|
-
}
|
|
1618
|
-
if (maths) {
|
|
1619
|
-
if (t.endsWith("$$")) maths = false;
|
|
1620
|
-
continue;
|
|
1621
|
-
}
|
|
1622
|
-
if (!t) break;
|
|
1623
|
-
const f = FENCE.exec(line);
|
|
1624
|
-
if (f) fence = f[1];
|
|
1625
|
-
else if (t.startsWith("$$") && !(t.length > 2 && t.endsWith("$$"))) maths = true;
|
|
1626
|
-
}
|
|
1627
|
-
out.push({
|
|
1628
|
-
from,
|
|
1629
|
-
to: i,
|
|
1630
|
-
text: lines.slice(from, i).join("\n")
|
|
1631
|
-
});
|
|
1632
|
-
}
|
|
1633
|
-
return out;
|
|
1634
|
-
}
|
|
1635
|
-
/** The anchor of the block at `index`. */
|
|
1636
|
-
function anchorOf(all, index) {
|
|
1637
|
-
const text = all[index].text;
|
|
1638
|
-
let nth = 0;
|
|
1639
|
-
for (let k = 0; k < index; k++) if (all[k].text === text) nth++;
|
|
1640
|
-
return {
|
|
1641
|
-
text,
|
|
1642
|
-
nth
|
|
1643
|
-
};
|
|
1644
|
-
}
|
|
1645
|
-
/** The index of the block an anchor names, or -1. A block that is now the only one with its text is found whatever its `nth`. */
|
|
1646
|
-
function findAnchor(all, anchor) {
|
|
1647
|
-
const same = [];
|
|
1648
|
-
all.forEach((b, k) => {
|
|
1649
|
-
if (b.text === anchor.text) same.push(k);
|
|
1650
|
-
});
|
|
1651
|
-
if (same.length === 1) return same[0];
|
|
1652
|
-
return same[anchor.nth] ?? -1;
|
|
1653
|
-
}
|
|
1654
|
-
//#endregion
|
|
1655
|
-
//#region packages/core/src/diff.ts
|
|
1656
|
-
var DiffError = class extends Error {};
|
|
1657
|
-
const TABLE_LIMIT = 4e6;
|
|
1658
|
-
function diffLines(a, b) {
|
|
1659
|
-
let start = 0;
|
|
1660
|
-
while (start < a.length && start < b.length && a[start] === b[start]) start++;
|
|
1661
|
-
let endA = a.length, endB = b.length;
|
|
1662
|
-
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
|
|
1663
|
-
endA--;
|
|
1664
|
-
endB--;
|
|
1665
|
-
}
|
|
1666
|
-
const n = endA - start, m = endB - start;
|
|
1667
|
-
if (!n && !m) return [];
|
|
1668
|
-
if (!n || !m || n * m > TABLE_LIMIT) return [{
|
|
1669
|
-
at: start,
|
|
1670
|
-
del: a.slice(start, endA),
|
|
1671
|
-
ins: b.slice(start, endB)
|
|
1672
|
-
}];
|
|
1673
|
-
const w = m + 1, table = new Uint32Array((n + 1) * w);
|
|
1674
|
-
for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) table[i * w + j] = a[start + i] === b[start + j] ? table[(i + 1) * w + j + 1] + 1 : Math.max(table[(i + 1) * w + j], table[i * w + j + 1]);
|
|
1675
|
-
const out = [];
|
|
1676
|
-
let open = null, i = 0, j = 0;
|
|
1677
|
-
const hunk = () => open ??= (out.push({
|
|
1678
|
-
at: start + i,
|
|
1679
|
-
del: [],
|
|
1680
|
-
ins: []
|
|
1681
|
-
}), out[out.length - 1]);
|
|
1682
|
-
while (i < n || j < m) if (i < n && j < m && a[start + i] === b[start + j]) {
|
|
1683
|
-
open = null;
|
|
1684
|
-
i++;
|
|
1685
|
-
j++;
|
|
1686
|
-
} else if (j < m && (i === n || table[i * w + j + 1] >= table[(i + 1) * w + j])) {
|
|
1687
|
-
hunk().ins.push(b[start + j]);
|
|
1688
|
-
j++;
|
|
1689
|
-
} else {
|
|
1690
|
-
hunk().del.push(a[start + i]);
|
|
1691
|
-
i++;
|
|
1692
|
-
}
|
|
1693
|
-
return out;
|
|
1694
|
-
}
|
|
1695
|
-
/** The later version, from the earlier and the hunks. Throws if the hunks were not made from `a`. */
|
|
1696
|
-
function applyHunks(a, hunks) {
|
|
1697
|
-
const out = [];
|
|
1698
|
-
let at = 0;
|
|
1699
|
-
for (const h of hunks) {
|
|
1700
|
-
if (h.at < at || h.at + h.del.length > a.length) throw new DiffError("a hunk is out of place");
|
|
1701
|
-
for (let k = 0; k < h.del.length; k++) if (a[h.at + k] !== h.del[k]) throw new DiffError("a hunk does not match the text");
|
|
1702
|
-
out.push(...a.slice(at, h.at), ...h.ins);
|
|
1703
|
-
at = h.at + h.del.length;
|
|
1704
|
-
}
|
|
1705
|
-
out.push(...a.slice(at));
|
|
1706
|
-
return out;
|
|
1707
|
-
}
|
|
1708
|
-
//#endregion
|
|
1709
|
-
//#region packages/core/src/journal.ts
|
|
1710
|
-
/** The kinds an entry may be. Format 1 also wrote `marks` (Iso's marking of an exercise): a journal moved in by
|
|
1711
|
-
* `isorite migrate` may hold them, and they are passed over. */
|
|
1712
|
-
const KINDS$1 = [
|
|
1713
|
-
"change",
|
|
1714
|
-
"proposal",
|
|
1715
|
-
"revise",
|
|
1716
|
-
"discard"
|
|
1717
|
-
];
|
|
1718
|
-
const SNAPSHOT_EVERY = 40;
|
|
1719
|
-
const SESSION_GAP_MS = 18e5;
|
|
1720
|
-
/** Your edits in a row are one moment, one layer of a note's history (M24, T), until 10 minutes pass with none. */
|
|
1721
|
-
const LAYER_IDLE_MS = 6e5;
|
|
1722
|
-
const hashText = (text) => sha256(text).slice(0, 16);
|
|
1723
|
-
/** A stamp for a new entry. `last` is the newest entry anywhere in the study base, which decides the session. */
|
|
1724
|
-
function stamp(who, device, last, now = Date.now()) {
|
|
1725
|
-
const id = newId(now);
|
|
1726
|
-
const fresh = !last || now - Date.parse(last.at) > 18e5;
|
|
1727
|
-
return {
|
|
1728
|
-
id,
|
|
1729
|
-
at: iso(now),
|
|
1730
|
-
session: fresh ? id : last.session,
|
|
1731
|
-
device,
|
|
1732
|
-
who
|
|
1733
|
-
};
|
|
1734
|
-
}
|
|
1735
|
-
/** The entries in a journal's text, each once, oldest first. Lines that do not parse are skipped. */
|
|
1736
|
-
function readJournal(text) {
|
|
1737
|
-
const byId = /* @__PURE__ */ new Map();
|
|
1738
|
-
for (const line of text.split("\n")) {
|
|
1739
|
-
if (!line.trim()) continue;
|
|
1740
|
-
let e;
|
|
1741
|
-
try {
|
|
1742
|
-
e = JSON.parse(line);
|
|
1743
|
-
} catch {
|
|
1744
|
-
continue;
|
|
1745
|
-
}
|
|
1746
|
-
if (typeof e !== "object" || e === null) continue;
|
|
1747
|
-
const entry = e;
|
|
1748
|
-
if (typeof entry.id !== "string" || !KINDS$1.includes(entry.kind) || byId.has(entry.id)) continue;
|
|
1749
|
-
byId.set(entry.id, entry);
|
|
1750
|
-
}
|
|
1751
|
-
return [...byId.keys()].sort().map((k) => byId.get(k));
|
|
1752
|
-
}
|
|
1753
|
-
/** An entry as the line appended to the journal. */
|
|
1754
|
-
const journalLine = (entry) => JSON.stringify(entry) + "\n";
|
|
1755
|
-
const isChange = (e) => e.kind === "change";
|
|
1756
|
-
/** The note as the journal has it after the entry `upTo` (or after the last), and the changes that could not be
|
|
1757
|
-
* replayed because the chain before them is missing. Null when the journal has no change yet. */
|
|
1758
|
-
function replay(entries, upTo) {
|
|
1759
|
-
let text = null;
|
|
1760
|
-
const broken = [];
|
|
1761
|
-
for (const e of entries) {
|
|
1762
|
-
if (upTo !== void 0 && e.id > upTo) break;
|
|
1763
|
-
if (!isChange(e)) continue;
|
|
1764
|
-
if (e.text !== void 0) text = e.text;
|
|
1765
|
-
else if (text !== null && hashText(text) === e.before) try {
|
|
1766
|
-
text = applyHunks(text.split("\n"), e.hunks).join("\n");
|
|
1767
|
-
} catch {
|
|
1768
|
-
broken.push(e.id);
|
|
1769
|
-
}
|
|
1770
|
-
else broken.push(e.id);
|
|
1771
|
-
}
|
|
1772
|
-
return text === null ? null : {
|
|
1773
|
-
text,
|
|
1774
|
-
hash: hashText(text),
|
|
1775
|
-
broken
|
|
1776
|
-
};
|
|
1777
|
-
}
|
|
1778
|
-
/** Who last wrote each line of the note as the journal last has it: `you`, `iso:…`, `agent:…` or `external`. Text
|
|
1779
|
-
* kept from a proposal is its proposer's. Null when the journal's chain is broken, as nothing can then be said. */
|
|
1780
|
-
function authors(entries) {
|
|
1781
|
-
const proposers = /* @__PURE__ */ new Map();
|
|
1782
|
-
let owners = null, text = null;
|
|
1783
|
-
for (const e of entries) {
|
|
1784
|
-
if (e.kind === "proposal") proposers.set(e.id, e.who);
|
|
1785
|
-
if (!isChange(e)) continue;
|
|
1786
|
-
const who = e.why === "keep" && e.proposal ? proposers.get(e.proposal) ?? e.who : e.who;
|
|
1787
|
-
if (owners === null || text === null) {
|
|
1788
|
-
if (e.text === void 0) return null;
|
|
1789
|
-
text = e.text.split("\n");
|
|
1790
|
-
owners = text.map(() => who);
|
|
1791
|
-
continue;
|
|
1792
|
-
}
|
|
1793
|
-
if (hashText(text.join("\n")) !== e.before) return null;
|
|
1794
|
-
const next = [];
|
|
1795
|
-
let at = 0;
|
|
1796
|
-
for (const h of e.hunks) {
|
|
1797
|
-
next.push(...owners.slice(at, h.at), ...h.ins.map(() => who));
|
|
1798
|
-
at = h.at + h.del.length;
|
|
1799
|
-
}
|
|
1800
|
-
next.push(...owners.slice(at));
|
|
1801
|
-
owners = next;
|
|
1802
|
-
text = applyHunks(text, e.hunks);
|
|
1803
|
-
}
|
|
1804
|
-
return owners;
|
|
1805
|
-
}
|
|
1806
|
-
/** A line's words, spaces and marks, in order: what is compared to tell who wrote what within a line. */
|
|
1807
|
-
const tokens = (line) => line.match(/\s+|[\p{L}\p{N}_]+|[^\s\p{L}\p{N}_]/gu) ?? [];
|
|
1808
|
-
/** Who wrote each word of the note as the journal last has it, line by line, as runs. Where a change replaced
|
|
1809
|
-
* lines, the old and the new are compared word by word, so a word kept keeps its writer and only the words put in
|
|
1810
|
-
* are the changer's. Text kept from a proposal is its proposer's, as in `authors`. Null when the chain is broken. */
|
|
1811
|
-
function wordAuthors(entries) {
|
|
1812
|
-
const proposers = /* @__PURE__ */ new Map();
|
|
1813
|
-
let lines = null, text = null;
|
|
1814
|
-
const line = (l, who) => {
|
|
1815
|
-
const t = tokens(l);
|
|
1816
|
-
return {
|
|
1817
|
-
t,
|
|
1818
|
-
w: t.map(() => who)
|
|
1819
|
-
};
|
|
1820
|
-
};
|
|
1821
|
-
for (const e of entries) {
|
|
1822
|
-
if (e.kind === "proposal") proposers.set(e.id, e.who);
|
|
1823
|
-
if (!isChange(e)) continue;
|
|
1824
|
-
const who = e.why === "keep" && e.proposal ? proposers.get(e.proposal) ?? e.who : e.who;
|
|
1825
|
-
if (lines === null || text === null) {
|
|
1826
|
-
if (e.text === void 0) return null;
|
|
1827
|
-
text = e.text.split("\n");
|
|
1828
|
-
lines = text.map((l) => line(l, who));
|
|
1829
|
-
continue;
|
|
1830
|
-
}
|
|
1831
|
-
if (hashText(text.join("\n")) !== e.before) return null;
|
|
1832
|
-
const next = [];
|
|
1833
|
-
let at = 0;
|
|
1834
|
-
for (const h of e.hunks) {
|
|
1835
|
-
next.push(...lines.slice(at, h.at));
|
|
1836
|
-
const old = lines.slice(h.at, h.at + h.del.length);
|
|
1837
|
-
const a = [], aw = [];
|
|
1838
|
-
old.forEach((l, k) => {
|
|
1839
|
-
if (k) {
|
|
1840
|
-
a.push("\n");
|
|
1841
|
-
aw.push("");
|
|
1842
|
-
}
|
|
1843
|
-
a.push(...l.t);
|
|
1844
|
-
aw.push(...l.w);
|
|
1845
|
-
});
|
|
1846
|
-
const b = [];
|
|
1847
|
-
h.ins.forEach((l, k) => {
|
|
1848
|
-
if (k) b.push("\n");
|
|
1849
|
-
b.push(...tokens(l));
|
|
1850
|
-
});
|
|
1851
|
-
const bw = b.map(() => who);
|
|
1852
|
-
const kept = [];
|
|
1853
|
-
let i = 0, j = 0;
|
|
1854
|
-
for (const d of diffLines(a, b)) {
|
|
1855
|
-
while (i < d.at) {
|
|
1856
|
-
kept.push([j, i]);
|
|
1857
|
-
i++;
|
|
1858
|
-
j++;
|
|
1859
|
-
}
|
|
1860
|
-
i += d.del.length;
|
|
1861
|
-
j += d.ins.length;
|
|
1862
|
-
}
|
|
1863
|
-
while (i < a.length && j < b.length) {
|
|
1864
|
-
kept.push([j, i]);
|
|
1865
|
-
i++;
|
|
1866
|
-
j++;
|
|
1867
|
-
}
|
|
1868
|
-
const words = (ts) => ts.filter((t) => /[\p{L}\p{N}]/u.test(t)).length;
|
|
1869
|
-
if (kept.filter(([jj]) => /[\p{L}\p{N}]/u.test(b[jj])).length * 2 >= words(b)) for (const [jj, ii] of kept) bw[jj] = aw[ii];
|
|
1870
|
-
let cur = {
|
|
1871
|
-
t: [],
|
|
1872
|
-
w: []
|
|
1873
|
-
};
|
|
1874
|
-
const made = [];
|
|
1875
|
-
b.forEach((tok, k) => {
|
|
1876
|
-
if (tok === "\n") {
|
|
1877
|
-
made.push(cur);
|
|
1878
|
-
cur = {
|
|
1879
|
-
t: [],
|
|
1880
|
-
w: []
|
|
1881
|
-
};
|
|
1882
|
-
} else {
|
|
1883
|
-
cur.t.push(tok);
|
|
1884
|
-
cur.w.push(bw[k]);
|
|
1885
|
-
}
|
|
1886
|
-
});
|
|
1887
|
-
if (h.ins.length) made.push(cur);
|
|
1888
|
-
next.push(...made);
|
|
1889
|
-
at = h.at + h.del.length;
|
|
1890
|
-
}
|
|
1891
|
-
next.push(...lines.slice(at));
|
|
1892
|
-
lines = next;
|
|
1893
|
-
text = applyHunks(text, e.hunks);
|
|
1894
|
-
}
|
|
1895
|
-
return (lines ?? []).map((l) => {
|
|
1896
|
-
const runs = [];
|
|
1897
|
-
let pos = 0;
|
|
1898
|
-
l.t.forEach((tok, k) => {
|
|
1899
|
-
const who = l.w[k], last = runs.at(-1);
|
|
1900
|
-
if (last && last.who === who && last.to === pos) last.to += tok.length;
|
|
1901
|
-
else runs.push({
|
|
1902
|
-
from: pos,
|
|
1903
|
-
to: pos + tok.length,
|
|
1904
|
-
who
|
|
1905
|
-
});
|
|
1906
|
-
pos += tok.length;
|
|
1907
|
-
});
|
|
1908
|
-
return runs;
|
|
1909
|
-
});
|
|
1910
|
-
}
|
|
1911
|
-
/** The note as it was after an entry. */
|
|
1912
|
-
const textAt = (entries, id) => replay(entries, id)?.text ?? null;
|
|
1913
|
-
/** The entry for a change of the note's file to `after`, or null when nothing changed. The difference is taken
|
|
1914
|
-
* from the journal's own last version, so the chain holds whatever the caller thought the file was. */
|
|
1915
|
-
function change(entries, after, s, why, extra = {}) {
|
|
1916
|
-
const head = replay(entries);
|
|
1917
|
-
if (head && head.text === after) return null;
|
|
1918
|
-
const changes = entries.filter(isChange);
|
|
1919
|
-
let sinceText = 0;
|
|
1920
|
-
for (let k = changes.length - 1; k >= 0 && changes[k].text === void 0; k--) sinceText++;
|
|
1921
|
-
const whole = !head || head.broken.length > 0 || sinceText + 1 >= 40;
|
|
1922
|
-
return {
|
|
1923
|
-
...s,
|
|
1924
|
-
kind: "change",
|
|
1925
|
-
why: head ? why : "start",
|
|
1926
|
-
before: head ? head.hash : null,
|
|
1927
|
-
after: hashText(after),
|
|
1928
|
-
hunks: head ? diffLines(head.text.split("\n"), after.split("\n")) : [],
|
|
1929
|
-
...whole ? { text: after } : {},
|
|
1930
|
-
...extra
|
|
1931
|
-
};
|
|
1932
|
-
}
|
|
1933
|
-
/** If the file is not what the journal last saw (it was edited elsewhere, or the note is new to the journal),
|
|
1934
|
-
* the entry that records it. Call before anything else is entered. */
|
|
1935
|
-
function reconcile(entries, file, s) {
|
|
1936
|
-
return change(entries, file, {
|
|
1937
|
-
...s,
|
|
1938
|
-
who: "external"
|
|
1939
|
-
}, "external");
|
|
1940
|
-
}
|
|
1941
|
-
/** The proposals neither kept nor discarded, as last revised, oldest first. A rewrite waits while any of its
|
|
1942
|
-
* regions does, and says which (`open`). */
|
|
1943
|
-
function pending(entries) {
|
|
1944
|
-
const open = /* @__PURE__ */ new Map();
|
|
1945
|
-
const settle = (id, region) => {
|
|
1946
|
-
const p = open.get(id);
|
|
1947
|
-
if (!p) return;
|
|
1948
|
-
const left = p.form === "rewrite" && region !== void 0 ? (p.open ?? []).filter((k) => k !== region) : [];
|
|
1949
|
-
if (p.form === "rewrite" && left.length) open.set(id, {
|
|
1950
|
-
...p,
|
|
1951
|
-
open: left
|
|
1952
|
-
});
|
|
1953
|
-
else open.delete(id);
|
|
1954
|
-
};
|
|
1955
|
-
for (const e of entries) if (e.kind === "proposal") open.set(e.id, e.form === "rewrite" ? {
|
|
1956
|
-
...e,
|
|
1957
|
-
open: e.regions.map((_, k) => k)
|
|
1958
|
-
} : e);
|
|
1959
|
-
else if (e.kind === "discard") settle(e.proposal, e.region);
|
|
1960
|
-
else if (e.kind === "change" && e.proposal && e.why === "keep") settle(e.proposal, e.region);
|
|
1961
|
-
else if (e.kind === "revise") {
|
|
1962
|
-
const p = open.get(e.proposal);
|
|
1963
|
-
if (!p) continue;
|
|
1964
|
-
if (p.form === "insert" && e.text !== void 0) open.set(p.id, {
|
|
1965
|
-
...p,
|
|
1966
|
-
text: e.text
|
|
1967
|
-
});
|
|
1968
|
-
if (p.form === "rewrite" && e.regions) open.set(p.id, {
|
|
1969
|
-
...p,
|
|
1970
|
-
regions: p.regions.map((r, k) => ({
|
|
1971
|
-
old: r.old,
|
|
1972
|
-
new: e.regions[k] ?? r.new
|
|
1973
|
-
}))
|
|
1974
|
-
});
|
|
1975
|
-
}
|
|
1976
|
-
return [...open.values()];
|
|
1977
|
-
}
|
|
1978
|
-
/** Where a passage is in the note now, as character offsets, or null when its text is no longer there. */
|
|
1979
|
-
function findPassage(note, p) {
|
|
1980
|
-
if (!p.text) return null;
|
|
1981
|
-
let at = -1;
|
|
1982
|
-
for (let n = 0; n <= p.nth; n++) {
|
|
1983
|
-
at = note.indexOf(p.text, at + 1);
|
|
1984
|
-
if (at < 0) break;
|
|
1985
|
-
}
|
|
1986
|
-
if (at < 0) {
|
|
1987
|
-
const first = note.indexOf(p.text);
|
|
1988
|
-
if (first < 0 || note.indexOf(p.text, first + 1) >= 0) return null;
|
|
1989
|
-
at = first;
|
|
1990
|
-
}
|
|
1991
|
-
return {
|
|
1992
|
-
from: at,
|
|
1993
|
-
to: at + p.text.length
|
|
1994
|
-
};
|
|
1995
|
-
}
|
|
1996
|
-
/** The passage between two character offsets of the note, as it is named in the journal. */
|
|
1997
|
-
function passageOf(note, from, to) {
|
|
1998
|
-
const text = note.slice(from, to);
|
|
1999
|
-
let nth = 0;
|
|
2000
|
-
for (let at = note.indexOf(text); at >= 0 && at < from; at = note.indexOf(text, at + 1)) nth++;
|
|
2001
|
-
return {
|
|
2002
|
-
text,
|
|
2003
|
-
nth
|
|
2004
|
-
};
|
|
2005
|
-
}
|
|
2006
|
-
/** Null when the place of new text cannot be found: the proposal is then shown apart from the text, and is not lost. */
|
|
2007
|
-
function locate(note, p) {
|
|
2008
|
-
if (p.form === "rewrite") return {
|
|
2009
|
-
form: "rewrite",
|
|
2010
|
-
regions: p.regions.map((r) => findPassage(note, r.old))
|
|
2011
|
-
};
|
|
2012
|
-
const all = blocks(note);
|
|
2013
|
-
const a = p.after ? findAnchor(all, p.after) : -1, b = p.before ? findAnchor(all, p.before) : -1;
|
|
2014
|
-
if (a >= 0) return {
|
|
2015
|
-
form: "insert",
|
|
2016
|
-
line: all[a].to
|
|
2017
|
-
};
|
|
2018
|
-
if (b >= 0) return {
|
|
2019
|
-
form: "insert",
|
|
2020
|
-
line: all[b].from
|
|
2021
|
-
};
|
|
2022
|
-
if (!p.after && !p.before) {
|
|
2023
|
-
if (all.length) return {
|
|
2024
|
-
form: "insert",
|
|
2025
|
-
line: all[all.length - 1].to
|
|
2026
|
-
};
|
|
2027
|
-
const lines = note.split("\n");
|
|
2028
|
-
let end = lines.length;
|
|
2029
|
-
while (end > 0 && !lines[end - 1].trim()) end--;
|
|
2030
|
-
return {
|
|
2031
|
-
form: "insert",
|
|
2032
|
-
line: end
|
|
2033
|
-
};
|
|
2034
|
-
}
|
|
2035
|
-
if (!p.after && all.length) return {
|
|
2036
|
-
form: "insert",
|
|
2037
|
-
line: all[0].from
|
|
2038
|
-
};
|
|
2039
|
-
return null;
|
|
2040
|
-
}
|
|
2041
|
-
/** The note with a proposal written in. For a rewrite, `region` writes that one alone; without it, every region
|
|
2042
|
-
* still open. Null when the place (or, for a rewrite, any of the places asked for) cannot be found. */
|
|
2043
|
-
function written(note, p, region) {
|
|
2044
|
-
if (p.form === "rewrite") {
|
|
2045
|
-
const places = (region !== void 0 ? [region] : p.open ?? p.regions.map((_, k) => k)).map((k) => ({
|
|
2046
|
-
k,
|
|
2047
|
-
at: p.regions[k] ? findPassage(note, p.regions[k].old) : null
|
|
2048
|
-
}));
|
|
2049
|
-
if (!places.length || places.some((x) => !x.at)) return null;
|
|
2050
|
-
let out = note;
|
|
2051
|
-
for (const { k, at } of places.sort((x, y) => y.at.from - x.at.from)) out = out.slice(0, at.from) + p.regions[k].new + out.slice(at.to);
|
|
2052
|
-
return out;
|
|
2053
|
-
}
|
|
2054
|
-
const place = locate(note, p);
|
|
2055
|
-
if (!place || place.form !== "insert") return null;
|
|
2056
|
-
const lines = note.split("\n");
|
|
2057
|
-
const blank = (k) => k < 0 || k >= lines.length || !lines[k].trim();
|
|
2058
|
-
const ins = p.text.replace(/^\n+|\n+$/g, "").split("\n");
|
|
2059
|
-
const at = place.line;
|
|
2060
|
-
lines.splice(at, 0, ...blank(at - 1) ? [] : [""], ...ins, ...blank(at) ? [] : [""]);
|
|
2061
|
-
return lines.join("\n");
|
|
2062
|
-
}
|
|
2063
|
-
/** A proposal to put `text` at a line of the note as it stands. */
|
|
2064
|
-
function insertAt(note, line) {
|
|
2065
|
-
const all = blocks(note);
|
|
2066
|
-
let prev = -1;
|
|
2067
|
-
for (let k = 0; k < all.length; k++) if (all[k].from < line) prev = k;
|
|
2068
|
-
return {
|
|
2069
|
-
form: "insert",
|
|
2070
|
-
after: prev >= 0 ? anchorOf(all, prev) : null,
|
|
2071
|
-
before: prev + 1 < all.length ? anchorOf(all, prev + 1) : null
|
|
2072
|
-
};
|
|
2073
|
-
}
|
|
2074
|
-
/** The journal as the timeline shows it. Your own edits in a row within a session are one moment, until ten minutes
|
|
2075
|
-
* pass with none (LAYER_IDLE_MS); a proposal is one moment whose `what` says how it ended. Revisions are not
|
|
2076
|
-
* moments. */
|
|
2077
|
-
function moments(entries) {
|
|
2078
|
-
const out = [], offered = /* @__PURE__ */ new Map(), asked = /* @__PURE__ */ new Map();
|
|
2079
|
-
/** Lines a change put in and took out: a note's first entry, all of its text. */
|
|
2080
|
-
const size = (c) => c.why === "start" || !c.hunks.length && c.before === null ? [(c.text ?? "").split("\n").length, 0] : [c.hunks.reduce((n, h) => n + h.ins.length, 0), c.hunks.reduce((n, h) => n + h.del.length, 0)];
|
|
2081
|
-
for (const e of entries) {
|
|
2082
|
-
const base = {
|
|
2083
|
-
ids: [e.id],
|
|
2084
|
-
at: e.at,
|
|
2085
|
-
session: e.session,
|
|
2086
|
-
who: e.who,
|
|
2087
|
-
added: 0,
|
|
2088
|
-
removed: 0
|
|
2089
|
-
};
|
|
2090
|
-
if (e.kind === "proposal") {
|
|
2091
|
-
const m = {
|
|
2092
|
-
...base,
|
|
2093
|
-
what: "offered",
|
|
2094
|
-
asked: e.asked
|
|
2095
|
-
};
|
|
2096
|
-
offered.set(e.id, m);
|
|
2097
|
-
asked.set(e.id, e.asked);
|
|
2098
|
-
out.push(m);
|
|
2099
|
-
} else if (e.kind === "discard") {
|
|
2100
|
-
const m = offered.get(e.proposal);
|
|
2101
|
-
if (m) {
|
|
2102
|
-
m.what = "discarded";
|
|
2103
|
-
m.ids.push(e.id);
|
|
2104
|
-
}
|
|
2105
|
-
} else if (e.kind === "change") {
|
|
2106
|
-
const m = e.proposal ? offered.get(e.proposal) : void 0;
|
|
2107
|
-
const [added, removed] = size(e);
|
|
2108
|
-
if (e.why === "keep" && m) {
|
|
2109
|
-
m.what = "kept";
|
|
2110
|
-
m.ids.push(e.id);
|
|
2111
|
-
m.at = e.at;
|
|
2112
|
-
m.added += added;
|
|
2113
|
-
m.removed += removed;
|
|
2114
|
-
continue;
|
|
2115
|
-
}
|
|
2116
|
-
const last = out[out.length - 1];
|
|
2117
|
-
if (e.why === "edit" && last && last.what === "edited" && last.who === e.who && last.session === e.session && Date.parse(e.at) - Date.parse(last.at) <= 6e5) {
|
|
2118
|
-
last.ids.push(e.id);
|
|
2119
|
-
last.at = e.at;
|
|
2120
|
-
last.added += added;
|
|
2121
|
-
last.removed += removed;
|
|
2122
|
-
continue;
|
|
2123
|
-
}
|
|
2124
|
-
const what = e.why === "start" ? e.who === "external" ? "found" : "wrote" : e.why === "edit" ? "edited" : e.why === "restore" ? "restored" : e.why === "keep" ? "kept" : e.why === "move" ? "moved" : "changed elsewhere";
|
|
2125
|
-
out.push({
|
|
2126
|
-
...base,
|
|
2127
|
-
what,
|
|
2128
|
-
added,
|
|
2129
|
-
removed,
|
|
2130
|
-
...e.asked ? { asked: e.asked } : {}
|
|
2131
|
-
});
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
2134
|
-
return out;
|
|
2135
|
-
}
|
|
2136
|
-
/** Where a line of the version before a change is in the version after it, or null if the change took it out. */
|
|
2137
|
-
function carried(hunks, line) {
|
|
2138
|
-
let shift = 0;
|
|
2139
|
-
for (const h of hunks) {
|
|
2140
|
-
if (line < h.at) break;
|
|
2141
|
-
if (line < h.at + h.del.length) return null;
|
|
2142
|
-
shift += h.ins.length - h.del.length;
|
|
2143
|
-
}
|
|
2144
|
-
return line + shift;
|
|
2145
|
-
}
|
|
2146
|
-
/** The lines of the version after a change that the change put there. */
|
|
2147
|
-
function putIn(c) {
|
|
2148
|
-
if (c.why === "start" || !c.hunks.length && c.before === null) return (c.text ?? "").split("\n").map((_, k) => k);
|
|
2149
|
-
const out = [];
|
|
2150
|
-
let shift = 0;
|
|
2151
|
-
for (const h of c.hunks) {
|
|
2152
|
-
for (let k = 0; k < h.ins.length; k++) out.push(h.at + shift + k);
|
|
2153
|
-
shift += h.ins.length - h.del.length;
|
|
2154
|
-
}
|
|
2155
|
-
return out;
|
|
2156
|
-
}
|
|
2157
|
-
/** The lines of the note as the journal has it now that a moment put there and that are still there, as runs of
|
|
2158
|
-
* lines (from 0, through the whole file, `to` exclusive). `removed` when it put lines in and none are left. A
|
|
2159
|
-
* moment that changed nothing in the file (Iso's text not kept, a marking) touches nothing. */
|
|
2160
|
-
function touched(entries, ids) {
|
|
2161
|
-
const mine = new Set(ids);
|
|
2162
|
-
let lines = /* @__PURE__ */ new Set(), put = false;
|
|
2163
|
-
for (const e of entries) {
|
|
2164
|
-
if (!isChange(e)) continue;
|
|
2165
|
-
const next = /* @__PURE__ */ new Set();
|
|
2166
|
-
for (const l of lines) {
|
|
2167
|
-
const at = carried(e.hunks, l);
|
|
2168
|
-
if (at !== null) next.add(at);
|
|
2169
|
-
}
|
|
2170
|
-
lines = next;
|
|
2171
|
-
if (mine.has(e.id)) for (const l of putIn(e)) {
|
|
2172
|
-
lines.add(l);
|
|
2173
|
-
put = true;
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
const sorted = [...lines].sort((a, b) => a - b), runs = [];
|
|
2177
|
-
for (const l of sorted) {
|
|
2178
|
-
const last = runs[runs.length - 1];
|
|
2179
|
-
if (last && last.to === l) last.to = l + 1;
|
|
2180
|
-
else runs.push({
|
|
2181
|
-
from: l,
|
|
2182
|
-
to: l + 1
|
|
2183
|
-
});
|
|
2184
|
-
}
|
|
2185
|
-
return {
|
|
2186
|
-
runs,
|
|
2187
|
-
removed: put && !runs.length
|
|
2188
|
-
};
|
|
2189
|
-
}
|
|
2190
|
-
/** The moments of one session across notes, in the order they began. `journals` holds each note's entries of that
|
|
2191
|
-
* session only. */
|
|
2192
|
-
function sessionMoments(journals) {
|
|
2193
|
-
const out = [];
|
|
2194
|
-
for (const [note, entries] of journals) for (const m of moments(entries)) out.push({
|
|
2195
|
-
...m,
|
|
2196
|
-
note
|
|
2197
|
-
});
|
|
2198
|
-
return out.sort((a, b) => a.ids[0] < b.ids[0] ? -1 : 1);
|
|
2199
|
-
}
|
|
2200
|
-
//#endregion
|
|
2201
|
-
//#region packages/core/src/own.ts
|
|
2202
|
-
const OWN_DIR = ".isorite";
|
|
2203
|
-
const OWN_FORMAT = 2;
|
|
2204
|
-
const GITATTRIBUTES = "*.jsonl merge=union\n";
|
|
2205
|
-
const SETTINGS_FILE = "settings.toml";
|
|
2206
|
-
function parseSettings(text) {
|
|
2207
|
-
if (!text) return {};
|
|
2208
|
-
let raw;
|
|
2209
|
-
try {
|
|
2210
|
-
raw = parse(text);
|
|
2211
|
-
} catch {
|
|
2212
|
-
return {};
|
|
2213
|
-
}
|
|
2214
|
-
return typeof raw.repository === "string" && raw.repository.trim() ? { repository: raw.repository.trim() } : {};
|
|
2215
|
-
}
|
|
2216
|
-
function settingsText(s) {
|
|
2217
|
-
return `# isorite's settings for this base (format 2)\n${stringify$1(s)}\n`;
|
|
2218
|
-
}
|
|
2219
|
-
const SAFE = /^[^\0]+$/;
|
|
2220
|
-
function checkId(id) {
|
|
2221
|
-
if (!SAFE.test(id) || id.startsWith("/") || id.split("/").some((part) => part === ".." || part === "." || !part)) throw new Error(`not a note's id: ${JSON.stringify(id)}`);
|
|
2222
|
-
return id;
|
|
2223
|
-
}
|
|
2224
|
-
/** Where a note's journal is. Drafts are journalled under their `@draft/` ids (and Understanding notes were, before
|
|
2225
|
-
* M24, under `@understanding/`). */
|
|
2226
|
-
const journalPath = (noteId) => `journal/${checkId(noteId)}.jsonl`;
|
|
2227
|
-
/** This device's record of usage: what was cleared from Iso's context, and what each turn cost (record.ts). */
|
|
2228
|
-
const recordPath = (device) => `record/${checkId(device)}.jsonl`;
|
|
2229
|
-
//#endregion
|
|
2230
|
-
//#region packages/core/src/study.ts
|
|
2231
|
-
const STUDY_FORMAT = 1;
|
|
2232
|
-
const CONFIG_FILE = "isorite.toml";
|
|
2233
|
-
const STUDY_DIR = "study";
|
|
2234
|
-
var StudyError = class extends Error {};
|
|
2235
|
-
function parseStudyConfig(source) {
|
|
2236
|
-
let raw;
|
|
2237
|
-
try {
|
|
2238
|
-
raw = parse(source);
|
|
2239
|
-
} catch (e) {
|
|
2240
|
-
throw new StudyError(`${CONFIG_FILE} is not valid TOML: ${e.message}`);
|
|
2241
|
-
}
|
|
2242
|
-
const base = raw.base ?? {};
|
|
2243
|
-
const format = raw.format === void 0 ? 1 : Number(raw.format);
|
|
2244
|
-
if (format !== 1) throw new StudyError(`${CONFIG_FILE} is format ${text(raw.format)}; this isorite reads format 1`);
|
|
2245
|
-
if (typeof base.path !== "string" || !base.path) throw new StudyError(`${CONFIG_FILE} needs [base] path, the knowledge base's folder`);
|
|
2246
|
-
return {
|
|
2247
|
-
format,
|
|
2248
|
-
base: {
|
|
2249
|
-
path: base.path,
|
|
2250
|
-
remote: text(base.remote ?? ""),
|
|
2251
|
-
root: text(base.root ?? "")
|
|
2252
|
-
}
|
|
2253
|
-
};
|
|
2254
|
-
}
|
|
2255
|
-
//#endregion
|
|
2256
|
-
//#region packages/core/src/map.ts
|
|
2257
|
-
/** The Atlas's model of a knowledge base: its folders and notes, and the links between notes, each with its strength.
|
|
2258
|
-
* A question or a problem is joined to each note it is about, or uses, as a "uses" link from it (M12). */
|
|
2259
|
-
function mapModel(base) {
|
|
2260
|
-
const order = base.prerequisiteOrder();
|
|
2261
|
-
const note = (id) => {
|
|
2262
|
-
const k = base.concepts.get(id).kind;
|
|
2263
|
-
return {
|
|
2264
|
-
kind: "concept",
|
|
2265
|
-
id: "c:" + id,
|
|
2266
|
-
ref: id,
|
|
2267
|
-
weight: 1,
|
|
2268
|
-
rank: order.get(id)?.depth ?? 0,
|
|
2269
|
-
...k === "note" ? {} : { of: k }
|
|
2270
|
-
};
|
|
2271
|
-
};
|
|
2272
|
-
const edges = [];
|
|
2273
|
-
for (const c of base.concepts.values()) {
|
|
2274
|
-
const strongest = /* @__PURE__ */ new Map();
|
|
2275
|
-
for (const l of c.links) {
|
|
2276
|
-
if (l.broken || l.kind !== "concept" || l.target === c.id || !base.concepts.has(l.target)) continue;
|
|
2277
|
-
strongest.set(l.target, Math.max(strongest.get(l.target) ?? 0, l.rel && STRENGTH[l.rel] || STRENGTH.uses));
|
|
2278
|
-
}
|
|
2279
|
-
for (const to of c.attached) if (!strongest.has(to)) strongest.set(to, STRENGTH.uses);
|
|
2280
|
-
for (const [to, s] of strongest) edges.push([
|
|
2281
|
-
c.id,
|
|
2282
|
-
to,
|
|
2283
|
-
s
|
|
2284
|
-
]);
|
|
2285
|
-
}
|
|
2286
|
-
const holdsFiles = /* @__PURE__ */ new Set();
|
|
2287
|
-
const parentOf = (p) => p.includes("/") ? p.slice(0, p.lastIndexOf("/")) : "";
|
|
2288
|
-
for (const f of base.files) holdsFiles.add(parentOf(f));
|
|
2289
|
-
const pictureFolder = (d) => {
|
|
2290
|
-
const dd = base.directories.get(d);
|
|
2291
|
-
const here = [...base.files].filter((f) => parentOf(f) === d);
|
|
2292
|
-
return !!d && (!dd || !dd.concepts.length && !dd.children.length) && here.length > 0 && here.every((f) => citeKind(f) === "image");
|
|
2293
|
-
};
|
|
2294
|
-
const refOf = (f) => citeKind(f) === "image" && pictureFolder(parentOf(f)) ? parentOf(f) + "/" : f;
|
|
2295
|
-
const fileEdges = /* @__PURE__ */ new Map();
|
|
2296
|
-
for (const c of base.concepts.values()) for (const x of c.cites) {
|
|
2297
|
-
if (x.broken || !base.files.has(x.target)) continue;
|
|
2298
|
-
const ref = refOf(x.target);
|
|
2299
|
-
if (!fileEdges.has(c.id)) fileEdges.set(c.id, /* @__PURE__ */ new Set());
|
|
2300
|
-
fileEdges.get(c.id).add(ref);
|
|
2301
|
-
}
|
|
2302
|
-
const rankOf = /* @__PURE__ */ new Map();
|
|
2303
|
-
for (const [from, refs] of fileEdges) for (const ref of refs) {
|
|
2304
|
-
edges.push([
|
|
2305
|
-
from,
|
|
2306
|
-
ref,
|
|
2307
|
-
STRENGTH.uses
|
|
2308
|
-
]);
|
|
2309
|
-
rankOf.set(ref, Math.max(rankOf.get(ref) ?? 0, order.get(from)?.depth ?? 0));
|
|
2310
|
-
}
|
|
2311
|
-
const filesIn = /* @__PURE__ */ new Map();
|
|
2312
|
-
for (const f of [...base.files].sort()) {
|
|
2313
|
-
const of = citeKind(f), ref = of ? refOf(f) : null;
|
|
2314
|
-
if (!of || !ref) continue;
|
|
2315
|
-
const at = ref.endsWith("/") ? parentOf(ref.slice(0, -1)) : parentOf(f);
|
|
2316
|
-
const list = filesIn.get(at) ?? [];
|
|
2317
|
-
if (!list.some((n) => n.ref === ref)) list.push({
|
|
2318
|
-
kind: "concept",
|
|
2319
|
-
id: "f:" + ref,
|
|
2320
|
-
ref,
|
|
2321
|
-
weight: 1,
|
|
2322
|
-
rank: rankOf.get(ref) ?? 0,
|
|
2323
|
-
of
|
|
2324
|
-
});
|
|
2325
|
-
filesIn.set(at, list);
|
|
2326
|
-
}
|
|
2327
|
-
const dir = (id) => {
|
|
2328
|
-
const d = base.directories.get(id);
|
|
2329
|
-
if (id && pictureFolder(id)) return null;
|
|
2330
|
-
const children = [
|
|
2331
|
-
...[...d.children].sort().map(dir).filter((c) => c !== null),
|
|
2332
|
-
...[...d.concepts].sort().map(note),
|
|
2333
|
-
...filesIn.get(id) ?? []
|
|
2334
|
-
];
|
|
2335
|
-
return id && !children.length && (holdsFiles.has(id) || d.children.length) ? null : {
|
|
2336
|
-
kind: "dir",
|
|
2337
|
-
id: "d:" + id,
|
|
2338
|
-
ref: id,
|
|
2339
|
-
children
|
|
2340
|
-
};
|
|
2341
|
-
};
|
|
2342
|
-
return {
|
|
2343
|
-
root: dir(""),
|
|
2344
|
-
edges
|
|
2345
|
-
};
|
|
2346
|
-
}
|
|
2347
|
-
/** Whether a map item is a file (M23), not a note: its ref has an extension, or is a folder of pictures. */
|
|
2348
|
-
const isFileRef = (ref) => ref.endsWith("/") || citeKind(ref) !== null;
|
|
2349
|
-
/** What lights when a note is chosen on the Atlas (refs, as the map model names notes). Where the base rates its
|
|
2350
|
-
* links, only "requires" is followed back; where it rates none, every drawn link is. */
|
|
2351
|
-
function reach(model, note) {
|
|
2352
|
-
const least = model.edges.some(([, , s]) => s >= STRENGTH.requires) ? STRENGTH.requires : STRENGTH.uses;
|
|
2353
|
-
const needs = /* @__PURE__ */ new Map(), neededBy = /* @__PURE__ */ new Map();
|
|
2354
|
-
const uses = /* @__PURE__ */ new Set(), also = /* @__PURE__ */ new Set(), weak = [];
|
|
2355
|
-
for (const [from, to, s] of model.edges) {
|
|
2356
|
-
if (from === to) continue;
|
|
2357
|
-
if (s < least) {
|
|
2358
|
-
if (from !== note && to !== note) continue;
|
|
2359
|
-
(s >= STRENGTH.uses ? uses : also).add(from === note ? to : from);
|
|
2360
|
-
if (s >= STRENGTH.uses) weak.push({
|
|
2361
|
-
from,
|
|
2362
|
-
to,
|
|
2363
|
-
kind: "uses"
|
|
2364
|
-
});
|
|
2365
|
-
continue;
|
|
2366
|
-
}
|
|
2367
|
-
needs.set(from, [...needs.get(from) ?? [], to]);
|
|
2368
|
-
neededBy.set(to, [...neededBy.get(to) ?? [], from]);
|
|
2369
|
-
}
|
|
2370
|
-
const depth = {}, via = {};
|
|
2371
|
-
const seen = /* @__PURE__ */ new Set([note]), requires = [], links = [], todo = [note];
|
|
2372
|
-
for (let k = 0; k < todo.length; k++) {
|
|
2373
|
-
const from = todo[k];
|
|
2374
|
-
for (const to of needs.get(from) ?? []) {
|
|
2375
|
-
links.push({
|
|
2376
|
-
from,
|
|
2377
|
-
to,
|
|
2378
|
-
kind: "requires"
|
|
2379
|
-
});
|
|
2380
|
-
if (seen.has(to)) continue;
|
|
2381
|
-
seen.add(to);
|
|
2382
|
-
requires.push(to);
|
|
2383
|
-
todo.push(to);
|
|
2384
|
-
depth[to] = (depth[from] ?? 0) + 1;
|
|
2385
|
-
via[to] = from;
|
|
2386
|
-
}
|
|
2387
|
-
}
|
|
2388
|
-
const kept = /* @__PURE__ */ new Map();
|
|
2389
|
-
for (const l of links) kept.set(l.from, (kept.get(l.from) ?? /* @__PURE__ */ new Set()).add(l.to));
|
|
2390
|
-
for (const l of links) {
|
|
2391
|
-
const met = /* @__PURE__ */ new Set(), walk = [...kept.get(l.from)].filter((t) => t !== l.to);
|
|
2392
|
-
let found = false;
|
|
2393
|
-
while (walk.length && !found) {
|
|
2394
|
-
const v = walk.pop();
|
|
2395
|
-
if (v === l.to) found = true;
|
|
2396
|
-
else if (v !== l.from && !met.has(v)) {
|
|
2397
|
-
met.add(v);
|
|
2398
|
-
walk.push(...kept.get(v) ?? []);
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
if (found) {
|
|
2402
|
-
l.spare = true;
|
|
2403
|
-
kept.get(l.from).delete(l.to);
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
const builds = [...new Set(neededBy.get(note) ?? [])];
|
|
2407
|
-
for (const from of builds) links.push({
|
|
2408
|
-
from,
|
|
2409
|
-
to: note,
|
|
2410
|
-
kind: "builds"
|
|
2411
|
-
});
|
|
2412
|
-
const own = /* @__PURE__ */ new Set([...requires, ...builds]);
|
|
2413
|
-
return {
|
|
2414
|
-
requires,
|
|
2415
|
-
depth,
|
|
2416
|
-
via,
|
|
2417
|
-
builds,
|
|
2418
|
-
uses: [...uses].filter((r) => !own.has(r)),
|
|
2419
|
-
also: [...also].filter((r) => !own.has(r) && !uses.has(r)),
|
|
2420
|
-
links: [...links, ...weak.filter((l) => !own.has(l.from === note ? l.to : l.from))]
|
|
2421
|
-
};
|
|
2422
|
-
}
|
|
2423
|
-
//#endregion
|
|
2424
|
-
//#region packages/core/src/agents.ts
|
|
2425
|
-
const NAMES = {
|
|
2426
|
-
opencode: "OpenCode",
|
|
2427
|
-
"claude-code": "Claude Code",
|
|
2428
|
-
pi: "Pi"
|
|
2429
|
-
};
|
|
2430
|
-
/** An agent's name as people say it, from its own (`opencode`) or from a journal's who (`agent:opencode`). Empty when it is not an agent. */
|
|
2431
|
-
function agentName(name) {
|
|
2432
|
-
const bare = name?.startsWith("agent:") ? name.slice(6) : name?.includes(":") ? "" : name ?? "";
|
|
2433
|
-
return bare ? NAMES[bare] ?? bare : "";
|
|
2434
|
-
}
|
|
2435
|
-
//#endregion
|
|
2436
|
-
//#region packages/core/src/timeline.ts
|
|
2437
|
-
const WHAT = {
|
|
2438
|
-
wrote: "wrote the note",
|
|
2439
|
-
found: "found",
|
|
2440
|
-
edited: "edited",
|
|
2441
|
-
kept: "kept",
|
|
2442
|
-
offered: "offered",
|
|
2443
|
-
discarded: "removed",
|
|
2444
|
-
restored: "restored an earlier version",
|
|
2445
|
-
moved: "moved the note",
|
|
2446
|
-
"changed elsewhere": "changed elsewhere"
|
|
2447
|
-
};
|
|
2448
|
-
/** Who did what: "you edited", "Iso · “give the Bethe lattice form” · kept". */
|
|
2449
|
-
function describe(m) {
|
|
2450
|
-
const who = m.who === "you" ? "you" : m.who === "external" ? "outside isorite" : m.who.startsWith("agent:") ? agentName(m.who) : "Iso";
|
|
2451
|
-
if (m.asked !== void 0) return `${who} · “${m.asked.length > 80 ? m.asked.slice(0, 80) + "…" : m.asked}” · ${m.what === "offered" ? "not kept yet" : WHAT[m.what]}`;
|
|
2452
|
-
if (m.who === "external") return m.what === "found" ? "found as it was, outside isorite" : "changed outside isorite";
|
|
2453
|
-
return `${who} ${WHAT[m.what]}`;
|
|
2454
|
-
}
|
|
2455
|
-
/** How long ago, in the largest whole unit. */
|
|
2456
|
-
function ago(at, now = Date.now()) {
|
|
2457
|
-
const s = Math.max(0, (now - Date.parse(at)) / 1e3);
|
|
2458
|
-
const [n, unit] = s < 90 ? [0, "now"] : s < 5400 ? [Math.round(s / 60), "minute"] : s < 129600 ? [Math.round(s / 3600), "hour"] : [Math.round(s / 86400), "day"];
|
|
2459
|
-
return unit === "now" ? "just now" : `${n} ${unit}${n === 1 ? "" : "s"} ago`;
|
|
2460
|
-
}
|
|
2461
|
-
/** A moment is named by its last entry: the one the note is shown at. */
|
|
2462
|
-
const momentKey = (m) => m.ids.at(-1);
|
|
2463
|
-
//#endregion
|
|
2464
|
-
export { ABOUT, AGENTS_FILE, Bundle, CONFIG_FILE, Concept, DEVICE_RE, DONE, DiffError, Directory, FrontmatterError, GITATTRIBUTES, ID_RE, KINDS, LAYER_IDLE_MS, LearnerError, MAX_EVENT_BYTES, OKF_VERSION, OWN_DIR, OWN_FORMAT, RATINGS, SESSION_GAP_MS, SETTINGS_FILE, SNAPSHOT_EVERY, STRENGTH, STUDY_DIR, STUDY_FORMAT, SearchIndex, StudyError, agentName, ago, anchorOf, applyHunks, authors, blocks, change, citeKind, cleanEvent, cmp, cmpTuple, codeTarget, contentHash, describe, diffLines, doneOf, dumpFrontmatter, editFrontmatter, embedFor, embedRefs, fileType, findAnchor, findPassage, floatRepr, frontmatterText, hashText, headings, idsOf, impliedLinks, insertAt, isFileRef, iso, journalLine, journalPath, kindOf, legacyId, linkRefs, linkTexts, locate, mapModel, mergeRecords, momentKey, moments, newId, noteIds, outwardLinks, pagerank, parseSettings, parseStudyConfig, parseYaml, passageOf, pending, pictureSize, pictureType, pyRepr, rating, reach, readJournal, readRecord, reconcile, recordPath, replay, round3, section, sessionMoments, settingsText, sha256, slugify, snippet, spliceText, splitFrontmatter, splitSource, stamp, strengthEdges, strip, stronglyConnected, text, textAt, toTime, tokenize, touched, unwrap, wordAuthors, written };
|