Not only can you use Publish’s built-in interfaces to develop plugins for extensions, but you can also use the other great libraries in the Publish suite (Ink, Plot, Sweep, Files, ShellOut, etc.) to do more creative things. This article introduces you to some of the other great members of the Publish suite while showing you some of the different extensions (adding tags, adding attributes, generating content with code, full-text search, command-line deployment). Before reading this article, it’s a good idea to read Publish blog (1) – Getting Started and Publish Blog (2) – Topic development. Basic knowledge of Publish. This article is a long one, so you can choose what you would like to read.

basis

PublishingContext

Publish has two Content concepts that we introduced in Publish Blogging (Part 1: Getting Started). The PublishingContext is the root container that contains all the information about your website project (Site, Section, Item, Page, etc.). When developing most extensions to Publish, you will need to work with a PublishingContext. Not only do we use it to get data, but we also have to call the methods provided if we want to change existing data or add a new Item or Page (by not creating a Markdown file in the Content). Such as mutateAllSections, addItem, etc.

Sequence in Pipeline

Publish executes each Step in the Pipeline, so you must put the Step and Plugin in the right place. For example, if you need to summarize all the data on your website, this process should be placed after addMarkdownFiles (where all the data is added to Content). If you want to add your own Deploy, do so after all the files have been generated. Examples will be given below.

Warm up

The following code, for example, is placed in the Myblog project (created in the first post and modified in the second).

To prepare

Please send

try Myblog().publish(withTheme: .foundation)
Copy the code

Switch to

try Myblog().publish(using: [
    .addMarkdownFiles(), // Import the Markdown file from the Content directory and parse it into PublishingContent
    .copyResources(), // Add Resource content to Output
    .generateHTML(withTheme:.foundation ), // Specify the template
    .generateRSSFeed(including: [.posts]), / / generated RSS
    .generateSiteMap() // Generate Site Map
])
Copy the code

Create a Step

Let’s take a look at the Step creation process with an official example. The initial state of the current navigation menu:

The following code changes the SectionID.

// Current Section Settings
enum SectionID: String.WebsiteSectionID {
        // Add the sections that you want your website to contain here:
        case posts //rawValue will affect the directory name of the Section's corresponding Content. The current directory is posts
        case about // If you change case abot = "about" then the directory name is "about", so you usually change the title method below
 }

/ / create a Step
extension PublishingStep where Site= =Myblog {
    static func addDefaultSectionTitles(a) -> Self {
      // Name is the step name, which is displayed on the console when the step is executed
        .step(named: "Default section titles") { context in / / PublishingContent instance
            context.mutateAllSections { section in // Use the built-in modification method
                switch section.id {
                case .posts:
                    section.title = "Article"  // The modified title will be displayed in the Nav above
                case .about:
                    section.title = "About" 
                }
            }
        }
    }
}
Copy the code

Add Step to main.swift pipeline:

    .addMarkdownFiles(),
    .addDefaultSectionTitles(), 
    .copyResources(),
Copy the code

The navigation menu after adding the Step:

Position in Pipeline

If you place addDefaultSectionTitles in front of addMarkdownFiles, you’ll see that the title of posts becomes

That’s because the current Content–posts directory has an index.md file. AddMarkdownFiles will set posts’ section.title using the title parsed from that file. There are two ways to solve this problem:

  1. I’m going to do that up hereaddDefaultSectionTitlesPlaced in theaddMarkdownFilesThe back of the
  2. Delete theindex.md

The equivalent of the Plugin

As mentioned in Publish Blog Creation (part 1) – Getting Started, Step and Plugin are equivalent. The above code, written as a Plugin, looks like this:

extension Plugin where Site= =Myblog{
    static func addDefaultSectionTitles(a) -> Self{
        Plugin(name:  "Default section titles"){
            context in
            context.mutateAllSections { section in
                switch section.id {
                case .posts:
                    section.title = "Article"
                case .about:
                    section.title = "About"
                }
            }
        }
    }
}
Copy the code

Add to Pipeline using the following method:

    .addMarkdownFiles(),
    .copyResources(),
    .installPlugin(.addDefaultSectionTitles()),
Copy the code

They have exactly the same effect.

Actual Combat 1: Add Bilibili tag parsing

Publish uses Ink as the parser for MarkDown. Ink, as part of the Publish suite, focuses on efficient markDown to HTML conversion. It allows users to customize and extend the MarkDown process of transforming HTML by adding modifier. Ink currently does not support all markdonw syntax, it is too complex to support (and syntax support is currently locked, if you want to expand by forkInk code, add it yourself).

In this example we try to add new escape functionality to the following MarkDown codeBlock syntax:

Aid is the AID number of the video in station B, and DANMU is the barrage switch

Let’s first create an Ink modifier

/* Each modifier corresponds to a MarkDown syntax type. Currently, the following types are supported: metadataKeys,metadataValues,blockquotes,codeBlocks,headings horizontalLines,html,images,inlineCode,links,lists,paragraphs,tables */
var bilibili = Modifier(target: .codeBlocks) { html, markdown in
     // HTML is the default HTML transformation result of Ink, and markDown is the original content corresponding to the target
     // firstSubstring is a quick pairing method of Publish suite Sweep.
    guard let content = markdown.firstSubstring(between: .prefix("```bilibili\n"), and: "\n` ` `") else {
        return html
    }
    var aid: String = ""
    var danmu: Int = 1
    // Scan also provides another pair fetching method of Sweep. The following code retrives the content between AID: and the newline
    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},])// The return value of the modifier is HTML code, in this case we do not need to use Ink's default conversion, directly rewrite all
    // In many cases, we may just make some changes to the HTML result of the default transformation
    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>
        """
}
Copy the code

Normally, we would wrap the above modifier in a Plugin and inject it with installPlugin, but now let’s create a new Step specifically to load the modifier

extension PublishingStep{
    static func addModifier(modifier:Modifier.modifierName name:String = "") -> Self{
        .step(named: "addModifier \(name)"){ context in
            context.markdownParser.addModifier(modifier)
        }
    }
}
Copy the code

It is now ready to add to the Main.swift Pipeline

.addModifier(modifier: bilibili,modifierName: "bilibili"), / / bilibili video
.addMarkdownFiles(),
Copy the code

The modifier will not be used immediately after the addition, when Pipeline execution to addMarkdownFiles to parse the MarkDown file will be called. Therefore, the position of the modifier must be placed in front of the resolution action.

Ink allows us to add multiple modifiers, even for the same target. So even though our code above is taking up the parsing of Markdown’s codeBlocks, as long as we pay attention to the order, we can all live peacefully together. For example:

 .installPlugin(.highlightJS()), // The syntax highlighting plugin is also used in the modifier mode, the corresponding is also codeBlock
 .addModifier(modifier: bilibili), // In this case, bilibili must be under highlightJS.
Copy the code

Ink will be invoked in the order in which modifier was added. The effect of adding the plug-in

You can check out the demo at www.fatbobman.com/video/.

The above code can be found in the sample template I provided

Escaping MarkDown to HTML via modifier is a common way in Publish. Almost all syntax highlighting, style injection, and so on take advantage of this.

Actual Practice 2: Add a count attribute to the Tag

In Publish, we can only get the allTags or the tags for each Item, but we don’t provide the exact number of items under each tag. In this example, we add the count attribute to the Tag.

// Since we don't want to count each time tag.count is called, all tags are counted in advance at once
// The result of the calculation is saved by class attributes or structure attributes for later use
struct CountTag{
    static var count:[Tag:Int] = [:]
    static func count<T:Website> (content:PublishingContext<T>){
        for tag in content.allTags{
          // Count the corresponding item under each tag and place it in the count
            count[tag] =  content.items(taggedWith: tag).count
        }
    }
}

extension Tag{
    public var count:Int{
        CountTag.count[self] ?? 0}}Copy the code

Create a Plugin that calls the active computation in the Pipeline

extension Plugin{
    static func countTag(a) -> Self{
        return Plugin(name: "countTag"){ content in
            return CountTag.count(content: content)
        }
    }
}
Copy the code

Add to Pipeline

.installPlugin(.countTag()),
Copy the code

Now we can get the data we need directly from tag.count in the topic, such as in the topic method makeTagListHTML:

.forEach(page.tags.sorted()) { tag in
       .li(
       .class(tag.colorfiedClass), // Tag. ColorfieldClass is added in the same way. At the end of this article, you'll see the address for the plugin
              .a(
               .href(context.site.path(for: tag)),
               .text("\(tag.string) (\(tag.count))")))}Copy the code

According to the results

Practice 3: Summarize articles by month

In Publish Creation Blog (ii) – Topic Development we discussed the six pages currently supported by Publish’s topics, including a summary page for Item and tag. This example demonstrates how to use code to create additional page types that are not supported by the theme.

At the end of this example, we’ll have Publish automatically generate the following pages:

// Create a Step
extension PublishingStep where Site= =FatbobmanBlog{
    static func makeDateArchive(a) -> Self{
        step(named: "Date Archive"){ content in
            var doc = Content(a)PublishingContext Publish when importing markDown files using addMarkdownFiles Content is created for each Item or Page. Since we are creating it directly in code, we cannot use markdown syntax. We must use HTML */ directly
            doc.title = "Timeline" 
            let archiveItems = dateArchive(items: content.allItems(sortedBy: \.date,order: .descending))
             // Generate HTML using Plot. The second article has more on Plot
            let html = Node.div(
                .forEach(archiveItems.keys.sorted(by: >)){ absoluteMonth in
                    .group(
                        .h3(.text("\(absoluteMonth.monthAndYear.year)years\(absoluteMonth.monthAndYear.month)Month. "")),
                        .ul(
                            .forEach(archiveItems[absoluteMonth]!){ item in
                                .li(
                                    .a(
                                        .href(item.path),
                                        .text(item.title)
                                    )
                                )
                            }
                        )
                    )
                }
            )
            // Render to a string
            doc.body.html = html.render()
            // In this example, we can generate the Page directly, or we can generate the Item, which must be created with the SectionID and Tags specified
            let page = Page(path: "archive", content:doc)
            content.addPage(page)
        }
    }
    // Summarize items by month
    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)
    }
}

Copy the code

Since the PublishingContent Step is required to aggregate all items in the PublishingContent, it should be executed in the Pipeline after all items have been loaded

.addMarkdownFiles(),
.makeDateArchive(),
Copy the code

Can visit www.fatbobman.com/archive/ to check the demo. The above code is available for download on Github.

Practice 4: Add search to Publish

Who wouldn’t want their Blog to support full-text search? For most static pages (such as Github. IO), it is difficult to rely on the server.

The following code is implemented in reference to the local-search-engine-in-hexo scenario. The solution proposed by local-search-Engin is to generate an XML or JSON file with all the articles to be retrieved. Before searching, users automatically download the file from the server and complete the search locally through javascript code. The javascripte code was created using hexo-theme-freemind. In addition, Liam Huang’s blog also helped me a lot.

The result is this:

Create a Step to generate an XML file at the end of the Pipeline for retrieval.

extension PublishingStep{
    static func makeSearchIndex(includeCode:Bool = true) -> PublishingStep{
        step(named: "make search index file"){ content in
            let xml = XML(
                .element(named: "search",nodes:[
                    // This section is written separately because sometimes the compiler will TimeOut more complex DSLS
                    // Compile time is too long. It's perfectly fine to separate. This happens in SwiftUI as well
                    .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 {
    // The format of the XML file is determined by local-search-engin, where Plot is used to convert the web site content into 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"),
                    // Added htmlForSearch method to Item
                    // Since my Blog contains a number of Code examples, I give the user the option to include Code in the retrieval file.
                    .raw("
       + item.htmlForSearch(includeCode: includeCode) + "]] >")
                ]),
                .forEach(item.tags){ tag in
                    .element(named:"tag",text:tag.string)
                }
            ])
        }
    }
}
Copy the code

I have to compliment Plot again for making the CREATION of the XML very easy.

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
            // Use Ink to get the paired content
            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
    }
}
Copy the code

Create the search box and search results container:

// The id and class in it should be kept as they are due to the need to work with javascript
extension Node where Context= =HTML.BodyContext {
    // Displays the Node with search results
    public static func searchResult(a) -> Node{
        .div(
            .id("local-search-result"),
            .class("local-search-result-cls"))}// Displays the Node for the search box
    public static func searchInput(a) -> 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) // The complete code can be downloaded later))}}Copy the code

In this case, I set the search function in the TAB list page (see Topic development for more information), so I put the above two nodes where I see fit in makeTagListHTML.

Since the javascript used for the search requires jQuery, a jQuery reference is added to the head (by overriding the head, only the makeTagListHTML reference is currently added).

Add to Pipeline

.makeSearchIndex(includeCode: false), // Index the code in the article according to your own needs
Copy the code

The full code is available for download on Github.

Actual Combat 5: Deployment

This last example is a bit of a stretch, mainly to introduce ShellOut, another member of the Publish suite.

ShellOut is a lightweight library that makes it easy for developers to call scripts or command-line tools from Swift code. In Publish, code that is deployed to Github using Publish Deploy uses this library.

import Foundation
import Publish
import ShellOut

extension PublishingStep where Site= =FatbobmanBlog{
    static func uploadToServer(a) -> 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") 
                // I deployed with SCP, you can do it any way you like
            }
            catch {
                print(error)
            }
        }
    }
}
Copy the code

In main.swift add:

var command:String = ""
if CommandLine.arguments.count > 1 {
    command = CommandLine.arguments[1]}try MyBlog().publish(
  .addMarkdownFiles(),
  .
  .if(command = = "--upload", .uploadToServer())
]
Copy the code

Swift run MyBlog –upload (MyBlog for your project name)

Other plug-in resources

There are not many Publish plugins and themes available on the Internet. The main focus is Github’s #publish-plugin.

Among them, the most used ones are:

  • The code for SplashPublishPlugin is highlighted
  • HighlightJSPublishPlugin code highlighted
  • ColorfulTagsPublishPlugin add color to the Tag

If you want to share your plugin on Github, be sure to put the publish-plugin tag on it so everyone can find it

The last

When I was about to complete this article, I received news of Zhao Yingjun’s death on my mobile phone. It’s sad to die young. When I think back to my treatment over the years, I feel a peaceful and happy life.

The days I’ve been using Publish have given me a sense of how to decorate my house. It’s not always good, but it’s always fun to be able to change your site step by step.

My personal blog, Swift Notepad, has more on Swift, SwiftUI, and CoreData.