mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
Render Markdown pages using Plot instead of Stencil
This commit is contained in:
parent
e22c17e810
commit
8b676c443a
17 changed files with 235 additions and 86 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]),
|
||||
|
|
|
|||
42
samhuri.net/Sources/samhuri.net/HTMLElements.swift
Normal file
42
samhuri.net/Sources/samhuri.net/HTMLElements.swift
Normal 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);
|
||||
});
|
||||
})();
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
41
samhuri.net/Sources/samhuri.net/TemplateContext.swift
Normal file
41
samhuri.net/Sources/samhuri.net/TemplateContext.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue