天天看點

用Publish建立部落格(三)——插件開發用Publish建立部落格(三)——插件開發

用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

的建立過程。目前導航菜單的初始狀态:

用Publish建立部落格(三)——插件開發用Publish建立部落格(三)——插件開發

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

後的導航菜單:

用Publish建立部落格(三)——插件開發用Publish建立部落格(三)——插件開發

image-20210203123545306

Pipeline中的位置

如果将

addDefaultSectionTitles

放置在

addMarkdownFiles

的前面,會發現

posts

的title變成了

用Publish建立部落格(三)——插件開發用Publish建立部落格(三)——插件開發

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

文法添加新的轉義功能:

用Publish建立部落格(三)——插件開發用Publish建立部落格(三)——插件開發

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建立部落格(三)——插件開發用Publish建立部落格(三)——插件開發

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))")               )          )  }           

複制

顯示結果

用Publish建立部落格(三)——插件開發用Publish建立部落格(三)——插件開發

image-20210203104002714

實戰3:将文章按月份彙總

在Publish建立部落格(二)——主題開發[8]中我們讨論過目前Publish的主題支援的六種頁面,其中有對

Item

以及

tag

的彙總頁面。本例示範一下如何用代碼建立主題不支援的其他頁面類型。

本例結束時,我們将讓Publish能夠自動生成如下的頁面:

用Publish建立部落格(三)——插件開發用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