@umami/shiso 1.10.0 → 1.12.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 (43) hide show
  1. package/bin/shiso.mjs +20 -1
  2. package/dist/chunks/App.js +363 -29
  3. package/dist/chunks/docs.js +33 -41
  4. package/dist/entry-client.js +1 -1
  5. package/dist/entry-server.js +30 -2
  6. package/docs.schema.json +90 -0
  7. package/mdx.config.ts +13 -97
  8. package/package.json +5 -4
  9. package/scripts/build-runtime.mjs +1 -0
  10. package/scripts/check-content.mjs +358 -0
  11. package/scripts/expand-navigation-globs.mjs +208 -0
  12. package/scripts/expand-openapi-navigation.mjs +94 -0
  13. package/scripts/generate-openapi.mjs +117 -0
  14. package/scripts/generate-search-index.mjs +31 -1
  15. package/scripts/lib/openapi.mjs +653 -0
  16. package/scripts/load-docs-config.mjs +52 -6
  17. package/scripts/load-shiso-config.mjs +45 -3
  18. package/scripts/prerender.mjs +83 -3
  19. package/scripts/vite-docs-config.mjs +27 -6
  20. package/src/App.tsx +0 -1
  21. package/src/components/CodeBlock.tsx +73 -9
  22. package/src/components/DocContent.tsx +24 -3
  23. package/src/components/Docs.tsx +6 -1
  24. package/src/components/OpenApiOperation.tsx +197 -0
  25. package/src/components/SideNav.tsx +8 -2
  26. package/src/components/docs/Card.tsx +1 -1
  27. package/src/components/docs/CodeGroup.tsx +6 -2
  28. package/src/components/docs/Expandable.tsx +1 -1
  29. package/src/components/docs/PropertiesTable.tsx +12 -25
  30. package/src/components/docs/styles.ts +7 -4
  31. package/src/entry-server.tsx +50 -0
  32. package/src/lib/code-blocks.ts +18 -0
  33. package/src/lib/code-meta.ts +87 -0
  34. package/src/lib/docs-config.ts +4 -1
  35. package/src/lib/openapi.generated.ts +4 -0
  36. package/src/lib/openapi.ts +63 -0
  37. package/src/lib/rehype-shiki.ts +196 -0
  38. package/src/lib/site-model.ts +2 -0
  39. package/src/lib/types.ts +116 -2
  40. package/src/styles/global.css +75 -76
  41. package/types/config.d.ts +11 -4
  42. package/vite.config.ts +49 -3
  43. package/CHANGELOG.md +0 -171
@@ -0,0 +1,196 @@
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 lang = resolveLanguage(requested);
137
+ const meta = parseCodeMeta(typeof code.data?.meta === 'string' ? code.data.meta : '');
138
+ // mdast-util-to-hast appends a trailing newline to the code text.
139
+ const source = toText(code).replace(/\n$/, '');
140
+
141
+ const highlighter = await getHighlighter(config.theme);
142
+ if (lang !== 'text') {
143
+ await ensureLanguage(highlighter, lang);
144
+ }
145
+
146
+ const hast = highlighter.codeToHast(source, {
147
+ lang,
148
+ themes: config.theme,
149
+ defaultColor: false,
150
+ transformers: [
151
+ transformerNotationHighlight({ matchAlgorithm: 'v3' }),
152
+ transformerNotationDiff({ matchAlgorithm: 'v3' }),
153
+ shisoLineMarks(new Set(meta.highlightLines), lang === 'diff'),
154
+ ],
155
+ }) as MdNode;
156
+
157
+ // root > pre > code
158
+ const shikiPre = hast.children?.[0] as MdNode;
159
+ const shikiCode = shikiPre?.children?.[0] as MdNode;
160
+ const lines = (shikiCode?.children || []).filter(node => node.tagName === 'span');
161
+ lines.forEach(normalizeLine);
162
+
163
+ const hasNotationDiff =
164
+ lang !== 'diff' && lines.some(line => (line.properties as Properties)['data-diff']);
165
+ const showLineNumbers = meta.showLineNumbers ?? config.lineNumbers;
166
+
167
+ code.children = shikiCode?.children || [];
168
+ pre.properties = {
169
+ ...(pre.properties as Properties),
170
+ 'data-language': lang,
171
+ ...(meta.title ? { 'data-title': meta.title } : {}),
172
+ ...(showLineNumbers ? { 'data-line-numbers': 'true' } : {}),
173
+ ...(meta.startLine ? { 'data-line-start': String(meta.startLine) } : {}),
174
+ 'data-line-count': String(lines.length),
175
+ ...(hasNotationDiff ? { 'data-diff-markers': 'true' } : {}),
176
+ };
177
+ }
178
+
179
+ export function rehypeShiki(config: ResolvedCodeBlockConfig) {
180
+ return async (tree: MdNode) => {
181
+ const tasks: Promise<void>[] = [];
182
+
183
+ walkTree(tree, pre => {
184
+ if (pre.tagName !== 'pre') {
185
+ return;
186
+ }
187
+
188
+ const code = pre.children?.find(child => child.tagName === 'code');
189
+ if (code) {
190
+ tasks.push(highlightBlock(pre, code, config));
191
+ }
192
+ });
193
+
194
+ await Promise.all(tasks);
195
+ };
196
+ }
@@ -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 || [],
package/src/lib/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { PluggableList } from 'unified';
2
+
1
3
  /* ---------------------------------------------------------------------------
2
4
  * Raw docs.json shapes
3
5
  * ------------------------------------------------------------------------- */
@@ -8,6 +10,8 @@ export interface PageObjectItem {
8
10
  label?: string;
9
11
  icon?: string;
10
12
  tag?: string;
13
+ /** HTTP method badge shown in the sidebar, set by OpenAPI navigation entries. */
14
+ method?: string;
11
15
  hidden?: boolean;
12
16
  }
13
17
 
@@ -21,7 +25,12 @@ export interface LinkItem {
21
25
  target?: LinkTarget;
22
26
  }
23
27
 
24
- export type PageItem = string | GroupItem | PageObjectItem | LinkItem;
28
+ export interface GlobItem {
29
+ glob: string;
30
+ exclude?: string[];
31
+ }
32
+
33
+ export type PageItem = string | GroupItem | PageObjectItem | LinkItem | GlobItem;
25
34
 
26
35
  export interface GroupItem {
27
36
  group: string;
@@ -113,6 +122,11 @@ export type LogoOption =
113
122
 
114
123
  /** Project-level settings supplied by shiso.config.ts. Mirrors the public
115
124
  * shape exported from "@umami/shiso/config". */
125
+ export interface MdxConfig {
126
+ remarkPlugins?: PluggableList;
127
+ rehypePlugins?: PluggableList;
128
+ }
129
+
116
130
  export interface ShisoConfig {
117
131
  /** Where docs pages are mounted within the site. Default "/docs"; "" for root. */
118
132
  docsPrefix?: string;
@@ -122,6 +136,8 @@ export interface ShisoConfig {
122
136
  siteUrl?: string;
123
137
  /** Locale used for deterministic date formatting. */
124
138
  locale?: string;
139
+ /** Build-time remark and rehype plugins. */
140
+ mdx?: MdxConfig;
125
141
  }
126
142
 
127
143
  /** ShisoConfig after defaults and normalization, as served by `virtual:shiso-config`. */
@@ -130,6 +146,8 @@ export interface ResolvedShisoConfig {
130
146
  contentDir: string;
131
147
  siteUrl?: string;
132
148
  locale: string;
149
+ /** Build-only compiler hooks. Removed from the virtual browser module. */
150
+ mdx?: MdxConfig;
133
151
  }
134
152
 
135
153
  export type LinkTarget = '_self' | '_blank';
@@ -236,9 +254,26 @@ export interface AppearanceConfig {
236
254
  strict?: boolean;
237
255
  }
238
256
 
257
+ export interface CodeBlockConfig {
258
+ /** Show line numbers on every block. Override per block with `showLineNumbers` or `hideLineNumbers`. */
259
+ lineNumbers?: boolean;
260
+ /** Bundled Shiki theme names for each color mode. */
261
+ theme?: {
262
+ light?: string;
263
+ dark?: string;
264
+ };
265
+ }
266
+
239
267
  export interface StylingConfig {
240
268
  /** Page eyebrow style: the section name (default) or the full breadcrumb path. */
241
269
  eyebrows?: 'section' | 'breadcrumbs';
270
+ /** Fenced code block defaults. */
271
+ codeBlocks?: CodeBlockConfig;
272
+ }
273
+
274
+ export interface ResolvedCodeBlockConfig {
275
+ lineNumbers: boolean;
276
+ theme: { light: string; dark: string };
242
277
  }
243
278
 
244
279
  export interface FontSpec {
@@ -345,6 +380,15 @@ export interface DocsConfig {
345
380
  search?: false | SearchConfig;
346
381
  interaction?: InteractionConfig;
347
382
  contextual?: ContextualConfig;
383
+ /** API reference settings: the OpenAPI spec powering `openapi:` frontmatter pages. */
384
+ api?: ApiConfig;
385
+ }
386
+
387
+ export interface ApiConfig {
388
+ /** Path to a local OpenAPI 3.x spec (JSON or YAML), relative to the project root. */
389
+ spec: string;
390
+ /** Folder inside the content directory for generated endpoint pages. Default "api-reference". */
391
+ directory?: string;
348
392
  }
349
393
 
350
394
  /* ---------------------------------------------------------------------------
@@ -387,6 +431,8 @@ export interface NormalizedDocsPage {
387
431
  tag?: string;
388
432
  /** Module key of the MDX file, e.g. "/content/docs/installation.mdx". */
389
433
  filePath: string;
434
+ /** HTTP method badge for API reference pages. */
435
+ method?: string;
390
436
  }
391
437
 
392
438
  /** A routed docs page in the navigation tree. */
@@ -530,7 +576,10 @@ export interface SiteModel {
530
576
  footer: NormalizedFooter | null;
531
577
  banner: BannerConfig | null;
532
578
  appearance: Required<AppearanceConfig>;
533
- styling: Required<Pick<StylingConfig, 'eyebrows'>>;
579
+ styling: {
580
+ eyebrows: 'section' | 'breadcrumbs';
581
+ codeBlocks: ResolvedCodeBlockConfig;
582
+ };
534
583
  search: import('@/lib/search/config').ResolvedSearchConfig;
535
584
  contextualOptions: ContextualOption[];
536
585
  error404: Required<Pick<Error404Config, 'redirect'>> & Error404Config;
@@ -574,6 +623,8 @@ export interface DocFrontmatter {
574
623
  timestamp?: boolean;
575
624
  /** Related pages rendered above the prev/next pager. */
576
625
  related?: RelatedEntry[];
626
+ /** Binds the page to an API operation, e.g. "GET /users/{id}". */
627
+ openapi?: string;
577
628
  [key: string]: unknown;
578
629
  }
579
630
 
@@ -582,3 +633,66 @@ export interface DocModule {
582
633
  frontmatter?: DocFrontmatter;
583
634
  toc?: TocEntry[];
584
635
  }
636
+
637
+ /* ---------------------------------------------------------------------------
638
+ * OpenAPI reference shapes (produced by scripts/lib/openapi.mjs)
639
+ * ------------------------------------------------------------------------- */
640
+
641
+ export interface SchemaNode {
642
+ name?: string;
643
+ /** Display label such as "string", "User[]", "enum<string>", or "oneOf". */
644
+ type: string;
645
+ required?: boolean;
646
+ deprecated?: boolean;
647
+ description?: string;
648
+ default?: string;
649
+ enum?: string[];
650
+ children?: SchemaNode[];
651
+ }
652
+
653
+ export interface OpenApiSample {
654
+ language: string;
655
+ label: string;
656
+ source: string;
657
+ /** Shiki-highlighted markup generated at build time. */
658
+ html?: string;
659
+ lineCount?: number;
660
+ }
661
+
662
+ export interface OpenApiResponse {
663
+ status: string;
664
+ description?: string;
665
+ contentType?: string;
666
+ schema?: SchemaNode;
667
+ example?: string;
668
+ exampleHtml?: string;
669
+ }
670
+
671
+ export interface NormalizedOperation {
672
+ id: string;
673
+ /** Frontmatter lookup key, e.g. "GET /users/{id}". */
674
+ key: string;
675
+ method: string;
676
+ path: string;
677
+ summary?: string;
678
+ description?: string;
679
+ tags: string[];
680
+ deprecated?: boolean;
681
+ parameters: {
682
+ query: SchemaNode[];
683
+ path: SchemaNode[];
684
+ header: SchemaNode[];
685
+ cookie: SchemaNode[];
686
+ };
687
+ requestBody?: {
688
+ required?: boolean;
689
+ contentType: string;
690
+ schema: SchemaNode;
691
+ example?: string;
692
+ exampleHtml?: string;
693
+ };
694
+ responses: OpenApiResponse[];
695
+ security: string[];
696
+ serverUrl: string;
697
+ samples: OpenApiSample[];
698
+ }
@@ -186,8 +186,8 @@
186
186
  font-size: 0.875rem;
187
187
  }
188
188
 
189
- /* Styled to match the PropertiesTable component: rounded outer border,
190
- horizontal row separators only, and no header fill. */
189
+ /* Shared by GFM tables and the PropertiesTable component: rounded outer border,
190
+ horizontal row separators only, and a card-colored header fill. */
191
191
  .docs-markdown table:not(:where(.not-prose, .not-prose *)) {
192
192
  width: 100%;
193
193
  margin: 1rem 0;
@@ -222,10 +222,19 @@
222
222
  }
223
223
 
224
224
  .docs-markdown th:not(:where(.not-prose, .not-prose *)) {
225
- background: transparent;
225
+ background: var(--card);
226
226
  font-weight: 600;
227
227
  }
228
228
 
229
+ /* The header fill must follow the table's rounded top corners. */
230
+ .docs-markdown th:first-child:not(:where(.not-prose, .not-prose *)) {
231
+ border-top-left-radius: calc(var(--radius-lg) - 1px);
232
+ }
233
+
234
+ .docs-markdown th:last-child:not(:where(.not-prose, .not-prose *)) {
235
+ border-top-right-radius: calc(var(--radius-lg) - 1px);
236
+ }
237
+
229
238
  .docs-markdown tbody tr:last-child td:not(:where(.not-prose, .not-prose *)) {
230
239
  border-bottom: none;
231
240
  }
@@ -247,88 +256,78 @@
247
256
  }
248
257
  }
249
258
 
250
- /* These rules are unlayered so they can override Highlight.js's unlayered theme. */
251
-
252
- code.hljs {
253
- background: transparent;
254
- }
255
-
256
- /* The scroll area owns code-block overflow and padding. */
257
- pre.code-block code.hljs {
258
- overflow: visible;
259
- padding: 0;
260
- }
261
-
262
- /* GitHub Dark syntax colors, scoped so the default GitHub theme stays light. */
263
- [data-theme="dark"] .hljs {
264
- color: #c9d1d9;
265
- }
259
+ @layer components {
260
+ /* Shiki output: one inline-block per line so highlight and diff bands span the
261
+ full scroll width. Line numbers and diff markers are pseudo-elements, which
262
+ keeps them out of the copy button, text selection, and the search index. */
263
+ .code-block .line {
264
+ position: relative;
265
+ display: inline-block;
266
+ box-sizing: border-box;
267
+ min-width: 100%;
268
+ min-height: 1lh;
269
+ padding-inline: 0.75rem 3rem;
270
+ }
266
271
 
267
- [data-theme="dark"]
268
- :is(
269
- .hljs-doctag,
270
- .hljs-keyword,
271
- .hljs-meta .hljs-keyword,
272
- .hljs-template-tag,
273
- .hljs-template-variable,
274
- .hljs-type,
275
- .hljs-variable.language_
276
- ) {
277
- color: #ff7b72;
278
- }
272
+ .code-block .line span {
273
+ color: var(--shiki-light);
274
+ font-style: var(--shiki-light-font-style, inherit);
275
+ font-weight: var(--shiki-light-font-weight, inherit);
276
+ text-decoration: var(--shiki-light-text-decoration, inherit);
277
+ }
279
278
 
280
- [data-theme="dark"]
281
- :is(.hljs-title, .hljs-title.class_, .hljs-title.class_.inherited__, .hljs-title.function_) {
282
- color: #d2a8ff;
283
- }
279
+ [data-theme="dark"] .code-block .line span {
280
+ color: var(--shiki-dark);
281
+ font-style: var(--shiki-dark-font-style, inherit);
282
+ font-weight: var(--shiki-dark-font-weight, inherit);
283
+ text-decoration: var(--shiki-dark-text-decoration, inherit);
284
+ }
284
285
 
285
- [data-theme="dark"]
286
- :is(
287
- .hljs-attr,
288
- .hljs-attribute,
289
- .hljs-literal,
290
- .hljs-meta,
291
- .hljs-number,
292
- .hljs-operator,
293
- .hljs-variable,
294
- .hljs-selector-attr,
295
- .hljs-selector-class,
296
- .hljs-selector-id,
297
- .hljs-section
298
- ) {
299
- color: #79c0ff;
300
- }
286
+ .code-block .line[data-highlighted] {
287
+ background: color-mix(in srgb, var(--primary) 10%, transparent);
288
+ box-shadow: inset 2px 0 var(--primary);
289
+ }
301
290
 
302
- [data-theme="dark"] :is(.hljs-regexp, .hljs-string, .hljs-meta .hljs-string) {
303
- color: #a5d6ff;
304
- }
291
+ .code-block .line[data-diff="add"] {
292
+ background: color-mix(in srgb, #3fb950 14%, transparent);
293
+ }
305
294
 
306
- [data-theme="dark"] :is(.hljs-built_in, .hljs-symbol) {
307
- color: #ffa657;
308
- }
295
+ .code-block .line[data-diff="remove"] {
296
+ background: color-mix(in srgb, #f85149 14%, transparent);
297
+ }
309
298
 
310
- [data-theme="dark"] :is(.hljs-comment, .hljs-code, .hljs-formula) {
311
- color: #8b949e;
312
- }
299
+ .code-block[data-diff-markers] .line {
300
+ padding-inline-start: 1.5rem;
301
+ }
313
302
 
314
- [data-theme="dark"] :is(.hljs-name, .hljs-quote, .hljs-selector-tag, .hljs-selector-pseudo) {
315
- color: #7ee787;
316
- }
303
+ .code-block[data-diff-markers] .line[data-diff]::after {
304
+ position: absolute;
305
+ top: 0;
306
+ left: 0.5rem;
307
+ color: var(--muted-foreground);
308
+ user-select: none;
309
+ }
317
310
 
318
- [data-theme="dark"] :is(.hljs-subst, .hljs-emphasis, .hljs-strong) {
319
- color: #c9d1d9;
320
- }
311
+ .code-block[data-diff-markers] .line[data-diff="add"]::after {
312
+ content: "+";
313
+ }
321
314
 
322
- [data-theme="dark"] .hljs-bullet {
323
- color: #f2cc60;
324
- }
315
+ .code-block[data-diff-markers] .line[data-diff="remove"]::after {
316
+ content: "-";
317
+ }
325
318
 
326
- [data-theme="dark"] .hljs-addition {
327
- color: #aff5b4;
328
- background-color: #033a16;
329
- }
319
+ /* `counter-reset` is set inline by CodeBlock so blocks can start at any line. */
320
+ .code-block[data-line-numbers] .line {
321
+ counter-increment: line;
322
+ }
330
323
 
331
- [data-theme="dark"] .hljs-deletion {
332
- color: #ffdcd7;
333
- background-color: #67060c;
324
+ .code-block[data-line-numbers] .line::before {
325
+ content: counter(line);
326
+ display: inline-block;
327
+ width: var(--code-gutter, 2ch);
328
+ margin-inline-end: 1rem;
329
+ color: var(--muted-foreground);
330
+ text-align: right;
331
+ user-select: none;
332
+ }
334
333
  }
package/types/config.d.ts CHANGED
@@ -1,7 +1,12 @@
1
- /**
2
- * Public types for shiso.config.ts. Kept self-contained (no imports) so the
3
- * config file typechecks in consuming projects without pulling in the runtime.
4
- */
1
+ import type { PluggableList } from 'unified';
2
+
3
+ /** Build-time Markdown and MDX compiler extensions. */
4
+ export interface MdxConfig {
5
+ /** Unified remark plugins, run before Shiso's code-title and table-of-contents transforms. */
6
+ remarkPlugins?: PluggableList;
7
+ /** Unified rehype plugins, run before Shiso's highlighting, image, and heading transforms. */
8
+ rehypePlugins?: PluggableList;
9
+ }
5
10
 
6
11
  /** Project-level settings supplied by shiso.config.ts. All fields are optional. */
7
12
  export interface ShisoConfig {
@@ -13,6 +18,8 @@ export interface ShisoConfig {
13
18
  siteUrl?: string;
14
19
  /** Locale used for deterministic date formatting. Default "en-US". */
15
20
  locale?: string;
21
+ /** Build-time remark and rehype plugins for Markdown and MDX content. */
22
+ mdx?: MdxConfig;
16
23
  }
17
24
 
18
25
  /** Identity helper that types a shiso.config.ts default export. */