Skip to content
Open
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
18 changes: 15 additions & 3 deletions packages/cli/src/lifecycles/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ function getLabelFromRoute(_route) {
.join(" ");
}

// decodeURIComponent is only meant to normalize percent-encoded filenames; raw
// frontmatter values (e.g. "100% Complete") can contain a bare "%" that throws
// URIError and aborts the whole build, so fall back to the original string
// https://github.com/ProjectEvergreen/greenwood/issues/1709
function safeDecodeURIComponent(value) {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}

function getIdFromRelativePathPath(relativePathPath, extension) {
return relativePathPath
.replace(`.${extension}`, "")
Expand Down Expand Up @@ -284,9 +296,9 @@ const generateGraph = async (compilation) => {
});

const page = {
id: decodeURIComponent(getIdFromRelativePathPath(relativePagePath, extension)),
label: decodeURIComponent(label),
title: title ? decodeURIComponent(title) : title,
id: safeDecodeURIComponent(getIdFromRelativePathPath(relativePagePath, extension)),
label: safeDecodeURIComponent(label),
title: title ? safeDecodeURIComponent(title) : title,
route: `${basePath}${route}`,
layout,
data: customData || {},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Use Case
* Run Greenwood build with a page whose frontmatter title/label contain a literal
* "%" that is not part of a valid percent-encoding sequence (e.g. "100% Complete").
*
* User Result
* Should generate a bare bones Greenwood build without crashing, with the "%"
* containing title reaching the output verbatim.
*
* User Command
* greenwood build
*
* User Config
* None
*
* User Workspace
* Greenwood default
* src/
* layouts/
* page.html
* pages/
* index.html (frontmatter: title "100% Complete", label "Save 20%")
*/
// https://github.com/ProjectEvergreen/greenwood/issues/1709
import fs from "node:fs";
import { JSDOM } from "jsdom";
import path from "node:path";
import { expect } from "chai";
import { runSmokeTest } from "../../../../../test/smoke-test.js";
import { getOutputTeardownFiles } from "../../../../../test/utils.js";
import { Runner } from "gallinago";
import { fileURLToPath } from "node:url";

describe("Build Greenwood With: ", function () {
const LABEL = "Frontmatter title and label containing a literal percent sign";
const cliPath = path.join(process.cwd(), "packages/cli/src/bin.js");
const outputPath = fileURLToPath(new URL(".", import.meta.url));
let runner;

before(function () {
this.context = {
publicDir: path.join(outputPath, "public"),
};
runner = new Runner();
});

describe(LABEL, function () {
before(async function () {
await runner.setup(outputPath);
await runner.runCommand(cliPath, "build");
});

runSmokeTest(["public", "index"], LABEL);

describe("Percent-containing Frontmatter Title", function () {
let dom;

before(async function () {
dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, "./index.html"));
});

it("should output an index.html file", function () {
expect(fs.existsSync(path.join(this.context.publicDir, "./index.html"))).to.be.true;
});

it("should keep the literal '%' in the frontmatter <title> verbatim", function () {
const title = dom.window.document.querySelector("head title").textContent;

expect(title).to.be.equal("100% Complete");
});
});
});

after(async function () {
await runner.teardown(getOutputTeardownFiles(outputPath));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>Placeholder Title</title>
</head>
<body>
<content-outlet></content-outlet>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: 100% Complete
label: Save 20%
---

<body>
<h1>Percent</h1>
</body>
Loading