lildocs 0.1.21 → 0.1.23

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 (36) hide show
  1. package/dist/cli.mjs +19239 -1145
  2. package/dist/core/github-pages.mjs +1 -1
  3. package/dist/{github-pages-clWLKtga.mjs → github-pages-CbcFNgKi.mjs} +17 -10
  4. package/dist/render/client/.vite/manifest.json +8 -0
  5. package/dist/render/client/assets/client-BCZ1mVzx.js +5311 -0
  6. package/dist/render/server/renderer.mjs +714 -0
  7. package/dist/render/styles.css +1 -1
  8. package/docs/assets/favicon.svg +13 -0
  9. package/docs/assets/logo.svg +12 -0
  10. package/docs/config.json +23 -0
  11. package/docs/features/api-reference.md +34 -0
  12. package/docs/features/content.md +86 -0
  13. package/docs/features/mermaid.md +34 -0
  14. package/docs/features/navigation.md +92 -0
  15. package/docs/features/search.md +28 -0
  16. package/docs/getting-started.md +94 -0
  17. package/docs/guides/github-pages.md +61 -0
  18. package/docs/images/example.svg +5 -0
  19. package/docs/index.md +46 -0
  20. package/docs/reference/cli.md +138 -0
  21. package/docs/reference/configuration.md +137 -0
  22. package/docs/reference/theming.md +218 -0
  23. package/package.json +14 -16
  24. package/dist/render/Layout.tsrx +0 -380
  25. package/dist/render/Search.tsrx +0 -414
  26. package/dist/render/client.tsrx +0 -52
  27. package/dist/render/copy-code.ts +0 -56
  28. package/dist/render/heading-links.ts +0 -85
  29. package/dist/render/navigation.ts +0 -16
  30. package/dist/render/paths.ts +0 -47
  31. package/dist/render/renderPage.tsrx +0 -14
  32. package/dist/render/section-highlight.ts +0 -63
  33. package/dist/render/sidebar.ts +0 -93
  34. package/dist/render/table-viewer.ts +0 -161
  35. package/dist/render/toc-visibility.ts +0 -78
  36. package/dist/render/types.ts +0 -9
@@ -1,414 +0,0 @@
1
- import { useEffect, useMemo, useRef, useState } from "octane";
2
- import { queueSectionHighlight } from "./section-highlight.js";
3
-
4
- export type SearchEntry = {
5
- kind: "page" | "section";
6
- route: string;
7
- title: string;
8
- pageTitle: string;
9
- headings: string[];
10
- text: string;
11
- excerpt: string;
12
- depth?: number;
13
- };
14
-
15
- type SearchMatch = {
16
- entry: SearchEntry;
17
- title: string;
18
- subtitle: string;
19
- score: number;
20
- terms: string[];
21
- type: "title" | "body";
22
- };
23
-
24
- export function SearchBox({
25
- anchorElement,
26
- index,
27
- siteRoot,
28
- preloadedUrls,
29
- issueUrl,
30
- }: {
31
- anchorElement: Element;
32
- index: SearchEntry[];
33
- siteRoot: URL;
34
- preloadedUrls: Set<string>;
35
- issueUrl?: string;
36
- }) @{
37
- const inputRef = useRef<HTMLInputElement | null>(null);
38
- const selectedRef = useRef<HTMLAnchorElement | null>(null);
39
- const [query, setQuery] = useState("");
40
- const [open, setOpen] = useState(false);
41
- const [selectedIndex, setSelectedIndex] = useState(-1);
42
- const matches = useMemo(() => searchIndex(index, query), [index, query]);
43
-
44
- useEffect(() => {
45
- selectedRef.current?.scrollIntoView({ block: "nearest" });
46
- preloadRelativeUrl(
47
- selectedRef.current?.getAttribute("href"),
48
- preloadedUrls,
49
- );
50
- }, [matches, preloadedUrls, selectedIndex]);
51
-
52
- useEffect(() => {
53
- const closeOnOutsidePointer = (event: PointerEvent) => {
54
- if (
55
- event.target instanceof Node && !anchorElement.contains(event.target)
56
- ) {
57
- setOpen(false);
58
- }
59
- };
60
- document.addEventListener("pointerdown", closeOnOutsidePointer);
61
- return () => document.removeEventListener(
62
- "pointerdown",
63
- closeOnOutsidePointer,
64
- );
65
- }, [anchorElement]);
66
-
67
- useEffect(() => {
68
- const toggleSearch = () => {
69
- if (document.activeElement === inputRef.current) {
70
- inputRef.current?.blur();
71
- setOpen(false);
72
- return;
73
- }
74
-
75
- document.documentElement.classList.remove("sidebar-collapsed");
76
- document.dispatchEvent(new CustomEvent("lildocs:sidebar-menu-open"));
77
- inputRef.current?.focus();
78
- setOpen(Boolean(query.trim()));
79
- };
80
- const handleShortcut = (event: KeyboardEvent) => {
81
- const key = event.key.toLowerCase();
82
- const hasCommandModifier = event.metaKey || event.ctrlKey;
83
- if (
84
- hasCommandModifier && (key === "k" || event.shiftKey && key === "f")
85
- ) {
86
- event.preventDefault();
87
- toggleSearch();
88
- }
89
- };
90
-
91
- document.addEventListener("lildocs:search-toggle", toggleSearch);
92
- document.addEventListener("keydown", handleShortcut);
93
- return () => {
94
- document.removeEventListener("lildocs:search-toggle", toggleSearch);
95
- document.removeEventListener("keydown", handleShortcut);
96
- };
97
- }, [query]);
98
-
99
- const clearSearch = () => {
100
- setQuery("");
101
- setSelectedIndex(-1);
102
- setOpen(false);
103
- };
104
-
105
- const selectResult = (nextIndex: number) => {
106
- if (matches.length === 0) {
107
- setSelectedIndex(-1);
108
- return;
109
- }
110
-
111
- setSelectedIndex((nextIndex + matches.length) % matches.length);
112
- };
113
-
114
- <>
115
- <span class="searchIcon ti ti-search" aria-hidden="true" />
116
- <input
117
- ref={inputRef}
118
- id="lildocs-search-input"
119
- type="search"
120
- placeholder="Search docs"
121
- aria-label="Search docs"
122
- aria-expanded={open}
123
- aria-controls="lildocs-search-results"
124
- value={query}
125
- onInput={(event) => {
126
- const nextQuery = (event.currentTarget as HTMLInputElement).value;
127
- setQuery(nextQuery);
128
- setSelectedIndex(nextQuery.trim() ? 0 : -1);
129
- setOpen(Boolean(nextQuery.trim()));
130
- }}
131
- onFocus={() => {
132
- if (query.trim()) {
133
- setOpen(true);
134
- }
135
- }}
136
- onKeyDown={(event) => {
137
- if (!open || matches.length === 0) {
138
- return;
139
- }
140
-
141
- if (event.key === "ArrowDown") {
142
- event.preventDefault();
143
- selectResult(selectedIndex + 1);
144
- return;
145
- }
146
-
147
- if (event.key === "ArrowUp") {
148
- event.preventDefault();
149
- selectResult(selectedIndex - 1);
150
- return;
151
- }
152
-
153
- if (event.key === "Enter") {
154
- event.preventDefault();
155
- const selected = selectedRef.current;
156
- if (selected) {
157
- preloadRelativeUrl(selected.getAttribute("href"), preloadedUrls);
158
- selected.click();
159
- }
160
- return;
161
- }
162
-
163
- if (event.key === "Escape") {
164
- setOpen(false);
165
- }
166
- }}
167
- />
168
- @if (open) {
169
- <div id="lildocs-search-results" class="searchResults" aria-live="polite">
170
- <div class="searchResultsContent">
171
- <header class="searchResultsHeader">
172
- <p class="searchResultsEyebrow">Search</p>
173
- <h2>
174
- Results for
175
- <span>
176
- {query as string}
177
- </span>
178
- </h2>
179
- <p class="searchResultsSummary">
180
- {matches.length}
181
-
182
- {matches.length === 1 ? "result" : "results"}
183
- </p>
184
- </header>
185
- @if (matches.length === 0) {
186
- <section class="searchEmpty">
187
- <p>No pages match this search.</p>
188
- @if (issueUrl) {
189
- <a href={issueUrlForQuery(issueUrl, query)}>
190
- Create a GitHub issue
191
- </a>
192
- }
193
- </section>
194
- } @else {
195
- <div class="searchResultList" role="listbox">
196
- @for (const match of matches; index matchIndex; key `${match.entry.route}:${match.title}`) {
197
- const selected = matchIndex === selectedIndex;
198
- const resultUrl = new URL(match.entry.route, siteRoot).href;
199
- <a
200
- ref={selected ? selectedRef : undefined}
201
- href={resultUrl}
202
- data-search-result="true"
203
- class={`searchResult${selected ? " selected" : ""}`}
204
- role="option"
205
- aria-selected={selected}
206
- onMouseOver={() => setSelectedIndex(matchIndex)}
207
- onClick={() => {
208
- if (match.entry.kind === "section") {
209
- queueSectionHighlight(resultUrl);
210
- }
211
- clearSearch();
212
- }}
213
- >
214
- <span class="searchResultType">
215
- {match.entry.kind === "page" ? "Page" : "Section"}
216
- </span>
217
- <span class="searchResultTitle">{renderHighlighted(
218
- match.title,
219
- match.terms,
220
- )}</span>
221
- <span class="searchResultSnippet">{renderHighlighted(
222
- match.subtitle,
223
- match.terms,
224
- )}</span>
225
- </a>
226
- }
227
- </div>
228
- }
229
- </div>
230
- </div>
231
- }
232
- </>
233
- }
234
-
235
- function issueUrlForQuery(issueUrl: string, query: string) {
236
- const url = new URL(issueUrl);
237
- const trimmedQuery = query.trim();
238
- if (trimmedQuery) {
239
- url.searchParams.set("title", `Missing docs for ${trimmedQuery}`);
240
- url.searchParams.set(
241
- "body",
242
- `I searched the docs for "${trimmedQuery}" but did not find a result.`,
243
- );
244
- }
245
-
246
- return url.href;
247
- }
248
-
249
- function searchIndex(index: SearchEntry[], query: string) {
250
- const normalizedQuery = normalizeQuery(query);
251
- if (!normalizedQuery) {
252
- return [];
253
- }
254
-
255
- const terms = normalizedQuery.split(/\s+/);
256
- const matches = index.map(
257
- (entry) => matchEntry(entry, terms, normalizedQuery),
258
- ).filter((match) => match !== undefined);
259
- const titleMatches = matches.filter((match) => match.type === "title");
260
- const sectionBodyMatches = matches.filter(
261
- (match) => match.type === "body" && match.entry.kind === "section",
262
- );
263
- const bodyMatches =
264
- sectionBodyMatches.length > 0
265
- ? sectionBodyMatches
266
- : matches.filter((match) => match.type === "body");
267
- const visibleMatches =
268
- titleMatches.length < 4 ? [...titleMatches, ...bodyMatches] : titleMatches;
269
-
270
- return visibleMatches.toSorted(
271
- (a, b) => b.score - a.score || a.title.localeCompare(b.title),
272
- ).slice(0, 8);
273
- }
274
-
275
- export function preloadRelativeUrl(
276
- href: string | null | undefined,
277
- preloadedUrls: Set<string>,
278
- ) {
279
- if (!href || href.startsWith("mailto:") || href.startsWith("#")) {
280
- return;
281
- }
282
-
283
- const url = new URL(href, document.baseURI);
284
- if (url.origin !== window.location.origin || preloadedUrls.has(url.href)) {
285
- return;
286
- }
287
-
288
- preloadedUrls.add(url.href);
289
- const preload = document.createElement("link");
290
- preload.rel = "prefetch";
291
- preload.href = url.href;
292
- document.head.append(preload);
293
- }
294
-
295
- function matchEntry(
296
- entry: SearchEntry,
297
- terms: string[],
298
- normalizedQuery: string,
299
- ): SearchMatch | undefined {
300
- const titleScore = scoreText(entry.title, terms, normalizedQuery);
301
- if (titleScore > 0) {
302
- return {
303
- entry,
304
- title: entry.title,
305
- subtitle: subtitleForEntry(entry),
306
- score: (entry.kind === "page" ? 300 : 250) + titleScore,
307
- terms,
308
- type: "title",
309
- };
310
- }
311
-
312
- const bodyScore = scoreText(entry.text, terms, normalizedQuery);
313
- if (bodyScore > 0) {
314
- return {
315
- entry,
316
- title: matchingSnippet(entry.excerpt, terms),
317
- subtitle: subtitleForEntry(entry),
318
- score: 20 + bodyScore,
319
- terms,
320
- type: "body",
321
- };
322
- }
323
-
324
- return undefined;
325
- }
326
-
327
- function scoreText(value: string, terms: string[], normalizedQuery: string) {
328
- const text = value.toLowerCase();
329
- if (!text) {
330
- return 0;
331
- }
332
-
333
- let score = 0;
334
- if (text === normalizedQuery) {
335
- score += 80;
336
- } else if (text.includes(normalizedQuery)) {
337
- score += 40;
338
- }
339
-
340
- for (const term of terms) {
341
- if (text.includes(term)) {
342
- score += 10;
343
- }
344
- }
345
-
346
- return score;
347
- }
348
-
349
- function subtitleForEntry(entry: SearchEntry) {
350
- if (entry.kind === "page") {
351
- return entry.headings.slice(0, 3).join(" / ") || "Page";
352
- }
353
-
354
- const parents = entry.headings.slice(0, -1);
355
- return uniqueParts([entry.pageTitle, ...parents]).join(" / ");
356
- }
357
-
358
- function matchingSnippet(text: string, terms: string[]) {
359
- const paragraphs = text.split(/\n{2,}/).map(normalizeSnippetPart).filter(
360
- Boolean,
361
- );
362
- const paragraph =
363
- paragraphs.find((part) => containsTerms(part, terms)) ?? paragraphs[0] ??
364
- "";
365
- const sentences =
366
- paragraph.match(/[^.!?]+(?:[.!?]+|$)/g)?.map(normalizeSnippetPart) ?? [];
367
-
368
- return sentences.find((sentence) => containsTerms(sentence, terms)) ??
369
- paragraph;
370
- }
371
-
372
- function containsTerms(value: string, terms: string[]) {
373
- const normalized = value.toLocaleLowerCase();
374
- return terms.every((term) => normalized.includes(term));
375
- }
376
-
377
- function normalizeSnippetPart(value: string) {
378
- return value.replace(/\s+/g, " ").trim();
379
- }
380
-
381
- function renderHighlighted(value: string, terms: string[]) {
382
- const matchingTerms = terms.filter(Boolean).toSorted(
383
- (a, b) => b.length - a.length,
384
- );
385
- if (matchingTerms.length === 0) {
386
- return value;
387
- }
388
-
389
- const pattern = new RegExp(`(${matchingTerms.map(escapeRegExp).join(
390
- "|",
391
- )})`, "gi");
392
- return value.split(pattern).map(
393
- (part) => matchingTerms.some((term) => part.toLowerCase() === term)
394
- ? <mark>
395
- {part as string}
396
- </mark>
397
- : (part),
398
- );
399
- }
400
-
401
- function normalizeQuery(value: string) {
402
- return value.replace(/[^\p{L}\p{N}]+/gu, " ").replace(
403
- /\s+/g,
404
- " ",
405
- ).trim().toLowerCase();
406
- }
407
-
408
- function uniqueParts(parts: string[]) {
409
- return parts.filter((part, index) => part && parts.indexOf(part) === index);
410
- }
411
-
412
- function escapeRegExp(value: string) {
413
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
414
- }
@@ -1,52 +0,0 @@
1
- import { createRoot } from "octane";
2
- import { initCopyCode } from "./copy-code.js";
3
- import { initHeadingLinks } from "./heading-links.js";
4
- import { initNavigation } from "./navigation.js";
5
- import { preloadRelativeUrl, SearchBox } from "./Search.tsrx";
6
- import { initSectionHighlights } from "./section-highlight.js";
7
- import { initSidebar } from "./sidebar.js";
8
- import { initTableViewer } from "./table-viewer.js";
9
- import { initTocVisibility } from "./toc-visibility.js";
10
- import type { SearchEntry } from "./Search.tsrx";
11
-
12
- initCopyCode();
13
- initHeadingLinks();
14
- initNavigation();
15
- initSectionHighlights();
16
- initSidebar();
17
- initTableViewer();
18
- initTocVisibility();
19
-
20
- void mountSearch();
21
-
22
- async function mountSearch() {
23
- const root = document.querySelector("#lildocs-search-root");
24
- if (!root) {
25
- return;
26
- }
27
-
28
- const embeddedIndex = document.querySelector("#lildocs-search-index");
29
- const base = new URL(window.lildocsSearchUrl ||
30
- "search-index.json", document.baseURI);
31
- const index = embeddedIndex?.textContent
32
- ? JSON.parse(embeddedIndex.textContent) as SearchEntry[]
33
- : await fetch(base).then((response) => response.json()) as SearchEntry[];
34
- const siteRoot = new URL(".", base);
35
- const preloadedUrls = new Set<string>();
36
-
37
- document.addEventListener("mouseover", (event) => {
38
- const link = (event.target as Element | null)?.closest?.("a[href]");
39
- if (!link) {
40
- return;
41
- }
42
- preloadRelativeUrl(link.getAttribute("href"), preloadedUrls);
43
- });
44
-
45
- createRoot(root).render(SearchBox, {
46
- anchorElement: root,
47
- index,
48
- siteRoot,
49
- preloadedUrls,
50
- issueUrl: window.lildocsIssueUrl,
51
- });
52
- }
@@ -1,56 +0,0 @@
1
- const icons = {
2
- copy: '<span class="ti ti-copy copyCodeIcon" aria-hidden="true"></span>',
3
- check:
4
- '<span class="ti ti-copy-check copyCodeIcon" aria-hidden="true"></span>',
5
- error:
6
- '<span class="ti ti-alert-circle copyCodeIcon" aria-hidden="true"></span>',
7
- };
8
-
9
- export function initCopyCode() {
10
- enhanceCodeBlocks();
11
- document.addEventListener("lildocs:page-view", enhanceCodeBlocks);
12
- }
13
-
14
- function enhanceCodeBlocks() {
15
- const blocks = document.querySelectorAll(".content article pre");
16
- if (!blocks.length || !navigator.clipboard?.writeText) {
17
- return;
18
- }
19
-
20
- for (const block of Array.from(blocks)) {
21
- if (block.parentElement?.classList.contains("copyCodeBlock")) {
22
- continue;
23
- }
24
-
25
- const copyText = block.textContent ?? "";
26
- const wrapper = document.createElement("div");
27
- wrapper.className = "copyCodeBlock";
28
- const button = document.createElement("button");
29
- button.type = "button";
30
- button.className = "copyCodeButton";
31
- button.setAttribute("aria-label", "Copy code");
32
- button.title = "Copy code";
33
- button.innerHTML = icons.copy;
34
-
35
- button.addEventListener("click", async () => {
36
- try {
37
- await navigator.clipboard.writeText(copyText);
38
- button.innerHTML = icons.check;
39
- button.setAttribute("aria-label", "Copied");
40
- button.title = "Copied";
41
- window.setTimeout(() => {
42
- button.innerHTML = icons.copy;
43
- button.setAttribute("aria-label", "Copy code");
44
- button.title = "Copy code";
45
- }, 1600);
46
- } catch {
47
- button.innerHTML = icons.error;
48
- button.setAttribute("aria-label", "Copy failed");
49
- button.title = "Copy failed";
50
- }
51
- });
52
-
53
- block.before(wrapper);
54
- wrapper.append(block, button);
55
- }
56
- }
@@ -1,85 +0,0 @@
1
- const headingSelector =
2
- ".content article h1[id], .content article h2[id], .content article h3[id], .content article h4[id], .content article h5[id], .content article h6[id]";
3
- const headingLinkIcons = {
4
- link: '<span class="ti ti-link" aria-hidden="true"></span>',
5
- check: '<span class="ti ti-check" aria-hidden="true"></span>',
6
- };
7
-
8
- export function initHeadingLinks() {
9
- enhanceHeadingLinks();
10
- document.addEventListener("lildocs:page-view", enhanceHeadingLinks);
11
- document.addEventListener("click", (event) => {
12
- if (!(event.target instanceof Element) || event.target.closest("a")) {
13
- return;
14
- }
15
-
16
- const heading = event.target.closest<HTMLHeadingElement>(headingSelector);
17
- if (!heading) {
18
- return;
19
- }
20
-
21
- void copyHeadingLink(heading);
22
- });
23
- }
24
-
25
- function enhanceHeadingLinks() {
26
- const headings =
27
- document.querySelectorAll<HTMLHeadingElement>(headingSelector);
28
-
29
- for (const heading of Array.from(headings)) {
30
- if (heading.querySelector(".headingLinkIcon")) {
31
- continue;
32
- }
33
-
34
- const icon = document.createElement("button");
35
- icon.type = "button";
36
- icon.className = "headingLinkIcon";
37
- icon.setAttribute("aria-label", "Copy link to heading");
38
- icon.title = "Copy link to heading";
39
- icon.innerHTML = headingLinkIcons.link;
40
- heading.append(icon);
41
- }
42
- }
43
-
44
- async function copyHeadingLink(heading: HTMLHeadingElement) {
45
- const url = new URL(window.location.href);
46
- url.hash = heading.tagName === "H1" ? "" : heading.id;
47
-
48
- try {
49
- await writeClipboard(url.href);
50
- const icon = heading.querySelector<HTMLButtonElement>(".headingLinkIcon");
51
- if (!icon) {
52
- return;
53
- }
54
-
55
- icon.classList.add("headingLinkCopied");
56
- icon.innerHTML = headingLinkIcons.check;
57
- icon.setAttribute("aria-label", "Copied");
58
- icon.title = "Copied";
59
- window.setTimeout(() => {
60
- icon.classList.remove("headingLinkCopied");
61
- icon.innerHTML = headingLinkIcons.link;
62
- icon.setAttribute("aria-label", "Copy link to heading");
63
- icon.title = "Copy link to heading";
64
- }, 1600);
65
- } catch {}
66
- }
67
-
68
- async function writeClipboard(value: string) {
69
- if (navigator.clipboard?.writeText) {
70
- await navigator.clipboard.writeText(value);
71
- return;
72
- }
73
-
74
- const textarea = document.createElement("textarea");
75
- textarea.value = value;
76
- textarea.style.position = "fixed";
77
- textarea.style.opacity = "0";
78
- document.body.append(textarea);
79
- textarea.select();
80
- const copied = document.execCommand("copy");
81
- textarea.remove();
82
- if (!copied) {
83
- throw new Error("Unable to copy heading link");
84
- }
85
- }
@@ -1,16 +0,0 @@
1
- import Swup from "swup";
2
-
3
- export function initNavigation() {
4
- if (window.location.protocol === "file:") {
5
- return;
6
- }
7
-
8
- const swup = new Swup({
9
- containers: ["#swup", "#lildocs-sidebar-navigation"],
10
- animationSelector: '[class*="transition-"]',
11
- });
12
-
13
- swup.hooks.on("page:view", () => {
14
- document.dispatchEvent(new CustomEvent("lildocs:page-view"));
15
- });
16
- }
@@ -1,47 +0,0 @@
1
- function routeDir(route: string) {
2
- const index = route.lastIndexOf("/");
3
- return index === -1 ? "." : route.slice(0, index);
4
- }
5
-
6
- function normalizeRoute(route: string) {
7
- const parts: string[] = [];
8
- for (const part of route.split("/")) {
9
- if (!part || part === ".") {
10
- continue;
11
- }
12
-
13
- if (part === "..") {
14
- parts.pop();
15
- } else {
16
- parts.push(part);
17
- }
18
- }
19
-
20
- return parts.join("/");
21
- }
22
-
23
- export function relativeUrl(fromRoute: string, toRoute: string) {
24
- const fromParts =
25
- routeDir(fromRoute) === "." ? [] : routeDir(fromRoute).split("/");
26
- const toParts = normalizeRoute(toRoute).split("/");
27
- while (
28
- fromParts.length > 0 &&
29
- toParts.length > 0 &&
30
- fromParts[0] === toParts[0]
31
- ) {
32
- fromParts.shift();
33
- toParts.shift();
34
- }
35
-
36
- const relativeParts = [...fromParts.map(() => ".."), ...toParts];
37
- const relative = relativeParts.join("/");
38
- return relative.startsWith(".") ? relative : `./${relative}`;
39
- }
40
-
41
- export function rootRelativeUrl(route: string, target: string) {
42
- const dir = routeDir(route);
43
- const depth = dir === "." ? 0 : dir.split("/").length;
44
- const prefix =
45
- depth === 0 ? "." : Array.from({ length: depth }, () => "..").join("/");
46
- return `${prefix}/${target}`;
47
- }
@@ -1,14 +0,0 @@
1
- import { renderToStaticMarkup } from "octane/server";
2
- import type { RenderPageProps } from "../core/frontend.js";
3
- import { Layout } from "./Layout.tsrx";
4
-
5
- export function renderPage(props: RenderPageProps) {
6
- const { html, css } = renderToStaticMarkup(Layout, props);
7
- const bodyStart = html.indexOf("<div class=\"pageShell\">");
8
- const head =
9
- bodyStart >= 0 ? html.slice(0, bodyStart) : "";
10
- const body =
11
- bodyStart >= 0 ? html.slice(bodyStart) : html;
12
- return `<!doctype html><html lang="en"><head>${head}<style>${props.css}</style>${css}</head>` +
13
- `<body>${body}</body></html>`;
14
- }