diff --git a/Readme.md b/Readme.md index 7b82c98..809e2ba 100644 --- a/Readme.md +++ b/Readme.md @@ -117,6 +117,8 @@ Execution, trying TDD for the first time: - [x] Replace site.json with Swift code + - [x] Move template rendering from SiteGenerator to samhuri.net + - [ ] Replace page template with Swift code - [ ] Replace projects.json with Swift code diff --git a/SiteGenerator/Package.swift b/SiteGenerator/Package.swift index 61e8b8f..f50404e 100644 --- a/SiteGenerator/Package.swift +++ b/SiteGenerator/Package.swift @@ -17,7 +17,6 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/johnsundell/ink.git", from: "0.1.0"), - .package(url: "https://github.com/stencilproject/Stencil.git", from: "0.13.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -26,7 +25,6 @@ let package = Package( name: "SiteGenerator", dependencies: [ "Ink", - "Stencil", ]), .testTarget( name: "SiteGeneratorTests", diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/AnyPlugin.swift b/SiteGenerator/Sources/SiteGenerator/Generator/AnyPlugin.swift deleted file mode 100644 index 6596d0c..0000000 --- a/SiteGenerator/Sources/SiteGenerator/Generator/AnyPlugin.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// AnyPlugin.swift -// SiteGenerator -// -// Created by Sami Samhuri on 2019-12-14. -// - -import Foundation - -struct AnyPlugin: Plugin { - private let _setUp: (Site, URL) throws -> Void - private let _render: (Site, URL, TemplateRenderer) throws -> Void - - init(_ plugin: PluginType) { - self._setUp = { site, sourceURL in - try plugin.setUp(site: site, sourceURL: sourceURL) - } - self._render = { site, targetURL, templateRenderer in - try plugin.render(site: site, targetURL: targetURL, templateRenderer: templateRenderer) - } - } - - func setUp(site: Site, sourceURL: URL) throws { - try _setUp(site, sourceURL) - } - - func render(site: Site, targetURL: URL, templateRenderer: TemplateRenderer) throws { - try _render(site, targetURL, templateRenderer) - } -} diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/SitePlugin.swift b/SiteGenerator/Sources/SiteGenerator/Generator/SitePlugin.swift deleted file mode 100644 index 3445a76..0000000 --- a/SiteGenerator/Sources/SiteGenerator/Generator/SitePlugin.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// SitePlugin.swift -// SiteGenerator -// -// Created by Sami Samhuri on 2019-12-10. -// - -import Foundation - -public enum SitePlugin: String, Codable { - case posts - case projects - - func construct(options: [String: Any]) -> AnyPlugin { - switch self { - case .posts: - let plugin = PostsPlugin(options: options) - return AnyPlugin(plugin) - - case .projects: - let plugin = ProjectsPlugin(options: options) - return AnyPlugin(plugin) - } - } -} diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/SiteTemplateRenderer.swift b/SiteGenerator/Sources/SiteGenerator/Generator/SiteTemplateRenderer.swift deleted file mode 100644 index 7bac757..0000000 --- a/SiteGenerator/Sources/SiteGenerator/Generator/SiteTemplateRenderer.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// 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(template: String, 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: template, context: context.dictionary) - return pageHTML - } - - func renderTemplate(name: String, context: [String: Any]) throws -> String { - let siteContext = SiteContext(site: site) - let contextDict = siteContext.dictionary.merging(context, uniquingKeysWith: { _, new in new }) - return try stencil.renderTemplate(name: name, context: contextDict) - } -} diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/TemplateRenderer.swift b/SiteGenerator/Sources/SiteGenerator/Generator/TemplateRenderer.swift deleted file mode 100644 index 14a069c..0000000 --- a/SiteGenerator/Sources/SiteGenerator/Generator/TemplateRenderer.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// templateRenderer.swift -// SiteGenerator -// -// Created by Sami Samhuri on 2019-12-03. -// - -import Foundation - -public protocol TemplateRenderer: AnyObject { - func renderPage(template: String, bodyHTML: String, metadata: [String: String]) throws -> String - func renderTemplate(name: String, context: [String: Any]) throws -> String -} diff --git a/SiteGenerator/Sources/SiteGenerator/MarkdownPageRenderer.swift b/SiteGenerator/Sources/SiteGenerator/MarkdownPageRenderer.swift new file mode 100644 index 0000000..61c7813 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/MarkdownPageRenderer.swift @@ -0,0 +1,12 @@ +// +// MarkdownPageRenderer.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-03. +// + +import Foundation + +public protocol MarkdownPageRenderer { + func renderPage(site: Site, bodyHTML: String, metadata: [String: String]) throws -> String +} diff --git a/SiteGenerator/Sources/SiteGenerator/Renderers/MarkdownRenderer.swift b/SiteGenerator/Sources/SiteGenerator/MarkdownRenderer.swift similarity index 82% rename from SiteGenerator/Sources/SiteGenerator/Renderers/MarkdownRenderer.swift rename to SiteGenerator/Sources/SiteGenerator/MarkdownRenderer.swift index 00c891a..251b005 100644 --- a/SiteGenerator/Sources/SiteGenerator/Renderers/MarkdownRenderer.swift +++ b/SiteGenerator/Sources/SiteGenerator/MarkdownRenderer.swift @@ -11,10 +11,10 @@ import Ink public final class MarkdownRenderer: Renderer { let fileManager: FileManager = .default let markdownParser = MarkdownParser() - let defaultTemplate: String + let pageRenderer: MarkdownPageRenderer - public init(defaultTemplate: String) { - self.defaultTemplate = defaultTemplate + public init(pageRenderer: MarkdownPageRenderer) { + self.pageRenderer = pageRenderer } public func canRenderFile(named filename: String, withExtension ext: String) -> Bool { @@ -22,11 +22,11 @@ public final class MarkdownRenderer: Renderer { } /// Parse Markdown and render it as HTML, running it through a Stencil template. - public func render(fileURL: URL, targetDir: URL, templateRenderer: TemplateRenderer) throws { + public func render(site: Site, fileURL: URL, targetDir: URL) throws { let bodyMarkdown = try String(contentsOf: fileURL, encoding: .utf8) let bodyHTML = markdownParser.html(from: bodyMarkdown).trimmingCharacters(in: .whitespacesAndNewlines) let metadata = try markdownMetadata(from: fileURL) - let pageHTML = try templateRenderer.renderPage(template: defaultTemplate, bodyHTML: bodyHTML, metadata: metadata) + let pageHTML = try pageRenderer.renderPage(site: site, bodyHTML: bodyHTML, metadata: metadata) let mdFilename = fileURL.lastPathComponent let htmlPath: String diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/Plugin.swift b/SiteGenerator/Sources/SiteGenerator/Plugin.swift similarity index 68% rename from SiteGenerator/Sources/SiteGenerator/Generator/Plugin.swift rename to SiteGenerator/Sources/SiteGenerator/Plugin.swift index 785527b..1dd1e99 100644 --- a/SiteGenerator/Sources/SiteGenerator/Generator/Plugin.swift +++ b/SiteGenerator/Sources/SiteGenerator/Plugin.swift @@ -10,5 +10,5 @@ import Foundation public protocol Plugin { func setUp(site: Site, sourceURL: URL) throws - func render(site: Site, targetURL: URL, templateRenderer: TemplateRenderer) throws + func render(site: Site, targetURL: URL) throws } diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/JSONFeed.swift b/SiteGenerator/Sources/SiteGenerator/Posts/Feeds/JSONFeed.swift similarity index 100% rename from SiteGenerator/Sources/SiteGenerator/Posts/JSONFeed.swift rename to SiteGenerator/Sources/SiteGenerator/Posts/Feeds/JSONFeed.swift diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/Feeds/JSONFeedWriter.swift b/SiteGenerator/Sources/SiteGenerator/Posts/Feeds/JSONFeedWriter.swift index a669f74..943dc2c 100644 --- a/SiteGenerator/Sources/SiteGenerator/Posts/Feeds/JSONFeedWriter.swift +++ b/SiteGenerator/Sources/SiteGenerator/Posts/Feeds/JSONFeedWriter.swift @@ -44,7 +44,7 @@ final class JSONFeedWriter { self.jsonFeed = feed } - func writeFeed(_ posts: [Post], site: Site, to targetURL: URL, with templateRenderer: TemplateRenderer) throws { + func writeFeed(_ posts: [Post], for site: Site, to targetURL: URL, with templateRenderer: PostsTemplateRenderer) throws { let items: [FeedItem] = try posts.map { post in let url = site.url.appendingPathComponent(post.path) return FeedItem( @@ -54,7 +54,7 @@ final class JSONFeedWriter { url: url.absoluteString, external_url: post.link?.absoluteString, author: FeedAuthor(name: post.author, avatar: nil, url: nil), - content_html: try templateRenderer.renderTemplate(name: "feed-post.html", context: [ + content_html: try templateRenderer.renderTemplate(.feedPost, site: site, context: [ "post": post, ]), tags: post.tags diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/RSSFeed.swift b/SiteGenerator/Sources/SiteGenerator/Posts/Feeds/RSSFeed.swift similarity index 100% rename from SiteGenerator/Sources/SiteGenerator/Posts/RSSFeed.swift rename to SiteGenerator/Sources/SiteGenerator/Posts/Feeds/RSSFeed.swift diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/Feeds/RSSFeedWriter.swift b/SiteGenerator/Sources/SiteGenerator/Posts/Feeds/RSSFeedWriter.swift index 5f4e131..e84eb14 100644 --- a/SiteGenerator/Sources/SiteGenerator/Posts/Feeds/RSSFeedWriter.swift +++ b/SiteGenerator/Sources/SiteGenerator/Posts/Feeds/RSSFeedWriter.swift @@ -44,7 +44,7 @@ final class RSSFeedWriter { self.feed = feed } - func writeFeed(_ posts: [Post], site: Site, to targetURL: URL, with templateRenderer: TemplateRenderer) throws { + func writeFeed(_ posts: [Post], for site: Site, to targetURL: URL, with templateRenderer: PostsTemplateRenderer) throws { let feedSite = FeedSite( title: site.title.escapedForXML(), description: site.description?.escapedForXML(), @@ -67,12 +67,12 @@ final class RSSFeedWriter { author: author.escapedForXML(), link: (post.link ?? url).absoluteString.escapedForXML(), guid: url.absoluteString.escapedForXML(), - body: try templateRenderer.renderTemplate(name: "feed-post.html", context: [ + body: try templateRenderer.renderTemplate(.feedPost, site: site, context: [ "post": post, ]).escapedForXML() ) } - let feedXML = try templateRenderer.renderTemplate(name: "feed.xml", context: [ + let feedXML = try templateRenderer.renderTemplate(.rssFeed, site: site, context: [ "site": feedSite, "feedURL": site.url.appendingPathComponent(feed.path).absoluteString.escapedForXML(), "posts": renderedPosts, diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/PostWriter.swift b/SiteGenerator/Sources/SiteGenerator/Posts/PostWriter.swift index 7203262..b9d83a0 100644 --- a/SiteGenerator/Sources/SiteGenerator/Posts/PostWriter.swift +++ b/SiteGenerator/Sources/SiteGenerator/Posts/PostWriter.swift @@ -20,9 +20,9 @@ final class PostWriter { // MARK: - Post pages extension PostWriter { - func writePosts(_ posts: [Post], to targetURL: URL, with templateRenderer: TemplateRenderer) throws { + func writePosts(_ posts: [Post], for site: Site, to targetURL: URL, with templateRenderer: PostsTemplateRenderer) throws { for post in posts { - let postHTML = try templateRenderer.renderTemplate(name: "post.html", context: [ + let postHTML = try templateRenderer.renderTemplate(.post, site: site, context: [ "title": post.title, "post": post, ]) @@ -42,8 +42,8 @@ extension PostWriter { // 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.html", context: [ + func writeRecentPosts(_ recentPosts: [Post], for site: Site, to targetURL: URL, with templateRenderer: PostsTemplateRenderer) throws { + let recentPostsHTML = try templateRenderer.renderTemplate(.recentPosts, site: site, context: [ "recentPosts": recentPosts, ]) let fileURL = targetURL.appendingPathComponent("index.html") @@ -55,9 +55,9 @@ extension PostWriter { // MARK: - Post archive page extension PostWriter { - func writeArchive(posts: PostsByYear, to targetURL: URL, with templateRenderer: TemplateRenderer) throws { + func writeArchive(posts: PostsByYear, for site: Site, to targetURL: URL, with templateRenderer: PostsTemplateRenderer) throws { let allYears = posts.byYear.keys.sorted(by: >) - let archiveHTML = try templateRenderer.renderTemplate(name: "posts-archive.html", context: [ + let archiveHTML = try templateRenderer.renderTemplate(.archive, site: site, context: [ "title": "Archive", "years": allYears.map { contextDictionaryForYearPosts(posts[$0]) }, ]) @@ -89,7 +89,7 @@ extension PostWriter { // MARK: - Yearly post index pages extension PostWriter { - func writeYearIndexes(posts: PostsByYear, to targetURL: URL, with templateRenderer: TemplateRenderer) throws { + func writeYearIndexes(posts: PostsByYear, for site: Site, to targetURL: URL, with templateRenderer: PostsTemplateRenderer) throws { for (year, yearPosts) in posts.byYear { let months = yearPosts.months.sorted(by: >) let yearDir = targetURL.appendingPathComponent(yearPosts.path) @@ -99,7 +99,7 @@ extension PostWriter { "year": year, "months": months.map { contextDictionaryForMonthPosts(posts[year][$0], year: year) }, ] - let yearHTML = try templateRenderer.renderTemplate(name: "posts-year.html", context: context) + let yearHTML = try templateRenderer.renderTemplate(.yearPosts, site: site, 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) @@ -110,12 +110,12 @@ extension PostWriter { // MARK: - Monthly post roll-up pages extension PostWriter { - func writeMonthRollups(posts: PostsByYear, to targetURL: URL, with templateRenderer: TemplateRenderer) throws { + func writeMonthRollups(posts: PostsByYear, for site: Site, to targetURL: URL, with templateRenderer: PostsTemplateRenderer) throws { for (year, yearPosts) in posts.byYear { for month in yearPosts.months { let monthPosts = yearPosts[month] let monthDir = targetURL.appendingPathComponent(monthPosts.path) - let monthHTML = try templateRenderer.renderTemplate(name: "posts-month.html", context: [ + let monthHTML = try templateRenderer.renderTemplate(.monthPosts, site: site, context: [ "title": "\(month.name) \(year)", "posts": monthPosts.posts.sorted(by: >), ]) diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/PostsPlugin.swift b/SiteGenerator/Sources/SiteGenerator/Posts/PostsPlugin.swift index e0d36d8..77b9a6d 100644 --- a/SiteGenerator/Sources/SiteGenerator/Posts/PostsPlugin.swift +++ b/SiteGenerator/Sources/SiteGenerator/Posts/PostsPlugin.swift @@ -8,17 +8,20 @@ import Foundation public final class PostsPlugin: Plugin { + let templateRenderer: PostsTemplateRenderer let postRepo: PostRepo let postWriter: PostWriter let jsonFeedWriter: JSONFeedWriter? let rssFeedWriter: RSSFeedWriter? init( + templateRenderer: PostsTemplateRenderer, postRepo: PostRepo = PostRepo(), postWriter: PostWriter = PostWriter(), jsonFeedWriter: JSONFeedWriter?, rssFeedWriter: RSSFeedWriter? ) { + self.templateRenderer = templateRenderer self.postRepo = postRepo self.postWriter = postWriter self.jsonFeedWriter = jsonFeedWriter @@ -27,39 +30,6 @@ public final class PostsPlugin: Plugin { // MARK: - Plugin methods - convenience init(options: [String: Any]) { - let postRepo: PostRepo - let postWriter: PostWriter - if let outputPath = options["path"] as? String { - postRepo = PostRepo(outputPath: outputPath) - postWriter = PostWriter(outputPath: outputPath) - } - else { - postRepo = PostRepo() - postWriter = PostWriter() - } - - let jsonFeedWriter: JSONFeedWriter? - if let jsonFeedPath = options["json_feed"] as? String { - let jsonFeed = JSONFeed(path: jsonFeedPath, avatarPath: nil, iconPath: nil, faviconPath: nil) - jsonFeedWriter = JSONFeedWriter(feed: jsonFeed) - } - else { - jsonFeedWriter = nil - } - - let rssFeedWriter: RSSFeedWriter? - if let rssFeedPath = options["rss_feed"] as? String { - let rssFeed = RSSFeed(path: rssFeedPath) - rssFeedWriter = RSSFeedWriter(feed: rssFeed) - } - else { - rssFeedWriter = nil - } - - self.init(postRepo: postRepo, postWriter: postWriter, jsonFeedWriter: jsonFeedWriter, rssFeedWriter: rssFeedWriter) - } - public func setUp(site: Site, sourceURL: URL) throws { guard postRepo.postDataExists(at: sourceURL) else { return @@ -68,17 +38,17 @@ public final class PostsPlugin: Plugin { try postRepo.readPosts(sourceURL: sourceURL) } - public func render(site: Site, targetURL: URL, templateRenderer: TemplateRenderer) throws { + public func render(site: Site, targetURL: URL) throws { guard !postRepo.isEmpty else { return } - 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) - try jsonFeedWriter?.writeFeed(postRepo.postsForFeed, site: site, to: targetURL, with: templateRenderer) - try rssFeedWriter?.writeFeed(postRepo.postsForFeed, site: site, to: targetURL, with: templateRenderer) + try postWriter.writeRecentPosts(postRepo.recentPosts, for: site, to: targetURL, with: templateRenderer) + try postWriter.writePosts(postRepo.sortedPosts, for: site, to: targetURL, with: templateRenderer) + try postWriter.writeArchive(posts: postRepo.posts, for: site, to: targetURL, with: templateRenderer) + try postWriter.writeYearIndexes(posts: postRepo.posts, for: site, to: targetURL, with: templateRenderer) + try postWriter.writeMonthRollups(posts: postRepo.posts, for: site, to: targetURL, with: templateRenderer) + try jsonFeedWriter?.writeFeed(postRepo.postsForFeed, for: site, to: targetURL, with: templateRenderer) + try rssFeedWriter?.writeFeed(postRepo.postsForFeed, for: site, to: targetURL, with: templateRenderer) } } diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/PostsPluginBuilder.swift b/SiteGenerator/Sources/SiteGenerator/Posts/PostsPluginBuilder.swift index af3a848..4126351 100644 --- a/SiteGenerator/Sources/SiteGenerator/Posts/PostsPluginBuilder.swift +++ b/SiteGenerator/Sources/SiteGenerator/Posts/PostsPluginBuilder.swift @@ -8,11 +8,14 @@ import Foundation public final class PostsPluginBuilder { + private let templateRenderer: PostsTemplateRenderer private var path: String? private var jsonFeed: JSONFeed? private var rssFeed: RSSFeed? - public init() {} + public init(templateRenderer: PostsTemplateRenderer) { + self.templateRenderer = templateRenderer + } public func path(_ path: String) -> PostsPluginBuilder { precondition(self.path == nil, "path is already defined") @@ -71,6 +74,7 @@ public final class PostsPluginBuilder { } return PostsPlugin( + templateRenderer: templateRenderer, postRepo: postRepo, postWriter: postWriter, jsonFeedWriter: jsonFeedWriter, diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/PostsTemplateRenderer.swift b/SiteGenerator/Sources/SiteGenerator/Posts/PostsTemplateRenderer.swift new file mode 100644 index 0000000..a2ddb61 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Posts/PostsTemplateRenderer.swift @@ -0,0 +1,22 @@ +// +// PostsTemplateRenderer.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-17. +// + +import Foundation + +public enum PostTemplate { + case archive + case feedPost + case monthPosts + case post + case recentPosts + case rssFeed + case yearPosts +} + +public protocol PostsTemplateRenderer { + func renderTemplate(_ template: PostTemplate, site: Site, context: [String: Any]) throws -> String +} diff --git a/SiteGenerator/Sources/SiteGenerator/Projects/ProjectsPlugin.swift b/SiteGenerator/Sources/SiteGenerator/Projects/ProjectsPlugin.swift index 86ae3be..bf4f24b 100644 --- a/SiteGenerator/Sources/SiteGenerator/Projects/ProjectsPlugin.swift +++ b/SiteGenerator/Sources/SiteGenerator/Projects/ProjectsPlugin.swift @@ -20,22 +20,17 @@ private struct Projects: Codable { final class ProjectsPlugin: Plugin { let fileManager: FileManager = .default let outputPath: String + let templateRenderer: ProjectsTemplateRenderer var projects: [Project] = [] var sourceURL: URL! - init(outputPath: String? = nil) { + init(templateRenderer: ProjectsTemplateRenderer, outputPath: String? = nil) { + self.templateRenderer = templateRenderer self.outputPath = outputPath ?? "projects" } - convenience init(options: [String: Any]) { - if let outputPath = options["path"] as? String { - self.init(outputPath: outputPath) - } - else { - self.init() - } - } + // MARK: - Plugin methods func setUp(site: Site, sourceURL: URL) throws { self.sourceURL = sourceURL @@ -47,7 +42,7 @@ final class ProjectsPlugin: Plugin { } } - func render(site: Site, targetURL: URL, templateRenderer: TemplateRenderer) throws { + func render(site: Site, targetURL: URL) throws { guard !projects.isEmpty else { return } @@ -55,7 +50,7 @@ final class ProjectsPlugin: Plugin { let projectsDir = targetURL.appendingPathComponent(outputPath) try fileManager.createDirectory(at: projectsDir, withIntermediateDirectories: true, attributes: nil) let projectsURL = projectsDir.appendingPathComponent("index.html") - let projectsHTML = try templateRenderer.renderTemplate(name: "projects.html", context: [ + let projectsHTML = try templateRenderer.renderTemplate(.projects, site: site, context: [ "title": "Projects", "projects": projects, ]) @@ -63,7 +58,7 @@ final class ProjectsPlugin: Plugin { for project in projects { let projectURL = projectsDir.appendingPathComponent("\(project.title)/index.html") - let projectHTML = try templateRenderer.renderTemplate(name: "project.html", context: [ + let projectHTML = try templateRenderer.renderTemplate(.project, site: site, context: [ "title": "\(project.title)", "project": project, ]) diff --git a/SiteGenerator/Sources/SiteGenerator/Projects/ProjectsTemplateRenderer.swift b/SiteGenerator/Sources/SiteGenerator/Projects/ProjectsTemplateRenderer.swift new file mode 100644 index 0000000..5d8e962 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Projects/ProjectsTemplateRenderer.swift @@ -0,0 +1,17 @@ +// +// ProjectsTemplateRenderer.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-17. +// + +import Foundation + +public enum ProjectTemplate { + case project + case projects +} + +public protocol ProjectsTemplateRenderer { + func renderTemplate(_ template: ProjectTemplate, site: Site, context: [String: Any]) throws -> String +} diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/Renderer.swift b/SiteGenerator/Sources/SiteGenerator/Renderer.swift similarity index 71% rename from SiteGenerator/Sources/SiteGenerator/Generator/Renderer.swift rename to SiteGenerator/Sources/SiteGenerator/Renderer.swift index d28018f..674d4b2 100644 --- a/SiteGenerator/Sources/SiteGenerator/Generator/Renderer.swift +++ b/SiteGenerator/Sources/SiteGenerator/Renderer.swift @@ -10,5 +10,5 @@ import Foundation public protocol Renderer { func canRenderFile(named filename: String, withExtension ext: String) -> Bool - func render(fileURL: URL, targetDir: URL, templateRenderer: TemplateRenderer) throws + func render(site: Site, fileURL: URL, targetDir: URL) throws } diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/Site.swift b/SiteGenerator/Sources/SiteGenerator/Site.swift similarity index 100% rename from SiteGenerator/Sources/SiteGenerator/Generator/Site.swift rename to SiteGenerator/Sources/SiteGenerator/Site.swift diff --git a/SiteGenerator/Sources/SiteGenerator/SiteBuilder.swift b/SiteGenerator/Sources/SiteGenerator/SiteBuilder.swift index ba7e6b4..9fb4c42 100644 --- a/SiteGenerator/Sources/SiteGenerator/SiteBuilder.swift +++ b/SiteGenerator/Sources/SiteGenerator/SiteBuilder.swift @@ -85,16 +85,16 @@ public final class SiteBuilder { // MARK: - Markdown public extension SiteBuilder { - func renderMarkdown(defaultTemplate: String) -> SiteBuilder { - renderer(MarkdownRenderer(defaultTemplate: defaultTemplate)) + func renderMarkdown(pageRenderer: MarkdownPageRenderer) -> SiteBuilder { + renderer(MarkdownRenderer(pageRenderer: pageRenderer)) } } // MARK: - Projects public extension SiteBuilder { - func projects(path: String? = nil) -> SiteBuilder { - plugin(ProjectsPlugin(outputPath: path)) + func projects(templateRenderer: ProjectsTemplateRenderer, path: String? = nil) -> SiteBuilder { + plugin(ProjectsPlugin(templateRenderer: templateRenderer, outputPath: path)) } } diff --git a/SiteGenerator/Sources/SiteGenerator/SiteGenerator.swift b/SiteGenerator/Sources/SiteGenerator/SiteGenerator.swift index c01a5bf..b94ab56 100644 --- a/SiteGenerator/Sources/SiteGenerator/SiteGenerator.swift +++ b/SiteGenerator/Sources/SiteGenerator/SiteGenerator.swift @@ -10,7 +10,6 @@ import Foundation public final class SiteGenerator { // Dependencies let fileManager: FileManager = .default - let templateRenderer: TemplateRenderer // Site properties public let site: Site @@ -22,9 +21,6 @@ public final class SiteGenerator { self.site = site self.sourceURL = sourceURL - let templatesURL = sourceURL.appendingPathComponent("templates") - self.templateRenderer = SiteTemplateRenderer(site: site, templatesURL: templatesURL) - try initializePlugins() } @@ -36,7 +32,7 @@ public final class SiteGenerator { public func generate(targetURL: URL) throws { for plugin in site.plugins { - try plugin.render(site: site, targetURL: targetURL, templateRenderer: templateRenderer) + try plugin.render(site: site, targetURL: targetURL) } let publicURL = sourceURL.appendingPathComponent("public") @@ -72,7 +68,7 @@ public final class SiteGenerator { let ext = String(filename.split(separator: ".").last!) for renderer in site.renderers { if renderer.canRenderFile(named: filename, withExtension: ext) { - try renderer.render(fileURL: fileURL, targetDir: targetDir, templateRenderer: templateRenderer) + try renderer.render(site: site, fileURL: fileURL, targetDir: targetDir) return } } diff --git a/gensite/Package.swift b/gensite/Package.swift index 5a546b3..404e926 100644 --- a/gensite/Package.swift +++ b/gensite/Package.swift @@ -10,11 +10,11 @@ let package = Package( .iOS(.v13), ], dependencies: [ - .package(path: "../samhuri_net"), + .package(path: "../samhuri.net"), ], targets: [ .target( name: "gensite", dependencies: [ - "samhuri_net", + "samhuri.net", ]), .testTarget(name: "gensiteTests", dependencies: ["gensite"]), ] diff --git a/gensite/Sources/gensite/main.swift b/gensite/Sources/gensite/main.swift index ebcd450..a07dfae 100644 --- a/gensite/Sources/gensite/main.swift +++ b/gensite/Sources/gensite/main.swift @@ -9,13 +9,6 @@ import Darwin import Foundation import samhuri_net -func main(sourcePath: String, targetPath: String, siteURLOverride: URL?) throws { - let sourceURL = URL(fileURLWithPath: sourcePath) - let targetURL = URL(fileURLWithPath: targetPath) - let site = samhuri_net() - try site.generate(sourceURL: sourceURL, targetURL: targetURL, siteURLOverride: siteURLOverride) -} - guard CommandLine.arguments.count >= 3 else { let name = CommandLine.arguments[0] fputs("Usage: \(name) ", stderr) @@ -51,7 +44,10 @@ else { } do { - try main(sourcePath: sourcePath, targetPath: targetPath, siteURLOverride: siteURLOverride) + let sourceURL = URL(fileURLWithPath: sourcePath) + let targetURL = URL(fileURLWithPath: targetPath) + let site = samhuri.net(siteURLOverride: siteURLOverride) + try site.generate(sourceURL: sourceURL, targetURL: targetURL) exit(0) } catch { diff --git a/samhuri_net/.gitignore b/samhuri.net/.gitignore similarity index 100% rename from samhuri_net/.gitignore rename to samhuri.net/.gitignore diff --git a/samhuri_net/Package.resolved b/samhuri.net/Package.resolved similarity index 100% rename from samhuri_net/Package.resolved rename to samhuri.net/Package.resolved diff --git a/samhuri_net/Package.swift b/samhuri.net/Package.swift similarity index 72% rename from samhuri_net/Package.swift rename to samhuri.net/Package.swift index fc42f39..ff67f36 100644 --- a/samhuri_net/Package.swift +++ b/samhuri.net/Package.swift @@ -4,7 +4,7 @@ import PackageDescription let package = Package( - name: "samhuri_net", + name: "samhuri.net", platforms: [ .macOS(.v10_15), .iOS(.v13), @@ -12,22 +12,24 @@ let package = Package( products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( - name: "samhuri_net", - targets: ["samhuri_net"]), + name: "samhuri.net", + targets: ["samhuri.net"]), ], dependencies: [ .package(path: "../SiteGenerator"), + .package(url: "https://github.com/stencilproject/Stencil.git", from: "0.13.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( - name: "samhuri_net", + name: "samhuri.net", dependencies: [ "SiteGenerator", + "Stencil", ]), .testTarget( - name: "samhuri_netTests", - dependencies: ["samhuri_net"]), + name: "samhuri.netTests", + dependencies: ["samhuri.net"]), ] ) diff --git a/samhuri_net/README.md b/samhuri.net/README.md similarity index 100% rename from samhuri_net/README.md rename to samhuri.net/README.md diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/Page.swift b/samhuri.net/Sources/samhuri.net/Page.swift similarity index 77% rename from SiteGenerator/Sources/SiteGenerator/Generator/Page.swift rename to samhuri.net/Sources/samhuri.net/Page.swift index 23929ec..aa3a7c1 100644 --- a/SiteGenerator/Sources/SiteGenerator/Generator/Page.swift +++ b/samhuri.net/Sources/samhuri.net/Page.swift @@ -1,6 +1,6 @@ // // Page.swift -// SiteGenerator +// samhuri.net // // Created by Sami Samhuri on 2019-12-01. // @@ -9,14 +9,12 @@ import Foundation struct Page { let title: String - let template: String? let styles: [String] let scripts: [String] } extension Page { init(metadata: [String: String]) { - let template = metadata["Template"] let styles = metadata["Styles", default: ""] .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespaces) } @@ -24,6 +22,6 @@ extension Page { .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespaces) } let title = metadata["Title", default: ""] - self.init(title: title, template: template, styles: styles, scripts: scripts) + self.init(title: title, styles: styles, scripts: scripts) } } diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/Contexts/PageContext.swift b/samhuri.net/Sources/samhuri.net/PageContext.swift similarity index 90% rename from SiteGenerator/Sources/SiteGenerator/Generator/Contexts/PageContext.swift rename to samhuri.net/Sources/samhuri.net/PageContext.swift index 061ba48..63c512d 100644 --- a/SiteGenerator/Sources/SiteGenerator/Generator/Contexts/PageContext.swift +++ b/samhuri.net/Sources/samhuri.net/PageContext.swift @@ -1,11 +1,12 @@ // -// File.swift -// +// PageContext.swift +// samhuri.net // // Created by Sami Samhuri on 2019-12-02. // import Foundation +import SiteGenerator struct PageContext { let site: Site diff --git a/samhuri.net/Sources/samhuri.net/PageRenderer.swift b/samhuri.net/Sources/samhuri.net/PageRenderer.swift new file mode 100644 index 0000000..c7a6fa5 --- /dev/null +++ b/samhuri.net/Sources/samhuri.net/PageRenderer.swift @@ -0,0 +1,87 @@ +// +// PageRenderer.swift +// samhuri.net +// +// Created by Sami Samhuri on 2019-12-17. +// + +import Foundation +import PathKit +import SiteGenerator +import Stencil + +final class PageRenderer { + @available(*, deprecated) + let stencil: Environment + + init(templatesURL: URL) { + let templatesPath = Path(templatesURL.path) + let loader = FileSystemLoader(paths: [templatesPath]) + self.stencil = Environment(loader: loader) + } +} + +extension PageRenderer: MarkdownPageRenderer { + func renderPage(site: Site, 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: "page.html", context: context.dictionary) + return pageHTML + } +} + +extension PostTemplate { + @available(*, deprecated) + var htmlFilename: String { + switch self { + case .archive: + return "posts-archive.html" + case .feedPost: + return "feed-post.html" + case .monthPosts: + return "posts-month.html" + case .post: + return "post.html" + case .recentPosts: + return "recent-posts.html" + case .rssFeed: + return "feed.xml" + case .yearPosts: + return "posts-year.html" + } + } +} + +extension PageRenderer: PostsTemplateRenderer { + func renderTemplate(_ template: PostTemplate, site: Site, context: [String : Any]) throws -> String { + let siteContext = SiteContext(site: site) + let contextDict = siteContext.dictionary.merging(context, uniquingKeysWith: { _, new in new }) + return try stencil.renderTemplate(name: template.htmlFilename, context: contextDict) + } +} + +extension ProjectTemplate { + @available(*, deprecated) + var htmlFilename: String { + switch self { + case .project: + return "project.html" + case .projects: + return "projects.html" + } + } +} + +extension PageRenderer: ProjectsTemplateRenderer { + func renderTemplate(_ template: ProjectTemplate, site: Site, context: [String : Any]) throws -> String { + let siteContext = SiteContext(site: site) + let contextDict = siteContext.dictionary.merging(context, uniquingKeysWith: { _, new in new }) + return try stencil.renderTemplate(name: template.htmlFilename, context: contextDict) + } +} + +extension Date { + var year: Int { + Calendar.current.dateComponents([.year], from: self).year! + } +} diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/Contexts/SiteContext.swift b/samhuri.net/Sources/samhuri.net/SiteContext.swift similarity index 92% rename from SiteGenerator/Sources/SiteGenerator/Generator/Contexts/SiteContext.swift rename to samhuri.net/Sources/samhuri.net/SiteContext.swift index dfb7b7d..2285897 100644 --- a/SiteGenerator/Sources/SiteGenerator/Generator/Contexts/SiteContext.swift +++ b/samhuri.net/Sources/samhuri.net/SiteContext.swift @@ -1,11 +1,12 @@ // // SiteContext.swift -// SiteGenerator +// samhuri.net // // Created by Sami Samhuri on 2019-12-01. // import Foundation +import SiteGenerator struct SiteContext { let site: Site diff --git a/samhuri.net/Sources/samhuri.net/samhuri.net.swift b/samhuri.net/Sources/samhuri.net/samhuri.net.swift new file mode 100644 index 0000000..ab90d20 --- /dev/null +++ b/samhuri.net/Sources/samhuri.net/samhuri.net.swift @@ -0,0 +1,47 @@ +import Foundation +import SiteGenerator + +public enum samhuri {} + +public extension samhuri { + struct net { + let siteURLOverride: URL? + + public init(siteURLOverride: URL? = nil) { + self.siteURLOverride = siteURLOverride + } + + func buildSite(renderer: PageRenderer) -> Site { + let postsPlugin = PostsPluginBuilder(templateRenderer: renderer) + .path("posts") + .jsonFeed( + avatarPath: "images/me.jpg", + iconPath: "images/apple-touch-icon-300.png", + faviconPath: "images/apple-touch-icon-80.png" + ) + .rssFeed() + .build() + + return 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(pageRenderer: renderer) + .projects(templateRenderer: renderer) + .plugin(postsPlugin) + .build() + } + + public func generate(sourceURL: URL, targetURL: URL) throws { + let templatesURL = sourceURL.appendingPathComponent("templates") + let renderer = PageRenderer(templatesURL: templatesURL) + let site = buildSite(renderer: renderer) + let generator = try SiteGenerator(sourceURL: sourceURL, site: site) + try generator.generate(targetURL: targetURL) + } + } +} diff --git a/samhuri_net/Tests/LinuxMain.swift b/samhuri.net/Tests/LinuxMain.swift similarity index 50% rename from samhuri_net/Tests/LinuxMain.swift rename to samhuri.net/Tests/LinuxMain.swift index 086b32d..5802d8e 100644 --- a/samhuri_net/Tests/LinuxMain.swift +++ b/samhuri.net/Tests/LinuxMain.swift @@ -1,7 +1,7 @@ import XCTest -import samhuri_netTests +import samhuri.net.Tests var tests = [XCTestCaseEntry]() -tests += samhuri_netTests.allTests() +tests += samhuri.net.Tests.allTests() XCTMain(tests) diff --git a/samhuri_net/Tests/samhuri_netTests/XCTestManifests.swift b/samhuri.net/Tests/samhuri.netTests/XCTestManifests.swift similarity index 71% rename from samhuri_net/Tests/samhuri_netTests/XCTestManifests.swift rename to samhuri.net/Tests/samhuri.netTests/XCTestManifests.swift index a258601..b619c5a 100644 --- a/samhuri_net/Tests/samhuri_netTests/XCTestManifests.swift +++ b/samhuri.net/Tests/samhuri.netTests/XCTestManifests.swift @@ -3,7 +3,7 @@ import XCTest #if !canImport(ObjectiveC) public func allTests() -> [XCTestCaseEntry] { return [ - testCase(samhuri_netTests.allTests), + testCase(samhuri.net.Tests.allTests), ] } #endif diff --git a/samhuri.net/Tests/samhuri.netTests/samhuri.netTests.swift b/samhuri.net/Tests/samhuri.netTests/samhuri.netTests.swift new file mode 100644 index 0000000..bc2baf8 --- /dev/null +++ b/samhuri.net/Tests/samhuri.netTests/samhuri.netTests.swift @@ -0,0 +1,14 @@ +import XCTest +@testable import samhuri_net + +extension samhuri.net { + final class Tests: XCTestCase { + func testExample() { + XCTAssert(true) + } + + static var allTests = [ + ("testExample", testExample), + ] + } +} diff --git a/samhuri_net/Sources/samhuri_net/samhuri_net.swift b/samhuri_net/Sources/samhuri_net/samhuri_net.swift deleted file mode 100644 index 1887844..0000000 --- a/samhuri_net/Sources/samhuri_net/samhuri_net.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import SiteGenerator - -public struct samhuri_net { - public init() {} - - public func generate(sourceURL: URL, targetURL: URL, siteURLOverride: URL? = nil) throws { - let postsPlugin = PostsPluginBuilder() - .path("posts") - .jsonFeed( - 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) - } -} diff --git a/samhuri_net/Tests/samhuri_netTests/samhuri_netTests.swift b/samhuri_net/Tests/samhuri_netTests/samhuri_netTests.swift deleted file mode 100644 index f684de9..0000000 --- a/samhuri_net/Tests/samhuri_netTests/samhuri_netTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import XCTest -@testable import samhuri_net - -final class samhuri_netTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(samhuri_net().text, "Hello, World!") - } - - static var allTests = [ - ("testExample", testExample), - ] -}