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,
|
||||
"styles": site.styles + page.styles,
|
||||
"scripts": site.scripts + page.scripts,
|
||||
"currentYear": Date.currentYear,
|
||||
"currentYear": Date().year,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ extension SiteContext: TemplateContext {
|
|||
"title": site.title,
|
||||
"styles": site.styles,
|
||||
"scripts": site.scripts,
|
||||
"currentYear": Date.currentYear,
|
||||
"currentYear": Date().year,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,11 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import PathKit
|
||||
import Stencil
|
||||
|
||||
public final class Generator: PluginDelegate, RendererDelegate {
|
||||
public final class Generator {
|
||||
// Dependencies
|
||||
let fileManager: FileManager = .default
|
||||
let templateRenderer: Environment
|
||||
let templateRenderer: TemplateRenderer
|
||||
|
||||
// Site properties
|
||||
let site: Site
|
||||
|
|
@ -21,13 +19,13 @@ public final class Generator: PluginDelegate, RendererDelegate {
|
|||
let renderers: [Renderer]
|
||||
|
||||
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")
|
||||
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.plugins = plugins
|
||||
self.renderers = renderers
|
||||
|
|
@ -39,7 +37,7 @@ public final class Generator: PluginDelegate, RendererDelegate {
|
|||
|
||||
public func generate(targetURL: URL) throws {
|
||||
for plugin in plugins {
|
||||
try plugin.render(targetURL: targetURL, delegate: self)
|
||||
try plugin.render(targetURL: targetURL, templateRenderer: templateRenderer)
|
||||
}
|
||||
|
||||
let publicURL = sourceURL.appendingPathComponent("public")
|
||||
|
|
@ -77,7 +75,7 @@ public final class Generator: PluginDelegate, RendererDelegate {
|
|||
let ext = String(filename.split(separator: ".").last!)
|
||||
for renderer in renderers {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -86,20 +84,4 @@ public final class Generator: PluginDelegate, RendererDelegate {
|
|||
let dest = targetDir.appendingPathComponent(filename)
|
||||
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
|
||||
|
||||
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 {
|
||||
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
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
|
@ -46,13 +46,13 @@ final class ProjectsPlugin: Plugin {
|
|||
let projectsDir = targetURL.appendingPathComponent(path)
|
||||
try fileManager.createDirectory(at: projectsDir, withIntermediateDirectories: true, attributes: nil)
|
||||
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)
|
||||
|
||||
for project in projects {
|
||||
let filename = "\(project.title).html"
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public final class LessRenderer: Renderer {
|
|||
}
|
||||
|
||||
/// 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 cssURL = targetDir.appendingPathComponent(filename.replacingOccurrences(of: ".less", with: ".css"))
|
||||
let less = try String(contentsOf: fileURL, encoding: .utf8)
|
||||
|
|
|
|||
|
|
@ -9,26 +9,26 @@ import Foundation
|
|||
import Ink
|
||||
|
||||
public final class MarkdownRenderer: Renderer {
|
||||
let mdParser = MarkdownParser()
|
||||
let markdownParser = MarkdownParser()
|
||||
|
||||
public func canRenderFile(named filename: String, withExtension ext: String) -> Bool {
|
||||
ext == "md"
|
||||
}
|
||||
|
||||
/// 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 htmlFilename = mdFilename.replacingOccurrences(of: ".md", with: ".html")
|
||||
let htmlURL = targetDir.appendingPathComponent(htmlFilename)
|
||||
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 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)
|
||||
}
|
||||
|
||||
func markdownMetadata(from url: URL) throws -> [String: String] {
|
||||
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 generator = try Generator(
|
||||
sourceURL: sourceURL,
|
||||
plugins: [ProjectsPlugin()],
|
||||
plugins: [ProjectsPlugin(), PostsPlugin()],
|
||||
renderers: [LessRenderer(), MarkdownRenderer()]
|
||||
)
|
||||
try generator.generate(targetURL: targetURL)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<article class="container">
|
||||
<header>
|
||||
<h1><a href="{{ post.link }}">→ {{ post.title }}</a></h1>
|
||||
<time>{{ post.date }}</time>
|
||||
<time>{{ post.formattedDate }}</time>
|
||||
<a class="permalink" href="{{ post.url }}">∞</a>
|
||||
</header>
|
||||
{{ post.body }}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<article class="container">
|
||||
<header>
|
||||
<h1><a href="{{ post.url }}">{{ post.title }}</a></h1>
|
||||
<time>{{ post.date }}</time>
|
||||
<time>{{ post.formattedDate }}</time>
|
||||
</header>
|
||||
{{ post.body }}
|
||||
</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