Browse Source

fix(render): quill html articles bypass marked, fix dark code block, fix image path

- 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)
main
Mohan 1 month ago
parent
commit
bb19a3ea05
  1. 77
      src/assets/markdown-override.css
  2. 3
      src/components/QuillEditor.vue
  3. 5
      src/utils/markdown.ts
  4. 25
      src/views/ArticleDetailView.vue

77
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;
}

3
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)

5
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)
}

25
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<ResponseArticleDetail>(
() => 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 })
<div class="detail-tags">
<span v-for="tag in article.tagNames" :key="tag" class="tag">{{ tag }}</span>
</div>
<div class="article-content markdown-body" v-html="renderedContent"></div>
<div class="article-content markdown-body" :class="{ 'markdown-body-dark': theme.isDark }" v-html="renderedContent"></div>
<CommentSection :slug="slug" />
</article>
</template>
@ -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;
}

Loading…
Cancel
Save