@solutiondesk/help 1.0.0-69d9f69 → 1.0.0-6f713c7
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 +60 -11
- package/dist/fumadocs.d.ts +187 -21
- package/dist/fumadocs.js +374 -57
- package/dist/index.d.cts +56 -4
- package/dist/index.d.ts +56 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,15 +29,15 @@ const { page } = await help.page("guides/login"); // one page with its text
|
|
|
29
29
|
|
|
30
30
|
| Method | API | Returns |
|
|
31
31
|
| --- | --- | --- |
|
|
32
|
-
| `site({ lang })` | `GET …/site` | The site (`name`, `languages`, `websiteUrl`) and its navigation tree: pages, folders (with index page, `root` tab, `defaultOpen`, `collapsible`), separators and links. Empty folders are left out. |
|
|
32
|
+
| `site({ lang })` | `GET …/site` | The site (`name`, `languages`, `websiteUrl`, its own building `blocks`) and its navigation tree: pages, folders (with index page, `listIndex`, `root` tab, `description`, `group`, `defaultOpen`, `collapsible`), separators and links. Empty folders are left out. |
|
|
33
33
|
| `pages({ lang })` | `GET …/pages` | Every visible page without its text, in reading order (for builds and sitemaps). |
|
|
34
|
-
| `page(path, { lang, redirect })` | `GET …/page?path=` | One page: `markdown`, `html`, `headings`, `blocks`, `breadcrumbs`, `previous`/`next`. The home page has the path `""`. |
|
|
34
|
+
| `page(path, { lang, redirect })` | `GET …/page?path=` | One page: `markdown`, `html`, `headings`, `blocks`, `customBlocks`, `breadcrumbs`, `previous`/`next`. The home page has the path `""`. |
|
|
35
35
|
| `pageMarkdown(path, { lang })` | `GET …/page.md?path=` | One page as plain Markdown. |
|
|
36
36
|
| `search(query, { lang, limit })` | `GET …/search?q=` | Up to 20 hits: title hits before heading hits before text hits, with an `excerpt` and the best `heading`. |
|
|
37
37
|
| `preview(token, { lang })` | `GET …/preview?token=` | A page in any state (drafts too) for your preview route. |
|
|
38
38
|
| `llmsTxt({ lang })` / `llmsFullTxt({ lang })` | `GET …/llms.txt`, `…/llms-full.txt` | The site for language models: the index, or every page as Markdown. |
|
|
39
39
|
|
|
40
|
-
`html` is safe to insert as it is: raw HTML in the Markdown is shown as text, links only go to web addresses, `mailto:`, paths and anchors, images only come from your help center. Headings carry the `id`s listed in `headings
|
|
40
|
+
`html` is safe to insert as it is: raw HTML in the Markdown is shown as text, links only go to web addresses, `mailto:`, paths and anchors, images only come from your help center. Headings carry the `id`s listed in `headings`, made the way GitHub and Fumadocs make them (umlauts kept, `-1` for a repeat), so a site moved from Fumadocs keeps its `#links`; a step's title is a heading too.
|
|
41
41
|
|
|
42
42
|
### Moved pages
|
|
43
43
|
|
|
@@ -103,15 +103,16 @@ The navigation becomes the files Fumadocs knows from a content folder:
|
|
|
103
103
|
| SolutionDesk | Fumadocs |
|
|
104
104
|
| --- | --- |
|
|
105
105
|
| page `guides/login` | `guides/login.mdx` (`slugs: ["guides", "login"]`) |
|
|
106
|
-
| home page | `index.mdx`
|
|
107
|
-
| folder `guides` | `guides/meta.json` with `title`, `icon`, `root`, `defaultOpen`, `collapsible`, `pages` |
|
|
106
|
+
| home page | `index.mdx` at the top, or `(core)/index.mdx` when it is a top-level group's own page |
|
|
107
|
+
| folder `guides` | `guides/meta.json` with `title`, `description` (shown under a tab's name), `pagesIndex` — or, with `listIndex`, `"index"` first in `pages` (after a leading separator), so the page is an entry and the name only opens the folder —, `icon`, `root`, `defaultOpen`, `collapsible`, `pages` |
|
|
108
|
+
| group folder `core` (adds no address part) | `(core)/` — a Fumadocs folder group, so `(core)/sign-up/index.mdx` is still `sign-up` |
|
|
108
109
|
| folder's own page | `guides/index.mdx` (`pagesIndex: "index"`, so it also works for `root` tabs) |
|
|
109
110
|
| any other page with the slug `index` | `guides/index_.mdx` (`slugs: ["guides", "index"]`), so it never collides with the folder's own page |
|
|
110
111
|
| order of the top level | `meta.json` (`title` = site name) |
|
|
111
112
|
| separator | `"---[Icon]Title---"` (a separator without a title is `"---"`) |
|
|
112
113
|
| link | `"[Icon][Title](url)"`, `"external:[Icon][Title](url)"` |
|
|
113
114
|
|
|
114
|
-
Every page's `data` holds `title`, `description`, `icon`, `path`, `lang`, `load()` (the page with its text, or `null` when the page was taken offline after the source was built — answer with a 404) and `structuredData()` (for Fumadocs search; empty for such a page, so it never breaks the search index).
|
|
115
|
+
Every page's `data` holds `title`, `description`, `icon`, `path`, `lang`, `blocks` (the site's own building blocks, for `HelpBody`), `load()` (the page with its text, or `null` when the page was taken offline after the source was built — answer with a 404) and `structuredData()` (for Fumadocs search; empty for such a page, so it never breaks the search index).
|
|
115
116
|
|
|
116
117
|
**Lazy or eager.** The page list has no texts, so by default (`pages: "lazy"`) building the source costs one request per language, and `page.data.load()` fetches a page's text when it is rendered. Cache that with your framework (the tags above). With `pages: "eager"` every page is fetched while building (`concurrency`, default 6) and kept in `page.data.page`: slower to build, nothing fetched later, best for a fully static export.
|
|
117
118
|
|
|
@@ -152,7 +153,7 @@ export default async function Page({ params }: { params: Promise<{ slug?: string
|
|
|
152
153
|
<DocsTitle>{page.title}</DocsTitle>
|
|
153
154
|
<DocsDescription>{page.description}</DocsDescription>
|
|
154
155
|
<DocsBody>
|
|
155
|
-
<HelpBody
|
|
156
|
+
<HelpBody page={page} blocks={entry.data.blocks} icon={icon} />
|
|
156
157
|
</DocsBody>
|
|
157
158
|
</DocsPage>
|
|
158
159
|
);
|
|
@@ -163,20 +164,67 @@ export async function generateStaticParams() {
|
|
|
163
164
|
}
|
|
164
165
|
```
|
|
165
166
|
|
|
166
|
-
`HelpBody` renders the Markdown with Fumadocs UI: `:::callout` → `Callout`, `::::steps`/`:::step` → `Steps`/`Step`, `::::tabs`/`:::tab` → `Tabs`/`Tab`, `::::cards`/`:::card` → `Cards`/`Card`, a code fence with `title="…"` → `CodeBlock`. Pass `headings` so the anchors match `getHelpToc`. Replace any part with `components`, for example syntax highlighting:
|
|
167
|
+
`page` gives `HelpBody` the page's `markdown`, `headings` and `locale`; you can also pass `markdown`, `headings` and `lang` yourself. `HelpBody` renders the Markdown with Fumadocs UI: `:::callout` → `Callout`, `::::steps`/`:::step` → `Steps`/`Step`, `::::tabs`/`:::tab` → `Tabs`/`Tab`, `::::cards`/`:::card` → `Cards`/`Card`, a code fence with `title="…"` → `CodeBlock`. Pass `headings` (or `page`) so the anchors match `getHelpToc`. Replace any part with `components`, for example syntax highlighting:
|
|
167
168
|
|
|
168
169
|
```tsx
|
|
169
170
|
import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";
|
|
170
171
|
|
|
171
172
|
<HelpBody
|
|
172
|
-
|
|
173
|
-
headings={page.headings}
|
|
173
|
+
page={page}
|
|
174
174
|
components={{ Code: ({ code, lang, title }) => <DynamicCodeBlock lang={lang ?? "text"} code={code} codeblock={{ title }} /> }}
|
|
175
175
|
/>;
|
|
176
176
|
```
|
|
177
177
|
|
|
178
178
|
The same safety rules as the API's `html` apply: raw HTML is shown as text, links only go to `http(s)`, `mailto:`, `/paths` and `#anchors`, images only come from `http(s)` or `/paths` — never to another host through `//`, `/\`, `\\` or control characters hidden in the address. A directive SolutionDesk doesn't know stays the text it was written as.
|
|
179
179
|
|
|
180
|
+
### Your own building blocks
|
|
181
|
+
|
|
182
|
+
In SolutionDesk, a doc site can define its own building blocks: what a block is called, whether it sits in a sentence, on its own line or around content, and which fields it has. How it looks is up to your website. Map each block's name to one of your components:
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
import { type HelpBlockProps, HelpBody } from "@solutiondesk/help/fumadocs";
|
|
186
|
+
import { icons } from "lucide-react";
|
|
187
|
+
|
|
188
|
+
// :app-button{icon="Palette" label="Your team"} — in a sentence
|
|
189
|
+
function AppButton({ icon, label }: HelpBlockProps<{ icon: string; label: string }>) {
|
|
190
|
+
const Icon = icon ? icons[icon as keyof typeof icons] : undefined;
|
|
191
|
+
return <kbd className="app-button">{Icon ? <Icon /> : null}{label}</kbd>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ::paid-addon{name="Events"} — on its own line
|
|
195
|
+
function PaidAddon({ name, lang }: HelpBlockProps<{ name: string }>) {
|
|
196
|
+
return <aside className="paid-addon">{lang === "de" ? "Kostenpflichtig" : "Paid add-on"}: {name}</aside>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// :::download-buttons{title="UXP installer" versionsHref="#versions"} … ::: — around content
|
|
200
|
+
function DownloadButtons({ title, versionsHref, children }: HelpBlockProps<{ title: string; versionsHref: string }>) {
|
|
201
|
+
return (
|
|
202
|
+
<section className="downloads">
|
|
203
|
+
<h4>{title}</h4>
|
|
204
|
+
{children}
|
|
205
|
+
{versionsHref ? <a href={versionsHref}>Older versions</a> : null}
|
|
206
|
+
</section>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
<HelpBody
|
|
211
|
+
page={page}
|
|
212
|
+
blocks={entry.data.blocks} // or `site.blocks` from `help.site()`
|
|
213
|
+
components={{ "app-button": AppButton, "paid-addon": PaidAddon, "download-buttons": DownloadButtons }}
|
|
214
|
+
/>;
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
A component gets:
|
|
218
|
+
|
|
219
|
+
- the block's **fields**, already checked the way SolutionDesk checks them: text fields as strings, on/off fields as `true`/`false`. A value that isn't valid (an unsafe address such as `javascript:`, a picture or video address that isn't allowed, an icon name that isn't one, an option that isn't offered) is left out, so every field may be missing — `HelpBlockProps<Fields>` types them as optional.
|
|
220
|
+
- `lang`: the page's language (`page.locale`, or the `lang` you pass) — no need to store it in the block.
|
|
221
|
+
- `block`: the block's name.
|
|
222
|
+
- `children`: for a block in a sentence or on its own line, its text in `[brackets]` (`:app-button[Share]{icon="Share2"}`); for a block around content, that content, rendered like the rest of the page (built-in and own blocks inside included).
|
|
223
|
+
|
|
224
|
+
`block`, `lang` and `children` always mean the above; SolutionDesk doesn't allow fields with these names. A block the site defines but you have no component for shows its **text version** (set in SolutionDesk, e.g. `Paid add-on: Events`) in a neutral `<span class="sd-block" data-block="…" data-props="…">` (in a sentence) or `<div class="sd-block">` (on its own line; around content with `<p class="sd-block-text">` and `<div class="sd-block-body">`) — the same as the API's `html`. A name the site doesn't define, or a block written in the wrong form, stays the text it was written as. Without `blocks`, every one of them stays text. Search data (`structuredData()`) and heading anchors use the text version as well, like the API's search.
|
|
225
|
+
|
|
226
|
+
`helpBlockValues(definition, attributes)` and `helpBlockText(definition, values, label?)` expose the same rules if you render blocks yourself.
|
|
227
|
+
|
|
180
228
|
### Refresh when something changes
|
|
181
229
|
|
|
182
230
|
In SolutionDesk set the site's webhook to a route on your website and copy the signing secret. The handler checks the `X-SolutionDesk-Signature` header and answers `401` when it doesn't match (or is older than 5 minutes) and `400` for a signed body that isn't a help webhook:
|
|
@@ -238,10 +286,11 @@ document.querySelector("article")!.innerHTML = page.html;
|
|
|
238
286
|
| Block | HTML |
|
|
239
287
|
| --- | --- |
|
|
240
288
|
| callout | `<div class="sd-callout" data-type="info\|warn\|error\|success\|idea" role="note">` with `<p class="sd-callout-title">` and `<div class="sd-callout-body">` |
|
|
241
|
-
| steps | `<ol class="sd-steps">` of `<li class="sd-step">` with `<
|
|
289
|
+
| steps | `<ol class="sd-steps">` of `<li class="sd-step">` with `<h3 class="sd-step-title" id="…">` and `<div class="sd-step-body">` |
|
|
242
290
|
| tabs | `<div class="sd-tabs">` of `<section class="sd-tab" data-title="…">` (build the tab buttons from `data-title`; `page.blocks` says when a page has tabs) |
|
|
243
291
|
| cards | `<div class="sd-cards">` of `<a class="sd-card" href="…" data-icon="Rocket">` (or `<div class="sd-card">`) with `<p class="sd-card-title">` and `<div class="sd-card-body">` |
|
|
244
292
|
| titled code | `<figure class="sd-code"><figcaption>client.ts</figcaption><pre><code class="language-ts">` |
|
|
293
|
+
| the site's own block | `<span class="sd-block" data-block="app-button" data-props="{…}">` in a sentence, `<div class="sd-block">` on its own line or around content (`<p class="sd-block-text">`, `<div class="sd-block-body">`); the text is the block's text version, `data-props` its checked values as JSON — hydrate your own component from it |
|
|
245
294
|
|
|
246
295
|
Build the table of contents from `page.headings` (`#id`), the breadcrumb from `page.breadcrumbs` and the footer links from `page.previous`/`page.next`.
|
|
247
296
|
|
package/dist/fumadocs.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { JSXElementConstructor, ReactNode, ComponentType } from 'react';
|
|
2
2
|
import { MetaData, PageData, StaticSource, VirtualFile, DynamicSource } from 'fumadocs-core/source';
|
|
3
3
|
import { TOCItemType } from 'fumadocs-core/toc';
|
|
4
4
|
|
|
@@ -12,7 +12,8 @@ import { TOCItemType } from 'fumadocs-core/toc';
|
|
|
12
12
|
* preview token names. Absent values are `null`, never a missing key.
|
|
13
13
|
*
|
|
14
14
|
* Paths: a page's path is its folder slugs plus its slug, without leading or
|
|
15
|
-
* trailing slash (`guides/login`)
|
|
15
|
+
* trailing slash (`guides/login`) — a `group` folder adds no slug; the site's
|
|
16
|
+
* home page has the path `""`.
|
|
16
17
|
*/
|
|
17
18
|
/** The doc site itself. */
|
|
18
19
|
interface HelpPublicSite {
|
|
@@ -25,6 +26,40 @@ interface HelpPublicSite {
|
|
|
25
26
|
languages: string[];
|
|
26
27
|
/** The docs website's base address (`https://help.acme.com/docs`), if set. */
|
|
27
28
|
websiteUrl: string | null;
|
|
29
|
+
/** The site's own building blocks pages may use (empty when it defines none). */
|
|
30
|
+
blocks: HelpPublicBlockDefinition[];
|
|
31
|
+
}
|
|
32
|
+
/** What a field of a site's own block holds (see {@link HelpPublicBlockField}). */
|
|
33
|
+
type HelpPublicBlockFieldType = "text" | "longText" | "icon" | "url" | "choice" | "image" | "video" | "boolean";
|
|
34
|
+
/**
|
|
35
|
+
* One field of a site's own block: an attribute of its directive
|
|
36
|
+
* (`key="value"`). `boolean` fields are on when written bare (`key`) or as
|
|
37
|
+
* `key="true"`; every other value is a string. `options` lists a `choice`
|
|
38
|
+
* field's values (null for other types).
|
|
39
|
+
*/
|
|
40
|
+
interface HelpPublicBlockField {
|
|
41
|
+
key: string;
|
|
42
|
+
label: string;
|
|
43
|
+
type: HelpPublicBlockFieldType;
|
|
44
|
+
required: boolean;
|
|
45
|
+
options: string[] | null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A building block the site defines itself — WHAT it is; how it looks is the
|
|
49
|
+
* website's own component. Written in Markdown as `:name[label]{…}` in running
|
|
50
|
+
* text (`inline`), `::name[label]{…}` alone on a line (`block`) or
|
|
51
|
+
* `:::name{…}` … `:::` around Markdown content (`container`). `text` is the
|
|
52
|
+
* template of its text version (`{key}` = a field's value, a `boolean` field's
|
|
53
|
+
* label when on; `{{`/`}}` = a brace) — what to show without a component.
|
|
54
|
+
*/
|
|
55
|
+
interface HelpPublicBlockDefinition {
|
|
56
|
+
/** The directive name (`app-button`). */
|
|
57
|
+
name: string;
|
|
58
|
+
/** Display name. */
|
|
59
|
+
label: string;
|
|
60
|
+
kind: "inline" | "block" | "container";
|
|
61
|
+
fields: HelpPublicBlockField[];
|
|
62
|
+
text: string;
|
|
28
63
|
}
|
|
29
64
|
/** A page in the navigation. */
|
|
30
65
|
interface HelpPublicTreePage {
|
|
@@ -43,16 +78,28 @@ interface HelpPublicTreeFolder {
|
|
|
43
78
|
/** Stable node id. */
|
|
44
79
|
id: string;
|
|
45
80
|
slug: string;
|
|
46
|
-
/** The folder's path (its index page, if any, lives here). */
|
|
81
|
+
/** The folder's path (its index page, if any, lives here; a group's is its parent's). */
|
|
47
82
|
path: string;
|
|
48
83
|
title: string;
|
|
84
|
+
/** A short line under a tab's name in the tab menu (Fumadocs `description`); null without one. */
|
|
85
|
+
description: string | null;
|
|
49
86
|
icon: string | null;
|
|
50
87
|
/** A tab (top level only; Fumadocs `root`). */
|
|
51
88
|
root: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Adds no segment to its pages' paths (Fumadocs `(name)` folder): `path` is
|
|
91
|
+
* the parent's, a top-level group's index page is the home (`""`).
|
|
92
|
+
*/
|
|
93
|
+
group: boolean;
|
|
52
94
|
defaultOpen: boolean;
|
|
53
95
|
collapsible: boolean;
|
|
54
96
|
/** The folder's own page, shown at the folder's path; null without one. */
|
|
55
97
|
index: HelpPublicTreePage | null;
|
|
98
|
+
/**
|
|
99
|
+
* List `index` as the folder's first entry (after a section title it starts
|
|
100
|
+
* with); the folder's name then only opens it (Fumadocs: `index` in `pages`).
|
|
101
|
+
*/
|
|
102
|
+
listIndex: boolean;
|
|
56
103
|
children: HelpPublicTreeNode[];
|
|
57
104
|
}
|
|
58
105
|
/** A labelled divider between entries (Fumadocs `---Title---`). */
|
|
@@ -129,12 +176,17 @@ interface HelpPublicPageLink {
|
|
|
129
176
|
}
|
|
130
177
|
/** One page with its text. */
|
|
131
178
|
interface HelpPublicPage extends HelpPublicPageSummary {
|
|
132
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* The page's Markdown with directives (`:::callout`, `::::steps`, …, and the
|
|
181
|
+
* site's own blocks as written — see `site.blocks`).
|
|
182
|
+
*/
|
|
133
183
|
markdown: string;
|
|
134
184
|
/** Safe server-rendered HTML of `markdown` (stable `sd-*` classes for the directives). */
|
|
135
185
|
html: string;
|
|
136
186
|
headings: HelpPublicHeading[];
|
|
137
187
|
blocks: HelpPublicBlock[];
|
|
188
|
+
/** The site's own building blocks the page uses, by name (in the site's order). */
|
|
189
|
+
customBlocks: string[];
|
|
138
190
|
breadcrumbs: HelpPublicBreadcrumb[];
|
|
139
191
|
previous: HelpPublicPageLink | null;
|
|
140
192
|
next: HelpPublicPageLink | null;
|
|
@@ -209,8 +261,8 @@ interface HelpWebhookBody {
|
|
|
209
261
|
* Parsing the help center's Markdown the way the API renders it
|
|
210
262
|
* (`packages/db/src/help-markdown.ts`): GFM plus container directives in the
|
|
211
263
|
* generic syntax of `remark-directive`. Only the directive names below mean
|
|
212
|
-
* something
|
|
213
|
-
*
|
|
264
|
+
* something, plus the site's own blocks (`blocks.ts`); every other directive
|
|
265
|
+
* is printed back as the text it was written as.
|
|
214
266
|
*/
|
|
215
267
|
/** The directive names the help center knows. */
|
|
216
268
|
declare const HELP_BLOCKS: readonly ["callout", "steps", "step", "tabs", "tab", "cards", "card"];
|
|
@@ -230,6 +282,38 @@ type HelpCalloutType = (typeof HELP_CALLOUT_TYPES)[number];
|
|
|
230
282
|
*/
|
|
231
283
|
declare function safeHelpHref(raw: string | null | undefined): string | null;
|
|
232
284
|
|
|
285
|
+
/**
|
|
286
|
+
* A doc site's own building blocks (`site.blocks`) in the page Markdown — the
|
|
287
|
+
* same rules as the API (`packages/db/src/help-blocks.ts`, `help-markdown.ts`,
|
|
288
|
+
* contract in `docs/help/06-eigene-bausteine.md`), copied so the package has no
|
|
289
|
+
* dependency on the API's code; `test/blocks.test.tsx` holds both in step.
|
|
290
|
+
*
|
|
291
|
+
* The Markdown parser (micromark's directive extension) finds the directives;
|
|
292
|
+
* a use only counts when its name is defined, it is written in its kind's form
|
|
293
|
+
* (`:name` inline, `::name` alone on a line, `:::name` around content) and its
|
|
294
|
+
* source re-reads under the API's stricter syntax (label and attributes on one
|
|
295
|
+
* line, the basic character references) — otherwise it stays literal text.
|
|
296
|
+
*/
|
|
297
|
+
/** A field's checked value: `boolean` fields are true/false, every other type a string. */
|
|
298
|
+
type HelpBlockValue = string | boolean;
|
|
299
|
+
/** A block's checked field values, in the definition's field order. */
|
|
300
|
+
type HelpBlockValues = Record<string, HelpBlockValue>;
|
|
301
|
+
/**
|
|
302
|
+
* A block's attributes as checked field values, by the API's rules: only
|
|
303
|
+
* defined fields; an absent `boolean` is `false`; an empty value counts as
|
|
304
|
+
* absent; invalid values (a `javascript:` link, a malformed icon, a choice
|
|
305
|
+
* not offered) and attributes without a field are left out.
|
|
306
|
+
*/
|
|
307
|
+
declare function helpBlockValues(block: Pick<HelpPublicBlockDefinition, "fields">, attributes: Readonly<Record<string, string>>): HelpBlockValues;
|
|
308
|
+
/**
|
|
309
|
+
* A block's text version: its template with every `{key}` replaced by the
|
|
310
|
+
* field's value (a `boolean` by its label when on, else nothing; a missing
|
|
311
|
+
* value by nothing), `{{`/`}}` as braces, then `label` after a space;
|
|
312
|
+
* whitespace collapsed. What `HelpBody` shows without a component, and what
|
|
313
|
+
* search and the outline read.
|
|
314
|
+
*/
|
|
315
|
+
declare function helpBlockText(block: Pick<HelpPublicBlockDefinition, "fields" | "text">, values: Readonly<HelpBlockValues>, label?: string): string;
|
|
316
|
+
|
|
233
317
|
interface HelpCalloutProps {
|
|
234
318
|
/** `info`, `warn`, `error`, `success` or `idea` (an unknown type arrives as `info`). */
|
|
235
319
|
type: HelpCalloutType;
|
|
@@ -241,6 +325,8 @@ interface HelpStepsProps {
|
|
|
241
325
|
}
|
|
242
326
|
interface HelpStepProps {
|
|
243
327
|
title?: string;
|
|
328
|
+
/** The title's anchor id (a step's title is a heading of the page, level 3). */
|
|
329
|
+
id?: string;
|
|
244
330
|
children: ReactNode;
|
|
245
331
|
}
|
|
246
332
|
interface HelpTabsProps {
|
|
@@ -288,6 +374,40 @@ interface HelpImageProps {
|
|
|
288
374
|
alt: string;
|
|
289
375
|
title?: string;
|
|
290
376
|
}
|
|
377
|
+
/** A field value of a site's own block, as its component receives it. */
|
|
378
|
+
type HelpBlockFieldValue = string | boolean | undefined;
|
|
379
|
+
/**
|
|
380
|
+
* The props of a component for one of the site's own blocks
|
|
381
|
+
* (`docs/help/06`). `Fields` names the block's fields: a `boolean` field is
|
|
382
|
+
* always there (`true`/`false`); any other field is a string, or absent when
|
|
383
|
+
* the page leaves it out or its value isn't valid (a `javascript:` link, an
|
|
384
|
+
* icon name that isn't one, a choice not offered) — even a required one, so
|
|
385
|
+
* every field is optional here.
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* type AppButtonProps = HelpBlockProps<{ icon: string; label: string }>;
|
|
389
|
+
* function AppButton({ icon, label, children, lang }: AppButtonProps) { … }
|
|
390
|
+
*/
|
|
391
|
+
type HelpBlockProps<Fields extends {
|
|
392
|
+
[K in keyof Fields]: HelpBlockFieldValue;
|
|
393
|
+
} = Record<never, never>> = {
|
|
394
|
+
[K in Exclude<keyof Fields, "block" | "lang" | "children">]?: Fields[K];
|
|
395
|
+
} & {
|
|
396
|
+
/** The block's name (`app-button`). */
|
|
397
|
+
block: string;
|
|
398
|
+
/** The page's language (`page.locale`, or `HelpBody`'s `lang`). */
|
|
399
|
+
lang: string;
|
|
400
|
+
/**
|
|
401
|
+
* `inline`/`block`: the `[label]` (rendered Markdown), if written;
|
|
402
|
+
* `container`: the rendered content — built-in and own blocks included.
|
|
403
|
+
*/
|
|
404
|
+
children?: ReactNode;
|
|
405
|
+
};
|
|
406
|
+
/**
|
|
407
|
+
* A component for a site's own block. Typed loosely so any component fits;
|
|
408
|
+
* type yours with {@link HelpBlockProps}.
|
|
409
|
+
*/
|
|
410
|
+
type HelpBlockComponent = JSXElementConstructor<never>;
|
|
291
411
|
/** Everything `HelpBody` renders through a component — replace any of them. */
|
|
292
412
|
interface HelpBodyComponents {
|
|
293
413
|
Callout: ComponentType<HelpCalloutProps>;
|
|
@@ -305,17 +425,47 @@ interface HelpBodyComponents {
|
|
|
305
425
|
}
|
|
306
426
|
/** The Fumadocs UI defaults. */
|
|
307
427
|
declare const helpBodyComponents: HelpBodyComponents;
|
|
428
|
+
/**
|
|
429
|
+
* `HelpBody`'s components: any of the built-in ones by their (capitalised)
|
|
430
|
+
* name, and a component per site block by the block's name (`app-button`).
|
|
431
|
+
*/
|
|
432
|
+
interface HelpBodyComponentMap extends Partial<HelpBodyComponents> {
|
|
433
|
+
[block: string]: HelpBlockComponent | undefined;
|
|
434
|
+
}
|
|
308
435
|
interface HelpBodyProps {
|
|
309
|
-
/**
|
|
310
|
-
|
|
436
|
+
/**
|
|
437
|
+
* The page from the API (`page.data.load()`, `help.page()`, `help.preview()`):
|
|
438
|
+
* its `markdown`, `headings` and `locale` (passed to block components as
|
|
439
|
+
* `lang`). Or pass `markdown` (and `headings`, `lang`) yourself.
|
|
440
|
+
*/
|
|
441
|
+
page?: {
|
|
442
|
+
markdown: string;
|
|
443
|
+
headings?: readonly HelpPublicHeading[];
|
|
444
|
+
locale?: string;
|
|
445
|
+
};
|
|
446
|
+
/** The page's Markdown with directives (`page.markdown` from the API); wins over `page`. */
|
|
447
|
+
markdown?: string;
|
|
311
448
|
/**
|
|
312
449
|
* The page's `headings` from the API: their ids become the headings' anchors,
|
|
313
450
|
* so they always match `getHelpToc(page.headings)`. Without them the ids are
|
|
314
451
|
* computed the API's way.
|
|
315
452
|
*/
|
|
316
453
|
headings?: readonly HelpPublicHeading[];
|
|
317
|
-
/**
|
|
318
|
-
|
|
454
|
+
/** The page's language for block components (default `page.locale`, else `""`). */
|
|
455
|
+
lang?: string;
|
|
456
|
+
/**
|
|
457
|
+
* The site's own blocks (`page.data.blocks` of a help source, or
|
|
458
|
+
* `site.blocks` from `help.site()`). Without them every such directive stays
|
|
459
|
+
* the text it was written as.
|
|
460
|
+
*/
|
|
461
|
+
blocks?: readonly HelpPublicBlockDefinition[];
|
|
462
|
+
/**
|
|
463
|
+
* Replace any of the default Fumadocs UI components (`Callout`, `Code`, …),
|
|
464
|
+
* and render the site's own blocks with your components, keyed by the
|
|
465
|
+
* block's name: `{ "app-button": AppButton }`. A defined block without a
|
|
466
|
+
* component shows its text version.
|
|
467
|
+
*/
|
|
468
|
+
components?: HelpBodyComponentMap;
|
|
319
469
|
/**
|
|
320
470
|
* Turn a card's Lucide icon name (`Rocket`) into an element, e.g. with
|
|
321
471
|
* `lucide-react`: `(name) => { const Icon = icons[name as keyof typeof icons]; return Icon ? <Icon /> : null; }`.
|
|
@@ -326,15 +476,20 @@ interface HelpBodyProps {
|
|
|
326
476
|
/**
|
|
327
477
|
* A help page's Markdown with its blocks (callout, steps, tabs, cards, titled
|
|
328
478
|
* code fences) as Fumadocs UI components — a React Server Component, also fine
|
|
329
|
-
* on the client. The
|
|
330
|
-
*
|
|
331
|
-
*
|
|
479
|
+
* on the client. The site's own blocks (`blocks`) render with your
|
|
480
|
+
* `components`, or as their text version. The API's safety rules apply: raw
|
|
481
|
+
* HTML is shown as text, links only to http(s), mailto, `/paths` and
|
|
482
|
+
* `#anchors`, images only from http(s) or `/paths`; unknown directives stay
|
|
483
|
+
* the text they were written as.
|
|
332
484
|
*
|
|
333
485
|
* @example
|
|
334
|
-
* const
|
|
335
|
-
*
|
|
486
|
+
* const entry = source.getPage(slugs);
|
|
487
|
+
* const page = await entry.data.load();
|
|
488
|
+
* <DocsBody>
|
|
489
|
+
* <HelpBody page={page} blocks={entry.data.blocks} components={{ "app-button": AppButton }} />
|
|
490
|
+
* </DocsBody>
|
|
336
491
|
*/
|
|
337
|
-
declare function HelpBody(
|
|
492
|
+
declare function HelpBody(props: HelpBodyProps): ReactNode;
|
|
338
493
|
|
|
339
494
|
/** Per-call options. */
|
|
340
495
|
interface RequestOptions {
|
|
@@ -477,9 +632,11 @@ interface HelpStructuredData {
|
|
|
477
632
|
/**
|
|
478
633
|
* A page's search data for Fumadocs (`createFromSource`): every heading with
|
|
479
634
|
* its anchor, and every paragraph, quote and table cell under the heading it
|
|
480
|
-
* belongs to — code is left out, as in Fumadocs' own `remark-structure`.
|
|
635
|
+
* belongs to — code is left out, as in Fumadocs' own `remark-structure`. The
|
|
636
|
+
* site's own `blocks` count as their text version (like the API's search): a
|
|
637
|
+
* container's text version is an entry of its own, followed by its content.
|
|
481
638
|
*/
|
|
482
|
-
declare function helpStructuredData(markdown: string, headings?: readonly HelpPublicHeading[]): HelpStructuredData;
|
|
639
|
+
declare function helpStructuredData(markdown: string, headings?: readonly HelpPublicHeading[], blocks?: readonly HelpPublicBlockDefinition[]): HelpStructuredData;
|
|
483
640
|
|
|
484
641
|
/** What every page of a help source carries in `page.data`. */
|
|
485
642
|
interface HelpPageData extends PageData {
|
|
@@ -491,6 +648,11 @@ interface HelpPageData extends PageData {
|
|
|
491
648
|
path: string;
|
|
492
649
|
/** The language this file was built for (the `lang` asked for; the API falls back per page). */
|
|
493
650
|
lang: string;
|
|
651
|
+
/**
|
|
652
|
+
* The site's own building blocks (`site.blocks`) — pass them to `HelpBody`
|
|
653
|
+
* (`blocks={page.data.blocks}`) so pages render them with your components.
|
|
654
|
+
*/
|
|
655
|
+
blocks: HelpPublicBlockDefinition[];
|
|
494
656
|
/**
|
|
495
657
|
* The page with its text (`markdown`, `html`, `headings`, `breadcrumbs`,
|
|
496
658
|
* `previous`/`next`). Fetched when called — or right away with
|
|
@@ -511,6 +673,8 @@ interface HelpPageData extends PageData {
|
|
|
511
673
|
/** What every folder's `meta.json` of a help source carries. */
|
|
512
674
|
interface HelpMetaData extends MetaData {
|
|
513
675
|
title?: string;
|
|
676
|
+
/** Shown under a tab's name in Fumadocs' tab menu. */
|
|
677
|
+
description?: string;
|
|
514
678
|
icon?: string;
|
|
515
679
|
root?: boolean;
|
|
516
680
|
defaultOpen?: boolean;
|
|
@@ -570,8 +734,10 @@ interface HelpFilesOptions {
|
|
|
570
734
|
/**
|
|
571
735
|
* The Fumadocs virtual files for one language of the site: a `.mdx` page file
|
|
572
736
|
* per page (a folder's own page as `<folder>/index.mdx`, the home page as
|
|
573
|
-
* `index.mdx` at the top, any other page named `index`
|
|
574
|
-
* `meta.json` per folder plus one at the top that fixes
|
|
737
|
+
* `index.mdx` at the top or in a top-level group, any other page named `index`
|
|
738
|
+
* as `index_.mdx`) and a `meta.json` per folder plus one at the top that fixes
|
|
739
|
+
* the order. A group folder is a `(slug)/` directory, so Fumadocs gives its
|
|
740
|
+
* pages the same addresses as the tree (no segment for the group). Every file
|
|
575
741
|
* path is unique. Exported for custom setups; `createHelpSource` uses it.
|
|
576
742
|
*/
|
|
577
743
|
declare function helpTreeToFiles(payload: HelpPublicSitePayload, options: HelpFilesOptions): HelpVirtualFile[];
|
|
@@ -611,4 +777,4 @@ declare function getHelpToc(headings: readonly HelpPublicHeading[] | {
|
|
|
611
777
|
headings: readonly HelpPublicHeading[];
|
|
612
778
|
}, options?: HelpTocOptions): TOCItemType[];
|
|
613
779
|
|
|
614
|
-
export { HELP_BLOCKS, HELP_CALLOUT_TYPES, type HelpBlockName, HelpBody, type HelpBodyComponents, type HelpBodyProps, type HelpCalloutProps, type HelpCalloutType, type HelpCardProps, type HelpCardsProps, type HelpCodeProps, type HelpFilesOptions, type HelpHeadingProps, type HelpImageProps, type HelpLinkProps, type HelpMetaData, type HelpPageData, type HelpRevalidateOptions, type HelpSource, type HelpSourceConfig, type HelpSourceOptions, type HelpStepProps, type HelpStepsProps, type HelpStructuredData, type HelpTabProps, type HelpTabsProps, type HelpTocOptions, type HelpVirtualFile, type HelpWebhookRequest, createHelpRevalidateHandler, createHelpSource, getHelpToc, helpBodyComponents, helpDynamicSource, helpMetaEntry, helpStructuredData, helpTreePaths, helpTreeToFiles, safeHelpHref };
|
|
780
|
+
export { HELP_BLOCKS, HELP_CALLOUT_TYPES, type HelpBlockComponent, type HelpBlockFieldValue, type HelpBlockName, type HelpBlockProps, type HelpBlockValue, type HelpBlockValues, HelpBody, type HelpBodyComponentMap, type HelpBodyComponents, type HelpBodyProps, type HelpCalloutProps, type HelpCalloutType, type HelpCardProps, type HelpCardsProps, type HelpCodeProps, type HelpFilesOptions, type HelpHeadingProps, type HelpImageProps, type HelpLinkProps, type HelpMetaData, type HelpPageData, type HelpRevalidateOptions, type HelpSource, type HelpSourceConfig, type HelpSourceOptions, type HelpStepProps, type HelpStepsProps, type HelpStructuredData, type HelpTabProps, type HelpTabsProps, type HelpTocOptions, type HelpVirtualFile, type HelpWebhookRequest, createHelpRevalidateHandler, createHelpSource, getHelpToc, helpBlockText, helpBlockValues, helpBodyComponents, helpDynamicSource, helpMetaEntry, helpStructuredData, helpTreePaths, helpTreeToFiles, safeHelpHref };
|
package/dist/fumadocs.js
CHANGED
|
@@ -1,26 +1,57 @@
|
|
|
1
|
+
import { directiveFromMarkdown } from 'mdast-util-directive';
|
|
2
|
+
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
3
|
+
import { gfmFromMarkdown } from 'mdast-util-gfm';
|
|
4
|
+
import { directive } from 'micromark-extension-directive';
|
|
5
|
+
import { gfm } from 'micromark-extension-gfm';
|
|
1
6
|
import { Callout } from 'fumadocs-ui/components/callout';
|
|
2
7
|
import { Card, Cards } from 'fumadocs-ui/components/card';
|
|
3
8
|
import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock';
|
|
4
9
|
import { Heading } from 'fumadocs-ui/components/heading';
|
|
5
10
|
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
|
6
11
|
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
|
7
|
-
import { Fragment as Fragment$1 } from 'react';
|
|
8
|
-
import { directiveFromMarkdown } from 'mdast-util-directive';
|
|
9
|
-
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
10
|
-
import { gfmFromMarkdown } from 'mdast-util-gfm';
|
|
11
|
-
import { directive } from 'micromark-extension-directive';
|
|
12
|
-
import { gfm } from 'micromark-extension-gfm';
|
|
12
|
+
import { Fragment as Fragment$1, createElement } from 'react';
|
|
13
13
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
14
14
|
|
|
15
|
-
// src/fumadocs/
|
|
15
|
+
// src/fumadocs/markdown.ts
|
|
16
16
|
var HELP_BLOCKS = ["callout", "steps", "step", "tabs", "tab", "cards", "card"];
|
|
17
17
|
var HELP_CALLOUT_TYPES = ["info", "warn", "error", "success", "idea"];
|
|
18
18
|
var isHelpBlock = (name) => HELP_BLOCKS.includes(name);
|
|
19
|
+
var CODE_FENCE = /^[ \t]*(`{3,}|~{3,})(.*)$/;
|
|
20
|
+
var CLOSING_FENCE = /^([ \t]*):(:{2,}[ \t]*)$/;
|
|
21
|
+
var MASK = "\uE000";
|
|
22
|
+
function maskCodeFences(markdown) {
|
|
23
|
+
if (markdown.includes(MASK)) return markdown;
|
|
24
|
+
let open = null;
|
|
25
|
+
return markdown.split("\n").map((line) => {
|
|
26
|
+
const fence = CODE_FENCE.exec(line);
|
|
27
|
+
if (open) {
|
|
28
|
+
if (fence?.[1]?.[0] === open.char && fence[1].length >= open.length && !fence[2]?.trim()) {
|
|
29
|
+
open = null;
|
|
30
|
+
return line;
|
|
31
|
+
}
|
|
32
|
+
return line.replace(CLOSING_FENCE, `$1${MASK}$2`);
|
|
33
|
+
}
|
|
34
|
+
if (fence?.[1] && !(fence[1][0] === "`" && fence[2]?.includes("`"))) {
|
|
35
|
+
open = { char: fence[1][0] ?? "`", length: fence[1].length };
|
|
36
|
+
}
|
|
37
|
+
return line;
|
|
38
|
+
}).join("\n");
|
|
39
|
+
}
|
|
40
|
+
function unmask(node) {
|
|
41
|
+
if ("value" in node && typeof node.value === "string" && node.value.includes(MASK)) {
|
|
42
|
+
node.value = node.value.replaceAll(MASK, ":");
|
|
43
|
+
}
|
|
44
|
+
if ("children" in node) for (const child of node.children) unmask(child);
|
|
45
|
+
}
|
|
19
46
|
function parseHelpMarkdown(markdown) {
|
|
20
|
-
|
|
47
|
+
const source = markdown ?? "";
|
|
48
|
+
const masked = maskCodeFences(source);
|
|
49
|
+
const root = fromMarkdown(masked, {
|
|
21
50
|
extensions: [gfm(), directive()],
|
|
22
51
|
mdastExtensions: [gfmFromMarkdown(), directiveFromMarkdown()]
|
|
23
52
|
});
|
|
53
|
+
if (masked !== source) unmask(root);
|
|
54
|
+
return root;
|
|
24
55
|
}
|
|
25
56
|
var CONTROL = /[\u0000-\u001f\u007f]/;
|
|
26
57
|
var PROTOCOL_RELATIVE = /^[\\/]{2}/;
|
|
@@ -40,10 +71,14 @@ function safeHelpImageSrc(raw) {
|
|
|
40
71
|
if (/^\/[^\s]*$/.test(src)) return src;
|
|
41
72
|
return null;
|
|
42
73
|
}
|
|
43
|
-
function plainText(node) {
|
|
74
|
+
function plainText(node, override) {
|
|
75
|
+
const own = override?.(node);
|
|
76
|
+
if (own !== void 0) return own;
|
|
44
77
|
if ("value" in node && typeof node.value === "string") return node.value;
|
|
45
78
|
if (node.type === "image") return node.alt ?? "";
|
|
46
|
-
if ("children" in node)
|
|
79
|
+
if ("children" in node) {
|
|
80
|
+
return node.children.map((child) => plainText(child, override)).join("");
|
|
81
|
+
}
|
|
47
82
|
return "";
|
|
48
83
|
}
|
|
49
84
|
function sourceOf(node, markdown) {
|
|
@@ -77,34 +112,31 @@ function codeInfo(lang, meta) {
|
|
|
77
112
|
const title = (match ? match[1] ?? match[2] ?? match[3] : void 0)?.trim() || void 0;
|
|
78
113
|
return { language, title };
|
|
79
114
|
}
|
|
80
|
-
var
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
};
|
|
95
|
-
function headingSlug(text) {
|
|
96
|
-
return text.normalize("NFC").toLowerCase().replace(/[äöüßæœøåđðþłı]/g, (ch) => TRANSLITERATIONS[ch] ?? ch).normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 96).replace(/-+$/, "");
|
|
115
|
+
var GITHUB_SLUG_DROP = /[^\p{L}\p{M}\p{N}\p{Pc} -]/gu;
|
|
116
|
+
function headingId(text, used) {
|
|
117
|
+
const base = text.toLowerCase().replace(GITHUB_SLUG_DROP, "").replace(/ /g, "-") || "section";
|
|
118
|
+
let id = base;
|
|
119
|
+
while (used.has(id)) {
|
|
120
|
+
const count = (used.get(base) ?? 0) + 1;
|
|
121
|
+
used.set(base, count);
|
|
122
|
+
id = `${base}-${count}`;
|
|
123
|
+
}
|
|
124
|
+
used.set(id, 0);
|
|
125
|
+
return id;
|
|
126
|
+
}
|
|
127
|
+
function isTitledStep(node) {
|
|
128
|
+
return node.type === "containerDirective" && node.name === "step" && !!directiveTitle(node);
|
|
97
129
|
}
|
|
98
130
|
function collectHeadings(root) {
|
|
99
131
|
const out = [];
|
|
100
132
|
const walk = (node) => {
|
|
101
|
-
if (node.type === "heading") out.push(node);
|
|
133
|
+
if (node.type === "heading" || isTitledStep(node)) out.push(node);
|
|
102
134
|
if ("children" in node) for (const child of node.children) walk(child);
|
|
103
135
|
};
|
|
104
136
|
walk(root);
|
|
105
137
|
return out;
|
|
106
138
|
}
|
|
107
|
-
function headingIds(root, known) {
|
|
139
|
+
function headingIds(root, known, override) {
|
|
108
140
|
const headings = collectHeadings(root);
|
|
109
141
|
const ids = /* @__PURE__ */ new Map();
|
|
110
142
|
if (known && known.length === headings.length) {
|
|
@@ -114,15 +146,231 @@ function headingIds(root, known) {
|
|
|
114
146
|
return ids;
|
|
115
147
|
}
|
|
116
148
|
const used = /* @__PURE__ */ new Map();
|
|
149
|
+
const withoutBlocks = override ? (node) => {
|
|
150
|
+
if (override(node) === void 0) return void 0;
|
|
151
|
+
if (node.type !== "textDirective") return "";
|
|
152
|
+
return node.children.map((child) => plainText(child)).join("").trim();
|
|
153
|
+
} : void 0;
|
|
117
154
|
for (const heading of headings) {
|
|
118
|
-
const text = plainText(heading).replace(/\s+/g, " ").trim();
|
|
119
|
-
|
|
120
|
-
const seen = used.get(base) ?? 0;
|
|
121
|
-
used.set(base, seen + 1);
|
|
122
|
-
ids.set(heading, seen === 0 ? base : `${base}-${seen + 1}`);
|
|
155
|
+
const text = heading.type === "heading" ? plainText(heading, withoutBlocks) : (directiveTitle(heading) ?? "").replace(/\s+/g, " ").trim();
|
|
156
|
+
ids.set(heading, headingId(text, used));
|
|
123
157
|
}
|
|
124
158
|
return ids;
|
|
125
159
|
}
|
|
160
|
+
|
|
161
|
+
// src/fumadocs/blocks.ts
|
|
162
|
+
var TEXT_VALUE_MAX = 300;
|
|
163
|
+
var LONG_VALUE_MAX = 2e3;
|
|
164
|
+
var ICON = /^[A-Za-z][A-Za-z0-9]{0,47}$/;
|
|
165
|
+
var NAMED_ENTITIES = {
|
|
166
|
+
amp: "&",
|
|
167
|
+
lt: "<",
|
|
168
|
+
gt: ">",
|
|
169
|
+
quot: '"',
|
|
170
|
+
apos: "'",
|
|
171
|
+
nbsp: "\xA0"
|
|
172
|
+
};
|
|
173
|
+
function decodeEntities(value) {
|
|
174
|
+
return value.replace(/&(#x[0-9a-f]{1,6}|#[0-9]{1,7}|[a-z]+);/gi, (match, ref) => {
|
|
175
|
+
if (ref[0] === "#") {
|
|
176
|
+
const code = ref[1] === "x" || ref[1] === "X" ? parseInt(ref.slice(2), 16) : Number(ref.slice(1));
|
|
177
|
+
return Number.isFinite(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : match;
|
|
178
|
+
}
|
|
179
|
+
return NAMED_ENTITIES[ref.toLowerCase()] ?? match;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
var ATTRIBUTE = /^(?:#([^\s"'<>=`}#.]+)|\.([^\s"'<>=`}#.]+)|([A-Za-z_:][A-Za-z0-9_.:-]*)(?:[ \t]*=[ \t]*(?:"([^"\n]*)"|'([^'\n]*)'|([^\s"'<>=`}]+)))?)/;
|
|
183
|
+
function parseAttributes(src) {
|
|
184
|
+
const attributes = {};
|
|
185
|
+
let pos = 1;
|
|
186
|
+
for (; ; ) {
|
|
187
|
+
while (src[pos] === " " || src[pos] === " ") pos++;
|
|
188
|
+
if (src[pos] === "}") return { attributes, length: pos + 1 };
|
|
189
|
+
const match = ATTRIBUTE.exec(src.slice(pos));
|
|
190
|
+
if (!match) return null;
|
|
191
|
+
const [whole, id, cls, key, dq, sq, bare] = match;
|
|
192
|
+
if (id !== void 0) attributes.id = decodeEntities(id);
|
|
193
|
+
else if (cls !== void 0)
|
|
194
|
+
attributes.class = attributes.class ? `${attributes.class} ${cls}` : cls;
|
|
195
|
+
else if (key !== void 0) attributes[key] = decodeEntities(dq ?? sq ?? bare ?? "");
|
|
196
|
+
pos += whole.length;
|
|
197
|
+
if (src[pos] !== " " && src[pos] !== " " && src[pos] !== "}") return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function parseLabel(src) {
|
|
201
|
+
let depth = 0;
|
|
202
|
+
for (let pos = 0; pos < src.length; pos++) {
|
|
203
|
+
const char = src[pos];
|
|
204
|
+
if (char === "\n") return null;
|
|
205
|
+
if (char === "\\") {
|
|
206
|
+
pos++;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (char === "[") depth++;
|
|
210
|
+
else if (char === "]") {
|
|
211
|
+
depth--;
|
|
212
|
+
if (depth === 0) return { label: src.slice(1, pos), length: pos + 1 };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
function parseDirectiveTail(src) {
|
|
218
|
+
let rest = src;
|
|
219
|
+
let label;
|
|
220
|
+
let attributes = {};
|
|
221
|
+
if (rest.startsWith("[")) {
|
|
222
|
+
const parsed = parseLabel(rest);
|
|
223
|
+
if (!parsed) return null;
|
|
224
|
+
label = parsed.label;
|
|
225
|
+
rest = rest.slice(parsed.length);
|
|
226
|
+
}
|
|
227
|
+
if (rest.startsWith("{")) {
|
|
228
|
+
const parsed = parseAttributes(rest);
|
|
229
|
+
if (!parsed) return null;
|
|
230
|
+
attributes = parsed.attributes;
|
|
231
|
+
rest = rest.slice(parsed.length);
|
|
232
|
+
}
|
|
233
|
+
return { ...label !== void 0 ? { label } : {}, attributes, rest };
|
|
234
|
+
}
|
|
235
|
+
function plainLabel(label) {
|
|
236
|
+
if (label === void 0) return void 0;
|
|
237
|
+
const text = label.replace(/\\(.)/g, "$1").replace(/[*_`~]/g, "").trim();
|
|
238
|
+
return text || void 0;
|
|
239
|
+
}
|
|
240
|
+
var isVideoAddress = (value) => safeHelpHref(value) === value && (/^https:\/\//i.test(value) || value.startsWith("/"));
|
|
241
|
+
function fieldValue(field, raw) {
|
|
242
|
+
if (field.type === "boolean") {
|
|
243
|
+
const flag = raw.trim().toLowerCase();
|
|
244
|
+
if (flag === "" || flag === "true") return true;
|
|
245
|
+
if (flag === "false") return false;
|
|
246
|
+
return void 0;
|
|
247
|
+
}
|
|
248
|
+
const value = raw.trim();
|
|
249
|
+
switch (field.type) {
|
|
250
|
+
case "text":
|
|
251
|
+
return !/[\r\n]/.test(value) && value.length <= TEXT_VALUE_MAX ? value : void 0;
|
|
252
|
+
case "longText":
|
|
253
|
+
return value.length <= LONG_VALUE_MAX ? value : void 0;
|
|
254
|
+
case "icon":
|
|
255
|
+
return ICON.test(value) ? value : void 0;
|
|
256
|
+
case "url":
|
|
257
|
+
return value.length <= LONG_VALUE_MAX && safeHelpHref(value) === value ? value : void 0;
|
|
258
|
+
case "choice":
|
|
259
|
+
return field.options?.includes(value) ? value : void 0;
|
|
260
|
+
case "image":
|
|
261
|
+
return value.length <= LONG_VALUE_MAX && safeHelpImageSrc(value) === value ? value : void 0;
|
|
262
|
+
case "video":
|
|
263
|
+
return value.length <= LONG_VALUE_MAX && isVideoAddress(value) ? value : void 0;
|
|
264
|
+
default:
|
|
265
|
+
return void 0;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function helpBlockValues(block, attributes) {
|
|
269
|
+
const values = {};
|
|
270
|
+
for (const field of block.fields) {
|
|
271
|
+
const raw = Object.hasOwn(attributes, field.key) ? attributes[field.key] : void 0;
|
|
272
|
+
if (field.type === "boolean") {
|
|
273
|
+
if (raw === void 0) {
|
|
274
|
+
values[field.key] = false;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
} else if (raw === void 0 || raw.trim() === "") {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const value = fieldValue(field, raw);
|
|
281
|
+
if (value !== void 0) values[field.key] = value;
|
|
282
|
+
}
|
|
283
|
+
return values;
|
|
284
|
+
}
|
|
285
|
+
function isTemplate(template) {
|
|
286
|
+
for (let i = 0; i < template.length; i++) {
|
|
287
|
+
const char = template[i];
|
|
288
|
+
if ((char === "{" || char === "}") && template[i + 1] === char) {
|
|
289
|
+
i++;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (char === "}") return false;
|
|
293
|
+
if (char !== "{") continue;
|
|
294
|
+
const end = template.indexOf("}", i + 1);
|
|
295
|
+
if (end === -1) return false;
|
|
296
|
+
const key = template.slice(i + 1, end).trim();
|
|
297
|
+
if (!key || key.includes("{")) return false;
|
|
298
|
+
i = end;
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
function helpBlockText(block, values, label) {
|
|
303
|
+
const template = block.text ?? "";
|
|
304
|
+
const fields = new Map(block.fields.map((f) => [f.key, f]));
|
|
305
|
+
let text = "";
|
|
306
|
+
if (isTemplate(template)) {
|
|
307
|
+
for (let i = 0; i < template.length; i++) {
|
|
308
|
+
const char = template[i] ?? "";
|
|
309
|
+
if ((char === "{" || char === "}") && template[i + 1] === char) {
|
|
310
|
+
text += char;
|
|
311
|
+
i++;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (char !== "{") {
|
|
315
|
+
text += char;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const end = template.indexOf("}", i + 1);
|
|
319
|
+
const key = template.slice(i + 1, end).trim();
|
|
320
|
+
const value = values[key];
|
|
321
|
+
if (value === true) text += fields.get(key)?.label ?? "";
|
|
322
|
+
else if (typeof value === "string") text += value;
|
|
323
|
+
i = end;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return `${text} ${label ?? ""}`.replace(/\s+/g, " ").trim();
|
|
327
|
+
}
|
|
328
|
+
var FORMS = {
|
|
329
|
+
textDirective: "inline",
|
|
330
|
+
leafDirective: "block",
|
|
331
|
+
containerDirective: "container"
|
|
332
|
+
};
|
|
333
|
+
function helpBlockMatcher(blocks, markdown) {
|
|
334
|
+
const defined = new Map((blocks ?? []).map((block) => [block.name, block]));
|
|
335
|
+
const cache = /* @__PURE__ */ new Map();
|
|
336
|
+
const find = (node) => {
|
|
337
|
+
if (defined.size === 0) return null;
|
|
338
|
+
if (node.type !== "textDirective" && node.type !== "leafDirective" && node.type !== "containerDirective") {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
const block = defined.get(node.name);
|
|
342
|
+
if (!block || block.kind !== FORMS[node.type]) return null;
|
|
343
|
+
const start = node.position?.start.offset;
|
|
344
|
+
const end = node.position?.end.offset;
|
|
345
|
+
if (start === void 0 || end === void 0) return null;
|
|
346
|
+
const lineEnd = markdown.indexOf("\n", start);
|
|
347
|
+
const line = markdown.slice(start, lineEnd === -1 ? void 0 : lineEnd);
|
|
348
|
+
const head = /^(:+)([A-Za-z][A-Za-z0-9_-]*)/.exec(line);
|
|
349
|
+
const colons = head?.[1]?.length ?? 0;
|
|
350
|
+
const form = colons === 1 ? "textDirective" : colons === 2 ? "leafDirective" : "containerDirective";
|
|
351
|
+
if (!head || form !== node.type || head[2] !== node.name) return null;
|
|
352
|
+
const tail = parseDirectiveTail(line.slice(head[0].length));
|
|
353
|
+
if (!tail) return null;
|
|
354
|
+
if (node.type === "textDirective") {
|
|
355
|
+
if (line.length - tail.rest.length !== end - start) return null;
|
|
356
|
+
} else if (tail.rest.trim() !== "") return null;
|
|
357
|
+
const values = helpBlockValues(block, tail.attributes);
|
|
358
|
+
const label = node.type === "containerDirective" ? void 0 : plainLabel(tail.label);
|
|
359
|
+
return { block, node, values, text: helpBlockText(block, values, label) };
|
|
360
|
+
};
|
|
361
|
+
const use = (node) => {
|
|
362
|
+
if (!cache.has(node)) cache.set(node, find(node));
|
|
363
|
+
return cache.get(node) ?? null;
|
|
364
|
+
};
|
|
365
|
+
const text = (node) => {
|
|
366
|
+
const found = use(node);
|
|
367
|
+
if (!found) return void 0;
|
|
368
|
+
if (found.node.type !== "containerDirective") return found.text;
|
|
369
|
+
const inner = contentOf(found.node).map((child) => plainText(child, text)).join(" ");
|
|
370
|
+
return `${found.text} ${inner}`.trim();
|
|
371
|
+
};
|
|
372
|
+
return { use, text };
|
|
373
|
+
}
|
|
126
374
|
var PLAIN_CODE_COLORS = {
|
|
127
375
|
"--shiki-light": "var(--color-fd-foreground)",
|
|
128
376
|
"--shiki-dark": "var(--color-fd-foreground)"
|
|
@@ -130,8 +378,8 @@ var PLAIN_CODE_COLORS = {
|
|
|
130
378
|
var helpBodyComponents = {
|
|
131
379
|
Callout: ({ type, title, children }) => /* @__PURE__ */ jsx(Callout, { type, title, children }),
|
|
132
380
|
Steps: ({ children }) => /* @__PURE__ */ jsx(Steps, { children }),
|
|
133
|
-
Step: ({ title, children }) => /* @__PURE__ */ jsxs(Step, { children: [
|
|
134
|
-
title ? /* @__PURE__ */ jsx("h3",
|
|
381
|
+
Step: ({ title, id, children }) => /* @__PURE__ */ jsxs(Step, { children: [
|
|
382
|
+
title ? /* @__PURE__ */ jsx(Heading, { as: "h3", id, children: title }) : null,
|
|
135
383
|
children
|
|
136
384
|
] }),
|
|
137
385
|
Tabs: ({ items, children }) => /* @__PURE__ */ jsx(Tabs, { items, children }),
|
|
@@ -152,6 +400,15 @@ var helpBodyComponents = {
|
|
|
152
400
|
Link: ({ href, title, children }) => /* @__PURE__ */ jsx("a", { href, title, children }),
|
|
153
401
|
Image: ({ src, alt, title }) => /* @__PURE__ */ jsx("img", { src, alt, title, loading: "lazy" })
|
|
154
402
|
};
|
|
403
|
+
var BUILT_IN_KEYS = Object.keys(helpBodyComponents);
|
|
404
|
+
function pickBuiltIns(components) {
|
|
405
|
+
const out = {};
|
|
406
|
+
for (const key of BUILT_IN_KEYS) {
|
|
407
|
+
const component = components[key];
|
|
408
|
+
if (component) out[key] = component;
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
155
412
|
var normalizeId = (id) => id.trim().replace(/\s+/g, " ").toLowerCase();
|
|
156
413
|
function collectDefinitions(root) {
|
|
157
414
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -254,8 +511,30 @@ function renderTabs(children, ctx) {
|
|
|
254
511
|
);
|
|
255
512
|
}) });
|
|
256
513
|
}
|
|
514
|
+
function renderCustomBlock(use, ctx) {
|
|
515
|
+
const { block, node, values, text } = use;
|
|
516
|
+
const isContainer = node.type === "containerDirective";
|
|
517
|
+
const children = node.type === "containerDirective" ? renderAll(contentOf(node), { ...ctx, tight: false }) : node.children.length ? renderAll(node.children, { ...ctx, inline: true }) : void 0;
|
|
518
|
+
const Component = Object.hasOwn(ctx.custom, block.name) ? ctx.custom[block.name] : void 0;
|
|
519
|
+
if (Component) {
|
|
520
|
+
return createElement(Component, { ...values, block: block.name, lang: ctx.lang }, children);
|
|
521
|
+
}
|
|
522
|
+
const attributes = {
|
|
523
|
+
className: "sd-block",
|
|
524
|
+
"data-block": block.name,
|
|
525
|
+
"data-props": JSON.stringify(values)
|
|
526
|
+
};
|
|
527
|
+
if (block.kind === "inline") return /* @__PURE__ */ jsx("span", { ...attributes, children: text });
|
|
528
|
+
if (!isContainer) return /* @__PURE__ */ jsx("div", { ...attributes, children: text });
|
|
529
|
+
return /* @__PURE__ */ jsxs("div", { ...attributes, children: [
|
|
530
|
+
text ? /* @__PURE__ */ jsx("p", { className: "sd-block-text", children: text }) : null,
|
|
531
|
+
/* @__PURE__ */ jsx("div", { className: "sd-block-body", children })
|
|
532
|
+
] });
|
|
533
|
+
}
|
|
257
534
|
function renderDirective(node, ctx) {
|
|
258
535
|
const { c } = ctx;
|
|
536
|
+
const custom = ctx.blocks.use(node);
|
|
537
|
+
if (custom) return renderCustomBlock(custom, ctx);
|
|
259
538
|
if (!isHelpBlock(node.name)) {
|
|
260
539
|
const source = sourceOf(node, ctx.markdown);
|
|
261
540
|
const lines = source.split("\n");
|
|
@@ -275,7 +554,7 @@ function renderDirective(node, ctx) {
|
|
|
275
554
|
return /* @__PURE__ */ jsx(c.Steps, { children: content.map(
|
|
276
555
|
(child, i) => isDirective(child, "step") ? (
|
|
277
556
|
// biome-ignore lint/suspicious/noArrayIndexKey: static content, never reordered.
|
|
278
|
-
/* @__PURE__ */ jsx(c.Step, { title: directiveTitle(child), children: renderAll(contentOf(child), ctx) }, i)
|
|
557
|
+
/* @__PURE__ */ jsx(c.Step, { title: directiveTitle(child), id: ctx.ids.get(child), children: renderAll(contentOf(child), ctx) }, i)
|
|
279
558
|
) : (
|
|
280
559
|
// A loose block between steps becomes a step of its own.
|
|
281
560
|
// biome-ignore lint/suspicious/noArrayIndexKey: static content, never reordered.
|
|
@@ -283,7 +562,7 @@ function renderDirective(node, ctx) {
|
|
|
283
562
|
)
|
|
284
563
|
) });
|
|
285
564
|
case "step":
|
|
286
|
-
return /* @__PURE__ */ jsx(c.Steps, { children: /* @__PURE__ */ jsx(c.Step, { title, children: renderAll(content, ctx) }) });
|
|
565
|
+
return /* @__PURE__ */ jsx(c.Steps, { children: /* @__PURE__ */ jsx(c.Step, { title, id: ctx.ids.get(node), children: renderAll(content, ctx) }) });
|
|
287
566
|
case "tabs":
|
|
288
567
|
return renderTabs(content, ctx);
|
|
289
568
|
case "tab":
|
|
@@ -358,24 +637,34 @@ function renderNode(node, ctx) {
|
|
|
358
637
|
return null;
|
|
359
638
|
case "containerDirective":
|
|
360
639
|
return renderDirective(node, { ...ctx, tight: false });
|
|
361
|
-
// Not ours: printed back as written.
|
|
362
640
|
case "leafDirective":
|
|
641
|
+
case "textDirective": {
|
|
642
|
+
const custom = ctx.blocks.use(node);
|
|
643
|
+
if (custom) return renderCustomBlock(custom, ctx);
|
|
644
|
+
return node.type === "leafDirective" ? literalBlock(sourceOf(node, ctx.markdown)) : sourceOf(node, ctx.markdown);
|
|
645
|
+
}
|
|
646
|
+
// Not ours: printed back as written.
|
|
363
647
|
case "footnoteDefinition":
|
|
364
648
|
return literalBlock(sourceOf(node, ctx.markdown));
|
|
365
|
-
case "textDirective":
|
|
366
649
|
case "footnoteReference":
|
|
367
650
|
return sourceOf(node, ctx.markdown);
|
|
368
651
|
default:
|
|
369
652
|
return "children" in node ? renderAll(node.children, ctx) : "value" in node ? String(node.value) : plainText(node);
|
|
370
653
|
}
|
|
371
654
|
}
|
|
372
|
-
function HelpBody(
|
|
373
|
-
const
|
|
655
|
+
function HelpBody(props) {
|
|
656
|
+
const { page, components = {}, icon } = props;
|
|
657
|
+
const source = props.markdown ?? page?.markdown ?? "";
|
|
658
|
+
const headings = props.headings ?? page?.headings;
|
|
374
659
|
const root = parseHelpMarkdown(source);
|
|
660
|
+
const blocks = helpBlockMatcher(props.blocks, source);
|
|
375
661
|
const ctx = {
|
|
376
662
|
markdown: source,
|
|
377
|
-
|
|
378
|
-
|
|
663
|
+
blocks,
|
|
664
|
+
custom: components,
|
|
665
|
+
lang: props.lang ?? page?.locale ?? "",
|
|
666
|
+
ids: headingIds(root, headings, blocks.text),
|
|
667
|
+
c: { ...helpBodyComponents, ...pickBuiltIns(components) },
|
|
379
668
|
icon,
|
|
380
669
|
definitions: collectDefinitions(root),
|
|
381
670
|
linkless: false,
|
|
@@ -484,12 +773,17 @@ function parseBody(raw) {
|
|
|
484
773
|
}
|
|
485
774
|
|
|
486
775
|
// src/fumadocs/structured.ts
|
|
487
|
-
function helpStructuredData(markdown, headings) {
|
|
488
|
-
const
|
|
489
|
-
const
|
|
776
|
+
function helpStructuredData(markdown, headings, blocks) {
|
|
777
|
+
const source = markdown ?? "";
|
|
778
|
+
const root = parseHelpMarkdown(source);
|
|
779
|
+
const matcher = helpBlockMatcher(blocks, source);
|
|
780
|
+
const ids = headingIds(root, headings, matcher.text);
|
|
490
781
|
const out = { headings: [], contents: [] };
|
|
491
782
|
let current;
|
|
492
|
-
const text = (node) => plainText(node).replace(/\s+/g, " ").trim();
|
|
783
|
+
const text = (node) => plainText(node, matcher.text).replace(/\s+/g, " ").trim();
|
|
784
|
+
const add = (content) => {
|
|
785
|
+
if (content) out.contents.push({ heading: current, content });
|
|
786
|
+
};
|
|
493
787
|
const walk = (node) => {
|
|
494
788
|
if (node.type === "heading") {
|
|
495
789
|
current = ids.get(node);
|
|
@@ -497,9 +791,22 @@ function helpStructuredData(markdown, headings) {
|
|
|
497
791
|
if (current && content) out.headings.push({ id: current, content });
|
|
498
792
|
return;
|
|
499
793
|
}
|
|
794
|
+
if (isTitledStep(node)) {
|
|
795
|
+
current = ids.get(node);
|
|
796
|
+
const content = directiveTitle(node) ?? "";
|
|
797
|
+
if (current) out.headings.push({ id: current, content });
|
|
798
|
+
for (const child of contentOf(node)) walk(child);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const use = matcher.use(node);
|
|
802
|
+
if (use) {
|
|
803
|
+
add(use.text);
|
|
804
|
+
if (use.node.type === "containerDirective")
|
|
805
|
+
for (const child of contentOf(use.node)) walk(child);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
500
808
|
if (node.type === "paragraph" || node.type === "tableCell") {
|
|
501
|
-
|
|
502
|
-
if (content) out.contents.push({ heading: current, content });
|
|
809
|
+
add(text(node));
|
|
503
810
|
return;
|
|
504
811
|
}
|
|
505
812
|
if (node.type === "code" || node.type === "html" || node.type === "definition") return;
|
|
@@ -519,12 +826,13 @@ var pageFileName = (page) => {
|
|
|
519
826
|
const name = lastSegment(page.path);
|
|
520
827
|
return name === "index" ? "index_" : name;
|
|
521
828
|
};
|
|
829
|
+
var folderDirName = (folder) => folder.group ? `(${folder.slug})` : folder.slug;
|
|
522
830
|
function helpMetaEntry(node) {
|
|
523
831
|
switch (node.type) {
|
|
524
832
|
case "page":
|
|
525
833
|
return pageFileName(node);
|
|
526
834
|
case "folder":
|
|
527
|
-
return node
|
|
835
|
+
return folderDirName(node);
|
|
528
836
|
case "separator": {
|
|
529
837
|
const title = node.title?.replace(/---/g, "\u2013").trim();
|
|
530
838
|
if (!title) return "---";
|
|
@@ -539,6 +847,7 @@ function helpTreeToFiles(payload, options) {
|
|
|
539
847
|
const ext = (name, extension) => `${name}${options.suffix ? `.${options.suffix}` : ""}.${extension}`;
|
|
540
848
|
const join = (dir, name) => dir ? `${dir}/${name}` : name;
|
|
541
849
|
const seen = /* @__PURE__ */ new Set();
|
|
850
|
+
const blocks = payload.site.blocks ?? [];
|
|
542
851
|
const addPage = (page, file) => {
|
|
543
852
|
if (seen.has(file)) return;
|
|
544
853
|
seen.add(file);
|
|
@@ -551,25 +860,33 @@ function helpTreeToFiles(payload, options) {
|
|
|
551
860
|
...page.icon ? { icon: page.icon } : {},
|
|
552
861
|
path: page.path,
|
|
553
862
|
lang: options.lang,
|
|
863
|
+
blocks,
|
|
554
864
|
load,
|
|
555
865
|
structuredData: async () => {
|
|
556
866
|
const full = await load();
|
|
557
|
-
return full ? helpStructuredData(full.markdown, full.headings) : { headings: [], contents: [] };
|
|
867
|
+
return full ? helpStructuredData(full.markdown, full.headings, blocks) : { headings: [], contents: [] };
|
|
558
868
|
},
|
|
559
869
|
...prefetched ? { page: prefetched } : {}
|
|
560
870
|
};
|
|
561
871
|
files.push({ type: "page", path: file, slugs: slugsOf(page.path), data });
|
|
562
872
|
};
|
|
873
|
+
const folderPages = (folder) => {
|
|
874
|
+
const pages = folder.children.map(helpMetaEntry);
|
|
875
|
+
if (!folder.index || !folder.listIndex) return pages;
|
|
876
|
+
const at = folder.children[0]?.type === "separator" ? 1 : 0;
|
|
877
|
+
return [...pages.slice(0, at), "index", ...pages.slice(at)];
|
|
878
|
+
};
|
|
563
879
|
const addFolder = (folder, dir) => {
|
|
564
880
|
if (folder.index) addPage(folder.index, join(dir, ext("index", "mdx")));
|
|
565
881
|
const data = {
|
|
566
882
|
title: folder.title,
|
|
883
|
+
...folder.description ? { description: folder.description } : {},
|
|
567
884
|
...folder.icon ? { icon: folder.icon } : {},
|
|
568
885
|
...folder.root ? { root: true } : {},
|
|
569
886
|
defaultOpen: folder.defaultOpen,
|
|
570
887
|
collapsible: folder.collapsible,
|
|
571
|
-
...folder.index ? { pagesIndex: "index" } : {},
|
|
572
|
-
pages: folder
|
|
888
|
+
...folder.index && !folder.listIndex ? { pagesIndex: "index" } : {},
|
|
889
|
+
pages: folderPages(folder)
|
|
573
890
|
};
|
|
574
891
|
files.push({ type: "meta", path: join(dir, ext("meta", "json")), data });
|
|
575
892
|
addChildren(folder.children, dir);
|
|
@@ -577,9 +894,9 @@ function helpTreeToFiles(payload, options) {
|
|
|
577
894
|
const addChildren = (nodes, dir) => {
|
|
578
895
|
for (const node of nodes) {
|
|
579
896
|
if (node.type === "page") {
|
|
580
|
-
addPage(node,
|
|
897
|
+
addPage(node, join(dir, ext(pageFileName(node), "mdx")));
|
|
581
898
|
} else if (node.type === "folder") {
|
|
582
|
-
addFolder(node, join(dir, node
|
|
899
|
+
addFolder(node, join(dir, folderDirName(node)));
|
|
583
900
|
}
|
|
584
901
|
}
|
|
585
902
|
};
|
|
@@ -661,4 +978,4 @@ function getHelpToc(headings, options = {}) {
|
|
|
661
978
|
return list.filter((heading) => heading.level >= min && heading.level <= max && heading.id).map((heading) => ({ title: heading.text, url: `#${heading.id}`, depth: heading.level }));
|
|
662
979
|
}
|
|
663
980
|
|
|
664
|
-
export { HELP_BLOCKS, HELP_CALLOUT_TYPES, HelpBody, createHelpRevalidateHandler, createHelpSource, getHelpToc, helpBodyComponents, helpDynamicSource, helpMetaEntry, helpStructuredData, helpTreePaths, helpTreeToFiles, safeHelpHref };
|
|
981
|
+
export { HELP_BLOCKS, HELP_CALLOUT_TYPES, HelpBody, createHelpRevalidateHandler, createHelpSource, getHelpToc, helpBlockText, helpBlockValues, helpBodyComponents, helpDynamicSource, helpMetaEntry, helpStructuredData, helpTreePaths, helpTreeToFiles, safeHelpHref };
|
package/dist/index.d.cts
CHANGED
|
@@ -139,7 +139,8 @@ declare function verifyWebhookSignature(secret: string, rawBody: WebhookBody, he
|
|
|
139
139
|
* preview token names. Absent values are `null`, never a missing key.
|
|
140
140
|
*
|
|
141
141
|
* Paths: a page's path is its folder slugs plus its slug, without leading or
|
|
142
|
-
* trailing slash (`guides/login`)
|
|
142
|
+
* trailing slash (`guides/login`) — a `group` folder adds no slug; the site's
|
|
143
|
+
* home page has the path `""`.
|
|
143
144
|
*/
|
|
144
145
|
/** The doc site itself. */
|
|
145
146
|
interface HelpPublicSite {
|
|
@@ -152,6 +153,40 @@ interface HelpPublicSite {
|
|
|
152
153
|
languages: string[];
|
|
153
154
|
/** The docs website's base address (`https://help.acme.com/docs`), if set. */
|
|
154
155
|
websiteUrl: string | null;
|
|
156
|
+
/** The site's own building blocks pages may use (empty when it defines none). */
|
|
157
|
+
blocks: HelpPublicBlockDefinition[];
|
|
158
|
+
}
|
|
159
|
+
/** What a field of a site's own block holds (see {@link HelpPublicBlockField}). */
|
|
160
|
+
type HelpPublicBlockFieldType = "text" | "longText" | "icon" | "url" | "choice" | "image" | "video" | "boolean";
|
|
161
|
+
/**
|
|
162
|
+
* One field of a site's own block: an attribute of its directive
|
|
163
|
+
* (`key="value"`). `boolean` fields are on when written bare (`key`) or as
|
|
164
|
+
* `key="true"`; every other value is a string. `options` lists a `choice`
|
|
165
|
+
* field's values (null for other types).
|
|
166
|
+
*/
|
|
167
|
+
interface HelpPublicBlockField {
|
|
168
|
+
key: string;
|
|
169
|
+
label: string;
|
|
170
|
+
type: HelpPublicBlockFieldType;
|
|
171
|
+
required: boolean;
|
|
172
|
+
options: string[] | null;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* A building block the site defines itself — WHAT it is; how it looks is the
|
|
176
|
+
* website's own component. Written in Markdown as `:name[label]{…}` in running
|
|
177
|
+
* text (`inline`), `::name[label]{…}` alone on a line (`block`) or
|
|
178
|
+
* `:::name{…}` … `:::` around Markdown content (`container`). `text` is the
|
|
179
|
+
* template of its text version (`{key}` = a field's value, a `boolean` field's
|
|
180
|
+
* label when on; `{{`/`}}` = a brace) — what to show without a component.
|
|
181
|
+
*/
|
|
182
|
+
interface HelpPublicBlockDefinition {
|
|
183
|
+
/** The directive name (`app-button`). */
|
|
184
|
+
name: string;
|
|
185
|
+
/** Display name. */
|
|
186
|
+
label: string;
|
|
187
|
+
kind: "inline" | "block" | "container";
|
|
188
|
+
fields: HelpPublicBlockField[];
|
|
189
|
+
text: string;
|
|
155
190
|
}
|
|
156
191
|
/** A page in the navigation. */
|
|
157
192
|
interface HelpPublicTreePage {
|
|
@@ -170,16 +205,28 @@ interface HelpPublicTreeFolder {
|
|
|
170
205
|
/** Stable node id. */
|
|
171
206
|
id: string;
|
|
172
207
|
slug: string;
|
|
173
|
-
/** The folder's path (its index page, if any, lives here). */
|
|
208
|
+
/** The folder's path (its index page, if any, lives here; a group's is its parent's). */
|
|
174
209
|
path: string;
|
|
175
210
|
title: string;
|
|
211
|
+
/** A short line under a tab's name in the tab menu (Fumadocs `description`); null without one. */
|
|
212
|
+
description: string | null;
|
|
176
213
|
icon: string | null;
|
|
177
214
|
/** A tab (top level only; Fumadocs `root`). */
|
|
178
215
|
root: boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Adds no segment to its pages' paths (Fumadocs `(name)` folder): `path` is
|
|
218
|
+
* the parent's, a top-level group's index page is the home (`""`).
|
|
219
|
+
*/
|
|
220
|
+
group: boolean;
|
|
179
221
|
defaultOpen: boolean;
|
|
180
222
|
collapsible: boolean;
|
|
181
223
|
/** The folder's own page, shown at the folder's path; null without one. */
|
|
182
224
|
index: HelpPublicTreePage | null;
|
|
225
|
+
/**
|
|
226
|
+
* List `index` as the folder's first entry (after a section title it starts
|
|
227
|
+
* with); the folder's name then only opens it (Fumadocs: `index` in `pages`).
|
|
228
|
+
*/
|
|
229
|
+
listIndex: boolean;
|
|
183
230
|
children: HelpPublicTreeNode[];
|
|
184
231
|
}
|
|
185
232
|
/** A labelled divider between entries (Fumadocs `---Title---`). */
|
|
@@ -256,12 +303,17 @@ interface HelpPublicPageLink {
|
|
|
256
303
|
}
|
|
257
304
|
/** One page with its text. */
|
|
258
305
|
interface HelpPublicPage extends HelpPublicPageSummary {
|
|
259
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* The page's Markdown with directives (`:::callout`, `::::steps`, …, and the
|
|
308
|
+
* site's own blocks as written — see `site.blocks`).
|
|
309
|
+
*/
|
|
260
310
|
markdown: string;
|
|
261
311
|
/** Safe server-rendered HTML of `markdown` (stable `sd-*` classes for the directives). */
|
|
262
312
|
html: string;
|
|
263
313
|
headings: HelpPublicHeading[];
|
|
264
314
|
blocks: HelpPublicBlock[];
|
|
315
|
+
/** The site's own building blocks the page uses, by name (in the site's order). */
|
|
316
|
+
customBlocks: string[];
|
|
265
317
|
breadcrumbs: HelpPublicBreadcrumb[];
|
|
266
318
|
previous: HelpPublicPageLink | null;
|
|
267
319
|
next: HelpPublicPageLink | null;
|
|
@@ -424,4 +476,4 @@ declare function createHelpClient(options: HelpClientOptions): HelpClient;
|
|
|
424
476
|
*/
|
|
425
477
|
declare function verifyHelpWebhook(secret: string, rawBody: WebhookBody, signatureHeader: string | null | undefined, options?: VerifyWebhookOptions): Promise<HelpWebhookBody | null>;
|
|
426
478
|
|
|
427
|
-
export { type BaseClientOptions, type CachedResponse, DEFAULT_BASE_URL, EVENT_HEADER, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type GetPageParams, type HelpClient, type HelpClientOptions, type HelpPageResult, type HelpPublicBlock, type HelpPublicBreadcrumb, type HelpPublicHeading, type HelpPublicPage, type HelpPublicPageLink, type HelpPublicPagePayload, type HelpPublicPageSummary, type HelpPublicPagesPayload, type HelpPublicPreviewPage, type HelpPublicPreviewPayload, type HelpPublicRedirect, type HelpPublicSearchHit, type HelpPublicSearchPayload, type HelpPublicSite, type HelpPublicSitePayload, type HelpPublicTreeFolder, type HelpPublicTreeLink, type HelpPublicTreeNode, type HelpPublicTreePage, type HelpPublicTreeSeparator, type HelpWebhookBody, type HelpWebhookEvent, type LangParams, type RequestOptions, SIGNATURE_HEADER, type SearchParams, SolutionDeskError, type VerifyWebhookOptions, type WebhookBody, createHelpClient, createMemoryEtagCache, normalizeHelpPath, verifyHelpWebhook, verifyWebhookSignature };
|
|
479
|
+
export { type BaseClientOptions, type CachedResponse, DEFAULT_BASE_URL, EVENT_HEADER, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type GetPageParams, type HelpClient, type HelpClientOptions, type HelpPageResult, type HelpPublicBlock, type HelpPublicBlockDefinition, type HelpPublicBlockField, type HelpPublicBlockFieldType, type HelpPublicBreadcrumb, type HelpPublicHeading, type HelpPublicPage, type HelpPublicPageLink, type HelpPublicPagePayload, type HelpPublicPageSummary, type HelpPublicPagesPayload, type HelpPublicPreviewPage, type HelpPublicPreviewPayload, type HelpPublicRedirect, type HelpPublicSearchHit, type HelpPublicSearchPayload, type HelpPublicSite, type HelpPublicSitePayload, type HelpPublicTreeFolder, type HelpPublicTreeLink, type HelpPublicTreeNode, type HelpPublicTreePage, type HelpPublicTreeSeparator, type HelpWebhookBody, type HelpWebhookEvent, type LangParams, type RequestOptions, SIGNATURE_HEADER, type SearchParams, SolutionDeskError, type VerifyWebhookOptions, type WebhookBody, createHelpClient, createMemoryEtagCache, normalizeHelpPath, verifyHelpWebhook, verifyWebhookSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -139,7 +139,8 @@ declare function verifyWebhookSignature(secret: string, rawBody: WebhookBody, he
|
|
|
139
139
|
* preview token names. Absent values are `null`, never a missing key.
|
|
140
140
|
*
|
|
141
141
|
* Paths: a page's path is its folder slugs plus its slug, without leading or
|
|
142
|
-
* trailing slash (`guides/login`)
|
|
142
|
+
* trailing slash (`guides/login`) — a `group` folder adds no slug; the site's
|
|
143
|
+
* home page has the path `""`.
|
|
143
144
|
*/
|
|
144
145
|
/** The doc site itself. */
|
|
145
146
|
interface HelpPublicSite {
|
|
@@ -152,6 +153,40 @@ interface HelpPublicSite {
|
|
|
152
153
|
languages: string[];
|
|
153
154
|
/** The docs website's base address (`https://help.acme.com/docs`), if set. */
|
|
154
155
|
websiteUrl: string | null;
|
|
156
|
+
/** The site's own building blocks pages may use (empty when it defines none). */
|
|
157
|
+
blocks: HelpPublicBlockDefinition[];
|
|
158
|
+
}
|
|
159
|
+
/** What a field of a site's own block holds (see {@link HelpPublicBlockField}). */
|
|
160
|
+
type HelpPublicBlockFieldType = "text" | "longText" | "icon" | "url" | "choice" | "image" | "video" | "boolean";
|
|
161
|
+
/**
|
|
162
|
+
* One field of a site's own block: an attribute of its directive
|
|
163
|
+
* (`key="value"`). `boolean` fields are on when written bare (`key`) or as
|
|
164
|
+
* `key="true"`; every other value is a string. `options` lists a `choice`
|
|
165
|
+
* field's values (null for other types).
|
|
166
|
+
*/
|
|
167
|
+
interface HelpPublicBlockField {
|
|
168
|
+
key: string;
|
|
169
|
+
label: string;
|
|
170
|
+
type: HelpPublicBlockFieldType;
|
|
171
|
+
required: boolean;
|
|
172
|
+
options: string[] | null;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* A building block the site defines itself — WHAT it is; how it looks is the
|
|
176
|
+
* website's own component. Written in Markdown as `:name[label]{…}` in running
|
|
177
|
+
* text (`inline`), `::name[label]{…}` alone on a line (`block`) or
|
|
178
|
+
* `:::name{…}` … `:::` around Markdown content (`container`). `text` is the
|
|
179
|
+
* template of its text version (`{key}` = a field's value, a `boolean` field's
|
|
180
|
+
* label when on; `{{`/`}}` = a brace) — what to show without a component.
|
|
181
|
+
*/
|
|
182
|
+
interface HelpPublicBlockDefinition {
|
|
183
|
+
/** The directive name (`app-button`). */
|
|
184
|
+
name: string;
|
|
185
|
+
/** Display name. */
|
|
186
|
+
label: string;
|
|
187
|
+
kind: "inline" | "block" | "container";
|
|
188
|
+
fields: HelpPublicBlockField[];
|
|
189
|
+
text: string;
|
|
155
190
|
}
|
|
156
191
|
/** A page in the navigation. */
|
|
157
192
|
interface HelpPublicTreePage {
|
|
@@ -170,16 +205,28 @@ interface HelpPublicTreeFolder {
|
|
|
170
205
|
/** Stable node id. */
|
|
171
206
|
id: string;
|
|
172
207
|
slug: string;
|
|
173
|
-
/** The folder's path (its index page, if any, lives here). */
|
|
208
|
+
/** The folder's path (its index page, if any, lives here; a group's is its parent's). */
|
|
174
209
|
path: string;
|
|
175
210
|
title: string;
|
|
211
|
+
/** A short line under a tab's name in the tab menu (Fumadocs `description`); null without one. */
|
|
212
|
+
description: string | null;
|
|
176
213
|
icon: string | null;
|
|
177
214
|
/** A tab (top level only; Fumadocs `root`). */
|
|
178
215
|
root: boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Adds no segment to its pages' paths (Fumadocs `(name)` folder): `path` is
|
|
218
|
+
* the parent's, a top-level group's index page is the home (`""`).
|
|
219
|
+
*/
|
|
220
|
+
group: boolean;
|
|
179
221
|
defaultOpen: boolean;
|
|
180
222
|
collapsible: boolean;
|
|
181
223
|
/** The folder's own page, shown at the folder's path; null without one. */
|
|
182
224
|
index: HelpPublicTreePage | null;
|
|
225
|
+
/**
|
|
226
|
+
* List `index` as the folder's first entry (after a section title it starts
|
|
227
|
+
* with); the folder's name then only opens it (Fumadocs: `index` in `pages`).
|
|
228
|
+
*/
|
|
229
|
+
listIndex: boolean;
|
|
183
230
|
children: HelpPublicTreeNode[];
|
|
184
231
|
}
|
|
185
232
|
/** A labelled divider between entries (Fumadocs `---Title---`). */
|
|
@@ -256,12 +303,17 @@ interface HelpPublicPageLink {
|
|
|
256
303
|
}
|
|
257
304
|
/** One page with its text. */
|
|
258
305
|
interface HelpPublicPage extends HelpPublicPageSummary {
|
|
259
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* The page's Markdown with directives (`:::callout`, `::::steps`, …, and the
|
|
308
|
+
* site's own blocks as written — see `site.blocks`).
|
|
309
|
+
*/
|
|
260
310
|
markdown: string;
|
|
261
311
|
/** Safe server-rendered HTML of `markdown` (stable `sd-*` classes for the directives). */
|
|
262
312
|
html: string;
|
|
263
313
|
headings: HelpPublicHeading[];
|
|
264
314
|
blocks: HelpPublicBlock[];
|
|
315
|
+
/** The site's own building blocks the page uses, by name (in the site's order). */
|
|
316
|
+
customBlocks: string[];
|
|
265
317
|
breadcrumbs: HelpPublicBreadcrumb[];
|
|
266
318
|
previous: HelpPublicPageLink | null;
|
|
267
319
|
next: HelpPublicPageLink | null;
|
|
@@ -424,4 +476,4 @@ declare function createHelpClient(options: HelpClientOptions): HelpClient;
|
|
|
424
476
|
*/
|
|
425
477
|
declare function verifyHelpWebhook(secret: string, rawBody: WebhookBody, signatureHeader: string | null | undefined, options?: VerifyWebhookOptions): Promise<HelpWebhookBody | null>;
|
|
426
478
|
|
|
427
|
-
export { type BaseClientOptions, type CachedResponse, DEFAULT_BASE_URL, EVENT_HEADER, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type GetPageParams, type HelpClient, type HelpClientOptions, type HelpPageResult, type HelpPublicBlock, type HelpPublicBreadcrumb, type HelpPublicHeading, type HelpPublicPage, type HelpPublicPageLink, type HelpPublicPagePayload, type HelpPublicPageSummary, type HelpPublicPagesPayload, type HelpPublicPreviewPage, type HelpPublicPreviewPayload, type HelpPublicRedirect, type HelpPublicSearchHit, type HelpPublicSearchPayload, type HelpPublicSite, type HelpPublicSitePayload, type HelpPublicTreeFolder, type HelpPublicTreeLink, type HelpPublicTreeNode, type HelpPublicTreePage, type HelpPublicTreeSeparator, type HelpWebhookBody, type HelpWebhookEvent, type LangParams, type RequestOptions, SIGNATURE_HEADER, type SearchParams, SolutionDeskError, type VerifyWebhookOptions, type WebhookBody, createHelpClient, createMemoryEtagCache, normalizeHelpPath, verifyHelpWebhook, verifyWebhookSignature };
|
|
479
|
+
export { type BaseClientOptions, type CachedResponse, DEFAULT_BASE_URL, EVENT_HEADER, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type GetPageParams, type HelpClient, type HelpClientOptions, type HelpPageResult, type HelpPublicBlock, type HelpPublicBlockDefinition, type HelpPublicBlockField, type HelpPublicBlockFieldType, type HelpPublicBreadcrumb, type HelpPublicHeading, type HelpPublicPage, type HelpPublicPageLink, type HelpPublicPagePayload, type HelpPublicPageSummary, type HelpPublicPagesPayload, type HelpPublicPreviewPage, type HelpPublicPreviewPayload, type HelpPublicRedirect, type HelpPublicSearchHit, type HelpPublicSearchPayload, type HelpPublicSite, type HelpPublicSitePayload, type HelpPublicTreeFolder, type HelpPublicTreeLink, type HelpPublicTreeNode, type HelpPublicTreePage, type HelpPublicTreeSeparator, type HelpWebhookBody, type HelpWebhookEvent, type LangParams, type RequestOptions, SIGNATURE_HEADER, type SearchParams, SolutionDeskError, type VerifyWebhookOptions, type WebhookBody, createHelpClient, createMemoryEtagCache, normalizeHelpPath, verifyHelpWebhook, verifyWebhookSignature };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solutiondesk/help",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-6f713c7",
|
|
4
4
|
"description": "Typed, zero-dependency client for the SolutionDesk public help center API, plus a Fumadocs adapter: navigation, pages, search, previews, llms.txt and webhook verification.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"solutiondesk",
|