|
| 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 | + */ |
1 | 11 | 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(); |
2 | 20 | changelog = addFrontmatter(changelog);
|
3 |
| - changelog = pushHeadings(changelog); |
4 |
| - changelog = fixUserLinks(changelog); |
5 |
| - changelog = fixPRLinks(changelog); |
6 | 21 | changelog = escapeMDXCharacters(changelog);
|
7 | 22 | return changelog;
|
8 | 23 | }
|
9 | 24 |
|
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 | +}; |
18 | 37 |
|
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); |
22 | 48 |
|
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); |
26 | 58 |
|
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}`; |
29 | 122 | }
|
30 | 123 |
|
| 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 | + */ |
31 | 130 | function escapeMDXCharacters(changelog) {
|
32 | 131 | return changelog.replaceAll(/<|>/g, (match) => {
|
33 | 132 | return match === '<' ? '<' : '>';
|
|
0 commit comments