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: