---
url: /blog-ocean-world/markdown-it-custom-syntax.md
description: 顏文字換行會原地變為一坨亂碼，該怎麼辦呢？(PД`q｡)
---

![markdown-it-custom-syntax](/markdown-it-custom-syntax.webp){.cover}

# 臉被切到了怎麼辦！來自定義 markdown 語法吧

段落文字太長時，在句尾的顏文字很容易被換行，像這樣 (＃°
Д°)

一秒便變成了一坨不知所云的亂碼。%( ´•̥̥̥ ω •̥̥̥\` )%

原本解法是像這樣 `<span class="text-nowrap">(>'-'<)</span>`，把顏文字用 HTML 包起來。

有效，但是實在不夠~~懶人~~優雅。%( ・ิω・ิ)%

既然此部落格基於 markdown 寫作，那麼就來調整一下 markdown-it 吧！%◝(≧∀≦)◟%

## 自定義語法

markdown-it 設計之初就考慮到了擴展性，自定義語法小菜一碟。

查了一圈 markdown 還沒被使用的符號，看起來 `%` 還不錯，所以預計使用 % 符號建立一個決不換行的區塊。

也就是 `%(>'-'<)%` 自動變為 `<span class="text-nowrap">(>'-'<)</span>`

首先建立一個 plugin。

`.vitepress\plugin\markdown-it-nowrap.ts`

```ts
import type MarkdownIt from 'markdown-it'

const RULE_NAME = 'nowrap_span'
const MARKER = '%'

/** 緊跟在數字後面的 % 是百分比（4.2%、100%），不是標記 */
function isPercentSign(source: string, markerIndex: number) {
  const previousChar = source[markerIndex - 1]
  return previousChar !== undefined && previousChar >= '0' && previousChar <= '9'
}

/** 使用 MARKER 包圍文字，即可建立不換行的元素
 * https://vitepress.dev/guide/markdown#advanced-configuration
 *
 * 內文的百分比會跟標記衝突，例如「掉 4.2%。[連結](url) 差 50%」，
 * 兩個百分比之間的文字（含連結）會整段被當成顏文字吞掉，
 * 所以數字後面的 % 一律視為百分比，且顏文字不跨行。
 *
 * @param md
 */
export function markdownItNowrap(md: MarkdownIt) {
  /** 解析 % 區塊
   *
   * 顏文字僅用於 inline 內容，所以註冊在 inline ruler 之前
   */
  md.inline.ruler.before('emphasis', RULE_NAME, (state, silent) => {
    const start = state.pos
    const markerLength = MARKER.length

    // 如果不是 %，直接跳過
    if (state.src.slice(start, start + markerLength) !== MARKER) {
      return false
    }

    if (isPercentSign(state.src, start)) {
      return false
    }

    let end = state.src.indexOf(MARKER, start + markerLength)
    while (end !== -1 && isPercentSign(state.src, end)) {
      end = state.src.indexOf(MARKER, end + markerLength)
    }
    if (end === -1) {
      return false
    }

    // 顏文字不會跨行，跨行代表這個 % 沒有配對
    if (state.src.slice(start, end).includes('\n')) {
      return false
    }

    if (!silent) {
      const token = state.push(RULE_NAME, '', 0)
      token.content = state.src.slice(start + markerLength, end).trim()
    }

    state.pos = end + markerLength
    return true
  })

  /** 渲染自定義區塊內容 */
  md.renderer.rules[RULE_NAME] = (tokens, idx) => {
    if (!tokens[idx]?.content) {
      return ''
    }

    return `<span class="text-nowrap">${tokens[idx].content}</span>`
  }
}

```

接著依照[文件指示](https://vitepress.dev/guide/markdown#advanced-configuration)，在 vitepress 使用 plugin。

`.vitepress\config.mts`

```ts
// ...

// https://vitepress.dev/reference/site-config
export default ({ mode }: { mode: string }) => {
  // ...

  return withMermaid({
    title: '鱈魚的魚缸',
    // ...

    markdown: {
      lineNumbers: true,
      config(md) {
        md.use((md) => markdownItBaseImg(md, mode))
        md.use(markdownItNowrap) // [!code ++]
      },
    },

    // ...
  })
}
```

完工！

現在被 % 包圍的文字都會自動變成帶有 text-nowrap class 的 `span` 了！

![nowrap-span](/markdown-it-custom-syntax/nowrap-span.png)

對原理有興趣的讀者們，歡迎來[看看這篇文章](https://ithelp.ithome.com.tw/articles/10354983)。%◝( •ω• )◟%

## 總結 🐟

* markdown-it 可自由擴展自定義語法
* 透過自定義語法讓顏文字不會被換行
