From 5323184cd2f61ed7b98d889adebeae37d7fe1126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Wed, 11 Dec 2024 14:42:23 +0100 Subject: [PATCH 1/4] fix: use `remark` to transform changelogs better --- apify-docs-theme/package.json | 3 +- apify-docs-theme/src/markdown.js | 103 ++++++++++++++++++++++++++----- package-lock.json | 28 ++++++++- 3 files changed, 115 insertions(+), 19 deletions(-) diff --git a/apify-docs-theme/package.json b/apify-docs-theme/package.json index 6e3018ca65..9953f41c25 100644 --- a/apify-docs-theme/package.json +++ b/apify-docs-theme/package.json @@ -26,7 +26,8 @@ "babel-loader": "^9.1.3", "docusaurus-gtm-plugin": "^0.0.2", "postcss-preset-env": "^9.3.0", - "prism-react-renderer": "^2.0.6" + "prism-react-renderer": "^2.0.6", + "remark": "^15.0.1" }, "peerDependencies": { "clsx": "*", diff --git a/apify-docs-theme/src/markdown.js b/apify-docs-theme/src/markdown.js index de9504d738..5d2c41be9e 100644 --- a/apify-docs-theme/src/markdown.js +++ b/apify-docs-theme/src/markdown.js @@ -1,31 +1,102 @@ +const { remark } = require('remark'); + function updateChangelog(changelog) { + const tree = remark.parse(changelog); + + bumpHeadingsLevels(tree); + linkifyUserTags(tree); + prettifyPRLinks(tree); + + changelog = remark.stringify(tree); changelog = addFrontmatter(changelog); - changelog = pushHeadings(changelog); - changelog = fixUserLinks(changelog); - changelog = fixPRLinks(changelog); changelog = escapeMDXCharacters(changelog); return changelog; } -function addFrontmatter(changelog, header = 'Changelog') { - return `--- -title: ${header} -sidebar_label: ${header} -toc_max_heading_level: 2 ---- -${changelog}`; +function bumpHeadingsLevels(tree) { + tree.children?.forEach((child) => { + if (child.type === 'heading') { + child.depth += 1; + } + + bumpHeadingsLevels(child); + }); } -function pushHeadings(changelog) { - return changelog.replaceAll(/\n#[^#]/g, '\n## '); +function linkifyUserTags(tree) { + for (let i = 0; i < tree.children?.length; i++) { + const child = tree.children[i]; + if (child.type === 'text') { + const userTagRegex = /@([a-zA-Z0-9-]+)(\s|$)/g; + const match = userTagRegex.exec(child.value); + + if (match) { + const [_, username, ending] = match; + const before = child.value.slice(0, match.index); + const after = child.value.slice(userTagRegex.lastIndex); + + const link = { + type: 'link', + url: `https://github.com/${username}`, + children: [{ type: 'text', value: `@${username}` }], + }; + child.value = before; + + tree.children.splice(i + 1, 0, link); + + if (after) { + tree.children.splice(i + 2, 0, { type: 'text', value: `${ending}${after}` }); + } + + i += 2; + } + } + + linkifyUserTags(child); + } } -function fixUserLinks(changelog) { - return changelog.replaceAll(/by @([a-zA-Z0-9-]+)/g, 'by [@$1](https://github.com/$1)'); +// If there is a PR URL (https://github.com/**/**/pull/number) in the text body, split the body, and replace the URL with a link to the PR (the text should be the PR number). +function prettifyPRLinks(tree) { + for (let i = 0; i < tree.children?.length; i++) { + const child = tree.children[i]; + if (child.type === 'text') { + const prLinkRegex = /https:\/\/github.com\/.*\/pull\/(\d+)/g; + const match = prLinkRegex.exec(child.value); + + if (match) { + const [_, prNumber] = match; + const before = child.value.slice(0, match.index); + const after = child.value.slice(prLinkRegex.lastIndex); + + const link = { + type: 'link', + url: match[0], + children: [{ type: 'text', value: `#${prNumber}` }], + }; + child.value = before; + + tree.children.splice(i + 1, 0, link); + + if (after) { + tree.children.splice(i + 2, 0, { type: 'text', value: after }); + } + + i += 2; + } + } + + prettifyPRLinks(child); + } } -function fixPRLinks(changelog) { - return changelog.replaceAll(/(((https?:\/\/)?(www.)?)?github.com\/[^\s]*?\/pull\/([0-9]+))/g, '[#$5]($1)'); +function addFrontmatter(changelog, header = 'Changelog') { + return `--- +title: ${header} +sidebar_label: ${header} +toc_max_heading_level: 3 +--- +${changelog}`; } function escapeMDXCharacters(changelog) { diff --git a/package-lock.json b/package-lock.json index 280fb35b03..f269f58063 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,7 +72,7 @@ }, "apify-docs-theme": { "name": "@apify/docs-theme", - "version": "1.0.145", + "version": "1.0.146", "license": "ISC", "dependencies": { "@apify/docs-search-modal": "^1.1.1", @@ -82,7 +82,8 @@ "babel-loader": "^9.1.3", "docusaurus-gtm-plugin": "^0.0.2", "postcss-preset-env": "^9.3.0", - "prism-react-renderer": "^2.0.6" + "prism-react-renderer": "^2.0.6", + "remark": "^15.0.1" }, "peerDependencies": { "clsx": "*", @@ -30865,6 +30866,21 @@ "node": ">= 0.10" } }, + "node_modules/remark": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", + "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", + "dependencies": { + "@types/mdast": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-directive": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.0.tgz", @@ -31578,6 +31594,14 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", From 31e8bb9cfb28887da088a860ff54cd96a211d21f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Wed, 11 Dec 2024 14:55:00 +0100 Subject: [PATCH 2/4] docs: add function docstrings --- apify-docs-theme/src/markdown.js | 43 +++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/apify-docs-theme/src/markdown.js b/apify-docs-theme/src/markdown.js index 5d2c41be9e..ad06110ea6 100644 --- a/apify-docs-theme/src/markdown.js +++ b/apify-docs-theme/src/markdown.js @@ -1,5 +1,10 @@ const { remark } = require('remark'); +/** + * Updates the markdown content for better UX and compatibility with Docusaurus v3. + * @param {string} changelog The markdown content. + * @returns {string} The updated markdown content. + */ function updateChangelog(changelog) { const tree = remark.parse(changelog); @@ -13,6 +18,13 @@ function updateChangelog(changelog) { return changelog; } +/** + * Bumps the headings levels in the markdown content. This function increases the depth + * of all headings in the content by 1. This is useful when the content is included in + * another markdown file with a higher-level heading. + * @param {*} tree Remark AST tree. + * @returns {void} Nothing. This function modifies the tree in place. + */ function bumpHeadingsLevels(tree) { tree.children?.forEach((child) => { if (child.type === 'heading') { @@ -23,6 +35,12 @@ function bumpHeadingsLevels(tree) { }); } +/** + * Links user tags in the markdown content. This function replaces the user tags + * (e.g. `@username`) with a link to the user's GitHub profile (just like GitHub's UI). + * @param {*} tree Remark AST tree. + * @returns {void} Nothing. This function modifies the tree in place. + */ function linkifyUserTags(tree) { for (let i = 0; i < tree.children?.length; i++) { const child = tree.children[i]; @@ -56,7 +74,12 @@ function linkifyUserTags(tree) { } } -// If there is a PR URL (https://github.com/**/**/pull/number) in the text body, split the body, and replace the URL with a link to the PR (the text should be the PR number). +/** + * Prettifies PR links in the markdown content. Just like GitHub's UI, this function + * replaces the full PR URL with a link represented by the PR number (prefixed by a hashtag). + * @param {*} tree Remark AST tree. + * @returns {void} Nothing. This function modifies the tree in place. + */ function prettifyPRLinks(tree) { for (let i = 0; i < tree.children?.length; i++) { const child = tree.children[i]; @@ -90,15 +113,27 @@ function prettifyPRLinks(tree) { } } -function addFrontmatter(changelog, header = 'Changelog') { +/** + * Adds frontmatter to the markdown content. + * @param {string} changelog The markdown content. + * @param {string} title The frontmatter title. + * @returns {string} The markdown content with frontmatter. + */ +function addFrontmatter(changelog, title = 'Changelog') { return `--- -title: ${header} -sidebar_label: ${header} +title: ${title} +sidebar_label: ${title} toc_max_heading_level: 3 --- ${changelog}`; } +/** + * Escapes the MDX-related characters in the markdown content. + * This is required by Docusaurus v3 and its dependencies (see the v3 [migration guide](https://docusaurus.io/docs/migration/v3#common-mdx-problems)). + * @param {string} changelog The markdown content. + * @returns {string} The markdown content with escaped MDX characters. + */ function escapeMDXCharacters(changelog) { return changelog.replaceAll(/<|>/g, (match) => { return match === '<' ? '<' : '>'; From d3b78571661c9374958310885a8c87f828f65c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Wed, 11 Dec 2024 15:14:11 +0100 Subject: [PATCH 3/4] chore: fix lint --- apify-docs-theme/src/markdown.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apify-docs-theme/src/markdown.js b/apify-docs-theme/src/markdown.js index ad06110ea6..da44d27648 100644 --- a/apify-docs-theme/src/markdown.js +++ b/apify-docs-theme/src/markdown.js @@ -49,7 +49,8 @@ function linkifyUserTags(tree) { const match = userTagRegex.exec(child.value); if (match) { - const [_, username, ending] = match; + const username = match[1]; + const ending = match[2] === ' ' ? ' ' : ''; const before = child.value.slice(0, match.index); const after = child.value.slice(userTagRegex.lastIndex); @@ -88,7 +89,7 @@ function prettifyPRLinks(tree) { const match = prLinkRegex.exec(child.value); if (match) { - const [_, prNumber] = match; + const prNumber = match[1]; const before = child.value.slice(0, match.index); const after = child.value.slice(prLinkRegex.lastIndex); From 3ae9a003cc5acb1f3e49e087a876cf5030832882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Thu, 12 Dec 2024 13:27:32 +0100 Subject: [PATCH 4/4] chore: rewrite with `unified` pipelines --- apify-docs-theme/package.json | 5 +- apify-docs-theme/src/markdown.js | 154 +++++++++++++++---------------- package-lock.json | 86 ++++++++++++----- 3 files changed, 139 insertions(+), 106 deletions(-) diff --git a/apify-docs-theme/package.json b/apify-docs-theme/package.json index 9953f41c25..2c4068dd94 100644 --- a/apify-docs-theme/package.json +++ b/apify-docs-theme/package.json @@ -27,7 +27,10 @@ "docusaurus-gtm-plugin": "^0.0.2", "postcss-preset-env": "^9.3.0", "prism-react-renderer": "^2.0.6", - "remark": "^15.0.1" + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.5", + "unist-util-visit-parents": "^3.1.1" }, "peerDependencies": { "clsx": "*", diff --git a/apify-docs-theme/src/markdown.js b/apify-docs-theme/src/markdown.js index da44d27648..d1d5b295b0 100644 --- a/apify-docs-theme/src/markdown.js +++ b/apify-docs-theme/src/markdown.js @@ -1,4 +1,7 @@ -const { remark } = require('remark'); +const remarkParse = require('remark-parse'); +const remarkStringify = require('remark-stringify'); +const { unified } = require('unified'); +const visitParents = require('unist-util-visit-parents'); /** * Updates the markdown content for better UX and compatibility with Docusaurus v3. @@ -6,13 +9,14 @@ const { remark } = require('remark'); * @returns {string} The updated markdown content. */ function updateChangelog(changelog) { - const tree = remark.parse(changelog); - - bumpHeadingsLevels(tree); - linkifyUserTags(tree); - prettifyPRLinks(tree); - - changelog = remark.stringify(tree); + const pipeline = unified() + .use(remarkParse) + .use(incrementHeadingLevels) + .use(prettifyPRLinks) + .use(linkifyUserTags) + .use(remarkStringify); + + changelog = pipeline.processSync(changelog).toString(); changelog = addFrontmatter(changelog); changelog = escapeMDXCharacters(changelog); return changelog; @@ -25,15 +29,11 @@ function updateChangelog(changelog) { * @param {*} tree Remark AST tree. * @returns {void} Nothing. This function modifies the tree in place. */ -function bumpHeadingsLevels(tree) { - tree.children?.forEach((child) => { - if (child.type === 'heading') { - child.depth += 1; - } - - bumpHeadingsLevels(child); +const incrementHeadingLevels = () => (tree) => { + visitParents(tree, 'heading', (node) => { + node.depth += 1; }); -} +}; /** * Links user tags in the markdown content. This function replaces the user tags @@ -41,39 +41,35 @@ function bumpHeadingsLevels(tree) { * @param {*} tree Remark AST tree. * @returns {void} Nothing. This function modifies the tree in place. */ -function linkifyUserTags(tree) { - for (let i = 0; i < tree.children?.length; i++) { - const child = tree.children[i]; - if (child.type === 'text') { - const userTagRegex = /@([a-zA-Z0-9-]+)(\s|$)/g; - const match = userTagRegex.exec(child.value); - - if (match) { - const username = match[1]; - const ending = match[2] === ' ' ? ' ' : ''; - const before = child.value.slice(0, match.index); - const after = child.value.slice(userTagRegex.lastIndex); - - const link = { - type: 'link', - url: `https://github.com/${username}`, - children: [{ type: 'text', value: `@${username}` }], - }; - child.value = before; - - tree.children.splice(i + 1, 0, link); - - if (after) { - tree.children.splice(i + 2, 0, { type: 'text', value: `${ending}${after}` }); - } - - i += 2; - } - } - - linkifyUserTags(child); - } -} +const linkifyUserTags = () => (tree) => { + visitParents(tree, 'text', (node, parents) => { + const userTagRegex = /@([a-zA-Z0-9-]+)(\s|$)/g; + const match = userTagRegex.exec(node.value); + + if (!match) return; + + const directParent = parents[parents.length - 1]; + const nodeIndexInParent = directParent.children.findIndex((x) => x === node); + + const username = match[1]; + const ending = match[2] === ' ' ? ' ' : ''; + const before = node.value.slice(0, match.index); + const after = node.value.slice(userTagRegex.lastIndex); + + const link = { + type: 'link', + url: `https://github.com/${username}`, + children: [{ type: 'text', value: `@${username}` }], + }; + node.value = before; + directParent.children.splice(nodeIndexInParent + 1, 0, link); + + if (!after) return nodeIndexInParent + 2; + + directParent.children.splice(nodeIndexInParent + 2, 0, { type: 'text', value: `${ending}${after}` }); + return nodeIndexInParent + 3; + }); +}; /** * Prettifies PR links in the markdown content. Just like GitHub's UI, this function @@ -81,38 +77,34 @@ function linkifyUserTags(tree) { * @param {*} tree Remark AST tree. * @returns {void} Nothing. This function modifies the tree in place. */ -function prettifyPRLinks(tree) { - for (let i = 0; i < tree.children?.length; i++) { - const child = tree.children[i]; - if (child.type === 'text') { - const prLinkRegex = /https:\/\/github.com\/.*\/pull\/(\d+)/g; - const match = prLinkRegex.exec(child.value); - - if (match) { - const prNumber = match[1]; - const before = child.value.slice(0, match.index); - const after = child.value.slice(prLinkRegex.lastIndex); - - const link = { - type: 'link', - url: match[0], - children: [{ type: 'text', value: `#${prNumber}` }], - }; - child.value = before; - - tree.children.splice(i + 1, 0, link); - - if (after) { - tree.children.splice(i + 2, 0, { type: 'text', value: after }); - } - - i += 2; - } - } - - prettifyPRLinks(child); - } -} +const prettifyPRLinks = () => (tree) => { + visitParents(tree, 'text', (node, parents) => { + const prLinkRegex = /https:\/\/github.com\/[^\s]+\/pull\/(\d+)/g; + const match = prLinkRegex.exec(node.value); + + if (!match) return; + + const directParent = parents[parents.length - 1]; + const nodeIndexInParent = directParent.children.findIndex((x) => x === node); + + const prNumber = match[1]; + const before = node.value.slice(0, match.index); + const after = node.value.slice(prLinkRegex.lastIndex); + + const link = { + type: 'link', + url: match[0], + children: [{ type: 'text', value: `#${prNumber}` }], + }; + node.value = before; + + directParent.children.splice(nodeIndexInParent + 1, 0, link); + if (!after) return nodeIndexInParent + 1; + + directParent.children.splice(nodeIndexInParent + 2, 0, { type: 'text', value: after }); + return nodeIndexInParent + 2; + }); +}; /** * Adds frontmatter to the markdown content. diff --git a/package-lock.json b/package-lock.json index f269f58063..1d2f772ef9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,7 +83,10 @@ "docusaurus-gtm-plugin": "^0.0.2", "postcss-preset-env": "^9.3.0", "prism-react-renderer": "^2.0.6", - "remark": "^15.0.1" + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.5", + "unist-util-visit-parents": "^3.1.1" }, "peerDependencies": { "clsx": "*", @@ -20519,6 +20522,20 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/mdast-util-directive/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", @@ -20556,6 +20573,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mdast-util-find-and-replace/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-from-markdown": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", @@ -30866,21 +30897,6 @@ "node": ">= 0.10" } }, - "node_modules/remark": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", - "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", - "dependencies": { - "@types/mdast": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/remark-directive": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.0.tgz", @@ -31594,14 +31610,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/remark/node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "dependencies": { - "@types/unist": "*" - } - }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -34305,6 +34313,36 @@ } }, "node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/unist-util-visit-parents/node_modules/unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit/node_modules/unist-util-visit-parents": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==",