作者:HelloGitHub-追夢人物
文中涉及的示例代碼,已同步更新到 HelloGitHub-Team 倉庫
上一篇中我們使用了 Markdown 來為文章提供排版支持。Markdown 在解析內容的同時還可以自動提取整個內容的目錄結構,現在我們來使用 Markdown 為文章自動生成目錄。
在文中插入目錄
先來回顧一下博客的 Post(文章)模型,其中 body
是我們存儲 Markdown 文本的字段:
blog/models.py
from django.db import models
class Post(models.Model):
# Other fields ...
body = models.TextField()
再來回顧一下文章詳情頁的視圖,我們在 detail
視圖函數中將 post
的 body
字段中的 Markdown 文本解析成了 HTML 文本,然后傳遞給模板顯示。
blog/views.py
def detail(request, pk):
post = get_object_or_404(Post, pk=pk)
post.body = markdown.markdown(post.body,
extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
'markdown.extensions.toc',
])
return render(request, 'blog/detail.html', context={'post': post})
markdown.markdown()
方法把 post.body
中的 Markdown 文本解析成了 HTML 文本。同時我們還給該方法提供了一個 extensions
的額外參數。其中 markdown.extensions.toc
就是自動生成目錄的拓展(這里可以看出我們有先見之明,如果你之前沒有添加的話記得現在添加進去)。
在渲染 Markdown 文本時加入了 toc 拓展后,就可以在文中插入目錄了。方法是在書寫 Markdown 文本時,在你想生成目錄的地方插入 [TOC]
標記即可。例如新寫一篇 Markdown 博文,其 Markdown 文本內容如下:
[TOC]
## 我是標題一
這是標題一下的正文
## 我是標題二
這是標題二下的正文
### 我是標題二下的子標題
這是標題二下的子標題的正文
## 我是標題三
這是標題三下的正文
其最終解析后的效果就是:
原本 [TOC]
標記的地方被內容的目錄替換了。
在頁面的任何地方插入目錄
上述方式的一個局限性就是只能通過 [TOC]
標記在文章內容中插入目錄。如果我想在頁面的其它地方,比如側邊欄插入一個目錄該怎么做呢?方法其實也很簡單,只需要稍微改動一下解析 Markdown 文本內容的方式即可,具體代碼就像這樣:
blog/views.py
def detail(request, pk):
post = get_object_or_404(Post, pk=pk)
md = markdown.Markdown(extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
'markdown.extensions.toc',
])
post.body = md.convert(post.body)
post.toc = md.toc
return render(request, 'blog/detail.html', context={'post': post})
和之前的代碼不同,我們沒有直接用 markdown.markdown()
方法來渲染 post.body
中的內容,而是先實例化了一個 markdown.Markdown
對象 md
,和 markdown.markdown()
方法一樣,也傳入了 extensions
參數。接着我們便使用該實例的 convert
方法將 post.body
中的 Markdown 文本解析成 HTML 文本。而一旦調用該方法后,實例 md
就會多出一個 toc
屬性,這個屬性的值就是內容的目錄,我們把 md.toc
的值賦給 post.toc
屬性(要注意這個 post 實例本身是沒有 toc 屬性的,我們給它動態添加了 toc 屬性,這就是 Python 動態語言的好處)。
接下來就在博客文章詳情頁的文章目錄側邊欄渲染文章的目錄吧!刪掉占位用的目錄內容,替換成如下代碼:
{% 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 就提取不出目錄結構,post.toc 就是一個空的 div 標簽,如下:
<div class="toc">...............................
<ul></ul>
</div>
對於這種沒有目錄結構的文章,在側邊欄顯示一個目錄是沒有意義的,所以我們希望只有在文章存在目錄結構時,才顯示側邊欄的目錄。那么應該怎么做呢?
分析 toc 的內容,如果有目錄結構,ul 標簽中就有值,否則就沒有值。我們可以使用正則表達式來測試 ul 標簽中是否包裹有元素來確定是否存在目錄。
def detail(request, pk):
post = get_object_or_404(Post, pk=pk)
md = markdown.Markdown(extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
'markdown.extensions.toc',
])
post.body = md.convert(post.body)
m = re.search(r'<div class="toc">\s*<ul>(.*)</ul>\s*</div>', md.toc, re.S)
post.toc = m.group(1) if m is not None else ''
return render(request, 'blog/detail.html', context={'post': post})
這里我們正則表達式去匹配生成的目錄中包裹在 ul 標簽中的內容,如果不為空,說明目錄,就把 ul 標簽中的值提取出來(目的是只要包含目錄內容的最核心部分,多余的 HTML 標簽結構丟掉)賦值給 post.toc
;否則,將 post 的 toc 置為空字符串,然后我們就可以在模板中通過判斷 post.toc 是否為空,來決定是否顯示側欄目錄:
{% block toc %}
{% if post.toc %}
<div class="widget widget-content">
<h3 class="widget-title">文章目錄</h3>
<div class="toc">
<ul>
{{ post.toc|safe }}
</ul>
</div>
</div>
{% endif %}
{% endblock toc %}
這里我們看到了一個新的模板標簽 {% if %}
,這個標簽用來做條件判斷,和 Python 中的 if 條件判斷是類似的。
美化標題的錨點 URL
文章內容的標題被設置了錨點,點擊目錄中的某個標題,頁面就會跳到該文章內容中標題所在的位置,這時候瀏覽器的 URL 顯示的值可能不太美觀,比如像下面的樣子:
#_1
就是錨點,Markdown 在設置錨點時利用的是標題的值,由於通常我們的標題都是中文,Markdown 沒法處理,所以它就忽略的標題的值,而是簡單地在后面加了個 _1 這樣的錨點值。為了解決這一個問題,需要修改一下傳給 extentions
的參數,其具體做法如下:
blog/views.py
from django.utils.text import slugify
from markdown.extensions.toc import TocExtension
def detail(request, pk):
post = get_object_or_404(Post, pk=pk)
md = markdown.Markdown(extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
# 記得在頂部引入 TocExtension 和 slugify
TocExtension(slugify=slugify),
])
post.body = md.convert(post.body)
m = re.search(r'<div class="toc">\s*<ul>(.*)</ul>\s*</div>', md.toc, re.S)
post.toc = m.group(1) if m is not None else ''
return render(request, 'blog/detail.html', context={'post': post})
和之前不同的是,extensions
中的 toc
拓展不再是字符串 markdown.extensions.toc
,而是 TocExtension
的實例。TocExtension
在實例化時其 slugify
參數可以接受一個函數,這個函數將被用於處理標題的錨點值。Markdown 內置的處理方法不能處理中文標題,所以我們使用了 django.utils.text
中的 slugify
方法,該方法可以很好地處理中文。
這時候標題的錨點 URL 變得好看多了。
歡迎關注 HelloGitHub 公眾號,獲取更多開源項目的資料和內容