超能郭工
前端坤坤的小破站,IKUN永不过时 🐔

博客中常用的 Markdown 处理

1 年前 技术分享

现在写博客基本离不开 Markdown 的处理了,也趁着重构学习一下Markdown大致的处理逻辑博客中常用的 Markdown 处理

安装依赖

bash
yarn add markdown-it
# 需要支持ts时添加
yarn add @types/markdown-it -D

扩展Markdown

多列图片

常常图片截图太长导致,导致文字都被图片挤到不起眼的地方,时而也希望两张图在一行,更好的展示对比情况等。

46f508cc-5db9-48d6-a5c7-aafb3bf525e8.png

Untitled.png

javascript
import markdownItContainer from "markdown-it-container";

/**
* 让 markdown 支持多列
* 示例:
::: column 列数
![img](img_url)
![img](img_url)
:::
*/
markdown.use(markdownItContainer, "column", {
  validate: function (params: string) {
    return params.trim().match(/^column\s+(.*)$/);
  },
  render: function (tokens: any, idx: number) {
    const m = tokens[idx].info.trim().match(/^column\s+(.*)$/);
    if (tokens[idx].nesting === 1) {
      // opening tag
      return `<div class="column" style="--article-columns: ${markdown.utils.escapeHtml(
        m[1]
      )}">`;
    } else {
      // closing tag
      return "</div>";
    }
  },
});

代码折叠及展示

javascript
import copySvg from "~/assets/svg/copy.svg?raw";
import arrowSvg from "~/assets/svg/arrow.svg?raw";

// 对应渲染代码及 mermaid图的方法与事件在 Markdown.vue 执行
markdown.renderer.rules.fence = (tokens, idx) => {
  const token = tokens[idx];
  const attrs = token.info.trim().split(" ");
  const lang = attrs[0] || "plaintext";
  const str = token.content;
  const title = attrs[1] || "";
  if (lang === "mermaid") {
    return `<pre class="mermaid">${str}</pre>`;
  }
  return `
    <figure class="code">
      <div class="header">
        <div class="title">
          <span class="collapse-icon" title="折叠代码">${arrowSvg}</span>
          ${title ? `<b>${title}</b>` : ""}
          <span class="lang">${lang}</span>
        </div>
        <div class="tool">
          <span class="copy-icon" title="复制代码">${copySvg}</span>
        </div>
      </div>
      <pre class="line-numbers"><code class="language-${lang}">${markdown.utils.escapeHtml(
    str
  )}</code></pre>
    </figure>`;
};

图片URL处理及增加 lazy

javascript
markdown.renderer.rules.image = function (tokens, idx, options, env, self) {
  let src = tokens[idx].attrGet("src");
  if (src) {
    // 为图片增加懒加载标识
    tokens[idx].attrSet("alt", tokens[idx].content);
    tokens[idx].attrSet("title", tokens[idx].content);
    tokens[idx].attrSet("src", "/img/placeholder.svg");
    if (src.indexOf("http") === -1) {
      // 当图片为站点本地图片时则对链接增加前缀与图片处理参数
      src = mediaUrl(src || "");
    }
    tokens[idx].attrSet("data-src", src);
    tokens[idx].attrSet("loading", "lazy");
    // env.gallary.push(src)
  }
  return self.renderToken(tokens, idx, options);
};

外链增加ref

javascript
markdown.renderer.rules.link_open = function (tokens, idx, options, env, self) {
  let href = tokens[idx]?.attrGet("href") || "";
  if (tokens[idx]?.attrIndex("target") < 0 && href.indexOf("http") > -1) {
    tokens[idx].attrPush(["target", "_blank"]);
    tokens[idx].attrPush(["ref", "noopener nofollow"]);
  }
  return self.renderToken(tokens, idx, options);
};

目录生成

javascript
function uuid() {
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
    var r = (Math.random() * 16) | 0,
      v = c == "x" ? r : (r & 0x3) | 0x8;
    return v.toString(16);
  });
}

markdown.core.ruler.push("toc", (state) => {
  if (typeof state.env?.tocCallback === "function") {
    let tocArr = state.tokens.reduce((arr: IToc[], token, idx) => {
      if (
        token.type == "heading_open" &&
        state.tokens[idx + 1] &&
        state.tokens[idx + 1].type === "inline"
      ) {
        let inline = state.tokens[idx + 1];
        let level = token.tag.replace("h", "");
        token.attrPush(["level", level]);
        let headerName =
          inline.children?.reduce((acc, token) => {
            return acc + (token.type == "text" ? token.content : "");
          }, "") || "";
        let anchorName = encodeURIComponent(headerName || uuid());
        token.attrPush(["id", anchorName]);
        arr.push({
          id: anchorName,
          title: headerName,
          level: parseInt(level),
          children: [],
        });
      }
      return arr;
    }, []);
    // 将tocArr 按level 变成树级
    const tree = tocArr.reduce((acc: IToc[], item: IToc) => {
      let currentLevel = acc;
      let lastItem = null;

      // Traverse the tree to find the correct parent based on the level
      for (let i = 1; i < item.level; i++) {
        if (lastItem && lastItem.children) {
          currentLevel = lastItem.children;
        } else {
          // If no children array exists, create one
          if (lastItem) {
            lastItem.children = [];
            currentLevel = lastItem.children;
          }
        }
        lastItem = currentLevel[currentLevel.length - 1];
      }

      // Add the current item to the correct level
      currentLevel.push(item);

      return acc;
    }, []);
    state.env.tocCallback.call(undefined, tree);
  }
});

最终代码

plugins/markdown.ts javascript
import markdownIt from "markdown-it";
import copySvg from "~/assets/svg/copy.svg?raw";
import arrowSvg from "~/assets/svg/arrow.svg?raw";
import markdownItContainer from "markdown-it-container";
import type { IToc } from "~/types";

const markdown = markdownIt({
  html: true,
  typographer: true,
  langPrefix: "",
});

markdown.use(markdownItContainer, "column", {
  validate: function (params: string) {
    return params.trim().match(/^column\s+(.*)$/);
  },
  render: function (tokens: any, idx: number) {
    const m = tokens[idx].info.trim().match(/^column\s+(.*)$/);
    if (tokens[idx].nesting === 1) {
      // opening tag
      return `<div class="column" style="--article-columns: ${markdown.utils.escapeHtml(
        m[1]
      )}">`;
    } else {
      // closing tag
      return "</div>";
    }
  },
});

markdown.renderer.rules.fence = (tokens, idx) => {
  const token = tokens[idx];
  const attrs = token.info.trim().split(" ");
  const lang = attrs[0] || "plaintext";
  const str = token.content;
  const title = attrs[1] || "";
  if (lang === "mermaid") {
    return `<pre class="mermaid">${str}</pre>`;
  }
  return `
    <figure class="code">
      <div class="header">
        <div class="title">
          <span class="collapse-icon" title="折叠代码">${arrowSvg}</span>
          ${title ? `<b>${title}</b>` : ""}
          <span class="lang">${lang}</span>
        </div>
        <div class="tool">
          <span class="copy-icon" title="复制代码">${copySvg}</span>
        </div>
      </div>
      <pre class="line-numbers"><code class="language-${lang}">${markdown.utils.escapeHtml(
    str
  )}</code></pre>
    </figure>`;
};

markdown.renderer.rules.image = function (tokens, idx, options, env, self) {
  let src = tokens[idx].attrGet("src");
  if (src) {
    // 为图片增加懒加载标识
    tokens[idx].attrSet("alt", tokens[idx].content);
    tokens[idx].attrSet("title", tokens[idx].content);
    tokens[idx].attrSet("src", "/img/placeholder.svg");
    if (src.indexOf("http") === -1) {
      // 当图片为站点本地图片时则对链接增加前缀与图片处理参数
      src = mediaUrl(src || "");
    }
    tokens[idx].attrSet("data-src", src);
    tokens[idx].attrSet("loading", "lazy");
    // env.gallary.push(src)
  }
  return self.renderToken(tokens, idx, options);
};

markdown.renderer.rules.link_open = function (tokens, idx, options, env, self) {
  let href = tokens[idx]?.attrGet("href") || "";
  if (tokens[idx]?.attrIndex("target") < 0 && href.indexOf("http") > -1) {
    tokens[idx].attrPush(["target", "_blank"]);
    tokens[idx].attrPush(["ref", "noopener nofollow"]);
  }
  return self.renderToken(tokens, idx, options);
};

function uuid() {
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
    var r = (Math.random() * 16) | 0,
      v = c == "x" ? r : (r & 0x3) | 0x8;
    return v.toString(16);
  });
}

markdown.core.ruler.push("toc", (state) => {
  if (typeof state.env?.tocCallback === "function") {
    let tocArr = state.tokens.reduce((arr: IToc[], token, idx) => {
      if (
        token.type == "heading_open" &&
        state.tokens[idx + 1] &&
        state.tokens[idx + 1].type === "inline"
      ) {
        let inline = state.tokens[idx + 1];
        let level = token.tag.replace("h", "");
        token.attrPush(["level", level]);
        let headerName =
          inline.children?.reduce((acc, token) => {
            return acc + (token.type == "text" ? token.content : "");
          }, "") || "";
        let anchorName = encodeURIComponent(headerName || uuid());
        token.attrPush(["id", anchorName]);
        arr.push({
          id: anchorName,
          title: headerName,
          level: parseInt(level),
          children: [],
        });
      }
      return arr;
    }, []);
    // 将tocArr 按level 变成树级
    const tree = tocArr.reduce((acc: IToc[], item: IToc) => {
      let currentLevel = acc;
      let lastItem = null;

      // Traverse the tree to find the correct parent based on the level
      for (let i = 1; i < item.level; i++) {
        if (lastItem && lastItem.children) {
          currentLevel = lastItem.children;
        } else {
          // If no children array exists, create one
          if (lastItem) {
            lastItem.children = [];
            currentLevel = lastItem.children;
          }
        }
        lastItem = currentLevel[currentLevel.length - 1];
      }

      // Add the current item to the correct level
      currentLevel.push(item);

      return acc;
    }, []);
    state.env.tocCallback.call(undefined, tree);
  }
});

export default defineNuxtPlugin(async (nuxtApp) => {
  nuxtApp.provide("md", markdown);
});

Markdown.vue html
<template>
  <article
    class="article prose prose-neutral dark:prose-invert max-w-full break-words"
  >
    <div v-html="content"></div>
  </article>
  <!-- 自写的简易图集展示 -->
  <Gallery
    :images="gallery.images"
    :index="gallery.index"
    v-model:show="gallery.show"
  ></Gallery>
</template>

<script lang="ts" setup>
import { lazyLoad } from "unlazy";
import type { IToc } from "~/types";

const props = defineProps<{
  content: string;
  toc?: boolean;
}>();

const emit = defineEmits<{
  (e: "toc", toc: IToc[]): void;
}>();

const { $md } = useNuxtApp();
const colorMode = useColorMode();

const content = computed(() => {
  return $md.render(props.content, {
    toc: props.toc,
    tocCallback(tocArr: IToc[]) {
      emit("toc", tocArr);
    },
  });
});

//引入第三方的Prism.js代码
const thirdPartyCDN = [
  "https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.27.0/plugins/line-numbers/prism-line-numbers.min.css",
  "https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.27.0/components/prism-core.min.js",
  "https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.27.0/plugins/autoloader/prism-autoloader.min.js",
  "https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.27.0/plugins/line-numbers/prism-line-numbers.min.js",
  "https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/mermaid/8.14.0/mermaid.min.js",
];

useHead(
  thirdPartyCDN.reduce(
    (acc, url, idx) => {
      if (url.lastIndexOf(".css") > -1) {
        acc.link.push({
          hid: `thirdPartyCss${idx}`,
          rel: "stylesheet",
          href: url,
        });
      } else if (url.lastIndexOf(".js") > -1) {
        acc.script.push({
          hid: `thirdPartyJs${idx}`,
          src: url,
          defer: true,
          tagPosition: "bodyClose",
        });
      }
      return acc;
    },
    { link: [], script: [] } as any
  )
);


watch(
  colorMode,
  () => {
    useHead({
      link: [
        {
          rel: "stylesheet",
          id: "prismTheme",
          href: `https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.27.0/themes/${
            colorMode.value == "light" ? "prism" : "prism-tomorrow"
          }.min.css`,
        },
      ],
    });
  },
  {
    immediate: true,
  }
);

const initMermaid = () => {
  if (window.mermaid) {
    try {
      window.mermaid.initialize({ startOnLoad: true });
      // 查找所有未渲染的mermaid图表并渲染
      document
        .querySelectorAll('.mermaid:not([data-processed="true"])')
        .forEach((elem) => {
          window.mermaid.init(undefined, elem);
        });
    } catch (error) {
      console.error("Mermaid初始化失败:", error);
    }
  }
};

const gallery = reactive<{
  images: string[];
  index: number;
  show: boolean;
}>({
  images: [],
  index: 0,
  show: false,
});

watch(
  () => props.content,
  () => {
    nextTick(() => {
      lazyLoad("article img[loading='lazy']");
      initPrism();
      initMermaid();
    });
  },
  { immediate: false }
);

const { copy } = useClipboard();
onMounted(() => {
  lazyLoad("article img[loading='lazy']");
  initPrism();
  initMermaid();
});

function initPrism() {
  window.Prism?.highlightAll();
  document.querySelectorAll("figure.code").forEach((item) => {
    item.querySelector(".copy-icon")?.addEventListener("click", () => {
      const text = item.querySelector("pre")?.textContent;
      copy(text || "");
      useToast("复制成功");
    });
    item.querySelector(".collapse-icon")?.addEventListener("click", () => {
      item.classList.toggle("collapse");
    });
  });
  document.querySelectorAll("article img").forEach((elem: Element, idx) => {
    let src = elem.getAttribute("data-src") || elem.getAttribute("src");
    if (src) {
      gallery.images.push(src);
      function handleClickImage() {
        gallery.index = idx;
        gallery.show = true;
      }
      elem.addEventListener("click", handleClickImage);
    }
  });
}
</script>

<style lang="stylus">
.article{
  figure.code{
    @apply: "relative rounded-md overflow-hidden b-1 auto-border-line";
    pre{
      @apply: "m-0 py-m1 bg-[#f9f9f9] dark:bg-[#272b33] rounded-none max-h-[70vh] transition-all duration-300";
    }
    .copy-icon{
      @apply "cursor-pointer"
    }
    .header {
      @apply: "flex justify-between items-center auto-text-title text-bm p-m2 b-b-1 auto-border-line";

      .collapse-icon{
        @apply "rotate-90 transition-transform duration-300 cursor-pointer"
      }
      div{
        @apply: "flex items-center gap-m1";
      }
    }

    .lang{
      @apply: "auto-text-regular uppercase"
    }

    &.collapse{
      .collapse-icon{
        @apply "rotate-0"
      }
      pre{
        @apply "max-h-0 py-0 overflow-hidden"
      }
    }
  }
  img{
    @apply "max-h-70vh w-auto"
  }

  .column{
    @apply "grid gap-m1 justify-items-center"
    grid-template-columns: repeat(var(--article-columns), minmax(0, 1fr));
    @apply "<md:grid-cols-1"
  }
}
</style>

标签: JS/TS
博客中常用的 Markdown 处理
本站除注明转载外均为原创文章,采用 CC BY-NC-ND 4.0 协议。转载请注明出处,不得用于商业用途
评论