用Publish建立部落格(三)——插件開發
如想獲得更好的閱讀效果可以通路我的部落格 www.fatbobman.com[1] 我的部落格也是用Publish建立的。
我們不僅可以利用Publish内置的接口來開發插件進行擴充,同時還可以使用Publish套件中其他的優秀庫(Ink、Plot、Sweep、Files、ShellOut等)來完成更多的創意。本文将通過幾個執行個體(添加标簽、增加屬性、用代碼生成内容、全文搜尋、指令行部署)在展示不同擴充手段的同時向大家介紹Publish套件中其他的優秀成員。在閱讀本文前,最好能先閱讀用Publish建立部落格(一)——入門[2]、用Publish建立部落格(二)——主題開發[3]。對Publish有個基本了解。本文篇幅較長,你可以選擇自己感興趣的實戰内容閱讀。
基礎
PublishingContext
在用Publish建立部落格(一)——入門[4]中我們介紹過Publish有兩個Content概念。其中
PublishingContext
作為根容器包含了你網站項目的全部資訊(
Site
、
Section
、
Item
、
Page
等)。在對Publish進行的大多數擴充開發時,都需要和
PublishingContext
打交道。不僅通過它來擷取資料,而且如果要對現有資料進行改動或者添加新的
Item
、
Page
時(在
Content
中采用不建立
markdown
檔案的方式)也必須要調用其提供的方法。比如
mutateAllSections
、
addItem
等。
Pipeline中的順序
Publish會逐個執行Pipeline中的
Step
,是以必須要在正确的位置放置
Step
和
Plugin
。比如需要對網站的所有資料進行彙總,則該處理過程應該放置在
addMarkdownFiles
(資料都被添加進
Content
)之後;而如果想添加自己的部署(
Deploy
),則應放置在生成所有檔案之後。下面會通過例子具體說明。
熱身
下面的代碼,是以放置在 Myblog
(第一篇中建立,并在第二篇中進行了修改)項目裡為例。
準備
請将
try Myblog().publish(withTheme: .foundation)
複制
換成
try Myblog().publish(using: [ .addMarkdownFiles(), //導入Content目錄下的markdown檔案,并解析添加到PublishingContent中 .copyResources(), //将Resource内容添加到Output中 .generateHTML(withTheme:.foundation ), //指定模闆 .generateRSSFeed(including: [.posts]), //生成RSS .generateSiteMap() //生成Site Map])
複制
建立Step
我們先通過官方的一個例子了解一下
Step
的建立過程。目前導航菜單的初始狀态:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAjM2EzLcd3LcJzLcJzdllmVldWYtl2Pn5GcuMTYjNWN2ITO3UGN2EGNmFDOjFjM1kTOhhDZ2IGMiZGOvwFO0IDM1IzNtUGall3LcVmdhNXLwRHdo9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.png)
image-20210203121214511
下面的代碼将改變SectionID。
//目前的Section設定enum SectionID: String, WebsiteSectionID { // Add the sections that you want your website to contain here: case posts //rawValue 将影響該Section對應的Content的目錄名。目前的目錄為posts case about //如果改成 case abot = "關于" 則目錄名為“關于”,是以通常會采用下方更改title的方法 }//建立Stepextension PublishingStep where Site == Myblog { static func addDefaultSectionTitles() -> Self { //name為step名稱,在執行該Step時在控制台顯示 .step(named: "Default section titles") { context in //PublishingContent執行個體 context.mutateAllSections { section in //使用内置的修改方法 switch section.id { case .posts: section.title = "文章" //修改後的title,将顯示在上方的Nav中 case .about: section.title = "關于" } } } }}
複制
将
Step
添加到
main.swift
的
pipeline
中:
.addMarkdownFiles(), .addDefaultSectionTitles(), .copyResources(),
複制
添加該
Step
後的導航菜單:
image-20210203123545306
Pipeline中的位置
如果将
addDefaultSectionTitles
放置在
addMarkdownFiles
的前面,會發現
posts
的title變成了
image-20210203123440066
這是因為,目前的
Content--posts
目錄中有一個
index.md
檔案。
addMarkdownFiles
會使用從該檔案中解析的
title
來設定
posts
的
Section.title
。解決的方法有兩種:
1.向上面那樣将
addDefaultSectionTitles
放置在
addMarkdownFiles
的後面2.删除掉
index.md
等效的Plugin
在用Publish建立部落格(一)——入門[5]中提過
Step
和
Plugin
在作用上是等效的。上面的代碼用
Plugin
的方式編寫是下面的樣子:
extension Plugin where Site == Myblog{ static func addDefaultSectionTitles() -> Self{ Plugin(name: "Default section titles"){ context in context.mutateAllSections { section in switch section.id { case .posts: section.title = "文章" case .about: section.title = "關于" } } } }}
複制
在
Pipeline中
使用下面的方式添加:
.addMarkdownFiles(), .copyResources(), .installPlugin(.addDefaultSectionTitles()),
複制
它們的效果完全一樣。
實戰1:添加Bilibili标簽解析
Publish使用Ink[6]作為
markdown
的解析器。
Ink
作為Publish套件的一部分,着重點在
markdown
到
HTML
的高效轉換。它讓使用者可以通過添加
modifier
的方式,對
markdown
轉換
HTML
的過程進行定制和擴充。
Ink
目前并不支援全部的
markdonw
文法,太複雜的它不支援(而且文法支援目前是鎖死的,如想擴充必須fork
Ink
代碼,自行添加)。
在本例中我們嘗試為如下
markdown
的
codeBlock
文法添加新的轉義功能:
image-20210203142914881
aid
為B站視訊的
aid
号碼,
danmu
為
彈幕
開關
讓我們首先建立一個
Ink
的
modifier
/*每個modifier對應一個markdown文法類型。目前支援的類型有: metadataKeys,metadataValues,blockquotes,codeBlocks,headings horizontalLines,html,images,inlineCode,links,lists,paragraphs,tables*/var bilibili = Modifier(target: .codeBlocks) { html, markdown in // html為Ink預設的HTML轉換結果,markdown為該target對應的原始内容 // firstSubstring是Publish套件中的Sweep提供的快速配對方法. guard let content = markdown.firstSubstring(between: .prefix("```bilibili\n"), and: "\n```") else { return html } var aid: String = "" var danmu: Int = 1 // scan也是Sweep中提供另一種配對擷取方式,下面的代碼是擷取aid:和換行之間的内容 content.scan(using: [ Matcher(identifier: "aid: ", terminator: "\n", allowMultipleMatches: false) { match, _ in aid = String(match) }, Matcher(identifiers: ["danmu: "], terminators: ["\n", .end], allowMultipleMatches: false) { match, _ in danmu = match == "true" ? 1 : 0 }, ]) //modifier的傳回值為HTML代碼,本例中我們不需要使用Ink的預設轉換,直接全部重寫 //在很多的情況下,我們可能隻是在預設轉換的html結果上做出一定的修改即可 return """ <div style="position: relative; padding: 30% 45% ; margin-top:20px;margin-bottom:20px"> <iframe style="position: absolute; width: 100%; height: 100%; left: 0; top: 0;" src="https://player.bilibili.com/player.html?aid=\(aid)&page=1&as_wide=1&high_quality=1&danmaku=\(danmu)" frameborder="no" scrolling="no"></iframe> </div> """}
複制
通常情況下,我們會将上面的
modifier
包裹在一個
Plugin
中,通過
installPlugin
來注入,不過現在我們直接建立一個新的
Step
專門來加載
modifier
extension PublishingStep{ static func addModifier(modifier:Modifier,modifierName name:String = "") -> Self{ .step(named: "addModifier \(name)"){ context in context.markdownParser.addModifier(modifier) } }}
複制
現在就可以在
main.swift
的
Pipeline
中添加了
.addModifier(modifier: bilibili,modifierName: "bilibili"), //bilibili視訊.addMarkdownFiles(),
複制
modifier
在添加後并不會立即使用,當Pipeline執行到
addMarkdownFiles
對
markdown
檔案進行解析時才會調用。是以
modifier
的位置一定要放在解析動作的前面。
Ink
允許我們添加多個
modifier
,即使是同一個
target
。是以盡管我們上面的代碼是占用了對
markdown
的
codeBlocks
的解析,但隻要我們注意順序,就都可以和平共處。比如下面:
.installPlugin(.highlightJS()), //文法高亮插件,也是采用modifier方式,對應的也是codeBlock .addModifier(modifier: bilibili), //在這種狀況下,bilibili必須在highlightJS下方。
複制
Ink
将按照
modifier
的添加順序來調用。添加該插件後的效果
publish-3-bilibili-videodemo
可以直接在https://www.fatbobman.com/video/檢視示範效果。
上面代碼在我提供的範例模闆[7]中可以找到
通過
modifier
擴充
markdown
到
HTML
的轉義是Publish中很常見的一種方式。幾乎所有的文法高亮、
style
注入等都利用了這個手段。
實戰2:為Tag添加計數屬性
在Publish中,我們隻能擷取
allTags
或者每個
Item
的
tags
,但并不提供每個
tag
下到底有幾個
Item
。本例我們便為
Tag
增加
count
屬性。
//由于我們并不想在每次調用tag.count的時候進行計算,是以一次性将所有的tag都提前計算好//計算結果通過類屬性或結構屬性來儲存,以便後面使用struct CountTag{ static var count:[Tag:Int] = [:] static func count<T:Website>(content:PublishingContext<T>){ for tag in content.allTags{ //将計算每個tag下對應的item,放置在count中 count[tag] = content.items(taggedWith: tag).count } }}extension Tag{ public var count:Int{ CountTag.count[self] ?? 0 }}
複制
建立一個調用在
Pipeline
中激活計算的
Plugin
extension Plugin{ static func countTag() -> Self{ return Plugin(name: "countTag"){ content in return CountTag.count(content: content) } }}
複制
在
Pipeline
中加入
.installPlugin(.countTag()),
複制
現在我們就可在主題中直接通過
tag.count
來擷取所需資料了,比如在主題方法
makeTagListHTML
中:
.forEach(page.tags.sorted()) { tag in .li( .class(tag.colorfiedClass), //tag.colorfieldClass 也是通過相同手段增加的屬性,在文章最後會有該插件的擷取位址 .a( .href(context.site.path(for: tag)), .text("\(tag.string) (\(tag.count))") ) ) }
複制
顯示結果
image-20210203104002714
實戰3:将文章按月份彙總
在Publish建立部落格(二)——主題開發[8]中我們讨論過目前Publish的主題支援的六種頁面,其中有對
Item
以及
tag
的彙總頁面。本例示範一下如何用代碼建立主題不支援的其他頁面類型。
本例結束時,我們将讓Publish能夠自動生成如下的頁面:
publish-3-dateAchive
//建立一個Stepextension PublishingStep where Site == FatbobmanBlog{ static func makeDateArchive() -> Self{ step(named: "Date Archive"){ content in var doc = Content() /*建立一個Content,此處的Content是裝載頁面内容的,不是PublishingContext Publish在使用addMarkdownFiles導入markdown檔案時,會為每個Item或Page建立Content 由于我們是使用代碼直接建立,是以不能使用markdown文法,必須直接使用HTML */ doc.title = "時間線" let archiveItems = dateArchive(items: content.allItems(sortedBy: \.date,order: .descending)) //使用Plot生成HTML,第二篇文章有Plot的更多介紹 let html = Node.div( .forEach(archiveItems.keys.sorted(by: >)){ absoluteMonth in .group( .h3(.text("\(absoluteMonth.monthAndYear.year)年\(absoluteMonth.monthAndYear.month)月")), .ul( .forEach(archiveItems[absoluteMonth]!){ item in .li( .a( .href(item.path), .text(item.title) ) ) } ) ) } ) //渲染成字元串 doc.body.html = html.render() //本例中直接生成了Page,也可以生成Item,Item需在建立時指定SectionID以及Tags let page = Page(path: "archive", content:doc) content.addPage(page) } } //對Item按月份彙總 fileprivate static func dateArchive(items:[Item<Site>]) -> [Int:[Item<Site>]]{ let result = Dictionary(grouping: items, by: {$0.date.absoluteMonth}) return result }}extension Date{ var absoluteMonth:Int{ let calendar = Calendar.current let component = calendar.dateComponents([.year,.month], from: self) return component.year! * 12 + component.month! }}extension Int{ var monthAndYear:(year:Int,month:Int){ let month = self % 12 let year = self / 12 return (year,month) }}
複制
由于該
Step
需要對
PublishingContent
中的所有
Item
進行彙總,是以在
Pipeline
中應該在所有内容都裝載後再執行
.addMarkdownFiles(),.makeDateArchive(),
複制
可以通路https://www.fatbobman.com/archive/檢視示範。上面的代碼可以在Github[9]下載下傳。
實戰4:給Publish添加搜尋功能
誰不想讓自己的Blog支援全文搜尋呢?對于多數的靜态頁面來說(比如github.io),是很難依靠服務端來實作的。
下面的代碼是在參照local-search-engine-in-Hexo[10]的方案實作的。
local-search-engin
提出的解決方式是,将網站的全部需檢索文章内容生成一個
xml
或
json
檔案。使用者搜尋前,自動從服務端下載下傳該檔案,通過javascript代碼在本地完成搜尋工作。javascripte代碼[11]使用的是
hexo-theme-freemind
建立的。另外 Liam Huang的這篇部落格[12]也給了我很大的幫助。
最後實作的效果是這樣的:
建立一個
Step
用來在
Pipeline
的末端生成用于檢索的
xml
檔案。
extension PublishingStep{ static func makeSearchIndex(includeCode:Bool = true) -> PublishingStep{ step(named: "make search index file"){ content in let xml = XML( .element(named: "search",nodes:[ //之是以将這個部分分開寫,是因為有時候編譯器對于複雜一點的DSL會TimeOut //提示編譯時間過長。分開則完全沒有問題。這種情況在SwiftUI中也會遇到 .entry(content:content,includeCode: includeCode) ]) ) let result = xml.render() do { try content.createFile(at: Path("/Output/search.xml")).write(result) } catch { print("Failed to make search index file error:\(error)") } } }}extension Node { //這個xml檔案的格式是local-search-engin确定的,這裡使用Plot把網站内容轉換成xml static func entry<Site: Website>(content:PublishingContext<Site>,includeCode:Bool) -> Node{ let items = content.allItems(sortedBy: \.date) return .forEach(items.enumerated()){ index,item in .element(named: "entry",nodes: [ .element(named: "title", text: item.title), .selfClosedElement(named: "link", attributes: [.init(name: "href", value: "/" + item.path.string)] ), .element(named: "url", text: "/" + item.path.string), .element(named: "content", nodes: [ .attribute(named: "type", value: "html"), //為Item增加了htmlForSearch方法 //由于我的Blog的文章中包含不少代碼範例,是以讓使用者選擇是否在檢索檔案中包含Code。 .raw("<![CDATA[" + item.htmlForSearch(includeCode: includeCode) + "]]>") ]), .forEach(item.tags){ tag in .element(named:"tag",text:tag.string) } ]) } }}
複制
我需要再稱贊一下Plot[13],它讓我非常輕松地完成了
xml
的建立工作。
extension Item{ public func htmlForSearch(includeCode:Bool = true) -> String{ var result = body.html result = result.replacingOccurrences(of: "]]>", with: "]>") if !includeCode { var search = true var check = false while search{ check = false //使用Ink來擷取配對内容 result.scan(using: [.init(identifier: "<code>", terminator: "</code>", allowMultipleMatches: false, handler: { match,range in result.removeSubrange(range) check = true })]) if !check {search = false} } } return result }}
複制
建立
搜尋框
和
搜尋結果容器
:
//裡面的id和class由于要和javascript配合,需保持現狀extension Node where Context == HTML.BodyContext { //顯示搜尋結果的Node public static func searchResult() -> Node{ .div( .id("local-search-result"), .class("local-search-result-cls") ) } //顯示搜尋框的Node public static func searchInput() -> Node{ .div( .form( .class("site-search-form"), .input( .class("st-search-input"), .attribute(named: "type", value: "text"), .id("local-search-input"), .required(true) ), .a( .class("clearSearchInput"), .href("javascript:"), .onclick("document.getElementById('local-search-input').value = '';") ) ), .script( .id("local.search.active"), .raw( """ var inputArea = document.querySelector("#local-search-input"); inputArea.onclick = function(){ getSearchFile(); this.onclick = null } inputArea.onkeydown = function(){ if(event.keyCode == 13) return false } """ ) ), .script( .raw(searchJS) //完整的代碼後面可以下載下傳 ) ) }}
複制
本例中,我将搜尋功能設定在标簽清單的頁面中(更多資訊檢視主題開發[14]),是以在
makeTagListHTML
中将上面兩個
Node
放到自己認為合适的地方。
由于搜尋用的javascript需要用到
jQuery
,是以在
head
中添加了jQuery的引用(通過覆寫了
head
,目前隻為
makeTagListHTML
添加了引用)。
在Pipeline中加入
.makeSearchIndex(includeCode: false), //根據自己需要決定是否索引文章中的代碼
複制
完整的代碼可以在Github[15]下載下傳。
實戰5:部署
最後這個執行個體略微有點牽強,主要是為了介紹Publish套件中的另外一員ShellOut[16]。
ShellOut
是一個很輕量的庫,它的作用是友善開發者從Swift代碼中調用腳本或指令行工具。在Publish中,使用
publish deploy
進行Github部署的代碼便使用了這個庫。
import Foundationimport Publishimport ShellOutextension PublishingStep where Site == FatbobmanBlog{ static func uploadToServer() -> Self{ step(named: "update files to fatbobman.com"){ content in print("uploading......") do { try shellOut(to: "scp -i ~/.ssh/id_rsa -r ~/myBlog/Output [email protected]:/var/www") //我是采用scp部署的,你可以用任何你習慣的方式 } catch { print(error) } } }}
複制
在
main.swift
添加:
var command:String = ""if CommandLine.arguments.count > 1 { command = CommandLine.arguments[1]}try MyBlog().publish( .addMarkdownFiles(), ... .if(command == "--upload", .uploadToServer())]
複制
執行
swift run MyBlog --upload
即可完成網站生成+上傳(MyBlog為你的項目名稱)
其他的插件資源
目前Publish的插件和主題在網際網路上能夠找到的并不很多,主要集中在Github的#publish-plugin[17]上。
其中使用量比較大的有:
•SplashPublishPlugin[18] 代碼高亮•HighlightJSPublishPlugin[19] 代碼高亮•ColorfulTagsPublishPlugin[20] 給Tag添加顔色
如果想在Github上分享你制作的plugin,請務必打上
publish-plugin
标簽以便于大家查找
最後
就在即将完成這篇稿件的時候,手機上收到了
趙英俊
因病過世的新聞。英年早逝,令人唏噓。回想到自己這些年經曆的治療過程,由衷地感覺平靜、幸福的生活真好。
在使用Publish的這些天,讓我找到了裝修房子的感覺。雖然不一定做的多好,但網站能按自己的想法逐漸變化真是樂趣無窮。
引用連結
[1]
www.fatbobman.com: http://www.fatbobman.com
[2]
用Publish建立部落格(一)——入門: https://www.fatbobman.com/posts/publish-1/
[3]
用Publish建立部落格(二)——主題開發: https://www.fatbobman.com/posts/publish-2/
[6]
Ink: https://github.com/JohnSundell/Ink
[7]
範例模闆: https://github.com/fatbobman/PublishThemeForFatbobmanBlog
[9]
Github: https://github.com/fatbobman/Archive_Article_By_Month_Publish_Plugin
[10]
local-search-engine-in-Hexo: https://github.com/wzpan/hexo-generator-search
[11]
javascripte代碼: https://github.com/wzpan/hexo-theme-freemind/blob/master/source/js/search.js
[12]
部落格: https://liam.page/2017/09/21/local-search-engine-in-Hexo-site/
[13]
Plot: https://github.com/JohnSundell/Plot
[14]
主題開發: https://www.fatbobman.com/posts/publish-2/
[15]
Github: https://github.com/fatbobman/local-search-engine-for-Publish
[16]
ShellOut: https://github.com/JohnSundell/ShellOut
[17]
Github的#publish-plugin: https://github.com/topics/publish-plugin?l=swift
[18]
SplashPublishPlugin: https://github.com/JohnSundell/SplashPublishPlugin
[19]
HighlightJSPublishPlugin: https://github.com/alex-ross/HighlightJSPublishPlugin
[20]
ColorfulTagsPublishPlugin: https://github.com/Ze0nC/ColorfulTagsPublishPlugin