website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
20e9f21
— pyrossh
2026-11-06T15:29:20+05:30
Fix markdown links and render docs on file pages
- assets/css/markdown.css +7 -0
- assets/css/repos.css +8 -0
- src/_data/localRepos.js +8 -4
- src/_data/repoFiles.js +16 -5
- src/_helpers/gitRepos.js +24 -0
- src/_helpers/repoMarkdown.js +87 -0
- src/_includes/file-layout.njk +2 -0
- tests/pages.test.js +14 -0
- tests/repoMarkdown.test.js +142 -0
assets/css/markdown.css
CHANGED
|
@@ -39,6 +39,13 @@
|
|
|
39
39
|
padding-bottom: 1rem;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/* shared.css resets links to inherit their color with no underline, which
|
|
43
|
+
leaves links in prose indistinguishable from the text around them. */
|
|
44
|
+
a[href] {
|
|
45
|
+
color: var(--color-link);
|
|
46
|
+
text-decoration: underline;
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
/* shared.css resets ul/ol to list-style: none globally (for nav-style
|
|
43
50
|
lists elsewhere in the site), so markdown content needs its bullets/
|
|
44
51
|
numbering and indentation restored here, plus breathing room between
|
assets/css/repos.css
CHANGED
|
@@ -375,6 +375,14 @@
|
|
|
375
375
|
border-radius: 0;
|
|
376
376
|
}
|
|
377
377
|
}
|
|
378
|
+
|
|
379
|
+
/* Rendered markdown brings its own padding (see #repo-content
|
|
380
|
+
.markdown-body), so the pane only supplies the matching background
|
|
381
|
+
for documents shorter than the pane. */
|
|
382
|
+
&:has(> .markdown-body) {
|
|
383
|
+
padding: 0;
|
|
384
|
+
background: var(--color-box-bg);
|
|
385
|
+
}
|
|
378
386
|
}
|
|
379
387
|
}
|
|
380
388
|
|
src/_data/localRepos.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import MarkdownIt from "markdown-it";
|
|
2
1
|
import { discoverLocalRepos, resolveReadmeImages } from "../_helpers/gitRepos.js";
|
|
3
|
-
import {
|
|
2
|
+
import { renderRepoMarkdown } from "../_helpers/repoMarkdown.js";
|
|
4
3
|
|
|
4
|
+
const renderReadme = (repo) =>
|
|
5
|
+
resolveReadmeImages(
|
|
5
|
-
|
|
6
|
+
renderRepoMarkdown(repo.readme, { repoId: repo.id, files: repo.files, filePath: "README.md" }),
|
|
7
|
+
repo.id,
|
|
8
|
+
repo.dir,
|
|
9
|
+
);
|
|
6
10
|
|
|
7
11
|
export default async () => {
|
|
8
12
|
const repos = discoverLocalRepos();
|
|
9
13
|
return repos.map((repo) => ({
|
|
10
14
|
...repo,
|
|
11
|
-
readmeHtml: repo.readme ?
|
|
15
|
+
readmeHtml: repo.readme ? renderReadme(repo) : undefined,
|
|
12
16
|
}));
|
|
13
17
|
};
|
src/_data/repoFiles.js
CHANGED
|
@@ -7,20 +7,28 @@ import {
|
|
|
7
7
|
LARGE_FILE_SIZE,
|
|
8
8
|
discoverLocalRepos,
|
|
9
9
|
getHeadSha,
|
|
10
|
+
inlineMarkdownImages,
|
|
10
11
|
isDirty,
|
|
11
12
|
toDataUri,
|
|
12
13
|
} from "../_helpers/gitRepos.js";
|
|
13
14
|
import { highlightCode } from "../_helpers/highlightCode.js";
|
|
14
15
|
import { mapPool } from "../_helpers/pool.js";
|
|
16
|
+
import { renderRepoMarkdown } from "../_helpers/repoMarkdown.js";
|
|
15
17
|
|
|
16
18
|
const CONCURRENCY = 8;
|
|
19
|
+
const MARKDOWN_EXTENSIONS = new Set(["md", "markdown"]);
|
|
17
20
|
|
|
18
|
-
const renderFile = (
|
|
21
|
+
const renderFile = (repo, file) => {
|
|
19
|
-
const full = path.join(
|
|
22
|
+
const full = path.join(repo.dir, file.path);
|
|
20
23
|
if (BINARY_EXTENSIONS.has(file.ext)) return { kind: "binary" };
|
|
21
24
|
if (file.size > LARGE_FILE_SIZE) return { kind: "large" };
|
|
22
25
|
if (IMAGE_EXTENSIONS.has(file.ext)) return { kind: "image", src: toDataUri(fs.readFileSync(full), file.ext) };
|
|
26
|
+
const source = fs.readFileSync(full, "utf8");
|
|
27
|
+
if (MARKDOWN_EXTENSIONS.has(file.ext)) {
|
|
28
|
+
const html = renderRepoMarkdown(source, { repoId: repo.id, files: repo.files, filePath: file.path });
|
|
29
|
+
return { kind: "markdown", html: inlineMarkdownImages(html, repo.dir, file.path) };
|
|
30
|
+
}
|
|
23
|
-
const { html, lang } = highlightCode(
|
|
31
|
+
const { html, lang } = highlightCode(source, file.ext);
|
|
24
32
|
return { kind: "text", html, lang };
|
|
25
33
|
};
|
|
26
34
|
|
|
@@ -30,13 +38,16 @@ export default async () => {
|
|
|
30
38
|
for (const repo of repos) {
|
|
31
39
|
const sha = getHeadSha(repo.dir);
|
|
32
40
|
const cacheable = sha && !isDirty(repo.dir);
|
|
41
|
+
// The version is part of the key because the cache is otherwise keyed by
|
|
42
|
+
// HEAD: bump it when renderFile's output changes, or unchanged repos keep
|
|
43
|
+
// serving the old render.
|
|
33
|
-
const cacheKey = `files-${repo.id}`;
|
|
44
|
+
const cacheKey = `files-v2-${repo.id}`;
|
|
34
45
|
|
|
35
46
|
let entries = cacheable ? readCache(cacheKey, sha) : null;
|
|
36
47
|
if (!entries) {
|
|
37
48
|
entries = await mapPool(repo.files, CONCURRENCY, async (file) => ({
|
|
38
49
|
file,
|
|
39
|
-
render: renderFile(repo
|
|
50
|
+
render: renderFile(repo, file),
|
|
40
51
|
}));
|
|
41
52
|
if (cacheable) writeCache(cacheKey, sha, entries);
|
|
42
53
|
}
|
src/_helpers/gitRepos.js
CHANGED
|
@@ -220,6 +220,16 @@ export const toDataUri = (buf, ext) => {
|
|
|
220
220
|
return `data:${mime};base64,${buf.toString("base64")}`;
|
|
221
221
|
};
|
|
222
222
|
|
|
223
|
+
// Percent-decodes a path from a markdown link or image. Malformed escapes are
|
|
224
|
+
// kept as written, since hand-written markdown can contain a bare `%`.
|
|
225
|
+
export const decodePath = (encoded) => {
|
|
226
|
+
try {
|
|
227
|
+
return decodeURIComponent(encoded);
|
|
228
|
+
} catch {
|
|
229
|
+
return encoded;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
223
233
|
const isAbsoluteSrc = (src) => /^([a-z][a-z0-9+.-]*:)?\/\//i.test(src) || src.startsWith("data:");
|
|
224
234
|
|
|
225
235
|
const IMG_SRC_PATTERNS = [
|
|
@@ -260,6 +270,20 @@ export const resolveReadmeImages = (html, repoId, repoDir) =>
|
|
|
260
270
|
return `${pre}/repos/${repoId}/raw/${path.posix.normalize(src)}${post}`;
|
|
261
271
|
});
|
|
262
272
|
|
|
273
|
+
// Markdown shown on a file page. Only README images get a raw/ copy, so these
|
|
274
|
+
// are inlined the same way image files' own pages are, with paths relative to
|
|
275
|
+
// the markdown file rather than the repo root.
|
|
276
|
+
export const inlineMarkdownImages = (html, repoDir, filePath) =>
|
|
277
|
+
html.replace(/(<img\b[^>]*?(?<![\w-])src\s*=\s*)(["'])(.*?)\2/gi, (match, pre, quote, src) => {
|
|
278
|
+
if (!src || isAbsoluteSrc(src)) return match;
|
|
279
|
+
const relPath = path.posix.join(path.posix.dirname(filePath), decodePath(src.split(/[?#]/)[0]));
|
|
280
|
+
if (!isLocalRepoImage(repoDir, relPath)) return match;
|
|
281
|
+
const full = path.join(repoDir, relPath);
|
|
282
|
+
if (fs.statSync(full).size > LARGE_FILE_SIZE) return match;
|
|
283
|
+
const ext = path.extname(relPath).slice(1).toLowerCase();
|
|
284
|
+
return `${pre}${quote}${toDataUri(fs.readFileSync(full), ext)}${quote}`;
|
|
285
|
+
});
|
|
286
|
+
|
|
263
287
|
const readCargoVersion = (repoDir) => {
|
|
264
288
|
const p = path.join(repoDir, "Cargo.toml");
|
|
265
289
|
if (!fs.existsSync(p)) return undefined;
|
src/_helpers/repoMarkdown.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import MarkdownIt from "markdown-it";
|
|
3
|
+
import { decodePath } from "./gitRepos.js";
|
|
4
|
+
import { highlightFenceCode } from "./highlightCode.js";
|
|
5
|
+
|
|
6
|
+
const md = new MarkdownIt({ html: true, linkify: true, highlight: highlightFenceCode });
|
|
7
|
+
|
|
8
|
+
// Same ids GitHub generates, since that's what `#section` links in these repos
|
|
9
|
+
// were written against. A repeated heading gets -1, -2, ... appended.
|
|
10
|
+
const headingSlug = (text) =>
|
|
11
|
+
text
|
|
12
|
+
.trim()
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^\p{L}\p{M}\p{N}\s_-]/gu, "")
|
|
15
|
+
.replace(/\s/g, "-");
|
|
16
|
+
|
|
17
|
+
md.renderer.rules.heading_open = (tokens, idx, options, env, self) => {
|
|
18
|
+
const text = tokens[idx + 1].children
|
|
19
|
+
.filter((t) => t.type === "text" || t.type === "code_inline")
|
|
20
|
+
.map((t) => t.content)
|
|
21
|
+
.join("");
|
|
22
|
+
const slug = headingSlug(text);
|
|
23
|
+
// An image-only heading has no text to link to, so it gets no id.
|
|
24
|
+
if (slug) {
|
|
25
|
+
let id = slug;
|
|
26
|
+
for (let n = 1; env.headingIds.has(id); n++) id = `${slug}-${n}`;
|
|
27
|
+
env.headingIds.add(id);
|
|
28
|
+
tokens[idx].attrSet("id", id);
|
|
29
|
+
}
|
|
30
|
+
return self.renderToken(tokens, idx, options);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// The first line must look like a YAML key, so a document that merely opens
|
|
34
|
+
// with a `---` rule keeps its content.
|
|
35
|
+
const FRONT_MATTER = /^---\r?\n[\w-]+:[\s\S]*?\r?\n---\r?\n/;
|
|
36
|
+
|
|
37
|
+
const encodePath = (filePath) => filePath.split("/").map(encodeURIComponent).join("/");
|
|
38
|
+
|
|
39
|
+
// Folders have no page of their own, so a link to one opens its README or else
|
|
40
|
+
// its first file, which also expands that folder in the file tree.
|
|
41
|
+
const findLinkTarget = (files, target) => {
|
|
42
|
+
if (files.some((f) => f.path === target)) return target;
|
|
43
|
+
const inFolder = files.filter((f) => f.path.startsWith(`${target}/`));
|
|
44
|
+
const readme = inFolder.find((f) => f.path.toLowerCase() === `${target}/readme.md`.toLowerCase());
|
|
45
|
+
return (readme ?? inFolder[0])?.path;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Repo markdown links relative to its own file, as GitHub resolves them. Here
|
|
49
|
+
// that file's content lives at /repos/<id>/files/<path>/, so those links need
|
|
50
|
+
// rewriting, and a leading `/` means the repo root. Targets not in the repo are
|
|
51
|
+
// left as written. `href` is still HTML-escaped, so `&` is `&` here.
|
|
52
|
+
const resolveLink = (href, repoId, files, filePath) => {
|
|
53
|
+
if (/^(#|\/\/|[a-z][a-z0-9+.-]*:)/i.test(href)) return href;
|
|
54
|
+
|
|
55
|
+
const [, linkPath, suffix] = href.match(/^([^?#]*)(.*)$/s);
|
|
56
|
+
if (!linkPath) return href;
|
|
57
|
+
|
|
58
|
+
const decoded = decodePath(linkPath.replaceAll("&", "&"));
|
|
59
|
+
const base = decoded.startsWith("/") ? "." : path.posix.dirname(filePath);
|
|
60
|
+
const target = path.posix.join(base, decoded).replace(/\/$/, "");
|
|
61
|
+
if (target === ".") return `/repos/${repoId}/files/${suffix}`;
|
|
62
|
+
|
|
63
|
+
const file = findLinkTarget(files, target);
|
|
64
|
+
if (file) return `/repos/${repoId}/files/${encodePath(file)}/${suffix}`;
|
|
65
|
+
|
|
66
|
+
// Scheme-less external links, e.g. [Teensy](www.pjrc.com/store/teensy3.html)
|
|
67
|
+
return /^www\./i.test(href) ? `https://${href}` : href;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Render a markdown file from a repo, pointing its relative links at this
|
|
72
|
+
* site's file pages. Runs over the final HTML rather than a markdown-it link
|
|
73
|
+
* rule so raw <a> tags in the markdown are covered too; code blocks can't match
|
|
74
|
+
* because their `<` is already escaped.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} source markdown text
|
|
77
|
+
* @param {{ repoId: string, files: { path: string }[], filePath: string }} context
|
|
78
|
+
* filePath is the markdown file's path within the repo
|
|
79
|
+
* @returns {string} HTML
|
|
80
|
+
*/
|
|
81
|
+
export const renderRepoMarkdown = (source, { repoId, files, filePath }) =>
|
|
82
|
+
md
|
|
83
|
+
.render(source.replace(FRONT_MATTER, ""), { headingIds: new Set() })
|
|
84
|
+
.replace(
|
|
85
|
+
/(<a\b[^>]*?(?<![\w-])href\s*=\s*)(["'])(.*?)\2/gi,
|
|
86
|
+
(_, pre, quote, href) => `${pre}${quote}${resolveLink(href, repoId, files, filePath)}${quote}`,
|
|
87
|
+
);
|
src/_includes/file-layout.njk
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
<p>File too large to display ({{ size }} bytes).</p>
|
|
13
13
|
{% elif render.kind == "image" %}
|
|
14
14
|
<img src="{{ render.src }}" alt="{{ filepath }}" />
|
|
15
|
+
{% elif render.kind == "markdown" %}
|
|
16
|
+
<article class="markdown-body">{{ render.html | safe }}</article>
|
|
15
17
|
{% else %}
|
|
16
18
|
<pre class="code-view" data-lang="{{ render.lang }}"><code class="language-{{ render.lang }}">{{ render.html | safe }}</code></pre>
|
|
17
19
|
{% endif %}
|
tests/pages.test.js
CHANGED
|
@@ -237,6 +237,20 @@ describe("repo pages", () => {
|
|
|
237
237
|
}
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
+
test("markdown files render as documents with visible links", async () => {
|
|
241
|
+
await goto(`/repos/${repoId}/files/README.md/`);
|
|
242
|
+
await page.locator("#file-content > .markdown-body").waitFor();
|
|
243
|
+
assert.equal(await page.locator("#file-content pre.code-view").count(), 0);
|
|
244
|
+
|
|
245
|
+
const link = page.locator("#file-content .markdown-body a[href]").first();
|
|
246
|
+
const style = await link.evaluate((el) => {
|
|
247
|
+
const { color, textDecorationLine } = getComputedStyle(el);
|
|
248
|
+
return { color, textDecorationLine, parentColor: getComputedStyle(el.parentElement).color };
|
|
249
|
+
});
|
|
250
|
+
assert.equal(style.textDecorationLine, "underline");
|
|
251
|
+
assert.notEqual(style.color, style.parentColor);
|
|
252
|
+
});
|
|
253
|
+
|
|
240
254
|
test("repo files index renders the tree and a default file", async () => {
|
|
241
255
|
const response = await goto(`/repos/${repoId}/files/`);
|
|
242
256
|
assert.equal(response.status(), 200);
|
tests/repoMarkdown.test.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { after, describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { LARGE_FILE_SIZE, inlineMarkdownImages } from "../src/_helpers/gitRepos.js";
|
|
7
|
+
import { renderRepoMarkdown } from "../src/_helpers/repoMarkdown.js";
|
|
8
|
+
|
|
9
|
+
const files = ["README.md", "docs/language.md", "docs/std.md", "examples/arrays.plum", "examples/basics.plum", "guide/intro.md", "guide/README.md", "my docs/a b.md", "c#/q&a.md", "www.site/index.md"].map((p) => ({ path: p }));
|
|
10
|
+
|
|
11
|
+
const render = (source, filePath = "README.md") => renderRepoMarkdown(source, { repoId: "plum", files, filePath });
|
|
12
|
+
const href = (source, filePath) => render(source, filePath).match(/href="([^"]*)"/)[1];
|
|
13
|
+
|
|
14
|
+
describe("renderRepoMarkdown links", () => {
|
|
15
|
+
test("relative file links point at the repo's file pages", () => {
|
|
16
|
+
assert.equal(href("[x](docs/language.md)"), "/repos/plum/files/docs/language.md/");
|
|
17
|
+
assert.equal(href("[x](./docs/language.md)"), "/repos/plum/files/docs/language.md/");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("links resolve against the markdown file's own folder", () => {
|
|
21
|
+
assert.equal(href("[x](std.md)", "docs/language.md"), "/repos/plum/files/docs/std.md/");
|
|
22
|
+
assert.equal(href("[x](../README.md)", "docs/language.md"), "/repos/plum/files/README.md/");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("a section in another file keeps its fragment", () => {
|
|
26
|
+
assert.equal(href("[x](docs/std.md#strings)"), "/repos/plum/files/docs/std.md/#strings");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("folder links open the folder's README, else its first file", () => {
|
|
30
|
+
assert.equal(href("[x](guide/)"), "/repos/plum/files/guide/README.md/");
|
|
31
|
+
assert.equal(href("[x](../examples)", "docs/language.md"), "/repos/plum/files/examples/arrays.plum/");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("a link to the repo root opens the Code tab", () => {
|
|
35
|
+
assert.equal(href("[x](..)", "docs/language.md"), "/repos/plum/files/");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("paths with spaces stay encoded", () => {
|
|
39
|
+
assert.equal(href("[x](my%20docs/a%20b.md)"), "/repos/plum/files/my%20docs/a%20b.md/");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("filenames with URL-special characters are encoded per segment", () => {
|
|
43
|
+
assert.equal(href("[x](c%23/q&a.md)"), "/repos/plum/files/c%23/q%26a.md/");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("a leading slash means the repo root", () => {
|
|
47
|
+
assert.equal(href("[x](/docs/std.md)", "guide/intro.md"), "/repos/plum/files/docs/std.md/");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("raw HTML links are resolved too, with either quote style", () => {
|
|
51
|
+
assert.equal(href('<a href="docs/std.md">x</a>'), "/repos/plum/files/docs/std.md/");
|
|
52
|
+
assert.match(render("<a href='docs/std.md'>x</a>"), /href='\/repos\/plum\/files\/docs\/std\.md\/'/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("data-href isn't treated as the link", () => {
|
|
56
|
+
assert.match(render('<a data-href="docs/std.md">x</a>'), /data-href="docs\/std\.md"/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("other links are left alone", () => {
|
|
60
|
+
for (const link of ["https://example.com/a.md", "mailto:[email protected]", "#section", "/posts/", "?x=1", "docs/missing.md", "../../outside.md"]) {
|
|
61
|
+
assert.equal(href(`[x](${link})`), link);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("scheme-less www links become external, unless the repo has that path", () => {
|
|
66
|
+
assert.equal(href("[x](www.pjrc.com/store/teensy3.html)"), "https://www.pjrc.com/store/teensy3.html");
|
|
67
|
+
assert.equal(href("[x](www.site/index.md)"), "/repos/plum/files/www.site/index.md/");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("links shown in code blocks are not rewritten", () => {
|
|
71
|
+
assert.match(render('```html\n<a href="docs/std.md">x</a>\n```'), /docs\/std\.md/);
|
|
72
|
+
assert.doesNotMatch(render('```html\n<a href="docs/std.md">x</a>\n```'), /\/repos\/plum/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("renderRepoMarkdown headings", () => {
|
|
77
|
+
test("headings get ids that match section links", () => {
|
|
78
|
+
assert.match(render("## Pattern matching"), /<h2 id="pattern-matching">/);
|
|
79
|
+
assert.match(render("## The `plum` CLI: tools!"), /<h2 id="the-plum-cli-tools">/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("repeated headings get distinct ids, in each render", () => {
|
|
83
|
+
const html = render("## Usage\n\n## Usage");
|
|
84
|
+
assert.match(html, /id="usage"/);
|
|
85
|
+
assert.match(html, /id="usage-1"/);
|
|
86
|
+
assert.match(render("## Usage"), /id="usage"/);
|
|
87
|
+
assert.doesNotMatch(render("## Usage"), /usage-1/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a heading that collides with a generated id still gets a unique one", () => {
|
|
91
|
+
const ids = [...render("## Usage\n\n## Usage\n\n## Usage-1").matchAll(/id="([^"]+)"/g)].map((m) => m[1]);
|
|
92
|
+
assert.equal(new Set(ids).size, 3);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("image-only headings get no id", () => {
|
|
96
|
+
assert.match(render("## "), /<h2>/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("combining marks are kept", () => {
|
|
100
|
+
assert.match(render("## हिन्दी"), /id="हिन्दी"/);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("renderRepoMarkdown front matter", () => {
|
|
105
|
+
test("isn't rendered as content", () => {
|
|
106
|
+
const html = render("---\ntitle: Hello\n---\n\n# Body");
|
|
107
|
+
assert.doesNotMatch(html, /title: Hello/);
|
|
108
|
+
assert.match(html, /<h1 id="body">/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("a document opening with a rule keeps its content", () => {
|
|
112
|
+
assert.match(render("---\n\nKeep me\n\n---\n\nAnd me"), /Keep me/);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("inlineMarkdownImages", () => {
|
|
117
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "repo-markdown-"));
|
|
118
|
+
const repoDir = path.join(tempDir, "repo");
|
|
119
|
+
fs.mkdirSync(path.join(repoDir, "docs"), { recursive: true });
|
|
120
|
+
fs.writeFileSync(path.join(repoDir, "docs", "my img.png"), "png");
|
|
121
|
+
fs.writeFileSync(path.join(repoDir, "docs", "big.png"), Buffer.alloc(LARGE_FILE_SIZE + 1));
|
|
122
|
+
fs.writeFileSync(path.join(tempDir, "outside.png"), "png");
|
|
123
|
+
const inline = (html) => inlineMarkdownImages(html, repoDir, "docs/game.md");
|
|
124
|
+
|
|
125
|
+
after(() => fs.rmSync(tempDir, { recursive: true, force: true }));
|
|
126
|
+
|
|
127
|
+
test("inlines images relative to the markdown file, ignoring encoding and suffixes", () => {
|
|
128
|
+
assert.match(inline('<img src="my%20img.png?raw=true">'), /<img src="data:image\/png;base64,/);
|
|
129
|
+
assert.match(inline("<img src='my%20img.png'>"), /<img src='data:image\/png;base64,/);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("leaves oversized, missing and out-of-repo images alone", () => {
|
|
133
|
+
for (const html of ['<img src="big.png">', '<img src="nope.png">', '<img src="../../outside.png">']) {
|
|
134
|
+
assert.equal(inline(html), html);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("data-src isn't treated as the image", () => {
|
|
139
|
+
const html = '<img data-src="my%20img.png">';
|
|
140
|
+
assert.equal(inline(html), html);
|
|
141
|
+
});
|
|
142
|
+
});
|