Render all posts pages and RSS feed with Plot and drop Stencil

This commit is contained in:
Sami Samhuri 2019-12-22 17:25:13 -08:00
parent f5aa5d71b3
commit 4318c0b903
75 changed files with 541 additions and 878 deletions

View file

@ -1,2 +1,2 @@
exclude = "{$exclude,SiteGenerator,samhuri.net,gensite,www,tweets,wayback,actual}"
exclude = "{$exclude,samhuri.net,gensite,www,tweets,wayback,actual}"
include = "{$include,.gitignore}"

View file

@ -111,7 +111,7 @@ Execution, trying TDD for the first time:
- [x] Minify JS? Now that we're keeping node, why not ... Nope! Ditched node too
- [ ] Convert to a system of packages: SiteGenerator, samhuri_net, and gensite (executable)
- [x] Convert to a system of packages: SiteGenerator, samhuri_net, and gensite (executable)
- [x] Create new packages and distribute the code accordingly
@ -125,27 +125,27 @@ Execution, trying TDD for the first time:
- [x] Replace project templates with Swift code
- [ ] Replace post templates with Swift code
- [x] Replace post templates with Swift code
- [x] Archive
- [ ] Year posts
- [x] Year posts
- [ ] Month posts
- [x] Month posts
- [ ] Post
- [x] Post
- [ ] Feed post
- [x] Recent posts
- [ ] Munge relative URLs in the RSS and JSON feeds to be absolute instead
- [x] Replace RSS feed with Swift code
- [ ] Recent posts
- [x] Feed post template
- [ ] RSS feed
- [x] RSS feed template
- [ ] Replace RSS feed template with Swift code
- [x] Munge relative URLs in the RSS and JSON feeds to be absolute instead
- [ ] Remove stencil
- [x] Remove stencil
- [x] Add a server for local use and simple production setups (or use a file watcher + `python -m SimpleHTTPServer`?)

View file

@ -1,6 +0,0 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
/.swiftpm
xcuserdata/

View file

@ -1,16 +0,0 @@
{
"object": {
"pins": [
{
"package": "Ink",
"repositoryURL": "https://github.com/johnsundell/ink.git",
"state": {
"branch": null,
"revision": "c88bbce588a1ebfde2cf4d61eb9865a3edaa27d4",
"version": "0.2.0"
}
}
]
},
"version": 1
}

View file

@ -1,33 +0,0 @@
// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "SiteGenerator",
platforms: [
.macOS(.v10_15),
.iOS(.v13),
],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(
name: "SiteGenerator",
targets: ["SiteGenerator"]),
],
dependencies: [
.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.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "SiteGenerator",
dependencies: [
"Ink",
]),
.testTarget(
name: "SiteGeneratorTests",
dependencies: ["SiteGenerator"]),
]
)

View file

@ -1,5 +0,0 @@
# SiteGenerator
A static site generator.
See https://github.com/samsonjs/samhuri.net for details.

View file

@ -1,22 +0,0 @@
//
// Date+Sugar.swift
// SiteGenerator
//
// Created by Sami Samhuri on 2019-12-02.
//
import Foundation
extension Date {
var year: Int {
Calendar.current.dateComponents([.year], from: self).year!
}
var month: Int {
Calendar.current.dateComponents([.month], from: self).month!
}
var day: Int {
Calendar.current.dateComponents([.day], from: self).day!
}
}

View file

@ -1,76 +0,0 @@
//
// RSSFeedWriter.swift
// SiteGenerator
//
// Created by Sami Samhuri on 2019-12-10.
//
import Foundation
private struct FeedSite {
let title: String
let description: String?
let url: String
}
private struct FeedPost {
let title: String
let date: String
let author: String
let link: String
let guid: String
let body: String
}
private let rfc822Formatter: DateFormatter = {
let f = DateFormatter()
f.locale = Locale(identifier: "en_US_POSIX")
f.dateFormat = "EEE, d MMM yyyy HH:mm:ss ZZZ"
return f
}()
private extension Date {
var rfc822: String {
rfc822Formatter.string(from: self)
}
}
final class RSSFeedWriter {
let fileManager: FileManager
let feed: RSSFeed
init(fileManager: FileManager = .default, feed: RSSFeed) {
self.fileManager = fileManager
self.feed = feed
}
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(),
url: site.url.absoluteString.escapedForXML()
)
let renderedPosts: [FeedPost] = try posts.map { post in
let title = post.isLink ? "\(post.title)" : post.title
let author = "\(site.email) (\(post.author))"
let url = site.url.appendingPathComponent(post.path)
return FeedPost(
title: title.escapedForXML(),
date: post.date.rfc822.escapedForXML(),
author: author.escapedForXML(),
link: (post.link ?? url).absoluteString.escapedForXML(),
guid: url.absoluteString.escapedForXML(),
body: try templateRenderer.renderTemplate(.feedPost, site: site, context: [
"post": post,
]).escapedForXML()
)
}
let feedXML = try templateRenderer.renderTemplate(.rssFeed, site: site, context: [
"site": feedSite,
"feedURL": site.url.appendingPathComponent(feed.path).absoluteString.escapedForXML(),
"posts": renderedPosts,
])
let feedURL = targetURL.appendingPathComponent(feed.path)
try feedXML.write(to: feedURL, atomically: true, encoding: .utf8)
}
}

View file

@ -1,22 +0,0 @@
//
// 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
}

View file

@ -1,20 +0,0 @@
//
// Project.swift
// SiteGenerator
//
// Created by Sami Samhuri on 2019-12-02.
//
import Foundation
public struct Project {
public let title: String
public let description: String
public let url: URL
public init(title: String, description: String, url: URL) {
self.title = title
self.description = description
self.url = url
}
}

View file

@ -1,7 +0,0 @@
import XCTest
import SiteGeneratorTests
var tests = [XCTestCaseEntry]()
tests += SiteGeneratorTests.allTests()
XCTMain(tests)

View file

@ -1,47 +0,0 @@
import XCTest
@testable import SiteGenerator
final class SiteGeneratorTests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
// Some of the APIs that we use below are available in macOS 10.13 and above.
guard #available(macOS 10.13, *) else {
return
}
let fooBinary = productsDirectory.appendingPathComponent("SiteGenerator")
let process = Process()
process.executableURL = fooBinary
let pipe = Pipe()
process.standardOutput = pipe
try process.run()
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)
XCTAssertEqual(output, "")
}
/// Returns path to the built products directory.
var productsDirectory: URL {
#if os(macOS)
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
return bundle.bundleURL.deletingLastPathComponent()
}
fatalError("couldn't find the products directory")
#else
return Bundle.main.bundleURL
#endif
}
static var allTests = [
("testExample", testExample),
]
}

View file

@ -1,9 +0,0 @@
import XCTest
#if !canImport(ObjectiveC)
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(SiteGeneratorTests.allTests),
]
}
#endif

View file

@ -10,15 +10,6 @@
"version": "0.2.0"
}
},
{
"package": "PathKit",
"repositoryURL": "https://github.com/kylef/PathKit.git",
"state": {
"branch": null,
"revision": "e2f5be30e4c8f531c9c1e8765aa7b71c0a45d7a0",
"version": "0.9.2"
}
},
{
"package": "Plot",
"repositoryURL": "https://github.com/johnsundell/plot.git",
@ -27,24 +18,6 @@
"revision": "dd7fce79ce4802afdc7d45ce34bddc5cea566202",
"version": "0.2.0"
}
},
{
"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"
}
}
]
},

View file

@ -18,13 +18,13 @@ If you don't care what I did or why then you can just [see the updated script][g
The diff command is repeated. This is any easy win:
<pre>
```bash
diff-index() {
git diff-index -p -M --cached HEAD -- "$@"
}
if diff-index '*Tests.swift' | ...
</pre>
```
You get the idea.
@ -34,7 +34,9 @@ One problem is that the bootstrap script uses an absolute path when creating a s
That's easily fixed by using a relative path to your pre-commit hook, like so:
<pre>ln -s ../../scripts/pre-commit.sh .git/hooks/pre-commit</pre>
```bash
ln -s ../../scripts/pre-commit.sh .git/hooks/pre-commit
```
Ah, this is more flexible! Of course if you ever move the script itself then it's on you to update the symlink and bootstrap.sh, but that was already the case anyway.
@ -46,42 +48,42 @@ Ok great so this script tells me there are errors. Well, script, what exactly _a
First ignore the fact I'm talking to a shell script. I don't get out much. Anyway... now we need to pull out the regular expressions and globs so we can reuse them to show what the actual errors are if we find any.
<pre>
```bash
test_pattern='^\+\s*\b(fdescribe|fit|fcontext|xdescribe|xit|xcontext)\('
test_glob='*Tests.swift *Specs.swift'
if diff-index $test_glob | egrep "$test_pattern" >/dev/null 2>&1
...
</pre>
```
_Pro tip: I prefixed test\_pattern with `\b` to only match word boundaries to reduce false positives._
And:
<pre>
```bash
misplaced_pattern='misplaced="YES"'
misplaced_glob='*.xib *.storyboard'
if diff-index $misplaced_glob | grep '^+' | egrep "$misplaced_pattern" >/dev/null 2>&1
...
</pre>
```
You may notice that I snuck in `*Specs.swift` as well. Let's not be choosy about file naming.
Then we need to show where the errors are by using `diff-indef`, with an `|| true` at the end because the whole script fails if any single command fails, and `git diff-index` regularly exits with non-zero status (I didn't look into why that is).
<pre>
```bash
echo "COMMIT REJECTED for fdescribe/fit/fcontext/xdescribe/xit/xcontext." >&2
echo "Remove focused and disabled tests before committing." >&2
diff-index $test_glob | egrep -2 "$test_pattern" || true >&2
echo '----' >&2
</pre>
```
And for misplaced views:
<pre>
```bash
echo "COMMIT REJECTED for misplaced views. Correct them before committing." >&2
git grep -E "$misplaced_pattern" $misplaced_glob || true >&2
echo '----' >&2
</pre>
```
## Fix all the things, at once
@ -91,15 +93,21 @@ The first step is to exit at the end using a code in a variable that is set to 1
Up top:
<pre>failed=0</pre>
```bash
failed=0
```
In the middle, where we detect errors:
<pre>failed=1</pre>
```bash
failed=1
```
And at the bottom:
<pre>exit $failed</pre>
```bash
exit $failed
```
That's all there is to it. If we don't exit early then all the code runs.
@ -113,7 +121,7 @@ Those were all the obvious improvements in my mind and now I'm using this modifi
Here's the whole thing put together:
<pre>
```bash
#!/usr/bin/env bash
#
# Based on http://merowing.info/2016/08/setting-up-pre-commit-hook-for-ios/
@ -148,4 +156,4 @@ then
fi
exit $failed
</pre>
```

View file

@ -10,15 +10,6 @@
"version": "0.2.0"
}
},
{
"package": "PathKit",
"repositoryURL": "https://github.com/kylef/PathKit.git",
"state": {
"branch": null,
"revision": "e2f5be30e4c8f531c9c1e8765aa7b71c0a45d7a0",
"version": "0.9.2"
}
},
{
"package": "Plot",
"repositoryURL": "https://github.com/johnsundell/plot.git",
@ -27,24 +18,6 @@
"revision": "dd7fce79ce4802afdc7d45ce34bddc5cea566202",
"version": "0.2.0"
}
},
{
"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"
}
}
]
},

View file

@ -16,8 +16,7 @@ let package = Package(
targets: ["samhuri.net"]),
],
dependencies: [
.package(path: "../SiteGenerator"),
.package(url: "https://github.com/stencilproject/Stencil.git", from: "0.13.0"),
.package(url: "https://github.com/johnsundell/ink.git", from: "0.2.0"),
.package(url: "https://github.com/johnsundell/plot.git", from: "0.2.0"),
],
targets: [
@ -26,9 +25,8 @@ let package = Package(
.target(
name: "samhuri.net",
dependencies: [
"Ink",
"Plot",
"SiteGenerator",
"Stencil",
]),
.testTarget(
name: "samhuri.netTests",

View file

@ -11,4 +11,12 @@ extension Date {
var year: Int {
Calendar.current.dateComponents([.year], from: self).year!
}
var month: Int {
Calendar.current.dateComponents([.month], from: self).month!
}
var day: Int {
Calendar.current.dateComponents([.day], from: self).day!
}
}

View file

@ -6,7 +6,6 @@
//
import Foundation
import SiteGenerator
struct Page {
let title: String

View file

@ -7,22 +7,8 @@
import Foundation
import Plot
import SiteGenerator
#warning("Deprecated imports")
import PathKit
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)
}
func render(_ body: Node<HTML.BodyContext>, context: TemplateContext) -> String {
Template.site(body: body, context: context).render(indentedBy: .spaces(2))
}
@ -30,51 +16,8 @@ final class PageRenderer {
extension PageRenderer: MarkdownPageRenderer {
func renderPage(site: Site, bodyHTML: String, metadata: [String: String]) throws -> String {
let pageTitle = metadata["Title", default: ""]
let pageTitle = metadata["Title"]
let context = SiteContext(site: site, subtitle: pageTitle, templateAssets: .none())
return render(.page(title: pageTitle, bodyHTML: bodyHTML), context: context)
}
}
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, subtitle: nil, templateAssets: .none())
let contextDict = siteContext.dictionary.merging(context, uniquingKeysWith: { _, new in new })
return try stencil.renderTemplate(name: template.htmlFilename, context: contextDict)
}
}
extension PageRenderer: ProjectsTemplateRenderer {
func renderProjects(_ projects: [Project], site: Site, assets: TemplateAssets) throws -> String {
let context = SiteContext(site: site, subtitle: "Projects", templateAssets: assets)
return render(.projects(projects), context: context)
}
func renderProject(_ project: Project, site: Site, assets: TemplateAssets) throws -> String {
let projectContext = ProjectContext(project: project, site: site, templateAssets: assets)
let context = SiteContext(site: site, subtitle: project.title, templateAssets: assets)
return render(.project(projectContext), context: context)
return render(.page(title: pageTitle ?? "", bodyHTML: bodyHTML), context: context)
}
}

View file

@ -0,0 +1,30 @@
//
// FeedPostTemplate.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-22.
//
import Foundation
import Plot
private extension Node where Context == HTML.BodyContext {
static func link(_ attributes: Attribute<HTML.LinkContext>...) -> Self {
.element(named: "link", attributes: attributes)
}
}
extension Node where Context == HTML.BodyContext {
static func feedPost(_ post: Post, url: URL, styles: [URL]) -> Self {
.group([
.group(styles.map { style in
.link(.rel(.stylesheet), .href(style), .type("text/css"))
}),
.div(
.p(.class("time"), .text(post.formattedDate)),
.raw(post.body),
.p(.a(.class("permalink"), .href(url), ""))
),
])
}
}

View file

@ -0,0 +1,22 @@
//
// MonthPostsTemplate.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-22.
//
import Foundation
import Plot
extension Node where Context == HTML.BodyContext {
static func monthPosts(_ posts: MonthPosts) -> Self {
.group([
.div(.class("container"),
.h1("\(posts.month.name) \(posts.year)")
),
.group(posts.posts.sorted(by: >).map { post in
.div(.class("container"), self.post(post))
})
])
}
}

View file

@ -1,8 +1,69 @@
//
// File.swift
//
// PageRenderer+Posts.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-22.
//
import Foundation
import Plot
extension PageRenderer: PostsTemplateRenderer {
func renderArchive(postsByYear: PostsByYear, site: Site, assets: TemplateAssets) throws -> String {
let context = SiteContext(site: site, subtitle: "Archive", templateAssets: assets)
return render(.archive(postsByYear), context: context)
}
func renderYearPosts(_ yearPosts: YearPosts, site: Site, assets: TemplateAssets) throws -> String {
let context = SiteContext(site: site, subtitle: yearPosts.title, templateAssets: assets)
return render(.yearPosts(yearPosts), context: context)
}
func renderMonthPosts(_ posts: MonthPosts, site: Site, assets: TemplateAssets) throws -> String {
let context = SiteContext(site: site, subtitle: "\(posts.month.name) \(posts.year)", templateAssets: assets)
return render(.monthPosts(posts), context: context)
}
func renderPost(_ post: Post, site: Site, assets: TemplateAssets) throws -> String {
let context = SiteContext(site: site, subtitle: post.title, templateAssets: assets)
return render(.post(post), context: context)
}
func renderRecentPosts(_ posts: [Post], site: Site, assets: TemplateAssets) throws -> String {
let context = SiteContext(site: site, subtitle: nil, templateAssets: assets)
return render(.recentPosts(posts), context: context)
}
// MARK: - Feeds
func renderFeedPost(_ post: Post, site: Site, assets: TemplateAssets) throws -> String {
let context = SiteContext(site: site, subtitle: post.title, templateAssets: assets)
let url = site.url.appendingPathComponent(post.path)
// Turn relative URLs into absolute ones.
return Node.feedPost(post, url: url, styles: context.styles)
.render(indentedBy: .spaces(2))
.replacingOccurrences(of: "href=\"/", with: "href=\"\(site.url)/")
.replacingOccurrences(of: "src=\"/", with: "src=\"\(site.url)/")
}
func renderRSSFeed(posts: [Post], feedURL: URL, site: Site, assets: TemplateAssets) throws -> String {
try RSS(
.title(site.title),
.if(site.description != nil, .description(site.description!)),
.link(site.url),
.pubDate(posts[0].date),
.atomLink(feedURL),
.group(posts.map { post in
let url = site.url.appendingPathComponent(post.path)
return .item(
.title(post.isLink ? "\(post.title)" : post.title),
.pubDate(post.date),
.element(named: "author", text: post.author),
.link(url),
.guid(.text(url.absoluteString), .isPermaLink(true)),
.content(try renderFeedPost(post, site: site, assets: assets))
)
})
).render(indentedBy: .spaces(2))
}
}

View file

@ -0,0 +1,34 @@
//
// PostTemplate.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-22.
//
import Foundation
import Plot
extension Node where Context == HTML.BodyContext {
static func post(_ post: Post) -> Self {
.group([
.article(
.header(
.h2(postTitleLink(post)),
.time(.text(post.formattedDate)),
.a(.class("permalink"), .href(post.path), "")
),
.raw(post.body)
),
.div(.class("row clearfix"),
.p(.class("fin"), .i(.class("fa fa-code")))
)
])
}
static func postTitleLink(_ post: Post) -> Self {
.if(post.isLink,
.a(.href(post.link?.absoluteString ?? post.path), "\(post.title)"),
else: .a(.href(post.path), .text(post.title))
)
}
}

View file

@ -0,0 +1,22 @@
//
// ArchiveTemplate.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-21.
//
import Foundation
import Plot
extension Node where Context == HTML.BodyContext {
static func archive(_ postsByYear: PostsByYear) -> Self {
.group([
.div(.class("container"),
.h1("Archive")
),
.group(postsByYear.years.sorted(by: >).map { year in
.yearPosts(postsByYear[year])
}),
])
}
}

View file

@ -0,0 +1,17 @@
//
// RecentPostsTemplate.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-22.
//
import Foundation
import Plot
extension Node where Context == HTML.BodyContext {
static func recentPosts(_ posts: [Post]) -> Self {
.div(.class("container"),
.group(posts.map(post))
)
}
}

View file

@ -0,0 +1,51 @@
//
// YearPostsTemplate.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-21.
//
import Foundation
import Plot
extension Node where Context == HTML.BodyContext {
static func yearPosts(_ posts: YearPosts) -> Self {
.div(.class("container"),
.h2(.class("year"),
.a(.href(posts.path), .text(posts.title))
),
.group(posts.months.sorted(by: >).map { month in
.monthTitles(posts[month])
})
)
}
static func monthTitles(_ posts: MonthPosts) -> Self {
.group([
.h3(.class("month"),
.a(.href(posts.path), "\(posts.month.name)")
),
.ul(.class("archive"),
.group(posts.posts.sorted(by: >).map { post in
.postItem(post, date: "\(post.date.day) \(posts.month.abbreviation)")
})
),
])
}
}
extension Node where Context == HTML.ListContext {
static func postItem(_ post: Post, date: Node<HTML.BodyContext>) -> Self {
.if(post.isLink, .li(
.a(.href(post.link?.absoluteString ?? post.path), "\(post.title)"),
.time(date),
.a(.class("permalink"), .href(post.path), "")
),
else: .li(
.a(.href(post.path), .text(post.title)),
.time(date)
)
)
}
}

View file

@ -1,8 +1,22 @@
//
// File.swift
//
// PageRenderer+Projects.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-22.
//
import Foundation
import Plot
extension PageRenderer: ProjectsTemplateRenderer {
func renderProjects(_ projects: [Project], site: Site, assets: TemplateAssets) throws -> String {
let context = SiteContext(site: site, subtitle: "Projects", templateAssets: assets)
return render(.projects(projects), context: context)
}
func renderProject(_ project: Project, site: Site, assets: TemplateAssets) throws -> String {
let projectContext = ProjectContext(project: project, site: site, templateAssets: assets)
let context = SiteContext(site: site, subtitle: project.title, templateAssets: assets)
return render(.project(projectContext), context: context)
}
}

View file

@ -6,7 +6,6 @@
//
import Foundation
import SiteGenerator
struct ProjectContext: TemplateContext {
let site: Site

View file

@ -1,5 +1,5 @@
//
// ProjectTemplates.swift
// ProjectTemplate.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-19.
@ -7,29 +7,9 @@
import Foundation
import Plot
import SiteGenerator
extension Node where Context == HTML.BodyContext {
static func projects(_ projects: [Project]) -> Node<HTML.BodyContext> {
.group([
.article(.class("container"),
.h1("Projects"),
.group(projects.map { project in
.div(.class("project-listing"),
.h4(.a(.href(project.url), .text(project.title))),
.p(.class("description"), .text(project.description))
)
})
),
.div(.class("row clearfix"),
.p(.class("fin"), .i(.class("fa fa-code")))
)
])
}
static func project(_ context: ProjectContext) -> Node<HTML.BodyContext> {
static func project(_ context: ProjectContext) -> Self {
.group([
.article(.class("container project"),
// projects.js picks up this data-title attribute and uses it to render all the Github stuff

View file

@ -0,0 +1,30 @@
//
// ProjectsTemplate.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-19.
//
import Foundation
import Plot
extension Node where Context == HTML.BodyContext {
static func projects(_ projects: [Project]) -> Self {
.group([
.article(.class("container"),
.h1("Projects"),
.group(projects.map { project in
.div(.class("project-listing"),
.h4(.a(.href(project.url), .text(project.title))),
.p(.class("description"), .text(project.description))
)
})
),
.div(.class("row clearfix"),
.p(.class("fin"), .i(.class("fa fa-code")))
)
])
}
}

View file

@ -1,16 +1,16 @@
//
// BuiltSite.swift
// SiteGenerator
// samhuri.net
//
// 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]
struct BuiltSite {
let site: Site
let plugins: [Plugin]
let renderers: [Renderer]
init(site: Site, plugins: [Plugin], renderers: [Renderer]) {
self.site = site

View file

@ -1,12 +1,12 @@
//
// MarkdownPageRenderer.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-03.
//
import Foundation
public protocol MarkdownPageRenderer {
protocol MarkdownPageRenderer {
func renderPage(site: Site, bodyHTML: String, metadata: [String: String]) throws -> String
}

View file

@ -1,6 +1,6 @@
//
// MarkdownRenderer.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-02.
//
@ -8,21 +8,21 @@
import Foundation
import Ink
public final class MarkdownRenderer: Renderer {
final class MarkdownRenderer: Renderer {
let fileManager: FileManager = .default
let markdownParser = MarkdownParser()
let pageRenderer: MarkdownPageRenderer
public init(pageRenderer: MarkdownPageRenderer) {
init(pageRenderer: MarkdownPageRenderer) {
self.pageRenderer = pageRenderer
}
public func canRenderFile(named filename: String, withExtension ext: String) -> Bool {
func canRenderFile(named filename: String, withExtension ext: String) -> Bool {
ext == "md"
}
/// Parse Markdown and render it as HTML, running it through a Stencil template.
public func render(site: Site, fileURL: URL, targetDir: URL) throws {
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)

View file

@ -1,13 +1,13 @@
//
// Plugin.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-02.
//
import Foundation
public protocol Plugin {
protocol Plugin {
func setUp(site: Site, sourceURL: URL) throws
func render(site: Site, targetURL: URL) throws

View file

@ -1,6 +1,6 @@
//
// JSONFeed.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-15.
//

View file

@ -1,6 +1,6 @@
//
// JSONFeedWriter.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-10.
//
@ -54,9 +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(.feedPost, site: site, context: [
"post": post,
]),
content_html: try templateRenderer.renderFeedPost(post, site: site, assets: .none()),
tags: post.tags
)
}

View file

@ -1,6 +1,6 @@
//
// RSSFeed.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-15.
//

View file

@ -0,0 +1,25 @@
//
// RSSFeedWriter.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-10.
//
import Foundation
final class RSSFeedWriter {
let fileManager: FileManager
let feed: RSSFeed
init(fileManager: FileManager = .default, feed: RSSFeed) {
self.fileManager = fileManager
self.feed = feed
}
func writeFeed(_ posts: [Post], for site: Site, to targetURL: URL, with templateRenderer: PostsTemplateRenderer) throws {
let feedURL = site.url.appendingPathComponent(feed.path)
let feedXML = try templateRenderer.renderRSSFeed(posts: posts, feedURL: feedURL, site: site, assets: .none())
let feedFileURL = targetURL.appendingPathComponent(feed.path)
try feedXML.write(to: feedFileURL, atomically: true, encoding: .utf8)
}
}

View file

@ -1,6 +1,6 @@
//
// XMLEscape.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-12.
//
@ -8,6 +8,7 @@
import Foundation
extension String {
@available(*, deprecated)
func escapedForXML() -> String {
replacingOccurrences(of: "&", with: "&amp;")
.replacingOccurrences(of: "<", with: "&lt;")

View file

@ -1,6 +1,6 @@
//
// Month.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-03.
//

View file

@ -1,6 +1,6 @@
//
// Post.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-01.
//
@ -18,11 +18,6 @@ struct Post {
let body: String
let path: String
// These are computed properties but are computed eagerly because
// Stencil is unable to use real computed properties at this time.
let isLink: Bool
let day: Int
init(slug: String, title: String, author: String, date: Date, formattedDate: String, link: URL?, tags: [String], body: String, path: String) {
self.slug = slug
self.title = title
@ -33,10 +28,10 @@ struct Post {
self.tags = tags
self.body = body
self.path = path
}
// Eagerly computed properties
self.isLink = link != nil
self.day = date.day
var isLink: Bool {
link != nil
}
}

View file

@ -1,6 +1,6 @@
//
// PostRepo.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-09.
//

View file

@ -1,6 +1,6 @@
//
// PostTransformer.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-09.
//

View file

@ -1,6 +1,6 @@
//
// PostWriter.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-09.
//
@ -22,10 +22,7 @@ final class PostWriter {
extension PostWriter {
func writePosts(_ posts: [Post], for site: Site, to targetURL: URL, with templateRenderer: PostsTemplateRenderer) throws {
for post in posts {
let postHTML = try templateRenderer.renderTemplate(.post, site: site, context: [
"title": post.title,
"post": post,
])
let postHTML = try templateRenderer.renderPost(post, site: site, assets: .none())
let postURL = targetURL
.appendingPathComponent(outputPath)
.appendingPathComponent(filePath(date: post.date, slug: post.slug))
@ -43,9 +40,7 @@ extension PostWriter {
extension PostWriter {
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 recentPostsHTML = try templateRenderer.renderRecentPosts(recentPosts, site: site, assets: .none())
let fileURL = targetURL.appendingPathComponent("index.html")
try fileManager.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
try recentPostsHTML.write(to: fileURL, atomically: true, encoding: .utf8)
@ -56,50 +51,20 @@ extension PostWriter {
extension PostWriter {
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(.archive, site: site, context: [
"title": "Archive",
"years": allYears.map { contextDictionaryForYearPosts(posts[$0]) },
])
let archiveHTML = try templateRenderer.renderArchive(postsByYear: posts, site: site, assets: .none())
let archiveURL = targetURL.appendingPathComponent(outputPath).appendingPathComponent("index.html")
try fileManager.createDirectory(at: archiveURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
try archiveHTML.write(to: archiveURL, atomically: true, encoding: .utf8)
}
private func contextDictionaryForYearPosts(_ posts: YearPosts) -> [String: Any] {
[
"path": posts.path,
"title": posts.title,
"months": posts.months.sorted(by: >).map { month in
contextDictionaryForMonthPosts(posts[month], year: posts.year)
},
]
}
private func contextDictionaryForMonthPosts(_ posts: MonthPosts, year: Int) -> [String: Any] {
[
"path": posts.path,
"name": posts.month.name,
"abbreviation": posts.month.abbreviation,
"posts": posts.posts.sorted(by: >),
]
}
}
// MARK: - Yearly post index pages
extension PostWriter {
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: >)
for yearPosts in posts.byYear.values {
let yearDir = targetURL.appendingPathComponent(yearPosts.path)
let context: [String: Any] = [
"title": yearPosts.title,
"path": yearPosts.path,
"year": year,
"months": months.map { contextDictionaryForMonthPosts(posts[year][$0], year: year) },
]
let yearHTML = try templateRenderer.renderTemplate(.yearPosts, site: site, context: context)
let yearHTML = try templateRenderer.renderYearPosts(yearPosts, site: site, assets: .none())
let yearURL = yearDir.appendingPathComponent("index.html")
try fileManager.createDirectory(at: yearDir, withIntermediateDirectories: true, attributes: nil)
try yearHTML.write(to: yearURL, atomically: true, encoding: .utf8)
@ -111,14 +76,10 @@ extension PostWriter {
extension PostWriter {
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]
for yearPosts in posts.byYear.values {
for monthPosts in yearPosts.byMonth.values {
let monthDir = targetURL.appendingPathComponent(monthPosts.path)
let monthHTML = try templateRenderer.renderTemplate(.monthPosts, site: site, context: [
"title": "\(month.name) \(year)",
"posts": monthPosts.posts.sorted(by: >),
])
let monthHTML = try templateRenderer.renderMonthPosts(monthPosts, site: site, assets: .none())
let monthURL = monthDir.appendingPathComponent("index.html")
try fileManager.createDirectory(at: monthDir, withIntermediateDirectories: true, attributes: nil)
try monthHTML.write(to: monthURL, atomically: true, encoding: .utf8)

View file

@ -1,6 +1,6 @@
//
// Posts.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-03.
//
@ -19,6 +19,10 @@ struct MonthPosts {
var isEmpty: Bool {
posts.isEmpty
}
var year: Int {
posts[0].date.year
}
}
// MARK: -
@ -62,6 +66,14 @@ struct PostsByYear {
posts.forEach { add(post: $0) }
}
var isEmpty: Bool {
byYear.isEmpty || byYear.values.allSatisfy { $0.isEmpty }
}
var years: [Int] {
Array(byYear.keys)
}
subscript(year: Int) -> YearPosts {
get {
byYear[year, default: YearPosts(year: year, byMonth: [:], path: "\(path)/\(year)")]
@ -71,10 +83,6 @@ struct PostsByYear {
}
}
var isEmpty: Bool {
byYear.isEmpty || byYear.values.allSatisfy { $0.isEmpty }
}
mutating func add(post: Post) {
let (year, month) = (post.date.year, Month(post.date.month))
self[year][month].posts.append(post)

View file

@ -1,13 +1,13 @@
//
// PostsPlugin.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-03.
//
import Foundation
public final class PostsPlugin: Plugin {
final class PostsPlugin: Plugin {
let templateRenderer: PostsTemplateRenderer
let postRepo: PostRepo
let postWriter: PostWriter
@ -30,7 +30,7 @@ public final class PostsPlugin: Plugin {
// MARK: - Plugin methods
public func setUp(site: Site, sourceURL: URL) throws {
func setUp(site: Site, sourceURL: URL) throws {
guard postRepo.postDataExists(at: sourceURL) else {
return
}
@ -38,7 +38,7 @@ public final class PostsPlugin: Plugin {
try postRepo.readPosts(sourceURL: sourceURL)
}
public func render(site: Site, targetURL: URL) throws {
func render(site: Site, targetURL: URL) throws {
guard !postRepo.isEmpty else {
return
}

View file

@ -1,29 +1,29 @@
//
// PostsPluginBuilder.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-15.
//
import Foundation
public final class PostsPluginBuilder {
final class PostsPluginBuilder {
private let templateRenderer: PostsTemplateRenderer
private var path: String?
private var jsonFeed: JSONFeed?
private var rssFeed: RSSFeed?
public init(templateRenderer: PostsTemplateRenderer) {
init(templateRenderer: PostsTemplateRenderer) {
self.templateRenderer = templateRenderer
}
public func path(_ path: String) -> PostsPluginBuilder {
func path(_ path: String) -> PostsPluginBuilder {
precondition(self.path == nil, "path is already defined")
self.path = path
return self
}
public func jsonFeed(
func jsonFeed(
path: String? = nil,
avatarPath: String? = nil,
iconPath: String? = nil,
@ -39,13 +39,13 @@ public final class PostsPluginBuilder {
return self
}
public func rssFeed(path: String? = nil) -> PostsPluginBuilder {
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 {
func build() -> PostsPlugin {
let postRepo: PostRepo
let postWriter: PostWriter
if let outputPath = path {

View file

@ -0,0 +1,26 @@
//
// PostsTemplateRenderer.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-17.
//
import Foundation
protocol PostsTemplateRenderer {
func renderArchive(postsByYear: PostsByYear, site: Site, assets: TemplateAssets) throws -> String
func renderYearPosts(_ yearPosts: YearPosts, site: Site, assets: TemplateAssets) throws -> String
func renderMonthPosts(_ posts: MonthPosts, site: Site, assets: TemplateAssets) throws -> String
func renderPost(_ post: Post, site: Site, assets: TemplateAssets) throws -> String
func renderRecentPosts(_ posts: [Post], site: Site, assets: TemplateAssets) throws -> String
// MARK: - Feeds
func renderFeedPost(_ post: Post, site: Site, assets: TemplateAssets) throws -> String
func renderRSSFeed(posts: [Post], feedURL: URL, site: Site, assets: TemplateAssets) throws -> String
}

View file

@ -0,0 +1,20 @@
//
// Project.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-02.
//
import Foundation
struct Project {
let title: String
let description: String
let url: URL
init(title: String, description: String, url: URL) {
self.title = title
self.description = description
self.url = url
}
}

View file

@ -1,6 +1,6 @@
//
// ProjectsPlugin.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-02.
//
@ -12,7 +12,7 @@ struct PartialProject {
let description: String
}
public final class ProjectsPlugin: Plugin {
final class ProjectsPlugin: Plugin {
let fileManager: FileManager = .default
let outputPath: String
let partialProjects: [PartialProject]
@ -36,7 +36,7 @@ public final class ProjectsPlugin: Plugin {
// MARK: - Plugin methods
public func setUp(site: Site, sourceURL: URL) throws {
func setUp(site: Site, sourceURL: URL) throws {
self.sourceURL = sourceURL
projects = partialProjects.map { partial in
Project(
@ -47,7 +47,7 @@ public final class ProjectsPlugin: Plugin {
}
}
public func render(site: Site, targetURL: URL) throws {
func render(site: Site, targetURL: URL) throws {
guard !projects.isEmpty else {
return
}

View file

@ -1,41 +1,41 @@
//
// ProjectsPluginBuilder.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-19.
//
import Foundation
public final class ProjectsPluginBuilder {
final class ProjectsPluginBuilder {
let templateRenderer: ProjectsTemplateRenderer
private var path: String?
private var projects: [PartialProject] = []
private var projectAssets: TemplateAssets?
public init(templateRenderer: ProjectsTemplateRenderer) {
init(templateRenderer: ProjectsTemplateRenderer) {
self.templateRenderer = templateRenderer
}
public func path(_ path: String) -> ProjectsPluginBuilder {
func path(_ path: String) -> ProjectsPluginBuilder {
precondition(self.path == nil, "path is already defined")
self.path = path
return self
}
public func projectAssets(_ projectAssets: TemplateAssets) -> ProjectsPluginBuilder {
func projectAssets(_ projectAssets: TemplateAssets) -> ProjectsPluginBuilder {
precondition(self.projectAssets == nil, "projectAssets are already defined")
self.projectAssets = projectAssets
return self
}
public func add(_ title: String, description: String) -> ProjectsPluginBuilder {
func add(_ title: String, description: String) -> ProjectsPluginBuilder {
let project = PartialProject(title: title, description: description)
projects.append(project)
return self
}
public func build() -> ProjectsPlugin {
func build() -> ProjectsPlugin {
if projects.isEmpty {
print("WARNING: No projects have been added")
}

View file

@ -1,13 +1,13 @@
//
// ProjectsTemplateRenderer.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-17.
//
import Foundation
public protocol ProjectsTemplateRenderer {
protocol ProjectsTemplateRenderer {
func renderProjects(_ projects: [Project], site: Site, assets: TemplateAssets) throws -> String
func renderProject(_ project: Project, site: Site, assets: TemplateAssets) throws -> String
}

View file

@ -1,13 +1,13 @@
//
// Renderer.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-02.
//
import Foundation
public protocol Renderer {
protocol Renderer {
func canRenderFile(named filename: String, withExtension ext: String) -> Bool
func render(site: Site, fileURL: URL, targetDir: URL) throws

View file

@ -1,24 +1,24 @@
//
// Site.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-01.
//
import Foundation
public struct Site {
public let author: String
public let email: String
public let title: String
public let description: String?
public let url: URL
public let styles: [String]
public let scripts: [String]
public let renderers: [Renderer]
public let plugins: [Plugin]
struct Site {
let author: String
let email: String
let title: String
let description: String?
let url: URL
let styles: [String]
let scripts: [String]
let renderers: [Renderer]
let plugins: [Plugin]
public init(
init(
author: String,
email: String,
title: String,

View file

@ -1,13 +1,13 @@
//
// SiteBuilder.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-15.
//
import Foundation
public final class SiteBuilder {
final class SiteBuilder {
private let title: String
private let description: String?
private let author: String
@ -20,7 +20,7 @@ public final class SiteBuilder {
private var plugins: [Plugin] = []
private var renderers: [Renderer] = []
public init(
init(
title: String,
description: String? = nil,
author: String,
@ -34,27 +34,27 @@ public final class SiteBuilder {
self.url = url
}
public func styles(_ styles: String...) -> SiteBuilder {
func styles(_ styles: String...) -> SiteBuilder {
self.styles.append(contentsOf: styles)
return self
}
public func scripts(_ scripts: String...) -> SiteBuilder {
func scripts(_ scripts: String...) -> SiteBuilder {
self.scripts.append(contentsOf: scripts)
return self
}
public func plugin(_ plugin: Plugin) -> SiteBuilder {
func plugin(_ plugin: Plugin) -> SiteBuilder {
plugins.append(plugin)
return self
}
public func renderer(_ renderer: Renderer) -> SiteBuilder {
func renderer(_ renderer: Renderer) -> SiteBuilder {
renderers.append(renderer)
return self
}
public func build() -> Site {
func build() -> Site {
Site(
author: author,
email: email,
@ -71,7 +71,7 @@ public final class SiteBuilder {
// MARK: - Markdown
public extension SiteBuilder {
extension SiteBuilder {
func renderMarkdown(pageRenderer: MarkdownPageRenderer) -> SiteBuilder {
renderer(MarkdownRenderer(pageRenderer: pageRenderer))
}

View file

@ -1,23 +1,23 @@
//
// SiteGenerator.swift
// SiteGenerator
// samhuri.net.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-01.
//
import Foundation
public final class SiteGenerator {
final class SiteGenerator {
// Dependencies
let fileManager: FileManager = .default
// Site properties
public let site: Site
public let sourceURL: URL
let site: Site
let sourceURL: URL
let ignoredFilenames = [".DS_Store", ".gitkeep"]
public init(sourceURL: URL, site: Site) throws {
init(sourceURL: URL, site: Site) throws {
self.site = site
self.sourceURL = sourceURL
@ -30,7 +30,7 @@ public final class SiteGenerator {
}
}
public func generate(targetURL: URL) throws {
func generate(targetURL: URL) throws {
for plugin in site.plugins {
try plugin.render(site: site, targetURL: targetURL)
}
@ -63,18 +63,24 @@ public final class SiteGenerator {
}
}
func renderOrCopyFile(url fileURL: URL, targetDir: URL) throws {
let filename = fileURL.lastPathComponent
func renderOrCopyFile(url sourceURL: URL, targetDir: URL) throws {
let filename = sourceURL.lastPathComponent
let targetURL = targetDir.appendingPathComponent(filename)
// Clear the way so write operations don't fail later on.
if fileManager.fileExists(atPath: targetURL.path) {
try fileManager.removeItem(at: targetURL)
}
let ext = String(filename.split(separator: ".").last!)
for renderer in site.renderers {
if renderer.canRenderFile(named: filename, withExtension: ext) {
try renderer.render(site: site, fileURL: fileURL, targetDir: targetDir)
try renderer.render(site: site, fileURL: sourceURL, targetDir: targetDir)
return
}
}
// Not handled by any renderer. Copy the file unchanged.
let dest = targetDir.appendingPathComponent(filename)
try fileManager.copyItem(at: fileURL, to: dest)
try fileManager.copyItem(at: sourceURL, to: targetURL)
}
}

View file

@ -1,22 +1,22 @@
//
// TemplateAssets.swift
// SiteGenerator
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-20.
//
import Foundation
public struct TemplateAssets {
public let scripts: [String]
public let styles: [String]
struct TemplateAssets {
let scripts: [String]
let styles: [String]
public init(scripts: [String], styles: [String]) {
init(scripts: [String], styles: [String]) {
self.scripts = scripts
self.styles = styles
}
public static func none() -> TemplateAssets {
static func none() -> TemplateAssets {
TemplateAssets(scripts: [], styles: [])
}
}

View file

@ -9,23 +9,23 @@ import Foundation
import Plot
extension Node where Context == HTML.HeadContext {
static func jsonFeedLink(_ url: URLRepresentable, title: String) -> Node<HTML.HeadContext> {
static func jsonFeedLink(_ url: URLRepresentable, title: String) -> Self {
.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> {
static func appleTouchIcon(_ url: URLRepresentable) -> Self {
.link(.attribute(named: "rel", value: "apple-touch-icon"), .href(url))
}
static func safariPinnedTabIcon(_ url: URLRepresentable, color: String) -> Node<HTML.HeadContext> {
static func safariPinnedTabIcon(_ url: URLRepresentable, color: String) -> Self {
.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> {
static func asyncStylesheetLinks(_ urls: [URLRepresentable]) -> Self {
.script("""
(function() {
var urls = [\(urls.map { "'\($0)'" }.joined(separator: ", "))];
@ -40,3 +40,9 @@ extension Node where Context == HTML.BodyContext {
""")
}
}
extension Node where Context == HTML.BodyContext {
static func time(_ nodes: Node<HTML.BodyContext>...) -> Self {
.element(named: "time", nodes: nodes)
}
}

View file

@ -1,39 +0,0 @@
//
// PageContext.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-02.
//
import Foundation
import SiteGenerator
struct PageContext: TemplateContext {
let site: Site
@available(*, deprecated) let body: String
let page: Page
@available(*, deprecated) let metadata: [String: String]
var title: String {
"\(site.title): \(page.title)"
}
var templateAssets: TemplateAssets {
page.templateAssets
}
}
extension PageContext {
@available(*, deprecated)
var dictionary: [String: Any] {
[
"site": site,
"body": body,
"page": page,
"metadata": metadata,
"styles": site.styles + templateAssets.styles,
"scripts": site.scripts + templateAssets.scripts,
"currentYear": Date().year,
]
}
}

View file

@ -1,6 +1,6 @@
//
// PartialTemplates.swift
//
// PageTemplate.swift
// samhuri.net
//
// Created by Sami Samhuri on 2019-12-19.
//
@ -9,7 +9,7 @@ import Foundation
import Plot
extension Node where Context == HTML.BodyContext {
static func page(title: String, bodyHTML: String) -> Node<HTML.BodyContext> {
static func page(title: String, bodyHTML: String) -> Self {
.group([
.article(.class("container"),
.h1(.text(title)),

View file

@ -6,7 +6,6 @@
//
import Foundation
import SiteGenerator
struct SiteContext: TemplateContext {
let site: Site

View file

@ -6,7 +6,6 @@
//
import Foundation
import SiteGenerator
protocol TemplateContext {
// Concrete requirements, must be implemented

View file

@ -1,5 +1,4 @@
import Foundation
import SiteGenerator
public enum samhuri {}
@ -53,8 +52,7 @@ public extension samhuri {
email: "sami@samhuri.net",
url: siteURLOverride ?? URL(string: "https://samhuri.net")!
)
.styles("normalize.css", "style.css")
.styles("https://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css")
.styles("normalize.css", "style.css", "font-awesome.min.css")
.renderMarkdown(pageRenderer: renderer)
.plugin(projectsPlugin)
.plugin(postsPlugin)
@ -62,8 +60,7 @@ public extension samhuri {
}
public func generate(sourceURL: URL, targetURL: URL) throws {
let templatesURL = sourceURL.appendingPathComponent("templates")
let renderer = PageRenderer(templatesURL: templatesURL)
let renderer = PageRenderer()
let site = buildSite(renderer: renderer)
let generator = try SiteGenerator(sourceURL: sourceURL, site: site)
try generator.generate(targetURL: targetURL)

View file

@ -1,7 +0,0 @@
<div id="article">
<p class="time">{{ post.date }}</p>
{{ post.body }}
{% if post.isLink %}
<p><a class="permalink" href="{{ post.url }}">&infin;</a></p>
{% endif %}
</div>

View file

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://samhuri.net/css/normalize.css" type="text/css"?>
<?xml-stylesheet href="https://samhuri.net/css/style.css" type="text/css"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ site.title }}</title>
<description>{{ site.description }}</description>
<link>{{ site.url }}</link>
<pubDate>{{ posts[0].date }}</pubDate>
<atom:link href="{{ feedURL }}" rel="self" type="application/rss+xml" />
{% for post in posts %}
<item>
<title>{{ post.title }}</title>
<description>{{ post.body }}</description>
<pubDate>{{ post.date }}</pubDate>
<author>{{ post.author }}</author>
<link>{{ post.link }}</link>
<guid>{{ post.guid }}</guid>
</item>
{% endfor %}
</channel>
</rss>

View file

@ -1,11 +0,0 @@
<article class="container">
<header>
<h2><a href="{{ post.link }}">&rarr; {{ post.title }}</a></h2>
<time>{{ post.formattedDate }}</time>
<a class="permalink" href="{{ post.path }}">&infin;</a>
</header>
{{ post.body }}
</article>
<div class="row clearfix">
<p class="fin clearfix"><i class="fa fa-code"></i></p>
</div>

View file

@ -1,11 +0,0 @@
<article class="container">
<header>
<h2><a href="{{ post.path }}">{{ post.title }}</a></h2>
<time>{{ post.formattedDate }}</time>
<a class="permalink" href="{{ post.path }}">&infin;</a>
</header>
{{ post.body }}
</article>
<div class="row clearfix">
<p class="fin"><i class="fa fa-code"></i></p>
</div>

View file

@ -1,5 +0,0 @@
{% if post.isLink %}
{% include "partial-post-link.html" %}
{% else %}
{% include "partial-post-text.html" %}
{% endif %}

View file

@ -1,5 +0,0 @@
{% extends "samhuri.net.html" %}
{% block body %}
{% include "partial-post.html" %}
{% endblock %}

View file

@ -1,38 +0,0 @@
{% extends "samhuri.net.html" %}
{% block body %}
<div class="container">
<h1>{{ title }}</h1>
</div>
{% for year in years %}
<div class="container">
<h2 class="year"><a href="{{ year.path }}">{{ year.title }}</a></h2>
{% for month in year.months %}
<h3 class="month">
<a href="{{ month.path }}">{{ month.name }}</a>
</h3>
<ul class="archive">
{% for post in month.posts %}
<li>
{% if post.isLink %}
<a href="{{ post.link }}">&rarr; {{ post.title }}</a>
{% else %}
<a href="{{ post.path }}">{{ post.title }}</a>
{% endif %}
<time>{{ post.day }} {{ month.abbreviation }}</time>
{% if post.isLink %}
<a class="permalink" href="{{ post.path }}">&infin;</a>
{% endif %}
</li>
{% endfor %}
</ul>
{% endfor %}
</div>
{% endfor %}
{% endblock %}

View file

@ -1,13 +0,0 @@
{% extends "samhuri.net.html" %}
{% block body %}
<div class="container">
<h1>{{ title }}</h1>
</div>
{% for post in posts %}
{% include "partial-post.html" post %}
{% endfor %}
{% endblock %}

View file

@ -1,32 +0,0 @@
{% extends "samhuri.net.html" %}
{% block body %}
<div class="container">
<h1>{{ title }}</h1>
{% for month in months %}
<h2 class="month">
<a href="{{ month.path }}">{{ month.name }}</a>
</h2>
<ul class="archive">
{% for post in month.posts %}
<li>
{% if post.isLink %}
<a href="{{ post.link }}">&rarr; {{ post.title }}</a>
{% else %}
<a href="{{ post.path }}">{{ post.title }}</a>
{% endif %}
<time>{{ post.day }} {{ month.abbreviation }}</time>
{% if post.isLink %}
<a class="permalink" href="{{ post.path }}">&infin;</a>
{% endif %}
</li>
{% endfor %}
</ul>
{% endfor %}
</div>
{% endblock %}

View file

@ -1,11 +0,0 @@
{% extends "samhuri.net.html" %}
{% block body %}
<div class="container">
{% for post in recentPosts %}
{% include "partial-post.html" post %}
{% endfor %}
</div>
{% endblock %}

View file

@ -1,82 +0,0 @@
<!doctype html>
<html lang="en">
<!-- meow -->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
{% if title %}
<title>{{ site.title }}: {{ title }}</title>
{% elif page.title %}
<title>{{ site.title }}: {{ page.title }}</title>
{% else %}
<title>{{ site.title }}</title>
{% endif %}
<link rel="icon" type="image/png" href="/images/favicon-32x32.png">
<link rel="shortcut icon" href="/images/favicon.ico">
<link rel="apple-touch-icon" href="/images/apple-touch-icon.png">
<link rel="mask-icon" href="/images/safari-pinned-tab.svg" color="#aa0000">
<link rel="manifest" href="/images/manifest.json">
<meta name="msapplication-config" content="/images/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<link rel="author" type="text/plain" href="/humans.txt">
<link rel="alternate" type="application/rss+xml" href="{{ site.url }}/feed.xml" title="{{ site.title }}">
<link rel="alternate" type="application/json" href="{{ site.url }}/feed.json" title="{{ site.title }}">
<link rel="dns-prefetch" href="https://use.typekit.net">
<link rel="dns-prefetch" href="https://netdna.bootstrapcdn.com">
<link rel="dns-prefetch" href="https://gist.github.com">
</head>
<body>
<header class="primary">
<div class="title">
<h1><a href="/">{{ site.title }}</a></h1>
<br>
<h4>By <a href="/about">{{ site.author }}</a></h4>
</div>
<nav>
<ul>
<li><a href="/about">About</a></li>
<li><a href="/posts">Archive</a></li>
<li><a href="/projects">Projects</a></li>
<li class="twitter"><a href="https://twitter.com/_sjs"><i class="fa fa-twitter"></i></a></li>
<li class="github"><a href="https://github.com/samsonjs"><i class="fa fa-github"></i></a></li>
<li class="email"><a href="mailto:sami@samhuri.net"><i class="fa fa-envelope"></i></a></li>
<li class="rss"><a href="/feed.xml"><i class="fa fa-rss"></i></a></li>
</ul>
</nav>
<div class="clearfix"></div>
</header>
{% block body %}{{ body }}{% endblock %}
<footer class="container">
&copy; 2006 - {{ currentYear }} <a href="/about">{{ site.author }}</a>
</footer>
{% for style in styles %}
<script type="text/javascript">
(function() {
var css = document.createElement('link');
css.href = '/css/{{ style }}';
css.rel = 'stylesheet';
css.type = 'text/css';
document.getElementsByTagName('head')[0].appendChild(css);
})();
</script>
{% endfor %}
<script src="https://use.typekit.net/tcm1whv.js" crossorigin="anonymous"></script>
<script>try{Typekit.load({ async: true });}catch(e){}</script>
{% for script in scripts %}
<script defer src="{{ script }}"></script>
{% endfor %}
</body>
</html>