@dominikcz/greg 0.9.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +397 -0
  2. package/bin/greg.js +241 -0
  3. package/bin/init.js +351 -0
  4. package/bin/templates/docs/getting-started.md +47 -0
  5. package/bin/templates/docs/index.md +11 -0
  6. package/bin/templates/greg.config.js +39 -0
  7. package/bin/templates/greg.config.ts +38 -0
  8. package/bin/templates/index.html +16 -0
  9. package/bin/templates/src/App.svelte +5 -0
  10. package/bin/templates/src/app.css +20 -0
  11. package/bin/templates/src/main.js +9 -0
  12. package/bin/templates/svelte.config.js +1 -0
  13. package/bin/templates/tsconfig.json +21 -0
  14. package/bin/templates/vite.config.js +23 -0
  15. package/docs/__partials/markdown/examples/basic.md +4 -0
  16. package/docs/__partials/markdown/examples/diff.md +10 -0
  17. package/docs/__partials/markdown/examples/focus.md +5 -0
  18. package/docs/__partials/markdown/examples/language-title.md +3 -0
  19. package/docs/__partials/markdown/examples/line-highlighting.md +5 -0
  20. package/docs/__partials/markdown/examples/line-numbers.md +5 -0
  21. package/docs/__partials/note.md +4 -0
  22. package/docs/guide/__shared-warning.md +4 -0
  23. package/docs/guide/asset-handling.md +88 -0
  24. package/docs/guide/deploying.md +162 -0
  25. package/docs/guide/getting-started.md +334 -0
  26. package/docs/guide/index.md +23 -0
  27. package/docs/guide/localization.md +290 -0
  28. package/docs/guide/markdown/code.md +95 -0
  29. package/docs/guide/markdown/components-and-mermaid.md +43 -0
  30. package/docs/guide/markdown/containers.md +110 -0
  31. package/docs/guide/markdown/header-anchors.md +34 -0
  32. package/docs/guide/markdown/includes.md +84 -0
  33. package/docs/guide/markdown/index.md +20 -0
  34. package/docs/guide/markdown/inline-attributes.md +21 -0
  35. package/docs/guide/markdown/links-and-toc.md +64 -0
  36. package/docs/guide/markdown/math.md +54 -0
  37. package/docs/guide/markdown/syntax-highlighting.md +75 -0
  38. package/docs/guide/routing.md +150 -0
  39. package/docs/guide/using-svelte.md +88 -0
  40. package/docs/guide/versioning.md +281 -0
  41. package/docs/incompatibilities.md +48 -0
  42. package/docs/index.md +43 -0
  43. package/docs/reference/badge.md +100 -0
  44. package/docs/reference/carbon-ads.md +46 -0
  45. package/docs/reference/code-group.md +126 -0
  46. package/docs/reference/home-page.md +232 -0
  47. package/docs/reference/index.md +18 -0
  48. package/docs/reference/markdowndocs.md +275 -0
  49. package/docs/reference/outline.md +79 -0
  50. package/docs/reference/search.md +263 -0
  51. package/docs/reference/steps.md +200 -0
  52. package/docs/reference/team-page.md +189 -0
  53. package/docs/reference/theme.md +150 -0
  54. package/fakeDocsGenerator/generate_docs.js +310 -0
  55. package/package.json +92 -0
  56. package/scripts/build-versions.js +609 -0
  57. package/scripts/generate-static.js +79 -0
  58. package/scripts/render-markdown.js +420 -0
  59. package/src/lib/MarkdownDocs/AiChat.svelte +936 -0
  60. package/src/lib/MarkdownDocs/BackToTop.svelte +68 -0
  61. package/src/lib/MarkdownDocs/Breadcrumb.svelte +68 -0
  62. package/src/lib/MarkdownDocs/DocsNavigation.svelte +149 -0
  63. package/src/lib/MarkdownDocs/DocsSiteHeader.svelte +758 -0
  64. package/src/lib/MarkdownDocs/DocsVersionSwitcher.svelte +103 -0
  65. package/src/lib/MarkdownDocs/MarkdownDocs.svelte +2115 -0
  66. package/src/lib/MarkdownDocs/MarkdownRenderer.svelte +487 -0
  67. package/src/lib/MarkdownDocs/Outline.svelte +238 -0
  68. package/src/lib/MarkdownDocs/PrevNext.svelte +115 -0
  69. package/src/lib/MarkdownDocs/SearchModal.svelte +1241 -0
  70. package/src/lib/MarkdownDocs/TreeView.svelte +32 -0
  71. package/src/lib/MarkdownDocs/TreeViewItem.svelte +219 -0
  72. package/src/lib/MarkdownDocs/VersionOutdatedNotice.svelte +72 -0
  73. package/src/lib/MarkdownDocs/__tests__/codeDirectives.test.js +54 -0
  74. package/src/lib/MarkdownDocs/__tests__/common.test.js +41 -0
  75. package/src/lib/MarkdownDocs/__tests__/docsExamplesLint.test.js +77 -0
  76. package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/__partial-basic.md +3 -0
  77. package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/snippet.js +9 -0
  78. package/src/lib/MarkdownDocs/__tests__/fixtures/includes/part.md +11 -0
  79. package/src/lib/MarkdownDocs/__tests__/fixtures/includes/wrapper.md +5 -0
  80. package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.js +8 -0
  81. package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.md +5 -0
  82. package/src/lib/MarkdownDocs/__tests__/helpers.js +67 -0
  83. package/src/lib/MarkdownDocs/__tests__/localeUtils.test.js +204 -0
  84. package/src/lib/MarkdownDocs/__tests__/markdown.test.js +704 -0
  85. package/src/lib/MarkdownDocs/__tests__/markdownRendererRuntime.test.js +65 -0
  86. package/src/lib/MarkdownDocs/__tests__/searchIndexBuilder.test.js +117 -0
  87. package/src/lib/MarkdownDocs/__tests__/sqliteStore.test.js +202 -0
  88. package/src/lib/MarkdownDocs/__tests__/useRouter.test.js +16 -0
  89. package/src/lib/MarkdownDocs/ai/adapters/customAdapter.js +14 -0
  90. package/src/lib/MarkdownDocs/ai/adapters/customAdapter.ts +43 -0
  91. package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.js +81 -0
  92. package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.ts +116 -0
  93. package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.js +92 -0
  94. package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.ts +137 -0
  95. package/src/lib/MarkdownDocs/ai/aiProvider.ts +31 -0
  96. package/src/lib/MarkdownDocs/ai/characters.js +52 -0
  97. package/src/lib/MarkdownDocs/ai/characters.ts +69 -0
  98. package/src/lib/MarkdownDocs/ai/chunkStore.ts +25 -0
  99. package/src/lib/MarkdownDocs/ai/chunker.js +85 -0
  100. package/src/lib/MarkdownDocs/ai/chunker.ts +135 -0
  101. package/src/lib/MarkdownDocs/ai/docLinker.js +26 -0
  102. package/src/lib/MarkdownDocs/ai/docLinker.ts +36 -0
  103. package/src/lib/MarkdownDocs/ai/promptBuilder.js +33 -0
  104. package/src/lib/MarkdownDocs/ai/promptBuilder.ts +53 -0
  105. package/src/lib/MarkdownDocs/ai/ragPipeline.js +54 -0
  106. package/src/lib/MarkdownDocs/ai/ragPipeline.ts +106 -0
  107. package/src/lib/MarkdownDocs/ai/stores/memoryStore.js +88 -0
  108. package/src/lib/MarkdownDocs/ai/stores/memoryStore.ts +112 -0
  109. package/src/lib/MarkdownDocs/ai/stores/sqliteStore.ts +372 -0
  110. package/src/lib/MarkdownDocs/ai/types.ts +71 -0
  111. package/src/lib/MarkdownDocs/aiServer.js +288 -0
  112. package/src/lib/MarkdownDocs/codeDirectives.js +191 -0
  113. package/src/lib/MarkdownDocs/codeFenceInfo.js +45 -0
  114. package/src/lib/MarkdownDocs/codeGroup.ts +46 -0
  115. package/src/lib/MarkdownDocs/common.ts +47 -0
  116. package/src/lib/MarkdownDocs/docsUtils.js +281 -0
  117. package/src/lib/MarkdownDocs/index.plugins.js +22 -0
  118. package/src/lib/MarkdownDocs/layouts/LayoutDoc.svelte +8 -0
  119. package/src/lib/MarkdownDocs/layouts/LayoutHome.svelte +58 -0
  120. package/src/lib/MarkdownDocs/layouts/LayoutPage.svelte +9 -0
  121. package/src/lib/MarkdownDocs/loadGregConfig.js +82 -0
  122. package/src/lib/MarkdownDocs/localeUtils.ts +682 -0
  123. package/src/lib/MarkdownDocs/markdownRendererRuntime.ts +314 -0
  124. package/src/lib/MarkdownDocs/mermaidThemes.js +319 -0
  125. package/src/lib/MarkdownDocs/navigationUtils.js +22 -0
  126. package/src/lib/MarkdownDocs/rehypeCodeGroup.js +326 -0
  127. package/src/lib/MarkdownDocs/rehypeCodeTitle.js +96 -0
  128. package/src/lib/MarkdownDocs/rehypeToc.js +170 -0
  129. package/src/lib/MarkdownDocs/remarkCodeMeta.js +22 -0
  130. package/src/lib/MarkdownDocs/remarkContainers.js +329 -0
  131. package/src/lib/MarkdownDocs/remarkCustomAnchors.js +42 -0
  132. package/src/lib/MarkdownDocs/remarkEscapeSvelte.js +33 -0
  133. package/src/lib/MarkdownDocs/remarkGlobalComponents.js +65 -0
  134. package/src/lib/MarkdownDocs/remarkImports.js +461 -0
  135. package/src/lib/MarkdownDocs/remarkImportsBrowser.js +349 -0
  136. package/src/lib/MarkdownDocs/remarkInlineAttrs.js +95 -0
  137. package/src/lib/MarkdownDocs/remarkMathToHtml.js +138 -0
  138. package/src/lib/MarkdownDocs/searchIndexBuilder.js +497 -0
  139. package/src/lib/MarkdownDocs/searchServer.js +263 -0
  140. package/src/lib/MarkdownDocs/treeViewTypes.ts +11 -0
  141. package/src/lib/MarkdownDocs/useRouter.svelte.ts +114 -0
  142. package/src/lib/MarkdownDocs/useSplitter.svelte.ts +33 -0
  143. package/src/lib/MarkdownDocs/versioningDefaults.js +20 -0
  144. package/src/lib/MarkdownDocs/vitePluginAiServer.js +204 -0
  145. package/src/lib/MarkdownDocs/vitePluginCopyDocs.js +153 -0
  146. package/src/lib/MarkdownDocs/vitePluginFrontmatter.js +109 -0
  147. package/src/lib/MarkdownDocs/vitePluginGregConfig.js +108 -0
  148. package/src/lib/MarkdownDocs/vitePluginSearchIndex.js +57 -0
  149. package/src/lib/MarkdownDocs/vitePluginSearchServer.js +190 -0
  150. package/src/lib/components/Badge.svelte +59 -0
  151. package/src/lib/components/Button.svelte +138 -0
  152. package/src/lib/components/CarbonAds.svelte +99 -0
  153. package/src/lib/components/CodeGroup.svelte +102 -0
  154. package/src/lib/components/Feature.svelte +209 -0
  155. package/src/lib/components/Features.svelte +123 -0
  156. package/src/lib/components/Hero.svelte +399 -0
  157. package/src/lib/components/Image.svelte +128 -0
  158. package/src/lib/components/Link.svelte +105 -0
  159. package/src/lib/components/SocialLink.svelte +84 -0
  160. package/src/lib/components/SocialLinks.svelte +33 -0
  161. package/src/lib/components/Steps.svelte +143 -0
  162. package/src/lib/components/TeamMember.svelte +273 -0
  163. package/src/lib/components/TeamMembers.svelte +81 -0
  164. package/src/lib/components/TeamPage.svelte +65 -0
  165. package/src/lib/components/TeamPageSection.svelte +108 -0
  166. package/src/lib/components/TeamPageTitle.svelte +89 -0
  167. package/src/lib/components/index.js +24 -0
  168. package/src/lib/portal/context.js +12 -0
  169. package/src/lib/portal/index.js +3 -0
  170. package/src/lib/portal/portal.svelte +14 -0
  171. package/src/lib/portal/slot.svelte +8 -0
  172. package/src/lib/scss/__code.scss +128 -0
  173. package/src/lib/scss/__containers.scss +99 -0
  174. package/src/lib/scss/__markdown.scss +447 -0
  175. package/src/lib/scss/__scrollbar.scss +60 -0
  176. package/src/lib/scss/__steps.scss +100 -0
  177. package/src/lib/scss/__theme.scss +238 -0
  178. package/src/lib/scss/__toc.scss +55 -0
  179. package/src/lib/scss/__utilities.scss +7 -0
  180. package/src/lib/scss/greg.scss +9 -0
  181. package/src/lib/spinner/spinner.svelte +42 -0
  182. package/svelte.config.js +146 -0
  183. package/types/index.d.ts +456 -0
@@ -0,0 +1,326 @@
1
+ import { visit, SKIP } from 'unist-util-visit';
2
+
3
+ const START_DELIMITER_REGEX = /^::: code-group(?:\s+labels=\[([^\]]+)\])?$/;
4
+ const END_DELIMITER = ':::';
5
+
6
+ const languageLabelMap = {
7
+ javascript: 'js',
8
+ typescript: 'ts',
9
+ bash: 'sh',
10
+ shell: 'sh',
11
+ sh: 'sh',
12
+ };
13
+
14
+ function getNodeText(node) {
15
+ if (!node) return '';
16
+ if (node.type === 'text') return node.value ?? '';
17
+ if (!node.children) return '';
18
+ return node.children.map(getNodeText).join('');
19
+ }
20
+
21
+ function isHeadingElement(node) {
22
+ return node?.type === 'element' && /^h[1-6]$/i.test(String(node.tagName ?? ''));
23
+ }
24
+
25
+ function isCodeGroupStartNode(node) {
26
+ if (node?.type !== 'element' || node.tagName !== 'p') return false;
27
+ return START_DELIMITER_REGEX.test(getNodeText(node).trim());
28
+ }
29
+
30
+ function isEndDelimiterNode(node) {
31
+ if (!node) return false;
32
+ if (node.type === 'element' && node.tagName === 'p') {
33
+ return getNodeText(node).trim() === END_DELIMITER;
34
+ }
35
+ if (node.type === 'raw' || node.type === 'text') {
36
+ return String(node.value ?? '').trim() === END_DELIMITER;
37
+ }
38
+ return false;
39
+ }
40
+
41
+ function splitNodeWithAttachedEndDelimiter(node) {
42
+ if (!node) return null;
43
+
44
+ let kind = null;
45
+ let value = '';
46
+
47
+ if (node.type === 'raw' || node.type === 'text') {
48
+ kind = node.type;
49
+ value = String(node.value ?? '');
50
+ } else if (
51
+ node.type === 'element' &&
52
+ node.tagName === 'p' &&
53
+ Array.isArray(node.children) &&
54
+ node.children.every((child) => child?.type === 'text')
55
+ ) {
56
+ kind = 'p-text';
57
+ value = node.children.map((child) => String(child.value ?? '')).join('');
58
+ }
59
+
60
+ if (!kind) return null;
61
+
62
+ const match = value.match(/^(.*)\r?\n\s*:::\s*$/s);
63
+ if (!match) return null;
64
+
65
+ const prefix = match[1] ?? '';
66
+ const delimiterNode = {
67
+ type: 'element',
68
+ tagName: 'p',
69
+ properties: {},
70
+ children: [{ type: 'text', value: END_DELIMITER }],
71
+ };
72
+
73
+ if (!prefix.trim()) return [delimiterNode];
74
+
75
+ if (kind === 'raw' || kind === 'text') {
76
+ return [{ ...node, value: prefix }, delimiterNode];
77
+ }
78
+
79
+ return [
80
+ {
81
+ ...node,
82
+ children: [{ type: 'text', value: prefix }],
83
+ },
84
+ delimiterNode,
85
+ ];
86
+ }
87
+
88
+ function stripTrailingDelimiter(node) {
89
+ if (!node || (node.type !== 'raw' && node.type !== 'text')) return node;
90
+ const value = String(node.value ?? '');
91
+ if (!/(?:^|\r?\n)\s*:::\s*$/.test(value)) return node;
92
+ const stripped = value.replace(/\r?\n\s*:::\s*$/, '');
93
+ if (!stripped.trim()) return null;
94
+ return { ...node, value: stripped };
95
+ }
96
+
97
+ function isIgnorableNode(node) {
98
+ if (!node) return true;
99
+ if (node.type === 'text') return !String(node.value ?? '').trim();
100
+ if (node.type === 'comment') return true;
101
+ if (node.type === 'element' && node.tagName === 'p') {
102
+ const text = getNodeText(node).trim();
103
+ return text.length === 0;
104
+ }
105
+ return false;
106
+ }
107
+
108
+ function normalizeClassNameList(value) {
109
+ if (Array.isArray(value)) return value;
110
+ if (typeof value === 'string') return value.split(/\s+/).filter(Boolean);
111
+ return [];
112
+ }
113
+
114
+ function normalizeLanguageLabel(value) {
115
+ const normalized = String(value ?? '').trim().toLowerCase();
116
+ if (!normalized) return '';
117
+ return languageLabelMap[normalized] ?? normalized;
118
+ }
119
+
120
+ function findLanguageFromClassName(className) {
121
+ const classList = normalizeClassNameList(className);
122
+ const match = classList.find((name) => /^language-/.test(name));
123
+ if (!match) return '';
124
+ return normalizeLanguageLabel(match.replace(/^language-/, ''));
125
+ }
126
+
127
+ function getElementProperty(properties, keys) {
128
+ for (const key of keys) {
129
+ if (properties?.[key] !== undefined) return String(properties[key]);
130
+ }
131
+ return '';
132
+ }
133
+
134
+ function inferLabelFromElement(node) {
135
+ if (!node || node.type !== 'element') return '';
136
+
137
+ const explicitTitle = getElementProperty(node.properties, ['data-code-title', 'dataCodeTitle']);
138
+ if (explicitTitle.trim()) return explicitTitle.trim();
139
+
140
+ const directLang = getElementProperty(node.properties, ['data-code-lang', 'dataCodeLang', 'data-language', 'dataLanguage']);
141
+ if (directLang.trim()) return normalizeLanguageLabel(directLang);
142
+
143
+ const fromClass = findLanguageFromClassName(node.properties?.className);
144
+ if (fromClass) return fromClass;
145
+
146
+ for (const child of node.children ?? []) {
147
+ const nested = inferLabelFromElement(child);
148
+ if (nested) return nested;
149
+ }
150
+
151
+ return '';
152
+ }
153
+
154
+ function inferLabelFromRaw(node) {
155
+ if (!node || node.type !== 'raw') return '';
156
+ const raw = String(node.value ?? '');
157
+
158
+ const titleMatch = raw.match(/data-code-title=["']([^"']+)["']/i);
159
+ if (titleMatch?.[1]) return titleMatch[1].trim();
160
+
161
+ const langAttr = raw.match(/data-(?:code-)?lang(?:uage)?=["']([^"']+)["']/i);
162
+ if (langAttr?.[1]) return normalizeLanguageLabel(langAttr[1]);
163
+
164
+ const classLang = raw.match(/class=["'][^"']*language-([a-z0-9_-]+)[^"']*["']/i);
165
+ if (classLang?.[1]) return normalizeLanguageLabel(classLang[1]);
166
+
167
+ return '';
168
+ }
169
+
170
+ function inferLabelFromBlock(block, index) {
171
+ if (block?.type === 'element') {
172
+ const label = inferLabelFromElement(block);
173
+ if (label) return label;
174
+ }
175
+
176
+ if (block?.type === 'raw') {
177
+ const label = inferLabelFromRaw(block);
178
+ if (label) return label;
179
+ }
180
+
181
+ return `Tab ${index + 1}`;
182
+ }
183
+
184
+ function createBlockWrapper(block, index) {
185
+ const isActive = index === 0;
186
+ return {
187
+ type: 'element',
188
+ tagName: 'div',
189
+ properties: {
190
+ className: ['rcg-block', ...(isActive ? ['active'] : [])],
191
+ ...(isActive ? {} : { hidden: true }),
192
+ },
193
+ children: [block],
194
+ };
195
+ }
196
+
197
+ function createTabs(tabLabels, uniqueId) {
198
+ return {
199
+ type: 'element',
200
+ tagName: 'div',
201
+ properties: { className: ['rcg-tab-container'], role: 'tablist' },
202
+ children: tabLabels.map((label, index) => ({
203
+ type: 'element',
204
+ tagName: 'button',
205
+ properties: {
206
+ type: 'button',
207
+ className: ['rcg-tab', ...(index === 0 ? ['active'] : [])],
208
+ role: 'tab',
209
+ 'aria-selected': index === 0 ? 'true' : 'false',
210
+ 'aria-controls': `${uniqueId}-block-${index}`,
211
+ id: `${uniqueId}-tab-${index}`,
212
+ },
213
+ children: [{ type: 'text', value: label }],
214
+ })),
215
+ };
216
+ }
217
+
218
+ function createStaticBlockWrapper(block, uniqueId, index) {
219
+ const isActive = index === 0;
220
+ return {
221
+ type: 'element',
222
+ tagName: 'div',
223
+ properties: {
224
+ className: ['rcg-block', ...(isActive ? ['active'] : [])],
225
+ role: 'tabpanel',
226
+ 'aria-labelledby': `${uniqueId}-tab-${index}`,
227
+ id: `${uniqueId}-block-${index}`,
228
+ ...(isActive ? {} : { hidden: true }),
229
+ },
230
+ children: [block],
231
+ };
232
+ }
233
+
234
+ export default function rehypeCodeGroup(options = {}) {
235
+ const output = options.output === 'static-tabs' ? 'static-tabs' : 'hydrated-element';
236
+ return (tree) => {
237
+ let counter = 0;
238
+
239
+ visit(tree, 'element', (node, index, parent) => {
240
+ if (!parent || typeof index !== 'number') return;
241
+ if (node.tagName !== 'p') return;
242
+
243
+ const startText = getNodeText(node).trim();
244
+ const startMatch = startText.match(START_DELIMITER_REGEX);
245
+ if (!startMatch) return;
246
+
247
+ let endIndex = -1;
248
+ for (let i = index + 1; i < parent.children.length; i++) {
249
+ const candidate = parent.children[i];
250
+ if (isEndDelimiterNode(candidate)) {
251
+ endIndex = i;
252
+ break;
253
+ }
254
+
255
+ const splitNodes = splitNodeWithAttachedEndDelimiter(candidate);
256
+ if (splitNodes) {
257
+ parent.children.splice(i, 1, ...splitNodes);
258
+ endIndex = splitNodes.length === 1 ? i : i + 1;
259
+ break;
260
+ }
261
+ }
262
+
263
+ let implicitBoundaryIndex = -1;
264
+ if (endIndex === -1) {
265
+ for (let i = index + 1; i < parent.children.length; i++) {
266
+ const candidate = parent.children[i];
267
+ if (isHeadingElement(candidate) || isCodeGroupStartNode(candidate)) {
268
+ implicitBoundaryIndex = i;
269
+ break;
270
+ }
271
+ }
272
+ if (implicitBoundaryIndex === -1) return;
273
+ }
274
+
275
+ const explicitLabels = (startMatch[1] ?? '').split(',').map((label) => label.trim()).filter(Boolean);
276
+ const sliceEnd = endIndex !== -1 ? endIndex : implicitBoundaryIndex;
277
+ const between = parent.children.slice(index + 1, sliceEnd);
278
+ const normalizedBlocks = between
279
+ .map((child) => stripTrailingDelimiter(child))
280
+ .filter(Boolean);
281
+ const codeBlocks = normalizedBlocks.filter(
282
+ (child) => !isIgnorableNode(child) && !isEndDelimiterNode(child),
283
+ );
284
+ const usedLabels = codeBlocks.map((block, blockIndex) => {
285
+ return explicitLabels[blockIndex] ?? inferLabelFromBlock(block, blockIndex);
286
+ });
287
+
288
+ const uniqueId = `rcg-${counter++}`;
289
+
290
+ const groupNode = output === 'static-tabs'
291
+ ? {
292
+ type: 'element',
293
+ tagName: 'div',
294
+ properties: { className: ['rehype-code-group'] },
295
+ children: [
296
+ createTabs(usedLabels, uniqueId),
297
+ ...codeBlocks.map((block, blockIndex) =>
298
+ createStaticBlockWrapper(block, uniqueId, blockIndex),
299
+ ),
300
+ ],
301
+ }
302
+ : {
303
+ type: 'element',
304
+ tagName: 'codegroup',
305
+ properties: {
306
+ className: ['rehype-code-group'],
307
+ 'data-codegroup-tabs': JSON.stringify(usedLabels),
308
+ 'data-codegroup-active': '0',
309
+ },
310
+ children: codeBlocks.map((block, blockIndex) => createBlockWrapper(block, blockIndex)),
311
+ };
312
+
313
+ const deleteCount = endIndex !== -1
314
+ ? endIndex - index + 1
315
+ : implicitBoundaryIndex - index;
316
+ parent.children.splice(index, deleteCount, groupNode);
317
+
318
+ return [SKIP, index + 1];
319
+ });
320
+
321
+ visit(tree, 'element', (node) => {
322
+ if (!node.properties?.className) return;
323
+ node.properties.className = normalizeClassNameList(node.properties.className);
324
+ });
325
+ };
326
+ }
@@ -0,0 +1,96 @@
1
+ import { visit } from 'unist-util-visit';
2
+
3
+ /**
4
+ * Rehype plugin that wraps fenced code blocks that have a `data-code-title`
5
+ * attribute in a container div and prepends a title bar, similar to how
6
+ * code groups display their tab labels.
7
+ *
8
+ * Code blocks processed by the Shiki highlighter become `raw` nodes in the
9
+ * hast tree (mdsvex wraps them in {@html `…`}). The title is carried as an
10
+ * HTML attribute inside the raw string, e.g.:
11
+ * <pre data-code-title="filename.js" data-code-lang="js">…</pre>
12
+ *
13
+ * Output:
14
+ * <div class="code-block-with-title">
15
+ * <div class="code-block-title">filename.js</div>
16
+ * {raw node}
17
+ * </div>
18
+ *
19
+ * Code blocks that already live inside a code-group tab panel (.rcg-block)
20
+ * are intentionally skipped – their title is already shown as the tab label.
21
+ */
22
+
23
+ const TITLE_ATTR_RE = /data-code-title=["']([^"']+)["']/i;
24
+
25
+ function normalizeClassNameList(value) {
26
+ if (Array.isArray(value)) return value;
27
+ if (typeof value === 'string') return value.split(/\s+/).filter(Boolean);
28
+ return [];
29
+ }
30
+
31
+ function getTitleFromElement(node) {
32
+ if (!node || node.type !== 'element') return '';
33
+ const own = node.properties?.['data-code-title'] ?? node.properties?.dataCodeTitle;
34
+ if (typeof own === 'string' && own.trim()) return own.trim();
35
+
36
+ if (node.tagName === 'pre') {
37
+ for (const child of node.children ?? []) {
38
+ if (child?.type !== 'element' || child.tagName !== 'code') continue;
39
+ const nested = child.properties?.['data-code-title'] ?? child.properties?.dataCodeTitle;
40
+ if (typeof nested === 'string' && nested.trim()) return nested.trim();
41
+ }
42
+ }
43
+
44
+ return '';
45
+ }
46
+
47
+ function wrapWithTitle(title, node) {
48
+ const titleNode = {
49
+ type: 'element',
50
+ tagName: 'div',
51
+ properties: { className: ['code-block-title'] },
52
+ children: [{ type: 'text', value: title }],
53
+ };
54
+
55
+ return {
56
+ type: 'element',
57
+ tagName: 'div',
58
+ properties: { className: ['code-block-with-title'] },
59
+ children: [titleNode, node],
60
+ };
61
+ }
62
+
63
+ export default function rehypeCodeTitle() {
64
+ return (tree) => {
65
+ visit(tree, 'element', (node, index, parent) => {
66
+ if (!parent || typeof index !== 'number') return;
67
+ if (node.tagName !== 'pre') return;
68
+
69
+ // Skip blocks inside code-group tab panels
70
+ const parentClasses = normalizeClassNameList(parent.properties?.className);
71
+ if (parentClasses.includes('rcg-block')) return;
72
+
73
+ const title = getTitleFromElement(node);
74
+ if (!title) return;
75
+
76
+ parent.children[index] = wrapWithTitle(title, node);
77
+ });
78
+
79
+ visit(tree, 'raw', (node, index, parent) => {
80
+ if (!parent || typeof index !== 'number') return;
81
+
82
+ // Skip blocks inside code-group tab panels
83
+ const parentClasses = normalizeClassNameList(parent.properties?.className);
84
+ if (parentClasses.includes('rcg-block')) return;
85
+
86
+ const raw = String(node.value ?? '');
87
+ const titleMatch = raw.match(TITLE_ATTR_RE);
88
+ if (!titleMatch) return;
89
+
90
+ const title = titleMatch[1].trim();
91
+ if (!title) return;
92
+
93
+ parent.children[index] = wrapWithTitle(title, node);
94
+ });
95
+ };
96
+ }
@@ -0,0 +1,170 @@
1
+ import { visit, SKIP } from 'unist-util-visit';
2
+
3
+ /**
4
+ * Default options — compatible with @mdit-vue/plugin-toc
5
+ * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-toc
6
+ *
7
+ * @typedef {Object} TocOptions
8
+ * @property {RegExp} pattern - Placeholder pattern. Default: /^\[\[toc\]\]$/i
9
+ * @property {number[]} level - Heading levels to include. Default: [2, 3]
10
+ * @property {string} containerTag - Container element tag. Default: 'nav'
11
+ * @property {string} containerClass - Container element class. Default: 'table-of-contents'
12
+ * @property {'ul'|'ol'} listTag - List element tag. Default: 'ul'
13
+ * @property {string} listClass - List element class. Default: ''
14
+ * @property {string} itemClass - List item class. Default: ''
15
+ * @property {string} linkTag - Link tag. Default: 'a'
16
+ * @property {string} linkClass - Link class. Default: ''
17
+ * @property {Function} format - Optional heading text formatter. Default: undefined
18
+ */
19
+
20
+ const defaults = {
21
+ pattern: /^\[\[toc\]\]$/i,
22
+ level: [2, 3],
23
+ containerTag: 'nav',
24
+ containerClass: 'table-of-contents',
25
+ listTag: 'ul',
26
+ listClass: '',
27
+ itemClass: '',
28
+ linkTag: 'a',
29
+ linkClass: '',
30
+ format: undefined,
31
+ };
32
+
33
+ /**
34
+ * Rehype plugin: replaces [[TOC]] paragraph with a nested TOC tree,
35
+ * compatible with @mdit-vue/plugin-toc HTML output and options.
36
+ *
37
+ * @param {Partial<typeof defaults>} userOptions
38
+ */
39
+ export function rehypeTocPlaceholder(userOptions = {}) {
40
+ const options = { ...defaults, ...userOptions };
41
+
42
+ return (tree) => {
43
+ // Collect headings matching the requested levels
44
+ const headings = [];
45
+ visit(tree, 'element', (node) => {
46
+ const level = parseInt(node.tagName.slice(1), 10);
47
+ if (
48
+ node.tagName.match(/^h[1-6]$/) &&
49
+ options.level.includes(level) &&
50
+ node.properties?.id
51
+ ) {
52
+ const text = options.format
53
+ ? options.format(collectText(node))
54
+ : collectText(node);
55
+ headings.push({ id: node.properties.id, text, level });
56
+ }
57
+ });
58
+
59
+ if (!headings.length) return;
60
+
61
+ // Find [[TOC]] paragraph and replace it
62
+ visit(tree, 'element', (node, index, parent) => {
63
+ if (node.tagName !== 'p' || !parent || index == null) return;
64
+ const raw = collectText(node).trim();
65
+ if (!options.pattern.test(raw)) return;
66
+
67
+ const nested = buildNestedTree(headings);
68
+ const listNode = renderList(nested, options);
69
+
70
+ const containerProps = options.containerClass
71
+ ? { className: options.containerClass.split(' ').filter(Boolean) }
72
+ : {};
73
+
74
+ parent.children.splice(index, 1, {
75
+ type: 'element',
76
+ tagName: options.containerTag,
77
+ properties: containerProps,
78
+ children: [listNode],
79
+ });
80
+ });
81
+ };
82
+ }
83
+
84
+ /** Collect all text content from a node, skipping header-anchor elements */
85
+ function collectText(node) {
86
+ let text = '';
87
+ visit(node, (n) => {
88
+ if (n.type === 'element' && (n.properties?.class === 'header-anchor' || n.properties?.className?.includes?.('header-anchor'))) return SKIP;
89
+ if (n.type === 'text') text += n.value;
90
+ });
91
+ return text;
92
+ }
93
+
94
+ /**
95
+ * Build a nested tree from a flat headings list,
96
+ * matching mdit-vue's resolveHeadersFromTokens behavior.
97
+ */
98
+ function buildNestedTree(headings) {
99
+ const root = [];
100
+ const stack = [];
101
+
102
+ for (const h of headings) {
103
+ const node = { ...h, children: [] };
104
+
105
+ while (stack.length > 0 && stack[stack.length - 1].level >= h.level) {
106
+ stack.pop();
107
+ }
108
+
109
+ if (stack.length === 0) {
110
+ root.push(node);
111
+ } else {
112
+ stack[stack.length - 1].children.push(node);
113
+ }
114
+
115
+ stack.push(node);
116
+ }
117
+
118
+ return root;
119
+ }
120
+
121
+ /**
122
+ * Recursively render nested heading tree to hast nodes.
123
+ * Output matches mdit-vue's createRenderHeaders structure:
124
+ * <ul>
125
+ * <li><a href="#id">text</a><ul>...</ul></li>
126
+ * </ul>
127
+ */
128
+ function renderList(nodes, options) {
129
+ const listProps = options.listClass
130
+ ? { className: options.listClass.split(' ').filter(Boolean) }
131
+ : {};
132
+
133
+ const items = nodes.map((node) => {
134
+ const linkProps = { href: `#${node.id}` };
135
+ if (options.linkClass) {
136
+ linkProps.className = options.linkClass.split(' ').filter(Boolean);
137
+ }
138
+
139
+ const liChildren = [
140
+ {
141
+ type: 'element',
142
+ tagName: options.linkTag,
143
+ properties: linkProps,
144
+ children: [{ type: 'text', value: node.text }],
145
+ },
146
+ ];
147
+
148
+ if (node.children.length > 0) {
149
+ liChildren.push(renderList(node.children, options));
150
+ }
151
+
152
+ const itemProps = options.itemClass
153
+ ? { className: options.itemClass.split(' ').filter(Boolean) }
154
+ : {};
155
+
156
+ return {
157
+ type: 'element',
158
+ tagName: 'li',
159
+ properties: itemProps,
160
+ children: liChildren,
161
+ };
162
+ });
163
+
164
+ return {
165
+ type: 'element',
166
+ tagName: options.listTag,
167
+ properties: listProps,
168
+ children: items,
169
+ };
170
+ }
@@ -0,0 +1,22 @@
1
+ import { visit } from 'unist-util-visit';
2
+ import { normalizeCodeFenceInfo } from './codeFenceInfo.js';
3
+
4
+ const TITLE_RE = /\[([^\]]+)\]/;
5
+
6
+ export function remarkCodeMeta() {
7
+ return (tree) => {
8
+ visit(tree, 'code', (node) => {
9
+ const { lang, meta } = normalizeCodeFenceInfo(node.lang ?? '', node.meta ?? '');
10
+ const titleMatch = meta.match(TITLE_RE);
11
+ const title = titleMatch?.[1]?.trim() ?? '';
12
+
13
+ node.data = node.data ?? {};
14
+ node.data.hProperties = {
15
+ ...(node.data.hProperties ?? {}),
16
+ ...(lang ? { 'data-code-lang': lang } : {}),
17
+ ...(meta ? { 'data-code-meta': meta } : {}),
18
+ ...(title ? { 'data-code-title': title } : {}),
19
+ };
20
+ });
21
+ };
22
+ }