diff --git a/Readme.md b/Readme.md
index 4368679..5ba8492 100644
--- a/Readme.md
+++ b/Readme.md
@@ -69,7 +69,7 @@ Execution, trying TDD for the first time:
- [x] 301 redirect /archive to /posts, and update the header link
- - [x] Check and delete _data.json filse
+ - [x] Check and delete _data.json files
- [x] Search for other _data.json and .ejs files and eliminate any that are found
@@ -79,9 +79,21 @@ Execution, trying TDD for the first time:
- [x] Find a way to add the site name to HTML titles rendered by plugins
-- [ ] Clean up the posts plugin
+- [x] Clean up the posts plugin
- - [ ] Why don't plain data structures always work with Stencil? Maybe computed properties are a no-go but we can at least use structs instead of dictionaries for the actual rendering
+ - [x] Why don't plain data structures always work with Stencil? Maybe computed properties are a no-go but we can at least use structs instead of dictionaries for the actual rendering
+
+ - [x] Separate I/O from transformations
+
+ - [x] Factor the core logic out of PostsPlugin ... separate I/O from transformations? Is that an improvement or does it obscure what's happening?
+
+ - [x] Stop validating metadata in Post, do that when rendering markdown
+
+ - [x] Remove RenderedPost
+
+ - [x] Move all dictionary conversions for use in template contexts to extensions
+
+ - [x] Stop using dictionaries for template contexts, use structs w/ computed properties
- [ ] Consider using Swift for samhuri.net as well, and then making SiteGenerator a package that it uses ... then we can use Plot or pointfree.co's swift-html
diff --git a/SiteGenerator/Sources/SiteGenerator/FunctionComposition.swift b/SiteGenerator/Sources/SiteGenerator/FunctionComposition.swift
new file mode 100644
index 0000000..9e6b8d3
--- /dev/null
+++ b/SiteGenerator/Sources/SiteGenerator/FunctionComposition.swift
@@ -0,0 +1,34 @@
+//
+// FunctionComposition.swift
+// SiteGenerator
+//
+// Created by Sami Samhuri on 2019-11-18.
+//
+
+import Foundation
+
+infix operator |> :AdditionPrecedence
+
+// MARK: Synchronous
+
+public func |> (
+ f: @escaping (A) -> B,
+ g: @escaping (B) -> C
+) -> (A) -> C {
+ return { a in
+ let b = f(a)
+ let c = g(b)
+ return c
+ }
+}
+
+public func |> (
+ f: @escaping (A) throws -> B,
+ g: @escaping (B) throws -> C
+) -> (A) throws -> C {
+ return { a in
+ let b = try f(a)
+ let c = try g(b)
+ return c
+ }
+}
diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/Page.swift b/SiteGenerator/Sources/SiteGenerator/Generator/Page.swift
index d2df94c..23929ec 100644
--- a/SiteGenerator/Sources/SiteGenerator/Generator/Page.swift
+++ b/SiteGenerator/Sources/SiteGenerator/Generator/Page.swift
@@ -17,8 +17,12 @@ struct Page {
extension Page {
init(metadata: [String: String]) {
let template = metadata["Template"]
- let styles = metadata["Styles", default: ""].split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
- let scripts = metadata["Scripts", default: ""].split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
+ let styles = metadata["Styles", default: ""]
+ .split(separator: ",")
+ .map { $0.trimmingCharacters(in: .whitespaces) }
+ let scripts = metadata["Scripts", default: ""]
+ .split(separator: ",")
+ .map { $0.trimmingCharacters(in: .whitespaces) }
let title = metadata["Title", default: ""]
self.init(title: title, template: template, styles: styles, scripts: scripts)
}
diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/SiteTemplateRenderer.swift b/SiteGenerator/Sources/SiteGenerator/Generator/SiteTemplateRenderer.swift
index e5b04fd..4bd0a9b 100644
--- a/SiteGenerator/Sources/SiteGenerator/Generator/SiteTemplateRenderer.swift
+++ b/SiteGenerator/Sources/SiteGenerator/Generator/SiteTemplateRenderer.swift
@@ -30,7 +30,6 @@ final class SiteTemplateRenderer: TemplateRenderer {
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 keys: \(contextDict.keys.sorted().joined(separator: ", "))")
return try stencil.renderTemplate(name: "\(siteContext.template).html", context: contextDict)
}
}
diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/Month.swift b/SiteGenerator/Sources/SiteGenerator/Posts/Month.swift
index 0f1fd16..6d6e92a 100644
--- a/SiteGenerator/Sources/SiteGenerator/Posts/Month.swift
+++ b/SiteGenerator/Sources/SiteGenerator/Posts/Month.swift
@@ -8,6 +8,8 @@
import Foundation
struct Month: Equatable {
+ static let all = (1 ... 12).map(Month.init(_:))
+
static let names = [
"January", "Februrary", "March", "April",
"May", "June", "July", "August",
@@ -34,7 +36,7 @@ struct Month: Equatable {
Month.names[number - 1]
}
- var abbreviatedName: String {
+ var abbreviation: String {
String(name.prefix(3))
}
}
diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/Post.swift b/SiteGenerator/Sources/SiteGenerator/Posts/Post.swift
index 3c95fb2..f492048 100644
--- a/SiteGenerator/Sources/SiteGenerator/Posts/Post.swift
+++ b/SiteGenerator/Sources/SiteGenerator/Posts/Post.swift
@@ -15,76 +15,39 @@ struct Post {
let formattedDate: String
let link: URL?
let tags: [String]
- let bodyMarkdown: String
+ let body: String
+ let path: String
- var dictionary: [String: Any] {
- var result: [String: Any] = [
- "slug": slug,
- "title": title,
- "author": author,
- "day": date.day,
- "month": date.month,
- "year": date.year,
- "formattedDate": formattedDate,
- "tags": tags
- ]
- if let link = link {
- result["isLink"] = true
- result["link"] = link
- }
- return result
- }
+ // These are computed properties but are computed eagerly because
+ // Stencil is unable to use real computed properties at this time.
+ let isLink: Bool
+ let day: Int
- func dictionary(withPath path: String) -> [String: Any] {
- var dict = dictionary
- dict["path"] = path
- return dict
- }
-}
-
-/// 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(slug: String, bodyMarkdown: String, metadata: [String: String]) throws {
+ init(slug: String, title: String, author: String, date: Date, formattedDate: String, link: URL?, tags: [String], body: String, path: String) {
self.slug = slug
- self.bodyMarkdown = bodyMarkdown
+ self.title = title
+ self.author = author
+ self.date = date
+ self.formattedDate = formattedDate
+ self.link = link
+ self.tags = tags
+ self.body = body
+ self.path = path
- let requiredKeys = ["Title", "Author", "Date", "Timestamp"]
- let missingKeys = requiredKeys.filter { metadata[$0] == nil }
- guard missingKeys.isEmpty else {
- throw Error.deficientMetadata(missingKeys: missingKeys)
- }
+ // Eagerly computed properties
+ self.isLink = link != nil
+ self.day = date.day
+ }
+}
- 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
- }
- if let string = metadata["Tags"] {
- tags = string.split(separator: ",").map({ $0.trimmingCharacters(in: .whitespaces) })
- }
- else {
- tags = []
- }
+extension Post: Comparable {
+ static func <(lhs: Self, rhs: Self) -> Bool {
+ lhs.date < rhs.date
}
}
extension Post: CustomDebugStringConvertible {
var debugDescription: String {
- ""
+ ""
}
}
diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/PostRepo.swift b/SiteGenerator/Sources/SiteGenerator/Posts/PostRepo.swift
new file mode 100644
index 0000000..29d0193
--- /dev/null
+++ b/SiteGenerator/Sources/SiteGenerator/Posts/PostRepo.swift
@@ -0,0 +1,85 @@
+//
+// PostRepo.swift
+// SiteGenerator
+//
+// Created by Sami Samhuri on 2019-12-09.
+//
+
+import Foundation
+
+struct RawPost {
+ let slug: String
+ let markdown: String
+}
+
+final class PostRepo {
+ let postsPath = "posts"
+ let recentPostsCount = 10
+
+ let fileManager: FileManager
+ let postTransformer: PostTransformer
+
+ private(set) var posts: PostsByYear!
+
+ init(fileManager: FileManager = .default, postTransformer: PostTransformer = PostTransformer()) {
+ self.fileManager = fileManager
+ self.postTransformer = postTransformer
+ }
+
+ var isEmpty: Bool {
+ posts == nil || posts.isEmpty
+ }
+
+ var sortedPosts: [Post] {
+ posts?.flattened().sorted(by: >) ?? []
+ }
+
+ var recentPosts: [Post] {
+ Array(sortedPosts.prefix(recentPostsCount))
+ }
+
+ func postDataExists(at sourceURL: URL) -> Bool {
+ let postsURL = sourceURL.appendingPathComponent(postsPath)
+ return fileManager.fileExists(atPath: postsURL.path)
+ }
+
+ func readPosts(sourceURL: URL, makePath: (Date, _ slug: String) -> String) throws {
+ let posts = try readRawPosts(sourceURL: sourceURL)
+ .map { try postTransformer.makePost(from: $0, makePath: makePath) }
+ self.posts = PostsByYear(posts: posts)
+ }
+
+ private func readRawPosts(sourceURL: URL) throws -> [RawPost] {
+ let postsURL = sourceURL.appendingPathComponent(postsPath)
+ return try enumerateMarkdownFiles(directory: postsURL)
+ .compactMap { url in
+ do {
+ return try readRawPost(url: url)
+ }
+ catch {
+ print("error: Cannot read post from \(url): \(error)")
+ return nil
+ }
+ }
+ }
+
+ private func readRawPost(url: URL) throws -> RawPost {
+ let slug = url.deletingPathExtension().lastPathComponent
+ let markdown = try String(contentsOf: url)
+ return RawPost(slug: slug, markdown: markdown)
+ }
+
+ private func enumerateMarkdownFiles(directory: URL) throws -> [URL] {
+ 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] : []
+ }
+ }
+ }
+}
diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/PostTransformer.swift b/SiteGenerator/Sources/SiteGenerator/Posts/PostTransformer.swift
new file mode 100644
index 0000000..89380cd
--- /dev/null
+++ b/SiteGenerator/Sources/SiteGenerator/Posts/PostTransformer.swift
@@ -0,0 +1,84 @@
+//
+// PostTransformer.swift
+// SiteGenerator
+//
+// Created by Sami Samhuri on 2019-12-09.
+//
+
+import Foundation
+import Ink
+
+final class PostTransformer {
+ let markdownParser: MarkdownParser
+
+ init(markdownParser: MarkdownParser = MarkdownParser()) {
+ self.markdownParser = markdownParser
+ }
+
+ func makePost(from rawPost: RawPost, makePath: (Date, _ slug: String) -> String) throws -> Post {
+ let result = markdownParser.parse(rawPost.markdown)
+ let metadata = try parseMetadata(result.metadata)
+ let path = makePath(metadata.date, rawPost.slug)
+ return Post(
+ slug: rawPost.slug,
+ title: metadata.title,
+ author: metadata.author,
+ date: metadata.date,
+ formattedDate: metadata.formattedDate,
+ link: metadata.link,
+ tags: metadata.tags,
+ body: result.html,
+ path: path
+ )
+ }
+}
+
+struct ParsedMetadata {
+ let title: String
+ let author: String
+ let date: Date
+ let formattedDate: String
+ let link: URL?
+ let tags: [String]
+}
+
+extension PostTransformer {
+ enum Error: Swift.Error {
+ case deficientMetadata(missingKeys: [String])
+ case invalidTimestamp(String)
+ }
+
+ func parseMetadata(_ metadata: [String: String]) throws -> ParsedMetadata {
+ let requiredKeys = ["Title", "Author", "Date", "Timestamp"]
+ let missingKeys = requiredKeys.filter { metadata[$0] == nil }
+ guard missingKeys.isEmpty else {
+ throw Error.deficientMetadata(missingKeys: missingKeys)
+ }
+ guard let timeInterval = TimeInterval(metadata["Timestamp"]!) else {
+ throw Error.invalidTimestamp(metadata["Timestamp"]!)
+ }
+
+ let title = metadata["Title"]!
+ let author = metadata["Author"]!
+ let date = Date(timeIntervalSince1970: timeInterval)
+ let formattedDate = metadata["Date"]!
+
+ let link: URL?
+ if let urlString = metadata["Link"] {
+ link = URL(string: urlString)!
+ }
+ else {
+ link = nil
+ }
+
+ let tags: [String]
+ if let string = metadata["Tags"] {
+ tags = string.split(separator: ",").map({ $0.trimmingCharacters(in: .whitespaces) })
+ }
+ else {
+ tags = []
+ }
+
+ return ParsedMetadata(title: title, author: author, date: date, formattedDate: formattedDate, link: link, tags: tags)
+ }
+}
diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/PostWriter.swift b/SiteGenerator/Sources/SiteGenerator/Posts/PostWriter.swift
new file mode 100644
index 0000000..5fc2fb0
--- /dev/null
+++ b/SiteGenerator/Sources/SiteGenerator/Posts/PostWriter.swift
@@ -0,0 +1,139 @@
+//
+// PostWriter.swift
+// SiteGenerator
+//
+// Created by Sami Samhuri on 2019-12-09.
+//
+
+import Foundation
+
+final class PostWriter {
+ let fileManager: FileManager
+ let outputPath: String
+
+ init(fileManager: FileManager = .default, outputPath: String = "posts") {
+ self.fileManager = fileManager
+ self.outputPath = outputPath
+ }
+
+ func urlPath(year: Int) -> String {
+ "/\(outputPath)/\(year)"
+ }
+
+ func urlPath(year: Int, month: Month) -> String {
+ urlPath(year: year).appending("/\(month.padded)")
+ }
+
+ func urlPathForPost(date: Date, slug: String) -> String {
+ urlPath(year: date.year, month: Month(date.month)).appending("/\(slug).html")
+ }
+}
+
+// MARK: - Post pages
+
+extension PostWriter {
+ func writePosts(_ posts: [Post], to targetURL: URL, with templateRenderer: TemplateRenderer) throws {
+ for post in posts {
+ let postHTML = try templateRenderer.renderTemplate(name: "post", context: [
+ "title": post.title,
+ "post": post,
+ ])
+ let postURL = targetURL
+ .appendingPathComponent(outputPath)
+ .appendingPathComponent(filePath(date: post.date, slug: post.slug))
+ try fileManager.createDirectory(at: postURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
+ try postHTML.write(to: postURL, atomically: true, encoding: .utf8)
+ }
+ }
+
+ private func filePath(date: Date, slug: String) -> String {
+ "/\(date.year)/\(Month(date.month).padded)/\(slug).html"
+ }
+}
+
+// MARK: - Recent posts page
+
+extension PostWriter {
+ func writeRecentPosts(_ recentPosts: [Post], to targetURL: URL, with templateRenderer: TemplateRenderer) throws {
+ let recentPostsHTML = try templateRenderer.renderTemplate(name: "recent-posts", context: [
+ "recentPosts": recentPosts,
+ ])
+ let fileURL = targetURL.appendingPathComponent("index.html")
+ try fileManager.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
+ try recentPostsHTML.write(to: fileURL, atomically: true, encoding: .utf8)
+ }
+}
+
+// MARK: - Post archive page
+
+extension PostWriter {
+ func writeArchive(posts: PostsByYear, to targetURL: URL, with templateRenderer: TemplateRenderer) throws {
+ let allYears = posts.byYear.keys.sorted(by: >)
+ let archiveHTML = try templateRenderer.renderTemplate(name: "posts-archive", context: [
+ "title": "Archive",
+ "years": allYears.map { contextDictionaryForYearPosts(posts[$0]) },
+ ])
+ let archiveURL = targetURL.appendingPathComponent(outputPath).appendingPathComponent("index.html")
+ try fileManager.createDirectory(at: archiveURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
+ try archiveHTML.write(to: archiveURL, atomically: true, encoding: .utf8)
+ }
+
+ private func contextDictionaryForYearPosts(_ posts: YearPosts) -> [String: Any] {
+ [
+ "path": urlPath(year: posts.year),
+ "title": posts.title,
+ "months": posts.months.sorted(by: >).map { month in
+ contextDictionaryForMonthPosts(posts[month], year: posts.year)
+ },
+ ]
+ }
+
+ private func contextDictionaryForMonthPosts(_ posts: MonthPosts, year: Int) -> [String: Any] {
+ [
+ "path": urlPath(year: year, month: posts.month),
+ "name": posts.month.name,
+ "abbreviation": posts.month.abbreviation,
+ "posts": posts.posts.sorted(by: >),
+ ]
+ }
+}
+
+// MARK: - Yearly post index pages
+
+extension PostWriter {
+ func writeYearIndexes(posts: PostsByYear, to targetURL: URL, with templateRenderer: TemplateRenderer) throws {
+ for (year, yearPosts) in posts.byYear {
+ let months = yearPosts.months.sorted(by: >)
+ let yearDir = targetURL.appendingPathComponent(urlPath(year: year))
+ let context: [String: Any] = [
+ "title": yearPosts.title,
+ "path": urlPath(year: year),
+ "year": year,
+ "months": months.map { contextDictionaryForMonthPosts(posts[year][$0], year: year) },
+ ]
+ let yearHTML = try templateRenderer.renderTemplate(name: "posts-year", context: context)
+ let yearURL = yearDir.appendingPathComponent("index.html")
+ try fileManager.createDirectory(at: yearDir, withIntermediateDirectories: true, attributes: nil)
+ try yearHTML.write(to: yearURL, atomically: true, encoding: .utf8)
+ }
+ }
+}
+
+// MARK: - Monthly post roll-up pages
+
+extension PostWriter {
+ func writeMonthRollups(posts: PostsByYear, to targetURL: URL, with templateRenderer: TemplateRenderer) throws {
+ for (year, yearPosts) in posts.byYear {
+ for month in yearPosts.months {
+ let monthDir = targetURL.appendingPathComponent(urlPath(year: year, month: month))
+ let monthHTML = try templateRenderer.renderTemplate(name: "posts-month", context: [
+ "title": "\(month.name) \(year)",
+ "posts": yearPosts[month].posts.sorted(by: >),
+ ])
+ let monthURL = monthDir.appendingPathComponent("index.html")
+ try fileManager.createDirectory(at: monthDir, withIntermediateDirectories: true, attributes: nil)
+ try monthHTML.write(to: monthURL, atomically: true, encoding: .utf8)
+ }
+ }
+ }
+}
diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/PostsByYear.swift b/SiteGenerator/Sources/SiteGenerator/Posts/PostsByYear.swift
index ce06227..726284d 100644
--- a/SiteGenerator/Sources/SiteGenerator/Posts/PostsByYear.swift
+++ b/SiteGenerator/Sources/SiteGenerator/Posts/PostsByYear.swift
@@ -11,6 +11,10 @@ struct MonthPosts {
let month: Month
var posts: [Post]
+ var title: String {
+ month.padded
+ }
+
var isEmpty: Bool {
posts.isEmpty
}
@@ -20,6 +24,18 @@ struct YearPosts {
let year: Int
var byMonth: [Month: MonthPosts]
+ var title: String {
+ "\(year)"
+ }
+
+ var isEmpty: Bool {
+ byMonth.isEmpty || byMonth.values.allSatisfy { $0.isEmpty }
+ }
+
+ var months: [Month] {
+ Array(byMonth.keys)
+ }
+
subscript(month: Month) -> MonthPosts {
get {
byMonth[month, default: MonthPosts(month: month, posts: [])]
@@ -28,10 +44,6 @@ struct YearPosts {
byMonth[month] = newValue
}
}
-
- var isEmpty: Bool {
- byMonth.isEmpty || byMonth.values.allSatisfy { $0.isEmpty }
- }
}
struct PostsByYear {
@@ -60,8 +72,8 @@ struct PostsByYear {
self[year][month].posts.append(post)
}
- /// Returns posts sorted by reverse date.
+ /// Returns an array of all posts.
func flattened() -> [Post] {
- byYear.values.flatMap { $0.byMonth.values.flatMap { $0.posts } }.sorted { $1.date < $0.date }
+ byYear.values.flatMap { $0.byMonth.values.flatMap { $0.posts } }
}
}
diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/PostsPlugin.swift b/SiteGenerator/Sources/SiteGenerator/Posts/PostsPlugin.swift
index f63b9c0..d6ee7d0 100644
--- a/SiteGenerator/Sources/SiteGenerator/Posts/PostsPlugin.swift
+++ b/SiteGenerator/Sources/SiteGenerator/Posts/PostsPlugin.swift
@@ -6,225 +6,38 @@
//
import Foundation
-import Ink
final class PostsPlugin: Plugin {
- let fileManager: FileManager = .default
- let markdownParser = MarkdownParser()
- let postsPath: String
- let recentPostsPath: String
+ let postRepo: PostRepo
+ let postWriter: PostWriter
- var posts: PostsByYear!
- var sourceURL: URL!
-
- init(postsPath: String = "posts", recentPostsPath: String = "index.html") {
- self.postsPath = postsPath
- self.recentPostsPath = recentPostsPath
+ init(
+ postRepo: PostRepo = PostRepo(),
+ postWriter: PostWriter = PostWriter()
+ ) {
+ self.postRepo = postRepo
+ self.postWriter = postWriter
}
+ // MARK: - Plugin methods
+
func setUp(sourceURL: URL) throws {
- self.sourceURL = sourceURL
- let postsURL = sourceURL.appendingPathComponent("posts")
- guard fileManager.fileExists(atPath: postsURL.path) else {
+ guard postRepo.postDataExists(at: sourceURL) else {
return
}
- let posts = try enumerateMarkdownFiles(directory: postsURL)
- .compactMap { (url: URL) -> Post? in
- do {
- let markdown = try String(contentsOf: url)
- let result = markdownParser.parse(markdown)
- let slug = url.deletingPathExtension().lastPathComponent
- return try Post(slug: slug, bodyMarkdown: result.html, metadata: result.metadata)
- }
- catch {
- print("Cannot create post from markdown file \(url): \(error)")
- return nil
- }
- }
- self.posts = PostsByYear(posts: posts)
+ try postRepo.readPosts(sourceURL: sourceURL, makePath: postWriter.urlPathForPost)
}
func render(targetURL: URL, templateRenderer: TemplateRenderer) throws {
- guard posts != nil, !posts.isEmpty else {
+ guard !postRepo.isEmpty else {
return
}
- let postsDir = targetURL.appendingPathComponent(postsPath)
- try renderPostsByDate(postsDir: postsDir, templateRenderer: templateRenderer)
- try renderYears(postsDir: postsDir, templateRenderer: templateRenderer)
- try renderMonths(postsDir: postsDir, templateRenderer: templateRenderer)
- try renderArchive(postsDir: postsDir, templateRenderer: templateRenderer)
- try renderRecentPosts(targetURL: targetURL, templateRenderer: templateRenderer)
- }
-
- func renderPostsByDate(postsDir: URL, templateRenderer: TemplateRenderer) throws {
- print("renderPostsByDate(postsDir: \(postsDir), templateRenderer: \(templateRenderer)")
- for post in posts.flattened() {
- let monthDir = postsDir
- .appendingPathComponent("\(post.date.year)")
- .appendingPathComponent(Month(post.date.month).padded)
- try renderPost(post, monthDir: monthDir, templateRenderer: templateRenderer)
- }
- }
-
- func renderRecentPosts(targetURL: URL, templateRenderer: TemplateRenderer) throws {
- print("renderRecentPosts(targetURL: \(targetURL), templateRenderer: \(templateRenderer)")
- let recentPosts = posts.flattened().prefix(10)
- let renderedRecentPosts: [[String: Any]] = recentPosts.map { post in
- let html = markdownParser.html(from: post.bodyMarkdown)
- let path = self.path(for: post)
- return RenderedPost(path: path, post: post, body: html).dictionary
- }
- let recentPostsHTML = try templateRenderer.renderTemplate(name: "recent-posts", context: [
- "recentPosts": renderedRecentPosts,
- ])
- let fileURL = targetURL.appendingPathComponent(recentPostsPath)
- try recentPostsHTML.write(to: fileURL, atomically: true, encoding: .utf8)
- }
-
- func renderArchive(postsDir: URL, templateRenderer: TemplateRenderer) throws {
- print("renderArchive(postsDir: \(postsDir), templateRenderer: \(templateRenderer)")
- let allYears = posts.byYear.keys.sorted(by: >)
- let allMonths = (1 ... 12).map(Month.init(_:))
- let yearsWithPostsByMonthForContext: [[String: Any]] = allYears.map { year in
- [
- "path": self.path(year: year),
- "title": "\(year)",
- "months": posts[year].byMonth.keys.sorted(by: >).map { (month: Month) -> [String: Any] in
- let sortedPosts = posts[year][month].posts.sorted(by: { $0.date > $1.date })
- return [
- "path": self.path(year: year, month: month),
- "title": month.padded,
- "posts": sortedPosts.map { $0.dictionary(withPath: self.path(for: $0)) },
- ]
- },
- ]
- }
- let context: [String: Any] = [
- "title": "Archive",
- "years": yearsWithPostsByMonthForContext,
- "monthNames": allMonths.reduce(into: [String: String](), { dict, month in
- dict[month.padded] = month.name
- }),
- "monthAbbreviations": allMonths.reduce(into: [String: String](), { dict, month in
- dict[month.padded] = month.abbreviatedName
- }),
- ]
- let archiveHTML = try templateRenderer.renderTemplate(name: "posts-archive", context: context)
- let archiveURL = postsDir.appendingPathComponent("index.html")
- try archiveHTML.write(to: archiveURL, atomically: true, encoding: .utf8)
- }
-
- func renderYears(postsDir: URL, templateRenderer: TemplateRenderer) throws {
- print("renderYears(postsDir: \(postsDir), templateRenderer: \(templateRenderer)")
- let allMonths = (1 ... 12).map(Month.init(_:))
- for (year, monthPosts) in posts.byYear.sorted(by: { $1.key < $0.key }) {
- let yearDir = postsDir.appendingPathComponent("\(year)")
- var sortedPostsByMonth: [Month: [Post]] = [:]
- for month in allMonths {
- let sortedPosts = monthPosts[month].posts.sorted(by: { $1.date < $0.date })
- if !sortedPosts.isEmpty {
- sortedPostsByMonth[month] = sortedPosts
- }
- }
-
- try fileManager.createDirectory(at: yearDir, withIntermediateDirectories: true, attributes: nil)
- let months = Array(sortedPostsByMonth.keys.sorted().reversed())
- let postsByMonthForContext: [String: [[String: Any]]] = sortedPostsByMonth.reduce(into: [:]) { dict, pair in
- let (month, posts) = pair
- dict[month.padded] = posts.map { $0.dictionary(withPath: self.path(for: $0)) }
- }
- let context: [String: Any] = [
- "title": "\(year)",
- "path": postsPath,
- "year": year,
- "months": months.map { $0.padded },
- "monthNames": months.reduce(into: [String: String](), { dict, month in
- dict[month.padded] = month.name
- }),
- "monthAbbreviations": months.reduce(into: [String: String](), { dict, month in
- dict[month.padded] = month.abbreviatedName
- }),
- "postsByMonth": postsByMonthForContext,
- ]
- 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)
- }
- }
-
- func renderMonths(postsDir: URL, templateRenderer: TemplateRenderer) throws {
- print("renderMonths(postsDir: \(postsDir), templateRenderer: \(templateRenderer)")
- let allMonths = (1 ... 12).map(Month.init(_:))
- for (year, monthPosts) in posts.byYear.sorted(by: { $1.key < $0.key }) {
- let yearDir = postsDir.appendingPathComponent("\(year)")
- for month in allMonths {
- let sortedPosts = monthPosts[month].posts.sorted(by: { $1.date < $0.date })
- guard !sortedPosts.isEmpty else {
- continue
- }
-
- let renderedPosts = sortedPosts.map { post -> RenderedPost in
- let path = self.path(for: post)
- let bodyHTML = markdownParser.html(from: post.bodyMarkdown)
- return RenderedPost(path: path, post: post, body: bodyHTML)
- }
- let monthDir = yearDir.appendingPathComponent(month.padded)
- try fileManager.createDirectory(at: monthDir, withIntermediateDirectories: true, attributes: nil)
- let context: [String: Any] = [
- "title": "\(month.name) \(year)",
- "posts": renderedPosts.map { $0.dictionary },
- ]
- 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)
- }
- }
- }
-
- private func renderPost(_ post: Post, monthDir: URL, templateRenderer: TemplateRenderer) throws {
- print("renderPost(\(post.debugDescription), monthDir: \(monthDir), templateRenderer: \(templateRenderer)")
- try fileManager.createDirectory(at: monthDir, withIntermediateDirectories: true, attributes: nil)
- let filename = "\(post.slug).html"
- let path = self.path(for: post)
- let postURL = monthDir.appendingPathComponent(filename)
- let bodyHTML = markdownParser.html(from: post.bodyMarkdown)
- let renderedPost = RenderedPost(path: path, post: post, body: bodyHTML)
- let postHTML = try templateRenderer.renderTemplate(name: "post", context: [
- "title": "\(renderedPost.post.title)",
- "post": renderedPost.dictionary,
- ])
- try postHTML.write(to: postURL, atomically: true, encoding: .utf8)
- }
-
- private func enumerateMarkdownFiles(directory: URL) throws -> [URL] {
- 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] : []
- }
- }
- }
-
- private func path(for post: Post) -> String {
- path(year: post.date.year, month: Month(post.date.month), filename: "\(post.slug).html")
- }
-
- private func path(year: Int) -> String {
- "/\(postsPath)/\(year)"
- }
-
- private func path(year: Int, month: Month) -> String {
- path(year: year).appending("/\(month.padded)")
- }
-
- private func path(year: Int, month: Month, filename: String) -> String {
- path(year: year, month: month).appending("/\(filename)")
+ try postWriter.writeRecentPosts(postRepo.recentPosts, to: targetURL, with: templateRenderer)
+ try postWriter.writePosts(postRepo.sortedPosts, to: targetURL, with: templateRenderer)
+ try postWriter.writeArchive(posts: postRepo.posts, to: targetURL, with: templateRenderer)
+ try postWriter.writeYearIndexes(posts: postRepo.posts, to: targetURL, with: templateRenderer)
+ try postWriter.writeMonthRollups(posts: postRepo.posts, to: targetURL, with: templateRenderer)
}
}
diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/RenderedPost.swift b/SiteGenerator/Sources/SiteGenerator/Posts/RenderedPost.swift
deleted file mode 100644
index 245b603..0000000
--- a/SiteGenerator/Sources/SiteGenerator/Posts/RenderedPost.swift
+++ /dev/null
@@ -1,27 +0,0 @@
-//
-// RenderedPost.swift
-// SiteGenerator
-//
-// Created by Sami Samhuri on 2019-12-03.
-//
-
-import Foundation
-
-struct RenderedPost {
- let path: String
- let post: Post
- let body: String
-
- var dictionary: [String: Any] {
- [
- "author": post.author,
- "title": post.title,
- "date": post.date,
- "day": post.date.day,
- "formattedDate": post.formattedDate,
- "link": post.link as Any,
- "path": path,
- "body": body,
- ]
- }
-}
diff --git a/templates/posts-archive.html b/templates/posts-archive.html
index 6e4b9c0..7e20452 100644
--- a/templates/posts-archive.html
+++ b/templates/posts-archive.html
@@ -12,7 +12,7 @@
{% for month in year.months %}
@@ -23,7 +23,7 @@
{% else %}
{{ post.title }}
{% endif %}
-
+
{% if post.isLink %}
∞
{% endif %}
diff --git a/templates/posts-year.html b/templates/posts-year.html
index c5bf75f..c3206e6 100644
--- a/templates/posts-year.html
+++ b/templates/posts-year.html
@@ -7,18 +7,21 @@
{% for month in months %}
- {% for post in postsByMonth[month] %}
+ {% for post in month.posts %}
-
{% if post.isLink %}
→ {{ post.title }}
{% else %}
{{ post.title }}
{% endif %}
-
+
+ {% if post.isLink %}
+ ∞
+ {% endif %}
{% endfor %}