@umami/shiso 1.11.0 → 1.13.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.
Files changed (148) hide show
  1. package/bin/shiso.mjs +20 -1
  2. package/dist/chunks/App.js +412 -51
  3. package/dist/chunks/abnfDiagram-VCTEODGH.js +109 -0
  4. package/dist/chunks/arc.js +130 -0
  5. package/dist/chunks/architecture-7GRP2DOG.js +4 -0
  6. package/dist/chunks/architectureDiagram-5GKGNRK7.js +7466 -0
  7. package/dist/chunks/array.js +8 -0
  8. package/dist/chunks/blockDiagram-I7D4REHJ.js +2546 -0
  9. package/dist/chunks/c4Diagram-7LVT6UL2.js +3654 -0
  10. package/dist/chunks/channel.js +9 -0
  11. package/dist/chunks/chunk-2Q5K7J3B.js +21 -0
  12. package/dist/chunks/chunk-4HAMMTFA.js +6106 -0
  13. package/dist/chunks/chunk-5VM5RSS4.js +21 -0
  14. package/dist/chunks/chunk-75Z2AOVW.js +2185 -0
  15. package/dist/chunks/chunk-DU6HZSFF.js +8729 -0
  16. package/dist/chunks/chunk-F27PBJKO.js +95 -0
  17. package/dist/chunks/chunk-FOHPRMQF.js +27162 -0
  18. package/dist/chunks/chunk-GMAD6QVW.js +2369 -0
  19. package/dist/chunks/chunk-GVQU2GXP.js +58 -0
  20. package/dist/chunks/chunk-IMKFNOWR.js +2593 -0
  21. package/dist/chunks/chunk-JWPE2WC7.js +12 -0
  22. package/dist/chunks/chunk-L3NEJ4N5.js +420 -0
  23. package/dist/chunks/chunk-OSK3NFVY.js +1029 -0
  24. package/dist/chunks/chunk-P2QGCYS3.js +95 -0
  25. package/dist/chunks/chunk-POPQ4Y6H.js +33 -0
  26. package/dist/chunks/chunk-PWAF6VOD.js +467 -0
  27. package/dist/chunks/chunk-SHT3W25Y.js +4813 -0
  28. package/dist/chunks/chunk-SVP7TREG.js +686 -0
  29. package/dist/chunks/chunk-TICWLB2K.js +3800 -0
  30. package/dist/chunks/chunk-XXDRQBXY.js +12 -0
  31. package/dist/chunks/chunk-Y2CYZVJY.js +15 -0
  32. package/dist/chunks/classDiagram-ZZMXUADV.js +33 -0
  33. package/dist/chunks/classDiagram-v2-VYDZK3BY.js +33 -0
  34. package/dist/chunks/cose-bilkent-JH36ORCC.js +4268 -0
  35. package/dist/chunks/cynefin-OW5HDTMX.js +4 -0
  36. package/dist/chunks/cynefinDiagram-5FMLGOSQ.js +482 -0
  37. package/dist/chunks/cytoscape.esm.js +28282 -0
  38. package/dist/chunks/dagre-GXQ25YYZ.js +583 -0
  39. package/dist/chunks/dagre.js +4235 -0
  40. package/dist/chunks/defaultLocale.js +246 -0
  41. package/dist/chunks/diagram-S7CK7UJ4.js +402 -0
  42. package/dist/chunks/diagram-UQ7AKVKN.js +291 -0
  43. package/dist/chunks/diagram-VSXAHHWV.js +553 -0
  44. package/dist/chunks/diagram-VX7I27RA.js +798 -0
  45. package/dist/chunks/diagram-Z3DM3KII.js +186 -0
  46. package/dist/chunks/dist.js +92 -0
  47. package/dist/chunks/docs.js +989 -93
  48. package/dist/chunks/ebnfDiagram-PWID7BFC.js +123 -0
  49. package/dist/chunks/erDiagram-RLTQ6QDP.js +2295 -0
  50. package/dist/chunks/eventmodeling-NTZA5JFV.js +4 -0
  51. package/dist/chunks/flowDiagram-HODETNUW.js +17 -0
  52. package/dist/chunks/ganttDiagram-EL5Y4UJY.js +3928 -0
  53. package/dist/chunks/gitGraph-4MIJSDKK.js +4 -0
  54. package/dist/chunks/gitGraphDiagram-WWUBYQGX.js +1153 -0
  55. package/dist/chunks/graphlib.js +4194 -0
  56. package/dist/chunks/info-A6RAGUB7.js +4 -0
  57. package/dist/chunks/infoDiagram-27XIBGKW.js +25 -0
  58. package/dist/chunks/init.js +14 -0
  59. package/dist/chunks/ishikawaDiagram-5VMMS53U.js +971 -0
  60. package/dist/chunks/journeyDiagram-3NMN7TZE.js +1226 -0
  61. package/dist/chunks/kanban-definition-UXKFOSKX.js +1241 -0
  62. package/dist/chunks/katex.js +26463 -0
  63. package/dist/chunks/line.js +48 -0
  64. package/dist/chunks/linear.js +391 -0
  65. package/dist/chunks/mermaid-parser.core.js +754 -0
  66. package/dist/chunks/mermaid.core.js +4898 -0
  67. package/dist/chunks/mindmap-definition-YA3MSWOX.js +1309 -0
  68. package/dist/chunks/ordinal.js +84 -0
  69. package/dist/chunks/packet-AYTQ26CC.js +4 -0
  70. package/dist/chunks/path.js +106 -0
  71. package/dist/chunks/pegDiagram-XKGWAZYB.js +115 -0
  72. package/dist/chunks/pie-WAS4IAKB.js +4 -0
  73. package/dist/chunks/pieDiagram-E7YTZNPT.js +297 -0
  74. package/dist/chunks/quadrantDiagram-AXDQQJYC.js +2234 -0
  75. package/dist/chunks/radar-RG4KPBEZ.js +4 -0
  76. package/dist/chunks/railroad-74A4TZTK.js +4 -0
  77. package/dist/chunks/railroad-abnf-HS5TGJTU.js +4 -0
  78. package/dist/chunks/railroad-ebnf-LZEXJU2U.js +4 -0
  79. package/dist/chunks/railroad-peg-WCYAUIDC.js +4 -0
  80. package/dist/chunks/railroadDiagram-O6MQD6OU.js +89 -0
  81. package/dist/chunks/requirementDiagram-BXWQKSXE.js +2464 -0
  82. package/dist/chunks/rough.esm.js +1396 -0
  83. package/dist/chunks/sankeyDiagram-P5KCCOFB.js +1308 -0
  84. package/dist/chunks/sequenceDiagram-WJ2MYXX4.js +5445 -0
  85. package/dist/chunks/sizeCapture-INFHLROL.js +56 -0
  86. package/dist/chunks/src.js +2653 -0
  87. package/dist/chunks/stateDiagram-D77RDMKH.js +368 -0
  88. package/dist/chunks/stateDiagram-v2-MP3YSRHH.js +33 -0
  89. package/dist/chunks/swimlanes-42K2YHIH.js +6925 -0
  90. package/dist/chunks/swimlanesDiagram-VR7AAH4N.js +32 -0
  91. package/dist/chunks/timeline-definition-24CTP7MA.js +1507 -0
  92. package/dist/chunks/treeView-Q6P3EWNA.js +4 -0
  93. package/dist/chunks/treemap-WGGIJYW6.js +4 -0
  94. package/dist/chunks/vennDiagram-4TSXK5OY.js +2803 -0
  95. package/dist/chunks/wardley-WFR3VGLG.js +4 -0
  96. package/dist/chunks/wardleyDiagram-VM6X3IG4.js +877 -0
  97. package/dist/chunks/xychartDiagram-S5SC5T6Z.js +2698 -0
  98. package/dist/components.js +2 -2
  99. package/dist/entry-client.js +2 -2
  100. package/dist/entry-server.js +31 -3
  101. package/docs.schema.json +90 -0
  102. package/mdx.config.ts +13 -97
  103. package/package.json +6 -4
  104. package/scripts/build-runtime.mjs +1 -0
  105. package/scripts/check-content.mjs +358 -0
  106. package/scripts/expand-navigation-globs.mjs +277 -0
  107. package/scripts/expand-openapi-navigation.mjs +94 -0
  108. package/scripts/generate-openapi.mjs +117 -0
  109. package/scripts/generate-search-index.mjs +31 -1
  110. package/scripts/lib/openapi.mjs +653 -0
  111. package/scripts/load-docs-config.mjs +52 -6
  112. package/scripts/load-shiso-config.mjs +45 -3
  113. package/scripts/prerender.mjs +83 -3
  114. package/scripts/vite-docs-config.mjs +27 -6
  115. package/src/App.tsx +0 -1
  116. package/src/components/CodeBlock.tsx +108 -8
  117. package/src/components/DocContent.tsx +18 -0
  118. package/src/components/Docs.tsx +45 -17
  119. package/src/components/OpenApiOperation.tsx +205 -0
  120. package/src/components/SideNav.tsx +13 -2
  121. package/src/components/docs/Changelog.tsx +163 -0
  122. package/src/components/docs/CodeGroup.tsx +6 -2
  123. package/src/components/docs/Mermaid.tsx +164 -0
  124. package/src/components/docs/Panel.tsx +23 -0
  125. package/src/components/docs/ParamField.tsx +17 -5
  126. package/src/components/docs/PropertiesTable.tsx +2 -2
  127. package/src/components/docs/ResponseField.tsx +17 -5
  128. package/src/components/docs/Tiles.tsx +59 -0
  129. package/src/components/docs/Tree.tsx +359 -0
  130. package/src/components/docs/Update.tsx +72 -0
  131. package/src/components/docs/index.ts +7 -0
  132. package/src/components/docs/panel-context.tsx +25 -0
  133. package/src/components/docs/styles.ts +49 -0
  134. package/src/components/icons/index.ts +4 -0
  135. package/src/entry-server.tsx +50 -0
  136. package/src/lib/code-blocks.ts +18 -0
  137. package/src/lib/code-meta.ts +87 -0
  138. package/src/lib/docs-config.ts +4 -1
  139. package/src/lib/openapi.generated.ts +4 -0
  140. package/src/lib/openapi.ts +63 -0
  141. package/src/lib/rehype-shiki.ts +213 -0
  142. package/src/lib/site-model.ts +2 -0
  143. package/src/lib/types.ts +116 -2
  144. package/src/styles/global.css +63 -73
  145. package/types/components.d.ts +79 -0
  146. package/types/config.d.ts +11 -4
  147. package/vite.config.ts +49 -3
  148. package/CHANGELOG.md +0 -171
@@ -11,6 +11,7 @@ import {
11
11
  getLocaleByPathname,
12
12
  getRedirects,
13
13
  getSeo,
14
+ siteConfig,
14
15
  siteName,
15
16
  standalonePages,
16
17
  } from '@/lib/site-config';
@@ -27,6 +28,13 @@ export interface SitemapEntry {
27
28
  lastmod?: string;
28
29
  }
29
30
 
31
+ export interface LlmsPage {
32
+ route: string;
33
+ filePath: string;
34
+ title: string;
35
+ description?: string;
36
+ }
37
+
30
38
  /** Base-relative routes for every scope. The prerenderer prepends the deploy base itself. */
31
39
  export function getRoutes(): string[] {
32
40
  return [...docsSite.pages.map(page => page.url), ...standalonePages.map(page => page.path)];
@@ -51,6 +59,46 @@ export function getMarkdownPages(): { route: string; filePath: string }[] {
51
59
  ];
52
60
  }
53
61
 
62
+ /** Navigable Markdown pages used to generate llms.txt and llms-full.txt. */
63
+ export function getLlmsPages(): LlmsPage[] {
64
+ const { indexing } = getSeo();
65
+ const pages: LlmsPage[] = [];
66
+
67
+ for (const page of docsSite.pages) {
68
+ const doc = getDocModule(page.filePath);
69
+
70
+ if (
71
+ doc?.frontmatter?.noindex === true ||
72
+ ((page.hidden || getScopeForPage(docsSite, page).hidden) && indexing !== 'all')
73
+ ) {
74
+ continue;
75
+ }
76
+
77
+ pages.push({
78
+ route: page.url,
79
+ filePath: page.filePath,
80
+ title: doc?.frontmatter?.title || page.label,
81
+ description: doc?.frontmatter?.description,
82
+ });
83
+ }
84
+
85
+ for (const page of standalonePages) {
86
+ if (page.filePath.endsWith('.tsx')) continue;
87
+
88
+ const doc = getDocModule(page.filePath);
89
+ if (doc?.frontmatter?.noindex === true) continue;
90
+
91
+ pages.push({
92
+ route: page.path,
93
+ filePath: page.filePath,
94
+ title: doc?.frontmatter?.title || page.title || page.path,
95
+ description: doc?.frontmatter?.description,
96
+ });
97
+ }
98
+
99
+ return pages;
100
+ }
101
+
54
102
  /**
55
103
  * Absolute URLs for the sitemap, honoring `seo.indexing` and per-page
56
104
  * noindex. Empty when the shiso.config `siteUrl` is not configured, since a sitemap
@@ -106,4 +154,6 @@ export function render(url: string): RenderResult {
106
154
  return { html, head: renderHeadToString(buildHead(url)), htmlAttrs: getLocaleByPathname(url) };
107
155
  }
108
156
 
157
+ export const siteDescription = siteConfig.description;
158
+
109
159
  export { docsHomeUrl, siteName };
@@ -0,0 +1,18 @@
1
+ // Relative import: this module is also loaded by vite.config.ts, which esbuild
2
+ // bundles without applying the '@/' resolve alias.
3
+ import type { ResolvedCodeBlockConfig, StylingConfig } from './types.ts';
4
+
5
+ export const DEFAULT_CODE_THEME = { light: 'github-light', dark: 'github-dark' } as const;
6
+
7
+ /** Applies defaults to `styling.codeBlocks` from docs.json. */
8
+ export function resolveCodeBlockConfig(styling?: StylingConfig): ResolvedCodeBlockConfig {
9
+ const codeBlocks = styling?.codeBlocks;
10
+
11
+ return {
12
+ lineNumbers: codeBlocks?.lineNumbers === true,
13
+ theme: {
14
+ light: codeBlocks?.theme?.light || DEFAULT_CODE_THEME.light,
15
+ dark: codeBlocks?.theme?.dark || DEFAULT_CODE_THEME.dark,
16
+ },
17
+ };
18
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Parses the fenced code meta string (the text after the language tag) into
3
+ * the directives Shiso understands:
4
+ *
5
+ * ```ts title="app.ts" {1,3-5} showLineNumbers=10
6
+ *
7
+ * Anything unrecognized becomes the title, so the bare label form used by
8
+ * `CodeGroup` (```bash npm) keeps working.
9
+ */
10
+
11
+ export interface CodeMeta {
12
+ title?: string;
13
+ highlightLines: number[];
14
+ showLineNumbers?: boolean;
15
+ startLine?: number;
16
+ /** Unrecognized tokens, in order. */
17
+ rest: string[];
18
+ }
19
+
20
+ /** Splits on whitespace while keeping quoted segments (title="my file.ts") intact. */
21
+ function tokenize(meta: string): string[] {
22
+ return meta.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
23
+ }
24
+
25
+ function parseLineList(list: string): number[] {
26
+ const lines = new Set<number>();
27
+
28
+ for (const part of list.split(',')) {
29
+ const range = part.trim();
30
+ if (!range) {
31
+ continue;
32
+ }
33
+
34
+ const [from, to = from] = range.split('-').map(Number);
35
+ if (!Number.isInteger(from) || from < 1 || !Number.isInteger(to)) {
36
+ continue;
37
+ }
38
+
39
+ for (let line = from; line <= to; line += 1) {
40
+ lines.add(line);
41
+ }
42
+ }
43
+
44
+ return [...lines].sort((a, b) => a - b);
45
+ }
46
+
47
+ function unquote(value: string): string {
48
+ return value.replace(/^(["'])(.*)\1$/, '$2');
49
+ }
50
+
51
+ export function parseCodeMeta(meta: string | undefined): CodeMeta {
52
+ const result: CodeMeta = { highlightLines: [], rest: [] };
53
+ const lines = new Set<number>();
54
+
55
+ for (const token of tokenize((meta || '').trim())) {
56
+ const highlight = token.match(/^\{([\d,\s-]+)\}$/);
57
+ const title = token.match(/^title=(.+)$/);
58
+ const numbered = token.match(/^showLineNumbers(?:=(\d+))?$/);
59
+
60
+ if (highlight) {
61
+ for (const line of parseLineList(highlight[1])) {
62
+ lines.add(line);
63
+ }
64
+ } else if (title) {
65
+ result.title = unquote(title[1]);
66
+ } else if (numbered) {
67
+ result.showLineNumbers = true;
68
+ if (numbered[1]) {
69
+ result.startLine = Number(numbered[1]);
70
+ }
71
+ } else if (token === 'hideLineNumbers') {
72
+ result.showLineNumbers = false;
73
+ } else {
74
+ result.rest.push(unquote(token));
75
+ }
76
+ }
77
+
78
+ result.highlightLines = [...lines].sort((a, b) => a - b);
79
+
80
+ // Back-compat: a bare label (```bash npm) is the title, which CodeGroup uses
81
+ // as the tab name.
82
+ if (!result.title && result.rest.length) {
83
+ result.title = result.rest.join(' ');
84
+ }
85
+
86
+ return result;
87
+ }
@@ -391,6 +391,7 @@ interface PendingPage {
391
391
  hidden?: boolean;
392
392
  icon?: string;
393
393
  tag?: string;
394
+ method?: string;
394
395
  }
395
396
 
396
397
  type PendingNode =
@@ -424,7 +425,7 @@ function addPage(
424
425
  pageRef: string,
425
426
  context: WalkContext,
426
427
  state: WalkState,
427
- extra: { label?: string; icon?: string; tag?: string; hidden?: boolean } = {},
428
+ extra: { label?: string; icon?: string; tag?: string; method?: string; hidden?: boolean } = {},
428
429
  ): number {
429
430
  const { fileSlug, slug } = normalizePageReference(pageRef);
430
431
  const order = state.order.value++;
@@ -440,6 +441,7 @@ function addPage(
440
441
  hidden: extra.hidden || context.hidden || undefined,
441
442
  icon: extra.icon,
442
443
  tag: extra.tag,
444
+ method: extra.method,
443
445
  });
444
446
 
445
447
  return order;
@@ -497,6 +499,7 @@ function collectPages(items: PageItem[], context: WalkContext, state: WalkState)
497
499
  label,
498
500
  icon: typeof item.icon === 'string' ? item.icon : undefined,
499
501
  tag: typeof item.tag === 'string' ? item.tag : undefined,
502
+ method: typeof item.method === 'string' ? item.method : undefined,
500
503
  hidden: item.hidden === true,
501
504
  }),
502
505
  });
@@ -0,0 +1,4 @@
1
+ // Build-time alias target. Shiso replaces this module with the project's generated operations.
2
+ import type { NormalizedOperation } from '@/lib/types';
3
+
4
+ export const OPENAPI_OPERATIONS: Record<string, NormalizedOperation> = {};
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Runtime helpers for OpenAPI reference pages. Operation data is produced at
3
+ * build time by scripts/generate-openapi.mjs and reaches the client through
4
+ * the aliased openapi.generated module.
5
+ */
6
+
7
+ import type { BadgeColor } from '@/components/docs/Badge';
8
+ import { OPENAPI_OPERATIONS } from '@/lib/openapi.generated';
9
+ import { createSlugger } from '@/lib/slug';
10
+ import type { NormalizedOperation, TocEntry } from '@/lib/types';
11
+
12
+ export const METHOD_COLORS: Record<string, BadgeColor> = {
13
+ GET: 'green',
14
+ POST: 'blue',
15
+ PUT: 'orange',
16
+ PATCH: 'purple',
17
+ DELETE: 'red',
18
+ };
19
+
20
+ export function methodColor(method?: string): BadgeColor {
21
+ return (method && METHOD_COLORS[method.toUpperCase()]) || 'gray';
22
+ }
23
+
24
+ export function statusColor(status: string): BadgeColor {
25
+ if (status.startsWith('2')) return 'green';
26
+ if (status.startsWith('3')) return 'blue';
27
+ if (status.startsWith('4') || status.startsWith('5')) return 'red';
28
+ return 'gray';
29
+ }
30
+
31
+ /** Looks up the operation bound by an `openapi:` frontmatter value. */
32
+ export function getOperation(key?: unknown): NormalizedOperation | undefined {
33
+ if (typeof key !== 'string' || !key.trim()) {
34
+ return undefined;
35
+ }
36
+
37
+ const [method, ...rest] = key.trim().split(/\s+/);
38
+ return OPENAPI_OPERATIONS[`${method.toUpperCase()} ${rest.join(' ')}`];
39
+ }
40
+
41
+ export function hasParameters(operation: NormalizedOperation): boolean {
42
+ const { query, path, header, cookie } = operation.parameters;
43
+ return (
44
+ query.length + path.length + header.length + cookie.length > 0 || operation.security.length > 0
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Section headings for an operation, in render order. The single source of
50
+ * truth for section ids: the component, the table of contents, the content
51
+ * checker, and the search indexer all derive their anchors from these labels.
52
+ */
53
+ export function operationSections(operation: NormalizedOperation): TocEntry[] {
54
+ const slugger = createSlugger();
55
+ const names = [
56
+ hasParameters(operation) ? 'Parameters' : undefined,
57
+ operation.requestBody ? 'Request body' : undefined,
58
+ operation.responses.length ? 'Responses' : undefined,
59
+ operation.samples.length ? 'Code samples' : undefined,
60
+ ].filter((name): name is string => Boolean(name));
61
+
62
+ return names.map(name => ({ name, id: slugger.slug(name), size: 2 }));
63
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Highlights fenced code blocks with Shiki at build time.
3
+ *
4
+ * Output contract (consumed by `components/CodeBlock.tsx` and global.css):
5
+ * - `<pre>` carries `data-language`, and when present `data-title`,
6
+ * `data-line-numbers`, `data-line-start`, `data-line-count`, `data-diff-markers`.
7
+ * - `<code class="language-x">` keeps its class; its children are Shiki's
8
+ * `<span class="line">` elements, each optionally marked `data-highlighted`
9
+ * or `data-diff="add|remove"`.
10
+ * - Token spans carry only `--shiki-light` / `--shiki-dark` variables so the
11
+ * markup is identical in both color modes and CSS picks the theme.
12
+ */
13
+
14
+ import { transformerNotationDiff, transformerNotationHighlight } from '@shikijs/transformers';
15
+ import {
16
+ bundledLanguages,
17
+ bundledThemes,
18
+ createHighlighter,
19
+ type Highlighter,
20
+ type ShikiTransformer,
21
+ } from 'shiki';
22
+ // Relative imports: this module is also loaded by vite.config.ts, which esbuild
23
+ // bundles without applying the '@/' resolve alias.
24
+ import { parseCodeMeta } from './code-meta.ts';
25
+ import { type MdNode, toText, walkTree } from './mdast.ts';
26
+ import type { ResolvedCodeBlockConfig } from './types.ts';
27
+
28
+ type Properties = Record<string, unknown>;
29
+
30
+ const highlighters = new Map<string, Promise<Highlighter>>();
31
+ const languageLoads = new Map<string, Promise<void>>();
32
+ const warnedLanguages = new Set<string>();
33
+
34
+ function assertTheme(name: string, key: 'light' | 'dark') {
35
+ if (!(name in bundledThemes)) {
36
+ throw new Error(
37
+ `[shiso] Unknown code theme "${name}" in docs.json styling.codeBlocks.theme.${key}. ` +
38
+ 'Use a bundled Shiki theme name (https://shiki.style/themes).',
39
+ );
40
+ }
41
+ }
42
+
43
+ function getHighlighter(theme: ResolvedCodeBlockConfig['theme']): Promise<Highlighter> {
44
+ const key = `${theme.light}|${theme.dark}`;
45
+ let highlighter = highlighters.get(key);
46
+
47
+ if (!highlighter) {
48
+ assertTheme(theme.light, 'light');
49
+ assertTheme(theme.dark, 'dark');
50
+ highlighter = createHighlighter({ themes: [theme.light, theme.dark], langs: [] });
51
+ highlighters.set(key, highlighter);
52
+ }
53
+
54
+ return highlighter;
55
+ }
56
+
57
+ async function ensureLanguage(highlighter: Highlighter, lang: string) {
58
+ if (highlighter.getLoadedLanguages().includes(lang)) {
59
+ return;
60
+ }
61
+
62
+ let load = languageLoads.get(lang);
63
+ if (!load) {
64
+ load = highlighter.loadLanguage(lang as keyof typeof bundledLanguages);
65
+ languageLoads.set(lang, load);
66
+ }
67
+
68
+ await load;
69
+ }
70
+
71
+ function resolveLanguage(requested: string | undefined): string {
72
+ if (!requested || requested === 'text' || requested === 'plaintext' || requested === 'txt') {
73
+ return 'text';
74
+ }
75
+
76
+ if (requested in bundledLanguages) {
77
+ return requested;
78
+ }
79
+
80
+ if (!warnedLanguages.has(requested)) {
81
+ warnedLanguages.add(requested);
82
+ console.warn(`[shiso] No syntax grammar for "${requested}"; rendering it as plain text.`);
83
+ }
84
+
85
+ return 'text';
86
+ }
87
+
88
+ /** Marks meta `{1,3-5}` lines and, for `diff` blocks, the added/removed lines. */
89
+ function shisoLineMarks(highlight: Set<number>, diffLanguage: boolean): ShikiTransformer {
90
+ return {
91
+ name: 'shiso:line-marks',
92
+ line(node, line) {
93
+ if (highlight.has(line)) {
94
+ node.properties['data-highlighted'] = '';
95
+ }
96
+
97
+ if (diffLanguage) {
98
+ const text = toText(node as unknown as MdNode);
99
+ if (/^\+(?!\+\+)/.test(text)) {
100
+ node.properties['data-diff'] = 'add';
101
+ } else if (/^-(?!--)/.test(text)) {
102
+ node.properties['data-diff'] = 'remove';
103
+ }
104
+ }
105
+ },
106
+ };
107
+ }
108
+
109
+ /** Converts transformer classes (`highlighted`, `diff add|remove`) into data attributes. */
110
+ function normalizeLine(line: MdNode) {
111
+ const properties = (line.properties || {}) as Properties;
112
+ // Shiki transformers write `class`; mdast-util-to-hast writes `className`.
113
+ const classes = [properties.class, properties.className]
114
+ .flatMap(value => (Array.isArray(value) ? value : String(value || '').split(' ')))
115
+ .map(String)
116
+ .filter(Boolean);
117
+
118
+ if (classes.includes('highlighted')) {
119
+ properties['data-highlighted'] = '';
120
+ }
121
+ if (classes.includes('diff')) {
122
+ properties['data-diff'] = classes.includes('remove') ? 'remove' : 'add';
123
+ }
124
+
125
+ delete properties.class;
126
+ properties.className = ['line'];
127
+ line.properties = properties;
128
+ }
129
+
130
+ async function highlightBlock(pre: MdNode, code: MdNode, config: ResolvedCodeBlockConfig) {
131
+ const codeProperties = (code.properties || {}) as Properties;
132
+ const classes = Array.isArray(codeProperties.className)
133
+ ? codeProperties.className.map(String)
134
+ : [];
135
+ const requested = classes.map(value => value.match(/^language-(\S+)$/)?.[1]).find(Boolean);
136
+ const meta = parseCodeMeta(typeof code.data?.meta === 'string' ? code.data.meta : '');
137
+
138
+ // Mermaid diagrams render client-side via the <Mermaid> component; keep the
139
+ // raw definition so CodeBlock can hand it over untouched.
140
+ if (requested === 'mermaid') {
141
+ const metaRaw = typeof code.data?.meta === 'string' ? code.data.meta : '';
142
+ const placement = metaRaw.match(/placement=["'](top-left|top-right|bottom-left|bottom-right)["']/)?.[1];
143
+ const actions = metaRaw.match(/actions=\{(true|false)\}/)?.[1];
144
+ pre.properties = {
145
+ ...(pre.properties as Properties),
146
+ 'data-language': 'mermaid',
147
+ ...(meta.title ? { 'data-title': meta.title } : {}),
148
+ ...(placement ? { 'data-placement': placement } : {}),
149
+ ...(actions ? { 'data-actions': actions } : {}),
150
+ };
151
+ return;
152
+ }
153
+
154
+ const lang = resolveLanguage(requested);
155
+ // mdast-util-to-hast appends a trailing newline to the code text.
156
+ const source = toText(code).replace(/\n$/, '');
157
+
158
+ const highlighter = await getHighlighter(config.theme);
159
+ if (lang !== 'text') {
160
+ await ensureLanguage(highlighter, lang);
161
+ }
162
+
163
+ const hast = highlighter.codeToHast(source, {
164
+ lang,
165
+ themes: config.theme,
166
+ defaultColor: false,
167
+ transformers: [
168
+ transformerNotationHighlight({ matchAlgorithm: 'v3' }),
169
+ transformerNotationDiff({ matchAlgorithm: 'v3' }),
170
+ shisoLineMarks(new Set(meta.highlightLines), lang === 'diff'),
171
+ ],
172
+ }) as MdNode;
173
+
174
+ // root > pre > code
175
+ const shikiPre = hast.children?.[0] as MdNode;
176
+ const shikiCode = shikiPre?.children?.[0] as MdNode;
177
+ const lines = (shikiCode?.children || []).filter(node => node.tagName === 'span');
178
+ lines.forEach(normalizeLine);
179
+
180
+ const hasNotationDiff =
181
+ lang !== 'diff' && lines.some(line => (line.properties as Properties)['data-diff']);
182
+ const showLineNumbers = meta.showLineNumbers ?? config.lineNumbers;
183
+
184
+ code.children = shikiCode?.children || [];
185
+ pre.properties = {
186
+ ...(pre.properties as Properties),
187
+ 'data-language': lang,
188
+ ...(meta.title ? { 'data-title': meta.title } : {}),
189
+ ...(showLineNumbers ? { 'data-line-numbers': 'true' } : {}),
190
+ ...(meta.startLine ? { 'data-line-start': String(meta.startLine) } : {}),
191
+ 'data-line-count': String(lines.length),
192
+ ...(hasNotationDiff ? { 'data-diff-markers': 'true' } : {}),
193
+ };
194
+ }
195
+
196
+ export function rehypeShiki(config: ResolvedCodeBlockConfig) {
197
+ return async (tree: MdNode) => {
198
+ const tasks: Promise<void>[] = [];
199
+
200
+ walkTree(tree, pre => {
201
+ if (pre.tagName !== 'pre') {
202
+ return;
203
+ }
204
+
205
+ const code = pre.children?.find(child => child.tagName === 'code');
206
+ if (code) {
207
+ tasks.push(highlightBlock(pre, code, config));
208
+ }
209
+ });
210
+
211
+ await Promise.all(tasks);
212
+ };
213
+ }
@@ -1,3 +1,4 @@
1
+ import { resolveCodeBlockConfig } from '@/lib/code-blocks';
1
2
  import { toAbsoluteUrl, toHref } from '@/lib/paths';
2
3
  import { resolveSearchConfig } from '@/lib/search/config';
3
4
  import type {
@@ -130,6 +131,7 @@ export function resolveSiteModel(
130
131
  },
131
132
  styling: {
132
133
  eyebrows: config.styling?.eyebrows === 'breadcrumbs' ? 'breadcrumbs' : 'section',
134
+ codeBlocks: resolveCodeBlockConfig(config.styling),
133
135
  },
134
136
  search: resolveSearchConfig(config.search),
135
137
  contextualOptions: config.contextual?.options || [],