mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-04-27 14:57:40 +00:00
Port site.json to Swift code in samhuri_net module
This commit is contained in:
parent
487875098a
commit
44fef3fb78
20 changed files with 319 additions and 152 deletions
|
|
@ -115,7 +115,7 @@ Execution, trying TDD for the first time:
|
||||||
|
|
||||||
- [x] Create new packages and distribute the code accordingly
|
- [x] Create new packages and distribute the code accordingly
|
||||||
|
|
||||||
- [ ] Replace site.json with Swift code
|
- [x] Replace site.json with Swift code
|
||||||
|
|
||||||
- [ ] Replace page template with Swift code
|
- [ ] Replace page template with Swift code
|
||||||
|
|
||||||
|
|
|
||||||
20
SiteGenerator/Sources/SiteGenerator/BuiltSite.swift
Normal file
20
SiteGenerator/Sources/SiteGenerator/BuiltSite.swift
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// BuiltSite.swift
|
||||||
|
// SiteGenerator
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2019-12-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct BuiltSite {
|
||||||
|
public let site: Site
|
||||||
|
public let plugins: [Plugin]
|
||||||
|
public let renderers: [Renderer]
|
||||||
|
|
||||||
|
init(site: Site, plugins: [Plugin], renderers: [Renderer]) {
|
||||||
|
self.site = site
|
||||||
|
self.plugins = plugins
|
||||||
|
self.renderers = renderers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,10 +15,6 @@ struct PageContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PageContext {
|
extension PageContext {
|
||||||
var template: String {
|
|
||||||
page.template ?? site.template
|
|
||||||
}
|
|
||||||
|
|
||||||
var dictionary: [String: Any] {
|
var dictionary: [String: Any] {
|
||||||
[
|
[
|
||||||
"site": site,
|
"site": site,
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,9 @@ import Foundation
|
||||||
|
|
||||||
struct SiteContext {
|
struct SiteContext {
|
||||||
let site: Site
|
let site: Site
|
||||||
let template: String
|
|
||||||
|
|
||||||
init(site: Site, template: String? = nil) {
|
init(site: Site) {
|
||||||
self.site = site
|
self.site = site
|
||||||
self.template = template ?? site.template
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
//
|
|
||||||
// HumanSite.swift
|
|
||||||
// SiteGenerator
|
|
||||||
//
|
|
||||||
// Created by Sami Samhuri on 2019-12-01.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// This is used to make the JSON simpler to write with optionals.
|
|
||||||
struct HumanSite: Codable {
|
|
||||||
let author: String
|
|
||||||
let email: String?
|
|
||||||
let title: String
|
|
||||||
let description: String?
|
|
||||||
let url: URL
|
|
||||||
let template: String?
|
|
||||||
let styles: [String]?
|
|
||||||
let scripts: [String]?
|
|
||||||
let avatar: String?
|
|
||||||
let icon: String?
|
|
||||||
let favicon: String?
|
|
||||||
let plugins: [String: [String: String]]?
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Site {
|
|
||||||
init(humanSite: HumanSite) {
|
|
||||||
self.init(
|
|
||||||
author: humanSite.author,
|
|
||||||
email: humanSite.email,
|
|
||||||
title: humanSite.title,
|
|
||||||
description: humanSite.description,
|
|
||||||
url: humanSite.url,
|
|
||||||
template: humanSite.template ?? "page",
|
|
||||||
styles: humanSite.styles ?? [],
|
|
||||||
scripts: humanSite.scripts ?? [],
|
|
||||||
avatarPath: humanSite.avatar,
|
|
||||||
iconPath: humanSite.icon,
|
|
||||||
faviconPath: humanSite.favicon,
|
|
||||||
plugins: (humanSite.plugins ?? [:]).reduce(into: [:], { dict, pair in
|
|
||||||
let (name, options) = pair
|
|
||||||
guard let sitePlugin = SitePlugin(rawValue: name) else {
|
|
||||||
print("warning: unknown site plugin \"\(name)\"")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dict[sitePlugin] = options
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -12,23 +12,31 @@ public struct Site {
|
||||||
public let email: String?
|
public let email: String?
|
||||||
public let title: String
|
public let title: String
|
||||||
public let description: String?
|
public let description: String?
|
||||||
public var url: URL
|
public let url: URL
|
||||||
public let template: String
|
|
||||||
public let styles: [String]
|
public let styles: [String]
|
||||||
public let scripts: [String]
|
public let scripts: [String]
|
||||||
|
public let renderers: [Renderer]
|
||||||
|
public let plugins: [Plugin]
|
||||||
|
|
||||||
// Used for JSON feed
|
public init(
|
||||||
public let avatarPath: String?
|
author: String,
|
||||||
public let iconPath: String?
|
email: String?,
|
||||||
public let faviconPath: String?
|
title: String,
|
||||||
|
description: String?,
|
||||||
public let plugins: [SitePlugin: [String: Any]]
|
url: URL,
|
||||||
}
|
styles: [String],
|
||||||
|
scripts: [String],
|
||||||
extension Site {
|
renderers: [Renderer],
|
||||||
static func decode(from url: URL) throws -> Site {
|
plugins: [Plugin]
|
||||||
let json = try Data(contentsOf: url)
|
) {
|
||||||
let humanSite = try JSONDecoder().decode(HumanSite.self, from: json)
|
self.author = author
|
||||||
return Site(humanSite: humanSite)
|
self.email = email
|
||||||
|
self.title = title
|
||||||
|
self.description = description
|
||||||
|
self.url = url
|
||||||
|
self.styles = styles
|
||||||
|
self.scripts = scripts
|
||||||
|
self.renderers = renderers
|
||||||
|
self.plugins = plugins
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,16 @@ final class SiteTemplateRenderer: TemplateRenderer {
|
||||||
self.stencil = Environment(loader: loader)
|
self.stencil = Environment(loader: loader)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderPage(bodyHTML: String, metadata: [String: String]) throws -> String {
|
func renderPage(template: String, bodyHTML: String, metadata: [String: String]) throws -> String {
|
||||||
let page = Page(metadata: metadata)
|
let page = Page(metadata: metadata)
|
||||||
let context = PageContext(site: site, body: bodyHTML, 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)
|
let pageHTML = try stencil.renderTemplate(name: template, context: context.dictionary)
|
||||||
return pageHTML
|
return pageHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderTemplate(name: String?, context: [String: Any]) throws -> String {
|
func renderTemplate(name: String, context: [String: Any]) throws -> String {
|
||||||
let siteContext = SiteContext(site: site, template: name)
|
let siteContext = SiteContext(site: site)
|
||||||
let contextDict = siteContext.dictionary.merging(context, uniquingKeysWith: { _, new in new })
|
let contextDict = siteContext.dictionary.merging(context, uniquingKeysWith: { _, new in new })
|
||||||
return try stencil.renderTemplate(name: siteContext.template, context: contextDict)
|
return try stencil.renderTemplate(name: name, context: contextDict)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public protocol TemplateRenderer: AnyObject {
|
public protocol TemplateRenderer: AnyObject {
|
||||||
func renderPage(bodyHTML: String, metadata: [String: String]) throws -> String
|
func renderPage(template: String, bodyHTML: String, metadata: [String: String]) throws -> String
|
||||||
func renderTemplate(name: String?, context: [String: Any]) throws -> String
|
func renderTemplate(name: String, context: [String: Any]) throws -> String
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,11 @@ private struct FeedItem: Codable {
|
||||||
|
|
||||||
final class JSONFeedWriter {
|
final class JSONFeedWriter {
|
||||||
let fileManager: FileManager
|
let fileManager: FileManager
|
||||||
let feedPath: String
|
let jsonFeed: JSONFeed
|
||||||
|
|
||||||
init(fileManager: FileManager = .default, feedPath: String = "feed.json") {
|
init(fileManager: FileManager = .default, feed: JSONFeed) {
|
||||||
self.fileManager = fileManager
|
self.fileManager = fileManager
|
||||||
self.feedPath = feedPath
|
self.jsonFeed = feed
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeFeed(_ posts: [Post], site: Site, to targetURL: URL, with templateRenderer: TemplateRenderer) throws {
|
func writeFeed(_ posts: [Post], site: Site, to targetURL: URL, with templateRenderer: TemplateRenderer) throws {
|
||||||
|
|
@ -60,21 +60,21 @@ final class JSONFeedWriter {
|
||||||
tags: post.tags
|
tags: post.tags
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
let avatar = site.avatarPath.map(site.url.appendingPathComponent)
|
let avatar = jsonFeed.avatarPath.map(site.url.appendingPathComponent)
|
||||||
let feed: Feed = Feed(
|
let feed: Feed = Feed(
|
||||||
title: site.title,
|
title: site.title,
|
||||||
home_page_url: site.url.absoluteString,
|
home_page_url: site.url.absoluteString,
|
||||||
feed_url: site.url.appendingPathComponent(feedPath).absoluteString,
|
feed_url: site.url.appendingPathComponent(jsonFeed.path).absoluteString,
|
||||||
author: FeedAuthor(name: site.author, avatar: avatar?.absoluteString, url: site.url.absoluteString),
|
author: FeedAuthor(name: site.author, avatar: avatar?.absoluteString, url: site.url.absoluteString),
|
||||||
icon: site.iconPath.map(site.url.appendingPathComponent)?.absoluteString,
|
icon: jsonFeed.iconPath.map(site.url.appendingPathComponent)?.absoluteString,
|
||||||
favicon: site.faviconPath.map(site.url.appendingPathComponent)?.absoluteString,
|
favicon: jsonFeed.faviconPath.map(site.url.appendingPathComponent)?.absoluteString,
|
||||||
items: items
|
items: items
|
||||||
)
|
)
|
||||||
let encoder = JSONEncoder()
|
let encoder = JSONEncoder()
|
||||||
encoder.dateEncodingStrategy = .iso8601
|
encoder.dateEncodingStrategy = .iso8601
|
||||||
encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes]
|
encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes]
|
||||||
let feedJSON = try encoder.encode(feed)
|
let feedJSON = try encoder.encode(feed)
|
||||||
let feedURL = targetURL.appendingPathComponent(feedPath)
|
let feedURL = targetURL.appendingPathComponent(jsonFeed.path)
|
||||||
try feedJSON.write(to: feedURL, options: [.atomic])
|
try feedJSON.write(to: feedURL, options: [.atomic])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,11 @@ private extension Date {
|
||||||
|
|
||||||
final class RSSFeedWriter {
|
final class RSSFeedWriter {
|
||||||
let fileManager: FileManager
|
let fileManager: FileManager
|
||||||
let feedPath: String
|
let feed: RSSFeed
|
||||||
|
|
||||||
init(fileManager: FileManager = .default, feedPath: String = "feed.xml") {
|
init(fileManager: FileManager = .default, feed: RSSFeed) {
|
||||||
self.fileManager = fileManager
|
self.fileManager = fileManager
|
||||||
self.feedPath = feedPath
|
self.feed = feed
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeFeed(_ posts: [Post], site: Site, to targetURL: URL, with templateRenderer: TemplateRenderer) throws {
|
func writeFeed(_ posts: [Post], site: Site, to targetURL: URL, with templateRenderer: TemplateRenderer) throws {
|
||||||
|
|
@ -74,10 +74,10 @@ final class RSSFeedWriter {
|
||||||
}
|
}
|
||||||
let feedXML = try templateRenderer.renderTemplate(name: "feed.xml", context: [
|
let feedXML = try templateRenderer.renderTemplate(name: "feed.xml", context: [
|
||||||
"site": feedSite,
|
"site": feedSite,
|
||||||
"feedURL": site.url.appendingPathComponent(feedPath).absoluteString.escapedForXML(),
|
"feedURL": site.url.appendingPathComponent(feed.path).absoluteString.escapedForXML(),
|
||||||
"posts": renderedPosts,
|
"posts": renderedPosts,
|
||||||
])
|
])
|
||||||
let feedURL = targetURL.appendingPathComponent(feedPath)
|
let feedURL = targetURL.appendingPathComponent(feed.path)
|
||||||
try feedXML.write(to: feedURL, atomically: true, encoding: .utf8)
|
try feedXML.write(to: feedURL, atomically: true, encoding: .utf8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
SiteGenerator/Sources/SiteGenerator/Posts/JSONFeed.swift
Normal file
15
SiteGenerator/Sources/SiteGenerator/Posts/JSONFeed.swift
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
//
|
||||||
|
// JSONFeed.swift
|
||||||
|
// SiteGenerator
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2019-12-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct JSONFeed {
|
||||||
|
let path: String
|
||||||
|
let avatarPath: String?
|
||||||
|
let iconPath: String?
|
||||||
|
let faviconPath: String?
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
final class PostsPlugin: Plugin {
|
public final class PostsPlugin: Plugin {
|
||||||
let postRepo: PostRepo
|
let postRepo: PostRepo
|
||||||
let postWriter: PostWriter
|
let postWriter: PostWriter
|
||||||
let jsonFeedWriter: JSONFeedWriter?
|
let jsonFeedWriter: JSONFeedWriter?
|
||||||
|
|
@ -41,7 +41,8 @@ final class PostsPlugin: Plugin {
|
||||||
|
|
||||||
let jsonFeedWriter: JSONFeedWriter?
|
let jsonFeedWriter: JSONFeedWriter?
|
||||||
if let jsonFeedPath = options["json_feed"] as? String {
|
if let jsonFeedPath = options["json_feed"] as? String {
|
||||||
jsonFeedWriter = JSONFeedWriter(feedPath: jsonFeedPath)
|
let jsonFeed = JSONFeed(path: jsonFeedPath, avatarPath: nil, iconPath: nil, faviconPath: nil)
|
||||||
|
jsonFeedWriter = JSONFeedWriter(feed: jsonFeed)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
jsonFeedWriter = nil
|
jsonFeedWriter = nil
|
||||||
|
|
@ -49,7 +50,8 @@ final class PostsPlugin: Plugin {
|
||||||
|
|
||||||
let rssFeedWriter: RSSFeedWriter?
|
let rssFeedWriter: RSSFeedWriter?
|
||||||
if let rssFeedPath = options["rss_feed"] as? String {
|
if let rssFeedPath = options["rss_feed"] as? String {
|
||||||
rssFeedWriter = RSSFeedWriter(feedPath: rssFeedPath)
|
let rssFeed = RSSFeed(path: rssFeedPath)
|
||||||
|
rssFeedWriter = RSSFeedWriter(feed: rssFeed)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
rssFeedWriter = nil
|
rssFeedWriter = nil
|
||||||
|
|
@ -58,7 +60,7 @@ final class PostsPlugin: Plugin {
|
||||||
self.init(postRepo: postRepo, postWriter: postWriter, jsonFeedWriter: jsonFeedWriter, rssFeedWriter: rssFeedWriter)
|
self.init(postRepo: postRepo, postWriter: postWriter, jsonFeedWriter: jsonFeedWriter, rssFeedWriter: rssFeedWriter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setUp(site: Site, sourceURL: URL) throws {
|
public func setUp(site: Site, sourceURL: URL) throws {
|
||||||
guard postRepo.postDataExists(at: sourceURL) else {
|
guard postRepo.postDataExists(at: sourceURL) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +68,7 @@ final class PostsPlugin: Plugin {
|
||||||
try postRepo.readPosts(sourceURL: sourceURL)
|
try postRepo.readPosts(sourceURL: sourceURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(site: Site, targetURL: URL, templateRenderer: TemplateRenderer) throws {
|
public func render(site: Site, targetURL: URL, templateRenderer: TemplateRenderer) throws {
|
||||||
guard !postRepo.isEmpty else {
|
guard !postRepo.isEmpty else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
//
|
||||||
|
// PostsPluginBuilder.swift
|
||||||
|
// SiteGenerator
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2019-12-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public final class PostsPluginBuilder {
|
||||||
|
private var path: String?
|
||||||
|
private var jsonFeed: JSONFeed?
|
||||||
|
private var rssFeed: RSSFeed?
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
public func path(_ path: String) -> PostsPluginBuilder {
|
||||||
|
precondition(self.path == nil, "path is already defined")
|
||||||
|
self.path = path
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
public func jsonFeed(
|
||||||
|
path: String? = nil,
|
||||||
|
avatarPath: String? = nil,
|
||||||
|
iconPath: String? = nil,
|
||||||
|
faviconPath: String? = nil
|
||||||
|
) -> PostsPluginBuilder {
|
||||||
|
precondition(jsonFeed == nil, "JSON feed is already defined")
|
||||||
|
jsonFeed = JSONFeed(
|
||||||
|
path: path ?? "feed.json",
|
||||||
|
avatarPath: avatarPath,
|
||||||
|
iconPath: iconPath,
|
||||||
|
faviconPath: faviconPath
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
public func rssFeed(path: String? = nil) -> PostsPluginBuilder {
|
||||||
|
precondition(rssFeed == nil, "RSS feed is already defined")
|
||||||
|
rssFeed = RSSFeed(path: path ?? "feed.xml")
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
public func build() -> PostsPlugin {
|
||||||
|
let postRepo: PostRepo
|
||||||
|
let postWriter: PostWriter
|
||||||
|
if let outputPath = path {
|
||||||
|
postRepo = PostRepo(outputPath: outputPath)
|
||||||
|
postWriter = PostWriter(outputPath: outputPath)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
postRepo = PostRepo()
|
||||||
|
postWriter = PostWriter()
|
||||||
|
}
|
||||||
|
|
||||||
|
let jsonFeedWriter: JSONFeedWriter?
|
||||||
|
if let jsonFeed = jsonFeed {
|
||||||
|
jsonFeedWriter = JSONFeedWriter(feed: jsonFeed)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
jsonFeedWriter = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let rssFeedWriter: RSSFeedWriter?
|
||||||
|
if let rssFeed = rssFeed {
|
||||||
|
rssFeedWriter = RSSFeedWriter(feed: rssFeed)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rssFeedWriter = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return PostsPlugin(
|
||||||
|
postRepo: postRepo,
|
||||||
|
postWriter: postWriter,
|
||||||
|
jsonFeedWriter: jsonFeedWriter,
|
||||||
|
rssFeedWriter: rssFeedWriter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
SiteGenerator/Sources/SiteGenerator/Posts/RSSFeed.swift
Normal file
12
SiteGenerator/Sources/SiteGenerator/Posts/RSSFeed.swift
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
//
|
||||||
|
// RSSFeed.swift
|
||||||
|
// SiteGenerator
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2019-12-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct RSSFeed {
|
||||||
|
let path: String
|
||||||
|
}
|
||||||
|
|
@ -24,8 +24,8 @@ final class ProjectsPlugin: Plugin {
|
||||||
var projects: [Project] = []
|
var projects: [Project] = []
|
||||||
var sourceURL: URL!
|
var sourceURL: URL!
|
||||||
|
|
||||||
init(outputPath: String = "projects") {
|
init(outputPath: String? = nil) {
|
||||||
self.outputPath = outputPath
|
self.outputPath = outputPath ?? "projects"
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(options: [String: Any]) {
|
convenience init(options: [String: Any]) {
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,11 @@ import Ink
|
||||||
public final class MarkdownRenderer: Renderer {
|
public final class MarkdownRenderer: Renderer {
|
||||||
let fileManager: FileManager = .default
|
let fileManager: FileManager = .default
|
||||||
let markdownParser = MarkdownParser()
|
let markdownParser = MarkdownParser()
|
||||||
|
let defaultTemplate: String
|
||||||
|
|
||||||
public init() {}
|
public init(defaultTemplate: String) {
|
||||||
|
self.defaultTemplate = defaultTemplate
|
||||||
|
}
|
||||||
|
|
||||||
public func canRenderFile(named filename: String, withExtension ext: String) -> Bool {
|
public func canRenderFile(named filename: String, withExtension ext: String) -> Bool {
|
||||||
ext == "md"
|
ext == "md"
|
||||||
|
|
@ -23,7 +26,7 @@ public final class MarkdownRenderer: Renderer {
|
||||||
let bodyMarkdown = try String(contentsOf: fileURL, encoding: .utf8)
|
let bodyMarkdown = try String(contentsOf: fileURL, encoding: .utf8)
|
||||||
let bodyHTML = markdownParser.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 templateRenderer.renderPage(bodyHTML: bodyHTML, metadata: metadata)
|
let pageHTML = try templateRenderer.renderPage(template: defaultTemplate, bodyHTML: bodyHTML, metadata: metadata)
|
||||||
|
|
||||||
let mdFilename = fileURL.lastPathComponent
|
let mdFilename = fileURL.lastPathComponent
|
||||||
let htmlPath: String
|
let htmlPath: String
|
||||||
|
|
|
||||||
105
SiteGenerator/Sources/SiteGenerator/SiteBuilder.swift
Normal file
105
SiteGenerator/Sources/SiteGenerator/SiteBuilder.swift
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
//
|
||||||
|
// SiteBuilder.swift
|
||||||
|
// SiteGenerator
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2019-12-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public final class SiteBuilder {
|
||||||
|
private let author: String
|
||||||
|
private let title: String
|
||||||
|
private let url: URL
|
||||||
|
|
||||||
|
private var email: String?
|
||||||
|
private var description: String?
|
||||||
|
|
||||||
|
private var styles: [String] = []
|
||||||
|
private var scripts: [String] = []
|
||||||
|
|
||||||
|
private var plugins: [Plugin] = []
|
||||||
|
private var renderers: [Renderer] = []
|
||||||
|
|
||||||
|
public init(
|
||||||
|
author: String,
|
||||||
|
email: String? = nil,
|
||||||
|
title: String,
|
||||||
|
description: String? = nil,
|
||||||
|
url: URL
|
||||||
|
) {
|
||||||
|
self.author = author
|
||||||
|
self.email = email
|
||||||
|
self.title = title
|
||||||
|
self.description = description
|
||||||
|
self.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
public func email(_ email: String) -> SiteBuilder {
|
||||||
|
precondition(self.email == nil, "email is already defined")
|
||||||
|
self.email = email
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
public func description(_ description: String) -> SiteBuilder {
|
||||||
|
precondition(self.description == nil, "description is already defined")
|
||||||
|
self.description = description
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
public func styles(_ styles: String...) -> SiteBuilder {
|
||||||
|
self.styles.append(contentsOf: styles)
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scripts(_ scripts: String...) -> SiteBuilder {
|
||||||
|
self.scripts.append(contentsOf: scripts)
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
public func plugin(_ plugin: Plugin) -> SiteBuilder {
|
||||||
|
plugins.append(plugin)
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
public func renderer(_ renderer: Renderer) -> SiteBuilder {
|
||||||
|
renderers.append(renderer)
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
public func build() -> Site {
|
||||||
|
Site(
|
||||||
|
author: author,
|
||||||
|
email: email,
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
url: url,
|
||||||
|
styles: styles,
|
||||||
|
scripts: scripts,
|
||||||
|
renderers: renderers,
|
||||||
|
plugins: plugins
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Markdown
|
||||||
|
|
||||||
|
public extension SiteBuilder {
|
||||||
|
func renderMarkdown(defaultTemplate: String) -> SiteBuilder {
|
||||||
|
renderer(MarkdownRenderer(defaultTemplate: defaultTemplate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Projects
|
||||||
|
|
||||||
|
public extension SiteBuilder {
|
||||||
|
func projects(path: String? = nil) -> SiteBuilder {
|
||||||
|
plugin(ProjectsPlugin(outputPath: path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Posts
|
||||||
|
|
||||||
|
public extension SiteBuilder {
|
||||||
|
// anything nice we can do there?
|
||||||
|
}
|
||||||
|
|
@ -15,44 +15,27 @@ public final class SiteGenerator {
|
||||||
// Site properties
|
// Site properties
|
||||||
public let site: Site
|
public let site: Site
|
||||||
public let sourceURL: URL
|
public let sourceURL: URL
|
||||||
public private(set) var plugins: [Plugin] = []
|
|
||||||
public let renderers: [Renderer]
|
|
||||||
|
|
||||||
let ignoredFilenames = [".DS_Store", ".gitkeep"]
|
let ignoredFilenames = [".DS_Store", ".gitkeep"]
|
||||||
|
|
||||||
public init(sourceURL: URL, siteURLOverride: URL? = nil, renderers: [Renderer]) throws {
|
public init(sourceURL: URL, site: Site) throws {
|
||||||
let siteURL = sourceURL.appendingPathComponent("site.json")
|
self.site = site
|
||||||
var site = try Site.decode(from: siteURL)
|
self.sourceURL = sourceURL
|
||||||
if let url = siteURLOverride {
|
|
||||||
print("Overriding site URL \(site.url) with \(url)")
|
|
||||||
site.url = url
|
|
||||||
}
|
|
||||||
|
|
||||||
let templatesURL = sourceURL.appendingPathComponent("templates")
|
let templatesURL = sourceURL.appendingPathComponent("templates")
|
||||||
self.templateRenderer = SiteTemplateRenderer(site: site, templatesURL: templatesURL)
|
self.templateRenderer = SiteTemplateRenderer(site: site, templatesURL: templatesURL)
|
||||||
|
|
||||||
self.site = site
|
|
||||||
self.sourceURL = sourceURL
|
|
||||||
self.renderers = renderers
|
|
||||||
|
|
||||||
try initializePlugins()
|
try initializePlugins()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func initializePlugins() throws {
|
private func initializePlugins() throws {
|
||||||
plugins = site.plugins.map { pair in
|
for plugin in site.plugins {
|
||||||
let (sitePlugin, options) = pair
|
|
||||||
return sitePlugin.construct(options: options)
|
|
||||||
}
|
|
||||||
try plugins.forEach(addPlugin)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func addPlugin(_ plugin: Plugin) throws {
|
|
||||||
try plugin.setUp(site: site, sourceURL: sourceURL)
|
try plugin.setUp(site: site, sourceURL: sourceURL)
|
||||||
plugins.append(plugin)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func generate(targetURL: URL) throws {
|
public func generate(targetURL: URL) throws {
|
||||||
for plugin in plugins {
|
for plugin in site.plugins {
|
||||||
try plugin.render(site: site, targetURL: targetURL, templateRenderer: templateRenderer)
|
try plugin.render(site: site, targetURL: targetURL, templateRenderer: templateRenderer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,7 +70,7 @@ public final class SiteGenerator {
|
||||||
func renderOrCopyFile(url fileURL: URL, targetDir: URL) throws {
|
func renderOrCopyFile(url fileURL: URL, targetDir: URL) throws {
|
||||||
let filename = fileURL.lastPathComponent
|
let filename = fileURL.lastPathComponent
|
||||||
let ext = String(filename.split(separator: ".").last!)
|
let ext = String(filename.split(separator: ".").last!)
|
||||||
for renderer in renderers {
|
for renderer in site.renderers {
|
||||||
if renderer.canRenderFile(named: filename, withExtension: ext) {
|
if renderer.canRenderFile(named: filename, withExtension: ext) {
|
||||||
try renderer.render(fileURL: fileURL, targetDir: targetDir, templateRenderer: templateRenderer)
|
try renderer.render(fileURL: fileURL, targetDir: targetDir, templateRenderer: templateRenderer)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,28 @@ public struct samhuri_net {
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public func generate(sourceURL: URL, targetURL: URL, siteURLOverride: URL? = nil) throws {
|
public func generate(sourceURL: URL, targetURL: URL, siteURLOverride: URL? = nil) throws {
|
||||||
let generator = try SiteGenerator(
|
let postsPlugin = PostsPluginBuilder()
|
||||||
sourceURL: sourceURL,
|
.path("posts")
|
||||||
siteURLOverride: siteURLOverride,
|
.jsonFeed(
|
||||||
renderers: [MarkdownRenderer()]
|
avatarPath: "images/me.jpg",
|
||||||
|
iconPath: "images/apple-touch-icon-300.png",
|
||||||
|
faviconPath: "images/apple-touch-icon-80.png"
|
||||||
)
|
)
|
||||||
|
.rssFeed()
|
||||||
|
.build()
|
||||||
|
let site = SiteBuilder(
|
||||||
|
author: "Sami Samhuri",
|
||||||
|
email: "sami@samhuri.net",
|
||||||
|
title: "samhuri.net",
|
||||||
|
description: "just some blog",
|
||||||
|
url: siteURLOverride ?? URL(string: "https://samhuri.net")!
|
||||||
|
)
|
||||||
|
.styles("css/normalize.css", "css/style.css")
|
||||||
|
.renderMarkdown(defaultTemplate: "page.html")
|
||||||
|
.projects()
|
||||||
|
.plugin(postsPlugin)
|
||||||
|
.build()
|
||||||
|
let generator = try SiteGenerator(sourceURL: sourceURL, site: site)
|
||||||
try generator.generate(targetURL: targetURL)
|
try generator.generate(targetURL: targetURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
site.json
22
site.json
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"title": "samhuri.net",
|
|
||||||
"description": "just some blog",
|
|
||||||
"author": "Sami Samhuri",
|
|
||||||
"email": "sami@samhuri.net",
|
|
||||||
"url": "https://samhuri.net",
|
|
||||||
"styles": [
|
|
||||||
"/css/normalize.css",
|
|
||||||
"/css/style.css"
|
|
||||||
],
|
|
||||||
"scripts": [],
|
|
||||||
"icon": "images/apple-touch-icon-300.png",
|
|
||||||
"favicon": "images/apple-touch-icon-80.png",
|
|
||||||
"avatar": "images/me.jpg",
|
|
||||||
"plugins": {
|
|
||||||
"projects": {},
|
|
||||||
"posts": {
|
|
||||||
"json_feed": "feed.json",
|
|
||||||
"rss_feed": "feed.xml"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue