@lukoweb/apitogo 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lib/shiki.ts CHANGED
@@ -1,179 +1,179 @@
1
- import rehypeShikiFromHighlighter, {
2
- type RehypeShikiCoreOptions,
3
- } from "@shikijs/rehype/core";
4
- import {
5
- transformerMetaHighlight,
6
- transformerMetaWordHighlight,
7
- } from "@shikijs/transformers";
8
- import type { Root } from "hast";
9
- import { toJsxRuntime } from "hast-util-to-jsx-runtime";
10
- import { createElement, Fragment } from "react";
11
- import { jsx, jsxs } from "react/jsx-runtime";
12
- import type {
13
- BundledTheme,
14
- CodeOptionsMultipleThemes,
15
- HighlighterCore,
16
- } from "shiki";
17
- import { getSingletonHighlighterCore } from "shiki/core";
18
- import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
19
- import type { Pluggable } from "unified";
20
- import { visit } from "unist-util-visit";
21
- import { HIGHLIGHT_CODE_BLOCK_CLASS } from "./shiki-constants.js";
22
- import { cn } from "./util/cn.js";
23
-
24
- export {
25
- HIGHLIGHT_CODE_BLOCK_CLASS,
26
- defaultLanguages,
27
- } from "./shiki-constants.js";
28
-
29
- export const highlighterPromise = getSingletonHighlighterCore({
30
- engine: createJavaScriptRegexEngine({ forgiving: true }),
31
- langAlias: {
32
- markup: "html",
33
- svg: "xml",
34
- mathml: "xml",
35
- atom: "xml",
36
- ssml: "xml",
37
- rss: "xml",
38
- webmanifest: "json",
39
- },
40
- });
41
-
42
- type ThemesRecord = CodeOptionsMultipleThemes<BundledTheme>["themes"];
43
-
44
- const warnedLanguages = new Set<string>();
45
-
46
- const warnUnloadedLanguage = (lang: string, highlighter: HighlighterCore) => {
47
- const resolved = highlighter.resolveLangAlias(lang);
48
- if (
49
- warnedLanguages.has(lang) ||
50
- resolved === "ansi" ||
51
- resolved === "text" ||
52
- highlighter.getLoadedLanguages().includes(resolved)
53
- )
54
- return;
55
- warnedLanguages.add(lang);
56
- // biome-ignore lint/suspicious/noConsole: Intentional warning
57
- console.warn(
58
- `Language "${lang}" is not loaded for syntax highlighting. ` +
59
- `Add it to \`syntaxHighlighting.languages\` in your config. Falling back to plain text.\n` +
60
- `See https://docs.apitogo.com/docs/markdown/code-blocks#configuration`,
61
- );
62
- };
63
-
64
- export const parseMetaString = (str: string) => {
65
- const matches = str.matchAll(
66
- /([a-z0-9]+)(?:=(["'])(.*?)\2|=(.*?)(?:\s|$)|(?:\s|$))/gi,
67
- );
68
- return Object.fromEntries(
69
- Array.from(matches).map((match) => {
70
- const key = match[1];
71
- const raw = match[3] ?? match[4];
72
- const value =
73
- raw == null || raw === "true" ? true : raw === "false" ? false : raw;
74
- return [key, value];
75
- }),
76
- );
77
- };
78
-
79
- export const defaultHighlightOptions = {
80
- themes: {
81
- light: "github-light",
82
- dark: "github-dark",
83
- },
84
- defaultColor: false,
85
- defaultLanguage: "text",
86
- fallbackLanguage: "text",
87
- inline: "tailing-curly-colon",
88
- addLanguageClass: true,
89
- transformers: [transformerMetaHighlight(), transformerMetaWordHighlight()],
90
- parseMetaString,
91
- } satisfies RehypeShikiCoreOptions;
92
-
93
- const rehypeCodeBlockPlugin = () => (tree: Root) => {
94
- visit(tree, "element", (node, _index, parent) => {
95
- if (node.tagName !== "code") return;
96
-
97
- const isCodeBlock = parent?.type === "element" && parent.tagName === "pre";
98
- node.properties.inline = JSON.stringify(!isCodeBlock);
99
-
100
- // Pass through properties from <pre> to <code> so we can handle it in `code` only
101
- if (isCodeBlock) {
102
- node.properties = {
103
- ...node.properties,
104
- ...structuredClone(parent.properties),
105
- class: cn(node.properties.class, parent.properties.class),
106
- };
107
- parent.properties = {};
108
- }
109
- });
110
- };
111
-
112
- const rehypeWarnUnloadedLanguages =
113
- (highlighter: HighlighterCore) => () => (tree: Root) => {
114
- visit(tree, "element", (node) => {
115
- if (!node.properties.className) return;
116
- if (node.tagName !== "code" && node.tagName !== "pre") return;
117
-
118
- const lang = (node.properties.className as string[] | undefined)
119
- ?.find((c) => c.startsWith("language-"))
120
- ?.slice("language-".length);
121
-
122
- if (lang) warnUnloadedLanguage(lang, highlighter);
123
- });
124
- };
125
-
126
- export const createConfiguredShikiRehypePlugins = (
127
- highlighterInstance: HighlighterCore,
128
- themes: ThemesRecord = defaultHighlightOptions.themes,
129
- ) => [
130
- rehypeWarnUnloadedLanguages(highlighterInstance),
131
- [
132
- rehypeShikiFromHighlighter,
133
- highlighterInstance,
134
- { ...defaultHighlightOptions, themes },
135
- ] satisfies Pluggable,
136
- rehypeCodeBlockPlugin,
137
- ];
138
-
139
- export const highlight = (
140
- highlighter: HighlighterCore,
141
- code: string,
142
- lang = "text",
143
- themes: ThemesRecord = defaultHighlightOptions.themes,
144
- meta?: string,
145
- ) => {
146
- const resolved = highlighter.resolveLangAlias(lang);
147
- const effectiveLang = highlighter.getLoadedLanguages().includes(resolved)
148
- ? lang
149
- : "text";
150
-
151
- if (effectiveLang !== lang) warnUnloadedLanguage(lang, highlighter);
152
-
153
- const value = highlighter.codeToHast(code, {
154
- lang: effectiveLang,
155
- ...defaultHighlightOptions,
156
- themes,
157
- ...(meta && { meta: { __raw: meta } }),
158
- });
159
-
160
- // Shiki always outputs <pre><code>...</code></pre>, but callers of this
161
- // function (HighlightedCode, CodeTabs) provide their own wrapper so the
162
- // <pre> is unwanted. We reuse rehypeCodeBlockPlugin to merge <pre> props
163
- // onto <code>, then strip <pre> during JSX conversion.
164
- rehypeCodeBlockPlugin()(value);
165
-
166
- return toJsxRuntime(value, {
167
- Fragment,
168
- jsx,
169
- jsxs,
170
- components: {
171
- pre: (props) => props.children,
172
- code: (props) =>
173
- createElement("code", {
174
- ...props,
175
- className: cn(props.className, HIGHLIGHT_CODE_BLOCK_CLASS),
176
- }),
177
- },
178
- });
179
- };
1
+ import rehypeShikiFromHighlighter, {
2
+ type RehypeShikiCoreOptions,
3
+ } from "@shikijs/rehype/core";
4
+ import {
5
+ transformerMetaHighlight,
6
+ transformerMetaWordHighlight,
7
+ } from "@shikijs/transformers";
8
+ import type { Root } from "hast";
9
+ import { toJsxRuntime } from "hast-util-to-jsx-runtime";
10
+ import { createElement, Fragment } from "react";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ import type {
13
+ BundledTheme,
14
+ CodeOptionsMultipleThemes,
15
+ HighlighterCore,
16
+ } from "shiki";
17
+ import { getSingletonHighlighterCore } from "shiki/core";
18
+ import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
19
+ import type { Pluggable } from "unified";
20
+ import { visit } from "unist-util-visit";
21
+ import { HIGHLIGHT_CODE_BLOCK_CLASS } from "./shiki-constants.js";
22
+ import { cn } from "./util/cn.js";
23
+
24
+ export {
25
+ HIGHLIGHT_CODE_BLOCK_CLASS,
26
+ defaultLanguages,
27
+ } from "./shiki-constants.js";
28
+
29
+ export const highlighterPromise = getSingletonHighlighterCore({
30
+ engine: createJavaScriptRegexEngine({ forgiving: true }),
31
+ langAlias: {
32
+ markup: "html",
33
+ svg: "xml",
34
+ mathml: "xml",
35
+ atom: "xml",
36
+ ssml: "xml",
37
+ rss: "xml",
38
+ webmanifest: "json",
39
+ },
40
+ });
41
+
42
+ type ThemesRecord = CodeOptionsMultipleThemes<BundledTheme>["themes"];
43
+
44
+ const warnedLanguages = new Set<string>();
45
+
46
+ const warnUnloadedLanguage = (lang: string, highlighter: HighlighterCore) => {
47
+ const resolved = highlighter.resolveLangAlias(lang);
48
+ if (
49
+ warnedLanguages.has(lang) ||
50
+ resolved === "ansi" ||
51
+ resolved === "text" ||
52
+ highlighter.getLoadedLanguages().includes(resolved)
53
+ )
54
+ return;
55
+ warnedLanguages.add(lang);
56
+ // biome-ignore lint/suspicious/noConsole: Intentional warning
57
+ console.warn(
58
+ `Language "${lang}" is not loaded for syntax highlighting. ` +
59
+ `Add it to \`syntaxHighlighting.languages\` in your config. Falling back to plain text.\n` +
60
+ `See https://docs.apitogo.com/docs/markdown/code-blocks#configuration`,
61
+ );
62
+ };
63
+
64
+ export const parseMetaString = (str: string) => {
65
+ const matches = str.matchAll(
66
+ /([a-z0-9]+)(?:=(["'])(.*?)\2|=(.*?)(?:\s|$)|(?:\s|$))/gi,
67
+ );
68
+ return Object.fromEntries(
69
+ Array.from(matches).map((match) => {
70
+ const key = match[1];
71
+ const raw = match[3] ?? match[4];
72
+ const value =
73
+ raw == null || raw === "true" ? true : raw === "false" ? false : raw;
74
+ return [key, value];
75
+ }),
76
+ );
77
+ };
78
+
79
+ export const defaultHighlightOptions = {
80
+ themes: {
81
+ light: "github-light",
82
+ dark: "github-dark",
83
+ },
84
+ defaultColor: false,
85
+ defaultLanguage: "text",
86
+ fallbackLanguage: "text",
87
+ inline: "tailing-curly-colon",
88
+ addLanguageClass: true,
89
+ transformers: [transformerMetaHighlight(), transformerMetaWordHighlight()],
90
+ parseMetaString,
91
+ } satisfies RehypeShikiCoreOptions;
92
+
93
+ const rehypeCodeBlockPlugin = () => (tree: Root) => {
94
+ visit(tree, "element", (node, _index, parent) => {
95
+ if (node.tagName !== "code") return;
96
+
97
+ const isCodeBlock = parent?.type === "element" && parent.tagName === "pre";
98
+ node.properties.inline = JSON.stringify(!isCodeBlock);
99
+
100
+ // Pass through properties from <pre> to <code> so we can handle it in `code` only
101
+ if (isCodeBlock) {
102
+ node.properties = {
103
+ ...node.properties,
104
+ ...structuredClone(parent.properties),
105
+ class: cn(node.properties.class, parent.properties.class),
106
+ };
107
+ parent.properties = {};
108
+ }
109
+ });
110
+ };
111
+
112
+ const rehypeWarnUnloadedLanguages =
113
+ (highlighter: HighlighterCore) => () => (tree: Root) => {
114
+ visit(tree, "element", (node) => {
115
+ if (!node.properties.className) return;
116
+ if (node.tagName !== "code" && node.tagName !== "pre") return;
117
+
118
+ const lang = (node.properties.className as string[] | undefined)
119
+ ?.find((c) => c.startsWith("language-"))
120
+ ?.slice("language-".length);
121
+
122
+ if (lang) warnUnloadedLanguage(lang, highlighter);
123
+ });
124
+ };
125
+
126
+ export const createConfiguredShikiRehypePlugins = (
127
+ highlighterInstance: HighlighterCore,
128
+ themes: ThemesRecord = defaultHighlightOptions.themes,
129
+ ) => [
130
+ rehypeWarnUnloadedLanguages(highlighterInstance),
131
+ [
132
+ rehypeShikiFromHighlighter,
133
+ highlighterInstance,
134
+ { ...defaultHighlightOptions, themes },
135
+ ] satisfies Pluggable,
136
+ rehypeCodeBlockPlugin,
137
+ ];
138
+
139
+ export const highlight = (
140
+ highlighter: HighlighterCore,
141
+ code: string,
142
+ lang = "text",
143
+ themes: ThemesRecord = defaultHighlightOptions.themes,
144
+ meta?: string,
145
+ ) => {
146
+ const resolved = highlighter.resolveLangAlias(lang);
147
+ const effectiveLang = highlighter.getLoadedLanguages().includes(resolved)
148
+ ? lang
149
+ : "text";
150
+
151
+ if (effectiveLang !== lang) warnUnloadedLanguage(lang, highlighter);
152
+
153
+ const value = highlighter.codeToHast(code, {
154
+ lang: effectiveLang,
155
+ ...defaultHighlightOptions,
156
+ themes,
157
+ ...(meta && { meta: { __raw: meta } }),
158
+ });
159
+
160
+ // Shiki always outputs <pre><code>...</code></pre>, but callers of this
161
+ // function (HighlightedCode, CodeTabs) provide their own wrapper so the
162
+ // <pre> is unwanted. We reuse rehypeCodeBlockPlugin to merge <pre> props
163
+ // onto <code>, then strip <pre> during JSX conversion.
164
+ rehypeCodeBlockPlugin()(value);
165
+
166
+ return toJsxRuntime(value, {
167
+ Fragment,
168
+ jsx,
169
+ jsxs,
170
+ components: {
171
+ pre: (props) => props.children,
172
+ code: (props) =>
173
+ createElement("code", {
174
+ ...props,
175
+ className: cn(props.className, HIGHLIGHT_CODE_BLOCK_CLASS),
176
+ }),
177
+ },
178
+ });
179
+ };
@@ -15,10 +15,22 @@ import { getZudokuRootDir } from "../cli/common/package-json.js";
15
15
  import { loadZudokuConfig } from "../config/loader.js";
16
16
  import { CdnUrlSchema } from "../config/validators/ZudokuConfig.js";
17
17
  import { joinUrl } from "../lib/util/joinUrl.js";
18
+ import { ensurePagefindDevStub } from "./pagefind-dev-stub.js";
18
19
  import { findPackageRoot } from "./package-root.js";
19
20
  import vitePlugin from "./plugin.js";
20
21
  import { getZuploSystemConfigurations } from "./zuplo.js";
21
22
 
23
+ /** Resolved absolute public directory (Vite default: `<root>/public`). */
24
+ const resolveMergedPublicDir = (
25
+ rootDir: string,
26
+ merged: InlineConfig,
27
+ ): string | undefined => {
28
+ if (merged.publicDir === false) return undefined;
29
+ const rel =
30
+ typeof merged.publicDir === "string" ? merged.publicDir : "public";
31
+ return path.resolve(rootDir, rel);
32
+ };
33
+
22
34
  export type ZudokuConfigEnv = ConfigEnv & {
23
35
  mode: "development" | "production";
24
36
  };
@@ -230,5 +242,13 @@ export async function getViteConfig(
230
242
  }
231
243
  }
232
244
 
245
+ // Create pagefind stub before Vite starts (dep scan / esbuild can open this path before plugin hooks run).
246
+ if (config.search?.type === "pagefind") {
247
+ const publicDir = resolveMergedPublicDir(dir, mergedViteConfig);
248
+ if (publicDir) {
249
+ await ensurePagefindDevStub(publicDir);
250
+ }
251
+ }
252
+
233
253
  return mergedViteConfig;
234
254
  }
@@ -1,17 +1,17 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
-
4
- const NOT_BUILT_YET = 'throw new Error("NOT_BUILT_YET");';
5
-
6
- /**
7
- * Writes a minimal `pagefind.js` under the Vite public dir so dev-time
8
- * `import("/pagefind/pagefind.js")` resolves and the real index can be built later.
9
- */
10
- export async function ensurePagefindDevStub(publicDir: string): Promise<void> {
11
- const pagefindPath = path.join(publicDir, "pagefind/pagefind.js");
12
- const exists = await fs.stat(pagefindPath).catch(() => false);
13
- if (!exists) {
14
- await fs.mkdir(path.dirname(pagefindPath), { recursive: true });
15
- await fs.writeFile(pagefindPath, NOT_BUILT_YET);
16
- }
17
- }
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const NOT_BUILT_YET = 'throw new Error("NOT_BUILT_YET");';
5
+
6
+ /**
7
+ * Writes a minimal `pagefind.js` under the Vite public dir so dev-time
8
+ * `import("/pagefind/pagefind.js")` resolves and the real index can be built later.
9
+ */
10
+ export async function ensurePagefindDevStub(publicDir: string): Promise<void> {
11
+ const pagefindPath = path.join(publicDir, "pagefind/pagefind.js");
12
+ const exists = await fs.stat(pagefindPath).catch(() => false);
13
+ if (!exists) {
14
+ await fs.mkdir(path.dirname(pagefindPath), { recursive: true });
15
+ await fs.writeFile(pagefindPath, NOT_BUILT_YET);
16
+ }
17
+ }
@@ -12,10 +12,7 @@ export const viteSearchPlugin = (): Plugin => {
12
12
  name: "zudoku-search-plugin",
13
13
  async configResolved(config) {
14
14
  resolvedViteConfig = config;
15
- if (
16
- config.publicDir &&
17
- getCurrentConfig()?.search?.type === "pagefind"
18
- ) {
15
+ if (config.publicDir && getCurrentConfig()?.search?.type === "pagefind") {
19
16
  await ensurePagefindDevStub(config.publicDir);
20
17
  }
21
18
  },
@@ -23,10 +20,7 @@ export const viteSearchPlugin = (): Plugin => {
23
20
  if (id === virtualModuleId) {
24
21
  return resolvedVirtualModuleId;
25
22
  }
26
- if (
27
- id === "/pagefind/pagefind.js" &&
28
- resolvedViteConfig?.publicDir
29
- ) {
23
+ if (id === "/pagefind/pagefind.js" && resolvedViteConfig?.publicDir) {
30
24
  return path.join(resolvedViteConfig.publicDir, "pagefind/pagefind.js");
31
25
  }
32
26
  },