diff --git a/package.json b/package.json
index 4e84fe7f1d..1b77452d1c 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,8 @@
},
"pnpm": {
"patchedDependencies": {
- "readable-stream@2.3.8": "patches/readable-stream@2.3.8.patch"
+ "readable-stream@2.3.8": "patches/readable-stream@2.3.8.patch",
+ "comark@0.6.2": "patches/comark@0.6.2.patch"
},
"peerDependencyRules": {
"allowedVersions": {
diff --git a/packages/assets/styles/classes.scss b/packages/assets/styles/classes.scss
index 1891dbda89..69c4615458 100644
--- a/packages/assets/styles/classes.scss
+++ b/packages/assets/styles/classes.scss
@@ -825,15 +825,8 @@ a:not(.no-click-animation),
padding-block-start: 0;
}
- blockquote,
- details,
- dl,
- ol,
- p,
- code,
- pre,
- table,
- ul {
+
+ > * {
margin-top: 0;
margin-bottom: 16px;
}
@@ -844,13 +837,31 @@ a:not(.no-click-animation),
line-height: 1.5;
}
- ul.contains-task-list,
- li.task-list-item {
- list-style: none;
+ p {
+ margin-top: 0;
+ margin-bottom: 16px;
}
- .task-list-item-checkbox {
- margin-right: 0.5em;
+ ul {
+ list-style: disc;
+ padding-left: 1.5em;
+
+ ul {
+ list-style: circle;
+
+ ul {
+ list-style: square;
+ }
+ }
+ }
+
+ ol {
+ list-style: decimal;
+ padding-left: 1.5em;
+ }
+
+ li.task-list-item {
+ list-style: none;
}
h1 {
diff --git a/packages/assets/styles/highlightjs.scss b/packages/assets/styles/highlightjs.scss
index 9b5eb74335..1c6bf43f29 100644
--- a/packages/assets/styles/highlightjs.scss
+++ b/packages/assets/styles/highlightjs.scss
@@ -70,20 +70,3 @@
.hljs-strong {
font-weight: bold;
}
-
-pre {
- background-color: #222222;
- padding: 1em 1em 1em 1em;
- border-width: 5px;
- border-radius: 2em;
- border-color: var(--color-divider);
- overflow-x: hidden;
-
- code {
- line-height: 100%;
- padding: 0.2em;
- letter-spacing: -0.05em;
- word-break: normal;
- font-family: monospace;
- }
-}
diff --git a/packages/ui/src/components/base/MarkdownBody.vue b/packages/ui/src/components/base/MarkdownBody.vue
index e5d676f7fd..18cecec1fa 100644
--- a/packages/ui/src/components/base/MarkdownBody.vue
+++ b/packages/ui/src/components/base/MarkdownBody.vue
@@ -37,6 +37,7 @@ import MarkdownCollectionEmbed from './markdown/MarkdownCollectionEmbed.vue'
import MarkdownHighlightedPre from './markdown/MarkdownHighlightedPre.vue'
import MarkdownOrganizationEmbed from './markdown/MarkdownOrganizationEmbed.vue'
import MarkdownProjectEmbed from './markdown/MarkdownProjectEmbed.vue'
+import MarkdownTaskCheckbox from './markdown/MarkdownTaskCheckbox.vue'
import MarkdownUserEmbed from './markdown/MarkdownUserEmbed.vue'
defineOptions({
@@ -82,6 +83,7 @@ const components = computed(() =>
user: MarkdownUserEmbed,
organization: MarkdownOrganizationEmbed,
collection: MarkdownCollectionEmbed,
+ input: MarkdownTaskCheckbox,
...(props.highlight ? { pre: MarkdownHighlightedPre } : {}),
},
)
diff --git a/packages/ui/src/components/base/markdown/MarkdownTaskCheckbox.vue b/packages/ui/src/components/base/markdown/MarkdownTaskCheckbox.vue
new file mode 100644
index 0000000000..92c7a7c6db
--- /dev/null
+++ b/packages/ui/src/components/base/markdown/MarkdownTaskCheckbox.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/packages/utils/markdown/html-block.ts b/packages/utils/markdown/html-block.ts
new file mode 100644
index 0000000000..2672741d77
--- /dev/null
+++ b/packages/utils/markdown/html-block.ts
@@ -0,0 +1,128 @@
+import { defineComarkPlugin } from 'comark/parse'
+import { Parser as HtmlTagParser } from 'htmlparser2'
+import type Token from 'markdown-it/lib/token.mjs'
+
+import type MarkdownIt = require('markdown-it')
+
+// This is here to keep backwards compatibility
+// Comark closes most open html tags when it hits a blank line,
+// we make it only do that if we can't find a closing tag, in order to mimic the side effect of
+// markdown-it emitting html entirely raw (which your browser is then lenient with unclosed tags)
+// whereas comark does its fancy AST which means it can't just emit the html raw
+// Thank you for coming to my TED talk -chyz
+
+type RuleBlock = MarkdownIt.ParserBlock.RuleBlock
+type StateBlock = MarkdownIt.StateBlock
+
+// If you know a better way to do this go ahead
+function findHtmlBlockRuleFn(md: MarkdownIt): RuleBlock | undefined {
+ const rules = (md.block.ruler as unknown as { __rules__: { name: string; fn: RuleBlock }[] })
+ .__rules__
+ return rules?.find((r) => r.name === 'comark_html_block')?.fn
+}
+
+function extractOpenTagName(line: string): string | undefined {
+ return /^<([a-zA-Z][a-zA-Z0-9-]*)/.exec(line)?.[1]?.toLowerCase()
+}
+
+function parseOpenTagAttrs(line: string): [string, string][] {
+ const attrs: [string, string][] = []
+ const parser = new HtmlTagParser({
+ onopentag(_name, attrObj) {
+ for (const [key, value] of Object.entries(attrObj)) attrs.push([key, value])
+ },
+ })
+ parser.write(line)
+ parser.end()
+ return attrs
+}
+
+function markdownItModrinthHtmlBlock(md: MarkdownIt) {
+ const htmlBlockFn = findHtmlBlockRuleFn(md)
+ if (!htmlBlockFn) return
+
+ md.block.ruler.before(
+ 'comark_html_block',
+ 'modrinth_html_block',
+ (state: StateBlock, startLine: number, endLine: number, silent: boolean) => {
+ if (silent) return htmlBlockFn(state, startLine, endLine, true)
+
+ const tokensBefore = state.tokens.length
+ const lineBefore = state.line
+ const matched = htmlBlockFn(state, startLine, endLine, false)
+ if (!matched) return false
+
+ const token = state.tokens[state.tokens.length - 1]
+ if (token.type !== 'html_block' || !token.map) return true
+
+ const firstLine = (token.content.split('\n')[0] ?? '').trim()
+ if (firstLine.endsWith('/>')) return true
+
+ const tagName = extractOpenTagName(firstLine)
+ if (!tagName) return true
+
+ const openLineRe = new RegExp(`^<${tagName}(\\s|/?>|$)`, 'i')
+ const closeLineRe = new RegExp(`^${tagName}\\s*>$`, 'i')
+ let depth = 0
+ let closeLine = -1
+ for (let line = startLine + 1; line < endLine; line++) {
+ if (state.sCount[line] < state.blkIndent) break
+ const p = state.bMarks[line] + state.tShift[line]
+ const mx = state.eMarks[line]
+ const text = state.src.slice(p, mx).trim()
+ if (openLineRe.test(text) && !text.endsWith('/>')) {
+ depth++
+ continue
+ }
+ if (closeLineRe.test(text)) {
+ if (depth > 0) {
+ depth--
+ continue
+ }
+ closeLine = line
+ break
+ }
+ }
+ if (closeLine === -1 || closeLine < token.map[1]) return true
+
+ state.tokens.length = tokensBefore
+ state.line = lineBefore
+
+ const attrs = parseOpenTagAttrs(firstLine)
+ const oldParent = state.parentType
+ const oldLineMax = state.lineMax
+ state.parentType = 'comark_block' as StateBlock['parentType']
+ state.lineMax = closeLine
+
+ const tokenOpen: Token & { block?: boolean } = state.push('mdc_block_open', tagName, 1)
+ tokenOpen.block = true
+ tokenOpen.map = [startLine, closeLine + 1]
+ for (const [key, value] of attrs) tokenOpen.attrSet(key, value)
+
+ const blkIndent = state.blkIndent
+ state.blkIndent = 0
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const env = state.env as any
+ env.comarkBlockTokens ||= []
+ env.comarkBlockTokens.unshift(tokenOpen)
+ state.md.block.tokenize(state, startLine + 1, closeLine)
+ state.blkIndent = blkIndent
+ env.comarkBlockTokens.shift()
+
+ const tokenClose: Token & { block?: boolean } = state.push('mdc_block_close', tagName, -1)
+ tokenClose.map = [startLine, closeLine + 1]
+ tokenClose.block = true
+
+ state.parentType = oldParent
+ state.lineMax = oldLineMax
+ state.line = closeLine + 1
+ return true
+ },
+ { alt: ['paragraph', 'reference', 'blockquote'] },
+ )
+}
+
+export const modrinthHtmlBlock = defineComarkPlugin(() => ({
+ name: 'modrinth-html-block',
+ markdownItPlugins: [markdownItModrinthHtmlBlock],
+}))
diff --git a/packages/utils/markdown/parse.ts b/packages/utils/markdown/parse.ts
index 8f634055fa..7958e44ec7 100644
--- a/packages/utils/markdown/parse.ts
+++ b/packages/utils/markdown/parse.ts
@@ -16,6 +16,7 @@ import taskList from 'comark/plugins/task-list'
import { visitAsync } from 'comark/utils'
import { modrinthEmbedSyntax } from './embeds'
+import { modrinthHtmlBlock } from './html-block'
import { modrinthResolveMedia } from './media'
import { modrinthSecurity } from './security'
@@ -44,6 +45,7 @@ export const modrinthPlugins: ComarkPlugin[] = [
// frontmatter()
// heading()
html(),
+ modrinthHtmlBlock(),
// json-render()
// math(), // needs dep
// mermaid(), // needs dep
@@ -53,7 +55,7 @@ export const modrinthPlugins: ComarkPlugin[] = [
modrinthResolveMedia(),
modrinthSecurity(),
// shiki(), // needs dep + we have highlight.js
- // summary() // irrelevant
+ // summary(), //TODO
taskList(),
// toc() // would be cool to have but would need heavy changes to project pages
]
diff --git a/packages/utils/package.json b/packages/utils/package.json
index 917a805ad0..3fb1c695f0 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -10,7 +10,8 @@
},
"devDependencies": {
"@modrinth/api-client": "workspace:*",
- "@modrinth/tooling-config": "workspace:*"
+ "@modrinth/tooling-config": "workspace:*",
+ "@types/markdown-it": "14.1.2"
},
"dependencies": {
"@codemirror/commands": "^6.3.2",
@@ -24,6 +25,7 @@
"dayjs": "^1.11.10",
"highlight.js": "^11.9.0",
"highlightjs-mcfunction": "github:modrinth/better-highlightjs-mcfunction",
+ "htmlparser2": "^12.0.0",
"ofetch": "^1.3.4",
"three": "^0.172.0"
}
diff --git a/patches/comark@0.6.2.patch b/patches/comark@0.6.2.patch
new file mode 100644
index 0000000000..f8053d6cb6
--- /dev/null
+++ b/patches/comark@0.6.2.patch
@@ -0,0 +1,21 @@
+diff --git a/dist/internal/parse/token-processor.js b/dist/internal/parse/token-processor.js
+index 440841f8d9252067681f09df935dd0287fff2cf0..71c44f24a932a1f900329800f28029206aa5a13f 100644
+--- a/dist/internal/parse/token-processor.js
++++ b/dist/internal/parse/token-processor.js
+@@ -483,6 +483,20 @@ function processBlockChildren(tokens, startIndex, closeType, inlineOnly, inHeadi
+ }
+ i++;
+ }
++ else if (token.type === 'paragraph_open' && token.hidden) {
++ i++;
++ if (tokens[i] && tokens[i].type === 'inline') {
++ nodes.push(...processInlineTokens(tokens[i].children || [], inHeading));
++ i++;
++ }
++ if (tokens[i] && tokens[i].type === 'paragraph_close') {
++ i++;
++ }
++ }
+ else {
+ const result = processBlockToken(tokens, i, insideNestedContext, state);
+ i = result.nextIndex;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 204f5e6a0d..7ce89af57f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false
patchedDependencies:
+ comark@0.6.2:
+ hash: 4e73fba3ed127a788219ec2e7a413966f48fc7695802a1b0f2694ef2686d475c
+ path: patches/comark@0.6.2.patch
readable-stream@2.3.8:
hash: 3045f7adf989b4e668a4a13819b7285b0de186b79bbf22408acaf2a41c6eaa86
path: patches/readable-stream@2.3.8.patch
@@ -708,7 +711,7 @@ importers:
version: 4.7.0
comark:
specifier: ^0.6.2
- version: 0.6.2
+ version: 0.6.2(patch_hash=4e73fba3ed127a788219ec2e7a413966f48fc7695802a1b0f2694ef2686d475c)
dayjs:
specifier: ^1.11.10
version: 1.11.19
@@ -718,12 +721,12 @@ importers:
es-toolkit:
specifier: ^1.44.0
version: 1.44.0
- flatpickr:
- specifier: ^4.6.13
- version: 4.6.13
fabric:
specifier: ^7.4.0
version: 7.4.0
+ flatpickr:
+ specifier: ^4.6.13
+ version: 4.6.13
floating-vue:
specifier: ^5.2.2
version: 5.2.2(@nuxt/kit@3.21.0(magicast@0.5.1))(vue@3.5.27(typescript@5.9.3))
@@ -859,7 +862,7 @@ importers:
version: 0.172.0
comark:
specifier: ^0.6.2
- version: 0.6.2
+ version: 0.6.2(patch_hash=4e73fba3ed127a788219ec2e7a413966f48fc7695802a1b0f2694ef2686d475c)
dayjs:
specifier: ^1.11.10
version: 1.11.19
@@ -869,6 +872,9 @@ importers:
highlightjs-mcfunction:
specifier: github:modrinth/better-highlightjs-mcfunction
version: https://codeload.github.com/modrinth/better-highlightjs-mcfunction/tar.gz/aa999b763fd792ffb950d28347eeb6811c83ea8e
+ htmlparser2:
+ specifier: ^12.0.0
+ version: 12.0.0
ofetch:
specifier: ^1.3.4
version: 1.5.1
@@ -882,6 +888,9 @@ importers:
'@modrinth/tooling-config':
specifier: workspace:*
version: link:../tooling-config
+ '@types/markdown-it':
+ specifier: 14.1.2
+ version: 14.1.2
packages:
@@ -4685,6 +4694,9 @@ packages:
'@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
+ '@types/markdown-it@14.1.2':
+ resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
+
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -10419,8 +10431,8 @@ packages:
vue-component-type-helpers@3.2.4:
resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==}
- vue-component-type-helpers@3.3.10:
- resolution: {integrity: sha512-t7IQivQ3oD4D01b7s7a9AWzHAcr3DGBIa/1jZREsFQJcFgSL92gqUkqNiHTYgzTt7QDuJa0I9wCeDwEtZICoBQ==}
+ vue-component-type-helpers@3.3.11:
+ resolution: {integrity: sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==}
vue-confetti-explosion@1.0.2:
resolution: {integrity: sha512-80OboM3/6BItIoZ6DpNcZFqGpF607kjIVc5af56oKgtFmt5yWehvJeoYhkzYlqxrqdBe0Ko4Ie3bWrmLau+dJw==}
@@ -10715,11 +10727,6 @@ packages:
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
- xss@1.0.15:
- resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==}
- engines: {node: '>= 0.10.0'}
- hasBin: true
-
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
@@ -11303,13 +11310,13 @@ snapshots:
'@comark/html@0.6.2':
dependencies:
- comark: 0.6.2
+ comark: 0.6.2(patch_hash=4e73fba3ed127a788219ec2e7a413966f48fc7695802a1b0f2694ef2686d475c)
transitivePeerDependencies:
- rangi
'@comark/vue@0.6.2(vue@3.5.27(typescript@5.9.3))':
dependencies:
- comark: 0.6.2
+ comark: 0.6.2(patch_hash=4e73fba3ed127a788219ec2e7a413966f48fc7695802a1b0f2694ef2686d475c)
vue: 3.5.27(typescript@5.9.3)
transitivePeerDependencies:
- rangi
@@ -14179,7 +14186,7 @@ snapshots:
storybook: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
type-fest: 2.19.0
vue: 3.5.27(typescript@5.9.3)
- vue-component-type-helpers: 3.3.10
+ vue-component-type-helpers: 3.3.11
'@stripe/stripe-js@7.9.0': {}
@@ -14518,6 +14525,11 @@ snapshots:
'@types/linkify-it@5.0.0': {}
+ '@types/markdown-it@14.1.2':
+ dependencies:
+ '@types/linkify-it': 5.0.0
+ '@types/mdurl': 2.0.0
+
'@types/mdast@4.0.4':
dependencies:
'@types/unist': 3.0.3
@@ -15939,7 +15951,7 @@ snapshots:
colord@2.9.3: {}
- comark@0.6.2:
+ comark@0.6.2(patch_hash=4e73fba3ed127a788219ec2e7a413966f48fc7695802a1b0f2694ef2686d475c):
dependencies:
entities: 8.0.0
htmlparser2: 12.0.0
@@ -21616,7 +21628,7 @@ snapshots:
vue-component-type-helpers@3.2.4: {}
- vue-component-type-helpers@3.3.10: {}
+ vue-component-type-helpers@3.3.11: {}
vue-confetti-explosion@1.0.2(vue@3.5.27(typescript@5.9.3)):
dependencies:
@@ -21920,11 +21932,6 @@ snapshots:
xmlchars@2.2.0:
optional: true
- xss@1.0.15:
- dependencies:
- commander: 2.20.3
- cssfilter: 0.0.10
-
xtend@4.0.2: {}
xxhash-wasm@1.1.0: {}