From bb19a3ea05d4d061568403657a09385541914665 Mon Sep 17 00:00:00 2001 From: Mohan Date: Mon, 10 Aug 2026 20:40:24 +0800 Subject: [PATCH] fix(render): quill html articles bypass marked, fix dark code block, fix image path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - markdown.ts: renderArticle() with starts-with-< detection, Quill HTML → DOMPurify direct, old Markdown → marked - ArticleDetailView: use useTheme to toggle markdown-body-dark class, renderArticle() replaces renderMarkdown - markdown-override.css (new): dark mode code blocks (#151b23 bg), Quill .ql-code-block-container styling - QuillEditor.vue: normalize image URL (prepend / if no scheme/leading slash) --- src/assets/markdown-override.css | 77 ++++++++++++++++++++++++++++++++ src/components/QuillEditor.vue | 3 +- src/utils/markdown.ts | 5 ++- src/views/ArticleDetailView.vue | 25 +++++------ 4 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 src/assets/markdown-override.css diff --git a/src/assets/markdown-override.css b/src/assets/markdown-override.css new file mode 100644 index 0000000..544ae43 --- /dev/null +++ b/src/assets/markdown-override.css @@ -0,0 +1,77 @@ +/* Dark mode GitHub markdown - uses palette from github-markdown-dark.css */ +.markdown-body.markdown-body-dark { + color: #f0f6fc; +} + +.markdown-body.markdown-body-dark a { + color: #58a6ff; +} + +.markdown-body.markdown-body-dark blockquote { + color: #9198a1; + border-left-color: #3d444d; +} + +.markdown-body.markdown-body-dark .highlight pre, +.markdown-body.markdown-body-dark pre { + background-color: #151b23; + color: #f0f6fc; + border: 1px solid #3d444d; + border-radius: 6px; +} + +.markdown-body.markdown-body-dark pre code, +.markdown-body.markdown-body-dark tt { + background-color: transparent; +} + +.markdown-body.markdown-body-dark code { + background-color: #151b23; + color: #f0f6fc; +} + +.markdown-body.markdown-body-dark table tr { + background-color: #0d1117; + border-top-color: #3d444d; +} + +.markdown-body.markdown-body-dark table tr:nth-child(2n) { + background-color: #151b23; +} + +.markdown-body.markdown-body-dark table th, +.markdown-body.markdown-body-dark table td { + border-color: #3d444d; +} + +.markdown-body.markdown-body-dark hr { + border-color: #3d444d; +} + +.markdown-body.markdown-body-dark img { + background-color: transparent; +} + +/* Quill code block styling for article detail */ +.markdown-body .ql-code-block-container { + background-color: #f6f8fa; + border-radius: 6px; + padding: 16px; + margin: 16px 0; + overflow-x: auto; + font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + font-size: 85%; + line-height: 1.6; + border: 1px solid #d0d7de; +} + +.markdown-body .ql-code-block { + white-space: pre-wrap; + word-break: break-word; +} + +.markdown-body.markdown-body-dark .ql-code-block-container { + background-color: #151b23; + border-color: #3d444d; + color: #f0f6fc; +} \ No newline at end of file diff --git a/src/components/QuillEditor.vue b/src/components/QuillEditor.vue index 106b13c..983038a 100644 --- a/src/components/QuillEditor.vue +++ b/src/components/QuillEditor.vue @@ -55,7 +55,8 @@ onMounted(() => { if (url && quill) { const sel = quill.getSelection() const index = sel ? sel.index : 0 - quill.insertEmbed(index, 'image', url) + const finalUrl = /^https?:\/\//i.test(url) || url.startsWith('/') ? url : '/' + url + quill.insertEmbed(index, 'image', finalUrl) } } catch (e) { console.error(e) diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts index c07cf55..e98943f 100644 --- a/src/utils/markdown.ts +++ b/src/utils/markdown.ts @@ -16,7 +16,10 @@ marked.use( }) ) -export function renderMarkdown(content: string): string { +export function renderArticle(content: string): string { + if (content.trimStart().startsWith('<')) { + return DOMPurify.sanitize(content) + } const rawHtml = marked.parse(content, { async: false }) as string return DOMPurify.sanitize(rawHtml) } \ No newline at end of file diff --git a/src/views/ArticleDetailView.vue b/src/views/ArticleDetailView.vue index 834406a..564cfee 100644 --- a/src/views/ArticleDetailView.vue +++ b/src/views/ArticleDetailView.vue @@ -2,18 +2,20 @@ import { ref, computed, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { articleApi } from '@/api/client' -import { renderMarkdown } from '@/utils/markdown' -import DOMPurify from 'dompurify' +import { renderArticle } from '@/utils/markdown' import { setPageMeta } from '@/utils/seo' import { useApi } from '@/composables/useApi' +import { useTheme } from '@/composables/useTheme' import CommentSection from '@/components/CommentSection.vue' import ArticleMetaBar from '@/components/ArticleMetaBar.vue' import type { ArticleDetail, ResponseArticleDetail } from '@/generated/api' import 'github-markdown-css/github-markdown-light.css' +import '@/assets/markdown-override.css' const route = useRoute() const router = useRouter() const slug = computed(() => String(route.params.slug ?? '')) +const theme = useTheme() const { data: resp, loading, error, execute } = useApi( () => articleApi.apiArticlesSlugGet(slug.value).then((r) => r.data) @@ -25,18 +27,9 @@ const starred = ref(false) const starCount = ref(0) const starBusy = ref(false) -const isHtmlContent = computed(() => { - const c = article.value?.content - if (!c) return false - return /<[a-z][\s\S]*>/i.test(c) -}) - const renderedContent = computed(() => { if (!article.value?.content) return '' - if (isHtmlContent.value) { - return DOMPurify.sanitize(article.value.content) - } - return renderMarkdown(article.value.content) + return renderArticle(article.value.content) }) async function toggleStar() { @@ -111,7 +104,7 @@ watch(slug, load, { immediate: true })
{{ tag }}
-
+
@@ -172,7 +165,11 @@ watch(slug, load, { immediate: true }) :deep(.markdown-body h1), :deep(.markdown-body h2), :deep(.markdown-body h3), -:deep(.markdown-body h4) { +:deep(.markdown-body h4), +:deep(.markdown-body-dark h1), +:deep(.markdown-body-dark h2), +:deep(.markdown-body-dark h3), +:deep(.markdown-body-dark h4) { font-family: 'Noto Serif SC', 'SimSun', 'STSong', serif; }