@lukoweb/apitogo 0.1.18 → 0.1.20

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
+ };
@@ -24,6 +24,7 @@ import {
24
24
  } from "./config.js";
25
25
  import { getDevHtml } from "./html.js";
26
26
  import { buildPagefindDevIndex } from "./pagefind-dev-index.js";
27
+ import { ensurePagefindDevStub } from "./pagefind-dev-stub.js";
27
28
 
28
29
  const DEFAULT_DEV_PORT = 3000;
29
30
 
@@ -75,6 +76,16 @@ export class DevServer {
75
76
 
76
77
  const server = await this.createNodeServer(config);
77
78
 
79
+ if (config.search?.type === "pagefind" && viteConfig.publicDir !== false) {
80
+ const publicRoot = path.resolve(
81
+ this.options.dir,
82
+ typeof viteConfig.publicDir === "string"
83
+ ? viteConfig.publicDir
84
+ : "public",
85
+ );
86
+ await ensurePagefindDevStub(publicRoot);
87
+ }
88
+
78
89
  const mergedViteConfig = mergeConfig(viteConfig, {
79
90
  server: {
80
91
  hmr: { server },
@@ -179,16 +190,8 @@ export class DevServer {
179
190
  `Server-side rendering ${this.options.ssr ? "enabled" : "disabled"}`,
180
191
  );
181
192
 
182
- if (config.search?.type === "pagefind") {
183
- const pagefindPath = path.join(
184
- vite.config.publicDir,
185
- "pagefind/pagefind.js",
186
- );
187
- const exists = await fs.stat(pagefindPath).catch(() => false);
188
- if (!exists) {
189
- await fs.mkdir(path.dirname(pagefindPath), { recursive: true });
190
- await fs.writeFile(pagefindPath, 'throw new Error("NOT_BUILT_YET");');
191
- }
193
+ if (config.search?.type === "pagefind" && vite.config.publicDir) {
194
+ await ensurePagefindDevStub(vite.config.publicDir);
192
195
  }
193
196
 
194
197
  vite.middlewares.use(async (req, res) => {
@@ -0,0 +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,16 +1,28 @@
1
- import type { Plugin } from "vite";
1
+ import path from "node:path";
2
+ import type { Plugin, ResolvedConfig } from "vite";
2
3
  import { getCurrentConfig } from "../config/loader.js";
4
+ import { ensurePagefindDevStub } from "./pagefind-dev-stub.js";
3
5
 
4
6
  export const viteSearchPlugin = (): Plugin => {
5
7
  const virtualModuleId = "virtual:zudoku-search-plugin";
6
8
  const resolvedVirtualModuleId = `\0${virtualModuleId}`;
9
+ let resolvedViteConfig: ResolvedConfig | undefined;
7
10
 
8
11
  return {
9
12
  name: "zudoku-search-plugin",
13
+ async configResolved(config) {
14
+ resolvedViteConfig = config;
15
+ if (config.publicDir && getCurrentConfig()?.search?.type === "pagefind") {
16
+ await ensurePagefindDevStub(config.publicDir);
17
+ }
18
+ },
10
19
  resolveId(id) {
11
20
  if (id === virtualModuleId) {
12
21
  return resolvedVirtualModuleId;
13
22
  }
23
+ if (id === "/pagefind/pagefind.js" && resolvedViteConfig?.publicDir) {
24
+ return path.join(resolvedViteConfig.publicDir, "pagefind/pagefind.js");
25
+ }
14
26
  },
15
27
  async load(id) {
16
28
  if (id !== resolvedVirtualModuleId) return;