markdown文本解析成html,并生成大纲

文章讲述了在Vue应用中如何将Markdown转换为HTML并正确渲染样式,遇到的问题如样式不符和DOM操作时机,以及如何通过正则表达式生成HTML内容的大纲,并介绍了使用IntersectionObserver实现滚动导航和高亮功能。
摘要由CSDN通过智能技术生成

markdown转换成html参考:在vue中将markdown转换成html并渲染样式_如何把一个makkerdown 渲染成一个网页-CSDN博客

渲染后样式跟预期不符

添加css即可,一般直接在父标签上设定一个类名然后给子标签加样式:

可以在github上找一个用:GitHub - sindresorhus/github-markdown-css: The minimal amount of CSS to replicate the GitHub Markdown style

转换成html无法通过js操作

这是因为转换后dom加载需要一定的时间,在这个时间内操作会出现 null 问题,解决:

setTimeout(() => {
      //要处理转换html后的逻辑
  },1)

一般参数1毫秒即可,最好设大一点

生成大纲

拿到所有子标签

当dom渲染上去后,我们去获取到用于渲染html文本的标签的所有子标签

document.querySelector(".markdown-body").children
通过正则表达式提取出h1-h10之间的标签
const regex =  /h(10|[1-9])/g;
      for (let i = 0; i < document.querySelector(".markdown-body").children.length; i++){
        if (htmlContent[i].localName.match(regex)) {
            
        }
}
处理每个标题

大纲一般都有父子关系,我们需要去对每个标题进行父子关系的处理,比如标题 3 在标题 1 的后面,那他就是标题 1 的子标题

const  treeData = ref([]);
//是否找到子目录
let isFind = false;
//解析子目录
const setSubDirectory = (directory, level, label) => {
  //表示当前的目录层级
  const curr = directory
  //表示递归的目录层级
  const children = directory.children
  if (children.length > 0) {
    //递归子目录,同层级下,数组下标在其范围即可
    setSubDirectory(directory.children[children.length-1],level,label)
  }
  //子目录已经归位
  if (isFind === true) return;
  //当前层级小于等于目录层级,表示找到了目录
  if (curr.level < level) {
    isFind = true
    curr.children.push({ level, label,children: []})
  }
}

//解析html,生成目录树
const setTreeDataByHtml = (htmlContent) => {
  const regex =  /h(10|[1-9])/g;
      for (let i = 0; i < htmlContent.length; i++){
        if (htmlContent[i].localName.match(regex)) {
          const level = parseInt(htmlContent[i].localName.replace("h", ""))
          const label = htmlContent[i].innerText
          //不需要找子目录
          if (treeData.value.length === 0 || treeData.value[treeData.value.length - 1].level >= level) {
              treeData.value.push({ label, level,children: []})
          } else {
            isFind = false
            setSubDirectory(treeData.value[treeData.value.length - 1],level,label)
          } 
        }
}
}

细节:我们将标题分为两类,一种是直接可以用作父标题,一种则是需要添加在父标题中的子标题

如果当前标题的层级小于等于数据中最后一个父标题的层级,那么他就是父标题,直接添加到数组中即可

子标题通过递归的方式,先找到最底层的标题,依次往上寻找,直到找到合适的位置即可

合适的位置:子标题的层级大于父标题的层级,这里我们用level来表示

解析生成大纲

通过 element-plus 中的 tree组件库来实现

测试结果

完整代码

tips: 复制过去记得改,是拿以前项目写的

文章详情页,在此引用大纲页面,为 ArticleTree,调用 ArticleTree 里的函数生成大纲的函数在82行

<template>
  <div class="container view">
    <div class="card">
      <div class="card-body">
        <h3>{{ share.title }}</h3>
        <span class="author_descibe" style="margin-left: 0px">作者:</span>
        <router-link class="ToOpenShare" :to="{ name: 'home' }">
          <img :src="share.authorPhoto" alt="" />
          <span class="author" style="font-size: 18px; margin-left: 10px"
            >{{ share.author }}
          </span>
        </router-link>
        <span class="createtime">{{ share.createtime }}</span>
        <span class="reading_descibe">阅读</span>
        <span class="reading">{{ share.reading }}</span>
        <hr />

        <div v-html="share.content" class="markdown-body"></div>
      </div>
    </div>

    <hr />

    <CommentView :shareId="route.params.shareId" />
  </div>
 <ArticleTree ref="ArticleTreeRef"/> 
</template>

<script>
import $ from "jquery";
import { useRoute } from "vue-router";
import { useStore } from "vuex";
import { reactive, onMounted,ref } from "vue";
import CommentView from "@/views/share/comment/CommentView.vue";
import { marked } from 'marked';
import ArticleTree from "./ArticleTree.vue";

export default {
  components: {
    CommentView,ArticleTree
  },
  setup() {
    const store = useStore();
    const route = useRoute();
    const ArticleTreeRef = ref(null)
    const share = reactive({
      title: "",
      createtime: "",
      content: "",
      reading: null,
      authorPhoto: "",
      author: "",
    });
    onMounted(() => {
    const link = document.createElement('link')
    link.type = 'text/css'
    link.rel = 'stylesheet'
    link.href = 'https://cdn.bootcss.com/github-markdown-css/2.10.0/github-markdown.min.css'
    document.head.appendChild(link)
});

    
    //打开某一分享页面就调用
    const getShare = () => {
      $.ajax({
        url: "https://app5608.acapp.acwing.com.cn/api/get/share/",
        type: "get",
        data: {
          userId: store.state.user.id,
          shareId: route.params.shareId,
        },
        headers: {
          Authorization: "Bearer " + store.state.user.token,
        },

        success(resp) {
          // eslint-disable-next-line no-empty
          if (resp.error_message === "successfully") {
            share.title = resp.share.title;
            share.createtime = resp.share.createTime;
            share.content = marked(resp.share.content)
            setTimeout(() => {
      ArticleTreeRef.value.setTreeDataByHtml(document.querySelector(".markdown-body").children)
  },1)
            share.reading = resp.share.reading;
            share.authorPhoto = resp.authorPhoto;
            share.author = resp.author;
          }
        },
        error() {},
      });
    };

    getShare();

    return {
      share,
      route,
      ArticleTreeRef,
    };
  },
};
</script>

<style scoped>
.view {
  margin-top: 20px;
}

.container {
  max-width: 900px;
}
hr {
  color: gray;
}
img {
  border-radius: 50%;
  width: 4vh;
  height: 4vh;
  margin-left: 10px;
  line-height: 15px;
}

.author_descibe,
.createtime,
.reading_descibe,
.reading {
  font-size: 12px;
  color: gray;
  margin-left: 10px;
}

.ToOpenShare {
  color: rgb(51, 122, 199);
  text-decoration: none;
  line-height: 30px;
  font-size: 16px;
}

.ToOpenShare:hover {
  color: rgb(35, 82, 124);
  text-decoration: underline;
}
</style>

ArticleTree

<!-- 将文章目录以树的形式展示 -->
<template>
  <el-tree :data="treeData" :expand-on-click-node="false" class="tree"></el-tree>
</template>

<script setup>
import { ref,defineExpose } from "vue";
const  treeData = ref([]);
//是否找到子目录
let isFind = false;
//解析子目录
const setSubDirectory = (directory, level, label) => {
  //表示当前的目录层级
  const curr = directory
  //表示递归的目录层级
  const children = directory.children
  if (children.length > 0) {
    //递归子目录,同层级下,数组下标在其范围即可
    setSubDirectory(directory.children[children.length-1],level,label)
  }
  //子目录已经归位
  if (isFind === true) return;
  //当前层级小于等于目录层级,表示找到了目录
  if (curr.level < level) {
    isFind = true
    curr.children.push({ level, label,children: []})
  }
}

//解析html,生成目录树
const setTreeDataByHtml = (htmlContent) => {
  const regex =  /h(10|[1-9])/g;
      for (let i = 0; i < htmlContent.length; i++){
        if (htmlContent[i].localName.match(regex)) {
          const level = parseInt(htmlContent[i].localName.replace("h", ""))
          const label = htmlContent[i].innerText
          //不需要找子目录
          if (treeData.value.length === 0 || treeData.value[treeData.value.length - 1].level >= level) {
              treeData.value.push({ label, level,children: []})
          } else {
            isFind = false
            setSubDirectory(treeData.value[treeData.value.length - 1],level,label)
          } 
        }
}
}
defineExpose({
  setTreeDataByHtml
})
</script>


<style scoped>
.tree{
  width: 20%;
  position: fixed;
  top: 80px;  /* 从页面顶部的距离 */
  right: 50px; /* 从页面左侧的距离 */
}
</style>

点击目录导航到对应标题位置

只要获取到dom实例,通过scrollIntoView函数即可完成

示例:

element.scrollIntoView({
          behavior: "instant",
          block: "center",
        });

当滚动条滚动到标题位置,设置目录里标题高亮

通过IntersectionObserver实例来做,当监听的dom出现在视图区域中,会触发回调函数,从而来设置高亮

示例:

// 创建 Intersection Observer 实例
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      // 目标元素进入可视区域
      treeRef.value.setCurrentKey(entry.target.id)
    }
  });
}, { threshold: 0 });
//将要监听的目标元素加入Observer中
observer.observe(htmlContent[i])

htmlContent[i] 代表你要监听目标元素的dom

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值