mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-04-27 14:57:40 +00:00
Shuffle some more code around and clean things up
This commit is contained in:
parent
f71c9aabbb
commit
9dfd5080ef
19 changed files with 179 additions and 167 deletions
|
|
@ -0,0 +1,16 @@
|
||||||
|
//
|
||||||
|
// FileManager+DirectoryExistence.swift
|
||||||
|
// samhuri.net
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2020-01-01.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension FileManager {
|
||||||
|
func directoryExists(at fileURL: URL) -> Bool {
|
||||||
|
var isDir: ObjCBool = false
|
||||||
|
_ = fileExists(atPath: fileURL.path, isDirectory: &isDir)
|
||||||
|
return isDir.boolValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -49,6 +49,9 @@ extension FilePermissions: RawRepresentable {
|
||||||
|
|
||||||
extension FilePermissions: ExpressibleByStringLiteral {
|
extension FilePermissions: ExpressibleByStringLiteral {
|
||||||
init(stringLiteral value: String) {
|
init(stringLiteral value: String) {
|
||||||
|
guard let _ = FilePermissions(string: value) else {
|
||||||
|
fatalError("Invalid FilePermissions string literal: \(value)")
|
||||||
|
}
|
||||||
self.init(string: value)!
|
self.init(string: value)!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,33 +23,28 @@ struct Permissions: OptionSet {
|
||||||
}
|
}
|
||||||
|
|
||||||
init?(string: String) {
|
init?(string: String) {
|
||||||
|
guard string.count == 3 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
self.init(rawValue: 0)
|
self.init(rawValue: 0)
|
||||||
|
|
||||||
switch string[string.startIndex] {
|
switch string[string.startIndex] {
|
||||||
case "r":
|
case "r": insert(.read)
|
||||||
insert(.read)
|
case "-": break
|
||||||
case "-":
|
default: return nil
|
||||||
break
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch string[string.index(string.startIndex, offsetBy: 1)] {
|
switch string[string.index(string.startIndex, offsetBy: 1)] {
|
||||||
case "w":
|
case "w": insert(.write)
|
||||||
insert(.write)
|
case "-": break
|
||||||
case "-":
|
default: return nil
|
||||||
break
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch string[string.index(string.startIndex, offsetBy: 2)] {
|
switch string[string.index(string.startIndex, offsetBy: 2)] {
|
||||||
case "x":
|
case "x": insert(.execute)
|
||||||
insert(.execute)
|
case "-": break
|
||||||
case "-":
|
default: return nil
|
||||||
break
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +61,9 @@ extension Permissions: CustomStringConvertible {
|
||||||
|
|
||||||
extension Permissions: ExpressibleByStringLiteral {
|
extension Permissions: ExpressibleByStringLiteral {
|
||||||
init(stringLiteral value: String) {
|
init(stringLiteral value: String) {
|
||||||
|
guard let _ = Permissions(string: value) else {
|
||||||
|
fatalError("Invalid Permissions string literal: \(value)")
|
||||||
|
}
|
||||||
self.init(string: value)!
|
self.init(string: value)!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct Month: Equatable {
|
struct Month: Equatable {
|
||||||
static let all = (1 ... 12).map(Month.init(_:))
|
static let all = names.map(Month.init(_:))
|
||||||
|
|
||||||
static let names = [
|
static let names = [
|
||||||
"January", "February", "March", "April",
|
"January", "February", "March", "April",
|
||||||
|
|
@ -18,14 +18,22 @@ struct Month: Equatable {
|
||||||
|
|
||||||
let number: Int
|
let number: Int
|
||||||
|
|
||||||
init(_ number: Int) {
|
init?(_ number: Int) {
|
||||||
precondition((1 ... 12).contains(number), "Month number must be from 1 to 12, got \(number)")
|
guard number < Month.all.count else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
self.number = number
|
self.number = number
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ name: String) {
|
init?(_ name: String) {
|
||||||
precondition(Month.names.contains(name), "Month name is unknown: \(name)")
|
guard let index = Month.names.firstIndex(of: name) else {
|
||||||
self.number = 1 + Month.names.firstIndex(of: name)!
|
return nil
|
||||||
|
}
|
||||||
|
self.number = index + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ date: Date) {
|
||||||
|
self.init(date.month)!
|
||||||
}
|
}
|
||||||
|
|
||||||
var padded: String {
|
var padded: String {
|
||||||
|
|
@ -55,12 +63,18 @@ extension Month: Comparable {
|
||||||
|
|
||||||
extension Month: ExpressibleByIntegerLiteral {
|
extension Month: ExpressibleByIntegerLiteral {
|
||||||
init(integerLiteral value: Int) {
|
init(integerLiteral value: Int) {
|
||||||
self.init(value)
|
guard let _ = Month(value) else {
|
||||||
|
fatalError("Invalid month number in string literal: \(value)")
|
||||||
|
}
|
||||||
|
self.init(value)!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Month: ExpressibleByStringLiteral {
|
extension Month: ExpressibleByStringLiteral {
|
||||||
init(stringLiteral value: String) {
|
init(stringLiteral value: String) {
|
||||||
self.init(value)
|
guard let _ = Month(value) else {
|
||||||
|
fatalError("Invalid month name in string literal: \(value)")
|
||||||
|
}
|
||||||
|
self.init(value)!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import Foundation
|
||||||
|
|
||||||
struct MonthPosts {
|
struct MonthPosts {
|
||||||
let month: Month
|
let month: Month
|
||||||
var posts: [Post]
|
private(set) var posts: [Post]
|
||||||
let path: String
|
let path: String
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
|
|
@ -23,4 +23,8 @@ struct MonthPosts {
|
||||||
var year: Int {
|
var year: Int {
|
||||||
posts[0].date.year
|
posts[0].date.year
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutating func add(post: Post) {
|
||||||
|
posts.append(post)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ struct PostsByYear {
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func add(post: Post) {
|
mutating func add(post: Post) {
|
||||||
let (year, month) = (post.date.year, Month(post.date.month))
|
let (year, month) = (post.date.year, Month(post.date))
|
||||||
self[year][month].posts.append(post)
|
self[year].add(post: post, to: month)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an array of all posts.
|
/// Returns an array of all posts.
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ struct YearPosts {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutating func add(post: Post, to month: Month) {
|
||||||
|
self[month].add(post: post)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns an array of all posts.
|
/// Returns an array of all posts.
|
||||||
func flattened() -> [Post] {
|
func flattened() -> [Post] {
|
||||||
byMonth.values.flatMap { $0.posts }
|
byMonth.values.flatMap { $0.posts }
|
||||||
|
|
|
||||||
48
samhuri.net/Sources/samhuri.net/Posts/PostMetadata.swift
Normal file
48
samhuri.net/Sources/samhuri.net/Posts/PostMetadata.swift
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
//
|
||||||
|
// PostMetadata.swift
|
||||||
|
// samhuri.net
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2020-01-01.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PostMetadata {
|
||||||
|
let title: String
|
||||||
|
let author: String
|
||||||
|
let date: Date
|
||||||
|
let formattedDate: String
|
||||||
|
let link: URL?
|
||||||
|
let tags: [String]
|
||||||
|
let scripts: [String]
|
||||||
|
let styles: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PostMetadata {
|
||||||
|
enum Error: Swift.Error {
|
||||||
|
case deficientMetadata(slug: String, missingKeys: [String], metadata: [String: String])
|
||||||
|
case invalidTimestamp(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(dictionary: [String: String], slug: String) throws {
|
||||||
|
let requiredKeys = ["Title", "Author", "Date", "Timestamp"]
|
||||||
|
let missingKeys = requiredKeys.filter { dictionary[$0] == nil }
|
||||||
|
guard missingKeys.isEmpty else {
|
||||||
|
throw Error.deficientMetadata(slug: slug, missingKeys: missingKeys, metadata: dictionary)
|
||||||
|
}
|
||||||
|
guard let timestamp = dictionary["Timestamp"], let timeInterval = TimeInterval(timestamp) else {
|
||||||
|
throw Error.invalidTimestamp(dictionary["Timestamp"]!)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(
|
||||||
|
title: dictionary["Title"]!,
|
||||||
|
author: dictionary["Author"]!,
|
||||||
|
date: Date(timeIntervalSince1970: timeInterval),
|
||||||
|
formattedDate: dictionary["Date"]!,
|
||||||
|
link: dictionary["Link"].flatMap { URL(string: $0) },
|
||||||
|
tags: dictionary.commaSeparatedList(key: "Tags"),
|
||||||
|
scripts: dictionary.commaSeparatedList(key: "Scripts"),
|
||||||
|
styles: dictionary.commaSeparatedList(key: "Styles")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Ink
|
||||||
|
|
||||||
struct RawPost {
|
struct RawPost {
|
||||||
let slug: String
|
let slug: String
|
||||||
|
|
@ -18,13 +19,13 @@ final class PostRepo {
|
||||||
let feedPostsCount = 30
|
let feedPostsCount = 30
|
||||||
|
|
||||||
let fileManager: FileManager
|
let fileManager: FileManager
|
||||||
let outputPath: String
|
let markdownParser: MarkdownParser
|
||||||
|
|
||||||
private(set) var posts: PostsByYear!
|
private(set) var posts: PostsByYear!
|
||||||
|
|
||||||
init(fileManager: FileManager = .default, outputPath: String = "posts") {
|
init(fileManager: FileManager = .default, markdownParser: MarkdownParser = MarkdownParser()) {
|
||||||
self.fileManager = fileManager
|
self.fileManager = fileManager
|
||||||
self.outputPath = outputPath
|
self.markdownParser = markdownParser
|
||||||
}
|
}
|
||||||
|
|
||||||
var isEmpty: Bool {
|
var isEmpty: Bool {
|
||||||
|
|
@ -32,7 +33,7 @@ final class PostRepo {
|
||||||
}
|
}
|
||||||
|
|
||||||
var sortedPosts: [Post] {
|
var sortedPosts: [Post] {
|
||||||
posts?.flattened().sorted(by: >) ?? []
|
posts.flattened().sorted(by: >)
|
||||||
}
|
}
|
||||||
|
|
||||||
var recentPosts: [Post] {
|
var recentPosts: [Post] {
|
||||||
|
|
@ -48,14 +49,46 @@ final class PostRepo {
|
||||||
return fileManager.fileExists(atPath: postsURL.path)
|
return fileManager.fileExists(atPath: postsURL.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func readPosts(sourceURL: URL) throws {
|
func readPosts(sourceURL: URL, outputPath: String) throws {
|
||||||
let postTransformer = PostTransformer(outputPath: outputPath)
|
|
||||||
let posts = try readRawPosts(sourceURL: sourceURL)
|
let posts = try readRawPosts(sourceURL: sourceURL)
|
||||||
.map(postTransformer.makePost)
|
.map { try makePost(from: $0, outputPath: outputPath) }
|
||||||
self.posts = PostsByYear(posts: posts, path: "/\(outputPath)")
|
self.posts = PostsByYear(posts: posts, path: "/\(outputPath)")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func readRawPosts(sourceURL: URL) throws -> [RawPost] {
|
private extension PostRepo {
|
||||||
|
func makePost(from rawPost: RawPost, outputPath: String) throws -> Post {
|
||||||
|
let result = markdownParser.parse(rawPost.markdown)
|
||||||
|
let metadata = try PostMetadata(dictionary: result.metadata, slug: rawPost.slug)
|
||||||
|
let path = pathForPost(root: outputPath, date: metadata.date, slug: rawPost.slug)
|
||||||
|
return Post(
|
||||||
|
slug: rawPost.slug,
|
||||||
|
title: metadata.title,
|
||||||
|
author: metadata.author,
|
||||||
|
date: metadata.date,
|
||||||
|
formattedDate: metadata.formattedDate,
|
||||||
|
link: metadata.link,
|
||||||
|
tags: metadata.tags,
|
||||||
|
scripts: metadata.scripts,
|
||||||
|
styles: metadata.styles,
|
||||||
|
body: result.html,
|
||||||
|
path: path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathForPost(root: String, date: Date, slug: String) -> String {
|
||||||
|
// format: /{root}/{year}/{month}/{slug}
|
||||||
|
// e.g. /posts/2019/12/first-post
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
root,
|
||||||
|
"\(date.year)",
|
||||||
|
Month(date).padded,
|
||||||
|
slug,
|
||||||
|
].joined(separator: "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRawPosts(sourceURL: URL) throws -> [RawPost] {
|
||||||
let postsURL = sourceURL.appendingPathComponent(postsPath)
|
let postsURL = sourceURL.appendingPathComponent(postsPath)
|
||||||
return try enumerateMarkdownFiles(directory: postsURL)
|
return try enumerateMarkdownFiles(directory: postsURL)
|
||||||
.compactMap { url in
|
.compactMap { url in
|
||||||
|
|
@ -69,22 +102,20 @@ final class PostRepo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func readRawPost(url: URL) throws -> RawPost {
|
func readRawPost(url: URL) throws -> RawPost {
|
||||||
let slug = url.deletingPathExtension().lastPathComponent
|
let slug = url.deletingPathExtension().lastPathComponent
|
||||||
let markdown = try String(contentsOf: url)
|
let markdown = try String(contentsOf: url)
|
||||||
return RawPost(slug: slug, markdown: markdown)
|
return RawPost(slug: slug, markdown: markdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func enumerateMarkdownFiles(directory: URL) throws -> [URL] {
|
func enumerateMarkdownFiles(directory: URL) throws -> [URL] {
|
||||||
return try fileManager.contentsOfDirectory(atPath: directory.path).flatMap { (filename: String) -> [URL] in
|
return try fileManager.contentsOfDirectory(atPath: directory.path).flatMap { (name: String) -> [URL] in
|
||||||
let fileURL = directory.appendingPathComponent(filename)
|
let url = directory.appendingPathComponent(name)
|
||||||
var isDir: ObjCBool = false
|
if fileManager.directoryExists(at: url) {
|
||||||
_ = fileManager.fileExists(atPath: fileURL.path, isDirectory: &isDir)
|
return try enumerateMarkdownFiles(directory: url)
|
||||||
if isDir.boolValue {
|
|
||||||
return try enumerateMarkdownFiles(directory: fileURL)
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return fileURL.pathExtension == "md" ? [fileURL] : []
|
return url.pathExtension == "md" ? [url] : []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
//
|
|
||||||
// PostTransformer.swift
|
|
||||||
// samhuri.net
|
|
||||||
//
|
|
||||||
// Created by Sami Samhuri on 2019-12-09.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Ink
|
|
||||||
|
|
||||||
final class PostTransformer {
|
|
||||||
let markdownParser: MarkdownParser
|
|
||||||
let outputPath: String
|
|
||||||
|
|
||||||
init(markdownParser: MarkdownParser = MarkdownParser(), outputPath: String = "posts") {
|
|
||||||
self.markdownParser = markdownParser
|
|
||||||
self.outputPath = outputPath
|
|
||||||
}
|
|
||||||
|
|
||||||
func makePost(from rawPost: RawPost) throws -> Post {
|
|
||||||
let result = markdownParser.parse(rawPost.markdown)
|
|
||||||
let metadata = try parseMetadata(result.metadata, slug: rawPost.slug)
|
|
||||||
let path = pathForPost(date: metadata.date, slug: rawPost.slug)
|
|
||||||
return Post(
|
|
||||||
slug: rawPost.slug,
|
|
||||||
title: metadata.title,
|
|
||||||
author: metadata.author,
|
|
||||||
date: metadata.date,
|
|
||||||
formattedDate: metadata.formattedDate,
|
|
||||||
link: metadata.link,
|
|
||||||
tags: metadata.tags,
|
|
||||||
scripts: metadata.scripts,
|
|
||||||
styles: metadata.styles,
|
|
||||||
body: result.html,
|
|
||||||
path: path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func pathForPost(date: Date, slug: String) -> String {
|
|
||||||
// format: /posts/2019/12/first-post
|
|
||||||
[
|
|
||||||
"",
|
|
||||||
outputPath,
|
|
||||||
"\(date.year)",
|
|
||||||
Month(date.month).padded,
|
|
||||||
slug,
|
|
||||||
].joined(separator: "/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct ParsedMetadata {
|
|
||||||
let title: String
|
|
||||||
let author: String
|
|
||||||
let date: Date
|
|
||||||
let formattedDate: String
|
|
||||||
let link: URL?
|
|
||||||
let tags: [String]
|
|
||||||
let scripts: [String]
|
|
||||||
let styles: [String]
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension PostTransformer {
|
|
||||||
enum Error: Swift.Error {
|
|
||||||
case deficientMetadata(slug: String, missingKeys: [String], metadata: [String: String])
|
|
||||||
case invalidTimestamp(String)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseMetadata(_ metadata: [String: String], slug: String) throws -> ParsedMetadata {
|
|
||||||
let requiredKeys = ["Title", "Author", "Date", "Timestamp"]
|
|
||||||
let missingKeys = requiredKeys.filter { metadata[$0] == nil }
|
|
||||||
guard missingKeys.isEmpty else {
|
|
||||||
throw Error.deficientMetadata(slug: slug, missingKeys: missingKeys, metadata: metadata)
|
|
||||||
}
|
|
||||||
guard let timeInterval = TimeInterval(metadata["Timestamp"]!) else {
|
|
||||||
throw Error.invalidTimestamp(metadata["Timestamp"]!)
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = metadata["Title"]!
|
|
||||||
let author = metadata["Author"]!
|
|
||||||
let date = Date(timeIntervalSince1970: timeInterval)
|
|
||||||
let formattedDate = metadata["Date"]!
|
|
||||||
let link = metadata["Link"].flatMap { URL(string: $0) }
|
|
||||||
let tags = metadata.commaSeparatedList(key: "Tags")
|
|
||||||
let scripts = metadata.commaSeparatedList(key: "Scripts")
|
|
||||||
let styles = metadata.commaSeparatedList(key: "Styles")
|
|
||||||
|
|
||||||
return ParsedMetadata(
|
|
||||||
title: title,
|
|
||||||
author: author,
|
|
||||||
date: date,
|
|
||||||
formattedDate: formattedDate,
|
|
||||||
link: link,
|
|
||||||
tags: tags,
|
|
||||||
scripts: scripts,
|
|
||||||
styles: styles
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -31,7 +31,7 @@ extension PostWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func filePath(date: Date, slug: String) -> String {
|
private func filePath(date: Date, slug: String) -> String {
|
||||||
"/\(date.year)/\(Month(date.month).padded)/\(slug)/index.html"
|
"/\(date.year)/\(Month(date).padded)/\(slug)/index.html"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,14 +47,11 @@ extension PostsPlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
func build() -> PostsPlugin {
|
func build() -> PostsPlugin {
|
||||||
let postRepo: PostRepo
|
|
||||||
let postWriter: PostWriter
|
let postWriter: PostWriter
|
||||||
if let outputPath = path {
|
if let outputPath = path {
|
||||||
postRepo = PostRepo(outputPath: outputPath)
|
|
||||||
postWriter = PostWriter(outputPath: outputPath)
|
postWriter = PostWriter(outputPath: outputPath)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
postRepo = PostRepo()
|
|
||||||
postWriter = PostWriter()
|
postWriter = PostWriter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,7 +73,7 @@ extension PostsPlugin {
|
||||||
|
|
||||||
return PostsPlugin(
|
return PostsPlugin(
|
||||||
renderer: renderer,
|
renderer: renderer,
|
||||||
postRepo: postRepo,
|
postRepo: PostRepo(),
|
||||||
postWriter: postWriter,
|
postWriter: postWriter,
|
||||||
jsonFeedWriter: jsonFeedWriter,
|
jsonFeedWriter: jsonFeedWriter,
|
||||||
rssFeedWriter: rssFeedWriter
|
rssFeedWriter: rssFeedWriter
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ final class PostsPlugin: Plugin {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try postRepo.readPosts(sourceURL: sourceURL)
|
try postRepo.readPosts(sourceURL: sourceURL, outputPath: postWriter.outputPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(site: Site, targetURL: URL) throws {
|
func render(site: Site, targetURL: URL) throws {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ extension PageRenderer: RSSFeedRendering {
|
||||||
func renderRSSFeed(posts: [Post], feedURL: URL, site: Site) throws -> String {
|
func renderRSSFeed(posts: [Post], feedURL: URL, site: Site) throws -> String {
|
||||||
try RSS(
|
try RSS(
|
||||||
.title(site.title),
|
.title(site.title),
|
||||||
.if(site.description != nil, .description(site.description!)),
|
.description(site.description),
|
||||||
.link(site.url),
|
.link(site.url),
|
||||||
.pubDate(posts[0].date),
|
.pubDate(posts[0].date),
|
||||||
.atomLink(feedURL),
|
.atomLink(feedURL),
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ final class ProjectsPlugin: Plugin {
|
||||||
let projectAssets: TemplateAssets
|
let projectAssets: TemplateAssets
|
||||||
|
|
||||||
var projects: [Project] = []
|
var projects: [Project] = []
|
||||||
var sourceURL: URL!
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
projects: [PartialProject],
|
projects: [PartialProject],
|
||||||
|
|
@ -39,7 +38,6 @@ final class ProjectsPlugin: Plugin {
|
||||||
// MARK: - Plugin methods
|
// MARK: - Plugin methods
|
||||||
|
|
||||||
func setUp(site: Site, sourceURL: URL) throws {
|
func setUp(site: Site, sourceURL: URL) throws {
|
||||||
self.sourceURL = sourceURL
|
|
||||||
projects = partialProjects.map { partial in
|
projects = partialProjects.map { partial in
|
||||||
Project(
|
Project(
|
||||||
title: partial.title,
|
title: partial.title,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import Foundation
|
||||||
import Ink
|
import Ink
|
||||||
|
|
||||||
final class MarkdownRenderer: Renderer {
|
final class MarkdownRenderer: Renderer {
|
||||||
let fileManager: FileManager = .default
|
|
||||||
let fileWriter: FileWriting
|
let fileWriter: FileWriting
|
||||||
let markdownParser = MarkdownParser()
|
let markdownParser = MarkdownParser()
|
||||||
let pageRenderer: PageRendering
|
let pageRenderer: PageRendering
|
||||||
|
|
@ -25,7 +24,7 @@ final class MarkdownRenderer: Renderer {
|
||||||
|
|
||||||
/// Parse Markdown and render it as HTML, running it through a Stencil template.
|
/// Parse Markdown and render it as HTML, running it through a Stencil template.
|
||||||
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 bodyMarkdown = try String(contentsOf: fileURL)
|
||||||
let bodyHTML = markdownParser.html(from: bodyMarkdown).trimmingCharacters(in: .whitespacesAndNewlines)
|
let bodyHTML = markdownParser.html(from: bodyMarkdown).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let metadata = try markdownMetadata(from: fileURL)
|
let metadata = try markdownMetadata(from: fileURL)
|
||||||
let pageHTML = try pageRenderer.renderPage(site: site, bodyHTML: bodyHTML, metadata: metadata)
|
let pageHTML = try pageRenderer.renderPage(site: site, bodyHTML: bodyHTML, metadata: metadata)
|
||||||
|
|
@ -43,7 +42,7 @@ final class MarkdownRenderer: Renderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
func markdownMetadata(from url: URL) throws -> [String: String] {
|
func markdownMetadata(from url: URL) throws -> [String: String] {
|
||||||
let md = try String(contentsOf: url, encoding: .utf8)
|
let md = try String(contentsOf: url)
|
||||||
return markdownParser.parse(md).metadata
|
return markdownParser.parse(md).metadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import Foundation
|
||||||
extension Site {
|
extension Site {
|
||||||
final class Builder {
|
final class Builder {
|
||||||
private let title: String
|
private let title: String
|
||||||
private let description: String?
|
private let description: String
|
||||||
private let author: String
|
private let author: String
|
||||||
private let email: String
|
private let email: String
|
||||||
private let url: URL
|
private let url: URL
|
||||||
|
|
@ -23,7 +23,7 @@ extension Site {
|
||||||
|
|
||||||
init(
|
init(
|
||||||
title: String,
|
title: String,
|
||||||
description: String? = nil,
|
description: String,
|
||||||
author: String,
|
author: String,
|
||||||
email: String,
|
email: String,
|
||||||
url: URL
|
url: URL
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ struct Site {
|
||||||
let author: String
|
let author: String
|
||||||
let email: String
|
let email: String
|
||||||
let title: String
|
let title: String
|
||||||
let description: String?
|
let description: String
|
||||||
let url: URL
|
let url: URL
|
||||||
let styles: [String]
|
let styles: [String]
|
||||||
let scripts: [String]
|
let scripts: [String]
|
||||||
|
|
|
||||||
|
|
@ -41,17 +41,15 @@ final class SiteGenerator {
|
||||||
|
|
||||||
// Recursively copy or render every file in the given path.
|
// Recursively copy or render every file in the given path.
|
||||||
func renderPath(_ path: String, to targetURL: URL) throws {
|
func renderPath(_ path: String, to targetURL: URL) throws {
|
||||||
for filename in try fileManager.contentsOfDirectory(atPath: path) {
|
for name in try fileManager.contentsOfDirectory(atPath: path) {
|
||||||
guard !ignoredFilenames.contains(filename) else {
|
guard !ignoredFilenames.contains(name) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recurse into subdirectories, updating the target directory as well.
|
// Recurse into subdirectories, updating the target directory as well.
|
||||||
let fileURL = URL(fileURLWithPath: path).appendingPathComponent(filename)
|
let url = URL(fileURLWithPath: path).appendingPathComponent(name)
|
||||||
var isDir: ObjCBool = false
|
guard !fileManager.directoryExists(at: url) else {
|
||||||
_ = fileManager.fileExists(atPath: fileURL.path, isDirectory: &isDir)
|
try renderPath(url.path, to: targetURL.appendingPathComponent(name))
|
||||||
guard !isDir.boolValue else {
|
|
||||||
try renderPath(fileURL.path, to: targetURL.appendingPathComponent(filename))
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,7 +57,7 @@ final class SiteGenerator {
|
||||||
try fileManager.createDirectory(at: targetURL, withIntermediateDirectories: true, attributes: nil)
|
try fileManager.createDirectory(at: targetURL, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
|
||||||
// Process the file, transforming it if necessary.
|
// Process the file, transforming it if necessary.
|
||||||
try renderOrCopyFile(url: fileURL, targetDir: targetURL)
|
try renderOrCopyFile(url: url, targetDir: targetURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue