handoff-app 1.1.2-0 → 1.1.3-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Changelog.md +44 -0
- package/dist/cli.js +1 -1
- package/dist/transformers/utils/handlebars.js +14 -7
- package/package.json +2 -1
- package/src/app/components/ComponentGuidelines.tsx +3 -1
- package/src/app/components/Markdown/MarkdownComponents.tsx +183 -50
- package/src/app/components/Navigation/AnchorNav.tsx +28 -28
- package/src/app/components/util/index.ts +93 -79
- package/src/app/css/index.css +4 -0
- package/src/app/pages/{[level1] → [...slug]}/index.tsx +24 -31
- package/src/app/pages/assets/fonts.tsx +3 -2
- package/src/app/pages/assets/index.tsx +3 -2
- package/src/app/pages/assets/logos.tsx +3 -2
- package/src/app/pages/foundations/colors.tsx +3 -2
- package/src/app/pages/foundations/effects.tsx +3 -2
- package/src/app/pages/foundations/icons/index.tsx +3 -2
- package/src/app/pages/foundations/index.tsx +3 -2
- package/src/app/pages/foundations/logo.tsx +3 -2
- package/src/app/pages/foundations/typography.tsx +3 -2
- package/src/app/pages/index.tsx +3 -2
- package/src/app/pages/system/component/[component]/index.tsx +3 -2
- package/src/app/pages/system/index.tsx +3 -2
- package/src/app/pages/system/tokens/foundations/colors.tsx +3 -2
- package/src/app/pages/system/tokens/foundations/effects.tsx +3 -2
- package/src/app/pages/system/tokens/foundations/typography.tsx +3 -2
- package/src/cli.ts +1 -1
- package/src/transformers/utils/handlebars.ts +13 -7
- package/src/app/pages/[level1]/[level2]/index.tsx +0 -104
package/Changelog.md
CHANGED
|
@@ -6,6 +6,50 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
6
6
|
and this project adheres to
|
|
7
7
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## \[1.1.3] - 2026-03-27
|
|
10
|
+
|
|
11
|
+
This release reworks the markdown page rendering system with four improvements:
|
|
12
|
+
n-level deep routing, automatic table-of-contents generation, standardized code
|
|
13
|
+
block styling, and GitHub Flavored Markdown table support.
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- **N-level deep markdown routing** — Replaced the fixed two-level routing
|
|
18
|
+
(`[level1]` / `[level1]/[level2]`) with a single `[...slug]` catch-all route
|
|
19
|
+
that supports markdown pages at any directory depth. A new recursive
|
|
20
|
+
`buildCatchAllStaticPaths` function walks `config/docs` and `pages`
|
|
21
|
+
directories to collect `.md` files at any nesting level. The sidebar menu now
|
|
22
|
+
also recursively scans directories to surface deeply nested pages.
|
|
23
|
+
|
|
24
|
+
- **Automatic table of contents** — The existing but unused `PageTOC` component
|
|
25
|
+
is now wired into the catch-all markdown page. It scans rendered headings
|
|
26
|
+
(h1–h6) via a `MutationObserver` and populates the right-hand "On This Page"
|
|
27
|
+
anchor nav automatically. Fixed a timing bug where headings were scanned
|
|
28
|
+
before the markdown content was in the DOM, and fixed anchor IDs that were
|
|
29
|
+
incorrectly prefixed with `#` (producing `##id` in URLs).
|
|
30
|
+
|
|
31
|
+
- **Standardized code block styling** — Markdown fenced code blocks now render
|
|
32
|
+
with a toolbar header matching the site's `CodeHighlight` component: a
|
|
33
|
+
language icon, filename or language label, and a copy-to-clipboard button. A
|
|
34
|
+
new `remarkCodeMeta` remark plugin preserves the fence info string so metadata
|
|
35
|
+
like `` ```bash filename="deploy.sh" `` is parsed and displayed. The Prism
|
|
36
|
+
theme respects dark mode via `next-themes`, and registered languages now
|
|
37
|
+
include bash, css, scss, jsx, tsx, typescript, yaml, handlebars, and more.
|
|
38
|
+
|
|
39
|
+
- **Markdown table support** — Added `remark-gfm` to all `ReactMarkdown`
|
|
40
|
+
instances across the codebase (17 files). Pipe tables, strikethrough,
|
|
41
|
+
autolinks, and task lists now render correctly. Tables are styled via Tailwind
|
|
42
|
+
Typography's `.prose table` defaults.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- Deleted `src/app/pages/[level1]/index.tsx` and
|
|
47
|
+
`src/app/pages/[level1]/[level2]/index.tsx` (replaced by `[...slug]`).
|
|
48
|
+
- Removed `buildL1StaticPaths`, `buildL2StaticPaths`, and the `SubPageType`
|
|
49
|
+
interface from `src/app/components/util/index.ts`.
|
|
50
|
+
- `ComponentGuidelines` now uses `MarkdownComponents` and the remark plugins
|
|
51
|
+
for consistent rendering.
|
|
52
|
+
|
|
9
53
|
## \[1.1.2] - 2026-03-26
|
|
10
54
|
|
|
11
55
|
This release improves integration with downstream platforms (for example HubSpot) by letting
|
package/dist/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ const registerHandlebarsHelpers = (data, injectFieldWrappers, extend) => {
|
|
|
17
17
|
handlebars_1.default.registerHelper('field', function (field, options) {
|
|
18
18
|
if (injectFieldWrappers) {
|
|
19
19
|
if (!field) {
|
|
20
|
-
// This
|
|
20
|
+
// This debugging isn't helpful in the context of the component library
|
|
21
21
|
// Logger.error(`Missing field declaration for ${data.id}`);
|
|
22
22
|
return options.fn(this);
|
|
23
23
|
}
|
|
@@ -44,12 +44,19 @@ const registerHandlebarsHelpers = (data, injectFieldWrappers, extend) => {
|
|
|
44
44
|
handlebars_1.default.registerHelper('eq', function (a, b) {
|
|
45
45
|
return a === b;
|
|
46
46
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
if (extend) {
|
|
48
|
+
try {
|
|
49
|
+
extend({
|
|
50
|
+
handlebars: handlebars_1.default,
|
|
51
|
+
componentId: data.id,
|
|
52
|
+
properties: data.properties,
|
|
53
|
+
injectFieldWrappers,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
logger_1.Logger.error(`registerHandlebarsHelpers hook failed for ${data.id}`, err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
53
60
|
};
|
|
54
61
|
exports.registerHandlebarsHelpers = registerHandlebarsHelpers;
|
|
55
62
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "handoff-app",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3-0",
|
|
4
4
|
"description": "Automated documentation toolchain for building client side documentation from figma",
|
|
5
5
|
"author ": {
|
|
6
6
|
"name": "Convertiv",
|
|
@@ -109,6 +109,7 @@
|
|
|
109
109
|
"react-scroll": "^1.8.9",
|
|
110
110
|
"react-syntax-highlighter": "^16.1.0",
|
|
111
111
|
"rehype-raw": "^7.0.0",
|
|
112
|
+
"remark-gfm": "^4.0.1",
|
|
112
113
|
"sass": "^1.85.0",
|
|
113
114
|
"tailwind-merge": "^2.5.5",
|
|
114
115
|
"tailwindcss": "^4.1.14",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import ReactMarkdown from 'react-markdown';
|
|
3
3
|
import rehypeRaw from 'rehype-raw';
|
|
4
|
+
import remarkGfm from 'remark-gfm';
|
|
5
|
+
import { MarkdownComponents, remarkCodeMeta } from './Markdown/MarkdownComponents';
|
|
4
6
|
|
|
5
7
|
export interface ComponentGuidelinesProps {
|
|
6
8
|
content: string;
|
|
@@ -10,7 +12,7 @@ export const ComponentGuidelines: React.FC<ComponentGuidelinesProps> = ({ conten
|
|
|
10
12
|
return (
|
|
11
13
|
<>
|
|
12
14
|
<div className="prose">
|
|
13
|
-
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{content}</ReactMarkdown>
|
|
15
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>{content}</ReactMarkdown>
|
|
14
16
|
</div>
|
|
15
17
|
</>
|
|
16
18
|
);
|
|
@@ -1,23 +1,56 @@
|
|
|
1
|
+
import { useTheme } from 'next-themes';
|
|
1
2
|
import React, { ReactElement } from 'react';
|
|
3
|
+
import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash';
|
|
4
|
+
import css from 'react-syntax-highlighter/dist/esm/languages/prism/css';
|
|
5
|
+
import handlebars from 'react-syntax-highlighter/dist/esm/languages/prism/handlebars';
|
|
2
6
|
import js from 'react-syntax-highlighter/dist/esm/languages/prism/javascript';
|
|
3
7
|
import json from 'react-syntax-highlighter/dist/esm/languages/prism/json';
|
|
8
|
+
import jsx from 'react-syntax-highlighter/dist/esm/languages/prism/jsx';
|
|
4
9
|
import markdown from 'react-syntax-highlighter/dist/esm/languages/prism/markdown';
|
|
5
10
|
import sass from 'react-syntax-highlighter/dist/esm/languages/prism/scss';
|
|
6
|
-
import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/
|
|
11
|
+
import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/tsx';
|
|
12
|
+
import typescript from 'react-syntax-highlighter/dist/esm/languages/prism/typescript';
|
|
7
13
|
import html from 'react-syntax-highlighter/dist/esm/languages/prism/xml-doc';
|
|
14
|
+
import yaml from 'react-syntax-highlighter/dist/esm/languages/prism/yaml';
|
|
8
15
|
import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light';
|
|
9
|
-
import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
16
|
+
import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
17
|
+
import CopyCode from '../CopyCode';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
Braces,
|
|
21
|
+
Code,
|
|
22
|
+
FileCode2,
|
|
23
|
+
FileText,
|
|
24
|
+
FileType,
|
|
25
|
+
Terminal,
|
|
26
|
+
} from 'lucide-react';
|
|
27
|
+
|
|
28
|
+
SyntaxHighlighter.registerLanguage('bash', bash);
|
|
29
|
+
SyntaxHighlighter.registerLanguage('sh', bash);
|
|
30
|
+
SyntaxHighlighter.registerLanguage('shell', bash);
|
|
31
|
+
SyntaxHighlighter.registerLanguage('zsh', bash);
|
|
32
|
+
SyntaxHighlighter.registerLanguage('css', css);
|
|
33
|
+
SyntaxHighlighter.registerLanguage('handlebars', handlebars);
|
|
34
|
+
SyntaxHighlighter.registerLanguage('hbs', handlebars);
|
|
35
|
+
SyntaxHighlighter.registerLanguage('javascript', js);
|
|
10
36
|
SyntaxHighlighter.registerLanguage('js', js);
|
|
11
37
|
SyntaxHighlighter.registerLanguage('json', json);
|
|
38
|
+
SyntaxHighlighter.registerLanguage('jsx', jsx);
|
|
12
39
|
SyntaxHighlighter.registerLanguage('markdown', markdown);
|
|
40
|
+
SyntaxHighlighter.registerLanguage('md', markdown);
|
|
13
41
|
SyntaxHighlighter.registerLanguage('sass', sass);
|
|
42
|
+
SyntaxHighlighter.registerLanguage('scss', sass);
|
|
14
43
|
SyntaxHighlighter.registerLanguage('tsx', tsx);
|
|
44
|
+
SyntaxHighlighter.registerLanguage('typescript', typescript);
|
|
45
|
+
SyntaxHighlighter.registerLanguage('ts', typescript);
|
|
15
46
|
SyntaxHighlighter.registerLanguage('html', html);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
47
|
+
SyntaxHighlighter.registerLanguage('xml', html);
|
|
48
|
+
SyntaxHighlighter.registerLanguage('yaml', yaml);
|
|
49
|
+
SyntaxHighlighter.registerLanguage('yml', yaml);
|
|
50
|
+
|
|
19
51
|
export type CustomRenderers = {
|
|
20
52
|
code: (props: any) => ReactElement;
|
|
53
|
+
pre: (props: any) => ReactElement;
|
|
21
54
|
h1: (props: any) => ReactElement;
|
|
22
55
|
h2: (props: any) => ReactElement;
|
|
23
56
|
h3: (props: any) => ReactElement;
|
|
@@ -27,32 +60,27 @@ export type CustomRenderers = {
|
|
|
27
60
|
paragraph: (props: { children: React.ReactNode }) => ReactElement;
|
|
28
61
|
};
|
|
29
62
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
63
|
+
function extractText(node: any): string {
|
|
64
|
+
if (typeof node === 'string') return node;
|
|
65
|
+
if (typeof node === 'number') return String(node);
|
|
66
|
+
if (Array.isArray(node)) return node.map(extractText).join('');
|
|
67
|
+
if (node?.props?.children) return extractText(node.props.children);
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const Headings = (element: any) => {
|
|
36
72
|
const { children, node } = element;
|
|
37
|
-
// Access actual (string) value of heading
|
|
38
73
|
if (children[0]) {
|
|
39
|
-
const heading = children[0];
|
|
40
74
|
const type = node.tagName || 'h6';
|
|
41
75
|
|
|
42
|
-
|
|
43
|
-
let anchor = heading.toString().toLowerCase();
|
|
44
|
-
|
|
45
|
-
// Clean anchor (replace special characters whitespaces).
|
|
46
|
-
// Alternatively, use encodeURIComponent() if you don't care about
|
|
47
|
-
// pretty anchor links
|
|
76
|
+
let anchor = extractText(children).toLowerCase();
|
|
48
77
|
anchor = anchor.replace(/[^a-zA-Z0-9 ]/g, '');
|
|
49
78
|
anchor = anchor.replace(/ /g, '-');
|
|
50
79
|
|
|
51
|
-
// Utility
|
|
52
80
|
const container = (children: React.ReactNode): React.ReactNode => (
|
|
53
81
|
<>
|
|
54
82
|
{children}
|
|
55
|
-
<a id={anchor} href={`#${anchor}`} className="doc-link"
|
|
83
|
+
<a id={anchor} href={`#${anchor}`} className="doc-link"></a>
|
|
56
84
|
</>
|
|
57
85
|
);
|
|
58
86
|
|
|
@@ -69,22 +97,147 @@ const Headings = (element) => {
|
|
|
69
97
|
return <h5 className="text-sm font-bold">{container(children)}</h5>;
|
|
70
98
|
case 'h6':
|
|
71
99
|
return <h6 className="text-xs font-bold">{container(children)}</h6>;
|
|
72
|
-
|
|
73
100
|
default:
|
|
74
|
-
return (
|
|
75
|
-
<h6 className="text-xs font-bold">
|
|
76
|
-
{JSON.stringify(type)}: {container(children)}
|
|
77
|
-
</h6>
|
|
78
|
-
);
|
|
101
|
+
return <h6 className="text-xs font-bold">{container(children)}</h6>;
|
|
79
102
|
}
|
|
80
103
|
} else {
|
|
81
104
|
return <h1>children</h1>;
|
|
82
105
|
}
|
|
83
106
|
};
|
|
84
107
|
|
|
108
|
+
const LANGUAGE_ICONS: Record<string, React.FC<{ className?: string }>> = {
|
|
109
|
+
bash: Terminal,
|
|
110
|
+
sh: Terminal,
|
|
111
|
+
shell: Terminal,
|
|
112
|
+
zsh: Terminal,
|
|
113
|
+
js: FileCode2,
|
|
114
|
+
javascript: FileCode2,
|
|
115
|
+
ts: FileCode2,
|
|
116
|
+
typescript: FileCode2,
|
|
117
|
+
jsx: FileCode2,
|
|
118
|
+
tsx: FileCode2,
|
|
119
|
+
css: FileType,
|
|
120
|
+
scss: FileType,
|
|
121
|
+
sass: FileType,
|
|
122
|
+
html: FileCode2,
|
|
123
|
+
xml: FileCode2,
|
|
124
|
+
json: Braces,
|
|
125
|
+
yaml: FileText,
|
|
126
|
+
yml: FileText,
|
|
127
|
+
markdown: FileText,
|
|
128
|
+
md: FileText,
|
|
129
|
+
handlebars: Code,
|
|
130
|
+
hbs: Code,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const LANGUAGE_LABELS: Record<string, string> = {
|
|
134
|
+
bash: 'Bash',
|
|
135
|
+
sh: 'Shell',
|
|
136
|
+
shell: 'Shell',
|
|
137
|
+
zsh: 'Zsh',
|
|
138
|
+
js: 'JavaScript',
|
|
139
|
+
javascript: 'JavaScript',
|
|
140
|
+
ts: 'TypeScript',
|
|
141
|
+
typescript: 'TypeScript',
|
|
142
|
+
jsx: 'JSX',
|
|
143
|
+
tsx: 'TSX',
|
|
144
|
+
css: 'CSS',
|
|
145
|
+
scss: 'SCSS',
|
|
146
|
+
sass: 'SASS',
|
|
147
|
+
html: 'HTML',
|
|
148
|
+
xml: 'XML',
|
|
149
|
+
json: 'JSON',
|
|
150
|
+
yaml: 'YAML',
|
|
151
|
+
yml: 'YAML',
|
|
152
|
+
markdown: 'Markdown',
|
|
153
|
+
md: 'Markdown',
|
|
154
|
+
handlebars: 'Handlebars',
|
|
155
|
+
hbs: 'Handlebars',
|
|
156
|
+
text: 'Code',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
function parseMeta(meta: string | undefined): Record<string, string> {
|
|
160
|
+
if (!meta) return {};
|
|
161
|
+
const result: Record<string, string> = {};
|
|
162
|
+
const regex = /(\w+)="([^"]+)"/g;
|
|
163
|
+
let match;
|
|
164
|
+
while ((match = regex.exec(meta)) !== null) {
|
|
165
|
+
result[match[1]] = match[2];
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function MarkdownCodeBlock(props: any) {
|
|
171
|
+
const { className, children, node, 'data-meta': dataMeta, ...rest } = props;
|
|
172
|
+
const { resolvedTheme } = useTheme();
|
|
173
|
+
const isDark = resolvedTheme === 'dark';
|
|
174
|
+
|
|
175
|
+
const langMatch = /language-(\w+)/.exec(className || '');
|
|
176
|
+
const isBlock = String(children).includes('\n');
|
|
177
|
+
|
|
178
|
+
if (!langMatch && !isBlock) {
|
|
179
|
+
return <code className={className} {...rest}>{children}</code>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const lang = langMatch ? langMatch[1] : 'text';
|
|
183
|
+
const meta = parseMeta(dataMeta);
|
|
184
|
+
const filename = meta.filename;
|
|
185
|
+
const codeString = String(children).replace(/\n$/, '');
|
|
186
|
+
const IconComponent = LANGUAGE_ICONS[lang] || Code;
|
|
187
|
+
const langLabel = LANGUAGE_LABELS[lang] || lang;
|
|
188
|
+
|
|
189
|
+
const theme = isDark ? { ...oneDark } : { ...oneLight };
|
|
190
|
+
theme['pre[class*="language-"]'] = {
|
|
191
|
+
...theme['pre[class*="language-"]'],
|
|
192
|
+
overflow: 'auto',
|
|
193
|
+
margin: '0',
|
|
194
|
+
borderRadius: '0 0 0.5rem 0.5rem',
|
|
195
|
+
background: isDark ? '#1e1e1e' : '#fafafa',
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div className="not-prose my-4 min-w-0 max-w-full overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
|
200
|
+
<div className="flex items-center justify-between bg-gray-50 px-4 py-2 dark:bg-gray-800">
|
|
201
|
+
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
|
202
|
+
<IconComponent className="h-4 w-4" />
|
|
203
|
+
<span>{filename || langLabel}</span>
|
|
204
|
+
</div>
|
|
205
|
+
<CopyCode code={codeString} />
|
|
206
|
+
</div>
|
|
207
|
+
<SyntaxHighlighter
|
|
208
|
+
style={theme}
|
|
209
|
+
language={lang}
|
|
210
|
+
PreTag="div"
|
|
211
|
+
showLineNumbers={lang !== 'text'}
|
|
212
|
+
wrapLines={true}
|
|
213
|
+
useInlineStyles={true}
|
|
214
|
+
>
|
|
215
|
+
{codeString}
|
|
216
|
+
</SyntaxHighlighter>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
85
221
|
/**
|
|
86
|
-
*
|
|
222
|
+
* Remark plugin that preserves the code fence meta string
|
|
223
|
+
* (the text after the language identifier) as a hast data attribute.
|
|
87
224
|
*/
|
|
225
|
+
export function remarkCodeMeta() {
|
|
226
|
+
return (tree: any) => {
|
|
227
|
+
const walk = (node: any) => {
|
|
228
|
+
if (node.type === 'code' && node.meta) {
|
|
229
|
+
node.data = node.data || {};
|
|
230
|
+
node.data.hProperties = node.data.hProperties || {};
|
|
231
|
+
node.data.hProperties['data-meta'] = node.meta;
|
|
232
|
+
}
|
|
233
|
+
if (node.children) {
|
|
234
|
+
node.children.forEach(walk);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
walk(tree);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
88
241
|
export const MarkdownComponents: CustomRenderers = {
|
|
89
242
|
h1: Headings,
|
|
90
243
|
h2: Headings,
|
|
@@ -93,28 +246,8 @@ export const MarkdownComponents: CustomRenderers = {
|
|
|
93
246
|
h5: Headings,
|
|
94
247
|
h6: Headings,
|
|
95
248
|
paragraph: ({ children }) => <p>{children}</p>,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const match = /language-(\w+)/.exec(className || '');
|
|
99
|
-
// This was designed to flatten out children arrays, but I think its not needed
|
|
100
|
-
// if (props.children[0]) {
|
|
101
|
-
// props.children[0] = props.children[0].toString().trim();
|
|
102
|
-
// }
|
|
103
|
-
return match ? (
|
|
104
|
-
<SyntaxHighlighter
|
|
105
|
-
// @ts-ignore
|
|
106
|
-
style={oneLight}
|
|
107
|
-
language={match[1]}
|
|
108
|
-
PreTag="div"
|
|
109
|
-
showLineNumbers={true}
|
|
110
|
-
wrapLines={true}
|
|
111
|
-
useInlineStyles={true}
|
|
112
|
-
{...props}
|
|
113
|
-
></SyntaxHighlighter>
|
|
114
|
-
) : (
|
|
115
|
-
<code {...props} />
|
|
116
|
-
);
|
|
117
|
-
},
|
|
249
|
+
pre: ({ children }) => <>{children}</>,
|
|
250
|
+
code: MarkdownCodeBlock,
|
|
118
251
|
};
|
|
119
252
|
|
|
120
253
|
export default Headings;
|
|
@@ -18,45 +18,45 @@ interface TOCProps {
|
|
|
18
18
|
export function PageTOC({ body, title }: TOCProps) {
|
|
19
19
|
const [headers, setHeaders] = React.useState<{ id: string; title: string | null; level: number }[]>([]);
|
|
20
20
|
const router = useRouter();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
setHeaders(headers);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
const headersCallback = React.useCallback(getHeaders, [body]);
|
|
21
|
+
|
|
22
|
+
const scanHeaders = React.useCallback(() => {
|
|
23
|
+
if (!body.current) return;
|
|
24
|
+
const found = Array.from(body.current.querySelectorAll('h1, h2, h3, h4, h5, h6')).map((node) => ({
|
|
25
|
+
id: anchorSlugify(node?.textContent?.toString() ?? ''),
|
|
26
|
+
title: node.textContent,
|
|
27
|
+
level: parseInt(node.tagName[1]),
|
|
28
|
+
}));
|
|
29
|
+
setHeaders(found);
|
|
30
|
+
}, [body]);
|
|
35
31
|
|
|
36
32
|
useEffect(() => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
};
|
|
45
|
-
|
|
33
|
+
// Scan after paint to catch SSR-hydrated content
|
|
34
|
+
requestAnimationFrame(scanHeaders);
|
|
35
|
+
|
|
36
|
+
// Watch for dynamically inserted headings (client-rendered markdown)
|
|
37
|
+
const el = body.current;
|
|
38
|
+
if (!el) return;
|
|
39
|
+
const observer = new MutationObserver(scanHeaders);
|
|
40
|
+
observer.observe(el, { childList: true, subtree: true });
|
|
41
|
+
return () => observer.disconnect();
|
|
42
|
+
}, [body, scanHeaders]);
|
|
43
|
+
|
|
46
44
|
useEffect(() => {
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
const handler = () => requestAnimationFrame(scanHeaders);
|
|
46
|
+
router.events.on('routeChangeComplete', handler);
|
|
47
|
+
return () => router.events.off('routeChangeComplete', handler);
|
|
48
|
+
}, [router, scanHeaders]);
|
|
49
49
|
return (
|
|
50
50
|
<AnchorNav
|
|
51
51
|
title={title}
|
|
52
52
|
groups={headers.reduce((acc, header) => {
|
|
53
53
|
if (header.level === 1) {
|
|
54
|
-
acc.push({ [
|
|
54
|
+
acc.push({ [header.id]: header.title ?? '' });
|
|
55
55
|
} else {
|
|
56
56
|
if (acc.length === 0) {
|
|
57
|
-
acc.push({ [
|
|
57
|
+
acc.push({ [header.id]: header.title ?? '' });
|
|
58
58
|
} else {
|
|
59
|
-
acc[acc.length - 1][
|
|
59
|
+
acc[acc.length - 1][header.id] = header.title ?? '';
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
return acc;
|