isorite 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core.js +175 -14
- package/dist/main.js +1217 -52
- package/notes/algorithms/the-quadtree/subdivision.svg +15 -0
- package/notes/algorithms/the-quadtree.md +2 -0
- package/package.json +2 -1
- package/plugin/server.ts +1 -1
- package/web/_app/immutable/assets/{0.CTbO0owi.css → 0.DcsDj0O1.css} +1 -1
- package/web/_app/immutable/assets/5.DbDKXCoO.css +1 -0
- package/web/_app/immutable/assets/NoteEditor.S3q5lmKq.css +1 -0
- package/web/_app/immutable/assets/drawn.CHbcoujQ.css +1 -0
- package/web/_app/immutable/chunks/{8wVGBFSs.js → -2Meesge.js} +3 -3
- package/web/_app/immutable/chunks/{BL3mLym4.js → 9z49435t.js} +1 -1
- package/web/_app/immutable/chunks/{DMAYXnpq.js → BA8Hpxhg.js} +1 -1
- package/web/_app/immutable/chunks/{CKSRTWxH.js → BAclEN0O.js} +1 -1
- package/web/_app/immutable/chunks/{DECNRCf2.js → BLmF1B2i.js} +1 -1
- package/web/_app/immutable/chunks/{CiVjR8gq.js → BTclWEfx.js} +1 -1
- package/web/_app/immutable/chunks/{Cp_JbuGh.js → BWVMfAMh.js} +1 -1
- package/web/_app/immutable/chunks/{D4fLEcOQ.js → BXAOBcPb.js} +1 -1
- package/web/_app/immutable/chunks/BYSDTLqw.js +1 -0
- package/web/_app/immutable/chunks/{DDuWClBR.js → BgT6ZdzK.js} +1 -1
- package/web/_app/immutable/chunks/{CO2Y5y64.js → Bq2GBAJ0.js} +1 -1
- package/web/_app/immutable/chunks/BqrMZ9MX.js +1 -0
- package/web/_app/immutable/chunks/{BN6xhG8e.js → Bt7VOT5H.js} +1 -1
- package/web/_app/immutable/chunks/{BGbXxNFa.js → BuUzVkG2.js} +1 -1
- package/web/_app/immutable/chunks/{Brxvm_b1.js → By4S_gy5.js} +1 -1
- package/web/_app/immutable/chunks/{CRIvE37b.js → BymwHy7o.js} +1 -1
- package/web/_app/immutable/chunks/{BrqJKzQU.js → CD6xPodA.js} +1 -1
- package/web/_app/immutable/chunks/{BXF2XoRC.js → CMeq8te_.js} +1 -1
- package/web/_app/immutable/chunks/CODXTnUW.js +523 -0
- package/web/_app/immutable/chunks/{CEufJyoj.js → CQd9D8Nc.js} +1 -1
- package/web/_app/immutable/chunks/{BrGmYbJT.js → CT_Ry4q8.js} +1 -1
- package/web/_app/immutable/chunks/{CQadubS9.js → C_amgxV1.js} +1 -1
- package/web/_app/immutable/chunks/{GRrQoDGJ.js → CfL6O_ia.js} +1 -1
- package/web/_app/immutable/chunks/{spNKdrPy.js → CjtKPNlv.js} +1 -1
- package/web/_app/immutable/chunks/{DxLkKbK2.js → Cl5zRP_n.js} +1 -1
- package/web/_app/immutable/chunks/{CETSRtnV.js → CyFLqlvP.js} +1 -1
- package/web/_app/immutable/chunks/{Bq5hCPe1.js → Czz9qOh8.js} +1 -1
- package/web/_app/immutable/chunks/{BguXRgvc.js → D3-rqfsM.js} +1 -1
- package/web/_app/immutable/chunks/{bcQIrid8.js → D5XlJBy8.js} +1 -1
- package/web/_app/immutable/chunks/{TuFg0KqZ.js → D5vocsTl.js} +1 -1
- package/web/_app/immutable/chunks/{CTscJvp6.js → D95RKsvN.js} +1 -1
- package/web/_app/immutable/chunks/{BzI3yqBQ.js → DBC2xjOo.js} +1 -1
- package/web/_app/immutable/chunks/DDVefAkE.js +7 -0
- package/web/_app/immutable/chunks/{BsLNwr5A.js → DF89zyH-.js} +1 -1
- package/web/_app/immutable/chunks/DGmSCGD3.js +1 -0
- package/web/_app/immutable/chunks/{DacMdKjW.js → DNjLZcwq.js} +1 -1
- package/web/_app/immutable/chunks/{C-dfcq9W.js → DRX-TQzj.js} +1 -1
- package/web/_app/immutable/chunks/{BunrC_8P.js → DX4bJJO0.js} +1 -1
- package/web/_app/immutable/chunks/{EQVRMnZU.js → DXK7PB4g.js} +1 -1
- package/web/_app/immutable/chunks/{mbLqADPr.js → DXTK-eyo.js} +1 -1
- package/web/_app/immutable/chunks/{DII7jNnq.js → DaulBP5z.js} +1 -1
- package/web/_app/immutable/chunks/{wCTXGvXm.js → DbNETcPs.js} +1 -1
- package/web/_app/immutable/chunks/Db_gdTNI.js +1 -0
- package/web/_app/immutable/chunks/{CX-xckzu.js → DfCZPwp1.js} +1 -1
- package/web/_app/immutable/chunks/{DKNOMI4Y.js → Dh1lBxfx.js} +1 -1
- package/web/_app/immutable/chunks/DktGnzDy.js +151 -0
- package/web/_app/immutable/chunks/{BgczuZKA.js → DkxkvOjx.js} +1 -1
- package/web/_app/immutable/chunks/{0M1khDs2.js → DlXrOzlh.js} +1 -1
- package/web/_app/immutable/chunks/{B85D_PlV.js → Dpa4Syyy.js} +1 -1
- package/web/_app/immutable/chunks/DshHzxgH.js +1 -0
- package/web/_app/immutable/chunks/DuLysVkb.js +1 -0
- package/web/_app/immutable/chunks/{CdY1YAit.js → EMo1IeTW.js} +1 -1
- package/web/_app/immutable/chunks/F7HCRtmu.js +1 -0
- package/web/_app/immutable/chunks/Iqr2CudW.js +3 -0
- package/web/_app/immutable/chunks/{CoyQYage.js → RFxekmX-.js} +1 -1
- package/web/_app/immutable/chunks/{vk5Wkr31.js → RlApGeew.js} +1 -1
- package/web/_app/immutable/chunks/{CLVUDRRk.js → Xm0YTnxV.js} +1 -1
- package/web/_app/immutable/chunks/{CZ8wTb_M.js → gPtCGHiy.js} +1 -1
- package/web/_app/immutable/chunks/{B3aPQAYq.js → mLFVM17H.js} +1 -1
- package/web/_app/immutable/chunks/{D4SKbj9K.js → oWM_kgYq.js} +1 -1
- package/web/_app/immutable/chunks/{wj7G6pU5.js → rghE0CVz.js} +1 -1
- package/web/_app/immutable/chunks/{B9YLdnR_.js → tUSDgbpy.js} +1 -1
- package/web/_app/immutable/chunks/{DTdlqvEb.js → vfDvz3qP.js} +1 -1
- package/web/_app/immutable/chunks/{BRKS-CCX.js → whhiiiga.js} +1 -1
- package/web/_app/immutable/chunks/wxBqjpRy.js +1 -0
- package/web/_app/immutable/entry/app.wp4GfiBT.js +2 -0
- package/web/_app/immutable/entry/start.PPXyd7_0.js +1 -0
- package/web/_app/immutable/nodes/0.JerS9yOA.js +26 -0
- package/web/_app/immutable/nodes/1.C-RxYNx3.js +1 -0
- package/web/_app/immutable/nodes/2.Bbkruoyi.js +1 -0
- package/web/_app/immutable/nodes/3.BkpGKa9k.js +1 -0
- package/web/_app/immutable/nodes/4.Bj25nsxG.js +2 -0
- package/web/_app/immutable/nodes/5.D9sfzyBX.js +9 -0
- package/web/_app/immutable/nodes/6.Di1re0aR.js +1 -0
- package/web/_app/immutable/nodes/7.MWEn-qPC.js +1 -0
- package/web/_app/version.json +1 -1
- package/web/index.html +18 -17
- package/web/kit/chart.js +55 -0
- package/web/kit/isorite.css +32 -0
- package/web/kit/kit.css +80 -0
- package/web/kit/kit.js +543 -0
- package/web/kit/licenses/d3.txt +13 -0
- package/web/kit/licenses/interval-tree-1d.txt +22 -0
- package/web/kit/licenses/isoformat.txt +13 -0
- package/web/kit/licenses/katex.txt +21 -0
- package/web/kit/licenses/observablehq-plot.txt +13 -0
- package/web/kit/licenses/three.txt +21 -0
- package/web/kit/math.css +1 -0
- package/web/kit/math.js +257 -0
- package/web/kit/model.js +4236 -0
- package/web/kit/plain.css +16 -0
- package/web/_app/immutable/assets/5.TsqJFEFF.css +0 -1
- package/web/_app/immutable/assets/NoteEditor.8wWtZK_A.css +0 -1
- package/web/_app/immutable/chunks/CBWiKnLz.js +0 -1
- package/web/_app/immutable/chunks/CHbKgHy3.js +0 -1
- package/web/_app/immutable/chunks/CJ1dhP0p.js +0 -1
- package/web/_app/immutable/chunks/CLkY0zVi.js +0 -1
- package/web/_app/immutable/chunks/D-aFnd2T.js +0 -1
- package/web/_app/immutable/chunks/DRQEUdad.js +0 -1
- package/web/_app/immutable/chunks/DZYaqCwi.js +0 -1
- package/web/_app/immutable/chunks/D_19S_Iu.js +0 -1
- package/web/_app/immutable/chunks/DcgxJ8bF.js +0 -673
- package/web/_app/immutable/chunks/Ddgejj-u.js +0 -1
- package/web/_app/immutable/chunks/nMarIA7J.js +0 -3
- package/web/_app/immutable/chunks/vUsuwDSf.js +0 -6
- package/web/_app/immutable/entry/app.CswuY3sh.js +0 -2
- package/web/_app/immutable/entry/start.D8ZvSyBD.js +0 -1
- package/web/_app/immutable/nodes/0.DBoa3NAQ.js +0 -26
- package/web/_app/immutable/nodes/1.DrwFerb8.js +0 -1
- package/web/_app/immutable/nodes/2.DPmbppXV.js +0 -1
- package/web/_app/immutable/nodes/3.BW7AbIrS.js +0 -1
- package/web/_app/immutable/nodes/4.B5HAyMKs.js +0 -2
- package/web/_app/immutable/nodes/5.BvScSoCM.js +0 -5
- package/web/_app/immutable/nodes/6.Bna6qY2G.js +0 -1
- package/web/_app/immutable/nodes/7.BxQaF3lD.js +0 -1
package/dist/main.js
CHANGED
|
@@ -18,7 +18,7 @@ import { Hono } from "hono";
|
|
|
18
18
|
import { streamSSE } from "hono/streaming";
|
|
19
19
|
import { z } from "zod";
|
|
20
20
|
import { OpenCode } from "@opencode/client";
|
|
21
|
-
import { appendFile, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
21
|
+
import { appendFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
22
22
|
import { createInterface } from "node:readline/promises";
|
|
23
23
|
import { createServer } from "node:net";
|
|
24
24
|
import { isSea } from "node:sea";
|
|
@@ -43,6 +43,342 @@ var __exportAll = (all, no_symbols) => {
|
|
|
43
43
|
return target;
|
|
44
44
|
};
|
|
45
45
|
//#endregion
|
|
46
|
+
//#region packages/kit/src/assemble.ts
|
|
47
|
+
function meta(html, name) {
|
|
48
|
+
for (const m of html.matchAll(/<meta\b[^>]*>/gi)) {
|
|
49
|
+
const tag = m[0];
|
|
50
|
+
if ((tag.match(/\bname\s*=\s*["']([^"']*)["']/i)?.[1])?.toLowerCase() === name) return unescape(tag.match(/\bcontent\s*=\s*"([^"]*)"|\bcontent\s*=\s*'([^']*)'/i)?.slice(1).find((x) => x !== void 0) ?? "");
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
function manifestOf(html) {
|
|
55
|
+
return {
|
|
56
|
+
title: unescape(html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1]?.trim() ?? ""),
|
|
57
|
+
description: meta(html, "description") ?? "",
|
|
58
|
+
author: meta(html, "author") ?? "",
|
|
59
|
+
date: meta(html, "date") ?? "",
|
|
60
|
+
kit: meta(html, "isorite:kit"),
|
|
61
|
+
uses: (meta(html, "isorite:uses") ?? "").split(/[\s,]+/).filter(Boolean)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** The artifact with its manifest as given (write_artifact, plan-m23 §3.5): its title, description, author, date, kit
|
|
65
|
+
* and uses set in its head, put in where missing, so they're always right whatever the file said. A file with no
|
|
66
|
+
* head or no document around it is given one. */
|
|
67
|
+
function withManifest(html, m) {
|
|
68
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
69
|
+
let doc = html.trim();
|
|
70
|
+
if (!/<html\b/i.test(doc)) doc = /<head\b/i.test(doc) ? `<!doctype html>\n<html lang="en">\n${doc}\n</html>` : `<!doctype html>\n<html lang="en">\n<head>\n</head>\n<body>\n${doc.replace(/<\/?body\b[^>]*>/gi, "")}\n</body>\n</html>`;
|
|
71
|
+
if (!/<head\b/i.test(doc)) doc = doc.replace(/<html\b[^>]*>/i, (h) => `${h}\n<head>\n</head>`);
|
|
72
|
+
if (!/^<!doctype/i.test(doc)) doc = `<!doctype html>\n${doc}`;
|
|
73
|
+
const metas = [
|
|
74
|
+
["description", m.description],
|
|
75
|
+
["author", m.author],
|
|
76
|
+
["date", m.date],
|
|
77
|
+
["isorite:kit", m.kit ?? void 0],
|
|
78
|
+
["isorite:uses", m.uses?.join(" ")]
|
|
79
|
+
];
|
|
80
|
+
for (const [name, value] of metas) {
|
|
81
|
+
if (value === void 0) continue;
|
|
82
|
+
const tag = `<meta name="${name}" content="${esc(value)}">`, re = new RegExp(`<meta\\b[^>]*\\bname\\s*=\\s*["']${name.replace(":", "\\:")}["'][^>]*>`, "i");
|
|
83
|
+
doc = re.test(doc) ? doc.replace(re, () => tag) : doc.replace(/<head\b[^>]*>/i, (h) => `${h}\n${tag}`);
|
|
84
|
+
}
|
|
85
|
+
if (m.title !== void 0) {
|
|
86
|
+
const t = `<title>${esc(m.title)}</title>`;
|
|
87
|
+
doc = /<title\b[^>]*>[\s\S]*?<\/title>/i.test(doc) ? doc.replace(/<title\b[^>]*>[\s\S]*?<\/title>/i, () => t) : doc.replace(/<head\b[^>]*>/i, (h) => `${h}\n${t}`);
|
|
88
|
+
}
|
|
89
|
+
if (!/<meta\b[^>]*charset/i.test(doc)) doc = doc.replace(/<head\b[^>]*>/i, (h) => `${h}\n<meta charset="utf-8">`);
|
|
90
|
+
return doc.endsWith("\n") ? doc : doc + "\n";
|
|
91
|
+
}
|
|
92
|
+
/** The kit's parts an artifact uses beyond the core: Plot with a chart, three.js with 3D. */
|
|
93
|
+
function partsUsed(html) {
|
|
94
|
+
const parts = [];
|
|
95
|
+
if (/<iso-chart[\s>]|\bPlot\.\w/.test(html)) parts.push("chart");
|
|
96
|
+
if (/<iso-model[\s>]|<iso-chart3d[\s>]|\bTHREE\.\w/.test(html)) parts.push("model");
|
|
97
|
+
const text = html.replace(/<script\b[\s\S]*?<\/script>/gi, "").replace(/<style\b[\s\S]*?<\/style>/gi, "");
|
|
98
|
+
if (/<iso-math[\s>]|\biso\.math\(|\$\$[\s\S]+?\$\$|\\\(|\$(?=\S)[^$\n]+?(?<=\S)\$(?!\d)/.test(text) || /\biso\.math\(/.test(html)) parts.push("math");
|
|
99
|
+
return parts;
|
|
100
|
+
}
|
|
101
|
+
/** The artifact as one document: the CSP, the kit's themes, components and the parts it uses, inlined at the top of
|
|
102
|
+
* its head (any kit <link> or <script src> it wrote is taken out), with its data too for a standalone export. */
|
|
103
|
+
function assemble(html, kit, opts = {}) {
|
|
104
|
+
const look = opts.look ?? "isorite", ground = opts.ground ?? "lapis";
|
|
105
|
+
const body = html.replace(/<link\b[^>]*\bhref\s*=\s*["'](?:\.\/)?kit\/[^"']*["'][^>]*>\s*/gi, "").replace(/<script\b[^>]*\bsrc\s*=\s*["'](?:\.\/)?kit\/[^"']*["'][^>]*>\s*<\/script>\s*/gi, "");
|
|
106
|
+
const data = Object.entries(opts.data ?? {}).map(([p, v]) => typeof v === "string" ? `<script type="text/plain" data-path="${attr(p)}">${safe(v)}<\/script>` : `<script type="application/octet-stream" data-encoding="base64" data-path="${attr(p)}">${base64(v)}<\/script>`);
|
|
107
|
+
const parts = partsUsed(body).map((p) => `${p === "math" ? `<style>\n${safe(kit["math.css"])}\n</style>\n` : ""}<script>\n${safe(kit[`${p}.js`])}\n<\/script>`);
|
|
108
|
+
const head = [
|
|
109
|
+
CSP,
|
|
110
|
+
`<style data-look="isorite">\n${safe(kit["isorite.css"])}\n</style>`,
|
|
111
|
+
`<style data-look="plain">\n${safe(kit["plain.css"])}\n</style>`,
|
|
112
|
+
`<style>\n${safe(kit["kit.css"])}\n</style>`,
|
|
113
|
+
...data,
|
|
114
|
+
`<script>\n${safe(kit["kit.js"])}\n<\/script>`,
|
|
115
|
+
...parts
|
|
116
|
+
].join("\n");
|
|
117
|
+
return (/<head\b[^>]*>/i.test(body) ? body.replace(/<head\b[^>]*>/i, (h) => `${h}\n${head}\n`) : `<!doctype html><html><head><meta charset="utf-8">\n${head}\n</head><body>${body}</body></html>`).replace(/<html\b([^>]*)>/i, (_, a) => `<html${a.replace(/\s+data-(?:look|ground)\s*=\s*("[^"]*"|'[^']*'|\S+)/gi, "")} data-look="${look}" data-ground="${ground}">`);
|
|
118
|
+
}
|
|
119
|
+
function base64(bytes) {
|
|
120
|
+
let s = "";
|
|
121
|
+
for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
122
|
+
return btoa(s);
|
|
123
|
+
}
|
|
124
|
+
var KIT_FILES, unescape, CSP, safe, attr;
|
|
125
|
+
var init_assemble = __esmMin((() => {
|
|
126
|
+
KIT_FILES = [
|
|
127
|
+
"kit.js",
|
|
128
|
+
"chart.js",
|
|
129
|
+
"model.js",
|
|
130
|
+
"math.js",
|
|
131
|
+
"math.css",
|
|
132
|
+
"isorite.css",
|
|
133
|
+
"plain.css",
|
|
134
|
+
"kit.css"
|
|
135
|
+
];
|
|
136
|
+
unescape = (s) => s.replace(/"/g, "\"").replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
137
|
+
CSP = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; media-src data: blob:; connect-src 'none'">`;
|
|
138
|
+
safe = (t) => t.replace(/<\/(script|style)/gi, "<\\/$1");
|
|
139
|
+
attr = (s) => s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
140
|
+
}));
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region packages/kit/src/rules.ts
|
|
143
|
+
/** The files an artifact reads, from its markup and script: data="…", an iso-model's src, iso.data/text/bytes("…"). */
|
|
144
|
+
function readsOf(html) {
|
|
145
|
+
const own = html.replace(/<!--[\s\S]*?-->/g, ""), reads = /* @__PURE__ */ new Set();
|
|
146
|
+
for (const r of own.matchAll(/iso\.(?:data|text|bytes)\(\s*["'`]([^"'`$]+)["'`]/g)) reads.add(r[1]);
|
|
147
|
+
for (const t of own.matchAll(TAG)) {
|
|
148
|
+
const a = attrsOf(t[2]);
|
|
149
|
+
(a.data ?? "").split(/[\s,]+/).filter(Boolean).forEach((f) => reads.add(f));
|
|
150
|
+
if (t[1].toLowerCase() === "iso-model" && a.src) reads.add(a.src);
|
|
151
|
+
}
|
|
152
|
+
return [...reads];
|
|
153
|
+
}
|
|
154
|
+
function checkArtifact(html, ctx = {}) {
|
|
155
|
+
const out = [], m = manifestOf(html);
|
|
156
|
+
const err = (rule, message) => out.push({
|
|
157
|
+
rule,
|
|
158
|
+
level: "error",
|
|
159
|
+
message
|
|
160
|
+
});
|
|
161
|
+
const warn = (rule, message) => out.push({
|
|
162
|
+
rule,
|
|
163
|
+
level: "warning",
|
|
164
|
+
message
|
|
165
|
+
});
|
|
166
|
+
const own = html.replace(/<!--[\s\S]*?-->/g, "");
|
|
167
|
+
if (m.kit === null) err(1, "no <meta name=\"isorite:kit\" content=\"1\">: without it isorite gives it no kit");
|
|
168
|
+
if (!m.title) err(1, "no <title>");
|
|
169
|
+
if (!m.description) err(1, "no <meta name=\"description\" content=\"…\">: one sentence on what it shows");
|
|
170
|
+
const reads = /* @__PURE__ */ new Set();
|
|
171
|
+
for (const r of own.matchAll(/iso\.(?:data|text|bytes)\(\s*["'`]([^"'`$]+)["'`]/g)) reads.add(r[1]);
|
|
172
|
+
const files = (a) => (a.data ?? "").split(/[\s,]+/).filter(Boolean);
|
|
173
|
+
for (const t of own.matchAll(TAG)) {
|
|
174
|
+
const a = attrsOf(t[2]);
|
|
175
|
+
files(a).forEach((f) => reads.add(f));
|
|
176
|
+
if (t[1].toLowerCase() === "iso-model" && a.src) reads.add(a.src);
|
|
177
|
+
}
|
|
178
|
+
for (const r of reads) if (!m.uses.includes(r)) err(2, `it reads ${r}, which isn't in <meta name="isorite:uses">`);
|
|
179
|
+
if (ctx.files) {
|
|
180
|
+
for (const u of m.uses) if (!ctx.files.includes(u)) err(2, `${u} is in isorite:uses but there's no such file beside it${ctx.files.length ? ` (there are: ${ctx.files.join(", ")})` : ""}`);
|
|
181
|
+
}
|
|
182
|
+
if (ctx.uses && [...ctx.uses].sort().join() !== [...m.uses].sort().join()) err(2, `isorite:uses (${m.uses.join(" ") || "none"}) and the uses given (${ctx.uses.join(" ") || "none"}) differ`);
|
|
183
|
+
if (ctx.columns) for (const t of own.matchAll(TAG)) {
|
|
184
|
+
const a = attrsOf(t[2]), fs = files(a), known = fs.map((f) => ctx.columns[f]);
|
|
185
|
+
if (!fs.length || known.some((k) => !k)) continue;
|
|
186
|
+
const have = [...new Set(known.flat()), ...fs.length > 1 ? ["file"] : []];
|
|
187
|
+
for (const k of COLUMN_ATTRS) for (const c of (a[k] ?? "").split(/[\s,]+/).filter(Boolean)) {
|
|
188
|
+
const name = c.replace(/^abs\((.+)\)$/, "$1");
|
|
189
|
+
if (k === "at" && t[1].toLowerCase() !== "iso-chart3d" && !have.includes(name)) continue;
|
|
190
|
+
if (!have.includes(name)) err(3, `<${t[1]} data="${a.data}" ${k}="${a[k]}">: ${a.data} has no column "${name}" (it has ${have.join(", ")})`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const code = own.replace(/<meta\b[^>]*>/gi, "").replace(/\btone\s*=\s*"[^"]*"/g, "");
|
|
194
|
+
const colour = code.match(/#[0-9a-fA-F]{3,8}\b(?![-\w])|\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\(\s*[\d.]/i) ?? code.match(/\b(?:fill|stroke|fillStyle|strokeStyle|color|background(?:-color)?|shadowColor|border(?:-color)?)\s*[:=]\s*["'`]?(?:white|black|red|blue|green|gray|grey|orange|yellow|purple|pink|navy|teal|silver)\b/i);
|
|
195
|
+
if (colour) err(4, `a colour of its own (${colour[0].trim().slice(0, 40)}): use the theme's, var(--lattice) in CSS or iso.tokens().lattice in script`);
|
|
196
|
+
const font = code.match(/font-family\s*:\s*[^;"}]*|\bfont\s*[:=]\s*["'`]?[^;"'`}]*(?<![.\w-])(?:sans-serif|serif|monospace|Arial|Helvetica|system-ui|Georgia|Times|Courier|Inter|Roboto)\b/i);
|
|
197
|
+
if (font && !/var\(--font|\$\{/.test(font[0])) err(4, `a font of its own (${font[0].slice(0, 50)}): use var(--font-ui), var(--font-text) or iso.tokens().ui`);
|
|
198
|
+
if (/border-radius\s*:/i.test(code)) err(4, "border-radius: the theme decides corners");
|
|
199
|
+
const urls = code.replace(/http:\/\/www\.w3\.org\/[\w/.-]+/g, "").match(/\bhttps?:\/\/[^\s"'`)<]+/);
|
|
200
|
+
if (urls) err(5, `a web address (${urls[0].slice(0, 60)}): an artifact reaches nothing outside the file`);
|
|
201
|
+
if (/<script\b[^>]*\bsrc\s*=/i.test(code.replace(/<script\b[^>]*\bsrc\s*=\s*["'](?:\.\/)?kit\/[^"']*["'][^>]*>/gi, ""))) err(5, "a <script src>: everything is in the file, and the kit is given");
|
|
202
|
+
if (/<link\b[^>]*\bhref\s*=\s*["'](?!(?:\.\/)?kit\/)/i.test(code)) err(5, "a <link href>: everything is in the file, and the kit is given");
|
|
203
|
+
if (/\b(?:fetch|XMLHttpRequest|WebSocket|EventSource|importScripts)\s*\(|\bimport\s*\(/.test(code)) err(5, "fetch, XMLHttpRequest or import(): read files with iso.data(path)");
|
|
204
|
+
if (html.length > 2e5) err(6, `${Math.round(html.length / 1024)} kB, over 200 kB: keep data in a data file, not in the page`);
|
|
205
|
+
const directive = own.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]*>/g, " ").match(/\b(?:drag(?:ging)? to|click (?:here|to|on)|tap to|scroll to|hover (?:to|over)|use the slider|press (?:the|play)|try (?:dragging|moving|clicking))\b[^.]*/i);
|
|
206
|
+
if (directive) warn(7, `text that explains how to use it ("${directive[0].trim().slice(0, 60)}"): say what a thing is, not what to do with it`);
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
var COLUMN_ATTRS, TAG, attrsOf;
|
|
210
|
+
var init_rules = __esmMin((() => {
|
|
211
|
+
init_assemble();
|
|
212
|
+
COLUMN_ATTRS = [
|
|
213
|
+
"x",
|
|
214
|
+
"y",
|
|
215
|
+
"z",
|
|
216
|
+
"by",
|
|
217
|
+
"column",
|
|
218
|
+
"columns",
|
|
219
|
+
"at"
|
|
220
|
+
];
|
|
221
|
+
TAG = /<(iso-[\w-]+)\b([^>]*)>/gi;
|
|
222
|
+
attrsOf = (s) => Object.fromEntries([...s.matchAll(/([\w-]+)\s*=\s*"([^"]*)"/g)].map((m) => [m[1].toLowerCase(), m[2]]));
|
|
223
|
+
}));
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region packages/kit/src/data.ts
|
|
226
|
+
/** One line of delimited text into fields; quotes may hold the delimiter, newlines and doubled quotes. */
|
|
227
|
+
function* records(text, sep) {
|
|
228
|
+
let field = "", row = [], quoted = false;
|
|
229
|
+
for (let i = 0; i < text.length; i++) {
|
|
230
|
+
const c = text[i];
|
|
231
|
+
if (quoted) {
|
|
232
|
+
if (c === "\"") {
|
|
233
|
+
if (text[i + 1] === "\"") {
|
|
234
|
+
field += "\"";
|
|
235
|
+
i++;
|
|
236
|
+
} else quoted = false;
|
|
237
|
+
} else field += c;
|
|
238
|
+
} else if (c === "\"" && field === "") quoted = true;
|
|
239
|
+
else if (c === sep) {
|
|
240
|
+
row.push(field);
|
|
241
|
+
field = "";
|
|
242
|
+
} else if (c === "\n" || c === "\r") {
|
|
243
|
+
if (c === "\r" && text[i + 1] === "\n") i++;
|
|
244
|
+
row.push(field);
|
|
245
|
+
field = "";
|
|
246
|
+
if (row.length > 1 || row[0] !== "") yield row;
|
|
247
|
+
row = [];
|
|
248
|
+
} else field += c;
|
|
249
|
+
}
|
|
250
|
+
row.push(field);
|
|
251
|
+
if (row.length > 1 || row[0] !== "") yield row;
|
|
252
|
+
}
|
|
253
|
+
function parseDelimited(text, sep = ",") {
|
|
254
|
+
const it = records(text.replace(/^/, ""), sep), head = it.next();
|
|
255
|
+
if (head.done) return [];
|
|
256
|
+
const cols = head.value.map((c) => c.trim());
|
|
257
|
+
const out = [];
|
|
258
|
+
for (const r of it) out.push(Object.fromEntries(cols.map((c, i) => [c, value(r[i] ?? "")])));
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
function parseData(path, text) {
|
|
262
|
+
const ext = path.toLowerCase().split(".").pop();
|
|
263
|
+
if (ext === "json") {
|
|
264
|
+
const j = JSON.parse(text);
|
|
265
|
+
if (Array.isArray(j)) return j;
|
|
266
|
+
const inner = j && typeof j === "object" ? Object.values(j).find(Array.isArray) : null;
|
|
267
|
+
if (inner) return inner;
|
|
268
|
+
throw new Error(`${path}: not an array of rows`);
|
|
269
|
+
}
|
|
270
|
+
if (ext === "jsonl" || ext === "ndjson") return text.split(/\r?\n/).filter((l) => l.trim()).map((l) => JSON.parse(l));
|
|
271
|
+
if (ext === "tsv") return parseDelimited(text, " ");
|
|
272
|
+
if (ext === "csv") return parseDelimited(text, ",");
|
|
273
|
+
throw new Error(`${path}: the kit reads CSV, TSV, JSON and JSON lines`);
|
|
274
|
+
}
|
|
275
|
+
/** Every column any row has, in the order they first appear. */
|
|
276
|
+
function columnsOf(rows) {
|
|
277
|
+
const seen = /* @__PURE__ */ new Set();
|
|
278
|
+
for (const r of rows.slice(0, 1e3)) for (const k of Object.keys(r)) seen.add(k);
|
|
279
|
+
return [...seen];
|
|
280
|
+
}
|
|
281
|
+
/** What read_data tells Iso: enough to build on a file without reading it. */
|
|
282
|
+
function shapeOf(path, rows, head = 5) {
|
|
283
|
+
return {
|
|
284
|
+
path,
|
|
285
|
+
rows: rows.length,
|
|
286
|
+
head: rows.slice(0, head),
|
|
287
|
+
columns: columnsOf(rows).map((name) => {
|
|
288
|
+
const vs = rows.map((r) => r[name]), present = vs.filter((v) => v !== null && v !== void 0 && v !== ""), empty = vs.length - present.length;
|
|
289
|
+
if (!present.length) return {
|
|
290
|
+
name,
|
|
291
|
+
type: "empty",
|
|
292
|
+
empty
|
|
293
|
+
};
|
|
294
|
+
if (present.every((v) => typeof v === "number")) {
|
|
295
|
+
let min = Infinity, max = -Infinity, sum = 0;
|
|
296
|
+
for (const v of present) {
|
|
297
|
+
if (v < min) min = v;
|
|
298
|
+
if (v > max) max = v;
|
|
299
|
+
sum += v;
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
name,
|
|
303
|
+
type: "number",
|
|
304
|
+
min,
|
|
305
|
+
max,
|
|
306
|
+
mean: sum / present.length,
|
|
307
|
+
empty
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (present.every((v) => typeof v === "boolean")) return {
|
|
311
|
+
name,
|
|
312
|
+
type: "boolean",
|
|
313
|
+
empty
|
|
314
|
+
};
|
|
315
|
+
const distinct = [...new Set(present.map(String))];
|
|
316
|
+
return {
|
|
317
|
+
name,
|
|
318
|
+
type: "text",
|
|
319
|
+
distinct: distinct.length,
|
|
320
|
+
values: distinct.slice(0, 12),
|
|
321
|
+
empty
|
|
322
|
+
};
|
|
323
|
+
})
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/** The shape as text, the way Iso reads it. */
|
|
327
|
+
function describeShape(s) {
|
|
328
|
+
const g = (x) => Number.isInteger(x) ? String(x) : String(+x.toPrecision(4));
|
|
329
|
+
const lines = [`${s.path}: ${s.rows} rows, ${s.columns.length} columns`];
|
|
330
|
+
for (const c of s.columns) {
|
|
331
|
+
const empty = c.empty ? `, ${c.empty} empty` : "";
|
|
332
|
+
if (c.type === "number") lines.push(`- ${c.name}: number, ${g(c.min)} to ${g(c.max)} (mean ${g(c.mean)})${empty}`);
|
|
333
|
+
else if (c.type === "text") lines.push(`- ${c.name}: text, ${c.distinct} distinct: ${c.values.join(", ")}${c.distinct > c.values.length ? ", …" : ""}${empty}`);
|
|
334
|
+
else lines.push(`- ${c.name}: ${c.type}${empty}`);
|
|
335
|
+
}
|
|
336
|
+
const cols = s.columns.map((c) => c.name);
|
|
337
|
+
lines.push("First rows:", cols.join(","), ...s.head.map((r) => cols.map((c) => r[c] ?? "").join(",")));
|
|
338
|
+
return lines.join("\n");
|
|
339
|
+
}
|
|
340
|
+
var NUMBER, value;
|
|
341
|
+
var init_data = __esmMin((() => {
|
|
342
|
+
NUMBER = /^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?$/;
|
|
343
|
+
value = (s) => s === "" ? null : NUMBER.test(s.trim()) ? +s : s;
|
|
344
|
+
}));
|
|
345
|
+
//#endregion
|
|
346
|
+
//#region packages/kit/src/context.gen.ts
|
|
347
|
+
var REFERENCE, RECIPES;
|
|
348
|
+
var init_context_gen = __esmMin((() => {
|
|
349
|
+
REFERENCE = "# The kit (isorite kit 1)\n\nAn artifact is one HTML file: markup, perhaps a script, and a manifest in its `<head>`. isorite gives it the kit:\nthe look (every colour, font and corner), the elements below, and `window.iso`. Write what it shows and does, never\nhow it looks: no colour, font family or border-radius of your own.\n\n## The file\n\n```html\n<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Title</title>\n<meta name=\"description\" content=\"One sentence: what it shows, and what can be done with it.\">\n<meta name=\"isorite:kit\" content=\"1\">\n<meta name=\"isorite:uses\" content=\"runs.csv other.csv\">\n<style>/* layout only, if needed */</style>\n</head>\n<body>\n<iso-page>\n <header><h1>Title</h1><p>The question it answers, in a sentence or two.</p></header>\n <iso-grid>\n <iso-panel class=\"wide\"><h2>Name <span>a short reading</span></h2> … </iso-panel>\n <iso-panel><h2>…</h2> … </iso-panel>\n </iso-grid>\n</iso-page>\n</body>\n</html>\n```\n\n`write_artifact` fills in the title, description and manifest from its arguments, so they are always right.\n\n## Layout\n\n- `<iso-grid>`: panels in two columns (`style=\"--cols: 3\"` for three); `<iso-panel class=\"wide\">` spans the row.\n- `<iso-panel>`: its first child is `<h2>`; a `<span>` in the `h2` is a soft aside. A `<p>` in a panel is a\n caption: what the panel shows, in a sentence.\n- `<iso-row>`: controls side by side. `<iso-stats>`: big numbers side by side.\n\n## Data, with no script\n\nEvery element with `data=\"file.csv\"` reads it itself. Several files are one table:\n`data=\"euler.csv leapfrog.csv\"` adds a column `file` (`euler`, `leapfrog`) to compare them with `by=\"file\"`.\n`where=\"integrator=leapfrog\"` keeps some rows; `where=\"t<=$t\"` compares with a control's value. `abs(col)` is a\ncolumn's size.\n\n- `<iso-chart data=\"f.csv\" x=\"t\" y=\"energy\" by=\"body\">`: a chart.\n - `type`: `line` (default), `dot`, `bar`, `area`, `step`.\n - `y` may name several columns: `y=\"kinetic potential total\"`, each its own line.\n - `by`: a column whose values are the series.\n - `x-scale`, `y-scale`: `linear` or `log`, or `$name` to follow a control (`y-scale=\"$scale\"`).\n - `x-label`, `y-label`: the axis with its unit (`\"time (s)\"`). `height` in pixels (260).\n - `at=\"t\"`: follows the control `t`. A rule where x is its value; or, when the data has a column `t` and x is\n another column (a path in x and y over time), a dot on each series at the row whose `t` is nearest it.\n- `<iso-table data=\"f.csv\" columns=\"a b c\" limit=\"50\">`: the rows.\n- `<iso-stat data=\"f.csv\" column=\"err\" of=\"max\" label=\"worst error\" tone=\"gold\">`: one number. `of`: `min`, `max`,\n `mean`, `sum`, `count`, `first`, `last`. Tones: `gold`, `blue`, `green`, `rose`. Without `data`, set it from a\n script: `el.value = 0.0023`.\n- `<iso-chart3d data=\"f.csv\" x=\"x\" y=\"y\" z=\"z\" by=\"body\" at=\"t\">`: points or paths in 3D, turned by dragging.\n- `<iso-model src=\"part.glb\">`: a 3D model (glTF, GLB, STL, OBJ), turned by dragging. List it in isorite:uses.\n\n## Text and maths\n\n- A `<p>` in a panel is its caption. Maths is written as in a note: `$E = \\tfrac12 m v^2$` inline, `$$…$$` on a\n line of its own; the kit sets it. `<iso-math>\\frac{a}{b}</iso-math>` is a formula alone.\n- Never name the theme's colours in text (\"the lattice line\"): a chart's legend says which series is which.\n\n## Controls\n\nEach writes its value to `iso.state[name]`; elements that name it (`at=\"t\"`, `$t`) follow it.\n\n- `<iso-slider name=\"dt\" label=\"time step\" min=\"0.001\" max=\"0.05\" step=\"0.001\" value=\"0.01\" digits=\"3\">`\n- `<iso-select name=\"scale\" label=\"scale\" options=\"linear log\">`: one of several.\n- `<iso-toggle name=\"softened\" label=\"softened\" value=\"true\">`: on or off.\n- `<iso-play name=\"t\" min=\"0\" max=\"12\" speed=\"1.5\">`: plays `iso.state.t` from min to max, and pauses.\n- `<iso-note href=\"physics/softening\">softening</iso-note>`: opens a note in isorite.\n\n## Script, when markup isn't enough\n\nFor a simulation or anything computed. Put it at the end of the body.\n\n- `const rows = await iso.data(\"f.csv\")`: the rows as objects, numbers as numbers.\n- `iso.state.name`, `iso.watch((name) => …)`: the shared state; `watch` runs after any change.\n- `iso.loop((dt, t) => …)`: runs every frame (dt in seconds), paused when hidden. Returns a stop function.\n- `<iso-canvas height=\"320\" id=\"sim\">` and `document.getElementById(\"sim\").draw = (ctx, w, h, t) => { … }`: draw on\n a canvas sized to its panel; call `.redraw()` after a change. `t` is the theme: `t.text`, `t.soft`, `t.faint`,\n `t.line`, `t.lattice` (blue), `t.growth` (gold), `t.malachite` (green), `t.rose`, `t.series[i]`, `t.ui` (the font).\n- A chart of what the script computes: `chart.rows = [{ t: 0, energy: -1.3 }, …]` on an `<iso-chart>` with no\n `data`.\n- `iso.format(x)`: a number to read (3 figures, 2.3×10⁻³). `iso.tokens()`: the theme, as `t` above.\n- In CSS: `var(--text)`, `var(--text-soft)`, `var(--text-faint)`, `var(--line)`, `var(--lattice)`, `var(--growth)`,\n `var(--malachite)`, `var(--rose)`, `var(--font-ui)`, `var(--font-text)`.\n\n## Rules (write_artifact checks them, and refuses a file that breaks one)\n\n1. Every file read (`data=\"…\"`, `src=\"…\"`, `iso.data(\"…\")`) is in `uses`, and exists: call `read_data` first.\n2. Every column named is in its file, spelt as `read_data` gives it.\n3. No colour, font family or border-radius of its own: no `#hex`, `rgb(…)`, `white`, `Arial`. Use the tokens.\n4. No network: no `http…` address, no `<script src>`, no `fetch`. The kit is given; everything else is in the file.\n5. Under 200 kB: data stays in data files.\n6. No text that tells how to use it (\"drag to rotate\", \"click here\"): a label says what a thing is.\n\n## What good is\n\nEvery panel answers a question about the data, with a caption saying what to see. Numbers are read out. Axes say\nwhat they measure, with units. Controls change what is drawn at once. Nothing is decoration.\n";
|
|
350
|
+
RECIPES = {
|
|
351
|
+
"dashboard": {
|
|
352
|
+
"about": "a dashboard of one data file: numbers, charts and a table, in markup only",
|
|
353
|
+
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>The pendulum's period</title>\n<meta name=\"description\" content=\"Forty timed swings at eight lengths: how the period grows with length, how close it comes to 2π√(L/g), and how much the trials scatter.\">\n<meta name=\"isorite:kit\" content=\"1\">\n<meta name=\"isorite:uses\" content=\"pendulum.csv\">\n</head>\n<body>\n<!-- Recipe 1: a dashboard of one data file, in markup only. Change: the file, the numbers, the charts' columns, the\n captions. Every column named here must be one read_data gave. -->\n<iso-page>\n <header>\n <h1>The pendulum's period</h1>\n <p>Does a longer pendulum swing slower, and by as much as theory says?</p>\n </header>\n <iso-grid>\n <iso-panel class=\"wide\">\n <h2>In numbers <span>40 swings</span></h2>\n <iso-stats>\n <iso-stat data=\"pendulum.csv\" column=\"period_s\" of=\"min\" label=\"shortest period (s)\" tone=\"blue\"></iso-stat>\n <iso-stat data=\"pendulum.csv\" column=\"period_s\" of=\"max\" label=\"longest period (s)\" tone=\"gold\"></iso-stat>\n <iso-stat data=\"pendulum.csv\" column=\"temp_c\" of=\"mean\" label=\"mean temperature (°C)\"></iso-stat>\n <iso-stat data=\"pendulum.csv\" column=\"trial\" of=\"count\" label=\"swings timed\"></iso-stat>\n </iso-stats>\n </iso-panel>\n <iso-panel>\n <h2>Period against length</h2>\n <iso-chart data=\"pendulum.csv\" type=\"dot\" x=\"length_m\" y=\"period_s\" x-label=\"length (m)\" y-label=\"period (s)\"></iso-chart>\n <p>The period rises with length, more slowly as the pendulum gets longer: a square root.</p>\n </iso-panel>\n <iso-panel>\n <h2>On log axes <span>slope ½ is √L</span></h2>\n <iso-chart data=\"pendulum.csv\" type=\"dot\" x=\"length_m\" y=\"period_s\" x-scale=\"log\" y-scale=\"log\" x-label=\"length (m)\" y-label=\"period (s)\"></iso-chart>\n <p>A straight line here is a power law; its slope, near one half, is the exponent.</p>\n </iso-panel>\n <iso-panel class=\"wide\">\n <h2>Each trial <span>the scatter is the stopwatch</span></h2>\n <iso-chart data=\"pendulum.csv\" type=\"line\" x=\"length_m\" y=\"period_s\" by=\"trial\" height=\"200\" x-label=\"length (m)\" y-label=\"period (s)\"></iso-chart>\n </iso-panel>\n <iso-panel class=\"wide\">\n <h2>The measurements</h2>\n <iso-table data=\"pendulum.csv\" columns=\"length_m trial period_s temp_c\" limit=\"40\"></iso-table>\n </iso-panel>\n </iso-grid>\n</iso-page>\n</body>\n</html>\n"
|
|
354
|
+
},
|
|
355
|
+
"compare": {
|
|
356
|
+
"about": "two data files with the same columns, compared: together on one chart, and side by side",
|
|
357
|
+
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>A lid against an open mug</title>\n<meta name=\"description\" content=\"Two mugs of the same coffee cooling for an hour, one with a lid: their temperature together, the gap between them, and where each stands at a chosen minute.\">\n<meta name=\"isorite:kit\" content=\"1\">\n<meta name=\"isorite:uses\" content=\"mug-open.csv mug-lid.csv\">\n</head>\n<body>\n<!-- Recipe 2: comparing two data files with the same columns. data=\"a.csv b.csv\" reads both as one table with a\n column `file` (\"mug-open\", \"mug-lid\"), so by=\"file\" draws one series each and where=\"file=…\" picks one.\n Change: the files, the columns, the captions. -->\n<iso-page>\n <header>\n <h1>A lid against an open mug</h1>\n <p>How much longer does a lid keep coffee hot?</p>\n </header>\n <iso-grid>\n <iso-panel class=\"wide\">\n <h2>At one minute</h2>\n <iso-row>\n <iso-slider name=\"t\" label=\"minute\" min=\"0\" max=\"60\" step=\"2\" value=\"20\" digits=\"0\"></iso-slider>\n <iso-select name=\"scale\" label=\"temperature scale\" options=\"linear log\"></iso-select>\n </iso-row>\n <iso-stats>\n <iso-stat data=\"mug-open.csv\" column=\"temp_c\" of=\"last\" where=\"t_min<=$t\" label=\"open (°C)\" tone=\"blue\"></iso-stat>\n <iso-stat data=\"mug-lid.csv\" column=\"temp_c\" of=\"last\" where=\"t_min<=$t\" label=\"with a lid (°C)\" tone=\"gold\"></iso-stat>\n <iso-stat data=\"mug-open.csv\" column=\"temp_c\" of=\"last\" label=\"open after the hour (°C)\"></iso-stat>\n <iso-stat data=\"mug-lid.csv\" column=\"temp_c\" of=\"last\" label=\"lid after the hour (°C)\"></iso-stat>\n </iso-stats>\n </iso-panel>\n <iso-panel class=\"wide\">\n <h2>Both, cooling</h2>\n <iso-chart data=\"mug-open.csv mug-lid.csv\" x=\"t_min\" y=\"temp_c\" by=\"file\" at=\"t\" y-scale=\"$scale\" x-label=\"time (min)\" y-label=\"temperature (°C)\" height=\"300\"></iso-chart>\n <p>Both fall fast at first and slower as they near the room's 21 °C; the lid halves the early loss.</p>\n </iso-panel>\n <iso-panel>\n <h2>Open</h2>\n <iso-table data=\"mug-open.csv\" limit=\"12\"></iso-table>\n </iso-panel>\n <iso-panel>\n <h2>With a lid</h2>\n <iso-table data=\"mug-lid.csv\" limit=\"12\"></iso-table>\n </iso-panel>\n </iso-grid>\n</iso-page>\n</body>\n</html>\n"
|
|
358
|
+
},
|
|
359
|
+
"simulation": {
|
|
360
|
+
"about": "a simulation with parameters, play and reset, drawn on a canvas, with a live chart (a script)",
|
|
361
|
+
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>A damped spring</title>\n<meta name=\"description\" content=\"A mass on a spring, integrated live: change the stiffness and the damping and see the motion, the energy draining away, and the period against theory's.\">\n<meta name=\"isorite:kit\" content=\"1\">\n<meta name=\"isorite:uses\" content=\"\">\n<style>\n /* Layout only. */\n .readout { margin-top: 12px; }\n</style>\n</head>\n<body>\n<!-- Recipe 3: a simulation with parameters and play. The parts to change are marked CHANGE: the state (what the\n model is), step() (how it moves), draw() (what's on the canvas), and what's measured. The rest stays. -->\n<iso-page>\n <header>\n <h1>A damped spring</h1>\n <p>How do stiffness and damping shape an oscillation?</p>\n </header>\n <iso-grid>\n <iso-panel class=\"wide\">\n <h2>The model</h2>\n <iso-row>\n <iso-slider name=\"k\" label=\"stiffness k (N/m)\" min=\"1\" max=\"40\" step=\"0.5\" value=\"10\" digits=\"1\"></iso-slider>\n <iso-slider name=\"c\" label=\"damping c (kg/s)\" min=\"0\" max=\"2\" step=\"0.05\" value=\"0.3\" digits=\"2\"></iso-slider>\n </iso-row>\n <iso-row>\n <iso-toggle name=\"playing\" label=\"play\" value=\"true\"></iso-toggle>\n <button type=\"button\" class=\"iso-button\" id=\"reset\">reset</button>\n </iso-row>\n <iso-stats class=\"readout\">\n <iso-stat id=\"time\" label=\"time (s)\"></iso-stat>\n <iso-stat id=\"energy\" label=\"energy left\" tone=\"gold\"></iso-stat>\n <iso-stat id=\"period\" label=\"period, theory (s)\" tone=\"blue\"></iso-stat>\n </iso-stats>\n </iso-panel>\n <iso-panel>\n <h2>The mass</h2>\n <iso-canvas id=\"scene\" height=\"300\"></iso-canvas>\n </iso-panel>\n <iso-panel>\n <h2>Energy <span>a share of the start</span></h2>\n <iso-chart id=\"chart\" x=\"t\" y=\"energy\" x-label=\"time (s)\" y-label=\"energy left\" height=\"300\"></iso-chart>\n <p>With no damping the energy stays; damping drains it, faster the larger c is.</p>\n </iso-panel>\n </iso-grid>\n</iso-page>\n<script>\n // CHANGE: the model's state and its start.\n const m = 1;\n let s, history;\n function reset() { s = { t: 0, x: 1, v: 0 }; history = []; }\n reset();\n const energy = () => 0.5 * m * s.v * s.v + 0.5 * iso.state.k * s.x * s.x;\n let e0 = energy();\n\n // CHANGE: one step of the model (here semi-implicit Euler, small steps).\n function step(dt) {\n const a = (-iso.state.k * s.x - iso.state.c * s.v) / m;\n s.v += a * dt;\n s.x += s.v * dt;\n s.t += dt;\n }\n\n // CHANGE: what's drawn. t is the theme: t.line, t.text, t.soft, t.faint, t.lattice, t.growth, t.ui.\n const scene = document.getElementById(\"scene\");\n scene.draw = (ctx, w, h, t) => {\n const cx = w / 2, y0 = 24, rest = h * 0.5, y = rest + s.x * h * 0.3;\n ctx.strokeStyle = t.line; ctx.lineWidth = 1;\n ctx.beginPath(); ctx.moveTo(cx - 60, y0); ctx.lineTo(cx + 60, y0); ctx.stroke();\n ctx.setLineDash([3, 4]); ctx.beginPath(); ctx.moveTo(cx - 80, rest); ctx.lineTo(cx + 80, rest); ctx.stroke(); ctx.setLineDash([]);\n // The spring, as a zigzag from the ceiling to the mass.\n ctx.strokeStyle = t.soft; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(cx, y0);\n for (let i = 1; i < 16; i++) ctx.lineTo(cx + (i % 2 ? 12 : -12), y0 + (y - 20 - y0) * i / 16);\n ctx.lineTo(cx, y - 20); ctx.stroke();\n ctx.fillStyle = t.lattice; ctx.fillRect(cx - 20, y - 20, 40, 40);\n ctx.fillStyle = t.faint; ctx.font = `11px ${t.ui}`; ctx.fillText(\"rest\", cx + 84, rest + 4);\n };\n\n // Measured, and shown: the readouts and the chart, a few times a second.\n const chart = document.getElementById(\"chart\");\n let since = 0;\n function show() {\n document.getElementById(\"time\").value = s.t.toFixed(1);\n document.getElementById(\"energy\").value = iso.format(energy() / e0);\n document.getElementById(\"period\").value = iso.format(2 * Math.PI * Math.sqrt(m / iso.state.k));\n chart.rows = history;\n }\n\n iso.loop((dt) => {\n if (!iso.state.playing) return;\n for (let i = 0; i < 20; i++) step(dt / 20);\n scene.redraw();\n since += dt;\n if (since > 0.1) { since = 0; history.push({ t: +s.t.toFixed(2), energy: energy() / e0 }); if (history.length > 600) history.shift(); show(); }\n });\n iso.watch((name) => { if (name === \"k\") { e0 = energy(); } show(); scene.redraw(); });\n document.getElementById(\"reset\").addEventListener(\"click\", () => { reset(); e0 = energy(); show(); scene.redraw(); });\n show();\n<\/script>\n</body>\n</html>\n"
|
|
362
|
+
},
|
|
363
|
+
"explorable": {
|
|
364
|
+
"about": "an explorable explanation: inputs, numbers derived from them, and linked views (a script)",
|
|
365
|
+
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Why a fast wheel turns backwards</title>\n<meta name=\"description\" content=\"Sample a sine too slowly and a slower one appears: choose the signal's frequency and the sampling rate, and see the samples, the wave they seem to come from, and where every frequency lands.\">\n<meta name=\"isorite:kit\" content=\"1\">\n<meta name=\"isorite:uses\" content=\"\">\n</head>\n<body>\n<!-- Recipe 4: an explorable explanation. An input, numbers derived from it, and linked views that all follow the same\n state; captions say what to see. CHANGE: the inputs (sliders), derive() (the numbers), the drawing, and the\n chart's rows. Keep: every view redrawn from iso.state in one place, update(). -->\n<iso-page>\n <header>\n <h1>Why a fast wheel turns backwards</h1>\n <p>Sampled too slowly, a signal is indistinguishable from a slower one. Which one, and when?</p>\n </header>\n <iso-grid>\n <iso-panel class=\"wide\">\n <h2>The signal and the sampling</h2>\n <iso-row>\n <iso-slider name=\"f\" label=\"signal (Hz)\" min=\"0.2\" max=\"12\" step=\"0.1\" value=\"7\" digits=\"1\"></iso-slider>\n <iso-slider name=\"fs\" label=\"samples per second\" min=\"2\" max=\"24\" step=\"1\" value=\"8\" digits=\"0\"></iso-slider>\n </iso-row>\n <iso-stats>\n <iso-stat id=\"nyquist\" label=\"Nyquist limit (Hz)\" tone=\"blue\"></iso-stat>\n <iso-stat id=\"apparent\" label=\"what the samples show (Hz)\" tone=\"gold\"></iso-stat>\n <iso-stat id=\"verdict\" label=\"faithful?\"></iso-stat>\n </iso-stats>\n </iso-panel>\n <iso-panel class=\"wide\">\n <h2>One second <span>the signal, its samples, and the slower wave through them</span></h2>\n <iso-canvas id=\"wave\" height=\"260\"></iso-canvas>\n <p>Gold dots are the samples. Above the Nyquist limit the blue wave, slower, passes through every one of them as\n well as the true signal does: from the samples alone, the two can't be told apart.</p>\n </iso-panel>\n <iso-panel class=\"wide\">\n <h2>Where every frequency lands</h2>\n <iso-chart id=\"fold\" x=\"signal\" y=\"apparent\" at=\"f\" x-label=\"signal (Hz)\" y-label=\"frequency the samples show (Hz)\" height=\"220\"></iso-chart>\n <p>The frequency seen climbs with the signal's up to half the sampling rate, then folds back down:\n $f_\\text{seen} = |f - f_s \\operatorname{round}(f / f_s)|$. See <iso-note href=\"signals/sampling\">sampling</iso-note>.</p>\n </iso-panel>\n </iso-grid>\n</iso-page>\n<script>\n // CHANGE: the numbers derived from the inputs.\n function derive(f, fs) {\n const nyquist = fs / 2;\n const folded = Math.abs(f - fs * Math.round(f / fs));\n return { nyquist, apparent: folded, faithful: f < nyquist };\n }\n\n // CHANGE: the drawing. t is the theme (t.line, t.soft, t.faint, t.lattice, t.growth, t.ui).\n const wave = document.getElementById(\"wave\");\n wave.draw = (ctx, w, h, t) => {\n const { f, fs } = iso.state, d = derive(f, fs), mid = h / 2, amp = h * 0.38;\n const X = (s) => 40 + s * (w - 60), Y = (v) => mid - v * amp;\n ctx.strokeStyle = t.line; ctx.lineWidth = 1;\n ctx.beginPath(); ctx.moveTo(X(0), mid); ctx.lineTo(X(1), mid); ctx.stroke();\n const curve = (freq, colour, width) => {\n ctx.strokeStyle = colour; ctx.lineWidth = width; ctx.beginPath();\n for (let i = 0; i <= 600; i++) { const s = i / 600, v = Math.sin(2 * Math.PI * freq * s); i ? ctx.lineTo(X(s), Y(v)) : ctx.moveTo(X(s), Y(v)); }\n ctx.stroke();\n };\n curve(f, t.soft, 1);\n // The slower wave through the same samples: the signal less the nearest whole number of sampling rates.\n if (!d.faithful) curve(f - fs * Math.round(f / fs), t.lattice, 2);\n ctx.fillStyle = t.growth;\n for (let k = 0; k <= fs; k++) { const s = k / fs; ctx.beginPath(); ctx.arc(X(s), Y(Math.sin(2 * Math.PI * f * s)), 4, 0, 2 * Math.PI); ctx.fill(); }\n ctx.fillStyle = t.faint; ctx.font = `11px ${t.ui}`; ctx.fillText(\"0 s\", X(0) - 8, h - 6); ctx.fillText(\"1 s\", X(1) - 8, h - 6);\n };\n\n // One place redraws every view from the state.\n const fold = document.getElementById(\"fold\");\n function update() {\n const { f, fs } = iso.state, d = derive(f, fs);\n document.getElementById(\"nyquist\").value = iso.format(d.nyquist);\n document.getElementById(\"apparent\").value = iso.format(d.apparent);\n document.getElementById(\"verdict\").value = d.faithful ? \"yes\" : \"no: aliased\";\n // CHANGE: the chart's rows, computed from the state.\n const rows = [];\n for (let s = 0; s <= 12; s += 0.05) rows.push({ signal: +s.toFixed(2), apparent: derive(s, fs).apparent });\n fold.rows = rows;\n wave.redraw();\n }\n iso.watch(update);\n update();\n<\/script>\n</body>\n</html>\n"
|
|
366
|
+
},
|
|
367
|
+
"viewer": {
|
|
368
|
+
"about": "a 3D model's viewer (glTF, GLB, STL, OBJ), with what it is beside it",
|
|
369
|
+
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>The corner bracket</title>\n<meta name=\"description\" content=\"The printed corner bracket in 3D, with its dimensions and the notes on how it was designed and tested.\">\n<meta name=\"isorite:kit\" content=\"1\">\n<meta name=\"isorite:uses\" content=\"bracket.stl\">\n</head>\n<body>\n<!-- Recipe 5: a 3D model's viewer, with what it is beside it. iso-model shows glTF, GLB, STL and OBJ; list the file\n in isorite:uses. CHANGE: the model's file, the numbers, the notes it links to. -->\n<iso-page>\n <header>\n <h1>The corner bracket</h1>\n <p>The part as printed: an L of two plates, 6 cm by 5 cm.</p>\n </header>\n <iso-grid style=\"--cols: 3\">\n <iso-panel class=\"wide\">\n <h2>The part <span>bracket.stl</span></h2>\n <iso-model src=\"bracket.stl\" height=\"380\"></iso-model>\n </iso-panel>\n <iso-panel>\n <h2>Size</h2>\n <iso-stats>\n <iso-stat label=\"long arm (mm)\" value=\"60\" tone=\"blue\"></iso-stat>\n <iso-stat label=\"short arm (mm)\" value=\"50\"></iso-stat>\n <iso-stat label=\"thickness (mm)\" value=\"10\"></iso-stat>\n </iso-stats>\n </iso-panel>\n <iso-panel>\n <h2>Printed</h2>\n <iso-stats>\n <iso-stat label=\"infill\" value=\"40%\"></iso-stat>\n <iso-stat label=\"mass (g)\" value=\"31\" tone=\"gold\"></iso-stat>\n </iso-stats>\n </iso-panel>\n <iso-panel>\n <h2>Notes</h2>\n <p><iso-note href=\"design/bracket-loads\">The loads it takes</iso-note>, and\n <iso-note href=\"design/print-settings\">how it was printed</iso-note>.</p>\n </iso-panel>\n </iso-grid>\n</iso-page>\n</body>\n</html>\n"
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}));
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region packages/kit/src/index.ts
|
|
375
|
+
var init_src$2 = __esmMin((() => {
|
|
376
|
+
init_assemble();
|
|
377
|
+
init_rules();
|
|
378
|
+
init_data();
|
|
379
|
+
init_context_gen();
|
|
380
|
+
}));
|
|
381
|
+
//#endregion
|
|
46
382
|
//#region packages/core/src/text.ts
|
|
47
383
|
/** Compare by code point, as Python sorts strings. */
|
|
48
384
|
function cmp(a, b) {
|
|
@@ -142,7 +478,7 @@ function normpath(p) {
|
|
|
142
478
|
}
|
|
143
479
|
return "/".repeat(initial) + out.join("/") || ".";
|
|
144
480
|
}
|
|
145
|
-
/** SHA-256 as hex, synchronous and dependency-free, so the core runs anywhere. */
|
|
481
|
+
/** SHA-256 as hex, synchronous and dependency-free, so the core runs anywhere: of text (as UTF-8), or of bytes. */
|
|
146
482
|
function sha256(message) {
|
|
147
483
|
const K = new Uint32Array([
|
|
148
484
|
1116352408,
|
|
@@ -210,7 +546,7 @@ function sha256(message) {
|
|
|
210
546
|
3204031479,
|
|
211
547
|
3329325298
|
|
212
548
|
]);
|
|
213
|
-
const data = new TextEncoder().encode(message);
|
|
549
|
+
const data = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
214
550
|
const len = data.length;
|
|
215
551
|
const padded = new Uint8Array(len + 9 + 63 >> 6 << 6);
|
|
216
552
|
padded.set(data);
|
|
@@ -377,7 +713,7 @@ function linkTexts(body) {
|
|
|
377
713
|
return out;
|
|
378
714
|
}
|
|
379
715
|
/** Every link and image in a body, in order: where it points, its title, and whether it is an image
|
|
380
|
-
* (``, which for an artifact or a picture means "shown here", an embed). */
|
|
716
|
+
* (``, which for an artifact or a picture means "shown here", an embed), with an image's alt text. */
|
|
381
717
|
function citeRefs(body) {
|
|
382
718
|
const { tokens } = parse$1(body);
|
|
383
719
|
const out = [];
|
|
@@ -391,7 +727,8 @@ function citeRefs(body) {
|
|
|
391
727
|
else if (c.type === "image") out.push({
|
|
392
728
|
href: c.attrGet("src") ?? "",
|
|
393
729
|
title: c.attrGet("title"),
|
|
394
|
-
embed: true
|
|
730
|
+
embed: true,
|
|
731
|
+
alt: c.content
|
|
395
732
|
});
|
|
396
733
|
}
|
|
397
734
|
return out;
|
|
@@ -446,6 +783,94 @@ var init_markdown = __esmMin((() => {
|
|
|
446
783
|
SPACES = /[\s_]+/gu;
|
|
447
784
|
}));
|
|
448
785
|
//#endregion
|
|
786
|
+
//#region packages/core/src/picture.ts
|
|
787
|
+
/** Width and height in pixels, or null. */
|
|
788
|
+
function pictureSize(bytes, type) {
|
|
789
|
+
const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength), n = bytes.length;
|
|
790
|
+
const at = (i, s) => [...s].every((c, k) => bytes[i + k] === c.charCodeAt(0));
|
|
791
|
+
const ok = (w, h) => w > 0 && h > 0 ? {
|
|
792
|
+
width: w,
|
|
793
|
+
height: h
|
|
794
|
+
} : null;
|
|
795
|
+
try {
|
|
796
|
+
if (type === "image/png" && n >= 24 && at(12, "IHDR")) return ok(v.getUint32(16), v.getUint32(20));
|
|
797
|
+
if (type === "image/gif" && n >= 10 && at(0, "GIF")) return ok(v.getUint16(6, true), v.getUint16(8, true));
|
|
798
|
+
if (type === "image/jpeg") {
|
|
799
|
+
for (let i = 2; i + 9 < n;) {
|
|
800
|
+
if (bytes[i] !== 255) return null;
|
|
801
|
+
const m = bytes[i + 1];
|
|
802
|
+
if (m >= 192 && m <= 207 && m !== 196 && m !== 200 && m !== 204) return ok(v.getUint16(i + 7), v.getUint16(i + 5));
|
|
803
|
+
i += 2 + v.getUint16(i + 2);
|
|
804
|
+
}
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
if (type === "image/webp" && n >= 30 && at(0, "RIFF") && at(8, "WEBP")) {
|
|
808
|
+
if (at(12, "VP8 ")) return ok(v.getUint16(26, true) & 16383, v.getUint16(28, true) & 16383);
|
|
809
|
+
if (at(12, "VP8L")) {
|
|
810
|
+
const b = v.getUint32(21, true);
|
|
811
|
+
return ok((b & 16383) + 1, (b >> 14 & 16383) + 1);
|
|
812
|
+
}
|
|
813
|
+
if (at(12, "VP8X")) return ok(1 + (bytes[24] | bytes[25] << 8 | bytes[26] << 16), 1 + (bytes[27] | bytes[28] << 8 | bytes[29] << 16));
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
if (type === "image/svg+xml") {
|
|
817
|
+
const head = new TextDecoder().decode(bytes.subarray(0, 4096)), tag = /<svg\b[^>]*>/i.exec(head)?.[0] ?? "";
|
|
818
|
+
const attr = (name) => new RegExp(`\\s${name}\\s*=\\s*["']\\s*([\\d.]+)\\s*(px)?\\s*["']`, "i").exec(tag);
|
|
819
|
+
const w = attr("width"), h = attr("height");
|
|
820
|
+
if (w && h) return ok(Math.round(Number(w[1])), Math.round(Number(h[1])));
|
|
821
|
+
const box = /\sviewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(tag);
|
|
822
|
+
return box ? ok(Math.round(Number(box[1])), Math.round(Number(box[2]))) : null;
|
|
823
|
+
}
|
|
824
|
+
} catch {}
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
var TYPES$1, pictureType, OTHER, fileType;
|
|
828
|
+
var init_picture = __esmMin((() => {
|
|
829
|
+
TYPES$1 = {
|
|
830
|
+
png: "image/png",
|
|
831
|
+
jpg: "image/jpeg",
|
|
832
|
+
jpeg: "image/jpeg",
|
|
833
|
+
gif: "image/gif",
|
|
834
|
+
svg: "image/svg+xml",
|
|
835
|
+
webp: "image/webp",
|
|
836
|
+
avif: "image/avif"
|
|
837
|
+
};
|
|
838
|
+
pictureType = (path) => {
|
|
839
|
+
const dot = path.lastIndexOf(".");
|
|
840
|
+
return dot < 0 || path.lastIndexOf("/") > dot ? null : TYPES$1[path.slice(dot + 1).toLowerCase()] ?? null;
|
|
841
|
+
};
|
|
842
|
+
OTHER = {
|
|
843
|
+
html: "text/html",
|
|
844
|
+
htm: "text/html",
|
|
845
|
+
csv: "text/csv",
|
|
846
|
+
tsv: "text/tab-separated-values",
|
|
847
|
+
json: "application/json",
|
|
848
|
+
jsonl: "application/jsonl",
|
|
849
|
+
ndjson: "application/x-ndjson",
|
|
850
|
+
txt: "text/plain",
|
|
851
|
+
tex: "text/plain",
|
|
852
|
+
pdf: "application/pdf",
|
|
853
|
+
glb: "model/gltf-binary",
|
|
854
|
+
gltf: "model/gltf+json",
|
|
855
|
+
stl: "model/stl",
|
|
856
|
+
obj: "model/obj",
|
|
857
|
+
zip: "application/zip",
|
|
858
|
+
parquet: "application/vnd.apache.parquet",
|
|
859
|
+
arrow: "application/vnd.apache.arrow.file",
|
|
860
|
+
npy: "application/octet-stream",
|
|
861
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
862
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
863
|
+
epub: "application/epub+zip",
|
|
864
|
+
odt: "application/vnd.oasis.opendocument.text"
|
|
865
|
+
};
|
|
866
|
+
fileType = (path) => {
|
|
867
|
+
const dot = path.lastIndexOf(".");
|
|
868
|
+
if (dot < 0 || path.lastIndexOf("/") > dot) return null;
|
|
869
|
+
const ext = path.slice(dot + 1).toLowerCase();
|
|
870
|
+
return ext === "md" ? null : pictureType(path) ?? OTHER[ext] ?? "application/octet-stream";
|
|
871
|
+
};
|
|
872
|
+
}));
|
|
873
|
+
//#endregion
|
|
449
874
|
//#region packages/core/src/kinds.ts
|
|
450
875
|
function kindOf(type) {
|
|
451
876
|
const t = type.trim().toLowerCase();
|
|
@@ -518,11 +943,25 @@ function outwardLinks(body) {
|
|
|
518
943
|
}
|
|
519
944
|
seen.add(href);
|
|
520
945
|
}
|
|
946
|
+
for (const { href, embed, alt } of citeRefs(body)) {
|
|
947
|
+
if (!embed || seen.has(href)) continue;
|
|
948
|
+
const c = codeTarget(href);
|
|
949
|
+
if (!c || !pictureType(c.path)) continue;
|
|
950
|
+
out.push({
|
|
951
|
+
kind: "code",
|
|
952
|
+
href,
|
|
953
|
+
text: alt || c.path.split("/").at(-1),
|
|
954
|
+
path: c.path,
|
|
955
|
+
picture: true
|
|
956
|
+
});
|
|
957
|
+
seen.add(href);
|
|
958
|
+
}
|
|
521
959
|
return out;
|
|
522
960
|
}
|
|
523
961
|
var ABOUT, DONE, EMBED;
|
|
524
962
|
var init_kinds = __esmMin((() => {
|
|
525
963
|
init_markdown();
|
|
964
|
+
init_picture();
|
|
526
965
|
init_text();
|
|
527
966
|
ABOUT = {
|
|
528
967
|
question: "about",
|
|
@@ -540,16 +979,24 @@ var init_kinds = __esmMin((() => {
|
|
|
540
979
|
function comparePaths(a, b) {
|
|
541
980
|
return cmpTuple(a.split("/"), b.split("/"));
|
|
542
981
|
}
|
|
543
|
-
var AGENTS_FILE,
|
|
982
|
+
var AGENTS_FILE, KINDS$2, citeKind, isEntry, Concept, Directory, SCHEME, LOG_DATE, Bundle, oneLine, escape;
|
|
544
983
|
var init_bundle = __esmMin((() => {
|
|
545
984
|
init_frontmatter();
|
|
546
985
|
init_markdown();
|
|
547
986
|
init_kinds();
|
|
548
987
|
init_text();
|
|
549
988
|
AGENTS_FILE = "AGENTS.md";
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
989
|
+
KINDS$2 = [
|
|
990
|
+
["artifact", /\.html?$/i],
|
|
991
|
+
["image", /\.(png|jpe?g|gif|svg|webp|avif)$/i],
|
|
992
|
+
["data", /\.(csv|tsv|json|jsonl|ndjson|parquet|arrow|feather|npy|npz|xlsx|xls|h5|hdf5|nc)$/i],
|
|
993
|
+
["document", /\.(pdf|docx?|odt|rtf|epub|pptx?|odp|txt|tex)$/i]
|
|
994
|
+
];
|
|
995
|
+
citeKind = (path) => {
|
|
996
|
+
const name = basename$1(path);
|
|
997
|
+
if (/\.md$/i.test(name) || !/\.[^.]+$/.test(name.replace(/^\./, ""))) return null;
|
|
998
|
+
return KINDS$2.find(([, re]) => re.test(name))?.[0] ?? "misc";
|
|
999
|
+
};
|
|
553
1000
|
isEntry = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
554
1001
|
Concept = class {
|
|
555
1002
|
id;
|
|
@@ -725,7 +1172,7 @@ var init_bundle = __esmMin((() => {
|
|
|
725
1172
|
concept.links = links;
|
|
726
1173
|
concept.outward = outwardLinks(concept.body);
|
|
727
1174
|
for (const link of links) if (link.broken) this.issue(concept.path, "warning", "broken-link", `broken link to ${link.target}`);
|
|
728
|
-
for (const { href, title, embed } of citeRefs(concept.body)) {
|
|
1175
|
+
for (const { href, title, embed, alt } of citeRefs(concept.body)) {
|
|
729
1176
|
const path = this.filePath(concept, href), kind = path === null ? null : citeKind(path);
|
|
730
1177
|
if (path === null || kind === null) continue;
|
|
731
1178
|
const t = strip((title ?? "").toLowerCase());
|
|
@@ -735,10 +1182,11 @@ var init_bundle = __esmMin((() => {
|
|
|
735
1182
|
embed,
|
|
736
1183
|
broken: !this.files.has(path),
|
|
737
1184
|
rel: rating(title),
|
|
738
|
-
align: t === "left" ? "left" : t === "center" || t === "centre" ? "center" : null
|
|
1185
|
+
align: t === "left" ? "left" : t === "center" || t === "centre" ? "center" : null,
|
|
1186
|
+
...embed && alt ? { alt } : {}
|
|
739
1187
|
};
|
|
740
1188
|
concept.cites.push(cite);
|
|
741
|
-
if (cite.broken) this.issue(concept.path, "warning", "broken-link", `broken link to ${path}`);
|
|
1189
|
+
if (cite.broken) this.issue(concept.path, "warning", "broken-link", kind === "image" ? `picture not in the notes: ${path}` : `broken link to ${path}`);
|
|
742
1190
|
}
|
|
743
1191
|
}
|
|
744
1192
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -1809,13 +2257,60 @@ function mapModel(base) {
|
|
|
1809
2257
|
s
|
|
1810
2258
|
]);
|
|
1811
2259
|
}
|
|
2260
|
+
const holdsFiles = /* @__PURE__ */ new Set();
|
|
2261
|
+
const parentOf = (p) => p.includes("/") ? p.slice(0, p.lastIndexOf("/")) : "";
|
|
2262
|
+
for (const f of base.files) holdsFiles.add(parentOf(f));
|
|
2263
|
+
const pictureFolder = (d) => {
|
|
2264
|
+
const dd = base.directories.get(d);
|
|
2265
|
+
const here = [...base.files].filter((f) => parentOf(f) === d);
|
|
2266
|
+
return !!d && (!dd || !dd.concepts.length && !dd.children.length) && here.length > 0 && here.every((f) => citeKind(f) === "image");
|
|
2267
|
+
};
|
|
2268
|
+
const refOf = (f) => citeKind(f) === "image" && pictureFolder(parentOf(f)) ? parentOf(f) + "/" : f;
|
|
2269
|
+
const fileEdges = /* @__PURE__ */ new Map();
|
|
2270
|
+
for (const c of base.concepts.values()) for (const x of c.cites) {
|
|
2271
|
+
if (x.broken || !base.files.has(x.target)) continue;
|
|
2272
|
+
const ref = refOf(x.target);
|
|
2273
|
+
if (!fileEdges.has(c.id)) fileEdges.set(c.id, /* @__PURE__ */ new Set());
|
|
2274
|
+
fileEdges.get(c.id).add(ref);
|
|
2275
|
+
}
|
|
2276
|
+
const rankOf = /* @__PURE__ */ new Map();
|
|
2277
|
+
for (const [from, refs] of fileEdges) for (const ref of refs) {
|
|
2278
|
+
edges.push([
|
|
2279
|
+
from,
|
|
2280
|
+
ref,
|
|
2281
|
+
STRENGTH.uses
|
|
2282
|
+
]);
|
|
2283
|
+
rankOf.set(ref, Math.max(rankOf.get(ref) ?? 0, order.get(from)?.depth ?? 0));
|
|
2284
|
+
}
|
|
2285
|
+
const filesIn = /* @__PURE__ */ new Map();
|
|
2286
|
+
for (const f of [...base.files].sort()) {
|
|
2287
|
+
const of = citeKind(f), ref = of ? refOf(f) : null;
|
|
2288
|
+
if (!of || !ref) continue;
|
|
2289
|
+
const at = ref.endsWith("/") ? parentOf(ref.slice(0, -1)) : parentOf(f);
|
|
2290
|
+
const list = filesIn.get(at) ?? [];
|
|
2291
|
+
if (!list.some((n) => n.ref === ref)) list.push({
|
|
2292
|
+
kind: "concept",
|
|
2293
|
+
id: "f:" + ref,
|
|
2294
|
+
ref,
|
|
2295
|
+
weight: 1,
|
|
2296
|
+
rank: rankOf.get(ref) ?? 0,
|
|
2297
|
+
of
|
|
2298
|
+
});
|
|
2299
|
+
filesIn.set(at, list);
|
|
2300
|
+
}
|
|
1812
2301
|
const dir = (id) => {
|
|
1813
2302
|
const d = base.directories.get(id);
|
|
1814
|
-
return
|
|
2303
|
+
if (id && pictureFolder(id)) return null;
|
|
2304
|
+
const children = [
|
|
2305
|
+
...[...d.children].sort().map(dir).filter((c) => c !== null),
|
|
2306
|
+
...[...d.concepts].sort().map(note),
|
|
2307
|
+
...filesIn.get(id) ?? []
|
|
2308
|
+
];
|
|
2309
|
+
return id && !children.length && (holdsFiles.has(id) || d.children.length) ? null : {
|
|
1815
2310
|
kind: "dir",
|
|
1816
2311
|
id: "d:" + id,
|
|
1817
2312
|
ref: id,
|
|
1818
|
-
children
|
|
2313
|
+
children
|
|
1819
2314
|
};
|
|
1820
2315
|
};
|
|
1821
2316
|
return {
|
|
@@ -1824,12 +2319,14 @@ function mapModel(base) {
|
|
|
1824
2319
|
};
|
|
1825
2320
|
}
|
|
1826
2321
|
var init_map = __esmMin((() => {
|
|
2322
|
+
init_bundle();
|
|
1827
2323
|
init_graph();
|
|
1828
2324
|
}));
|
|
1829
2325
|
//#endregion
|
|
1830
2326
|
//#region packages/core/src/index.ts
|
|
1831
2327
|
var init_src$1 = __esmMin((() => {
|
|
1832
2328
|
init_bundle();
|
|
2329
|
+
init_picture();
|
|
1833
2330
|
init_frontmatter();
|
|
1834
2331
|
init_source();
|
|
1835
2332
|
init_markdown();
|
|
@@ -1847,6 +2344,7 @@ var init_src$1 = __esmMin((() => {
|
|
|
1847
2344
|
}));
|
|
1848
2345
|
//#endregion
|
|
1849
2346
|
//#region packages/core/src/node.ts
|
|
2347
|
+
init_src$2();
|
|
1850
2348
|
init_src$1();
|
|
1851
2349
|
init_bundle();
|
|
1852
2350
|
init_text();
|
|
@@ -1884,8 +2382,9 @@ function loadBundle(root) {
|
|
|
1884
2382
|
}
|
|
1885
2383
|
//#endregion
|
|
1886
2384
|
//#region packages/engine/src/api.ts
|
|
1887
|
-
var EngineError, Conflict;
|
|
2385
|
+
var MAX_FILE_BYTES, EngineError, Conflict;
|
|
1888
2386
|
var init_api = __esmMin((() => {
|
|
2387
|
+
MAX_FILE_BYTES = 2e7;
|
|
1889
2388
|
EngineError = class extends Error {
|
|
1890
2389
|
status;
|
|
1891
2390
|
constructor(message, status = 400) {
|
|
@@ -2032,7 +2531,8 @@ async function completeOnce(http, p, key, call, heard) {
|
|
|
2032
2531
|
...call.tools?.length ? {
|
|
2033
2532
|
tools: call.tools,
|
|
2034
2533
|
tool_choice: call.toolChoice ?? "auto"
|
|
2035
|
-
} : {}
|
|
2534
|
+
} : {},
|
|
2535
|
+
...call.effort && p.name === OPENROUTER.name ? { reasoning: { effort: call.effort } } : {}
|
|
2036
2536
|
});
|
|
2037
2537
|
let res;
|
|
2038
2538
|
try {
|
|
@@ -2290,13 +2790,14 @@ async function rounds(o) {
|
|
|
2290
2790
|
let firstIn = 0;
|
|
2291
2791
|
for (let round = 0, calls = 1;; round++, calls++) {
|
|
2292
2792
|
let first = true;
|
|
2293
|
-
const ask = (maxTokens) => complete(o.http, o.provider, o.key, {
|
|
2793
|
+
const ask = (maxTokens, effort) => complete(o.http, o.provider, o.key, {
|
|
2294
2794
|
model,
|
|
2295
2795
|
messages,
|
|
2296
2796
|
maxTokens,
|
|
2297
2797
|
tools: most ? o.lookup.defs() : [],
|
|
2298
2798
|
toolChoice: round < most ? "auto" : "none",
|
|
2299
2799
|
signal: o.signal,
|
|
2800
|
+
...effort ? { effort } : {},
|
|
2300
2801
|
onText: (piece) => {
|
|
2301
2802
|
o.onText(piece, first && round > 0);
|
|
2302
2803
|
first = false;
|
|
@@ -2313,10 +2814,14 @@ async function rounds(o) {
|
|
|
2313
2814
|
add(r);
|
|
2314
2815
|
if (round === 0) firstIn = r.usage.tokensIn;
|
|
2315
2816
|
if (empty(r)) {
|
|
2316
|
-
r = await ask(o.maxTokens * 3);
|
|
2817
|
+
r = await ask(o.maxTokens * 3, "low");
|
|
2317
2818
|
add(r);
|
|
2318
2819
|
if (empty(r)) throw new ModelError(`${model} used its whole output budget (${Math.max(o.maxTokens * 3, o.provider.maxTokens ?? 0)} tokens) thinking, and wrote nothing. Try a lower tier, or raise max_tokens under [provider] in config.toml.`, 502, { kind: "cut" });
|
|
2319
2820
|
}
|
|
2821
|
+
if (!r.text.trim() && !r.calls.length && !r.cut) {
|
|
2822
|
+
r = await ask(o.maxTokens);
|
|
2823
|
+
add(r);
|
|
2824
|
+
}
|
|
2320
2825
|
if (!r.calls.length || round >= most) return {
|
|
2321
2826
|
text: r.text,
|
|
2322
2827
|
usage,
|
|
@@ -2699,6 +3204,9 @@ something at one place in the note they have open.`;
|
|
|
2699
3204
|
is drawn as the diagram. Draw one where a picture of how things stand to
|
|
2700
3205
|
each other helps, or when asked; not by habit. Maths in a label goes
|
|
2701
3206
|
between $$ and $$.
|
|
3207
|
+
- A picture is shown as . You can't see it: what
|
|
3208
|
+
you know of it is the words in the brackets. Keep a picture's line as it is
|
|
3209
|
+
when you rewrite around it, and never make up a picture's path.
|
|
2702
3210
|
- Ordinary Markdown otherwise. No HTML.`;
|
|
2703
3211
|
FORMAT_RULES = FORMAT;
|
|
2704
3212
|
LOOKUP = (rounds, meaning) => `# Looking things up
|
|
@@ -2807,7 +3315,7 @@ var init_lookup = __esmMin((() => {
|
|
|
2807
3315
|
...str$1,
|
|
2808
3316
|
description: "The note's id or path: basics/entropy or /basics/entropy.md."
|
|
2809
3317
|
} }, ["id"]),
|
|
2810
|
-
fn$1("read_note", "Read one section of a note, by its heading, or the whole note when no section is named. Prefer a section.", {
|
|
3318
|
+
fn$1("read_note", "Read one section of a note, by its heading, or the whole note when no section is named. Prefer a section. A picture in it is : you have its words, not the picture.", {
|
|
2811
3319
|
id: str$1,
|
|
2812
3320
|
section: {
|
|
2813
3321
|
...str$1,
|
|
@@ -3229,11 +3737,12 @@ function turnMessages(a) {
|
|
|
3229
3737
|
PRACTICE,
|
|
3230
3738
|
LEARNING,
|
|
3231
3739
|
MODE_RULES[a.mode],
|
|
3740
|
+
...a.mode === "write" ? [ARTIFACTS] : [],
|
|
3232
3741
|
ASKING(a.mode === "write"),
|
|
3233
3742
|
...checking ? [CHECKING] : [],
|
|
3234
3743
|
...understood.length ? [UNDERSTANDING] : [],
|
|
3235
3744
|
FORMAT_RULES,
|
|
3236
|
-
LOOKING(ROUNDS[a.tier], a.meaning, a.lookup)
|
|
3745
|
+
LOOKING(a.mode === "write" ? Math.max(ROUNDS[a.tier], 6) : ROUNDS[a.tier], a.meaning, a.lookup)
|
|
3237
3746
|
].join("\n\n");
|
|
3238
3747
|
const briefed = a.briefings?.length ? `# Earlier in this conversation, summed up\n\n${a.briefings.join("\n\n---\n\n")}` : "";
|
|
3239
3748
|
const talk = a.earlier.map((t) => turnExchange(t).text).join("\n\n---\n\n");
|
|
@@ -3312,7 +3821,7 @@ function turnMessages(a) {
|
|
|
3312
3821
|
]
|
|
3313
3822
|
};
|
|
3314
3823
|
}
|
|
3315
|
-
var MODES, ITEMS, CONVERSATIONS_DIR, conversationPath, WHO, ISORITE, PRACTICE, LEARNING, ASK, WRITE, UNDERSTANDING, ASKING, CHECKING, MODE_RULES, LOOKING, COMPACT$1, str, fn, POINT_TO_NOTE, EDIT_NOTE$1, WRITE_NOTE$1, MOVE_NOTE, DELETE_NOTE, WRITE_UNDERSTANDING, ASK_QUESTION, OFFER_TEXT, MODE_TOOLS;
|
|
3824
|
+
var MODES, ITEMS, CONVERSATIONS_DIR, conversationPath, WHO, ISORITE, PRACTICE, LEARNING, ASK, WRITE, UNDERSTANDING, ARTIFACTS, ASKING, CHECKING, MODE_RULES, LOOKING, COMPACT$1, str, fn, POINT_TO_NOTE, EDIT_NOTE$1, WRITE_NOTE$1, MOVE_NOTE, DELETE_NOTE, WRITE_UNDERSTANDING, ASK_QUESTION, READ_DATA, ARTIFACT_RECIPE, WRITE_ARTIFACT, READ_ARTIFACT, OFFER_TEXT, MODE_TOOLS;
|
|
3316
3825
|
var init_turn = __esmMin((() => {
|
|
3317
3826
|
init_iso();
|
|
3318
3827
|
init_loop();
|
|
@@ -3350,6 +3859,11 @@ what you change is shown to them there as you do it.`;
|
|
|
3350
3859
|
problem note is practice that \`uses\` notes. Theirs alone to mark:
|
|
3351
3860
|
\`resolved\` or \`solved\`, with the day. \`[[!name]]\` alone on a line
|
|
3352
3861
|
embeds one in a note.
|
|
3862
|
+
- A problem of several steps is several problem notes, each linked to the one
|
|
3863
|
+
before it as "requires", in a folder of their own when they belong
|
|
3864
|
+
together. A problem has no steps inside it.
|
|
3865
|
+
- Files beside the notes that are not notes (data, documents, pictures,
|
|
3866
|
+
artifacts) are linked as any note is: [runs](/simulation/runs.csv).
|
|
3353
3867
|
- Every change you make is marked as yours, in the note's history, and they
|
|
3354
3868
|
can undo it.
|
|
3355
3869
|
|
|
@@ -3435,6 +3949,22 @@ far, from its Understanding note.
|
|
|
3435
3949
|
linked: what they wrote in a note, a question of theirs open or resolved,
|
|
3436
3950
|
a problem solved or not. Say nothing the evidence does not show.
|
|
3437
3951
|
- Never invent a note, a link or a quotation.`;
|
|
3952
|
+
ARTIFACTS = `# Artifacts
|
|
3953
|
+
|
|
3954
|
+
An artifact is an HTML page beside their notes that isorite shows working: a
|
|
3955
|
+
dashboard of their data, a comparison, a simulation, an explorable
|
|
3956
|
+
explanation, a 3D model's viewer. Make one when they ask for something to see
|
|
3957
|
+
or try; not for what an answer or a note says as well.
|
|
3958
|
+
|
|
3959
|
+
1. artifact_recipe with the recipe nearest what they asked: dashboard,
|
|
3960
|
+
compare, simulation, explorable or viewer. It gives a complete working
|
|
3961
|
+
artifact and the kit's reference. Start from the recipe and change what it
|
|
3962
|
+
marks; keep the rest.
|
|
3963
|
+
2. read_data for every data file it will read. Use the columns as given.
|
|
3964
|
+
3. write_artifact with the whole file. It is checked and run. If it reports a
|
|
3965
|
+
problem, fix that and write the whole file again.
|
|
3966
|
+
4. Say in a sentence what it shows, linked: [what it shows](/folder/name.html).
|
|
3967
|
+
Link it from the note it belongs to if they want it there.`;
|
|
3438
3968
|
ASKING = (write) => `# Asking back, and their own answers
|
|
3439
3969
|
|
|
3440
3970
|
- Where a question to them would do more than an answer (what is unclear in
|
|
@@ -3582,6 +4112,50 @@ was not said.`;
|
|
|
3582
4112
|
"text",
|
|
3583
4113
|
"about"
|
|
3584
4114
|
]);
|
|
4115
|
+
READ_DATA = fn("read_data", "A data file's shape: its columns with their types and ranges, the row count and the first rows. Read it before writing anything that uses the file, so the column names are real.", { path: {
|
|
4116
|
+
...str,
|
|
4117
|
+
description: "The file's path in the base: folder/name.csv."
|
|
4118
|
+
} }, ["path"]);
|
|
4119
|
+
ARTIFACT_RECIPE = fn("artifact_recipe", "A recipe for an artifact: a complete working one to start from, the kit's reference, and the rules write_artifact checks. dashboard: numbers, charts and a table of one data file. compare: two data files side by side. simulation: a model with parameters and play. explorable: inputs, derived numbers and linked views. viewer: a 3D model.", { name: {
|
|
4120
|
+
type: "string",
|
|
4121
|
+
enum: [
|
|
4122
|
+
"dashboard",
|
|
4123
|
+
"compare",
|
|
4124
|
+
"simulation",
|
|
4125
|
+
"explorable",
|
|
4126
|
+
"viewer"
|
|
4127
|
+
]
|
|
4128
|
+
} }, ["name"]);
|
|
4129
|
+
WRITE_ARTIFACT = fn("write_artifact", "Write an artifact: a whole HTML file on the kit. The title, description and uses given here are written into it. It is checked, and run where it can be; the result says what to fix. Nothing is written while a rule is broken.", {
|
|
4130
|
+
path: {
|
|
4131
|
+
...str,
|
|
4132
|
+
description: "Where: folder/name.html, beside the notes it belongs to."
|
|
4133
|
+
},
|
|
4134
|
+
title: str,
|
|
4135
|
+
description: {
|
|
4136
|
+
...str,
|
|
4137
|
+
description: "One sentence: what it shows, and what can be done with it."
|
|
4138
|
+
},
|
|
4139
|
+
uses: {
|
|
4140
|
+
type: "array",
|
|
4141
|
+
items: str,
|
|
4142
|
+
description: "The data or model files it reads, relative to it (runs.csv). Those its markup and script name are added."
|
|
4143
|
+
},
|
|
4144
|
+
html: {
|
|
4145
|
+
...str,
|
|
4146
|
+
description: "The whole file."
|
|
4147
|
+
}
|
|
4148
|
+
}, [
|
|
4149
|
+
"path",
|
|
4150
|
+
"title",
|
|
4151
|
+
"description",
|
|
4152
|
+
"uses",
|
|
4153
|
+
"html"
|
|
4154
|
+
]);
|
|
4155
|
+
READ_ARTIFACT = fn("read_artifact", "An artifact's whole file, to change it and write it again.", { path: {
|
|
4156
|
+
...str,
|
|
4157
|
+
description: "folder/name.html"
|
|
4158
|
+
} }, ["path"]);
|
|
3585
4159
|
OFFER_TEXT = fn("offer_text", "Offer them their own answers, lightly joined, to insert at the end of a note as their writing. Only their words.", {
|
|
3586
4160
|
id: {
|
|
3587
4161
|
...str,
|
|
@@ -3596,7 +4170,8 @@ was not said.`;
|
|
|
3596
4170
|
ask: [
|
|
3597
4171
|
POINT_TO_NOTE,
|
|
3598
4172
|
ASK_QUESTION,
|
|
3599
|
-
OFFER_TEXT
|
|
4173
|
+
OFFER_TEXT,
|
|
4174
|
+
READ_DATA
|
|
3600
4175
|
],
|
|
3601
4176
|
write: [
|
|
3602
4177
|
EDIT_NOTE$1,
|
|
@@ -3605,7 +4180,11 @@ was not said.`;
|
|
|
3605
4180
|
DELETE_NOTE,
|
|
3606
4181
|
POINT_TO_NOTE,
|
|
3607
4182
|
ASK_QUESTION,
|
|
3608
|
-
OFFER_TEXT
|
|
4183
|
+
OFFER_TEXT,
|
|
4184
|
+
READ_DATA,
|
|
4185
|
+
ARTIFACT_RECIPE,
|
|
4186
|
+
WRITE_ARTIFACT,
|
|
4187
|
+
READ_ARTIFACT
|
|
3609
4188
|
]
|
|
3610
4189
|
};
|
|
3611
4190
|
}));
|
|
@@ -3810,6 +4389,11 @@ function checkNoteId(id) {
|
|
|
3810
4389
|
if (!id || id.length > 400 || parts.some((p) => !p || BAD_PART.test(p))) throw new EngineError(`not a note's id: ${JSON.stringify(id)}`);
|
|
3811
4390
|
return id;
|
|
3812
4391
|
}
|
|
4392
|
+
/** A file's path in the base as Iso gives one: no leading slash, no "..", nothing hidden. */
|
|
4393
|
+
function filePathOf(raw) {
|
|
4394
|
+
const p = String(raw ?? "").trim().replace(/^\/+/, "");
|
|
4395
|
+
return p && !p.split("/").some((x) => !x || x === "." || x === ".." || x.startsWith(".")) ? p : null;
|
|
4396
|
+
}
|
|
3813
4397
|
async function walk(files, root) {
|
|
3814
4398
|
const out = [];
|
|
3815
4399
|
const at = (path) => root && path ? `${root}/${path}` : root || path;
|
|
@@ -3847,7 +4431,7 @@ function keepDone(before, next) {
|
|
|
3847
4431
|
return next;
|
|
3848
4432
|
}
|
|
3849
4433
|
}
|
|
3850
|
-
var SIGN_IN_MS, fromBase, REPLY_TOKENS, NOTE_TOKENS, DRAFT_PREFIX, DRAFT_DIR, isDraftId, bare, inBase, BAD_PART, TURN_ID, escapeRe, MAX_SENT, BRIEF, NOTE_ARG, MARKED, EDIT_NOTE, WRITE_NOTE, Engine;
|
|
4434
|
+
var SIGN_IN_MS, fromBase, REPLY_TOKENS, NOTE_TOKENS, ARTIFACT_TOKENS, DRAFT_PREFIX, DRAFT_DIR, isDraftId, bare, inBase, BAD_PART, TURN_ID, escapeRe, MAX_SENT, BRIEF, NOTE_ARG, MARKED, EDIT_NOTE, WRITE_NOTE, Engine;
|
|
3851
4435
|
var init_engine = __esmMin((() => {
|
|
3852
4436
|
init_src$1();
|
|
3853
4437
|
init_api();
|
|
@@ -3863,6 +4447,7 @@ var init_engine = __esmMin((() => {
|
|
|
3863
4447
|
fromBase = (base, text) => `(From the base ${base}: name its notes ${base}:<id>, or give base "${base}".)\n${text}`;
|
|
3864
4448
|
REPLY_TOKENS = 2500;
|
|
3865
4449
|
NOTE_TOKENS = 6e3;
|
|
4450
|
+
ARTIFACT_TOKENS = 32e3;
|
|
3866
4451
|
DRAFT_PREFIX = "@draft/";
|
|
3867
4452
|
DRAFT_DIR = "drafts";
|
|
3868
4453
|
isDraftId = (id) => id.startsWith(DRAFT_PREFIX);
|
|
@@ -4030,23 +4615,91 @@ var init_engine = __esmMin((() => {
|
|
|
4030
4615
|
const notes = {}, broken = await this.brokenCode();
|
|
4031
4616
|
for (const c of this.bundle.concepts.values()) {
|
|
4032
4617
|
const bad = broken.get(c.id) ?? [];
|
|
4033
|
-
const links = c.outward.map((o) => ({
|
|
4618
|
+
const links = c.outward.filter((o) => !(o.kind === "code" && o.picture)).map((o) => ({
|
|
4034
4619
|
kind: o.kind,
|
|
4035
4620
|
href: o.href,
|
|
4036
4621
|
text: o.text,
|
|
4037
4622
|
...bad.includes(o.href) ? { broken: true } : {}
|
|
4038
4623
|
}));
|
|
4624
|
+
const pictures = [], shown = /* @__PURE__ */ new Set();
|
|
4625
|
+
for (const p of c.cites) {
|
|
4626
|
+
if (p.kind !== "image" || !p.embed || shown.has(p.target)) continue;
|
|
4627
|
+
shown.add(p.target);
|
|
4628
|
+
pictures.push({
|
|
4629
|
+
href: p.target,
|
|
4630
|
+
path: p.target,
|
|
4631
|
+
from: "notes",
|
|
4632
|
+
text: p.alt || p.target.split("/").at(-1),
|
|
4633
|
+
...p.broken ? { broken: true } : {}
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
4636
|
+
for (const o of c.outward) {
|
|
4637
|
+
if (o.kind !== "code" || !o.picture) continue;
|
|
4638
|
+
pictures.push({
|
|
4639
|
+
href: o.href,
|
|
4640
|
+
path: o.path,
|
|
4641
|
+
from: "code",
|
|
4642
|
+
text: o.text,
|
|
4643
|
+
...bad.includes(o.href) ? { broken: true } : {}
|
|
4644
|
+
});
|
|
4645
|
+
}
|
|
4039
4646
|
notes[c.id] = {
|
|
4040
4647
|
title: c.title,
|
|
4041
4648
|
tokens: estimate(c.body.trim()),
|
|
4042
4649
|
...c.kind !== "note" ? { kind: c.kind } : {},
|
|
4043
4650
|
...c.done ? { done: c.done } : {},
|
|
4044
|
-
...links.length ? { links } : {}
|
|
4651
|
+
...links.length ? { links } : {},
|
|
4652
|
+
...pictures.length ? { pictures } : {}
|
|
4045
4653
|
};
|
|
4046
4654
|
}
|
|
4655
|
+
const model = mapModel(this.bundle), files = {};
|
|
4656
|
+
const items = [];
|
|
4657
|
+
const walk = (d) => {
|
|
4658
|
+
for (const c of d.children) if (c.kind === "dir") walk(c);
|
|
4659
|
+
else if (c.id.startsWith("f:")) items.push(c.ref);
|
|
4660
|
+
};
|
|
4661
|
+
walk(model.root);
|
|
4662
|
+
const all = new Set(items);
|
|
4663
|
+
for (const ref of items) {
|
|
4664
|
+
if (ref.endsWith("/")) {
|
|
4665
|
+
const count = [...this.bundle.files].filter((f) => f.startsWith(ref) && !f.slice(ref.length).includes("/")).length;
|
|
4666
|
+
files[ref] = {
|
|
4667
|
+
title: `${ref.slice(0, -1).split("/").at(-1)} · ${count} ${count === 1 ? "picture" : "pictures"}`,
|
|
4668
|
+
kind: "image",
|
|
4669
|
+
count
|
|
4670
|
+
};
|
|
4671
|
+
continue;
|
|
4672
|
+
}
|
|
4673
|
+
const kind = citeKind(ref), name = ref.split("/").at(-1);
|
|
4674
|
+
if (kind !== "artifact") {
|
|
4675
|
+
files[ref] = {
|
|
4676
|
+
title: name,
|
|
4677
|
+
kind
|
|
4678
|
+
};
|
|
4679
|
+
continue;
|
|
4680
|
+
}
|
|
4681
|
+
const html = await this.opts.notes.read(ref).catch(() => null), m = html ? manifestOf(html) : null;
|
|
4682
|
+
files[ref] = {
|
|
4683
|
+
title: m?.title || name,
|
|
4684
|
+
kind
|
|
4685
|
+
};
|
|
4686
|
+
const dir = ref.split("/").slice(0, -1);
|
|
4687
|
+
for (const u of m?.uses ?? []) {
|
|
4688
|
+
const parts = [...dir];
|
|
4689
|
+
for (const p of u.split("/")) if (p === "..") parts.pop();
|
|
4690
|
+
else if (p && p !== ".") parts.push(p);
|
|
4691
|
+
const to = parts.join("/");
|
|
4692
|
+
if (all.has(to) && !model.edges.some(([a, b]) => a === ref && b === to)) model.edges.push([
|
|
4693
|
+
ref,
|
|
4694
|
+
to,
|
|
4695
|
+
STRENGTH.uses
|
|
4696
|
+
]);
|
|
4697
|
+
}
|
|
4698
|
+
}
|
|
4047
4699
|
return {
|
|
4048
|
-
model
|
|
4049
|
-
notes
|
|
4700
|
+
model,
|
|
4701
|
+
notes,
|
|
4702
|
+
...items.length ? { files } : {}
|
|
4050
4703
|
};
|
|
4051
4704
|
}
|
|
4052
4705
|
/** The base's settings, from `.isorite/settings.toml`. */
|
|
@@ -4400,7 +5053,7 @@ var init_engine = __esmMin((() => {
|
|
|
4400
5053
|
}
|
|
4401
5054
|
async deleteNote(id, base, who = "you") {
|
|
4402
5055
|
if (!inBase(id)) throw new EngineError("only a note of the base is deleted here");
|
|
4403
|
-
const
|
|
5056
|
+
const { files: notes, path } = this.where(id);
|
|
4404
5057
|
await this.one(async () => {
|
|
4405
5058
|
const now = await notes.read(path);
|
|
4406
5059
|
if (now === null) throw new Conflict(`${id} is gone`, null);
|
|
@@ -4438,8 +5091,113 @@ var init_engine = __esmMin((() => {
|
|
|
4438
5091
|
this.queue = run.catch(() => void 0);
|
|
4439
5092
|
return run;
|
|
4440
5093
|
}
|
|
5094
|
+
/** A file in the notes that isn't a note (M23: any kind; M22 gave pictures only), or a picture in the base's
|
|
5095
|
+
* repository. Within its folder, and (in the notes) never in a hidden folder such as `.isorite/`. */
|
|
5096
|
+
async file(path, from = "notes") {
|
|
5097
|
+
const type = from === "code" ? pictureType(path) : fileType(path);
|
|
5098
|
+
if (!type) return null;
|
|
5099
|
+
let files, at;
|
|
5100
|
+
if (from === "code") {
|
|
5101
|
+
const target = codeTarget(`code:${path}`), where = (await this.settingsOfBase()).repository;
|
|
5102
|
+
files = target && where ? this.opts.repository?.(where) ?? null : null;
|
|
5103
|
+
at = target?.path ?? "";
|
|
5104
|
+
} else {
|
|
5105
|
+
at = path.replace(/^\/+/, "");
|
|
5106
|
+
files = at.split("/").some((p) => !p || p === "." || p === ".." || p.startsWith(".")) ? null : this.opts.notes;
|
|
5107
|
+
}
|
|
5108
|
+
const bytes = files ? await files.readBytes(at) : null;
|
|
5109
|
+
if (!bytes) return null;
|
|
5110
|
+
if (bytes.length > 2e7) throw new EngineError(`${path} is larger than isorite gives (${MAX_FILE_BYTES / 1e6} MB)`, 413);
|
|
5111
|
+
return {
|
|
5112
|
+
bytes,
|
|
5113
|
+
type,
|
|
5114
|
+
version: sha256(bytes).slice(0, 16)
|
|
5115
|
+
};
|
|
5116
|
+
}
|
|
5117
|
+
/** What was read of each picture, kept while its size and time are the same. */
|
|
5118
|
+
pictureCache = /* @__PURE__ */ new Map();
|
|
5119
|
+
/** Every picture in the notes (M22), found afresh: one put in the folder by hand since the base was opened is
|
|
5120
|
+
* among them, and from then on in the bundle too, so a link to it isn't broken. A hidden folder is never looked in. */
|
|
5121
|
+
async pictures() {
|
|
5122
|
+
const found = [];
|
|
5123
|
+
const visit = async (dir) => {
|
|
5124
|
+
for (const f of await this.opts.notes.list(dir)) {
|
|
5125
|
+
if (f.name.startsWith(".")) continue;
|
|
5126
|
+
const path = dir ? `${dir}/${f.name}` : f.name;
|
|
5127
|
+
if (f.dir) await visit(path);
|
|
5128
|
+
else if (pictureType(f.name)) found.push({
|
|
5129
|
+
path,
|
|
5130
|
+
...f.size === void 0 ? {} : { size: f.size },
|
|
5131
|
+
...f.mtime === void 0 ? {} : { mtime: f.mtime }
|
|
5132
|
+
});
|
|
5133
|
+
}
|
|
5134
|
+
};
|
|
5135
|
+
await visit("");
|
|
5136
|
+
const out = [], seen = /* @__PURE__ */ new Set();
|
|
5137
|
+
for (const f of found) {
|
|
5138
|
+
seen.add(f.path);
|
|
5139
|
+
const kept = this.pictureCache.get(f.path);
|
|
5140
|
+
if (kept && f.size !== void 0 && kept.size === f.size && kept.mtime === f.mtime) {
|
|
5141
|
+
out.push(kept.info);
|
|
5142
|
+
continue;
|
|
5143
|
+
}
|
|
5144
|
+
const bytes = f.size !== void 0 && f.size > 2e7 ? null : await this.opts.notes.readBytes(f.path);
|
|
5145
|
+
if (!bytes) continue;
|
|
5146
|
+
const px = pictureSize(bytes, pictureType(f.path));
|
|
5147
|
+
const info = {
|
|
5148
|
+
path: f.path,
|
|
5149
|
+
bytes: bytes.length,
|
|
5150
|
+
width: px?.width ?? null,
|
|
5151
|
+
height: px?.height ?? null,
|
|
5152
|
+
sha256: sha256(bytes)
|
|
5153
|
+
};
|
|
5154
|
+
this.pictureCache.set(f.path, {
|
|
5155
|
+
...f.size === void 0 ? {} : { size: f.size },
|
|
5156
|
+
...f.mtime === void 0 ? {} : { mtime: f.mtime },
|
|
5157
|
+
info
|
|
5158
|
+
});
|
|
5159
|
+
out.push(info);
|
|
5160
|
+
}
|
|
5161
|
+
for (const path of this.pictureCache.keys()) if (!seen.has(path)) this.pictureCache.delete(path);
|
|
5162
|
+
let changed = false;
|
|
5163
|
+
for (const path of seen) if (!this.files.has(path)) {
|
|
5164
|
+
this.files.set(path, { path });
|
|
5165
|
+
changed = true;
|
|
5166
|
+
}
|
|
5167
|
+
for (const path of [...this.files.keys()]) if (pictureType(path) && !seen.has(path)) {
|
|
5168
|
+
this.files.delete(path);
|
|
5169
|
+
changed = true;
|
|
5170
|
+
}
|
|
5171
|
+
if (changed) this.built = this.index = null;
|
|
5172
|
+
return out.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
5173
|
+
}
|
|
5174
|
+
/** Put a picture in the notes (M22). The path must be a picture's, out of any hidden folder, and free: a file
|
|
5175
|
+
* that's there is never replaced. Not journaled: a picture has no history, and stays when its link goes. */
|
|
5176
|
+
savePicture(path, bytes) {
|
|
5177
|
+
return this.one(async () => {
|
|
5178
|
+
const type = pictureType(path), parts = path.split("/");
|
|
5179
|
+
if (!type || parts.some((p) => !p || p === "." || p === ".." || p.startsWith(".")) || /[\\\x00-\x1f]/.test(path)) throw new EngineError(`not a picture's path: ${path}`, 400);
|
|
5180
|
+
if (bytes.length > 2e7) throw new EngineError(`the picture is larger than isorite takes (${MAX_FILE_BYTES / 1e6} MB)`, 413);
|
|
5181
|
+
if (await this.opts.notes.readBytes(path) !== null) throw new Conflict(`${path} is there already`, null);
|
|
5182
|
+
await this.opts.notes.writeBytes(path, bytes);
|
|
5183
|
+
this.files.set(path, { path });
|
|
5184
|
+
this.built = this.index = null;
|
|
5185
|
+
const px = pictureSize(bytes, type);
|
|
5186
|
+
return {
|
|
5187
|
+
path,
|
|
5188
|
+
bytes: bytes.length,
|
|
5189
|
+
width: px?.width ?? null,
|
|
5190
|
+
height: px?.height ?? null,
|
|
5191
|
+
sha256: sha256(bytes)
|
|
5192
|
+
};
|
|
5193
|
+
});
|
|
5194
|
+
}
|
|
4441
5195
|
/** Where a note's file is: drafts and Understanding notes are among isorite's own files. */
|
|
4442
5196
|
where(id) {
|
|
5197
|
+
if (inBase(id) && citeKind(id) === "artifact") return {
|
|
5198
|
+
files: this.opts.notes,
|
|
5199
|
+
path: checkNoteId(id)
|
|
5200
|
+
};
|
|
4443
5201
|
if (!inBase(id)) return {
|
|
4444
5202
|
files: this.opts.own,
|
|
4445
5203
|
path: `${isDraftId(id) ? DRAFT_DIR : UNDERSTANDING_DIR}/${checkNoteId(bare(id))}.md`
|
|
@@ -5802,9 +6560,9 @@ var init_engine = __esmMin((() => {
|
|
|
5802
6560
|
tier: req.tier,
|
|
5803
6561
|
messages,
|
|
5804
6562
|
lookup: tools,
|
|
5805
|
-
maxTokens: REPLY_TOKENS * 2,
|
|
5806
6563
|
signal: abort.signal,
|
|
5807
|
-
|
|
6564
|
+
maxTokens: mode === "write" ? ARTIFACT_TOKENS : REPLY_TOKENS * 2,
|
|
6565
|
+
most: mode === "write" ? Math.max(ROUNDS[req.tier], 6) : ROUNDS[req.tier],
|
|
5808
6566
|
onText: (delta, again) => this.publish({
|
|
5809
6567
|
type: "iso-text",
|
|
5810
6568
|
request: turn,
|
|
@@ -6115,8 +6873,101 @@ var init_engine = __esmMin((() => {
|
|
|
6115
6873
|
});
|
|
6116
6874
|
return `Your reading of ${title(id)} is written.`;
|
|
6117
6875
|
}
|
|
6876
|
+
if (name === "read_data") {
|
|
6877
|
+
const path = filePathOf(args.path);
|
|
6878
|
+
if (!path) return `${String(args.path ?? "")} is not a file's path in their base.`;
|
|
6879
|
+
const got = await this.file(path).catch(() => null);
|
|
6880
|
+
if (!got) return `There is no file ${path}.${this.nearFiles(path)}`;
|
|
6881
|
+
try {
|
|
6882
|
+
return describeShape(shapeOf(path, parseData(path, new TextDecoder().decode(got.bytes))));
|
|
6883
|
+
} catch (e) {
|
|
6884
|
+
return `${path} could not be read as data: ${e.message}`;
|
|
6885
|
+
}
|
|
6886
|
+
}
|
|
6887
|
+
if (name === "artifact_recipe") {
|
|
6888
|
+
const r = RECIPES[String(args.name ?? "")];
|
|
6889
|
+
if (!r) return `There's no recipe ${String(args.name ?? "")}: there are ${Object.keys(RECIPES).join(", ")}.`;
|
|
6890
|
+
return `${REFERENCE}\n\n# The recipe: ${String(args.name)}\n\n${r.about}. A complete working artifact; change what it marks, keep the rest.\n\n\`\`\`html\n${r.html}\`\`\``;
|
|
6891
|
+
}
|
|
6892
|
+
if (name === "read_artifact") {
|
|
6893
|
+
const path = filePathOf(args.path);
|
|
6894
|
+
if (!path || citeKind(path) !== "artifact") return `${String(args.path ?? "")} is not an artifact's path (folder/name.html).`;
|
|
6895
|
+
const got = await this.opts.notes.read(path);
|
|
6896
|
+
return got === null ? `There is no artifact ${path}.` : got;
|
|
6897
|
+
}
|
|
6898
|
+
if (name === "write_artifact") return this.writeArtifact(args, t);
|
|
6118
6899
|
return `${name} cannot be used yet.`;
|
|
6119
6900
|
}
|
|
6901
|
+
/** Files in the same folder as a path, to say which there are when one isn't. */
|
|
6902
|
+
nearFiles(path) {
|
|
6903
|
+
const dir = path.split("/").slice(0, -1).join("/"), there = [...this.files.keys()].filter((f) => !f.endsWith(".md") && f.split("/").slice(0, -1).join("/") === dir);
|
|
6904
|
+
return there.length ? ` In ${dir || "the top folder"} there are: ${there.map((f) => f.split("/").at(-1)).join(", ")}.` : "";
|
|
6905
|
+
}
|
|
6906
|
+
/** write_artifact (M23, plan-m23 §3.5, §3.7): the manifest written as given, the rules checked before anything is
|
|
6907
|
+
* written, then written and journalled as a note is, and run where it can be, with all it reports said back. */
|
|
6908
|
+
async writeArtifact(args, t) {
|
|
6909
|
+
const path = filePathOf(args.path);
|
|
6910
|
+
if (!path || citeKind(path) !== "artifact" || !inBase(path)) return `${String(args.path ?? "")} is not an artifact's path: give folder/name.html.`;
|
|
6911
|
+
const raw = String(args.html ?? "");
|
|
6912
|
+
if (!raw.trim()) return "Give the whole file, in html.";
|
|
6913
|
+
const given = (Array.isArray(args.uses) ? args.uses : typeof args.uses === "string" ? args.uses.split(/[\s,]+/) : []).map((u) => String(u));
|
|
6914
|
+
const uses = [...new Set([
|
|
6915
|
+
...given,
|
|
6916
|
+
...manifestOf(raw).uses,
|
|
6917
|
+
...readsOf(raw)
|
|
6918
|
+
].map((u) => u.trim().replace(/^\.\//, "")).filter(Boolean))];
|
|
6919
|
+
const title = String(args.title ?? "").trim() || manifestOf(raw).title || path.split("/").at(-1).replace(/\.html?$/, "").replace(/-/g, " ");
|
|
6920
|
+
const html = withManifest(raw, {
|
|
6921
|
+
title,
|
|
6922
|
+
description: String(args.description ?? "").trim() || manifestOf(raw).description,
|
|
6923
|
+
author: "Iso",
|
|
6924
|
+
date: new Date(this.opts.clock.now()).toISOString().slice(0, 10),
|
|
6925
|
+
kit: "1",
|
|
6926
|
+
uses
|
|
6927
|
+
});
|
|
6928
|
+
const dir = path.split("/").slice(0, -1);
|
|
6929
|
+
const at = (rel) => {
|
|
6930
|
+
const parts = [...dir];
|
|
6931
|
+
for (const p of rel.split("/")) if (p === "..") parts.pop();
|
|
6932
|
+
else if (p && p !== ".") parts.push(p);
|
|
6933
|
+
return parts.join("/");
|
|
6934
|
+
};
|
|
6935
|
+
const files = [], columns = {};
|
|
6936
|
+
for (const u of uses) {
|
|
6937
|
+
const got = await this.file(at(u)).catch(() => null);
|
|
6938
|
+
if (!got) continue;
|
|
6939
|
+
files.push(u);
|
|
6940
|
+
if (citeKind(u) === "data") try {
|
|
6941
|
+
columns[u] = columnsOf(parseData(u, new TextDecoder().decode(got.bytes)));
|
|
6942
|
+
} catch {}
|
|
6943
|
+
}
|
|
6944
|
+
const broken = checkArtifact(html, {
|
|
6945
|
+
files,
|
|
6946
|
+
columns
|
|
6947
|
+
}), errors = broken.filter((b) => b.level === "error");
|
|
6948
|
+
const list = (bs) => bs.map((b) => `- ${b.message}`).join("\n");
|
|
6949
|
+
if (errors.length) return `Not written: ${errors.length === 1 ? "a rule is" : "rules are"} broken.\n${list(errors)}\nFix ${errors.length === 1 ? "it" : "them"} and call write_artifact again with the whole file.`;
|
|
6950
|
+
const was = await this.opts.notes.read(path);
|
|
6951
|
+
const saved = await this.one(() => this.put(path, html, was === null ? null : hashText(was), "edit", { asked: t.asked }, t.who));
|
|
6952
|
+
if (saved.entry) await t.item({
|
|
6953
|
+
kind: "change",
|
|
6954
|
+
note: path,
|
|
6955
|
+
entry: saved.entry,
|
|
6956
|
+
why: was === null ? "create" : "edit",
|
|
6957
|
+
title,
|
|
6958
|
+
added: html.split("\n").length,
|
|
6959
|
+
removed: was?.split("\n").length ?? 0
|
|
6960
|
+
});
|
|
6961
|
+
const warnings = broken.filter((b) => b.level === "warning");
|
|
6962
|
+
const run = await this.opts.renderArtifact?.(path, html).catch(() => null) ?? null;
|
|
6963
|
+
const problems = [
|
|
6964
|
+
...run && !run.rendered ? ["it didn't start: the kit never said it was ready"] : [],
|
|
6965
|
+
...(run?.errors ?? []).map((e) => `it reported: ${e}`),
|
|
6966
|
+
...run && run.rendered && run.drawn === 0 ? ["it drew nothing: no chart, canvas or 3D view has anything in it"] : [],
|
|
6967
|
+
...warnings.map((w) => w.message)
|
|
6968
|
+
];
|
|
6969
|
+
return `${was === null ? "Written" : "Written anew"}: ${path}. ${run ? "It was run, with every control touched" : "It couldn't be run here (no browser), so only its rules were checked"}${problems.length ? `, and:\n${problems.map((p) => `- ${p}`).join("\n")}\nFix these and call write_artifact again with the whole file.` : run ? ", with no error." : "."}`;
|
|
6970
|
+
}
|
|
6120
6971
|
/** A note's Understanding note (M10): Iso's reading and your scratchpad. */
|
|
6121
6972
|
async understanding(note) {
|
|
6122
6973
|
if (!inBase(checkNoteId(note))) throw new EngineError("only a note of the base has an Understanding note");
|
|
@@ -6491,6 +7342,8 @@ var init_ports = __esmMin((() => {
|
|
|
6491
7342
|
systemClock = { now: () => Date.now() };
|
|
6492
7343
|
MemoryFiles = class {
|
|
6493
7344
|
files = /* @__PURE__ */ new Map();
|
|
7345
|
+
/** Files held as bytes (pictures), apart from text. */
|
|
7346
|
+
bytes = /* @__PURE__ */ new Map();
|
|
6494
7347
|
constructor(files = {}) {
|
|
6495
7348
|
for (const [path, text] of Object.entries(files)) this.files.set(path, text);
|
|
6496
7349
|
}
|
|
@@ -6503,22 +7356,42 @@ var init_ports = __esmMin((() => {
|
|
|
6503
7356
|
async append(path, text) {
|
|
6504
7357
|
this.files.set(path, (this.files.get(path) ?? "") + text);
|
|
6505
7358
|
}
|
|
7359
|
+
async readBytes(path) {
|
|
7360
|
+
const b = this.bytes.get(path), t = this.files.get(path);
|
|
7361
|
+
return b ?? (t === void 0 ? null : new TextEncoder().encode(t));
|
|
7362
|
+
}
|
|
7363
|
+
async writeBytes(path, bytes) {
|
|
7364
|
+
this.files.delete(path);
|
|
7365
|
+
this.bytes.set(path, bytes);
|
|
7366
|
+
}
|
|
6506
7367
|
async remove(path) {
|
|
6507
7368
|
this.files.delete(path);
|
|
7369
|
+
this.bytes.delete(path);
|
|
6508
7370
|
}
|
|
6509
7371
|
async list(dir) {
|
|
6510
7372
|
const prefix = dir ? dir + "/" : "";
|
|
6511
7373
|
const found = /* @__PURE__ */ new Map();
|
|
6512
|
-
for (const path of this.files.keys()) {
|
|
7374
|
+
for (const path of [...this.files.keys(), ...this.bytes.keys()]) {
|
|
6513
7375
|
if (!path.startsWith(prefix)) continue;
|
|
6514
7376
|
const rest = path.slice(prefix.length), cut = rest.indexOf("/");
|
|
6515
|
-
if (cut
|
|
6516
|
-
|
|
7377
|
+
if (cut >= 0) {
|
|
7378
|
+
found.set(rest.slice(0, cut), {
|
|
7379
|
+
name: rest.slice(0, cut),
|
|
7380
|
+
dir: true
|
|
7381
|
+
});
|
|
7382
|
+
continue;
|
|
7383
|
+
}
|
|
7384
|
+
const b = this.bytes.get(path);
|
|
7385
|
+
found.set(rest, b ? {
|
|
7386
|
+
name: rest,
|
|
7387
|
+
dir: false,
|
|
7388
|
+
size: b.length
|
|
7389
|
+
} : {
|
|
7390
|
+
name: rest,
|
|
7391
|
+
dir: false
|
|
7392
|
+
});
|
|
6517
7393
|
}
|
|
6518
|
-
return [...found
|
|
6519
|
-
name,
|
|
6520
|
-
dir: isDir
|
|
6521
|
-
}));
|
|
7394
|
+
return [...found.values()];
|
|
6522
7395
|
}
|
|
6523
7396
|
};
|
|
6524
7397
|
MemorySecrets = class {
|
|
@@ -7978,6 +8851,7 @@ var Terminal = class {
|
|
|
7978
8851
|
};
|
|
7979
8852
|
//#endregion
|
|
7980
8853
|
//#region packages/server/src/http.ts
|
|
8854
|
+
init_src$1();
|
|
7981
8855
|
init_src();
|
|
7982
8856
|
const LOOPBACK = /* @__PURE__ */ new Set([
|
|
7983
8857
|
"localhost",
|
|
@@ -7999,6 +8873,8 @@ const TYPES = {
|
|
|
7999
8873
|
".map": "application/json",
|
|
8000
8874
|
".txt": "text/plain; charset=utf-8"
|
|
8001
8875
|
};
|
|
8876
|
+
/** What a picture may do when opened on its own: be drawn, and nothing else (no script, no fetch, no form). */
|
|
8877
|
+
const PICTURE_CSP = "sandbox; default-src 'none'; img-src data:; style-src 'unsafe-inline'";
|
|
8002
8878
|
const Edit = z.object({
|
|
8003
8879
|
base: z.string().max(64),
|
|
8004
8880
|
text: z.string()
|
|
@@ -8221,6 +9097,42 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal
|
|
|
8221
9097
|
app.get("/api/map", (c) => answer(c, () => api.map()));
|
|
8222
9098
|
app.get("/api/timeline", (c) => answer(c, () => api.timeline()));
|
|
8223
9099
|
app.get("/api/search", (c) => answer(c, () => api.search(c.req.query("q") ?? "")));
|
|
9100
|
+
app.get("/api/pictures", (c) => answer(c, () => api.pictures()));
|
|
9101
|
+
app.post("/api/file", async (c) => {
|
|
9102
|
+
let origin = "";
|
|
9103
|
+
try {
|
|
9104
|
+
origin = new URL(c.req.header("origin") ?? "").host;
|
|
9105
|
+
} catch {}
|
|
9106
|
+
if (!c.req.header("origin") || origin !== c.req.header("host")) return refuse(c, 403, "cross-origin request refused");
|
|
9107
|
+
if (c.req.header("x-isorite-token") !== token) return refuse(c, 403, "bad token");
|
|
9108
|
+
const path = c.req.query("path") ?? "", type = pictureType(path);
|
|
9109
|
+
if (!type || (c.req.header("content-type") ?? "").split(";")[0].trim().toLowerCase() !== type) return refuse(c, 415, "a picture, of the type its name says");
|
|
9110
|
+
if (Number(c.req.header("content-length") ?? 0) > 2e7) return refuse(c, 413, "picture too large");
|
|
9111
|
+
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
9112
|
+
if (bytes.length > 2e7) return refuse(c, 413, "picture too large");
|
|
9113
|
+
return answer(c, () => api.savePicture(path, bytes));
|
|
9114
|
+
});
|
|
9115
|
+
app.get("/api/file", async (c) => {
|
|
9116
|
+
const code = c.req.query("code"), path = code ?? c.req.query("path") ?? "";
|
|
9117
|
+
let found;
|
|
9118
|
+
try {
|
|
9119
|
+
found = await api.file(path, code === void 0 ? "notes" : "code");
|
|
9120
|
+
} catch (e) {
|
|
9121
|
+
if (e instanceof EngineError) return refuse(c, e.status, e.message);
|
|
9122
|
+
throw e;
|
|
9123
|
+
}
|
|
9124
|
+
if (!found) return refuse(c, 404, `no file at ${path}`);
|
|
9125
|
+
const etag = `"${found.version}"`;
|
|
9126
|
+
const headers = {
|
|
9127
|
+
"Content-Type": found.type,
|
|
9128
|
+
"Cache-Control": "no-cache",
|
|
9129
|
+
ETag: etag,
|
|
9130
|
+
"X-Content-Type-Options": "nosniff",
|
|
9131
|
+
"Content-Security-Policy": PICTURE_CSP
|
|
9132
|
+
};
|
|
9133
|
+
if (c.req.header("if-none-match") === etag) return c.body(null, 304, headers);
|
|
9134
|
+
return c.body(found.bytes, 200, headers);
|
|
9135
|
+
});
|
|
8224
9136
|
app.put("/api/note", async (c) => {
|
|
8225
9137
|
const refused = writeRefused(c);
|
|
8226
9138
|
if (refused) return refused;
|
|
@@ -8555,12 +9467,26 @@ var NodeFiles = class {
|
|
|
8555
9467
|
throw e;
|
|
8556
9468
|
}
|
|
8557
9469
|
}
|
|
8558
|
-
|
|
9470
|
+
async readBytes(path) {
|
|
9471
|
+
try {
|
|
9472
|
+
return new Uint8Array(await readFile(this.at(path)));
|
|
9473
|
+
} catch (e) {
|
|
9474
|
+
if (gone(e)) return null;
|
|
9475
|
+
throw e;
|
|
9476
|
+
}
|
|
9477
|
+
}
|
|
8559
9478
|
async write(path, text) {
|
|
9479
|
+
await this.replace(path, text);
|
|
9480
|
+
}
|
|
9481
|
+
async writeBytes(path, bytes) {
|
|
9482
|
+
await this.replace(path, bytes);
|
|
9483
|
+
}
|
|
9484
|
+
/** Written beside the file and renamed over it, so a reader never sees half a note. */
|
|
9485
|
+
async replace(path, data) {
|
|
8560
9486
|
const full = this.at(path), beside = `${full}.${process.pid}.${this.n++}.tmp`;
|
|
8561
9487
|
await mkdir(dirname(full), { recursive: true });
|
|
8562
9488
|
try {
|
|
8563
|
-
await writeFile(beside,
|
|
9489
|
+
await writeFile(beside, data, typeof data === "string" ? "utf8" : void 0);
|
|
8564
9490
|
await rename(beside, full);
|
|
8565
9491
|
} catch (e) {
|
|
8566
9492
|
await rm(beside, { force: true });
|
|
@@ -8574,10 +9500,27 @@ var NodeFiles = class {
|
|
|
8574
9500
|
}
|
|
8575
9501
|
async list(dir) {
|
|
8576
9502
|
try {
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
9503
|
+
const at = this.at(dir), entries = await readdir(at, { withFileTypes: true });
|
|
9504
|
+
return (await Promise.all(entries.filter((e) => e.isDirectory() || e.isFile()).map(async (e) => {
|
|
9505
|
+
if (e.isDirectory()) return {
|
|
9506
|
+
name: e.name,
|
|
9507
|
+
dir: true
|
|
9508
|
+
};
|
|
9509
|
+
if (e.name.endsWith(".md")) return {
|
|
9510
|
+
name: e.name,
|
|
9511
|
+
dir: false
|
|
9512
|
+
};
|
|
9513
|
+
const s = await stat(resolve(at, e.name)).catch(() => null);
|
|
9514
|
+
return s ? {
|
|
9515
|
+
name: e.name,
|
|
9516
|
+
dir: false,
|
|
9517
|
+
size: s.size,
|
|
9518
|
+
mtime: s.mtimeMs
|
|
9519
|
+
} : {
|
|
9520
|
+
name: e.name,
|
|
9521
|
+
dir: false
|
|
9522
|
+
};
|
|
9523
|
+
}))).sort((a, b) => a.name < b.name ? -1 : 1);
|
|
8581
9524
|
} catch (e) {
|
|
8582
9525
|
if (gone(e)) return [];
|
|
8583
9526
|
throw e;
|
|
@@ -8836,7 +9779,7 @@ var Setup = class {
|
|
|
8836
9779
|
};
|
|
8837
9780
|
//#endregion
|
|
8838
9781
|
//#region packages/server/src/version.ts
|
|
8839
|
-
const VERSION = "0.
|
|
9782
|
+
const VERSION = "0.11.0";
|
|
8840
9783
|
//#endregion
|
|
8841
9784
|
//#region packages/server/src/git.ts
|
|
8842
9785
|
/** The top of the repository `folder` is in, or null: the nearest folder holding `.git`, a directory or, in a
|
|
@@ -8890,9 +9833,155 @@ function openStudyBase(path) {
|
|
|
8890
9833
|
};
|
|
8891
9834
|
}
|
|
8892
9835
|
//#endregion
|
|
9836
|
+
//#region packages/server/src/render.ts
|
|
9837
|
+
/** A Chromium on this machine: ISORITE_BROWSER, Playwright's own, or a browser installed in the usual places. */
|
|
9838
|
+
function findBrowser(env = process.env) {
|
|
9839
|
+
if (env.ISORITE_BROWSER && existsSync(env.ISORITE_BROWSER)) return env.ISORITE_BROWSER;
|
|
9840
|
+
const cache = env.PLAYWRIGHT_BROWSERS_PATH ?? (process.platform === "darwin" ? join(homedir(), "Library", "Caches", "ms-playwright") : process.platform === "win32" ? join(env.LOCALAPPDATA ?? "", "ms-playwright") : join(homedir(), ".cache", "ms-playwright"));
|
|
9841
|
+
const found = [];
|
|
9842
|
+
try {
|
|
9843
|
+
for (const d of readdirSync(cache).filter((d) => /^chromium(_headless_shell)?-\d+$/.test(d)).sort().reverse()) for (const p of [
|
|
9844
|
+
"chrome-linux/headless_shell",
|
|
9845
|
+
"chrome-headless-shell-linux64/chrome-headless-shell",
|
|
9846
|
+
"chrome-linux/chrome",
|
|
9847
|
+
"chrome-linux64/chrome",
|
|
9848
|
+
"chrome-mac/headless_shell",
|
|
9849
|
+
"chrome-headless-shell-mac-arm64/chrome-headless-shell",
|
|
9850
|
+
"chrome-mac/Chromium.app/Contents/MacOS/Chromium",
|
|
9851
|
+
"chrome-win/headless_shell.exe",
|
|
9852
|
+
"chrome-headless-shell-win64/chrome-headless-shell.exe",
|
|
9853
|
+
"chrome-win/chrome.exe"
|
|
9854
|
+
]) found.push(join(cache, d, p));
|
|
9855
|
+
} catch {}
|
|
9856
|
+
found.push("/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/snap/bin/chromium", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium", join(env["PROGRAMFILES"] ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe"), join(env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)", "Microsoft", "Edge", "Application", "msedge.exe"));
|
|
9857
|
+
return found.find((p) => existsSync(p)) ?? null;
|
|
9858
|
+
}
|
|
9859
|
+
/** The host: a sandboxed frame given the document, and the bridge, answering files through window.isoRead. */
|
|
9860
|
+
const HOST = `(doc, ground) => {
|
|
9861
|
+
const f = document.getElementById("f");
|
|
9862
|
+
window.said = []; window.ready = false;
|
|
9863
|
+
const tell = (m, t = []) => f.contentWindow.postMessage({ isorite: 0, ...m }, "*", t);
|
|
9864
|
+
addEventListener("message", async (e) => {
|
|
9865
|
+
const m = e.data;
|
|
9866
|
+
if (e.source !== f.contentWindow || !m || m.isorite !== 0) return;
|
|
9867
|
+
if (m.type === "ready") { window.ready = true; tell({ type: "ground", ground }); tell({ type: "look", look: "isorite" }); }
|
|
9868
|
+
else if (m.type === "file" || m.type === "bytes") {
|
|
9869
|
+
const got = await window.isoRead(m.path);
|
|
9870
|
+
if (got.error) tell({ type: "reply", id: m.id, error: got.error });
|
|
9871
|
+
else if (m.type === "bytes") { const buf = got.b64 !== undefined ? Uint8Array.from(atob(got.b64), (c) => c.charCodeAt(0)).buffer : new TextEncoder().encode(got.text).buffer; tell({ type: "reply", id: m.id, bytes: buf }, [buf]); }
|
|
9872
|
+
else tell({ type: "reply", id: m.id, text: got.text ?? atob(got.b64) });
|
|
9873
|
+
}
|
|
9874
|
+
else if (m.type === "height") f.style.height = Math.min(8000, Math.ceil(m.height) + 4) + "px";
|
|
9875
|
+
else if (m.type === "error") window.said.push(String(m.message));
|
|
9876
|
+
});
|
|
9877
|
+
f.srcdoc = doc;
|
|
9878
|
+
}`;
|
|
9879
|
+
/** What it drew: SVG marks, a table's cells, and 100 for each canvas with anything on it (a WebGL one, if it has a size). */
|
|
9880
|
+
const DRAWN = `(() => {
|
|
9881
|
+
let n = document.querySelectorAll("svg path, svg line, svg circle, svg rect, svg polyline, svg polygon, svg text, iso-table td").length;
|
|
9882
|
+
for (const c of document.querySelectorAll("canvas")) {
|
|
9883
|
+
try {
|
|
9884
|
+
const g = c.getContext("2d");
|
|
9885
|
+
if (g) { const d = g.getImageData(0, 0, c.width, c.height).data; if (d.some((v, i) => v !== d[i % 4])) n += 100; }
|
|
9886
|
+
else n += 100;
|
|
9887
|
+
} catch { n += 100; }
|
|
9888
|
+
}
|
|
9889
|
+
return n;
|
|
9890
|
+
})()`;
|
|
9891
|
+
/** Every control touched: sliders to three quarters, choices to their second, buttons pressed, a drag on a canvas. */
|
|
9892
|
+
const TOUCH = `(async () => {
|
|
9893
|
+
for (const r of document.querySelectorAll("input[type=range]")) { r.value = String(+r.min + (+r.max - +r.min) * 0.75); r.dispatchEvent(new Event("input", { bubbles: true })); r.dispatchEvent(new Event("change", { bubbles: true })); }
|
|
9894
|
+
for (const s of document.querySelectorAll("select")) if (s.options.length > 1) { s.selectedIndex = 1; s.dispatchEvent(new Event("change", { bubbles: true })); }
|
|
9895
|
+
for (const b of document.querySelectorAll("button, input[type=checkbox]")) if (!b.closest("iso-note")) b.click();
|
|
9896
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
9897
|
+
const c = document.querySelector("canvas");
|
|
9898
|
+
if (c) {
|
|
9899
|
+
const r = c.getBoundingClientRect(), at = (t, x) => c.dispatchEvent(new PointerEvent(t, { clientX: r.left + x, clientY: r.top + r.height / 2, bubbles: true, pointerId: 1 }));
|
|
9900
|
+
at("pointerdown", r.width / 2); at("pointermove", r.width / 2 + 60); at("pointerup", r.width / 2 + 60);
|
|
9901
|
+
c.dispatchEvent(new MouseEvent("click", { clientX: r.left + r.width / 3, clientY: r.top + r.height / 3, bubbles: true }));
|
|
9902
|
+
}
|
|
9903
|
+
})()`;
|
|
9904
|
+
let launched = null;
|
|
9905
|
+
async function browser(path) {
|
|
9906
|
+
launched ??= (async () => {
|
|
9907
|
+
const { chromium } = await import("playwright-core");
|
|
9908
|
+
return chromium.launch({
|
|
9909
|
+
executablePath: path,
|
|
9910
|
+
args: [
|
|
9911
|
+
"--use-gl=angle",
|
|
9912
|
+
"--use-angle=swiftshader",
|
|
9913
|
+
"--enable-unsafe-swiftshader"
|
|
9914
|
+
]
|
|
9915
|
+
});
|
|
9916
|
+
})().catch((e) => {
|
|
9917
|
+
launched = null;
|
|
9918
|
+
throw e;
|
|
9919
|
+
});
|
|
9920
|
+
return launched;
|
|
9921
|
+
}
|
|
9922
|
+
/** Render an assembled artifact (packages/kit's assemble) unseen, answer its bridge from `read` for the files in
|
|
9923
|
+
* `uses`, touch its controls, and say what happened. */
|
|
9924
|
+
async function renderArtifact(doc, uses, read, opts) {
|
|
9925
|
+
const b = await browser(opts.executable), width = opts.width ?? 1100;
|
|
9926
|
+
const page = await b.newPage({ viewport: {
|
|
9927
|
+
width,
|
|
9928
|
+
height: 900
|
|
9929
|
+
} });
|
|
9930
|
+
const errors = [];
|
|
9931
|
+
page.on("console", (m) => {
|
|
9932
|
+
if (m.type?.() === "error") errors.push(m.text());
|
|
9933
|
+
});
|
|
9934
|
+
page.on("pageerror", (e) => errors.push(e.message ?? String(e)));
|
|
9935
|
+
const work = (async () => {
|
|
9936
|
+
await page.exposeFunction("isoRead", async (rel) => {
|
|
9937
|
+
if (!uses.includes(rel)) return { error: `${rel} is not in the artifact's manifest (isorite:uses)` };
|
|
9938
|
+
const got = await read(rel);
|
|
9939
|
+
if (got === null) return { error: `${rel}: no such file beside the artifact` };
|
|
9940
|
+
return typeof got === "string" ? { text: got } : { b64: Buffer.from(got).toString("base64") };
|
|
9941
|
+
});
|
|
9942
|
+
await page.setContent(`<!doctype html><html><body style="margin:0;background:${opts.ground === "quartz" ? "#F2F0EA" : "#07101C"}"><iframe id="f" sandbox="allow-scripts" style="display:block;width:${width}px;height:900px;border:0"></iframe></body></html>`);
|
|
9943
|
+
await page.evaluate(`(${HOST})(${JSON.stringify(doc)}, ${JSON.stringify(opts.ground ?? "lapis")})`);
|
|
9944
|
+
await page.waitForTimeout(2e3);
|
|
9945
|
+
const ready = await page.evaluate("window.ready");
|
|
9946
|
+
const frame = page.frames().find((fr) => fr !== page.mainFrame());
|
|
9947
|
+
const height = await page.evaluate("document.getElementById(\"f\").offsetHeight");
|
|
9948
|
+
const drawn = frame ? await frame.evaluate(DRAWN) : 0;
|
|
9949
|
+
let screenshot;
|
|
9950
|
+
if (opts.screenshot) {
|
|
9951
|
+
await page.setViewportSize({
|
|
9952
|
+
width,
|
|
9953
|
+
height: Math.max(400, Math.min(height, 4e3))
|
|
9954
|
+
});
|
|
9955
|
+
screenshot = await page.screenshot({ fullPage: true });
|
|
9956
|
+
}
|
|
9957
|
+
if (frame) await frame.evaluate(TOUCH);
|
|
9958
|
+
await page.waitForTimeout(600);
|
|
9959
|
+
const said = await page.evaluate("window.said");
|
|
9960
|
+
return {
|
|
9961
|
+
rendered: ready,
|
|
9962
|
+
errors: [.../* @__PURE__ */ new Set([...said, ...errors])].slice(0, 12),
|
|
9963
|
+
drawn,
|
|
9964
|
+
height,
|
|
9965
|
+
...screenshot ? { screenshot } : {}
|
|
9966
|
+
};
|
|
9967
|
+
})();
|
|
9968
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve({
|
|
9969
|
+
rendered: false,
|
|
9970
|
+
errors: ["it didn't finish starting within the time allowed: a loop that never ends?"],
|
|
9971
|
+
drawn: 0,
|
|
9972
|
+
height: 0
|
|
9973
|
+
}), opts.timeout ?? 2e4));
|
|
9974
|
+
try {
|
|
9975
|
+
return await Promise.race([work, timeout]);
|
|
9976
|
+
} finally {
|
|
9977
|
+
await page.close().catch(() => void 0);
|
|
9978
|
+
}
|
|
9979
|
+
}
|
|
9980
|
+
//#endregion
|
|
8893
9981
|
//#region packages/server/src/serve.ts
|
|
8894
9982
|
init_src$1();
|
|
8895
9983
|
init_src();
|
|
9984
|
+
init_src$2();
|
|
8896
9985
|
/** The built app: in a checkout, where npm run build puts it; installed from npm, in the package's web/. */
|
|
8897
9986
|
const WEB_DIR = [join(dirname(fileURLToPath(import.meta.url)), "..", "web"), join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "app", "build")].find((dir) => existsSync(join(dir, "index.html"))) ?? join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "app", "build");
|
|
8898
9987
|
const moveIt = (study) => `${study} is a study base, which this isorite no longer reads (format 1). Move it into its notes with: isorite migrate ${study}`;
|
|
@@ -8950,9 +10039,28 @@ async function openEngine({ notes }, device = deviceId(), config = configPath(),
|
|
|
8950
10039
|
name: `vectors-${createHash("sha256").update(notes).digest("hex").slice(0, 16)}.json`
|
|
8951
10040
|
}
|
|
8952
10041
|
} : {},
|
|
8953
|
-
...extras.elsewhere ? { elsewhere: extras.elsewhere } : {}
|
|
10042
|
+
...extras.elsewhere ? { elsewhere: extras.elsewhere } : {},
|
|
10043
|
+
renderArtifact: (path, html) => runArtifact(notes, path, html)
|
|
8954
10044
|
});
|
|
8955
10045
|
}
|
|
10046
|
+
/** Run an artifact unseen: null when there's no browser, or no built kit, to run it with. */
|
|
10047
|
+
async function runArtifact(notes, path, html) {
|
|
10048
|
+
const executable = findBrowser(), kitDir = join(WEB_DIR, "kit");
|
|
10049
|
+
if (!executable || !existsSync(join(kitDir, "kit.js"))) return null;
|
|
10050
|
+
const kit = Object.fromEntries(KIT_FILES.map((f) => [f, readFileSync(join(kitDir, f), "utf8")]));
|
|
10051
|
+
const dir = dirname(resolve(notes, path)), uses = manifestOf(html).uses;
|
|
10052
|
+
const read = async (rel) => {
|
|
10053
|
+
const at = resolve(dir, rel);
|
|
10054
|
+
if (!at.startsWith(resolve(notes) + sep) || !existsSync(at)) return null;
|
|
10055
|
+
return /\.(csv|tsv|json|jsonl|ndjson|txt|gltf|obj)$/i.test(at) ? readFileSync(at, "utf8") : new Uint8Array(readFileSync(at));
|
|
10056
|
+
};
|
|
10057
|
+
const got = await renderArtifact(assemble(html, kit), uses, read, { executable });
|
|
10058
|
+
return {
|
|
10059
|
+
rendered: got.rendered,
|
|
10060
|
+
errors: got.errors,
|
|
10061
|
+
drawn: got.drawn
|
|
10062
|
+
};
|
|
10063
|
+
}
|
|
8956
10064
|
/** The guide's finder, as a provider: no key, no sign-in, and a name that says what it is. */
|
|
8957
10065
|
const { auth: _auth, ...openrouter } = OPENROUTER;
|
|
8958
10066
|
const FINDER = {
|
|
@@ -9683,6 +10791,8 @@ async function doctor(folder, o = {}) {
|
|
|
9683
10791
|
say("browser pages", asking.length ? "fail" : "ok", asking.length ? `${asking.join(" and ")} asked for a password (401, Basic): that is not isorite's server; stop it, or give isorite another port (--port <n>)` : `isorite's pages at ${url}/ ask for no password. ${hint}`);
|
|
9684
10792
|
}
|
|
9685
10793
|
if (env.SSH_CONNECTION) say("browser", "ok", `over SSH: on your own machine, ssh -L ${port}:localhost:${port} <this host>, then http://localhost:${port}/`);
|
|
10794
|
+
const chrome = findBrowser(env);
|
|
10795
|
+
say("artifacts", chrome ? "ok" : "warn", chrome ? `run unseen when Iso writes one, in ${chrome}` : "no Chromium found to run them unseen, so only their rules are checked when Iso writes one; ISORITE_BROWSER names one (Chrome, Chromium, Edge)");
|
|
9686
10796
|
let model = null;
|
|
9687
10797
|
if (env.ISORITE_EMBED === "off") say("search by meaning", "skip", "switched off (ISORITE_EMBED=off)");
|
|
9688
10798
|
else {
|
|
@@ -11079,6 +12189,19 @@ const finderHttp = (pace = 0) => scriptedHttp(finderAnswer, pace);
|
|
|
11079
12189
|
//#region packages/guide/src/node.ts
|
|
11080
12190
|
/** In a checkout, the package's notes/; in the npm package, where this is bundled into dist/, the guide/ beside it. */
|
|
11081
12191
|
const GUIDE_DIR = [join(import.meta.dirname, "..", "guide"), join(import.meta.dirname, "..", "notes")].find((d) => existsSync(join(d, "start-here.md"))) ?? join(import.meta.dirname, "..", "notes");
|
|
12192
|
+
/** The guide's files that aren't notes, by path, as bytes: its pictures (M22), and its artifacts and their data (M23). */
|
|
12193
|
+
function loadGuidePictures(dir = GUIDE_DIR) {
|
|
12194
|
+
const out = {};
|
|
12195
|
+
const visit = (at) => {
|
|
12196
|
+
for (const name of readdirSync(at)) {
|
|
12197
|
+
const path = join(at, name);
|
|
12198
|
+
if (statSync(path).isDirectory()) visit(path);
|
|
12199
|
+
else if (/\.(png|jpe?g|gif|svg|webp|avif|html|csv|tsv|json|jsonl|stl|glb|gltf|obj|pdf)$/i.test(name)) out[relative(dir, path).split(sep).join("/")] = new Uint8Array(readFileSync(path));
|
|
12200
|
+
}
|
|
12201
|
+
};
|
|
12202
|
+
visit(dir);
|
|
12203
|
+
return out;
|
|
12204
|
+
}
|
|
11082
12205
|
/** The guide's files as they stand in the package: its notes and its AGENTS.md, by path. */
|
|
11083
12206
|
function loadGuide(dir = GUIDE_DIR) {
|
|
11084
12207
|
const out = {};
|
|
@@ -11106,7 +12229,7 @@ function writeGuide(folder) {
|
|
|
11106
12229
|
fresh: false
|
|
11107
12230
|
};
|
|
11108
12231
|
if (existsSync(at) && readdirSync(at).some((f) => f !== ".isorite")) throw new StudyError(`${at} has files in it that are not the guide: name another folder, isorite guide <folder>`);
|
|
11109
|
-
for (const [path, text] of Object.entries(loadGuide())) {
|
|
12232
|
+
for (const [path, text] of [...Object.entries(loadGuide()), ...Object.entries(loadGuidePictures())]) {
|
|
11110
12233
|
mkdirSync(dirname(join(at, path)), { recursive: true });
|
|
11111
12234
|
writeFileSync(join(at, path), text);
|
|
11112
12235
|
}
|
|
@@ -11155,8 +12278,21 @@ async function writeExample(folder, now = Date.now()) {
|
|
|
11155
12278
|
mkdirSync(dirname(at), { recursive: true });
|
|
11156
12279
|
writeFileSync(at, text);
|
|
11157
12280
|
}
|
|
12281
|
+
copyPictures(NOTES_DIR, notes);
|
|
11158
12282
|
return { notes };
|
|
11159
12283
|
}
|
|
12284
|
+
/** The pictures among a folder's notes (M22), copied as they are: they have no history. */
|
|
12285
|
+
function copyPictures(from, to, dir = from) {
|
|
12286
|
+
for (const name of readdirSync(dir)) {
|
|
12287
|
+
const path = join(dir, name);
|
|
12288
|
+
if (statSync(path).isDirectory()) copyPictures(from, to, path);
|
|
12289
|
+
else if (citeKind(name) === "image") {
|
|
12290
|
+
const at = join(to, relative(from, path));
|
|
12291
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
12292
|
+
copyFileSync(path, at);
|
|
12293
|
+
}
|
|
12294
|
+
}
|
|
12295
|
+
}
|
|
11160
12296
|
const GEOMETRY_DIR = join(import.meta.dirname, "..", "geometry");
|
|
11161
12297
|
/** The geometry example's notes, and the code beside them, as they stand today, by path. */
|
|
11162
12298
|
function loadGeometry(dir = GEOMETRY_DIR) {
|
|
@@ -11282,12 +12418,14 @@ function bases(args, { config = configPath(), out = console.log } = {}) {
|
|
|
11282
12418
|
}
|
|
11283
12419
|
//#endregion
|
|
11284
12420
|
//#region packages/server/src/main.ts
|
|
12421
|
+
init_src$2();
|
|
11285
12422
|
init_src$1();
|
|
11286
12423
|
init_model();
|
|
11287
12424
|
const USAGE = `isorite: an agentic harness for understanding
|
|
11288
12425
|
|
|
11289
12426
|
isorite new <folder> start a knowledge base: the folder, empty, with isorite's .isorite/
|
|
11290
|
-
isorite check <notes>
|
|
12427
|
+
isorite check <notes> [--unused] check a folder of notes: frontmatter, links, cycles; --unused lists
|
|
12428
|
+
the pictures no note shows
|
|
11291
12429
|
isorite serve <notes> [--port <n>] [--allow-host <name>]
|
|
11292
12430
|
open it in the browser, on this machine only
|
|
11293
12431
|
isorite migrate <study base> [--merge] move a study base (format 1) into its notes' .isorite/; exercises
|
|
@@ -11324,7 +12462,7 @@ const USAGE = `isorite: an agentic harness for understanding
|
|
|
11324
12462
|
the browser; run again, as you left it. Iso is your model, or with
|
|
11325
12463
|
none set up, the guide's finder (also: isorite --tutorial)
|
|
11326
12464
|
`;
|
|
11327
|
-
function check(notes, out = console.log) {
|
|
12465
|
+
function check(notes, out = console.log, unused = false) {
|
|
11328
12466
|
const root = resolve(notes);
|
|
11329
12467
|
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
11330
12468
|
out(`no folder at ${root}`);
|
|
@@ -11333,14 +12471,41 @@ function check(notes, out = console.log) {
|
|
|
11333
12471
|
const bundle = loadBundle(root), issues = bundle.lint(), cycles = bundle.requiresCycles();
|
|
11334
12472
|
for (const i of issues) out(`${i.level.padEnd(7)} ${i.path}: ${i.message}`);
|
|
11335
12473
|
for (const c of cycles) out(`warning requires cycle: ${c.join(" -> ")}`);
|
|
11336
|
-
|
|
11337
|
-
|
|
12474
|
+
let artifactErrors = 0, artifactWarnings = 0;
|
|
12475
|
+
for (const f of [...bundle.files].filter((x) => citeKind(x) === "artifact").sort()) {
|
|
12476
|
+
const html = readFileSync(join(root, f), "utf8");
|
|
12477
|
+
if (manifestOf(html).kit === null) continue;
|
|
12478
|
+
const dir = dirname(join(root, f)), files = manifestOf(html).uses.filter((u) => existsSync(join(dir, u)));
|
|
12479
|
+
const columns = Object.fromEntries(files.filter((u) => citeKind(u) === "data").flatMap((u) => {
|
|
12480
|
+
try {
|
|
12481
|
+
return [[u, columnsOf(parseData(u, readFileSync(join(dir, u), "utf8")))]];
|
|
12482
|
+
} catch {
|
|
12483
|
+
return [];
|
|
12484
|
+
}
|
|
12485
|
+
}));
|
|
12486
|
+
for (const b of checkArtifact(html, {
|
|
12487
|
+
files,
|
|
12488
|
+
columns
|
|
12489
|
+
})) {
|
|
12490
|
+
out(`${b.level.padEnd(7)} ${f}: ${b.message}`);
|
|
12491
|
+
if (b.level === "error") artifactErrors++;
|
|
12492
|
+
else artifactWarnings++;
|
|
12493
|
+
}
|
|
12494
|
+
}
|
|
12495
|
+
const errors = issues.filter((i) => i.level === "error").length + artifactErrors;
|
|
12496
|
+
out(`${bundle.concepts.size} notes in ${bundle.directories.size} folders; ${errors} errors, ${issues.length - issues.filter((i) => i.level === "error").length + artifactWarnings} warnings, ${cycles.length} requires cycles`);
|
|
12497
|
+
if (unused) {
|
|
12498
|
+
const cited = new Set([...bundle.concepts.values()].flatMap((c) => c.cites.map((x) => x.target)));
|
|
12499
|
+
const idle = [...bundle.files].filter((f) => citeKind(f) === "image" && !cited.has(f)).sort();
|
|
12500
|
+
for (const f of idle) out(`unused ${f}`);
|
|
12501
|
+
out(idle.length ? `${idle.length} ${idle.length === 1 ? "picture" : "pictures"} no note shows` : "every picture is shown by a note");
|
|
12502
|
+
}
|
|
11338
12503
|
return errors ? 1 : 0;
|
|
11339
12504
|
}
|
|
11340
12505
|
function main(argv) {
|
|
11341
12506
|
const [command, ...rest] = argv;
|
|
11342
12507
|
try {
|
|
11343
|
-
if (command === "check" && rest
|
|
12508
|
+
if (command === "check" && rest.find((r) => !r.startsWith("--"))) return check(rest.find((r) => !r.startsWith("--")), console.log, rest.includes("--unused"));
|
|
11344
12509
|
if (command === "new" && rest[0]) {
|
|
11345
12510
|
const made = newBase(rest[0]);
|
|
11346
12511
|
console.log(`${made}: a knowledge base, empty. Open it in the browser, or beside your code in the terminal:\n isorite serve ${made}\n cd <your code> && isorite term ${made}`);
|