|
| 1 | +import type { Root, Element, Text, ElementContent, Parents } from 'hast' |
| 2 | +import { visitParents } from 'unist-util-visit-parents' |
| 3 | +import type { Transformer } from 'unified' |
| 4 | + |
| 5 | +// This plugin improves table rendering on reference pages by inserting a <wbr> |
| 6 | +// element into code terms that use camelcase, slashes, or underscores, inspired |
| 7 | +// by http://heap.ch/blog/2016/01/19/camelwrap/ |
| 8 | +// |
| 9 | +// It runs server-side on the HTML AST so the break opportunities are present in |
| 10 | +// the rendered markup (both the HTML string and the hast that React renders), |
| 11 | +// rather than being mutated into the DOM imperatively on the client after |
| 12 | +// hydration. It only applies to `<code>` inside a `<table>`, matching the |
| 13 | +// previous client-side selector `#article-contents table code`. |
| 14 | + |
| 15 | +const wordsLongerThan18Chars = /[\S]{18,}/g |
| 16 | +const camelCaseChars = /([a-z])([A-Z])/g |
| 17 | +const underscoresAfter12thChar = /([\w:]{12}[^_]*?)_/g |
| 18 | +const slashChars = /([/\\])/g |
| 19 | + |
| 20 | +// A sentinel that cannot appear in source text; marks where a <wbr> goes. |
| 21 | +const WBR = '\u0000' |
| 22 | + |
| 23 | +function withBreakOpportunities(value: string): string { |
| 24 | + return value.replace(wordsLongerThan18Chars, (str) => |
| 25 | + str |
| 26 | + // GraphQL code terms use camelcase |
| 27 | + .replace(camelCaseChars, `$1${WBR}$2`) |
| 28 | + // REST code terms use underscores. To keep word breaks looking nice, only |
| 29 | + // break on underscores after the 12th char so `has_organization_projects` |
| 30 | + // breaks after `has_organization` instead of after `has_`. |
| 31 | + .replace(underscoresAfter12thChar, `$1_${WBR}`) |
| 32 | + // Some Actions reference pages have tables with code terms separated by slashes. |
| 33 | + .replace(slashChars, `$1${WBR}`), |
| 34 | + ) |
| 35 | +} |
| 36 | + |
| 37 | +// A fresh <wbr> element per insertion, so inserted nodes never share the same |
| 38 | +// `properties`/`children` object references (which could be mutated later). |
| 39 | +function createWbrElement(): Element { |
| 40 | + return { type: 'element', tagName: 'wbr', properties: {}, children: [] } |
| 41 | +} |
| 42 | + |
| 43 | +// Replace a text node's value with a sequence of text nodes interleaved with |
| 44 | +// <wbr> elements at each break opportunity. Returns null when nothing changed. |
| 45 | +function splitTextNode(node: Text): ElementContent[] | null { |
| 46 | + const replaced = withBreakOpportunities(node.value) |
| 47 | + if (!replaced.includes(WBR)) return null |
| 48 | + |
| 49 | + const out: ElementContent[] = [] |
| 50 | + const parts = replaced.split(WBR) |
| 51 | + for (const [index, part] of parts.entries()) { |
| 52 | + if (part) out.push({ type: 'text', value: part }) |
| 53 | + if (index < parts.length - 1) out.push(createWbrElement()) |
| 54 | + } |
| 55 | + return out |
| 56 | +} |
| 57 | + |
| 58 | +// Walk every descendant text node of `code`, inserting <wbr> elements in place. |
| 59 | +// This naturally handles the case where the code term's text lives inside a |
| 60 | +// child anchor element. |
| 61 | +function insertWordBreaks(code: Element): void { |
| 62 | + const transform = (parent: Element): void => { |
| 63 | + const next: ElementContent[] = [] |
| 64 | + for (const child of parent.children) { |
| 65 | + if (child.type === 'text') { |
| 66 | + const split = splitTextNode(child) |
| 67 | + next.push(...(split ?? [child])) |
| 68 | + } else { |
| 69 | + if (child.type === 'element') transform(child) |
| 70 | + next.push(child) |
| 71 | + } |
| 72 | + } |
| 73 | + parent.children = next |
| 74 | + } |
| 75 | + transform(code) |
| 76 | +} |
| 77 | + |
| 78 | +function hasTableAncestor(ancestors: Parents[]): boolean { |
| 79 | + return ancestors.some((ancestor) => ancestor.type === 'element' && ancestor.tagName === 'table') |
| 80 | +} |
| 81 | + |
| 82 | +export default function wrapCodeTerms(): Transformer<Root> { |
| 83 | + return (tree: Root) => |
| 84 | + visitParents(tree, 'element', (node, ancestors) => { |
| 85 | + const el = node as Element |
| 86 | + if (el.tagName !== 'code') return |
| 87 | + if (!hasTableAncestor(ancestors)) return |
| 88 | + insertWordBreaks(el) |
| 89 | + }) |
| 90 | +} |
0 commit comments