import { createHtmlRenderer } from '@comark/html'
import type { ComponentRenderFn } from '@comark/html/render'
import type { ComarkPlugin, ElementNode, MarkdownDocument, Node } from 'comark'
import { parseMarkdown } from 'comark'
import { defineComarkPlugin } from 'comark/parse'
import alert from 'comark/plugins/alert'
import attributes from 'comark/plugins/attributes'
import binding from 'comark/plugins/binding'
import breaks from 'comark/plugins/breaks'
import components from 'comark/plugins/components'
import emoji from 'comark/plugins/emoji'
import footnotes from 'comark/plugins/footnotes'
import html from 'comark/plugins/html'
import punctuation from 'comark/plugins/punctuation'
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'
export { modrinthSecurity, securityOptions } from './security'
export type { ElementNode, MarkdownDocument, Node } from 'comark'
export { visit } from 'comark/utils'
export function getFenceLanguage(preNode: ElementNode): string | undefined {
return typeof preNode[1]?.language === 'string' ? (preNode[1].language as string) : undefined
}
export function getFenceCodeText(preNode: ElementNode): string {
const codeNode = preNode[2] as ElementNode | undefined
return typeof codeNode?.[2] === 'string' ? codeNode[2] : ''
}
export const modrinthPlugins: ComarkPlugin[] = [
alert(),
attributes(), // safe cuz we filter attributes, ends up being a nice shorthand for any places people want to use the attributes we allow?
binding(),
// breaks(), // makes newlines work like newlines //TODO: make this an option?
components(),
emoji(),
footnotes(),
// frontmatter()
// heading()
html(),
modrinthHtmlBlock(),
// json-render()
// math(), // needs dep
// mermaid(), // needs dep
punctuation(),
// rangi() // needs dep + we have highlight.js + if we do want to switch I'd say shiki is better
modrinthEmbedSyntax(),
modrinthResolveMedia(),
modrinthSecurity(),
// shiki(), // needs dep + we have highlight.js
// summary(), //TODO
taskList(),
// toc() // would be cool to have but would need heavy changes to project pages
]
export function createModrinthHtmlRenderer(
extraComponents: Record = {},
) {
const renderer = createHtmlRenderer({
linkify: true,
registerDefaultPlugins: false,
plugins: modrinthPlugins,
components: extraComponents,
})
return (markdown: string): Promise => renderer(markdown)
}
export const renderString = createModrinthHtmlRenderer()
export function parseModrinthMarkdown(markdown: string): Promise {
return parseMarkdown(markdown, {
linkify: true,
registerDefaultPlugins: false,
plugins: modrinthPlugins,
})
}
const blogRelativeUrlAttrs: Record = { a: 'href', img: 'src' }
function createBlogUrlRewritePlugin(baseUrl: string, siteUrl: string) {
return defineComarkPlugin(() => ({
name: 'modrinth-blog-relative-urls',
async post(state) {
await visitAsync(
state.tree,
(node) => typeof node !== 'string' && node[0] !== null,
(node) => {
const element = node as ElementNode
const attrName = blogRelativeUrlAttrs[element[0].toLowerCase()]
const value = attrName ? element[1][attrName] : undefined
if (typeof value !== 'string' || !value) return
element[1][attrName] = new URL(value, baseUrl).href.replace(siteUrl, '')
},
)
},
}))()
}
export function createBlogHtmlRenderer(baseUrl: string, siteUrl: string) {
const renderer = createHtmlRenderer({
linkify: true,
registerDefaultPlugins: false,
plugins: [
alert(),
attributes(),
components(),
html(),
taskList(),
createBlogUrlRewritePlugin(baseUrl, siteUrl),
],
})
return (markdown: string): Promise => renderer(markdown)
}
const basicMarkdownAllowedTags = ['a', 'strong', 'em', 'code', 'br']
export const modrinthBasicPlugins = [
modrinthResolveMedia(),
modrinthSecurity({
allowedProtocols: ['http', 'https', 'mailto'],
allowedTags: basicMarkdownAllowedTags,
tagFallback: (element) => {
const text = element
.slice(2)
.filter((child): child is string => typeof child === 'string')
.join('')
return text ? text : false
},
}),
]
export function modrinthForceLinkTarget(target: string) {
return defineComarkPlugin(() => ({
name: 'modrinth-force-link-target',
async post(state) {
await visitAsync(
state.tree,
(node) => typeof node !== 'string' && node[0] === 'a',
(node) => {
;(node as ElementNode)[1].target = target
},
)
},
}))()
}
function buildBasicRenderer(target?: string) {
return createHtmlRenderer({
linkify: true,
unwrap: 'p',
registerDefaultPlugins: false,
plugins: target ? [...modrinthBasicPlugins, modrinthForceLinkTarget(target)] : modrinthBasicPlugins,
})
}
const defaultBasicRenderer = buildBasicRenderer()
export const renderBasicInlineMarkdown = (
markdown: string,
options: {
target?: string
} = {},
): Promise => {
if (!options.target) return defaultBasicRenderer(markdown)
return buildBasicRenderer(options.target)(markdown)
}