mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
Render all posts pages and RSS feed with Plot and drop Stencil
This commit is contained in:
parent
f5aa5d71b3
commit
4318c0b903
75 changed files with 541 additions and 878 deletions
|
|
@ -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}"
|
||||
|
|
|
|||
22
Readme.md
22
Readme.md
|
|
@ -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`?)
|
||||
|
||||
|
|
|
|||
6
SiteGenerator/.gitignore
vendored
6
SiteGenerator/.gitignore
vendored
|
|
@ -1,6 +0,0 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
/.swiftpm
|
||||
xcuserdata/
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"]),
|
||||
]
|
||||
)
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# SiteGenerator
|
||||
|
||||
A static site generator.
|
||||
|
||||
See https://github.com/samsonjs/samhuri.net for details.
|
||||
|
|
@ -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!
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import XCTest
|
||||
|
||||
import SiteGeneratorTests
|
||||
|
||||
var tests = [XCTestCaseEntry]()
|
||||
tests += SiteGeneratorTests.allTests()
|
||||
XCTMain(tests)
|
||||
|
|
@ -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),
|
||||
]
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import XCTest
|
||||
|
||||
#if !canImport(ObjectiveC)
|
||||
public func allTests() -> [XCTestCaseEntry] {
|
||||
return [
|
||||
testCase(SiteGeneratorTests.allTests),
|
||||
]
|
||||
}
|
||||
#endif
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
```
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import SiteGenerator
|
||||
|
||||
struct Page {
|
||||
let title: String
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
samhuri.net/Sources/samhuri.net/Posts/FeedPostTemplate.swift
Normal file
30
samhuri.net/Sources/samhuri.net/Posts/FeedPostTemplate.swift
Normal 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), "∞"))
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
34
samhuri.net/Sources/samhuri.net/Posts/PostTemplate.swift
Normal file
34
samhuri.net/Sources/samhuri.net/Posts/PostTemplate.swift
Normal 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))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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])
|
||||
}),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import SiteGenerator
|
||||
|
||||
struct ProjectContext: TemplateContext {
|
||||
let site: Site
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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")))
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// JSONFeed.swift
|
||||
// SiteGenerator
|
||||
// samhuri.net
|
||||
//
|
||||
// Created by Sami Samhuri on 2019-12-15.
|
||||
//
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// RSSFeed.swift
|
||||
// SiteGenerator
|
||||
// samhuri.net
|
||||
//
|
||||
// Created by Sami Samhuri on 2019-12-15.
|
||||
//
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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: "&")
|
||||
.replacingOccurrences(of: "<", with: "<")
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// Month.swift
|
||||
// SiteGenerator
|
||||
// samhuri.net
|
||||
//
|
||||
// Created by Sami Samhuri on 2019-12-03.
|
||||
//
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// PostRepo.swift
|
||||
// SiteGenerator
|
||||
// samhuri.net
|
||||
//
|
||||
// Created by Sami Samhuri on 2019-12-09.
|
||||
//
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// PostTransformer.swift
|
||||
// SiteGenerator
|
||||
// samhuri.net
|
||||
//
|
||||
// Created by Sami Samhuri on 2019-12-09.
|
||||
//
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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: [])
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import SiteGenerator
|
||||
|
||||
struct SiteContext: TemplateContext {
|
||||
let site: Site
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import SiteGenerator
|
||||
|
||||
protocol TemplateContext {
|
||||
// Concrete requirements, must be implemented
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }}">∞</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<article class="container">
|
||||
<header>
|
||||
<h2><a href="{{ post.link }}">→ {{ post.title }}</a></h2>
|
||||
<time>{{ post.formattedDate }}</time>
|
||||
<a class="permalink" href="{{ post.path }}">∞</a>
|
||||
</header>
|
||||
{{ post.body }}
|
||||
</article>
|
||||
<div class="row clearfix">
|
||||
<p class="fin clearfix"><i class="fa fa-code"></i></p>
|
||||
</div>
|
||||
|
|
@ -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 }}">∞</a>
|
||||
</header>
|
||||
{{ post.body }}
|
||||
</article>
|
||||
<div class="row clearfix">
|
||||
<p class="fin"><i class="fa fa-code"></i></p>
|
||||
</div>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{% if post.isLink %}
|
||||
{% include "partial-post-link.html" %}
|
||||
{% else %}
|
||||
{% include "partial-post-text.html" %}
|
||||
{% endif %}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{% extends "samhuri.net.html" %}
|
||||
|
||||
{% block body %}
|
||||
{% include "partial-post.html" %}
|
||||
{% endblock %}
|
||||
|
|
@ -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 }}">→ {{ 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 }}">∞</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 }}">→ {{ 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 }}">∞</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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">
|
||||
© 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>
|
||||
Loading…
Reference in a new issue