Add a marked extension that tokenizes math delimiters and renders them with KaTeX: `$$ ... $$` / `\[ ... \]` as block, `$ ... $` / `\( ... \)` and mid-paragraph `$$ ... $$` as inline. Inline `$ ... $` requires a non-space after the opening `$`, no newline inside, and a non-digit after the closing `$`, so prose like "$100 から $200" is left alone; `\$` still escapes a literal dollar sign. Parse errors fall back to KaTeX's red inline message instead of throwing, and math whose closing delimiter has not streamed in yet stays plain text. DOMPurify strips `semantics` and `annotation` by default, which would leak the raw TeX out of KaTeX's MathML output, so both are allowed explicitly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123 lines
2.5 KiB
Vue
123 lines
2.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { marked } from 'marked'
|
|
import DOMPurify from 'dompurify'
|
|
import 'katex/dist/katex.min.css'
|
|
import { mathExtension, mathSanitizeConfig } from '../utils/math'
|
|
|
|
const props = defineProps<{
|
|
content: string
|
|
pending?: boolean
|
|
}>()
|
|
|
|
marked.setOptions({ breaks: true, gfm: true })
|
|
marked.use(mathExtension)
|
|
|
|
function closeOpenFences(src: string): string {
|
|
const fenceCount = (src.match(/^```/gm) ?? []).length
|
|
if (fenceCount % 2 !== 0) return src + '\n```'
|
|
return src
|
|
}
|
|
|
|
const html = computed(() => {
|
|
const raw = props.pending ? closeOpenFences(props.content) : props.content
|
|
return DOMPurify.sanitize(marked.parse(raw) as string, mathSanitizeConfig)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="md" v-html="html" />
|
|
</template>
|
|
|
|
<style scoped>
|
|
.md { line-height: 1.7; word-break: break-word; }
|
|
|
|
/* インライン要素 */
|
|
.md :deep(code) {
|
|
background: #1a1a1a;
|
|
color: #e2e8f0;
|
|
padding: 1px 5px;
|
|
border-radius: 4px;
|
|
font-size: 0.875em;
|
|
font-family: 'Consolas', 'Menlo', monospace;
|
|
}
|
|
|
|
/* コードブロック */
|
|
.md :deep(pre) {
|
|
background: #0f0f0f;
|
|
border: 1px solid #2a2a2a;
|
|
border-radius: 8px;
|
|
padding: 12px 14px;
|
|
overflow-x: auto;
|
|
margin: 8px 0;
|
|
}
|
|
.md :deep(pre code) {
|
|
background: none;
|
|
padding: 0;
|
|
font-size: 0.85em;
|
|
color: #e2e8f0;
|
|
}
|
|
|
|
/* 見出し */
|
|
.md :deep(h1),
|
|
.md :deep(h2),
|
|
.md :deep(h3) {
|
|
margin: 12px 0 4px;
|
|
color: #f1f5f9;
|
|
font-weight: 600;
|
|
}
|
|
.md :deep(h1) { font-size: 1.2em; }
|
|
.md :deep(h2) { font-size: 1.1em; }
|
|
.md :deep(h3) { font-size: 1em; }
|
|
|
|
/* リスト */
|
|
.md :deep(ul),
|
|
.md :deep(ol) {
|
|
padding-left: 20px;
|
|
margin: 4px 0;
|
|
}
|
|
.md :deep(li) { margin: 2px 0; }
|
|
|
|
/* 段落 */
|
|
.md :deep(p) { margin: 4px 0; }
|
|
|
|
/* 引用 */
|
|
.md :deep(blockquote) {
|
|
border-left: 3px solid #444;
|
|
margin: 6px 0;
|
|
padding: 4px 12px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
/* テーブル */
|
|
.md :deep(table) {
|
|
border-collapse: collapse;
|
|
margin: 8px 0;
|
|
font-size: 0.9em;
|
|
width: 100%;
|
|
}
|
|
.md :deep(th),
|
|
.md :deep(td) {
|
|
border: 1px solid #333;
|
|
padding: 6px 10px;
|
|
text-align: left;
|
|
}
|
|
.md :deep(th) { background: #1a1a1a; color: #cbd5e1; }
|
|
|
|
/* 水平線 */
|
|
.md :deep(hr) { border-color: #333; margin: 10px 0; }
|
|
|
|
/* 数式 (KaTeX) */
|
|
.md :deep(.katex) { font-size: 1.05em; }
|
|
.md :deep(.katex-display) {
|
|
margin: 10px 0;
|
|
padding: 2px 0;
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
}
|
|
|
|
/* リンク */
|
|
.md :deep(a) { color: #60a5fa; text-decoration: none; }
|
|
.md :deep(a:hover) { text-decoration: underline; }
|
|
</style>
|