diff --git a/Readme.md b/Readme.md index 809e2ba..fe2c9c1 100644 --- a/Readme.md +++ b/Readme.md @@ -119,7 +119,7 @@ Execution, trying TDD for the first time: - [x] Move template rendering from SiteGenerator to samhuri.net - - [ ] Replace page template with Swift code + - [x] Replace page template with Swift code - [ ] Replace projects.json with Swift code diff --git a/SiteGenerator/Package.resolved b/SiteGenerator/Package.resolved index 87ad4ee..d3725af 100644 --- a/SiteGenerator/Package.resolved +++ b/SiteGenerator/Package.resolved @@ -6,35 +6,8 @@ "repositoryURL": "https://github.com/johnsundell/ink.git", "state": { "branch": null, - "revision": "af743ad6882bfe1adb0acf7453d36d2075ebb1d5", - "version": "0.1.3" - } - }, - { - "package": "PathKit", - "repositoryURL": "https://github.com/kylef/PathKit.git", - "state": { - "branch": null, - "revision": "e2f5be30e4c8f531c9c1e8765aa7b71c0a45d7a0", - "version": "0.9.2" - } - }, - { - "package": "Spectre", - "repositoryURL": "https://github.com/kylef/Spectre.git", - "state": { - "branch": null, - "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", - "version": "0.9.0" - } - }, - { - "package": "Stencil", - "repositoryURL": "https://github.com/stencilproject/Stencil.git", - "state": { - "branch": null, - "revision": "0e9a78d6584e3812cd9c09494d5c7b483e8f533c", - "version": "0.13.1" + "revision": "c88bbce588a1ebfde2cf4d61eb9865a3edaa27d4", + "version": "0.2.0" } } ] diff --git a/SiteGenerator/Package.swift b/SiteGenerator/Package.swift index f50404e..614671e 100644 --- a/SiteGenerator/Package.swift +++ b/SiteGenerator/Package.swift @@ -16,7 +16,7 @@ let package = Package( targets: ["SiteGenerator"]), ], dependencies: [ - .package(url: "https://github.com/johnsundell/ink.git", from: "0.1.0"), + .package(url: "https://github.com/johnsundell/ink.git", from: "0.2.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/SiteGenerator/Sources/SiteGenerator/Posts/Feeds/RSSFeedWriter.swift b/SiteGenerator/Sources/SiteGenerator/Posts/Feeds/RSSFeedWriter.swift index e84eb14..f20df3c 100644 --- a/SiteGenerator/Sources/SiteGenerator/Posts/Feeds/RSSFeedWriter.swift +++ b/SiteGenerator/Sources/SiteGenerator/Posts/Feeds/RSSFeedWriter.swift @@ -52,14 +52,7 @@ final class RSSFeedWriter { ) let renderedPosts: [FeedPost] = try posts.map { post in let title = post.isLink ? "→ \(post.title)" : post.title - let author: String = { - if let email = site.email { - return "\(email) (\(post.author))" - } - else { - return post.author - } - }() + let author = "\(site.email) (\(post.author))" let url = site.url.appendingPathComponent(post.path) return FeedPost( title: title.escapedForXML(), diff --git a/SiteGenerator/Sources/SiteGenerator/Site.swift b/SiteGenerator/Sources/SiteGenerator/Site.swift index 9e1472d..29d198a 100644 --- a/SiteGenerator/Sources/SiteGenerator/Site.swift +++ b/SiteGenerator/Sources/SiteGenerator/Site.swift @@ -9,7 +9,7 @@ import Foundation public struct Site { public let author: String - public let email: String? + public let email: String public let title: String public let description: String? public let url: URL @@ -20,7 +20,7 @@ public struct Site { public init( author: String, - email: String?, + email: String, title: String, description: String?, url: URL, diff --git a/SiteGenerator/Sources/SiteGenerator/SiteBuilder.swift b/SiteGenerator/Sources/SiteGenerator/SiteBuilder.swift index 9fb4c42..d402b23 100644 --- a/SiteGenerator/Sources/SiteGenerator/SiteBuilder.swift +++ b/SiteGenerator/Sources/SiteGenerator/SiteBuilder.swift @@ -8,13 +8,12 @@ import Foundation public final class SiteBuilder { - private let author: String private let title: String + private let description: String? + private let author: String + private let email: String private let url: URL - private var email: String? - private var description: String? - private var styles: [String] = [] private var scripts: [String] = [] @@ -22,29 +21,17 @@ public final class SiteBuilder { private var renderers: [Renderer] = [] public init( - author: String, - email: String? = nil, title: String, description: String? = nil, + author: String, + email: String, 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.author = author self.email = email - return self - } - - public func description(_ description: String) -> SiteBuilder { - precondition(self.description == nil, "description is already defined") - self.description = description - return self + self.url = url } public func styles(_ styles: String...) -> SiteBuilder { diff --git a/gensite/Package.resolved b/gensite/Package.resolved index dd6c068..0784686 100644 --- a/gensite/Package.resolved +++ b/gensite/Package.resolved @@ -19,6 +19,15 @@ "version": "0.9.2" } }, + { + "package": "Plot", + "repositoryURL": "https://github.com/johnsundell/plot.git", + "state": { + "branch": null, + "revision": "dd7fce79ce4802afdc7d45ce34bddc5cea566202", + "version": "0.2.0" + } + }, { "package": "Spectre", "repositoryURL": "https://github.com/kylef/Spectre.git", diff --git a/samhuri.net/Package.resolved b/samhuri.net/Package.resolved index dd6c068..0784686 100644 --- a/samhuri.net/Package.resolved +++ b/samhuri.net/Package.resolved @@ -19,6 +19,15 @@ "version": "0.9.2" } }, + { + "package": "Plot", + "repositoryURL": "https://github.com/johnsundell/plot.git", + "state": { + "branch": null, + "revision": "dd7fce79ce4802afdc7d45ce34bddc5cea566202", + "version": "0.2.0" + } + }, { "package": "Spectre", "repositoryURL": "https://github.com/kylef/Spectre.git", diff --git a/samhuri.net/Package.swift b/samhuri.net/Package.swift index ff67f36..2da060c 100644 --- a/samhuri.net/Package.swift +++ b/samhuri.net/Package.swift @@ -18,6 +18,7 @@ let package = Package( dependencies: [ .package(path: "../SiteGenerator"), .package(url: "https://github.com/stencilproject/Stencil.git", from: "0.13.0"), + .package(url: "https://github.com/johnsundell/plot.git", from: "0.2.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -25,6 +26,7 @@ let package = Package( .target( name: "samhuri.net", dependencies: [ + "Plot", "SiteGenerator", "Stencil", ]), diff --git a/samhuri.net/Sources/samhuri.net/HTMLElements.swift b/samhuri.net/Sources/samhuri.net/HTMLElements.swift new file mode 100644 index 0000000..0f9591e --- /dev/null +++ b/samhuri.net/Sources/samhuri.net/HTMLElements.swift @@ -0,0 +1,42 @@ +// +// HTMLElements.swift +// samhuri.net +// +// Created by Sami Samhuri on 2019-12-18. +// + +import Foundation +import Plot + +extension Node where Context == HTML.HeadContext { + static func jsonFeedLink(_ url: URLRepresentable, title: String) -> Node { + .link(.rel(.alternate), .href(url), .type("application/json"), .attribute(named: "title", value: title)) + } +} + +extension Node where Context == HTML.HeadContext { + static func appleTouchIcon(_ url: URLRepresentable) -> Node { + .link(.attribute(named: "rel", value: "apple-touch-icon"), .href(url)) + } + + static func safariPinnedTabIcon(_ url: URLRepresentable, color: String) -> Node { + .link(.attribute(named: "rel", value: "mask-icon"), .attribute(named: "color", value: color), .href(url)) + } +} + +extension Node where Context == HTML.BodyContext { + static func asyncStylesheetLinks(_ urls: [URLRepresentable]) -> Node { + .script(""" + (function() { + var urls = [\(urls.map { "'\($0)'" }.joined(separator: ", "))]; + urls.forEach(function(url) { + var css = document.createElement('link'); + css.href = url; + css.rel = 'stylesheet'; + css.type = 'text/css'; + document.getElementsByTagName('head')[0].appendChild(css); + }); + })(); + """) + } +} diff --git a/samhuri.net/Sources/samhuri.net/PageContext.swift b/samhuri.net/Sources/samhuri.net/PageContext.swift index 63c512d..731ab0e 100644 --- a/samhuri.net/Sources/samhuri.net/PageContext.swift +++ b/samhuri.net/Sources/samhuri.net/PageContext.swift @@ -8,14 +8,31 @@ import Foundation import SiteGenerator -struct PageContext { +struct PageContext: TemplateContext { let site: Site - let body: String + @available(*, deprecated) let body: String let page: Page - let metadata: [String: String] + @available(*, deprecated) let metadata: [String: String] + + var title: String { + "\(site.title): \(page.title)" + } + + var styles: [URL] { + (site.styles + page.styles).map { style in + style.hasPrefix("http") ? URL(string: style)! : url(for: style) + } + } + + var scripts: [URL] { + (site.scripts + page.scripts).map { script in + script.hasPrefix("http") ? URL(string: script)! : url(for: script) + } + } } extension PageContext { + @available(*, deprecated) var dictionary: [String: Any] { [ "site": site, diff --git a/samhuri.net/Sources/samhuri.net/PageRenderer.swift b/samhuri.net/Sources/samhuri.net/PageRenderer.swift index c7a6fa5..b4001df 100644 --- a/samhuri.net/Sources/samhuri.net/PageRenderer.swift +++ b/samhuri.net/Sources/samhuri.net/PageRenderer.swift @@ -6,8 +6,11 @@ // import Foundation -import PathKit +import Plot import SiteGenerator + +#warning("Deprecated imports") +import PathKit import Stencil final class PageRenderer { @@ -19,14 +22,79 @@ final class PageRenderer { let loader = FileSystemLoader(paths: [templatesPath]) self.stencil = Environment(loader: loader) } + + func siteTemplate(body: Node, context: TemplateContext) -> HTML { + HTML(.lang("en"), + .head( + .encoding(.utf8), + .viewport(.accordingToDevice), + .title(context.title), + .siteName(context.site.title), + .url(context.site.url), + .rssFeedLink(context.url(for: "feed.xml"), title: context.site.title), + .jsonFeedLink(context.url(for: "feed.json"), title: context.site.title), + .link(.rel(.author), .type("text/plain"), .href(context.url(for: "humans.txt"))), + .link(.rel(.icon), .type("image/png"), .href(context.imageURL("favicon-32x32.png"))), + .link(.rel(.shortcutIcon), .href(context.imageURL("favicon.icon"))), + .appleTouchIcon(context.imageURL("apple-touch-icon.png")), + .safariPinnedTabIcon(context.imageURL("safari-pinned-tab.svg"), color: "#aa0000"), + .link(.attribute(named: "rel", value: "manifest"), .href(context.imageURL("manifest.json"))), + .meta(.name("msapplication-config"), .content(context.imageURL("browserconfig.xml").absoluteString)), + .meta(.name("theme-color"), .content("#ffffff")), + .link(.rel(.dnsPrefetch), .href("https://use.typekit.net")), + .link(.rel(.dnsPrefetch), .href("https://netdna.bootstrapcdn.com")), + .link(.rel(.dnsPrefetch), .href("https://gist.github.com")) + ), + .body( + .header(.class("primary"), + .div(.class("title"), + .h1(.a(.href(context.site.url), .text(context.site.title))), + .br(), + .h4(.text("By "), .a(.href(context.url(for: "about")), .text(context.site.author))) + ), + .nav( + .ul( + .li(.a(.href(context.url(for: "about")), "About")), + .li(.a(.href(context.url(for: "posts")), "Archive")), + .li(.a(.href(context.url(for: "projects")), "Projects")), + .li(.class("twitter"), .a(.href("https://twitter.com/_sjs"), .i(.class("fa fa-twitter")))), + .li(.class("github"), .a(.href("https://github.com/samsonjs"), .i(.class("fa fa-github")))), + .li(.class("email"), .a(.href("mailto:\(context.site.email)"), .i(.class("fa fa-envelope")))), + .li(.class("rss"), .a(.href(context.url(for: "feed.xml")), .i(.class("fa fa-rss")))) + ) + ), + .div(.class("clearfix")) + ), + body, + .footer(.class("container"), + "© 2006 - \(context.currentYear)", + .a(.href(context.url(for: "about")), .text(context.site.author)) + ), + .asyncStylesheetLinks(context.styles), + .group(context.scripts.map { script in + .script(.attribute(named: "defer"), .src(script)) + }), + .script(.src("https://use.typekit.net/tcm1whv.js"), .attribute(named: "crossorigin", value: "anonymous")), + .script("try{Typekit.load({ async: true });}catch(e){}") + ) + ) + } } 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 + let body: Node = .group([ + .article(.class("container"), + .h1(.text(page.title)), + .raw(bodyHTML) + ), + .div(.class("row clearfix"), + .p(.class("fin"), .i(.class("fa fa-code"))) + ) + ]) + return siteTemplate(body: body, context: context).render(indentedBy: .spaces(2)) } } diff --git a/samhuri.net/Sources/samhuri.net/SiteContext.swift b/samhuri.net/Sources/samhuri.net/SiteContext.swift index 2285897..63fe678 100644 --- a/samhuri.net/Sources/samhuri.net/SiteContext.swift +++ b/samhuri.net/Sources/samhuri.net/SiteContext.swift @@ -8,11 +8,33 @@ import Foundation import SiteGenerator -struct SiteContext { +struct SiteContext: TemplateContext { let site: Site + let subtitle: String? - init(site: Site) { + init(site: Site, subtitle: String? = nil) { self.site = site + self.subtitle = subtitle + } + + var title: String { + guard let subtitle = subtitle else { + return site.title + } + + return "\(site.title): \(subtitle)" + } + + var styles: [URL] { + site.styles.map { style in + style.hasPrefix("http") ? URL(string: style)! : url(for: style) + } + } + + var scripts: [URL] { + site.scripts.map { script in + script.hasPrefix("http") ? URL(string: script)! : url(for: script) + } } } diff --git a/samhuri.net/Sources/samhuri.net/TemplateContext.swift b/samhuri.net/Sources/samhuri.net/TemplateContext.swift new file mode 100644 index 0000000..f42b4da --- /dev/null +++ b/samhuri.net/Sources/samhuri.net/TemplateContext.swift @@ -0,0 +1,41 @@ +// +// TemplateContext.swift +// samhuri.net +// +// Created by Sami Samhuri on 2019-12-18. +// + +import Foundation +import SiteGenerator + +protocol TemplateContext { + // Concrete requirements, must be implemented + + var site: Site { get } + var title: String { get } + var styles: [URL] { get } + var scripts: [URL] { get } + + // These all have default implementations + + var currentYear: Int { get } + + func url(for path: String) -> URL + func imageURL(_ filename: String) -> URL +} + +extension TemplateContext { + var currentYear: Int { + Date().year + } + + func url(for path: String) -> URL { + site.url.appendingPathComponent(path) + } + + func imageURL(_ filename: String) -> URL { + site.url + .appendingPathComponent("images") + .appendingPathComponent(filename) + } +} diff --git a/samhuri.net/Sources/samhuri.net/samhuri.net.swift b/samhuri.net/Sources/samhuri.net/samhuri.net.swift index ab90d20..5cade17 100644 --- a/samhuri.net/Sources/samhuri.net/samhuri.net.swift +++ b/samhuri.net/Sources/samhuri.net/samhuri.net.swift @@ -23,13 +23,14 @@ public extension samhuri { .build() return SiteBuilder( - author: "Sami Samhuri", - email: "sami@samhuri.net", title: "samhuri.net", description: "just some blog", + author: "Sami Samhuri", + email: "sami@samhuri.net", url: siteURLOverride ?? URL(string: "https://samhuri.net")! ) .styles("/css/normalize.css", "/css/style.css") + .styles("https://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css") .renderMarkdown(pageRenderer: renderer) .projects(templateRenderer: renderer) .plugin(postsPlugin) diff --git a/templates/page.html b/templates/page.html deleted file mode 100644 index 7a4ce4a..0000000 --- a/templates/page.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "samhuri.net.html" %} - -{% block body %} -
-

{{ page.title }}

- {{ body }} -
-
-

-
-{% endblock %} diff --git a/templates/samhuri.net.html b/templates/samhuri.net.html index 959570f..cdd3edc 100644 --- a/templates/samhuri.net.html +++ b/templates/samhuri.net.html @@ -31,11 +31,7 @@ -{% if bodyClassNames %} - -{% else %} -{% endif %}