diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 641d523fbdd2..000000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: CodeQL analysis - -# Runs CodeQL security scanning for the internal and public docs repositories. - -on: - pull_request: - branches: - - main - # Push runs give pull request scans a base branch for comparison. - push: - branches: - - main - -permissions: - actions: read - contents: read - security-events: write - -# Newer runs cancel queued or running scans for the same ref. -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' - cancel-in-progress: true - -jobs: - build: - if: github.repository == 'github/docs-internal' || github.repository == 'github/docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 - with: - languages: javascript - - uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 - # Pull request scans stay non-blocking so analysis errors do not hold up merges. - # Push runs must fail so the alert steps below fire. - continue-on-error: ${{ github.event_name == 'pull_request' }} - - - uses: ./.github/actions/create-workflow-failure-issue - id: create-failure-issue - if: ${{ failure() && github.event_name != 'pull_request' }} - with: - token: ${{ secrets.DOCS_BOT_PAT_BASE }} - - - uses: ./.github/actions/slack-alert - if: ${{ failure() && github.event_name != 'pull_request' }} - with: - slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - issue_url: ${{ steps.create-failure-issue.outputs.issue_url }} diff --git a/content/copilot/concepts/billing-and-usage/copilot-usage-metrics/copilot-metrics.md b/content/copilot/concepts/billing-and-usage/copilot-usage-metrics/copilot-metrics.md index 1d9bd8b42604..5477866c41ee 100644 --- a/content/copilot/concepts/billing-and-usage/copilot-usage-metrics/copilot-metrics.md +++ b/content/copilot/concepts/billing-and-usage/copilot-usage-metrics/copilot-metrics.md @@ -147,6 +147,53 @@ This shows the relative impact of deeper adoption, independent of how many users For example, if users in the engaged cohorts averaged 20 pull requests/user/month and the passive cohort averaged 10, then the equation is 20 / 10 = 2x multiplier. +### Understanding industry comparisons + +The impact dashboard's **Industry comparison** widget compares two measures for your enterprise, or for an organization that isn't owned by an enterprise, with aggregated results from participating peers: + +* **Agentic adoption**: The percentage of active users in your enterprise or organization who are agent-first or multi-agent users. +* **Leverage**: The ratio between the pull request merge rate for agentic users and the rate for non-agentic users (per user per month). For example, a leverage score of 4.1x means that agentic users merged pull requests at 4.1 times the rate of non-agentic users during the reporting period. + +Each comparison covers the 28-day reporting period shown in the widget. Peer cohorts use the company size and industry selected when you opt in. Company size options are 1–999, 1,000–4,999, and 5,000+ employees, and there are ten industry options. These values are self-reported and aren't verified. You can compare with peers in your selected industry or across all industries. + +When enough eligible peers are available, each card shows: + +* Where your score ranks within the cohort. +* The difference between your score and the peer median. +* The difference between your score and the top decile. + +The top-decile comparison requires a larger peer cohort than the median comparison, so the median can be available when the top decile isn't. Enterprises and organizations with very few active users can view comparisons, but they aren't counted toward any peer cohort. + +The chart plots your enterprise's score against the distribution of peer scores and marks the peer median and top decile. Some markers may be unavailable if the cohort does not contain enough eligible peers. + +For comparisons within your selected industry, the dashboard expands the peer cohort in this order until enough eligible peers are available: + +1. Participating peers with the same company size and industry. +1. Participating peers with the same company size, across all industries. + +Comparisons always use peers with the same company size. The widget shows which peer cohort was used, so you can tell when a comparison has been expanded beyond your selected industry. + +For {% data variables.product.prodname_ghe_cloud %} with data residency, peer cohorts include only participants hosted in the same data residency region. The dashboard applies the same cohort expansion within that region and does not combine data across regions. + +The dashboard does not use a cohort based only on industry. Comparisons across all industries use participating peers, rather than all {% data variables.product.github %} users or enterprises. + +Participation is controlled by the **Copilot metrics industry comparison** policy and is disabled by default. Enterprises, and organizations that aren't owned by an enterprise, can participate. Organizations owned by an enterprise participate through their enterprise. Enterprises used only to assign {% data variables.copilot.copilot_business_short %} licenses can't participate, because leverage can't be calculated for them. Enabling the policy both contributes your aggregated metrics to peer cohorts and lets you view comparisons. For details, see [AUTOTITLE](/copilot/how-tos/administer-copilot/view-impact-dashboard#opting-in-to-industry-comparisons). + +Rankings appear only after enough enterprises and organizations have opted in to form a peer cohort. If there are not enough eligible peers after the dashboard applies the fallback cohorts, it doesn't show a ranking. A ranking is also unavailable when your own score can't be calculated, such as when there are no active users in the reporting period or no non-agentic users to provide a baseline for leverage. + +#### How industry comparisons protect your data + +* **Opt-in only.** Nothing is contributed until an enterprise owner, or the owner of an organization that isn't owned by an enterprise, enables the **Copilot metrics industry comparison** policy. Enterprises used only to assign {% data variables.copilot.copilot_business_short %} licenses never contribute. +* **Only two summary scores are contributed.** Each participant contributes its agentic adoption and leverage scores for the 28-day reporting period. No user names, user-level activity, code, repository content, or pull request content is contributed. +* **Results are aggregated.** Your scores are combined with those of other participants to calculate cohort statistics, such as the median and top decile. Other participants can't see your enterprise or organization name, which enterprises and organizations participate, or how many participants are in a cohort. +* **Cohort statistics aren't linked to participants.** When cohort statistics are calculated, participants' scores are combined into results that are stored only by date, company size, and industry. These results contain no enterprise, organization, or user identifiers, so they can't be linked back to a specific participant. +* **Rankings are calculated on {% data variables.product.github %}'s servers.** Your own scores are stored with the rest of your {% data variables.product.prodname_copilot_short %} usage metrics. To show your ranking, {% data variables.product.github %} compares your scores with the cohort statistics for the same reporting period. Other participants' scores are never looked up or compared individually. +* **Small cohorts are suppressed.** A cohort's statistics are shown only when it contains enough participants and users. Enterprises and organizations with very few active users aren't counted toward any cohort, so a small number of developers can't shape the results. +* **Company size and industry stay private.** The company size and industry you select are used only to group participants into cohorts. They aren't shown to other participants or verified by {% data variables.product.github %}. +* **Data residency is respected.** For {% data variables.product.prodname_ghe_cloud %} with data residency, data is never combined across data residency regions. +* **Results stay in the dashboard.** Industry comparisons aren't included in downloads, NDJSON exports, or the {% data variables.product.prodname_copilot_short %} usage metrics API. +* **Aggregated results are short-lived.** Cohort statistics are recalculated daily and deleted after 30 days. If you opt out, the dashboard stops showing comparisons immediately, and your data is excluded from cohorts starting with the next daily update. Each daily calculation uses your current opt-in setting, so if earlier results are recalculated after you opt out, your data isn't added back. + ### Estimating potential return on investment The impact dashboard's **Potential return on investment** section provides a comparison of cost and pull request output between **Phase 0-1 Passive and Code First Users** and **Phase 2-3 Agent First Users**. diff --git a/content/copilot/how-tos/administer-copilot/view-impact-dashboard.md b/content/copilot/how-tos/administer-copilot/view-impact-dashboard.md index 33f4a8448d06..cf8dc8e2f3d4 100644 --- a/content/copilot/how-tos/administer-copilot/view-impact-dashboard.md +++ b/content/copilot/how-tos/administer-copilot/view-impact-dashboard.md @@ -26,6 +26,40 @@ For a detailed explanation of what the dashboard shows, including adoption cohor {% data reusables.copilot.access-copilot-metrics-dashboard %} 1. In the left sidebar, click **{% data variables.product.prodname_copilot_short %} impact**. +## Comparing your impact with peers + +The **Industry comparison** widget shows how your agentic adoption and leverage compare with participating peers. Industry comparisons are available for enterprises and for organizations that aren't owned by an enterprise. Organizations owned by an enterprise participate through their enterprise, and enterprises used only to assign {% data variables.copilot.copilot_business_short %} licenses can't participate, because leverage can't be calculated for them. + +### Opting in to industry comparisons + +Participation is controlled by the **Copilot metrics industry comparison** policy, which is disabled by default. When the policy is enabled, your enterprise or organization contributes aggregated {% data variables.product.prodname_copilot_short %} metrics to peer cohorts and can view comparisons. When it's disabled, you don't contribute data or see comparisons. + +Enterprise owners, and owners of organizations that aren't owned by an enterprise, can opt in from the widget or by enabling the policy. The widget and the policy use the same setting. + +1. In the **Industry comparison** widget, click **Opt-in for industry comparison** and review the information about how your data is used. +1. Select values from the **Industry type** and **Company size** dropdown menus. Both values are required to create a peer cohort. +1. Click **Opt in**. + +### Viewing industry comparisons + +1. Use the scope selector to switch between **All industries** and your selected industry. +1. Review the comparison cards for the selected 28-day reporting period: + * **Your agentic adoption** shows the percentage of active users who are agent-first or multi-agent users. + * **Your leverage** compares the pull request merge rate per user per month for agentic users with the rate for non-agentic users. +1. For each card, review your percentile ranking and your difference from the peer median and, if shown, the top decile. The ranking line shows the peer cohort used for the comparison. Use the chart to see your score, the peer median, and the top decile within the peer distribution. + +Rankings appear only after enough enterprises and organizations have opted in to form a peer cohort. The median comparison is shown first, and the top-decile comparison is added when the cohort grows larger. If your selected industry doesn't have enough participants yet, the dashboard compares you with peers of the same company size across all industries. If that cohort doesn't have enough participants either, the dashboard doesn't show a ranking. Especially soon after launch, you might see only the median, all industries instead of your selected industry, or no ranking until more peers opt in. + +A ranking also isn't shown when your own score can't be calculated for the reporting period. For example, there might be no active users, or all active users might already be agent-first or multi-agent users, leaving no non-agentic users to compare leverage with. + +For details about what data is shared and how it's protected, see [AUTOTITLE](/copilot/concepts/copilot-usage-metrics/copilot-metrics#how-industry-comparisons-protect-your-data). + +### Changing your peer cohort or opting out + +To change your peer cohort, click {% octicon "gear" aria-label="Industry comparison settings" %} in the widget, then choose a different industry or company size. The widget uses your new selection right away, and your data moves to the new peer cohort with the next daily update. + +To stop participating, click {% octicon "gear" aria-label="Industry comparison settings" %} in the widget and select **Opt-out** or set the **Copilot metrics industry comparison** policy to **Disabled**. The dashboard stops showing comparisons immediately, and your data is excluded from peer cohorts starting with the next daily update. Previously calculated aggregate results don't identify your enterprise or organization and are deleted after 30 days. + ## Estimating potential return on investment The "Potential return on investment" section provides a directional comparison of cost and pull request output across adoption phases. diff --git a/content/copilot/reference/copilot-usage-metrics/interpret-copilot-metrics.md b/content/copilot/reference/copilot-usage-metrics/interpret-copilot-metrics.md index 18123e5c7eed..c7bad3c420b1 100644 --- a/content/copilot/reference/copilot-usage-metrics/interpret-copilot-metrics.md +++ b/content/copilot/reference/copilot-usage-metrics/interpret-copilot-metrics.md @@ -16,7 +16,7 @@ category: - Track Copilot usage --- -After you’ve viewed the {% data variables.product.prodname_copilot_short %} usage metrics dashboard, you can use this article to interpret each chart and identify opportunities to increase adoption and engagement. +Use this article to interpret usage and adoption metrics across the {% data variables.product.prodname_copilot_short %} usage metrics and impact dashboards, and to identify opportunities to increase adoption and engagement. ## Reviewing overall usage trends @@ -105,6 +105,22 @@ The adoption multiplier compares engaged users (Phase 1, 2, or 3) against passiv > [!NOTE] > The adoption multiplier compares two different populations of users, not the same users over time. Differences in team composition, project complexity, or seniority between engaged and passive users can influence the comparison as much as {% data variables.product.prodname_copilot_short %} adoption itself. +## Reviewing industry comparisons + +The **Industry comparison** widget compares your enterprise's agentic adoption and leverage with aggregated results from participating peers. Review the percentile ranking, peer median, top decile, and peer distribution together. Do not treat any single value as a target. + +| Signal | What it suggests | Suggested action | +|:--|:--|:--| +| Agentic adoption is above the peer median | A larger share of your users have adopted agent-first or multi-agent workflows than in the comparison cohort. | Continue the enablement practices that support agentic workflows, and review which teams still have lower adoption. | +| Agentic adoption is below the peer median | Your enterprise has room to expand adoption of agent-first and multi-agent workflows. | Review the adoption cohort distribution, then prioritize onboarding or enablement for teams that remain passive or code-first. | +| Leverage is above the peer median | The ratio of merge rates between agentic and non-agentic users is higher in your enterprise than in the peer cohort. | Review the practices of teams with strong agentic adoption and consider whether those practices can be applied more broadly. | +| Leverage is below the peer median | The ratio of merge rates between agentic and non-agentic users is lower than for the comparison cohort. | Check team composition, project complexity, and how recently agentic workflows were introduced before changing your rollout plan. | +| Your ranking improves across reporting periods | Your enterprise's position has improved relative to the participating peer cohort. | Confirm that the improvement appears alongside sustained adoption and pull request trends before expanding enablement investment. | +| Results are unavailable | There are not enough eligible participating peers of the same company size to provide the comparison, even after the dashboard expands the peer cohort to all industries. Results are also unavailable when your own score can't be calculated, such as when there are no active users or all active users are already agent-first or multi-agent users, leaving no non-agentic users to compare leverage with. | Review your enterprise's or organization's own adoption and leverage trends where available until a peer comparison is available. | +| Your ranking changes but your score doesn't | The participating peer cohort changed, or the dashboard compared you with a different cohort than in the previous period. | Check which peer cohort the ranking line shows before comparing results across periods. | + +Industry comparisons show associations, not causation. Differences in team composition, project type, engineering practices, and adoption maturity can affect both measures. The participating peer cohort can also change between reporting periods. + ## Reviewing potential return on investment The **Potential return on investment** section compares {% data variables.product.prodname_copilot_short %} cost, estimated percentage of payroll, and pull requests per developer per month across earlier and agent-first adoption phases. Treat these comparisons as directional associations, not proof that a change in adoption caused a change in output. diff --git a/data/code-languages.yml b/data/code-languages.yml index 5162abfb4f9f..8ef3348c5bfd 100644 --- a/data/code-languages.yml +++ b/data/code-languages.yml @@ -46,6 +46,9 @@ jsonc: markdown: name: Markdown comment: xml +mermaid: + name: Mermaid + comment: percent perl: name: Perl comment: number diff --git a/package-lock.json b/package-lock.json index e07ed2e634b5..1eb2f1ec0125 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,6 +65,7 @@ "mdast-util-to-string": "^4.0.0", "next": "^16.3.8", "parse5": "8.0.1", + "pele": "1.3.0", "quick-lru": "7.0.1", "react": "^19.2.5", "react-dom": "^19.2.5", @@ -12735,6 +12736,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pele": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pele/-/pele-1.3.0.tgz", + "integrity": "sha512-Jz3SW2OYo4yLkrrNuQwfg93el1SEGsFVTd7c/XVvZEry6UfR8ncvxoZqAefEpXOFpJRwhmXwWq17aGHg8W0vuA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", diff --git a/package.json b/package.json index cd24f908500b..a67bc0c40d9d 100644 --- a/package.json +++ b/package.json @@ -224,6 +224,7 @@ "mdast-util-to-string": "^4.0.0", "next": "^16.3.8", "parse5": "8.0.1", + "pele": "1.3.0", "quick-lru": "7.0.1", "react": "^19.2.5", "react-dom": "^19.2.5", diff --git a/src/content-render/stylesheets/index.scss b/src/content-render/stylesheets/index.scss index 43671f543aa8..a997e252e7d9 100644 --- a/src/content-render/stylesheets/index.scss +++ b/src/content-render/stylesheets/index.scss @@ -4,5 +4,6 @@ @import "article-section-framing.scss"; @import "syntax-highlighting.scss"; @import "alerts.scss"; +@import "mermaid.scss"; @import "octicon-table-optimization.scss"; @import "accessibility.scss"; diff --git a/src/content-render/stylesheets/mermaid.scss b/src/content-render/stylesheets/mermaid.scss new file mode 100644 index 000000000000..7cad56b02b12 --- /dev/null +++ b/src/content-render/stylesheets/mermaid.scss @@ -0,0 +1,27 @@ +// Maps Pele diagram colors and font to Brand and Primer tokens, so diagrams match the page. +.mermaid-diagram { + margin-bottom: 1rem; + overflow-x: auto; + + .pele { + --pele-bg: var(--brand-color-canvas-default); + --pele-fg: var(--brand-color-text-default); + --pele-muted: var(--brand-color-text-muted); + --pele-line: var(--brand-color-text-default); + --pele-surface: var(--brand-color-canvas-subtle); + --pele-surface-alt: var(--bgColor-neutral-muted); + --pele-border: var(--brand-color-border-default); + --pele-accent: var(--fgColor-accent); + --pele-critical: var(--fgColor-danger); + --pele-series-1: var(--data-blue-color-emphasis); + --pele-series-2: var(--data-orange-color-emphasis); + --pele-series-3: var(--data-green-color-emphasis); + --pele-series-4: var(--data-red-color-emphasis); + --pele-series-5: var(--data-teal-color-emphasis); + --pele-series-6: var(--data-yellow-color-emphasis); + --pele-series-7: var(--data-purple-color-emphasis); + --pele-series-8: var(--data-brown-color-emphasis); + // Pele sizes nodes from Helvetica-like metrics on the server. Mona Sans fits inside that size. + --pele-font: var(--brand-body-fontFamily); + } +} diff --git a/src/content-render/tests/mermaid.ts b/src/content-render/tests/mermaid.ts new file mode 100644 index 000000000000..07b0d1bcfb55 --- /dev/null +++ b/src/content-render/tests/mermaid.ts @@ -0,0 +1,79 @@ +import { describe, expect, test } from 'vitest' +import { load } from 'cheerio' + +import { renderContent } from '@/content-render/index' + +const flowchart = ` +\`\`\`mermaid +flowchart LR + accTitle: Publishing flow + accDescr: A draft goes to review, then gets published + A[Draft] --> B[Review] --> C[Published] +\`\`\` +` + +describe('mermaid', () => { + test('renders a mermaid fence to inline SVG', async () => { + const $ = load(await renderContent(flowchart)) + const svg = $('.mermaid-diagram > svg.pele') + expect(svg.length).toBe(1) + expect(svg.attr('viewBox')).toMatch(/^0 0 \d+/) + expect(svg.find('title').text()).toBe('Publishing flow') + expect($('pre').length).toBe(0) + }) + + test('flips wide flowcharts to vertical so they fit the article column', async () => { + const wide = Array.from({ length: 12 }, (_, i) => ` N${i}[Step number ${i}]`).join(' -->\n') + const $ = load(await renderContent(`\`\`\`mermaid\nflowchart LR\n${wide}\n\`\`\``)) + const [, , width] = ($('svg.pele').attr('viewBox') || '').split(' ').map(Number) + expect(width).toBeLessThanOrEqual(720) + }) + + test('keeps the source direction for flowcharts that fit', async () => { + const $ = load(await renderContent(flowchart)) + const [, , width, height] = ($('svg.pele').attr('viewBox') || '').split(' ').map(Number) + expect(width).toBeGreaterThan(height) + }) + + test('gives each diagram distinct accessibility IDs', async () => { + const $ = load(await renderContent(`${flowchart}\n${flowchart}`)) + const ids = $('svg.pele title') + .map((_, el) => $(el).attr('id')) + .get() + expect(ids.length).toBe(2) + expect(new Set(ids).size).toBe(2) + }) + + test('keeps mermaid source nested in a text fence as code', async () => { + const $ = load(await renderContent(`\`\`\`\`text\n${flowchart}\n\`\`\`\``)) + expect($('svg.pele').length).toBe(0) + expect($('pre').text()).toContain('flowchart LR') + }) + + test('keeps invalid diagrams as code blocks', async () => { + const $ = load(await renderContent('```mermaid\nflowchart LR\n A -->\n```')) + expect($('svg.pele').length).toBe(0) + expect($('pre code.language-mermaid').text()).toContain('A -->') + }) + + test('renders hexagon nodes escaped from Liquid', async () => { + const source = 'flowchart LR\n A{% raw %}{{Decide}}{% endraw %} --> B[Ship]' + const $ = load(await renderContent(`\`\`\`mermaid\n${source}\n\`\`\``)) + expect($('svg.pele').length).toBe(1) + expect($('svg.pele').text()).toContain('Decide') + }) + + test('does not render images inside diagrams', async () => { + const source = 'flowchart LR\n A@{ img: "https://example.com/logo.png", label: "Logo" }' + const $ = load(await renderContent(`\`\`\`mermaid\n${source}\n\`\`\``)) + expect($('svg.pele').length).toBe(1) + expect($('image').length).toBe(0) + }) + + test('does not render links inside diagrams', async () => { + const source = 'flowchart LR\n A[Docs]\n click A "https://example.com"' + const $ = load(await renderContent(`\`\`\`mermaid\n${source}\n\`\`\``)) + expect($('svg.pele').length).toBe(1) + expect($('svg.pele a').length).toBe(0) + }) +}) diff --git a/src/content-render/tests/render-to-hast.ts b/src/content-render/tests/render-to-hast.ts index 0bfddca3c574..8c0a20ddc6bc 100644 --- a/src/content-render/tests/render-to-hast.ts +++ b/src/content-render/tests/render-to-hast.ts @@ -44,6 +44,10 @@ const fixtures: Array<{ name: string; template: string }> = [ name: 'external image', template: '![A logo](https://example.com/logo.png)', }, + { + name: 'mermaid diagram', + template: '```mermaid\nflowchart LR\n A[Draft] --> B[Published]\n```', + }, ] describe('renderUnifiedToHast', () => { diff --git a/src/content-render/unified/README.md b/src/content-render/unified/README.md index cb00579c0798..4be63cc78faa 100644 --- a/src/content-render/unified/README.md +++ b/src/content-render/unified/README.md @@ -21,3 +21,18 @@ Authoring rules: - For HTML fences, add a line such as `` after the annotations to keep syntax highlighting. `parse-info-string.ts` must run before `remark-rehype`, and `annotate` must run before `highlight`. + +## Mermaid diagrams + +The `mermaid` plugin renders fenced code blocks with the `mermaid` language to inline SVG with [Pele](https://pele.run). Pages need no client JavaScript, and diagrams follow the page color mode through the variables in `stylesheets/mermaid.scss`. + +Authoring rules: + +- Use a `mermaid` fence. To show Mermaid source without rendering it, nest the fence in a `text` fence. +- Add `accTitle:` and `accDescr:` lines, so screen readers get a title and description. +- Liquid runs first, so `{% data %}` and `{% ifversion %}` work inside diagrams. Escape hexagon nodes such as `A{{label}}` with `{% raw %}`, or Liquid treats them as output tags. +- Pele lays out diagrams to fit the 720px article column. Flowcharts too wide for the column switch to a vertical layout. +- Links and images inside diagrams do not render. +- Invalid diagrams render as code blocks, and the server logs a warning. + +`mermaid` must run before `codeHeader` and `highlight`. diff --git a/src/content-render/unified/mermaid.ts b/src/content-render/unified/mermaid.ts new file mode 100644 index 000000000000..d275dcdf4801 --- /dev/null +++ b/src/content-render/unified/mermaid.ts @@ -0,0 +1,58 @@ +// Renders ```mermaid fences to inline SVG with Pele, so diagrams need no client JavaScript. +// Fences nested in a ````text fence keep their language as text and stay as source. + +import { visit, SKIP } from 'unist-util-visit' +import { h } from 'hastscript' +import { fromHtml } from 'hast-util-from-html' +import { toString } from 'hast-util-to-string' +import murmur from 'imurmurhash' +import { PeleError, render } from 'pele' +import type { Element, Root } from 'hast' +import { createLogger } from '@/observability/logger' + +const logger = createLogger(import.meta.url) + +// Matches the max-width of the article column in ArticleGridLayout and ArticleInlineLayout. +const ARTICLE_WIDTH = 720 + +export default function mermaid() { + return (tree: Root) => { + let count = 0 + visit(tree, 'element', (node, index, parent) => { + if (index === undefined || !parent || !isMermaidBlock(node)) return + const source = toString(node) + // Diagrams on one page need distinct title and description IDs. + const idPrefix = `mermaid-${murmur(source).result().toString(36)}-${count++}` + try { + const { svg } = render(source, { + maxWidth: ARTICLE_WIDTH, + // Pele flips layouts to vertical only below the breakpoint and only when they do not fit. + directionBreakpoint: ARTICLE_WIDTH + 1, + idPrefix, + // Diagram links and images skip docs link rewriting and checks. + images: false, + links: false, + }) + const diagram = fromHtml(svg, { fragment: true }).children as Element[] + parent.children[index] = h('div', { className: 'mermaid-diagram' }, diagram) + return SKIP + } catch (error) { + if (!(error instanceof PeleError)) throw error + // Leave the source as a code block, so one bad diagram does not break the page. + logger.warn('Could not render Mermaid diagram', { + code: error.code, + line: error.line, + column: error.column, + }) + } + }) + } +} + +function isMermaidBlock(node: Element): boolean { + if (node.tagName !== 'pre') return false + const code = node.children[0] + if (code?.type !== 'element' || code.tagName !== 'code') return false + const classNames = code.properties.className + return Array.isArray(classNames) && classNames.includes('language-mermaid') +} diff --git a/src/content-render/unified/processor.ts b/src/content-render/unified/processor.ts index 4ac6aadad296..bc2f532a5c27 100644 --- a/src/content-render/unified/processor.ts +++ b/src/content-render/unified/processor.ts @@ -15,6 +15,7 @@ import powershell from 'highlight.js/lib/languages/powershell' import graphql from 'highlight.js/lib/languages/graphql' import html from 'rehype-stringify' import codeHeader from './code-header' +import mermaid from './mermaid' import rewriteLocalLinks from './rewrite-local-links' import rewriteImgSources from './rewrite-asset-urls' import rewriteAssetImgTags from './rewrite-asset-img-tags' @@ -52,6 +53,7 @@ export function createProcessor(context: Context): UnifiedProcessor { .use(slug) .use(useEnglishHeadings as unknown as (ctx: Context) => void, context || {}) .use(headingLinks) + .use(mermaid) .use(codeHeader) .use(annotate as unknown as (ctx: Context) => void, context) // TypeScript's unified plugin types do not model rehype-highlight's lowlight options.