imprensa 0.1.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.
- package/CHANGELOG.md +15 -0
- package/README.md +51 -0
- package/default.css +239 -0
- package/dist/client-runtime-D7MhMWCo.d.mts +45 -0
- package/dist/components/doc.d.mts +2 -0
- package/dist/components/doc.mjs +2 -0
- package/dist/components/icons.d.mts +23 -0
- package/dist/components/icons.mjs +40 -0
- package/dist/components/index.d.mts +33 -0
- package/dist/components/index.mjs +253 -0
- package/dist/core/client-runtime.d.mts +2 -0
- package/dist/core/client-runtime.mjs +121 -0
- package/dist/core/prerender-core.d.mts +2 -0
- package/dist/core/prerender-core.mjs +2 -0
- package/dist/core/runtime.d.mts +3 -0
- package/dist/core/runtime.mjs +3 -0
- package/dist/core/shiki-build.d.mts +9 -0
- package/dist/core/shiki-build.mjs +34 -0
- package/dist/doc-pager-D-YhwEQN.d.mts +27 -0
- package/dist/doc-toolbar-DUQS2gnK.mjs +460 -0
- package/dist/docs/config.d.mts +13 -0
- package/dist/docs/config.mjs +10 -0
- package/dist/docs/landing-shiki.d.mts +7 -0
- package/dist/docs/landing-shiki.mjs +7 -0
- package/dist/docs/mdx.d.mts +79 -0
- package/dist/docs/mdx.mjs +293 -0
- package/dist/docs/rehype.d.mts +25 -0
- package/dist/docs/rehype.mjs +2 -0
- package/dist/frontmatter-DVneGjCO.mjs +16 -0
- package/dist/global-search-Dfv8DYN3.mjs +310 -0
- package/dist/index.d.mts +41 -0
- package/dist/index.mjs +668 -0
- package/dist/prerender-core-D4Li--RS.mjs +172 -0
- package/dist/prerender-core-DBi9ntWW.d.mts +48 -0
- package/dist/rehype-BWpGaBql.mjs +182 -0
- package/dist/search-store-DDGHRAKl.mjs +64 -0
- package/dist/shiki-gFey7C-z.d.mts +3289 -0
- package/dist/sidebar-layout-DsEhSkJS.mjs +43 -0
- package/dist/socials-BIszPk-A.d.mts +8 -0
- package/docs/architecture.md +26 -0
- package/docs/integration-notes.md +6 -0
- package/index.d.ts +49 -0
- package/package.json +128 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
//#region src/core/prerender-head.ts
|
|
2
|
+
function flattenTagProps(entry) {
|
|
3
|
+
const props = {};
|
|
4
|
+
for (const [key, val] of Object.entries(entry)) if (typeof val === "string") props[key] = val;
|
|
5
|
+
else if (typeof val === "number" || typeof val === "boolean") props[key] = String(val);
|
|
6
|
+
return props;
|
|
7
|
+
}
|
|
8
|
+
function isPlainTag(entry) {
|
|
9
|
+
return !("call" in entry);
|
|
10
|
+
}
|
|
11
|
+
function headMetaEntries(head) {
|
|
12
|
+
if (!Array.isArray(head.meta)) return [];
|
|
13
|
+
return head.meta.flatMap((entry) => typeof entry === "object" && entry !== null && isPlainTag(entry) ? [flattenTagProps(entry)] : []);
|
|
14
|
+
}
|
|
15
|
+
function headLinkEntries(head) {
|
|
16
|
+
if (!Array.isArray(head.link)) return [];
|
|
17
|
+
return head.link.flatMap((entry) => typeof entry === "object" && entry !== null && isPlainTag(entry) ? [flattenTagProps(entry)] : []);
|
|
18
|
+
}
|
|
19
|
+
function appendCanonicalTags(links, meta, canonicalUrl) {
|
|
20
|
+
return {
|
|
21
|
+
links: [...links.filter((link) => link.rel !== "canonical"), {
|
|
22
|
+
rel: "canonical",
|
|
23
|
+
href: canonicalUrl
|
|
24
|
+
}],
|
|
25
|
+
meta: [
|
|
26
|
+
...meta.filter((item) => item.property !== "og:url" && item.name !== "twitter:url"),
|
|
27
|
+
{
|
|
28
|
+
property: "og:url",
|
|
29
|
+
content: canonicalUrl
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "twitter:url",
|
|
33
|
+
content: canonicalUrl
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/core/shiki-client-langs.ts
|
|
40
|
+
function normalizeShikiLangId(lang) {
|
|
41
|
+
return lang === "ts" ? "typescript" : lang;
|
|
42
|
+
}
|
|
43
|
+
/** Grammars from `shiki.langs` (normalized), used for MDX build and browser alike. */
|
|
44
|
+
function resolveShikiLangs(options) {
|
|
45
|
+
if (options === false) return ["typescript"];
|
|
46
|
+
return [...new Set(options?.langs ?? ["typescript"])].map(normalizeShikiLangId);
|
|
47
|
+
}
|
|
48
|
+
/** Theme ids from `shiki.themes` (values), used for MDX build and browser alike. */
|
|
49
|
+
function resolveShikiThemeIds(options) {
|
|
50
|
+
if (options === false) return ["night-owl-light", "houston"];
|
|
51
|
+
const shiki = options ?? {};
|
|
52
|
+
if (!shiki.themes) return ["night-owl-light", "houston"];
|
|
53
|
+
return [...new Set(Object.values(shiki.themes))];
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/core/snippet-shiki.ts
|
|
57
|
+
const WRAPPER_CLASS = "max-w-full overflow-x-auto rounded-xl border border-areia-border text-xs leading-relaxed [&_pre]:min-w-max [&_pre]:p-4 [&_pre]:text-xs [&_pre]:leading-relaxed [&_pre]:!m-0";
|
|
58
|
+
function themePair(shiki) {
|
|
59
|
+
if (shiki === false || !shiki?.themes) return {
|
|
60
|
+
light: "night-owl-light",
|
|
61
|
+
dark: "houston"
|
|
62
|
+
};
|
|
63
|
+
return shiki.themes;
|
|
64
|
+
}
|
|
65
|
+
async function getHighlighter(shiki) {
|
|
66
|
+
const { createConfiguredHighlighterCore } = await import("./core/shiki-build.mjs");
|
|
67
|
+
return createConfiguredHighlighterCore(resolveShikiThemeIds(shiki), resolveShikiLangs(shiki));
|
|
68
|
+
}
|
|
69
|
+
async function codeToSnippetHtml(code, lang, shiki) {
|
|
70
|
+
const h = await getHighlighter(shiki);
|
|
71
|
+
const themes = themePair(shiki);
|
|
72
|
+
return `<div class="${WRAPPER_CLASS}" data-imprensa-snippet>${h.codeToHtml(code, {
|
|
73
|
+
lang,
|
|
74
|
+
themes
|
|
75
|
+
})}</div>`;
|
|
76
|
+
}
|
|
77
|
+
const SNIPPET_SLOT_RE = /(<div data-ilha-slot="[^"]*" data-ilha-props='[^']*'>)(<div class="[^"]*" data-imprensa-snippet>[\s\S]*?<\/div>)(<\/div>)/g;
|
|
78
|
+
/** Paint Snippet islands in prerendered HTML (same Shiki themes as MDX). */
|
|
79
|
+
async function paintSnippetSlotsInHtml(html, shiki) {
|
|
80
|
+
if (shiki === false) return html;
|
|
81
|
+
let out = html;
|
|
82
|
+
const matches = [...html.matchAll(SNIPPET_SLOT_RE)];
|
|
83
|
+
for (const match of matches) {
|
|
84
|
+
const propsMatch = match[1].match(/data-ilha-props='([^']*)'/);
|
|
85
|
+
if (!propsMatch) continue;
|
|
86
|
+
try {
|
|
87
|
+
const props = JSON.parse(propsMatch[1]);
|
|
88
|
+
if (typeof props.code !== "string" || typeof props.lang !== "string") continue;
|
|
89
|
+
const painted = await codeToSnippetHtml(props.code, props.lang, shiki);
|
|
90
|
+
out = out.replace(match[0], `${match[1]}${painted}${match[3]}`);
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/core/prerender-core.ts
|
|
97
|
+
function encodeBase64(value) {
|
|
98
|
+
const bufferCtor = globalThis.Buffer;
|
|
99
|
+
if (bufferCtor) return bufferCtor.from(value, "utf8").toString("base64");
|
|
100
|
+
return btoa(unescape(encodeURIComponent(value)));
|
|
101
|
+
}
|
|
102
|
+
function createPrerender(options) {
|
|
103
|
+
return async function prerender(data) {
|
|
104
|
+
const url = data?.url ?? "/";
|
|
105
|
+
const mdxPage = await options.renderMdx?.(url);
|
|
106
|
+
options.setPrerenderedMdxHtml?.(mdxPage?.html);
|
|
107
|
+
let renderedHtml = await options.pageRouter.renderHydratable(url, options.registry, { snapshot: true });
|
|
108
|
+
if (options.shiki !== false) renderedHtml = await paintSnippetSlotsInHtml(renderedHtml, options.shiki);
|
|
109
|
+
const html = renderedHtml.replace(/<script/gi, "<script").replace(/<\/script>/gi, "</script>");
|
|
110
|
+
const canonicalUrl = options.hostname ? new URL(url, options.hostname).href : void 0;
|
|
111
|
+
const mergedHead = {
|
|
112
|
+
...options.headDefaults,
|
|
113
|
+
...await options.getMdxHead?.(url)
|
|
114
|
+
};
|
|
115
|
+
let linkTags = headLinkEntries(mergedHead);
|
|
116
|
+
let metaTags = headMetaEntries(mergedHead);
|
|
117
|
+
if (canonicalUrl) {
|
|
118
|
+
const canonical = appendCanonicalTags(linkTags, metaTags, canonicalUrl);
|
|
119
|
+
linkTags = canonical.links;
|
|
120
|
+
metaTags = canonical.meta;
|
|
121
|
+
}
|
|
122
|
+
const head = {};
|
|
123
|
+
if (mergedHead.title) head.title = String(mergedHead.title);
|
|
124
|
+
const elements = /* @__PURE__ */ new Set();
|
|
125
|
+
for (const m of metaTags) elements.add({
|
|
126
|
+
type: "meta",
|
|
127
|
+
props: m
|
|
128
|
+
});
|
|
129
|
+
for (const l of linkTags) elements.add({
|
|
130
|
+
type: "link",
|
|
131
|
+
props: l
|
|
132
|
+
});
|
|
133
|
+
if (mdxPage && options.hostname) {
|
|
134
|
+
const pageUrl = new URL(url, options.hostname).href;
|
|
135
|
+
const metaList = metaTags;
|
|
136
|
+
const title = typeof mergedHead.title === "string" && mergedHead.title || metaList.find((m) => m.property === "og:title")?.content || "Documentation";
|
|
137
|
+
const description = metaList.find((m) => m.name === "description")?.content;
|
|
138
|
+
const jsonLd = {
|
|
139
|
+
"@context": "https://schema.org",
|
|
140
|
+
"@type": "TechArticle",
|
|
141
|
+
headline: title,
|
|
142
|
+
url: pageUrl,
|
|
143
|
+
mainEntityOfPage: pageUrl,
|
|
144
|
+
...description ? { description } : {},
|
|
145
|
+
publisher: {
|
|
146
|
+
"@type": "Organization",
|
|
147
|
+
name: "Imprensa",
|
|
148
|
+
url: options.hostname
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
elements.add({
|
|
152
|
+
type: "script",
|
|
153
|
+
props: { type: "application/ld+json" },
|
|
154
|
+
children: JSON.stringify(jsonLd)
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (elements.size > 0) head.elements = elements;
|
|
158
|
+
const links = options.pageRouter.routes().map((route) => route.pattern).filter((link) => !link.includes("*"));
|
|
159
|
+
for (const routePath of options.mdxRoutes ?? []) links.push(routePath);
|
|
160
|
+
return {
|
|
161
|
+
html,
|
|
162
|
+
head: Object.keys(head).length > 0 ? head : void 0,
|
|
163
|
+
links: new Set(links),
|
|
164
|
+
data: mdxPage ? {
|
|
165
|
+
mdxHtmlBase64: encodeBase64(mdxPage.html),
|
|
166
|
+
mdxPath: mdxPage.path
|
|
167
|
+
} : void 0
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
export { resolveShikiThemeIds as i, codeToSnippetHtml as n, resolveShikiLangs as r, createPrerender as t };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { t as ImprensaShikiOptions } from "./shiki-gFey7C-z.mjs";
|
|
2
|
+
import { HydrateOptions, RouterBuilder } from "@ilha/router";
|
|
3
|
+
import { PrerenderArguments } from "vite-prerender-plugin";
|
|
4
|
+
import { Island } from "ilha";
|
|
5
|
+
import { ResolvableHead } from "unhead/types";
|
|
6
|
+
|
|
7
|
+
//#region src/core/ilha-types.d.ts
|
|
8
|
+
/** Island registry produced by `@ilha/router` codegen (`ilha:pages/*`). */
|
|
9
|
+
type ImprensaIslandRegistry = Record<string, Island<Record<string, unknown>, Record<string, unknown>>>;
|
|
10
|
+
/** Re-export router surface used by imprensa runtime and prerender (matches `RouterBuilder`). */
|
|
11
|
+
type ImprensaPageRouter = RouterBuilder;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/core/prerender-core.d.ts
|
|
14
|
+
/** Minimal router surface for apps that re-export codegen `pageRouter` into prerender. */
|
|
15
|
+
type RouterLike = Pick<ImprensaPageRouter, "mount" | "hydrate" | "renderHydratable" | "routes">;
|
|
16
|
+
type RenderedMdx = {
|
|
17
|
+
html: string;
|
|
18
|
+
path: string;
|
|
19
|
+
} | undefined;
|
|
20
|
+
type ImprensaPrerenderOptions = {
|
|
21
|
+
pageRouter: RouterLike;
|
|
22
|
+
registry: ImprensaIslandRegistry;
|
|
23
|
+
mdxRoutes?: Iterable<string>;
|
|
24
|
+
renderMdx?: (url: string) => RenderedMdx | Promise<RenderedMdx>;
|
|
25
|
+
setPrerenderedMdxHtml?: (html: string | undefined) => void;
|
|
26
|
+
getMdxHead?: (url: string) => ResolvableHead | undefined | Promise<ResolvableHead | undefined>;
|
|
27
|
+
headDefaults?: ResolvableHead | null;
|
|
28
|
+
hostname?: string;
|
|
29
|
+
shiki?: ImprensaShikiOptions;
|
|
30
|
+
};
|
|
31
|
+
declare function createPrerender(options: ImprensaPrerenderOptions): (data?: PrerenderArguments) => Promise<{
|
|
32
|
+
html: string;
|
|
33
|
+
head: {
|
|
34
|
+
title?: string;
|
|
35
|
+
elements?: Set<{
|
|
36
|
+
type: string;
|
|
37
|
+
props: Record<string, string>;
|
|
38
|
+
children?: string;
|
|
39
|
+
}>;
|
|
40
|
+
} | undefined;
|
|
41
|
+
links: Set<string>;
|
|
42
|
+
data: {
|
|
43
|
+
mdxHtmlBase64: string;
|
|
44
|
+
mdxPath: string;
|
|
45
|
+
} | undefined;
|
|
46
|
+
}>;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { ImprensaIslandRegistry as a, HydrateOptions as i, RouterLike as n, createPrerender as r, ImprensaPrerenderOptions as t };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
//#region src/docs/rehype/types.ts
|
|
4
|
+
function isLocalLink(value) {
|
|
5
|
+
return typeof value === "string" && value.startsWith("/") && !value.startsWith("//") && !value.startsWith("/__");
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/docs/rehype/route-graph.ts
|
|
9
|
+
const routesFile = path.join(process.cwd(), ".ilha", "routes.ts");
|
|
10
|
+
const pagesDir = path.join(process.cwd(), "src", "pages");
|
|
11
|
+
let routeCache;
|
|
12
|
+
function normalizeRoute(value) {
|
|
13
|
+
try {
|
|
14
|
+
return new URL(value, "http://localhost").pathname.replace(/\/+$/, "") || "/";
|
|
15
|
+
} catch {
|
|
16
|
+
return value.replace(/\/+$/, "") || "/";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function routePatternToRegex(pattern) {
|
|
20
|
+
if (pattern === "/") return /^\/$/;
|
|
21
|
+
const source = pattern.replace(/\/+$/, "").split("/").filter(Boolean).map((segment) => {
|
|
22
|
+
if (segment === "*") return "[^/]+";
|
|
23
|
+
if (segment.startsWith(":")) return "[^/]+";
|
|
24
|
+
return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
+
}).join("/");
|
|
26
|
+
return new RegExp(`^/${source}$`);
|
|
27
|
+
}
|
|
28
|
+
function pageFileToRoute(filePath) {
|
|
29
|
+
const routePath = path.relative(pagesDir, filePath).replace(/\\/g, "/").replace(/\.(?:[cm]?[jt]sx?|mdx?)$/, "").replace(/\/index$/, "").split("/").filter((segment) => !segment.startsWith("+") && !/^\(.+\)$/.test(segment)).join("/");
|
|
30
|
+
if (!routePath || routePath === "index") return "/";
|
|
31
|
+
if (routePath.includes("[")) return void 0;
|
|
32
|
+
return normalizeRoute(`/${routePath}`);
|
|
33
|
+
}
|
|
34
|
+
function walk(dir, callback) {
|
|
35
|
+
if (!fs.existsSync(dir)) return;
|
|
36
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
37
|
+
const entryPath = path.join(dir, entry.name);
|
|
38
|
+
if (entry.isDirectory()) walk(entryPath, callback);
|
|
39
|
+
else callback(entryPath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function getRoutes() {
|
|
43
|
+
if (routeCache) return routeCache;
|
|
44
|
+
const concrete = /* @__PURE__ */ new Set();
|
|
45
|
+
const patterns = [];
|
|
46
|
+
if (fs.existsSync(routesFile)) {
|
|
47
|
+
const source = fs.readFileSync(routesFile, "utf8");
|
|
48
|
+
for (const match of source.matchAll(/\.route\(\s*["'`]([^"'`]+)["'`]/g)) {
|
|
49
|
+
const routePath = normalizeRoute(match[1]);
|
|
50
|
+
if (routePath.includes("**")) continue;
|
|
51
|
+
if (/[*:]/.test(routePath)) patterns.push(routePatternToRegex(routePath));
|
|
52
|
+
else concrete.add(routePath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
walk(pagesDir, (filePath) => {
|
|
56
|
+
if (!/\.(?:[cm]?[jt]sx?|mdx?)$/.test(filePath)) return;
|
|
57
|
+
const routePath = pageFileToRoute(filePath);
|
|
58
|
+
if (routePath) concrete.add(routePath);
|
|
59
|
+
});
|
|
60
|
+
routeCache = {
|
|
61
|
+
concrete,
|
|
62
|
+
patterns
|
|
63
|
+
};
|
|
64
|
+
return routeCache;
|
|
65
|
+
}
|
|
66
|
+
function hasRoute(href) {
|
|
67
|
+
const routePath = normalizeRoute(href);
|
|
68
|
+
const { concrete, patterns } = getRoutes();
|
|
69
|
+
return concrete.has(routePath) || patterns.some((pattern) => pattern.test(routePath));
|
|
70
|
+
}
|
|
71
|
+
function walkPages(callback) {
|
|
72
|
+
walk(pagesDir, callback);
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/docs/rehype/heading-utils.ts
|
|
76
|
+
let anchorCache;
|
|
77
|
+
function textContent(node) {
|
|
78
|
+
if (!node) return "";
|
|
79
|
+
if (typeof node.value === "string") return node.value;
|
|
80
|
+
return (node.children ?? []).map(textContent).join("");
|
|
81
|
+
}
|
|
82
|
+
function slugifyHeading(value) {
|
|
83
|
+
return value.trim().toLowerCase().replace(/<[^>]+>/g, "").replace(/[^\w\s-]/g, "").replace(/\s+/g, "-");
|
|
84
|
+
}
|
|
85
|
+
function collectHeadings(node, headings = []) {
|
|
86
|
+
if (!node) return headings;
|
|
87
|
+
if (node.type === "element" && /^h[1-6]$/.test(node.tagName ?? "")) headings.push(node);
|
|
88
|
+
for (const child of node.children ?? []) if (child) collectHeadings(child, headings);
|
|
89
|
+
return headings;
|
|
90
|
+
}
|
|
91
|
+
function headingId(node) {
|
|
92
|
+
const id = node.properties?.id;
|
|
93
|
+
return typeof id === "string" ? id : slugifyHeading(textContent(node));
|
|
94
|
+
}
|
|
95
|
+
function markdownHeadingIds(source) {
|
|
96
|
+
const ids = /* @__PURE__ */ new Set();
|
|
97
|
+
for (const match of source.matchAll(/^#{1,6}\s+(.+)$/gm)) ids.add(slugifyHeading(match[1].replace(/\s+#+\s*$/, "")));
|
|
98
|
+
return ids;
|
|
99
|
+
}
|
|
100
|
+
function getAnchorMap() {
|
|
101
|
+
if (anchorCache) return anchorCache;
|
|
102
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
103
|
+
walkPages((filePath) => {
|
|
104
|
+
if (!/\.mdx?$/.test(filePath)) return;
|
|
105
|
+
const routePath = pageFileToRoute(filePath);
|
|
106
|
+
if (!routePath) return;
|
|
107
|
+
anchors.set(routePath, markdownHeadingIds(fs.readFileSync(filePath, "utf8")));
|
|
108
|
+
});
|
|
109
|
+
anchorCache = anchors;
|
|
110
|
+
return anchorCache;
|
|
111
|
+
}
|
|
112
|
+
function fail(file, node, rule, reason) {
|
|
113
|
+
const place = node?.position?.start ? {
|
|
114
|
+
line: node.position.start.line,
|
|
115
|
+
column: node.position.start.column
|
|
116
|
+
} : void 0;
|
|
117
|
+
const filePath = file.path ? path.relative(process.cwd(), file.path) : "unknown file";
|
|
118
|
+
const suffix = place?.line ? `:${place.line}:${place.column ?? 1}` : "";
|
|
119
|
+
throw new Error(`${filePath}${suffix} error ${rule} ${reason}`);
|
|
120
|
+
}
|
|
121
|
+
function visitLinks(node, callback) {
|
|
122
|
+
if (!node) return;
|
|
123
|
+
if (node.type === "element" && node.tagName === "a" && isLocalLink(node.properties?.href)) callback(node, node.properties.href);
|
|
124
|
+
for (const child of node.children ?? []) if (child) visitLinks(child, callback);
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/docs/rehype/preview.ts
|
|
128
|
+
function rehypePreview() {
|
|
129
|
+
function visit(node) {
|
|
130
|
+
if (!node.children) return;
|
|
131
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
132
|
+
const child = node.children[i];
|
|
133
|
+
if (child.type === "element" && child.tagName === "pre" && child.children?.[0]?.tagName === "code") {
|
|
134
|
+
const code = child.children[0];
|
|
135
|
+
if (!(code.properties?.metastring ?? code.properties?.meta ?? "").includes("preview")) {
|
|
136
|
+
visit(child);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const text = textContent(code);
|
|
140
|
+
node.children[i] = {
|
|
141
|
+
type: "element",
|
|
142
|
+
tagName: "Preview",
|
|
143
|
+
properties: { code: text },
|
|
144
|
+
children: []
|
|
145
|
+
};
|
|
146
|
+
} else visit(child);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return (tree) => visit(tree);
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/docs/rehype/dead-links.ts
|
|
153
|
+
function rehypeDeadLinks() {
|
|
154
|
+
return (tree, file) => {
|
|
155
|
+
const headings = collectHeadings(tree);
|
|
156
|
+
const h1s = headings.filter((heading) => heading.tagName === "h1");
|
|
157
|
+
if (h1s.length !== 1) fail(file, h1s[1] ?? headings[0], "rehype-heading-structure", `Expected exactly one h1, found ${h1s.length}.`);
|
|
158
|
+
for (let index = 1; index < headings.length; index++) {
|
|
159
|
+
const previousLevel = Number(headings[index - 1].tagName?.slice(1));
|
|
160
|
+
const currentLevel = Number(headings[index].tagName?.slice(1));
|
|
161
|
+
if (currentLevel > previousLevel + 1) fail(file, headings[index], "rehype-heading-structure", `Skipped heading level from h${previousLevel} to h${currentLevel}.`);
|
|
162
|
+
}
|
|
163
|
+
const ids = /* @__PURE__ */ new Set();
|
|
164
|
+
for (const heading of headings) {
|
|
165
|
+
const id = headingId(heading);
|
|
166
|
+
if (ids.has(id)) fail(file, heading, "rehype-duplicate-heading-id", `Duplicate heading id "${id}".`);
|
|
167
|
+
ids.add(id);
|
|
168
|
+
}
|
|
169
|
+
const currentRoute = file.path ? pageFileToRoute(file.path) : void 0;
|
|
170
|
+
visitLinks(tree, (node, href) => {
|
|
171
|
+
const [rawPath, rawHash] = href.split("#");
|
|
172
|
+
const routePath = normalizeRoute(rawPath || currentRoute || "/");
|
|
173
|
+
if (!hasRoute(routePath)) fail(file, node, "rehype-dead-links", `Dead link "${href}". No matching route was found in .ilha/routes.ts or src/pages.`);
|
|
174
|
+
if (rawHash) {
|
|
175
|
+
const anchor = decodeURIComponent(rawHash);
|
|
176
|
+
if (!(routePath === currentRoute ? ids : getAnchorMap().get(routePath))?.has(anchor)) fail(file, node, "rehype-dead-anchor-links", `Dead anchor link "${href}". No heading id "${anchor}" exists on "${routePath}".`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
export { rehypePreview as n, rehypeDeadLinks as t };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import MiniSearch from "minisearch";
|
|
2
|
+
import { searchDocuments } from "imprensa/mdx";
|
|
3
|
+
import { createStore } from "@ilha/store";
|
|
4
|
+
//#region src/components/search-core.tsx
|
|
5
|
+
const searchIndex = new MiniSearch({
|
|
6
|
+
fields: ["title", "text"],
|
|
7
|
+
storeFields: [
|
|
8
|
+
"title",
|
|
9
|
+
"path",
|
|
10
|
+
"text"
|
|
11
|
+
],
|
|
12
|
+
searchOptions: {
|
|
13
|
+
boost: { title: 3 },
|
|
14
|
+
fuzzy: .2,
|
|
15
|
+
prefix: true
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
searchIndex.addAll(searchDocuments);
|
|
19
|
+
function getQueryTerms(query) {
|
|
20
|
+
return query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
function getTextMatchIndex(text, query) {
|
|
23
|
+
const normalizedText = text.toLowerCase();
|
|
24
|
+
return getQueryTerms(query).map((term) => normalizedText.indexOf(term)).filter((index) => index >= 0).sort((a, b) => a - b)[0];
|
|
25
|
+
}
|
|
26
|
+
function getMatchedExcerpt(text, query) {
|
|
27
|
+
const firstMatch = getTextMatchIndex(text, query);
|
|
28
|
+
if (firstMatch === void 0) return void 0;
|
|
29
|
+
const start = Math.max(0, firstMatch - 60);
|
|
30
|
+
const end = Math.min(text.length, firstMatch + 140);
|
|
31
|
+
const excerpt = text.slice(start, end).trim();
|
|
32
|
+
return `${start > 0 ? "…" : ""}${excerpt}${end < text.length ? "…" : ""}`;
|
|
33
|
+
}
|
|
34
|
+
function getSearchResults(query) {
|
|
35
|
+
const trimmedQuery = query.trim();
|
|
36
|
+
if (!trimmedQuery) return [];
|
|
37
|
+
return searchIndex.search(trimmedQuery).slice(0, 8).map((result) => ({
|
|
38
|
+
id: result.id,
|
|
39
|
+
title: result.title,
|
|
40
|
+
path: result.path,
|
|
41
|
+
text: result.text
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/components/search-store.ts
|
|
46
|
+
const searchStore = createStore({
|
|
47
|
+
open: false,
|
|
48
|
+
query: ""
|
|
49
|
+
});
|
|
50
|
+
/** Hoisted for `bind:open` / portaled bridge — same alien-signals graph as islands. */
|
|
51
|
+
const searchOpen = searchStore.bind((s) => s.open);
|
|
52
|
+
const searchQuery = searchStore.bind((s) => s.query);
|
|
53
|
+
function closeSearch() {
|
|
54
|
+
searchOpen(false);
|
|
55
|
+
}
|
|
56
|
+
function openSearch() {
|
|
57
|
+
searchOpen(true);
|
|
58
|
+
}
|
|
59
|
+
function toggleSearch() {
|
|
60
|
+
if (searchOpen()) closeSearch();
|
|
61
|
+
else openSearch();
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { toggleSearch as a, searchQuery as i, openSearch as n, getMatchedExcerpt as o, searchOpen as r, getSearchResults as s, closeSearch as t };
|