@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.
- package/CHANGELOG.md +86 -0
- package/README.md +141 -3
- package/dist/board.d.ts +9 -3
- package/dist/board.d.ts.map +1 -1
- package/dist/board.js +17 -9
- package/dist/board.js.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/files/changes.d.ts +4 -3
- package/dist/files/changes.d.ts.map +1 -1
- package/dist/files/changes.js +2 -2
- package/dist/files/changes.js.map +1 -1
- package/dist/files/list.d.ts +1 -1
- package/dist/files/list.d.ts.map +1 -1
- package/dist/files/list.js +1 -1
- package/dist/files/list.js.map +1 -1
- package/dist/files/url.d.ts +2 -0
- package/dist/files/url.d.ts.map +1 -0
- package/dist/files/url.js +4 -0
- package/dist/files/url.js.map +1 -0
- package/dist/http/assets.d.ts.map +1 -1
- package/dist/http/assets.js +21 -6
- package/dist/http/assets.js.map +1 -1
- package/dist/http/events.d.ts +1 -1
- package/dist/http/events.d.ts.map +1 -1
- package/dist/http/events.js +1 -1
- package/dist/http/events.js.map +1 -1
- package/dist/http/identity.d.ts +7 -0
- package/dist/http/identity.d.ts.map +1 -0
- package/dist/http/identity.js +38 -0
- package/dist/http/identity.js.map +1 -0
- package/dist/http/loopback.js +2 -2
- package/dist/http/loopback.js.map +1 -1
- package/dist/http/page.d.ts +4 -3
- package/dist/http/page.d.ts.map +1 -1
- package/dist/http/page.js +37 -15
- package/dist/http/page.js.map +1 -1
- package/dist/http/search.d.ts +5 -0
- package/dist/http/search.d.ts.map +1 -0
- package/dist/http/search.js +11 -0
- package/dist/http/search.js.map +1 -0
- package/dist/http/work.d.ts +6 -0
- package/dist/http/work.d.ts.map +1 -0
- package/dist/http/work.js +22 -0
- package/dist/http/work.js.map +1 -0
- package/dist/metadata/backlinks.d.ts +8 -0
- package/dist/metadata/backlinks.d.ts.map +1 -0
- package/dist/metadata/backlinks.js +29 -0
- package/dist/metadata/backlinks.js.map +1 -0
- package/dist/metadata/fields.d.ts +29 -0
- package/dist/metadata/fields.d.ts.map +1 -0
- package/dist/metadata/fields.js +25 -0
- package/dist/metadata/fields.js.map +1 -0
- package/dist/metadata/links.d.ts +4 -0
- package/dist/metadata/links.d.ts.map +1 -0
- package/dist/metadata/links.js +18 -0
- package/dist/metadata/links.js.map +1 -0
- package/dist/metadata/locations.d.ts +3 -0
- package/dist/metadata/locations.d.ts.map +1 -0
- package/dist/metadata/locations.js +35 -0
- package/dist/metadata/locations.js.map +1 -0
- package/dist/metadata/model.d.ts +15 -0
- package/dist/metadata/model.d.ts.map +1 -0
- package/dist/metadata/model.js +13 -0
- package/dist/metadata/model.js.map +1 -0
- package/dist/metadata/parse.d.ts +16 -0
- package/dist/metadata/parse.d.ts.map +1 -0
- package/dist/metadata/parse.js +48 -0
- package/dist/metadata/parse.js.map +1 -0
- package/dist/metadata/problems.d.ts +4 -0
- package/dist/metadata/problems.d.ts.map +1 -0
- package/dist/metadata/problems.js +57 -0
- package/dist/metadata/problems.js.map +1 -0
- package/dist/metadata/relationshipLabel.d.ts +2 -0
- package/dist/metadata/relationshipLabel.d.ts.map +1 -0
- package/dist/metadata/relationshipLabel.js +13 -0
- package/dist/metadata/relationshipLabel.js.map +1 -0
- package/dist/metadata/schema.d.ts +34 -0
- package/dist/metadata/schema.d.ts.map +1 -0
- package/dist/metadata/schema.js +27 -0
- package/dist/metadata/schema.js.map +1 -0
- package/dist/metadata/sourceLinks.d.ts +5 -0
- package/dist/metadata/sourceLinks.d.ts.map +1 -0
- package/dist/metadata/sourceLinks.js +44 -0
- package/dist/metadata/sourceLinks.js.map +1 -0
- package/dist/page/backlinksHtml.d.ts +3 -0
- package/dist/page/backlinksHtml.d.ts.map +1 -0
- package/dist/page/backlinksHtml.js +16 -0
- package/dist/page/backlinksHtml.js.map +1 -0
- package/dist/page/board.d.ts +2 -2
- package/dist/page/board.d.ts.map +1 -1
- package/dist/page/board.js +16 -6
- package/dist/page/board.js.map +1 -1
- package/dist/page/client.d.ts +2 -1
- package/dist/page/client.d.ts.map +1 -1
- package/dist/page/client.js +65 -7
- package/dist/page/client.js.map +1 -1
- package/dist/page/criteriaEvidence.d.ts +4 -0
- package/dist/page/criteriaEvidence.d.ts.map +1 -0
- package/dist/page/criteriaEvidence.js +27 -0
- package/dist/page/criteriaEvidence.js.map +1 -0
- package/dist/page/diagrams.d.ts +1 -1
- package/dist/page/diagrams.d.ts.map +1 -1
- package/dist/page/diagrams.js +12 -4
- package/dist/page/diagrams.js.map +1 -1
- package/dist/page/document.d.ts +2 -2
- package/dist/page/document.d.ts.map +1 -1
- package/dist/page/document.js +7 -4
- package/dist/page/document.js.map +1 -1
- package/dist/page/escape.d.ts.map +1 -1
- package/dist/page/escape.js +2 -2
- package/dist/page/escape.js.map +1 -1
- package/dist/page/libraryScript.d.ts +2 -0
- package/dist/page/libraryScript.d.ts.map +1 -0
- package/dist/page/libraryScript.js +73 -0
- package/dist/page/libraryScript.js.map +1 -0
- package/dist/page/metadata.d.ts +5 -0
- package/dist/page/metadata.d.ts.map +1 -0
- package/dist/page/metadata.js +34 -0
- package/dist/page/metadata.js.map +1 -0
- package/dist/page/metadataDetails.d.ts +4 -0
- package/dist/page/metadataDetails.d.ts.map +1 -0
- package/dist/page/metadataDetails.js +21 -0
- package/dist/page/metadataDetails.js.map +1 -0
- package/dist/page/metadataLinks.d.ts +6 -0
- package/dist/page/metadataLinks.d.ts.map +1 -0
- package/dist/page/metadataLinks.js +31 -0
- package/dist/page/metadataLinks.js.map +1 -0
- package/dist/page/nav.d.ts +2 -2
- package/dist/page/nav.d.ts.map +1 -1
- package/dist/page/nav.js +4 -4
- package/dist/page/nav.js.map +1 -1
- package/dist/page/navigationScript.d.ts +2 -0
- package/dist/page/navigationScript.d.ts.map +1 -0
- package/dist/page/navigationScript.js +104 -0
- package/dist/page/navigationScript.js.map +1 -0
- package/dist/page/readingStateScript.d.ts +2 -0
- package/dist/page/readingStateScript.d.ts.map +1 -0
- package/dist/page/readingStateScript.js +62 -0
- package/dist/page/readingStateScript.js.map +1 -0
- package/dist/page/readingStyles.d.ts +2 -0
- package/dist/page/readingStyles.d.ts.map +1 -0
- package/dist/page/readingStyles.js +29 -0
- package/dist/page/readingStyles.js.map +1 -0
- package/dist/page/readingTools.d.ts +3 -0
- package/dist/page/readingTools.d.ts.map +1 -0
- package/dist/page/readingTools.js +10 -0
- package/dist/page/readingTools.js.map +1 -0
- package/dist/page/recordedEvidence.d.ts +4 -0
- package/dist/page/recordedEvidence.d.ts.map +1 -0
- package/dist/page/recordedEvidence.js +10 -0
- package/dist/page/recordedEvidence.js.map +1 -0
- package/dist/page/searchDialog.d.ts +3 -0
- package/dist/page/searchDialog.d.ts.map +1 -0
- package/dist/page/searchDialog.js +16 -0
- package/dist/page/searchDialog.js.map +1 -0
- package/dist/page/searchResultsScript.d.ts +2 -0
- package/dist/page/searchResultsScript.d.ts.map +1 -0
- package/dist/page/searchResultsScript.js +64 -0
- package/dist/page/searchResultsScript.js.map +1 -0
- package/dist/page/searchScript.d.ts +2 -0
- package/dist/page/searchScript.d.ts.map +1 -0
- package/dist/page/searchScript.js +73 -0
- package/dist/page/searchScript.js.map +1 -0
- package/dist/page/shell.d.ts +1 -1
- package/dist/page/shell.d.ts.map +1 -1
- package/dist/page/shell.js +23 -5
- package/dist/page/shell.js.map +1 -1
- package/dist/page/stateScript.d.ts +2 -0
- package/dist/page/stateScript.d.ts.map +1 -0
- package/dist/page/stateScript.js +49 -0
- package/dist/page/stateScript.js.map +1 -0
- package/dist/page/style.d.ts.map +1 -1
- package/dist/page/style.js +14 -34
- package/dist/page/style.js.map +1 -1
- package/dist/page/swap.d.ts +1 -1
- package/dist/page/swap.d.ts.map +1 -1
- package/dist/page/swap.js +4 -1
- package/dist/page/swap.js.map +1 -1
- package/dist/page/workspaceStyles.d.ts +3 -0
- package/dist/page/workspaceStyles.d.ts.map +1 -0
- package/dist/page/workspaceStyles.js +79 -0
- package/dist/page/workspaceStyles.js.map +1 -0
- package/dist/render/board.d.ts +8 -8
- package/dist/render/board.d.ts.map +1 -1
- package/dist/render/board.js +7 -7
- package/dist/render/board.js.map +1 -1
- package/dist/render/code.d.ts +1 -1
- package/dist/render/code.js +5 -5
- package/dist/render/code.js.map +1 -1
- package/dist/render/documentHeadings.d.ts +667 -0
- package/dist/render/documentHeadings.d.ts.map +1 -0
- package/dist/render/documentHeadings.js +45 -0
- package/dist/render/documentHeadings.js.map +1 -0
- package/dist/render/documentLinks.d.ts +659 -0
- package/dist/render/documentLinks.d.ts.map +1 -0
- package/dist/render/documentLinks.js +19 -0
- package/dist/render/documentLinks.js.map +1 -0
- package/dist/render/footnotes.d.ts +1 -1
- package/dist/render/footnotes.js +2 -2
- package/dist/render/footnotes.js.map +1 -1
- package/dist/render/highlighter.d.ts +1 -1
- package/dist/render/highlighter.js +2 -2
- package/dist/render/highlighter.js.map +1 -1
- package/dist/render/markdown.d.ts +7 -1
- package/dist/render/markdown.d.ts.map +1 -1
- package/dist/render/markdown.js +13 -2
- package/dist/render/markdown.js.map +1 -1
- package/dist/search/collector.d.ts +13 -0
- package/dist/search/collector.d.ts.map +1 -0
- package/dist/search/collector.js +40 -0
- package/dist/search/collector.js.map +1 -0
- package/dist/search/entries.d.ts +10 -0
- package/dist/search/entries.d.ts.map +1 -0
- package/dist/search/entries.js +37 -0
- package/dist/search/entries.js.map +1 -0
- package/dist/search/linkCollector.d.ts +8 -0
- package/dist/search/linkCollector.d.ts.map +1 -0
- package/dist/search/linkCollector.js +16 -0
- package/dist/search/linkCollector.js.map +1 -0
- package/dist/search/match.d.ts +14 -0
- package/dist/search/match.d.ts.map +1 -0
- package/dist/search/match.js +26 -0
- package/dist/search/match.js.map +1 -0
- package/dist/search/metadataEntries.d.ts +4 -0
- package/dist/search/metadataEntries.d.ts.map +1 -0
- package/dist/search/metadataEntries.js +40 -0
- package/dist/search/metadataEntries.js.map +1 -0
- package/dist/search/snapshot.d.ts +13 -0
- package/dist/search/snapshot.d.ts.map +1 -0
- package/dist/search/snapshot.js +50 -0
- package/dist/search/snapshot.js.map +1 -0
- package/dist/serve.d.ts +1 -1
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +1 -1
- package/dist/serve.js.map +1 -1
- package/dist/views/items.d.ts +11 -0
- package/dist/views/items.d.ts.map +1 -0
- package/dist/views/items.js +48 -0
- package/dist/views/items.js.map +1 -0
- package/dist/views/projection.d.ts +15 -0
- package/dist/views/projection.d.ts.map +1 -0
- package/dist/views/projection.js +47 -0
- package/dist/views/projection.js.map +1 -0
- package/dist/views/savedScript.d.ts +2 -0
- package/dist/views/savedScript.d.ts.map +1 -0
- package/dist/views/savedScript.js +89 -0
- package/dist/views/savedScript.js.map +1 -0
- package/dist/views/state.d.ts +15 -0
- package/dist/views/state.d.ts.map +1 -0
- package/dist/views/state.js +45 -0
- package/dist/views/state.js.map +1 -0
- package/dist/views/tools.d.ts +3 -0
- package/dist/views/tools.d.ts.map +1 -0
- package/dist/views/tools.js +11 -0
- package/dist/views/tools.js.map +1 -0
- package/dist/views/workCards.d.ts +4 -0
- package/dist/views/workCards.d.ts.map +1 -0
- package/dist/views/workCards.js +23 -0
- package/dist/views/workCards.js.map +1 -0
- package/dist/views/workControls.d.ts +4 -0
- package/dist/views/workControls.d.ts.map +1 -0
- package/dist/views/workControls.js +32 -0
- package/dist/views/workControls.js.map +1 -0
- package/dist/views/workDetail.d.ts +13 -0
- package/dist/views/workDetail.d.ts.map +1 -0
- package/dist/views/workDetail.js +26 -0
- package/dist/views/workDetail.js.map +1 -0
- package/dist/views/workHtml.d.ts +15 -0
- package/dist/views/workHtml.d.ts.map +1 -0
- package/dist/views/workHtml.js +35 -0
- package/dist/views/workHtml.js.map +1 -0
- package/dist/views/workStyle.d.ts +2 -0
- package/dist/views/workStyle.d.ts.map +1 -0
- package/dist/views/workStyle.js +18 -0
- package/dist/views/workStyle.js.map +1 -0
- package/dist/views/workTable.d.ts +4 -0
- package/dist/views/workTable.d.ts.map +1 -0
- package/dist/views/workTable.js +19 -0
- package/dist/views/workTable.js.map +1 -0
- package/package.json +42 -33
- package/src/board.ts +25 -12
- package/src/cli.ts +3 -3
- package/src/files/changes.ts +7 -6
- package/src/files/list.ts +3 -3
- package/src/files/url.ts +3 -0
- package/src/http/assets.ts +21 -6
- package/src/http/events.ts +2 -2
- package/src/http/identity.ts +62 -0
- package/src/http/loopback.ts +2 -2
- package/src/http/page.ts +56 -19
- package/src/http/search.ts +13 -0
- package/src/http/work.ts +41 -0
- package/src/metadata/backlinks.ts +39 -0
- package/src/metadata/fields.ts +26 -0
- package/src/metadata/links.ts +19 -0
- package/src/metadata/locations.ts +34 -0
- package/src/metadata/model.ts +27 -0
- package/src/metadata/parse.ts +62 -0
- package/src/metadata/problems.ts +62 -0
- package/src/metadata/relationshipLabel.ts +13 -0
- package/src/metadata/schema.ts +36 -0
- package/src/metadata/sourceLinks.ts +46 -0
- package/src/page/backlinksHtml.ts +18 -0
- package/src/page/board.ts +16 -6
- package/src/page/client.ts +65 -7
- package/src/page/criteriaEvidence.ts +36 -0
- package/src/page/diagrams.ts +12 -4
- package/src/page/document.ts +8 -5
- package/src/page/escape.ts +2 -2
- package/src/page/libraryScript.ts +72 -0
- package/src/page/metadata.ts +40 -0
- package/src/page/metadataDetails.ts +24 -0
- package/src/page/metadataLinks.ts +35 -0
- package/src/page/nav.ts +9 -8
- package/src/page/navigationScript.ts +103 -0
- package/src/page/readingStateScript.ts +61 -0
- package/src/page/readingStyles.ts +28 -0
- package/src/page/readingTools.ts +12 -0
- package/src/page/recordedEvidence.ts +12 -0
- package/src/page/searchDialog.ts +16 -0
- package/src/page/searchResultsScript.ts +63 -0
- package/src/page/searchScript.ts +72 -0
- package/src/page/shell.ts +24 -5
- package/src/page/stateScript.ts +48 -0
- package/src/page/style.ts +14 -35
- package/src/page/swap.ts +4 -1
- package/src/page/workspaceStyles.ts +79 -0
- package/src/render/board.ts +19 -19
- package/src/render/code.ts +5 -5
- package/src/render/documentHeadings.ts +52 -0
- package/src/render/documentLinks.ts +19 -0
- package/src/render/footnotes.ts +2 -2
- package/src/render/highlighter.ts +2 -2
- package/src/render/markdown.ts +21 -4
- package/src/search/collector.ts +44 -0
- package/src/search/entries.ts +51 -0
- package/src/search/linkCollector.ts +16 -0
- package/src/search/match.ts +28 -0
- package/src/search/metadataEntries.ts +42 -0
- package/src/search/snapshot.ts +60 -0
- package/src/serve.ts +1 -1
- package/src/views/items.ts +55 -0
- package/src/views/projection.ts +59 -0
- package/src/views/savedScript.ts +88 -0
- package/src/views/state.ts +47 -0
- package/src/views/tools.ts +11 -0
- package/src/views/workCards.ts +26 -0
- package/src/views/workControls.ts +38 -0
- package/src/views/workDetail.ts +28 -0
- package/src/views/workHtml.ts +39 -0
- package/src/views/workStyle.ts +17 -0
- package/src/views/workTable.ts +21 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/src/index.ts +0 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MetadataModel } from "./model.ts";
|
|
2
|
+
|
|
3
|
+
export function identityUrl(id: string): string {
|
|
4
|
+
return `/_board/item/${encodeURIComponent(id)}/`;
|
|
5
|
+
}
|
|
6
|
+
export function referenceTarget(reference: string, model: MetadataModel): string | undefined {
|
|
7
|
+
if (model.unavailable.length > 0) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const [id = "", criterion] = reference.split("#");
|
|
11
|
+
const targets = model.ids.get(id);
|
|
12
|
+
if (targets?.length !== 1) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
if (criterion && targets[0]?.parsed.fields.criteria?.filter((item) => item.id === criterion).length !== 1) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return identityUrl(id) + (criterion ? `#criterion-${encodeURIComponent(criterion)}` : "");
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isMap, isNode, isScalar, isSeq, type LineCounter, type Node, type YAMLMap } from "yaml";
|
|
2
|
+
|
|
3
|
+
function recordMap(node: YAMLMap, field: string, counter: LineCounter, lines: Record<string, number>) {
|
|
4
|
+
for (const pair of node.items) {
|
|
5
|
+
if (!isScalar(pair.key) || typeof pair.key.value !== "string") {
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
8
|
+
const key = field ? `${field}.${pair.key.value}` : pair.key.value;
|
|
9
|
+
visit(pair.value, key, counter, lines);
|
|
10
|
+
if (pair.key.range) {
|
|
11
|
+
lines[key] = counter.linePos(pair.key.range[0]).line + 1;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function visit(value: unknown, field: string, counter: LineCounter, lines: Record<string, number>) {
|
|
16
|
+
if (!isNode(value)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (value.range && field) {
|
|
20
|
+
lines[field] = counter.linePos(value.range[0]).line + 1;
|
|
21
|
+
}
|
|
22
|
+
if (isMap(value)) {
|
|
23
|
+
recordMap(value, field, counter, lines);
|
|
24
|
+
} else if (isSeq(value)) {
|
|
25
|
+
value.items.forEach((item, index) => {
|
|
26
|
+
visit(item, `${field}.${index}`, counter, lines);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function metadataLocations(node: Node | null, counter: LineCounter): Readonly<Record<string, number>> {
|
|
31
|
+
const lines: Record<string, number> = {};
|
|
32
|
+
visit(node, "", counter, lines);
|
|
33
|
+
return lines;
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Diagnostic, ParsedMetadata } from "./parse.ts";
|
|
2
|
+
import { metadataProblems } from "./problems.ts";
|
|
3
|
+
|
|
4
|
+
export interface MetadataDocument {
|
|
5
|
+
readonly file: string;
|
|
6
|
+
readonly links?: readonly string[];
|
|
7
|
+
readonly parsed: ParsedMetadata;
|
|
8
|
+
}
|
|
9
|
+
export interface MetadataModel {
|
|
10
|
+
readonly diagnostics: ReadonlyMap<string, readonly Diagnostic[]>;
|
|
11
|
+
readonly documents: readonly MetadataDocument[];
|
|
12
|
+
readonly ids: ReadonlyMap<string, readonly MetadataDocument[]>;
|
|
13
|
+
readonly rootFile: string | undefined;
|
|
14
|
+
readonly unavailable: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function metadataModel(documents: readonly MetadataDocument[], unavailable: readonly string[] = [], rootFile?: string): MetadataModel {
|
|
18
|
+
const ids = new Map<string, MetadataDocument[]>();
|
|
19
|
+
for (const document of documents) {
|
|
20
|
+
const id = document.parsed.fields.id;
|
|
21
|
+
if (id) {
|
|
22
|
+
ids.set(id, [...(ids.get(id) ?? []), document]);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const diagnostics = new Map(documents.map((document) => [document.file, metadataProblems(document, ids, unavailable)]));
|
|
26
|
+
return { diagnostics, documents, ids, rootFile, unavailable };
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { LineCounter, parseDocument } from "yaml";
|
|
2
|
+
import { metadataFields } from "./fields.ts";
|
|
3
|
+
import { metadataLocations } from "./locations.ts";
|
|
4
|
+
import type { Metadata } from "./schema.ts";
|
|
5
|
+
|
|
6
|
+
export interface Diagnostic {
|
|
7
|
+
readonly field?: string;
|
|
8
|
+
readonly line: number;
|
|
9
|
+
readonly message: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ParsedMetadata {
|
|
12
|
+
readonly body: string;
|
|
13
|
+
readonly bodyLine: number;
|
|
14
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
15
|
+
readonly fields: Metadata;
|
|
16
|
+
readonly lines: Readonly<Record<string, number>>;
|
|
17
|
+
readonly raw?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function metadataParse(source: string): ParsedMetadata {
|
|
21
|
+
const plain: ParsedMetadata = { body: source, bodyLine: 1, diagnostics: [], fields: {}, lines: {} };
|
|
22
|
+
const opening = /^\uFEFF?---[ \t]*\r?\n/u.exec(source);
|
|
23
|
+
if (!opening) {
|
|
24
|
+
return plain;
|
|
25
|
+
}
|
|
26
|
+
const remainder = source.slice(opening[0].length);
|
|
27
|
+
const closing = /^---[ \t]*\r?$/mu.exec(remainder);
|
|
28
|
+
if (!closing) {
|
|
29
|
+
return { ...plain, diagnostics: [{ line: 1, message: "Unterminated frontmatter; the original Markdown is shown." }] };
|
|
30
|
+
}
|
|
31
|
+
let end = opening[0].length + closing.index + closing[0].length;
|
|
32
|
+
if (source[end] === "\n") {
|
|
33
|
+
end += 1;
|
|
34
|
+
}
|
|
35
|
+
const raw = source.slice(0, end);
|
|
36
|
+
const body = source.slice(end);
|
|
37
|
+
const bodyLine = raw.split("\n").length;
|
|
38
|
+
const counter = new LineCounter();
|
|
39
|
+
const doc = parseDocument(remainder.slice(0, closing.index), { lineCounter: counter });
|
|
40
|
+
const lines = metadataLocations(doc.contents, counter);
|
|
41
|
+
const diagnostics: Diagnostic[] = [...doc.errors, ...doc.warnings].map((error) => ({
|
|
42
|
+
line: (error.linePos?.[0].line ?? 1) + 1,
|
|
43
|
+
message: error.message,
|
|
44
|
+
}));
|
|
45
|
+
const result = { body, bodyLine, diagnostics, fields: {}, lines, raw };
|
|
46
|
+
if (doc.errors.length > 0 || doc.warnings.length > 0) {
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
let data: unknown;
|
|
50
|
+
try {
|
|
51
|
+
data = doc.toJS({ maxAliasCount: 50 });
|
|
52
|
+
} catch {
|
|
53
|
+
return { ...result, diagnostics: [...diagnostics, { line: 2, message: "Frontmatter aliases could not be expanded safely." }] };
|
|
54
|
+
}
|
|
55
|
+
if (data === null) {
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
if (typeof data !== "object" || Array.isArray(data)) {
|
|
59
|
+
return { ...plain, diagnostics: [{ line: 2, message: "Frontmatter must be a mapping; the original Markdown is shown." }] };
|
|
60
|
+
}
|
|
61
|
+
return { ...result, ...metadataFields(data, lines) };
|
|
62
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { MetadataDocument } from "./model.ts";
|
|
2
|
+
import type { Diagnostic } from "./parse.ts";
|
|
3
|
+
|
|
4
|
+
function referenceProblem(target: string, ids: ReadonlyMap<string, readonly MetadataDocument[]>): string | undefined {
|
|
5
|
+
const [id = "", criterion] = target.split("#");
|
|
6
|
+
const matches = ids.get(id);
|
|
7
|
+
if (!matches?.length) {
|
|
8
|
+
return `Unresolved reference ${target}.`;
|
|
9
|
+
}
|
|
10
|
+
if (matches.length > 1) {
|
|
11
|
+
return `Ambiguous reference ${target}; the ID is duplicated.`;
|
|
12
|
+
}
|
|
13
|
+
if (criterion && matches[0]?.parsed.fields.criteria?.filter((item) => item.id === criterion).length !== 1) {
|
|
14
|
+
return `Unresolved or duplicated criterion ${target}.`;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
function criterionProblems(document: MetadataDocument): readonly Diagnostic[] {
|
|
19
|
+
const criteria = document.parsed.fields.criteria ?? [];
|
|
20
|
+
return criteria.flatMap((criterion, index) =>
|
|
21
|
+
criteria.filter((item) => item.id === criterion.id).length > 1
|
|
22
|
+
? [{ field: "criteria", line: document.parsed.lines[`criteria.${index}`] ?? 2, message: `Duplicate criterion ID ${criterion.id}.` }]
|
|
23
|
+
: [],
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
function referenceProblems(document: MetadataDocument, ids: ReadonlyMap<string, readonly MetadataDocument[]>): readonly Diagnostic[] {
|
|
27
|
+
const { fields, lines } = document.parsed;
|
|
28
|
+
const references = [
|
|
29
|
+
...(fields.relationships?.map((link, index) => ({ field: "relationships", key: `relationships.${index}.target`, target: link.target })) ?? []),
|
|
30
|
+
...(fields.items?.map((target, index) => ({ field: "items", key: `items.${index}`, target })) ?? []),
|
|
31
|
+
...(fields.evidence?.flatMap((item, index) =>
|
|
32
|
+
item.criterion ? [{ field: "evidence", key: `evidence.${index}.criterion`, target: item.criterion }] : [],
|
|
33
|
+
) ?? []),
|
|
34
|
+
];
|
|
35
|
+
return references.flatMap((reference) => {
|
|
36
|
+
const message = referenceProblem(reference.target, ids);
|
|
37
|
+
return message ? [{ field: reference.field, line: lines[reference.key] ?? lines[reference.field] ?? 2, message }] : [];
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function metadataProblems(document: MetadataDocument, ids: ReadonlyMap<string, readonly MetadataDocument[]>, unavailable: readonly string[]) {
|
|
41
|
+
const { parsed } = document;
|
|
42
|
+
const problems = [...parsed.diagnostics, ...criterionProblems(document)];
|
|
43
|
+
const matches = parsed.fields.id ? ids.get(parsed.fields.id) : undefined;
|
|
44
|
+
if (matches && matches.length > 1) {
|
|
45
|
+
problems.push({
|
|
46
|
+
field: "id",
|
|
47
|
+
line: parsed.lines.id ?? 2,
|
|
48
|
+
message: `Duplicate ID ${parsed.fields.id}: ${matches.map((item) => item.file).join(", ")}. No source is selected.`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (unavailable.length > 0) {
|
|
52
|
+
if (parsed.raw !== undefined) {
|
|
53
|
+
problems.push({
|
|
54
|
+
line: 1,
|
|
55
|
+
message: `Workspace index incomplete; could not read ${unavailable.join(", ")}. Identity and references cannot be validated completely.`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
problems.push(...referenceProblems(document, ids));
|
|
60
|
+
}
|
|
61
|
+
return problems;
|
|
62
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const labels = new Map([
|
|
2
|
+
["board member", "Board membership"],
|
|
3
|
+
["criterion evidence", "Criterion evidence"],
|
|
4
|
+
["depends_on", "Depends on"],
|
|
5
|
+
["evidence source", "Evidence source"],
|
|
6
|
+
["implements", "Implements"],
|
|
7
|
+
["informs", "Informs"],
|
|
8
|
+
["relates_to", "Related to"],
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export function relationshipLabel(kind: string): string {
|
|
12
|
+
return labels.get(kind) ?? kind;
|
|
13
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
export const Identity = Schema.String.check(Schema.isPattern(/^[a-zA-Z\d][a-zA-Z\d._-]*$/u));
|
|
4
|
+
export const Reference = Schema.String.check(Schema.isPattern(/^[a-zA-Z\d][a-zA-Z\d._-]*(?:#[a-zA-Z\d][a-zA-Z\d._-]*)?$/u));
|
|
5
|
+
const Text = Schema.String.check(Schema.isPattern(/\S/u));
|
|
6
|
+
const Criterion = Schema.Struct({ id: Identity, text: Text });
|
|
7
|
+
const Relationship = Schema.Struct({ kind: Schema.Literals(["implements", "informs", "depends_on", "relates_to"]), target: Reference });
|
|
8
|
+
const Evidence = Schema.Struct({
|
|
9
|
+
checkedRevision: Schema.optional(Schema.String.check(Schema.isPattern(/^(?:[a-f\d]{40}|[a-f\d]{64})$/iu))),
|
|
10
|
+
criterion: Schema.optional(Reference),
|
|
11
|
+
method: Schema.optional(Text),
|
|
12
|
+
observedAt: Schema.optional(
|
|
13
|
+
Text.check(
|
|
14
|
+
Schema.makeFilter(
|
|
15
|
+
(value) =>
|
|
16
|
+
(Number.isFinite(Date.parse(value)) && /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/u.exec(value) !== null)
|
|
17
|
+
|| "Expected an ISO timestamp with timezone",
|
|
18
|
+
),
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
outcome: Schema.optional(Text),
|
|
22
|
+
source: Text,
|
|
23
|
+
}).pipe(Schema.encodeKeys({ checkedRevision: "checked_revision", observedAt: "observed_at" }));
|
|
24
|
+
|
|
25
|
+
export const Metadata = Schema.Struct({
|
|
26
|
+
criteria: Schema.optional(Schema.Array(Criterion)),
|
|
27
|
+
evidence: Schema.optional(Schema.Array(Evidence)),
|
|
28
|
+
id: Schema.optional(Identity),
|
|
29
|
+
items: Schema.optional(Schema.Array(Identity)),
|
|
30
|
+
kind: Schema.optional(Schema.Literals(["task", "investigation", "decision", "result", "project", "board"])),
|
|
31
|
+
nextAction: Schema.optional(Text),
|
|
32
|
+
owner: Schema.optional(Text),
|
|
33
|
+
relationships: Schema.optional(Schema.Array(Relationship)),
|
|
34
|
+
status: Schema.optional(Text),
|
|
35
|
+
}).pipe(Schema.encodeKeys({ nextAction: "next_action" }));
|
|
36
|
+
export type Metadata = typeof Metadata.Type;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { fileUrl } from "#files/url.ts";
|
|
2
|
+
import { referenceTarget } from "./links.ts";
|
|
3
|
+
import type { MetadataModel } from "./model.ts";
|
|
4
|
+
|
|
5
|
+
const BASE = "http://work-board.local";
|
|
6
|
+
const absolute = /^(?:[a-z][a-z\d+.-]*:|\/\/)/iu;
|
|
7
|
+
|
|
8
|
+
export function sourceHref(source: string, file: string): string | undefined {
|
|
9
|
+
try {
|
|
10
|
+
const url = new URL(source, `${BASE}${fileUrl(file)}`);
|
|
11
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return absolute.exec(source) === null ? url.pathname + url.search + url.hash : url.href;
|
|
15
|
+
} catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function referenceFile(reference: string, model: MetadataModel): string | undefined {
|
|
21
|
+
if (!referenceTarget(reference, model)) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return model.ids.get(reference.split("#")[0] ?? "")?.[0]?.file;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sourceFile(source: string, file: string, model: MetadataModel): string | undefined {
|
|
28
|
+
const href = sourceHref(source, file);
|
|
29
|
+
if (!href?.startsWith("/")) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const url = new URL(href, BASE);
|
|
33
|
+
try {
|
|
34
|
+
const path = decodeURIComponent(url.pathname);
|
|
35
|
+
if (path === "/") {
|
|
36
|
+
return model.rootFile;
|
|
37
|
+
}
|
|
38
|
+
const identity = /^\/_board\/item\/(?<id>[^/]+)\/$/u.exec(path)?.groups?.id;
|
|
39
|
+
if (identity) {
|
|
40
|
+
return referenceFile(identity, model);
|
|
41
|
+
}
|
|
42
|
+
return /\.md$/iu.exec(path) === null ? undefined : path.slice(1);
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { backlinksOf } from "#metadata/backlinks.ts";
|
|
2
|
+
import type { MetadataModel } from "#metadata/model.ts";
|
|
3
|
+
import { relationshipLabel } from "#metadata/relationshipLabel.ts";
|
|
4
|
+
import { escapeHtml } from "./escape.ts";
|
|
5
|
+
import { documentReference } from "./metadataLinks.ts";
|
|
6
|
+
|
|
7
|
+
export function backlinksHtml(file: string, model: MetadataModel): string {
|
|
8
|
+
const links = backlinksOf(file, model);
|
|
9
|
+
if (links.length === 0) {
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
const entries = [...new Map(links.map((link) => [`${link.file}:${link.kind}:${link.line ?? ""}`, link])).values()];
|
|
13
|
+
const incomplete =
|
|
14
|
+
model.unavailable.length > 0
|
|
15
|
+
? "<p>Available source references only; unreadable files prevent complete backlink and identity validation.</p>"
|
|
16
|
+
: "";
|
|
17
|
+
return `<section class="work-context" aria-label="Source backlinks"><details data-key="work-backlinks"><summary>Referenced by (${entries.length})</summary>${incomplete}<ul>${entries.map((link) => `<li>${escapeHtml(relationshipLabel(link.kind))}: <a href="${escapeHtml(documentReference(link.file, model))}">${escapeHtml(link.file)}${link.line === undefined ? "" : `:${link.line}`}</a></li>`).join("")}</ul><p>These are explicit source links, not inferred blockers or workflow rules.</p></details></section>`;
|
|
18
|
+
}
|
package/src/page/board.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
import { type Board, countsOf, type Section } from "
|
|
3
|
-
import {
|
|
2
|
+
import { type Board, countsOf, type Section } from "#render/board.ts";
|
|
3
|
+
import { documentHeadings } from "#render/documentHeadings.ts";
|
|
4
|
+
import { renderMarkdown } from "#render/markdown.ts";
|
|
4
5
|
import { escapeHtml } from "./escape.ts";
|
|
6
|
+
import { readingTools } from "./readingTools.ts";
|
|
5
7
|
import { updated } from "./updated.ts";
|
|
6
8
|
|
|
7
9
|
const counts = (board: Board): string =>
|
|
@@ -12,22 +14,30 @@ const counts = (board: Board): string =>
|
|
|
12
14
|
type Render = (fragment: string) => ReturnType<typeof renderMarkdown>;
|
|
13
15
|
|
|
14
16
|
const section = Effect.fn("WorkBoard.section")(function* ({ heading, items, notes }: Section, render: Render) {
|
|
15
|
-
const
|
|
17
|
+
const title = yield* render(heading);
|
|
16
18
|
const note = notes === "" ? "" : `<div class="notes">${yield* render(notes)}</div>`;
|
|
17
|
-
|
|
19
|
+
const rendered = yield* Effect.forEach(items, (item) => Effect.map(render(item), (html) => `<article class="item">${html}</article>`));
|
|
20
|
+
return `<section>${title}${note}${rendered.join("")}</section>`;
|
|
18
21
|
});
|
|
19
22
|
|
|
20
|
-
export const boardHtml = Effect.fn("WorkBoard.boardHtml")(function* (board: Board, fallbackTitle: string, modified: number) {
|
|
23
|
+
export const boardHtml = Effect.fn("WorkBoard.boardHtml")(function* (board: Board, fallbackTitle: string, modified: number, metadata = "") {
|
|
21
24
|
let fragments = 0;
|
|
25
|
+
const headings = documentHeadings();
|
|
22
26
|
const render: Render = (fragment) => {
|
|
23
27
|
fragments += 1;
|
|
24
|
-
return renderMarkdown(board.definitions === "" ? fragment : `${fragment}\n\n${board.definitions}`,
|
|
28
|
+
return renderMarkdown(board.definitions === "" ? fragment : `${fragment}\n\n${board.definitions}`, {
|
|
29
|
+
file: fallbackTitle,
|
|
30
|
+
headings,
|
|
31
|
+
idPrefix: `part-${fragments}-`,
|
|
32
|
+
});
|
|
25
33
|
};
|
|
26
34
|
const title = board.title === "" ? `<h1>${escapeHtml(fallbackTitle)}</h1>` : yield* render(board.title);
|
|
27
35
|
const intro = board.intro === "" ? "" : `<div class="intro">${yield* render(board.intro)}</div>`;
|
|
28
36
|
const sections = yield* Effect.forEach(board.sections, (each) => section(each, render));
|
|
29
37
|
const footer = board.footer === "" ? "" : `<footer>${yield* render(board.footer)}</footer>`;
|
|
30
38
|
return [
|
|
39
|
+
readingTools(headings.entries),
|
|
40
|
+
metadata,
|
|
31
41
|
`<header class="board-head">${title}${intro}`,
|
|
32
42
|
`<p class="counts">${counts(board)}</p>`,
|
|
33
43
|
`<p class="meta">${updated(modified)}</p></header>`,
|
package/src/page/client.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export const client = `
|
|
2
|
+
import "/_board/saved-views.js";
|
|
3
|
+
import "/_board/preferences.js";
|
|
4
|
+
import "/_board/search.js";
|
|
2
5
|
import { renderDiagrams } from "/_board/diagrams.js";
|
|
3
|
-
import {
|
|
6
|
+
import { applyPage } from "/_board/page-state.js";
|
|
7
|
+
import { pageVersion } from "/_board/navigation.js";
|
|
8
|
+
import { remember } from "/_board/swap.js";
|
|
4
9
|
|
|
5
10
|
const since = (modified) => {
|
|
6
11
|
const seconds = Math.max(0, Math.round((Date.now() - modified) / 1000));
|
|
@@ -32,22 +37,23 @@ const show = () => {
|
|
|
32
37
|
};
|
|
33
38
|
|
|
34
39
|
const load = async () => {
|
|
40
|
+
const version = pageVersion();
|
|
41
|
+
const path = location.pathname + location.search;
|
|
35
42
|
try {
|
|
36
|
-
const response = await fetch(
|
|
43
|
+
const response = await fetch(path, { cache: "no-store" });
|
|
37
44
|
const next = new DOMParser().parseFromString(await response.text(), "text/html");
|
|
38
45
|
const files = next.getElementById("files");
|
|
39
46
|
const doc = next.getElementById("doc");
|
|
47
|
+
if (version !== pageVersion()) return;
|
|
40
48
|
if (files === null || doc === null) {
|
|
41
49
|
failure = "refresh failed (" + response.status + ")";
|
|
42
50
|
return;
|
|
43
51
|
}
|
|
44
52
|
failure = "";
|
|
45
|
-
|
|
46
|
-
swap(document.getElementById("doc"), doc);
|
|
47
|
-
document.title = next.title;
|
|
53
|
+
applyPage(next, true);
|
|
48
54
|
tick();
|
|
49
|
-
renderDiagrams(document);
|
|
50
55
|
} catch {
|
|
56
|
+
if (version !== pageVersion()) return;
|
|
51
57
|
failure = "refresh failed";
|
|
52
58
|
}
|
|
53
59
|
};
|
|
@@ -71,6 +77,7 @@ const refresh = async () => {
|
|
|
71
77
|
const events = new EventSource("/events");
|
|
72
78
|
events.addEventListener("ready", () => {
|
|
73
79
|
connected = true;
|
|
80
|
+
document.dispatchEvent(new Event("board-index-change"));
|
|
74
81
|
show();
|
|
75
82
|
if (dropped) refresh();
|
|
76
83
|
dropped = false;
|
|
@@ -80,11 +87,62 @@ const down = () => {
|
|
|
80
87
|
dropped = true;
|
|
81
88
|
show();
|
|
82
89
|
};
|
|
83
|
-
events.addEventListener("change", refresh);
|
|
90
|
+
events.addEventListener("change", () => { refresh(); document.dispatchEvent(new Event("board-index-change")); });
|
|
84
91
|
events.addEventListener("down", down);
|
|
85
92
|
events.addEventListener("error", down);
|
|
86
93
|
|
|
94
|
+
document.addEventListener("board-page", tick);
|
|
87
95
|
tick();
|
|
88
96
|
setInterval(tick, 5000);
|
|
89
97
|
renderDiagrams(document);
|
|
90
98
|
`;
|
|
99
|
+
export const preferences = `
|
|
100
|
+
const root = document.documentElement;
|
|
101
|
+
const key = "work-board:appearance:" + root.dataset.workspace;
|
|
102
|
+
const theme = document.getElementById("theme");
|
|
103
|
+
const density = document.getElementById("density");
|
|
104
|
+
const sidebar = document.getElementById("sidebar");
|
|
105
|
+
const toggle = document.getElementById("sidebar-toggle");
|
|
106
|
+
const status = document.getElementById("preference-status");
|
|
107
|
+
const system = matchMedia("(prefers-color-scheme: dark)");
|
|
108
|
+
let saved = {};
|
|
109
|
+
try {
|
|
110
|
+
const value = JSON.parse(localStorage.getItem(key) ?? "{}");
|
|
111
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) saved = value;
|
|
112
|
+
} catch {
|
|
113
|
+
status.textContent = "Preferences apply to this page only.";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
theme.value = ["light", "dark"].includes(saved.theme) ? saved.theme : "system";
|
|
117
|
+
density.value = saved.density === "compact" ? "compact" : "comfortable";
|
|
118
|
+
let closed = typeof saved.closed === "boolean" ? saved.closed : matchMedia("(max-width: 760px)").matches;
|
|
119
|
+
|
|
120
|
+
const appearance = () => {
|
|
121
|
+
root.dataset.theme = theme.value;
|
|
122
|
+
root.dataset.scheme = theme.value === "system" ? (system.matches ? "dark" : "light") : theme.value;
|
|
123
|
+
root.dataset.density = density.value;
|
|
124
|
+
root.dataset.sidebar = closed ? "closed" : "open";
|
|
125
|
+
sidebar.hidden = closed;
|
|
126
|
+
toggle.setAttribute("aria-expanded", String(!closed));
|
|
127
|
+
document.dispatchEvent(new Event("board-theme"));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const save = () => {
|
|
131
|
+
appearance();
|
|
132
|
+
try {
|
|
133
|
+
localStorage.setItem(key, JSON.stringify({ theme: theme.value, density: density.value, closed }));
|
|
134
|
+
status.textContent = "";
|
|
135
|
+
} catch {
|
|
136
|
+
status.textContent = "Preferences apply to this page only.";
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
theme.addEventListener("change", save);
|
|
141
|
+
density.addEventListener("change", save);
|
|
142
|
+
toggle.addEventListener("click", () => {
|
|
143
|
+
closed = !closed;
|
|
144
|
+
save();
|
|
145
|
+
});
|
|
146
|
+
system.addEventListener("change", appearance);
|
|
147
|
+
appearance();
|
|
148
|
+
`;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { referenceTarget } from "#metadata/links.ts";
|
|
2
|
+
import type { MetadataModel } from "#metadata/model.ts";
|
|
3
|
+
import type { ParsedMetadata } from "#metadata/parse.ts";
|
|
4
|
+
import { escapeHtml } from "./escape.ts";
|
|
5
|
+
import { documentReference } from "./metadataLinks.ts";
|
|
6
|
+
|
|
7
|
+
function evidenceFor(reference: string, model: MetadataModel): string {
|
|
8
|
+
const records = model.documents.flatMap(
|
|
9
|
+
(document) =>
|
|
10
|
+
document.parsed.fields.evidence?.flatMap((evidence, index) =>
|
|
11
|
+
evidence.criterion === reference ? [{ file: document.file, index, line: document.parsed.lines[`evidence.${index}.source`] }] : [],
|
|
12
|
+
) ?? [],
|
|
13
|
+
);
|
|
14
|
+
if (records.length === 0) {
|
|
15
|
+
return "<p>No recorded evidence naming this criterion. Acceptance is not established.</p>";
|
|
16
|
+
}
|
|
17
|
+
return `<p>${records.length} recorded ${records.length === 1 ? "claim" : "claims"}, not verified acceptance:</p><ul>${records.map((record) => `<li><a href="${escapeHtml(documentReference(record.file, model, `#recorded-evidence-${record.index}`))}">${escapeHtml(record.file)}${record.line === undefined ? "" : `:${record.line}`} — open recorded evidence</a></li>`).join("")}</ul>`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function criteriaEvidence(parsed: ParsedMetadata, model: MetadataModel): string {
|
|
21
|
+
const criteria = parsed.fields.criteria ?? [];
|
|
22
|
+
if (criteria.length === 0) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
return `<h3>Criteria and recorded evidence</h3><ul>${criteria
|
|
26
|
+
.map((criterion) => {
|
|
27
|
+
const unique = criteria.filter((candidate) => candidate.id === criterion.id).length === 1;
|
|
28
|
+
const reference = parsed.fields.id ? `${parsed.fields.id}#${criterion.id}` : "";
|
|
29
|
+
const evidence =
|
|
30
|
+
reference && referenceTarget(reference, model)
|
|
31
|
+
? evidenceFor(reference, model)
|
|
32
|
+
: "<p>Evidence association requires unique item/criterion IDs and a readable workspace. Acceptance is not established.</p>";
|
|
33
|
+
return `<li${unique ? ` id="criterion-${escapeHtml(criterion.id)}" tabindex="-1"` : ""}><b>${escapeHtml(criterion.id)}</b>: ${escapeHtml(criterion.text)}${evidence}</li>`;
|
|
34
|
+
})
|
|
35
|
+
.join("")}</ul>`;
|
|
36
|
+
}
|
package/src/page/diagrams.ts
CHANGED
|
@@ -13,7 +13,7 @@ const load = async () => {
|
|
|
13
13
|
securityLevel: "strict",
|
|
14
14
|
theme: "base",
|
|
15
15
|
themeVariables: {
|
|
16
|
-
darkMode: dark
|
|
16
|
+
darkMode: schemeNow() === "dark",
|
|
17
17
|
background: token("--bg"),
|
|
18
18
|
primaryColor: token("--surface"),
|
|
19
19
|
primaryTextColor: token("--text"),
|
|
@@ -26,7 +26,8 @@ const load = async () => {
|
|
|
26
26
|
return mermaid;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
const schemeNow = () => (dark.matches ? "dark" : "light");
|
|
29
|
+
const schemeNow = () => document.documentElement.dataset.scheme ?? (dark.matches ? "dark" : "light");
|
|
30
|
+
let lastScheme = schemeNow();
|
|
30
31
|
|
|
31
32
|
const draw = async (figure) => {
|
|
32
33
|
const source = figure.querySelector(".diagram-source").textContent;
|
|
@@ -65,8 +66,15 @@ export const renderDiagrams = (root, selector = "figure.diagram:not([data-state=
|
|
|
65
66
|
return Promise.all([...root.querySelectorAll(selector)].map(draw));
|
|
66
67
|
};
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
const themeChanged = () => {
|
|
70
|
+
const scheme = schemeNow();
|
|
71
|
+
if (lastScheme === scheme) return;
|
|
72
|
+
lastScheme = scheme;
|
|
73
|
+
const doc = document.getElementById("doc");
|
|
74
|
+
if (doc) doc.dataset.scheme = scheme;
|
|
69
75
|
mermaid = undefined;
|
|
70
76
|
renderDiagrams(document, "figure.diagram");
|
|
71
|
-
}
|
|
77
|
+
};
|
|
78
|
+
document.addEventListener("board-theme", themeChanged);
|
|
79
|
+
dark.addEventListener("change", themeChanged);
|
|
72
80
|
`;
|
package/src/page/document.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
import type { MarkdownFile } from "
|
|
3
|
-
import {
|
|
2
|
+
import type { MarkdownFile } from "#files/list.ts";
|
|
3
|
+
import { documentHeadings } from "#render/documentHeadings.ts";
|
|
4
|
+
import { renderMarkdown } from "#render/markdown.ts";
|
|
4
5
|
import { escapeHtml } from "./escape.ts";
|
|
6
|
+
import { readingTools } from "./readingTools.ts";
|
|
5
7
|
import { updated } from "./updated.ts";
|
|
6
8
|
|
|
7
|
-
export const documentHtml = Effect.fn("WorkBoard.documentHtml")(function* (source: string, file: MarkdownFile) {
|
|
8
|
-
const
|
|
9
|
-
|
|
9
|
+
export const documentHtml = Effect.fn("WorkBoard.documentHtml")(function* (source: string, file: MarkdownFile, metadata = "") {
|
|
10
|
+
const headings = documentHeadings();
|
|
11
|
+
const html = yield* renderMarkdown(source, { file: file.path, headings });
|
|
12
|
+
return `${readingTools(headings.entries)}<p class="meta">${escapeHtml(file.path)} · ${updated(file.modified)}</p>${metadata}<article class="doc">${html}</article>`;
|
|
10
13
|
});
|
package/src/page/escape.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
const entities: Readonly<Record<string, string>> = { "
|
|
1
|
+
const entities: Readonly<Record<string, string>> = { "'": "'", '"': """, "&": "&", "<": "<", ">": ">" };
|
|
2
2
|
|
|
3
|
-
export const escapeHtml = (text: string): string => text.replaceAll(/[&<>"']/
|
|
3
|
+
export const escapeHtml = (text: string): string => text.replaceAll(/[&<>"']/gu, (character) => entities[character] ?? character);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function libraryScript(): string {
|
|
2
|
+
return `
|
|
3
|
+
const root = document.documentElement;
|
|
4
|
+
const key = "work-board:library:" + root.dataset.workspace;
|
|
5
|
+
const library = document.getElementById("reading-library");
|
|
6
|
+
let favorites = [];
|
|
7
|
+
let recents = [];
|
|
8
|
+
let notice = "";
|
|
9
|
+
const valid = (items) => Array.isArray(items) ? items.filter((item) => item && typeof item.file === "string" && item.file !== "" && typeof item.title === "string") : [];
|
|
10
|
+
try {
|
|
11
|
+
const saved = JSON.parse(localStorage.getItem(key) ?? "{}");
|
|
12
|
+
favorites = valid(saved?.favorites);
|
|
13
|
+
recents = valid(saved?.recents);
|
|
14
|
+
} catch { notice = "Reading history applies to this page only."; }
|
|
15
|
+
const save = () => {
|
|
16
|
+
try { localStorage.setItem(key, JSON.stringify({ favorites, recents })); notice = ""; }
|
|
17
|
+
catch { notice = "Reading history applies to this page only."; }
|
|
18
|
+
};
|
|
19
|
+
const hrefOf = (file) => "/" + file.split("/").map(encodeURIComponent).join("/");
|
|
20
|
+
const list = (title, items) => {
|
|
21
|
+
const heading = document.createElement("h2");
|
|
22
|
+
heading.className = "nav-heading";
|
|
23
|
+
heading.textContent = title;
|
|
24
|
+
const ul = document.createElement("ul");
|
|
25
|
+
const available = new Set([...document.querySelectorAll("#files a")].map((a) => new URL(a.href).pathname));
|
|
26
|
+
for (const item of items) {
|
|
27
|
+
const li = document.createElement("li");
|
|
28
|
+
const a = document.createElement("a");
|
|
29
|
+
a.href = hrefOf(item.file);
|
|
30
|
+
a.textContent = item.title;
|
|
31
|
+
if (item.file === document.getElementById("doc").dataset.file) a.setAttribute("aria-current", "page");
|
|
32
|
+
if (!available.has(a.pathname)) {
|
|
33
|
+
a.textContent += " (missing)";
|
|
34
|
+
a.className = "missing";
|
|
35
|
+
}
|
|
36
|
+
li.append(a); ul.append(li);
|
|
37
|
+
}
|
|
38
|
+
return [heading, ul];
|
|
39
|
+
};
|
|
40
|
+
export const refreshLibrary = (visit = false) => {
|
|
41
|
+
const doc = document.getElementById("doc");
|
|
42
|
+
const title = doc.querySelector("h1")?.textContent.trim() || doc.dataset.file;
|
|
43
|
+
document.title = title ? title + " · Work Board" : "Work Board";
|
|
44
|
+
const active = document.querySelector('#files a[aria-current="page"]');
|
|
45
|
+
if (active) active.title = title;
|
|
46
|
+
const toggle = document.getElementById("favorite-toggle");
|
|
47
|
+
if (toggle) {
|
|
48
|
+
const saved = favorites.some((item) => item.file === doc.dataset.file);
|
|
49
|
+
toggle.disabled = false;
|
|
50
|
+
toggle.setAttribute("aria-pressed", String(saved));
|
|
51
|
+
toggle.textContent = saved ? "Remove favorite" : "Save favorite";
|
|
52
|
+
if (visit) {
|
|
53
|
+
recents = [{ file: doc.dataset.file, title }, ...recents.filter((item) => item.file !== doc.dataset.file)].slice(0, 10);
|
|
54
|
+
save();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const status = document.createElement("p");
|
|
58
|
+
status.className = "library-status";
|
|
59
|
+
status.setAttribute("role", "status");
|
|
60
|
+
status.textContent = notice;
|
|
61
|
+
library.replaceChildren(...(favorites.length ? list("Favorites", favorites) : []), ...(recents.length ? list("Recent documents", recents) : []), ...(notice ? [status] : []));
|
|
62
|
+
library.hidden = favorites.length === 0 && recents.length === 0 && notice === "";
|
|
63
|
+
};
|
|
64
|
+
document.addEventListener("click", (event) => {
|
|
65
|
+
if (!event.target.closest?.("#favorite-toggle")) return;
|
|
66
|
+
const doc = document.getElementById("doc");
|
|
67
|
+
const file = doc.dataset.file;
|
|
68
|
+
favorites = favorites.some((item) => item.file === file) ? favorites.filter((item) => item.file !== file) : [...favorites, { file, title: doc.querySelector("h1")?.textContent.trim() || file }];
|
|
69
|
+
save(); refreshLibrary();
|
|
70
|
+
});
|
|
71
|
+
`;
|
|
72
|
+
}
|