Render Markdown pages using Plot instead of Stencil

This commit is contained in:
Sami Samhuri 2019-12-18 23:04:02 -08:00
parent e22c17e810
commit 8b676c443a
17 changed files with 235 additions and 86 deletions

View file

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

View file

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

View file

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

View file

@ -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(),

View file

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

View file

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

View file

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

View file

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

View file

@ -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",
]),

View file

@ -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<HTML.HeadContext> {
.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<HTML.HeadContext> {
.link(.attribute(named: "rel", value: "apple-touch-icon"), .href(url))
}
static func safariPinnedTabIcon(_ url: URLRepresentable, color: String) -> Node<HTML.HeadContext> {
.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<HTML.BodyContext> {
.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);
});
})();
""")
}
}

View file

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

View file

@ -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<HTML.BodyContext>, 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<HTML.BodyContext> = .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))
}
}

View file

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

View file

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

View file

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

View file

@ -1,11 +0,0 @@
{% extends "samhuri.net.html" %}
{% block body %}
<article class="container">
<h1>{{ page.title }}</h1>
{{ body }}
</article>
<div class="row clearfix">
<p class="fin"><i class="fa fa-code"></i></p>
</div>
{% endblock %}

View file

@ -31,11 +31,7 @@
<link rel="dns-prefetch" href="https://gist.github.com">
</head>
{% if bodyClassNames %}
<body class="{{ bodyClassNames }}">
{% else %}
<body>
{% endif %}
<header class="primary">
<div class="title">
<h1><a href="/">{{ site.title }}</a></h1>