markdown-itの導入は公式を参考にすれば問題なくできますが、Pluginが多くて時間が掛かるのと、Vuetifyの環境に入れると、間隔が狭まったり位置がズレたりするので、demoページを参考にlayoutや他のページに影響ないようにCSS(style scoped)で調整したので、メモしておきます。
GitHub – markdown-it/markdown-it
markdown-it demo
markdown-it導入
demoページのが動くように下記を追加します。
必要なのものに絞っても良いのですが、あって困るものはなさそう。
% yarn add -D markdown-it @types/markdown-it \ markdown-it-sanitizer \ markdown-it-link-attributes \ markdown-it-emoji \ markdown-it-sub \ markdown-it-sup \ markdown-it-ins \ markdown-it-mark \ markdown-it-footnote @types/markdown-it-footnote \ markdown-it-deflist \ markdown-it-abbr \ highlight.js
highlight.jsは、Code(```js
等で囲った所)に使われます。
今回はgithub.cssを使用。いい感じですね。
Component作成
@nuxtjs/markdownit(Nuxt2)では、nuxt.config.jsで設定して、@md(Inject)で使えました。
Plugin作れば同じようにする事も可能なのですが、
共通になり自由度が低いので、Componentを作る事にしました。
components/app/Markdown.vue
新しく作るので、Composition APIで書く事にします。
propsでパラメータ渡せば、ある程度、呼び出し側で調整できるようになりますが、
今は使わないので、必要になった時に追加する想定です。
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="source != null" v-html="md.render(source)" />
</template>
<script setup lang="ts">
import MarkdownIt from 'markdown-it'
import sanitizer from 'markdown-it-sanitizer'
import link from 'markdown-it-link-attributes'
import emoji from 'markdown-it-emoji'
import sub from 'markdown-it-sub'
import sup from 'markdown-it-sup'
import ins from 'markdown-it-ins'
import mark from 'markdown-it-mark'
import footnote from 'markdown-it-footnote'
import deflist from 'markdown-it-deflist'
import abbr from 'markdown-it-abbr'
import hljs from 'highlight.js'
// import 'highlight.js/styles/github.min.css'
import 'highlight.js/styles/github-dark.min.css'
importは動的に変えられないので、別途対応しました。
→ 適用するCSSをテーマで切り替える(markdown-itのhighlight.js)
defineProps({
source: {
type: String,
default: null
}
})
const md = new MarkdownIt({ // https://github.com/markdown-it/markdown-it, https://markdown-it.github.io/
html: true,
breaks: true,
linkify: true,
typographer: true,
highlight: function (str: string, lang: string) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(str, { language: lang }).value
// return '<pre class="hljs"><code>' + hljs.highlight(str, { language: lang, ignoreIllegals: true }).value + '</code></pre>'
/* c8 ignore start */
} catch (__) {}
}
return ''
// return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'
/* c8 ignore stop */
}
})
.use(sanitizer) // https://github.com/svbergerem/markdown-it-sanitizer, NOTE: 「html: true」で「<script>」等が、サニタイズされる
.use(link, { // https://github.com/crookedneighbor/markdown-it-link-attributes
attrs: {
target: '_blank',
rel: 'noopener'
}
})
.use(emoji) // https://github.com/markdown-it/markdown-it-emoji
.use(sub) // https://github.com/markdown-it/markdown-it-sub
.use(sup) // https://github.com/markdown-it/markdown-it-sup
.use(ins) // https://github.com/markdown-it/markdown-it-ins
.use(mark) // https://github.com/markdown-it/markdown-it-mark
.use(footnote) // https://github.com/markdown-it/markdown-it-footnote
.use(deflist) // https://github.com/markdown-it/markdown-it-deflist
.use(abbr) // https://github.com/markdown-it/markdown-it-abbr
</script>
highlightは公式にあった1つ目にしています。コメントアウトしているのは2つ目のものです。
呼び出し側は普通にこんな感じで。
<AppMarkdown :source="space.description" />
デザイン調整
bootstrapとかを入れれば、ある程度は改善しますが、style scopedでimportしても、layoutにも影響してしまったりしたので、結局、自前で組む事にしました。
この方が安全だし、自由度もあるので良いかも。
before | after |
<style scoped>
/* NOTE: VuetifyのCSS Resetで崩れる+調整の為 */
div >>> hr {
margin: 20px 0;
}
div >>> p {
margin: 0 0 10px 0;
}
div >>> h1, div >>> h2, div >>> h3 {
margin: 20px 0 10px 0;
}
div >>> h4, div >>> h5, div >>> h6 {
margin: 10px 0;
}
div >>> blockquote {
margin: 0 0 20px 0;
padding: 10px 20px;
border-left: 5px solid rgb(var(--v-theme-background-2)); /* <- #eee; */
}
div >>> ul, div >>> ol {
margin: 0 0 10px 0;
padding: 0 0 0 40px;
}
div >>> ul ul, div >>> ol ul, div >>> ul ol, div >>> ol ol {
margin: 0;
}
div >>> code {
padding: 2px 4px;
font-size: 90%;
color: rgb(var(--v-theme-accent)); /* <- #d73a49; <- #c7254e; */
background-color: rgb(var(--v-theme-background-1)); /* <- #f9f2f4; */
border-radius: 4px;
}
div >>> pre {
padding: 9.5px;
margin: 0 0 10px 0;
word-break: break-all;
background-color: rgb(var(--v-theme-background-1)); /* <- #f5f5f5; */
border: 1px solid rgb(var(--v-theme-background-2)); /* <- #ccc; */
border-radius: 4px;
}
div >>> pre code {
padding: 0;
font-size: inherit;
color: inherit;
white-space: pre-wrap;
}
div >>> table {
width: 100%;
margin: 0 0 20px 0;
border-collapse: collapse;
}
div >>> th {
padding: 8px;
border-bottom: 2px solid rgb(var(--v-theme-background-2)); /* <- #ddd; */
}
div >>> tr:nth-child(odd) > td {
background-color: rgb(var(--v-theme-background-1)); /* <- #f9f9f9; */
}
div >>> td {
padding: 8px;
border-top: 1px solid rgb(var(--v-theme-background-2)); /* <- #ddd; */
}
div >>> img {
max-width: 35%;
vertical-align: middle;
}
</style>
plugins/vuetify.ts
light/darkテーマに対応できるように、colorsを追加しています。
export const vuetify = createVuetify({
theme: {
defaultTheme: 'dark',
themes: { // https://vuetifyjs.com/en/styles/colors/#material-colors
light: {
dark: false,
colors: {
accent: '#FF8F00', // amber-darken-3 <- Nuxt2(blue-accent-1)
+ 'background-1': '#FAFAFA', // grey-lighten-5
+ 'background-2': '#E0E0E0' // grey-lighten-2
}
},
dark: {
dark: true,
colors: {
accent: '#FF8F00', // amber-darken-3 <- Nuxt2(grey.darken3)
+ 'background-1': '#424242', // grey-darken-3
+ 'background-2': '#616161' // grey-darken-2
markdown-it-sanitizerの効果を確認
元データ
<script>alert('動いちゃダメ');</script> <table><tr>テーブル</tr></table> <hr/> 改行<br/>される
なし | あり |
brやhrはどちらも使えましたが、scriptやtableはサニタイズされて文字として表示されました。
Vitestでテストを書く
最低限、動く事を確認しているだけですが、testも書いたので、載せておきます。
test/components/app/Markdown.test.ts
import { mount } from '@vue/test-utils'
import Component from '~/components/app/Markdown.vue'
describe('Markdown.vue', () => {
const mountFunction = (props = {}) => {
const wrapper = mount(Component, {
props
})
expect(wrapper.vm).toBeTruthy()
return wrapper
}
// テスト内容
const viewTest = (wrapper: any, data: string) => {
expect(wrapper.html()).toMatch(data)
}
// テストケース
it('[sourceがない]表示されない', () => {
const wrapper = mountFunction({})
viewTest(wrapper, '<!--v-if-->')
})
it('[sourceがある]表示される', () => {
const wrapper = mountFunction({ source: '# test' })
viewTest(wrapper, '<h1>test</h1>')
})
it('[sourceがある(highlight)]表示される', () => {
const wrapper = mountFunction({ source: '```js\ntest\n```\n' })
viewTest(wrapper, '<pre><code class="language-js">test\n</code></pre>')
})
})
今回のコミット内容
良い感じに導入できました。参考になれば幸いです。