mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
WIP: Add a plugin to render posts, months & years not working yet
This commit is contained in:
parent
947fb4ec3a
commit
b00a48b096
23 changed files with 492 additions and 98 deletions
|
|
@ -1,14 +0,0 @@
|
||||||
//
|
|
||||||
// Date+CurrentYear.swift
|
|
||||||
// SiteGenerator
|
|
||||||
//
|
|
||||||
// Created by Sami Samhuri on 2019-12-02.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
extension Date {
|
|
||||||
static var currentYear: Int {
|
|
||||||
Calendar.current.dateComponents([.year], from: Date()).year!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
22
SiteGenerator/Sources/SiteGenerator/Date+Sugar.swift
Normal file
22
SiteGenerator/Sources/SiteGenerator/Date+Sugar.swift
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// Date+Sugar.swift
|
||||||
|
// SiteGenerator
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2019-12-02.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Date {
|
||||||
|
var year: Int {
|
||||||
|
Calendar.current.dateComponents([.year], from: self).year!
|
||||||
|
}
|
||||||
|
|
||||||
|
var month: Int {
|
||||||
|
Calendar.current.dateComponents([.month], from: self).month!
|
||||||
|
}
|
||||||
|
|
||||||
|
var day: Int {
|
||||||
|
Calendar.current.dateComponents([.day], from: self).day!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ extension PageContext: TemplateContext {
|
||||||
"metadata": metadata,
|
"metadata": metadata,
|
||||||
"styles": site.styles + page.styles,
|
"styles": site.styles + page.styles,
|
||||||
"scripts": site.scripts + page.scripts,
|
"scripts": site.scripts + page.scripts,
|
||||||
"currentYear": Date.currentYear,
|
"currentYear": Date().year,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ extension SiteContext: TemplateContext {
|
||||||
"title": site.title,
|
"title": site.title,
|
||||||
"styles": site.styles,
|
"styles": site.styles,
|
||||||
"scripts": site.scripts,
|
"scripts": site.scripts,
|
||||||
"currentYear": Date.currentYear,
|
"currentYear": Date().year,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,11 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import PathKit
|
|
||||||
import Stencil
|
|
||||||
|
|
||||||
public final class Generator: PluginDelegate, RendererDelegate {
|
public final class Generator {
|
||||||
// Dependencies
|
// Dependencies
|
||||||
let fileManager: FileManager = .default
|
let fileManager: FileManager = .default
|
||||||
let templateRenderer: Environment
|
let templateRenderer: TemplateRenderer
|
||||||
|
|
||||||
// Site properties
|
// Site properties
|
||||||
let site: Site
|
let site: Site
|
||||||
|
|
@ -21,13 +19,13 @@ public final class Generator: PluginDelegate, RendererDelegate {
|
||||||
let renderers: [Renderer]
|
let renderers: [Renderer]
|
||||||
|
|
||||||
public init(sourceURL: URL, plugins: [Plugin], renderers: [Renderer]) throws {
|
public init(sourceURL: URL, plugins: [Plugin], renderers: [Renderer]) throws {
|
||||||
let templatesURL = sourceURL.appendingPathComponent("templates")
|
|
||||||
let templatesPath = Path(templatesURL.path)
|
|
||||||
let loader = FileSystemLoader(paths: [templatesPath])
|
|
||||||
self.templateRenderer = Environment(loader: loader)
|
|
||||||
|
|
||||||
let siteURL = sourceURL.appendingPathComponent("site.json")
|
let siteURL = sourceURL.appendingPathComponent("site.json")
|
||||||
self.site = try Site.decode(from: siteURL)
|
let site = try Site.decode(from: siteURL)
|
||||||
|
|
||||||
|
let templatesURL = sourceURL.appendingPathComponent("templates")
|
||||||
|
self.templateRenderer = SiteTemplateRenderer(site: site, templatesURL: templatesURL)
|
||||||
|
|
||||||
|
self.site = site
|
||||||
self.sourceURL = sourceURL
|
self.sourceURL = sourceURL
|
||||||
self.plugins = plugins
|
self.plugins = plugins
|
||||||
self.renderers = renderers
|
self.renderers = renderers
|
||||||
|
|
@ -39,7 +37,7 @@ public final class Generator: PluginDelegate, RendererDelegate {
|
||||||
|
|
||||||
public func generate(targetURL: URL) throws {
|
public func generate(targetURL: URL) throws {
|
||||||
for plugin in plugins {
|
for plugin in plugins {
|
||||||
try plugin.render(targetURL: targetURL, delegate: self)
|
try plugin.render(targetURL: targetURL, templateRenderer: templateRenderer)
|
||||||
}
|
}
|
||||||
|
|
||||||
let publicURL = sourceURL.appendingPathComponent("public")
|
let publicURL = sourceURL.appendingPathComponent("public")
|
||||||
|
|
@ -77,7 +75,7 @@ public final class Generator: PluginDelegate, RendererDelegate {
|
||||||
let ext = String(filename.split(separator: ".").last!)
|
let ext = String(filename.split(separator: ".").last!)
|
||||||
for renderer in renderers {
|
for renderer in renderers {
|
||||||
if renderer.canRenderFile(named: filename, withExtension: ext) {
|
if renderer.canRenderFile(named: filename, withExtension: ext) {
|
||||||
try renderer.render(fileURL: fileURL, targetDir: targetDir, delegate: self)
|
try renderer.render(fileURL: fileURL, targetDir: targetDir, templateRenderer: templateRenderer)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -86,20 +84,4 @@ public final class Generator: PluginDelegate, RendererDelegate {
|
||||||
let dest = targetDir.appendingPathComponent(filename)
|
let dest = targetDir.appendingPathComponent(filename)
|
||||||
try fileManager.copyItem(at: fileURL, to: dest)
|
try fileManager.copyItem(at: fileURL, to: dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - PluginDelegate and RendererDelegate
|
|
||||||
|
|
||||||
public func renderPage(bodyHTML: String, metadata: [String: String]) throws -> String {
|
|
||||||
let page = Page(metadata: metadata)
|
|
||||||
let context = PageContext(site: site, body: bodyHTML, page: page, metadata: metadata)
|
|
||||||
let pageHTML = try templateRenderer.renderTemplate(name: "\(context.template).html", context: context.dictionary)
|
|
||||||
return pageHTML
|
|
||||||
}
|
|
||||||
|
|
||||||
public func renderTemplate(name: String?, context: [String: Any]) throws -> String {
|
|
||||||
let siteContext = SiteContext(site: site, template: name)
|
|
||||||
let contextDict = siteContext.dictionary.merging(context, uniquingKeysWith: { _, new in new })
|
|
||||||
print("Rendering \(siteContext.template) with context \(contextDict)")
|
|
||||||
return try templateRenderer.renderTemplate(name: "\(siteContext.template).html", context: contextDict)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public protocol PluginDelegate: AnyObject {
|
|
||||||
func renderPage(bodyHTML: String, metadata: [String: String]) throws -> String
|
|
||||||
func renderTemplate(name: String?, context: [String: Any]) throws -> String
|
|
||||||
}
|
|
||||||
|
|
||||||
public protocol Plugin {
|
public protocol Plugin {
|
||||||
func setUp(sourceURL: URL) throws
|
func setUp(sourceURL: URL) throws
|
||||||
|
|
||||||
func render(targetURL: URL, delegate: PluginDelegate) throws
|
func render(targetURL: URL, templateRenderer: TemplateRenderer) throws
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public protocol RendererDelegate: AnyObject {
|
|
||||||
func renderPage(bodyHTML: String, metadata: [String: String]) throws -> String
|
|
||||||
func renderTemplate(name: String?, context: [String: Any]) throws -> String
|
|
||||||
}
|
|
||||||
|
|
||||||
public protocol Renderer {
|
public protocol Renderer {
|
||||||
func canRenderFile(named filename: String, withExtension ext: String) -> Bool
|
func canRenderFile(named filename: String, withExtension ext: String) -> Bool
|
||||||
|
|
||||||
func render(fileURL: URL, targetDir: URL, delegate: RendererDelegate) throws
|
func render(fileURL: URL, targetDir: URL, templateRenderer: TemplateRenderer) throws
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
//
|
||||||
|
// SiteTemplateRenderer.swift
|
||||||
|
// SiteGenerator
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2019-12-03.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import PathKit
|
||||||
|
import Stencil
|
||||||
|
|
||||||
|
final class SiteTemplateRenderer: TemplateRenderer {
|
||||||
|
let site: Site
|
||||||
|
let stencil: Environment
|
||||||
|
|
||||||
|
init(site: Site, templatesURL: URL) {
|
||||||
|
self.site = site
|
||||||
|
let templatesPath = Path(templatesURL.path)
|
||||||
|
let loader = FileSystemLoader(paths: [templatesPath])
|
||||||
|
self.stencil = Environment(loader: loader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderPage(bodyHTML: String, metadata: [String: String]) throws -> String {
|
||||||
|
let page = Page(metadata: metadata)
|
||||||
|
let context = PageContext(site: site, body: bodyHTML, page: page, metadata: metadata)
|
||||||
|
let pageHTML = try stencil.renderTemplate(name: "\(context.template).html", context: context.dictionary)
|
||||||
|
return pageHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTemplate(name: String?, context: [String: Any]) throws -> String {
|
||||||
|
let siteContext = SiteContext(site: site, template: name)
|
||||||
|
let contextDict = siteContext.dictionary.merging(context, uniquingKeysWith: { _, new in new })
|
||||||
|
print("Rendering \(siteContext.template) with context \(contextDict)")
|
||||||
|
return try stencil.renderTemplate(name: "\(siteContext.template).html", context: contextDict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
//
|
||||||
|
// templateRenderer.swift
|
||||||
|
// SiteGenerator
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2019-12-03.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public protocol TemplateRenderer: AnyObject {
|
||||||
|
func renderPage(bodyHTML: String, metadata: [String: String]) throws -> String
|
||||||
|
func renderTemplate(name: String?, context: [String: Any]) throws -> String
|
||||||
|
}
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
//
|
|
||||||
// Post.swift
|
|
||||||
// SiteGenerator
|
|
||||||
//
|
|
||||||
// Created by Sami Samhuri on 2019-12-01.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct Post {
|
|
||||||
let date: Date
|
|
||||||
let formattedDate: String
|
|
||||||
let title: String
|
|
||||||
let slug: String
|
|
||||||
let author: String
|
|
||||||
let tags: [String]
|
|
||||||
let body: String
|
|
||||||
|
|
||||||
var path: String {
|
|
||||||
let dateComponents = Calendar.current.dateComponents([.year], from: date)
|
|
||||||
let year = dateComponents.year!
|
|
||||||
let month = dateComponents.month!
|
|
||||||
return "/" + [
|
|
||||||
"posts",
|
|
||||||
"\(year)",
|
|
||||||
"\(month)",
|
|
||||||
"\(slug)",
|
|
||||||
].joined(separator: "/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
30
SiteGenerator/Sources/SiteGenerator/Posts/Month.swift
Normal file
30
SiteGenerator/Sources/SiteGenerator/Posts/Month.swift
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
//
|
||||||
|
// Month.swift
|
||||||
|
// SiteGenerator
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2019-12-03.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Month {
|
||||||
|
static let names = [
|
||||||
|
"January", "Februrary", "March", "April",
|
||||||
|
"May", "June", "July", "August",
|
||||||
|
"September", "October", "November", "December"
|
||||||
|
]
|
||||||
|
|
||||||
|
let number: Int
|
||||||
|
|
||||||
|
var padded: String {
|
||||||
|
String(format: "%02d", number)
|
||||||
|
}
|
||||||
|
|
||||||
|
var name: String {
|
||||||
|
Month.names[number]
|
||||||
|
}
|
||||||
|
|
||||||
|
var abbreviatedName: String {
|
||||||
|
String(name.prefix(3))
|
||||||
|
}
|
||||||
|
}
|
||||||
74
SiteGenerator/Sources/SiteGenerator/Posts/Post.swift
Normal file
74
SiteGenerator/Sources/SiteGenerator/Posts/Post.swift
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
//
|
||||||
|
// Post.swift
|
||||||
|
// SiteGenerator
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2019-12-01.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Post {
|
||||||
|
let slug: String
|
||||||
|
let title: String
|
||||||
|
let author: String
|
||||||
|
let date: Date
|
||||||
|
let formattedDate: String
|
||||||
|
let link: URL?
|
||||||
|
let tags: [String]
|
||||||
|
let bodyMarkdown: String
|
||||||
|
|
||||||
|
var isLink: Bool {
|
||||||
|
link != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var path: String {
|
||||||
|
let dateComponents = Calendar.current.dateComponents([.year, .month], from: date)
|
||||||
|
let year = dateComponents.year!
|
||||||
|
let month = dateComponents.month!
|
||||||
|
return "/" + [
|
||||||
|
"posts",
|
||||||
|
"\(year)",
|
||||||
|
"\(month)",
|
||||||
|
"\(slug)",
|
||||||
|
].joined(separator: "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Posts are sorted in reverse date order.
|
||||||
|
extension Post: Comparable {
|
||||||
|
static func < (lhs: Self, rhs: Self) -> Bool {
|
||||||
|
rhs.date < lhs.date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Post {
|
||||||
|
enum Error: Swift.Error {
|
||||||
|
case deficientMetadata(missingKeys: [String])
|
||||||
|
}
|
||||||
|
|
||||||
|
init(bodyMarkdown: String, metadata: [String: String]) throws {
|
||||||
|
self.bodyMarkdown = bodyMarkdown
|
||||||
|
|
||||||
|
let requiredKeys = ["Slug", "Title", "Author", "Date", "Timestamp", "Tags", "Path_deprecated"]
|
||||||
|
let missingKeys = requiredKeys.filter { metadata[$0] == nil }
|
||||||
|
guard missingKeys.isEmpty else {
|
||||||
|
throw Error.deficientMetadata(missingKeys: missingKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
slug = metadata["Slug"]!
|
||||||
|
title = metadata["Title"]!
|
||||||
|
author = metadata["Author"]!
|
||||||
|
date = Date(timeIntervalSince1970: TimeInterval(metadata["Timestamp"]!)!)
|
||||||
|
formattedDate = metadata["Date"]!
|
||||||
|
if let urlString = metadata["Link"] {
|
||||||
|
link = URL(string: urlString)!
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
link = nil
|
||||||
|
}
|
||||||
|
tags = metadata["Tags"]!.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
|
||||||
|
|
||||||
|
let handWrittenPath = metadata["Path_deprecated"]!
|
||||||
|
assert(path == handWrittenPath, "FUCK: Generated path (\(path)) doesn't match the hand-written one \(handWrittenPath)")
|
||||||
|
}
|
||||||
|
}
|
||||||
67
SiteGenerator/Sources/SiteGenerator/Posts/PostsByYear.swift
Normal file
67
SiteGenerator/Sources/SiteGenerator/Posts/PostsByYear.swift
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
//
|
||||||
|
// Posts.swift
|
||||||
|
// SiteGenerator
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2019-12-03.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct MonthPosts {
|
||||||
|
let month: Int
|
||||||
|
var posts: [Post]
|
||||||
|
|
||||||
|
var isEmpty: Bool {
|
||||||
|
posts.isEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct YearPosts {
|
||||||
|
let year: Int
|
||||||
|
var byMonth: [Int: MonthPosts]
|
||||||
|
|
||||||
|
subscript(month: Int) -> MonthPosts {
|
||||||
|
get {
|
||||||
|
byMonth[month, default: MonthPosts(month: month, posts: [])]
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
byMonth[month] = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEmpty: Bool {
|
||||||
|
byMonth.isEmpty || byMonth.values.allSatisfy { $0.isEmpty }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PostsByYear {
|
||||||
|
private(set) var byYear: [Int: YearPosts]
|
||||||
|
|
||||||
|
init(posts: [Post]) {
|
||||||
|
byYear = [:]
|
||||||
|
posts.forEach { add(post: $0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
subscript(year: Int) -> YearPosts {
|
||||||
|
get {
|
||||||
|
byYear[year, default: YearPosts(year: year, byMonth: [:])]
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
byYear[year] = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEmpty: Bool {
|
||||||
|
byYear.isEmpty || byYear.values.allSatisfy { $0.isEmpty }
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func add(post: Post) {
|
||||||
|
let (year, month) = (post.date.year, post.date.month)
|
||||||
|
self[year][month].posts.append(post)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns posts sorted by reverse date.
|
||||||
|
func flattened() -> [Post] {
|
||||||
|
byYear.values.flatMap { $0.byMonth.values.flatMap { $0.posts } }.sorted { $1.date < $0.date }
|
||||||
|
}
|
||||||
|
}
|
||||||
146
SiteGenerator/Sources/SiteGenerator/Posts/PostsPlugin.swift
Normal file
146
SiteGenerator/Sources/SiteGenerator/Posts/PostsPlugin.swift
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
//
|
||||||
|
// PostsPlugin.swift
|
||||||
|
// SiteGenerator
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2019-12-03.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Ink
|
||||||
|
|
||||||
|
final class PostsPlugin: Plugin {
|
||||||
|
let fileManager: FileManager = .default
|
||||||
|
let markdownParser = MarkdownParser()
|
||||||
|
let path: String
|
||||||
|
|
||||||
|
var posts: PostsByYear!
|
||||||
|
var sourceURL: URL!
|
||||||
|
|
||||||
|
init(path: String = "posts") {
|
||||||
|
self.path = path
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUp(sourceURL: URL) throws {
|
||||||
|
self.sourceURL = sourceURL
|
||||||
|
let postsURL = sourceURL.appendingPathComponent("posts")
|
||||||
|
guard fileManager.fileExists(atPath: postsURL.path) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let posts = try enumerateMarkdownFiles(directory: postsURL)
|
||||||
|
.compactMap { (url: URL) -> Post? in
|
||||||
|
guard let result = (try? String(contentsOf: url)).map(markdownParser.parse) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
return try Post(bodyMarkdown: "(TEST)", metadata: result.metadata)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
print("Cannot create post from markdown file \(url): \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print("posts: \(posts)")
|
||||||
|
self.posts = PostsByYear(posts: posts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func render(targetURL: URL, templateRenderer: TemplateRenderer) throws {
|
||||||
|
guard posts != nil, !posts.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let postsDir = targetURL.appendingPathComponent(path)
|
||||||
|
try renderRecentPosts(postsDir: postsDir, templateRenderer: templateRenderer)
|
||||||
|
try renderYearsAndMonths(postsDir: postsDir, templateRenderer: templateRenderer)
|
||||||
|
try renderPostsByDate(postsDir: postsDir, templateRenderer: templateRenderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderRecentPosts(postsDir: URL, templateRenderer: TemplateRenderer) throws {
|
||||||
|
print("renderRecentPosts(postsDir: \(postsDir), templateRenderer: \(templateRenderer)")
|
||||||
|
let recentPosts = posts.flattened().prefix(10)
|
||||||
|
try fileManager.createDirectory(at: postsDir, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
let recentPostsURL = postsDir.appendingPathComponent("index.html")
|
||||||
|
let recentPostsHTML = try templateRenderer.renderTemplate(name: "recent-posts", context: ["recentPosts": recentPosts])
|
||||||
|
try recentPostsHTML.write(to: recentPostsURL, atomically: true, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderPostsByDate(postsDir: URL, templateRenderer: TemplateRenderer) throws {
|
||||||
|
print("renderPostsByDate(postsDir: \(postsDir), templateRenderer: \(templateRenderer)")
|
||||||
|
for post in posts.flattened() {
|
||||||
|
let monthDir = postsDir
|
||||||
|
.appendingPathComponent(String(format: "%02d", post.date.year))
|
||||||
|
.appendingPathComponent(String(format: "%02d", post.date.month))
|
||||||
|
try renderPost(post, monthDir: monthDir, templateRenderer: templateRenderer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderYearsAndMonths(postsDir: URL, templateRenderer: TemplateRenderer) throws {
|
||||||
|
print("renderYearsAndMonths(postsDir: \(postsDir), templateRenderer: \(templateRenderer)")
|
||||||
|
let allMonths = (1 ... 12).reversed().map(Month.init)
|
||||||
|
for (year, monthPosts) in posts.byYear.sorted(by: { $1.key < $0.key }) {
|
||||||
|
let yearDir = postsDir.appendingPathComponent("\(year)")
|
||||||
|
var sortedPostsByMonth: [Int: [RenderedPost]] = [:]
|
||||||
|
for month in allMonths {
|
||||||
|
let sortedPosts = monthPosts[month.number].posts.sorted(by: { $1.date < $0.date })
|
||||||
|
guard !sortedPosts.isEmpty else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderedPosts = sortedPosts.map { post -> RenderedPost in
|
||||||
|
let bodyHTML = markdownParser.html(from: post.bodyMarkdown)
|
||||||
|
return RenderedPost(post: post, body: bodyHTML)
|
||||||
|
}
|
||||||
|
sortedPostsByMonth[month.number] = renderedPosts
|
||||||
|
|
||||||
|
let monthDir = yearDir.appendingPathComponent(month.padded)
|
||||||
|
try fileManager.createDirectory(at: monthDir, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
let context: [String: Any] = ["path": path, "month": month, "posts": renderedPosts]
|
||||||
|
let monthHTML = try templateRenderer.renderTemplate(name: "posts-month", context: context)
|
||||||
|
let monthURL = monthDir.appendingPathComponent("index.html")
|
||||||
|
try monthHTML.write(to: monthURL, atomically: true, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
try fileManager.createDirectory(at: yearDir, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
let context: [String: Any] = [
|
||||||
|
"path": path,
|
||||||
|
"year": year,
|
||||||
|
"months": sortedPostsByMonth.keys.sorted().reversed().map(Month.init),
|
||||||
|
"postsByMonth": sortedPostsByMonth,
|
||||||
|
]
|
||||||
|
let yearHTML = try templateRenderer.renderTemplate(name: "posts-year", context: context)
|
||||||
|
let yearURL = yearDir.appendingPathComponent("index.html")
|
||||||
|
try yearHTML.write(to: yearURL, atomically: true, encoding: .utf8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func renderPost(_ post: Post, monthDir: URL, templateRenderer: TemplateRenderer) throws {
|
||||||
|
print("renderPost(\(post), monthDir: \(monthDir), templateRenderer: \(templateRenderer)")
|
||||||
|
try fileManager.createDirectory(at: monthDir, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
let filename = "\(post.slug).html"
|
||||||
|
let postURL = monthDir.appendingPathComponent(filename)
|
||||||
|
let templateName = self.templateName(for: post)
|
||||||
|
let bodyHTML = markdownParser.html(from: post.bodyMarkdown)
|
||||||
|
let renderedPost = RenderedPost(post: post, body: bodyHTML)
|
||||||
|
let postHTML = try templateRenderer.renderTemplate(name: templateName, context: ["post": renderedPost])
|
||||||
|
try postHTML.write(to: postURL, atomically: true, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func templateName(for post: Post) -> String {
|
||||||
|
post.isLink ? "post-link" : "post-text"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func enumerateMarkdownFiles(directory: URL) throws -> [URL] {
|
||||||
|
print("enumerateMarkdownFiles(directory: \(directory))")
|
||||||
|
return try fileManager.contentsOfDirectory(atPath: directory.path).flatMap { (filename: String) -> [URL] in
|
||||||
|
let fileURL = directory.appendingPathComponent(filename)
|
||||||
|
var isDir: ObjCBool = false
|
||||||
|
fileManager.fileExists(atPath: fileURL.path, isDirectory: &isDir)
|
||||||
|
if isDir.boolValue {
|
||||||
|
return try enumerateMarkdownFiles(directory: fileURL)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return fileURL.pathExtension == "md" ? [fileURL] : []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
SiteGenerator/Sources/SiteGenerator/Posts/RenderedPost.swift
Normal file
27
SiteGenerator/Sources/SiteGenerator/Posts/RenderedPost.swift
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
//
|
||||||
|
// RenderedPost.swift
|
||||||
|
// SiteGenerator
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2019-12-03.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct RenderedPost {
|
||||||
|
let post: Post
|
||||||
|
let body: String
|
||||||
|
|
||||||
|
var author: String { post.author }
|
||||||
|
|
||||||
|
var title: String { post.title }
|
||||||
|
|
||||||
|
var date: Date { post.date }
|
||||||
|
|
||||||
|
var formattedDate: String { post.formattedDate }
|
||||||
|
|
||||||
|
var isLink: Bool { post.isLink }
|
||||||
|
|
||||||
|
var link: URL? { post.link }
|
||||||
|
|
||||||
|
var path: String { post.path }
|
||||||
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ final class ProjectsPlugin: Plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(targetURL: URL, delegate: PluginDelegate) throws {
|
func render(targetURL: URL, templateRenderer: TemplateRenderer) throws {
|
||||||
guard !projects.isEmpty else {
|
guard !projects.isEmpty else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -46,13 +46,13 @@ final class ProjectsPlugin: Plugin {
|
||||||
let projectsDir = targetURL.appendingPathComponent(path)
|
let projectsDir = targetURL.appendingPathComponent(path)
|
||||||
try fileManager.createDirectory(at: projectsDir, withIntermediateDirectories: true, attributes: nil)
|
try fileManager.createDirectory(at: projectsDir, withIntermediateDirectories: true, attributes: nil)
|
||||||
let projectsURL = projectsDir.appendingPathComponent("index.html")
|
let projectsURL = projectsDir.appendingPathComponent("index.html")
|
||||||
let projectsHTML = try delegate.renderTemplate(name: "projects", context: ["projects": projects])
|
let projectsHTML = try templateRenderer.renderTemplate(name: "projects", context: ["projects": projects])
|
||||||
try projectsHTML.write(to: projectsURL, atomically: true, encoding: .utf8)
|
try projectsHTML.write(to: projectsURL, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
for project in projects {
|
for project in projects {
|
||||||
let filename = "\(project.title).html"
|
let filename = "\(project.title).html"
|
||||||
let projectURL = projectsDir.appendingPathComponent(filename)
|
let projectURL = projectsDir.appendingPathComponent(filename)
|
||||||
let projectHTML = try delegate.renderTemplate(name: "project", context: ["project": project])
|
let projectHTML = try templateRenderer.renderTemplate(name: "project", context: ["project": project])
|
||||||
try projectHTML.write(to: projectURL, atomically: true, encoding: .utf8)
|
try projectHTML.write(to: projectURL, atomically: true, encoding: .utf8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ public final class LessRenderer: Renderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse Less and render it as CSS.
|
/// Parse Less and render it as CSS.
|
||||||
public func render(fileURL: URL, targetDir: URL, delegate: RendererDelegate) throws {
|
public func render(fileURL: URL, targetDir: URL, templateRenderer: TemplateRenderer) throws {
|
||||||
let filename = fileURL.lastPathComponent
|
let filename = fileURL.lastPathComponent
|
||||||
let cssURL = targetDir.appendingPathComponent(filename.replacingOccurrences(of: ".less", with: ".css"))
|
let cssURL = targetDir.appendingPathComponent(filename.replacingOccurrences(of: ".less", with: ".css"))
|
||||||
let less = try String(contentsOf: fileURL, encoding: .utf8)
|
let less = try String(contentsOf: fileURL, encoding: .utf8)
|
||||||
|
|
|
||||||
|
|
@ -9,26 +9,26 @@ import Foundation
|
||||||
import Ink
|
import Ink
|
||||||
|
|
||||||
public final class MarkdownRenderer: Renderer {
|
public final class MarkdownRenderer: Renderer {
|
||||||
let mdParser = MarkdownParser()
|
let markdownParser = MarkdownParser()
|
||||||
|
|
||||||
public func canRenderFile(named filename: String, withExtension ext: String) -> Bool {
|
public func canRenderFile(named filename: String, withExtension ext: String) -> Bool {
|
||||||
ext == "md"
|
ext == "md"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse Markdown and render it as HTML, running it through a Stencil template.
|
/// Parse Markdown and render it as HTML, running it through a Stencil template.
|
||||||
public func render(fileURL: URL, targetDir: URL, delegate: RendererDelegate) throws {
|
public func render(fileURL: URL, targetDir: URL, templateRenderer: TemplateRenderer) throws {
|
||||||
let mdFilename = fileURL.lastPathComponent
|
let mdFilename = fileURL.lastPathComponent
|
||||||
let htmlFilename = mdFilename.replacingOccurrences(of: ".md", with: ".html")
|
let htmlFilename = mdFilename.replacingOccurrences(of: ".md", with: ".html")
|
||||||
let htmlURL = targetDir.appendingPathComponent(htmlFilename)
|
let htmlURL = targetDir.appendingPathComponent(htmlFilename)
|
||||||
let bodyMarkdown = try String(contentsOf: fileURL, encoding: .utf8)
|
let bodyMarkdown = try String(contentsOf: fileURL, encoding: .utf8)
|
||||||
let bodyHTML = mdParser.html(from: bodyMarkdown).trimmingCharacters(in: .whitespacesAndNewlines)
|
let bodyHTML = markdownParser.html(from: bodyMarkdown).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let metadata = try markdownMetadata(from: fileURL)
|
let metadata = try markdownMetadata(from: fileURL)
|
||||||
let pageHTML = try delegate.renderPage(bodyHTML: bodyHTML, metadata: metadata)
|
let pageHTML = try templateRenderer.renderPage(bodyHTML: bodyHTML, metadata: metadata)
|
||||||
try pageHTML.write(to: htmlURL, atomically: true, encoding: .utf8)
|
try pageHTML.write(to: htmlURL, atomically: true, encoding: .utf8)
|
||||||
}
|
}
|
||||||
|
|
||||||
func markdownMetadata(from url: URL) throws -> [String: String] {
|
func markdownMetadata(from url: URL) throws -> [String: String] {
|
||||||
let md = try String(contentsOf: url, encoding: .utf8)
|
let md = try String(contentsOf: url, encoding: .utf8)
|
||||||
return mdParser.parse(md).metadata
|
return markdownParser.parse(md).metadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ func main(sourcePath: String, targetPath: String) throws {
|
||||||
let targetURL = URL(fileURLWithPath: targetPath)
|
let targetURL = URL(fileURLWithPath: targetPath)
|
||||||
let generator = try Generator(
|
let generator = try Generator(
|
||||||
sourceURL: sourceURL,
|
sourceURL: sourceURL,
|
||||||
plugins: [ProjectsPlugin()],
|
plugins: [ProjectsPlugin(), PostsPlugin()],
|
||||||
renderers: [LessRenderer(), MarkdownRenderer()]
|
renderers: [LessRenderer(), MarkdownRenderer()]
|
||||||
)
|
)
|
||||||
try generator.generate(targetURL: targetURL)
|
try generator.generate(targetURL: targetURL)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<article class="container">
|
<article class="container">
|
||||||
<header>
|
<header>
|
||||||
<h1><a href="{{ post.link }}">→ {{ post.title }}</a></h1>
|
<h1><a href="{{ post.link }}">→ {{ post.title }}</a></h1>
|
||||||
<time>{{ post.date }}</time>
|
<time>{{ post.formattedDate }}</time>
|
||||||
<a class="permalink" href="{{ post.url }}">∞</a>
|
<a class="permalink" href="{{ post.url }}">∞</a>
|
||||||
</header>
|
</header>
|
||||||
{{ post.body }}
|
{{ post.body }}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<article class="container">
|
<article class="container">
|
||||||
<header>
|
<header>
|
||||||
<h1><a href="{{ post.url }}">{{ post.title }}</a></h1>
|
<h1><a href="{{ post.url }}">{{ post.title }}</a></h1>
|
||||||
<time>{{ post.date }}</time>
|
<time>{{ post.formattedDate }}</time>
|
||||||
</header>
|
</header>
|
||||||
{{ post.body }}
|
{{ post.body }}
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
24
templates/posts-month.html
Normal file
24
templates/posts-month.html
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends "samhuri.net.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% for post in posts %}
|
||||||
|
<article class="container">
|
||||||
|
<header>
|
||||||
|
{% if post.isLink %}
|
||||||
|
<h2><a href="{{ post.link }}">→ {{ post.title }}</a></h2>
|
||||||
|
<time>{{ post.formattedDate }}</time>
|
||||||
|
<a class="permalink" href="{{ post.path }}">∞</a>
|
||||||
|
{% else %}
|
||||||
|
<h2><a href="{{ post.path }}">{{ post.title }}</a></h2>
|
||||||
|
<time>{{ post.formattedDate }}</time>
|
||||||
|
{% endif %}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{{ post.body }}
|
||||||
|
</article>
|
||||||
|
<div class="row clearfix">
|
||||||
|
<p class="fin"><i class="fa fa-code"></i></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
27
templates/posts-year.html
Normal file
27
templates/posts-year.html
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends "samhuri.net.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="container">
|
||||||
|
<h2>{{ year }}</h2>
|
||||||
|
|
||||||
|
{% for month in months %}
|
||||||
|
<h3>
|
||||||
|
<a href="/{{ path }}/{{ year }}/{{ month.padded }}">{{ month.name }}</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<ul class="archive">
|
||||||
|
{% for post in postsByMonth[month.number] %}
|
||||||
|
<li>
|
||||||
|
{% if post.isLink %}
|
||||||
|
<a href="{{ post.path }}">→ {{ post.title }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ post.path }}">{{ post.title }}</a>
|
||||||
|
{% endif %}
|
||||||
|
<time>{{ post.date.day }} {{ month.abbreviatedName }}</time>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Loading…
Reference in a new issue