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

今回のコミット内容

良い感じに導入できました。参考になれば幸いです。

origin#507 markdown-it導入・デザイン調整、test作成

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です