@brett_lamy/docstream 1.18.1 → 1.19.1

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 (41) hide show
  1. package/README.md +65 -19
  2. package/dist/docs/DocsRenderer.d.ts.map +1 -1
  3. package/dist/docs/DocsRenderer.js +14 -3
  4. package/dist/docs/DocsRenderer.js.map +1 -1
  5. package/dist/gitbook/parse.js +10 -2
  6. package/dist/gitbook/parse.js.map +1 -1
  7. package/dist/openapi/OpenApiOperation.d.ts.map +1 -1
  8. package/dist/openapi/OpenApiOperation.js +13 -5
  9. package/dist/openapi/OpenApiOperation.js.map +1 -1
  10. package/dist/replay/ReplayStageImpl.d.ts.map +1 -1
  11. package/dist/replay/ReplayStageImpl.js +4 -1
  12. package/dist/replay/ReplayStageImpl.js.map +1 -1
  13. package/dist/site/DocsSite.d.ts +74 -0
  14. package/dist/site/DocsSite.d.ts.map +1 -0
  15. package/dist/site/DocsSite.js +479 -0
  16. package/dist/site/DocsSite.js.map +1 -0
  17. package/dist/site/index.d.ts +7 -0
  18. package/dist/site/index.d.ts.map +1 -0
  19. package/dist/site/index.js +5 -0
  20. package/dist/site/index.js.map +1 -0
  21. package/dist/site/search.d.ts +28 -0
  22. package/dist/site/search.d.ts.map +1 -0
  23. package/dist/site/search.js +125 -0
  24. package/dist/site/search.js.map +1 -0
  25. package/dist/site/site.css +816 -0
  26. package/dist/site/tree.d.ts +105 -0
  27. package/dist/site/tree.d.ts.map +1 -0
  28. package/dist/site/tree.js +411 -0
  29. package/dist/site/tree.js.map +1 -0
  30. package/dist/styles.css +1 -0
  31. package/package.json +8 -1
  32. package/src/docs/DocsRenderer.tsx +14 -3
  33. package/src/gitbook/parse.ts +10 -2
  34. package/src/openapi/OpenApiOperation.tsx +13 -5
  35. package/src/replay/ReplayStageImpl.tsx +4 -1
  36. package/src/site/DocsSite.tsx +958 -0
  37. package/src/site/index.ts +18 -0
  38. package/src/site/search.ts +138 -0
  39. package/src/site/site.css +816 -0
  40. package/src/site/tree.ts +482 -0
  41. package/src/styles.css +1 -0
@@ -0,0 +1,18 @@
1
+ // A GitBook-style docs site from a folder of Markdown files. Root-entry safe: no almost-node.
2
+ export { DocsSite } from "./DocsSite"
3
+ export type { DocsSiteLink, DocsSiteProps, DocsSiteRouting, DocsSiteTheme } from "./DocsSite"
4
+ export {
5
+ SITE_FRONTMATTER_KEYS,
6
+ buildDocsTree,
7
+ findLinkedPage,
8
+ humanize,
9
+ isMarkdownPath,
10
+ normalizePath,
11
+ readFrontmatter,
12
+ resolveRelativePath,
13
+ slugForPath,
14
+ stripOrderPrefix,
15
+ } from "./tree"
16
+ export type { BuildDocsTreeOptions, DocsFiles, DocsNavItem, DocsPage, DocsTree } from "./tree"
17
+ export { createDocsSearchIndex, searchableText } from "./search"
18
+ export type { DocsSearchIndex, DocsSearchResult, DocsSearchSection } from "./search"
@@ -0,0 +1,138 @@
1
+ /* Client-side full-text search over a docs tree: each page is split into sections at its
2
+ headings, so a hit can jump straight to the heading it was found under. */
3
+ import { slugify } from "../gitbook/outline"
4
+ import { plainTitle, type DocsPage, type DocsTree } from "./tree"
5
+
6
+ export interface DocsSearchSection {
7
+ page: DocsPage
8
+ /** The heading this section starts at; undefined for the text before the first heading. */
9
+ heading?: string
10
+ /** The heading's anchor id. */
11
+ anchor?: string
12
+ text: string
13
+ }
14
+
15
+ export interface DocsSearchResult {
16
+ page: DocsPage
17
+ heading?: string
18
+ anchor?: string
19
+ /** A short excerpt around the first match, or the section's opening words. */
20
+ snippet: string
21
+ /** [start, end) of the match inside `snippet`, when the match is in the text. */
22
+ highlight?: [number, number]
23
+ score: number
24
+ }
25
+
26
+ export interface DocsSearchIndex {
27
+ sections: DocsSearchSection[]
28
+ search(query: string, limit?: number): DocsSearchResult[]
29
+ }
30
+
31
+ const HEADING = /^(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/
32
+ const TABS_TITLE = /^\{%\s*tabs\b[^%]*\btitle="([^"]+)"/
33
+
34
+ /** Markdown → searchable plain text (tags, HTML and punctuation removed, code kept). */
35
+ export function searchableText(markdown: string): string {
36
+ return markdown
37
+ .replace(/^(`{3,}|~{3,}).*$/gm, " ")
38
+ .replace(/\{%[^%]*%\}/g, " ")
39
+ .replace(/<[^>]+>/g, " ")
40
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
41
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
42
+ .replace(/^\[\^[^\]]+\]:.*$/gm, " ")
43
+ .replace(/\[\^[^\]]+\]/g, "")
44
+ .replace(/[*~`>|#]+/g, " ")
45
+ // `_` only as emphasis at a word's edge, so `@brett_lamy` and `snake_case` stay whole.
46
+ .replace(/(^|[\s(])_+|_+(?=[\s).,;:!?]|$)/g, "$1 ")
47
+ .replace(/^\s*[-+]\s+(\[.\]\s+)?/gm, " ")
48
+ .replace(/\$\$/g, " ")
49
+ .replace(/\s+/g, " ")
50
+ .trim()
51
+ }
52
+
53
+ function sectionsOf(page: DocsPage): DocsSearchSection[] {
54
+ const out: DocsSearchSection[] = []
55
+ let heading: string | undefined
56
+ let buf: string[] = []
57
+ let fence: string | null = null
58
+ const flush = () => {
59
+ const text = searchableText(buf.join("\n"))
60
+ if (text || heading) out.push({ page, heading, anchor: heading ? slugify(heading) : undefined, text })
61
+ buf = []
62
+ }
63
+ for (const line of page.body.split("\n")) {
64
+ const trimmed = line.trim()
65
+ const f = /^(`{3,}|~{3,})/.exec(trimmed)
66
+ if (f) {
67
+ if (!fence) fence = f[1]![0]!
68
+ else if (f[1]![0] === fence) fence = null
69
+ buf.push(line)
70
+ continue
71
+ }
72
+ const h = fence ? null : HEADING.exec(line) ?? TABS_TITLE.exec(trimmed)
73
+ if (h) {
74
+ flush()
75
+ heading = plainTitle(h.length === 3 ? h[2]! : h[1]!)
76
+ continue
77
+ }
78
+ buf.push(line)
79
+ }
80
+ flush()
81
+ return out
82
+ }
83
+
84
+ function snippetAround(text: string, at: number, length: number): { snippet: string; highlight?: [number, number] } {
85
+ if (at < 0) return { snippet: text.length > 140 ? `${text.slice(0, 140).trimEnd()}…` : text }
86
+ const start = Math.max(0, at - 50)
87
+ const end = Math.min(text.length, at + length + 90)
88
+ const lead = start > 0 ? "…" : ""
89
+ const snippet = `${lead}${text.slice(start, end).trim()}${end < text.length ? "…" : ""}`
90
+ const offset = lead.length + (at - start) - (text.slice(start, end).length - text.slice(start, end).trimStart().length)
91
+ return { snippet, highlight: [offset, offset + length] }
92
+ }
93
+
94
+ /** An in-memory index of every page; `search` ranks title, then heading, then body matches. */
95
+ export function createDocsSearchIndex(tree: DocsTree): DocsSearchIndex {
96
+ const sections = tree.pages.flatMap(sectionsOf)
97
+ return {
98
+ sections,
99
+ search(query, limit = 20) {
100
+ const q = query.trim().toLowerCase()
101
+ if (!q) return []
102
+ const terms = q.split(/\s+/).filter(Boolean)
103
+ const results: DocsSearchResult[] = []
104
+ for (const s of sections) {
105
+ const title = s.page.title.toLowerCase()
106
+ const heading = s.heading?.toLowerCase() ?? ""
107
+ const text = s.text.toLowerCase()
108
+ const haystack = `${title} ${heading} ${text}`
109
+ if (!terms.every((t) => haystack.includes(t))) continue
110
+ let score = 0
111
+ for (const t of terms) {
112
+ if (title.startsWith(t)) score += 12
113
+ else if (title.includes(t)) score += 8
114
+ if (heading.startsWith(t)) score += 6
115
+ else if (heading.includes(t)) score += 4
116
+ if (text.includes(t)) score += 1
117
+ }
118
+ if (title === q) score += 20
119
+ if (heading === q) score += 10
120
+ // A page's opening section carries its title match; deeper sections rank by their heading.
121
+ if (s.heading) score -= title.includes(terms[0]!) ? 6 : 0
122
+ const at = text.indexOf(terms[0]!)
123
+ results.push({ page: s.page, heading: s.heading, anchor: s.anchor, score, ...snippetAround(s.text, at, at >= 0 ? terms[0]!.length : 0) })
124
+ }
125
+ results.sort((a, b) => b.score - a.score)
126
+ const seen = new Set<string>()
127
+ const out: DocsSearchResult[] = []
128
+ for (const r of results) {
129
+ const key = `${r.page.slug}#${r.anchor ?? ""}`
130
+ if (seen.has(key)) continue
131
+ seen.add(key)
132
+ out.push(r)
133
+ if (out.length >= limit) break
134
+ }
135
+ return out
136
+ },
137
+ }
138
+ }