1. 程式人生 > >HelloDjango 第 10 篇:小細節 Markdown 文章自動生成目錄,提升閱讀體驗

HelloDjango 第 10 篇:小細節 Markdown 文章自動生成目錄,提升閱讀體驗

目錄

  • 在文中插入目錄
  • 在頁面的任何地方插入目錄
  • 處理空目錄
  • 美化標題的錨點 URL

作者: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 檢視函式中將 postbody 欄位中的 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 文字時,在你想生成目錄的地方插入 `` 標記即可。例如新寫一篇 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 顯示的值可能不太美觀,比如像下面的樣子:

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

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

#_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 變得好看多了。

http://127.0.0.1:8000/posts/8/#我是標題一

http://127.0.0.1:8000/posts/8/#我是標題二下的子標題

歡迎關注 HelloGitHub 公眾號,獲取更多開源專案的資料和