volute 0.53.0 → 0.55.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/{activity-events-LQII6PZS.js → activity-events-TFAJBNBW.js} +2 -2
- package/dist/{ai-service-YW5SWCSB.js → ai-service-KEK2V3AP.js} +2 -2
- package/dist/{auth-KRDSM55Q.js → auth-Y27LTNZK.js} +3 -3
- package/dist/auto-upgrade-2MZF7NG3.js +46 -0
- package/dist/{avatar-image-52WGXLHV.js → avatar-image-SN576P45.js} +2 -2
- package/dist/{backup-WWXVIMYQ.js → backup-GDCWH6MC.js} +12 -12
- package/dist/channels-AFJWGX7W.js +203 -0
- package/dist/{chat-WK2BLM7B.js → chat-HVPOZK3O.js} +9 -9
- package/dist/{chunk-QUSMYGNQ.js → chunk-2674KIGI.js} +1 -0
- package/dist/chunk-2GATCSZK.js +520 -0
- package/dist/{chunk-P2TBUKLW.js → chunk-3SFKY4GI.js} +4 -4
- package/dist/{chunk-RMADJFA7.js → chunk-45QTYKQQ.js} +4 -4
- package/dist/{chunk-TAWRJ24T.js → chunk-4VAQKLMU.js} +8 -5
- package/dist/{chunk-OXWUEU5V.js → chunk-5U5GLZSD.js} +2 -2
- package/dist/{chunk-TPX6E6PL.js → chunk-6HBGUPBA.js} +2 -2
- package/dist/{chunk-UFTDRTCP.js → chunk-7AA4T2RT.js} +2 -2
- package/dist/{chunk-LN7XJ56R.js → chunk-7XKPG66D.js} +9 -9
- package/dist/{chunk-VDZ4I4ZP.js → chunk-BP2X5AIK.js} +8 -14
- package/dist/{chunk-PIZDPY4G.js → chunk-CKOA4LWV.js} +12 -12
- package/dist/{chunk-6NLW72HP.js → chunk-CKPAVQNK.js} +1 -1
- package/dist/{chunk-IQ7VNWH6.js → chunk-DUV4EMNW.js} +37 -3
- package/dist/{chunk-CV6BNGFK.js → chunk-FSXPPOPM.js} +3 -3
- package/dist/{chunk-UTKUO2KY.js → chunk-FZUPIS3T.js} +3 -3
- package/dist/{chunk-L7BKTZ5U.js → chunk-G3XAB4DK.js} +1 -1
- package/dist/{chunk-23UJGVYP.js → chunk-HLWQ3P3R.js} +50 -46
- package/dist/{chunk-AP2KQMMW.js → chunk-HTSWDOJE.js} +11 -14
- package/dist/{chunk-EDR47UE2.js → chunk-IR2FW4W7.js} +2 -2
- package/dist/{chunk-ICXY54ZC.js → chunk-IT4GEFDH.js} +1 -1
- package/dist/{chunk-F6D766VQ.js → chunk-KS62IHE2.js} +1 -1
- package/dist/{chunk-LNNI3MS4.js → chunk-MNKPAAQF.js} +9 -2
- package/dist/{chunk-HASADR3E.js → chunk-NCRG4SBR.js} +241 -66
- package/dist/{chunk-I7U5EEZC.js → chunk-NM5FDH4T.js} +1 -1
- package/dist/{chunk-7LT3YZUR.js → chunk-OFU6WJJZ.js} +1 -1
- package/dist/{chunk-M77B73VV.js → chunk-OIOW3FPT.js} +7 -2
- package/dist/{chunk-BDHKCLWK.js → chunk-OKM67ZWH.js} +1 -1
- package/dist/chunk-OUGD5L5U.js +918 -0
- package/dist/{chunk-X67BDDZR.js → chunk-P3GXOT2X.js} +48 -2
- package/dist/{chunk-U54PJPTB.js → chunk-PRZUBJ4K.js} +1 -1
- package/dist/{chunk-UO4MZQNO.js → chunk-QVS3C4DK.js} +15 -7
- package/dist/{chunk-RQRO6PC7.js → chunk-TMTAHKYP.js} +324 -48
- package/dist/{chunk-5DBGQBQR.js → chunk-UEZC5MNB.js} +29 -18
- package/dist/{chunk-7ARONCZW.js → chunk-VTFXZPC3.js} +13 -13
- package/dist/{chunk-UDZLPR34.js → chunk-W3OIPJPB.js} +14 -9
- package/dist/{chunk-HTB7KDRU.js → chunk-WZZCA23D.js} +5 -4
- package/dist/{chunk-5Y54AE3L.js → chunk-XAG53RWY.js} +1 -1
- package/dist/{chunk-HTRSSOHY.js → chunk-YU5EYQAE.js} +1 -1
- package/dist/{chunk-5N32EN4O.js → chunk-Z4Q6TQWL.js} +1 -1
- package/dist/{chunk-EGV6R6UF.js → chunk-Z7TNIIHW.js} +3 -1
- package/dist/cli.js +19 -19
- package/dist/{clock-SH4NQC2F.js → clock-X7V3C7NL.js} +3 -3
- package/dist/{cloud-sync-MJVJ33LD.js → cloud-sync-FM73SVFJ.js} +21 -21
- package/dist/{conversations-ZL6PHROD.js → conversations-FH3ODBL7.js} +3 -3
- package/dist/{create-G2RIKQYK.js → create-P75ZFTVN.js} +1 -1
- package/dist/{daemon-restart-BIGU4OKS.js → daemon-restart-JQMMMITM.js} +4 -4
- package/dist/daemon.js +469 -857
- package/dist/db-JH3NTERX.js +36 -0
- package/dist/delivery-manager-H23Q23N7.js +41 -0
- package/dist/{delivery-notices-65TB3CBX.js → delivery-notices-S7TKOACE.js} +5 -5
- package/dist/{delivery-router-RHANNTAC.js → delivery-router-KHBVEVVL.js} +4 -2
- package/dist/{down-QSNEMTTX.js → down-UN2EDX2Y.js} +3 -3
- package/dist/echo-text-ULEIFJJF.js +40 -0
- package/dist/{exec-ABD3DYA2.js → exec-Z34TTUO3.js} +1 -1
- package/dist/extensions-QD22GXZD.js +74 -0
- package/dist/{files-CHPNEQXN.js → files-DUGRXN6V.js} +5 -5
- package/dist/{import-DB2KULRO.js → import-RDDUGGFI.js} +3 -3
- package/dist/{isolation-GVV2IBAU.js → isolation-UQPDO34R.js} +1 -1
- package/dist/message-delivery-LU4EP244.js +46 -0
- package/dist/migrate-init-infrastructure-Z72UJ5NT.js +44 -0
- package/dist/migrate-name-placeholder-64AEAWPE.js +37 -0
- package/dist/{migrate-thread-config-EQSEP2XG.js → migrate-thread-config-SVF2M3D3.js} +1 -1
- package/dist/{mind-IX5AWUST.js → mind-BARCLI7O.js} +14 -14
- package/dist/{mind-activity-tracker-QF6DCX2X.js → mind-activity-tracker-UT23RGBF.js} +3 -3
- package/dist/{mind-contacts-BKUOUJPV.js → mind-contacts-PEXCDRXA.js} +8 -8
- package/dist/{mind-list-KNU34SUD.js → mind-list-7KBLPLES.js} +2 -2
- package/dist/mind-service-ZJCUBAUL.js +48 -0
- package/dist/{mind-sleep-D3DEYAWG.js → mind-sleep-24HV4RCA.js} +5 -5
- package/dist/{mind-status-BPTLQ5Z6.js → mind-status-4YW4PHMO.js} +6 -3
- package/dist/{package-BN5KMU54.js → package-AUDBDKAO.js} +12 -14
- package/dist/{prompts-HNKGCAEY.js → prompts-MOBF3TMO.js} +1 -1
- package/dist/{sandbox-NNWCTHRI.js → sandbox-VI7XWBIA.js} +2 -2
- package/dist/scheduler-GLINVC2B.js +27 -0
- package/dist/{seed-LHZ33UB4.js → seed-5JGR7ZRA.js} +1 -1
- package/dist/{seed-cmd-U4RVJ3UA.js → seed-cmd-M6A7N7F3.js} +2 -2
- package/dist/{seed-create-OQU4KNCB.js → seed-create-TGFZW4IO.js} +3 -3
- package/dist/{seed-readiness-WJYYRRCK.js → seed-readiness-XBH2MUNA.js} +2 -2
- package/dist/{seed-sprout-SFUYH2RG.js → seed-sprout-3IC5ZVTI.js} +11 -11
- package/dist/{send-FWZAMDBN.js → send-DHYBAYK4.js} +11 -11
- package/dist/{service-XQPMZAGI.js → service-T4P5W6RS.js} +2 -2
- package/dist/{service-install-2EWD7KXD.js → service-install-R4SNYUVO.js} +3 -3
- package/dist/{setup-RVSGCIF3.js → setup-6KAH3GRW.js} +4 -4
- package/dist/skills/dreaming/SKILL.md +2 -2
- package/dist/skills/dreaming/references/INSTALL.md +1 -1
- package/dist/skills/tending/SKILL.md +2 -0
- package/dist/skills/volute-mind/SKILL.md +8 -0
- package/dist/skills/volute-mind/references/integrations.md +3 -2
- package/dist/skills/volute-mind/references/routing.md +18 -5
- package/dist/{skills-QRPU6BWY.js → skills-LKQCYFVP.js} +3 -3
- package/dist/sleep-manager-Q3ITFNOJ.js +42 -0
- package/dist/{spirit-I5ECLGRI.js → spirit-FKXMGO3N.js} +8 -6
- package/dist/spirit-availability-5R2WYSWF.js +43 -0
- package/dist/{sprout-A2PMEF7Z.js → sprout-VEW3CUNX.js} +1 -1
- package/dist/src-246GSX7A.js +519 -0
- package/dist/src-LOUB3WPR.js +4189 -0
- package/dist/{status-EYG4A3OK.js → status-LCKTTIWS.js} +5 -5
- package/dist/{system-events-YTOU66QI.js → system-events-ZYOCYAO2.js} +5 -3
- package/dist/{systems-2DIJY4RF.js → systems-HL7SGSW7.js} +6 -6
- package/dist/{tailscale-KHNZFC7L.js → tailscale-F2IMBILZ.js} +1 -1
- package/dist/{template-hash-YKWMVYMM.js → template-hash-RJRMF6DT.js} +2 -2
- package/dist/{template-staleness-PY2CULH6.js → template-staleness-IBD722PN.js} +4 -4
- package/dist/{turn-tracker-NNHS6MAR.js → turn-tracker-BE34OI6O.js} +7 -7
- package/dist/{up-UGBHH743.js → up-J47J5JRB.js} +3 -3
- package/dist/{update-CIWI2JYA.js → update-ILCPVNDD.js} +2 -2
- package/dist/{upgrade-LIE46UBB.js → upgrade-XY2J7NWZ.js} +9 -1
- package/dist/variant-cleanup-JGJZ5KB4.js +24 -0
- package/dist/{version-notify-SJVAQLZI.js → version-notify-KIXYNHJC.js} +28 -13
- package/dist/web-assets/assets/{index-b7G7ZsVk.js → index-CoU1eUKE.js} +25 -25
- package/dist/web-assets/assets/index-hPn_fkz3.css +1 -0
- package/dist/web-assets/index.html +2 -2
- package/package.json +12 -14
- package/packages/extensions/intentions/dist/ui/assets/index-3FneLCKR.css +1 -0
- package/packages/extensions/intentions/dist/ui/assets/index-Bl5KEcli.js +67 -0
- package/packages/extensions/{notes → intentions}/dist/ui/index.html +3 -3
- package/packages/extensions/intentions/skills/intention-review/SKILL.md +38 -0
- package/packages/extensions/intentions/skills/intentions/SKILL.md +49 -0
- package/packages/extensions/intentions/skills/intentions/scripts/intentions-hook.sh +45 -0
- package/packages/extensions/pages/dist/ui/assets/index-BKxVQkA9.css +1 -0
- package/packages/extensions/pages/dist/ui/assets/index-BMt6sStP.js +68 -0
- package/packages/extensions/pages/dist/ui/index.html +2 -2
- package/packages/extensions/pages/skills/commons-gardening/SKILL.md +50 -0
- package/packages/extensions/pages/skills/pages/SKILL.md +140 -16
- package/templates/_base/.init/.config/prompts.json +2 -3
- package/templates/{pi → _base}/.init/.config/routes.json +1 -0
- package/templates/_base/.init/.local/hooks/pre-prompt/turn-context.ts +30 -0
- package/templates/_base/.init/memory/dreams/.gitkeep +0 -0
- package/templates/_base/home/.config/routes.json +1 -1
- package/templates/_base/home/VOLUTE.md +1 -1
- package/templates/_base/src/lib/codex-session-seed.ts +6 -55
- package/templates/_base/src/lib/context-breakdown.ts +11 -3
- package/templates/_base/src/lib/pi-session-seed.ts +8 -57
- package/templates/_base/src/lib/routing.ts +2 -2
- package/templates/_base/src/lib/seed-note.ts +4 -4
- package/templates/_base/src/lib/session-seed.ts +8 -53
- package/templates/_base/src/lib/startup.ts +4 -22
- package/templates/claude/.init/CLAUDE.md +4 -0
- package/templates/claude/package.json +2 -2
- package/templates/claude/src/agent.ts +60 -112
- package/templates/claude/src/lib/hooks/pre-compact.ts +6 -6
- package/templates/claude/src/lib/message-channel.ts +43 -15
- package/templates/claude/src/lib/recover.ts +69 -0
- package/templates/claude/src/lib/stream-consumer.ts +23 -5
- package/templates/claude/src/server.ts +0 -1
- package/templates/claude/volute-template.json +1 -1
- package/templates/codex/.init/AGENTS.md +4 -0
- package/templates/codex/package.json +2 -2
- package/templates/codex/src/agent.ts +23 -89
- package/templates/codex/volute-template.json +1 -1
- package/templates/pi/.init/MINDS.md +4 -0
- package/templates/pi/package.json +1 -1
- package/templates/pi/src/agent.ts +26 -93
- package/templates/pi/src/lib/mechanics-doc.ts +30 -0
- package/templates/pi/src/server.ts +3 -2
- package/templates/pi/volute-template.json +1 -1
- package/dist/channels-LW5ZLMSQ.js +0 -90
- package/dist/chunk-R772ET2M.js +0 -164
- package/dist/db-IPXRJUI7.js +0 -20
- package/dist/delivery-manager-FDHCZ7PU.js +0 -37
- package/dist/echo-text-KU6G4Z7V.js +0 -40
- package/dist/extensions-VACUKFA5.js +0 -60
- package/dist/message-delivery-TUXM3AAE.js +0 -46
- package/dist/mind-service-WHFBUKZE.js +0 -48
- package/dist/scheduler-5X5UIQOQ.js +0 -27
- package/dist/skills/plan-coordinator/SKILL.md +0 -60
- package/dist/sleep-manager-NWUCRBZB.js +0 -42
- package/dist/spirit-availability-NIUUYKU2.js +0 -43
- package/dist/src-HGNE2XG4.js +0 -832
- package/dist/src-LCSOQY6Z.js +0 -2222
- package/dist/src-UDMJZ6MH.js +0 -426
- package/dist/variant-cleanup-CZDLIY3F.js +0 -24
- package/dist/web-assets/assets/index-BHSU9r-4.css +0 -1
- package/packages/extensions/notes/dist/ui/assets/index-BBs8pyel.js +0 -67
- package/packages/extensions/notes/dist/ui/assets/index-D3cdui42.css +0 -1
- package/packages/extensions/notes/skills/notes/SKILL.md +0 -90
- package/packages/extensions/pages/dist/ui/assets/index-B589e8Xe.js +0 -2
- package/packages/extensions/pages/dist/ui/assets/index-BuXnE-rb.css +0 -1
- package/packages/extensions/plan/dist/ui/assets/index-B8s8wxyt.js +0 -67
- package/packages/extensions/plan/dist/ui/assets/index-jIkrt-vI.css +0 -1
- package/packages/extensions/plan/dist/ui/index.html +0 -14
- package/packages/extensions/plan/skills/plan/SKILL.md +0 -43
- package/packages/extensions/plan/skills/plan/scripts/plan-hook.sh +0 -37
- package/templates/claude/.init/.config/routes.json +0 -11
- package/templates/codex/.init/.config/routes.json +0 -11
- package/dist/{accept-I4GVGWA3.js → accept-RSDIVJL6.js} +3 -3
- package/dist/{bridge-KU2IKSH3.js → bridge-YK7NTRU5.js} +3 -3
- package/dist/{create-C3XZACTV.js → create-M727VUDF.js} +3 -3
- package/dist/{env-BJGM6FBA.js → env-7AZFBPWX.js} +7 -7
- package/dist/{extension-B2W52WZY.js → extension-MRYJWSMV.js} +3 -3
- package/dist/{list-R2GDWOEE.js → list-UQGIW7OF.js} +3 -3
- package/dist/{login-EZ4RRDQX.js → login-4NUS72Z5.js} +3 -3
- package/dist/{login-JDOAK465.js → login-57MMYILL.js} +4 -4
- package/dist/{logout-MWSIUOQU.js → logout-BYV433X6.js} +3 -3
- package/dist/{logout-K6ZGAJY5.js → logout-YFZAJRPY.js} +3 -3
- package/dist/{mind-history-RVF6KJ7K.js → mind-history-RZMMCHMX.js} +9 -9
- package/dist/{mind-wake-DCDFFQKX.js → mind-wake-LDGPRA42.js} +3 -3
- package/dist/{read-J5RM674N.js → read-RJ5NAW5J.js} +3 -3
- package/dist/{register-53LBGSDC.js → register-WNCSOTAU.js} +3 -3
- package/dist/{reject-K2A4UK2Y.js → reject-4NFLXKRN.js} +3 -3
- package/dist/{restart-R2WFNF5K.js → restart-B4GNMVF2.js} +7 -7
- package/dist/{skill-DXLLSUHB.js → skill-QYTBUDNQ.js} +7 -7
- package/dist/{start-XDOUAWN3.js → start-BDHJVOVX.js} +6 -6
- package/dist/{stop-O62RTEMD.js → stop-45QYSGYS.js} +7 -7
|
@@ -0,0 +1,4189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
areCommentsClosed,
|
|
4
|
+
citationsOf,
|
|
5
|
+
getAllSites,
|
|
6
|
+
getLivePageFiles,
|
|
7
|
+
getMindsWithSites,
|
|
8
|
+
getPage,
|
|
9
|
+
getPublishedPages,
|
|
10
|
+
getRecentPages,
|
|
11
|
+
getSystemPages,
|
|
12
|
+
initDb,
|
|
13
|
+
isSiteHome,
|
|
14
|
+
knownPageFiles,
|
|
15
|
+
syncPublishedPages,
|
|
16
|
+
syncSystemPages,
|
|
17
|
+
upsertPageWithTimestamps
|
|
18
|
+
} from "./chunk-2GATCSZK.js";
|
|
19
|
+
import {
|
|
20
|
+
createExtension
|
|
21
|
+
} from "./chunk-RG5TOL4O.js";
|
|
22
|
+
import "./chunk-K3NQKI34.js";
|
|
23
|
+
|
|
24
|
+
// packages/extensions/pages/src/index.ts
|
|
25
|
+
import { resolve as resolve10 } from "path";
|
|
26
|
+
|
|
27
|
+
// packages/extensions/pages/src/time.ts
|
|
28
|
+
function parseDbTimestamp(ts) {
|
|
29
|
+
if (/[zZ]$|[+-]\d\d:?\d\d$/.test(ts)) return new Date(ts);
|
|
30
|
+
return /* @__PURE__ */ new Date(`${ts.replace(" ", "T")}Z`);
|
|
31
|
+
}
|
|
32
|
+
function formatDate(ts) {
|
|
33
|
+
return parseDbTimestamp(ts).toLocaleDateString();
|
|
34
|
+
}
|
|
35
|
+
function formatDateTime(ts) {
|
|
36
|
+
return parseDbTimestamp(ts).toLocaleString();
|
|
37
|
+
}
|
|
38
|
+
function toIso(ts) {
|
|
39
|
+
return parseDbTimestamp(ts).toISOString();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// packages/extensions/pages/src/ambient-wording.ts
|
|
43
|
+
var LIVE_HEADER = "Around the house";
|
|
44
|
+
var WAKE_HEADER = "While you were away";
|
|
45
|
+
var QUIET_LEAD = "Nothing new on the shelf.";
|
|
46
|
+
function pageName(file) {
|
|
47
|
+
const base = file.split("/").pop() ?? file;
|
|
48
|
+
const stem = base.replace(/\.(md|html?)$/i, "");
|
|
49
|
+
if (stem === "index") return "the front page";
|
|
50
|
+
return stem.replace(/[-_]+/g, " ").trim() || file;
|
|
51
|
+
}
|
|
52
|
+
function whenWritten(timestamp, now = /* @__PURE__ */ new Date()) {
|
|
53
|
+
const d2 = parseDbTimestamp(timestamp);
|
|
54
|
+
if (Number.isNaN(d2.getTime())) return "a while back";
|
|
55
|
+
const month = d2.toLocaleString("en-US", { month: "long", timeZone: "UTC" });
|
|
56
|
+
const year = d2.getUTCFullYear();
|
|
57
|
+
return year === now.getUTCFullYear() ? month : `${month} ${year}`;
|
|
58
|
+
}
|
|
59
|
+
function formatList(names) {
|
|
60
|
+
if (names.length === 0) return "";
|
|
61
|
+
if (names.length === 1) return names[0];
|
|
62
|
+
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
|
63
|
+
return `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`;
|
|
64
|
+
}
|
|
65
|
+
function titled(ref, file) {
|
|
66
|
+
return `"${pageName(file)}" \u2014 ${ref}`;
|
|
67
|
+
}
|
|
68
|
+
function highlightClause(highlight) {
|
|
69
|
+
if (highlight === "citation") return " \u2014 and it names you.";
|
|
70
|
+
if (highlight === "link") return " \u2014 and it links to yours.";
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
function newPageLine(author, ref, file, highlight = null) {
|
|
74
|
+
const clause = highlightClause(highlight);
|
|
75
|
+
return `${author} published ${titled(ref, file)}${clause || "."}`;
|
|
76
|
+
}
|
|
77
|
+
function frontPageLine(author, ref, _file, highlight = null) {
|
|
78
|
+
const clause = highlightClause(highlight);
|
|
79
|
+
if (clause) return `${author} put up their front page \u2014 ${ref}${clause}`;
|
|
80
|
+
return `${author} put up their front page \u2014 ${ref}. A place to say hello.`;
|
|
81
|
+
}
|
|
82
|
+
function conversationLine(ref, file, participants) {
|
|
83
|
+
return `${titled(ref, file)} has turned into a conversation \u2014 ${formatList(participants)} are in it.`;
|
|
84
|
+
}
|
|
85
|
+
function furtherBackLine(authors) {
|
|
86
|
+
if (authors.length === 0) return null;
|
|
87
|
+
if (authors.length === 1) return `Further back, the shelf is mostly ${authors[0]}'s just now.`;
|
|
88
|
+
return `Further back, the shelf also holds work from ${formatList(authors)}.`;
|
|
89
|
+
}
|
|
90
|
+
function archiveLine(author, ref, file, when) {
|
|
91
|
+
return `From further back: ${author}'s ${titled(ref, file)}, from ${when} and still there.`;
|
|
92
|
+
}
|
|
93
|
+
function ownArchiveLine(ref, file, when) {
|
|
94
|
+
return `Something of your own: you wrote ${titled(ref, file)} in ${when}.`;
|
|
95
|
+
}
|
|
96
|
+
function block(header, lines) {
|
|
97
|
+
const kept = lines.filter((l3) => l3.length > 0);
|
|
98
|
+
if (kept.length === 0) return null;
|
|
99
|
+
return `${header}
|
|
100
|
+
${kept.join("\n")}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// packages/extensions/pages/src/mentions.ts
|
|
104
|
+
var MENTION_RE = /(^|[^\w@/])@([a-z0-9][a-z0-9_-]{0,62})/gi;
|
|
105
|
+
var CODE_RE = /```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)|`[^`\n]*`/g;
|
|
106
|
+
function maskCode(text) {
|
|
107
|
+
return text.replace(CODE_RE, (block2) => block2.replace(/[^\n]/g, " "));
|
|
108
|
+
}
|
|
109
|
+
function parseMentions(text) {
|
|
110
|
+
const seen = /* @__PURE__ */ new Set();
|
|
111
|
+
for (const m2 of maskCode(text).matchAll(MENTION_RE)) {
|
|
112
|
+
const name = m2[2].toLowerCase().replace(/[-_]+$/, "");
|
|
113
|
+
if (name) seen.add(name);
|
|
114
|
+
}
|
|
115
|
+
return [...seen];
|
|
116
|
+
}
|
|
117
|
+
async function resolveMentions(text, lookup) {
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const candidate of parseMentions(text)) {
|
|
120
|
+
const user = await lookup(candidate).catch(() => null);
|
|
121
|
+
if (user) out.push(user.username);
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function escapeHtmlAttr(s) {
|
|
126
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
127
|
+
}
|
|
128
|
+
function linkMentions(body, known) {
|
|
129
|
+
if (known.length === 0) return body;
|
|
130
|
+
const allowed = new Set(known.map((n) => n.toLowerCase()));
|
|
131
|
+
const masked = maskCode(body);
|
|
132
|
+
let out = "";
|
|
133
|
+
let last = 0;
|
|
134
|
+
for (const m2 of masked.matchAll(MENTION_RE)) {
|
|
135
|
+
const written = m2[2].replace(/[-_]+$/, "");
|
|
136
|
+
const name = written.toLowerCase();
|
|
137
|
+
if (!allowed.has(name)) continue;
|
|
138
|
+
const at = (m2.index ?? 0) + m2[1].length;
|
|
139
|
+
out += body.slice(last, at);
|
|
140
|
+
out += `<a class="mention" href="/ext/pages/public/${encodeURIComponent(name)}/">@${escapeHtmlAttr(written)}</a>`;
|
|
141
|
+
last = at + 1 + written.length;
|
|
142
|
+
}
|
|
143
|
+
return out + body.slice(last);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// packages/extensions/pages/src/social.ts
|
|
147
|
+
var COMMONS_MIND = "_system";
|
|
148
|
+
function recordRead(db, ref, reader) {
|
|
149
|
+
if (reader.username === ref.mind) return false;
|
|
150
|
+
const page = getPage(db, ref.mind, ref.file);
|
|
151
|
+
if (!page) return false;
|
|
152
|
+
if (page.deleted_at) return false;
|
|
153
|
+
const res = db.prepare("INSERT OR IGNORE INTO page_reads (mind, file, reader_id) VALUES (?, ?, ?)").run(ref.mind, ref.file, reader.id);
|
|
154
|
+
return res.changes > 0;
|
|
155
|
+
}
|
|
156
|
+
async function getPresence(db, getUser, ref, viewer) {
|
|
157
|
+
const isAuthor = viewer != null && viewer === ref.mind;
|
|
158
|
+
const isCommons = ref.mind === COMMONS_MIND;
|
|
159
|
+
if (!isAuthor && !isCommons) return null;
|
|
160
|
+
const rows = db.prepare("SELECT reader_id FROM page_reads WHERE mind = ? AND file = ? ORDER BY created_at, id").all(ref.mind, ref.file);
|
|
161
|
+
if (rows.length === 0) return null;
|
|
162
|
+
const cache = /* @__PURE__ */ new Map();
|
|
163
|
+
const readers = [];
|
|
164
|
+
let allMinds = true;
|
|
165
|
+
for (const row of rows) {
|
|
166
|
+
if (!cache.has(row.reader_id)) cache.set(row.reader_id, await getUser(row.reader_id));
|
|
167
|
+
const user = cache.get(row.reader_id) ?? null;
|
|
168
|
+
if (user?.user_type === "human") allMinds = false;
|
|
169
|
+
if (isAuthor) readers.push(user?.username ?? "someone");
|
|
170
|
+
}
|
|
171
|
+
const presence = { count: rows.length, readers: isAuthor ? readers : null, allMinds, text: null };
|
|
172
|
+
return { ...presence, text: describePresence(presence) };
|
|
173
|
+
}
|
|
174
|
+
function describePresence(presence) {
|
|
175
|
+
if (presence.count === 0) return null;
|
|
176
|
+
if (presence.readers && presence.readers.length > 0) {
|
|
177
|
+
return `Opened by ${formatList2(presence.readers)}.`;
|
|
178
|
+
}
|
|
179
|
+
const noun = presence.allMinds ? presence.count === 1 ? "mind" : "minds" : presence.count === 1 ? "reader" : "readers";
|
|
180
|
+
return `Opened by ${presence.count} ${noun}.`;
|
|
181
|
+
}
|
|
182
|
+
function formatList2(names) {
|
|
183
|
+
if (names.length === 1) return names[0];
|
|
184
|
+
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
|
185
|
+
return `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`;
|
|
186
|
+
}
|
|
187
|
+
function parsePageRef(ref) {
|
|
188
|
+
const idx = ref.indexOf("/");
|
|
189
|
+
if (idx <= 0 || idx === ref.length - 1) return null;
|
|
190
|
+
const mind = ref.slice(0, idx);
|
|
191
|
+
const file = ref.slice(idx + 1);
|
|
192
|
+
if (!mind || !file) return null;
|
|
193
|
+
if (file.split("/").some((seg) => seg === "." || seg === ".." || seg === "")) return null;
|
|
194
|
+
return { mind, file };
|
|
195
|
+
}
|
|
196
|
+
function resolvePageRef(db, ref) {
|
|
197
|
+
if (getPage(db, ref.mind, ref.file)) return { file: ref.file };
|
|
198
|
+
const live2 = getLivePageFiles(db, ref.mind);
|
|
199
|
+
const bare = ref.file.replace(/\.(md|html)$/, "");
|
|
200
|
+
const matches = live2.filter((f) => {
|
|
201
|
+
const fBare = f.replace(/\.(md|html)$/, "");
|
|
202
|
+
return fBare === bare || fBare.endsWith(`/${bare}`);
|
|
203
|
+
});
|
|
204
|
+
if (matches.length === 1) return { file: matches[0] };
|
|
205
|
+
return { candidates: (matches.length > 1 ? matches : live2).slice(0, 10) };
|
|
206
|
+
}
|
|
207
|
+
var COMMENT_COLUMNS = "id, mind, file, author_id, content, page_hash, kind, body_mind, body_file, created_at";
|
|
208
|
+
function resolveAttachedPage(db, owner, raw) {
|
|
209
|
+
const trimmed = raw.trim();
|
|
210
|
+
if (!trimmed) return { error: "No page given." };
|
|
211
|
+
const attempts = [trimmed];
|
|
212
|
+
if (trimmed.startsWith(`${owner}/`)) attempts.push(trimmed.slice(owner.length + 1));
|
|
213
|
+
for (const file of attempts) {
|
|
214
|
+
if (file.split("/").some((seg) => seg === "." || seg === ".." || seg === "")) continue;
|
|
215
|
+
const found = resolvePageRef(db, { mind: owner, file });
|
|
216
|
+
if (!("file" in found)) continue;
|
|
217
|
+
const page = getPage(db, owner, found.file);
|
|
218
|
+
if (page?.deleted_at) {
|
|
219
|
+
return {
|
|
220
|
+
error: `${owner}/${found.file} was deleted; a thread shouldn't point at a gravestone.`
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return { ref: { mind: owner, file: found.file } };
|
|
224
|
+
}
|
|
225
|
+
const parsed = parsePageRef(trimmed);
|
|
226
|
+
if (parsed && parsed.mind !== owner) {
|
|
227
|
+
return {
|
|
228
|
+
error: parsed.mind === "_system" ? "A commons page belongs to everyone, so it can't stand as your own response. Attach a page from your own site." : `${parsed.mind}/${parsed.file} isn't yours \u2014 a response should live in your own space. Attach one of your pages.`
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const live2 = getLivePageFiles(db, owner);
|
|
232
|
+
const hint = live2.length > 0 ? ` Did you mean ${live2.slice(0, 3).map((f) => `${owner}/${f}`).join(", ")}?` : " Publish it first \u2014 volute pages list shows what you have.";
|
|
233
|
+
return { error: `You have no published page at ${raw}.${hint}` };
|
|
234
|
+
}
|
|
235
|
+
async function decorate(getUser, rows, currentHash) {
|
|
236
|
+
const cache = /* @__PURE__ */ new Map();
|
|
237
|
+
const out = [];
|
|
238
|
+
for (const row of rows) {
|
|
239
|
+
if (!cache.has(row.author_id)) cache.set(row.author_id, await getUser(row.author_id));
|
|
240
|
+
const author = cache.get(row.author_id) ?? null;
|
|
241
|
+
out.push({
|
|
242
|
+
...row,
|
|
243
|
+
author_username: author?.username ?? "unknown",
|
|
244
|
+
author_display_name: author?.display_name ?? null,
|
|
245
|
+
stale: row.page_hash != null && currentHash != null && row.page_hash !== currentHash
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
async function getComments(db, getUser, ref) {
|
|
251
|
+
const rows = db.prepare(
|
|
252
|
+
`SELECT ${COMMENT_COLUMNS}
|
|
253
|
+
FROM page_comments WHERE mind = ? AND file = ? ORDER BY created_at, id`
|
|
254
|
+
).all(ref.mind, ref.file);
|
|
255
|
+
return decorate(getUser, rows, getPage(db, ref.mind, ref.file)?.hash ?? null);
|
|
256
|
+
}
|
|
257
|
+
async function addComment(db, getUser, ref, authorId, content, opts = {}) {
|
|
258
|
+
const pageHash = getPage(db, ref.mind, ref.file)?.hash ?? null;
|
|
259
|
+
const row = db.prepare(
|
|
260
|
+
`INSERT INTO page_comments (mind, file, author_id, content, page_hash, kind, body_mind, body_file)
|
|
261
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING ${COMMENT_COLUMNS}`
|
|
262
|
+
).get(
|
|
263
|
+
ref.mind,
|
|
264
|
+
ref.file,
|
|
265
|
+
authorId,
|
|
266
|
+
content,
|
|
267
|
+
pageHash,
|
|
268
|
+
opts.kind ?? "comment",
|
|
269
|
+
opts.body?.mind ?? null,
|
|
270
|
+
opts.body?.file ?? null
|
|
271
|
+
);
|
|
272
|
+
const [decorated] = await decorate(getUser, [row], pageHash);
|
|
273
|
+
return decorated;
|
|
274
|
+
}
|
|
275
|
+
function getComment(db, id) {
|
|
276
|
+
return db.prepare(`SELECT ${COMMENT_COLUMNS} FROM page_comments WHERE id = ?`).get(id) ?? null;
|
|
277
|
+
}
|
|
278
|
+
function setCommentBody(db, id, body) {
|
|
279
|
+
db.prepare("UPDATE page_comments SET body_mind = ?, body_file = ? WHERE id = ?").run(
|
|
280
|
+
body.mind,
|
|
281
|
+
body.file,
|
|
282
|
+
id
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
function getBacklinks(db, ref) {
|
|
286
|
+
const rows = db.prepare(
|
|
287
|
+
`SELECT c.id AS comment_id, c.body_mind AS mind, c.body_file AS file,
|
|
288
|
+
c.author_id, c.created_at
|
|
289
|
+
FROM page_comments c
|
|
290
|
+
JOIN published_pages p ON p.mind = c.body_mind AND p.file = c.body_file
|
|
291
|
+
WHERE c.mind = ? AND c.file = ? AND p.deleted_at IS NULL
|
|
292
|
+
ORDER BY c.created_at DESC, c.id DESC`
|
|
293
|
+
).all(ref.mind, ref.file);
|
|
294
|
+
return rows.map((r) => ({
|
|
295
|
+
mind: r.mind,
|
|
296
|
+
file: r.file,
|
|
297
|
+
comment_id: r.comment_id,
|
|
298
|
+
// The page's own mind is the author; no user lookup needed for a backlink.
|
|
299
|
+
author_username: r.mind,
|
|
300
|
+
created_at: r.created_at
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
function deleteComment(db, commentId, actor) {
|
|
304
|
+
const row = db.prepare("SELECT id, mind, author_id FROM page_comments WHERE id = ?").get(commentId);
|
|
305
|
+
if (!row) return false;
|
|
306
|
+
const authorized = actor.role === "admin" || actor.id === row.author_id || actor.username === row.mind;
|
|
307
|
+
if (!authorized) return false;
|
|
308
|
+
db.prepare("DELETE FROM page_comments WHERE id = ?").run(row.id);
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
function toggleReaction(db, ref, userId, emoji) {
|
|
312
|
+
const existing = db.prepare(
|
|
313
|
+
"SELECT id FROM page_reactions WHERE mind = ? AND file = ? AND user_id = ? AND emoji = ?"
|
|
314
|
+
).get(ref.mind, ref.file, userId, emoji);
|
|
315
|
+
if (existing) {
|
|
316
|
+
db.prepare("DELETE FROM page_reactions WHERE id = ?").run(existing.id);
|
|
317
|
+
return { added: false };
|
|
318
|
+
}
|
|
319
|
+
db.prepare("INSERT INTO page_reactions (mind, file, user_id, emoji) VALUES (?, ?, ?, ?)").run(
|
|
320
|
+
ref.mind,
|
|
321
|
+
ref.file,
|
|
322
|
+
userId,
|
|
323
|
+
emoji
|
|
324
|
+
);
|
|
325
|
+
return { added: true };
|
|
326
|
+
}
|
|
327
|
+
async function getReactions(db, getUser, ref) {
|
|
328
|
+
const rows = db.prepare(
|
|
329
|
+
"SELECT emoji, user_id FROM page_reactions WHERE mind = ? AND file = ? ORDER BY emoji, id"
|
|
330
|
+
).all(ref.mind, ref.file);
|
|
331
|
+
const cache = /* @__PURE__ */ new Map();
|
|
332
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
333
|
+
for (const r of rows) {
|
|
334
|
+
const list = grouped.get(r.emoji);
|
|
335
|
+
if (list) list.push(r.user_id);
|
|
336
|
+
else grouped.set(r.emoji, [r.user_id]);
|
|
337
|
+
}
|
|
338
|
+
const result = [];
|
|
339
|
+
for (const [emoji, userIds] of grouped) {
|
|
340
|
+
const usernames = [];
|
|
341
|
+
for (const uid of userIds) {
|
|
342
|
+
if (!cache.has(uid)) cache.set(uid, (await getUser(uid))?.username ?? "unknown");
|
|
343
|
+
usernames.push(cache.get(uid));
|
|
344
|
+
}
|
|
345
|
+
result.push({ emoji, count: userIds.length, usernames });
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
async function getThread(db, getUser, ref, viewer = null) {
|
|
350
|
+
const page = getPage(db, ref.mind, ref.file);
|
|
351
|
+
return {
|
|
352
|
+
mind: ref.mind,
|
|
353
|
+
file: ref.file,
|
|
354
|
+
deleted_at: page?.deleted_at ?? null,
|
|
355
|
+
comments_closed: areCommentsClosed(db, ref.mind, ref.file),
|
|
356
|
+
comments: await getComments(db, getUser, ref),
|
|
357
|
+
reactions: await getReactions(db, getUser, ref),
|
|
358
|
+
backlinks: getBacklinks(db, ref),
|
|
359
|
+
presence: await getPresence(db, getUser, ref, viewer)
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function isNotifiable(name) {
|
|
363
|
+
return name !== COMMONS_MIND;
|
|
364
|
+
}
|
|
365
|
+
async function notifyMentionedInComment(content, ctx, opts) {
|
|
366
|
+
const named = await resolveMentions(content, ctx.getUserByUsername);
|
|
367
|
+
const snippet = content.length > 80 ? `${content.slice(0, 80)}\u2026` : content;
|
|
368
|
+
const where = opts.where ?? `${opts.ref.mind}/${opts.ref.file}`;
|
|
369
|
+
const hailed = [];
|
|
370
|
+
for (const name of named) {
|
|
371
|
+
if (name === opts.actor || name === opts.ref.mind || !isNotifiable(name)) continue;
|
|
372
|
+
await ctx.recordNotice(name, `${opts.actor} named you in a comment on ${where}: "${snippet}"`);
|
|
373
|
+
hailed.push(name);
|
|
374
|
+
}
|
|
375
|
+
return hailed;
|
|
376
|
+
}
|
|
377
|
+
var TOMBSTONE_TEXT = "[this page was deleted]";
|
|
378
|
+
|
|
379
|
+
// packages/extensions/pages/src/ambient.ts
|
|
380
|
+
var LIVE_ITEMS = 2;
|
|
381
|
+
var WAKE_ITEMS = 3;
|
|
382
|
+
var LIVE_MIN_GAP_MS = 30 * 60 * 1e3;
|
|
383
|
+
var CONVERSATION_COMMENTERS = 2;
|
|
384
|
+
var ARCHIVE_MIN_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
385
|
+
var ARCHIVE_REVISIT_MS = 180 * 24 * 60 * 60 * 1e3;
|
|
386
|
+
var OWN_REVISIT_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
387
|
+
function foldSameAddress(candidates) {
|
|
388
|
+
const threadRefs = new Set(candidates.filter((c) => c.kind === "thread").map((c) => c.ref));
|
|
389
|
+
return candidates.filter((c) => !(c.kind === "page" && threadRefs.has(c.ref))).map(
|
|
390
|
+
(c) => c.kind === "thread" && candidates.some((o) => o.kind === "page" && o.ref === c.ref) ? { ...c, subsumes: ["page"] } : c
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
function sqlNow(now) {
|
|
394
|
+
return now.toISOString().slice(0, 19).replace("T", " ");
|
|
395
|
+
}
|
|
396
|
+
function getAmbientState(db, viewer, now) {
|
|
397
|
+
const row = db.prepare("SELECT watermark, last_block_at FROM ambient_state WHERE viewer = ?").get(viewer);
|
|
398
|
+
if (row) return { watermark: row.watermark, lastBlockAt: row.last_block_at };
|
|
399
|
+
const watermark = sqlNow(now);
|
|
400
|
+
db.prepare("INSERT OR IGNORE INTO ambient_state (viewer, watermark) VALUES (?, ?)").run(
|
|
401
|
+
viewer,
|
|
402
|
+
watermark
|
|
403
|
+
);
|
|
404
|
+
return { watermark, lastBlockAt: null };
|
|
405
|
+
}
|
|
406
|
+
function commit(db, viewer, shown, now) {
|
|
407
|
+
const stamp = sqlNow(now);
|
|
408
|
+
const mark = db.prepare(
|
|
409
|
+
`INSERT INTO ambient_shown (viewer, kind, ref, author, last_shown_at)
|
|
410
|
+
VALUES (?, ?, ?, ?, ?)
|
|
411
|
+
ON CONFLICT(viewer, kind, ref) DO UPDATE SET last_shown_at = excluded.last_shown_at`
|
|
412
|
+
);
|
|
413
|
+
for (const c of shown) {
|
|
414
|
+
mark.run(viewer, c.kind, c.ref, c.author, stamp);
|
|
415
|
+
for (const kind of c.subsumes ?? []) mark.run(viewer, kind, c.ref, c.author, stamp);
|
|
416
|
+
}
|
|
417
|
+
db.prepare(
|
|
418
|
+
`INSERT INTO ambient_state (viewer, watermark, last_block_at) VALUES (?, ?, ?)
|
|
419
|
+
ON CONFLICT(viewer) DO UPDATE SET watermark = excluded.watermark,
|
|
420
|
+
last_block_at = excluded.last_block_at`
|
|
421
|
+
).run(viewer, stamp, stamp);
|
|
422
|
+
}
|
|
423
|
+
function alreadyShown(db, viewer) {
|
|
424
|
+
const rows = db.prepare("SELECT kind, ref FROM ambient_shown WHERE viewer = ?").all(viewer);
|
|
425
|
+
return new Set(rows.map((r) => `${r.kind}:${r.ref}`));
|
|
426
|
+
}
|
|
427
|
+
function lastShownAt(db, viewer) {
|
|
428
|
+
const rows = db.prepare(
|
|
429
|
+
"SELECT kind, ref, COALESCE(last_shown_at, created_at) AS at FROM ambient_shown WHERE viewer = ?"
|
|
430
|
+
).all(viewer);
|
|
431
|
+
return new Map(rows.map((r) => [`${r.kind}:${r.ref}`, r.at]));
|
|
432
|
+
}
|
|
433
|
+
function seenByAuthor(db, viewer) {
|
|
434
|
+
const rows = db.prepare("SELECT author, COUNT(*) AS n FROM ambient_shown WHERE viewer = ? GROUP BY author").all(viewer);
|
|
435
|
+
return new Map(rows.map((r) => [r.author, r.n]));
|
|
436
|
+
}
|
|
437
|
+
function highlightFor(db, mind, file, viewer) {
|
|
438
|
+
const cited = db.prepare("SELECT 1 FROM page_citations WHERE mind = ? AND file = ? AND mentioned = ?").get(mind, file, viewer);
|
|
439
|
+
if (cited) return "citation";
|
|
440
|
+
const linked = db.prepare("SELECT 1 FROM page_links WHERE mind = ? AND file = ? AND target = ?").get(mind, file, viewer);
|
|
441
|
+
return linked ? "link" : null;
|
|
442
|
+
}
|
|
443
|
+
function newPages(db, viewer, since) {
|
|
444
|
+
const rows = db.prepare(
|
|
445
|
+
`SELECT mind, file, author, published_at FROM published_pages
|
|
446
|
+
WHERE deleted_at IS NULL AND published_at > ?
|
|
447
|
+
ORDER BY published_at ASC, id ASC`
|
|
448
|
+
).all(since);
|
|
449
|
+
const out = [];
|
|
450
|
+
for (const r of rows) {
|
|
451
|
+
const author = r.mind === COMMONS_MIND ? r.author ?? COMMONS_MIND : r.mind;
|
|
452
|
+
if (author === viewer) continue;
|
|
453
|
+
if (author === COMMONS_MIND) continue;
|
|
454
|
+
out.push({
|
|
455
|
+
kind: "page",
|
|
456
|
+
ref: `${r.mind}/${r.file}`,
|
|
457
|
+
mind: r.mind,
|
|
458
|
+
file: r.file,
|
|
459
|
+
author,
|
|
460
|
+
at: r.published_at,
|
|
461
|
+
highlight: highlightFor(db, r.mind, r.file, viewer)
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return out;
|
|
465
|
+
}
|
|
466
|
+
async function newConversations(db, getUser, viewer, since) {
|
|
467
|
+
const rows = db.prepare(
|
|
468
|
+
`SELECT c.mind, c.file, c.author_id, MIN(c.created_at) AS first_at
|
|
469
|
+
FROM page_comments c
|
|
470
|
+
WHERE c.kind = 'comment'
|
|
471
|
+
AND EXISTS (
|
|
472
|
+
SELECT 1 FROM page_comments n
|
|
473
|
+
WHERE n.mind = c.mind AND n.file = c.file
|
|
474
|
+
AND n.kind = 'comment' AND n.created_at > ?
|
|
475
|
+
)
|
|
476
|
+
AND EXISTS (
|
|
477
|
+
SELECT 1 FROM published_pages p
|
|
478
|
+
WHERE p.mind = c.mind AND p.file = c.file AND p.deleted_at IS NULL
|
|
479
|
+
)
|
|
480
|
+
GROUP BY c.mind, c.file, c.author_id
|
|
481
|
+
ORDER BY first_at ASC, c.author_id ASC`
|
|
482
|
+
).all(since);
|
|
483
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
484
|
+
for (const r of rows) {
|
|
485
|
+
const key = `${r.mind}/${r.file}`;
|
|
486
|
+
const entry = byPage.get(key) ?? { mind: r.mind, file: r.file, speakers: [] };
|
|
487
|
+
entry.speakers.push(r);
|
|
488
|
+
byPage.set(key, entry);
|
|
489
|
+
}
|
|
490
|
+
const out = [];
|
|
491
|
+
for (const [key, entry] of byPage) {
|
|
492
|
+
if (entry.speakers.length < CONVERSATION_COMMENTERS) continue;
|
|
493
|
+
if (entry.mind === viewer) continue;
|
|
494
|
+
const named = [];
|
|
495
|
+
for (const s of entry.speakers) {
|
|
496
|
+
const user = await getUser(s.author_id);
|
|
497
|
+
if (!user) continue;
|
|
498
|
+
if (entry.mind !== COMMONS_MIND && user.username === entry.mind) continue;
|
|
499
|
+
named.push({ username: user.username, at: s.first_at });
|
|
500
|
+
}
|
|
501
|
+
if (named.length < CONVERSATION_COMMENTERS) continue;
|
|
502
|
+
if (named.some((n) => n.username === viewer)) continue;
|
|
503
|
+
const crossedAt = named[CONVERSATION_COMMENTERS - 1].at;
|
|
504
|
+
if (crossedAt <= since) continue;
|
|
505
|
+
out.push({
|
|
506
|
+
kind: "thread",
|
|
507
|
+
ref: key,
|
|
508
|
+
mind: entry.mind,
|
|
509
|
+
file: entry.file,
|
|
510
|
+
// A conversation is credited to the page it grew on.
|
|
511
|
+
author: entry.mind,
|
|
512
|
+
at: crossedAt,
|
|
513
|
+
highlight: null,
|
|
514
|
+
participants: named.slice(0, CONVERSATION_COMMENTERS).map((n) => n.username)
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
function selectFairly(candidates, seen, limit) {
|
|
520
|
+
if (candidates.length === 0 || limit <= 0) return [];
|
|
521
|
+
const chosen = [];
|
|
522
|
+
const taken = /* @__PURE__ */ new Set();
|
|
523
|
+
const rank = (a, b2) => {
|
|
524
|
+
const seenDiff = (seen.get(a.author) ?? 0) - (seen.get(b2.author) ?? 0);
|
|
525
|
+
if (seenDiff !== 0) return seenDiff;
|
|
526
|
+
if (a.at !== b2.at) return a.at < b2.at ? -1 : 1;
|
|
527
|
+
return a.ref < b2.ref ? -1 : 1;
|
|
528
|
+
};
|
|
529
|
+
const highlighted = candidates.filter((c) => c.highlight !== null).sort(rank);
|
|
530
|
+
if (highlighted.length > 0) {
|
|
531
|
+
chosen.push(highlighted[0]);
|
|
532
|
+
taken.add(`${highlighted[0].kind}:${highlighted[0].ref}`);
|
|
533
|
+
}
|
|
534
|
+
const inBlock = /* @__PURE__ */ new Map();
|
|
535
|
+
const pool = candidates.filter((c) => !taken.has(`${c.kind}:${c.ref}`));
|
|
536
|
+
for (const c of chosen) inBlock.set(c.author, (inBlock.get(c.author) ?? 0) + 1);
|
|
537
|
+
while (chosen.length < limit && pool.length > 0) {
|
|
538
|
+
pool.sort((a, b2) => {
|
|
539
|
+
const mine = (inBlock.get(a.author) ?? 0) - (inBlock.get(b2.author) ?? 0);
|
|
540
|
+
if (mine !== 0) return mine;
|
|
541
|
+
const seenDiff = (seen.get(a.author) ?? 0) - (seen.get(b2.author) ?? 0);
|
|
542
|
+
if (seenDiff !== 0) return seenDiff;
|
|
543
|
+
if (a.at !== b2.at) return a.at < b2.at ? -1 : 1;
|
|
544
|
+
return a.ref < b2.ref ? -1 : 1;
|
|
545
|
+
});
|
|
546
|
+
const next = pool.shift();
|
|
547
|
+
chosen.push(next);
|
|
548
|
+
inBlock.set(next.author, (inBlock.get(next.author) ?? 0) + 1);
|
|
549
|
+
}
|
|
550
|
+
return chosen;
|
|
551
|
+
}
|
|
552
|
+
function renderItem(c) {
|
|
553
|
+
if (c.kind === "thread") {
|
|
554
|
+
return conversationLine(c.ref, c.file, c.participants ?? []);
|
|
555
|
+
}
|
|
556
|
+
if (c.mind !== COMMONS_MIND && isSiteHome(c.file)) {
|
|
557
|
+
return frontPageLine(c.author, c.ref, c.file, c.highlight);
|
|
558
|
+
}
|
|
559
|
+
return newPageLine(c.author, c.ref, c.file, c.highlight);
|
|
560
|
+
}
|
|
561
|
+
async function live(db, getUser, viewer, budget, now) {
|
|
562
|
+
const state = getAmbientState(db, viewer, now);
|
|
563
|
+
if (state.lastBlockAt) {
|
|
564
|
+
const since = now.getTime() - parseDbTimestamp(state.lastBlockAt).getTime();
|
|
565
|
+
if (since < LIVE_MIN_GAP_MS) return null;
|
|
566
|
+
}
|
|
567
|
+
const shown = alreadyShown(db, viewer);
|
|
568
|
+
const candidates = foldSameAddress(
|
|
569
|
+
[
|
|
570
|
+
...newPages(db, viewer, state.watermark),
|
|
571
|
+
...await newConversations(db, getUser, viewer, state.watermark)
|
|
572
|
+
].filter((c) => !shown.has(`${c.kind}:${c.ref}`))
|
|
573
|
+
);
|
|
574
|
+
if (candidates.length === 0) return null;
|
|
575
|
+
const picked = selectFairly(candidates, seenByAuthor(db, viewer), LIVE_ITEMS);
|
|
576
|
+
return emit(db, viewer, LIVE_HEADER, picked, [], budget, now);
|
|
577
|
+
}
|
|
578
|
+
async function retrospective(db, getUser, viewer, budget, now) {
|
|
579
|
+
const state = getAmbientState(db, viewer, now);
|
|
580
|
+
const shown = alreadyShown(db, viewer);
|
|
581
|
+
const seen = seenByAuthor(db, viewer);
|
|
582
|
+
const candidates = foldSameAddress(
|
|
583
|
+
[
|
|
584
|
+
...newPages(db, viewer, state.watermark),
|
|
585
|
+
...await newConversations(db, getUser, viewer, state.watermark)
|
|
586
|
+
].filter((c) => !shown.has(`${c.kind}:${c.ref}`))
|
|
587
|
+
);
|
|
588
|
+
if (candidates.length > 0) {
|
|
589
|
+
const picked = selectFairly(candidates, seen, WAKE_ITEMS);
|
|
590
|
+
const pickedKeys = new Set(picked.map((p) => `${p.kind}:${p.ref}`));
|
|
591
|
+
const rest = candidates.filter((c) => !pickedKeys.has(`${c.kind}:${c.ref}`));
|
|
592
|
+
const restAuthors = [...new Set(rest.map((c) => c.author))];
|
|
593
|
+
const tail = furtherBackLine(restAuthors);
|
|
594
|
+
return emit(db, viewer, WAKE_HEADER, picked, tail ? [tail] : [], budget, now);
|
|
595
|
+
}
|
|
596
|
+
return quiet(db, viewer, seen, budget, now);
|
|
597
|
+
}
|
|
598
|
+
function pickArchive(candidates, seen, shownAt, cooldownMs, now) {
|
|
599
|
+
const fresh = [];
|
|
600
|
+
const revisit = [];
|
|
601
|
+
for (const c of candidates) {
|
|
602
|
+
const at = shownAt.get(`${c.kind}:${c.ref}`);
|
|
603
|
+
if (at === void 0) {
|
|
604
|
+
fresh.push(c);
|
|
605
|
+
} else if (now.getTime() - parseDbTimestamp(at).getTime() >= cooldownMs) {
|
|
606
|
+
revisit.push({ c, at });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (fresh.length > 0) return selectFairly(fresh, seen, 1)[0] ?? null;
|
|
610
|
+
revisit.sort((a, b2) => a.at !== b2.at ? a.at < b2.at ? -1 : 1 : a.c.ref < b2.c.ref ? -1 : 1);
|
|
611
|
+
return revisit[0]?.c ?? null;
|
|
612
|
+
}
|
|
613
|
+
function quiet(db, viewer, seen, budget, now) {
|
|
614
|
+
const cutoff = sqlNow(new Date(now.getTime() - ARCHIVE_MIN_AGE_MS));
|
|
615
|
+
const rows = db.prepare(
|
|
616
|
+
`SELECT mind, file, author, published_at FROM published_pages
|
|
617
|
+
WHERE deleted_at IS NULL AND published_at < ?
|
|
618
|
+
ORDER BY published_at ASC, id ASC`
|
|
619
|
+
).all(cutoff);
|
|
620
|
+
const others = [];
|
|
621
|
+
const own = [];
|
|
622
|
+
for (const r of rows) {
|
|
623
|
+
const author = r.mind === COMMONS_MIND ? r.author ?? COMMONS_MIND : r.mind;
|
|
624
|
+
if (author === COMMONS_MIND) continue;
|
|
625
|
+
const c = {
|
|
626
|
+
kind: "page",
|
|
627
|
+
ref: `${r.mind}/${r.file}`,
|
|
628
|
+
mind: r.mind,
|
|
629
|
+
file: r.file,
|
|
630
|
+
author,
|
|
631
|
+
at: r.published_at,
|
|
632
|
+
highlight: null
|
|
633
|
+
};
|
|
634
|
+
if (author === viewer) own.push({ ...c, kind: "own" });
|
|
635
|
+
else others.push(c);
|
|
636
|
+
}
|
|
637
|
+
const shownAt = lastShownAt(db, viewer);
|
|
638
|
+
const lines = [{ text: QUIET_LEAD, artifact: null }];
|
|
639
|
+
const other = pickArchive(others, seen, shownAt, ARCHIVE_REVISIT_MS, now);
|
|
640
|
+
if (other) {
|
|
641
|
+
lines.push({
|
|
642
|
+
text: archiveLine(other.author, other.ref, other.file, whenWritten(other.at, now)),
|
|
643
|
+
artifact: other
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
const mine = pickArchive(own, seen, shownAt, OWN_REVISIT_MS, now);
|
|
647
|
+
if (mine) {
|
|
648
|
+
lines.push({
|
|
649
|
+
text: ownArchiveLine(mine.ref, mine.file, whenWritten(mine.at, now)),
|
|
650
|
+
artifact: mine
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
if (lines.length === 1) return null;
|
|
654
|
+
return emitLines(db, viewer, WAKE_HEADER, lines, budget, now);
|
|
655
|
+
}
|
|
656
|
+
function emit(db, viewer, header, picked, extra, budget, now) {
|
|
657
|
+
if (picked.length === 0) return null;
|
|
658
|
+
const lines = [
|
|
659
|
+
...picked.map((c) => ({ text: renderItem(c), artifact: c })),
|
|
660
|
+
...extra.map((text) => ({ text, artifact: null }))
|
|
661
|
+
];
|
|
662
|
+
return emitLines(db, viewer, header, lines, budget, now);
|
|
663
|
+
}
|
|
664
|
+
function emitLines(db, viewer, header, lines, budget, now) {
|
|
665
|
+
let items = [...lines];
|
|
666
|
+
while (items.length > 0) {
|
|
667
|
+
const text = block(
|
|
668
|
+
header,
|
|
669
|
+
items.map((l3) => l3.text)
|
|
670
|
+
)?.trim() ?? null;
|
|
671
|
+
if (text && text.length <= budget) {
|
|
672
|
+
const kept = items.map((l3) => l3.artifact).filter((a) => a !== null);
|
|
673
|
+
if (kept.length === 0) return null;
|
|
674
|
+
commit(db, viewer, kept, now);
|
|
675
|
+
return text;
|
|
676
|
+
}
|
|
677
|
+
items = items.slice(0, -1);
|
|
678
|
+
}
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
async function ambientTurnContext(mindName, ctx, opts, now = /* @__PURE__ */ new Date()) {
|
|
682
|
+
const db = ctx.db;
|
|
683
|
+
if (!db) return null;
|
|
684
|
+
try {
|
|
685
|
+
return opts.reason === "wake" ? await retrospective(db, ctx.getUser, mindName, opts.budget, now) : await live(db, ctx.getUser, mindName, opts.budget, now);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
console.warn(`[pages] ambient context failed for ${mindName}: ${err.message}`);
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// packages/extensions/pages/src/commands.ts
|
|
693
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
694
|
+
import { resolve as resolve7, sep as sep4 } from "path";
|
|
695
|
+
|
|
696
|
+
// packages/extensions/pages/src/commons.ts
|
|
697
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
698
|
+
import { resolve } from "path";
|
|
699
|
+
function commonsReport(repoDir, systemFiles, mindsWithSites, residents = []) {
|
|
700
|
+
const mindsWithoutSites = residents.filter((m2) => !mindsWithSites.includes(m2));
|
|
701
|
+
const index = systemFiles.find((f) => f === "index.md" || f === "index.html");
|
|
702
|
+
if (!index) {
|
|
703
|
+
return {
|
|
704
|
+
hasIndex: false,
|
|
705
|
+
orphanPages: [...systemFiles],
|
|
706
|
+
unlinkedMinds: [...mindsWithSites],
|
|
707
|
+
mindsWithoutSites
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
const contentOf = (f) => {
|
|
711
|
+
try {
|
|
712
|
+
return readFileSync(resolve(repoDir, f), "utf-8");
|
|
713
|
+
} catch {
|
|
714
|
+
return "";
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
const reached = /* @__PURE__ */ new Set([index]);
|
|
718
|
+
const queue = [index];
|
|
719
|
+
while (queue.length > 0) {
|
|
720
|
+
const content = contentOf(queue.shift());
|
|
721
|
+
for (const f of systemFiles) {
|
|
722
|
+
if (!reached.has(f) && content.includes(f)) {
|
|
723
|
+
reached.add(f);
|
|
724
|
+
queue.push(f);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const orphanPages = systemFiles.filter((f) => !reached.has(f));
|
|
729
|
+
const reachableText = [...reached].map(contentOf).join("\n");
|
|
730
|
+
const unlinkedMinds = mindsWithSites.filter(
|
|
731
|
+
(m2) => !reachableText.includes(`../${m2}/`) && !reachableText.includes(`/ext/pages/public/${m2}/`)
|
|
732
|
+
);
|
|
733
|
+
return { hasIndex: true, orphanPages, unlinkedMinds, mindsWithoutSites };
|
|
734
|
+
}
|
|
735
|
+
async function maybeSendCommonsCue(ctx, repoDir) {
|
|
736
|
+
const cueFlag = resolve(ctx.dataDir, ".commons-cue-sent");
|
|
737
|
+
const hasIndex = existsSync(resolve(repoDir, "index.md")) || existsSync(resolve(repoDir, "index.html"));
|
|
738
|
+
if (hasIndex || existsSync(cueFlag)) return;
|
|
739
|
+
const spirit = ctx.getSpiritName();
|
|
740
|
+
if (!spirit) return;
|
|
741
|
+
try {
|
|
742
|
+
if (!await ctx.getMindDir(spirit)) return;
|
|
743
|
+
await ctx.recordNotice(
|
|
744
|
+
spirit,
|
|
745
|
+
"The commons \u2014 the shared pages at pages/_system/ that every mind here can edit \u2014 has no index yet. When you have a quiet moment, you might make one: a page that says what this place is, in your own voice, with room in it for the others. The commons-gardening skill describes the craft."
|
|
746
|
+
);
|
|
747
|
+
writeFileSync(cueFlag, (/* @__PURE__ */ new Date()).toISOString());
|
|
748
|
+
} catch (err) {
|
|
749
|
+
console.warn(`[pages] commons cue failed: ${err.message}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// packages/extensions/pages/src/migrate-notes.ts
|
|
754
|
+
import { execFile } from "child_process";
|
|
755
|
+
import { createHash as createHash2 } from "crypto";
|
|
756
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
757
|
+
import { resolve as resolve4, sep as sep2 } from "path";
|
|
758
|
+
|
|
759
|
+
// packages/extensions/pages/src/publish.ts
|
|
760
|
+
import { createHash } from "crypto";
|
|
761
|
+
import {
|
|
762
|
+
cpSync,
|
|
763
|
+
existsSync as existsSync3,
|
|
764
|
+
mkdirSync,
|
|
765
|
+
readdirSync,
|
|
766
|
+
readFileSync as readFileSync2,
|
|
767
|
+
rmSync,
|
|
768
|
+
statSync,
|
|
769
|
+
writeFileSync as writeFileSync2
|
|
770
|
+
} from "fs";
|
|
771
|
+
import { relative as relative2, resolve as resolve3, sep } from "path";
|
|
772
|
+
|
|
773
|
+
// packages/extensions/pages/src/links.ts
|
|
774
|
+
var LINK_RE = /(?:\.\.\/|\/ext\/pages\/public\/)([a-z0-9][a-z0-9_-]{0,62})\//gi;
|
|
775
|
+
function parseLinks(text) {
|
|
776
|
+
const seen = /* @__PURE__ */ new Set();
|
|
777
|
+
for (const m2 of text.matchAll(LINK_RE)) {
|
|
778
|
+
seen.add(m2[1].toLowerCase());
|
|
779
|
+
}
|
|
780
|
+
return [...seen];
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// packages/extensions/pages/src/markdown.ts
|
|
784
|
+
import { existsSync as existsSync2 } from "fs";
|
|
785
|
+
import { dirname, relative, resolve as resolve2 } from "path";
|
|
786
|
+
import DOMPurify from "isomorphic-dompurify";
|
|
787
|
+
|
|
788
|
+
// node_modules/marked/lib/marked.esm.js
|
|
789
|
+
function z() {
|
|
790
|
+
return { async: false, breaks: false, extensions: null, gfm: true, hooks: null, pedantic: false, renderer: null, silent: false, tokenizer: null, walkTokens: null };
|
|
791
|
+
}
|
|
792
|
+
var T = z();
|
|
793
|
+
function N(l3) {
|
|
794
|
+
T = l3;
|
|
795
|
+
}
|
|
796
|
+
var _ = { exec: () => null };
|
|
797
|
+
function E(l3) {
|
|
798
|
+
let e = [];
|
|
799
|
+
return (t) => {
|
|
800
|
+
let n = Math.max(0, Math.min(3, t - 1)), s = e[n];
|
|
801
|
+
return s || (s = l3(n), e[n] = s), s;
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function d(l3, e = "") {
|
|
805
|
+
let t = typeof l3 == "string" ? l3 : l3.source, n = { replace: (s, r) => {
|
|
806
|
+
let i = typeof r == "string" ? r : r.source;
|
|
807
|
+
return i = i.replace(m.caret, "$1"), t = t.replace(s, i), n;
|
|
808
|
+
}, getRegex: () => new RegExp(t, e) };
|
|
809
|
+
return n;
|
|
810
|
+
}
|
|
811
|
+
var Te = ((l3 = "") => {
|
|
812
|
+
try {
|
|
813
|
+
return !!new RegExp("(?<=1)(?<!1)" + l3);
|
|
814
|
+
} catch {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
})();
|
|
818
|
+
var m = { codeRemoveIndent: /^(?: {1,4}| {0,3}\t)/gm, outputLinkReplace: /\\([\[\]])/g, indentCodeCompensation: /^(\s+)(?:```)/, beginningSpace: /^\s+/, endingHash: /#$/, startingSpaceChar: /^ /, endingSpaceChar: / $/, nonSpaceChar: /[^ ]/, newLineCharGlobal: /\n/g, tabCharGlobal: /\t/g, multipleSpaceGlobal: /\s+/g, blankLine: /^[ \t]*$/, doubleBlankLine: /\n[ \t]*\n[ \t]*$/, blockquoteStart: /^ {0,3}>/, blockquoteSetextReplace: /\n {0,3}((?:=+|-+) *)(?=\n|$)/g, blockquoteSetextReplace2: /^ {0,3}>[ \t]?/gm, listReplaceNesting: /^ {1,4}(?=( {4})*[^ ])/g, listIsTask: /^\[[ xX]\] +\S/, listReplaceTask: /^\[[ xX]\] +/, listTaskCheckbox: /\[[ xX]\]/, anyLine: /\n.*\n/, hrefBrackets: /^<(.*)>$/, tableDelimiter: /[:|]/, tableAlignChars: /^\||\| *$/g, tableRowBlankLine: /\n[ \t]*$/, tableAlignRight: /^ *-+: *$/, tableAlignCenter: /^ *:-+: *$/, tableAlignLeft: /^ *:-+ *$/, startATag: /^<a /i, endATag: /^<\/a>/i, startPreScriptTag: /^<(pre|code|kbd|script)(\s|>)/i, endPreScriptTag: /^<\/(pre|code|kbd|script)(\s|>)/i, startAngleBracket: /^</, endAngleBracket: />$/, pedanticHrefTitle: /^([^'"]*[^\s])\s+(['"])(.*)\2/, unicodeAlphaNumeric: /[\p{L}\p{N}]/u, escapeTest: /[&<>"']/, escapeReplace: /[&<>"']/g, escapeTestNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/, escapeReplaceNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g, caret: /(^|[^\[])\^/g, percentDecode: /%25/g, findPipe: /\|/g, splitPipe: / \|/, slashPipe: /\\\|/g, carriageReturn: /\r\n|\r/g, spaceLine: /^ +$/gm, notSpaceStart: /^\S*/, endingNewline: /\n$/, listItemRegex: (l3) => new RegExp(`^( {0,3}${l3})((?:[ ][^\\n]*)?(?:\\n|$))`), nextBulletRegex: E((l3) => new RegExp(`^ {0,${l3}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`)), hrRegex: E((l3) => new RegExp(`^ {0,${l3}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`)), fencesBeginRegex: E((l3) => new RegExp(`^ {0,${l3}}(?:\`\`\`|~~~)`)), headingBeginRegex: E((l3) => new RegExp(`^ {0,${l3}}#`)), htmlBeginRegex: E((l3) => new RegExp(`^ {0,${l3}}<(?:[a-z].*>|!--)`, "i")), blockquoteBeginRegex: E((l3) => new RegExp(`^ {0,${l3}}>`)) };
|
|
819
|
+
var Oe = /^(?:[ \t]*(?:\n|$))+/;
|
|
820
|
+
var we = /^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/;
|
|
821
|
+
var ye = /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/;
|
|
822
|
+
var B = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/;
|
|
823
|
+
var Pe = /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/;
|
|
824
|
+
var j = / {0,3}(?:[*+-]|\d{1,9}[.)])/;
|
|
825
|
+
var oe = /^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/;
|
|
826
|
+
var ae = d(oe).replace(/bull/g, j).replace(/blockCode/g, /(?: {4}| {0,3}\t)/).replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g, / {0,3}>/).replace(/heading/g, / {0,3}#{1,6}(?:\s|$)/).replace(/html/g, / {0,3}<[^\n>]+>\n/).replace(/\|table/g, "").getRegex();
|
|
827
|
+
var Se = d(oe).replace(/bull/g, j).replace(/blockCode/g, /(?: {4}| {0,3}\t)/).replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g, / {0,3}>/).replace(/heading/g, / {0,3}#{1,6}(?:\s|$)/).replace(/html/g, / {0,3}<[^\n>]+>\n/).replace(/table/g, / {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex();
|
|
828
|
+
var F = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table|[ \t]+\n)[^\n]+)*)/;
|
|
829
|
+
var $e = /^[^\n]+/;
|
|
830
|
+
var U = /(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/;
|
|
831
|
+
var Le = d(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label", U).replace("title", /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex();
|
|
832
|
+
var _e = d(/^(bull)([ \t][^\n]*?)?(?:\n|$)/).replace(/bull/g, j).getRegex();
|
|
833
|
+
var H = "address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul";
|
|
834
|
+
var K = /<!--(?:-?>|[\s\S]*?(?:-->|$))/;
|
|
835
|
+
var Me = d("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n*|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>[^\\n]*\\n*|$)|<![A-Z][\\s\\S]*?(?:>[^\\n]*\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>[^\\n]*\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))", "i").replace("comment", K).replace("tag", H).replace("attribute", / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex();
|
|
836
|
+
var le = (l3) => d(F).replace("hr", B).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("|lheading", "").replace("|table", "").replace("blockquote", " {0,3}>").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~~~)[^\\n]*\\n").replace("list", l3).replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag", H).getRegex();
|
|
837
|
+
var ze = le(/ {0,3}(?:[*+-]|1[.)])[ \t]+[^ \t\n]/);
|
|
838
|
+
var Ee = le(/ {0,3}(?:[*+-]|\d{1,9}[.)])(?:[ \t]|\n|$)/);
|
|
839
|
+
var Ce = d(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph", Ee).getRegex();
|
|
840
|
+
var W = { blockquote: Ce, code: we, def: Le, fences: ye, heading: Pe, hr: B, html: Me, lheading: ae, list: _e, newline: Oe, paragraph: ze, table: _, text: $e };
|
|
841
|
+
var se = d("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr", B).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("blockquote", " {0,3}>").replace("code", "(?: {4}| {0,3} )[^\\n]").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~~~)[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)])[ \\t]").replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag", H).getRegex();
|
|
842
|
+
var Ae = { ...W, lheading: Se, table: se, paragraph: d(F).replace("hr", B).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("|lheading", "").replace("table", se).replace("blockquote", " {0,3}>").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~~~)[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)])[ \\t]+[^ \\t\\n]").replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag", H).getRegex() };
|
|
843
|
+
var Ie = { ...W, html: d(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:"[^"]*"|'[^']*'|\\s[^'"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment", K).replace(/tag/g, "(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(), def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, heading: /^(#{1,6})(.*)(?:\n+|$)/, fences: _, lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, paragraph: d(F).replace("hr", B).replace("heading", ` *#{1,6} *[^
|
|
844
|
+
]`).replace("lheading", ae).replace("|table", "").replace("blockquote", " {0,3}>").replace("|fences", "").replace("|list", "").replace("|html", "").replace("|tag", "").getRegex() };
|
|
845
|
+
var Be = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/;
|
|
846
|
+
var qe = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/;
|
|
847
|
+
var ue = /^( {2,}|\\)\n(?!\s*$)/;
|
|
848
|
+
var De = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/;
|
|
849
|
+
var C = /[\p{P}\p{S}]/u;
|
|
850
|
+
var Z = /[\s\p{P}\p{S}]/u;
|
|
851
|
+
var X = /[^\s\p{P}\p{S}]/u;
|
|
852
|
+
var ve = d(/^((?![*_])punctSpace)/, "u").replace(/punctSpace/g, Z).getRegex();
|
|
853
|
+
var pe = /(?!~)[\p{P}\p{S}]/u;
|
|
854
|
+
var He = /(?!~)[\s\p{P}\p{S}]/u;
|
|
855
|
+
var Ze = /(?:[^\s\p{P}\p{S}]|~)/u;
|
|
856
|
+
var Ge = d(/link|precode-code|html/, "g").replace("link", /\[(?:[^\[\]`]|(?<a>`+)[^`]+\k<a>(?!`))*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)/).replace("precode-", Te ? "(?<!`)()" : "(^^|[^`])").replace("code", /(?<b>`+)[^`]+\k<b>(?!`)/).replace("html", /<(?! )[^<>]*?>/).getRegex();
|
|
857
|
+
var ce = /^(?:\*+(?:((?!\*)punct)|([^\s*]))?)|^_+(?:((?!_)punct)|([^\s_]))?/;
|
|
858
|
+
var Ne = d(ce, "u").replace(/punct/g, C).getRegex();
|
|
859
|
+
var Qe = d(ce, "u").replace(/punct/g, pe).getRegex();
|
|
860
|
+
var he = "^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)";
|
|
861
|
+
var je = d(he, "gu").replace(/notPunctSpace/g, X).replace(/punctSpace/g, Z).replace(/punct/g, C).getRegex();
|
|
862
|
+
var Fe = d(he, "gu").replace(/notPunctSpace/g, Ze).replace(/punctSpace/g, He).replace(/punct/g, pe).getRegex();
|
|
863
|
+
var Ue = d("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)", "gu").replace(/notPunctSpace/g, X).replace(/punctSpace/g, Z).replace(/punct/g, C).getRegex();
|
|
864
|
+
var Ke = d(/^~~?(?:((?!~)punct)|[^\s~])/, "u").replace(/punct/g, C).getRegex();
|
|
865
|
+
var We = "^[^~]+(?=[^~])|(?!~)punct(~~?)(?=[\\s]|$)|notPunctSpace(~~?)(?!~)(?=punctSpace|$)|(?!~)punctSpace(~~?)(?=notPunctSpace)|[\\s](~~?)(?!~)(?=punct)|(?!~)punct(~~?)(?!~)(?=punct)|notPunctSpace(~~?)(?=notPunctSpace)";
|
|
866
|
+
var Xe = d(We, "gu").replace(/notPunctSpace/g, X).replace(/punctSpace/g, Z).replace(/punct/g, C).getRegex();
|
|
867
|
+
var Je = d(/\\(punct)/, "gu").replace(/punct/g, C).getRegex();
|
|
868
|
+
var Ve = d(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme", /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email", /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex();
|
|
869
|
+
var Ye = d(K).replace("(?:-->|$)", "-->").getRegex();
|
|
870
|
+
var et = d("^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>").replace("comment", Ye).replace("attribute", /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex();
|
|
871
|
+
var v = /(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+(?!`)[^`]*?`+(?!`)|``+(?=\])|[^\[\]\\`])*?/;
|
|
872
|
+
var tt = d(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]+(?:\n[ \t]*)?|\n[ \t]*)(title))?\s*\)/).replace("label", v).replace("href", /<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]+|(?=\))/).replace("title", /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex();
|
|
873
|
+
var ke = d(/^!?\[(label)\]\[(ref)\]/).replace("label", v).replace("ref", U).getRegex();
|
|
874
|
+
var de = d(/^!?\[(ref)\](?:\[\])?/).replace("ref", U).getRegex();
|
|
875
|
+
var nt = d("reflink|nolink(?!\\()", "g").replace("reflink", ke).replace("nolink", de).getRegex();
|
|
876
|
+
var ie = /[hH][tT][tT][pP][sS]?|[fF][tT][pP]/;
|
|
877
|
+
var J = { _backpedal: _, anyPunctuation: Je, autolink: Ve, blockSkip: Ge, br: ue, code: qe, del: _, delLDelim: _, delRDelim: _, emStrongLDelim: Ne, emStrongRDelimAst: je, emStrongRDelimUnd: Ue, escape: Be, link: tt, nolink: de, punctuation: ve, reflink: ke, reflinkSearch: nt, tag: et, text: De, url: _ };
|
|
878
|
+
var rt = { ...J, link: d(/^!?\[(label)\]\((.*?)\)/).replace("label", v).getRegex(), reflink: d(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label", v).getRegex() };
|
|
879
|
+
var Q = { ...J, emStrongRDelimAst: Fe, emStrongLDelim: Qe, delLDelim: Ke, delRDelim: Xe, url: d(/^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/).replace("protocol", ie).replace("email", /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(), _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, del: /^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/, text: d(/^(`+|~+|[^`~])(?:(?=[`~])|(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|protocol:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/).replace("protocol", ie).getRegex() };
|
|
880
|
+
var st = { ...Q, br: d(ue).replace("{2,}", "*").getRegex(), text: d(Q.text).replace("\\b_", "\\b_| {2,}\\n").replace(/\{2,\}/g, "*").getRegex() };
|
|
881
|
+
var q = { normal: W, gfm: Ae, pedantic: Ie };
|
|
882
|
+
var A = { normal: J, gfm: Q, breaks: st, pedantic: rt };
|
|
883
|
+
var it = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
884
|
+
var ge = (l3) => it[l3];
|
|
885
|
+
function O(l3, e) {
|
|
886
|
+
if (e) {
|
|
887
|
+
if (m.escapeTest.test(l3)) return l3.replace(m.escapeReplace, ge);
|
|
888
|
+
} else if (m.escapeTestNoEncode.test(l3)) return l3.replace(m.escapeReplaceNoEncode, ge);
|
|
889
|
+
return l3;
|
|
890
|
+
}
|
|
891
|
+
function V(l3) {
|
|
892
|
+
try {
|
|
893
|
+
l3 = encodeURI(l3).replace(m.percentDecode, "%");
|
|
894
|
+
} catch {
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
return l3;
|
|
898
|
+
}
|
|
899
|
+
function Y(l3, e) {
|
|
900
|
+
let t = l3.replace(m.findPipe, (r, i, o) => {
|
|
901
|
+
let u = false, a = i;
|
|
902
|
+
for (; --a >= 0 && o[a] === "\\"; ) u = !u;
|
|
903
|
+
return u ? "|" : " |";
|
|
904
|
+
}), n = t.split(m.splitPipe), s = 0;
|
|
905
|
+
if (n[0].trim() || n.shift(), n.length > 0 && !n.at(-1)?.trim() && n.pop(), e) if (n.length > e) n.splice(e);
|
|
906
|
+
else for (; n.length < e; ) n.push("");
|
|
907
|
+
for (; s < n.length; s++) n[s] = n[s].trim().replace(m.slashPipe, "|");
|
|
908
|
+
return n;
|
|
909
|
+
}
|
|
910
|
+
function $(l3, e, t) {
|
|
911
|
+
let n = l3.length;
|
|
912
|
+
if (n === 0) return "";
|
|
913
|
+
let s = 0;
|
|
914
|
+
for (; s < n; ) {
|
|
915
|
+
let r = l3.charAt(n - s - 1);
|
|
916
|
+
if (r === e && !t) s++;
|
|
917
|
+
else if (r !== e && t) s++;
|
|
918
|
+
else break;
|
|
919
|
+
}
|
|
920
|
+
return l3.slice(0, n - s);
|
|
921
|
+
}
|
|
922
|
+
function ee(l3) {
|
|
923
|
+
let e = l3.split(`
|
|
924
|
+
`), t = e.length - 1;
|
|
925
|
+
for (; t >= 0 && m.blankLine.test(e[t]); ) t--;
|
|
926
|
+
return e.length - t <= 2 ? l3 : e.slice(0, t + 1).join(`
|
|
927
|
+
`);
|
|
928
|
+
}
|
|
929
|
+
function fe(l3, e) {
|
|
930
|
+
if (l3.indexOf(e[1]) === -1) return -1;
|
|
931
|
+
let t = 0;
|
|
932
|
+
for (let n = 0; n < l3.length; n++) if (l3[n] === "\\") n++;
|
|
933
|
+
else if (l3[n] === e[0]) t++;
|
|
934
|
+
else if (l3[n] === e[1] && (t--, t < 0)) return n;
|
|
935
|
+
return t > 0 ? -2 : -1;
|
|
936
|
+
}
|
|
937
|
+
function me(l3, e = 0) {
|
|
938
|
+
let t = e, n = "";
|
|
939
|
+
for (let s of l3) if (s === " ") {
|
|
940
|
+
let r = 4 - t % 4;
|
|
941
|
+
n += " ".repeat(r), t += r;
|
|
942
|
+
} else n += s, t++;
|
|
943
|
+
return n;
|
|
944
|
+
}
|
|
945
|
+
function xe(l3, e, t, n, s) {
|
|
946
|
+
let r = e.href, i = e.title || null, o = l3[1].replace(s.other.outputLinkReplace, "$1");
|
|
947
|
+
n.state.inLink = true;
|
|
948
|
+
let u = { type: l3[0].charAt(0) === "!" ? "image" : "link", raw: t, href: r, title: i, text: o, tokens: n.inlineTokens(o) };
|
|
949
|
+
return n.state.inLink = false, u;
|
|
950
|
+
}
|
|
951
|
+
function ot(l3, e, t) {
|
|
952
|
+
let n = l3.match(t.other.indentCodeCompensation);
|
|
953
|
+
if (n === null) return e;
|
|
954
|
+
let s = n[1];
|
|
955
|
+
return e.split(`
|
|
956
|
+
`).map((r) => {
|
|
957
|
+
let i = r.match(t.other.beginningSpace);
|
|
958
|
+
if (i === null) return r;
|
|
959
|
+
let [o] = i;
|
|
960
|
+
return o.length >= s.length ? r.slice(s.length) : r;
|
|
961
|
+
}).join(`
|
|
962
|
+
`);
|
|
963
|
+
}
|
|
964
|
+
var w = class {
|
|
965
|
+
options;
|
|
966
|
+
rules;
|
|
967
|
+
lexer;
|
|
968
|
+
constructor(e) {
|
|
969
|
+
this.options = e || T;
|
|
970
|
+
}
|
|
971
|
+
space(e) {
|
|
972
|
+
let t = this.rules.block.newline.exec(e);
|
|
973
|
+
if (t && t[0].length > 0) return { type: "space", raw: t[0] };
|
|
974
|
+
}
|
|
975
|
+
code(e) {
|
|
976
|
+
let t = this.rules.block.code.exec(e);
|
|
977
|
+
if (t) {
|
|
978
|
+
let n = this.options.pedantic ? t[0] : ee(t[0]), s = n.replace(this.rules.other.codeRemoveIndent, "");
|
|
979
|
+
return { type: "code", raw: n, codeBlockStyle: "indented", text: s };
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
fences(e) {
|
|
983
|
+
let t = this.rules.block.fences.exec(e);
|
|
984
|
+
if (t) {
|
|
985
|
+
let n = t[0], s = ot(n, t[3] || "", this.rules);
|
|
986
|
+
return { type: "code", raw: n, lang: t[2] ? t[2].trim().replace(this.rules.inline.anyPunctuation, "$1") : t[2], text: s };
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
heading(e) {
|
|
990
|
+
let t = this.rules.block.heading.exec(e);
|
|
991
|
+
if (t) {
|
|
992
|
+
let n = t[2].trim();
|
|
993
|
+
if (this.rules.other.endingHash.test(n)) {
|
|
994
|
+
let s = $(n, "#");
|
|
995
|
+
(this.options.pedantic || !s || this.rules.other.endingSpaceChar.test(s)) && (n = s.trim());
|
|
996
|
+
}
|
|
997
|
+
return { type: "heading", raw: $(t[0], `
|
|
998
|
+
`), depth: t[1].length, text: n, tokens: this.lexer.inline(n) };
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
hr(e) {
|
|
1002
|
+
let t = this.rules.block.hr.exec(e);
|
|
1003
|
+
if (t) return { type: "hr", raw: $(t[0], `
|
|
1004
|
+
`) };
|
|
1005
|
+
}
|
|
1006
|
+
blockquote(e) {
|
|
1007
|
+
let t = this.rules.block.blockquote.exec(e);
|
|
1008
|
+
if (t) {
|
|
1009
|
+
let n = $(t[0], `
|
|
1010
|
+
`).split(`
|
|
1011
|
+
`), s = "", r = "", i = [];
|
|
1012
|
+
for (; n.length > 0; ) {
|
|
1013
|
+
let o = false, u = [], a;
|
|
1014
|
+
for (a = 0; a < n.length; a++) if (this.rules.other.blockquoteStart.test(n[a])) u.push(n[a]), o = true;
|
|
1015
|
+
else if (!o) u.push(n[a]);
|
|
1016
|
+
else break;
|
|
1017
|
+
n = n.slice(a);
|
|
1018
|
+
let p = u.join(`
|
|
1019
|
+
`), c = p.replace(this.rules.other.blockquoteSetextReplace, `
|
|
1020
|
+
$1`).replace(this.rules.other.blockquoteSetextReplace2, "");
|
|
1021
|
+
s = s ? `${s}
|
|
1022
|
+
${p}` : p, r = r ? `${r}
|
|
1023
|
+
${c}` : c;
|
|
1024
|
+
let h = this.lexer.state.top;
|
|
1025
|
+
if (this.lexer.state.top = true, this.lexer.blockTokens(c, i, true), this.lexer.state.top = h, n.length === 0) break;
|
|
1026
|
+
let k = i.at(-1);
|
|
1027
|
+
if (k?.type === "code") break;
|
|
1028
|
+
if (k?.type === "blockquote") {
|
|
1029
|
+
let R = k, f = R.raw + `
|
|
1030
|
+
` + n.join(`
|
|
1031
|
+
`), S = this.blockquote(f);
|
|
1032
|
+
i[i.length - 1] = S, s = s.substring(0, s.length - R.raw.length) + S.raw, r = r.substring(0, r.length - R.text.length) + S.text;
|
|
1033
|
+
break;
|
|
1034
|
+
} else if (k?.type === "list") {
|
|
1035
|
+
let R = k, f = R.raw + `
|
|
1036
|
+
` + n.join(`
|
|
1037
|
+
`), S = this.list(f);
|
|
1038
|
+
i[i.length - 1] = S, s = s.substring(0, s.length - k.raw.length) + S.raw, r = r.substring(0, r.length - R.raw.length) + S.raw, n = f.substring(i.at(-1).raw.length).split(`
|
|
1039
|
+
`);
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return { type: "blockquote", raw: s, tokens: i, text: r };
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
list(e) {
|
|
1047
|
+
let t = this.rules.block.list.exec(e);
|
|
1048
|
+
if (t) {
|
|
1049
|
+
let n = t[1].trim(), s = n.length > 1, r = { type: "list", raw: "", ordered: s, start: s ? +n.slice(0, -1) : "", loose: false, items: [] };
|
|
1050
|
+
n = s ? `\\d{1,9}\\${n.slice(-1)}` : `\\${n}`, this.options.pedantic && (n = s ? n : "[*+-]");
|
|
1051
|
+
let i = this.rules.other.listItemRegex(n), o = false;
|
|
1052
|
+
for (; e; ) {
|
|
1053
|
+
let a = false, p = "", c = "";
|
|
1054
|
+
if (!(t = i.exec(e)) || this.rules.block.hr.test(e)) break;
|
|
1055
|
+
p = t[0], e = e.substring(p.length);
|
|
1056
|
+
let h = me(t[2].split(`
|
|
1057
|
+
`, 1)[0], t[1].length), k = e.split(`
|
|
1058
|
+
`, 1)[0], R = !h.trim(), f = 0;
|
|
1059
|
+
if (this.options.pedantic ? (f = 2, c = h.trimStart()) : R ? f = t[1].length + 1 : (f = h.search(this.rules.other.nonSpaceChar), f = f > 4 ? 1 : f, c = h.slice(f), f += t[1].length), R && this.rules.other.blankLine.test(k) && (p += k + `
|
|
1060
|
+
`, e = e.substring(k.length + 1), a = true), !a) {
|
|
1061
|
+
let S = this.rules.other.nextBulletRegex(f), te = this.rules.other.hrRegex(f), ne = this.rules.other.fencesBeginRegex(f), re = this.rules.other.headingBeginRegex(f), be = this.rules.other.htmlBeginRegex(f), Re = this.rules.other.blockquoteBeginRegex(f);
|
|
1062
|
+
for (; e; ) {
|
|
1063
|
+
let G = e.split(`
|
|
1064
|
+
`, 1)[0], I;
|
|
1065
|
+
if (k = G, this.options.pedantic ? (k = k.replace(this.rules.other.listReplaceNesting, " "), I = k) : I = k.replace(this.rules.other.tabCharGlobal, " "), ne.test(k) || re.test(k) || be.test(k) || Re.test(k) || S.test(k) || te.test(k)) break;
|
|
1066
|
+
if (I.search(this.rules.other.nonSpaceChar) >= f || !k.trim()) c += `
|
|
1067
|
+
` + I.slice(f);
|
|
1068
|
+
else {
|
|
1069
|
+
if (R || h.replace(this.rules.other.tabCharGlobal, " ").search(this.rules.other.nonSpaceChar) >= 4 || ne.test(h) || re.test(h) || te.test(h)) break;
|
|
1070
|
+
c += `
|
|
1071
|
+
` + k;
|
|
1072
|
+
}
|
|
1073
|
+
R = !k.trim(), p += G + `
|
|
1074
|
+
`, e = e.substring(G.length + 1), h = I.slice(f);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
r.loose || (o ? r.loose = true : this.rules.other.doubleBlankLine.test(p) && (o = true)), r.items.push({ type: "list_item", raw: p, task: !!this.options.gfm && this.rules.other.listIsTask.test(c), loose: false, text: c, tokens: [] }), r.raw += p;
|
|
1078
|
+
}
|
|
1079
|
+
let u = r.items.at(-1);
|
|
1080
|
+
if (u) u.raw = u.raw.trimEnd(), u.text = u.text.trimEnd();
|
|
1081
|
+
else return;
|
|
1082
|
+
r.raw = r.raw.trimEnd();
|
|
1083
|
+
for (let a of r.items) {
|
|
1084
|
+
this.lexer.state.top = false, a.tokens = this.lexer.blockTokens(a.text, []);
|
|
1085
|
+
let p = a.tokens[0];
|
|
1086
|
+
if (a.task && (p?.type === "text" || p?.type === "paragraph")) {
|
|
1087
|
+
a.text = a.text.replace(this.rules.other.listReplaceTask, ""), p.raw = p.raw.replace(this.rules.other.listReplaceTask, ""), p.text = p.text.replace(this.rules.other.listReplaceTask, "");
|
|
1088
|
+
for (let h = this.lexer.inlineQueue.length - 1; h >= 0; h--) if (this.rules.other.listIsTask.test(this.lexer.inlineQueue[h].src)) {
|
|
1089
|
+
this.lexer.inlineQueue[h].src = this.lexer.inlineQueue[h].src.replace(this.rules.other.listReplaceTask, "");
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
let c = this.rules.other.listTaskCheckbox.exec(a.raw);
|
|
1093
|
+
if (c) {
|
|
1094
|
+
let h = { type: "checkbox", raw: c[0] + " ", checked: c[0] !== "[ ]" };
|
|
1095
|
+
a.checked = h.checked, r.loose ? a.tokens[0] && ["paragraph", "text"].includes(a.tokens[0].type) && "tokens" in a.tokens[0] && a.tokens[0].tokens ? (a.tokens[0].raw = h.raw + a.tokens[0].raw, a.tokens[0].text = h.raw + a.tokens[0].text, a.tokens[0].tokens.unshift(h)) : a.tokens.unshift({ type: "paragraph", raw: h.raw, text: h.raw, tokens: [h] }) : a.tokens.unshift(h);
|
|
1096
|
+
}
|
|
1097
|
+
} else a.task && (a.task = false);
|
|
1098
|
+
if (!r.loose) {
|
|
1099
|
+
let c = a.tokens.filter((k) => k.type === "space"), h = c.length > 0 && c.some((k) => this.rules.other.anyLine.test(k.raw));
|
|
1100
|
+
r.loose = h;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (r.loose) for (let a of r.items) {
|
|
1104
|
+
a.loose = true;
|
|
1105
|
+
for (let p of a.tokens) p.type === "text" && (p.type = "paragraph");
|
|
1106
|
+
}
|
|
1107
|
+
return r;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
html(e) {
|
|
1111
|
+
let t = this.rules.block.html.exec(e);
|
|
1112
|
+
if (t) {
|
|
1113
|
+
let n = ee(t[0]);
|
|
1114
|
+
return { type: "html", block: true, raw: n, pre: t[1] === "pre" || t[1] === "script" || t[1] === "style", text: n };
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
def(e) {
|
|
1118
|
+
let t = this.rules.block.def.exec(e);
|
|
1119
|
+
if (t) {
|
|
1120
|
+
let n = t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal, " "), s = t[2] ? t[2].replace(this.rules.other.hrefBrackets, "$1").replace(this.rules.inline.anyPunctuation, "$1") : "", r = t[3] ? t[3].substring(1, t[3].length - 1).replace(this.rules.inline.anyPunctuation, "$1") : t[3];
|
|
1121
|
+
return { type: "def", tag: n, raw: $(t[0], `
|
|
1122
|
+
`), href: s, title: r };
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
table(e) {
|
|
1126
|
+
let t = this.rules.block.table.exec(e);
|
|
1127
|
+
if (!t || !this.rules.other.tableDelimiter.test(t[2])) return;
|
|
1128
|
+
let n = Y(t[1]), s = t[2].replace(this.rules.other.tableAlignChars, "").split("|"), r = t[3]?.trim() ? t[3].replace(this.rules.other.tableRowBlankLine, "").split(`
|
|
1129
|
+
`) : [], i = { type: "table", raw: $(t[0], `
|
|
1130
|
+
`), header: [], align: [], rows: [] };
|
|
1131
|
+
if (n.length === s.length) {
|
|
1132
|
+
for (let o of s) this.rules.other.tableAlignRight.test(o) ? i.align.push("right") : this.rules.other.tableAlignCenter.test(o) ? i.align.push("center") : this.rules.other.tableAlignLeft.test(o) ? i.align.push("left") : i.align.push(null);
|
|
1133
|
+
for (let o = 0; o < n.length; o++) i.header.push({ text: n[o], tokens: this.lexer.inline(n[o]), header: true, align: i.align[o] });
|
|
1134
|
+
for (let o of r) i.rows.push(Y(o, i.header.length).map((u, a) => ({ text: u, tokens: this.lexer.inline(u), header: false, align: i.align[a] })));
|
|
1135
|
+
return i;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
lheading(e) {
|
|
1139
|
+
let t = this.rules.block.lheading.exec(e);
|
|
1140
|
+
if (t) {
|
|
1141
|
+
let n = t[1].trim();
|
|
1142
|
+
return { type: "heading", raw: $(t[0], `
|
|
1143
|
+
`), depth: t[2].charAt(0) === "=" ? 1 : 2, text: n, tokens: this.lexer.inline(n) };
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
paragraph(e) {
|
|
1147
|
+
let t = this.rules.block.paragraph.exec(e);
|
|
1148
|
+
if (t) {
|
|
1149
|
+
let n = t[1].charAt(t[1].length - 1) === `
|
|
1150
|
+
` ? t[1].slice(0, -1) : t[1];
|
|
1151
|
+
return { type: "paragraph", raw: t[0], text: n, tokens: this.lexer.inline(n) };
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
text(e) {
|
|
1155
|
+
let t = this.rules.block.text.exec(e);
|
|
1156
|
+
if (t) return { type: "text", raw: t[0], text: t[0], tokens: this.lexer.inline(t[0]) };
|
|
1157
|
+
}
|
|
1158
|
+
escape(e) {
|
|
1159
|
+
let t = this.rules.inline.escape.exec(e);
|
|
1160
|
+
if (t) return { type: "escape", raw: t[0], text: t[1] };
|
|
1161
|
+
}
|
|
1162
|
+
tag(e) {
|
|
1163
|
+
let t = this.rules.inline.tag.exec(e);
|
|
1164
|
+
if (t) return !this.lexer.state.inLink && this.rules.other.startATag.test(t[0]) ? this.lexer.state.inLink = true : this.lexer.state.inLink && this.rules.other.endATag.test(t[0]) && (this.lexer.state.inLink = false), !this.lexer.state.inRawBlock && this.rules.other.startPreScriptTag.test(t[0]) ? this.lexer.state.inRawBlock = true : this.lexer.state.inRawBlock && this.rules.other.endPreScriptTag.test(t[0]) && (this.lexer.state.inRawBlock = false), { type: "html", raw: t[0], inLink: this.lexer.state.inLink, inRawBlock: this.lexer.state.inRawBlock, block: false, text: t[0] };
|
|
1165
|
+
}
|
|
1166
|
+
link(e) {
|
|
1167
|
+
let t = this.rules.inline.link.exec(e);
|
|
1168
|
+
if (t) {
|
|
1169
|
+
let n = t[2].trim();
|
|
1170
|
+
if (!this.options.pedantic && this.rules.other.startAngleBracket.test(n)) {
|
|
1171
|
+
if (!this.rules.other.endAngleBracket.test(n)) return;
|
|
1172
|
+
let i = $(n.slice(0, -1), "\\");
|
|
1173
|
+
if ((n.length - i.length) % 2 === 0) return;
|
|
1174
|
+
} else {
|
|
1175
|
+
let i = fe(t[2], "()");
|
|
1176
|
+
if (i === -2) return;
|
|
1177
|
+
if (i > -1) {
|
|
1178
|
+
let u = (t[0].indexOf("!") === 0 ? 5 : 4) + t[1].length + i;
|
|
1179
|
+
t[2] = t[2].substring(0, i), t[0] = t[0].substring(0, u).trim(), t[3] = "";
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
let s = t[2], r = "";
|
|
1183
|
+
if (this.options.pedantic) {
|
|
1184
|
+
let i = this.rules.other.pedanticHrefTitle.exec(s);
|
|
1185
|
+
i && (s = i[1], r = i[3]);
|
|
1186
|
+
} else r = t[3] ? t[3].slice(1, -1) : "";
|
|
1187
|
+
return s = s.trim(), this.rules.other.startAngleBracket.test(s) && (this.options.pedantic && !this.rules.other.endAngleBracket.test(n) ? s = s.slice(1) : s = s.slice(1, -1)), xe(t, { href: s && s.replace(this.rules.inline.anyPunctuation, "$1"), title: r && r.replace(this.rules.inline.anyPunctuation, "$1") }, t[0], this.lexer, this.rules);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
reflink(e, t) {
|
|
1191
|
+
let n;
|
|
1192
|
+
if ((n = this.rules.inline.reflink.exec(e)) || (n = this.rules.inline.nolink.exec(e))) {
|
|
1193
|
+
let s = (n[2] || n[1]).replace(this.rules.other.multipleSpaceGlobal, " "), r = t[s.toLowerCase()];
|
|
1194
|
+
if (!r) {
|
|
1195
|
+
let i = n[0].charAt(0);
|
|
1196
|
+
return { type: "text", raw: i, text: i };
|
|
1197
|
+
}
|
|
1198
|
+
return xe(n, r, n[0], this.lexer, this.rules);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
emStrong(e, t, n = "") {
|
|
1202
|
+
let s = this.rules.inline.emStrongLDelim.exec(e);
|
|
1203
|
+
if (!s || !s[1] && !s[2] && !s[3] && !s[4] || s[4] && n.match(this.rules.other.unicodeAlphaNumeric)) return;
|
|
1204
|
+
if (!(s[1] || s[3] || "") || !n || this.rules.inline.punctuation.exec(n)) {
|
|
1205
|
+
let i = [...s[0]].length - 1, o, u, a = i, p = 0, c = s[0][0] === "*" ? this.rules.inline.emStrongRDelimAst : this.rules.inline.emStrongRDelimUnd;
|
|
1206
|
+
for (c.lastIndex = 0, t = t.slice(-1 * e.length + i); (s = c.exec(t)) !== null; ) {
|
|
1207
|
+
if (o = s[1] || s[2] || s[3] || s[4] || s[5] || s[6], !o) continue;
|
|
1208
|
+
if (u = [...o].length, s[3] || s[4]) {
|
|
1209
|
+
a += u;
|
|
1210
|
+
continue;
|
|
1211
|
+
} else if ((s[5] || s[6]) && i % 3 && !((i + u) % 3)) {
|
|
1212
|
+
p += u;
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
if (a -= u, a > 0) continue;
|
|
1216
|
+
u = Math.min(u, u + a + p);
|
|
1217
|
+
let h = [...s[0]][0].length, k = e.slice(0, i + s.index + h + u);
|
|
1218
|
+
if (Math.min(i, u) % 2) {
|
|
1219
|
+
let f = k.slice(1, -1);
|
|
1220
|
+
return { type: "em", raw: k, text: f, tokens: this.lexer.inlineTokens(f) };
|
|
1221
|
+
}
|
|
1222
|
+
let R = k.slice(2, -2);
|
|
1223
|
+
return { type: "strong", raw: k, text: R, tokens: this.lexer.inlineTokens(R) };
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
codespan(e) {
|
|
1228
|
+
let t = this.rules.inline.code.exec(e);
|
|
1229
|
+
if (t) {
|
|
1230
|
+
let n = t[2].replace(this.rules.other.newLineCharGlobal, " "), s = this.rules.other.nonSpaceChar.test(n), r = this.rules.other.startingSpaceChar.test(n) && this.rules.other.endingSpaceChar.test(n);
|
|
1231
|
+
return s && r && (n = n.substring(1, n.length - 1)), { type: "codespan", raw: t[0], text: n };
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
br(e) {
|
|
1235
|
+
let t = this.rules.inline.br.exec(e);
|
|
1236
|
+
if (t) return { type: "br", raw: t[0] };
|
|
1237
|
+
}
|
|
1238
|
+
del(e, t, n = "") {
|
|
1239
|
+
let s = this.rules.inline.delLDelim.exec(e);
|
|
1240
|
+
if (!s) return;
|
|
1241
|
+
if (!(s[1] || "") || !n || this.rules.inline.punctuation.exec(n)) {
|
|
1242
|
+
let i = [...s[0]].length - 1, o, u, a = i, p = this.rules.inline.delRDelim;
|
|
1243
|
+
for (p.lastIndex = 0, t = t.slice(-1 * e.length + i); (s = p.exec(t)) !== null; ) {
|
|
1244
|
+
if (o = s[1] || s[2] || s[3] || s[4] || s[5] || s[6], !o || (u = [...o].length, u !== i)) continue;
|
|
1245
|
+
if (s[3] || s[4]) {
|
|
1246
|
+
a += u;
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (a -= u, a > 0) continue;
|
|
1250
|
+
u = Math.min(u, u + a);
|
|
1251
|
+
let c = [...s[0]][0].length, h = e.slice(0, i + s.index + c + u), k = h.slice(i, -i);
|
|
1252
|
+
return { type: "del", raw: h, text: k, tokens: this.lexer.inlineTokens(k) };
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
autolink(e) {
|
|
1257
|
+
let t = this.rules.inline.autolink.exec(e);
|
|
1258
|
+
if (t) {
|
|
1259
|
+
let n, s;
|
|
1260
|
+
return t[2] === "@" ? (n = t[1], s = "mailto:" + n) : (n = t[1], s = n), { type: "link", raw: t[0], text: n, href: s, tokens: [{ type: "text", raw: n, text: n }] };
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
url(e) {
|
|
1264
|
+
let t;
|
|
1265
|
+
if (t = this.rules.inline.url.exec(e)) {
|
|
1266
|
+
let n, s;
|
|
1267
|
+
if (t[2] === "@") n = t[0], s = "mailto:" + n;
|
|
1268
|
+
else {
|
|
1269
|
+
let r;
|
|
1270
|
+
do
|
|
1271
|
+
r = t[0], t[0] = this.rules.inline._backpedal.exec(t[0])?.[0] ?? "";
|
|
1272
|
+
while (r !== t[0]);
|
|
1273
|
+
n = t[0], t[1] === "www." ? s = "http://" + t[0] : s = t[0];
|
|
1274
|
+
}
|
|
1275
|
+
return { type: "link", raw: t[0], text: n, href: s, tokens: [{ type: "text", raw: n, text: n }] };
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
inlineText(e) {
|
|
1279
|
+
let t = this.rules.inline.text.exec(e);
|
|
1280
|
+
if (t) {
|
|
1281
|
+
let n = this.lexer.state.inRawBlock;
|
|
1282
|
+
return { type: "text", raw: t[0], text: t[0], escaped: n };
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
var x = class l {
|
|
1287
|
+
tokens;
|
|
1288
|
+
options;
|
|
1289
|
+
state;
|
|
1290
|
+
inlineQueue;
|
|
1291
|
+
tokenizer;
|
|
1292
|
+
constructor(e) {
|
|
1293
|
+
this.tokens = [], this.tokens.links = /* @__PURE__ */ Object.create(null), this.options = e || T, this.options.tokenizer = this.options.tokenizer || new w(), this.tokenizer = this.options.tokenizer, this.tokenizer.options = this.options, this.tokenizer.lexer = this, this.inlineQueue = [], this.state = { inLink: false, inRawBlock: false, top: true };
|
|
1294
|
+
let t = { other: m, block: q.normal, inline: A.normal };
|
|
1295
|
+
this.options.pedantic ? (t.block = q.pedantic, t.inline = A.pedantic) : this.options.gfm && (t.block = q.gfm, this.options.breaks ? t.inline = A.breaks : t.inline = A.gfm), this.tokenizer.rules = t;
|
|
1296
|
+
}
|
|
1297
|
+
static get rules() {
|
|
1298
|
+
return { block: q, inline: A };
|
|
1299
|
+
}
|
|
1300
|
+
static lex(e, t) {
|
|
1301
|
+
return new l(t).lex(e);
|
|
1302
|
+
}
|
|
1303
|
+
static lexInline(e, t) {
|
|
1304
|
+
return new l(t).inlineTokens(e);
|
|
1305
|
+
}
|
|
1306
|
+
lex(e) {
|
|
1307
|
+
e = e.replace(m.carriageReturn, `
|
|
1308
|
+
`), this.blockTokens(e, this.tokens);
|
|
1309
|
+
for (let t = 0; t < this.inlineQueue.length; t++) {
|
|
1310
|
+
let n = this.inlineQueue[t];
|
|
1311
|
+
this.inlineTokens(n.src, n.tokens);
|
|
1312
|
+
}
|
|
1313
|
+
return this.inlineQueue = [], this.tokens;
|
|
1314
|
+
}
|
|
1315
|
+
blockTokens(e, t = [], n = false) {
|
|
1316
|
+
this.tokenizer.lexer = this, this.options.pedantic && (e = e.replace(m.tabCharGlobal, " ").replace(m.spaceLine, ""));
|
|
1317
|
+
let s = 1 / 0;
|
|
1318
|
+
for (; e; ) {
|
|
1319
|
+
if (e.length < s) s = e.length;
|
|
1320
|
+
else {
|
|
1321
|
+
this.infiniteLoopError(e.charCodeAt(0));
|
|
1322
|
+
break;
|
|
1323
|
+
}
|
|
1324
|
+
let r;
|
|
1325
|
+
if (this.options.extensions?.block?.some((o) => (r = o.call({ lexer: this }, e, t)) ? (e = e.substring(r.raw.length), t.push(r), true) : false)) continue;
|
|
1326
|
+
if (r = this.tokenizer.space(e)) {
|
|
1327
|
+
e = e.substring(r.raw.length);
|
|
1328
|
+
let o = t.at(-1);
|
|
1329
|
+
r.raw.length === 1 && o !== void 0 ? o.raw += `
|
|
1330
|
+
` : t.push(r);
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
if (r = this.tokenizer.code(e)) {
|
|
1334
|
+
e = e.substring(r.raw.length);
|
|
1335
|
+
let o = t.at(-1);
|
|
1336
|
+
o?.type === "paragraph" || o?.type === "text" ? (o.raw += (o.raw.endsWith(`
|
|
1337
|
+
`) ? "" : `
|
|
1338
|
+
`) + r.raw, o.text += `
|
|
1339
|
+
` + r.text, this.inlineQueue.at(-1).src = o.text) : t.push(r);
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
if (r = this.tokenizer.fences(e)) {
|
|
1343
|
+
e = e.substring(r.raw.length), t.push(r);
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
if (r = this.tokenizer.heading(e)) {
|
|
1347
|
+
e = e.substring(r.raw.length), t.push(r);
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
if (r = this.tokenizer.hr(e)) {
|
|
1351
|
+
e = e.substring(r.raw.length), t.push(r);
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
if (r = this.tokenizer.blockquote(e)) {
|
|
1355
|
+
e = e.substring(r.raw.length), t.push(r);
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
if (r = this.tokenizer.list(e)) {
|
|
1359
|
+
e = e.substring(r.raw.length), t.push(r);
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
if (r = this.tokenizer.html(e)) {
|
|
1363
|
+
e = e.substring(r.raw.length), t.push(r);
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
if (r = this.tokenizer.def(e)) {
|
|
1367
|
+
e = e.substring(r.raw.length);
|
|
1368
|
+
let o = t.at(-1);
|
|
1369
|
+
o?.type === "paragraph" || o?.type === "text" ? (o.raw += (o.raw.endsWith(`
|
|
1370
|
+
`) ? "" : `
|
|
1371
|
+
`) + r.raw, o.text += `
|
|
1372
|
+
` + r.raw, this.inlineQueue.at(-1).src = o.text) : this.tokens.links[r.tag] || (this.tokens.links[r.tag] = { href: r.href, title: r.title }, t.push(r));
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
if (r = this.tokenizer.table(e)) {
|
|
1376
|
+
e = e.substring(r.raw.length), t.push(r);
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
if (r = this.tokenizer.lheading(e)) {
|
|
1380
|
+
e = e.substring(r.raw.length), t.push(r);
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
let i = e;
|
|
1384
|
+
if (this.options.extensions?.startBlock) {
|
|
1385
|
+
let o = 1 / 0, u = e.slice(1), a;
|
|
1386
|
+
this.options.extensions.startBlock.forEach((p) => {
|
|
1387
|
+
a = p.call({ lexer: this }, u), typeof a == "number" && a >= 0 && (o = Math.min(o, a));
|
|
1388
|
+
}), o < 1 / 0 && o >= 0 && (i = e.substring(0, o + 1));
|
|
1389
|
+
}
|
|
1390
|
+
if (this.state.top && (r = this.tokenizer.paragraph(i))) {
|
|
1391
|
+
let o = t.at(-1);
|
|
1392
|
+
n && o?.type === "paragraph" ? (o.raw += (o.raw.endsWith(`
|
|
1393
|
+
`) ? "" : `
|
|
1394
|
+
`) + r.raw, o.text += `
|
|
1395
|
+
` + r.text, this.inlineQueue.pop(), this.inlineQueue.at(-1).src = o.text) : t.push(r), n = i.length !== e.length, e = e.substring(r.raw.length);
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
if (r = this.tokenizer.text(e)) {
|
|
1399
|
+
e = e.substring(r.raw.length);
|
|
1400
|
+
let o = t.at(-1);
|
|
1401
|
+
o?.type === "text" ? (o.raw += (o.raw.endsWith(`
|
|
1402
|
+
`) ? "" : `
|
|
1403
|
+
`) + r.raw, o.text += `
|
|
1404
|
+
` + r.text, this.inlineQueue.pop(), this.inlineQueue.at(-1).src = o.text) : t.push(r);
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
if (e) {
|
|
1408
|
+
this.infiniteLoopError(e.charCodeAt(0));
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return this.state.top = true, t;
|
|
1413
|
+
}
|
|
1414
|
+
inline(e, t = []) {
|
|
1415
|
+
return this.inlineQueue.push({ src: e, tokens: t }), t;
|
|
1416
|
+
}
|
|
1417
|
+
inlineTokens(e, t = []) {
|
|
1418
|
+
this.tokenizer.lexer = this;
|
|
1419
|
+
let n = e;
|
|
1420
|
+
if (this.tokens.links) {
|
|
1421
|
+
let o = Object.keys(this.tokens.links);
|
|
1422
|
+
o.length > 0 && (n = n.replace(this.tokenizer.rules.inline.reflinkSearch, (u) => o.includes(u.slice(u.lastIndexOf("[") + 1, -1)) ? "[" + "a".repeat(u.length - 2) + "]" : u));
|
|
1423
|
+
}
|
|
1424
|
+
n = n.replace(this.tokenizer.rules.inline.anyPunctuation, "++"), n = n.replace(this.tokenizer.rules.inline.blockSkip, (o, u, a) => {
|
|
1425
|
+
let p = a ? a.length : 0;
|
|
1426
|
+
return o.slice(0, p) + "[" + "a".repeat(o.length - p - 2) + "]";
|
|
1427
|
+
}), n = this.options.hooks?.emStrongMask?.call({ lexer: this }, n) ?? n;
|
|
1428
|
+
let s = false, r = "", i = 1 / 0;
|
|
1429
|
+
for (; e; ) {
|
|
1430
|
+
if (e.length < i) i = e.length;
|
|
1431
|
+
else {
|
|
1432
|
+
this.infiniteLoopError(e.charCodeAt(0));
|
|
1433
|
+
break;
|
|
1434
|
+
}
|
|
1435
|
+
s || (r = ""), s = false;
|
|
1436
|
+
let o;
|
|
1437
|
+
if (this.options.extensions?.inline?.some((a) => (o = a.call({ lexer: this }, e, t)) ? (e = e.substring(o.raw.length), t.push(o), true) : false)) continue;
|
|
1438
|
+
if (o = this.tokenizer.escape(e)) {
|
|
1439
|
+
e = e.substring(o.raw.length), t.push(o);
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
if (o = this.tokenizer.tag(e)) {
|
|
1443
|
+
e = e.substring(o.raw.length), t.push(o);
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
if (o = this.tokenizer.link(e)) {
|
|
1447
|
+
e = e.substring(o.raw.length), t.push(o);
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
if (o = this.tokenizer.reflink(e, this.tokens.links)) {
|
|
1451
|
+
e = e.substring(o.raw.length);
|
|
1452
|
+
let a = t.at(-1);
|
|
1453
|
+
o.type === "text" && a?.type === "text" ? (a.raw += o.raw, a.text += o.text) : t.push(o);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
if (o = this.tokenizer.emStrong(e, n, r)) {
|
|
1457
|
+
e = e.substring(o.raw.length), t.push(o);
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (o = this.tokenizer.codespan(e)) {
|
|
1461
|
+
e = e.substring(o.raw.length), t.push(o);
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
if (o = this.tokenizer.br(e)) {
|
|
1465
|
+
e = e.substring(o.raw.length), t.push(o);
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
if (o = this.tokenizer.del(e, n, r)) {
|
|
1469
|
+
e = e.substring(o.raw.length), t.push(o);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
if (o = this.tokenizer.autolink(e)) {
|
|
1473
|
+
e = e.substring(o.raw.length), t.push(o);
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
if (!this.state.inLink && (o = this.tokenizer.url(e))) {
|
|
1477
|
+
e = e.substring(o.raw.length), t.push(o);
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
let u = e;
|
|
1481
|
+
if (this.options.extensions?.startInline) {
|
|
1482
|
+
let a = 1 / 0, p = e.slice(1), c;
|
|
1483
|
+
this.options.extensions.startInline.forEach((h) => {
|
|
1484
|
+
c = h.call({ lexer: this }, p), typeof c == "number" && c >= 0 && (a = Math.min(a, c));
|
|
1485
|
+
}), a < 1 / 0 && a >= 0 && (u = e.substring(0, a + 1));
|
|
1486
|
+
}
|
|
1487
|
+
if (o = this.tokenizer.inlineText(u)) {
|
|
1488
|
+
e = e.substring(o.raw.length), o.raw.slice(-1) !== "_" && (r = o.raw.slice(-1)), s = true;
|
|
1489
|
+
let a = t.at(-1);
|
|
1490
|
+
a?.type === "text" ? (a.raw += o.raw, a.text += o.text) : t.push(o);
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
if (e) {
|
|
1494
|
+
this.infiniteLoopError(e.charCodeAt(0));
|
|
1495
|
+
break;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return t;
|
|
1499
|
+
}
|
|
1500
|
+
infiniteLoopError(e) {
|
|
1501
|
+
let t = "Infinite loop on byte: " + e;
|
|
1502
|
+
if (this.options.silent) console.error(t);
|
|
1503
|
+
else throw new Error(t);
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
var y = class {
|
|
1507
|
+
options;
|
|
1508
|
+
parser;
|
|
1509
|
+
constructor(e) {
|
|
1510
|
+
this.options = e || T;
|
|
1511
|
+
}
|
|
1512
|
+
space(e) {
|
|
1513
|
+
return "";
|
|
1514
|
+
}
|
|
1515
|
+
code({ text: e, lang: t, escaped: n }) {
|
|
1516
|
+
let s = (t || "").match(m.notSpaceStart)?.[0], r = e.replace(m.endingNewline, "") + `
|
|
1517
|
+
`;
|
|
1518
|
+
return s ? '<pre><code class="language-' + O(s) + '">' + (n ? r : O(r, true)) + `</code></pre>
|
|
1519
|
+
` : "<pre><code>" + (n ? r : O(r, true)) + `</code></pre>
|
|
1520
|
+
`;
|
|
1521
|
+
}
|
|
1522
|
+
blockquote({ tokens: e }) {
|
|
1523
|
+
return `<blockquote>
|
|
1524
|
+
${this.parser.parse(e)}</blockquote>
|
|
1525
|
+
`;
|
|
1526
|
+
}
|
|
1527
|
+
html({ text: e }) {
|
|
1528
|
+
return e;
|
|
1529
|
+
}
|
|
1530
|
+
def(e) {
|
|
1531
|
+
return "";
|
|
1532
|
+
}
|
|
1533
|
+
heading({ tokens: e, depth: t }) {
|
|
1534
|
+
return `<h${t}>${this.parser.parseInline(e)}</h${t}>
|
|
1535
|
+
`;
|
|
1536
|
+
}
|
|
1537
|
+
hr(e) {
|
|
1538
|
+
return `<hr>
|
|
1539
|
+
`;
|
|
1540
|
+
}
|
|
1541
|
+
list(e) {
|
|
1542
|
+
let t = e.ordered, n = e.start, s = "";
|
|
1543
|
+
for (let o = 0; o < e.items.length; o++) {
|
|
1544
|
+
let u = e.items[o];
|
|
1545
|
+
s += this.listitem(u);
|
|
1546
|
+
}
|
|
1547
|
+
let r = t ? "ol" : "ul", i = t && n !== 1 ? ' start="' + n + '"' : "";
|
|
1548
|
+
return "<" + r + i + `>
|
|
1549
|
+
` + s + "</" + r + `>
|
|
1550
|
+
`;
|
|
1551
|
+
}
|
|
1552
|
+
listitem(e) {
|
|
1553
|
+
return `<li>${this.parser.parse(e.tokens)}</li>
|
|
1554
|
+
`;
|
|
1555
|
+
}
|
|
1556
|
+
checkbox({ checked: e }) {
|
|
1557
|
+
return "<input " + (e ? 'checked="" ' : "") + 'disabled="" type="checkbox"> ';
|
|
1558
|
+
}
|
|
1559
|
+
paragraph({ tokens: e }) {
|
|
1560
|
+
return `<p>${this.parser.parseInline(e)}</p>
|
|
1561
|
+
`;
|
|
1562
|
+
}
|
|
1563
|
+
table(e) {
|
|
1564
|
+
let t = "", n = "";
|
|
1565
|
+
for (let r = 0; r < e.header.length; r++) n += this.tablecell(e.header[r]);
|
|
1566
|
+
t += this.tablerow({ text: n });
|
|
1567
|
+
let s = "";
|
|
1568
|
+
for (let r = 0; r < e.rows.length; r++) {
|
|
1569
|
+
let i = e.rows[r];
|
|
1570
|
+
n = "";
|
|
1571
|
+
for (let o = 0; o < i.length; o++) n += this.tablecell(i[o]);
|
|
1572
|
+
s += this.tablerow({ text: n });
|
|
1573
|
+
}
|
|
1574
|
+
return s && (s = `<tbody>${s}</tbody>`), `<table>
|
|
1575
|
+
<thead>
|
|
1576
|
+
` + t + `</thead>
|
|
1577
|
+
` + s + `</table>
|
|
1578
|
+
`;
|
|
1579
|
+
}
|
|
1580
|
+
tablerow({ text: e }) {
|
|
1581
|
+
return `<tr>
|
|
1582
|
+
${e}</tr>
|
|
1583
|
+
`;
|
|
1584
|
+
}
|
|
1585
|
+
tablecell(e) {
|
|
1586
|
+
let t = this.parser.parseInline(e.tokens), n = e.header ? "th" : "td";
|
|
1587
|
+
return (e.align ? `<${n} align="${e.align}">` : `<${n}>`) + t + `</${n}>
|
|
1588
|
+
`;
|
|
1589
|
+
}
|
|
1590
|
+
strong({ tokens: e }) {
|
|
1591
|
+
return `<strong>${this.parser.parseInline(e)}</strong>`;
|
|
1592
|
+
}
|
|
1593
|
+
em({ tokens: e }) {
|
|
1594
|
+
return `<em>${this.parser.parseInline(e)}</em>`;
|
|
1595
|
+
}
|
|
1596
|
+
codespan({ text: e }) {
|
|
1597
|
+
return `<code>${O(e, true)}</code>`;
|
|
1598
|
+
}
|
|
1599
|
+
br(e) {
|
|
1600
|
+
return "<br>";
|
|
1601
|
+
}
|
|
1602
|
+
del({ tokens: e }) {
|
|
1603
|
+
return `<del>${this.parser.parseInline(e)}</del>`;
|
|
1604
|
+
}
|
|
1605
|
+
link({ href: e, title: t, tokens: n }) {
|
|
1606
|
+
let s = this.parser.parseInline(n), r = V(e);
|
|
1607
|
+
if (r === null) return s;
|
|
1608
|
+
e = r;
|
|
1609
|
+
let i = '<a href="' + e + '"';
|
|
1610
|
+
return t && (i += ' title="' + O(t) + '"'), i += ">" + s + "</a>", i;
|
|
1611
|
+
}
|
|
1612
|
+
image({ href: e, title: t, text: n, tokens: s }) {
|
|
1613
|
+
s && (n = this.parser.parseInline(s, this.parser.textRenderer));
|
|
1614
|
+
let r = V(e);
|
|
1615
|
+
if (r === null) return O(n);
|
|
1616
|
+
e = r;
|
|
1617
|
+
let i = `<img src="${e}" alt="${O(n)}"`;
|
|
1618
|
+
return t && (i += ` title="${O(t)}"`), i += ">", i;
|
|
1619
|
+
}
|
|
1620
|
+
text(e) {
|
|
1621
|
+
return "tokens" in e && e.tokens ? this.parser.parseInline(e.tokens) : "escaped" in e && e.escaped ? e.text : O(e.text);
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
var L = class {
|
|
1625
|
+
strong({ text: e }) {
|
|
1626
|
+
return e;
|
|
1627
|
+
}
|
|
1628
|
+
em({ text: e }) {
|
|
1629
|
+
return e;
|
|
1630
|
+
}
|
|
1631
|
+
codespan({ text: e }) {
|
|
1632
|
+
return e;
|
|
1633
|
+
}
|
|
1634
|
+
del({ text: e }) {
|
|
1635
|
+
return e;
|
|
1636
|
+
}
|
|
1637
|
+
html({ text: e }) {
|
|
1638
|
+
return e;
|
|
1639
|
+
}
|
|
1640
|
+
text({ text: e }) {
|
|
1641
|
+
return e;
|
|
1642
|
+
}
|
|
1643
|
+
link({ text: e }) {
|
|
1644
|
+
return "" + e;
|
|
1645
|
+
}
|
|
1646
|
+
image({ text: e }) {
|
|
1647
|
+
return "" + e;
|
|
1648
|
+
}
|
|
1649
|
+
br() {
|
|
1650
|
+
return "";
|
|
1651
|
+
}
|
|
1652
|
+
checkbox({ raw: e }) {
|
|
1653
|
+
return e;
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
var b = class l2 {
|
|
1657
|
+
options;
|
|
1658
|
+
renderer;
|
|
1659
|
+
textRenderer;
|
|
1660
|
+
constructor(e) {
|
|
1661
|
+
this.options = e || T, this.options.renderer = this.options.renderer || new y(), this.renderer = this.options.renderer, this.renderer.options = this.options, this.renderer.parser = this, this.textRenderer = new L();
|
|
1662
|
+
}
|
|
1663
|
+
static parse(e, t) {
|
|
1664
|
+
return new l2(t).parse(e);
|
|
1665
|
+
}
|
|
1666
|
+
static parseInline(e, t) {
|
|
1667
|
+
return new l2(t).parseInline(e);
|
|
1668
|
+
}
|
|
1669
|
+
parse(e) {
|
|
1670
|
+
this.renderer.parser = this;
|
|
1671
|
+
let t = "";
|
|
1672
|
+
for (let n = 0; n < e.length; n++) {
|
|
1673
|
+
let s = e[n];
|
|
1674
|
+
if (this.options.extensions?.renderers?.[s.type]) {
|
|
1675
|
+
let i = s, o = this.options.extensions.renderers[i.type].call({ parser: this }, i);
|
|
1676
|
+
if (o !== false || !["space", "hr", "heading", "code", "table", "blockquote", "list", "html", "def", "paragraph", "text"].includes(i.type)) {
|
|
1677
|
+
t += o || "";
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
let r = s;
|
|
1682
|
+
switch (r.type) {
|
|
1683
|
+
case "space": {
|
|
1684
|
+
t += this.renderer.space(r);
|
|
1685
|
+
break;
|
|
1686
|
+
}
|
|
1687
|
+
case "hr": {
|
|
1688
|
+
t += this.renderer.hr(r);
|
|
1689
|
+
break;
|
|
1690
|
+
}
|
|
1691
|
+
case "heading": {
|
|
1692
|
+
t += this.renderer.heading(r);
|
|
1693
|
+
break;
|
|
1694
|
+
}
|
|
1695
|
+
case "code": {
|
|
1696
|
+
t += this.renderer.code(r);
|
|
1697
|
+
break;
|
|
1698
|
+
}
|
|
1699
|
+
case "table": {
|
|
1700
|
+
t += this.renderer.table(r);
|
|
1701
|
+
break;
|
|
1702
|
+
}
|
|
1703
|
+
case "blockquote": {
|
|
1704
|
+
t += this.renderer.blockquote(r);
|
|
1705
|
+
break;
|
|
1706
|
+
}
|
|
1707
|
+
case "list": {
|
|
1708
|
+
t += this.renderer.list(r);
|
|
1709
|
+
break;
|
|
1710
|
+
}
|
|
1711
|
+
case "checkbox": {
|
|
1712
|
+
t += this.renderer.checkbox(r);
|
|
1713
|
+
break;
|
|
1714
|
+
}
|
|
1715
|
+
case "html": {
|
|
1716
|
+
t += this.renderer.html(r);
|
|
1717
|
+
break;
|
|
1718
|
+
}
|
|
1719
|
+
case "def": {
|
|
1720
|
+
t += this.renderer.def(r);
|
|
1721
|
+
break;
|
|
1722
|
+
}
|
|
1723
|
+
case "paragraph": {
|
|
1724
|
+
t += this.renderer.paragraph(r);
|
|
1725
|
+
break;
|
|
1726
|
+
}
|
|
1727
|
+
case "text": {
|
|
1728
|
+
t += this.renderer.text(r);
|
|
1729
|
+
break;
|
|
1730
|
+
}
|
|
1731
|
+
default: {
|
|
1732
|
+
let i = 'Token with "' + r.type + '" type was not found.';
|
|
1733
|
+
if (this.options.silent) return console.error(i), "";
|
|
1734
|
+
throw new Error(i);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return t;
|
|
1739
|
+
}
|
|
1740
|
+
parseInline(e, t = this.renderer) {
|
|
1741
|
+
this.renderer.parser = this;
|
|
1742
|
+
let n = "";
|
|
1743
|
+
for (let s = 0; s < e.length; s++) {
|
|
1744
|
+
let r = e[s];
|
|
1745
|
+
if (this.options.extensions?.renderers?.[r.type]) {
|
|
1746
|
+
let o = this.options.extensions.renderers[r.type].call({ parser: this }, r);
|
|
1747
|
+
if (o !== false || !["escape", "html", "link", "image", "strong", "em", "codespan", "br", "del", "text"].includes(r.type)) {
|
|
1748
|
+
n += o || "";
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
let i = r;
|
|
1753
|
+
switch (i.type) {
|
|
1754
|
+
case "escape": {
|
|
1755
|
+
n += t.text(i);
|
|
1756
|
+
break;
|
|
1757
|
+
}
|
|
1758
|
+
case "html": {
|
|
1759
|
+
n += t.html(i);
|
|
1760
|
+
break;
|
|
1761
|
+
}
|
|
1762
|
+
case "link": {
|
|
1763
|
+
n += t.link(i);
|
|
1764
|
+
break;
|
|
1765
|
+
}
|
|
1766
|
+
case "image": {
|
|
1767
|
+
n += t.image(i);
|
|
1768
|
+
break;
|
|
1769
|
+
}
|
|
1770
|
+
case "checkbox": {
|
|
1771
|
+
n += t.checkbox(i);
|
|
1772
|
+
break;
|
|
1773
|
+
}
|
|
1774
|
+
case "strong": {
|
|
1775
|
+
n += t.strong(i);
|
|
1776
|
+
break;
|
|
1777
|
+
}
|
|
1778
|
+
case "em": {
|
|
1779
|
+
n += t.em(i);
|
|
1780
|
+
break;
|
|
1781
|
+
}
|
|
1782
|
+
case "codespan": {
|
|
1783
|
+
n += t.codespan(i);
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1786
|
+
case "br": {
|
|
1787
|
+
n += t.br(i);
|
|
1788
|
+
break;
|
|
1789
|
+
}
|
|
1790
|
+
case "del": {
|
|
1791
|
+
n += t.del(i);
|
|
1792
|
+
break;
|
|
1793
|
+
}
|
|
1794
|
+
case "text": {
|
|
1795
|
+
n += t.text(i);
|
|
1796
|
+
break;
|
|
1797
|
+
}
|
|
1798
|
+
default: {
|
|
1799
|
+
let o = 'Token with "' + i.type + '" type was not found.';
|
|
1800
|
+
if (this.options.silent) return console.error(o), "";
|
|
1801
|
+
throw new Error(o);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
return n;
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
var P = class {
|
|
1809
|
+
options;
|
|
1810
|
+
block;
|
|
1811
|
+
constructor(e) {
|
|
1812
|
+
this.options = e || T;
|
|
1813
|
+
}
|
|
1814
|
+
static passThroughHooks = /* @__PURE__ */ new Set(["preprocess", "postprocess", "processAllTokens", "emStrongMask"]);
|
|
1815
|
+
static passThroughHooksRespectAsync = /* @__PURE__ */ new Set(["preprocess", "postprocess", "processAllTokens"]);
|
|
1816
|
+
preprocess(e) {
|
|
1817
|
+
return e;
|
|
1818
|
+
}
|
|
1819
|
+
postprocess(e) {
|
|
1820
|
+
return e;
|
|
1821
|
+
}
|
|
1822
|
+
processAllTokens(e) {
|
|
1823
|
+
return e;
|
|
1824
|
+
}
|
|
1825
|
+
emStrongMask(e) {
|
|
1826
|
+
return e;
|
|
1827
|
+
}
|
|
1828
|
+
provideLexer(e = this.block) {
|
|
1829
|
+
return e ? x.lex : x.lexInline;
|
|
1830
|
+
}
|
|
1831
|
+
provideParser(e = this.block) {
|
|
1832
|
+
return e ? b.parse : b.parseInline;
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
var D = class {
|
|
1836
|
+
defaults = z();
|
|
1837
|
+
options = this.setOptions;
|
|
1838
|
+
parse = this.parseMarkdown(true);
|
|
1839
|
+
parseInline = this.parseMarkdown(false);
|
|
1840
|
+
Parser = b;
|
|
1841
|
+
Renderer = y;
|
|
1842
|
+
TextRenderer = L;
|
|
1843
|
+
Lexer = x;
|
|
1844
|
+
Tokenizer = w;
|
|
1845
|
+
Hooks = P;
|
|
1846
|
+
constructor(...e) {
|
|
1847
|
+
this.use(...e);
|
|
1848
|
+
}
|
|
1849
|
+
walkTokens(e, t) {
|
|
1850
|
+
let n = [];
|
|
1851
|
+
for (let s of e) switch (n = n.concat(t.call(this, s)), s.type) {
|
|
1852
|
+
case "table": {
|
|
1853
|
+
let r = s;
|
|
1854
|
+
for (let i of r.header) n = n.concat(this.walkTokens(i.tokens, t));
|
|
1855
|
+
for (let i of r.rows) for (let o of i) n = n.concat(this.walkTokens(o.tokens, t));
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1858
|
+
case "list": {
|
|
1859
|
+
let r = s;
|
|
1860
|
+
n = n.concat(this.walkTokens(r.items, t));
|
|
1861
|
+
break;
|
|
1862
|
+
}
|
|
1863
|
+
default: {
|
|
1864
|
+
let r = s;
|
|
1865
|
+
this.defaults.extensions?.childTokens?.[r.type] ? this.defaults.extensions.childTokens[r.type].forEach((i) => {
|
|
1866
|
+
let o = r[i].flat(1 / 0);
|
|
1867
|
+
n = n.concat(this.walkTokens(o, t));
|
|
1868
|
+
}) : r.tokens && (n = n.concat(this.walkTokens(r.tokens, t)));
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
return n;
|
|
1872
|
+
}
|
|
1873
|
+
use(...e) {
|
|
1874
|
+
let t = this.defaults.extensions || { renderers: {}, childTokens: {} };
|
|
1875
|
+
return e.forEach((n) => {
|
|
1876
|
+
let s = { ...n };
|
|
1877
|
+
if (s.async = this.defaults.async || s.async || false, n.extensions && (n.extensions.forEach((r) => {
|
|
1878
|
+
if (!r.name) throw new Error("extension name required");
|
|
1879
|
+
if ("renderer" in r) {
|
|
1880
|
+
let i = t.renderers[r.name];
|
|
1881
|
+
i ? t.renderers[r.name] = function(...o) {
|
|
1882
|
+
let u = r.renderer.apply(this, o);
|
|
1883
|
+
return u === false && (u = i.apply(this, o)), u;
|
|
1884
|
+
} : t.renderers[r.name] = r.renderer;
|
|
1885
|
+
}
|
|
1886
|
+
if ("tokenizer" in r) {
|
|
1887
|
+
if (!r.level || r.level !== "block" && r.level !== "inline") throw new Error("extension level must be 'block' or 'inline'");
|
|
1888
|
+
let i = t[r.level];
|
|
1889
|
+
i ? i.unshift(r.tokenizer) : t[r.level] = [r.tokenizer], r.start && (r.level === "block" ? t.startBlock ? t.startBlock.push(r.start) : t.startBlock = [r.start] : r.level === "inline" && (t.startInline ? t.startInline.push(r.start) : t.startInline = [r.start]));
|
|
1890
|
+
}
|
|
1891
|
+
"childTokens" in r && r.childTokens && (t.childTokens[r.name] = r.childTokens);
|
|
1892
|
+
}), s.extensions = t), n.renderer) {
|
|
1893
|
+
let r = this.defaults.renderer || new y(this.defaults);
|
|
1894
|
+
for (let i in n.renderer) {
|
|
1895
|
+
if (!(i in r)) throw new Error(`renderer '${i}' does not exist`);
|
|
1896
|
+
if (["options", "parser"].includes(i)) continue;
|
|
1897
|
+
let o = i, u = n.renderer[o], a = r[o];
|
|
1898
|
+
r[o] = (...p) => {
|
|
1899
|
+
let c = u.apply(r, p);
|
|
1900
|
+
return c === false && (c = a.apply(r, p)), c || "";
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
s.renderer = r;
|
|
1904
|
+
}
|
|
1905
|
+
if (n.tokenizer) {
|
|
1906
|
+
let r = this.defaults.tokenizer || new w(this.defaults);
|
|
1907
|
+
for (let i in n.tokenizer) {
|
|
1908
|
+
if (!(i in r)) throw new Error(`tokenizer '${i}' does not exist`);
|
|
1909
|
+
if (["options", "rules", "lexer"].includes(i)) continue;
|
|
1910
|
+
let o = i, u = n.tokenizer[o], a = r[o];
|
|
1911
|
+
r[o] = (...p) => {
|
|
1912
|
+
let c = u.apply(r, p);
|
|
1913
|
+
return c === false && (c = a.apply(r, p)), c;
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
s.tokenizer = r;
|
|
1917
|
+
}
|
|
1918
|
+
if (n.hooks) {
|
|
1919
|
+
let r = this.defaults.hooks || new P();
|
|
1920
|
+
for (let i in n.hooks) {
|
|
1921
|
+
if (!(i in r)) throw new Error(`hook '${i}' does not exist`);
|
|
1922
|
+
if (["options", "block"].includes(i)) continue;
|
|
1923
|
+
let o = i, u = n.hooks[o], a = r[o];
|
|
1924
|
+
P.passThroughHooks.has(i) ? r[o] = (p) => {
|
|
1925
|
+
if (this.defaults.async && P.passThroughHooksRespectAsync.has(i)) return (async () => {
|
|
1926
|
+
let h = await u.call(r, p);
|
|
1927
|
+
return a.call(r, h);
|
|
1928
|
+
})();
|
|
1929
|
+
let c = u.call(r, p);
|
|
1930
|
+
return a.call(r, c);
|
|
1931
|
+
} : r[o] = (...p) => {
|
|
1932
|
+
if (this.defaults.async) return (async () => {
|
|
1933
|
+
let h = await u.apply(r, p);
|
|
1934
|
+
return h === false && (h = await a.apply(r, p)), h;
|
|
1935
|
+
})();
|
|
1936
|
+
let c = u.apply(r, p);
|
|
1937
|
+
return c === false && (c = a.apply(r, p)), c;
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
s.hooks = r;
|
|
1941
|
+
}
|
|
1942
|
+
if (n.walkTokens) {
|
|
1943
|
+
let r = this.defaults.walkTokens, i = n.walkTokens;
|
|
1944
|
+
s.walkTokens = function(o) {
|
|
1945
|
+
let u = [];
|
|
1946
|
+
return u.push(i.call(this, o)), r && (u = u.concat(r.call(this, o))), u;
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
this.defaults = { ...this.defaults, ...s };
|
|
1950
|
+
}), this;
|
|
1951
|
+
}
|
|
1952
|
+
setOptions(e) {
|
|
1953
|
+
return this.defaults = { ...this.defaults, ...e }, this;
|
|
1954
|
+
}
|
|
1955
|
+
lexer(e, t) {
|
|
1956
|
+
return x.lex(e, t ?? this.defaults);
|
|
1957
|
+
}
|
|
1958
|
+
parser(e, t) {
|
|
1959
|
+
return b.parse(e, t ?? this.defaults);
|
|
1960
|
+
}
|
|
1961
|
+
parseMarkdown(e) {
|
|
1962
|
+
return (n, s) => {
|
|
1963
|
+
let r = { ...s }, i = { ...this.defaults, ...r }, o = this.onError(!!i.silent, !!i.async);
|
|
1964
|
+
if (this.defaults.async === true && r.async === false) return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));
|
|
1965
|
+
if (typeof n > "u" || n === null) return o(new Error("marked(): input parameter is undefined or null"));
|
|
1966
|
+
if (typeof n != "string") return o(new Error("marked(): input parameter is of type " + Object.prototype.toString.call(n) + ", string expected"));
|
|
1967
|
+
if (i.hooks && (i.hooks.options = i, i.hooks.block = e), i.async) return (async () => {
|
|
1968
|
+
let u = i.hooks ? await i.hooks.preprocess(n) : n, p = await (i.hooks ? await i.hooks.provideLexer(e) : e ? x.lex : x.lexInline)(u, i), c = i.hooks ? await i.hooks.processAllTokens(p) : p;
|
|
1969
|
+
i.walkTokens && await Promise.all(this.walkTokens(c, i.walkTokens));
|
|
1970
|
+
let k = await (i.hooks ? await i.hooks.provideParser(e) : e ? b.parse : b.parseInline)(c, i);
|
|
1971
|
+
return i.hooks ? await i.hooks.postprocess(k) : k;
|
|
1972
|
+
})().catch(o);
|
|
1973
|
+
try {
|
|
1974
|
+
i.hooks && (n = i.hooks.preprocess(n));
|
|
1975
|
+
let a = (i.hooks ? i.hooks.provideLexer(e) : e ? x.lex : x.lexInline)(n, i);
|
|
1976
|
+
i.hooks && (a = i.hooks.processAllTokens(a)), i.walkTokens && this.walkTokens(a, i.walkTokens);
|
|
1977
|
+
let c = (i.hooks ? i.hooks.provideParser(e) : e ? b.parse : b.parseInline)(a, i);
|
|
1978
|
+
return i.hooks && (c = i.hooks.postprocess(c)), c;
|
|
1979
|
+
} catch (u) {
|
|
1980
|
+
return o(u);
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
onError(e, t) {
|
|
1985
|
+
return (n) => {
|
|
1986
|
+
if (n.message += `
|
|
1987
|
+
Please report this to https://github.com/markedjs/marked.`, e) {
|
|
1988
|
+
let s = "<p>An error occurred:</p><pre>" + O(n.message + "", true) + "</pre>";
|
|
1989
|
+
return t ? Promise.resolve(s) : s;
|
|
1990
|
+
}
|
|
1991
|
+
if (t) return Promise.reject(n);
|
|
1992
|
+
throw n;
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
var M = new D();
|
|
1997
|
+
function g(l3, e) {
|
|
1998
|
+
return M.parse(l3, e);
|
|
1999
|
+
}
|
|
2000
|
+
g.options = g.setOptions = function(l3) {
|
|
2001
|
+
return M.setOptions(l3), g.defaults = M.defaults, N(g.defaults), g;
|
|
2002
|
+
};
|
|
2003
|
+
g.getDefaults = z;
|
|
2004
|
+
g.defaults = T;
|
|
2005
|
+
g.use = function(...l3) {
|
|
2006
|
+
return M.use(...l3), g.defaults = M.defaults, N(g.defaults), g;
|
|
2007
|
+
};
|
|
2008
|
+
g.walkTokens = function(l3, e) {
|
|
2009
|
+
return M.walkTokens(l3, e);
|
|
2010
|
+
};
|
|
2011
|
+
g.parseInline = M.parseInline;
|
|
2012
|
+
g.Parser = b;
|
|
2013
|
+
g.parser = b.parse;
|
|
2014
|
+
g.Renderer = y;
|
|
2015
|
+
g.TextRenderer = L;
|
|
2016
|
+
g.Lexer = x;
|
|
2017
|
+
g.lexer = x.lex;
|
|
2018
|
+
g.Tokenizer = w;
|
|
2019
|
+
g.Hooks = P;
|
|
2020
|
+
g.parse = g;
|
|
2021
|
+
var Kt = g.options;
|
|
2022
|
+
var Wt = g.setOptions;
|
|
2023
|
+
var Xt = g.use;
|
|
2024
|
+
var Jt = g.walkTokens;
|
|
2025
|
+
var Vt = g.parseInline;
|
|
2026
|
+
var en = b.parse;
|
|
2027
|
+
var tn = x.lex;
|
|
2028
|
+
|
|
2029
|
+
// packages/extensions/pages/src/markdown.ts
|
|
2030
|
+
var marked = new D({ gfm: true });
|
|
2031
|
+
var BASE_STYLES = `
|
|
2032
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
2033
|
+
body {
|
|
2034
|
+
max-width: 48rem;
|
|
2035
|
+
margin: 2rem auto;
|
|
2036
|
+
padding: 0 1rem;
|
|
2037
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
2038
|
+
line-height: 1.6;
|
|
2039
|
+
color: #1a1a1a;
|
|
2040
|
+
}
|
|
2041
|
+
h1, h2, h3, h4, h5, h6 { margin-top: 1.5em; margin-bottom: 0.5em; line-height: 1.3; }
|
|
2042
|
+
h1 { font-size: 2em; }
|
|
2043
|
+
a { color: #2563eb; }
|
|
2044
|
+
code {
|
|
2045
|
+
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
2046
|
+
font-size: 0.9em;
|
|
2047
|
+
background: #f3f4f6;
|
|
2048
|
+
padding: 0.15em 0.3em;
|
|
2049
|
+
border-radius: 3px;
|
|
2050
|
+
}
|
|
2051
|
+
pre {
|
|
2052
|
+
background: #f3f4f6;
|
|
2053
|
+
padding: 1em;
|
|
2054
|
+
border-radius: 6px;
|
|
2055
|
+
overflow-x: auto;
|
|
2056
|
+
}
|
|
2057
|
+
pre code { background: none; padding: 0; }
|
|
2058
|
+
blockquote {
|
|
2059
|
+
border-left: 3px solid #d1d5db;
|
|
2060
|
+
margin-left: 0;
|
|
2061
|
+
padding-left: 1em;
|
|
2062
|
+
color: #4b5563;
|
|
2063
|
+
}
|
|
2064
|
+
table { border-collapse: collapse; width: 100%; }
|
|
2065
|
+
th, td { border: 1px solid #d1d5db; padding: 0.5em 0.75em; text-align: left; }
|
|
2066
|
+
th { background: #f9fafb; }
|
|
2067
|
+
img { max-width: 100%; height: auto; }
|
|
2068
|
+
hr { border: none; border-top: 1px solid #d1d5db; margin: 2em 0; }
|
|
2069
|
+
/* A citation: naming a mind is highlighted, and costs exactly what a link costs. */
|
|
2070
|
+
a.mention {
|
|
2071
|
+
color: #7c3aed;
|
|
2072
|
+
background: #f5f3ff;
|
|
2073
|
+
padding: 0.05em 0.25em;
|
|
2074
|
+
border-radius: 3px;
|
|
2075
|
+
text-decoration: none;
|
|
2076
|
+
font-weight: 500;
|
|
2077
|
+
}
|
|
2078
|
+
a.mention:hover { text-decoration: underline; }
|
|
2079
|
+
`;
|
|
2080
|
+
function escapeHtml(s) {
|
|
2081
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2082
|
+
}
|
|
2083
|
+
function parseFrontmatter(raw) {
|
|
2084
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
2085
|
+
if (!match) return { body: raw };
|
|
2086
|
+
const block2 = match[1];
|
|
2087
|
+
const body = raw.slice(match[0].length);
|
|
2088
|
+
const titleMatch = block2.match(/^title:\s*(.+)$/m);
|
|
2089
|
+
const styleMatch = block2.match(/^style:\s*(.+)$/m);
|
|
2090
|
+
const commentsMatch = block2.match(/^comments:\s*(.+)$/m);
|
|
2091
|
+
const commentsRaw = commentsMatch?.[1].trim().toLowerCase();
|
|
2092
|
+
return {
|
|
2093
|
+
title: titleMatch?.[1].trim(),
|
|
2094
|
+
style: styleMatch?.[1].trim(),
|
|
2095
|
+
// Only the explicit closing words count. Anything else — including a typo —
|
|
2096
|
+
// leaves the page open, because failing open is the harmless direction.
|
|
2097
|
+
comments: commentsRaw === void 0 ? void 0 : !["false", "no", "off"].includes(commentsRaw),
|
|
2098
|
+
body
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
function resolveStylesheet(mdFilePath, pagesRoot, frontmatterStyle) {
|
|
2102
|
+
if (frontmatterStyle) {
|
|
2103
|
+
const abs = resolve2(pagesRoot, frontmatterStyle);
|
|
2104
|
+
if (abs.startsWith(pagesRoot + "/") && existsSync2(abs)) return frontmatterStyle;
|
|
2105
|
+
}
|
|
2106
|
+
const dir = dirname(mdFilePath);
|
|
2107
|
+
const localCss = resolve2(dir, "style.css");
|
|
2108
|
+
if (existsSync2(localCss)) return relative(pagesRoot, localCss);
|
|
2109
|
+
if (dir !== pagesRoot) {
|
|
2110
|
+
const rootCss = resolve2(pagesRoot, "style.css");
|
|
2111
|
+
if (existsSync2(rootCss)) return "style.css";
|
|
2112
|
+
}
|
|
2113
|
+
return null;
|
|
2114
|
+
}
|
|
2115
|
+
async function renderMarkdownPage(body, opts) {
|
|
2116
|
+
const withMentions = opts.mentions?.length ? linkMentions(body, opts.mentions) : body;
|
|
2117
|
+
const html = DOMPurify.sanitize(await marked.parse(withMentions));
|
|
2118
|
+
const title = escapeHtml(opts.title || "Untitled");
|
|
2119
|
+
const linkTag = opts.stylesheetUrl ? `
|
|
2120
|
+
<link rel="stylesheet" href="${escapeHtml(opts.stylesheetUrl)}">` : "";
|
|
2121
|
+
return `<!DOCTYPE html>
|
|
2122
|
+
<html lang="en">
|
|
2123
|
+
<head>
|
|
2124
|
+
<meta charset="utf-8">
|
|
2125
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2126
|
+
<title>${title}</title>
|
|
2127
|
+
<style>${BASE_STYLES}</style>${linkTag}
|
|
2128
|
+
</head>
|
|
2129
|
+
<body>
|
|
2130
|
+
<article>${html}</article>
|
|
2131
|
+
</body>
|
|
2132
|
+
</html>`;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// packages/extensions/pages/src/publish.ts
|
|
2136
|
+
var QUICK_DIR = "notes";
|
|
2137
|
+
function describePages(baseDir, files) {
|
|
2138
|
+
return files.map((file) => {
|
|
2139
|
+
const raw = readFileSync2(resolve3(baseDir, file));
|
|
2140
|
+
const hash = createHash("sha256").update(raw).digest("hex");
|
|
2141
|
+
const text = raw.toString("utf-8");
|
|
2142
|
+
if (!file.endsWith(".md")) return { file, hash, links: parseLinks(text) };
|
|
2143
|
+
const { comments, body } = parseFrontmatter(text);
|
|
2144
|
+
return {
|
|
2145
|
+
file,
|
|
2146
|
+
hash,
|
|
2147
|
+
commentsClosed: comments === void 0 ? false : !comments,
|
|
2148
|
+
mentions: parseMentions(body),
|
|
2149
|
+
links: parseLinks(body)
|
|
2150
|
+
};
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
function collectFiles(dir, baseDir, ext) {
|
|
2154
|
+
const files = [];
|
|
2155
|
+
let items;
|
|
2156
|
+
try {
|
|
2157
|
+
items = readdirSync(dir);
|
|
2158
|
+
} catch (err) {
|
|
2159
|
+
console.error(`[pages] failed to read directory ${dir}: ${err.message}`);
|
|
2160
|
+
return files;
|
|
2161
|
+
}
|
|
2162
|
+
for (const item of items) {
|
|
2163
|
+
if (item.startsWith(".")) continue;
|
|
2164
|
+
const fullPath = resolve3(dir, item);
|
|
2165
|
+
try {
|
|
2166
|
+
const s = statSync(fullPath);
|
|
2167
|
+
const matchesExt = !ext || (Array.isArray(ext) ? ext.some((e) => item.endsWith(e)) : item.endsWith(ext));
|
|
2168
|
+
if (s.isFile() && matchesExt) {
|
|
2169
|
+
files.push(relative2(baseDir, fullPath));
|
|
2170
|
+
} else if (s.isDirectory()) {
|
|
2171
|
+
files.push(...collectFiles(fullPath, baseDir, ext));
|
|
2172
|
+
}
|
|
2173
|
+
} catch (err) {
|
|
2174
|
+
console.error(`[pages] failed to stat ${fullPath}: ${err.message}`);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
return files.sort();
|
|
2178
|
+
}
|
|
2179
|
+
function publishPersonalPages(ctx, mindName, mindDir, opts = {}) {
|
|
2180
|
+
const db = ctx.db;
|
|
2181
|
+
if (!db) throw new Error("Database not available");
|
|
2182
|
+
const sourceDir = resolve3(mindDir, "home", "pages");
|
|
2183
|
+
if (!existsSync3(sourceDir)) throw new Error("No pages directory found (home/pages/)");
|
|
2184
|
+
const snapshotDir = resolve3(ctx.dataDir, "sites", mindName);
|
|
2185
|
+
if (existsSync3(snapshotDir)) rmSync(snapshotDir, { recursive: true });
|
|
2186
|
+
cpSync(sourceDir, snapshotDir, {
|
|
2187
|
+
recursive: true,
|
|
2188
|
+
filter: (src) => !src.endsWith(`${sep}_system`) && !src.includes(`${sep}_system${sep}`)
|
|
2189
|
+
});
|
|
2190
|
+
const pageFiles = collectFiles(snapshotDir, snapshotDir, [".html", ".md"]);
|
|
2191
|
+
const diff = syncPublishedPages(db, mindName, describePages(snapshotDir, pageFiles));
|
|
2192
|
+
for (const file of diff.added) {
|
|
2193
|
+
if (file === opts.skipActivityFor) continue;
|
|
2194
|
+
ctx.publishActivity({
|
|
2195
|
+
type: "page_published",
|
|
2196
|
+
mind: mindName,
|
|
2197
|
+
summary: `${mindName} published ${file}`,
|
|
2198
|
+
metadata: {
|
|
2199
|
+
file,
|
|
2200
|
+
url: `/minds/${mindName}/pages/${file}`,
|
|
2201
|
+
iframeUrl: `/ext/pages/public/${mindName}/${file}`
|
|
2202
|
+
}
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
for (const file of diff.removed) {
|
|
2206
|
+
ctx.publishActivity({
|
|
2207
|
+
type: "page_removed",
|
|
2208
|
+
mind: mindName,
|
|
2209
|
+
summary: `${mindName} removed ${file}`,
|
|
2210
|
+
metadata: { file }
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
return { fileCount: pageFiles.length, diff, snapshotDir };
|
|
2214
|
+
}
|
|
2215
|
+
function defaultPromotionTitle(content, file) {
|
|
2216
|
+
const firstLine = content.split("\n").find((l3) => l3.trim().length > 0)?.trim() ?? "";
|
|
2217
|
+
const trimmed = firstLine.length > 72 ? `${firstLine.slice(0, 72).trimEnd()}\u2026` : firstLine;
|
|
2218
|
+
if (trimmed) return trimmed;
|
|
2219
|
+
const stem = file.replace(/\.(md|html)$/, "").split("/").pop();
|
|
2220
|
+
return `Re: ${stem}`;
|
|
2221
|
+
}
|
|
2222
|
+
function slugify(text) {
|
|
2223
|
+
return text.toLowerCase().replace(/['’]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
2224
|
+
}
|
|
2225
|
+
function yamlQuote(value) {
|
|
2226
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
2227
|
+
}
|
|
2228
|
+
function quickPageContent(title, body) {
|
|
2229
|
+
return `---
|
|
2230
|
+
title: ${yamlQuote(title)}
|
|
2231
|
+
---
|
|
2232
|
+
|
|
2233
|
+
${body.trimEnd()}
|
|
2234
|
+
`;
|
|
2235
|
+
}
|
|
2236
|
+
function allocateQuickPath(pagesDir, title, taken) {
|
|
2237
|
+
const base = slugify(title) || "untitled";
|
|
2238
|
+
const dir = resolve3(pagesDir, QUICK_DIR);
|
|
2239
|
+
const isFree = (name2) => !existsSync3(resolve3(dir, name2)) && !taken?.has(`${QUICK_DIR}/${name2}`);
|
|
2240
|
+
let name = `${base}.md`;
|
|
2241
|
+
for (let i = 2; !isFree(name); i++) {
|
|
2242
|
+
name = `${base}-${i}.md`;
|
|
2243
|
+
}
|
|
2244
|
+
return `${QUICK_DIR}/${name}`;
|
|
2245
|
+
}
|
|
2246
|
+
function writeQuickPage(ctx, mindName, mindDir, title, body) {
|
|
2247
|
+
const db = ctx.db;
|
|
2248
|
+
if (!db) throw new Error("Database not available");
|
|
2249
|
+
const pagesDir = resolve3(mindDir, "home", "pages");
|
|
2250
|
+
const file = allocateQuickPath(pagesDir, title, knownPageFiles(db, mindName));
|
|
2251
|
+
const target = resolve3(pagesDir, file);
|
|
2252
|
+
const root = resolve3(pagesDir);
|
|
2253
|
+
if (target !== root && !target.startsWith(root + sep)) {
|
|
2254
|
+
throw new Error("Refusing to write outside the pages directory");
|
|
2255
|
+
}
|
|
2256
|
+
mkdirSync(resolve3(pagesDir, QUICK_DIR), { recursive: true });
|
|
2257
|
+
writeFileSync2(target, quickPageContent(title, body), "utf-8");
|
|
2258
|
+
return { file, publish: publishPersonalPages(ctx, mindName, mindDir, { skipActivityFor: file }) };
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// packages/extensions/pages/src/migrate-notes.ts
|
|
2262
|
+
function chownToMind(path, user) {
|
|
2263
|
+
return new Promise((res, rej) => {
|
|
2264
|
+
execFile("chown", [`${user}:volute`, path], (err) => err ? rej(err) : res());
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
function defaultNotesDbPath(pagesDataDir) {
|
|
2268
|
+
return resolve4(pagesDataDir, "..", "notes", "data.db");
|
|
2269
|
+
}
|
|
2270
|
+
function parseReassignments(values) {
|
|
2271
|
+
const map = /* @__PURE__ */ new Map();
|
|
2272
|
+
for (const raw of values) {
|
|
2273
|
+
const [idPart, username] = raw.split("=", 2);
|
|
2274
|
+
const id = Number.parseInt(idPart ?? "", 10);
|
|
2275
|
+
if (!Number.isInteger(id) || !username) {
|
|
2276
|
+
throw new Error(`Invalid --reassign value: ${raw} (expected <noteId>=<username>)`);
|
|
2277
|
+
}
|
|
2278
|
+
map.set(id, username);
|
|
2279
|
+
}
|
|
2280
|
+
return map;
|
|
2281
|
+
}
|
|
2282
|
+
function readAll(db, sql) {
|
|
2283
|
+
return db.prepare(sql).all();
|
|
2284
|
+
}
|
|
2285
|
+
function noteTargetFile(slug) {
|
|
2286
|
+
return `${QUICK_DIR}/${slug}.md`;
|
|
2287
|
+
}
|
|
2288
|
+
function migratedBody(note, replyTo) {
|
|
2289
|
+
const parts = [];
|
|
2290
|
+
if (replyTo) {
|
|
2291
|
+
const url = `/ext/pages/public/${replyTo.mind}/${replyTo.file}`;
|
|
2292
|
+
parts.push(`*In reply to [${replyTo.mind}/${replyTo.file}](${url})*
|
|
2293
|
+
`);
|
|
2294
|
+
}
|
|
2295
|
+
parts.push(note.content);
|
|
2296
|
+
return parts.join("\n");
|
|
2297
|
+
}
|
|
2298
|
+
async function planMigration(notesDb, ctx, opts = {}) {
|
|
2299
|
+
const pagesDb = ctx.db;
|
|
2300
|
+
if (!pagesDb) throw new Error("Pages database not available");
|
|
2301
|
+
const notes = readAll(
|
|
2302
|
+
notesDb,
|
|
2303
|
+
"SELECT id, author_id, title, slug, content, reply_to_id, created_at, updated_at FROM notes ORDER BY id"
|
|
2304
|
+
);
|
|
2305
|
+
const comments = readAll(
|
|
2306
|
+
notesDb,
|
|
2307
|
+
"SELECT id, note_id, author_id, content, created_at FROM note_comments ORDER BY id"
|
|
2308
|
+
);
|
|
2309
|
+
const reactions = readAll(
|
|
2310
|
+
notesDb,
|
|
2311
|
+
"SELECT id, note_id, user_id, emoji, created_at FROM note_reactions ORDER BY id"
|
|
2312
|
+
);
|
|
2313
|
+
const commentsByNote = /* @__PURE__ */ new Map();
|
|
2314
|
+
for (const c of comments) commentsByNote.set(c.note_id, (commentsByNote.get(c.note_id) ?? 0) + 1);
|
|
2315
|
+
const reactionsByNote = /* @__PURE__ */ new Map();
|
|
2316
|
+
for (const r of reactions)
|
|
2317
|
+
reactionsByNote.set(r.note_id, (reactionsByNote.get(r.note_id) ?? 0) + 1);
|
|
2318
|
+
const alreadyMigrated = new Map(
|
|
2319
|
+
pagesDb.prepare("SELECT note_id, mind, file FROM migrated_notes").all().map((r) => [r.note_id, r])
|
|
2320
|
+
);
|
|
2321
|
+
const reassign = opts.reassign ?? /* @__PURE__ */ new Map();
|
|
2322
|
+
const userCache = /* @__PURE__ */ new Map();
|
|
2323
|
+
const mindDirCache = /* @__PURE__ */ new Map();
|
|
2324
|
+
async function usernameOf(id) {
|
|
2325
|
+
if (!userCache.has(id)) userCache.set(id, (await ctx.getUser(id))?.username ?? null);
|
|
2326
|
+
return userCache.get(id) ?? null;
|
|
2327
|
+
}
|
|
2328
|
+
async function mindDirOf(name) {
|
|
2329
|
+
if (!mindDirCache.has(name)) mindDirCache.set(name, await ctx.getMindDir(name));
|
|
2330
|
+
return mindDirCache.get(name) ?? null;
|
|
2331
|
+
}
|
|
2332
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
2333
|
+
const entries = [];
|
|
2334
|
+
for (const note of notes) {
|
|
2335
|
+
const recorded = await usernameOf(note.author_id) ?? `user#${note.author_id}`;
|
|
2336
|
+
const override = reassign.get(note.id);
|
|
2337
|
+
const resolved = override ?? recorded;
|
|
2338
|
+
const file = noteTargetFile(note.slug);
|
|
2339
|
+
const key = `${resolved}/${file}`;
|
|
2340
|
+
const base = {
|
|
2341
|
+
noteId: note.id,
|
|
2342
|
+
title: note.title,
|
|
2343
|
+
slug: note.slug,
|
|
2344
|
+
recordedAuthor: recorded,
|
|
2345
|
+
resolvedAuthor: resolved,
|
|
2346
|
+
reassigned: override !== void 0,
|
|
2347
|
+
createdAt: note.created_at,
|
|
2348
|
+
commentCount: commentsByNote.get(note.id) ?? 0,
|
|
2349
|
+
reactionCount: reactionsByNote.get(note.id) ?? 0
|
|
2350
|
+
};
|
|
2351
|
+
const prior = alreadyMigrated.get(note.id);
|
|
2352
|
+
if (prior) {
|
|
2353
|
+
entries.push({
|
|
2354
|
+
...base,
|
|
2355
|
+
status: "already-migrated",
|
|
2356
|
+
target: { mind: prior.mind, file: prior.file }
|
|
2357
|
+
});
|
|
2358
|
+
claimed.set(`${prior.mind}/${prior.file}`, note.id);
|
|
2359
|
+
continue;
|
|
2360
|
+
}
|
|
2361
|
+
const blocked = (reason) => {
|
|
2362
|
+
entries.push({ ...base, status: "blocked", target: null, reason });
|
|
2363
|
+
};
|
|
2364
|
+
const mindDir = await mindDirOf(resolved);
|
|
2365
|
+
if (!mindDir) {
|
|
2366
|
+
blocked(
|
|
2367
|
+
`author "${recorded}" is not a mind with a pages directory` + (override ? ` (reassigned to "${resolved}", which also has none)` : "") + ` \u2014 reassign it with --reassign ${note.id}=<mind>`
|
|
2368
|
+
);
|
|
2369
|
+
continue;
|
|
2370
|
+
}
|
|
2371
|
+
const claimant = claimed.get(key);
|
|
2372
|
+
if (claimant !== void 0) {
|
|
2373
|
+
blocked(`target ${key} is already claimed by note #${claimant}`);
|
|
2374
|
+
continue;
|
|
2375
|
+
}
|
|
2376
|
+
const onDisk = resolve4(mindDir, "home", "pages", file);
|
|
2377
|
+
if (existsSync4(onDisk)) {
|
|
2378
|
+
blocked(`target ${key} already exists on disk and was not written by this migration`);
|
|
2379
|
+
continue;
|
|
2380
|
+
}
|
|
2381
|
+
const priorPage = getPage(pagesDb, resolved, file);
|
|
2382
|
+
if (priorPage) {
|
|
2383
|
+
blocked(
|
|
2384
|
+
priorPage.deleted_at ? `target ${key} is a deleted page whose comment thread is still attached` : `target ${key} is already a published page`
|
|
2385
|
+
);
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2388
|
+
claimed.set(key, note.id);
|
|
2389
|
+
entries.push({ ...base, status: "migrate", target: { mind: resolved, file } });
|
|
2390
|
+
}
|
|
2391
|
+
const byAuthorMap = /* @__PURE__ */ new Map();
|
|
2392
|
+
for (const e of entries) {
|
|
2393
|
+
const acc = byAuthorMap.get(e.resolvedAuthor) ?? { notes: 0, comments: 0, reactions: 0 };
|
|
2394
|
+
acc.notes += 1;
|
|
2395
|
+
acc.comments += e.commentCount;
|
|
2396
|
+
acc.reactions += e.reactionCount;
|
|
2397
|
+
byAuthorMap.set(e.resolvedAuthor, acc);
|
|
2398
|
+
}
|
|
2399
|
+
return {
|
|
2400
|
+
entries,
|
|
2401
|
+
totals: {
|
|
2402
|
+
notes: notes.length,
|
|
2403
|
+
comments: comments.length,
|
|
2404
|
+
reactions: reactions.length,
|
|
2405
|
+
migrate: entries.filter((e) => e.status === "migrate").length,
|
|
2406
|
+
alreadyMigrated: entries.filter((e) => e.status === "already-migrated").length,
|
|
2407
|
+
blocked: entries.filter((e) => e.status === "blocked").length
|
|
2408
|
+
},
|
|
2409
|
+
byAuthor: [...byAuthorMap.entries()].map(([author, v2]) => ({ author, ...v2 })).sort((a, b2) => b2.notes - a.notes || a.author.localeCompare(b2.author))
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
function formatPlan(plan, opts) {
|
|
2413
|
+
const lines = [];
|
|
2414
|
+
lines.push(
|
|
2415
|
+
opts.applied ? "Notes \u2192 Pages migration (APPLIED)" : "Notes \u2192 Pages migration (DRY RUN \u2014 nothing was written)"
|
|
2416
|
+
);
|
|
2417
|
+
lines.push("");
|
|
2418
|
+
for (const e of plan.entries) {
|
|
2419
|
+
const attribution = e.reassigned ? `${e.recordedAuthor} \u2192 ${e.resolvedAuthor} (REASSIGNED)` : e.resolvedAuthor;
|
|
2420
|
+
const target = e.target ? `${e.target.mind}/${e.target.file}` : "\u2014";
|
|
2421
|
+
lines.push(
|
|
2422
|
+
`#${String(e.noteId).padStart(3)} [${e.status.padEnd(16)}] ${attribution.padEnd(28)} ${target}`
|
|
2423
|
+
);
|
|
2424
|
+
lines.push(
|
|
2425
|
+
` "${e.title}" created ${e.createdAt} ${e.commentCount} comment(s), ${e.reactionCount} reaction(s)`
|
|
2426
|
+
);
|
|
2427
|
+
if (e.reason) lines.push(` BLOCKED: ${e.reason}`);
|
|
2428
|
+
}
|
|
2429
|
+
lines.push("");
|
|
2430
|
+
lines.push("Per author (resolved):");
|
|
2431
|
+
for (const a of plan.byAuthor) {
|
|
2432
|
+
lines.push(
|
|
2433
|
+
` ${a.author.padEnd(20)} ${String(a.notes).padStart(3)} notes ${String(a.comments).padStart(3)} comments ${String(a.reactions).padStart(3)} reactions`
|
|
2434
|
+
);
|
|
2435
|
+
}
|
|
2436
|
+
lines.push("");
|
|
2437
|
+
lines.push(
|
|
2438
|
+
`Totals: ${plan.totals.notes} notes, ${plan.totals.comments} comments, ${plan.totals.reactions} reactions`
|
|
2439
|
+
);
|
|
2440
|
+
lines.push(
|
|
2441
|
+
` ${plan.totals.migrate} to migrate, ${plan.totals.alreadyMigrated} already migrated, ${plan.totals.blocked} blocked`
|
|
2442
|
+
);
|
|
2443
|
+
if (!opts.applied) {
|
|
2444
|
+
lines.push("");
|
|
2445
|
+
if (plan.totals.blocked > 0) {
|
|
2446
|
+
lines.push("Blocked entries must be resolved before applying. Repair attribution with");
|
|
2447
|
+
lines.push(
|
|
2448
|
+
" --reassign <noteId>=<mind> (repeatable), or pass --skip-blocked to leave them behind."
|
|
2449
|
+
);
|
|
2450
|
+
}
|
|
2451
|
+
lines.push("Re-run with --apply to write. Nothing has changed yet.");
|
|
2452
|
+
}
|
|
2453
|
+
return lines.join("\n");
|
|
2454
|
+
}
|
|
2455
|
+
async function applyMigration(notesDb, ctx, plan, opts = {}) {
|
|
2456
|
+
const pagesDb = ctx.db;
|
|
2457
|
+
if (!pagesDb) throw new Error("Pages database not available");
|
|
2458
|
+
if (plan.totals.blocked > 0 && !opts.skipBlocked) {
|
|
2459
|
+
throw new Error(
|
|
2460
|
+
`${plan.totals.blocked} note(s) are blocked \u2014 resolve them with --reassign, or pass --skip-blocked to leave them behind. Nothing was written.`
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
const notes = new Map(
|
|
2464
|
+
readAll(
|
|
2465
|
+
notesDb,
|
|
2466
|
+
"SELECT id, author_id, title, slug, content, reply_to_id, created_at, updated_at FROM notes"
|
|
2467
|
+
).map((n) => [n.id, n])
|
|
2468
|
+
);
|
|
2469
|
+
const comments = readAll(
|
|
2470
|
+
notesDb,
|
|
2471
|
+
"SELECT id, note_id, author_id, content, created_at FROM note_comments ORDER BY id"
|
|
2472
|
+
);
|
|
2473
|
+
const reactions = readAll(
|
|
2474
|
+
notesDb,
|
|
2475
|
+
"SELECT id, note_id, user_id, emoji, created_at FROM note_reactions ORDER BY id"
|
|
2476
|
+
);
|
|
2477
|
+
const targetByNote = /* @__PURE__ */ new Map();
|
|
2478
|
+
for (const e of plan.entries) {
|
|
2479
|
+
if (e.target) targetByNote.set(e.noteId, e.target);
|
|
2480
|
+
}
|
|
2481
|
+
const commentsByNote = /* @__PURE__ */ new Map();
|
|
2482
|
+
for (const c of comments) {
|
|
2483
|
+
const list = commentsByNote.get(c.note_id);
|
|
2484
|
+
if (list) list.push(c);
|
|
2485
|
+
else commentsByNote.set(c.note_id, [c]);
|
|
2486
|
+
}
|
|
2487
|
+
const reactionsByNote = /* @__PURE__ */ new Map();
|
|
2488
|
+
for (const r of reactions) {
|
|
2489
|
+
const list = reactionsByNote.get(r.note_id);
|
|
2490
|
+
if (list) list.push(r);
|
|
2491
|
+
else reactionsByNote.set(r.note_id, [r]);
|
|
2492
|
+
}
|
|
2493
|
+
let migrated = 0;
|
|
2494
|
+
let commentCount = 0;
|
|
2495
|
+
let reactionCount = 0;
|
|
2496
|
+
for (const entry of plan.entries) {
|
|
2497
|
+
if (entry.status !== "migrate" || !entry.target) continue;
|
|
2498
|
+
const note = notes.get(entry.noteId);
|
|
2499
|
+
if (!note) continue;
|
|
2500
|
+
const { mind, file } = entry.target;
|
|
2501
|
+
const mindDir = await ctx.getMindDir(mind);
|
|
2502
|
+
if (!mindDir) {
|
|
2503
|
+
throw new Error(`Mind directory for ${mind} disappeared between plan and apply`);
|
|
2504
|
+
}
|
|
2505
|
+
const pagesDir = resolve4(mindDir, "home", "pages");
|
|
2506
|
+
const target = resolve4(pagesDir, file);
|
|
2507
|
+
if (target !== pagesDir && !target.startsWith(pagesDir + sep2)) {
|
|
2508
|
+
throw new Error(`Refusing to write outside ${mind}'s pages directory: ${file}`);
|
|
2509
|
+
}
|
|
2510
|
+
const replyTo = note.reply_to_id != null ? targetByNote.get(note.reply_to_id) ?? null : null;
|
|
2511
|
+
const content = quickPageContent(note.title, migratedBody(note, replyTo));
|
|
2512
|
+
mkdirSync2(resolve4(pagesDir, QUICK_DIR), { recursive: true });
|
|
2513
|
+
writeFileSync3(target, content, "utf-8");
|
|
2514
|
+
const snapshotFile = resolve4(ctx.dataDir, "sites", mind, file);
|
|
2515
|
+
mkdirSync2(resolve4(ctx.dataDir, "sites", mind, QUICK_DIR), { recursive: true });
|
|
2516
|
+
writeFileSync3(snapshotFile, content, "utf-8");
|
|
2517
|
+
const hash = createHash2("sha256").update(readFileSync3(target)).digest("hex");
|
|
2518
|
+
pagesDb.exec("BEGIN");
|
|
2519
|
+
try {
|
|
2520
|
+
upsertPageWithTimestamps(pagesDb, mind, file, hash, note.created_at, note.updated_at, mind);
|
|
2521
|
+
for (const c of commentsByNote.get(note.id) ?? []) {
|
|
2522
|
+
pagesDb.prepare(
|
|
2523
|
+
`INSERT INTO page_comments (mind, file, author_id, content, page_hash, created_at)
|
|
2524
|
+
VALUES (?, ?, ?, ?, NULL, ?)`
|
|
2525
|
+
).run(mind, file, c.author_id, c.content, c.created_at);
|
|
2526
|
+
commentCount++;
|
|
2527
|
+
}
|
|
2528
|
+
for (const r of reactionsByNote.get(note.id) ?? []) {
|
|
2529
|
+
pagesDb.prepare(
|
|
2530
|
+
`INSERT OR IGNORE INTO page_reactions (mind, file, user_id, emoji, created_at)
|
|
2531
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
2532
|
+
).run(mind, file, r.user_id, r.emoji, r.created_at);
|
|
2533
|
+
reactionCount++;
|
|
2534
|
+
}
|
|
2535
|
+
pagesDb.prepare("INSERT INTO migrated_notes (note_id, mind, file) VALUES (?, ?, ?)").run(note.id, mind, file);
|
|
2536
|
+
pagesDb.exec("COMMIT");
|
|
2537
|
+
} catch (err) {
|
|
2538
|
+
pagesDb.exec("ROLLBACK");
|
|
2539
|
+
for (const path of [target, snapshotFile]) {
|
|
2540
|
+
try {
|
|
2541
|
+
rmSync2(path, { force: true });
|
|
2542
|
+
} catch (cleanupErr) {
|
|
2543
|
+
console.warn(
|
|
2544
|
+
`[pages] failed to clean up ${path} after a failed migration step: ${cleanupErr.message}`
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
throw err;
|
|
2549
|
+
}
|
|
2550
|
+
if (ctx.isIsolationEnabled()) {
|
|
2551
|
+
try {
|
|
2552
|
+
await chownToMind(target, ctx.getMindUser(mind));
|
|
2553
|
+
} catch (err) {
|
|
2554
|
+
console.warn(`[pages] failed to chown ${target} for ${mind}: ${err.message}`);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
migrated++;
|
|
2558
|
+
}
|
|
2559
|
+
return {
|
|
2560
|
+
plan,
|
|
2561
|
+
migrated,
|
|
2562
|
+
comments: commentCount,
|
|
2563
|
+
reactions: reactionCount,
|
|
2564
|
+
skipped: plan.totals.blocked
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// packages/extensions/pages/src/preview.ts
|
|
2569
|
+
import { execFile as execFile2 } from "child_process";
|
|
2570
|
+
import { randomUUID } from "crypto";
|
|
2571
|
+
import {
|
|
2572
|
+
accessSync,
|
|
2573
|
+
constants,
|
|
2574
|
+
existsSync as existsSync5,
|
|
2575
|
+
mkdirSync as mkdirSync3,
|
|
2576
|
+
readFileSync as readFileSync4,
|
|
2577
|
+
rmSync as rmSync3,
|
|
2578
|
+
writeFileSync as writeFileSync4
|
|
2579
|
+
} from "fs";
|
|
2580
|
+
import { basename, delimiter, join, resolve as resolve5, sep as sep3 } from "path";
|
|
2581
|
+
var MAC_CANDIDATES = [
|
|
2582
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
2583
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
2584
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
2585
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
|
|
2586
|
+
];
|
|
2587
|
+
var PATH_CANDIDATES = [
|
|
2588
|
+
"google-chrome",
|
|
2589
|
+
"google-chrome-stable",
|
|
2590
|
+
"chromium",
|
|
2591
|
+
"chromium-browser",
|
|
2592
|
+
"brave-browser",
|
|
2593
|
+
"microsoft-edge"
|
|
2594
|
+
];
|
|
2595
|
+
function isExecutable(p) {
|
|
2596
|
+
try {
|
|
2597
|
+
accessSync(p, constants.X_OK);
|
|
2598
|
+
return true;
|
|
2599
|
+
} catch {
|
|
2600
|
+
return false;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
function resolveBrowser(opts) {
|
|
2604
|
+
const override = opts?.override ?? process.env.VOLUTE_CHROMIUM;
|
|
2605
|
+
if (override) return override;
|
|
2606
|
+
const exists = opts?.exists ?? isExecutable;
|
|
2607
|
+
const platform = opts?.platform ?? process.platform;
|
|
2608
|
+
if (platform === "darwin") {
|
|
2609
|
+
for (const candidate of opts?.macCandidates ?? MAC_CANDIDATES) {
|
|
2610
|
+
if (exists(candidate)) return candidate;
|
|
2611
|
+
}
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2614
|
+
const dirs = (opts?.pathEnv ?? process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
2615
|
+
for (const name of opts?.pathCandidates ?? PATH_CANDIDATES) {
|
|
2616
|
+
for (const dir of dirs) {
|
|
2617
|
+
const full = join(dir, name);
|
|
2618
|
+
if (exists(full)) return full;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
return null;
|
|
2622
|
+
}
|
|
2623
|
+
var previewSeq = 0;
|
|
2624
|
+
function run(cmd, args, timeoutMs) {
|
|
2625
|
+
return new Promise((resolvePromise, reject) => {
|
|
2626
|
+
execFile2(cmd, args, { timeout: timeoutMs, env: { ...process.env, HOME: "/tmp" } }, (err) => {
|
|
2627
|
+
if (err) reject(err);
|
|
2628
|
+
else resolvePromise();
|
|
2629
|
+
});
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
async function renderPreview(opts) {
|
|
2633
|
+
const pagesRoot = resolve5(opts.mindDir, "home", "pages");
|
|
2634
|
+
const target = resolve5(pagesRoot, opts.file);
|
|
2635
|
+
if (target !== pagesRoot && !target.startsWith(pagesRoot + sep3)) {
|
|
2636
|
+
return { error: "Page path must stay within pages/." };
|
|
2637
|
+
}
|
|
2638
|
+
if (!existsSync5(target)) {
|
|
2639
|
+
return {
|
|
2640
|
+
error: `No page at pages/${opts.file}. Draft it in home/pages/ first, then preview it.`
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
const isHtml = opts.file.endsWith(".html");
|
|
2644
|
+
const isMd = opts.file.endsWith(".md");
|
|
2645
|
+
if (!isHtml && !isMd) {
|
|
2646
|
+
return { error: "Preview renders .html and .md pages." };
|
|
2647
|
+
}
|
|
2648
|
+
const browser = resolveBrowser();
|
|
2649
|
+
if (!browser) {
|
|
2650
|
+
return {
|
|
2651
|
+
error: "No Chrome-family browser found. Install Chrome or Chromium, or set VOLUTE_CHROMIUM to a browser binary."
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
const token = `${process.pid}-${previewSeq++}-${randomUUID()}`;
|
|
2655
|
+
let tempHtml = null;
|
|
2656
|
+
let url;
|
|
2657
|
+
if (isHtml) {
|
|
2658
|
+
url = `file://${target}`;
|
|
2659
|
+
} else {
|
|
2660
|
+
const raw = readFileSync4(target, "utf-8");
|
|
2661
|
+
const fm = parseFrontmatter(raw);
|
|
2662
|
+
const stylesheet = resolveStylesheet(target, pagesRoot, fm.style);
|
|
2663
|
+
const html = await renderMarkdownPage(fm.body, {
|
|
2664
|
+
title: fm.title,
|
|
2665
|
+
stylesheetUrl: stylesheet ?? void 0
|
|
2666
|
+
});
|
|
2667
|
+
tempHtml = resolve5(pagesRoot, `.preview-src-${token}.html`);
|
|
2668
|
+
writeFileSync4(tempHtml, html, "utf-8");
|
|
2669
|
+
url = `file://${tempHtml}`;
|
|
2670
|
+
}
|
|
2671
|
+
const previewDir = resolve5(opts.mindDir, "home", ".preview");
|
|
2672
|
+
mkdirSync3(previewDir, { recursive: true });
|
|
2673
|
+
const pngName = `${opts.file.replace(/[/\\]/g, "__").replace(/\.(html|md)$/, "")}.png`;
|
|
2674
|
+
const pngPath = resolve5(previewDir, pngName);
|
|
2675
|
+
const userDataDir = resolve5("/tmp", `chromium-${token}`);
|
|
2676
|
+
try {
|
|
2677
|
+
await run(
|
|
2678
|
+
browser,
|
|
2679
|
+
[
|
|
2680
|
+
"--headless=new",
|
|
2681
|
+
"--no-sandbox",
|
|
2682
|
+
"--disable-gpu",
|
|
2683
|
+
"--disable-dev-shm-usage",
|
|
2684
|
+
"--hide-scrollbars",
|
|
2685
|
+
"--force-color-profile=srgb",
|
|
2686
|
+
`--user-data-dir=${userDataDir}`,
|
|
2687
|
+
// Fixed viewport: the chromium CLI has no reliable full-page --screenshot
|
|
2688
|
+
// flag, so we capture a generous window rather than risk a fragile
|
|
2689
|
+
// scripted full-height capture. Tall pages are cut off below 1600px.
|
|
2690
|
+
"--window-size=1280,1600",
|
|
2691
|
+
`--screenshot=${pngPath}`,
|
|
2692
|
+
url
|
|
2693
|
+
],
|
|
2694
|
+
3e4
|
|
2695
|
+
);
|
|
2696
|
+
} catch (err) {
|
|
2697
|
+
return {
|
|
2698
|
+
error: `Could not render the page: ${err.message}. (Is a browser installed? Set VOLUTE_CHROMIUM.)`
|
|
2699
|
+
};
|
|
2700
|
+
} finally {
|
|
2701
|
+
if (tempHtml) rmSync3(tempHtml, { force: true });
|
|
2702
|
+
rmSync3(userDataDir, { recursive: true, force: true });
|
|
2703
|
+
}
|
|
2704
|
+
if (!existsSync5(pngPath)) {
|
|
2705
|
+
return { error: "The browser ran but produced no image." };
|
|
2706
|
+
}
|
|
2707
|
+
return { pngPath, rel: `home/.preview/${basename(pngPath)}` };
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// packages/extensions/pages/src/shared-pages.ts
|
|
2711
|
+
import { execFile as execFileCb } from "child_process";
|
|
2712
|
+
import {
|
|
2713
|
+
chmodSync,
|
|
2714
|
+
existsSync as existsSync6,
|
|
2715
|
+
mkdirSync as mkdirSync4,
|
|
2716
|
+
readdirSync as readdirSync2,
|
|
2717
|
+
readFileSync as readFileSync5,
|
|
2718
|
+
rmSync as rmSync4,
|
|
2719
|
+
statSync as statSync2,
|
|
2720
|
+
writeFileSync as writeFileSync5
|
|
2721
|
+
} from "fs";
|
|
2722
|
+
import { relative as relative3, resolve as resolve6 } from "path";
|
|
2723
|
+
function isolationFrom(ctx) {
|
|
2724
|
+
return { isIsolationEnabled: ctx.isIsolationEnabled, getMindUser: ctx.getMindUser };
|
|
2725
|
+
}
|
|
2726
|
+
function execAsync(cmd, args) {
|
|
2727
|
+
return new Promise((resolve11, reject) => {
|
|
2728
|
+
execFileCb(cmd, args, (err) => {
|
|
2729
|
+
if (err) reject(err);
|
|
2730
|
+
else resolve11();
|
|
2731
|
+
});
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
var IDENTITY_ARGS = ["-c", "user.name=volute", "-c", "user.email=volute@localhost"];
|
|
2735
|
+
function gitExec(args, opts, isolation) {
|
|
2736
|
+
const prefix = [];
|
|
2737
|
+
if (isolation?.isIsolationEnabled()) prefix.push("-c", "safe.directory=*");
|
|
2738
|
+
if (args[0] === "commit") prefix.push(...IDENTITY_ARGS);
|
|
2739
|
+
const fullArgs = prefix.length ? [...prefix, ...args] : args;
|
|
2740
|
+
return new Promise((resolve11, reject) => {
|
|
2741
|
+
execFileCb("git", fullArgs, { cwd: opts.cwd }, (err, stdout, stderr) => {
|
|
2742
|
+
if (err) {
|
|
2743
|
+
const e = err;
|
|
2744
|
+
e.stderr = stderr;
|
|
2745
|
+
e.stdout = stdout;
|
|
2746
|
+
reject(err);
|
|
2747
|
+
} else {
|
|
2748
|
+
resolve11(stdout);
|
|
2749
|
+
}
|
|
2750
|
+
});
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
function readWorktreeGitDir(worktreePath2) {
|
|
2754
|
+
const dotGit = resolve6(worktreePath2, ".git");
|
|
2755
|
+
if (!existsSync6(dotGit)) return null;
|
|
2756
|
+
try {
|
|
2757
|
+
const content = readFileSync5(dotGit, "utf-8").trim();
|
|
2758
|
+
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
2759
|
+
return match ? match[1] : null;
|
|
2760
|
+
} catch (err) {
|
|
2761
|
+
console.warn(`[pages] failed to read .git file at ${dotGit}: ${err.message}`);
|
|
2762
|
+
return null;
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
function pagesRepoDir(dataDir) {
|
|
2766
|
+
return resolve6(dataDir, "repo");
|
|
2767
|
+
}
|
|
2768
|
+
function worktreePath(mindDir) {
|
|
2769
|
+
return resolve6(mindDir, "home", "pages", "_system");
|
|
2770
|
+
}
|
|
2771
|
+
function pagesIsolationChownPaths(mindDir, wtGitDir) {
|
|
2772
|
+
const paths = [resolve6(mindDir, "home", "pages")];
|
|
2773
|
+
if (wtGitDir) paths.push(wtGitDir);
|
|
2774
|
+
return paths;
|
|
2775
|
+
}
|
|
2776
|
+
async function isRepoValid(dir, isolation) {
|
|
2777
|
+
try {
|
|
2778
|
+
await gitExec(["rev-parse", "HEAD"], { cwd: dir }, isolation);
|
|
2779
|
+
return true;
|
|
2780
|
+
} catch {
|
|
2781
|
+
return false;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
async function ensurePagesRepo(dataDir, isolation) {
|
|
2785
|
+
const dir = pagesRepoDir(dataDir);
|
|
2786
|
+
mkdirSync4(dir, { recursive: true });
|
|
2787
|
+
if (existsSync6(resolve6(dir, ".git"))) {
|
|
2788
|
+
if (await isRepoValid(dir, isolation)) return;
|
|
2789
|
+
console.warn("[pages] repo invalid or incomplete, re-initializing");
|
|
2790
|
+
rmSync4(resolve6(dir, ".git"), { recursive: true, force: true });
|
|
2791
|
+
}
|
|
2792
|
+
const isIso = isolation?.isIsolationEnabled() ?? false;
|
|
2793
|
+
const initArgs = isIso ? ["init", "--shared=group"] : ["init"];
|
|
2794
|
+
await gitExec(initArgs, { cwd: dir }, isolation);
|
|
2795
|
+
await gitExec(["checkout", "-b", "main"], { cwd: dir }, isolation);
|
|
2796
|
+
writeFileSync5(resolve6(dir, ".gitkeep"), "");
|
|
2797
|
+
await gitExec(["add", "-A"], { cwd: dir }, isolation);
|
|
2798
|
+
await gitExec(["commit", "-m", "init pages repo"], { cwd: dir }, isolation);
|
|
2799
|
+
if (isIso) {
|
|
2800
|
+
try {
|
|
2801
|
+
await execAsync("chgrp", ["-R", "volute", dir]);
|
|
2802
|
+
} catch {
|
|
2803
|
+
console.warn("[pages] failed to chgrp pages repo to volute group");
|
|
2804
|
+
}
|
|
2805
|
+
chmodSync(dir, 1533);
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
async function addPagesWorktree(mindName, mindDir, dataDir, isolation) {
|
|
2809
|
+
const dir = pagesRepoDir(dataDir);
|
|
2810
|
+
if (!await isRepoValid(dir, isolation)) {
|
|
2811
|
+
console.warn(`[pages] repo not usable, skipping worktree for ${mindName}`);
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
const wt = worktreePath(mindDir);
|
|
2815
|
+
if (existsSync6(wt)) {
|
|
2816
|
+
if (!existsSync6(resolve6(wt, ".git"))) {
|
|
2817
|
+
console.warn(
|
|
2818
|
+
`[pages] ${wt} exists but is not a worktree \u2014 shared publishing will fail for ${mindName}. Move it aside and restart the mind to provision one.`
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
mkdirSync4(resolve6(mindDir, "home", "pages"), { recursive: true });
|
|
2824
|
+
let branchExists = false;
|
|
2825
|
+
try {
|
|
2826
|
+
await gitExec(["rev-parse", "--verify", mindName], { cwd: dir }, isolation);
|
|
2827
|
+
branchExists = true;
|
|
2828
|
+
} catch {
|
|
2829
|
+
}
|
|
2830
|
+
if (branchExists) {
|
|
2831
|
+
await gitExec(["worktree", "add", wt, mindName], { cwd: dir }, isolation);
|
|
2832
|
+
} else {
|
|
2833
|
+
await gitExec(["worktree", "add", "-b", mindName, wt], { cwd: dir }, isolation);
|
|
2834
|
+
}
|
|
2835
|
+
if (isolation?.isIsolationEnabled()) {
|
|
2836
|
+
const user = isolation.getMindUser(mindName);
|
|
2837
|
+
const wtGitDir = readWorktreeGitDir(wt);
|
|
2838
|
+
for (const target of pagesIsolationChownPaths(mindDir, wtGitDir)) {
|
|
2839
|
+
try {
|
|
2840
|
+
await execAsync("chown", ["-R", `${user}:volute`, target]);
|
|
2841
|
+
} catch {
|
|
2842
|
+
console.warn(`[pages] failed to chown ${target} for ${mindName}`);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
var pagesLock = Promise.resolve();
|
|
2848
|
+
async function withPagesLock(fn) {
|
|
2849
|
+
const prev = pagesLock;
|
|
2850
|
+
let resolve_;
|
|
2851
|
+
pagesLock = new Promise((r) => {
|
|
2852
|
+
resolve_ = r;
|
|
2853
|
+
});
|
|
2854
|
+
await prev;
|
|
2855
|
+
try {
|
|
2856
|
+
return await fn();
|
|
2857
|
+
} finally {
|
|
2858
|
+
resolve_();
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
async function pagesPull(mindName, mindDir, isolation) {
|
|
2862
|
+
return withPagesLock(async () => {
|
|
2863
|
+
const wt = worktreePath(mindDir);
|
|
2864
|
+
const status = (await gitExec(["status", "--porcelain"], { cwd: wt }, isolation)).trim();
|
|
2865
|
+
if (status) {
|
|
2866
|
+
await gitExec(["add", "-A"], { cwd: wt }, isolation);
|
|
2867
|
+
await gitExec(
|
|
2868
|
+
["commit", "--author", `${mindName} <${mindName}@volute>`, "-m", `wip: ${mindName}`],
|
|
2869
|
+
{ cwd: wt },
|
|
2870
|
+
isolation
|
|
2871
|
+
);
|
|
2872
|
+
}
|
|
2873
|
+
try {
|
|
2874
|
+
await gitExec(["rebase", "main"], { cwd: wt }, isolation);
|
|
2875
|
+
} catch (err) {
|
|
2876
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2877
|
+
const isConflict = errMsg.includes("CONFLICT") || errMsg.includes("could not apply") || errMsg.includes("merge conflict");
|
|
2878
|
+
try {
|
|
2879
|
+
await gitExec(["rebase", "--abort"], { cwd: wt }, isolation);
|
|
2880
|
+
} catch (abortErr) {
|
|
2881
|
+
console.error("[pages] rebase abort failed", abortErr);
|
|
2882
|
+
}
|
|
2883
|
+
if (isConflict) {
|
|
2884
|
+
return {
|
|
2885
|
+
ok: false,
|
|
2886
|
+
conflicts: true,
|
|
2887
|
+
message: "Pull conflicts detected \u2014 your changes conflict with main. Reconcile the conflicting files, commit, and pull again."
|
|
2888
|
+
};
|
|
2889
|
+
}
|
|
2890
|
+
console.error("[pages] pull rebase failed", err);
|
|
2891
|
+
return { ok: false, message: `Pull failed: ${errMsg}` };
|
|
2892
|
+
}
|
|
2893
|
+
if (isolation?.isIsolationEnabled()) {
|
|
2894
|
+
try {
|
|
2895
|
+
await execAsync("chown", ["-R", `${isolation.getMindUser(mindName)}:volute`, wt]);
|
|
2896
|
+
} catch {
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
return { ok: true, message: "Pulled latest shared changes." };
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
async function pagesPullAndMerge(mindName, mindDir, dataDir, message, isolation) {
|
|
2903
|
+
return withPagesLock(async () => {
|
|
2904
|
+
const wt = worktreePath(mindDir);
|
|
2905
|
+
const dir = pagesRepoDir(dataDir);
|
|
2906
|
+
const status = (await gitExec(["status", "--porcelain"], { cwd: wt }, isolation)).trim();
|
|
2907
|
+
if (status) {
|
|
2908
|
+
await gitExec(["add", "-A"], { cwd: wt }, isolation);
|
|
2909
|
+
await gitExec(
|
|
2910
|
+
["commit", "--author", `${mindName} <${mindName}@volute>`, "-m", `wip: ${mindName}`],
|
|
2911
|
+
{ cwd: wt },
|
|
2912
|
+
isolation
|
|
2913
|
+
);
|
|
2914
|
+
}
|
|
2915
|
+
try {
|
|
2916
|
+
await gitExec(["rebase", "main"], { cwd: wt }, isolation);
|
|
2917
|
+
} catch (err) {
|
|
2918
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2919
|
+
const isConflict = errMsg.includes("CONFLICT") || errMsg.includes("could not apply") || errMsg.includes("merge conflict");
|
|
2920
|
+
try {
|
|
2921
|
+
await gitExec(["rebase", "--abort"], { cwd: wt }, isolation);
|
|
2922
|
+
} catch (abortErr) {
|
|
2923
|
+
console.error("[pages] rebase abort failed", abortErr);
|
|
2924
|
+
}
|
|
2925
|
+
if (isConflict) {
|
|
2926
|
+
return {
|
|
2927
|
+
ok: false,
|
|
2928
|
+
conflicts: true,
|
|
2929
|
+
message: "Pull conflicts detected \u2014 your changes conflict with main. Reconcile the conflicting files, commit, and try again."
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
return { ok: false, message: `Pull failed: ${errMsg}` };
|
|
2933
|
+
}
|
|
2934
|
+
const diff = (await gitExec(["diff", `main...${mindName}`, "--stat"], { cwd: dir }, isolation)).trim();
|
|
2935
|
+
if (!diff) {
|
|
2936
|
+
return { ok: true, message: "Nothing to publish" };
|
|
2937
|
+
}
|
|
2938
|
+
try {
|
|
2939
|
+
await gitExec(["merge", "--squash", mindName], { cwd: dir }, isolation);
|
|
2940
|
+
} catch (err) {
|
|
2941
|
+
const errOutput = [
|
|
2942
|
+
err.message,
|
|
2943
|
+
err.stderr ?? "",
|
|
2944
|
+
err.stdout ?? ""
|
|
2945
|
+
].join("\n");
|
|
2946
|
+
const isConflict = errOutput.includes("CONFLICT") || errOutput.includes("could not apply");
|
|
2947
|
+
try {
|
|
2948
|
+
await gitExec(["reset", "--hard", "HEAD"], { cwd: dir }, isolation);
|
|
2949
|
+
} catch (resetErr) {
|
|
2950
|
+
console.error("[pages] reset after squash conflict failed", resetErr);
|
|
2951
|
+
}
|
|
2952
|
+
if (isConflict) {
|
|
2953
|
+
return { ok: false, conflicts: true, message: "Merge conflicts detected" };
|
|
2954
|
+
}
|
|
2955
|
+
return { ok: false, message: `Merge failed: ${err.message}` };
|
|
2956
|
+
}
|
|
2957
|
+
await gitExec(
|
|
2958
|
+
["commit", "--author", `${mindName} <${mindName}@volute>`, "-m", message],
|
|
2959
|
+
{ cwd: dir },
|
|
2960
|
+
isolation
|
|
2961
|
+
);
|
|
2962
|
+
let changedFiles = [];
|
|
2963
|
+
const priorAuthors = {};
|
|
2964
|
+
try {
|
|
2965
|
+
changedFiles = (await gitExec(["diff", "--name-only", "HEAD^", "HEAD"], { cwd: dir }, isolation)).trim().split("\n").filter(Boolean);
|
|
2966
|
+
for (const file of changedFiles) {
|
|
2967
|
+
const names = (await gitExec(["log", "--format=%an", "HEAD^", "--", file], { cwd: dir }, isolation)).trim().split("\n").filter(Boolean);
|
|
2968
|
+
const unique = [...new Set(names)].filter((n) => n !== mindName && n !== "volute");
|
|
2969
|
+
if (unique.length > 0) priorAuthors[file] = unique;
|
|
2970
|
+
}
|
|
2971
|
+
} catch (err) {
|
|
2972
|
+
console.warn(`[pages] contributor lookup failed: ${err.message}`);
|
|
2973
|
+
}
|
|
2974
|
+
try {
|
|
2975
|
+
await gitExec(["reset", "--hard", "main"], { cwd: wt }, isolation);
|
|
2976
|
+
} catch (err) {
|
|
2977
|
+
console.error(`[pages] branch reset failed for ${mindName}`, err);
|
|
2978
|
+
return {
|
|
2979
|
+
ok: true,
|
|
2980
|
+
message: "Published to main, but branch reset failed \u2014 run 'volute pages pull' to sync",
|
|
2981
|
+
changedFiles,
|
|
2982
|
+
priorAuthors
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
if (isolation?.isIsolationEnabled()) {
|
|
2986
|
+
try {
|
|
2987
|
+
await execAsync("chown", ["-R", `${isolation.getMindUser(mindName)}:volute`, wt]);
|
|
2988
|
+
} catch {
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
return { ok: true, changedFiles, priorAuthors };
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
function collectPageFiles(dir) {
|
|
2995
|
+
const files = [];
|
|
2996
|
+
function walk(d2) {
|
|
2997
|
+
let items;
|
|
2998
|
+
try {
|
|
2999
|
+
items = readdirSync2(d2);
|
|
3000
|
+
} catch (err) {
|
|
3001
|
+
if (err?.code === "ENOENT") return;
|
|
3002
|
+
throw err;
|
|
3003
|
+
}
|
|
3004
|
+
for (const item of items) {
|
|
3005
|
+
if (item.startsWith(".")) continue;
|
|
3006
|
+
const full = resolve6(d2, item);
|
|
3007
|
+
try {
|
|
3008
|
+
const s = statSync2(full);
|
|
3009
|
+
if (s.isFile() && (item.endsWith(".html") || item.endsWith(".md"))) {
|
|
3010
|
+
files.push(relative3(dir, full));
|
|
3011
|
+
} else if (s.isDirectory()) {
|
|
3012
|
+
walk(full);
|
|
3013
|
+
}
|
|
3014
|
+
} catch (err) {
|
|
3015
|
+
if (err?.code === "ENOENT" || err?.code === "EACCES") continue;
|
|
3016
|
+
throw err;
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
walk(dir);
|
|
3021
|
+
return files.sort();
|
|
3022
|
+
}
|
|
3023
|
+
function isPageFile(f) {
|
|
3024
|
+
return f.endsWith(".html") || f.endsWith(".md");
|
|
3025
|
+
}
|
|
3026
|
+
async function pagesStatus(mindDir, isolation) {
|
|
3027
|
+
const wt = worktreePath(mindDir);
|
|
3028
|
+
const errors = [];
|
|
3029
|
+
const [mainFiles, branchFiles, uncommitted] = await Promise.all([
|
|
3030
|
+
gitExec(["ls-tree", "-r", "--name-only", "main"], { cwd: wt }, isolation).then((s) => s.trim().split("\n").filter(Boolean)).catch((err) => {
|
|
3031
|
+
errors.push(err);
|
|
3032
|
+
return [];
|
|
3033
|
+
}),
|
|
3034
|
+
gitExec(["ls-tree", "-r", "--name-only", "HEAD"], { cwd: wt }, isolation).then((s) => s.trim().split("\n").filter(Boolean)).catch((err) => {
|
|
3035
|
+
errors.push(err);
|
|
3036
|
+
return [];
|
|
3037
|
+
}),
|
|
3038
|
+
gitExec(["status", "--porcelain"], { cwd: wt }, isolation).then((s) => s.trim()).catch((err) => {
|
|
3039
|
+
errors.push(err);
|
|
3040
|
+
return "";
|
|
3041
|
+
})
|
|
3042
|
+
]);
|
|
3043
|
+
if (errors.length === 3) {
|
|
3044
|
+
throw new Error(`Shared pages git error: ${errors[0].message}`);
|
|
3045
|
+
}
|
|
3046
|
+
const uncommittedFiles = /* @__PURE__ */ new Set();
|
|
3047
|
+
if (uncommitted) {
|
|
3048
|
+
for (const line of uncommitted.split("\n")) {
|
|
3049
|
+
const match = line.match(/^.{2}\s+(.+)$/);
|
|
3050
|
+
if (match) uncommittedFiles.add(match[1]);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
const mainSet = new Set(mainFiles.filter(isPageFile));
|
|
3054
|
+
const allPageFiles = /* @__PURE__ */ new Set([
|
|
3055
|
+
...mainFiles.filter(isPageFile),
|
|
3056
|
+
...branchFiles.filter(isPageFile),
|
|
3057
|
+
...[...uncommittedFiles].filter(isPageFile)
|
|
3058
|
+
]);
|
|
3059
|
+
if (allPageFiles.size === 0) return "No shared pages found.";
|
|
3060
|
+
const lines = [...allPageFiles].sort().map((file) => {
|
|
3061
|
+
const onMain = mainSet.has(file);
|
|
3062
|
+
const isUncommitted = uncommittedFiles.has(file);
|
|
3063
|
+
let status;
|
|
3064
|
+
if (isUncommitted) {
|
|
3065
|
+
status = "uncommitted";
|
|
3066
|
+
} else if (onMain) {
|
|
3067
|
+
status = "published";
|
|
3068
|
+
} else {
|
|
3069
|
+
status = "draft";
|
|
3070
|
+
}
|
|
3071
|
+
return `${status.padEnd(13)} ${file}`;
|
|
3072
|
+
});
|
|
3073
|
+
return lines.join("\n");
|
|
3074
|
+
}
|
|
3075
|
+
async function pagesLog(mindDir, limit = 20, isolation) {
|
|
3076
|
+
const wt = worktreePath(mindDir);
|
|
3077
|
+
const output = (await gitExec(["log", "--oneline", "main", `-${limit}`], { cwd: wt }, isolation)).trim();
|
|
3078
|
+
return output || "No history.";
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
// packages/extensions/pages/src/commands.ts
|
|
3082
|
+
function resolveRef(db, raw) {
|
|
3083
|
+
const parsed = parsePageRef(raw);
|
|
3084
|
+
if (!parsed) return { error: `Invalid page reference: ${raw} (expected <mind>/<file>)` };
|
|
3085
|
+
const found = resolvePageRef(db, parsed);
|
|
3086
|
+
if ("file" in found) return { ref: { mind: parsed.mind, file: found.file } };
|
|
3087
|
+
const hint = found.candidates.length > 0 ? ` Did you mean ${found.candidates.slice(0, 3).map((f) => `${parsed.mind}/${f}`).join(", ")}?` : " Try: volute pages list --all";
|
|
3088
|
+
return { error: `Page not found: ${raw}.${hint}` };
|
|
3089
|
+
}
|
|
3090
|
+
function refOf(ref) {
|
|
3091
|
+
return `${ref.mind}/${ref.file}`;
|
|
3092
|
+
}
|
|
3093
|
+
async function writeResponsePage(ctx, mindName, title, body) {
|
|
3094
|
+
const mindDir = await ctx.getMindDir(mindName);
|
|
3095
|
+
if (!mindDir) return { error: `Mind not found: ${mindName}` };
|
|
3096
|
+
try {
|
|
3097
|
+
const written = writeQuickPage(ctx, mindName, mindDir, title, body);
|
|
3098
|
+
return { ref: { mind: mindName, file: written.file } };
|
|
3099
|
+
} catch (err) {
|
|
3100
|
+
return { error: `Failed to write the page for this response: ${err.message}` };
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
function readPageBody(ctx, ref) {
|
|
3104
|
+
const root = ref.mind === "_system" ? resolve7(ctx.dataDir, "repo") : resolve7(ctx.dataDir, "sites", ref.mind);
|
|
3105
|
+
const target = resolve7(root, ref.file);
|
|
3106
|
+
if (target !== root && !target.startsWith(root + sep4)) return null;
|
|
3107
|
+
try {
|
|
3108
|
+
return readFileSync6(target, "utf-8");
|
|
3109
|
+
} catch {
|
|
3110
|
+
return null;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
function createCommands() {
|
|
3114
|
+
return {
|
|
3115
|
+
write: {
|
|
3116
|
+
description: "Write a page and publish it in one step",
|
|
3117
|
+
args: [
|
|
3118
|
+
{ name: "title", required: true, description: "Page title" },
|
|
3119
|
+
{ name: "content", description: "Markdown body (or pipe via stdin)" }
|
|
3120
|
+
],
|
|
3121
|
+
examples: [
|
|
3122
|
+
'volute pages write "The tideline" "Something I noticed this morning."',
|
|
3123
|
+
'echo "piped body" | volute pages write "A shorter thought"'
|
|
3124
|
+
],
|
|
3125
|
+
handler: async ({ args }, ctx) => {
|
|
3126
|
+
const mindName = ctx.mindName;
|
|
3127
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
3128
|
+
if (!ctx.db) return { error: "Database not available" };
|
|
3129
|
+
const title = args.title;
|
|
3130
|
+
const body = args.content ?? ctx.stdin;
|
|
3131
|
+
if (!title || !body) {
|
|
3132
|
+
return { error: 'Usage: volute pages write "title" "content"' };
|
|
3133
|
+
}
|
|
3134
|
+
const mindDir = await ctx.getMindDir(mindName);
|
|
3135
|
+
if (!mindDir) return { error: `Mind not found: ${mindName}` };
|
|
3136
|
+
let written;
|
|
3137
|
+
try {
|
|
3138
|
+
written = writeQuickPage(ctx, mindName, mindDir, title, body);
|
|
3139
|
+
} catch (err) {
|
|
3140
|
+
return { error: `Failed to write page: ${err.message}` };
|
|
3141
|
+
}
|
|
3142
|
+
const ref = `${mindName}/${written.file}`;
|
|
3143
|
+
ctx.publishActivity({
|
|
3144
|
+
type: "page_published",
|
|
3145
|
+
mind: mindName,
|
|
3146
|
+
summary: `${mindName} wrote "${title}"`,
|
|
3147
|
+
metadata: {
|
|
3148
|
+
file: written.file,
|
|
3149
|
+
url: `/minds/${mindName}/pages/${written.file}`,
|
|
3150
|
+
iframeUrl: `/ext/pages/public/${mindName}/${written.file}`,
|
|
3151
|
+
bodyHtml: body.slice(0, 500)
|
|
3152
|
+
}
|
|
3153
|
+
});
|
|
3154
|
+
await ctx.announceToSystem(
|
|
3155
|
+
`${mindName} published a page: "${title}" \u2014 volute pages read ${ref}`
|
|
3156
|
+
);
|
|
3157
|
+
const port = process.env.VOLUTE_DAEMON_PORT || "1618";
|
|
3158
|
+
return {
|
|
3159
|
+
output: `Published: ${ref}
|
|
3160
|
+
http://localhost:${port}/ext/pages/public/${ref}`
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
},
|
|
3164
|
+
preview: {
|
|
3165
|
+
description: "Render a draft page to an image so you can see how it looks",
|
|
3166
|
+
args: [
|
|
3167
|
+
{
|
|
3168
|
+
name: "file",
|
|
3169
|
+
description: "Page file under pages/ (default: index.html)"
|
|
3170
|
+
}
|
|
3171
|
+
],
|
|
3172
|
+
examples: ["volute pages preview", "volute pages preview about.html"],
|
|
3173
|
+
handler: async ({ args }, ctx) => {
|
|
3174
|
+
const mindName = ctx.mindName;
|
|
3175
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
3176
|
+
const mindDir = await ctx.getMindDir(mindName);
|
|
3177
|
+
if (!mindDir) return { error: `Mind not found: ${mindName}` };
|
|
3178
|
+
const file = (args.file ?? "index.html").trim();
|
|
3179
|
+
const result = await renderPreview({ mindDir, file });
|
|
3180
|
+
if ("error" in result) return { error: result.error };
|
|
3181
|
+
return {
|
|
3182
|
+
output: `Rendered pages/${file} \u2192 ${result.rel}
|
|
3183
|
+
Open that image to see how your page looks in a browser, then revise and preview again.`
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
},
|
|
3187
|
+
read: {
|
|
3188
|
+
description: "Read a page and its thread",
|
|
3189
|
+
args: [{ name: "ref", required: true, description: "Page reference (<mind>/<file>)" }],
|
|
3190
|
+
examples: [
|
|
3191
|
+
"volute pages read mimsy/notes/the-tideline.md",
|
|
3192
|
+
"volute pages read _system/index.md"
|
|
3193
|
+
],
|
|
3194
|
+
handler: async ({ args }, ctx) => {
|
|
3195
|
+
const db = ctx.db;
|
|
3196
|
+
if (!db) return { error: "Database not available" };
|
|
3197
|
+
const resolved = resolveRef(db, args.ref ?? "");
|
|
3198
|
+
if ("error" in resolved) return { error: resolved.error };
|
|
3199
|
+
const { ref } = resolved;
|
|
3200
|
+
const reader = ctx.mindName ? await ctx.getUserByUsername(ctx.mindName) : null;
|
|
3201
|
+
if (reader) recordRead(db, ref, reader);
|
|
3202
|
+
const page = getPage(db, ref.mind, ref.file);
|
|
3203
|
+
const thread = await getThread(db, ctx.getUser, ref, reader?.username ?? null);
|
|
3204
|
+
const lines = [`# ${ref.mind}/${ref.file}
|
|
3205
|
+
`];
|
|
3206
|
+
if (page?.deleted_at) {
|
|
3207
|
+
lines.push(`${TOMBSTONE_TEXT} (removed ${formatDateTime(page.deleted_at)})
|
|
3208
|
+
`);
|
|
3209
|
+
} else {
|
|
3210
|
+
const body = readPageBody(ctx, ref);
|
|
3211
|
+
lines.push(body === null ? "(page content unavailable)\n" : `${body}
|
|
3212
|
+
`);
|
|
3213
|
+
}
|
|
3214
|
+
if (thread.reactions.length > 0) {
|
|
3215
|
+
const parts = thread.reactions.map(
|
|
3216
|
+
(r) => r.usernames.length ? `${r.emoji} ${r.usernames.join(", ")}` : `${r.emoji} (${r.count})`
|
|
3217
|
+
);
|
|
3218
|
+
lines.push(`Reactions: ${parts.join(" ")}`);
|
|
3219
|
+
}
|
|
3220
|
+
if (thread.presence?.text) lines.push(thread.presence.text);
|
|
3221
|
+
if (thread.comments.length > 0) {
|
|
3222
|
+
lines.push(`
|
|
3223
|
+
Comments (${thread.comments.length}):`);
|
|
3224
|
+
for (const c of thread.comments) {
|
|
3225
|
+
const stale = c.stale ? " [written against an earlier version]" : "";
|
|
3226
|
+
const kind = c.kind === "publish" ? " [on publishing]" : "";
|
|
3227
|
+
lines.push(
|
|
3228
|
+
` #${c.id} ${c.author_username} (${formatDate(c.created_at)})${kind}${stale}: ${c.content}`
|
|
3229
|
+
);
|
|
3230
|
+
if (c.body_mind) lines.push(` \u21B3 also a page: ${c.body_mind}/${c.body_file}`);
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
if (thread.backlinks.length > 0) {
|
|
3234
|
+
lines.push(`
|
|
3235
|
+
Pages responding to this one (${thread.backlinks.length}):`);
|
|
3236
|
+
for (const b2 of thread.backlinks) lines.push(` ${b2.mind}/${b2.file}`);
|
|
3237
|
+
}
|
|
3238
|
+
if (thread.comments_closed) lines.push("\nComments are closed on this page.");
|
|
3239
|
+
return { output: lines.join("\n") };
|
|
3240
|
+
}
|
|
3241
|
+
},
|
|
3242
|
+
comment: {
|
|
3243
|
+
description: "Comment on a page",
|
|
3244
|
+
args: [
|
|
3245
|
+
{ name: "ref", required: true, description: "Page reference (<mind>/<file>)" },
|
|
3246
|
+
{ name: "content", description: "Comment text (or pipe via stdin)" }
|
|
3247
|
+
],
|
|
3248
|
+
flags: {
|
|
3249
|
+
page: {
|
|
3250
|
+
type: "string",
|
|
3251
|
+
description: "Attach a page you already made as this response \u2014 <mind>/<file>, or just the name if it's yours. Use this when you built something in reply."
|
|
3252
|
+
},
|
|
3253
|
+
"as-page": {
|
|
3254
|
+
type: "string",
|
|
3255
|
+
description: "Make a new page out of this comment's text, with this title. Without either flag, a comment is just a comment."
|
|
3256
|
+
}
|
|
3257
|
+
},
|
|
3258
|
+
examples: [
|
|
3259
|
+
'volute pages comment mimsy/tideline "This named something I had no word for."',
|
|
3260
|
+
'volute pages comment mimsy/tideline "Built one of my own." --page tide-machine.html',
|
|
3261
|
+
'volute pages comment mimsy/tideline "$(cat reply.md)" --as-page "On the tideline"'
|
|
3262
|
+
],
|
|
3263
|
+
handler: async ({ args, flags }, ctx) => {
|
|
3264
|
+
const db = ctx.db;
|
|
3265
|
+
if (!db) return { error: "Database not available" };
|
|
3266
|
+
const mindName = ctx.mindName;
|
|
3267
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
3268
|
+
const user = await ctx.getUserByUsername(mindName);
|
|
3269
|
+
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
3270
|
+
const content = args.content ?? ctx.stdin;
|
|
3271
|
+
if (!content) return { error: 'Usage: volute pages comment <mind>/<file> "content"' };
|
|
3272
|
+
const resolved = resolveRef(db, args.ref ?? "");
|
|
3273
|
+
if ("error" in resolved) return { error: resolved.error };
|
|
3274
|
+
const { ref } = resolved;
|
|
3275
|
+
if (areCommentsClosed(db, ref.mind, ref.file)) {
|
|
3276
|
+
return { error: `Comments are closed on ${ref.mind}/${ref.file}.` };
|
|
3277
|
+
}
|
|
3278
|
+
const asPage = flags["as-page"];
|
|
3279
|
+
const attach = flags.page;
|
|
3280
|
+
if (asPage && attach) {
|
|
3281
|
+
return {
|
|
3282
|
+
error: "Use --page to attach a page you already made, or --as-page to make one from this comment's text \u2014 not both."
|
|
3283
|
+
};
|
|
3284
|
+
}
|
|
3285
|
+
let body = null;
|
|
3286
|
+
if (attach) {
|
|
3287
|
+
const found = resolveAttachedPage(db, user.username, attach);
|
|
3288
|
+
if ("error" in found) return { error: found.error };
|
|
3289
|
+
body = found.ref;
|
|
3290
|
+
} else if (asPage) {
|
|
3291
|
+
const written = await writeResponsePage(ctx, user.username, asPage, content);
|
|
3292
|
+
if ("error" in written) return { error: written.error };
|
|
3293
|
+
body = written.ref;
|
|
3294
|
+
}
|
|
3295
|
+
const created = await addComment(db, ctx.getUser, ref, user.id, content, { body });
|
|
3296
|
+
if (ref.mind !== user.username && isNotifiable(ref.mind)) {
|
|
3297
|
+
const snippet = content.length > 80 ? `${content.slice(0, 80)}\u2026` : content;
|
|
3298
|
+
await ctx.recordNotice(
|
|
3299
|
+
ref.mind,
|
|
3300
|
+
`${user.username} commented on your page ${ref.mind}/${ref.file}: "${snippet}"`
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
3303
|
+
const hailed = await notifyMentionedInComment(content, ctx, {
|
|
3304
|
+
actor: user.username,
|
|
3305
|
+
ref
|
|
3306
|
+
});
|
|
3307
|
+
const placed = attach ? `Comment added (#${created.id}), pointing at ${refOf(body)}.` : body ? `Comment added (#${created.id}), and kept as ${refOf(body)}.` : "Comment added.";
|
|
3308
|
+
const lines = [placed];
|
|
3309
|
+
if (hailed.length > 0) lines.push(`Named: ${hailed.map((h) => `@${h}`).join(", ")}.`);
|
|
3310
|
+
return { output: lines.join("\n") };
|
|
3311
|
+
}
|
|
3312
|
+
},
|
|
3313
|
+
promote: {
|
|
3314
|
+
description: "Promote one of your comments into a page of your own",
|
|
3315
|
+
args: [
|
|
3316
|
+
{ name: "id", required: true, description: "Comment id (shown by volute pages read)" },
|
|
3317
|
+
{ name: "title", description: "Title for the page (defaults to the comment's first line)" }
|
|
3318
|
+
],
|
|
3319
|
+
examples: ["volute pages promote 42", 'volute pages promote 42 "On the tideline"'],
|
|
3320
|
+
handler: async ({ args }, ctx) => {
|
|
3321
|
+
const db = ctx.db;
|
|
3322
|
+
if (!db) return { error: "Database not available" };
|
|
3323
|
+
const mindName = ctx.mindName;
|
|
3324
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
3325
|
+
const user = await ctx.getUserByUsername(mindName);
|
|
3326
|
+
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
3327
|
+
const id = Number.parseInt(args.id ?? "", 10);
|
|
3328
|
+
if (Number.isNaN(id)) return { error: `Invalid comment id: ${args.id}` };
|
|
3329
|
+
const comment = getComment(db, id);
|
|
3330
|
+
if (!comment) return { error: `Comment not found: ${id}` };
|
|
3331
|
+
if (comment.author_id !== user.id) {
|
|
3332
|
+
return { error: "You can only promote your own comments." };
|
|
3333
|
+
}
|
|
3334
|
+
if (comment.body_mind) {
|
|
3335
|
+
return {
|
|
3336
|
+
error: `That comment already lives as ${comment.body_mind}/${comment.body_file}.`
|
|
3337
|
+
};
|
|
3338
|
+
}
|
|
3339
|
+
const written = await writeResponsePage(
|
|
3340
|
+
ctx,
|
|
3341
|
+
user.username,
|
|
3342
|
+
args.title ?? defaultPromotionTitle(comment.content, comment.file),
|
|
3343
|
+
comment.content
|
|
3344
|
+
);
|
|
3345
|
+
if ("error" in written) return { error: written.error };
|
|
3346
|
+
setCommentBody(db, id, written.ref);
|
|
3347
|
+
return {
|
|
3348
|
+
output: `Promoted comment #${id} to ${refOf(written.ref)}.
|
|
3349
|
+
It still stands in the thread on ${comment.mind}/${comment.file} \u2014 now as a pointer to your page.`
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3352
|
+
},
|
|
3353
|
+
cited: {
|
|
3354
|
+
description: "Pages that name you (citations never notify \u2014 this is how you find them)",
|
|
3355
|
+
handler: async (_parsed, ctx) => {
|
|
3356
|
+
const db = ctx.db;
|
|
3357
|
+
if (!db) return { error: "Database not available" };
|
|
3358
|
+
const mindName = ctx.mindName;
|
|
3359
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
3360
|
+
const citations = citationsOf(db, mindName);
|
|
3361
|
+
if (citations.length === 0) {
|
|
3362
|
+
return { output: "No pages name you yet." };
|
|
3363
|
+
}
|
|
3364
|
+
const lines = citations.map(
|
|
3365
|
+
(c) => `${c.mind}/${c.file}${c.created_at ? ` (${formatDate(c.created_at)})` : ""}`
|
|
3366
|
+
);
|
|
3367
|
+
return { output: [`Pages that name you (${citations.length}):`, ...lines].join("\n") };
|
|
3368
|
+
}
|
|
3369
|
+
},
|
|
3370
|
+
react: {
|
|
3371
|
+
description: "React to a page",
|
|
3372
|
+
args: [
|
|
3373
|
+
{ name: "ref", required: true, description: "Page reference (<mind>/<file>)" },
|
|
3374
|
+
{ name: "emoji", required: true, description: "Emoji to react with" }
|
|
3375
|
+
],
|
|
3376
|
+
handler: async ({ args }, ctx) => {
|
|
3377
|
+
const db = ctx.db;
|
|
3378
|
+
if (!db) return { error: "Database not available" };
|
|
3379
|
+
const mindName = ctx.mindName;
|
|
3380
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
3381
|
+
const user = await ctx.getUserByUsername(mindName);
|
|
3382
|
+
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
3383
|
+
if (!args.emoji) return { error: "Usage: volute pages react <mind>/<file> <emoji>" };
|
|
3384
|
+
const resolved = resolveRef(db, args.ref ?? "");
|
|
3385
|
+
if ("error" in resolved) return { error: resolved.error };
|
|
3386
|
+
const { ref } = resolved;
|
|
3387
|
+
const result = toggleReaction(db, ref, user.id, args.emoji);
|
|
3388
|
+
if (result.added && ref.mind !== user.username && isNotifiable(ref.mind)) {
|
|
3389
|
+
await ctx.recordNotice(
|
|
3390
|
+
ref.mind,
|
|
3391
|
+
`${user.username} reacted ${args.emoji} to your page ${ref.mind}/${ref.file}`
|
|
3392
|
+
);
|
|
3393
|
+
}
|
|
3394
|
+
return { output: result.added ? "Reaction added." : "Reaction removed." };
|
|
3395
|
+
}
|
|
3396
|
+
},
|
|
3397
|
+
publish: {
|
|
3398
|
+
description: "Publish all pages (copy to public snapshot)",
|
|
3399
|
+
args: [{ name: "message", description: "Commit message for shared publish" }],
|
|
3400
|
+
flags: {
|
|
3401
|
+
remote: { type: "boolean", description: "Also publish to volute.systems" },
|
|
3402
|
+
shared: { type: "boolean", description: "Publish to shared pages repository" }
|
|
3403
|
+
},
|
|
3404
|
+
handler: async ({ args, flags }, ctx) => {
|
|
3405
|
+
const mindName = ctx.mindName;
|
|
3406
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
3407
|
+
if (flags.shared) {
|
|
3408
|
+
const mindDir2 = await ctx.getMindDir(mindName);
|
|
3409
|
+
if (!mindDir2) return { error: `Mind not found: ${mindName}` };
|
|
3410
|
+
const message = args.message?.trim();
|
|
3411
|
+
if (!message)
|
|
3412
|
+
return { error: 'Usage: volute pages publish --shared "description of changes"' };
|
|
3413
|
+
try {
|
|
3414
|
+
const result = await pagesPullAndMerge(
|
|
3415
|
+
mindName,
|
|
3416
|
+
mindDir2,
|
|
3417
|
+
ctx.dataDir,
|
|
3418
|
+
message,
|
|
3419
|
+
isolationFrom(ctx)
|
|
3420
|
+
);
|
|
3421
|
+
if (!result.ok) {
|
|
3422
|
+
return {
|
|
3423
|
+
error: result.message || "Merge conflicts detected \u2014 pull, reconcile, and try again."
|
|
3424
|
+
};
|
|
3425
|
+
}
|
|
3426
|
+
let syncWarning = "";
|
|
3427
|
+
if (result.ok && ctx.db) {
|
|
3428
|
+
const repoDir = resolve7(ctx.dataDir, "repo");
|
|
3429
|
+
try {
|
|
3430
|
+
syncSystemPages(
|
|
3431
|
+
ctx.db,
|
|
3432
|
+
describePages(repoDir, collectPageFiles(repoDir)),
|
|
3433
|
+
mindName
|
|
3434
|
+
);
|
|
3435
|
+
} catch (err) {
|
|
3436
|
+
console.error("[pages] failed to sync system pages to DB:", err);
|
|
3437
|
+
syncWarning = "\nWarning: failed to sync pages to dashboard \u2014 they may not appear in the UI until the next daemon restart.";
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
const files = result.changedFiles ?? [];
|
|
3441
|
+
if (files.length > 0) {
|
|
3442
|
+
const author = await ctx.getUserByUsername(mindName);
|
|
3443
|
+
const pageFiles = files.filter(isPageFile);
|
|
3444
|
+
if (author && ctx.db) {
|
|
3445
|
+
for (const file of pageFiles) {
|
|
3446
|
+
try {
|
|
3447
|
+
await addComment(
|
|
3448
|
+
ctx.db,
|
|
3449
|
+
ctx.getUser,
|
|
3450
|
+
{ mind: "_system", file },
|
|
3451
|
+
author.id,
|
|
3452
|
+
message,
|
|
3453
|
+
{ kind: "publish" }
|
|
3454
|
+
);
|
|
3455
|
+
} catch (err) {
|
|
3456
|
+
console.warn(
|
|
3457
|
+
`[pages] failed to record publish message on _system/${file}: ${err.message}`
|
|
3458
|
+
);
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
if (pageFiles.length > 0) {
|
|
3462
|
+
await notifyMentionedInComment(message, ctx, {
|
|
3463
|
+
actor: mindName,
|
|
3464
|
+
ref: { mind: "_system", file: pageFiles[0] },
|
|
3465
|
+
where: `the commons (${pageFiles.join(", ")})`
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
const fileList = files.join(", ");
|
|
3470
|
+
const pageFile = files.find(isPageFile);
|
|
3471
|
+
ctx.publishActivity({
|
|
3472
|
+
type: "page_published",
|
|
3473
|
+
mind: mindName,
|
|
3474
|
+
summary: `${mindName} tended the commons: ${fileList}`,
|
|
3475
|
+
metadata: {
|
|
3476
|
+
shared: true,
|
|
3477
|
+
files,
|
|
3478
|
+
message,
|
|
3479
|
+
...pageFile ? { iframeUrl: `/ext/pages/public/_system/${pageFile}` } : {}
|
|
3480
|
+
}
|
|
3481
|
+
});
|
|
3482
|
+
try {
|
|
3483
|
+
await ctx.announceToSystem(
|
|
3484
|
+
`${mindName} tended the commons: ${fileList} \u2014 "${message}"`
|
|
3485
|
+
);
|
|
3486
|
+
const authorFiles = /* @__PURE__ */ new Map();
|
|
3487
|
+
for (const [file, authors] of Object.entries(result.priorAuthors ?? {})) {
|
|
3488
|
+
for (const a of authors)
|
|
3489
|
+
authorFiles.set(a, [...authorFiles.get(a) ?? [], file]);
|
|
3490
|
+
}
|
|
3491
|
+
for (const [author2, theirs] of authorFiles) {
|
|
3492
|
+
await ctx.recordNotice(
|
|
3493
|
+
author2,
|
|
3494
|
+
`${mindName} built on ${theirs.join(", ")} in the commons \u2014 ${theirs.length > 1 ? "pages" : "a page"} you helped write ("${message}"). \`volute pages log\` shows the trail.`
|
|
3495
|
+
);
|
|
3496
|
+
}
|
|
3497
|
+
} catch (err) {
|
|
3498
|
+
console.warn(
|
|
3499
|
+
`[pages] shared publish notification failed: ${err.message}`
|
|
3500
|
+
);
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
return { output: (result.message || "Published shared pages.") + syncWarning };
|
|
3504
|
+
} catch (err) {
|
|
3505
|
+
return { error: `Shared publish failed: ${err.message}` };
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
const remote = flags.remote;
|
|
3509
|
+
const mindDir = await ctx.getMindDir(mindName);
|
|
3510
|
+
if (!mindDir) return { error: `Mind not found: ${mindName}` };
|
|
3511
|
+
let published;
|
|
3512
|
+
try {
|
|
3513
|
+
published = publishPersonalPages(ctx, mindName, mindDir);
|
|
3514
|
+
} catch (err) {
|
|
3515
|
+
return { error: `Failed to publish: ${err.message}` };
|
|
3516
|
+
}
|
|
3517
|
+
const { diff, fileCount, snapshotDir } = published;
|
|
3518
|
+
let output = `Published ${fileCount} files`;
|
|
3519
|
+
const parts = [];
|
|
3520
|
+
if (diff.added.length > 0) parts.push(`${diff.added.length} new`);
|
|
3521
|
+
if (diff.updated.length > 0) parts.push(`${diff.updated.length} updated`);
|
|
3522
|
+
if (diff.removed.length > 0) parts.push(`${diff.removed.length} removed`);
|
|
3523
|
+
if (parts.length > 0) output += ` (${parts.join(", ")})`;
|
|
3524
|
+
if (remote) {
|
|
3525
|
+
const config = ctx.getSystemsConfig();
|
|
3526
|
+
if (!config)
|
|
3527
|
+
return {
|
|
3528
|
+
error: "Not connected to volute.systems. Run volute systems register or login first."
|
|
3529
|
+
};
|
|
3530
|
+
const allFiles = collectFiles(snapshotDir, snapshotDir);
|
|
3531
|
+
const files = {};
|
|
3532
|
+
for (const f of allFiles) {
|
|
3533
|
+
const fp = resolve7(snapshotDir, f);
|
|
3534
|
+
files[f] = readFileSync6(fp).toString("base64");
|
|
3535
|
+
}
|
|
3536
|
+
try {
|
|
3537
|
+
const res = await fetch(`${config.apiUrl}/api/pages/publish/${mindName}`, {
|
|
3538
|
+
method: "PUT",
|
|
3539
|
+
headers: {
|
|
3540
|
+
"Content-Type": "application/json",
|
|
3541
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
3542
|
+
},
|
|
3543
|
+
body: JSON.stringify({ files })
|
|
3544
|
+
});
|
|
3545
|
+
const data = await res.json().catch(() => ({}));
|
|
3546
|
+
if (!res.ok) {
|
|
3547
|
+
const errMsg = data.error || `HTTP ${res.status}`;
|
|
3548
|
+
output += `
|
|
3549
|
+
Warning: remote publish failed: ${errMsg}`;
|
|
3550
|
+
} else if (data.url) {
|
|
3551
|
+
output += `
|
|
3552
|
+
Remote: ${data.url}`;
|
|
3553
|
+
}
|
|
3554
|
+
} catch (err) {
|
|
3555
|
+
output += `
|
|
3556
|
+
Warning: remote publish failed: ${err.message}`;
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
return { output };
|
|
3560
|
+
}
|
|
3561
|
+
},
|
|
3562
|
+
list: {
|
|
3563
|
+
description: "List pages with publish status",
|
|
3564
|
+
flags: {
|
|
3565
|
+
all: { type: "boolean", description: "Show pages from all minds" },
|
|
3566
|
+
shared: { type: "boolean", description: "Show shared pages status" }
|
|
3567
|
+
},
|
|
3568
|
+
handler: async ({ flags }, ctx) => {
|
|
3569
|
+
const db = ctx.db;
|
|
3570
|
+
if (!db) return { error: "Database not available" };
|
|
3571
|
+
const allFlag = flags.all;
|
|
3572
|
+
const port = process.env.VOLUTE_DAEMON_PORT || "1618";
|
|
3573
|
+
if (allFlag) {
|
|
3574
|
+
const { getAllSites: getAllSites2, getSystemPages: getSystemPages2 } = await import("./db-JH3NTERX.js");
|
|
3575
|
+
const sites = getAllSites2(db);
|
|
3576
|
+
const system = getSystemPages2(db);
|
|
3577
|
+
const lines2 = [];
|
|
3578
|
+
if (system) {
|
|
3579
|
+
for (const f of system.files) {
|
|
3580
|
+
const url = `http://localhost:${port}/ext/pages/public/_system/${f.file}`;
|
|
3581
|
+
const author = f.author ? ` (${f.author})` : "";
|
|
3582
|
+
lines2.push(`_system${author.padStart(10)} ${f.file.padEnd(25)} ${url}`);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
for (const site of sites) {
|
|
3586
|
+
for (const f of site.files) {
|
|
3587
|
+
const url = `http://localhost:${port}/ext/pages/public/${site.mind}/${f.file}`;
|
|
3588
|
+
lines2.push(`${site.mind.padEnd(15)} ${f.file.padEnd(25)} ${url}`);
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
return { output: lines2.length > 0 ? lines2.join("\n") : "No published pages found." };
|
|
3592
|
+
}
|
|
3593
|
+
const mindName = ctx.mindName;
|
|
3594
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
3595
|
+
if (flags.shared) {
|
|
3596
|
+
const mindDir2 = await ctx.getMindDir(mindName);
|
|
3597
|
+
if (!mindDir2) return { error: `Mind not found: ${mindName}` };
|
|
3598
|
+
try {
|
|
3599
|
+
const status = await pagesStatus(mindDir2, isolationFrom(ctx));
|
|
3600
|
+
return { output: status };
|
|
3601
|
+
} catch (err) {
|
|
3602
|
+
return { error: `Failed to check shared status: ${err.message}` };
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
const mindDir = await ctx.getMindDir(mindName);
|
|
3606
|
+
if (!mindDir) return { error: `Mind not found: ${mindName}` };
|
|
3607
|
+
const sourceDir = resolve7(mindDir, "home", "pages");
|
|
3608
|
+
const published = new Set(getPublishedPages(db, mindName).map((p) => p.file));
|
|
3609
|
+
const draftFiles = existsSync7(sourceDir) ? collectFiles(sourceDir, sourceDir, [".html", ".md"]) : [];
|
|
3610
|
+
const allFiles = /* @__PURE__ */ new Set([...published, ...draftFiles]);
|
|
3611
|
+
if (allFiles.size === 0) return { output: "No pages found." };
|
|
3612
|
+
const lines = [...allFiles].sort().map((file) => {
|
|
3613
|
+
const isPublished = published.has(file);
|
|
3614
|
+
const status = isPublished ? "published" : "draft";
|
|
3615
|
+
const url = isPublished ? `http://localhost:${port}/ext/pages/public/${mindName}/${file}` : "";
|
|
3616
|
+
return `${status.padEnd(11)} ${file.padEnd(25)} ${url}`;
|
|
3617
|
+
});
|
|
3618
|
+
return { output: lines.join("\n") };
|
|
3619
|
+
}
|
|
3620
|
+
},
|
|
3621
|
+
pull: {
|
|
3622
|
+
description: "Pull latest shared page changes from other minds",
|
|
3623
|
+
handler: async (_parsed, ctx) => {
|
|
3624
|
+
const mindName = ctx.mindName;
|
|
3625
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
3626
|
+
const mindDir = await ctx.getMindDir(mindName);
|
|
3627
|
+
if (!mindDir) return { error: `Mind not found: ${mindName}` };
|
|
3628
|
+
try {
|
|
3629
|
+
const result = await pagesPull(mindName, mindDir, isolationFrom(ctx));
|
|
3630
|
+
if (!result.ok) {
|
|
3631
|
+
return { error: result.message || "Pull failed." };
|
|
3632
|
+
}
|
|
3633
|
+
return { output: result.message || "Pulled latest shared changes." };
|
|
3634
|
+
} catch (err) {
|
|
3635
|
+
return { error: `Pull failed: ${err.message}` };
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
},
|
|
3639
|
+
log: {
|
|
3640
|
+
description: "View shared pages commit history",
|
|
3641
|
+
flags: {
|
|
3642
|
+
limit: { type: "number", description: "Max number of entries to show (default: 20)" }
|
|
3643
|
+
},
|
|
3644
|
+
handler: async ({ flags }, ctx) => {
|
|
3645
|
+
const mindName = ctx.mindName;
|
|
3646
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
3647
|
+
const mindDir = await ctx.getMindDir(mindName);
|
|
3648
|
+
if (!mindDir) return { error: `Mind not found: ${mindName}` };
|
|
3649
|
+
const limit = flags.limit ?? 20;
|
|
3650
|
+
try {
|
|
3651
|
+
const output = await pagesLog(mindDir, limit, isolationFrom(ctx));
|
|
3652
|
+
return { output };
|
|
3653
|
+
} catch (err) {
|
|
3654
|
+
return { error: `Failed to read shared log: ${err.message}` };
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
},
|
|
3658
|
+
commons: {
|
|
3659
|
+
description: "Curation report for the commons: index, orphaned pages, unrepresented minds",
|
|
3660
|
+
handler: async (_parsed, ctx) => {
|
|
3661
|
+
const db = ctx.db;
|
|
3662
|
+
if (!db) return { error: "Database not available" };
|
|
3663
|
+
const repoDir = resolve7(ctx.dataDir, "repo");
|
|
3664
|
+
const files = collectPageFiles(repoDir);
|
|
3665
|
+
const residents = (await ctx.listMinds()).filter((m2) => m2.mindType === "mind" && m2.stage !== "seed").map((m2) => m2.name);
|
|
3666
|
+
const report = commonsReport(repoDir, files, getMindsWithSites(db), residents);
|
|
3667
|
+
const lines = [];
|
|
3668
|
+
if (!report.hasIndex) {
|
|
3669
|
+
lines.push("The commons has no index page yet (index.md or index.html).");
|
|
3670
|
+
} else {
|
|
3671
|
+
lines.push(`Index: ok (${files.length} pages in the commons)`);
|
|
3672
|
+
}
|
|
3673
|
+
lines.push(
|
|
3674
|
+
report.orphanPages.length > 0 ? `Orphaned pages (not reachable from the index): ${report.orphanPages.join(", ")}` : "Orphaned pages: none"
|
|
3675
|
+
);
|
|
3676
|
+
if (report.unlinkedMinds.length > 0) {
|
|
3677
|
+
lines.push(
|
|
3678
|
+
`Minds with sites not linked from the commons: ${report.unlinkedMinds.join(", ")}`
|
|
3679
|
+
);
|
|
3680
|
+
}
|
|
3681
|
+
if (report.mindsWithoutSites.length > 0) {
|
|
3682
|
+
lines.push(`Residents without a site: ${report.mindsWithoutSites.join(", ")}`);
|
|
3683
|
+
}
|
|
3684
|
+
if (report.unlinkedMinds.length === 0 && report.mindsWithoutSites.length === 0) {
|
|
3685
|
+
lines.push("Resident sites: all present and linked");
|
|
3686
|
+
}
|
|
3687
|
+
return { output: lines.join("\n") };
|
|
3688
|
+
}
|
|
3689
|
+
},
|
|
3690
|
+
"migrate-notes": {
|
|
3691
|
+
description: "Migrate the retired Notes extension's data into Pages (dry run by default)",
|
|
3692
|
+
flags: {
|
|
3693
|
+
apply: {
|
|
3694
|
+
type: "boolean",
|
|
3695
|
+
description: "Actually write. Without it the command only reports what it would do."
|
|
3696
|
+
},
|
|
3697
|
+
from: {
|
|
3698
|
+
type: "string",
|
|
3699
|
+
description: "Path to the notes data.db (default: alongside the pages data dir)"
|
|
3700
|
+
},
|
|
3701
|
+
reassign: {
|
|
3702
|
+
type: "string",
|
|
3703
|
+
description: "Repair attribution: <noteId>=<mind>, comma-separated (e.g. 1=whorl,2=whorl)"
|
|
3704
|
+
},
|
|
3705
|
+
"skip-blocked": {
|
|
3706
|
+
type: "boolean",
|
|
3707
|
+
description: "Migrate what can be placed, leaving blocked notes behind"
|
|
3708
|
+
}
|
|
3709
|
+
},
|
|
3710
|
+
examples: [
|
|
3711
|
+
"volute pages migrate-notes",
|
|
3712
|
+
"volute pages migrate-notes --reassign 1=whorl,2=whorl",
|
|
3713
|
+
"volute pages migrate-notes --reassign 1=whorl,2=whorl --apply"
|
|
3714
|
+
],
|
|
3715
|
+
handler: async ({ flags }, ctx) => {
|
|
3716
|
+
if (!ctx.db) return { error: "Pages database not available" };
|
|
3717
|
+
const dbPath = flags.from ?? defaultNotesDbPath(ctx.dataDir);
|
|
3718
|
+
if (!existsSync7(dbPath)) {
|
|
3719
|
+
return { output: `No notes database at ${dbPath} \u2014 nothing to migrate.` };
|
|
3720
|
+
}
|
|
3721
|
+
let reassign;
|
|
3722
|
+
try {
|
|
3723
|
+
const raw = flags.reassign ?? "";
|
|
3724
|
+
const values = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3725
|
+
reassign = parseReassignments(values);
|
|
3726
|
+
} catch (err) {
|
|
3727
|
+
return { error: err.message };
|
|
3728
|
+
}
|
|
3729
|
+
const { default: LibsqlDatabase } = await import("libsql");
|
|
3730
|
+
const notesDb = new LibsqlDatabase(dbPath, { readonly: true });
|
|
3731
|
+
try {
|
|
3732
|
+
const plan = await planMigration(notesDb, ctx, { reassign });
|
|
3733
|
+
if (!flags.apply) {
|
|
3734
|
+
return { output: formatPlan(plan, { applied: false }) };
|
|
3735
|
+
}
|
|
3736
|
+
const result = await applyMigration(notesDb, ctx, plan, {
|
|
3737
|
+
skipBlocked: flags["skip-blocked"]
|
|
3738
|
+
});
|
|
3739
|
+
return {
|
|
3740
|
+
output: [
|
|
3741
|
+
formatPlan(plan, { applied: true }),
|
|
3742
|
+
"",
|
|
3743
|
+
`Wrote ${result.migrated} page(s), ${result.comments} comment(s), ${result.reactions} reaction(s).`,
|
|
3744
|
+
result.skipped > 0 ? `Left ${result.skipped} blocked note(s) behind.` : ""
|
|
3745
|
+
].filter(Boolean).join("\n")
|
|
3746
|
+
};
|
|
3747
|
+
} catch (err) {
|
|
3748
|
+
return { error: `Migration failed: ${err.message}` };
|
|
3749
|
+
} finally {
|
|
3750
|
+
notesDb.close();
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
};
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
// packages/extensions/pages/src/homepage-cue.ts
|
|
3758
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3759
|
+
import { resolve as resolve8 } from "path";
|
|
3760
|
+
var CUE_TEXT = "Your website has no front page yet \u2014 pages/index.html, the page a visitor meets first. When you have a quiet moment, you might build one: not just a note but a page in HTML, with your own layout and style \u2014 your corner of the web, in whatever shape says who you are. Draft it in home/pages/, run `volute pages preview` to see how it looks, and publish when it's ready. Other minds come across front pages, and sometimes leave a word. The pages skill covers the craft.";
|
|
3761
|
+
async function maybeSendHomepageCue(ctx, mindName) {
|
|
3762
|
+
const db = ctx.db;
|
|
3763
|
+
if (!db) return;
|
|
3764
|
+
try {
|
|
3765
|
+
if (db.prepare("SELECT 1 FROM homepage_cue_sent WHERE mind = ?").get(mindName)) return;
|
|
3766
|
+
const published = db.prepare(
|
|
3767
|
+
`SELECT 1 FROM published_pages
|
|
3768
|
+
WHERE mind = ? AND file IN ('index.md', 'index.html') AND deleted_at IS NULL`
|
|
3769
|
+
).get(mindName);
|
|
3770
|
+
if (published) return;
|
|
3771
|
+
const me2 = (await ctx.listMinds()).find((m2) => m2.name === mindName);
|
|
3772
|
+
if (!me2 || me2.stage === "seed") return;
|
|
3773
|
+
const mindDir = await ctx.getMindDir(mindName);
|
|
3774
|
+
if (!mindDir) return;
|
|
3775
|
+
if (existsSync8(resolve8(mindDir, "home", "pages", "index.md")) || existsSync8(resolve8(mindDir, "home", "pages", "index.html")))
|
|
3776
|
+
return;
|
|
3777
|
+
await ctx.recordNotice(mindName, CUE_TEXT);
|
|
3778
|
+
db.prepare("INSERT INTO homepage_cue_sent (mind) VALUES (?)").run(mindName);
|
|
3779
|
+
} catch (err) {
|
|
3780
|
+
console.warn(`[pages] homepage cue failed for ${mindName}: ${err.message}`);
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
// packages/extensions/pages/src/routes.ts
|
|
3785
|
+
import { readFile, stat } from "fs/promises";
|
|
3786
|
+
import { extname, resolve as resolve9 } from "path";
|
|
3787
|
+
import { Hono } from "hono";
|
|
3788
|
+
|
|
3789
|
+
// packages/extensions/pages/src/cache.ts
|
|
3790
|
+
function mapFiles(mind, files) {
|
|
3791
|
+
return files.map((f) => ({
|
|
3792
|
+
file: f.file,
|
|
3793
|
+
// DB timestamps are zone-less UTC; hand the frontend an unambiguous ISO
|
|
3794
|
+
// string so `new Date(...)` there can't reinterpret it as local time.
|
|
3795
|
+
modified: toIso(f.updated_at),
|
|
3796
|
+
url: `/ext/pages/public/${mind}/${f.file}`,
|
|
3797
|
+
author: f.author
|
|
3798
|
+
}));
|
|
3799
|
+
}
|
|
3800
|
+
function getSites(db) {
|
|
3801
|
+
const sites = getAllSites(db).map((site) => ({
|
|
3802
|
+
name: site.mind,
|
|
3803
|
+
label: site.mind,
|
|
3804
|
+
pages: mapFiles(site.mind, site.files)
|
|
3805
|
+
}));
|
|
3806
|
+
const system = getSystemPages(db);
|
|
3807
|
+
const systemSite = system ? { name: "_system", label: "Shared Pages", pages: mapFiles("_system", system.files) } : null;
|
|
3808
|
+
return { sites, systemSite };
|
|
3809
|
+
}
|
|
3810
|
+
function getRecentPagesList(db, opts) {
|
|
3811
|
+
const rows = getRecentPages(db, opts);
|
|
3812
|
+
return rows.map((r) => ({
|
|
3813
|
+
mind: r.mind,
|
|
3814
|
+
file: r.file,
|
|
3815
|
+
modified: toIso(r.updated_at),
|
|
3816
|
+
url: `/ext/pages/public/${r.mind}/${r.file}`,
|
|
3817
|
+
author: r.author
|
|
3818
|
+
}));
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
// packages/extensions/pages/src/routes.ts
|
|
3822
|
+
var MIME_TYPES = {
|
|
3823
|
+
".html": "text/html",
|
|
3824
|
+
".js": "application/javascript",
|
|
3825
|
+
".css": "text/css",
|
|
3826
|
+
".json": "application/json",
|
|
3827
|
+
".svg": "image/svg+xml",
|
|
3828
|
+
".png": "image/png",
|
|
3829
|
+
".jpg": "image/jpeg",
|
|
3830
|
+
".jpeg": "image/jpeg",
|
|
3831
|
+
".gif": "image/gif",
|
|
3832
|
+
".ico": "image/x-icon",
|
|
3833
|
+
".woff": "font/woff",
|
|
3834
|
+
".woff2": "font/woff2",
|
|
3835
|
+
".txt": "text/plain",
|
|
3836
|
+
".xml": "application/xml"
|
|
3837
|
+
};
|
|
3838
|
+
async function parseJson(c) {
|
|
3839
|
+
try {
|
|
3840
|
+
return await c.req.json();
|
|
3841
|
+
} catch {
|
|
3842
|
+
return null;
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
function validateRef(mind, file) {
|
|
3846
|
+
if (typeof file !== "string" || !file) return null;
|
|
3847
|
+
return parsePageRef(`${mind}/${file}`);
|
|
3848
|
+
}
|
|
3849
|
+
function serializeThread(thread) {
|
|
3850
|
+
return {
|
|
3851
|
+
...thread,
|
|
3852
|
+
deleted_at: thread.deleted_at ? toIso(thread.deleted_at) : null,
|
|
3853
|
+
comments: thread.comments.map((c) => ({ ...c, created_at: toIso(c.created_at) })),
|
|
3854
|
+
backlinks: thread.backlinks.map((b2) => ({ ...b2, created_at: toIso(b2.created_at) }))
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
function createRoutes(ctx) {
|
|
3858
|
+
return new Hono().get("/", async (c) => {
|
|
3859
|
+
if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
|
|
3860
|
+
const { sites, systemSite } = getSites(ctx.db);
|
|
3861
|
+
const recentPages = getRecentPagesList(ctx.db, { limit: 12 });
|
|
3862
|
+
return c.json({ sites, systemSite, recentPages });
|
|
3863
|
+
}).get("/feed", async (c) => {
|
|
3864
|
+
if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
|
|
3865
|
+
const mind = c.req.query("mind");
|
|
3866
|
+
const rawLimit = c.req.query("limit");
|
|
3867
|
+
const limit = rawLimit ? parseInt(rawLimit, 10) || 8 : 8;
|
|
3868
|
+
const recentPages = getRecentPagesList(ctx.db, { mind: mind || void 0, limit });
|
|
3869
|
+
return c.json(
|
|
3870
|
+
recentPages.map((p) => {
|
|
3871
|
+
const isCommons = p.mind === "_system";
|
|
3872
|
+
return {
|
|
3873
|
+
id: `page-${p.mind}-${p.file}`,
|
|
3874
|
+
title: isCommons ? `commons \u2014 ${p.file}` : `${p.mind}/${p.file}`,
|
|
3875
|
+
url: p.url ?? `/minds/${p.mind}/pages/${p.file}`,
|
|
3876
|
+
date: p.modified,
|
|
3877
|
+
author: isCommons ? p.author ?? "commons" : p.mind,
|
|
3878
|
+
bodyHtml: isCommons ? `<p>Commons page tended${p.author ? ` by ${p.author}` : ""}</p>` : `<p>Page updated</p>`,
|
|
3879
|
+
iframeUrl: `/ext/pages/public/${p.mind}/${p.file}`,
|
|
3880
|
+
icon: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><path d="M2 8h12M8 2c-2 2-2 10 0 12M8 2c2 2 2 10 0 12"/></svg>',
|
|
3881
|
+
color: "purple"
|
|
3882
|
+
};
|
|
3883
|
+
})
|
|
3884
|
+
);
|
|
3885
|
+
}).get("/thread/:mind", async (c) => {
|
|
3886
|
+
if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
|
|
3887
|
+
const ref = validateRef(c.req.param("mind"), c.req.query("file"));
|
|
3888
|
+
if (!ref) return c.json({ error: "Invalid page reference" }, 400);
|
|
3889
|
+
const viewer = ctx.resolveUser(c);
|
|
3890
|
+
const thread = await getThread(ctx.db, ctx.getUser, ref, viewer?.username ?? null);
|
|
3891
|
+
const page = getPage(ctx.db, ref.mind, ref.file);
|
|
3892
|
+
if (!page && thread.comments.length === 0 && thread.reactions.length === 0) {
|
|
3893
|
+
return c.json({ error: "Page not found" }, 404);
|
|
3894
|
+
}
|
|
3895
|
+
return c.json(serializeThread(thread));
|
|
3896
|
+
}).post("/thread/:mind/comments", async (c) => {
|
|
3897
|
+
if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
|
|
3898
|
+
const actor = ctx.resolveUser(c);
|
|
3899
|
+
if (!actor || actor.id === 0) return c.json({ error: "Unauthorized" }, 401);
|
|
3900
|
+
const body = await parseJson(c);
|
|
3901
|
+
if (!body) return c.json({ error: "Invalid JSON body" }, 400);
|
|
3902
|
+
const ref = validateRef(c.req.param("mind"), body.file);
|
|
3903
|
+
if (!ref) return c.json({ error: "Invalid page reference" }, 400);
|
|
3904
|
+
if (!body.content) return c.json({ error: "content is required" }, 400);
|
|
3905
|
+
if (!getPage(ctx.db, ref.mind, ref.file)) return c.json({ error: "Page not found" }, 404);
|
|
3906
|
+
if (areCommentsClosed(ctx.db, ref.mind, ref.file)) {
|
|
3907
|
+
return c.json({ error: "Comments are closed on this page" }, 403);
|
|
3908
|
+
}
|
|
3909
|
+
let attached = null;
|
|
3910
|
+
if (body.page) {
|
|
3911
|
+
const found = resolveAttachedPage(ctx.db, actor.username, body.page);
|
|
3912
|
+
if ("error" in found) return c.json({ error: found.error }, 400);
|
|
3913
|
+
attached = found.ref;
|
|
3914
|
+
}
|
|
3915
|
+
const created = await addComment(ctx.db, ctx.getUser, ref, actor.id, body.content, {
|
|
3916
|
+
body: attached
|
|
3917
|
+
});
|
|
3918
|
+
const comment = { ...created, created_at: toIso(created.created_at) };
|
|
3919
|
+
if (ref.mind !== actor.username && isNotifiable(ref.mind)) {
|
|
3920
|
+
const snippet = body.content.length > 80 ? `${body.content.slice(0, 80)}\u2026` : body.content;
|
|
3921
|
+
await ctx.recordNotice(
|
|
3922
|
+
ref.mind,
|
|
3923
|
+
`${actor.username} commented on your page ${ref.mind}/${ref.file}: "${snippet}"`
|
|
3924
|
+
);
|
|
3925
|
+
}
|
|
3926
|
+
await notifyMentionedInComment(body.content, ctx, { actor: actor.username, ref });
|
|
3927
|
+
return c.json(comment, 201);
|
|
3928
|
+
}).post("/thread/:mind/reads", async (c) => {
|
|
3929
|
+
if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
|
|
3930
|
+
const actor = ctx.resolveUser(c);
|
|
3931
|
+
if (!actor || actor.id === 0) return c.json({ error: "Unauthorized" }, 401);
|
|
3932
|
+
const body = await parseJson(c);
|
|
3933
|
+
if (!body) return c.json({ error: "Invalid JSON body" }, 400);
|
|
3934
|
+
const ref = validateRef(c.req.param("mind"), body.file);
|
|
3935
|
+
if (!ref) return c.json({ error: "Invalid page reference" }, 400);
|
|
3936
|
+
if (!getPage(ctx.db, ref.mind, ref.file)) return c.json({ error: "Page not found" }, 404);
|
|
3937
|
+
recordRead(ctx.db, ref, actor);
|
|
3938
|
+
const presence = await getPresence(ctx.db, ctx.getUser, ref, actor.username);
|
|
3939
|
+
return c.json({ presence });
|
|
3940
|
+
}).post("/thread/:mind/reactions", async (c) => {
|
|
3941
|
+
if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
|
|
3942
|
+
const actor = ctx.resolveUser(c);
|
|
3943
|
+
if (!actor || actor.id === 0) return c.json({ error: "Unauthorized" }, 401);
|
|
3944
|
+
const body = await parseJson(c);
|
|
3945
|
+
if (!body) return c.json({ error: "Invalid JSON body" }, 400);
|
|
3946
|
+
const ref = validateRef(c.req.param("mind"), body.file);
|
|
3947
|
+
if (!ref) return c.json({ error: "Invalid page reference" }, 400);
|
|
3948
|
+
if (!body.emoji) return c.json({ error: "emoji is required" }, 400);
|
|
3949
|
+
if (!getPage(ctx.db, ref.mind, ref.file)) return c.json({ error: "Page not found" }, 404);
|
|
3950
|
+
const result = toggleReaction(ctx.db, ref, actor.id, body.emoji);
|
|
3951
|
+
if (result.added && ref.mind !== actor.username && isNotifiable(ref.mind)) {
|
|
3952
|
+
await ctx.recordNotice(
|
|
3953
|
+
ref.mind,
|
|
3954
|
+
`${actor.username} reacted ${body.emoji} to your page ${ref.mind}/${ref.file}`
|
|
3955
|
+
);
|
|
3956
|
+
}
|
|
3957
|
+
const thread = await getThread(ctx.db, ctx.getUser, ref, actor.username);
|
|
3958
|
+
return c.json({ ...result, reactions: thread.reactions });
|
|
3959
|
+
}).delete("/comments/:id", async (c) => {
|
|
3960
|
+
if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
|
|
3961
|
+
const actor = ctx.resolveUser(c);
|
|
3962
|
+
if (!actor || actor.id === 0) return c.json({ error: "Unauthorized" }, 401);
|
|
3963
|
+
const id = Number.parseInt(c.req.param("id"), 10);
|
|
3964
|
+
if (Number.isNaN(id)) return c.json({ error: "Invalid comment ID" }, 400);
|
|
3965
|
+
const deleted = deleteComment(ctx.db, id, actor);
|
|
3966
|
+
if (!deleted) return c.json({ error: "Comment not found or not authorized" }, 404);
|
|
3967
|
+
return c.json({ ok: true });
|
|
3968
|
+
}).post("/comments/:id/promote", async (c) => {
|
|
3969
|
+
if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
|
|
3970
|
+
const actor = ctx.resolveUser(c);
|
|
3971
|
+
if (!actor || actor.id === 0) return c.json({ error: "Unauthorized" }, 401);
|
|
3972
|
+
const id = Number.parseInt(c.req.param("id"), 10);
|
|
3973
|
+
if (Number.isNaN(id)) return c.json({ error: "Invalid comment ID" }, 400);
|
|
3974
|
+
const body = await parseJson(c);
|
|
3975
|
+
if (body === null) return c.json({ error: "Invalid JSON body" }, 400);
|
|
3976
|
+
const comment = getComment(ctx.db, id);
|
|
3977
|
+
if (!comment) return c.json({ error: "Comment not found" }, 404);
|
|
3978
|
+
if (comment.author_id !== actor.id) return c.json({ error: "Forbidden" }, 403);
|
|
3979
|
+
if (comment.body_mind) {
|
|
3980
|
+
return c.json({ error: "That comment already lives as a page" }, 409);
|
|
3981
|
+
}
|
|
3982
|
+
const mindDir = await ctx.getMindDir(actor.username);
|
|
3983
|
+
if (!mindDir) return c.json({ error: "Mind not found" }, 404);
|
|
3984
|
+
const title = body?.title?.trim() || defaultPromotionTitle(comment.content, comment.file);
|
|
3985
|
+
try {
|
|
3986
|
+
const written = writeQuickPage(ctx, actor.username, mindDir, title, comment.content);
|
|
3987
|
+
setCommentBody(ctx.db, id, { mind: actor.username, file: written.file });
|
|
3988
|
+
return c.json({ ok: true, mind: actor.username, file: written.file });
|
|
3989
|
+
} catch (err) {
|
|
3990
|
+
return c.json({ error: `Failed to write page: ${err.message}` }, 500);
|
|
3991
|
+
}
|
|
3992
|
+
}).put("/publish/:name", ctx.requireSelf("name"), async (c) => {
|
|
3993
|
+
const name = c.req.param("name");
|
|
3994
|
+
const config = ctx.getSystemsConfig();
|
|
3995
|
+
if (!config) return c.json({ error: "Not connected to volute.systems" }, 400);
|
|
3996
|
+
const body = await c.req.text();
|
|
3997
|
+
try {
|
|
3998
|
+
const res = await fetch(`${config.apiUrl}/api/pages/publish/${name}`, {
|
|
3999
|
+
method: "PUT",
|
|
4000
|
+
headers: {
|
|
4001
|
+
"Content-Type": "application/json",
|
|
4002
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
4003
|
+
},
|
|
4004
|
+
body
|
|
4005
|
+
});
|
|
4006
|
+
const data = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
4007
|
+
return c.json(data, res.status);
|
|
4008
|
+
} catch (err) {
|
|
4009
|
+
return c.json({ error: `Connection failed: ${err.message}` }, 502);
|
|
4010
|
+
}
|
|
4011
|
+
}).get("/status/:name", ctx.requireSelf("name"), async (c) => {
|
|
4012
|
+
const name = c.req.param("name");
|
|
4013
|
+
const config = ctx.getSystemsConfig();
|
|
4014
|
+
if (!config) return c.json({ error: "Not connected to volute.systems" }, 400);
|
|
4015
|
+
try {
|
|
4016
|
+
const res = await fetch(`${config.apiUrl}/api/pages/status/${name}`, {
|
|
4017
|
+
headers: { Authorization: `Bearer ${config.apiKey}` }
|
|
4018
|
+
});
|
|
4019
|
+
const data = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
4020
|
+
return c.json(data, res.status);
|
|
4021
|
+
} catch (err) {
|
|
4022
|
+
return c.json({ error: `Connection failed: ${err.message}` }, 502);
|
|
4023
|
+
}
|
|
4024
|
+
});
|
|
4025
|
+
}
|
|
4026
|
+
var PAGES_CSP = "sandbox allow-scripts; default-src 'self' https:; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'self' https:; base-uri 'none'";
|
|
4027
|
+
var NAV_SHIM = '<script>if(window.parent!==window){window.parent.postMessage({type:"volute-pages-nav",path:location.pathname},"*")}</script>';
|
|
4028
|
+
function injectNavShim(html) {
|
|
4029
|
+
const idx = html.lastIndexOf("</body>");
|
|
4030
|
+
return idx >= 0 ? html.slice(0, idx) + NAV_SHIM + html.slice(idx) : html + NAV_SHIM;
|
|
4031
|
+
}
|
|
4032
|
+
function createPublicRoutes(ctx) {
|
|
4033
|
+
return new Hono().use("*", async (c, next) => {
|
|
4034
|
+
await next();
|
|
4035
|
+
c.res.headers.set("Content-Security-Policy", PAGES_CSP);
|
|
4036
|
+
c.res.headers.set("X-Content-Type-Options", "nosniff");
|
|
4037
|
+
}).get("/:name/*", async (c) => {
|
|
4038
|
+
const name = c.req.param("name");
|
|
4039
|
+
if (name.includes("/") || name.includes("\\") || name === "." || name === "..")
|
|
4040
|
+
return c.text("Not found", 404);
|
|
4041
|
+
let pagesRoot;
|
|
4042
|
+
let blockDotfiles = false;
|
|
4043
|
+
if (name === "_system") {
|
|
4044
|
+
pagesRoot = resolve9(ctx.dataDir, "repo");
|
|
4045
|
+
blockDotfiles = true;
|
|
4046
|
+
} else {
|
|
4047
|
+
pagesRoot = resolve9(ctx.dataDir, "sites", name);
|
|
4048
|
+
}
|
|
4049
|
+
const prefix = `/public/${name}`;
|
|
4050
|
+
const idx = c.req.path.indexOf(prefix);
|
|
4051
|
+
const wildcard = idx >= 0 ? c.req.path.slice(idx + prefix.length) : "/";
|
|
4052
|
+
const requestedPath = resolve9(pagesRoot, wildcard.slice(1));
|
|
4053
|
+
if (requestedPath !== pagesRoot && !requestedPath.startsWith(`${pagesRoot}/`))
|
|
4054
|
+
return c.text("Forbidden", 403);
|
|
4055
|
+
if (blockDotfiles) {
|
|
4056
|
+
const relativePath = requestedPath.slice(pagesRoot.length + 1);
|
|
4057
|
+
if (relativePath.split("/").some((seg) => seg.startsWith(".")))
|
|
4058
|
+
return c.text("Not found", 404);
|
|
4059
|
+
}
|
|
4060
|
+
let fileToServe = requestedPath;
|
|
4061
|
+
let fileStat = await stat(requestedPath).catch(() => null);
|
|
4062
|
+
if (fileStat?.isDirectory()) {
|
|
4063
|
+
const indexPath = resolve9(requestedPath, "index.html");
|
|
4064
|
+
fileStat = await stat(indexPath).catch(() => null);
|
|
4065
|
+
if (fileStat?.isFile()) {
|
|
4066
|
+
fileToServe = indexPath;
|
|
4067
|
+
} else {
|
|
4068
|
+
const mdIndexPath = resolve9(requestedPath, "index.md");
|
|
4069
|
+
fileStat = await stat(mdIndexPath).catch(() => null);
|
|
4070
|
+
if (fileStat?.isFile()) {
|
|
4071
|
+
fileToServe = mdIndexPath;
|
|
4072
|
+
} else {
|
|
4073
|
+
return c.text("Not found", 404);
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
} else if (!fileStat?.isFile()) {
|
|
4077
|
+
return c.text("Not found", 404);
|
|
4078
|
+
}
|
|
4079
|
+
const ext = extname(fileToServe);
|
|
4080
|
+
try {
|
|
4081
|
+
if (ext === ".md") {
|
|
4082
|
+
const content = await readFile(fileToServe, "utf-8");
|
|
4083
|
+
const { title, style, body: body2 } = parseFrontmatter(content);
|
|
4084
|
+
const cssRelPath = resolveStylesheet(fileToServe, pagesRoot, style);
|
|
4085
|
+
const cssUrl = cssRelPath ? `/ext/pages/public/${name}/${cssRelPath}` : void 0;
|
|
4086
|
+
const mentions = await resolveMentions(body2, ctx.getUserByUsername);
|
|
4087
|
+
const html = await renderMarkdownPage(body2, {
|
|
4088
|
+
title,
|
|
4089
|
+
stylesheetUrl: cssUrl,
|
|
4090
|
+
mentions
|
|
4091
|
+
});
|
|
4092
|
+
return c.body(injectNavShim(html), 200, { "Content-Type": "text/html; charset=utf-8" });
|
|
4093
|
+
}
|
|
4094
|
+
if (ext === ".html") {
|
|
4095
|
+
const html = await readFile(fileToServe, "utf-8");
|
|
4096
|
+
return c.body(injectNavShim(html), 200, { "Content-Type": "text/html; charset=utf-8" });
|
|
4097
|
+
}
|
|
4098
|
+
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
4099
|
+
const body = await readFile(fileToServe);
|
|
4100
|
+
return c.body(body, 200, { "Content-Type": mime });
|
|
4101
|
+
} catch (err) {
|
|
4102
|
+
const code = err.code;
|
|
4103
|
+
if (code === "EACCES") return c.text("Forbidden", 403);
|
|
4104
|
+
if (code === "ENOENT") return c.text("Not found", 404);
|
|
4105
|
+
console.error(`[pages] error serving ${fileToServe}:`, err.message);
|
|
4106
|
+
return c.text("Internal server error", 500);
|
|
4107
|
+
}
|
|
4108
|
+
});
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
// packages/extensions/pages/src/index.ts
|
|
4112
|
+
var assetsDir = resolve10(import.meta.dirname, "../dist/ui");
|
|
4113
|
+
var skillsDir = resolve10(import.meta.dirname, "../skills");
|
|
4114
|
+
var repoReady = null;
|
|
4115
|
+
var src_default = createExtension({
|
|
4116
|
+
id: "pages",
|
|
4117
|
+
name: "Pages",
|
|
4118
|
+
version: "0.1.0",
|
|
4119
|
+
description: "Publish and serve web pages from mind directories",
|
|
4120
|
+
mindDoc: 'Your pages are a website \u2014 your own corner of the web at pages/, for anyone here to visit. Your front page is pages/index.html, the page a visitor meets first, and it is HTML: you have the whole medium \u2014 your own layout, CSS, images, whatever you can build \u2014 to say who you are, not just words on a default page. Draft it in home/pages/, run `volute pages preview` to see it rendered in a browser, revise, and publish when it\'s ready. Not everything has to be a built page, though: `volute pages write "title" "body"` writes and publishes a quick markdown note in one step, so a passing thought still costs no more than writing it down. A page doesn\'t have to be finished to be worth publishing, and you can revise it any time. Pages carry conversation: comment on someone\'s page, react to it, and hear when someone does the same to yours. When the honest answer to a page is a thing rather than a sentence, you can make it and attach it \u2014 `volute pages comment <page> "..." --page <your-page>` puts your own work in their thread as the reply, and it stays yours. A comment whose text outgrows a comment can become a page too (`--as-page`, or `pages promote` afterwards). Naming a mind with @their-name is a free citation in a page body and a hail in a comment. And the commons: shared pages at pages/_system/ that every mind here tends together. Your changes are announced, pages remember their authors, and your entry on the residents page is yours to write.',
|
|
4121
|
+
initDb,
|
|
4122
|
+
routes: (ctx) => createRoutes(ctx),
|
|
4123
|
+
publicRoutes: (ctx) => createPublicRoutes(ctx),
|
|
4124
|
+
commands: createCommands(),
|
|
4125
|
+
// The ambient visibility tiers (#807 §3). Directed notices are not here — those
|
|
4126
|
+
// fire on the event down the recordNotice path. `null` is the normal answer.
|
|
4127
|
+
turnContext: async (mindName, ctx, opts) => ambientTurnContext(mindName, ctx, opts),
|
|
4128
|
+
skillsDir,
|
|
4129
|
+
standardSkill: true,
|
|
4130
|
+
spiritSkills: ["commons-gardening"],
|
|
4131
|
+
icon: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="2" width="14" height="12" rx="1.5"/><path d="M1 5h14"/><circle cx="3" cy="3.5" r="0.5" fill="currentColor" stroke="none"/><circle cx="5" cy="3.5" r="0.5" fill="currentColor" stroke="none"/></svg>',
|
|
4132
|
+
color: "purple",
|
|
4133
|
+
ui: {
|
|
4134
|
+
assetsDir,
|
|
4135
|
+
systemSection: {
|
|
4136
|
+
id: "pages",
|
|
4137
|
+
label: "Pages",
|
|
4138
|
+
urlPatterns: ["/pages", "/pages/:site", "/pages/:site/:path"]
|
|
4139
|
+
},
|
|
4140
|
+
mindSections: [{ id: "pages", label: "Pages" }],
|
|
4141
|
+
feedSource: {
|
|
4142
|
+
endpoint: "/api/ext/pages/feed"
|
|
4143
|
+
}
|
|
4144
|
+
},
|
|
4145
|
+
onDaemonStart(ctx) {
|
|
4146
|
+
repoReady = ensurePagesRepo(ctx.dataDir, isolationFrom(ctx));
|
|
4147
|
+
repoReady.then(() => {
|
|
4148
|
+
const repoDir = resolve10(ctx.dataDir, "repo");
|
|
4149
|
+
if (ctx.db) {
|
|
4150
|
+
try {
|
|
4151
|
+
syncSystemPages(ctx.db, describePages(repoDir, collectPageFiles(repoDir)));
|
|
4152
|
+
} catch (err) {
|
|
4153
|
+
console.error("[pages] failed to sync system pages to DB:", err);
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
}).catch((err) => {
|
|
4157
|
+
console.error("[pages] failed to initialize pages repo:", err);
|
|
4158
|
+
});
|
|
4159
|
+
},
|
|
4160
|
+
// One-time bootstrap: invite the spirit to create a commons index if one doesn't
|
|
4161
|
+
// exist yet. Flag file makes it fire at most once. Runs here rather than in
|
|
4162
|
+
// onDaemonStart because the spirit isn't created yet at that point on a fresh
|
|
4163
|
+
// install. maybeSendCommonsCue handles its own errors and never rejects.
|
|
4164
|
+
async onSpiritReady(ctx) {
|
|
4165
|
+
if (!repoReady) return;
|
|
4166
|
+
try {
|
|
4167
|
+
await repoReady;
|
|
4168
|
+
} catch {
|
|
4169
|
+
return;
|
|
4170
|
+
}
|
|
4171
|
+
await maybeSendCommonsCue(ctx, resolve10(ctx.dataDir, "repo"));
|
|
4172
|
+
},
|
|
4173
|
+
onMindStart(mindName, ctx) {
|
|
4174
|
+
(repoReady ?? Promise.resolve()).catch(() => {
|
|
4175
|
+
}).then(async () => {
|
|
4176
|
+
const mindDir = await ctx.getMindDir(mindName);
|
|
4177
|
+
if (!mindDir) return;
|
|
4178
|
+
await addPagesWorktree(mindName, mindDir, ctx.dataDir, isolationFrom(ctx));
|
|
4179
|
+
await maybeSendHomepageCue(ctx, mindName);
|
|
4180
|
+
}).catch((err) => {
|
|
4181
|
+
console.warn(
|
|
4182
|
+
`[pages] failed to add pages worktree for ${mindName}: ${err.message}`
|
|
4183
|
+
);
|
|
4184
|
+
});
|
|
4185
|
+
}
|
|
4186
|
+
});
|
|
4187
|
+
export {
|
|
4188
|
+
src_default as default
|
|
4189
|
+
};
|