diff --git a/src/components/SidebarItem/SidebarItem.jsx b/src/components/SidebarItem/SidebarItem.jsx
index 328c3dc5359c..2b405fa10fde 100644
--- a/src/components/SidebarItem/SidebarItem.jsx
+++ b/src/components/SidebarItem/SidebarItem.jsx
@@ -14,7 +14,8 @@ import list2Tree from "../../utilities/list2Tree/index.js";
* @returns {boolean}
*/
function isOpen(currentPage, url) {
- return new RegExp(`${currentPage}/?$`).test(url);
+ const normalize = (value = "") => value.replace(/\/?$/, "/");
+ return normalize(url) === normalize(currentPage);
}
/**
diff --git a/src/components/SidebarItem/SidebarItem.test.jsx b/src/components/SidebarItem/SidebarItem.test.jsx
index c1eb4a46bcfb..ad6eb678a31f 100644
--- a/src/components/SidebarItem/SidebarItem.test.jsx
+++ b/src/components/SidebarItem/SidebarItem.test.jsx
@@ -38,6 +38,18 @@ describe("SidebarItem", () => {
expect(wrapper.getAttribute("data-open")).toBe("true");
});
+ it("matches URLs with regexp characters literally", () => {
+ const { container } = renderWithRouter(
+ ,
+ );
+ const wrapper = container.firstChild;
+ expect(wrapper.getAttribute("data-open")).toBe("true");
+ });
+
it("toggles open state when chevron button is clicked", () => {
const anchors = [
{
diff --git a/src/remark-plugins/remark-refractor/index.mjs b/src/remark-plugins/remark-refractor/index.mjs
index d664b5130449..d60d45a5da55 100644
--- a/src/remark-plugins/remark-refractor/index.mjs
+++ b/src/remark-plugins/remark-refractor/index.mjs
@@ -51,16 +51,14 @@ function attacher({ include, exclude } = {}) {
try {
data.hChildren = refractor.highlight(node.value, lang).children;
for (const child of data.hChildren) {
- if (
- child.properties &&
- child.properties.className.includes("keyword")
- ) {
- if (child.children[1]) {
- child.properties.componentname = child.children[1].value.trim();
+ if (child.properties?.className?.includes("keyword")) {
+ const componentName = child.children?.[1]?.value?.trim();
+ if (componentName) {
+ child.properties.componentname = componentName;
}
- if (child.children[2]) {
- child.properties.url =
- child.children[2].children[0].value.replaceAll('"', "");
+ const url = child.children?.[2]?.children?.[0]?.value;
+ if (url) {
+ child.properties.url = url.replaceAll('"', "");
}
}
}
diff --git a/src/utilities/content-tree-enhancers.mjs b/src/utilities/content-tree-enhancers.mjs
index f65ea262fa38..afb2dcd774ea 100644
--- a/src/utilities/content-tree-enhancers.mjs
+++ b/src/utilities/content-tree-enhancers.mjs
@@ -52,11 +52,7 @@ export const enhance = (tree, options) => {
.use(extractAnchors, { anchors, levels: 3 })
.use(remarkRemoveHeadingId)
.use(remarkHtml)
- .process(content, (err) => {
- if (err) {
- throw err;
- }
- });
+ .processSync(content);
tree.anchors = anchors;
@@ -73,14 +69,16 @@ export const enhance = (tree, options) => {
const isBlogItem = normalizedPath.includes("/blog/");
if (isBlogItem) {
- const teaser = (body || "")
+ const teaserLines = (body || "")
.split("\n")
- .filter((line) => line.trim() && !line.trim().startsWith("#"))
+ .filter((line) => line.trim() && !line.trim().startsWith("#"));
+ const teaserText = teaserLines
.slice(0, 3)
.join(" ")
- .replaceAll(/\[([^\]]+)\]\([^)]+\)/g, "$1") // Strip markdown links but keep text
- .slice(0, 240);
- tree.teaser = `${teaser}...`;
+ .replaceAll(/\[([^\]]+)\]\([^)]+\)/g, "$1"); // Strip markdown links but keep text
+ const teaserContent = teaserText.slice(0, 240);
+ const isTruncated = teaserLines.length > 3 || teaserText.length > 240;
+ tree.teaser = isTruncated ? `${teaserContent}...` : teaserContent;
}
Object.assign(
diff --git a/src/utilities/content-tree-enhancers.test.mjs b/src/utilities/content-tree-enhancers.test.mjs
index dfb7d12da696..b84e54604f5c 100644
--- a/src/utilities/content-tree-enhancers.test.mjs
+++ b/src/utilities/content-tree-enhancers.test.mjs
@@ -1,6 +1,9 @@
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
// eslint-disable-next-line import/no-extraneous-dependencies
import { describe, expect } from "@jest/globals";
-import { restructure } from "./content-tree-enhancers.mjs";
+import { enhance, restructure } from "./content-tree-enhancers.mjs";
describe("restructure", () => {
it("applies filter result back to children array", () => {
@@ -55,3 +58,41 @@ describe("restructure", () => {
expect(root.children.map((item) => item.title)).toEqual(["API", "Guides"]);
});
});
+
+describe("enhance", () => {
+ const createBlogTree = (body) => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "webpack-blog-"));
+ const blogDir = path.join(root, "blog");
+ fs.mkdirSync(blogDir);
+ const filePath = path.join(blogDir, "example.mdx");
+ fs.writeFileSync(filePath, `---\ntitle: Example\n---\n\n${body}`);
+
+ return {
+ root,
+ tree: {
+ type: "file",
+ path: filePath,
+ extension: ".mdx",
+ name: "example.mdx",
+ },
+ };
+ };
+
+ it("does not append an ellipsis to an untruncated blog teaser", () => {
+ const { root, tree } = createBlogTree("Short body.");
+
+ enhance(tree, { dir: root });
+
+ expect(tree.teaser).toBe("Short body.");
+ });
+
+ it("appends an ellipsis to truncated blog teasers", () => {
+ const { root, tree } = createBlogTree(
+ ["First line.", "Second line.", "Third line.", "Fourth line."].join("\n"),
+ );
+
+ enhance(tree, { dir: root });
+
+ expect(tree.teaser).toBe("First line. Second line. Third line....");
+ });
+});
diff --git a/src/utilities/content-utils.mjs b/src/utilities/content-utils.mjs
index 187f4940495d..2115445b0b24 100644
--- a/src/utilities/content-utils.mjs
+++ b/src/utilities/content-utils.mjs
@@ -87,13 +87,11 @@ export const getPageTitle = (tree, path) => {
// non page found
if (!page) return "webpack";
- if (page) {
- if (path.includes("/printable")) {
- return "Combined printable page | webpack";
- }
- if (path === "/") return page.title || "webpack";
- return `${page.title} | webpack`;
+ if (path.includes("/printable")) {
+ return "Combined printable page | webpack";
}
+ if (path === "/") return page.title || "webpack";
+ return `${page.title} | webpack`;
};
export const getPageDescription = (tree, path) => {
diff --git a/src/utilities/flatten-content-tree.mjs b/src/utilities/flatten-content-tree.mjs
index b6fa5b89db33..cb7a0ef7e35b 100644
--- a/src/utilities/flatten-content-tree.mjs
+++ b/src/utilities/flatten-content-tree.mjs
@@ -7,11 +7,15 @@ export default (tree) => {
}
if ("children" in node) {
- node.children.map(crawl);
+ for (const child of node.children) {
+ crawl(child);
+ }
}
};
- tree.children.map(crawl);
+ for (const child of tree.children) {
+ crawl(child);
+ }
return paths;
};
diff --git a/src/utilities/process-readme.mjs b/src/utilities/process-readme.mjs
index 064b7b90e690..d39c4534e3d1 100644
--- a/src/utilities/process-readme.mjs
+++ b/src/utilities/process-readme.mjs
@@ -66,9 +66,10 @@ function linkFixerFactory(sourceUrl) {
// Lowercase all fragment links, since markdown generators do the same
if (href.includes("#")) {
- const [urlPath, urlFragment] = href.split("#");
-
- href = `${urlPath}#${urlFragment.toLowerCase()}`;
+ const fragmentIndex = href.indexOf("#");
+ href = `${href.slice(0, fragmentIndex)}#${href
+ .slice(fragmentIndex + 1)
+ .toLowerCase()}`;
}
if (oldHref !== href) {
@@ -79,16 +80,6 @@ function linkFixerFactory(sourceUrl) {
};
}
-function getMatches(string, regex) {
- const matches = [];
- let match;
-
- while ((match = regex.exec(string))) {
- matches.push(match);
- }
- return matches;
-}
-
export default function processREADME(body, options = {}) {
let processingString = body
// close
tags
@@ -141,10 +132,11 @@ export default function processREADME(body, options = {}) {
);
// find the loaders links
- const loaderMatches = getMatches(
- processingString,
- /https?:\/\/github.com\/(webpack|webpack-contrib)\/([-A-za-z0-9]+-loader\/?)([)"])/g,
- );
+ const loaderMatches = [
+ ...processingString.matchAll(
+ /https?:\/\/github.com\/(webpack|webpack-contrib)\/([-A-za-z0-9]+-loader\/?)([)"])/g,
+ ),
+ ];
// dont make relative links for excluded loaders
for (const match of loaderMatches) {
if (!excludedLoaders.includes(`${match[1]}/${match[2]}`)) {
@@ -155,10 +147,11 @@ export default function processREADME(body, options = {}) {
}
}
- const pluginMatches = getMatches(
- processingString,
- /https?:\/\/github.com\/(webpack|webpack-contrib)\/([-A-za-z0-9]+-plugin\/?)([)"])/g,
- );
+ const pluginMatches = [
+ ...processingString.matchAll(
+ /https?:\/\/github.com\/(webpack|webpack-contrib)\/([-A-za-z0-9]+-plugin\/?)([)"])/g,
+ ),
+ ];
// dont make relative links for excluded loaders
for (const match of pluginMatches) {
if (!excludedPlugins.includes(`${match[1]}/${match[2]}`)) {
diff --git a/src/utilities/process-readme.test.mjs b/src/utilities/process-readme.test.mjs
index 7a6c24f06c62..78b83c3b13e7 100644
--- a/src/utilities/process-readme.test.mjs
+++ b/src/utilities/process-readme.test.mjs
@@ -44,6 +44,17 @@ describe("processReadme", () => {
);
});
+ it("preserves additional hash fragments when lowercasing anchors", () => {
+ const options = {
+ source:
+ "https://raw.githubusercontent.com/webpack/postcss-loader/main/README.md",
+ };
+ const loaderMDData = "[Example](https://example.com/page#Section#Nested)";
+ expect(processReadme(loaderMDData, options)).toBe(
+ "[Example](https://example.com/page#section#nested)",
+ );
+ });
+
it("should preserve comments inside code blocks", () => {
const options = {
source: