@clidey/dory 1.0.8 → 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 +12 -1
- package/bin/chat-server.ts +270 -0
- package/bin/dist/analytics-server.d.ts +11 -0
- package/bin/dist/analytics-server.d.ts.map +1 -0
- package/bin/dist/analytics-server.js +121 -0
- package/bin/dist/analytics-server.js.map +1 -0
- package/bin/dist/automation.d.ts +35 -0
- package/bin/dist/automation.d.ts.map +1 -0
- package/bin/dist/automation.js +259 -0
- package/bin/dist/automation.js.map +1 -0
- package/bin/dist/chat-server.d.ts +8 -0
- package/bin/dist/chat-server.d.ts.map +1 -0
- package/bin/dist/chat-server.js +322 -0
- package/bin/dist/chat-server.js.map +1 -0
- package/bin/dist/check.d.ts +16 -0
- package/bin/dist/check.d.ts.map +1 -0
- package/bin/dist/check.js +247 -0
- package/bin/dist/check.js.map +1 -0
- package/bin/dist/dory.js +252 -402
- package/bin/dist/dory.js.map +1 -1
- package/bin/dist/i18n-init.d.ts +6 -0
- package/bin/dist/i18n-init.d.ts.map +1 -0
- package/bin/dist/i18n-init.js +56 -0
- package/bin/dist/i18n-init.js.map +1 -0
- package/bin/dist/mcp-http.d.ts +8 -0
- package/bin/dist/mcp-http.d.ts.map +1 -0
- package/bin/dist/mcp-http.js +72 -0
- package/bin/dist/mcp-http.js.map +1 -0
- package/bin/dist/mcp.d.ts +2 -0
- package/bin/dist/mcp.d.ts.map +1 -0
- package/bin/dist/mcp.js +81 -0
- package/bin/dist/mcp.js.map +1 -0
- package/bin/dist/preview-entry.d.ts +2 -0
- package/bin/dist/preview-entry.d.ts.map +1 -0
- package/bin/dist/preview-entry.js +6 -0
- package/bin/dist/preview-entry.js.map +1 -0
- package/bin/dist/preview-server.d.ts +5 -0
- package/bin/dist/preview-server.d.ts.map +1 -0
- package/bin/dist/preview-server.js +159 -0
- package/bin/dist/preview-server.js.map +1 -0
- package/bin/dist/project-config.d.ts +22 -0
- package/bin/dist/project-config.d.ts.map +1 -0
- package/bin/dist/project-config.js +44 -0
- package/bin/dist/project-config.js.map +1 -0
- package/dist/index.html +57 -0
- package/dist/runtime/dory-runtime.css +2 -0
- package/dist/runtime/dory-runtime.html +14 -0
- package/dist/runtime/dory-runtime.js +5490 -0
- package/docs/api-playground-demo.mdx +119 -0
- package/docs/api-reference/asyncapi.json +324 -0
- package/docs/api-reference/openapi.json +366 -0
- package/docs/api-reference/operations/create.mdx +5 -0
- package/docs/api-reference/operations/delete.mdx +5 -0
- package/docs/api-reference/operations/get.mdx +5 -0
- package/docs/api-reference/operations/update.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-chat-message.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-notifications.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-presence-update.mdx +5 -0
- package/docs/api-reference/websocket-operations/send-chat-message.mdx +5 -0
- package/docs/api-reference/websocket-operations/send-presence-update.mdx +5 -0
- package/docs/assets/dory.jpeg +0 -0
- package/docs/components/accordion.mdx +145 -0
- package/docs/components/callouts.mdx +134 -0
- package/docs/components/cards.mdx +153 -0
- package/docs/components/code-blocks.mdx +281 -0
- package/docs/components/expandable.mdx +189 -0
- package/docs/components/icons.mdx +71 -0
- package/docs/components/images.mdx +82 -0
- package/docs/components/latex.mdx +148 -0
- package/docs/components/layout.mdx +175 -0
- package/docs/components/lists.mdx +134 -0
- package/docs/components/mermaid.mdx +149 -0
- package/docs/components/overview.mdx +81 -0
- package/docs/components/source.mdx +130 -0
- package/docs/components/steps.mdx +122 -0
- package/docs/components/tables.mdx +114 -0
- package/docs/components/tags.mdx +179 -0
- package/docs/components/typography.mdx +185 -0
- package/docs/configuration/analytics.mdx +45 -0
- package/docs/configuration/chat.mdx +59 -0
- package/docs/configuration/dory-json.mdx +278 -0
- package/docs/configuration/embed.mdx +73 -0
- package/docs/configuration/fonts.mdx +86 -0
- package/docs/configuration/internationalization.mdx +61 -0
- package/docs/configuration/navigation.mdx +217 -0
- package/docs/configuration/theming.mdx +166 -0
- package/docs/dory.json +129 -0
- package/docs/embed-example.html +176 -0
- package/docs/favicon.svg +18 -0
- package/docs/features.mdx +440 -0
- package/docs/getting-started/automation.mdx +76 -0
- package/docs/getting-started/configuration.mdx +95 -0
- package/docs/getting-started/installation.mdx +88 -0
- package/docs/getting-started/quick-start.mdx +53 -0
- package/docs/getting-started/whodb-ee-apps.mdx +122 -0
- package/docs/introduction.mdx +50 -0
- package/docs/logo/logo.svg +18 -0
- package/docs/playgrounds/api-playground.mdx +122 -0
- package/docs/playgrounds/websocket-playground.mdx +98 -0
- package/docs/social-card.png +0 -0
- package/docs/social-card.svg +17 -0
- package/docs/websocket-playground-demo.mdx +135 -0
- package/package.json +23 -17
- package/scripts/verify-runtime-parity.mjs +146 -0
- package/src/app.css +19 -1
- package/src/assets/fonts/HankenGrotesk.ttf +0 -0
- package/src/assets/fonts/OFL.txt +94 -0
- package/src/components/chat-assistant.tsx +229 -0
- package/src/components/dropdown.tsx +3 -3
- package/src/components/error-boundary.tsx +2 -2
- package/src/components/hooks.tsx +20 -1
- package/src/components/navigation.tsx +82 -51
- package/src/components/notification.tsx +2 -3
- package/src/components/routes.tsx +17 -19
- package/src/components/search-index.test.ts +15 -0
- package/src/components/search-index.ts +49 -48
- package/src/components/search.tsx +208 -104
- package/src/components/store.ts +27 -9
- package/src/config/mdx-common.ts +11 -0
- package/src/config/mdx.ts +10 -11
- package/src/embed/loader.ts +0 -3
- package/src/embed/types.ts +0 -2
- package/src/embed/widget-iframe.ts +19 -21
- package/src/embedded-runtime/config.ts +138 -0
- package/src/embedded-runtime/index.tsx +680 -0
- package/src/embedded-runtime/runtime.html +14 -0
- package/src/entry-server.tsx +46 -124
- package/src/main.tsx +16 -4
- package/src/mdx/accordion.tsx +3 -3
- package/src/mdx/api-playground.tsx +18 -16
- package/src/mdx/async-api.tsx +2 -2
- package/src/mdx/card.tsx +3 -2
- package/src/mdx/code.tsx +11 -9
- package/src/mdx/fence.tsx +6 -5
- package/src/mdx/icon-grid.tsx +4 -1
- package/src/mdx/icon-names.test.ts +15 -0
- package/src/mdx/icon-names.ts +60 -0
- package/src/mdx/icon-source-variants.ts +102 -0
- package/src/mdx/icon.tsx +12 -73
- package/src/mdx/latex.tsx +3 -2
- package/src/mdx/mdx.tsx +1 -1
- package/src/mdx/mermaid.tsx +12 -6
- package/src/mdx/open-api.tsx +130 -74
- package/src/mdx/preprocess.test.ts +16 -0
- package/src/mdx/preprocess.ts +61 -0
- package/src/mdx/processor.ts +4 -96
- package/src/mdx/source.tsx +5 -4
- package/src/mdx/specimen.tsx +38 -6
- package/src/mdx/tabs.tsx +29 -21
- package/src/mdx/text.tsx +9 -9
- package/src/mdx/try-it-panel.tsx +284 -0
- package/src/mdx/type-scale.tsx +4 -4
- package/src/mdx/websocket-playground.tsx +21 -14
- package/src/plugins/analytics-injector.ts +8 -0
- package/src/plugins/chat-dev-server.ts +32 -0
- package/src/plugins/docs-assets.test.ts +25 -0
- package/src/plugins/docs-assets.ts +6 -2
- package/src/plugins/frontmatter-dev-server.ts +2 -2
- package/src/plugins/frontmatter-generator.ts +1 -1
- package/src/plugins/html-files-middleware.test.ts +21 -1
- package/src/plugins/html-files-middleware.ts +26 -6
- package/src/plugins/html-metadata-injector.ts +24 -4
- package/src/plugins/llm-txt-dev-server.ts +2 -2
- package/src/plugins/llm-txt-generator.ts +21 -4
- package/src/plugins/prerender.ts +47 -20
- package/src/plugins/robots-generator.ts +1 -1
- package/src/plugins/sanitize.ts +1 -1
- package/src/plugins/shared.ts +6 -18
- package/src/plugins/sitemap-generator.ts +1 -1
- package/src/plugins/social-preview.test.ts +71 -0
- package/src/plugins/social-preview.ts +87 -0
- package/src/plugins/unified-mdx-plugin.ts +1 -1
- package/src/theme.scss +630 -207
- package/src/types/config.ts +53 -1
- package/src/ui/breadcrumb.tsx +17 -0
- package/src/ui/docs-header-view.tsx +32 -0
- package/src/ui/docs-navigation-view.tsx +69 -0
- package/src/ui/docs-page-frame.tsx +50 -0
- package/src/ui/dory-mark.tsx +9 -0
- package/src/ui/header.tsx +33 -90
- package/src/ui/layout.tsx +99 -153
- package/src/ui/mobile-navigation.tsx +10 -8
- package/src/ui/page-actions.tsx +67 -0
- package/src/ui/page-feedback.tsx +50 -0
- package/src/ui/page-footer.tsx +23 -0
- package/src/ui/prev-next-link.tsx +15 -69
- package/src/ui/prev-next-view.tsx +27 -0
- package/src/ui/table-of-content-view.tsx +167 -0
- package/src/ui/table-of-content.tsx +25 -183
- package/src/utils/analytics.test.ts +36 -0
- package/src/utils/analytics.ts +35 -0
- package/src/utils/colors.ts +7 -0
- package/src/utils/content-scope.test.ts +26 -0
- package/src/utils/content-scope.ts +39 -0
- package/src/utils/search-content.ts +12 -0
- package/src/utils/theme.ts +1 -7
- package/vite.config.ssr.ts +3 -3
- package/vite.config.ts +21 -15
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { CatalogIcon } from '@clidey/ux'
|
|
2
|
+
import { useEffect, useState } from 'preact/hooks'
|
|
3
|
+
import classNames from 'classnames'
|
|
4
|
+
|
|
5
|
+
// A heading counts as current once it scrolls below the sticky top bar.
|
|
6
|
+
const ACTIVE_OFFSET = 120
|
|
7
|
+
|
|
8
|
+
type Heading = {
|
|
9
|
+
id: string
|
|
10
|
+
title: string
|
|
11
|
+
level: 2 | 3
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TableOfContentsLabels = {
|
|
15
|
+
onThisPage?: string
|
|
16
|
+
editThisPage?: string
|
|
17
|
+
reportIssue?: string
|
|
18
|
+
backToTop?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type TableOfContentsViewProps = {
|
|
22
|
+
pageKey: string
|
|
23
|
+
editUrl?: string | null
|
|
24
|
+
issueUrl?: string | null
|
|
25
|
+
labels?: TableOfContentsLabels
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function slugify(text: string): string {
|
|
29
|
+
return text
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.trim()
|
|
32
|
+
.replace(/[^\p{L}\p{N}\s_-]/gu, '')
|
|
33
|
+
.replace(/\s+/g, '-')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function headingText(heading: Element): string {
|
|
37
|
+
return Array.from(heading.childNodes)
|
|
38
|
+
.filter(node => !(node instanceof Element && node.classList.contains('dory-anchor')))
|
|
39
|
+
.map(node => node.textContent ?? '')
|
|
40
|
+
.join('')
|
|
41
|
+
.trim()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function collectHeadings(article: Element): Heading[] {
|
|
45
|
+
const headings: Heading[] = []
|
|
46
|
+
article.querySelectorAll('h2, h3').forEach(heading => {
|
|
47
|
+
if (heading.closest('.dory-specimen') || heading.hasAttribute('data-toc-ignore')) return
|
|
48
|
+
const title = headingText(heading)
|
|
49
|
+
const id = heading.id || slugify(title)
|
|
50
|
+
if (!id) return
|
|
51
|
+
if (!heading.id) heading.id = id
|
|
52
|
+
headings.push({ id, title, level: heading.tagName === 'H2' ? 2 : 3 })
|
|
53
|
+
})
|
|
54
|
+
return headings
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The shared Dory heading rail used by the site and the app documentation viewer. */
|
|
58
|
+
export function TableOfContentsView({ pageKey, editUrl, issueUrl, labels }: TableOfContentsViewProps) {
|
|
59
|
+
const [headings, setHeadings] = useState<Heading[]>([])
|
|
60
|
+
const [currentId, setCurrentId] = useState('')
|
|
61
|
+
const [progress, setProgress] = useState(0)
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const article = document.querySelector('.dory-article')
|
|
65
|
+
if (!article) return
|
|
66
|
+
const update = () => setHeadings(collectHeadings(article))
|
|
67
|
+
update()
|
|
68
|
+
// Mermaid and other components may add their headings after mount.
|
|
69
|
+
const observer = new MutationObserver(update)
|
|
70
|
+
observer.observe(article, { childList: true, subtree: true })
|
|
71
|
+
return () => observer.disconnect()
|
|
72
|
+
}, [pageKey])
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
function onScroll() {
|
|
76
|
+
const scrollable = document.documentElement.scrollHeight - window.innerHeight
|
|
77
|
+
const ratio = scrollable > 0 ? Math.min(1, window.scrollY / scrollable) : 1
|
|
78
|
+
setProgress(Math.round(ratio * 100))
|
|
79
|
+
|
|
80
|
+
let current = headings[0]?.id ?? ''
|
|
81
|
+
if (ratio >= 1 && headings.length > 0) {
|
|
82
|
+
current = headings[headings.length - 1].id
|
|
83
|
+
} else {
|
|
84
|
+
for (const heading of headings) {
|
|
85
|
+
const el = document.getElementById(heading.id)
|
|
86
|
+
if (el && el.getBoundingClientRect().top <= ACTIVE_OFFSET) current = heading.id
|
|
87
|
+
else if (el) break
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
setCurrentId(current)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onScroll()
|
|
94
|
+
window.addEventListener('scroll', onScroll, { passive: true })
|
|
95
|
+
window.addEventListener('resize', onScroll)
|
|
96
|
+
return () => {
|
|
97
|
+
window.removeEventListener('scroll', onScroll)
|
|
98
|
+
window.removeEventListener('resize', onScroll)
|
|
99
|
+
}
|
|
100
|
+
}, [headings])
|
|
101
|
+
|
|
102
|
+
let sectionNumber = 0
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<aside className="dory-toc hidden w-52 shrink-0 xl:block" aria-labelledby="on-this-page-title">
|
|
106
|
+
{headings.length > 0 && (
|
|
107
|
+
<div className="sticky top-24 max-h-[calc(100vh-7rem)] overflow-y-auto pb-6">
|
|
108
|
+
<div className="flex items-center justify-between">
|
|
109
|
+
<h2 id="on-this-page-title" className="dory-toc-label">{labels?.onThisPage ?? 'On this page'}</h2>
|
|
110
|
+
<span className="dory-toc-label">{progress}%</span>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="dory-toc-track mt-2.5" aria-hidden="true">
|
|
113
|
+
<div className="dory-toc-fill" style={{ width: `${progress}%` }} />
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<ol role="list" className="dory-toc-list mt-5">
|
|
117
|
+
{headings.map(heading => {
|
|
118
|
+
if (heading.level === 2) sectionNumber += 1
|
|
119
|
+
return (
|
|
120
|
+
<li key={heading.id}>
|
|
121
|
+
<a
|
|
122
|
+
href={`#${heading.id}`}
|
|
123
|
+
aria-current={heading.id === currentId ? 'location' : undefined}
|
|
124
|
+
className={classNames('dory-toc-link', {
|
|
125
|
+
active: heading.id === currentId,
|
|
126
|
+
'dory-toc-sub': heading.level === 3,
|
|
127
|
+
})}
|
|
128
|
+
>
|
|
129
|
+
{heading.level === 2 && (
|
|
130
|
+
<span className="dory-toc-number">{String(sectionNumber).padStart(2, '0')}</span>
|
|
131
|
+
)}
|
|
132
|
+
<span>{heading.title}</span>
|
|
133
|
+
</a>
|
|
134
|
+
</li>
|
|
135
|
+
)
|
|
136
|
+
})}
|
|
137
|
+
</ol>
|
|
138
|
+
|
|
139
|
+
<div className="dory-toc-actions mt-6 space-y-2.5 border-t border-[var(--border)] pt-5">
|
|
140
|
+
{editUrl && (
|
|
141
|
+
<a href={editUrl} target="_blank" rel="noopener noreferrer">
|
|
142
|
+
<CatalogIcon name="edit" aria-hidden="true" className="h-3.5 w-3.5" />
|
|
143
|
+
{labels?.editThisPage ?? 'Edit this page'}
|
|
144
|
+
</a>
|
|
145
|
+
)}
|
|
146
|
+
{issueUrl && (
|
|
147
|
+
<a href={issueUrl} target="_blank" rel="noopener noreferrer">
|
|
148
|
+
<CatalogIcon name="alert-circle" aria-hidden="true" className="h-3.5 w-3.5" />
|
|
149
|
+
{labels?.reportIssue ?? 'Report an issue'}
|
|
150
|
+
</a>
|
|
151
|
+
)}
|
|
152
|
+
<a
|
|
153
|
+
href="#"
|
|
154
|
+
onClick={event => {
|
|
155
|
+
event.preventDefault()
|
|
156
|
+
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<CatalogIcon name="chevron-up" aria-hidden="true" className="h-3.5 w-3.5" />
|
|
160
|
+
{labels?.backToTop ?? 'Back to top'}
|
|
161
|
+
</a>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</aside>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
@@ -1,192 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.replace(/[^\p{L}\p{N}\s_-]/gu, '')
|
|
15
|
-
.replace(/\s+/g, '-')
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type Section = {
|
|
19
|
-
id: string
|
|
20
|
-
title: string
|
|
21
|
-
children: Array<Subsection>
|
|
1
|
+
import { useEffect, useState } from 'preact/hooks'
|
|
2
|
+
import docsConfig from '../../docs/dory.json' with { type: 'json' }
|
|
3
|
+
import type { DoryConfig } from '../types/config'
|
|
4
|
+
import { usePathname } from '../components/hooks'
|
|
5
|
+
import { TableOfContentsView } from './table-of-content-view'
|
|
6
|
+
|
|
7
|
+
const config = docsConfig as DoryConfig
|
|
8
|
+
|
|
9
|
+
function editUrl(pathname: string) {
|
|
10
|
+
const repo = config.repository
|
|
11
|
+
if (!repo) return null
|
|
12
|
+
const dir = repo.path ? `${repo.path.replace(/\/$/, '')}/` : ''
|
|
13
|
+
return `${repo.url}/edit/${repo.branch ?? 'main'}/${dir}${pathname.replace(/^\//, '')}.mdx`
|
|
22
14
|
}
|
|
23
15
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
function issueUrl() {
|
|
17
|
+
const repo = config.repository
|
|
18
|
+
if (!repo) return null
|
|
19
|
+
const title = encodeURIComponent(`Docs: ${document.title}`)
|
|
20
|
+
const body = encodeURIComponent(`Page: ${window.location.origin}${window.location.pathname}`)
|
|
21
|
+
return `${repo.url}/issues/new?title=${title}&body=${body}`
|
|
27
22
|
}
|
|
28
23
|
|
|
24
|
+
/** Site TOC with repository actions; the embedded viewer uses the shared view directly. */
|
|
29
25
|
export function TableOfContents() {
|
|
30
|
-
const
|
|
31
|
-
const [
|
|
32
|
-
const [isContentLoaded, setIsContentLoaded] = useState(false)
|
|
33
|
-
|
|
34
|
-
const collectHeadings = useCallback(() => {
|
|
35
|
-
const article = document.querySelector('article')
|
|
36
|
-
if (!article) return
|
|
37
|
-
|
|
38
|
-
// Wait for content to be loaded
|
|
39
|
-
const observer = new MutationObserver(() => {
|
|
40
|
-
const headings = article.querySelectorAll('h2:not([style*="display: none"]), h3:not([style*="display: none"])')
|
|
41
|
-
const sections: Section[] = []
|
|
42
|
-
let currentSection: Section | null = null
|
|
43
|
-
|
|
44
|
-
// Heading text without the trailing "#" anchor link
|
|
45
|
-
const headingText = (heading: Element) =>
|
|
46
|
-
Array.from(heading.childNodes)
|
|
47
|
-
.filter((node) => !(node instanceof Element && node.classList.contains('dory-anchor')))
|
|
48
|
-
.map((node) => node.textContent ?? '')
|
|
49
|
-
.join('')
|
|
50
|
-
.trim()
|
|
51
|
-
|
|
52
|
-
headings.forEach((heading) => {
|
|
53
|
-
// Check if heading is a valid Element and is visible
|
|
54
|
-
if (!(heading instanceof Element)) return
|
|
55
|
-
const style = window.getComputedStyle(heading)
|
|
56
|
-
if (style.display === 'none' || style.visibility === 'hidden') return
|
|
57
|
-
|
|
58
|
-
// Prefer the build-time id from rehype-slug; only assign when missing
|
|
59
|
-
const id = heading.id || slugify(headingText(heading))
|
|
60
|
-
if (!heading.id && id) {
|
|
61
|
-
heading.id = id
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (heading.tagName === 'H2') {
|
|
65
|
-
currentSection = {
|
|
66
|
-
id,
|
|
67
|
-
title: headingText(heading),
|
|
68
|
-
children: []
|
|
69
|
-
}
|
|
70
|
-
sections.push(currentSection)
|
|
71
|
-
} else if (heading.tagName === 'H3' && currentSection) {
|
|
72
|
-
currentSection.children.push({
|
|
73
|
-
id,
|
|
74
|
-
title: headingText(heading)
|
|
75
|
-
})
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
setSections(sections)
|
|
80
|
-
setIsContentLoaded(true)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
observer.observe(article, {
|
|
84
|
-
childList: true,
|
|
85
|
-
subtree: true,
|
|
86
|
-
attributes: true,
|
|
87
|
-
attributeFilter: ['style', 'class']
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
return () => observer.disconnect()
|
|
91
|
-
}, [])
|
|
26
|
+
const pathname = usePathname()
|
|
27
|
+
const [links, setLinks] = useState<{ edit: string | null; issue: string | null }>({ edit: null, issue: null })
|
|
92
28
|
|
|
93
29
|
useEffect(() => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}, [collectHeadings])
|
|
97
|
-
|
|
98
|
-
const getHeadings = useCallback(() => {
|
|
99
|
-
return sections
|
|
100
|
-
.flatMap((section) => [section.id, ...section.children.map((child) => child.id)])
|
|
101
|
-
.map((id) => {
|
|
102
|
-
const el = document.getElementById(id)
|
|
103
|
-
if (!el) return null
|
|
104
|
-
const top = window.scrollY + el.getBoundingClientRect().top
|
|
105
|
-
return { id, top }
|
|
106
|
-
})
|
|
107
|
-
.filter((x): x is { id: string; top: number } => x !== null)
|
|
108
|
-
}, [sections])
|
|
109
|
-
|
|
110
|
-
useEffect(() => {
|
|
111
|
-
if (!isContentLoaded || sections.length === 0) return
|
|
112
|
-
const headings = getHeadings()
|
|
113
|
-
|
|
114
|
-
function onScroll() {
|
|
115
|
-
const top = window.scrollY
|
|
116
|
-
let current = headings[0].id
|
|
117
|
-
for (const heading of headings) {
|
|
118
|
-
if (top >= heading.top - 10) {
|
|
119
|
-
current = heading.id
|
|
120
|
-
} else {
|
|
121
|
-
break
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
setCurrentSection(current)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
window.addEventListener('scroll', onScroll, { passive: true })
|
|
128
|
-
onScroll()
|
|
129
|
-
return () => {
|
|
130
|
-
window.removeEventListener('scroll', onScroll)
|
|
131
|
-
}
|
|
132
|
-
}, [getHeadings, sections, isContentLoaded])
|
|
133
|
-
|
|
134
|
-
function isActive(section: Section | Subsection) {
|
|
135
|
-
return section.id === currentSection
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!isContentLoaded) {
|
|
139
|
-
return null
|
|
140
|
-
}
|
|
30
|
+
setLinks({ edit: editUrl(pathname), issue: issueUrl() })
|
|
31
|
+
}, [pathname])
|
|
141
32
|
|
|
142
|
-
return
|
|
143
|
-
<div className="hidden xl:sticky xl:top-[4.75rem] xl:-mr-6 xl:block xl:h-[calc(100vh-4.75rem)] xl:flex-none xl:overflow-y-auto xl:py-16 xl:pr-6 pl-8">
|
|
144
|
-
<nav aria-labelledby="on-this-page-title" className="w-56">
|
|
145
|
-
{sections.length > 0 && (
|
|
146
|
-
<>
|
|
147
|
-
<h2
|
|
148
|
-
id="on-this-page-title"
|
|
149
|
-
className="font-display text-sm font-medium text-foreground dark:text-white"
|
|
150
|
-
>
|
|
151
|
-
On this page
|
|
152
|
-
</h2>
|
|
153
|
-
<ol role="list" className="mt-4 space-y-3 text-sm">
|
|
154
|
-
{sections.map((section) => (
|
|
155
|
-
<li key={section.id}>
|
|
156
|
-
<h3>
|
|
157
|
-
<a
|
|
158
|
-
href={`#${section.id}`}
|
|
159
|
-
className={classNames({
|
|
160
|
-
"text-brand-foreground": isActive(section),
|
|
161
|
-
"font-normal text-muted-foreground hover:text-foreground/80 dark:hover:text-slate-300": !isActive(section),
|
|
162
|
-
})}>
|
|
163
|
-
{section.title}
|
|
164
|
-
</a>
|
|
165
|
-
</h3>
|
|
166
|
-
{section.children.length > 0 && (
|
|
167
|
-
<ol role="list" className="mt-2 space-y-3 pl-5 text-muted-foreground dark:text-muted-foreground">
|
|
168
|
-
{section.children.map((subSection) => (
|
|
169
|
-
<li key={subSection.id}>
|
|
170
|
-
<a
|
|
171
|
-
href={`#${subSection.id}`}
|
|
172
|
-
className={
|
|
173
|
-
isActive(subSection)
|
|
174
|
-
? 'text-brand-foreground'
|
|
175
|
-
: 'hover:text-muted-foreground dark:hover:text-slate-300'
|
|
176
|
-
}
|
|
177
|
-
>
|
|
178
|
-
{subSection.title}
|
|
179
|
-
</a>
|
|
180
|
-
</li>
|
|
181
|
-
))}
|
|
182
|
-
</ol>
|
|
183
|
-
)}
|
|
184
|
-
</li>
|
|
185
|
-
))}
|
|
186
|
-
</ol>
|
|
187
|
-
</>
|
|
188
|
-
)}
|
|
189
|
-
</nav>
|
|
190
|
-
</div>
|
|
191
|
-
)
|
|
33
|
+
return <TableOfContentsView pageKey={pathname} editUrl={links.edit} issueUrl={links.issue} />
|
|
192
34
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
describe('documentation analytics delivery', () => {
|
|
4
|
+
beforeEach(() => vi.resetModules())
|
|
5
|
+
afterEach(() => vi.unstubAllGlobals())
|
|
6
|
+
|
|
7
|
+
it('sends a redacted search query to PostHog without contacting a Dory server', async () => {
|
|
8
|
+
const capture = vi.fn()
|
|
9
|
+
const fetch = vi.fn()
|
|
10
|
+
vi.stubGlobal('window', { posthog: { capture } })
|
|
11
|
+
vi.stubGlobal('fetch', fetch)
|
|
12
|
+
const { captureDocsEvent } = await import('./analytics')
|
|
13
|
+
|
|
14
|
+
captureDocsEvent('docs_search', { query: 'api key=short123 email me@example.com', result_count: 0, path: '/intro' })
|
|
15
|
+
expect(capture).toHaveBeenCalledOnce()
|
|
16
|
+
expect(capture).toHaveBeenCalledWith('docs_search', expect.objectContaining({
|
|
17
|
+
query: expect.not.stringContaining('short123'),
|
|
18
|
+
result_count: 0,
|
|
19
|
+
path: '/intro',
|
|
20
|
+
}))
|
|
21
|
+
expect(capture.mock.calls[0][1].query).not.toContain('me@example.com')
|
|
22
|
+
expect(fetch).not.toHaveBeenCalled()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('does not require a Dory analytics server', async () => {
|
|
26
|
+
const capture = vi.fn()
|
|
27
|
+
const fetch = vi.fn()
|
|
28
|
+
vi.stubGlobal('window', { posthog: { capture } })
|
|
29
|
+
vi.stubGlobal('fetch', fetch)
|
|
30
|
+
const { captureDocsEvent } = await import('./analytics')
|
|
31
|
+
|
|
32
|
+
captureDocsEvent('docs_code_copied', { path: '/intro' })
|
|
33
|
+
expect(capture).toHaveBeenCalledOnce()
|
|
34
|
+
expect(fetch).not.toHaveBeenCalled()
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface Window {
|
|
3
|
+
posthog?: { capture: (event: string, properties?: Record<string, unknown>) => void }
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type DocsEvent =
|
|
8
|
+
| 'docs_search'
|
|
9
|
+
| 'docs_search_result_clicked'
|
|
10
|
+
| 'docs_page_feedback'
|
|
11
|
+
| 'docs_code_copied'
|
|
12
|
+
| 'docs_page_engaged'
|
|
13
|
+
| 'docs_chat_opened'
|
|
14
|
+
| 'docs_chat_question'
|
|
15
|
+
| 'docs_chat_answer'
|
|
16
|
+
| 'docs_chat_answer_copied'
|
|
17
|
+
| 'docs_chat_code_copied'
|
|
18
|
+
| 'docs_chat_error'
|
|
19
|
+
| 'docs_chat_source_clicked'
|
|
20
|
+
|
|
21
|
+
/** Search text may contain credentials or personal data typed by mistake. */
|
|
22
|
+
function sanitizeQuery(value: string): string {
|
|
23
|
+
return value.trim().slice(0, 160)
|
|
24
|
+
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[email]')
|
|
25
|
+
.replace(/\b(?:Bearer\s+\S+|sk-[A-Za-z0-9_-]{12,}|(?:api[\s_-]?key|token|password|secret)\s*[:=]\s*\S+)/gi, '[redacted]')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Sends a small event to the configured PostHog project. */
|
|
29
|
+
export function captureDocsEvent(event: DocsEvent, properties: Record<string, unknown>): void {
|
|
30
|
+
if (typeof window === 'undefined') return
|
|
31
|
+
const safeProperties = { ...properties }
|
|
32
|
+
if (typeof safeProperties.query === 'string') safeProperties.query = sanitizeQuery(safeProperties.query)
|
|
33
|
+
try { window.posthog?.capture(event, safeProperties) }
|
|
34
|
+
catch { /* Analytics must not interrupt reading. */ }
|
|
35
|
+
}
|
package/src/utils/colors.ts
CHANGED
|
@@ -16,6 +16,13 @@ export function loadColors() {
|
|
|
16
16
|
if (!colors) return;
|
|
17
17
|
|
|
18
18
|
const root = document.documentElement;
|
|
19
|
+
const light = colors.light || colors.primary;
|
|
20
|
+
const dark = colors.dark || colors.primary;
|
|
21
|
+
if (light || dark) {
|
|
22
|
+
root.setAttribute('data-dory-colors', 'true');
|
|
23
|
+
root.style.setProperty('--brand-light', light || dark);
|
|
24
|
+
root.style.setProperty('--brand-dark', dark || light);
|
|
25
|
+
}
|
|
19
26
|
|
|
20
27
|
// Brand accent colors
|
|
21
28
|
if (colors.primary) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { contentScope, scopedPath } from './content-scope';
|
|
3
|
+
import type { DoryConfig } from '../types/config';
|
|
4
|
+
|
|
5
|
+
const navigation = { tabs: [{ tab: 'Guides', groups: [{ group: 'Start', pages: ['intro'] }] }] };
|
|
6
|
+
const config = {
|
|
7
|
+
name: 'Example', favicon: 'favicon.svg', logo: { light: 'logo.svg', dark: 'logo.svg' },
|
|
8
|
+
navigation,
|
|
9
|
+
i18n: { defaultLocale: 'en', locales: [{ code: 'en', label: 'English' }, { code: 'fr', label: 'Français' }] },
|
|
10
|
+
versions: [{ id: 'v1', label: 'Version 1' }],
|
|
11
|
+
} satisfies DoryConfig;
|
|
12
|
+
|
|
13
|
+
describe('content scope', () => {
|
|
14
|
+
it('preserves current URLs for the default language and version', () => {
|
|
15
|
+
expect(contentScope(config, '/intro')).toMatchObject({ locale: 'en', version: '', prefix: '', page: '/intro' });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('resolves a translated, versioned page', () => {
|
|
19
|
+
expect(contentScope(config, '/fr/v1/intro')).toMatchObject({ locale: 'fr', version: 'v1', prefix: '/fr/v1', page: '/intro' });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('keeps the page when switching language and version', () => {
|
|
23
|
+
expect(scopedPath(config, '/fr/v1/intro', 'en', '')).toBe('/intro');
|
|
24
|
+
expect(scopedPath(config, '/intro', 'fr', 'v1')).toBe('/fr/v1/intro');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DoryConfig, DoryNavigation } from '../types/config.ts';
|
|
2
|
+
|
|
3
|
+
export interface ContentScope {
|
|
4
|
+
locale: string;
|
|
5
|
+
version: string;
|
|
6
|
+
prefix: string;
|
|
7
|
+
page: string;
|
|
8
|
+
navigation: DoryNavigation;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Content lives at the route it publishes: docs/fr/intro.mdx -> /fr/intro. */
|
|
12
|
+
export function contentScope(config: DoryConfig, pathname: string): ContentScope {
|
|
13
|
+
const parts = pathname.split('/').filter(Boolean);
|
|
14
|
+
const locales = config.i18n?.locales ?? [];
|
|
15
|
+
const defaultLocale = config.i18n?.defaultLocale ?? locales[0]?.code ?? 'en';
|
|
16
|
+
const localeEntry = locales.find(entry => entry.code === parts[0] && entry.code !== defaultLocale);
|
|
17
|
+
if (localeEntry) parts.shift();
|
|
18
|
+
|
|
19
|
+
const versionEntry = config.versions?.find(entry => entry.id && entry.id === parts[0]);
|
|
20
|
+
if (versionEntry) parts.shift();
|
|
21
|
+
|
|
22
|
+
const locale = localeEntry?.code ?? defaultLocale;
|
|
23
|
+
const version = versionEntry?.id ?? '';
|
|
24
|
+
const prefix = [localeEntry?.code, version].filter(Boolean).map(segment => `/${segment}`).join('');
|
|
25
|
+
return {
|
|
26
|
+
locale,
|
|
27
|
+
version,
|
|
28
|
+
prefix,
|
|
29
|
+
page: `/${parts.join('/')}`,
|
|
30
|
+
navigation: (version ? localeEntry?.versions?.[version] : undefined) ?? localeEntry?.navigation ?? versionEntry?.navigation ?? config.navigation,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function scopedPath(config: DoryConfig, pathname: string, locale: string, version: string): string {
|
|
35
|
+
const current = contentScope(config, pathname);
|
|
36
|
+
const defaultLocale = config.i18n?.defaultLocale ?? config.i18n?.locales[0]?.code ?? 'en';
|
|
37
|
+
const prefix = [locale !== defaultLocale ? locale : '', version].filter(Boolean).map(segment => `/${segment}`).join('');
|
|
38
|
+
return `${prefix}${current.page}`;
|
|
39
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Extracts the searchable prose from MDX in both builds and the runtime. */
|
|
2
|
+
export function stripMdxToText(content: string): string {
|
|
3
|
+
return content
|
|
4
|
+
.replace(/^import\s+.*$/gm, '')
|
|
5
|
+
.replace(/^export\s+.*$/gm, '')
|
|
6
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
7
|
+
.replace(/<[^>]+>/g, '')
|
|
8
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
9
|
+
.replace(/[#*_~`>|]/g, '')
|
|
10
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
11
|
+
.trim();
|
|
12
|
+
}
|
package/src/utils/theme.ts
CHANGED
|
@@ -5,7 +5,7 @@ const config = docsConfig as DoryConfig;
|
|
|
5
5
|
|
|
6
6
|
export function loadTheme() {
|
|
7
7
|
const root = document.documentElement;
|
|
8
|
-
const { theme
|
|
8
|
+
const { theme } = config;
|
|
9
9
|
|
|
10
10
|
// Apply forced dark/light mode
|
|
11
11
|
if (theme?.mode === 'dark') {
|
|
@@ -20,10 +20,4 @@ export function loadTheme() {
|
|
|
20
20
|
if (theme?.radius) {
|
|
21
21
|
root.style.setProperty('--radius', theme.radius);
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
// Mark as themed if custom background colors are provided
|
|
25
|
-
const hasCustomColors = colors?.background || colors?.surface || colors?.foreground;
|
|
26
|
-
if (hasCustomColors) {
|
|
27
|
-
root.setAttribute('data-dory-themed', 'true');
|
|
28
|
-
}
|
|
29
23
|
}
|
package/vite.config.ssr.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { defineConfig } from 'vite';
|
|
2
2
|
import preact from '@preact/preset-vite';
|
|
3
3
|
import mdx from '@mdx-js/rollup';
|
|
4
|
-
import { unifiedMdxPreprocessor } from './src/plugins/unified-mdx-plugin';
|
|
5
|
-
import { getMdxConfig } from './src/config/mdx';
|
|
6
|
-
import { getBasePath, readDoryConfig } from './src/plugins/shared';
|
|
4
|
+
import { unifiedMdxPreprocessor } from './src/plugins/unified-mdx-plugin.ts';
|
|
5
|
+
import { getMdxConfig } from './src/config/mdx.ts';
|
|
6
|
+
import { getBasePath, readDoryConfig } from './src/plugins/shared.ts';
|
|
7
7
|
|
|
8
8
|
// Minimal Vite config for the SSR build.
|
|
9
9
|
// Only includes plugins needed for MDX compilation — no sitemap, prerender,
|
package/vite.config.ts
CHANGED
|
@@ -2,20 +2,21 @@ import { defineConfig } from 'vite';
|
|
|
2
2
|
import preact from '@preact/preset-vite';
|
|
3
3
|
import mdx from '@mdx-js/rollup';
|
|
4
4
|
import tailwindcss from '@tailwindcss/vite'
|
|
5
|
-
import { unifiedMdxPreprocessor } from './src/plugins/unified-mdx-plugin';
|
|
6
|
-
import { llmTxtGenerator } from './src/plugins/llm-txt-generator';
|
|
7
|
-
import { llmTxtDevServer } from './src/plugins/llm-txt-dev-server';
|
|
8
|
-
import { frontmatterGenerator } from './src/plugins/frontmatter-generator';
|
|
9
|
-
import { frontmatterDevServer } from './src/plugins/frontmatter-dev-server';
|
|
10
|
-
import { docsAssetsPlugin } from './src/plugins/docs-assets';
|
|
11
|
-
import { htmlMetadataInjector } from './src/plugins/html-metadata-injector';
|
|
12
|
-
import { htmlFilesMiddleware } from './src/plugins/html-files-middleware';
|
|
13
|
-
import { analyticsInjector } from './src/plugins/analytics-injector';
|
|
14
|
-
import { sitemapGenerator } from './src/plugins/sitemap-generator';
|
|
15
|
-
import { robotsGenerator } from './src/plugins/robots-generator';
|
|
16
|
-
import { prerender } from './src/plugins/prerender';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
5
|
+
import { unifiedMdxPreprocessor } from './src/plugins/unified-mdx-plugin.ts';
|
|
6
|
+
import { llmTxtGenerator } from './src/plugins/llm-txt-generator.ts';
|
|
7
|
+
import { llmTxtDevServer } from './src/plugins/llm-txt-dev-server.ts';
|
|
8
|
+
import { frontmatterGenerator } from './src/plugins/frontmatter-generator.ts';
|
|
9
|
+
import { frontmatterDevServer } from './src/plugins/frontmatter-dev-server.ts';
|
|
10
|
+
import { docsAssetsPlugin } from './src/plugins/docs-assets.ts';
|
|
11
|
+
import { htmlMetadataInjector } from './src/plugins/html-metadata-injector.ts';
|
|
12
|
+
import { htmlFilesMiddleware } from './src/plugins/html-files-middleware.ts';
|
|
13
|
+
import { analyticsInjector } from './src/plugins/analytics-injector.ts';
|
|
14
|
+
import { sitemapGenerator } from './src/plugins/sitemap-generator.ts';
|
|
15
|
+
import { robotsGenerator } from './src/plugins/robots-generator.ts';
|
|
16
|
+
import { prerender } from './src/plugins/prerender.ts';
|
|
17
|
+
import { chatDevServer } from './src/plugins/chat-dev-server.ts';
|
|
18
|
+
import { getMdxConfig } from './src/config/mdx.ts';
|
|
19
|
+
import { getBasePath, readDoryConfig } from './src/plugins/shared.ts';
|
|
19
20
|
|
|
20
21
|
export default defineConfig(({ command }) => ({
|
|
21
22
|
// Serve and build under `basePath` from dory.json (asset URLs, import.meta.env.BASE_URL).
|
|
@@ -23,6 +24,7 @@ export default defineConfig(({ command }) => ({
|
|
|
23
24
|
plugins: [
|
|
24
25
|
// SINGLE SOURCE OF TRUTH: All MDX preprocessing happens in src/mdx/processor.ts
|
|
25
26
|
htmlFilesMiddleware(), // Must be first to intercept HTML file requests
|
|
27
|
+
chatDevServer(),
|
|
26
28
|
unifiedMdxPreprocessor(),
|
|
27
29
|
preact(),
|
|
28
30
|
mdx(getMdxConfig(command === 'serve')),
|
|
@@ -45,6 +47,11 @@ export default defineConfig(({ command }) => ({
|
|
|
45
47
|
'react-dom': 'preact/compat',
|
|
46
48
|
}
|
|
47
49
|
},
|
|
50
|
+
// Mermaid is loaded on demand; prebundle it at startup so opening a diagram
|
|
51
|
+
// does not trigger a late dependency-cache rebuild and stale module URL.
|
|
52
|
+
optimizeDeps: {
|
|
53
|
+
include: ['mermaid'],
|
|
54
|
+
},
|
|
48
55
|
publicDir: false, // Don't use a public directory since we need to import from docs
|
|
49
56
|
build: {
|
|
50
57
|
rollupOptions: {
|
|
@@ -54,7 +61,6 @@ export default defineConfig(({ command }) => ({
|
|
|
54
61
|
if (!id.includes('node_modules')) return;
|
|
55
62
|
if (/node_modules\/preact($|\/)/.test(id)) return 'vendor-preact';
|
|
56
63
|
if (id.includes('node_modules/@clidey/ux') || id.includes('node_modules/@headlessui/react')) return 'vendor-ui';
|
|
57
|
-
if (id.includes('node_modules/lucide-react')) return 'vendor-icons';
|
|
58
64
|
if (id.includes('node_modules/katex')) return 'katex';
|
|
59
65
|
if (id.includes('node_modules/flexsearch')) return 'flexsearch';
|
|
60
66
|
}
|