seemore 1.8.7 → 1.9.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/README.md +14 -2
- package/dist/cli/index.js +266 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +12 -0
- package/package.json +1 -1
- package/src/app/entry.prerender.tsx +39 -1
- package/src/app/export/exportPage.ts +416 -0
- package/src/app/export/standalone.ts +137 -0
- package/src/app/export/themeToggle.ts +12 -0
- package/src/app/layout/DocsLayout.tsx +2 -0
- package/src/app/layout/PageActions.tsx +83 -0
- package/src/app/styles/globals.css +235 -0
- package/src/shared/types.ts +9 -0
package/README.md
CHANGED
|
@@ -81,9 +81,9 @@ Typical use cases include:
|
|
|
81
81
|
- **Full MDX** — when Markdown isn't enough, `.mdx` pages take real JSX: your own React components, inline SVG, custom classes and CSS; `<Callout>`, `<Card>`, `<CodeBlockTabs>` and friends come built in, with no imports to write
|
|
82
82
|
- **Static export** — `seemore build` prerenders every page to its own HTML file, `404.html` included, and adds the conventions individual hosts look for (`_redirects`, `200.html`, `.nojekyll`)
|
|
83
83
|
- **Search built in** — static, zero-setup full-text search out of the box, with shareable highlighted results; [Algolia](https://algolia.com) and [Orama Cloud](https://orama.com) for hosted indexes
|
|
84
|
+
- **Page actions** — an Actions button on every page: export the page as one self-contained HTML file or print it to PDF, with the CLI equivalent in `seemore export <file>`
|
|
84
85
|
- **12 themes** — dark and light follow the system, with a toggle that remembers your choice; your own CSS always wins
|
|
85
86
|
- **Rich Markdown** — GitHub Flavoured Markdown, admonitions, steps, `[[wikilinks]]`, [Mermaid](https://mermaid.js.org) and [D2](https://d2lang.com) diagrams, click-to-zoom images, embedded PDFs
|
|
86
|
-
- **First-class code blocks** — build-time [Shiki](https://shiki.style) highlighting in the theme's own colours, with titles, line numbers, diff markers and focus
|
|
87
87
|
- **Editor integration** — one extension covers VS Code, Cursor, Antigravity and other VS Code-compatible editors, remote workspaces included
|
|
88
88
|
|
|
89
89
|
## View in your browser
|
|
@@ -147,6 +147,16 @@ The result is a `dist/` folder of plain web files: drop it on [Netlify](https://
|
|
|
147
147
|
> [!TIP]
|
|
148
148
|
> Project sites on GitHub Pages live under `username.github.io/my-repo/`, not the root, so set `base` once: `base: '/my-repo/'` (or `--base /my-repo/` on the CLI). Building under GitHub Actions without it set prints the exact line to add.
|
|
149
149
|
|
|
150
|
+
## Export page
|
|
151
|
+
|
|
152
|
+
An **Actions** button above every page exports just that page. **Export as HTML** writes one self-contained file — styles inlined, images embedded, diagrams kept — that opens offline from a double-click, ready to drop into Slack, email or an AI chat. The CLI produces the same HTML file without a browser:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npx seemore export docs/spec.md # writes spec.html next to the Markdown
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Which actions appear — or whether the button exists at all — is one line of config, `pageActions`. See [Configuration](#configuration) and the [features page](https://arifszn.github.io/seemore/features) for the details.
|
|
159
|
+
|
|
150
160
|
## Configuration
|
|
151
161
|
|
|
152
162
|
Optional — a folder with no config file builds correctly everywhere. To adjust things, create `seemore.config.ts` next to your content:
|
|
@@ -165,6 +175,7 @@ export default {
|
|
|
165
175
|
footer: { text: '© 2026' },
|
|
166
176
|
editLink: { base: 'https://github.com/you/repo/edit/main/docs' },
|
|
167
177
|
search: 'static', // or { provider: 'orama-cloud', endpoint, apiKey } / { provider: 'algolia', appId, apiKey, indexName }
|
|
178
|
+
pageActions: ['export-html', 'export-pdf'],
|
|
168
179
|
exclude: ['drafts/**'],
|
|
169
180
|
};
|
|
170
181
|
```
|
|
@@ -319,6 +330,7 @@ Pages are ordered by:
|
|
|
319
330
|
```
|
|
320
331
|
seemore [dir] start the dev server
|
|
321
332
|
seemore build [dir] build a static site into dist/
|
|
333
|
+
seemore export <file> export a page as a standalone HTML file
|
|
322
334
|
|
|
323
335
|
Options
|
|
324
336
|
--port <number> dev server port (default 4040)
|
|
@@ -326,7 +338,7 @@ Options
|
|
|
326
338
|
--open / --no-open open a browser on start (default: no)
|
|
327
339
|
--json print one machine-readable JSON line instead of the summary (dev only)
|
|
328
340
|
--config <path> path to seemore.config.ts
|
|
329
|
-
--out <dir> build output directory (default: dist)
|
|
341
|
+
--out <dir> build output directory (default: dist); for export, where the HTML file is written
|
|
330
342
|
--base <path> subpath the site is served from, e.g. /my-repo/
|
|
331
343
|
-h, --help show this message
|
|
332
344
|
-v, --version show the version
|
package/dist/cli/index.js
CHANGED
|
@@ -72,7 +72,7 @@ var init_paths = __esm({
|
|
|
72
72
|
|
|
73
73
|
// src/cli/index.ts
|
|
74
74
|
import { parseArgs } from "util";
|
|
75
|
-
import
|
|
75
|
+
import pc5 from "picocolors";
|
|
76
76
|
|
|
77
77
|
// src/cli/build.ts
|
|
78
78
|
import { mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync5 } from "fs";
|
|
@@ -130,6 +130,7 @@ var FEATURES = [
|
|
|
130
130
|
"search.highlight",
|
|
131
131
|
"social.cards"
|
|
132
132
|
];
|
|
133
|
+
var ACTION_IDS = ["export-html", "export-pdf"];
|
|
133
134
|
|
|
134
135
|
// src/node/config/features.ts
|
|
135
136
|
var FEATURE_DEFAULTS = {
|
|
@@ -246,6 +247,7 @@ var searchSchema = z.union([
|
|
|
246
247
|
indexName: z.string()
|
|
247
248
|
})
|
|
248
249
|
]);
|
|
250
|
+
var pageActionsSchema = z.array(z.enum(ACTION_IDS)).default(["export-html", "export-pdf"]);
|
|
249
251
|
var configSchema = z.object({
|
|
250
252
|
/**
|
|
251
253
|
* Optional here, but required whenever a config file exists — load.ts enforces that,
|
|
@@ -271,6 +273,7 @@ var configSchema = z.object({
|
|
|
271
273
|
text: z.string().default("Edit this page")
|
|
272
274
|
}).optional(),
|
|
273
275
|
search: searchSchema.default("static"),
|
|
276
|
+
pageActions: pageActionsSchema,
|
|
274
277
|
exclude: z.array(z.string()).default([])
|
|
275
278
|
});
|
|
276
279
|
|
|
@@ -296,6 +299,7 @@ function resolveConfig(input, options) {
|
|
|
296
299
|
footer: parsed.footer,
|
|
297
300
|
editLink: parsed.editLink,
|
|
298
301
|
search,
|
|
302
|
+
pageActions: parsed.pageActions,
|
|
299
303
|
exclude: parsed.exclude,
|
|
300
304
|
root: options.root,
|
|
301
305
|
configFile: options.configFile
|
|
@@ -393,8 +397,8 @@ function slugifySegment(segment) {
|
|
|
393
397
|
function toRoute(file) {
|
|
394
398
|
const posix = toPosix(file);
|
|
395
399
|
const segments = posix.split("/");
|
|
396
|
-
const
|
|
397
|
-
const stem =
|
|
400
|
+
const basename3 = segments.pop() ?? "";
|
|
401
|
+
const stem = basename3.replace(CONTENT_EXT, "");
|
|
398
402
|
const isIndex = INDEX_NAMES.has(stem.toLowerCase());
|
|
399
403
|
const slugs = segments.map(slugifySegment);
|
|
400
404
|
if (!isIndex) slugs.push(slugifySegment(stem));
|
|
@@ -460,10 +464,10 @@ function createLinkResolver(pages, base) {
|
|
|
460
464
|
const dir = withoutExt.split("/").slice(0, -1).join("/");
|
|
461
465
|
if (dir !== "") byPath.set(dir.toLowerCase(), page);
|
|
462
466
|
}
|
|
463
|
-
const
|
|
464
|
-
add(byName,
|
|
465
|
-
const slugged = slugifySegment(
|
|
466
|
-
if (slugged !==
|
|
467
|
+
const basename3 = withoutExt.split("/").pop() ?? "";
|
|
468
|
+
add(byName, basename3.toLowerCase(), page);
|
|
469
|
+
const slugged = slugifySegment(basename3);
|
|
470
|
+
if (slugged !== basename3.toLowerCase()) add(byName, slugged, page);
|
|
467
471
|
}
|
|
468
472
|
const pick = (candidates) => [...candidates].sort((a, b) => {
|
|
469
473
|
const depth = a.file.split("/").length - b.file.split("/").length;
|
|
@@ -523,9 +527,9 @@ function splitHash(value) {
|
|
|
523
527
|
if (index === -1) return [value, void 0];
|
|
524
528
|
return [value.slice(0, index), value.slice(index + 1)];
|
|
525
529
|
}
|
|
526
|
-
function joinPosix(fromDir,
|
|
530
|
+
function joinPosix(fromDir, relative4) {
|
|
527
531
|
const segments = [...fromDir];
|
|
528
|
-
for (const part of toPosix(
|
|
532
|
+
for (const part of toPosix(relative4).split("/")) {
|
|
529
533
|
if (part === "" || part === ".") continue;
|
|
530
534
|
if (part === "..") segments.pop();
|
|
531
535
|
else segments.push(part);
|
|
@@ -1387,6 +1391,7 @@ function clientConfig(ctx) {
|
|
|
1387
1391
|
nav: config.nav,
|
|
1388
1392
|
footer: config.footer,
|
|
1389
1393
|
editLink: config.editLink,
|
|
1394
|
+
pageActions: config.pageActions,
|
|
1390
1395
|
favicon: config.favicon === void 0 ? void 0 : withBase(config.base, `/${toPosix(config.favicon)}`),
|
|
1391
1396
|
search: config.search.provider === "static" ? { provider: "static", from: withBase(config.base, "/api/search.json") } : config.search,
|
|
1392
1397
|
contentRoot: config.root
|
|
@@ -1625,10 +1630,10 @@ async function loadPrerenderModule(ctx, ssrOutDir) {
|
|
|
1625
1630
|
await build(createViteConfig({ ctx, mode: "build", ssrOutDir }));
|
|
1626
1631
|
const entry = join6(ssrOutDir, "entry.prerender.js");
|
|
1627
1632
|
const loaded = await import(pathToFileURL(entry).href);
|
|
1628
|
-
if (typeof loaded.render !== "function" || typeof loaded.listRoutes !== "function") {
|
|
1629
|
-
throw new Error(`seemore: the prerender build at ${entry} did not export \`render\` and \`listRoutes\`.`);
|
|
1633
|
+
if (typeof loaded.render !== "function" || typeof loaded.renderArticle !== "function" || typeof loaded.listRoutes !== "function") {
|
|
1634
|
+
throw new Error(`seemore: the prerender build at ${entry} did not export \`render\`, \`renderArticle\` and \`listRoutes\`.`);
|
|
1630
1635
|
}
|
|
1631
|
-
return { render: loaded.render, listRoutes: loaded.listRoutes };
|
|
1636
|
+
return { render: loaded.render, renderArticle: loaded.renderArticle, listRoutes: loaded.listRoutes };
|
|
1632
1637
|
}
|
|
1633
1638
|
|
|
1634
1639
|
// src/node/social/cards.ts
|
|
@@ -1838,12 +1843,248 @@ async function runDev(options) {
|
|
|
1838
1843
|
};
|
|
1839
1844
|
}
|
|
1840
1845
|
|
|
1846
|
+
// src/cli/export.ts
|
|
1847
|
+
import { existsSync as existsSync4, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, readdirSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
1848
|
+
import { basename as basename2, dirname as dirname8, extname, join as join9, relative as relative3, resolve as resolve6 } from "path";
|
|
1849
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
1850
|
+
import pc4 from "picocolors";
|
|
1851
|
+
import { build as viteBuild2 } from "vite";
|
|
1852
|
+
init_paths();
|
|
1853
|
+
|
|
1854
|
+
// src/app/export/themeToggle.ts
|
|
1855
|
+
var THEME_TOGGLE = `<button type="button" class="seemore-export-theme-toggle" aria-label="Toggle dark mode">
|
|
1856
|
+
<svg class="seemore-icon-light" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
|
|
1857
|
+
<svg class="seemore-icon-dark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
|
|
1858
|
+
</button>`;
|
|
1859
|
+
|
|
1860
|
+
// src/cli/export.ts
|
|
1861
|
+
async function runExport(options) {
|
|
1862
|
+
const target = resolve6(options.cwd, options.file);
|
|
1863
|
+
if (!existsSync4(target) || !statSync(target).isFile()) {
|
|
1864
|
+
throw new Error(`No such file: ${options.file}`);
|
|
1865
|
+
}
|
|
1866
|
+
const contentRoot = resolveContentRoot(options.cwd, dirname8(target));
|
|
1867
|
+
const loaded = await loadConfig({ root: contentRoot, configPath: resolveConfigPath(options) });
|
|
1868
|
+
const config = {
|
|
1869
|
+
...loaded.config,
|
|
1870
|
+
base: options.base === void 0 ? loaded.config.base : normaliseBase(options.base)
|
|
1871
|
+
};
|
|
1872
|
+
if (!config.pageActions.includes("export-html")) {
|
|
1873
|
+
throw new Error(`\`pageActions\` in ${loaded.file ?? "seemore.config.ts"} does not include 'export-html', so this site's pages cannot be exported.`);
|
|
1874
|
+
}
|
|
1875
|
+
const ctx = createContext({ config, contentRoot });
|
|
1876
|
+
const scan2 = ctx.source.current();
|
|
1877
|
+
for (const warning of scan2.warnings) ctx.warnings.add(warning);
|
|
1878
|
+
const errors = ctx.errors();
|
|
1879
|
+
if (errors.length > 0) {
|
|
1880
|
+
throw new Error(`seemore found ${errors.length} problem(s) in ${contentRoot}:
|
|
1881
|
+
|
|
1882
|
+
${errors.join("\n\n")}`);
|
|
1883
|
+
}
|
|
1884
|
+
const page = ctx.pages().find((candidate) => candidate.absPath === canonicalise(target));
|
|
1885
|
+
if (page === void 0) {
|
|
1886
|
+
throw new Error(`${options.file} is not part of this site \u2014 excluded in the config, or outside ${relative3(options.cwd, contentRoot) || "."}.`);
|
|
1887
|
+
}
|
|
1888
|
+
const outDir = mkdtempSync2(join9(tmpdir3(), "seemore-export-"));
|
|
1889
|
+
const ssrOutDir = mkdtempSync2(join9(tmpdir3(), "seemore-export-ssr-"));
|
|
1890
|
+
try {
|
|
1891
|
+
await viteBuild2(createViteConfig({ ctx, mode: "build", outDir }));
|
|
1892
|
+
const css = readBuiltCss(outDir);
|
|
1893
|
+
const template = readFileSync6(join9(outDir, "index.html"), "utf8");
|
|
1894
|
+
const prerender = await loadPrerenderModule(ctx, ssrOutDir);
|
|
1895
|
+
const article = await prerender.renderArticle(page.url);
|
|
1896
|
+
const runtime = await bundleRuntime();
|
|
1897
|
+
const html = assemble({ article, css, runtime, config, outDir, contentRoot, template });
|
|
1898
|
+
const filename = `${basename2(target).replace(/\.(?:md|mdx)$/i, "")}.html`;
|
|
1899
|
+
const targetPath = options.out === void 0 ? join9(dirname8(target), filename) : join9(resolve6(options.cwd, options.out), filename);
|
|
1900
|
+
if (existsSync4(targetPath)) {
|
|
1901
|
+
console.log(pc4.yellow(`seemore replacing existing ${relative3(options.cwd, targetPath) || targetPath}`));
|
|
1902
|
+
}
|
|
1903
|
+
mkdirSync4(dirname8(targetPath), { recursive: true });
|
|
1904
|
+
writeFileSync6(targetPath, html, "utf8");
|
|
1905
|
+
ctx.warnings.flush();
|
|
1906
|
+
console.log(pc4.green(`seemore wrote ${relative3(options.cwd, targetPath) || targetPath} (${formatBytes2(html.length)})`));
|
|
1907
|
+
} finally {
|
|
1908
|
+
rmSync2(outDir, { recursive: true, force: true });
|
|
1909
|
+
rmSync2(ssrOutDir, { recursive: true, force: true });
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
function readBuiltCss(outDir) {
|
|
1913
|
+
const assets = join9(outDir, "assets");
|
|
1914
|
+
const files = existsSync4(assets) ? readdirSync(assets).filter((file) => file.endsWith(".css")) : [];
|
|
1915
|
+
if (files.length === 0) throw new Error("The build produced no stylesheet \u2014 the export would be unstyled.");
|
|
1916
|
+
return files.map((file) => readFileSync6(join9(assets, file), "utf8")).join("\n");
|
|
1917
|
+
}
|
|
1918
|
+
async function bundleRuntime() {
|
|
1919
|
+
const dir = mkdtempSync2(join9(tmpdir3(), "seemore-export-runtime-"));
|
|
1920
|
+
try {
|
|
1921
|
+
await viteBuild2({
|
|
1922
|
+
configFile: false,
|
|
1923
|
+
root: appRoot(),
|
|
1924
|
+
build: {
|
|
1925
|
+
outDir: dir,
|
|
1926
|
+
emptyOutDir: true,
|
|
1927
|
+
minify: true,
|
|
1928
|
+
target: "es2018",
|
|
1929
|
+
lib: {
|
|
1930
|
+
entry: join9(appRoot(), "export", "standalone.ts"),
|
|
1931
|
+
name: "seemoreExport",
|
|
1932
|
+
formats: ["iife"],
|
|
1933
|
+
fileName: () => "runtime.js"
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
return readFileSync6(join9(dir, "runtime.js"), "utf8");
|
|
1938
|
+
} finally {
|
|
1939
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
function assemble(input) {
|
|
1943
|
+
const { article, css, runtime, config, outDir, contentRoot, template } = input;
|
|
1944
|
+
const content = withExportToc(inlineHtmlAssets(article.html, config.base, outDir, contentRoot));
|
|
1945
|
+
const title = article.title === "" ? config.title : `${article.title} \xB7 ${config.title}`;
|
|
1946
|
+
const description = article.description ?? config.description;
|
|
1947
|
+
const favicon = faviconLink(config, contentRoot, template);
|
|
1948
|
+
return [
|
|
1949
|
+
"<!doctype html>",
|
|
1950
|
+
'<html lang="en">',
|
|
1951
|
+
"<head>",
|
|
1952
|
+
'<meta charset="utf-8">',
|
|
1953
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
1954
|
+
`<title>${escapeHtml(title)}</title>`,
|
|
1955
|
+
description === void 0 ? "" : `<meta name="description" content="${escapeHtml(description)}">`,
|
|
1956
|
+
'<meta name="generator" content="seemore">',
|
|
1957
|
+
favicon,
|
|
1958
|
+
`<style>
|
|
1959
|
+
${inlineCssUrls(css, config.base, outDir, contentRoot).replaceAll("</style", "<\\/style")}</style>`,
|
|
1960
|
+
"</head>",
|
|
1961
|
+
"<body>",
|
|
1962
|
+
`<main class="seemore-export-main">`,
|
|
1963
|
+
`<article class="seemore-article prose">${content}</article>`,
|
|
1964
|
+
"</main>",
|
|
1965
|
+
THEME_TOGGLE,
|
|
1966
|
+
`<script>${runtime.replaceAll("</script", "<\\/script")}</script>`,
|
|
1967
|
+
"</body>",
|
|
1968
|
+
"</html>"
|
|
1969
|
+
].filter((line) => line !== "").join("\n");
|
|
1970
|
+
}
|
|
1971
|
+
function inlineHtmlAssets(html, base, outDir, contentRoot) {
|
|
1972
|
+
return html.replace(
|
|
1973
|
+
/(<(?:img|embed|iframe|video|audio|source)\b[^>]*\bsrc=")([^"]*)(")/g,
|
|
1974
|
+
(whole, open, src, close) => {
|
|
1975
|
+
const file = resolveAsset(src, base, outDir, contentRoot);
|
|
1976
|
+
if (file === void 0) return whole;
|
|
1977
|
+
try {
|
|
1978
|
+
return `${open}${dataUriFor(file)}${close}`;
|
|
1979
|
+
} catch {
|
|
1980
|
+
return whole;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
function inlineCssUrls(css, base, outDir, contentRoot) {
|
|
1986
|
+
return css.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/g, (whole, _quote, raw) => {
|
|
1987
|
+
if (/^(?:data:|https?:)/i.test(raw)) return whole;
|
|
1988
|
+
const file = resolveAsset(raw, base, outDir, contentRoot);
|
|
1989
|
+
if (file === void 0) return whole;
|
|
1990
|
+
try {
|
|
1991
|
+
return `url("${dataUriFor(file)}")`;
|
|
1992
|
+
} catch {
|
|
1993
|
+
return whole;
|
|
1994
|
+
}
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
function resolveAsset(src, base, outDir, contentRoot) {
|
|
1998
|
+
if (src === "" || /^(?:data:|blob:)/i.test(src) || src.startsWith("#")) return void 0;
|
|
1999
|
+
let path = src.split("#")[0]?.split("?")[0] ?? "";
|
|
2000
|
+
if (base !== "/" && path.startsWith(base)) path = path.slice(base.length - 1);
|
|
2001
|
+
const suffix = path.replace(/^\/+/, "");
|
|
2002
|
+
for (const root of [outDir, contentRoot]) {
|
|
2003
|
+
const candidate = join9(root, suffix);
|
|
2004
|
+
if (existsSync4(candidate) && statSync(candidate).isFile()) return candidate;
|
|
2005
|
+
}
|
|
2006
|
+
return void 0;
|
|
2007
|
+
}
|
|
2008
|
+
var MIME = {
|
|
2009
|
+
".svg": "image/svg+xml",
|
|
2010
|
+
".png": "image/png",
|
|
2011
|
+
".jpg": "image/jpeg",
|
|
2012
|
+
".jpeg": "image/jpeg",
|
|
2013
|
+
".gif": "image/gif",
|
|
2014
|
+
".webp": "image/webp",
|
|
2015
|
+
".avif": "image/avif",
|
|
2016
|
+
".ico": "image/x-icon",
|
|
2017
|
+
".pdf": "application/pdf",
|
|
2018
|
+
".woff": "font/woff",
|
|
2019
|
+
".woff2": "font/woff2",
|
|
2020
|
+
".ttf": "font/ttf",
|
|
2021
|
+
".otf": "font/otf",
|
|
2022
|
+
".mp4": "video/mp4",
|
|
2023
|
+
".webm": "video/webm"
|
|
2024
|
+
};
|
|
2025
|
+
function dataUriFor(file) {
|
|
2026
|
+
const mime = MIME[extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
2027
|
+
return `data:${mime};base64,${readFileSync6(file).toString("base64")}`;
|
|
2028
|
+
}
|
|
2029
|
+
function faviconLink(config, contentRoot, template) {
|
|
2030
|
+
if (config.favicon !== void 0) {
|
|
2031
|
+
const file = join9(contentRoot, config.favicon);
|
|
2032
|
+
if (existsSync4(file)) {
|
|
2033
|
+
try {
|
|
2034
|
+
return `<link rel="icon" href="${dataUriFor(file)}" />`;
|
|
2035
|
+
} catch {
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
const matches = Array.from(template.matchAll(/<link rel="icon"[^>]*>/gi));
|
|
2040
|
+
return matches.at(-1)?.[0] ?? "";
|
|
2041
|
+
}
|
|
2042
|
+
function withExportToc(html) {
|
|
2043
|
+
const headings = Array.from(html.matchAll(/<h([23])\b[^>]*\bid="([^"]*)"[^>]*>([\s\S]*?)<\/h\1>/g)).map(
|
|
2044
|
+
(match) => ({
|
|
2045
|
+
depth: Number(match[1]),
|
|
2046
|
+
id: match[2] ?? "",
|
|
2047
|
+
text: decodeEntities((match[3] ?? "").replace(/<[^>]+>/g, ""))
|
|
2048
|
+
})
|
|
2049
|
+
);
|
|
2050
|
+
if (headings.length === 0) return html;
|
|
2051
|
+
const sections = [];
|
|
2052
|
+
for (const heading of headings) {
|
|
2053
|
+
if (heading.depth === 2 || sections.length === 0) {
|
|
2054
|
+
sections.push({ id: heading.id, text: heading.text, children: [] });
|
|
2055
|
+
} else {
|
|
2056
|
+
sections.at(-1)?.children.push({ id: heading.id, text: heading.text });
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
const items = sections.map((section) => {
|
|
2060
|
+
const self = `<a href="#${section.id}">${escapeHtml(section.text)}</a>`;
|
|
2061
|
+
if (section.children.length === 0) return `<li>${self}</li>`;
|
|
2062
|
+
const kids = section.children.map((child) => `<li><a href="#${child.id}">${escapeHtml(child.text)}</a></li>`).join("");
|
|
2063
|
+
return `<li>${self}<ul>${kids}</ul></li>`;
|
|
2064
|
+
}).join("");
|
|
2065
|
+
const toc = `<nav class="seemore-export-toc"><details><summary>On this page</summary><ul>${items}</ul></details></nav>`;
|
|
2066
|
+
const h1 = /<\/h1>/i.exec(html);
|
|
2067
|
+
return h1 === null ? toc + html : html.slice(0, h1.index + h1[0].length) + toc + html.slice(h1.index + h1[0].length);
|
|
2068
|
+
}
|
|
2069
|
+
function decodeEntities(text) {
|
|
2070
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll(""", '"').replaceAll("'", "'");
|
|
2071
|
+
}
|
|
2072
|
+
function escapeHtml(value) {
|
|
2073
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
2074
|
+
}
|
|
2075
|
+
function formatBytes2(size) {
|
|
2076
|
+
if (size < 1024) return `${size} B`;
|
|
2077
|
+
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} kB`;
|
|
2078
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1841
2081
|
// src/cli/index.ts
|
|
1842
2082
|
var USAGE = `
|
|
1843
|
-
${
|
|
2083
|
+
${pc5.bold("seemore")} \u2014 turn a folder of Markdown into a docs site
|
|
1844
2084
|
|
|
1845
2085
|
seemore [dir] start the dev server
|
|
1846
2086
|
seemore build [dir] build a static site into dist/
|
|
2087
|
+
seemore export <file> export a page as a standalone HTML file
|
|
1847
2088
|
|
|
1848
2089
|
Options
|
|
1849
2090
|
--port <number> dev server port (default 4040)
|
|
@@ -1851,7 +2092,7 @@ Options
|
|
|
1851
2092
|
--open / --no-open open a browser on start (default: no)
|
|
1852
2093
|
--json print one machine-readable JSON line instead of the summary (dev only)
|
|
1853
2094
|
--config <path> path to seemore.config.ts
|
|
1854
|
-
--out <dir> build output directory (default: dist)
|
|
2095
|
+
--out <dir> build output directory (default: dist); for export, where the HTML file is written
|
|
1855
2096
|
--base <path> subpath the site is served from, e.g. /my-repo/
|
|
1856
2097
|
-h, --help show this message
|
|
1857
2098
|
-v, --version show the version
|
|
@@ -1885,21 +2126,28 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1885
2126
|
return;
|
|
1886
2127
|
}
|
|
1887
2128
|
if (values.version === true) {
|
|
1888
|
-
const { readFileSync:
|
|
1889
|
-
const { join:
|
|
2129
|
+
const { readFileSync: readFileSync7 } = await import("fs");
|
|
2130
|
+
const { join: join10 } = await import("path");
|
|
1890
2131
|
const { packageRoot: packageRoot2 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
1891
|
-
const pkg = JSON.parse(
|
|
2132
|
+
const pkg = JSON.parse(readFileSync7(join10(packageRoot2(), "package.json"), "utf8"));
|
|
1892
2133
|
console.log(pkg.version);
|
|
1893
2134
|
return;
|
|
1894
2135
|
}
|
|
1895
2136
|
const [command, ...rest] = positionals;
|
|
1896
2137
|
const isBuild = command === "build";
|
|
2138
|
+
const isExport = command === "export";
|
|
1897
2139
|
const dir = isBuild ? rest[0] : command;
|
|
1898
2140
|
const shared = { cwd: process.cwd(), dir, configPath: values.config, base: values.base };
|
|
1899
2141
|
if (isBuild) {
|
|
1900
2142
|
await runBuild({ ...shared, outDir: values.out });
|
|
1901
2143
|
return;
|
|
1902
2144
|
}
|
|
2145
|
+
if (isExport) {
|
|
2146
|
+
const file = rest[0];
|
|
2147
|
+
if (file === void 0) throw new Error("Usage: seemore export <file> \u2014 name the Markdown file to export.");
|
|
2148
|
+
await runExport({ cwd: shared.cwd, file, out: values.out, configPath: values.config, base: values.base });
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
1903
2151
|
await runDev({
|
|
1904
2152
|
...shared,
|
|
1905
2153
|
port: values.port === void 0 ? void 0 : Number(values.port),
|
|
@@ -1910,7 +2158,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1910
2158
|
}
|
|
1911
2159
|
main().catch((error) => {
|
|
1912
2160
|
console.error(`
|
|
1913
|
-
${
|
|
2161
|
+
${pc5.red("seemore")} ${error instanceof Error ? error.message : String(error)}
|
|
1914
2162
|
`);
|
|
1915
2163
|
process.exitCode = 1;
|
|
1916
2164
|
});
|