diff --git a/packages/assets/icons/message-square-warning.svg b/packages/assets/icons/message-square-warning.svg new file mode 100644 index 000000000..b6a301170 --- /dev/null +++ b/packages/assets/icons/message-square-warning.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/assets/icons/octogon-alert.svg b/packages/assets/icons/octogon-alert.svg new file mode 100644 index 000000000..29d1b506d --- /dev/null +++ b/packages/assets/icons/octogon-alert.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/packages/assets/icons/triangle-alert.svg b/packages/assets/icons/triangle-alert.svg new file mode 100644 index 000000000..a14259515 --- /dev/null +++ b/packages/assets/icons/triangle-alert.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/assets/index.ts b/packages/assets/index.ts index 1eca3e5b2..9ca4b0502 100644 --- a/packages/assets/index.ts +++ b/packages/assets/index.ts @@ -160,6 +160,9 @@ import _XIcon from './icons/x.svg?component' import _XCircleIcon from './icons/x-circle.svg?component' import _ZoomInIcon from './icons/zoom-in.svg?component' import _ZoomOutIcon from './icons/zoom-out.svg?component' +import _MessageSquareWarning from "./icons/message-square-warning.svg?component" +import _OctogonAlert from "./icons/octogon-alert.svg?component" +import _TriangleAlert from "./icons/triangle-alert.svg?component" // Editor Icons import _BoldIcon from './icons/bold.svg?component' @@ -341,3 +344,6 @@ export const Heading2Icon = _Heading2Icon export const Heading3Icon = _Heading3Icon export const CardIcon = _CardIcon export const SparklesIcon = _SparklesIcon +export const MessageSquareWarningIcon = _MessageSquareWarning +export const OctogonAlertIcon = _OctogonAlert +export const TriangleAlertIcon = _TriangleAlert diff --git a/packages/assets/styles/classes.scss b/packages/assets/styles/classes.scss index 1576e0905..fc6c9657a 100644 --- a/packages/assets/styles/classes.scss +++ b/packages/assets/styles/classes.scss @@ -1182,3 +1182,96 @@ select { border-top-left-radius: var(--radius-md) !important; border-top-right-radius: var(--radius-md) !important; } + +// Callouts + +.markdown-alert { + padding: 0.5rem 1rem; + margin-bottom: 16px; + color: inherit; + position: relative; + + &::after { + position: absolute; + top: 0; + left: 0; + bottom: 0; + content: ''; + width: 0.25em; + border-radius: var(--radius-xl); + } + + :first-child { + margin-top: 0; + } + + :last-child { + margin-bottom: 0; + } + + & .markdown-alert-title { + display: flex; + font-weight: 500; + align-items: center; + line-height: 1; + } + + & .markdown-alert-title > svg { + margin-right: 0.5rem; + display: inline-block; + overflow: visible !important; + vertical-align: text-bottom; + height: 1.2em; + width: 1.2em; + } + + &.markdown-alert-note { + & .markdown-alert-title { + color: var(--color-blue); + } + + &::after { + background-color: var(--color-blue); + } + } + + &.markdown-alert-tip { + & .markdown-alert-title { + color: var(--color-green); + } + + &::after { + background-color: var(--color-green); + } + } + + &.markdown-alert-important { + & .markdown-alert-title { + color: var(--color-purple); + } + + &::after { + background-color: var(--color-purple); + } + } + + &.markdown-alert-warning { + & .markdown-alert-title { + color: var(--color-orange); + } + + &::after { + background-color: var(--color-orange); + } + } + + &.markdown-alert-caution { + & .markdown-alert-title { + color: var(--color-red); + } + + &::after { + background-color: var(--color-red); + } + } +} diff --git a/packages/utils/package.json b/packages/utils/package.json index 8a4141b05..459102f8c 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -20,9 +20,11 @@ "@codemirror/state": "^6.3.2", "@codemirror/view": "^6.22.1", "@types/markdown-it": "^14.1.1", + "@modrinth/assets": "workspace:*", "dayjs": "^1.11.10", "highlight.js": "^11.9.0", "markdown-it": "^14.1.0", + "markdown-it-github-alerts": "^0.3.0", "xss": "^1.0.14" } } diff --git a/packages/utils/parse.ts b/packages/utils/parse.ts index 296e9c031..1fdea5061 100644 --- a/packages/utils/parse.ts +++ b/packages/utils/parse.ts @@ -1,4 +1,12 @@ +import { + InfoIcon, + LightBulbIcon, + MessageSquareWarningIcon, + OctogonAlertIcon, + TriangleAlertIcon, +} from '@modrinth/assets' import MarkdownIt from 'markdown-it' +import MarkdownItGitHubAlerts from 'markdown-it-github-alerts' import { escapeAttrValue, FilterXSS, safeAttrValue, whiteList } from 'xss' export const configuredXss = new FilterXSS({ @@ -24,6 +32,19 @@ export const configuredXss = new FilterXSS({ source: ['media', 'sizes', 'src', 'srcset', 'type'], p: [...(whiteList.p || []), 'align'], div: [...(whiteList.p || []), 'align'], + svg: [ + 'aria-hidden', + 'width', + 'height', + 'viewBox', + 'fill', + 'stroke', + 'stroke-width', + 'stroke-linecap', + 'stroke-linejoin', + ], + path: ['d'], + circle: ['cx', 'cy', 'r'], }, css: { whiteList: { @@ -75,6 +96,28 @@ export const configuredXss = new FilterXSS({ } return `${name}="${escapeAttrValue(allowedClasses.join(' '))}"` } + + // For markdown callouts + if (name === 'class' && ['div', 'p'].includes(tag)) { + const classWhitelist = [ + 'markdown-alert', + 'markdown-alert-note', + 'markdown-alert-tip', + 'markdown-alert-warning', + 'markdown-alert-important', + 'markdown-alert-caution', + 'markdown-alert-title', + ] + + const allowed: string[] = [] + for (const className of value.split(/\s/g)) { + if (classWhitelist.includes(className)) { + allowed.push(className) + } + } + + return `${name}="${escapeAttrValue(allowed.join(' '))}"` + } }, safeAttrValue(tag, name, value, cssFilter) { if ( @@ -133,6 +176,16 @@ export const md = (options = {}) => { ...options, }) + md.use(MarkdownItGitHubAlerts, { + icons: { + note: InfoIcon, + tip: LightBulbIcon, + important: MessageSquareWarningIcon, + warning: TriangleAlertIcon, + caution: OctogonAlertIcon, + }, + }) + const defaultLinkOpenRenderer = md.renderer.rules.link_open || function (tokens, idx, options, _env, self) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65615b1dc..1edf070cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -411,6 +411,9 @@ importers: '@codemirror/view': specifier: ^6.22.1 version: 6.28.4 + '@modrinth/assets': + specifier: workspace:* + version: link:../assets '@types/markdown-it': specifier: ^14.1.1 version: 14.1.1 @@ -423,6 +426,9 @@ importers: markdown-it: specifier: ^14.1.0 version: 14.1.0 + markdown-it-github-alerts: + specifier: ^0.3.0 + version: 0.3.0(markdown-it@14.1.0) xss: specifier: ^1.0.14 version: 1.0.15 @@ -4431,6 +4437,11 @@ packages: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} + markdown-it-github-alerts@0.3.0: + resolution: {integrity: sha512-qyIuDyfdrVGHhY+E/44yMyNA3ZnayaT/KKT2VgkIz1nmrgiuPkdgPUh4YBZwgJ9VKEGJvGd82Ndrc4oGftrJWg==} + peerDependencies: + markdown-it: ^14.0.0 + markdown-it@13.0.2: resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==} hasBin: true @@ -11149,6 +11160,10 @@ snapshots: dependencies: semver: 6.3.1 + markdown-it-github-alerts@0.3.0(markdown-it@14.1.0): + dependencies: + markdown-it: 14.1.0 + markdown-it@13.0.2: dependencies: argparse: 2.0.1