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