diff --git a/src/content-render/stylesheets/alerts.scss b/src/content-render/stylesheets/alerts.scss new file mode 100644 index 0000000000..ef79e7f73b --- /dev/null +++ b/src/content-render/stylesheets/alerts.scss @@ -0,0 +1,35 @@ +// Largely identical styling from the monolith, +// but the color names match the Primer variables +// and we had to directly state a few props instead of using variables +// that are only in the monolith. + +$colors: "default", "muted", "subtle", "accent", "success", "attention", + "severe", "danger", "open", "closed", "done", "sponsors"; + +.ghd-alert { + padding: var(--base-size-8, 0.5rem) var(--base-size-16, 1rem); + border-left: 0.25em solid + var(--borderColor-default, var(--color-border-default)); + margin-bottom: 1rem; +} + +.ghd-alert > :last-child { + margin-bottom: 0; +} + +.ghd-alert-title { + display: flex; + font-weight: var(--base-text-weight-medium, 500); + align-items: center; + line-height: 1; +} + +@each $color in $colors { + .ghd-alert-#{$color} { + border-left-color: var(--fgColor-#{$color}, var(--color-#{$color}-fg)); + + .ghd-alert-title { + color: var(--fgColor-#{$color}, var(--color-#{$color}-fg)); + } + } +} diff --git a/src/content-render/stylesheets/index.scss b/src/content-render/stylesheets/index.scss index 9a2f5b29bc..047b4cd053 100644 --- a/src/content-render/stylesheets/index.scss +++ b/src/content-render/stylesheets/index.scss @@ -4,3 +4,4 @@ @import "markdown-overrides.scss"; @import "spotlight.scss"; @import "syntax-highlighting.scss"; +@import "alerts.scss"; diff --git a/src/content-render/unified/alerts.js b/src/content-render/unified/alerts.js new file mode 100644 index 0000000000..7f2a743680 --- /dev/null +++ b/src/content-render/unified/alerts.js @@ -0,0 +1,77 @@ +/* +Custom "Alerts", based on similar filter/styling in the monolith code. +*/ + +import { visit } from 'unist-util-visit' +import { h } from 'hastscript' +import octicons from '@primer/octicons' + +const alertTypes = { + NOTE: { icon: 'info', color: 'accent', title: 'Note' }, + IMPORTANT: { icon: 'report', color: 'done', title: 'Important' }, + WARNING: { icon: 'alert', color: 'attention', title: 'Warning' }, + TIP: { icon: 'light-bulb', color: 'success', title: 'Tip' }, + CAUTION: { icon: 'stop', color: 'danger', title: 'Caution' }, +} + +// Must contain one of [!NOTE], [!IMPORTANT], ... +const ALERT_REGEXP = new RegExp(`\\[!(${Object.keys(alertTypes).join('|')})\\]`, 'gi') + +const matcher = (node) => + node.type === 'element' && + node.tagName === 'blockquote' && + ALERT_REGEXP.test(JSON.stringify(node.children)) + +export default function alerts() { + return (tree) => { + visit(tree, matcher, (node) => { + const key = getAlertKey(node) + if (!(key in alertTypes)) { + console.warn( + `Alert key '${key}' should be all uppercase (change it to '${key.toUpperCase()}')`, + ) + } + const alertType = alertTypes[getAlertKey(node).toUpperCase()] + node.tagName = 'div' + node.properties.className = 'ghd-alert ghd-alert-' + alertType.color + node.children = [ + h('p', { className: 'ghd-alert-title' }, getOcticonSVG(alertType.icon), alertType.title), + ...removeAlertSyntax(node.children), + ] + }) + } +} + +function getAlertKey(node) { + const body = JSON.stringify(node.children) + const matches = body.match(ALERT_REGEXP) + return matches[0].slice(2, -1) +} + +function removeAlertSyntax(node) { + if (Array.isArray(node)) { + return node.map(removeAlertSyntax) + } + if (node.children) { + node.children = node.children.map(removeAlertSyntax) + } + if (node.value) { + node.value = node.value.replace(ALERT_REGEXP, '') + } + return node +} + +function getOcticonSVG(name) { + return h( + 'svg', + { + version: '1.1', + width: 16, + height: 16, + viewBox: '0 0 16 16', + className: 'octicon mr-2', + ariaHidden: true, + }, + h('path', { d: octicons[name].heights[16].path.match(/d="(.*)"/)[1] }), + ) +} diff --git a/src/content-render/unified/processor.js b/src/content-render/unified/processor.js index 98caaa15cc..6753bb40d2 100644 --- a/src/content-render/unified/processor.js +++ b/src/content-render/unified/processor.js @@ -25,6 +25,7 @@ import rewriteForRowheaders from './rewrite-for-rowheaders.js' import wrapProceduralImages from './wrap-procedural-images.js' import parseInfoString from './parse-info-string.js' import annotate from './annotate.js' +import alerts from './alerts.js' export function createProcessor(context) { return ( @@ -53,6 +54,7 @@ export function createProcessor(context) { .use(rewriteImgSources) .use(rewriteAssetImgTags) .use(rewriteLocalLinks, context) + .use(alerts) // HTML AST above ^^^ .use(html) // String below vvv diff --git a/src/fixtures/fixtures/content/get-started/markdown/alerts.md b/src/fixtures/fixtures/content/get-started/markdown/alerts.md new file mode 100644 index 0000000000..85769b84b9 --- /dev/null +++ b/src/fixtures/fixtures/content/get-started/markdown/alerts.md @@ -0,0 +1,35 @@ +--- +title: Alerts Markdown +shortTitle: Alerts +intro: Demonstrates the use of special alerts Markdown syntax +versions: + fpt: '*' + ghes: '*' + ghec: '*' +type: how_to +--- + +## Tip + +> [!TIP] +> Here's a free tip + +## Note + +> [!NOTE] +> A note. + +## Important + +> [!IMPORTANT] +> This is important + +## Warning + +> [!WARNING] +> Just a warning + +## Caution + +> [!CAUTION] +> Be careful! diff --git a/src/fixtures/fixtures/content/get-started/markdown/index.md b/src/fixtures/fixtures/content/get-started/markdown/index.md index e8f19ed567..6acdd6d617 100644 --- a/src/fixtures/fixtures/content/get-started/markdown/index.md +++ b/src/fixtures/fixtures/content/get-started/markdown/index.md @@ -10,4 +10,5 @@ children: - /intro - /permissions - /code-annotations + - /alerts --- diff --git a/src/fixtures/tests/markdown.js b/src/fixtures/tests/markdown.js index 4dd8c1afd8..1f87671f80 100644 --- a/src/fixtures/tests/markdown.js +++ b/src/fixtures/tests/markdown.js @@ -20,3 +20,30 @@ describe('markdown rendering', () => { expect(html).toMatch('HubGit Pages site') }) }) + +describe('alerts', () => { + test('basic rendering', async () => { + const $ = await getDOM('/get-started/markdown/alerts') + const alerts = $('#article-contents .ghd-alert') + // See src/fixtures/fixtures/content/get-started/markdown/alerts.md + // to be this confident in the assertions. + expect(alerts.length).toBe(5) + const svgs = $('svg', alerts) + expect(svgs.length).toBe(5) + const titles = $('.ghd-alert-title', alerts) + .map((_, el) => $(el).text()) + .get() + expect(titles).toEqual(['Tip', 'Note', 'Important', 'Warning', 'Caution']) + const bodies = $('p:nth-child(2)', alerts) + .map((_, el) => $(el).text()) + .get() + .map((s) => s.trim()) + expect(bodies).toEqual([ + "Here's a free tip", + 'A note.', + 'This is important', + 'Just a warning', + 'Be careful!', + ]) + }) +})