Factor most of the code out of PostsPlugin

This commit is contained in:
Sami Samhuri 2019-12-09 23:26:54 -08:00
parent 5fac69542c
commit 4b3dee6706
14 changed files with 434 additions and 311 deletions

View file

@ -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

View file

@ -0,0 +1,34 @@
//
// FunctionComposition.swift
// SiteGenerator
//
// Created by Sami Samhuri on 2019-11-18.
//
import Foundation
infix operator |> :AdditionPrecedence
// MARK: Synchronous
public func |> <A, B, C> (
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 |> <A, B, C> (
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
}
}

View file

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

View file

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

View file

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

View file

@ -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 {
"<Post slug=\(slug) title=\"\(title)\" date=\"\(formattedDate)\" link=\(link?.absoluteString ?? "no")>"
"<Post path=\(path) title=\"\(title)\" date=\"\(formattedDate)\" link=\(link?.absoluteString ?? "no")>"
}
}

View file

@ -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] : []
}
}
}
}

View file

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

View file

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

View file

@ -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 } }
}
}

View file

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

View file

@ -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,
]
}
}

View file

@ -12,7 +12,7 @@
{% for month in year.months %}
<h3>
<a href="{{ month.path }}">{{ monthNames[month.title] }}</a>
<a href="{{ month.path }}">{{ month.name }}</a>
</h3>
<ul class="archive">
@ -23,7 +23,7 @@
{% else %}
<a href="{{ post.path }}">{{ post.title }}</a>
{% endif %}
<time>{{ post.day }} {{ monthAbbreviations[month.title] }}</time>
<time>{{ post.day }} {{ month.abbreviation }}</time>
{% if post.isLink %}
<a class="permalink" href="{{ post.path }}">&infin;</a>
{% endif %}

View file

@ -7,18 +7,21 @@
{% for month in months %}
<h2>
<a href="/{{ path }}/{{ year }}/{{ month }}">{{ monthNames[month] }}</a>
<a href="{{ month.path }}">{{ month.name }}</a>
</h2>
<ul class="archive">
{% for post in postsByMonth[month] %}
{% for post in month.posts %}
<li>
{% if post.isLink %}
<a href="{{ post.path }}">&rarr; {{ post.title }}</a>
{% else %}
<a href="{{ post.path }}">{{ post.title }}</a>
{% endif %}
<time>{{ post.day }} {{ monthAbbreviations[month] }}</time>
<time>{{ post.day }} {{ month.abbreviation }}</time>
{% if post.isLink %}
<a class="permalink" href="{{ post.path }}">&infin;</a>
{% endif %}
</li>
{% endfor %}
</ul>