@shivaedev/work-board 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +141 -3
  3. package/dist/board.d.ts +9 -3
  4. package/dist/board.d.ts.map +1 -1
  5. package/dist/board.js +17 -9
  6. package/dist/board.js.map +1 -1
  7. package/dist/cli.js +3 -3
  8. package/dist/cli.js.map +1 -1
  9. package/dist/files/changes.d.ts +4 -3
  10. package/dist/files/changes.d.ts.map +1 -1
  11. package/dist/files/changes.js +2 -2
  12. package/dist/files/changes.js.map +1 -1
  13. package/dist/files/list.d.ts +1 -1
  14. package/dist/files/list.d.ts.map +1 -1
  15. package/dist/files/list.js +1 -1
  16. package/dist/files/list.js.map +1 -1
  17. package/dist/files/url.d.ts +2 -0
  18. package/dist/files/url.d.ts.map +1 -0
  19. package/dist/files/url.js +4 -0
  20. package/dist/files/url.js.map +1 -0
  21. package/dist/http/assets.d.ts.map +1 -1
  22. package/dist/http/assets.js +21 -6
  23. package/dist/http/assets.js.map +1 -1
  24. package/dist/http/events.d.ts +1 -1
  25. package/dist/http/events.d.ts.map +1 -1
  26. package/dist/http/events.js +1 -1
  27. package/dist/http/events.js.map +1 -1
  28. package/dist/http/identity.d.ts +7 -0
  29. package/dist/http/identity.d.ts.map +1 -0
  30. package/dist/http/identity.js +38 -0
  31. package/dist/http/identity.js.map +1 -0
  32. package/dist/http/loopback.js +2 -2
  33. package/dist/http/loopback.js.map +1 -1
  34. package/dist/http/page.d.ts +4 -3
  35. package/dist/http/page.d.ts.map +1 -1
  36. package/dist/http/page.js +37 -15
  37. package/dist/http/page.js.map +1 -1
  38. package/dist/http/search.d.ts +5 -0
  39. package/dist/http/search.d.ts.map +1 -0
  40. package/dist/http/search.js +11 -0
  41. package/dist/http/search.js.map +1 -0
  42. package/dist/http/work.d.ts +6 -0
  43. package/dist/http/work.d.ts.map +1 -0
  44. package/dist/http/work.js +22 -0
  45. package/dist/http/work.js.map +1 -0
  46. package/dist/metadata/backlinks.d.ts +8 -0
  47. package/dist/metadata/backlinks.d.ts.map +1 -0
  48. package/dist/metadata/backlinks.js +29 -0
  49. package/dist/metadata/backlinks.js.map +1 -0
  50. package/dist/metadata/fields.d.ts +29 -0
  51. package/dist/metadata/fields.d.ts.map +1 -0
  52. package/dist/metadata/fields.js +25 -0
  53. package/dist/metadata/fields.js.map +1 -0
  54. package/dist/metadata/links.d.ts +4 -0
  55. package/dist/metadata/links.d.ts.map +1 -0
  56. package/dist/metadata/links.js +18 -0
  57. package/dist/metadata/links.js.map +1 -0
  58. package/dist/metadata/locations.d.ts +3 -0
  59. package/dist/metadata/locations.d.ts.map +1 -0
  60. package/dist/metadata/locations.js +35 -0
  61. package/dist/metadata/locations.js.map +1 -0
  62. package/dist/metadata/model.d.ts +15 -0
  63. package/dist/metadata/model.d.ts.map +1 -0
  64. package/dist/metadata/model.js +13 -0
  65. package/dist/metadata/model.js.map +1 -0
  66. package/dist/metadata/parse.d.ts +16 -0
  67. package/dist/metadata/parse.d.ts.map +1 -0
  68. package/dist/metadata/parse.js +48 -0
  69. package/dist/metadata/parse.js.map +1 -0
  70. package/dist/metadata/problems.d.ts +4 -0
  71. package/dist/metadata/problems.d.ts.map +1 -0
  72. package/dist/metadata/problems.js +57 -0
  73. package/dist/metadata/problems.js.map +1 -0
  74. package/dist/metadata/relationshipLabel.d.ts +2 -0
  75. package/dist/metadata/relationshipLabel.d.ts.map +1 -0
  76. package/dist/metadata/relationshipLabel.js +13 -0
  77. package/dist/metadata/relationshipLabel.js.map +1 -0
  78. package/dist/metadata/schema.d.ts +34 -0
  79. package/dist/metadata/schema.d.ts.map +1 -0
  80. package/dist/metadata/schema.js +27 -0
  81. package/dist/metadata/schema.js.map +1 -0
  82. package/dist/metadata/sourceLinks.d.ts +5 -0
  83. package/dist/metadata/sourceLinks.d.ts.map +1 -0
  84. package/dist/metadata/sourceLinks.js +44 -0
  85. package/dist/metadata/sourceLinks.js.map +1 -0
  86. package/dist/page/backlinksHtml.d.ts +3 -0
  87. package/dist/page/backlinksHtml.d.ts.map +1 -0
  88. package/dist/page/backlinksHtml.js +16 -0
  89. package/dist/page/backlinksHtml.js.map +1 -0
  90. package/dist/page/board.d.ts +2 -2
  91. package/dist/page/board.d.ts.map +1 -1
  92. package/dist/page/board.js +16 -6
  93. package/dist/page/board.js.map +1 -1
  94. package/dist/page/client.d.ts +2 -1
  95. package/dist/page/client.d.ts.map +1 -1
  96. package/dist/page/client.js +65 -7
  97. package/dist/page/client.js.map +1 -1
  98. package/dist/page/criteriaEvidence.d.ts +4 -0
  99. package/dist/page/criteriaEvidence.d.ts.map +1 -0
  100. package/dist/page/criteriaEvidence.js +27 -0
  101. package/dist/page/criteriaEvidence.js.map +1 -0
  102. package/dist/page/diagrams.d.ts +1 -1
  103. package/dist/page/diagrams.d.ts.map +1 -1
  104. package/dist/page/diagrams.js +12 -4
  105. package/dist/page/diagrams.js.map +1 -1
  106. package/dist/page/document.d.ts +2 -2
  107. package/dist/page/document.d.ts.map +1 -1
  108. package/dist/page/document.js +7 -4
  109. package/dist/page/document.js.map +1 -1
  110. package/dist/page/escape.d.ts.map +1 -1
  111. package/dist/page/escape.js +2 -2
  112. package/dist/page/escape.js.map +1 -1
  113. package/dist/page/libraryScript.d.ts +2 -0
  114. package/dist/page/libraryScript.d.ts.map +1 -0
  115. package/dist/page/libraryScript.js +73 -0
  116. package/dist/page/libraryScript.js.map +1 -0
  117. package/dist/page/metadata.d.ts +5 -0
  118. package/dist/page/metadata.d.ts.map +1 -0
  119. package/dist/page/metadata.js +34 -0
  120. package/dist/page/metadata.js.map +1 -0
  121. package/dist/page/metadataDetails.d.ts +4 -0
  122. package/dist/page/metadataDetails.d.ts.map +1 -0
  123. package/dist/page/metadataDetails.js +21 -0
  124. package/dist/page/metadataDetails.js.map +1 -0
  125. package/dist/page/metadataLinks.d.ts +6 -0
  126. package/dist/page/metadataLinks.d.ts.map +1 -0
  127. package/dist/page/metadataLinks.js +31 -0
  128. package/dist/page/metadataLinks.js.map +1 -0
  129. package/dist/page/nav.d.ts +2 -2
  130. package/dist/page/nav.d.ts.map +1 -1
  131. package/dist/page/nav.js +4 -4
  132. package/dist/page/nav.js.map +1 -1
  133. package/dist/page/navigationScript.d.ts +2 -0
  134. package/dist/page/navigationScript.d.ts.map +1 -0
  135. package/dist/page/navigationScript.js +104 -0
  136. package/dist/page/navigationScript.js.map +1 -0
  137. package/dist/page/readingStateScript.d.ts +2 -0
  138. package/dist/page/readingStateScript.d.ts.map +1 -0
  139. package/dist/page/readingStateScript.js +62 -0
  140. package/dist/page/readingStateScript.js.map +1 -0
  141. package/dist/page/readingStyles.d.ts +2 -0
  142. package/dist/page/readingStyles.d.ts.map +1 -0
  143. package/dist/page/readingStyles.js +29 -0
  144. package/dist/page/readingStyles.js.map +1 -0
  145. package/dist/page/readingTools.d.ts +3 -0
  146. package/dist/page/readingTools.d.ts.map +1 -0
  147. package/dist/page/readingTools.js +10 -0
  148. package/dist/page/readingTools.js.map +1 -0
  149. package/dist/page/recordedEvidence.d.ts +4 -0
  150. package/dist/page/recordedEvidence.d.ts.map +1 -0
  151. package/dist/page/recordedEvidence.js +10 -0
  152. package/dist/page/recordedEvidence.js.map +1 -0
  153. package/dist/page/searchDialog.d.ts +3 -0
  154. package/dist/page/searchDialog.d.ts.map +1 -0
  155. package/dist/page/searchDialog.js +16 -0
  156. package/dist/page/searchDialog.js.map +1 -0
  157. package/dist/page/searchResultsScript.d.ts +2 -0
  158. package/dist/page/searchResultsScript.d.ts.map +1 -0
  159. package/dist/page/searchResultsScript.js +64 -0
  160. package/dist/page/searchResultsScript.js.map +1 -0
  161. package/dist/page/searchScript.d.ts +2 -0
  162. package/dist/page/searchScript.d.ts.map +1 -0
  163. package/dist/page/searchScript.js +73 -0
  164. package/dist/page/searchScript.js.map +1 -0
  165. package/dist/page/shell.d.ts +1 -1
  166. package/dist/page/shell.d.ts.map +1 -1
  167. package/dist/page/shell.js +23 -5
  168. package/dist/page/shell.js.map +1 -1
  169. package/dist/page/stateScript.d.ts +2 -0
  170. package/dist/page/stateScript.d.ts.map +1 -0
  171. package/dist/page/stateScript.js +49 -0
  172. package/dist/page/stateScript.js.map +1 -0
  173. package/dist/page/style.d.ts.map +1 -1
  174. package/dist/page/style.js +14 -34
  175. package/dist/page/style.js.map +1 -1
  176. package/dist/page/swap.d.ts +1 -1
  177. package/dist/page/swap.d.ts.map +1 -1
  178. package/dist/page/swap.js +4 -1
  179. package/dist/page/swap.js.map +1 -1
  180. package/dist/page/workspaceStyles.d.ts +3 -0
  181. package/dist/page/workspaceStyles.d.ts.map +1 -0
  182. package/dist/page/workspaceStyles.js +79 -0
  183. package/dist/page/workspaceStyles.js.map +1 -0
  184. package/dist/render/board.d.ts +8 -8
  185. package/dist/render/board.d.ts.map +1 -1
  186. package/dist/render/board.js +7 -7
  187. package/dist/render/board.js.map +1 -1
  188. package/dist/render/code.d.ts +1 -1
  189. package/dist/render/code.js +5 -5
  190. package/dist/render/code.js.map +1 -1
  191. package/dist/render/documentHeadings.d.ts +667 -0
  192. package/dist/render/documentHeadings.d.ts.map +1 -0
  193. package/dist/render/documentHeadings.js +45 -0
  194. package/dist/render/documentHeadings.js.map +1 -0
  195. package/dist/render/documentLinks.d.ts +659 -0
  196. package/dist/render/documentLinks.d.ts.map +1 -0
  197. package/dist/render/documentLinks.js +19 -0
  198. package/dist/render/documentLinks.js.map +1 -0
  199. package/dist/render/footnotes.d.ts +1 -1
  200. package/dist/render/footnotes.js +2 -2
  201. package/dist/render/footnotes.js.map +1 -1
  202. package/dist/render/highlighter.d.ts +1 -1
  203. package/dist/render/highlighter.js +2 -2
  204. package/dist/render/highlighter.js.map +1 -1
  205. package/dist/render/markdown.d.ts +7 -1
  206. package/dist/render/markdown.d.ts.map +1 -1
  207. package/dist/render/markdown.js +13 -2
  208. package/dist/render/markdown.js.map +1 -1
  209. package/dist/search/collector.d.ts +13 -0
  210. package/dist/search/collector.d.ts.map +1 -0
  211. package/dist/search/collector.js +40 -0
  212. package/dist/search/collector.js.map +1 -0
  213. package/dist/search/entries.d.ts +10 -0
  214. package/dist/search/entries.d.ts.map +1 -0
  215. package/dist/search/entries.js +37 -0
  216. package/dist/search/entries.js.map +1 -0
  217. package/dist/search/linkCollector.d.ts +8 -0
  218. package/dist/search/linkCollector.d.ts.map +1 -0
  219. package/dist/search/linkCollector.js +16 -0
  220. package/dist/search/linkCollector.js.map +1 -0
  221. package/dist/search/match.d.ts +14 -0
  222. package/dist/search/match.d.ts.map +1 -0
  223. package/dist/search/match.js +26 -0
  224. package/dist/search/match.js.map +1 -0
  225. package/dist/search/metadataEntries.d.ts +4 -0
  226. package/dist/search/metadataEntries.d.ts.map +1 -0
  227. package/dist/search/metadataEntries.js +40 -0
  228. package/dist/search/metadataEntries.js.map +1 -0
  229. package/dist/search/snapshot.d.ts +13 -0
  230. package/dist/search/snapshot.d.ts.map +1 -0
  231. package/dist/search/snapshot.js +50 -0
  232. package/dist/search/snapshot.js.map +1 -0
  233. package/dist/serve.d.ts +1 -1
  234. package/dist/serve.d.ts.map +1 -1
  235. package/dist/serve.js +1 -1
  236. package/dist/serve.js.map +1 -1
  237. package/dist/views/items.d.ts +11 -0
  238. package/dist/views/items.d.ts.map +1 -0
  239. package/dist/views/items.js +48 -0
  240. package/dist/views/items.js.map +1 -0
  241. package/dist/views/projection.d.ts +15 -0
  242. package/dist/views/projection.d.ts.map +1 -0
  243. package/dist/views/projection.js +47 -0
  244. package/dist/views/projection.js.map +1 -0
  245. package/dist/views/savedScript.d.ts +2 -0
  246. package/dist/views/savedScript.d.ts.map +1 -0
  247. package/dist/views/savedScript.js +89 -0
  248. package/dist/views/savedScript.js.map +1 -0
  249. package/dist/views/state.d.ts +15 -0
  250. package/dist/views/state.d.ts.map +1 -0
  251. package/dist/views/state.js +45 -0
  252. package/dist/views/state.js.map +1 -0
  253. package/dist/views/tools.d.ts +3 -0
  254. package/dist/views/tools.d.ts.map +1 -0
  255. package/dist/views/tools.js +11 -0
  256. package/dist/views/tools.js.map +1 -0
  257. package/dist/views/workCards.d.ts +4 -0
  258. package/dist/views/workCards.d.ts.map +1 -0
  259. package/dist/views/workCards.js +23 -0
  260. package/dist/views/workCards.js.map +1 -0
  261. package/dist/views/workControls.d.ts +4 -0
  262. package/dist/views/workControls.d.ts.map +1 -0
  263. package/dist/views/workControls.js +32 -0
  264. package/dist/views/workControls.js.map +1 -0
  265. package/dist/views/workDetail.d.ts +13 -0
  266. package/dist/views/workDetail.d.ts.map +1 -0
  267. package/dist/views/workDetail.js +26 -0
  268. package/dist/views/workDetail.js.map +1 -0
  269. package/dist/views/workHtml.d.ts +15 -0
  270. package/dist/views/workHtml.d.ts.map +1 -0
  271. package/dist/views/workHtml.js +35 -0
  272. package/dist/views/workHtml.js.map +1 -0
  273. package/dist/views/workStyle.d.ts +2 -0
  274. package/dist/views/workStyle.d.ts.map +1 -0
  275. package/dist/views/workStyle.js +18 -0
  276. package/dist/views/workStyle.js.map +1 -0
  277. package/dist/views/workTable.d.ts +4 -0
  278. package/dist/views/workTable.d.ts.map +1 -0
  279. package/dist/views/workTable.js +19 -0
  280. package/dist/views/workTable.js.map +1 -0
  281. package/package.json +42 -33
  282. package/src/board.ts +25 -12
  283. package/src/cli.ts +3 -3
  284. package/src/files/changes.ts +7 -6
  285. package/src/files/list.ts +3 -3
  286. package/src/files/url.ts +3 -0
  287. package/src/http/assets.ts +21 -6
  288. package/src/http/events.ts +2 -2
  289. package/src/http/identity.ts +62 -0
  290. package/src/http/loopback.ts +2 -2
  291. package/src/http/page.ts +56 -19
  292. package/src/http/search.ts +13 -0
  293. package/src/http/work.ts +41 -0
  294. package/src/metadata/backlinks.ts +39 -0
  295. package/src/metadata/fields.ts +26 -0
  296. package/src/metadata/links.ts +19 -0
  297. package/src/metadata/locations.ts +34 -0
  298. package/src/metadata/model.ts +27 -0
  299. package/src/metadata/parse.ts +62 -0
  300. package/src/metadata/problems.ts +62 -0
  301. package/src/metadata/relationshipLabel.ts +13 -0
  302. package/src/metadata/schema.ts +36 -0
  303. package/src/metadata/sourceLinks.ts +46 -0
  304. package/src/page/backlinksHtml.ts +18 -0
  305. package/src/page/board.ts +16 -6
  306. package/src/page/client.ts +65 -7
  307. package/src/page/criteriaEvidence.ts +36 -0
  308. package/src/page/diagrams.ts +12 -4
  309. package/src/page/document.ts +8 -5
  310. package/src/page/escape.ts +2 -2
  311. package/src/page/libraryScript.ts +72 -0
  312. package/src/page/metadata.ts +40 -0
  313. package/src/page/metadataDetails.ts +24 -0
  314. package/src/page/metadataLinks.ts +35 -0
  315. package/src/page/nav.ts +9 -8
  316. package/src/page/navigationScript.ts +103 -0
  317. package/src/page/readingStateScript.ts +61 -0
  318. package/src/page/readingStyles.ts +28 -0
  319. package/src/page/readingTools.ts +12 -0
  320. package/src/page/recordedEvidence.ts +12 -0
  321. package/src/page/searchDialog.ts +16 -0
  322. package/src/page/searchResultsScript.ts +63 -0
  323. package/src/page/searchScript.ts +72 -0
  324. package/src/page/shell.ts +24 -5
  325. package/src/page/stateScript.ts +48 -0
  326. package/src/page/style.ts +14 -35
  327. package/src/page/swap.ts +4 -1
  328. package/src/page/workspaceStyles.ts +79 -0
  329. package/src/render/board.ts +19 -19
  330. package/src/render/code.ts +5 -5
  331. package/src/render/documentHeadings.ts +52 -0
  332. package/src/render/documentLinks.ts +19 -0
  333. package/src/render/footnotes.ts +2 -2
  334. package/src/render/highlighter.ts +2 -2
  335. package/src/render/markdown.ts +21 -4
  336. package/src/search/collector.ts +44 -0
  337. package/src/search/entries.ts +51 -0
  338. package/src/search/linkCollector.ts +16 -0
  339. package/src/search/match.ts +28 -0
  340. package/src/search/metadataEntries.ts +42 -0
  341. package/src/search/snapshot.ts +60 -0
  342. package/src/serve.ts +1 -1
  343. package/src/views/items.ts +55 -0
  344. package/src/views/projection.ts +59 -0
  345. package/src/views/savedScript.ts +88 -0
  346. package/src/views/state.ts +47 -0
  347. package/src/views/tools.ts +11 -0
  348. package/src/views/workCards.ts +26 -0
  349. package/src/views/workControls.ts +38 -0
  350. package/src/views/workDetail.ts +28 -0
  351. package/src/views/workHtml.ts +39 -0
  352. package/src/views/workStyle.ts +17 -0
  353. package/src/views/workTable.ts +21 -0
  354. package/dist/index.d.ts +0 -2
  355. package/dist/index.d.ts.map +0 -1
  356. package/dist/index.js +0 -2
  357. package/dist/index.js.map +0 -1
  358. package/src/index.ts +0 -1
package/package.json CHANGED
@@ -1,57 +1,57 @@
1
1
  {
2
2
  "name": "@shivaedev/work-board",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Serve a folder of markdown files as a live local page that updates in place when a file changes",
5
- "type": "module",
6
- "license": "MIT",
5
+ "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/work-board#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/ShivaeDev/platform/issues"
8
+ },
7
9
  "repository": {
8
10
  "type": "git",
9
11
  "url": "git+https://github.com/ShivaeDev/platform.git",
10
12
  "directory": "packages/work-board"
11
13
  },
12
- "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/work-board#readme",
13
- "bugs": {
14
- "url": "https://github.com/ShivaeDev/platform/issues"
14
+ "license": "MIT",
15
+ "sideEffects": false,
16
+ "type": "module",
17
+ "imports": {
18
+ "#test/*": "./test/*",
19
+ "#*.ts": {
20
+ "source": "./src/*.ts",
21
+ "types": "./dist/*.d.ts",
22
+ "default": "./dist/*.js"
23
+ }
15
24
  },
16
- "engines": {
17
- "node": ">=24"
25
+ "exports": {
26
+ "./*.ts": {
27
+ "types": "./dist/*.d.ts",
28
+ "source": "./src/*.ts",
29
+ "import": "./dist/*.js",
30
+ "default": "./dist/*.js"
31
+ },
32
+ "./package.json": "./package.json"
18
33
  },
19
- "sideEffects": false,
20
34
  "bin": {
21
35
  "work-board": "./dist/cli.js"
22
36
  },
23
37
  "files": [
24
38
  "dist",
25
39
  "src",
40
+ "!src/**/*.test.ts",
41
+ "!src/**/*.spec.ts",
26
42
  "CHANGELOG.md",
27
43
  "README.md"
28
44
  ],
29
- "exports": {
30
- ".": {
31
- "types": "./dist/index.d.ts",
32
- "source": "./src/index.ts",
33
- "import": "./dist/index.js",
34
- "default": "./dist/index.js"
35
- },
36
- "./package.json": "./package.json"
37
- },
38
- "publishConfig": {
39
- "access": "public",
40
- "provenance": true
41
- },
42
45
  "dependencies": {
43
46
  "mermaid": "11.16.1",
44
47
  "satteri": "0.9.5",
45
- "shiki": "4.4.2"
46
- },
47
- "peerDependencies": {
48
- "@effect/platform-node": "4.0.0-rc.112",
49
- "@effect/platform-node-shared": "4.0.0-rc.112",
50
- "effect": "4.0.0-rc.112"
48
+ "shiki": "4.4.2",
49
+ "yaml": "2.9.0"
51
50
  },
52
51
  "devDependencies": {
53
52
  "@effect/platform-node": "4.0.0-rc.112",
54
53
  "@effect/platform-node-shared": "4.0.0-rc.112",
54
+ "@shivaedev/quality": "0.9.0",
55
55
  "@types/hast": "3.0.5",
56
56
  "@types/mdast": "4.0.4",
57
57
  "@types/node": "24.10.1",
@@ -61,13 +61,22 @@
61
61
  "typescript": "npm:@typescript/typescript6@6.0.2",
62
62
  "vitest": "4.1.9"
63
63
  },
64
+ "peerDependencies": {
65
+ "@effect/platform-node": "4.0.0-rc.112",
66
+ "@effect/platform-node-shared": "4.0.0-rc.112",
67
+ "effect": "4.0.0-rc.112"
68
+ },
69
+ "engines": {
70
+ "node": ">=24"
71
+ },
72
+ "publishConfig": {
73
+ "access": "public",
74
+ "provenance": true
75
+ },
64
76
  "scripts": {
65
77
  "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc6 --project tsconfig.build.json",
66
- "check": "biome check .",
78
+ "ready": "pnpm typecheck && pnpm test && pnpm build",
67
79
  "test": "vitest run",
68
- "test:package": "node scripts/test-package.mjs",
69
- "typecheck": "tsc --noEmit",
70
- "typecheck:compat": "tsc6 --noEmit",
71
- "ready": "pnpm check && pnpm typecheck && pnpm typecheck:compat && pnpm test && pnpm build && pnpm test:package"
80
+ "typecheck": "tsc --noEmit"
72
81
  }
73
82
  }
package/src/board.ts CHANGED
@@ -1,17 +1,22 @@
1
- import { Effect, type FileSystem, Layer, type Path } from "effect";
1
+ import { Effect, type FileSystem, Layer, type Path, type PlatformError } from "effect";
2
2
  import { HttpRouter, type HttpServerRequest, type HttpServerResponse } from "effect/unstable/http";
3
- import { watchChanges } from "./files/changes.ts";
4
- import { homeIn } from "./files/home.ts";
5
- import { ASSETS, MERMAID_ROUTE, mermaidFile, mermaidRoot } from "./http/assets.ts";
6
- import { events } from "./http/events.ts";
7
- import { loopbackOnly } from "./http/loopback.ts";
8
- import { page } from "./http/page.ts";
9
- import { respond } from "./http/respond.ts";
10
- import { Highlighter } from "./render/highlighter.ts";
3
+ import { watchChanges } from "#files/changes.ts";
4
+ import { type HomeMissing, homeIn } from "#files/home.ts";
5
+ import { ASSETS, MERMAID_ROUTE, type MermaidMissing, mermaidFile, mermaidRoot } from "#http/assets.ts";
6
+ import { events } from "#http/events.ts";
7
+ import { identity } from "#http/identity.ts";
8
+ import { loopbackOnly } from "#http/loopback.ts";
9
+ import { page } from "#http/page.ts";
10
+ import { respond } from "#http/respond.ts";
11
+ import { search } from "#http/search.ts";
12
+ import { work } from "#http/work.ts";
13
+ import type { RenderFailed } from "#render/failed.ts";
14
+ import { Highlighter } from "#render/highlighter.ts";
15
+ import { searchSnapshot } from "#search/snapshot.ts";
11
16
 
12
17
  export interface BoardOptions {
13
- readonly root: string;
14
18
  readonly home?: string | undefined;
19
+ readonly root: string;
15
20
  }
16
21
 
17
22
  type Services = Highlighter | FileSystem.FileSystem | Path.Path;
@@ -31,14 +36,22 @@ const routes = (options: BoardOptions) =>
31
36
  loopbackOnly((request) => Effect.provideContext(handler(request), context)),
32
37
  );
33
38
  yield* serve("/events", events(changes));
39
+ const index = yield* searchSnapshot(options.root, home, changes);
40
+ yield* serve("/_board/search", search(index));
41
+ yield* serve("/_board/work", work(index, changes, home));
34
42
  for (const [route, body, contentType] of ASSETS) {
35
43
  yield* serve(route, () => Effect.succeed(respond(body, contentType)));
36
44
  }
37
45
  yield* serve(MERMAID_ROUTE, mermaidFile(mermaid));
38
- const pages = page({ root: options.root, home }, changes);
46
+ const pages = page({ home, root: options.root }, changes, index);
47
+ yield* serve("/_board/item/*", identity(index, pages, changes, home));
39
48
  yield* serve("/", pages);
40
49
  yield* serve("/*", pages);
41
50
  }),
42
51
  );
43
52
 
44
- export const boardLayer = (options: BoardOptions) => routes(options).pipe(Layer.provide(Highlighter.layer));
53
+ type BoardError = HomeMissing | MermaidMissing | PlatformError.PlatformError | RenderFailed;
54
+ type BoardServices = FileSystem.FileSystem | HttpRouter.HttpRouter | Path.Path | HttpRouter.Request<"Error", PlatformError.PlatformError>;
55
+
56
+ export const boardLayer = (options: BoardOptions): Layer.Layer<never, BoardError, BoardServices> =>
57
+ routes(options).pipe(Layer.provide(Highlighter.layer));
package/src/cli.ts CHANGED
@@ -4,19 +4,19 @@ import { Effect, Layer, Option, Path } from "effect";
4
4
  import { Argument, Command, Flag } from "effect/unstable/cli";
5
5
  import { HOST, serveBoard } from "./serve.ts";
6
6
 
7
- const VERSION = "0.2.0";
7
+ const VERSION = "0.3.0";
8
8
 
9
9
  const serve = Effect.fn("WorkBoard.serve")(function* (input: { readonly dir: string; readonly port: number; readonly home: Option.Option<string> }) {
10
10
  const path = yield* Path.Path;
11
- return yield* Layer.launch(serveBoard({ root: path.resolve(input.dir), home: Option.getOrUndefined(input.home), port: input.port }));
11
+ return yield* Layer.launch(serveBoard({ home: Option.getOrUndefined(input.home), port: input.port, root: path.resolve(input.dir) }));
12
12
  });
13
13
 
14
14
  const workBoard = Command.make(
15
15
  "work-board",
16
16
  {
17
17
  dir: Argument.directory("dir", { mustExist: true }).pipe(Argument.withDescription("The folder of markdown files to serve")),
18
- port: Flag.integer("port").pipe(Flag.withDefault(4747), Flag.withDescription(`The port to listen on at ${HOST}`)),
19
18
  home: Flag.string("home").pipe(Flag.optional, Flag.withDescription("The file shown as a board at /, relative to the folder")),
19
+ port: Flag.integer("port").pipe(Flag.withDefault(4747), Flag.withDescription(`The port to listen on at ${HOST}`)),
20
20
  },
21
21
  serve,
22
22
  ).pipe(Command.withDescription("Serve a folder of markdown files as a live page that updates in place when a file changes."));
@@ -1,18 +1,19 @@
1
1
  import { Data, Deferred, Effect, FileSystem, Option, Path, type PlatformError, PubSub, Ref, Schedule, Stream } from "effect";
2
2
  import { isMarkdown, listMarkdown, type MarkdownFile } from "./list.ts";
3
3
 
4
- export type Change = { readonly _tag: "Changed"; readonly paths: ReadonlyArray<string> } | { readonly _tag: "Watching"; readonly watching: boolean };
4
+ export type Change = { readonly _tag: "Changed"; readonly paths: readonly string[] } | { readonly _tag: "Watching"; readonly watching: boolean };
5
5
 
6
6
  export interface Changes {
7
- readonly realRoot: string;
8
7
  readonly events: PubSub.PubSub<Change>;
8
+ readonly files: Effect.Effect<readonly MarkdownFile[], PlatformError.PlatformError>;
9
+ readonly realRoot: string;
10
+ readonly revision: Effect.Effect<number>;
9
11
  readonly watching: Ref.Ref<boolean>;
10
- readonly files: Effect.Effect<ReadonlyArray<MarkdownFile>, PlatformError.PlatformError>;
11
12
  }
12
13
 
13
14
  interface Cached {
15
+ readonly files: readonly MarkdownFile[];
14
16
  readonly generation: number;
15
- readonly files: ReadonlyArray<MarkdownFile>;
16
17
  }
17
18
 
18
19
  class WatchEnded extends Data.TaggedError("WatchEnded") {}
@@ -63,9 +64,9 @@ export const watchChanges = Effect.fn("WorkBoard.watchChanges")(function* (root:
63
64
  return hit.value.files;
64
65
  }
65
66
  const listed = yield* listMarkdown(root, realRoot).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
66
- yield* Ref.set(cached, Option.some({ generation: now, files: listed }));
67
+ yield* Ref.set(cached, Option.some({ files: listed, generation: now }));
67
68
  return listed;
68
69
  });
69
- const changes: Changes = { realRoot, events, watching, files };
70
+ const changes: Changes = { events, files, realRoot, revision: Ref.get(generation), watching };
70
71
  return changes;
71
72
  });
package/src/files/list.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Effect, FileSystem, Option, Path, type PlatformError } from "effect";
2
2
 
3
3
  export interface MarkdownFile {
4
- readonly path: string;
5
4
  readonly modified: number;
5
+ readonly path: string;
6
6
  }
7
7
 
8
8
  const skipped = (segment: string): boolean => segment.startsWith(".") || segment === "node_modules";
@@ -15,7 +15,7 @@ const folderOf = (path: string): string => path.slice(0, Math.max(0, path.lastIn
15
15
 
16
16
  const byFolder = (left: string, right: string): number => folderOf(left).localeCompare(folderOf(right)) || left.localeCompare(right);
17
17
 
18
- type Listing = Effect.Effect<ReadonlyArray<MarkdownFile>, PlatformError.PlatformError>;
18
+ type Listing = Effect.Effect<readonly MarkdownFile[], PlatformError.PlatformError>;
19
19
 
20
20
  export const listMarkdown = Effect.fn("WorkBoard.listMarkdown")(function* (root: string, realRoot: string) {
21
21
  const fs = yield* FileSystem.FileSystem;
@@ -35,7 +35,7 @@ export const listMarkdown = Effect.fn("WorkBoard.listMarkdown")(function* (root:
35
35
  return yield* Effect.orElseSucceed(folder(relative, real.value), () => []);
36
36
  }
37
37
  const modified = Option.match(info.value.mtime, { onNone: () => 0, onSome: (date) => date.getTime() });
38
- return info.value.type === "File" && relative.endsWith(".md") ? [{ path: relative, modified }] : [];
38
+ return info.value.type === "File" && relative.endsWith(".md") ? [{ modified, path: relative }] : [];
39
39
  });
40
40
  const folder = (relative: string, real: string): Listing =>
41
41
  Effect.gen(function* () {
@@ -0,0 +1,3 @@
1
+ export function fileUrl(file: string): string {
2
+ return `/${file.split("/").map(encodeURIComponent).join("/")}`;
3
+ }
@@ -1,15 +1,30 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { Data, Effect, FileSystem, Path } from "effect";
3
3
  import type { HttpRouter, HttpServerRequest } from "effect/unstable/http";
4
- import { client } from "../page/client.ts";
5
- import { diagrams } from "../page/diagrams.ts";
6
- import { style } from "../page/style.ts";
7
- import { swap } from "../page/swap.ts";
4
+ import { client, preferences } from "#page/client.ts";
5
+ import { diagrams } from "#page/diagrams.ts";
6
+ import { libraryScript } from "#page/libraryScript.ts";
7
+ import { navigationScript } from "#page/navigationScript.ts";
8
+ import { readingStateScript } from "#page/readingStateScript.ts";
9
+ import { searchResultsScript } from "#page/searchResultsScript.ts";
10
+ import { searchScript } from "#page/searchScript.ts";
11
+ import { pageStateScript } from "#page/stateScript.ts";
12
+ import { style } from "#page/style.ts";
13
+ import { swap } from "#page/swap.ts";
14
+ import { savedScript } from "#views/savedScript.ts";
8
15
  import { respond } from "./respond.ts";
9
16
 
10
17
  export const ASSETS: ReadonlyArray<readonly [HttpRouter.PathInput, string, string]> = [
11
18
  ["/_board/style.css", style, "text/css"],
19
+ ["/_board/saved-views.js", savedScript(), "text/javascript"],
12
20
  ["/_board/client.js", client, "text/javascript"],
21
+ ["/_board/preferences.js", preferences, "text/javascript"],
22
+ ["/_board/search.js", searchScript(), "text/javascript"],
23
+ ["/_board/search-results.js", searchResultsScript(), "text/javascript"],
24
+ ["/_board/library.js", libraryScript(), "text/javascript"],
25
+ ["/_board/navigation.js", navigationScript(), "text/javascript"],
26
+ ["/_board/page-state.js", pageStateScript(), "text/javascript"],
27
+ ["/_board/reading-state.js", readingStateScript(), "text/javascript"],
13
28
  ["/_board/diagrams.js", diagrams, "text/javascript"],
14
29
  ["/_board/swap.js", swap, "text/javascript"],
15
30
  ];
@@ -20,7 +35,7 @@ export class MermaidMissing extends Data.TaggedError("MermaidMissing")<{ readonl
20
35
 
21
36
  export const mermaidRoot = Effect.fn("WorkBoard.mermaidRoot")(function* () {
22
37
  const path = yield* Path.Path;
23
- const entry = yield* Effect.try({ try: () => createRequire(import.meta.url).resolve("mermaid"), catch: (cause) => new MermaidMissing({ cause }) });
38
+ const entry = yield* Effect.try({ catch: (cause) => new MermaidMissing({ cause }), try: () => createRequire(import.meta.url).resolve("mermaid") });
24
39
  return path.dirname(entry);
25
40
  });
26
41
 
@@ -29,7 +44,7 @@ export const mermaidFile = (mermaidRoot: string) =>
29
44
  const fs = yield* FileSystem.FileSystem;
30
45
  const path = yield* Path.Path;
31
46
  const file = path.resolve(mermaidRoot, new URL(request.url, "http://127.0.0.1").pathname.slice(MERMAID.length));
32
- if (!file.startsWith(`${mermaidRoot}${path.sep}`) || !file.endsWith(".mjs") || !(yield* fs.exists(file))) {
47
+ if (!(file.startsWith(`${mermaidRoot}${path.sep}`) && file.endsWith(".mjs") && (yield* fs.exists(file)))) {
33
48
  return respond("Not found", "text/plain", 404);
34
49
  }
35
50
  return respond(yield* fs.readFile(file), "text/javascript");
@@ -1,10 +1,10 @@
1
1
  import { Effect, PubSub, Ref, Stream } from "effect";
2
2
  import { Sse } from "effect/unstable/encoding";
3
3
  import { HttpServerResponse } from "effect/unstable/http";
4
- import type { Change, Changes } from "../files/changes.ts";
4
+ import type { Change, Changes } from "#files/changes.ts";
5
5
  import { HEADERS } from "./respond.ts";
6
6
 
7
- const event = (name: string, data = ""): Sse.Event => ({ _tag: "Event", event: name, id: undefined, data });
7
+ const event = (name: string, data = ""): Sse.Event => ({ _tag: "Event", data, event: name, id: undefined });
8
8
 
9
9
  const watching = (up: boolean): Sse.Event => event(up ? "ready" : "down");
10
10
 
@@ -0,0 +1,62 @@
1
+ import { Effect } from "effect";
2
+ import type { HttpServerRequest } from "effect/unstable/http";
3
+ import type { Changes } from "#files/changes.ts";
4
+ import { fileUrl } from "#files/url.ts";
5
+ import { escapeHtml } from "#page/escape.ts";
6
+ import { navHtml } from "#page/nav.ts";
7
+ import { shell } from "#page/shell.ts";
8
+ import type { searchSnapshot } from "#search/snapshot.ts";
9
+ import type { page } from "./page.ts";
10
+ import { respond } from "./respond.ts";
11
+
12
+ function requestedIdentity(pathname: string): string {
13
+ try {
14
+ return decodeURIComponent(pathname.slice("/_board/item/".length).replace(/\/$/u, ""));
15
+ } catch {
16
+ return "";
17
+ }
18
+ }
19
+
20
+ export function identity(
21
+ index: Effect.Success<ReturnType<typeof searchSnapshot>>,
22
+ pages: ReturnType<typeof page>,
23
+ changes: Changes,
24
+ home: string | undefined,
25
+ ) {
26
+ return Effect.fn("WorkBoard.identity")(function* (request: HttpServerRequest.HttpServerRequest) {
27
+ const pathname = new URL(request.url, "http://127.0.0.1").pathname;
28
+ const id = requestedIdentity(pathname);
29
+ if (!pathname.endsWith("/") && /\.md$/iu.exec(id) !== null) {
30
+ return yield* pages(request);
31
+ }
32
+ const snapshot = yield* index;
33
+ const matches = snapshot.model.ids.get(id) ?? [];
34
+ if (snapshot.unavailable.length > 0) {
35
+ return respond(
36
+ shell(
37
+ `Item ${id}`,
38
+ "",
39
+ `<p class="empty">The workspace index is incomplete. Could not read ${escapeHtml(snapshot.unavailable.join(", "))}; item identity could not be validated. Retry when the files are readable.</p>`,
40
+ changes.realRoot,
41
+ false,
42
+ ),
43
+ "text/html",
44
+ 503,
45
+ );
46
+ }
47
+ if (matches.length === 1 && matches[0]) {
48
+ return yield* pages(request, matches[0].file, id);
49
+ }
50
+ const text =
51
+ matches.length > 0
52
+ ? `The ID ${id} is duplicated. No document was selected.`
53
+ : `No item with ID ${id} is available. It may be missing, renamed, or invalid.`;
54
+ const choices = matches.map((document) => `<li><a href="${escapeHtml(fileUrl(document.file))}">${escapeHtml(document.file)}</a></li>`).join("");
55
+ const nav = navHtml(yield* changes.files, "", home);
56
+ return respond(
57
+ shell(`Item ${id}`, nav, `<p class="empty">${escapeHtml(text)}</p>${choices ? `<ul>${choices}</ul>` : ""}`, changes.realRoot, false),
58
+ "text/html",
59
+ matches.length > 0 ? 409 : 404,
60
+ );
61
+ });
62
+ }
@@ -13,8 +13,8 @@ const hostnameOf = (host: string): string | undefined => {
13
13
  };
14
14
 
15
15
  const loopbackAddress = (address: string): boolean => {
16
- const unmapped = address.replace(/^::ffff:/i, "");
17
- return unmapped === "::1" || /^127(?:\.\d{1,3}){3}$/.test(unmapped);
16
+ const unmapped = address.replace(/^::ffff:/iu, "");
17
+ return unmapped === "::1" || /^127(?:\.\d{1,3}){3}$/u.test(unmapped);
18
18
  };
19
19
 
20
20
  const fromLoopback = (request: HttpServerRequest.HttpServerRequest): boolean => {
package/src/http/page.ts CHANGED
@@ -1,22 +1,26 @@
1
1
  import { Effect, FileSystem, Option, Path } from "effect";
2
2
  import type { HttpServerRequest } from "effect/unstable/http";
3
- import type { Changes } from "../files/changes.ts";
4
- import { type MarkdownFile, within } from "../files/list.ts";
5
- import { boardHtml } from "../page/board.ts";
6
- import { documentHtml } from "../page/document.ts";
7
- import { escapeHtml } from "../page/escape.ts";
8
- import { navHtml } from "../page/nav.ts";
9
- import { shell } from "../page/shell.ts";
10
- import { boardOf } from "../render/board.ts";
3
+ import type { Changes } from "#files/changes.ts";
4
+ import { type MarkdownFile, within } from "#files/list.ts";
5
+ import { metadataModel } from "#metadata/model.ts";
6
+ import { metadataParse } from "#metadata/parse.ts";
7
+ import { boardHtml } from "#page/board.ts";
8
+ import { documentHtml } from "#page/document.ts";
9
+ import { escapeHtml } from "#page/escape.ts";
10
+ import { metadataHtml } from "#page/metadata.ts";
11
+ import { navHtml } from "#page/nav.ts";
12
+ import { shell } from "#page/shell.ts";
13
+ import { boardOf } from "#render/board.ts";
14
+ import type { searchSnapshot } from "#search/snapshot.ts";
11
15
  import { respond } from "./respond.ts";
12
16
 
13
17
  export interface PageOptions {
14
- readonly root: string;
15
18
  readonly home: string | undefined;
19
+ readonly root: string;
16
20
  }
17
21
 
18
22
  const requestedPath = (url: string): string => {
19
- const pathname = new URL(url, "http://127.0.0.1").pathname.replace(/^\/+/, "");
23
+ const pathname = new URL(url, "http://127.0.0.1").pathname.replace(/^\/+/u, "");
20
24
  try {
21
25
  return decodeURIComponent(pathname);
22
26
  } catch {
@@ -26,8 +30,8 @@ const requestedPath = (url: string): string => {
26
30
 
27
31
  const message = (text: string): string => `<p class="empty">${escapeHtml(text)}</p>`;
28
32
 
29
- export const page = (options: PageOptions, changes: Changes) => {
30
- const body = Effect.fn("WorkBoard.pageBody")(function* (file: MarkdownFile) {
33
+ export const page = (options: PageOptions, changes: Changes, index: Effect.Success<ReturnType<typeof searchSnapshot>>) => {
34
+ const body = Effect.fn("WorkBoard.pageBody")(function* (file: MarkdownFile, expectedIdentity?: string) {
31
35
  const fs = yield* FileSystem.FileSystem;
32
36
  const path = yield* Path.Path;
33
37
  const real = yield* Effect.option(fs.realPath(path.join(options.root, file.path)));
@@ -35,20 +39,53 @@ export const page = (options: PageOptions, changes: Changes) => {
35
39
  return Option.none();
36
40
  }
37
41
  const source = yield* fs.readFileString(real.value);
38
- return Option.some(file.path === options.home ? yield* boardHtml(boardOf(source), file.path, file.modified) : yield* documentHtml(source, file));
42
+ const parsed = metadataParse(source);
43
+ let identity: string | undefined;
44
+ const snapshot = yield* index;
45
+ const model = metadataModel(
46
+ [...snapshot.documents.filter((document) => document.file !== file.path), { file: file.path, parsed }],
47
+ snapshot.unavailable,
48
+ options.home ?? (yield* changes.files)[0]?.path,
49
+ );
50
+ const metadata = metadataHtml(parsed, file.path, model);
51
+ if (snapshot.unavailable.length === 0 && parsed.fields.id && model.ids.get(parsed.fields.id)?.length === 1) {
52
+ identity = parsed.fields.id;
53
+ }
54
+ if (expectedIdentity && identity !== expectedIdentity) {
55
+ return Option.none();
56
+ }
57
+ const html =
58
+ file.path === options.home
59
+ ? yield* boardHtml(boardOf(parsed.body), file.path, file.modified, metadata)
60
+ : yield* documentHtml(parsed.body, file, metadata);
61
+ return Option.some({ html, identity });
39
62
  });
40
- return Effect.fn("WorkBoard.page")(function* (request: HttpServerRequest.HttpServerRequest) {
63
+ return Effect.fn("WorkBoard.page")(function* (request: HttpServerRequest.HttpServerRequest, resolved?: string, expectedIdentity?: string) {
41
64
  const files = yield* changes.files;
42
- const requested = requestedPath(request.url) || options.home || files[0]?.path || "";
65
+ const requested = resolved ?? (requestedPath(request.url) || options.home || files[0]?.path || "");
43
66
  const file = files.find((candidate) => candidate.path === requested);
44
67
  const nav = navHtml(files, requested, options.home);
45
- const missing = () => respond(shell(requested, nav, message(`No markdown file at ${requested || options.root}.`)), "text/html", 404);
68
+ function layout(content: string, identity?: string) {
69
+ return shell(requested, nav, content, changes.realRoot, requested === options.home, identity);
70
+ }
71
+ const missing = () =>
72
+ respond(
73
+ layout(
74
+ message(
75
+ expectedIdentity
76
+ ? `No item with ID ${expectedIdentity} at ${requested}; its source identity changed or is invalid.`
77
+ : `No markdown file at ${requested || options.root}.`,
78
+ ),
79
+ ),
80
+ "text/html",
81
+ 404,
82
+ );
46
83
  if (file === undefined) {
47
84
  return missing();
48
85
  }
49
- return yield* body(file).pipe(
50
- Effect.map(Option.match({ onNone: missing, onSome: (main) => respond(shell(file.path, nav, main), "text/html") })),
51
- Effect.catch(() => Effect.succeed(respond(shell(file.path, nav, message(`${file.path} could not be read.`)), "text/html", 500))),
86
+ return yield* body(file, expectedIdentity).pipe(
87
+ Effect.map(Option.match({ onNone: missing, onSome: (main) => respond(layout(main.html, main.identity), "text/html") })),
88
+ Effect.catch(() => Effect.succeed(respond(layout(message(`${file.path} could not be read.`)), "text/html", 500))),
52
89
  );
53
90
  });
54
91
  };
@@ -0,0 +1,13 @@
1
+ import { Effect } from "effect";
2
+ import type { HttpServerRequest } from "effect/unstable/http";
3
+ import { searchMatch } from "#search/match.ts";
4
+ import type { searchSnapshot } from "#search/snapshot.ts";
5
+ import { respond } from "./respond.ts";
6
+
7
+ export function search(index: Effect.Success<ReturnType<typeof searchSnapshot>>) {
8
+ return Effect.fn("WorkBoard.search")(function* (request: HttpServerRequest.HttpServerRequest) {
9
+ const query = new URL(request.url, "http://127.0.0.1").searchParams.get("q") ?? "";
10
+ const snapshot = yield* index;
11
+ return respond(JSON.stringify({ ...searchMatch(snapshot.entries, query), unavailable: snapshot.unavailable }), "application/json");
12
+ });
13
+ }
@@ -0,0 +1,41 @@
1
+ import { Effect } from "effect";
2
+ import type { HttpServerRequest } from "effect/unstable/http";
3
+ import type { Changes } from "#files/changes.ts";
4
+ import { escapeHtml } from "#page/escape.ts";
5
+ import { navHtml } from "#page/nav.ts";
6
+ import { shell } from "#page/shell.ts";
7
+ import type { searchSnapshot } from "#search/snapshot.ts";
8
+ import { stateOf } from "#views/state.ts";
9
+ import { workHtml } from "#views/workHtml.ts";
10
+ import { respond } from "./respond.ts";
11
+
12
+ export function work(index: Effect.Success<ReturnType<typeof searchSnapshot>>, changes: Changes, home: string | undefined) {
13
+ return Effect.fn("WorkBoard.work")(function* (request: HttpServerRequest.HttpServerRequest) {
14
+ const snapshot = yield* index;
15
+ const nav = navHtml(yield* changes.files, "", home);
16
+ if (snapshot.unavailable.length > 0) {
17
+ return respond(
18
+ shell(
19
+ "Work",
20
+ nav,
21
+ `<h1>Workspace incomplete</h1><p class="empty">Could not read ${escapeHtml(snapshot.unavailable.join(", "))}. Work identity and view counts cannot be validated. Read available source files or retry when the workspace is readable.</p>`,
22
+ changes.realRoot,
23
+ true,
24
+ undefined,
25
+ true,
26
+ ),
27
+ "text/html",
28
+ 503,
29
+ );
30
+ }
31
+ const result = yield* workHtml(snapshot, stateOf(request.url)).pipe(
32
+ Effect.catch(() =>
33
+ Effect.succeed({
34
+ html: '<h1>Work unavailable</h1><p class="empty">The selected source could not be rendered. Read its Markdown file or retry.</p>',
35
+ status: 500,
36
+ }),
37
+ ),
38
+ );
39
+ return respond(shell("Work", nav, result.html, changes.realRoot, true, undefined, true), "text/html", result.status);
40
+ });
41
+ }
@@ -0,0 +1,39 @@
1
+ import type { MetadataDocument, MetadataModel } from "./model.ts";
2
+ import { referenceFile, sourceFile } from "./sourceLinks.ts";
3
+
4
+ export interface Backlink {
5
+ readonly file: string;
6
+ readonly kind: string;
7
+ readonly line?: number;
8
+ }
9
+
10
+ function references(document: MetadataDocument, model: MetadataModel) {
11
+ const { fields, lines } = document.parsed;
12
+ return [
13
+ ...(fields.relationships?.map((link, index) => ({
14
+ kind: link.kind,
15
+ line: lines[`relationships.${index}.target`],
16
+ target: referenceFile(link.target, model),
17
+ })) ?? []),
18
+ ...(fields.items?.map((item, index) => ({ kind: "board member", line: lines[`items.${index}`], target: referenceFile(item, model) })) ?? []),
19
+ ...(fields.evidence?.flatMap((evidence, index) => [
20
+ { kind: "evidence source", line: lines[`evidence.${index}.source`], target: sourceFile(evidence.source, document.file, model) },
21
+ {
22
+ kind: "criterion evidence",
23
+ line: lines[`evidence.${index}.criterion`],
24
+ target: evidence.criterion ? referenceFile(evidence.criterion, model) : undefined,
25
+ },
26
+ ]) ?? []),
27
+ ...(document.links?.map((link) => ({ kind: "Markdown link", line: undefined, target: sourceFile(link, document.file, model) })) ?? []),
28
+ ];
29
+ }
30
+
31
+ export function backlinksOf(file: string, model: MetadataModel): readonly Backlink[] {
32
+ return model.documents.flatMap((document) =>
33
+ document.file === file
34
+ ? []
35
+ : references(document, model)
36
+ .filter((reference) => reference.target === file)
37
+ .map((reference) => ({ file: document.file, kind: reference.kind, ...(reference.line === undefined ? {} : { line: reference.line }) })),
38
+ );
39
+ }
@@ -0,0 +1,26 @@
1
+ import { Option, Schema } from "effect";
2
+ import type { Diagnostic } from "./parse.ts";
3
+ import { Metadata } from "./schema.ts";
4
+
5
+ const schemas: ReadonlyMap<string, Schema.ConstraintDecoder<unknown, never>> = new Map(Object.entries(Metadata.to.fields));
6
+ const sourceKeys = new Map([["next_action", "nextAction"]]);
7
+
8
+ export function metadataFields(data: object, lines: Readonly<Record<string, number>>) {
9
+ const fields: Record<string, unknown> = {};
10
+ const diagnostics: Diagnostic[] = [];
11
+ for (const [field, value] of Object.entries(data)) {
12
+ const key = sourceKeys.get(field) ?? field;
13
+ const schema = field === "nextAction" ? undefined : schemas.get(key);
14
+ if (!schema) {
15
+ diagnostics.push({ field, line: lines[field] ?? 2, message: `Unknown field ${field}; kept in the original frontmatter.` });
16
+ continue;
17
+ }
18
+ const decoded = Schema.decodeUnknownOption(schema, { onExcessProperty: "error" })(value);
19
+ if (Option.isNone(decoded)) {
20
+ diagnostics.push({ field, line: lines[field] ?? 2, message: `Invalid ${field}; kept in the original frontmatter and not interpreted.` });
21
+ } else {
22
+ fields[key] = decoded.value;
23
+ }
24
+ }
25
+ return { diagnostics, fields: Schema.decodeUnknownSync(Schema.toType(Metadata))(fields) };
26
+ }