Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
為開發人員,我們依賴于靜態分析工具來檢查、lint(分析)和轉換我們的代碼。我們使用這些工具來幫助我們提高生產效率并生成更好的代碼。然而,當我們使用markdown編寫內容時,可用的工具就很少。
在本文中,我們將介紹如何開發一個Markdown擴展來解決在使用Markdown管理Django站點中的內容時遇到的挑戰。
你認為他們有linter嗎?
照片來自Pexels,由mali maeder拍攝
像每個網站一樣,我們在主頁、FAQ部分和“關于”頁面等地方都有不同類型的(大部分)靜態內容。很長一段時間以來,我們都是在Django模板中直接管理這些內容的。
當我們最終決定是時候將這些內容從模板轉移到數據庫中時,我們認為最好使用Markdown。從Markdown生成HTML更安全,它提供了一定程度的控制和一致性,并且對于非技術用戶來說更容易處理。隨著我們轉移過程的進展,我們注意到我們遺漏了一些東西:
當URL更改時,鏈接到內部頁面的鏈接可能會中斷。在Django模板和視圖中,我們使用了reverseand {% url %},但是這在普通的Markdown中是不可用的。
絕對內部連接不能在不同環境之間進行復制。這可以使用相對鏈接來解決,不過目前沒有開箱即用的增強這一點的方法。
無效鏈接會損害用戶體驗,并導致用戶質疑整個內容的可靠性。這并不是Markdown獨有的東西,只不過HTML模板是由對URL有一定了解的開發人員維護的。另一方面,Markdown文檔是為非技術寫作人員設計的。
當我研究這個問題時,我搜索了Python linters、Markdown預處理器和擴展來幫助生成更好的Markdown。結果都不是很好。一個引人注目的方法是使用Django模板來生成Markdown文檔。
使用Django模板,你可以使用諸如url之類的模板標記來反向查詢URL名稱,并配合使用條件、變量、日期格式和所有其他Django模板特性。這種方法本質上是使用Django模板作為Markdown文檔的預處理程序。
我個人認為這可能不是非技術作家的最佳解決方案。另外,我擔心提供對Django模板標記的訪問可能是危險的。
對這個問題有了更好的理解之后,我們準備在Python中更深入地研究Markdown。
要在Python中開始使用Markdown,我們先安裝markdown包:
接著,創建一個Markdown對象并使用其函數將一些Markdown轉換成HTML:
你現在可以在你的模板中使用這個HTML代碼片段。
基本的Markdown處理器提供了生成HTML內容的基本要素。對于更“新奇”的選項,Python markdown包包含了一些內置擴展。一個流行的擴展是“extra”擴展,除了其他東西之外,它增加了對隔離代碼塊的支持:
為了使用我們獨特的Django功能擴展Markdown,我們將開發自己的擴展。
如果你查看源代碼,你將看到要將markdown轉換為HTML, Markdown會使用多種不同的處理器。一種類型的處理器是內聯處理器。內聯處理器會匹配特定的內聯模式,如鏈接、反引號、粗體文本和帶下劃線的文本,并將它們轉換為HTML。
我們的Markdown擴展的主要目的是驗證和轉換鏈接。因此,我們最感興趣的內聯處理器是LinkInlineProcessor。這個處理器以[Haki的網站](https://hakibenito.com)的形式獲取markdown ,解析它并返回一個包含鏈接和文本的元組。
為了擴展該功能,我們擴展了LinkInlineProcessor并創建了一個Markdown.Extension, 我們用它來處理鏈接:
我們來將這段代碼分解一下::
DjangoUrlExtension擴展注冊了一個名為DjangoLinkInlineProcessor的內聯鏈接處理器。這個處理器將取代任何其他現有的鏈接處理器。
內聯處理器DjangoLinkInlineProcessor擴展了內置的LinkInlineProcessor,并在它處理的每個鏈接上調用clean_link函數。
clean_link函數接收一個鏈接和一個域名,并返回一個轉換后的鏈接。這就是我們要插入我們的實現的地方。
如何獲得網站域名
要識別到你自己網站的鏈接,你必須知道你的網站的域名。如果你正在使用Django的sites框架,那么你可以使用它來獲取當前域名。
我沒有把它包含在我的實現中,因為我們沒有使用sites框架。相反,我們在Django設置中設置了一個變量。
獲取當前域名的另一種方法是使用HttpRequest對象。如果內容只在你自己的站點中被編輯,你可以嘗試從請求對象中插入站點域名。這可能需要對你的實現進行一些更改。
要使用該擴展,請在初始化一個新的Markdown實例時添加它:
太好了,這個擴展已經被使用了,我們準備進入有趣的部分了!
既然我們得到了在所有鏈接上調用clean_link的擴展,那我們可以來實現我們的驗證和轉換邏輯。
要開始工作,我們將從一個簡單的驗證開始。mailto鏈接對于使用預定義的收件人地址、主題甚至消息正文打開用戶的電子郵件客戶端非常有用。
一個常見的mailto鏈接是這樣的:
這個鏈接將打開你的電子郵件客戶端,并設置成撰寫一封主題行為“我需要幫助!”的新電子郵件給“support@service.com”。
mailto鏈接不一定非要包含電子郵件地址。如果你看一看這篇文章底部的“分享”按鈕,你會發現像這樣的一個mailto鏈接:
這個mailto鏈接沒有包含收件人,僅包含了主題行和消息正文。
既然我們已經很好地理解了mailto鏈接是什么樣子的,我們就可以向clean_link函數添加第一個驗證:
為了驗證mailto鏈接,我們向clean_link中添加了以下代碼:
檢查鏈接是否以mailto:開頭,以識別相關鏈接。
使用正則表達式將鏈接分割到它的組件。
從mailto鏈接中刪除實際的電子郵件地址,并使用Django的EmailValidator驗證它。
注意,我們還添加了一種名為InvalidMarkdown的新異常類型。我們定義了自己的自定義異常類型,以將它與markdown本身所引發的其他錯誤區分開來。
自定義錯誤類
我曾經寫過關于自定義錯誤類的文章,為什么它們是有用的,以及你什么時候應該使用它們。
在我們繼續之前,讓我們添加一些測試,看看它的實際效果:
太棒了!按預期的運行了。
既然我們已經了解了mailto鏈接,我們也可以處理其他類型的鏈接:
外部鏈接
我們的Django應用程序外部的鏈接。
必須包含一個頁面跳轉協議(scheme):http或https。
理想情況下,我們還希望確保這些鏈接沒有被破壞,但我們現在不會這樣做。
內部鏈接
到我們的Django應用程序中的頁面的鏈接。
鏈接必須是相對的:這將允許我們在不同環境之間移動內容。
使用Django的URL名稱而不是一個URL路徑:這將允許我們安全地來回移動視圖,而不必擔心markdown內容中的失效鏈接。
鏈接可能包含查詢參數(?)和片段(#)。
SEO
從SEO的角度來看,公共URL不應該改變。當他們這樣做的時候,你應該使用重定向正確地處理它,否則你可能會受到搜索引擎的懲罰。
有了這個需求列表,我們就可以開始工作了。
解析URL名稱
要鏈接到內部頁面,我們希望編寫者提供一個URL名稱,而不是URL路徑。例如,假設我們有這個視圖:
這個頁面的URL路徑是https://example.com/, URL名稱是home。我們想要在我們的markdown鏈接中使用這個URL名稱home,就像這樣:
這將渲染到:
我們還想支持查詢參數和散列:
這將渲染到以下HTML:
在使用URL名稱時,如果我們更改了URL路徑,內容中的鏈接將不會被破壞。要檢查作者提供的href是否是一個有效的url_name,我們可以嘗試reverse它:
URL名稱“home”指向URL路徑“/”。當沒有匹配項時,將會引發一個異常:
在我們繼續之前,當URL名稱包含查詢參數或散列時,會發生什么:
這是有意義的,因為查詢參數和散列不是URL名稱的一部分。
要使用reverse并支持查詢參數和散列,我們首先需要清除值。然后,檢查它是一個有效的URL名稱,并返回包含查詢參數和散列的URL路徑,如果提供了的話:
這個代碼段使用一個正則表達式來以?或#的出現對href進行分割,并返回各部分。
請確保它可以工作:
太了不起了!作者們現在可以在Markdown中使用URL名稱了。它們還可以包括要添加到該URL的查詢參數和片段。
處理外部鏈接
要正確處理外部鏈接,我們需要檢查兩件事:
1.外部鏈接總是提供一個跳轉協議,http:或者https:。
2.阻止到我們自己網站的絕對鏈接。內部鏈接應該使用URL名稱。
到目前為止,我們已經處理了URL名稱和mailto鏈接。如果我們通過了這兩個檢查,這意味著href是一個URL。讓我們從檢查鏈接是否是鏈接到我們自己的網站開始:
函數urlparse會返回一個命名元組,該元組包含URL的不同部分。如果netloc屬性等于site_domain,那么該鏈接就確實是一個內部鏈接。
如果URL實際上是內部的,我們就需要終止。但是,請記住,作者們不一定是技術人員,因此我們希望幫助他們,并提供一個有用的錯誤消息。我們要求該內部鏈接使用URL名稱而不是URL路徑,所以最好讓作者們知道他們提供的路徑的URL名稱。
要獲得一個URL路徑的URL名稱,Django為我們提供了一個名為resolve的函數:
當找到匹配項時,resolve會返回一個ResolverMatch對象,其中包含URL名稱和其他信息。當沒有找到匹配項時,它就會引發一個錯誤:
這實際上就是Django在底層所做的工作,用來確定在一個新請求到來時執行哪個視圖函數。
為了給作者們提供更好的錯誤信息,我們可以使用來自ResolverMatch對象的URL名稱:
當我們識別出內部鏈接時,我們要處理兩種情況:
我們沒有識別出這個URL:這個URL很可能是不正確的。請作者檢查該URL是否有錯誤。
我們識別出了這個URL: 這個URL是正確的,所以就告訴作者應該使用什么URL名稱。
我們來實際地看一下它:
漂亮!外部鏈接被接受,內部鏈接被拒絕,并帶有一個有用的消息。
要求跳轉協議
我們要做的最后一件事是確保外部鏈接包含一個跳轉協議,要么是http:,要么是https:。讓我們將這最后一部分添加到函數clean_link:
使用解析后的URL,我們可以很容易地檢查跳轉協議。讓我們確保它正在工作:
我們向這個函數提供了一個沒有跳轉協議的鏈接,但是它運行失敗了,并顯示了一條有用的消息。太酷了!
這是clean_link函數的全部代碼:
要了解所有這些特性的一個實際用例是什么樣子的,請看下面的內容:
這將產生以下HTML:
不錯!
我們現在有一個很不錯的擴展,它可以驗證和轉換Markdown文檔中的鏈接!現在,在不同環境之間移動文檔和保持內容整潔要容易多了,最重要的是,可以保持正確和最新!
源碼
你可以在這個gist中找到全部源代碼。(地址:https://gist.github.com/hakib/73fccc340e855bb65f42197e298c0c7d )
本文中所描述的功能對我們很有用,但是你可能需要根據自己的需求對它進行調整。
如果你需要一些想法,那么除了這個擴展之外,我們還創建了一個markdown Preprocessor,它允許作者們在markdown中使用常量。例如,我們定義了一個名為SUPPORT_EMAIL的常量,我們像這樣使用它:
該預處理程序將用我們定義的文本替換字符串$SUPPORT_EMAIL,然后才渲染Markdown。
英文原文:https://hakibenita.com/django-markdown
譯者:Nothing
長文創作激勵計劃#
模板不僅僅是靜態的HTML,在渲染的過程中經常包含著一部分變量。在Django中,通過一個類似字典的對象context,把所有模板中需要用到的變量從view傳遞到template。比如搜索結果列表和推薦列表顯示樣式和顯示信息都一樣,此種情況下,若模板根據不同變量顯示不同數據的方式進行頁面的渲染,則需要創建不同的模板進行顯示,會做很多重復性的工作,并造成代碼的冗余和結構的繁雜。在模板中使用變量的便利遠不止此,例如還可以根據傳遞變量類型或者值得不同顯示不同的樣式和信息。
[!TIP]
與其它語言不同的是,在Django中,view中的變量不會自動在template中可用,必須通過view準確傳遞到template中,這樣設計的目的是為了避免破壞模板的命名空間。
和其它語言PHP、Vue中的用法一樣,Django的模板中的變量也是通過{{ }}進行表示,例如顯示變量name:{{ name }}。即使變量中包含一些特殊字符(如:<),也不需要擔心會影響顯示結果,因為Django的也會自動避免將其以HTML的形式進行顯示。
[!NOTE]
如果在模板中用到了沒有傳遞的變量,Django只會在相應位置不做任何顯示,而不會拋出異常。
示例:
1、以my_site項目為例,在Django應用myapp中創建templates文件夾,并在文件夾內創建welcome.html文件,其結構和內容如下:
示例結構
2、修改views.py文件,在其中添加welcome方法,其內容如下:
from django.http import HttpResponse
from django.shortcuts import render
# Create your views here.
def welcome(request):
name="World"
return render(request, "welcome.html" ,{"name":name})
3、在myapp中的urls.py進行配置,其內容如下:
from django.contrib import admin
from django.urls import path
from .views import welcome
urlpatterns = [
path('welcome/', welcome, name="welcome"),
]
4、修改my_site項目的urls.py文件,引用myapp中的urls.py,其內容如下:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path("", include("myapp.urls")),
]
5、通過python manage.py runserver命令運行項目,其顯示如下:
示例效果圖
該文章作為學習筆記進行分享和記錄,如有錯誤或其它觀點,請多多指教!??!
web開發中,經常需要顯示展示層次化的數據,比如文件目錄、產品分類目錄、組織結構等,這是典型的樹形數據結構。Django是web開發中使用非常廣泛的框架,那么你知道怎么使用Django模塊渲染樹形數據結構嗎?本文將會給出答案。
我們知道編程語言大多都有遞歸調用的能力,遍歷樹形數據結構往往都需要進行遞歸調用,而Django使用模板引擎進行頁面的渲染,而樹形結構數據往往都是從數據庫動態獲取的,嵌套的層級深度并不是確定的,因此Django模板必須能夠做到類似遞歸調用的能力才能做到渲染樹形數據結構。幸運地的是,Django模板引擎是支持遞歸引用的,接下來我們通過一個簡單的例子來詳細介紹Django模板如何通過include指令遞歸引用模塊文件,最終渲染出樹形目錄結構。這里不會從頭介紹Django如何創建項目,只針對關鍵文件進行講解,因此需要讀者具備一定的Django基礎。
一般情況下,樹形結構數據都是從數據庫動態生成的,為了突出重點,這里僅給出最終的樹形數據結構示例。我們把它寫在views.py文件中,tree_list為樹形數據,列表中的每一項包含id、name、parent_id、children,其中children為子節點,包含同樣的結構,可以一直嵌套下去。
from django.shortcuts import render
# Create your views here.
def index(request):
"""
tree_list數據結構形式:[
{
"id": xx,
"name": xxx,
"parent_id": xxx,
"children": [
{
"id": xx,
"name": xxx,
"parent_id": xxx,
"children": []
}
]
},
...
]
"""
context = {}
context["tree_list"] = [{
"id": 2,
"name": "類別A",
"parentId": None,
"children": [
{
"id": 8,
"name": "類別A1",
"parentId": 2,
"children": [
{
"id": 137,
"name": "類別A11",
"parentId": 8,
}
]
},
{
"id": 221,
"name": "類別A2",
"parentId": 2,
}
]
},
{
"id": 52,
"name": "類別B",
"parentId": None,
"children": [
{
"id": 54,
"name": "類別B1",
"parentId": 52,
"fileCount": 10,
"children": [
{
"id": 55,
"name": "類別B11",
"parentId": 54,
"children": [
{
"id": 56,
"name": "類別B111",
"parentId": 55,
"children": [
{
"id": 57,
"name": "類別B1111",
"parentId": 56,
"children": [
{
"id": 58,
"name": "類別B11111",
"parentId": 57,
}
]
}
]
}
]
}
]
}
]
},
{
"id": 53,
"name": "類別C",
"parentId": None,
"children": [
{
"id": 80,
"name": "類別C1",
"parentId": 53,
},
{
"id": 224,
"name": "類別C2",
"parentId": 53,
}
]
},
{
"id": 69,
"name": "類別D",
"parentId": None,
"children": [
{
"id": 70,
"name": "類別D1",
"parentId": 69,
"children": [
{
"id": 4,
"name": "類別D11",
"parentId": 70,
"children": [
{
"id": 51,
"name": "類別D111",
"parentId": 4,
}
]
}
]
},
{
"id": 91,
"name": "類別D2",
"parentId": 69,
},
{
"id": 102,
"name": "類別D3",
"parentId": 69,
},
{
"id": 113,
"name": "類別D4",
"parentId": 69,
},
{
"id": 121,
"name": "類別D5",
"parentId": 69,
},
{
"id": 136,
"name": "類別D6",
"parentId": 69,
},
{
"id": 140,
"name": "類別D7",
"parentId": 69,
"children": [
{
"id": 142,
"name": "類別D71",
"parentId": 140,
}
]
}
]
}
]
return render(request, 'demo/index.html', context)
templates/demo目錄下創建兩個模板文件index.html、children.html,index.html文件中關鍵部分的代碼為for循環指令包圍的部分,它負責遍歷上面提到的tree_list列表的每一項,也就是數據結構的第一級目錄,如果列表中的某一項children內容不為空,則執行指令{% include 'demo/children.html' with tree_list=item.children %},它的意思就相當于render(request, 'demo/children.html', item.children),也就是說插入當前項的子節點作為數據源渲染出的頁面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>django模板</title>
<style>
.flex {
display: flex;
}
.list-unstyled ul {
padding-left: 0;
list-style: none;
}
.tree {
padding: 0.3rem 1rem ;
background-color: #f5f5f5;
color: #333;
}
.tree li li {
padding-left: 0.5rem;
}
.tree li::before {
content: '\0203A';
opacity: 0;
}
.tree .expand::before {
content: '\0203A';
opacity: 1;
}
</style>
</head>
<body>
<h3>Django模板渲染樹形目錄示例</h3>
<div class="flex">
<div class="tree list-unstyled">
<ul>
{% for item in tree_list %}
{% if item.children %}
<li class="expand">
{{ item.name }}
{% include 'demo/children.html' with tree_list=item.children %}
</li>
{% else %}
<li>
{{ item.name }}
</li>
{% endif %}
{% endfor %}
</ul>
</div>
<div class="right"></div>
</div>
</body>
</html>
接下來看children.html,它看起來和前面是很類似的,只不過這里include指令中使用的模板就是自己本身,傳入的數據源逐層剝離出子節點,這就是和編程語言的遞歸是一樣的了,最終所有children節點都完全遍歷到并渲染出最終的html頁面,這樣就實現了渲染樹形結構數據。
<ul>
{% for item in tree_list %}
{% if item.children %}
<li class="expand">
{{ item.name }}
{% include 'demo/children.html' with tree_list=item.children %}
</li>
{% else %}
<li>
{{ item.name }}
</li>
{% endif %}
{% endfor %}
</ul>
最后渲染出的樹形目錄如下, index.html中寫了一點css改變了默認的樣式,你可以根據自己的需要使用成熟的UI框架來定制樹形目錄的樣式。本文到這里就結束了,希望能幫助到有需要的朋友,也歡迎大家多多關注我的公眾號【一點鑫得】,我將持續輸出有價值的內容。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。