feat: support GFM alerts & render perf (#446)
All checks were successful
Build and Deploy / build-and-deploy (push) Has been skipped

This commit is contained in:
njr 2024-11-22 15:39:28 +08:00 committed by GitHub
parent e032c06ba3
commit d8b14f5ce8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 407 additions and 51 deletions

View File

@ -35,6 +35,7 @@ Markdown 文档自动即时渲染为微信图文,让你不再为微信文章
- [x] 支持自定义 CSS 样式 - [x] 支持自定义 CSS 样式
- [x] 支持 Markdown 所有基础语法、代码块、LaTeX 公式 - [x] 支持 Markdown 所有基础语法、代码块、LaTeX 公式
- [x] 支持 [GFM 警告块](https://github.com/orgs/community/discussions/16925)
- [x] 支持浅色、深色两种编辑器外观 - [x] 支持浅色、深色两种编辑器外观
- [x] 支持 <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>F</kbd> 快速格式化文档 - [x] 支持 <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>F</kbd> 快速格式化文档
- [x] 支持色盘取色,快速替换文章整体色调 - [x] 支持色盘取色,快速替换文章整体色调

View File

@ -33,6 +33,54 @@ blockquote {
/* 引用段落样式 */ /* 引用段落样式 */
blockquote_p { blockquote_p {
} }
/* GFM note 样式 */
blockquote_note {
}
/* GFM tip 样式 */
blockquote_tip {
}
/* GFM important 样式 */
blockquote_important {
}
/* GFM warning 样式 */
blockquote_warning {
}
/* GFM caution 样式 */
blockquote_caution {
}
/* GFM 通用标题 */
blockquote_title {
}
/* GFM note 标题 */
blockquote_title_note {
}
/* GFM tip 标题 */
blockquote_title_tip {
}
/* GFM important 标题 */
blockquote_title_important {
}
/* GFM warning 标题 */
blockquote_title_warning {
}
/* GFM caution 标题 */
blockquote_title_caution {
}
/* GFM note 段落样式 */
blockquote_p_note {
}
/* GFM tip 段落样式 */
blockquote_p_tip {
}
/* GFM important 段落样式 */
blockquote_p_important {
}
/* GFM warning 段落样式 */
blockquote_p_warning {
}
/* GFM caution 段落样式 */
blockquote_p_caution {
}
/* 段落样式 */ /* 段落样式 */
p { p {
} }

View File

@ -32,6 +32,8 @@
--input:0 0% 89.8%; --input:0 0% 89.8%;
--ring:0 0% 3.9%; --ring:0 0% 3.9%;
--radius: 0.5rem; --radius: 0.5rem;
--blockquote-background: #f7f7f7;
} }
.dark { .dark {
@ -62,6 +64,8 @@
--border:0 0% 14.9%; --border:0 0% 14.9%;
--input:0 0% 14.9%; --input:0 0% 14.9%;
--ring:0 0% 83.1%; --ring:0 0% 83.1%;
--blockquote-background: #212121;
} }
} }

View File

@ -123,9 +123,20 @@ function copy() {
.replace(/top:(.*?)em/g, `transform: translateY($1em)`) .replace(/top:(.*?)em/g, `transform: translateY($1em)`)
// //
.replaceAll(`var(--el-text-color-regular)`, `#3f3f3f`) .replaceAll(`var(--el-text-color-regular)`, `#3f3f3f`)
.replaceAll(`var(--blockquote-background)`, `#f7f7f7`)
.replaceAll(`var(--md-primary-color)`, primaryColor.value) .replaceAll(`var(--md-primary-color)`, primaryColor.value)
.replaceAll(/--md-primary-color:.+?;/g, ``) .replaceAll(/--md-primary-color:.+?;/g, ``)
clipboardDiv.focus() clipboardDiv.focus()
// edge case: svg
const p = document.createElement(`p`)
p.style.fontSize = `0` // 0
p.style.lineHeight = `0` // 0
p.style.margin = `0` //
p.innerHTML = `&nbsp;`
clipboardDiv.insertBefore(p, clipboardDiv.firstChild)
window.getSelection()!.removeAllRanges() window.getSelection()!.removeAllRanges()
const range = document.createRange() const range = document.createRange()

View File

@ -82,7 +82,7 @@ const defaultTheme: Theme = {
'padding': `1em`, 'padding': `1em`,
'border-radius': `8px`, 'border-radius': `8px`,
'color': `rgba(0,0,0,0.5)`, 'color': `rgba(0,0,0,0.5)`,
'background': `#f7f7f7`, 'background': `var(--blockquote-background)`,
'margin': `2em 8px`, 'margin': `2em 8px`,
}, },
@ -91,7 +91,65 @@ const defaultTheme: Theme = {
'display': `block`, 'display': `block`,
'font-size': `1em`, 'font-size': `1em`,
'letter-spacing': `0.1em`, 'letter-spacing': `0.1em`,
'color': `rgb(80, 80, 80)`, 'color': `var(--el-text-color-regular)`,
},
blockquote_note: {
},
blockquote_tip: {
},
blockquote_important: {
},
blockquote_warning: {
},
blockquote_caution: {
},
// GFM 警告块标题
blockquote_title: {
'display': `flex`,
'align-items': `center`,
'gap': `0.5em`,
'margin-bottom': `0.5em`,
},
blockquote_title_note: {
color: `#478be6`,
},
blockquote_title_tip: {
color: `#57ab5a`,
},
blockquote_title_important: {
color: `#986ee2`,
},
blockquote_title_warning: {
color: `#c69026`,
},
blockquote_title_caution: {
color: `#e5534b`,
},
blockquote_p_note: {
},
blockquote_p_tip: {
},
blockquote_p_important: {
},
blockquote_p_warning: {
},
blockquote_p_caution: {
}, },
// 代码块 // 代码块
@ -230,87 +288,90 @@ const graceTheme = toMerged(defaultTheme, {
base: { base: {
}, },
block: { block: {
h1: { 'h1': {
'padding': `0.5em 1em`, 'padding': `0.5em 1em`,
'border-bottom': `2px solid var(--md-primary-color)`, 'border-bottom': `2px solid var(--md-primary-color)`,
'font-size': `1.4em`, 'font-size': `1.4em`,
'text-shadow': `2px 2px 4px rgba(0,0,0,0.1)`, 'text-shadow': `2px 2px 4px rgba(0,0,0,0.1)`,
}, },
h2: { 'h2': {
'padding': `0.3em 1em`, 'padding': `0.3em 1em`,
'border-radius': `8px`, 'border-radius': `8px`,
'font-size': `1.3em`, 'font-size': `1.3em`,
'box-shadow': `0 4px 6px rgba(0,0,0,0.1)`, 'box-shadow': `0 4px 6px rgba(0,0,0,0.1)`,
}, },
h3: { 'h3': {
'padding-left': `12px`, 'padding-left': `12px`,
'font-size': `1.2em`, 'font-size': `1.2em`,
'border-left': `4px solid var(--md-primary-color)`, 'border-left': `4px solid var(--md-primary-color)`,
'border-bottom': `1px dashed var(--md-primary-color)`, 'border-bottom': `1px dashed var(--md-primary-color)`,
}, },
h4: { 'h4': {
'font-size': `1.1em`, 'font-size': `1.1em`,
}, },
h5: { 'h5': {
'font-size': `1em`, 'font-size': `1em`,
}, },
h6: { 'h6': {
'font-size': `1em`, 'font-size': `1em`,
}, },
p: { 'p': {
}, },
blockquote: { 'blockquote': {
'font-style': `italic`, 'font-style': `italic`,
'padding': `1em 1em 1em 2em`, 'padding': `1em 1em 1em 2em`,
'border-left': `4px solid var(--md-primary-color)`, 'border-left': `4px solid var(--md-primary-color)`,
'border-radius': `6px`, 'border-radius': `6px`,
'color': `rgba(0,0,0,0.6)`, 'color': `rgba(0,0,0,0.6)`,
'background': `linear-gradient(to right, #f7f7f7, #ffffff)`,
'box-shadow': `0 4px 6px rgba(0,0,0,0.05)`, 'box-shadow': `0 4px 6px rgba(0,0,0,0.05)`,
}, },
blockquote_p: { 'blockquote_p': {
}, },
code_pre: { 'markdown-alert': {
'font-style': `italic`,
},
'code_pre': {
'box-shadow': `inset 0 0 10px rgba(0,0,0,0.05)`, 'box-shadow': `inset 0 0 10px rgba(0,0,0,0.05)`,
}, },
code: { 'code': {
'white-space': `pre-wrap`, 'white-space': `pre-wrap`,
'font-family': `'Fira Code', Menlo, Operator Mono, Consolas, Monaco, monospace`, 'font-family': `'Fira Code', Menlo, Operator Mono, Consolas, Monaco, monospace`,
}, },
image: { 'image': {
'border-radius': `8px`, 'border-radius': `8px`,
'box-shadow': `0 4px 8px rgba(0,0,0,0.1)`, 'box-shadow': `0 4px 8px rgba(0,0,0,0.1)`,
}, },
ol: { 'ol': {
'padding-left': `1.5em`, 'padding-left': `1.5em`,
}, },
ul: { 'ul': {
'list-style': `none`, 'list-style': `none`,
'padding-left': `1.5em`, 'padding-left': `1.5em`,
}, },
footnotes: { 'footnotes': {
}, },
figure: { 'figure': {
}, },
hr: { 'hr': {
height: `1px`, height: `1px`,
border: `none`, border: `none`,
margin: `2em 0`, margin: `2em 0`,

View File

@ -143,7 +143,8 @@ export const useStore = defineStore(`store`, () => {
// 更新编辑器 // 更新编辑器
const editorRefresh = () => { const editorRefresh = () => {
codeThemeChange() codeThemeChange()
renderer.reset({ status: isCiteStatus.value, legend: legend.value, isUseIndent: isUseIndent.value }) renderer.reset({ citeStatus: isCiteStatus.value, legend: legend.value, isUseIndent: isUseIndent.value })
let outputTemp = marked.parse(editor.value!.getValue()) as string let outputTemp = marked.parse(editor.value!.getValue()) as string
// 去除第一行的 margin-top // 去除第一行的 margin-top
@ -157,10 +158,15 @@ export const useStore = defineStore(`store`, () => {
outputTemp += ` outputTemp += `
<style> <style>
.hljs.code__pre > .mac-sign { .hljs.code__pre > .mac-sign {
display: inline-block; display: flex;
}
</style>
`
} }
.hljs.code__pre { outputTemp += `
<style>
.code__pre {
padding: 0 !important; padding: 0 !important;
} }
@ -172,7 +178,6 @@ export const useStore = defineStore(`store`, () => {
} }
</style> </style>
` `
}
output.value = outputTemp output.value = outputTemp
} }
@ -184,6 +189,7 @@ export const useStore = defineStore(`store`, () => {
renderer.setOptions({ renderer.setOptions({
theme: newTheme, theme: newTheme,
}) })
editorRefresh() editorRefresh()
} }
// 初始化 CSS 编辑器 // 初始化 CSS 编辑器

View File

@ -1,6 +1,9 @@
import type { PropertiesHyphen } from 'csstype' import type { PropertiesHyphen } from 'csstype'
export type Block = `h1` | `h2` | `h3` | `h4` | `h5` | `h6` | `p` | `blockquote` | `blockquote_p` | `code_pre` | `code` | `image` | `ol` | `ul` | `footnotes` | `figure` | `hr` import type { Token } from 'marked'
type GFMBlock = `blockquote_note` | `blockquote_tip` | `blockquote_important` | `blockquote_warning` | `blockquote_caution` | `blockquote_title` | `blockquote_title_note` | `blockquote_title_tip` | `blockquote_title_important` | `blockquote_title_warning` | `blockquote_title_caution` | `blockquote_p` | `blockquote_p_note` | `blockquote_p_tip` | `blockquote_p_important` | `blockquote_p_warning` | `blockquote_p_caution`
export type Block = `h1` | `h2` | `h3` | `h4` | `h5` | `h6` | `p` | `blockquote` | `blockquote_p` | `code_pre` | `code` | `image` | `ol` | `ul` | `footnotes` | `figure` | `hr` | GFMBlock
export type Inline = `listitem` | `codespan` | `link` | `wx_link` | `strong` | `table` | `thead` | `td` | `footnote` | `figcaption` | `em` export type Inline = `listitem` | `codespan` | `link` | `wx_link` | `strong` | `table` | `thead` | `td` | `footnote` | `figcaption` | `em`
interface CustomCSSProperties { interface CustomCSSProperties {
@ -12,8 +15,8 @@ export type ExtendedProperties = PropertiesHyphen & CustomCSSProperties
export interface Theme { export interface Theme {
base: ExtendedProperties base: ExtendedProperties
block: Record<Block, PropertiesHyphen> block: Record<Block, ExtendedProperties>
inline: Record<Inline, PropertiesHyphen> inline: Record<Inline, ExtendedProperties>
} }
export interface IOpts { export interface IOpts {
@ -22,7 +25,7 @@ export interface IOpts {
size: string size: string
isUseIndent: boolean isUseIndent: boolean
legend?: string legend?: string
status?: boolean citeStatus?: boolean
} }
export type ThemeStyles = Record<Block | Inline, ExtendedProperties> export type ThemeStyles = Record<Block | Inline, ExtendedProperties>
@ -32,3 +35,39 @@ export interface IConfigOption<VT = string> {
value: VT value: VT
desc: string desc: string
} }
/**
* Options for the `markedAlert` extension.
*/
export interface AlertOptions {
className?: string
variants?: AlertVariantItem[]
styles?: ThemeStyles
}
/**
* 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[]
}

159
src/utils/MDAlert.ts Normal file
View File

@ -0,0 +1,159 @@
import type { AlertOptions, AlertVariantItem } from '@/types'
import type { MarkedExtension, Tokens } from 'marked'
import { getStyleString } from '.'
/**
* 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), `i`).test(token.text),
)
if (matchedVariant) {
const {
type: variantType,
icon,
title = ucfirst(variantType),
titleClassName = `${className}-title`,
} = matchedVariant
const typeRegexp = new RegExp(createSyntaxPattern(variantType), `i`)
const { styles } = options
Object.assign(token, {
type: `alert`,
meta: {
className,
variant: variantType,
icon,
title,
titleClassName,
wrapperStyle: {
...styles?.blockquote,
...styles?.[`blockquote_${variantType}` as keyof typeof styles],
},
titleStyle: {
...styles?.blockquote_title,
...styles?.[`blockquote_title_${variantType}` as keyof typeof styles],
},
contentStyle: {
...styles?.blockquote_p,
...styles?.[`blockquote_p_${variantType}` as keyof typeof styles],
},
},
})
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 text = this.parser.parse(tokens)
text = text.replace(/<p .*?>/g, `<p style="${getStyleString(meta.contentStyle)}">`)
let tmpl = `<blockquote class="${meta.className} ${meta.className}-${meta.variant}" style="${getStyleString(meta.wrapperStyle)}">\n`
tmpl += `<p class="${meta.titleClassName}" style="${getStyleString(meta.titleStyle)}">`
tmpl += meta.icon.replace(
`<svg`,
`<svg style="fill: ${meta.titleStyle?.color ?? `inherit`}"`,
)
tmpl += meta.title
tmpl += `</p>\n`
tmpl += text
tmpl += `</blockquote>\n`
return tmpl
},
},
],
}
}
/**
* The default configuration for alert variants.
*/
const defaultAlertVariant: AlertVariantItem[] = [
{
type: `note`,
icon: `<svg class="octicon octicon-info" style="margin-right: 0.25em;" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg>`,
},
{
type: `tip`,
icon: `<svg class="octicon octicon-light-bulb" style="margin-right: 0.25em;" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg>`,
},
{
type: `important`,
icon: `<svg class="octicon octicon-report" style="margin-right: 0.25em;" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>`,
},
{
type: `warning`,
icon: `<svg class="octicon octicon-alert" style="margin-right: 0.25em;" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>`,
},
{
type: `caution`,
icon: `<svg class="octicon octicon-stop" style="margin-right: 0.25em;" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg>`,
},
]
/**
* 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}])\\s*?\n*`
}
/**
* Capitalizes the first letter of a string.
*/
export function ucfirst(str: string) {
return str.slice(0, 1).toUpperCase() + str.slice(1).toLowerCase()
}

View File

@ -1,4 +1,4 @@
import type { Block, Inline, Theme } from '@/types' import type { Block, ExtendedProperties, Inline, Theme } from '@/types'
import type { PropertiesHyphen } from 'csstype' import type { PropertiesHyphen } from 'csstype'
import { prefix } from '@/config' import { prefix } from '@/config'
@ -34,7 +34,7 @@ export function customizeTheme(theme: Theme, options: {
export function customCssWithTemplate(jsonString: Partial<Record<Block | Inline, PropertiesHyphen>>, color: string, theme: Theme) { export function customCssWithTemplate(jsonString: Partial<Record<Block | Inline, PropertiesHyphen>>, color: string, theme: Theme) {
const newTheme = customizeTheme(theme, { color }) const newTheme = customizeTheme(theme, { color })
const mergeProperties = <T extends Block | Inline = Block>(target: Record<T, PropertiesHyphen>, source: Partial<Record<Block | Inline, PropertiesHyphen>>, keys: T[]) => { const mergeProperties = <T extends Block | Inline = Block>(target: Record<T, PropertiesHyphen>, source: Partial<Record<Block | Inline | string, PropertiesHyphen>>, keys: T[]) => {
keys.forEach((key) => { keys.forEach((key) => {
if (source[key]) { if (source[key]) {
target[key] = Object.assign(target[key] || {}, source[key]) target[key] = Object.assign(target[key] || {}, source[key])
@ -54,7 +54,23 @@ export function customCssWithTemplate(jsonString: Partial<Record<Block | Inline,
`p`, `p`,
`hr`, `hr`,
`blockquote`, `blockquote`,
`blockquote_note`,
`blockquote_tip`,
`blockquote_important`,
`blockquote_warning`,
`blockquote_caution`,
`blockquote_p`, `blockquote_p`,
`blockquote_p_note`,
`blockquote_p_tip`,
`blockquote_p_important`,
`blockquote_p_warning`,
`blockquote_p_caution`,
`blockquote_title`,
`blockquote_title_note`,
`blockquote_title_tip`,
`blockquote_title_important`,
`blockquote_title_warning`,
`blockquote_title_caution`,
`image`, `image`,
`ul`, `ul`,
`ol`, `ol`,
@ -116,6 +132,15 @@ export function css2json(css: string): Partial<Record<Block | Inline, Properties
return json return json
} }
/**
* CSS
* @param {ExtendedProperties} style -
* @returns {string} - CSS
*/
export function getStyleString(style: ExtendedProperties) {
return Object.entries(style ?? {}).map(([key, value]) => `${key}: ${value}`).join(`; `)
}
/** /**
* *
* @param {string} content - * @param {string} content -

View File

@ -6,6 +6,8 @@ import hljs from 'highlight.js'
import { marked } from 'marked' import { marked } from 'marked'
import mermaid from 'mermaid' import mermaid from 'mermaid'
import { getStyleString } from '.'
import markedAlert from './MDAlert'
import { MDKatex } from './MDKatex' import { MDKatex } from './MDKatex'
marked.use(MDKatex({ nonStandard: true })) marked.use(MDKatex({ nonStandard: true }))
@ -58,9 +60,7 @@ function getStyles(styleMapping: ThemeStyles, tokenName: string, addition: strin
if (!dict) { if (!dict) {
return `` return ``
} }
const styles = Object.entries(dict) const styles = getStyleString(dict)
.map(([key, value]) => `${key}:${value}`)
.join(`;`)
return `style="${styles}${addition}"` return `style="${styles}${addition}"`
} }
@ -89,9 +89,9 @@ function transform(legend: string, text: string | null, title: string | null): s
const macCodeSvg = ` const macCodeSvg = `
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="45px" height="13px" viewBox="0 0 450 130"> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="45px" height="13px" viewBox="0 0 450 130">
<ellipse cx="65" cy="65" rx="50" ry="52" stroke="rgb(220,60,54)" stroke-width="2" fill="rgb(237,108,96)" /> <ellipse cx="50" cy="65" rx="50" ry="52" stroke="rgb(220,60,54)" stroke-width="2" fill="rgb(237,108,96)" />
<ellipse cx="225" cy="65" rx="50" ry="52" stroke="rgb(218,151,33)" stroke-width="2" fill="rgb(247,193,81)" /> <ellipse cx="225" cy="65" rx="50" ry="52" stroke="rgb(218,151,33)" stroke-width="2" fill="rgb(247,193,81)" />
<ellipse cx="385" cy="65" rx="50" ry="52" stroke="rgb(27,161,37)" stroke-width="2" fill="rgb(100,200,86)" /> <ellipse cx="400" cy="65" rx="50" ry="52" stroke="rgb(27,161,37)" stroke-width="2" fill="rgb(100,200,86)" />
</svg> </svg>
`.trim() `.trim()
@ -126,6 +126,7 @@ export function initRenderer(opts: IOpts) {
function setOptions(newOpts: Partial<IOpts>): void { function setOptions(newOpts: Partial<IOpts>): void {
opts = { ...opts, ...newOpts } opts = { ...opts, ...newOpts }
styleMapping = buildTheme(opts) styleMapping = buildTheme(opts)
marked.use(markedAlert({ styles: styleMapping }))
} }
const buildFootnotes = () => { const buildFootnotes = () => {
@ -211,18 +212,19 @@ export function initRenderer(opts: IOpts) {
return `<figure ${figureStyles}><img ${imgStyles} src="${href}" title="${title}" alt="${text}"/>${subText}</figure>` return `<figure ${figureStyles}><img ${imgStyles} src="${href}" title="${title}" alt="${text}"/>${subText}</figure>`
}, },
link({ href, title, text }: Tokens.Link): string { link({ href, title, text, tokens }: Tokens.Link): string {
const parsedText = this.parser.parseInline(tokens)
if (href.startsWith(`https://mp.weixin.qq.com`)) { if (href.startsWith(`https://mp.weixin.qq.com`)) {
return `<a href="${href}" title="${title || text}" ${styles(`wx_link`)}>${text}</a>` return `<a href="${href}" title="${title || text}" ${styles(`wx_link`)}>${parsedText}</a>`
} }
if (href === text) { if (href === text) {
return text return parsedText
} }
if (opts.status) { if (opts.citeStatus) {
const ref = addFootnote(title || text, href) const ref = addFootnote(title || text, href)
return `<span ${styles(`link`)}>${text}<sup>[${ref}]</sup></span>` return `<span ${styles(`link`)}>${parsedText}<sup>[${ref}]</sup></span>`
} }
return styledContent(`link`, text, `span`) return styledContent(`link`, parsedText, `span`)
}, },
strong({ tokens }: Tokens.Strong): string { strong({ tokens }: Tokens.Strong): string {