@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,349 @@
1
+ /**
2
+ * remarkImportsBrowser
3
+ *
4
+ * Browser-compatible port of remarkImports.js.
5
+ * Uses fetch() instead of node:fs for all file reads.
6
+ * Uses URL-based path resolution instead of node:path.
7
+ *
8
+ * Supports exactly the same syntax as remarkImports:
9
+ * <<< ./snippet.js — inline code snippet
10
+ * <!-- @include: ./other.md --> — inline markdown
11
+ * `@/` alias, regions, line ranges, etc.
12
+ */
13
+
14
+ import { unified } from 'unified';
15
+ import remarkParse from 'remark-parse';
16
+
17
+ // ── Regex patterns (same as remarkImports) ────────────────────────────────────
18
+
19
+ const SNIPPET_RE = /^<<<\s+(.+)$/;
20
+ const INCLUDE_RE = /^<!--\s*@include:\s*([^\s]+)\s*-->$/;
21
+ const TITLE_RE = /\s+\[([^\]]+)\]\s*$/;
22
+ const BRACES_RE = /\{([^}]*)\}\s*$/;
23
+ const REGION_RE = /^(.*?)#([^#{]+)$/;
24
+ const EXTERNAL_URL_RE = /^(?:[a-z][a-z\d+\-.]*:|\/\/)/i;
25
+
26
+ const extToLang = {
27
+ '.js': 'js', '.mjs': 'js', '.cjs': 'js',
28
+ '.ts': 'ts', '.tsx': 'tsx', '.jsx': 'jsx',
29
+ '.json': 'json', '.css': 'css', '.scss': 'scss',
30
+ '.html': 'html', '.md': 'md', '.sh': 'sh',
31
+ '.bash': 'bash', '.yml': 'yaml', '.yaml': 'yaml',
32
+ };
33
+
34
+ const PARSE_BASE_ORIGIN =
35
+ typeof window !== 'undefined' && window.location?.origin
36
+ ? window.location.origin
37
+ : 'https://example.invalid';
38
+
39
+ // ── URL / path utilities ──────────────────────────────────────────────────────
40
+
41
+ /** Browser-safe extname: '/foo/bar.js' → '.js' */
42
+ function extname(urlPath) {
43
+ const filename = (urlPath ?? '').split('/').pop() ?? '';
44
+ const dotIdx = filename.lastIndexOf('.');
45
+ return dotIdx > 0 ? filename.slice(dotIdx).toLowerCase() : '';
46
+ }
47
+
48
+ /**
49
+ * Resolve a remarkImports specifier to an absolute URL path.
50
+ *
51
+ * `@/path` or `@path` → /path (site root)
52
+ * /path → docsPrefix/path (relative to docsDir root)
53
+ * ./path ../path → resolved relative to currentDirUrl
54
+ */
55
+ function resolveImportUrl(specifier, currentDirUrl, docsPrefix) {
56
+ const cleanPrefix = docsPrefix.replace(/\/+$/, '');
57
+
58
+ if (specifier === '@') return '/';
59
+ if (specifier.startsWith('@/')) return '/' + specifier.slice(2);
60
+ if (specifier.startsWith('@')) return '/' + specifier.slice(1);
61
+
62
+ // /absolute → inside docsPrefix
63
+ if (specifier.startsWith('/')) {
64
+ if (specifier === cleanPrefix || specifier.startsWith(cleanPrefix + '/')) return specifier;
65
+ return cleanPrefix + specifier;
66
+ }
67
+
68
+ // relative (./foo, ../bar, plain name)
69
+ // Append trailing slash to currentDirUrl to ensure correct resolution
70
+ const base = currentDirUrl.endsWith('/') ? currentDirUrl : currentDirUrl + '/';
71
+ try {
72
+ return new URL(specifier, PARSE_BASE_ORIGIN + base).pathname;
73
+ } catch {
74
+ return cleanPrefix + '/' + specifier;
75
+ }
76
+ }
77
+
78
+ function inferLang(urlPath, langOverride) {
79
+ if (langOverride) return langOverride;
80
+ const ext = extname(urlPath);
81
+ return extToLang[ext] ?? ext.replace(/^\./, '') ?? '';
82
+ }
83
+
84
+ /** Normalize internal doc links: strip .md/.html, collapse /index */
85
+ function normalizeInternalLinkUrl(url) {
86
+ if (EXTERNAL_URL_RE.test(url)) return url;
87
+ const hashIdx = url.indexOf('#');
88
+ const pathPart = hashIdx >= 0 ? url.slice(0, hashIdx) : url;
89
+ const hashPart = hashIdx >= 0 ? url.slice(hashIdx) : '';
90
+ const normalized = pathPart
91
+ .replace(/\.(md|html)$/i, '')
92
+ .replace(/\/index$/, '');
93
+ return (normalized || '.') + hashPart;
94
+ }
95
+
96
+ function expandAliasInUrls(node, currentDirUrl, docsPrefix) {
97
+ if (node.type === 'link' && typeof node.url === 'string') {
98
+ if (node.url.startsWith('#')) return;
99
+ if (EXTERNAL_URL_RE.test(node.url)) return;
100
+ if (!node.data?.hProperties?.target) {
101
+ const resolved = resolveImportUrl(node.url, currentDirUrl, docsPrefix);
102
+ node.url = normalizeInternalLinkUrl(resolved);
103
+ }
104
+ }
105
+ }
106
+
107
+ // ── Fetch ─────────────────────────────────────────────────────────────────────
108
+
109
+ async function fetchText(url) {
110
+ try {
111
+ const res = await fetch(url);
112
+ if (!res.ok) return null;
113
+ return await res.text();
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ // ── Spec parsers (identical logic to remarkImports) ───────────────────────────
120
+
121
+ function escapeRegExp(value) {
122
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
123
+ }
124
+
125
+ function parseTitle(value) {
126
+ const match = value.match(TITLE_RE);
127
+ if (!match) return { text: value.trim(), title: '' };
128
+ return { text: value.slice(0, match.index).trim(), title: match[1].trim() };
129
+ }
130
+
131
+ function parseBraces(value) {
132
+ const match = value.match(BRACES_RE);
133
+ if (!match) return { text: value.trim(), braces: '' };
134
+ return { text: value.slice(0, match.index).trim(), braces: match[1].trim() };
135
+ }
136
+
137
+ function parseRegion(value) {
138
+ const match = value.match(REGION_RE);
139
+ if (!match) return { filePart: value.trim(), regionOrAnchor: '' };
140
+ return { filePart: match[1].trim(), regionOrAnchor: match[2].trim() };
141
+ }
142
+
143
+ function parseRange(value) {
144
+ const text = String(value ?? '').trim();
145
+ if (!text) return null;
146
+ if (/^\d+$/.test(text)) { const n = Number(text); return { start: n, end: n }; }
147
+ const match = text.match(/^(\d*)\s*,\s*(\d*)$/);
148
+ if (!match) return null;
149
+ return { start: match[1] ? Number(match[1]) : null, end: match[2] ? Number(match[2]) : null };
150
+ }
151
+
152
+ function selectLines(content, rangeText) {
153
+ const range = parseRange(rangeText);
154
+ if (!range) return content;
155
+ const lines = content.split(/\r?\n/);
156
+ const start = range.start ?? 1;
157
+ const end = range.end ?? lines.length;
158
+ return lines.slice(Math.max(1, start) - 1, Math.max(start, Math.min(end, lines.length))).join('\n');
159
+ }
160
+
161
+ function selectRegion(content, regionName) {
162
+ if (!regionName) return content;
163
+ const lines = content.split(/\r?\n/);
164
+ const re = escapeRegExp(regionName);
165
+ const startRe = new RegExp(`#region\\s+${re}\\s*$`, 'i');
166
+ const endRe = new RegExp(`#endregion\\s+${re}\\s*$`, 'i');
167
+ let start = -1, end = -1;
168
+ for (let i = 0; i < lines.length; i++) {
169
+ if (start === -1 && startRe.test(lines[i])) { start = i; continue; }
170
+ if (start !== -1 && endRe.test(lines[i])) { end = i; break; }
171
+ }
172
+ if (start === -1 || end === -1 || end <= start) return content;
173
+ return lines.slice(start + 1, end).join('\n');
174
+ }
175
+
176
+ function slugifyHeading(v) {
177
+ return v.toLowerCase().replace(/<[^>]+>/g, '').replace(/[^\w\s-]/g, '').trim().replace(/\s+/g, '-');
178
+ }
179
+
180
+ function getHeadingId(raw) {
181
+ const m = raw.match(/\s*\{#([^}]+)\}\s*$/);
182
+ if (m?.[1]) return m[1].trim().toLowerCase();
183
+ return slugifyHeading(raw.replace(/\s*\{#([^}]+)\}\s*$/, ''));
184
+ }
185
+
186
+ function selectMarkdownSectionByAnchor(content, anchor) {
187
+ if (!anchor) return content;
188
+ const lines = content.split(/\r?\n/);
189
+ const headingRe = /^(#{1,6})\s+(.+)$/;
190
+ const wanted = anchor.toLowerCase();
191
+ let startIndex = -1, startDepth = 0;
192
+ for (let i = 0; i < lines.length; i++) {
193
+ const m = lines[i].match(headingRe);
194
+ if (!m) continue;
195
+ if (getHeadingId(m[2]) === wanted) { startIndex = i; startDepth = m[1].length; break; }
196
+ }
197
+ if (startIndex === -1) return content;
198
+ let endIndex = lines.length;
199
+ for (let i = startIndex + 1; i < lines.length; i++) {
200
+ const m = lines[i].match(headingRe);
201
+ if (m && m[1].length <= startDepth) { endIndex = i; break; }
202
+ }
203
+ return lines.slice(startIndex, endIndex).join('\n');
204
+ }
205
+
206
+ function parseSnippetSpec(raw) {
207
+ const withTitle = parseTitle(raw);
208
+ const withBraces = parseBraces(withTitle.text);
209
+ const withRegion = parseRegion(withBraces.text);
210
+ let rangePart = '', langOverride = '', metaTail = '';
211
+ if (withBraces.braces) {
212
+ const tokens = withBraces.braces.split(/\s+/).filter(Boolean);
213
+ if (tokens.length > 0 && /^(\d+|\d*\s*,\s*\d*)$/.test(tokens[0])) rangePart = tokens.shift() ?? '';
214
+ if (tokens.length > 0 && !tokens[0].includes(':')) langOverride = tokens.shift() ?? '';
215
+ metaTail = tokens.join(' ').trim();
216
+ }
217
+ return { title: withTitle.title, filePart: withRegion.filePart, regionOrAnchor: withRegion.regionOrAnchor, rangePart, langOverride, metaTail };
218
+ }
219
+
220
+ function buildCodeMeta(title, metaTail) {
221
+ const parts = [];
222
+ if (metaTail) parts.push(metaTail);
223
+ if (title) parts.push(`[${title}]`);
224
+ return parts.join(' ').trim() || null;
225
+ }
226
+
227
+ function inlineNodeText(node) {
228
+ if (!node) return '';
229
+ if (node.type === 'text' || node.type === 'inlineCode') return node.value ?? '';
230
+ if (node.type === 'linkReference') return `[${node.label ?? node.identifier ?? ''}]`;
231
+ if (node.type === 'link') return node.children?.map(inlineNodeText).join('') ?? '';
232
+ if (node.children) return node.children.map(inlineNodeText).join('');
233
+ return '';
234
+ }
235
+
236
+ function looksLikeSvelteMarkup(content) {
237
+ const text = String(content ?? '').trim();
238
+ if (!text) return false;
239
+ const hasComponentTag = /<\/?[A-Z][A-Za-z0-9]*\b/.test(text);
240
+ const hasSvelteExpressionAttr = /=\s*\{/.test(text);
241
+ const hasSvelteBlockSyntax = /\{[#/:@][^}]+\}/.test(text);
242
+ return (hasComponentTag && hasSvelteExpressionAttr) || hasSvelteBlockSyntax;
243
+ }
244
+
245
+ // ── Async tree transformers ───────────────────────────────────────────────────
246
+
247
+ async function buildSnippetNode(rawSpec, currentDirUrl, docsPrefix) {
248
+ const parsed = parseSnippetSpec(rawSpec);
249
+ const url = resolveImportUrl(parsed.filePart, currentDirUrl, docsPrefix);
250
+ let content = await fetchText(url);
251
+ if (content === null) {
252
+ return { type: 'code', lang: 'text', meta: '', value: `[remarkImports] File not found: ${url}` };
253
+ }
254
+ if (parsed.regionOrAnchor) content = selectRegion(content, parsed.regionOrAnchor);
255
+ if (parsed.rangePart) content = selectLines(content, parsed.rangePart);
256
+ const lang = inferLang(url, parsed.langOverride);
257
+ return { type: 'code', lang: lang || null, meta: buildCodeMeta(parsed.title, parsed.metaTail), value: content };
258
+ }
259
+
260
+ async function buildIncludeNodes(rawSpec, currentDirUrl, docsPrefix) {
261
+ const withBraces = parseBraces(rawSpec);
262
+ const withRegion = parseRegion(withBraces.text);
263
+ const url = resolveImportUrl(withRegion.filePart, currentDirUrl, docsPrefix);
264
+ let content = await fetchText(url);
265
+ if (content === null) {
266
+ return {
267
+ nodes: [{ type: 'paragraph', children: [{ type: 'text', value: `[remarkImports] File not found: ${url}` }] }],
268
+ includeDirUrl: currentDirUrl,
269
+ };
270
+ }
271
+ if (withRegion.regionOrAnchor) {
272
+ const byRegion = selectRegion(content, withRegion.regionOrAnchor);
273
+ content = byRegion === content ? selectMarkdownSectionByAnchor(content, withRegion.regionOrAnchor) : byRegion;
274
+ }
275
+ if (withBraces.braces) content = selectLines(content, withBraces.braces);
276
+ // Strip frontmatter from included files
277
+ content = content.replace(/^---[\r\n][\s\S]*?[\r\n]---[\r\n]?/, '');
278
+
279
+ if (looksLikeSvelteMarkup(content)) {
280
+ const includeDirUrl = url.substring(0, url.lastIndexOf('/') + 1);
281
+ return { nodes: [{ type: 'html', value: content }], includeDirUrl };
282
+ }
283
+
284
+ const tree = unified().use(remarkParse).parse(content);
285
+ const includeDirUrl = url.substring(0, url.lastIndexOf('/') + 1);
286
+ return { nodes: tree.children ?? [], includeDirUrl };
287
+ }
288
+
289
+ function markNodesBaseDir(nodes, dirUrl) {
290
+ for (const node of nodes) {
291
+ node.data = node.data ?? {};
292
+ node.data.__includeDirUrl = dirUrl;
293
+ if (Array.isArray(node.children)) markNodesBaseDir(node.children, dirUrl);
294
+ }
295
+ }
296
+
297
+ async function transformChildren(children, currentDirUrl, docsPrefix, depth) {
298
+ if (!Array.isArray(children) || depth > 20) return;
299
+ for (let i = 0; i < children.length; i++) {
300
+ const node = children[i];
301
+ const nodeDir = node?.data?.__includeDirUrl || currentDirUrl;
302
+ expandAliasInUrls(node, nodeDir, docsPrefix);
303
+
304
+ if (node?.type === 'paragraph' && Array.isArray(node.children)) {
305
+ const line = node.children.map(inlineNodeText).join('').trim();
306
+ const m = line.match(SNIPPET_RE);
307
+ if (m?.[1]) {
308
+ const snippetNode = await buildSnippetNode(m[1].trim(), nodeDir, docsPrefix);
309
+ children.splice(i, 1, snippetNode);
310
+ continue;
311
+ }
312
+ }
313
+
314
+ if (node?.type === 'html') {
315
+ const line = String(node.value ?? '').trim();
316
+ const m = line.match(INCLUDE_RE);
317
+ if (m?.[1]) {
318
+ const included = await buildIncludeNodes(m[1].trim(), nodeDir, docsPrefix);
319
+ markNodesBaseDir(included.nodes, included.includeDirUrl);
320
+ children.splice(i, 1, ...included.nodes);
321
+ i -= 1;
322
+ continue;
323
+ }
324
+ }
325
+
326
+ if (Array.isArray(node?.children)) {
327
+ await transformChildren(node.children, nodeDir, docsPrefix, depth + 1);
328
+ }
329
+ }
330
+ }
331
+
332
+ // ── Plugin export ─────────────────────────────────────────────────────────────
333
+
334
+ /**
335
+ * @param {{ baseUrl?: string, docsPrefix?: string }} options
336
+ * baseUrl - the URL path of the current markdown file, e.g. '/docs/guide/getting-started.md'
337
+ * docsPrefix - the docs root prefix, e.g. '/docs' (default: '/docs')
338
+ */
339
+ export function remarkImportsBrowser(options = {}) {
340
+ const docsPrefix = (options.docsPrefix ?? '/docs').replace(/\/+$/, '');
341
+ const baseUrl = options.baseUrl ?? docsPrefix + '/index.md';
342
+ const currentDirUrl = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1);
343
+
344
+ return async (tree) => {
345
+ await transformChildren(tree.children, currentDirUrl, docsPrefix, 0);
346
+ };
347
+ }
348
+
349
+ export default remarkImportsBrowser;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * remarkInlineAttrs
3
+ *
4
+ * Lightweight replacement for remark-attr that does NOT use the legacy
5
+ * remark v2 tokenizer API (which breaks code-fence parsing).
6
+ *
7
+ * Supported syntax (inline, on the same line):
8
+ * [link text](url){key="value" key2="val2"}
9
+ * ![alt](src){width=200}
10
+ *
11
+ * The `{…}` block must:
12
+ * - Immediately follow a link/image (no space between `)` and `{`)
13
+ * - Be the last thing on the line (or followed only by whitespace)
14
+ *
15
+ * The parsed attributes are set on `node.data.hProperties` so rehype picks
16
+ * them up correctly.
17
+ *
18
+ * Note: heading `{#id}` syntax is handled by remarkCustomAnchors — this
19
+ * plugin intentionally ignores heading nodes.
20
+ */
21
+ import { visit } from 'unist-util-visit';
22
+
23
+ /** Parse `{key="value" key2=val2 .class #id}` into a plain object. */
24
+ function parseAttrs(raw) {
25
+ const attrs = {};
26
+ // key="value" or key='value' or key=bareword
27
+ const KV_RE = /([\w-]+)="([^"]*)"|([\w-]+)='([^']*)'|([\w-]+)=([\S]+)|(\.[\w-]+)|(#[\w-]+)/g;
28
+ let m;
29
+ while ((m = KV_RE.exec(raw)) !== null) {
30
+ if (m[1]) {
31
+ attrs[m[1]] = m[2]; // key="value"
32
+ } else if (m[3]) {
33
+ attrs[m[3]] = m[4]; // key='value'
34
+ } else if (m[5]) {
35
+ attrs[m[5]] = m[6]; // key=bareword
36
+ } else if (m[7]) {
37
+ // .class → append to className
38
+ const cls = m[7].slice(1);
39
+ attrs.class = attrs.class ? `${attrs.class} ${cls}` : cls;
40
+ } else if (m[8]) {
41
+ // #id
42
+ attrs.id = m[8].slice(1);
43
+ }
44
+ }
45
+ return attrs;
46
+ }
47
+
48
+ /**
49
+ * In the mdast, after micromark parses `[text](url){attrs}`, the `{attrs}`
50
+ * part ends up as a `text` node immediately following the `link` node inside
51
+ * the same `paragraph.children` array.
52
+ *
53
+ * This transform:
54
+ * 1. Finds link/image nodes followed by a text node starting with `{…}`.
55
+ * 2. Parses the attribute block.
56
+ * 3. Merges the attributes into `node.data.hProperties`.
57
+ * 4. Removes the `{…}` text from the following node (or removes the node
58
+ * if it becomes empty).
59
+ */
60
+ export function remarkInlineAttrs() {
61
+ return (tree) => {
62
+ visit(tree, 'paragraph', (node) => {
63
+ const children = node.children;
64
+ if (!Array.isArray(children)) return;
65
+
66
+ for (let i = 0; i < children.length; i++) {
67
+ const child = children[i];
68
+ if (child.type !== 'link' && child.type !== 'image') continue;
69
+
70
+ const next = children[i + 1];
71
+ if (!next || next.type !== 'text') continue;
72
+
73
+ // The attr block must start immediately (no leading space)
74
+ const attrMatch = next.value.match(/^\{([^}]*)\}/);
75
+ if (!attrMatch) continue;
76
+
77
+ const attrs = parseAttrs(attrMatch[1]);
78
+ if (Object.keys(attrs).length === 0) continue;
79
+
80
+ // Apply attributes to the link/image node
81
+ child.data = child.data ?? {};
82
+ child.data.hProperties = child.data.hProperties ?? {};
83
+ Object.assign(child.data.hProperties, attrs);
84
+
85
+ // Remove the matched `{…}` prefix from the next text node
86
+ const remainder = next.value.slice(attrMatch[0].length);
87
+ if (remainder === '') {
88
+ children.splice(i + 1, 1);
89
+ } else {
90
+ next.value = remainder;
91
+ }
92
+ }
93
+ });
94
+ };
95
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * remarkMathToHtml
3
+ *
4
+ * Scans remark text nodes for $...$ (inline) and $$...$$ (display) patterns
5
+ * and renders them server-side with MathJax (SVG output).
6
+ *
7
+ * NOTE: remark-math's micromark extension is NOT active during mdsvex's parse
8
+ * step (user plugins are added as AST transforms, not parser extensions).
9
+ * Therefore all math stays as plain text nodes, and this plugin handles them
10
+ * entirely through text-node scanning.
11
+ *
12
+ * After inserting block-level <div> nodes into paragraph children the plugin
13
+ * also splits those paragraphs so no <div> ends up inside a <p>.
14
+ */
15
+ import { visit } from 'unist-util-visit';
16
+ import { liteAdaptor } from 'mathjax-full/js/adaptors/liteAdaptor.js';
17
+ import { RegisterHTMLHandler } from 'mathjax-full/js/handlers/html.js';
18
+ import { AllPackages } from 'mathjax-full/js/input/tex/AllPackages.js';
19
+ import { TeX } from 'mathjax-full/js/input/tex.js';
20
+ import { SVG } from 'mathjax-full/js/output/svg.js';
21
+ import { mathjax } from 'mathjax-full/js/mathjax.js';
22
+
23
+ function createMathJax() {
24
+ const adaptor = liteAdaptor();
25
+ RegisterHTMLHandler(adaptor);
26
+ const input = new TeX({ packages: AllPackages });
27
+ const output = new SVG({ fontCache: 'none' });
28
+ const doc = mathjax.document('', { InputJax: input, OutputJax: output });
29
+ return { adaptor, doc };
30
+ }
31
+
32
+ // Matches $$...$$ OR $...$ (non-greedy; $$ takes priority via alternation order).
33
+ // Group 1 = display math content, Group 2 = inline math content.
34
+ const MATH_RE = /\$\$([\s\S]*?)\$\$|\$(?!\$)((?:[^$\\]|\\[\s\S])*?)\$/g;
35
+
36
+ export function remarkMathToHtml() {
37
+ const { adaptor, doc } = createMathJax();
38
+
39
+ function renderMath(latex, display) {
40
+ try {
41
+ const liteEl = doc.convert(latex, { display });
42
+ return adaptor.outerHTML(liteEl);
43
+ } catch {
44
+ return `<span class="math-error">[MathJax error]</span>`;
45
+ }
46
+ }
47
+
48
+ function wrapNode(svg, display) {
49
+ const tag = display ? 'div' : 'span';
50
+ const cls = display ? 'math math-display' : 'math math-inline';
51
+ return { type: 'html', value: `<${tag} class="${cls}">${svg}</${tag}>` };
52
+ }
53
+
54
+ /**
55
+ * Split a text node's value into parts according to MATH_RE.
56
+ * Returns null if no math was found.
57
+ */
58
+ function splitTextNode(value) {
59
+ const parts = [];
60
+ let last = 0;
61
+ MATH_RE.lastIndex = 0;
62
+ let match;
63
+ while ((match = MATH_RE.exec(value)) !== null) {
64
+ if (match.index > last) {
65
+ parts.push({ type: 'text', value: value.slice(last, match.index) });
66
+ }
67
+ const display = match[1] !== undefined;
68
+ const latex = display ? match[1] : match[2];
69
+ parts.push(wrapNode(renderMath(latex, display), display));
70
+ last = match.index + match[0].length;
71
+ }
72
+ if (parts.length === 0) return null;
73
+ if (last < value.length) {
74
+ parts.push({ type: 'text', value: value.slice(last) });
75
+ }
76
+ return parts;
77
+ }
78
+
79
+ return (tree) => {
80
+ // ── Pass 1: also convert any math/inlineMath nodes if remark-math DID fire ──
81
+ const mathNodes = [];
82
+ visit(tree, 'math', (node, index, parent) => {
83
+ if (!parent || index == null) return;
84
+ mathNodes.push({ node, index, parent, display: true });
85
+ });
86
+ visit(tree, 'inlineMath', (node, index, parent) => {
87
+ if (!parent || index == null) return;
88
+ mathNodes.push({ node, index, parent, display: false });
89
+ });
90
+ for (const { node, index, parent, display } of mathNodes) {
91
+ parent.children[index] = wrapNode(renderMath(node.value, display), display);
92
+ }
93
+
94
+ // ── Pass 2: scan ALL text nodes for $...$ and $$...$$ ────────────────────
95
+ const textNodes = [];
96
+ visit(tree, 'text', (node, index, parent) => {
97
+ if (!parent || index == null) return;
98
+ if (node.value.includes('$')) textNodes.push({ node, index, parent });
99
+ });
100
+
101
+ // Reverse so splicing doesn't shift earlier indices.
102
+ for (const { node, index, parent } of textNodes.reverse()) {
103
+ const parts = splitTextNode(node.value);
104
+ if (parts) parent.children.splice(index, 1, ...parts);
105
+ }
106
+
107
+ // ── Pass 3: lift block <div> nodes out of <p> to avoid invalid nesting ───
108
+ const paragraphs = [];
109
+ visit(tree, 'paragraph', (node, index, parent) => {
110
+ if (!parent || index == null) return;
111
+ if (node.children.some(c => c.type === 'html' && c.value.startsWith('<div'))) {
112
+ paragraphs.push({ node, index, parent });
113
+ }
114
+ });
115
+
116
+ for (const { node, index, parent } of paragraphs.reverse()) {
117
+ const replacement = [];
118
+ let buf = [];
119
+
120
+ const flush = () => {
121
+ const kept = buf.filter(c => !(c.type === 'text' && !c.value.trim()));
122
+ if (kept.length) replacement.push({ type: 'paragraph', children: kept });
123
+ buf = [];
124
+ };
125
+
126
+ for (const child of node.children) {
127
+ if (child.type === 'html' && child.value.startsWith('<div')) {
128
+ flush();
129
+ replacement.push(child);
130
+ } else {
131
+ buf.push(child);
132
+ }
133
+ }
134
+ flush();
135
+ parent.children.splice(index, 1, ...replacement);
136
+ }
137
+ };
138
+ }