@brett_lamy/docstream 1.7.0 → 1.9.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/README.md +18 -0
- package/dist/demo/DemoViewer.d.ts +8 -0
- package/dist/demo/DemoViewer.d.ts.map +1 -1
- package/dist/demo/DemoViewer.js +76 -3
- package/dist/demo/DemoViewer.js.map +1 -1
- package/dist/demo/PathBreadcrumb.d.ts +54 -0
- package/dist/demo/PathBreadcrumb.d.ts.map +1 -0
- package/dist/demo/PathBreadcrumb.js +245 -0
- package/dist/demo/PathBreadcrumb.js.map +1 -0
- package/dist/docs/DocsRenderer.d.ts +9 -2
- package/dist/docs/DocsRenderer.d.ts.map +1 -1
- package/dist/docs/DocsRenderer.js +13 -6
- package/dist/docs/DocsRenderer.js.map +1 -1
- package/dist/docs/DocumentProperties.d.ts +11 -0
- package/dist/docs/DocumentProperties.d.ts.map +1 -0
- package/dist/docs/DocumentProperties.js +63 -0
- package/dist/docs/DocumentProperties.js.map +1 -0
- package/dist/docs/TaskCheckbox.d.ts +24 -0
- package/dist/docs/TaskCheckbox.d.ts.map +1 -0
- package/dist/docs/TaskCheckbox.js +103 -0
- package/dist/docs/TaskCheckbox.js.map +1 -0
- package/dist/docs/properties.css +28 -0
- package/dist/docs/tasks.css +13 -0
- package/dist/gitbook/ast.d.ts +14 -0
- package/dist/gitbook/ast.d.ts.map +1 -1
- package/dist/gitbook/ast.js.map +1 -1
- package/dist/gitbook/index.d.ts +3 -0
- package/dist/gitbook/index.d.ts.map +1 -1
- package/dist/gitbook/index.js +2 -0
- package/dist/gitbook/index.js.map +1 -1
- package/dist/gitbook/parse.d.ts.map +1 -1
- package/dist/gitbook/parse.js +17 -3
- package/dist/gitbook/parse.js.map +1 -1
- package/dist/gitbook/properties.d.ts +15 -0
- package/dist/gitbook/properties.d.ts.map +1 -0
- package/dist/gitbook/properties.js +45 -0
- package/dist/gitbook/properties.js.map +1 -0
- package/dist/gitbook/serialize.d.ts.map +1 -1
- package/dist/gitbook/serialize.js +5 -2
- package/dist/gitbook/serialize.js.map +1 -1
- package/dist/gitbook/tasks.d.ts +11 -0
- package/dist/gitbook/tasks.d.ts.map +1 -0
- package/dist/gitbook/tasks.js +21 -0
- package/dist/gitbook/tasks.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/streamdown.d.ts +1 -1
- package/dist/streamdown.d.ts.map +1 -1
- package/dist/streamdown.js +2 -2
- package/dist/streamdown.js.map +1 -1
- package/dist/styles.css +228 -4
- package/package.json +15 -1
- package/src/demo/DemoViewer.tsx +98 -4
- package/src/demo/PathBreadcrumb.tsx +367 -0
- package/src/docs/DocsRenderer.tsx +22 -5
- package/src/docs/DocumentProperties.tsx +84 -0
- package/src/docs/TaskCheckbox.tsx +111 -0
- package/src/docs/properties.css +28 -0
- package/src/docs/tasks.css +13 -0
- package/src/gitbook/ast.ts +16 -0
- package/src/gitbook/index.ts +3 -0
- package/src/gitbook/parse.ts +15 -3
- package/src/gitbook/properties.ts +49 -0
- package/src/gitbook/serialize.ts +5 -2
- package/src/gitbook/tasks.ts +25 -0
- package/src/index.ts +7 -0
- package/src/streamdown.tsx +4 -0
- package/src/styles.css +228 -4
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.docs-document-properties {
|
|
2
|
+
color: var(--muted-foreground, #787878);
|
|
3
|
+
font: inherit;
|
|
4
|
+
font-size: 13px;
|
|
5
|
+
line-height: 1.5;
|
|
6
|
+
margin-block: 0 20px;
|
|
7
|
+
}
|
|
8
|
+
.docs-document-properties > summary { display: flex; align-items: center; gap: 6px; width: fit-content; padding-block: 5px; cursor: pointer; list-style: none; user-select: none; }
|
|
9
|
+
.docs-document-properties > summary::-webkit-details-marker { display: none; }
|
|
10
|
+
.docs-document-properties[open] > summary > svg { transform: rotate(90deg); }
|
|
11
|
+
.docs-properties-count { font-variant-numeric: tabular-nums; opacity: .65; margin-left: 2px; }
|
|
12
|
+
.docs-properties-list { margin: 6px 0; }
|
|
13
|
+
.docs-property-row { display: grid; grid-template-columns: minmax(80px, 130px) minmax(0, 1fr) 22px; align-items: start; gap: 10px; padding-block: 6px; border-bottom: 1px solid var(--border, #e8e8e8); }
|
|
14
|
+
.docs-property-row dt { padding-block: 3px; overflow-wrap: anywhere; }
|
|
15
|
+
.docs-property-row dd { margin: 0; min-width: 0; color: var(--foreground, #242424); }
|
|
16
|
+
.docs-property-value { display: block; white-space: pre-wrap; overflow-wrap: anywhere; padding-block: 3px; }
|
|
17
|
+
.docs-document-properties :is(input:not([type="checkbox"]), textarea) { box-sizing: border-box; width: 100%; min-width: 0; padding: 3px 5px; color: var(--foreground, #242424); font: inherit; line-height: inherit; background: transparent; border: 0; border-radius: 4px; }
|
|
18
|
+
.docs-document-properties textarea { resize: vertical; }
|
|
19
|
+
.docs-document-properties input[type="checkbox"] { accent-color: var(--primary, #a87900); margin: 6px 0; }
|
|
20
|
+
.docs-document-properties :is(input, textarea, button, summary):focus-visible { outline: 2px solid var(--primary, #a87900); outline-offset: 2px; }
|
|
21
|
+
.docs-document-properties button { display: inline-flex; align-items: center; justify-content: center; gap: 5px; color: inherit; background: transparent; border: 0; border-radius: 4px; font: inherit; cursor: pointer; }
|
|
22
|
+
.docs-property-remove { padding: 6px 3px; opacity: 0; }
|
|
23
|
+
.docs-property-row:hover .docs-property-remove, .docs-property-remove:focus-visible { opacity: 1; }
|
|
24
|
+
.docs-property-add { display: flex; align-items: center; gap: 8px; margin-block: 8px 0; padding: 3px 0; }
|
|
25
|
+
.docs-property-add input { max-width: 240px; }
|
|
26
|
+
.docs-property-add button { padding: 3px 6px; }
|
|
27
|
+
.docs-properties-error { color: var(--destructive, #bc3030); font-size: 12px; overflow-wrap: anywhere; }
|
|
28
|
+
.docs-properties-error pre { white-space: pre-wrap; }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
.docs-task-control { position: relative; display: inline-flex; width: 21px; height: 21px; flex: 0 0 21px; vertical-align: text-bottom; color: var(--muted-foreground, #7a7a7a); }
|
|
2
|
+
.docs-task-control .docs-task-checkbox { position: absolute; inset: 0; box-sizing: border-box; appearance: none; margin: 0; width: 100%; height: 100%; padding: 0; border: 0; border-radius: 50%; background: transparent; cursor: pointer; box-shadow: none; }
|
|
3
|
+
.docs-task-checkbox:focus-visible { outline: 2px solid var(--primary, #a87900); outline-offset: 3px; }
|
|
4
|
+
.docs-task-state-icon { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
|
|
5
|
+
.docs-task-control[data-task-state="verified"], .docs-task-control[data-task-state="in-progress"], .docs-task-control[data-task-state="implemented"] { color: var(--primary, #a87900); }
|
|
6
|
+
.docs-task-control[data-task-state="blocked"], .docs-task-control[data-task-state="refuted"] { color: var(--destructive, #bd3d36); }
|
|
7
|
+
.docs-task-state-menu { position: fixed; z-index: 10000; box-sizing: border-box; width: 284px; max-height: calc(100vh - 16px); overflow: auto; padding: 5px; border: 1px solid var(--border, #dedede); border-radius: 10px; background: var(--popover, #fff); color: var(--popover-foreground, #222); box-shadow: 0 8px 32px #0003, 0 1px 3px #0001; font-size: 13px; line-height: 1.4; }
|
|
8
|
+
.docs-task-state-menu > button { display: grid; grid-template-columns: 20px minmax(0, 1fr) 14px; align-items: center; gap: 9px; width: 100%; min-height: 35px; padding: 7px 8px; border: 0; border-radius: 5px; color: inherit; background: transparent; font: inherit; text-align: left; cursor: pointer; outline: none; }
|
|
9
|
+
.docs-task-state-menu > button:hover, .docs-task-state-menu > button:focus { color: var(--accent-foreground, #222); background: var(--accent, #eee); }
|
|
10
|
+
.docs-task-state-menu > button > svg:first-child { color: var(--muted-foreground, #777); }
|
|
11
|
+
.docs-task-state-menu > button[aria-checked="true"] > svg { color: var(--primary, #a87900); }
|
|
12
|
+
.docs-task-tooltip { position: fixed; z-index: 10001; transform: translate(-50%, -100%); border-radius: 5px; padding: 4px 8px; color: #fff; background: #303030; box-shadow: 0 2px 8px #0002; font: 12px/1.4 system-ui, sans-serif; white-space: nowrap; pointer-events: none; }
|
|
13
|
+
.docs-tasklist li > .docs-task-control { margin-top: 2px; }
|
package/src/gitbook/ast.ts
CHANGED
|
@@ -433,6 +433,8 @@ export interface ListItemNode extends ContainerSpacing {
|
|
|
433
433
|
type: "listItem"
|
|
434
434
|
children: Block[]
|
|
435
435
|
checked?: boolean // present only in task lists
|
|
436
|
+
/** Extended task state. Omitted for ordinary `[ ]` / `[x]` items for backwards compatibility. */
|
|
437
|
+
state?: TaskState
|
|
436
438
|
/** The item's marker as written (`*`, `3.`), when it isn't the one its list's pattern gives it. */
|
|
437
439
|
marker?: string
|
|
438
440
|
/** Spaces between the marker and the content (default 1). */
|
|
@@ -443,6 +445,8 @@ export interface ListItemNode extends ContainerSpacing {
|
|
|
443
445
|
checkMark?: "X"
|
|
444
446
|
}
|
|
445
447
|
|
|
448
|
+
export type TaskState = "pending" | "in-progress" | "implemented" | "blocked" | "parked" | "refuted" | "verified" | "cancelled"
|
|
449
|
+
|
|
446
450
|
export interface ListNode extends BlockSpacing {
|
|
447
451
|
type: "list"
|
|
448
452
|
ordered: boolean
|
|
@@ -563,9 +567,21 @@ export type Block =
|
|
|
563
567
|
| DefinitionNode
|
|
564
568
|
| RawNode
|
|
565
569
|
|
|
570
|
+
/** YAML frontmatter, kept separately from the document body. Comments, key order and quoting survive edits. */
|
|
571
|
+
export interface DocumentPropertiesNode {
|
|
572
|
+
/** YAML between the delimiters, using the document's normalized line endings. */
|
|
573
|
+
yaml: string
|
|
574
|
+
/** Opening delimiter as written (including an optional BOM). Defaults to `---`. */
|
|
575
|
+
opening?: string
|
|
576
|
+
/** Closing delimiter as written (`---` or `...`). Defaults to `---`. */
|
|
577
|
+
closing?: string
|
|
578
|
+
}
|
|
579
|
+
|
|
566
580
|
export interface DocumentNode {
|
|
567
581
|
type: "doc"
|
|
568
582
|
children: Block[]
|
|
583
|
+
/** First-party document properties from YAML frontmatter. */
|
|
584
|
+
properties?: DocumentPropertiesNode
|
|
569
585
|
/** Footnote citation definitions, in definition order. */
|
|
570
586
|
citations?: CitationDef[]
|
|
571
587
|
/** Blank lines before the trailing footnote definitions (default 1). */
|
package/src/gitbook/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Pure GitBook markdown engine (no React, no mermaid/streamdown). Safe to import
|
|
2
2
|
// in non-DOM environments such as a server-side AI agent or a Cloudflare Worker.
|
|
3
3
|
export type * from "./ast"
|
|
4
|
+
export { TASK_STATES, getTaskState, taskMarker, taskStateFromMarker } from "./tasks"
|
|
5
|
+
export { readDocumentProperties, removeDocumentProperty, setDocumentProperty } from "./properties"
|
|
6
|
+
export type { DocumentPropertyValue, ParsedDocumentProperties } from "./properties"
|
|
4
7
|
export { escapeAttr, unescapeAttr } from "./attrs"
|
|
5
8
|
export { closesFence, demoBody, fenceOpener, fenceTracker, parseDemoVariants, parseMarkdown, parseBlocks, trimPartialInlineToken } from "./parse"
|
|
6
9
|
export { fenceFor, serializeBlocks, serializeDemoFile, serializeMarkdown } from "./serialize"
|
package/src/gitbook/parse.ts
CHANGED
|
@@ -36,6 +36,7 @@ import type {
|
|
|
36
36
|
import { TAG_ATTRS, parseAttrs } from "./attrs"
|
|
37
37
|
import { containerTags, defaultDelimiterRow, definitionLine, delimiterAlign, fenceInfo, figureMarkdown, serializeBlocks, type TaggedNode } from "./serialize"
|
|
38
38
|
import { footnoteDefinitions, normalizeLabel, parseInline, plainText, refDefinitions } from "./inline"
|
|
39
|
+
import { taskStateFromMarker } from "./tasks"
|
|
39
40
|
|
|
40
41
|
// Minimal HTML-inline → markdown-inline bridge for HTML table cells.
|
|
41
42
|
function htmlToInlineMd(html: string): string {
|
|
@@ -256,7 +257,15 @@ export function footnoteLine(id: string, url: string, label?: string): string {
|
|
|
256
257
|
export function parseMarkdown(src: string): DocumentNode {
|
|
257
258
|
// Windows line endings throughout come back as such; mixed ones stay in the lines as written.
|
|
258
259
|
const crlf = src.includes("\r\n") && !/(^|[^\r])\n/.test(src)
|
|
259
|
-
|
|
260
|
+
let lines = src.split(crlf || !src.includes("\r\n") ? /\r?\n/ : "\n")
|
|
261
|
+
let properties: DocumentNode["properties"]
|
|
262
|
+
if (/^\uFEFF?---[ \t]*$/.test(lines[0])) {
|
|
263
|
+
const closing = lines.findIndex((line, index) => index > 0 && /^(?:---|\.\.\.)[ \t]*$/.test(line))
|
|
264
|
+
if (closing > 0) {
|
|
265
|
+
properties = { yaml: lines.slice(1, closing).join("\n"), opening: lines[0], closing: lines[closing] }
|
|
266
|
+
lines = lines.slice(closing + 1)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
260
269
|
refDefinitions.clear()
|
|
261
270
|
footnoteDefinitions.clear()
|
|
262
271
|
const fences = fenceTracker()
|
|
@@ -289,6 +298,7 @@ export function parseMarkdown(src: string): DocumentNode {
|
|
|
289
298
|
const trailing = group <= last
|
|
290
299
|
const { blocks, end } = parseBody(trailing ? lines.slice(0, group) : lines)
|
|
291
300
|
const doc: DocumentNode = { type: "doc", children: blocks }
|
|
301
|
+
if (properties) doc.properties = properties
|
|
292
302
|
if (footnoteDefinitions.size) {
|
|
293
303
|
doc.citations = [...footnoteDefinitions.entries()].map(([id, d]) => ({ id, ...d }))
|
|
294
304
|
}
|
|
@@ -1117,10 +1127,10 @@ function parseList(lines: string[], start: number): ParsedList {
|
|
|
1117
1127
|
let content = m[4]
|
|
1118
1128
|
let checked: boolean | undefined
|
|
1119
1129
|
let checkMark: string | undefined
|
|
1120
|
-
const taskMatch = content.match(/^\[([ xX])\]\s+(.*)$/)
|
|
1130
|
+
const taskMatch = content.match(/^\[([ xX~?bd!\-])\]\s+(.*)$/)
|
|
1121
1131
|
if (taskMatch) {
|
|
1122
1132
|
list.task = true
|
|
1123
|
-
checked = taskMatch[1]
|
|
1133
|
+
checked = /[xX]/.test(taskMatch[1])
|
|
1124
1134
|
checkMark = taskMatch[1]
|
|
1125
1135
|
content = taskMatch[2]
|
|
1126
1136
|
}
|
|
@@ -1158,6 +1168,8 @@ function parseList(lines: string[], start: number): ParsedList {
|
|
|
1158
1168
|
const para: Block = { type: "paragraph", children: parseInline(content) }
|
|
1159
1169
|
const children: Block[] = body.length ? [para, ...parseBlocks(body)] : [para]
|
|
1160
1170
|
const item: ListItemNode = checked === undefined ? { type: "listItem", children } : { type: "listItem", children, checked }
|
|
1171
|
+
const state = checkMark === undefined ? undefined : taskStateFromMarker(checkMark)
|
|
1172
|
+
if (state && state !== "pending" && state !== "verified") item.state = state
|
|
1161
1173
|
const expected = ordered ? `${(list.start ?? 1) + items.length}${list.delimiter ?? "."}` : (list.bullet ?? "-")
|
|
1162
1174
|
if (m[2] !== expected) item.marker = m[2]
|
|
1163
1175
|
if (m[3].length !== 1) item.pad = m[3].length
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { isMap, isScalar, parseDocument } from "yaml"
|
|
2
|
+
import type { DocumentPropertiesNode } from "./ast"
|
|
3
|
+
|
|
4
|
+
export type DocumentPropertyValue = string | number | boolean | null | DocumentPropertyValue[] | { [key: string]: DocumentPropertyValue }
|
|
5
|
+
export interface ParsedDocumentProperties {
|
|
6
|
+
values: Record<string, DocumentPropertyValue>
|
|
7
|
+
error?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Read properties without throwing on an incomplete or invalid document. YAML is never executed. */
|
|
11
|
+
export function readDocumentProperties(properties: DocumentPropertiesNode): ParsedDocumentProperties {
|
|
12
|
+
try {
|
|
13
|
+
const doc = parseDocument(properties.yaml)
|
|
14
|
+
if (doc.errors.length) return { values: {}, error: doc.errors[0].message }
|
|
15
|
+
if (doc.contents && !isMap(doc.contents)) return { values: {}, error: "Document properties must be a YAML mapping." }
|
|
16
|
+
const values = (doc.toJS({ maxAliasCount: 100 }) ?? {}) as Record<string, DocumentPropertyValue>
|
|
17
|
+
// Cyclic aliases can be preserved as YAML, but cannot be represented by editable JSON-like values.
|
|
18
|
+
JSON.stringify(values)
|
|
19
|
+
return { values }
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return { values: {}, error: error instanceof Error ? error.message : String(error) }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Change one property while retaining the other keys, comments, scalar styles and frontmatter delimiters. */
|
|
26
|
+
export function setDocumentProperty(properties: DocumentPropertiesNode, key: string, value: DocumentPropertyValue): DocumentPropertiesNode {
|
|
27
|
+
const doc = parseDocument(properties.yaml)
|
|
28
|
+
const parsed = readDocumentProperties(properties)
|
|
29
|
+
if (parsed.error) throw new Error(parsed.error)
|
|
30
|
+
const previous = doc.get(key, true)
|
|
31
|
+
const range = isScalar(previous) ? previous.range : undefined
|
|
32
|
+
doc.set(key, value)
|
|
33
|
+
const yaml = doc.toString().replace(/\n$/, "")
|
|
34
|
+
const next = parseDocument(yaml).get(key, true)
|
|
35
|
+
// A scalar edit is a source-range patch, leaving every other byte (including flow-list spacing) alone.
|
|
36
|
+
if (range && isScalar(next) && next.range) {
|
|
37
|
+
return { ...properties, yaml: properties.yaml.slice(0, range[0]) + yaml.slice(next.range[0], next.range[1]) + properties.yaml.slice(range[1]) }
|
|
38
|
+
}
|
|
39
|
+
return { ...properties, yaml }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Remove one property; the frontmatter remains available for adding more. */
|
|
43
|
+
export function removeDocumentProperty(properties: DocumentPropertiesNode, key: string): DocumentPropertiesNode {
|
|
44
|
+
const doc = parseDocument(properties.yaml)
|
|
45
|
+
const parsed = readDocumentProperties(properties)
|
|
46
|
+
if (parsed.error) throw new Error(parsed.error)
|
|
47
|
+
doc.delete(key)
|
|
48
|
+
return { ...properties, yaml: doc.toString().replace(/\n$/, "") }
|
|
49
|
+
}
|
package/src/gitbook/serialize.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { attr } from "./attrs"
|
|
|
27
27
|
import { normalizeLabel, serializeInline, serializeReference, withDocumentDefinitions } from "./inline"
|
|
28
28
|
import { plainText } from "./inline"
|
|
29
29
|
import { parseBlocks } from "./parse"
|
|
30
|
+
import { taskMarker } from "./tasks"
|
|
30
31
|
|
|
31
32
|
/** A `{% tag %}…{% endtag %}` container, block or item. */
|
|
32
33
|
export type TaggedNode = HintNode | TabsNode | TabNode | StepperNode | StepNode | ColumnsNode | ColumnNode | UpdatesNode | UpdateNode
|
|
@@ -114,7 +115,9 @@ export function serializeMarkdown(doc: DocumentNode): string {
|
|
|
114
115
|
documentCitations = citations
|
|
115
116
|
try {
|
|
116
117
|
return withDocumentDefinitions(defs, () => {
|
|
117
|
-
|
|
118
|
+
const properties = doc.properties
|
|
119
|
+
let out = properties ? `${properties.opening ?? "---"}\n${properties.yaml}${properties.yaml ? "\n" : ""}${properties.closing ?? "---"}\n` : ""
|
|
120
|
+
out += doc.children.map((b, k) => `${blankLines(b, k ? 1 : 0)}${serializeBlock(b)}\n`).join("")
|
|
118
121
|
// Footnote definitions not placed in the document end it, as a group.
|
|
119
122
|
const trailing = (doc.citations ?? []).filter((c) => !placed.has(c.id))
|
|
120
123
|
if (trailing.length) {
|
|
@@ -537,7 +540,7 @@ function inlineToHtml(nodes: Inline[]): string {
|
|
|
537
540
|
|
|
538
541
|
function serializeListItem(item: ListItemNode, list: ListNode, idx: number): string {
|
|
539
542
|
const marker = item.marker ?? (list.ordered ? `${(list.start ?? 1) + idx}${list.delimiter ?? "."}` : (list.bullet ?? "-"))
|
|
540
|
-
const check = list.task ? `[${
|
|
543
|
+
const check = list.task ? `[${taskMarker(item)}] ` : ""
|
|
541
544
|
const [first, ...rest] = item.children
|
|
542
545
|
const firstText = first?.type === "paragraph" ? serializeInline(first.children) : first ? serializeBlock(first) : ""
|
|
543
546
|
const restText = rest.length ? "\n" + indent(serializeBlocks(rest), " ".repeat(item.indent ?? 2)) : ""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ListItemNode, TaskState } from "./ast"
|
|
2
|
+
|
|
3
|
+
export const TASK_STATES: ReadonlyArray<{ id: TaskState; marker: string; label: string }> = [
|
|
4
|
+
{ id: "pending", marker: " ", label: "Pending" },
|
|
5
|
+
{ id: "in-progress", marker: "~", label: "In progress" },
|
|
6
|
+
{ id: "implemented", marker: "?", label: "Implemented · evidence needed" },
|
|
7
|
+
{ id: "blocked", marker: "b", label: "Blocked" },
|
|
8
|
+
{ id: "parked", marker: "d", label: "Parked · verification debt" },
|
|
9
|
+
{ id: "refuted", marker: "!", label: "Refuted" },
|
|
10
|
+
{ id: "verified", marker: "x", label: "Verified" },
|
|
11
|
+
{ id: "cancelled", marker: "-", label: "Cancelled" },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
export function taskStateFromMarker(marker: string): TaskState | undefined {
|
|
15
|
+
return TASK_STATES.find((state) => state.marker === (marker === "X" ? "x" : marker))?.id
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getTaskState(item: Pick<ListItemNode, "state" | "checked">): TaskState {
|
|
19
|
+
return item.state ?? (item.checked ? "verified" : "pending")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Explicit state wins; untouched uppercase `[X]` tasks retain their spelling. */
|
|
23
|
+
export function taskMarker(item: Pick<ListItemNode, "state" | "checked" | "checkMark">): string {
|
|
24
|
+
return item.state ? TASK_STATES.find((state) => state.id === item.state)?.marker ?? " " : item.checked ? item.checkMark ?? "x" : " "
|
|
25
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
export { GitbookStreamdown } from "./streamdown"
|
|
4
4
|
export type { GitbookStreamdownProps } from "./streamdown"
|
|
5
5
|
export { DocsRenderer, MarkdownContent } from "./docs/DocsRenderer"
|
|
6
|
+
export { TaskCheckbox, TASK_STATE_ICONS } from "./docs/TaskCheckbox"
|
|
7
|
+
export type { TaskCheckboxProps, TaskStateChangeHandler } from "./docs/TaskCheckbox"
|
|
8
|
+
export { TASK_STATES, getTaskState, taskMarker, taskStateFromMarker } from "./gitbook/tasks"
|
|
9
|
+
export { DocumentProperties } from "./docs/DocumentProperties"
|
|
10
|
+
export type { DocumentPropertiesProps } from "./docs/DocumentProperties"
|
|
11
|
+
export { readDocumentProperties, removeDocumentProperty, setDocumentProperty } from "./gitbook/properties"
|
|
12
|
+
export type { DocumentPropertyValue, ParsedDocumentProperties } from "./gitbook/properties"
|
|
6
13
|
export type { DocRenderOptions, LivePreviewProps, LivePreviewRenderer, SourceReferenceRenderer } from "./docs/DocsRenderer"
|
|
7
14
|
export { DocPageActions } from "./docs/PageActions"
|
|
8
15
|
export type { DocPageAction, DocPageActionsOptions, DocPageActionsProps } from "./docs/PageActions"
|
package/src/streamdown.tsx
CHANGED
|
@@ -35,6 +35,8 @@ export function GitbookStreamdown({
|
|
|
35
35
|
demoDependencies,
|
|
36
36
|
demoVFS,
|
|
37
37
|
pageActions,
|
|
38
|
+
documentProperties,
|
|
39
|
+
onTaskStateChange,
|
|
38
40
|
}: GitbookStreamdownProps) {
|
|
39
41
|
const content = markdown ?? children ?? ""
|
|
40
42
|
const gitbookDoc = useMemo(
|
|
@@ -59,6 +61,8 @@ export function GitbookStreamdown({
|
|
|
59
61
|
demoDependencies={demoDependencies}
|
|
60
62
|
demoVFS={demoVFS}
|
|
61
63
|
pageActions={isStreaming ? false : pageActions}
|
|
64
|
+
documentProperties={documentProperties}
|
|
65
|
+
onTaskStateChange={onTaskStateChange}
|
|
62
66
|
/>
|
|
63
67
|
</div>
|
|
64
68
|
)
|
package/src/styles.css
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import "./docs/properties.css";
|
|
2
|
+
@import "./docs/tasks.css";
|
|
1
3
|
|
|
2
4
|
[data-docstream],
|
|
3
5
|
.docs-article {
|
|
@@ -2064,13 +2066,76 @@
|
|
|
2064
2066
|
.docs-demo-files {
|
|
2065
2067
|
flex-shrink: 0;
|
|
2066
2068
|
box-sizing: border-box;
|
|
2067
|
-
|
|
2069
|
+
/* --docs-demo-files-w is set (px) once the reader drags the handle; 188px until then. */
|
|
2070
|
+
width: var(--docs-demo-files-w, 188px);
|
|
2068
2071
|
padding: 8px;
|
|
2069
2072
|
overflow-y: auto;
|
|
2070
2073
|
border-right: 1px solid var(--gb-border);
|
|
2071
2074
|
background: var(--gb-muted);
|
|
2072
2075
|
}
|
|
2073
2076
|
|
|
2077
|
+
/* The drag handle between the file list and the source: zero layout width, with a wider hit area
|
|
2078
|
+
and the same grip as the preview handle straddling the divider. */
|
|
2079
|
+
.docs-demo-files-handle {
|
|
2080
|
+
position: relative;
|
|
2081
|
+
z-index: 2;
|
|
2082
|
+
flex: 0 0 0;
|
|
2083
|
+
width: 0;
|
|
2084
|
+
cursor: col-resize;
|
|
2085
|
+
touch-action: none;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
.docs-demo-files-handle::before {
|
|
2089
|
+
position: absolute;
|
|
2090
|
+
top: 0;
|
|
2091
|
+
bottom: 0;
|
|
2092
|
+
left: -5px;
|
|
2093
|
+
width: 10px;
|
|
2094
|
+
content: "";
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
.docs-demo-files-handle::after {
|
|
2098
|
+
position: absolute;
|
|
2099
|
+
top: 50%;
|
|
2100
|
+
left: -2.5px;
|
|
2101
|
+
width: 5px;
|
|
2102
|
+
height: 40px;
|
|
2103
|
+
margin-top: -20px;
|
|
2104
|
+
border-radius: 3px;
|
|
2105
|
+
background: var(--gb-stage-dot);
|
|
2106
|
+
content: "";
|
|
2107
|
+
opacity: 0;
|
|
2108
|
+
transition: opacity 150ms ease, background 150ms ease, height 200ms ease, margin 200ms ease;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
.docs-demo-code:hover .docs-demo-files-handle::after {
|
|
2112
|
+
opacity: 1;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
.docs-demo-files-handle:hover::after,
|
|
2116
|
+
.docs-demo-files-handle:focus-visible::after,
|
|
2117
|
+
.docs-demo-files-handle[data-dragging]::after {
|
|
2118
|
+
height: 56px;
|
|
2119
|
+
margin-top: -28px;
|
|
2120
|
+
background: var(--gb-primary);
|
|
2121
|
+
opacity: 1;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
.docs-demo-files-handle:focus-visible {
|
|
2125
|
+
outline: none;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
.docs-demo-files-handle:focus-visible::before {
|
|
2129
|
+
outline: 2px solid var(--gb-primary);
|
|
2130
|
+
outline-offset: -1px;
|
|
2131
|
+
border-radius: 2px;
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
.docs-demo-code[data-dragging] {
|
|
2135
|
+
cursor: col-resize;
|
|
2136
|
+
user-select: none;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2074
2139
|
.docs-demo-files-h {
|
|
2075
2140
|
padding: 6px 8px 8px;
|
|
2076
2141
|
color: var(--gb-muted-foreground);
|
|
@@ -2143,18 +2208,174 @@
|
|
|
2143
2208
|
align-items: center;
|
|
2144
2209
|
justify-content: space-between;
|
|
2145
2210
|
gap: 8px;
|
|
2146
|
-
padding: 5px 6px 5px
|
|
2211
|
+
padding: 5px 6px 5px 8px;
|
|
2147
2212
|
border-bottom: 1px solid var(--gb-border);
|
|
2148
2213
|
color: var(--gb-muted-foreground);
|
|
2149
2214
|
font: 12px/1 var(--docs-demo-mono);
|
|
2150
2215
|
}
|
|
2151
2216
|
|
|
2152
|
-
|
|
2217
|
+
/* Breadcrumb path: one button per segment. The list is clipped to the header; the fit logic in
|
|
2218
|
+
PathBreadcrumb collapses the middle to a "…" before that matters. */
|
|
2219
|
+
.docs-demo-crumbs {
|
|
2220
|
+
position: relative;
|
|
2221
|
+
flex: 1;
|
|
2222
|
+
min-width: 0;
|
|
2223
|
+
overflow: hidden;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
.docs-demo-crumbs > .docs-demo-crumbs-list {
|
|
2227
|
+
display: flex;
|
|
2228
|
+
align-items: center;
|
|
2229
|
+
margin: 0;
|
|
2230
|
+
padding: 0;
|
|
2231
|
+
list-style: none;
|
|
2232
|
+
white-space: nowrap;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
.docs-demo-crumbs > .docs-demo-crumbs-measure {
|
|
2236
|
+
position: absolute;
|
|
2237
|
+
top: 0;
|
|
2238
|
+
left: 0;
|
|
2239
|
+
height: 0;
|
|
2240
|
+
visibility: hidden;
|
|
2241
|
+
pointer-events: none;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
.docs-demo-crumbs-list > .docs-demo-crumb-item {
|
|
2245
|
+
display: flex;
|
|
2246
|
+
flex: 0 0 auto;
|
|
2247
|
+
align-items: center;
|
|
2248
|
+
min-width: 0;
|
|
2249
|
+
margin: 0;
|
|
2250
|
+
padding: 0;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
.docs-demo-crumbs-list > .docs-demo-crumb-item[data-last] {
|
|
2254
|
+
flex: 0 1 auto;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
.docs-demo-crumb-sep {
|
|
2258
|
+
flex-shrink: 0;
|
|
2259
|
+
color: var(--gb-muted-foreground);
|
|
2260
|
+
opacity: 0.7;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
.docs-demo-crumb {
|
|
2264
|
+
display: inline-flex;
|
|
2265
|
+
align-items: center;
|
|
2266
|
+
gap: 5px;
|
|
2267
|
+
min-width: 0;
|
|
2268
|
+
box-sizing: border-box;
|
|
2269
|
+
height: 24px;
|
|
2270
|
+
padding: 0 6px;
|
|
2271
|
+
border: 0;
|
|
2272
|
+
border-radius: 6px;
|
|
2273
|
+
background: none;
|
|
2274
|
+
color: var(--gb-muted-foreground);
|
|
2275
|
+
font: inherit;
|
|
2276
|
+
cursor: pointer;
|
|
2277
|
+
transition: background 150ms ease, color 150ms ease;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
.docs-demo-crumb svg {
|
|
2281
|
+
flex-shrink: 0;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
.docs-demo-crumb-text {
|
|
2153
2285
|
overflow: hidden;
|
|
2154
2286
|
text-overflow: ellipsis;
|
|
2155
2287
|
white-space: nowrap;
|
|
2156
2288
|
}
|
|
2157
2289
|
|
|
2290
|
+
.docs-demo-crumb:hover,
|
|
2291
|
+
.docs-demo-crumb[aria-expanded="true"] {
|
|
2292
|
+
background: var(--gb-hover);
|
|
2293
|
+
color: var(--gb-panel-foreground);
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
.docs-demo-crumb[data-current] {
|
|
2297
|
+
color: var(--gb-panel-foreground);
|
|
2298
|
+
font-weight: 600;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
.docs-demo-crumb[data-current] svg {
|
|
2302
|
+
color: var(--gb-primary);
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
.docs-demo-crumb:focus-visible {
|
|
2306
|
+
outline: 2px solid var(--gb-primary);
|
|
2307
|
+
outline-offset: -2px;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
/* The sibling / hidden-segment menu. Portalled next to the docs root (so it isn't clipped by the
|
|
2311
|
+
viewer) and fixed under its trigger; --crumb-x / --crumb-y are set from the trigger's rect. */
|
|
2312
|
+
.docs-demo-crumb-menu {
|
|
2313
|
+
position: fixed;
|
|
2314
|
+
top: var(--crumb-y, 0);
|
|
2315
|
+
left: var(--crumb-x, 0);
|
|
2316
|
+
z-index: 60;
|
|
2317
|
+
box-sizing: border-box;
|
|
2318
|
+
min-width: 160px;
|
|
2319
|
+
max-width: min(320px, calc(100vw - 16px));
|
|
2320
|
+
max-height: min(320px, calc(100vh - 16px));
|
|
2321
|
+
padding: 4px;
|
|
2322
|
+
overflow-y: auto;
|
|
2323
|
+
border: 1px solid var(--gb-border);
|
|
2324
|
+
border-radius: 10px;
|
|
2325
|
+
background: var(--gb-panel);
|
|
2326
|
+
box-shadow: var(--gb-menu-shadow);
|
|
2327
|
+
color: var(--gb-panel-foreground);
|
|
2328
|
+
font: 12.5px/1.3 var(--docs-demo-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
.docs-demo-crumb-option {
|
|
2332
|
+
display: flex;
|
|
2333
|
+
align-items: center;
|
|
2334
|
+
gap: 7px;
|
|
2335
|
+
box-sizing: border-box;
|
|
2336
|
+
width: 100%;
|
|
2337
|
+
padding: 6px 8px;
|
|
2338
|
+
border: 0;
|
|
2339
|
+
border-radius: 6px;
|
|
2340
|
+
background: none;
|
|
2341
|
+
color: inherit;
|
|
2342
|
+
font: inherit;
|
|
2343
|
+
text-align: left;
|
|
2344
|
+
cursor: pointer;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
.docs-demo-crumb-option svg {
|
|
2348
|
+
flex-shrink: 0;
|
|
2349
|
+
color: var(--gb-muted-foreground);
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
.docs-demo-crumb-option:hover,
|
|
2353
|
+
.docs-demo-crumb-option:focus-visible {
|
|
2354
|
+
outline: none;
|
|
2355
|
+
background: var(--gb-hover);
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
.docs-demo-crumb-option[data-current] {
|
|
2359
|
+
color: var(--gb-primary);
|
|
2360
|
+
font-weight: 600;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
.docs-demo-crumb-option[data-current] svg {
|
|
2364
|
+
color: var(--gb-primary);
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
.docs-demo-crumb-option-text {
|
|
2368
|
+
flex: 1;
|
|
2369
|
+
min-width: 0;
|
|
2370
|
+
overflow: hidden;
|
|
2371
|
+
text-overflow: ellipsis;
|
|
2372
|
+
white-space: nowrap;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
.docs-demo-crumb-option .docs-demo-crumb-check {
|
|
2376
|
+
margin-left: auto;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2158
2379
|
.docs-demo-source-body {
|
|
2159
2380
|
flex: 1;
|
|
2160
2381
|
min-height: 0;
|
|
@@ -2195,7 +2416,8 @@
|
|
|
2195
2416
|
}
|
|
2196
2417
|
|
|
2197
2418
|
.docs-demo-files-h,
|
|
2198
|
-
.docs-demo-dir
|
|
2419
|
+
.docs-demo-dir,
|
|
2420
|
+
.docs-demo-files-handle {
|
|
2199
2421
|
display: none;
|
|
2200
2422
|
}
|
|
2201
2423
|
|
|
@@ -2338,6 +2560,8 @@
|
|
|
2338
2560
|
}
|
|
2339
2561
|
|
|
2340
2562
|
.docs-demo-handle::after,
|
|
2563
|
+
.docs-demo-files-handle::after,
|
|
2564
|
+
.docs-demo-crumb,
|
|
2341
2565
|
.docs-page-actions-more svg {
|
|
2342
2566
|
transition: none;
|
|
2343
2567
|
}
|