Markdown 自动生成文章目录

2017-06-1228649 阅读33 评论

我们的之前在博客中使用了 Markdown 来为文章提供排版支持。Markdown 在渲染内容的同时还可以自动提取整个内容的目录结构,现在我们来使用 Markdown 为文章自动生成目录。

在文中插入目录

先来回顾一下博客的 Post(文章)模型,其中 body 是我们存储 Markdown 文本的字段:

blog/models.py

from django.db import models

class Post(models.Model):
    # Other fields ...
    body = models.TextField()

再来回顾一下文章详情页的视图,我们在 PostDetailView 中将 post 的 body 字段中的 Markdown 文本渲染成了 HTML 文本,然后传递给模板显示。注意这里我们使用的是类视图,类视图的内容具体请参考 基于类的通用视图:ListView 和 DetailView。

blog/views.py

class PostDetailView(DetailView):
    # 这些属性的含义和 ListView 是一样的
    model = Post
    template_name = 'blog/detail.html'
    context_object_name = 'post'

    def get(self, request, *args, **kwargs):
        # ...

    def get_object(self, queryset=None):
        # 覆写 get_object 方法的目的是因为需要对 post 的 body 值进行渲染
        post = super(PostDetailView, self).get_object(queryset=None)
        post.body = markdown.markdown(post.body,
                                      extensions=[
                                          'markdown.extensions.extra',
                                          'markdown.extensions.codehilite',
                                          'markdown.extensions.toc',
                                      ])
        return post

    def get_context_data(self, **kwargs):
        # ...

看到 get_object 方法中的代码,markdown.markdown() 方法把 post.body 中的 Markdown 文本渲染成了 HTML 文本。同时我们还给该方法提供了一个 extensions 的额外参数。其中 markdown.extensions.codehilite 是代码高亮拓展,而 markdown.extensions.toc 就是自动生成目录的拓展(这里可以看出我们有先见之明,如果你之前没有添加的话记得现在添加进去)。

在渲染 Markdown 文本时加入了 toc 拓展后,就可以在文中插入目录了。方法是在书写 Markdown 文本时,在你想生成目录的地方插入 [TOC] 标记即可。例如新写一篇 Markdown 博文,其 Markdown 文本内容如下:

[TOC]

## 我是标题一

这是标题一下的正文

## 我是标题二

这是标题二下的正文

### 我是标题二下的子标题
这是标题二下的子标题的正文

## 我是标题三
这是标题三下的正文

其最终渲染后的效果就是:

Markdown文中目录

原本 [TOC] 标记的地方被内容的目录替换了。

在页面的任何地方插入目录

上述方式的一个局限局限性就是只能通过 [TOC] 标记在文章内容中插入目录。如果我想在页面的其它地方,比如侧边栏插入一个目录该怎么做呢?方法其实也很简单,只需要稍微改动一下渲染 Markdown 文本内容的方式即可,具体代码就像这样:

blog/views.py

class PostDetailView(DetailView):
    # 这些属性的含义和 ListView 是一样的
    model = Post
    template_name = 'blog/detail.html'
    context_object_name = 'post'

    def get(self, request, *args, **kwargs):
        # ...

    def get_object(self, queryset=None):
        # 覆写 get_object 方法的目的是因为需要对 post 的 body 值进行渲染
        md = markdown.Markdown(extensions=[
            'markdown.extensions.extra',
            'markdown.extensions.codehilite',
            'markdown.extensions.toc',
        ])
        post.body = md.convert(post.body)
        post.toc = md.toc
        return post

    def get_context_data(self, **kwargs):
        # ...

和之前的代码不同,在 get_object 方法中我们没有直接用 markdown.markdown() 方法来渲染 post.body 中的内容,而是先实例化了一个 markdown.Markdown 类 md,和 markdown.markdown() 方法一样,也传入了 extensions 参数。接着我们便使用该实例的 convert 方法将 post.body 中的 Markdown 文本渲染成 HTML 文本。而一旦调用该方法后,实例 md 就会多出一个 toc 属性,这个属性的值就是内容的目录,我们把 md.toc 的值赋给 post.toc 属性(要注意这个 post 实例本身是没有 md 属性的,我们给它动态添加了 md 属性,这就是 Python 动态语言的好处,不然这里还真不知道该怎么把 toc 的值传给模板)。

接下来就在博客文章详情页的文章目录侧边栏渲染文章的目录吧!删掉占位用的目录内容,替换成如下代码:

{% block toc %}
    <div class="widget widget-content">
        <h3 class="widget-title">文章目录</h3>
        {{ post.toc|safe }}
    </div>
{% endblock toc %}

即使用模板变量标签 {{ post.toc }} 显示模板变量的值,注意 post.toc 实际是一段 HTML 代码,我们知道 Django 会对模板中的 HTML 代码进行转义,所以要使用 safe 标签防止 Django 对其转义。其最终渲染后的效果就是:

Markdown自动生成的侧边栏目录

美化标题的锚点 URL

文章内容的标题被设置了锚点,点击目录中的某个标题,页面就会跳到该文章内容中标题所在的位置,这时候浏览器的 URL 显示的值可能不太美观,比如像下面的样子:

http://127.0.0.1:8000/post/8/#_1

http://127.0.0.1:8000/post/8/#_3

#_1 就是锚点,Markdown 在设置锚点时利用的是标题的值,由于通常我们的标题都是中文,Markdown 没法处理,所以它就忽略的标题的值,而是简单地在后面加了个 _1 这样的锚点值。为了解决这一个问题,我们需要修改一下传给 extentions 的参数,其具体做法如下:

blog/views.py

from django.utils.text import slugify
from markdown.extensions.toc import TocExtension

class PostDetailView(DetailView):
    # 这些属性的含义和 ListView 是一样的
    model = Post
    template_name = 'blog/detail.html'
    context_object_name = 'post'

    def get(self, request, *args, **kwargs):
        # ...

    def get_object(self, queryset=None):
        # 覆写 get_object 方法的目的是因为需要对 post 的 body 值进行渲染
        md = markdown.Markdown(extensions=[
            'markdown.extensions.extra',
            'markdown.extensions.codehilite',
            # 记得在顶部引入 TocExtension 和 slugify
            TocExtension(slugify=slugify),
        ])
        post.body = md.convert(post.body)
        post.toc = md.toc
        return post

    def get_context_data(self, **kwargs):
        # ...

和之前不同的是,extensions 中的 toc 拓展不再是字符串 markdown.extensions.toc ,而是 TocExtension 的实例。TocExtension 在实例化时其 slugify 参数可以接受一个函数作为参数,这个函数将被用于处理标题的锚点值。Markdown 内置的处理方法不能处理中文标题,所以我们使用了 django.utils.text 中的 slugify 方法,该方法可以很好地处理中文。

这时候标题的锚点 URL 变得好看多了。

http://127.0.0.1:8000/post/8/#我是标题一

http://127.0.0.1:8000/post/8/#我是标题二下的子标题

总结

本章节的代码位于:Step24: extract content automatically using markdown。

如果遇到问题,请通过下面的方式寻求帮助。

-- EOF --

33 评论
登录后回复
shuhehehe123
shuhehehe123
2019-08-28 17:41
    1. 段落引用

回复
shuhehehe123
shuhehehe123
2019-08-28 17:41

回复
推粪的小瓢虫丶
推粪的小瓢虫丶
2018-09-27 14:01

博主请问我用你的代码运行发现,post.body也会渲染toc,这样2个toc不是很美观,请问应该怎么解决呢?django是2版本的。

回复
追梦人物
追梦人物 推粪的小瓢虫丶
2018-11-24 17:10

一种解决方案就是使用 js 动态给 toc 添加样式

回复
Even
Even
2018-07-26 10:18

你好,我想咨询一下,我后端也用了markdown插件TinyMCE去保存文章,但是这样保存在数据库的数据就自动带了html标签,无法使用toc变量去传输目录。针对这种情况博主有好的解决方案吗?

回复
Even
Even Even
2018-07-26 10:19

"后端"改成"后台admin"

回复
Lenaa56
Lenaa56
2018-06-11 16:54

我的侧边栏为什么没有文章目录这一栏?

回复
LYCai
LYCai
2018-04-11 20:16

post.toc = md.toc没有值的,文章是有目录的,代码也试过复制你的代码,好奇怪

回复
xiaopanddxiong
xiaopanddxiong LYCai
2018-04-13 17:33

前面加一行:post = super(PostDetailView, self).get_object(queryset=None)

回复
LYCai
LYCai xiaopanddxiong
2018-04-14 16:24

试过了,但是还是没有值

回复
lanlingsheng
lanlingsheng LYCai
2018-04-14 20:07

检查下 blog/urls.py  是不是

  url(r'^post/(?P<pk>[0-9]+)/$', views.PostDetailView.as_view(), name='detail'),

回复
LYCai
LYCai lanlingsheng
2018-04-14 23:49

谢谢啦 , 原来是这个问题,太粗心了

回复
acaiblog
acaiblog lanlingsheng
2018-09-11 15:15

我在url.py添加这个参数后访问文章详情页面返回404错误?这是为什么呢?

回复
JonhoyChan
JonhoyChan
2018-03-06 20:04

博主你好,为什么我的url是这样的,用的python3#%E6%A0%87%E9%A2%98%E4%B8%80

还有post.toc = md.toc这段话里的md.toc在pycharm下会变黄,但是运行却正常,感觉很奇怪

回复
Stallionshell
Stallionshell
2017-11-22 14:21

博主你好,为什么在添加目录的get_object方法中,不需要先

post = super(PostDetailView, self).get_object(queryset=None)

实例化post呢?我测试了一下似乎不用不添加这个语句运行时也没有报错。

回复
追梦人物
追梦人物 Stallionshell
2017-11-22 16:36
回复
小鹏super30110
小鹏super30110
2017-08-03 12:03

请教下,如何在点击详情也上端的评论链接时也能跳转到先面的评论表单,或者已有评论列表起始位置,谢谢!

回复
追梦人物
追梦人物 小鹏super30110
2017-08-04 15:00

在你需要跳转的地方设置 #锚点,在跳转的链接中加入这个锚点即可。具体请搜索 html 文档锚点设置方法。

回复
李江波
李江波
2017-07-25 23:34

post.toc = md.toc
楼主,这个传过去没数据怎么回事

回复
追梦人物
追梦人物 李江波
2017-08-04 20:50

审核一下代码是不是写错了?然后看看是不是文章本身没有目录?

回复
LYCai
LYCai 李江波
2018-04-11 20:09

同问 你解决了吗

Image

回复
Xu Lichen
Xu Lichen
2017-07-25 17:58

我在运行的时候发现下行出现
TocExtension(slugify=slugify),
提示:没有slugify这个参数,然后我就去看markdown源码,发现TocExtension内只configs这一个参数,且源码内有
for key, value in configs:
self.setConfig(key, value)
想是对configs格式有要求,要key和value的值且要可迭代,
因此我就把代码改成TocExtension(configs=[('slugify', slugify)]),
之后运行正常,没有细究原因,仅供参考。

回复
追梦人物
追梦人物 Xu Lichen
2017-08-04 20:52

怀疑是 markdown 的版本和示例中的不同?最新版 markdown 是支持这个参数的。

回复
发情的兔
发情的兔
2017-07-23 16:08

"我们把 md.toc 的值赋给 post.md 属性"这里应该是post.toc吧?

回复
追梦人物
追梦人物 发情的兔
2017-07-24 21:03

貌似是的,感谢指正。

回复
mihelloO
mihelloO
2017-06-12 20:34

厉害!TocExtension(slugify=slugify) 这个博主哪里有相关资料。我看了源码才知道这个用法。

回复
追梦人物
追梦人物 mihelloO
2017-06-12 20:44

Markdown 的官方文档就有。

回复
mihelloO
mihelloO 追梦人物
2017-06-12 20:52

学习了,现在django进阶是跟着博主混了。

回复
追梦人物
追梦人物 mihelloO
2017-06-12 21:12

多看文档咯,最近在忙什么项目?

回复
mihelloO
mihelloO 追梦人物
2017-06-12 21:15

下个阶段搞个运维平台,管理服务器。刚搞了个小工具。实时浏览日志,就是用django channels做 web 版本的tail -f。以后多叨扰博主了,多请教你。

回复
追梦人物
追梦人物 mihelloO
2017-06-12 21:26

一起学习了,你说的这些我都没用过,该我向你学习。

回复
mihelloO
mihelloO 追梦人物
2017-06-12 22:16

一起学习,跟你学。我发现博客用手机无法微博登录,【访问出错】error:redirect_uri_mismatch

回复
追梦人物
追梦人物 mihelloO
2017-06-12 22:44

是的,微博登录在有些浏览器下有问题,也不知道怎么回事。

回复