Skip to content

Commit 49bd13d

Browse files
authored
fix: parse project changelogs with remark (#1342)
This PR adds actual parsing of the Markdown content instead of using RegExp-based transformations. This mitigates the problems with mismatched token delimiters (nested Markdown links etc.). Fixes the documentation build in https://github.com/apify/apify-client-js (which in turn unblocks the releases there). related to facebook/docusaurus#10739
1 parent d3daad8 commit 49bd13d

File tree

3 files changed

+186
-21
lines changed

3 files changed

+186
-21
lines changed

apify-docs-theme/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
"babel-loader": "^9.1.3",
2727
"docusaurus-gtm-plugin": "^0.0.2",
2828
"postcss-preset-env": "^9.3.0",
29-
"prism-react-renderer": "^2.0.6"
29+
"prism-react-renderer": "^2.0.6",
30+
"remark-parse": "^11.0.0",
31+
"remark-stringify": "^11.0.0",
32+
"unified": "^11.0.5",
33+
"unist-util-visit-parents": "^3.1.1"
3034
},
3135
"peerDependencies": {
3236
"clsx": "*",

apify-docs-theme/src/markdown.js

Lines changed: 118 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,132 @@
1+
const remarkParse = require('remark-parse');
2+
const remarkStringify = require('remark-stringify');
3+
const { unified } = require('unified');
4+
const visitParents = require('unist-util-visit-parents');
5+
6+
/**
7+
* Updates the markdown content for better UX and compatibility with Docusaurus v3.
8+
* @param {string} changelog The markdown content.
9+
* @returns {string} The updated markdown content.
10+
*/
111
function updateChangelog(changelog) {
12+
const pipeline = unified()
13+
.use(remarkParse)
14+
.use(incrementHeadingLevels)
15+
.use(prettifyPRLinks)
16+
.use(linkifyUserTags)
17+
.use(remarkStringify);
18+
19+
changelog = pipeline.processSync(changelog).toString();
220
changelog = addFrontmatter(changelog);
3-
changelog = pushHeadings(changelog);
4-
changelog = fixUserLinks(changelog);
5-
changelog = fixPRLinks(changelog);
621
changelog = escapeMDXCharacters(changelog);
722
return changelog;
823
}
924

10-
function addFrontmatter(changelog, header = 'Changelog') {
11-
return `---
12-
title: ${header}
13-
sidebar_label: ${header}
14-
toc_max_heading_level: 2
15-
---
16-
${changelog}`;
17-
}
25+
/**
26+
* Bumps the headings levels in the markdown content. This function increases the depth
27+
* of all headings in the content by 1. This is useful when the content is included in
28+
* another markdown file with a higher-level heading.
29+
* @param {*} tree Remark AST tree.
30+
* @returns {void} Nothing. This function modifies the tree in place.
31+
*/
32+
const incrementHeadingLevels = () => (tree) => {
33+
visitParents(tree, 'heading', (node) => {
34+
node.depth += 1;
35+
});
36+
};
1837

19-
function pushHeadings(changelog) {
20-
return changelog.replaceAll(/\n#[^#]/g, '\n## ');
21-
}
38+
/**
39+
* Links user tags in the markdown content. This function replaces the user tags
40+
* (e.g. `@username`) with a link to the user's GitHub profile (just like GitHub's UI).
41+
* @param {*} tree Remark AST tree.
42+
* @returns {void} Nothing. This function modifies the tree in place.
43+
*/
44+
const linkifyUserTags = () => (tree) => {
45+
visitParents(tree, 'text', (node, parents) => {
46+
const userTagRegex = /@([a-zA-Z0-9-]+)(\s|$)/g;
47+
const match = userTagRegex.exec(node.value);
2248

23-
function fixUserLinks(changelog) {
24-
return changelog.replaceAll(/by @([a-zA-Z0-9-]+)/g, 'by [@$1](https://github.com/$1)');
25-
}
49+
if (!match) return;
50+
51+
const directParent = parents[parents.length - 1];
52+
const nodeIndexInParent = directParent.children.findIndex((x) => x === node);
53+
54+
const username = match[1];
55+
const ending = match[2] === ' ' ? ' ' : '';
56+
const before = node.value.slice(0, match.index);
57+
const after = node.value.slice(userTagRegex.lastIndex);
2658

27-
function fixPRLinks(changelog) {
28-
return changelog.replaceAll(/(((https?:\/\/)?(www.)?)?github.com\/[^\s]*?\/pull\/([0-9]+))/g, '[#$5]($1)');
59+
const link = {
60+
type: 'link',
61+
url: `https://github.com/${username}`,
62+
children: [{ type: 'text', value: `@${username}` }],
63+
};
64+
node.value = before;
65+
directParent.children.splice(nodeIndexInParent + 1, 0, link);
66+
67+
if (!after) return nodeIndexInParent + 2;
68+
69+
directParent.children.splice(nodeIndexInParent + 2, 0, { type: 'text', value: `${ending}${after}` });
70+
return nodeIndexInParent + 3;
71+
});
72+
};
73+
74+
/**
75+
* Prettifies PR links in the markdown content. Just like GitHub's UI, this function
76+
* replaces the full PR URL with a link represented by the PR number (prefixed by a hashtag).
77+
* @param {*} tree Remark AST tree.
78+
* @returns {void} Nothing. This function modifies the tree in place.
79+
*/
80+
const prettifyPRLinks = () => (tree) => {
81+
visitParents(tree, 'text', (node, parents) => {
82+
const prLinkRegex = /https:\/\/github.com\/[^\s]+\/pull\/(\d+)/g;
83+
const match = prLinkRegex.exec(node.value);
84+
85+
if (!match) return;
86+
87+
const directParent = parents[parents.length - 1];
88+
const nodeIndexInParent = directParent.children.findIndex((x) => x === node);
89+
90+
const prNumber = match[1];
91+
const before = node.value.slice(0, match.index);
92+
const after = node.value.slice(prLinkRegex.lastIndex);
93+
94+
const link = {
95+
type: 'link',
96+
url: match[0],
97+
children: [{ type: 'text', value: `#${prNumber}` }],
98+
};
99+
node.value = before;
100+
101+
directParent.children.splice(nodeIndexInParent + 1, 0, link);
102+
if (!after) return nodeIndexInParent + 1;
103+
104+
directParent.children.splice(nodeIndexInParent + 2, 0, { type: 'text', value: after });
105+
return nodeIndexInParent + 2;
106+
});
107+
};
108+
109+
/**
110+
* Adds frontmatter to the markdown content.
111+
* @param {string} changelog The markdown content.
112+
* @param {string} title The frontmatter title.
113+
* @returns {string} The markdown content with frontmatter.
114+
*/
115+
function addFrontmatter(changelog, title = 'Changelog') {
116+
return `---
117+
title: ${title}
118+
sidebar_label: ${title}
119+
toc_max_heading_level: 3
120+
---
121+
${changelog}`;
29122
}
30123

124+
/**
125+
* Escapes the MDX-related characters in the markdown content.
126+
* This is required by Docusaurus v3 and its dependencies (see the v3 [migration guide](https://docusaurus.io/docs/migration/v3#common-mdx-problems)).
127+
* @param {string} changelog The markdown content.
128+
* @returns {string} The markdown content with escaped MDX characters.
129+
*/
31130
function escapeMDXCharacters(changelog) {
32131
return changelog.replaceAll(/<|>/g, (match) => {
33132
return match === '<' ? '&lt;' : '&gt;';

package-lock.json

Lines changed: 63 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)