{
value: VT
desc: string
}
+
+/**
+ * Options for the `markedAlert` extension.
+ */
+export interface AlertOptions {
+ className?: string
+ variants?: AlertVariantItem[]
+ theme?: Theme
+}
+
+/**
+ * Configuration for an alert type.
+ */
+export interface AlertVariantItem {
+ type: string
+ icon: string
+ title?: string
+ titleClassName?: string
+}
+
+/**
+ * Represents an alert token.
+ */
+export interface Alert {
+ type: `alert`
+ meta: {
+ className: string
+ variant: string
+ icon: string
+ title: string
+ titleClassName: string
+ }
+ raw: string
+ text: string
+ tokens: Token[]
+}
diff --git a/src/utils/MDAlert.ts b/src/utils/MDAlert.ts
new file mode 100644
index 0000000..6b0bca1
--- /dev/null
+++ b/src/utils/MDAlert.ts
@@ -0,0 +1,155 @@
+import type { AlertOptions, AlertVariantItem } from '@/types'
+import type { MarkedExtension, Tokens } from 'marked'
+
+/**
+ * https://github.com/bent10/marked-extensions/tree/main/packages/alert
+ * To support theme, we need to modify the source code.
+ * A [marked](https://marked.js.org/) extension to support [GFM alerts](https://github.com/orgs/community/discussions/16925).
+ */
+export default function markedAlert(options: AlertOptions = {}): MarkedExtension {
+ const { className = `markdown-alert`, variants = [] } = options
+ const resolvedVariants = resolveVariants(variants)
+
+ return {
+ walkTokens(token) {
+ if (token.type !== `blockquote`)
+ return
+
+ const matchedVariant = resolvedVariants.find(({ type }) =>
+ new RegExp(createSyntaxPattern(type)).test(token.text),
+ )
+
+ if (matchedVariant) {
+ const {
+ type: variantType,
+ icon,
+ title = ucfirst(variantType),
+ titleClassName = `${className}-title`,
+ } = matchedVariant
+ const typeRegexp = new RegExp(createSyntaxPattern(variantType))
+
+ Object.assign(token, {
+ type: `alert`,
+ meta: {
+ className,
+ variant: variantType,
+ icon,
+ title,
+ titleClassName,
+ style: {
+ ...options.theme?.block[className],
+ ...options.theme?.block[`${className}-${variantType}`],
+ },
+ titleStyle: {
+ ...options.theme?.block[titleClassName],
+ ...options.theme?.block[`${titleClassName}-${variantType}`],
+ },
+ },
+ })
+
+ console.log({
+ ...options.theme?.block[className],
+ ...options.theme?.block[`${className}-${variantType}`],
+ }, `style`)
+
+ const firstLine = token.tokens?.[0] as Tokens.Paragraph
+ const firstLineText = firstLine.raw?.replace(typeRegexp, ``).trim()
+
+ if (firstLineText) {
+ const patternToken = firstLine.tokens[0] as Tokens.Text
+
+ Object.assign(patternToken, {
+ raw: patternToken.raw.replace(typeRegexp, ``),
+ text: patternToken.text.replace(typeRegexp, ``),
+ })
+
+ if (firstLine.tokens[1]?.type === `br`) {
+ firstLine.tokens.splice(1, 1)
+ }
+ }
+ else {
+ token.tokens?.shift()
+ }
+ }
+ },
+ extensions: [
+ {
+ name: `alert`,
+ level: `block`,
+ renderer({ meta, tokens = [] }) {
+ let tmpl = `\n`
+
+ return tmpl
+ },
+ },
+ ],
+ }
+}
+
+/**
+ * The default configuration for alert variants.
+ */
+const defaultAlertVariant: AlertVariantItem[] = [
+ {
+ type: `note`,
+ icon: ``,
+ },
+ {
+ type: `tip`,
+ icon: ``,
+ },
+ {
+ type: `important`,
+ icon: ``,
+ },
+ {
+ type: `warning`,
+ icon: ``,
+ },
+ {
+ type: `caution`,
+ icon: ``,
+ },
+]
+
+/**
+ * Resolves the variants configuration, combining the provided variants with
+ * the default variants.
+ */
+export function resolveVariants(variants: AlertVariantItem[]) {
+ if (!variants.length)
+ return defaultAlertVariant
+
+ return Object.values(
+ [...defaultAlertVariant, ...variants].reduce(
+ (map, item) => {
+ map[item.type] = item
+ return map
+ },
+ {} as { [key: string]: AlertVariantItem },
+ ),
+ )
+}
+
+/**
+ * Returns regex pattern to match alert syntax.
+ */
+export function createSyntaxPattern(type: string) {
+ return `^(?:\\[!${type.toUpperCase()}])\\s*?\n*`
+}
+
+/**
+ * Capitalizes the first letter of a string.
+ */
+export function ucfirst(str: string) {
+ return str.slice(0, 1).toUpperCase() + str.slice(1).toLowerCase()
+}
diff --git a/src/utils/renderer.ts b/src/utils/renderer.ts
index 39c2ad7..17d12dd 100644
--- a/src/utils/renderer.ts
+++ b/src/utils/renderer.ts
@@ -6,6 +6,7 @@ import hljs from 'highlight.js'
import { marked } from 'marked'
import mermaid from 'mermaid'
+import markedAlert from './MDAlert'
import { MDKatex } from './MDKatex'
marked.use(MDKatex({ nonStandard: true }))
@@ -103,6 +104,8 @@ export function initRenderer(opts: IOpts) {
let listIndex: number = 0
let isOrdered: boolean = false
+ marked.use(markedAlert({ theme: opts.theme }))
+
function styles(tag: string, addition: string = ``): string {
return getStyles(styleMapping, tag, addition)
}
diff --git a/src/views/CodemirrorEditor.vue b/src/views/CodemirrorEditor.vue
index d1c1b64..cb338bf 100644
--- a/src/views/CodemirrorEditor.vue
+++ b/src/views/CodemirrorEditor.vue
@@ -519,6 +519,17 @@ onMounted(() => {
border-spacing: 0;
}
+:deep(.markdown-alert) {
+ display: block;
+ padding: 1em;
+ border-radius: 8px;
+ background: #f7f7f7;
+}
+
+:deep(.markdown-alert-title) {
+ font-weight: bold;
+}
+
.codeMirror-wrapper,
.preview-wrapper {
height: 100%;