isorite 0.9.1 → 0.10.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.
Files changed (108) hide show
  1. package/dist/core.js +84 -10
  2. package/dist/main.js +363 -32
  3. package/notes/algorithms/the-quadtree/subdivision.svg +15 -0
  4. package/notes/algorithms/the-quadtree.md +2 -0
  5. package/package.json +1 -1
  6. package/plugin/server.ts +1 -1
  7. package/web/_app/immutable/assets/{0.CTbO0owi.css → 0.CPW9fjPu.css} +1 -1
  8. package/web/_app/immutable/assets/NoteEditor.Wt5vU9Tb.css +1 -0
  9. package/web/_app/immutable/chunks/{8wVGBFSs.js → -2Meesge.js} +3 -3
  10. package/web/_app/immutable/chunks/{DDuWClBR.js → 6RXH8WsD.js} +1 -1
  11. package/web/_app/immutable/chunks/{BL3mLym4.js → 9z49435t.js} +1 -1
  12. package/web/_app/immutable/chunks/B0A4Snxv.js +6 -0
  13. package/web/_app/immutable/chunks/B8Qvvm53.js +1 -0
  14. package/web/_app/immutable/chunks/{DMAYXnpq.js → BA8Hpxhg.js} +1 -1
  15. package/web/_app/immutable/chunks/{CKSRTWxH.js → BAclEN0O.js} +1 -1
  16. package/web/_app/immutable/chunks/BJo70SOc.js +1 -0
  17. package/web/_app/immutable/chunks/{DECNRCf2.js → BLmF1B2i.js} +1 -1
  18. package/web/_app/immutable/chunks/{CiVjR8gq.js → BTclWEfx.js} +1 -1
  19. package/web/_app/immutable/chunks/{Cp_JbuGh.js → BWVMfAMh.js} +1 -1
  20. package/web/_app/immutable/chunks/{D4fLEcOQ.js → BXAOBcPb.js} +1 -1
  21. package/web/_app/immutable/chunks/BYSDTLqw.js +1 -0
  22. package/web/_app/immutable/chunks/{B3aPQAYq.js → BcgO-fBX.js} +1 -1
  23. package/web/_app/immutable/chunks/BimZp6R0.js +523 -0
  24. package/web/_app/immutable/chunks/{CO2Y5y64.js → Bq2GBAJ0.js} +1 -1
  25. package/web/_app/immutable/chunks/BqrMZ9MX.js +1 -0
  26. package/web/_app/immutable/chunks/{BN6xhG8e.js → Bt7VOT5H.js} +1 -1
  27. package/web/_app/immutable/chunks/{BGbXxNFa.js → BuUzVkG2.js} +1 -1
  28. package/web/_app/immutable/chunks/{Brxvm_b1.js → By4S_gy5.js} +1 -1
  29. package/web/_app/immutable/chunks/{CRIvE37b.js → BymwHy7o.js} +1 -1
  30. package/web/_app/immutable/chunks/{GRrQoDGJ.js → C5MPRibb.js} +1 -1
  31. package/web/_app/immutable/chunks/CBkjDkEN.js +151 -0
  32. package/web/_app/immutable/chunks/{BrqJKzQU.js → CD6xPodA.js} +1 -1
  33. package/web/_app/immutable/chunks/{CEufJyoj.js → CQd9D8Nc.js} +1 -1
  34. package/web/_app/immutable/chunks/{BrGmYbJT.js → CT_Ry4q8.js} +1 -1
  35. package/web/_app/immutable/chunks/{CQadubS9.js → C_amgxV1.js} +1 -1
  36. package/web/_app/immutable/chunks/CiU6tYJv.js +1 -0
  37. package/web/_app/immutable/chunks/{spNKdrPy.js → CjtKPNlv.js} +1 -1
  38. package/web/_app/immutable/chunks/{DxLkKbK2.js → Cl5zRP_n.js} +1 -1
  39. package/web/_app/immutable/chunks/{BXF2XoRC.js → CsRPJjQS.js} +1 -1
  40. package/web/_app/immutable/chunks/{CETSRtnV.js → CyFLqlvP.js} +1 -1
  41. package/web/_app/immutable/chunks/{Bq5hCPe1.js → Czz9qOh8.js} +1 -1
  42. package/web/_app/immutable/chunks/{BguXRgvc.js → D3-rqfsM.js} +1 -1
  43. package/web/_app/immutable/chunks/{bcQIrid8.js → D5XlJBy8.js} +1 -1
  44. package/web/_app/immutable/chunks/{TuFg0KqZ.js → D5vocsTl.js} +1 -1
  45. package/web/_app/immutable/chunks/{CTscJvp6.js → D95RKsvN.js} +1 -1
  46. package/web/_app/immutable/chunks/{BzI3yqBQ.js → DBC2xjOo.js} +1 -1
  47. package/web/_app/immutable/chunks/{DII7jNnq.js → DCD8_jrW.js} +1 -1
  48. package/web/_app/immutable/chunks/{BsLNwr5A.js → DF89zyH-.js} +1 -1
  49. package/web/_app/immutable/chunks/{DacMdKjW.js → DQjIi5Jv.js} +1 -1
  50. package/web/_app/immutable/chunks/{C-dfcq9W.js → DRX-TQzj.js} +1 -1
  51. package/web/_app/immutable/chunks/{BunrC_8P.js → DX4bJJO0.js} +1 -1
  52. package/web/_app/immutable/chunks/{EQVRMnZU.js → DXK7PB4g.js} +1 -1
  53. package/web/_app/immutable/chunks/{mbLqADPr.js → DXTK-eyo.js} +1 -1
  54. package/web/_app/immutable/chunks/{wCTXGvXm.js → DbNETcPs.js} +1 -1
  55. package/web/_app/immutable/chunks/{CX-xckzu.js → DfCZPwp1.js} +1 -1
  56. package/web/_app/immutable/chunks/{DKNOMI4Y.js → Dh1lBxfx.js} +1 -1
  57. package/web/_app/immutable/chunks/{BgczuZKA.js → DkxkvOjx.js} +1 -1
  58. package/web/_app/immutable/chunks/DlKQFacy.js +1 -0
  59. package/web/_app/immutable/chunks/{0M1khDs2.js → DlXrOzlh.js} +1 -1
  60. package/web/_app/immutable/chunks/{B85D_PlV.js → Dpa4Syyy.js} +1 -1
  61. package/web/_app/immutable/chunks/DshHzxgH.js +1 -0
  62. package/web/_app/immutable/chunks/{CdY1YAit.js → EMo1IeTW.js} +1 -1
  63. package/web/_app/immutable/chunks/Iqr2CudW.js +3 -0
  64. package/web/_app/immutable/chunks/{CoyQYage.js → RFxekmX-.js} +1 -1
  65. package/web/_app/immutable/chunks/{vk5Wkr31.js → RlApGeew.js} +1 -1
  66. package/web/_app/immutable/chunks/{CLVUDRRk.js → Xm0YTnxV.js} +1 -1
  67. package/web/_app/immutable/chunks/c7PFFMl4.js +1 -0
  68. package/web/_app/immutable/chunks/{CZ8wTb_M.js → gPtCGHiy.js} +1 -1
  69. package/web/_app/immutable/chunks/{D4SKbj9K.js → oWM_kgYq.js} +1 -1
  70. package/web/_app/immutable/chunks/{wj7G6pU5.js → rghE0CVz.js} +1 -1
  71. package/web/_app/immutable/chunks/{B9YLdnR_.js → tUSDgbpy.js} +1 -1
  72. package/web/_app/immutable/chunks/{DTdlqvEb.js → vfDvz3qP.js} +1 -1
  73. package/web/_app/immutable/chunks/{BRKS-CCX.js → whhiiiga.js} +1 -1
  74. package/web/_app/immutable/entry/app.iw6ZIOh2.js +2 -0
  75. package/web/_app/immutable/entry/start.CFnKMSWs.js +1 -0
  76. package/web/_app/immutable/nodes/0.BzEgwgsa.js +26 -0
  77. package/web/_app/immutable/nodes/1.rzuq9VRh.js +1 -0
  78. package/web/_app/immutable/nodes/2.D_jWsQLW.js +1 -0
  79. package/web/_app/immutable/nodes/3.BzRx3SiD.js +1 -0
  80. package/web/_app/immutable/nodes/4.DP2MGav1.js +2 -0
  81. package/web/_app/immutable/nodes/5.BPS-CCQT.js +5 -0
  82. package/web/_app/immutable/nodes/6.DQMCyNJ3.js +1 -0
  83. package/web/_app/immutable/nodes/7.MWEn-qPC.js +1 -0
  84. package/web/_app/version.json +1 -1
  85. package/web/index.html +17 -17
  86. package/web/_app/immutable/assets/NoteEditor.8wWtZK_A.css +0 -1
  87. package/web/_app/immutable/chunks/CBWiKnLz.js +0 -1
  88. package/web/_app/immutable/chunks/CHbKgHy3.js +0 -1
  89. package/web/_app/immutable/chunks/CJ1dhP0p.js +0 -1
  90. package/web/_app/immutable/chunks/CLkY0zVi.js +0 -1
  91. package/web/_app/immutable/chunks/D-aFnd2T.js +0 -1
  92. package/web/_app/immutable/chunks/DRQEUdad.js +0 -1
  93. package/web/_app/immutable/chunks/DZYaqCwi.js +0 -1
  94. package/web/_app/immutable/chunks/D_19S_Iu.js +0 -1
  95. package/web/_app/immutable/chunks/DcgxJ8bF.js +0 -673
  96. package/web/_app/immutable/chunks/Ddgejj-u.js +0 -1
  97. package/web/_app/immutable/chunks/nMarIA7J.js +0 -3
  98. package/web/_app/immutable/chunks/vUsuwDSf.js +0 -6
  99. package/web/_app/immutable/entry/app.CswuY3sh.js +0 -2
  100. package/web/_app/immutable/entry/start.D8ZvSyBD.js +0 -1
  101. package/web/_app/immutable/nodes/0.DBoa3NAQ.js +0 -26
  102. package/web/_app/immutable/nodes/1.DrwFerb8.js +0 -1
  103. package/web/_app/immutable/nodes/2.DPmbppXV.js +0 -1
  104. package/web/_app/immutable/nodes/3.BW7AbIrS.js +0 -1
  105. package/web/_app/immutable/nodes/4.B5HAyMKs.js +0 -2
  106. package/web/_app/immutable/nodes/5.BvScSoCM.js +0 -5
  107. package/web/_app/immutable/nodes/6.Bna6qY2G.js +0 -1
  108. 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";
@@ -142,7 +142,7 @@ function normpath(p) {
142
142
  }
143
143
  return "/".repeat(initial) + out.join("/") || ".";
144
144
  }
145
- /** SHA-256 as hex, synchronous and dependency-free, so the core runs anywhere. */
145
+ /** SHA-256 as hex, synchronous and dependency-free, so the core runs anywhere: of text (as UTF-8), or of bytes. */
146
146
  function sha256(message) {
147
147
  const K = new Uint32Array([
148
148
  1116352408,
@@ -210,7 +210,7 @@ function sha256(message) {
210
210
  3204031479,
211
211
  3329325298
212
212
  ]);
213
- const data = new TextEncoder().encode(message);
213
+ const data = typeof message === "string" ? new TextEncoder().encode(message) : message;
214
214
  const len = data.length;
215
215
  const padded = new Uint8Array(len + 9 + 63 >> 6 << 6);
216
216
  padded.set(data);
@@ -377,7 +377,7 @@ function linkTexts(body) {
377
377
  return out;
378
378
  }
379
379
  /** 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). */
380
+ * (`![…](…)`, which for an artifact or a picture means "shown here", an embed), with an image's alt text. */
381
381
  function citeRefs(body) {
382
382
  const { tokens } = parse$1(body);
383
383
  const out = [];
@@ -391,7 +391,8 @@ function citeRefs(body) {
391
391
  else if (c.type === "image") out.push({
392
392
  href: c.attrGet("src") ?? "",
393
393
  title: c.attrGet("title"),
394
- embed: true
394
+ embed: true,
395
+ alt: c.content
395
396
  });
396
397
  }
397
398
  return out;
@@ -446,6 +447,64 @@ var init_markdown = __esmMin((() => {
446
447
  SPACES = /[\s_]+/gu;
447
448
  }));
448
449
  //#endregion
450
+ //#region packages/core/src/picture.ts
451
+ /** Width and height in pixels, or null. */
452
+ function pictureSize(bytes, type) {
453
+ const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength), n = bytes.length;
454
+ const at = (i, s) => [...s].every((c, k) => bytes[i + k] === c.charCodeAt(0));
455
+ const ok = (w, h) => w > 0 && h > 0 ? {
456
+ width: w,
457
+ height: h
458
+ } : null;
459
+ try {
460
+ if (type === "image/png" && n >= 24 && at(12, "IHDR")) return ok(v.getUint32(16), v.getUint32(20));
461
+ if (type === "image/gif" && n >= 10 && at(0, "GIF")) return ok(v.getUint16(6, true), v.getUint16(8, true));
462
+ if (type === "image/jpeg") {
463
+ for (let i = 2; i + 9 < n;) {
464
+ if (bytes[i] !== 255) return null;
465
+ const m = bytes[i + 1];
466
+ if (m >= 192 && m <= 207 && m !== 196 && m !== 200 && m !== 204) return ok(v.getUint16(i + 7), v.getUint16(i + 5));
467
+ i += 2 + v.getUint16(i + 2);
468
+ }
469
+ return null;
470
+ }
471
+ if (type === "image/webp" && n >= 30 && at(0, "RIFF") && at(8, "WEBP")) {
472
+ if (at(12, "VP8 ")) return ok(v.getUint16(26, true) & 16383, v.getUint16(28, true) & 16383);
473
+ if (at(12, "VP8L")) {
474
+ const b = v.getUint32(21, true);
475
+ return ok((b & 16383) + 1, (b >> 14 & 16383) + 1);
476
+ }
477
+ if (at(12, "VP8X")) return ok(1 + (bytes[24] | bytes[25] << 8 | bytes[26] << 16), 1 + (bytes[27] | bytes[28] << 8 | bytes[29] << 16));
478
+ return null;
479
+ }
480
+ if (type === "image/svg+xml") {
481
+ const head = new TextDecoder().decode(bytes.subarray(0, 4096)), tag = /<svg\b[^>]*>/i.exec(head)?.[0] ?? "";
482
+ const attr = (name) => new RegExp(`\\s${name}\\s*=\\s*["']\\s*([\\d.]+)\\s*(px)?\\s*["']`, "i").exec(tag);
483
+ const w = attr("width"), h = attr("height");
484
+ if (w && h) return ok(Math.round(Number(w[1])), Math.round(Number(h[1])));
485
+ const box = /\sviewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(tag);
486
+ return box ? ok(Math.round(Number(box[1])), Math.round(Number(box[2]))) : null;
487
+ }
488
+ } catch {}
489
+ return null;
490
+ }
491
+ var TYPES$1, pictureType;
492
+ var init_picture = __esmMin((() => {
493
+ TYPES$1 = {
494
+ png: "image/png",
495
+ jpg: "image/jpeg",
496
+ jpeg: "image/jpeg",
497
+ gif: "image/gif",
498
+ svg: "image/svg+xml",
499
+ webp: "image/webp",
500
+ avif: "image/avif"
501
+ };
502
+ pictureType = (path) => {
503
+ const dot = path.lastIndexOf(".");
504
+ return dot < 0 || path.lastIndexOf("/") > dot ? null : TYPES$1[path.slice(dot + 1).toLowerCase()] ?? null;
505
+ };
506
+ }));
507
+ //#endregion
449
508
  //#region packages/core/src/kinds.ts
450
509
  function kindOf(type) {
451
510
  const t = type.trim().toLowerCase();
@@ -518,11 +577,25 @@ function outwardLinks(body) {
518
577
  }
519
578
  seen.add(href);
520
579
  }
580
+ for (const { href, embed, alt } of citeRefs(body)) {
581
+ if (!embed || seen.has(href)) continue;
582
+ const c = codeTarget(href);
583
+ if (!c || !pictureType(c.path)) continue;
584
+ out.push({
585
+ kind: "code",
586
+ href,
587
+ text: alt || c.path.split("/").at(-1),
588
+ path: c.path,
589
+ picture: true
590
+ });
591
+ seen.add(href);
592
+ }
521
593
  return out;
522
594
  }
523
595
  var ABOUT, DONE, EMBED;
524
596
  var init_kinds = __esmMin((() => {
525
597
  init_markdown();
598
+ init_picture();
526
599
  init_text();
527
600
  ABOUT = {
528
601
  question: "about",
@@ -725,7 +798,7 @@ var init_bundle = __esmMin((() => {
725
798
  concept.links = links;
726
799
  concept.outward = outwardLinks(concept.body);
727
800
  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)) {
801
+ for (const { href, title, embed, alt } of citeRefs(concept.body)) {
729
802
  const path = this.filePath(concept, href), kind = path === null ? null : citeKind(path);
730
803
  if (path === null || kind === null) continue;
731
804
  const t = strip((title ?? "").toLowerCase());
@@ -735,10 +808,11 @@ var init_bundle = __esmMin((() => {
735
808
  embed,
736
809
  broken: !this.files.has(path),
737
810
  rel: rating(title),
738
- align: t === "left" ? "left" : t === "center" || t === "centre" ? "center" : null
811
+ align: t === "left" ? "left" : t === "center" || t === "centre" ? "center" : null,
812
+ ...embed && alt ? { alt } : {}
739
813
  };
740
814
  concept.cites.push(cite);
741
- if (cite.broken) this.issue(concept.path, "warning", "broken-link", `broken link to ${path}`);
815
+ if (cite.broken) this.issue(concept.path, "warning", "broken-link", kind === "image" ? `picture not in the notes: ${path}` : `broken link to ${path}`);
742
816
  }
743
817
  }
744
818
  const byName = /* @__PURE__ */ new Map();
@@ -1809,13 +1883,16 @@ function mapModel(base) {
1809
1883
  s
1810
1884
  ]);
1811
1885
  }
1886
+ const holdsFiles = /* @__PURE__ */ new Set();
1887
+ for (const f of base.files) holdsFiles.add(f.includes("/") ? f.slice(0, f.lastIndexOf("/")) : "");
1812
1888
  const dir = (id) => {
1813
1889
  const d = base.directories.get(id);
1814
- return {
1890
+ const children = [...[...d.children].sort().map(dir).filter((c) => c !== null), ...[...d.concepts].sort().map(note)];
1891
+ return id && !children.length && (holdsFiles.has(id) || d.children.length) ? null : {
1815
1892
  kind: "dir",
1816
1893
  id: "d:" + id,
1817
1894
  ref: id,
1818
- children: [...[...d.children].sort().map(dir), ...[...d.concepts].sort().map(note)]
1895
+ children
1819
1896
  };
1820
1897
  };
1821
1898
  return {
@@ -1830,6 +1907,7 @@ var init_map = __esmMin((() => {
1830
1907
  //#region packages/core/src/index.ts
1831
1908
  var init_src$1 = __esmMin((() => {
1832
1909
  init_bundle();
1910
+ init_picture();
1833
1911
  init_frontmatter();
1834
1912
  init_source();
1835
1913
  init_markdown();
@@ -1884,8 +1962,9 @@ function loadBundle(root) {
1884
1962
  }
1885
1963
  //#endregion
1886
1964
  //#region packages/engine/src/api.ts
1887
- var EngineError, Conflict;
1965
+ var MAX_FILE_BYTES, EngineError, Conflict;
1888
1966
  var init_api = __esmMin((() => {
1967
+ MAX_FILE_BYTES = 2e7;
1889
1968
  EngineError = class extends Error {
1890
1969
  status;
1891
1970
  constructor(message, status = 400) {
@@ -2699,6 +2778,9 @@ something at one place in the note they have open.`;
2699
2778
  is drawn as the diagram. Draw one where a picture of how things stand to
2700
2779
  each other helps, or when asked; not by habit. Maths in a label goes
2701
2780
  between $$ and $$.
2781
+ - A picture is shown as ![what it shows](its/path.png). You can't see it: what
2782
+ you know of it is the words in the brackets. Keep a picture's line as it is
2783
+ when you rewrite around it, and never make up a picture's path.
2702
2784
  - Ordinary Markdown otherwise. No HTML.`;
2703
2785
  FORMAT_RULES = FORMAT;
2704
2786
  LOOKUP = (rounds, meaning) => `# Looking things up
@@ -2807,7 +2889,7 @@ var init_lookup = __esmMin((() => {
2807
2889
  ...str$1,
2808
2890
  description: "The note's id or path: basics/entropy or /basics/entropy.md."
2809
2891
  } }, ["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.", {
2892
+ 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 ![what it shows](path): you have its words, not the picture.", {
2811
2893
  id: str$1,
2812
2894
  section: {
2813
2895
  ...str$1,
@@ -4030,18 +4112,41 @@ var init_engine = __esmMin((() => {
4030
4112
  const notes = {}, broken = await this.brokenCode();
4031
4113
  for (const c of this.bundle.concepts.values()) {
4032
4114
  const bad = broken.get(c.id) ?? [];
4033
- const links = c.outward.map((o) => ({
4115
+ const links = c.outward.filter((o) => !(o.kind === "code" && o.picture)).map((o) => ({
4034
4116
  kind: o.kind,
4035
4117
  href: o.href,
4036
4118
  text: o.text,
4037
4119
  ...bad.includes(o.href) ? { broken: true } : {}
4038
4120
  }));
4121
+ const pictures = [], shown = /* @__PURE__ */ new Set();
4122
+ for (const p of c.cites) {
4123
+ if (p.kind !== "image" || !p.embed || shown.has(p.target)) continue;
4124
+ shown.add(p.target);
4125
+ pictures.push({
4126
+ href: p.target,
4127
+ path: p.target,
4128
+ from: "notes",
4129
+ text: p.alt || p.target.split("/").at(-1),
4130
+ ...p.broken ? { broken: true } : {}
4131
+ });
4132
+ }
4133
+ for (const o of c.outward) {
4134
+ if (o.kind !== "code" || !o.picture) continue;
4135
+ pictures.push({
4136
+ href: o.href,
4137
+ path: o.path,
4138
+ from: "code",
4139
+ text: o.text,
4140
+ ...bad.includes(o.href) ? { broken: true } : {}
4141
+ });
4142
+ }
4039
4143
  notes[c.id] = {
4040
4144
  title: c.title,
4041
4145
  tokens: estimate(c.body.trim()),
4042
4146
  ...c.kind !== "note" ? { kind: c.kind } : {},
4043
4147
  ...c.done ? { done: c.done } : {},
4044
- ...links.length ? { links } : {}
4148
+ ...links.length ? { links } : {},
4149
+ ...pictures.length ? { pictures } : {}
4045
4150
  };
4046
4151
  }
4047
4152
  return {
@@ -4438,6 +4543,107 @@ var init_engine = __esmMin((() => {
4438
4543
  this.queue = run.catch(() => void 0);
4439
4544
  return run;
4440
4545
  }
4546
+ /** A picture (M22), in the notes or in the base's repository. The path must be a picture's, within its folder,
4547
+ * and (in the notes) never in a hidden folder such as `.isorite/`: nothing but a picture is given this way. */
4548
+ async file(path, from = "notes") {
4549
+ const type = pictureType(path);
4550
+ if (!type) return null;
4551
+ let files, at;
4552
+ if (from === "code") {
4553
+ const target = codeTarget(`code:${path}`), where = (await this.settingsOfBase()).repository;
4554
+ files = target && where ? this.opts.repository?.(where) ?? null : null;
4555
+ at = target?.path ?? "";
4556
+ } else {
4557
+ at = path.replace(/^\/+/, "");
4558
+ files = at.split("/").some((p) => !p || p === "." || p === ".." || p.startsWith(".")) ? null : this.opts.notes;
4559
+ }
4560
+ const bytes = files ? await files.readBytes(at) : null;
4561
+ if (!bytes) return null;
4562
+ if (bytes.length > 2e7) throw new EngineError(`${path} is larger than isorite gives (${MAX_FILE_BYTES / 1e6} MB)`, 413);
4563
+ return {
4564
+ bytes,
4565
+ type,
4566
+ version: sha256(bytes).slice(0, 16)
4567
+ };
4568
+ }
4569
+ /** What was read of each picture, kept while its size and time are the same. */
4570
+ pictureCache = /* @__PURE__ */ new Map();
4571
+ /** Every picture in the notes (M22), found afresh: one put in the folder by hand since the base was opened is
4572
+ * 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. */
4573
+ async pictures() {
4574
+ const found = [];
4575
+ const visit = async (dir) => {
4576
+ for (const f of await this.opts.notes.list(dir)) {
4577
+ if (f.name.startsWith(".")) continue;
4578
+ const path = dir ? `${dir}/${f.name}` : f.name;
4579
+ if (f.dir) await visit(path);
4580
+ else if (pictureType(f.name)) found.push({
4581
+ path,
4582
+ ...f.size === void 0 ? {} : { size: f.size },
4583
+ ...f.mtime === void 0 ? {} : { mtime: f.mtime }
4584
+ });
4585
+ }
4586
+ };
4587
+ await visit("");
4588
+ const out = [], seen = /* @__PURE__ */ new Set();
4589
+ for (const f of found) {
4590
+ seen.add(f.path);
4591
+ const kept = this.pictureCache.get(f.path);
4592
+ if (kept && f.size !== void 0 && kept.size === f.size && kept.mtime === f.mtime) {
4593
+ out.push(kept.info);
4594
+ continue;
4595
+ }
4596
+ const bytes = f.size !== void 0 && f.size > 2e7 ? null : await this.opts.notes.readBytes(f.path);
4597
+ if (!bytes) continue;
4598
+ const px = pictureSize(bytes, pictureType(f.path));
4599
+ const info = {
4600
+ path: f.path,
4601
+ bytes: bytes.length,
4602
+ width: px?.width ?? null,
4603
+ height: px?.height ?? null,
4604
+ sha256: sha256(bytes)
4605
+ };
4606
+ this.pictureCache.set(f.path, {
4607
+ ...f.size === void 0 ? {} : { size: f.size },
4608
+ ...f.mtime === void 0 ? {} : { mtime: f.mtime },
4609
+ info
4610
+ });
4611
+ out.push(info);
4612
+ }
4613
+ for (const path of this.pictureCache.keys()) if (!seen.has(path)) this.pictureCache.delete(path);
4614
+ let changed = false;
4615
+ for (const path of seen) if (!this.files.has(path)) {
4616
+ this.files.set(path, { path });
4617
+ changed = true;
4618
+ }
4619
+ for (const path of [...this.files.keys()]) if (pictureType(path) && !seen.has(path)) {
4620
+ this.files.delete(path);
4621
+ changed = true;
4622
+ }
4623
+ if (changed) this.built = this.index = null;
4624
+ return out.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
4625
+ }
4626
+ /** Put a picture in the notes (M22). The path must be a picture's, out of any hidden folder, and free: a file
4627
+ * that's there is never replaced. Not journaled: a picture has no history, and stays when its link goes. */
4628
+ savePicture(path, bytes) {
4629
+ return this.one(async () => {
4630
+ const type = pictureType(path), parts = path.split("/");
4631
+ if (!type || parts.some((p) => !p || p === "." || p === ".." || p.startsWith(".")) || /[\\\x00-\x1f]/.test(path)) throw new EngineError(`not a picture's path: ${path}`, 400);
4632
+ if (bytes.length > 2e7) throw new EngineError(`the picture is larger than isorite takes (${MAX_FILE_BYTES / 1e6} MB)`, 413);
4633
+ if (await this.opts.notes.readBytes(path) !== null) throw new Conflict(`${path} is there already`, null);
4634
+ await this.opts.notes.writeBytes(path, bytes);
4635
+ this.files.set(path, { path });
4636
+ this.built = this.index = null;
4637
+ const px = pictureSize(bytes, type);
4638
+ return {
4639
+ path,
4640
+ bytes: bytes.length,
4641
+ width: px?.width ?? null,
4642
+ height: px?.height ?? null,
4643
+ sha256: sha256(bytes)
4644
+ };
4645
+ });
4646
+ }
4441
4647
  /** Where a note's file is: drafts and Understanding notes are among isorite's own files. */
4442
4648
  where(id) {
4443
4649
  if (!inBase(id)) return {
@@ -6491,6 +6697,8 @@ var init_ports = __esmMin((() => {
6491
6697
  systemClock = { now: () => Date.now() };
6492
6698
  MemoryFiles = class {
6493
6699
  files = /* @__PURE__ */ new Map();
6700
+ /** Files held as bytes (pictures), apart from text. */
6701
+ bytes = /* @__PURE__ */ new Map();
6494
6702
  constructor(files = {}) {
6495
6703
  for (const [path, text] of Object.entries(files)) this.files.set(path, text);
6496
6704
  }
@@ -6503,22 +6711,42 @@ var init_ports = __esmMin((() => {
6503
6711
  async append(path, text) {
6504
6712
  this.files.set(path, (this.files.get(path) ?? "") + text);
6505
6713
  }
6714
+ async readBytes(path) {
6715
+ const b = this.bytes.get(path), t = this.files.get(path);
6716
+ return b ?? (t === void 0 ? null : new TextEncoder().encode(t));
6717
+ }
6718
+ async writeBytes(path, bytes) {
6719
+ this.files.delete(path);
6720
+ this.bytes.set(path, bytes);
6721
+ }
6506
6722
  async remove(path) {
6507
6723
  this.files.delete(path);
6724
+ this.bytes.delete(path);
6508
6725
  }
6509
6726
  async list(dir) {
6510
6727
  const prefix = dir ? dir + "/" : "";
6511
6728
  const found = /* @__PURE__ */ new Map();
6512
- for (const path of this.files.keys()) {
6729
+ for (const path of [...this.files.keys(), ...this.bytes.keys()]) {
6513
6730
  if (!path.startsWith(prefix)) continue;
6514
6731
  const rest = path.slice(prefix.length), cut = rest.indexOf("/");
6515
- if (cut < 0) found.set(rest, false);
6516
- else found.set(rest.slice(0, cut), true);
6732
+ if (cut >= 0) {
6733
+ found.set(rest.slice(0, cut), {
6734
+ name: rest.slice(0, cut),
6735
+ dir: true
6736
+ });
6737
+ continue;
6738
+ }
6739
+ const b = this.bytes.get(path);
6740
+ found.set(rest, b ? {
6741
+ name: rest,
6742
+ dir: false,
6743
+ size: b.length
6744
+ } : {
6745
+ name: rest,
6746
+ dir: false
6747
+ });
6517
6748
  }
6518
- return [...found].map(([name, isDir]) => ({
6519
- name,
6520
- dir: isDir
6521
- }));
6749
+ return [...found.values()];
6522
6750
  }
6523
6751
  };
6524
6752
  MemorySecrets = class {
@@ -7978,6 +8206,7 @@ var Terminal = class {
7978
8206
  };
7979
8207
  //#endregion
7980
8208
  //#region packages/server/src/http.ts
8209
+ init_src$1();
7981
8210
  init_src();
7982
8211
  const LOOPBACK = /* @__PURE__ */ new Set([
7983
8212
  "localhost",
@@ -7999,6 +8228,8 @@ const TYPES = {
7999
8228
  ".map": "application/json",
8000
8229
  ".txt": "text/plain; charset=utf-8"
8001
8230
  };
8231
+ /** What a picture may do when opened on its own: be drawn, and nothing else (no script, no fetch, no form). */
8232
+ const PICTURE_CSP = "sandbox; default-src 'none'; img-src data:; style-src 'unsafe-inline'";
8002
8233
  const Edit = z.object({
8003
8234
  base: z.string().max(64),
8004
8235
  text: z.string()
@@ -8221,6 +8452,42 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal
8221
8452
  app.get("/api/map", (c) => answer(c, () => api.map()));
8222
8453
  app.get("/api/timeline", (c) => answer(c, () => api.timeline()));
8223
8454
  app.get("/api/search", (c) => answer(c, () => api.search(c.req.query("q") ?? "")));
8455
+ app.get("/api/pictures", (c) => answer(c, () => api.pictures()));
8456
+ app.post("/api/file", async (c) => {
8457
+ let origin = "";
8458
+ try {
8459
+ origin = new URL(c.req.header("origin") ?? "").host;
8460
+ } catch {}
8461
+ if (!c.req.header("origin") || origin !== c.req.header("host")) return refuse(c, 403, "cross-origin request refused");
8462
+ if (c.req.header("x-isorite-token") !== token) return refuse(c, 403, "bad token");
8463
+ const path = c.req.query("path") ?? "", type = pictureType(path);
8464
+ if (!type || (c.req.header("content-type") ?? "").split(";")[0].trim().toLowerCase() !== type) return refuse(c, 415, "a picture, of the type its name says");
8465
+ if (Number(c.req.header("content-length") ?? 0) > 2e7) return refuse(c, 413, "picture too large");
8466
+ const bytes = new Uint8Array(await c.req.arrayBuffer());
8467
+ if (bytes.length > 2e7) return refuse(c, 413, "picture too large");
8468
+ return answer(c, () => api.savePicture(path, bytes));
8469
+ });
8470
+ app.get("/api/file", async (c) => {
8471
+ const code = c.req.query("code"), path = code ?? c.req.query("path") ?? "";
8472
+ let found;
8473
+ try {
8474
+ found = await api.file(path, code === void 0 ? "notes" : "code");
8475
+ } catch (e) {
8476
+ if (e instanceof EngineError) return refuse(c, e.status, e.message);
8477
+ throw e;
8478
+ }
8479
+ if (!found) return refuse(c, 404, `no picture at ${path}`);
8480
+ const etag = `"${found.version}"`;
8481
+ const headers = {
8482
+ "Content-Type": found.type,
8483
+ "Cache-Control": "no-cache",
8484
+ ETag: etag,
8485
+ "X-Content-Type-Options": "nosniff",
8486
+ "Content-Security-Policy": PICTURE_CSP
8487
+ };
8488
+ if (c.req.header("if-none-match") === etag) return c.body(null, 304, headers);
8489
+ return c.body(found.bytes, 200, headers);
8490
+ });
8224
8491
  app.put("/api/note", async (c) => {
8225
8492
  const refused = writeRefused(c);
8226
8493
  if (refused) return refused;
@@ -8555,12 +8822,26 @@ var NodeFiles = class {
8555
8822
  throw e;
8556
8823
  }
8557
8824
  }
8558
- /** Written beside the file and renamed over it, so a reader never sees half a note. */
8825
+ async readBytes(path) {
8826
+ try {
8827
+ return new Uint8Array(await readFile(this.at(path)));
8828
+ } catch (e) {
8829
+ if (gone(e)) return null;
8830
+ throw e;
8831
+ }
8832
+ }
8559
8833
  async write(path, text) {
8834
+ await this.replace(path, text);
8835
+ }
8836
+ async writeBytes(path, bytes) {
8837
+ await this.replace(path, bytes);
8838
+ }
8839
+ /** Written beside the file and renamed over it, so a reader never sees half a note. */
8840
+ async replace(path, data) {
8560
8841
  const full = this.at(path), beside = `${full}.${process.pid}.${this.n++}.tmp`;
8561
8842
  await mkdir(dirname(full), { recursive: true });
8562
8843
  try {
8563
- await writeFile(beside, text, "utf8");
8844
+ await writeFile(beside, data, typeof data === "string" ? "utf8" : void 0);
8564
8845
  await rename(beside, full);
8565
8846
  } catch (e) {
8566
8847
  await rm(beside, { force: true });
@@ -8574,10 +8855,27 @@ var NodeFiles = class {
8574
8855
  }
8575
8856
  async list(dir) {
8576
8857
  try {
8577
- return (await readdir(this.at(dir), { withFileTypes: true })).filter((e) => e.isDirectory() || e.isFile()).map((e) => ({
8578
- name: e.name,
8579
- dir: e.isDirectory()
8580
- })).sort((a, b) => a.name < b.name ? -1 : 1);
8858
+ const at = this.at(dir), entries = await readdir(at, { withFileTypes: true });
8859
+ return (await Promise.all(entries.filter((e) => e.isDirectory() || e.isFile()).map(async (e) => {
8860
+ if (e.isDirectory()) return {
8861
+ name: e.name,
8862
+ dir: true
8863
+ };
8864
+ if (e.name.endsWith(".md")) return {
8865
+ name: e.name,
8866
+ dir: false
8867
+ };
8868
+ const s = await stat(resolve(at, e.name)).catch(() => null);
8869
+ return s ? {
8870
+ name: e.name,
8871
+ dir: false,
8872
+ size: s.size,
8873
+ mtime: s.mtimeMs
8874
+ } : {
8875
+ name: e.name,
8876
+ dir: false
8877
+ };
8878
+ }))).sort((a, b) => a.name < b.name ? -1 : 1);
8581
8879
  } catch (e) {
8582
8880
  if (gone(e)) return [];
8583
8881
  throw e;
@@ -8836,7 +9134,7 @@ var Setup = class {
8836
9134
  };
8837
9135
  //#endregion
8838
9136
  //#region packages/server/src/version.ts
8839
- const VERSION = "0.9.1";
9137
+ const VERSION = "0.10.0";
8840
9138
  //#endregion
8841
9139
  //#region packages/server/src/git.ts
8842
9140
  /** The top of the repository `folder` is in, or null: the nearest folder holding `.git`, a directory or, in a
@@ -11079,6 +11377,19 @@ const finderHttp = (pace = 0) => scriptedHttp(finderAnswer, pace);
11079
11377
  //#region packages/guide/src/node.ts
11080
11378
  /** In a checkout, the package's notes/; in the npm package, where this is bundled into dist/, the guide/ beside it. */
11081
11379
  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");
11380
+ /** The guide's pictures (M22), by path, as bytes. */
11381
+ function loadGuidePictures(dir = GUIDE_DIR) {
11382
+ const out = {};
11383
+ const visit = (at) => {
11384
+ for (const name of readdirSync(at)) {
11385
+ const path = join(at, name);
11386
+ if (statSync(path).isDirectory()) visit(path);
11387
+ else if (/\.(png|jpe?g|gif|svg|webp|avif)$/i.test(name)) out[relative(dir, path).split(sep).join("/")] = new Uint8Array(readFileSync(path));
11388
+ }
11389
+ };
11390
+ visit(dir);
11391
+ return out;
11392
+ }
11082
11393
  /** The guide's files as they stand in the package: its notes and its AGENTS.md, by path. */
11083
11394
  function loadGuide(dir = GUIDE_DIR) {
11084
11395
  const out = {};
@@ -11106,7 +11417,7 @@ function writeGuide(folder) {
11106
11417
  fresh: false
11107
11418
  };
11108
11419
  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())) {
11420
+ for (const [path, text] of [...Object.entries(loadGuide()), ...Object.entries(loadGuidePictures())]) {
11110
11421
  mkdirSync(dirname(join(at, path)), { recursive: true });
11111
11422
  writeFileSync(join(at, path), text);
11112
11423
  }
@@ -11155,8 +11466,21 @@ async function writeExample(folder, now = Date.now()) {
11155
11466
  mkdirSync(dirname(at), { recursive: true });
11156
11467
  writeFileSync(at, text);
11157
11468
  }
11469
+ copyPictures(NOTES_DIR, notes);
11158
11470
  return { notes };
11159
11471
  }
11472
+ /** The pictures among a folder's notes (M22), copied as they are: they have no history. */
11473
+ function copyPictures(from, to, dir = from) {
11474
+ for (const name of readdirSync(dir)) {
11475
+ const path = join(dir, name);
11476
+ if (statSync(path).isDirectory()) copyPictures(from, to, path);
11477
+ else if (citeKind(name) === "image") {
11478
+ const at = join(to, relative(from, path));
11479
+ mkdirSync(dirname(at), { recursive: true });
11480
+ copyFileSync(path, at);
11481
+ }
11482
+ }
11483
+ }
11160
11484
  const GEOMETRY_DIR = join(import.meta.dirname, "..", "geometry");
11161
11485
  /** The geometry example's notes, and the code beside them, as they stand today, by path. */
11162
11486
  function loadGeometry(dir = GEOMETRY_DIR) {
@@ -11287,7 +11611,8 @@ init_model();
11287
11611
  const USAGE = `isorite: an agentic harness for understanding
11288
11612
 
11289
11613
  isorite new <folder> start a knowledge base: the folder, empty, with isorite's .isorite/
11290
- isorite check <notes> check a folder of notes: frontmatter, links, cycles
11614
+ isorite check <notes> [--unused] check a folder of notes: frontmatter, links, cycles; --unused lists
11615
+ the pictures no note shows
11291
11616
  isorite serve <notes> [--port <n>] [--allow-host <name>]
11292
11617
  open it in the browser, on this machine only
11293
11618
  isorite migrate <study base> [--merge] move a study base (format 1) into its notes' .isorite/; exercises
@@ -11324,7 +11649,7 @@ const USAGE = `isorite: an agentic harness for understanding
11324
11649
  the browser; run again, as you left it. Iso is your model, or with
11325
11650
  none set up, the guide's finder (also: isorite --tutorial)
11326
11651
  `;
11327
- function check(notes, out = console.log) {
11652
+ function check(notes, out = console.log, unused = false) {
11328
11653
  const root = resolve(notes);
11329
11654
  if (!existsSync(root) || !statSync(root).isDirectory()) {
11330
11655
  out(`no folder at ${root}`);
@@ -11335,12 +11660,18 @@ function check(notes, out = console.log) {
11335
11660
  for (const c of cycles) out(`warning requires cycle: ${c.join(" -> ")}`);
11336
11661
  const errors = issues.filter((i) => i.level === "error").length;
11337
11662
  out(`${bundle.concepts.size} notes in ${bundle.directories.size} folders; ${errors} errors, ${issues.length - errors} warnings, ${cycles.length} requires cycles`);
11663
+ if (unused) {
11664
+ const cited = new Set([...bundle.concepts.values()].flatMap((c) => c.cites.map((x) => x.target)));
11665
+ const idle = [...bundle.files].filter((f) => citeKind(f) === "image" && !cited.has(f)).sort();
11666
+ for (const f of idle) out(`unused ${f}`);
11667
+ out(idle.length ? `${idle.length} ${idle.length === 1 ? "picture" : "pictures"} no note shows` : "every picture is shown by a note");
11668
+ }
11338
11669
  return errors ? 1 : 0;
11339
11670
  }
11340
11671
  function main(argv) {
11341
11672
  const [command, ...rest] = argv;
11342
11673
  try {
11343
- if (command === "check" && rest[0]) return check(rest[0]);
11674
+ if (command === "check" && rest.find((r) => !r.startsWith("--"))) return check(rest.find((r) => !r.startsWith("--")), console.log, rest.includes("--unused"));
11344
11675
  if (command === "new" && rest[0]) {
11345
11676
  const made = newBase(rest[0]);
11346
11677
  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}`);
@@ -0,0 +1,15 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 260" width="260" height="260" font-family="sans-serif">
2
+ <rect x="2" y="2" width="256" height="256" fill="none" stroke="#7a8794" stroke-width="2"/>
3
+ <g fill="none" stroke="#7a8794" stroke-width="1.5">
4
+ <path d="M130 2V258M2 130H258"/>
5
+ <path d="M66 2V130M2 66H130"/>
6
+ <path d="M194 130V258M130 194H258"/>
7
+ <path d="M34 2V66M2 34H66"/>
8
+ </g>
9
+ <g fill="#3f7fbf">
10
+ <circle cx="16" cy="18" r="5"/><circle cx="50" cy="48" r="5"/><circle cx="20" cy="52" r="5"/>
11
+ <circle cx="98" cy="40" r="5"/><circle cx="96" cy="104" r="5"/><circle cx="200" cy="70" r="5"/>
12
+ <circle cx="40" cy="190" r="5"/><circle cx="160" cy="160" r="5"/><circle cx="226" cy="168" r="5"/>
13
+ <circle cx="170" cy="226" r="5"/>
14
+ </g>
15
+ </svg>
@@ -6,4 +6,6 @@ description: The plane split into squares, and each square into four, until each
6
6
 
7
7
  A tree over the bodies in 2D. The root is a square holding them all; a square with more than one body is split into four. Each node also keeps its [centre of mass](/algorithms/centre-of-mass.md "requires"). In 3D it is an octree, with eight children.
8
8
 
9
+ ![Ten bodies: the square split until each square holds at most one](the-quadtree/subdivision.svg)
10
+
9
11
  How is a body inserted, and what does each node keep? I don't have this straight yet.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isorite",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "An agentic harness for understanding: your notes as a knowledge base, an Atlas of them, an agent that reads them, and study.",
5
5
  "license": "MIT",
6
6
  "type": "module",