diff --git a/package-lock.json b/package-lock.json index eff2cd84..eb6ed2b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@node-core/doc-kit", - "version": "1.4.2", + "version": "1.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@node-core/doc-kit", - "version": "1.4.2", + "version": "1.4.3", "dependencies": { "@actions/core": "^3.0.0", "@heroicons/react": "^2.2.0", @@ -48,10 +48,8 @@ "tinyglobby": "^0.2.17", "unified": "^11.0.5", "unist-builder": "^4.0.0", - "unist-util-find-after": "^5.0.0", "unist-util-position": "^5.0.0", "unist-util-remove": "^4.0.0", - "unist-util-select": "^5.1.0", "unist-util-visit": "^5.1.0", "yaml": "^2.9.0" }, @@ -4126,12 +4124,6 @@ "node": ">=4" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "license": "ISC" - }, "node_modules/brace-expansion": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", @@ -4555,22 +4547,6 @@ "node": ">= 8" } }, - "node_modules/css-selector-parser": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", - "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -8530,18 +8506,6 @@ } } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/object-deep-merge": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object-deep-merge/-/object-deep-merge-2.0.1.tgz", @@ -10624,20 +10588,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-find-after": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", - "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -10692,23 +10642,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-select/-/unist-util-select-5.1.0.tgz", - "integrity": "sha512-4A5mfokSHG/rNQ4g7gSbdEs+H586xyd24sdJqF1IWamqrLHvYb+DH48fzxowyOhOfK7YSqX+XlCojAyuuyyT2A==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "css-selector-parser": "^3.0.0", - "devlop": "^1.1.0", - "nth-check": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", diff --git a/package.json b/package.json index 715add8a..f624f81e 100644 --- a/package.json +++ b/package.json @@ -101,10 +101,8 @@ "tinyglobby": "^0.2.17", "unified": "^11.0.5", "unist-builder": "^4.0.0", - "unist-util-find-after": "^5.0.0", "unist-util-position": "^5.0.0", "unist-util-remove": "^4.0.0", - "unist-util-select": "^5.1.0", "unist-util-visit": "^5.1.0", "yaml": "^2.9.0" } diff --git a/src/generators/jsx-ast/utils/plugins/__tests__/transformer.test.mjs b/src/generators/jsx-ast/utils/plugins/__tests__/transformer.test.mjs index 4b7df072..7854b1e5 100644 --- a/src/generators/jsx-ast/utils/plugins/__tests__/transformer.test.mjs +++ b/src/generators/jsx-ast/utils/plugins/__tests__/transformer.test.mjs @@ -39,4 +39,76 @@ describe('jsx-ast transformer', () => { assert.equal(tree.children.includes(footnotes), false); assert.equal(layout.children.at(-1), footnotes); }); + + it('wraps tables in an overflow container and adds responsive labels', () => { + const table = { + type: 'element', + tagName: 'table', + properties: {}, + children: [ + { + type: 'element', + tagName: 'thead', + properties: {}, + children: [ + { + type: 'element', + tagName: 'tr', + properties: {}, + children: [ + { + type: 'element', + tagName: 'th', + properties: {}, + children: [{ type: 'text', value: 'Name' }], + }, + ], + }, + ], + }, + { + type: 'element', + tagName: 'tbody', + properties: {}, + children: [ + { + type: 'element', + tagName: 'tr', + properties: {}, + children: [ + { + type: 'element', + tagName: 'td', + properties: {}, + children: [{ type: 'text', value: 'Alice' }], + }, + ], + }, + ], + }, + ], + }; + + const tree = { + type: 'root', + children: [table], + }; + + transformer()(tree); + + const wrapper = tree.children[0]; + + assert.equal(wrapper.tagName, 'div'); + assert.deepEqual(wrapper.properties.className, ['overflow-container']); + + const transformedTable = wrapper.children[0]; + + assert.equal(transformedTable.tagName, 'table'); + assert.equal( + transformedTable.children[1].children[0].children[0].properties[ + 'data-label' + ], + 'Name' + ); + }); }); diff --git a/src/generators/jsx-ast/utils/plugins/alerts.mjs b/src/generators/jsx-ast/utils/plugins/alerts.mjs index 1abe1940..7fd90b7e 100644 --- a/src/generators/jsx-ast/utils/plugins/alerts.mjs +++ b/src/generators/jsx-ast/utils/plugins/alerts.mjs @@ -7,8 +7,13 @@ import { ALERT_MARKER, GITHUB_ALERT_TYPES } from '../../constants.mjs'; import { createJSXElement } from '../ast.mjs'; /** - * Converts a marker keyword into a human-readable title (e.g. `NOTE` -> `Note`). - * @param {string} type - The uppercase alert keyword + * Converts a marker keyword into a human-readable title. + * + * Example: + * NOTE -> Note + * + * @param {string} type + * @returns {string} */ const toTitle = type => type[0] + type.slice(1).toLowerCase(); @@ -17,46 +22,80 @@ const toTitle = type => type[0] + type.slice(1).toLowerCase(); */ const transformer = tree => { visit(tree, 'blockquote', (node, index, parent) => { - // The marker must be the leading text of the blockquote's first paragraph - const paragraph = node.children[0]; + /** + * Only root-level replacements need a parent. + */ + if (!parent || index === undefined) { + return; + } + + const children = node.children; + const paragraph = children[0]; + /** + * GitHub alerts must start with a paragraph. + */ if (paragraph?.type !== 'paragraph') { return; } - const text = paragraph.children[0]; + const paragraphChildren = paragraph.children; + const firstChild = paragraphChildren[0]; - if (text?.type !== 'text') { + /** + * The marker must be plain text. + */ + if (firstChild?.type !== 'text') { return; } - const match = text.value.match(ALERT_MARKER); + const match = ALERT_MARKER.exec(firstChild.value); if (!match) { return; } - // Strip the marker (and its trailing line break) from the leading text, - // dropping the now-empty text node — and its paragraph — if nothing remains. - text.value = text.value.slice(match[0].length); + /** + * Remove the alert marker. + * + * Example: + * + * "[!NOTE]\nhello" + * + * becomes: + * + * "hello" + */ + firstChild.value = firstChild.value.slice(match[0].length); - if (text.value === '') { - paragraph.children.shift(); + /** + * Remove empty nodes created by stripping the marker. + */ + if (firstChild.value === '') { + paragraphChildren.shift(); } - if (paragraph.children.length === 0) { - node.children.shift(); + if (paragraphChildren.length === 0) { + children.shift(); } + /** + * Replace the blockquote with AlertBox JSX. + */ parent.children[index] = createJSXElement(JSX_IMPORTS.AlertBox.name, { inline: false, - children: node.children, + children, level: GITHUB_ALERT_TYPES[match[1]], title: toTitle(match[1]), }); - // Skip the (now detached) blockquote's children, but revisit this index so - // the new AlertBox is descended into, allowing nested alerts to transform. + /** + * Skip the detached blockquote. + * + * Returning index causes the new AlertBox at this + * position to be visited again, allowing nested + * alerts to transform. + */ return [SKIP, index]; }); }; @@ -69,6 +108,6 @@ const transformer = tree => { * > [!NOTE] * > Highlights information that users should take into account. * - * @see https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts + * @see https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-syntax#alerts */ export default () => transformer; diff --git a/src/generators/jsx-ast/utils/plugins/transformer.mjs b/src/generators/jsx-ast/utils/plugins/transformer.mjs index 7d46e3ab..0d1a89b2 100644 --- a/src/generators/jsx-ast/utils/plugins/transformer.mjs +++ b/src/generators/jsx-ast/utils/plugins/transformer.mjs @@ -4,8 +4,9 @@ import { visit } from 'unist-util-visit'; import { TAG_TRANSFORMS } from '../../constants.mjs'; /** - * Checks whether a HAST node is the generated GFM footnotes section. - * @param {import('hast').Element} node + * Checks whether a node is the generated GFM footnotes section. + * + * @param {import('hast').Node} node */ const isFootnotesSection = node => node?.type === 'element' && @@ -14,11 +15,53 @@ const isFootnotesSection = node => node.properties?.className?.includes('footnotes')); /** - * Finds the generated page Layout node. - * @param {import('hast').Root} tree + * Checks whether a node is the generated Layout component. + * + * @param {import('unist').Node} node + */ +const isLayout = node => + node?.name === 'Layout' && Array.isArray(node.children); + +/** + * Adds responsive labels and wraps tables. + * + * @param {import('hast').Element} table + * @param {import('unist').Parent | undefined} parent + * @param {number | undefined} index */ -const findLayout = tree => - tree.children.find(node => node.name === 'Layout' && node.children); +const transformTable = (table, parent, index) => { + const thead = table.children.find(node => node.tagName === 'thead'); + + if (thead?.children?.[0]?.children) { + const headers = thead.children[0].children.map(toString); + const tbody = table.children.find(node => node.tagName === 'tbody'); + + if (tbody?.children) { + for (const row of tbody.children) { + for (const [cellIndex, cell] of (row.children ?? []).entries()) { + if (cell.tagName === 'td') { + cell.properties ??= {}; + cell.properties['data-label'] = headers[cellIndex]; + } + } + } + } + } + + /** + * Wrap