Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/helpers/__tests__/buildRobustness.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import fs from "fs";
import os from "os";
import path from "path";
import { createRequire } from "module";
import { describe, it, expect, afterEach } from "vitest";

const require = createRequire(import.meta.url);
const markdownIt = require("markdown-it");
const markdownItFootnote = require("markdown-it-footnote");
const sharp = require("sharp");
const { namedHeadingsFilter } = require("../utils");
const normalizeFavicon = require("../../site/normalize-favicon");
const frontPage = require("../../site/index.11tydata");

describe("fallback front page", () => {
it("sorts notes whose frontmatter title isn't a string", () => {
const note = (title, fileSlug) => ({
url: `/${fileSlug}/`,
fileSlug,
data: { "dg-publish": true, title },
});
const notes = frontPage.eleventyComputed.publishedNotes({
collections: { note: [note("Zebra", "z"), note(2024, "y"), note(undefined, "apple")] },
});
expect(notes.map((n) => n.title)).toEqual(["2024", "apple", "Zebra"]);
});
});

describe("named headings", () => {
it("renders a heading that contains a footnote reference", () => {
const md = markdownIt().use(markdownItFootnote).use(namedHeadingsFilter);
const html = md.render("# Character Sheet[^1]\n\nText\n\n[^1]: A footnote\n");
expect(html).toMatch(/<h1 id="[^"]+">Character Sheet<sup class="footnote-ref">/);
});
});

describe("favicon normalization", () => {
let dir;
afterEach(() => dir && fs.rmSync(dir, { recursive: true, force: true }));

const tmp = () => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), "favicon-"));
return dir;
};

it("wraps a PNG saved as favicon.svg so the favicon generator can read it", async () => {
const d = tmp();
const png = await sharp({
create: { width: 64, height: 64, channels: 4, background: "#c33" },
})
.png()
.toBuffer();
fs.writeFileSync(path.join(d, "favicon.svg"), png);

normalizeFavicon(path.join(d, "favicon.svg"), path.join(d, "out.svg"));

const out = fs.readFileSync(path.join(d, "out.svg"), "utf8");
expect(out).toMatch(/^<svg [^>]*><image [^>]*href="data:image\/png;base64,/);
const rendered = await sharp(Buffer.from(out)).resize(16, 16).raw().toBuffer({ resolveWithObject: true });
expect(rendered.info.width).toBe(16);
expect(rendered.data[0]).toBeGreaterThan(150); // the red came through
});

it("leaves a small SVG favicon unchanged", () => {
const d = tmp();
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><circle r="8"/></svg>';
fs.writeFileSync(path.join(d, "favicon.svg"), svg);

normalizeFavicon(path.join(d, "favicon.svg"), path.join(d, "out.svg"));

expect(fs.readFileSync(path.join(d, "out.svg"), "utf8")).toBe(svg);
});
});
4 changes: 3 additions & 1 deletion src/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ function namedHeadings(md, state) {

state.tokens.forEach(function(token, i) {
if (token.type === 'heading_open') {
var text = md.renderer.render(state.tokens[i + 1].children, md.options)
// Pass env through: renderers like markdown-it-footnote read
// env.docId and crash on a footnote inside a heading without it
var text = md.renderer.render(state.tokens[i + 1].children, md.options, state.env)
var id = headerToId(text);
var uniqId = uncollide(ids, id)
ids[uniqId] = true
Expand Down
4 changes: 3 additions & 1 deletion src/site/index.11tydata.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ const allSettings = [
...pluginLoader.getNoteSettingKeys(),
];

// Frontmatter titles aren't always strings (`title: 2024` parses as a
// number), and the listing sorts them with localeCompare
function noteTitle(note) {
return (note.data && note.data.title) || note.fileSlug || note.url;
return String((note.data && note.data.title) || note.fileSlug || note.url);
}

module.exports = {
Expand Down
38 changes: 37 additions & 1 deletion src/site/normalize-favicon.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,44 @@ const path = require("path");

const MAX_DIM = 512;

const RASTER_SIGNATURES = [
{ mime: "image/png", bytes: [0x89, 0x50, 0x4e, 0x47] },
{ mime: "image/jpeg", bytes: [0xff, 0xd8, 0xff] },
{ mime: "image/gif", bytes: [0x47, 0x49, 0x46, 0x38] },
{ mime: "image/webp", bytes: [0x52, 0x49, 0x46, 0x46], extra: [8, [0x57, 0x45, 0x42, 0x50]] },
];

function rasterMimeType(buffer) {
const startsWith = (offset, bytes) => bytes.every((b, i) => buffer[offset + i] === b);
const match = RASTER_SIGNATURES.find(
(sig) => startsWith(0, sig.bytes) && (!sig.extra || startsWith(sig.extra[0], sig.extra[1]))
);
return match ? match.mime : null;
}

// A PNG/JPEG/GIF/WebP uploaded as favicon.svg can't be read by the favicon
// generator, which failed the whole build. Embed it in an SVG instead.
function wrapRaster(buffer, mime) {
const href = `data:${mime};base64,${buffer.toString("base64")}`;
return (
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ` +
`width="${MAX_DIM}" height="${MAX_DIM}" viewBox="0 0 ${MAX_DIM} ${MAX_DIM}">` +
`<image width="${MAX_DIM}" height="${MAX_DIM}" preserveAspectRatio="xMidYMid meet" ` +
`href="${href}" xlink:href="${href}"/></svg>`
);
}

function normalizeFavicon(inputPath, outputPath) {
const content = fs.readFileSync(inputPath, "utf8");
const buffer = fs.readFileSync(inputPath);

const mime = rasterMimeType(buffer);
if (mime) {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, wrapRaster(buffer, mime));
return;
}

const content = buffer.toString("utf8");

const svgTagMatch = content.match(/<svg\b[^>]*>/i);
if (!svgTagMatch) {
Expand Down
Loading