|
| 1 | +// `<Signature>` and `<Param>` carry API metadata (added-in version, deprecation, |
| 2 | +// runtime requirements, types, defaults) as JSX attributes, which plain-text and |
| 3 | +// llms.txt exports would otherwise discard along with the tags. This rewrites each |
| 4 | +// opening tag into the same plain-text sentences the component renders, so the |
| 5 | +// metadata stays readable next to the member's heading. |
| 6 | + |
| 7 | +// Attribute values may contain `>` inside quotes or braces (e.g. |
| 8 | +// `runtime={{ 'Node.js': '>=22.2.0' }}`), so tag matching can't stop at the |
| 9 | +// first `>`. |
| 10 | +export const JSX_ATTRS = `(?:"[^"]*"|'[^']*'|\\{(?:[^{}]|\\{[^{}]*\\})*\\}|[^>"'{])*`; |
| 11 | + |
| 12 | +// Also matches the component's slot markers, so their section titles ("Arguments", |
| 13 | +// "Properties", "Returns") survive as text. |
| 14 | +const JSX_META_TAG = new RegExp( |
| 15 | + `<(Signature|Param)\\b(${JSX_ATTRS})\\/?>|<Fragment\\s+slot=["'](attributes|properties|returns)["']\\s*>`, |
| 16 | + 'g' |
| 17 | +); |
| 18 | + |
| 19 | +/** |
| 20 | + * @param {string} attrs |
| 21 | + * @param {string} name |
| 22 | + * @returns {string | undefined} |
| 23 | + */ |
| 24 | +const getAttr = (attrs, name) => { |
| 25 | + const match = attrs.match(new RegExp(`(?:^|\\s)${name}=(?:"([^"]*)"|'([^']*)')`)); |
| 26 | + return match ? (match[1] ?? match[2]) : undefined; |
| 27 | +}; |
| 28 | + |
| 29 | +/** |
| 30 | + * @param {string} attrs |
| 31 | + * @param {string} name |
| 32 | + * @returns {boolean} |
| 33 | + */ |
| 34 | +const hasFlag = (attrs, name) => new RegExp(`(?:^|\\s)${name}(?=\\s|$)`).test(attrs); |
| 35 | + |
| 36 | +/** |
| 37 | + * Replace `<Signature>`/`<Param>` opening tags with the sentences the component |
| 38 | + * renders ("Added in v4.16.0.", "options (Object, optional):", …). |
| 39 | + * |
| 40 | + * @param {string} content |
| 41 | + * @returns {string} |
| 42 | + */ |
| 43 | +export const signatureMetaToText = (content) => { |
| 44 | + // Title for the next `attributes` slot; set by the enclosing Signature's |
| 45 | + // `attributesTitle` prop. Matches are visited in document order, so the |
| 46 | + // Signature opening tag is always seen before its slots. |
| 47 | + let attributesTitle = 'Arguments'; |
| 48 | + return content.replace(JSX_META_TAG, (tag, component, attrs, slot, offset, source) => { |
| 49 | + // Keep the tag's own indentation, which mirrors the nesting depth in the |
| 50 | + // source, so replacements stay visually grouped under their section. |
| 51 | + const lineStart = source.lastIndexOf('\n', offset - 1) + 1; |
| 52 | + const beforeTag = source.slice(lineStart, offset); |
| 53 | + const indent = /^[ \t]*$/.test(beforeTag) ? beforeTag : ''; |
| 54 | + |
| 55 | + if (slot) { |
| 56 | + const title = |
| 57 | + slot === 'attributes' ? attributesTitle : slot === 'properties' ? 'Properties' : 'Returns'; |
| 58 | + return `\n\n${indent}${title}:\n\n`; |
| 59 | + } |
| 60 | + |
| 61 | + const since = getAttr(attrs, 'since'); |
| 62 | + const deprecated = getAttr(attrs, 'deprecated'); |
| 63 | + |
| 64 | + if (component === 'Param') { |
| 65 | + const name = getAttr(attrs, 'name'); |
| 66 | + if (!name) return tag; |
| 67 | + const details = [ |
| 68 | + getAttr(attrs, 'type'), |
| 69 | + hasFlag(attrs, 'optional') && 'optional', |
| 70 | + getAttr(attrs, 'default') && `default: ${getAttr(attrs, 'default')}`, |
| 71 | + since && `added in ${since}`, |
| 72 | + deprecated && `deprecated in ${deprecated}`, |
| 73 | + ].filter(Boolean); |
| 74 | + return `\n\n${indent}- ${name}${details.length ? ` (${details.join(', ')})` : ''}:\n\n`; |
| 75 | + } |
| 76 | + |
| 77 | + attributesTitle = getAttr(attrs, 'attributesTitle') ?? 'Arguments'; |
| 78 | + const runtime = [...attrs.matchAll(/['"]([^'"]+)['"]\s*:\s*['"]([^'"]+)['"]/g)] |
| 79 | + .map(([, engine, constraint]) => `${engine} ${constraint}`) |
| 80 | + .join(', '); |
| 81 | + const lines = [ |
| 82 | + getAttr(attrs, 'type') && `Type: ${getAttr(attrs, 'type')}.`, |
| 83 | + getAttr(attrs, 'returns') && `Returns: ${getAttr(attrs, 'returns')}.`, |
| 84 | + since && `Added in ${since}.`, |
| 85 | + deprecated && `Deprecated in ${deprecated}.`, |
| 86 | + runtime && `Requires runtime: ${runtime}.`, |
| 87 | + ].filter(Boolean); |
| 88 | + return lines.length ? `\n\n${lines.join(' ')}\n\n` : tag; |
| 89 | + }); |
| 90 | +}; |
0 commit comments