diff --git a/Readme.md b/Readme.md index fd079dd..64f3e5d 100644 --- a/Readme.md +++ b/Readme.md @@ -21,13 +21,17 @@ Execution, trying TDD for the first time: - [x] Write a site generator that renders www/index.html from site.json - - [ ] Port _layout.ejs to Swift code + - [x] Add support for site styles - - [ ] Add support for CSS files + - [ ] Add support for page styles - - [ ] Transform LESS into CSS + - [x] Add support for site scripts - - [ ] Add support for JS files + - [ ] Add support for page scripts + + - [x] Add support for CSS files + + - [x] Transform LESS into CSS - [ ] Migrate projects to the new site generator diff --git a/SiteGenerator/Sources/SiteGenerator/Generator.swift b/SiteGenerator/Sources/SiteGenerator/Generator.swift index 9cd189e..2ae4bbf 100644 --- a/SiteGenerator/Sources/SiteGenerator/Generator.swift +++ b/SiteGenerator/Sources/SiteGenerator/Generator.swift @@ -13,8 +13,8 @@ import Stencil public final class Generator { private let fileManager: FileManager = .default - public let site: Site - public let sourceURL: URL + let site: Site + let sourceURL: URL private let lessParser: LessParser private let mdParser: MarkdownParser @@ -63,7 +63,7 @@ public final class Generator { case "md": let htmlURL = targetURL.appendingPathComponent(filename.replacingOccurrences(of: ".md", with: ".html")) - try renderMarkdown(from: fileURL, to: htmlURL, template: "site", context: [:]) + try renderMarkdown(from: fileURL, to: htmlURL) default: // Who knows. Copy the file unchanged. @@ -83,18 +83,52 @@ public final class Generator { func renderMarkdown( from sourceURL: URL, - to targetURL: URL, - template: String, - context: [String: Any] + to targetURL: URL ) throws { let bodyMarkdown = try String(contentsOf: sourceURL, encoding: .utf8) - let bodyResult = mdParser.parse(bodyMarkdown) - let bodyHTML = bodyResult.html.trimmingCharacters(in: .whitespacesAndNewlines) - var context = context - context["site"] = site - context["body"] = bodyHTML - context.merge(bodyResult.metadata, uniquingKeysWith: { _, new in new }) - let siteHTML = try templateRenderer.renderTemplate(name: "\(template).html", context: context) + let bodyHTML = mdParser.html(from: bodyMarkdown).trimmingCharacters(in: .whitespacesAndNewlines) + let metadata = try markdownMetadata(from: sourceURL) + let context = templateContext(url: sourceURL, metadata: metadata, body: bodyHTML) + let siteHTML = try templateRenderer.renderTemplate(name: "\(context.template).html", context: context.dictionary) try siteHTML.write(to: targetURL, atomically: true, encoding: .utf8) } + + func markdownMetadata(from url: URL) throws -> [String: String] { + let md = try String(contentsOf: url, encoding: .utf8) + return mdParser.parse(md).metadata + } + + func templateContext(url: URL, metadata: [String: String], body: String) -> TemplateContext { + let template = metadata["Template"] + let styles = metadata["Styles"]?.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) } + let scripts = metadata["Scripts"]?.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) } + + #warning("FIXME: this is all fucked ... we need to handle index, archive, etc. differently, just make them built in pages and use a templates in templates/archive.html or something like that") + + switch url.lastPathComponent { + /* + case "index.md": + #warning("TODO: fetch recentPosts") + let index = Index(template: template, styles: styles, scripts: scripts, recentPosts: []) + return .index(index) + + case "archive.md": + let archive = Archive(title: "Archive", template: template, styles: styles, scripts: scripts) + return .archive(archive) + case "projects.md": +*/ + + default: + let title = metadata["Title"]! + let page = DefaultPage(title: title, template: template, styles: styles ?? [], scripts: scripts ?? []) + return TemplateContext(site: site, pageType: .page(page)) + } +// case projects(Projects) +// case project(Project) +// let title = (metadata["Title"] as? String) ?? "" +// case post(Post) +// let title = (metadata["Title"] as? String) ?? "" +// case page(DefaultPage) + let title = (metadata["Title"] as? String) ?? "" + } } diff --git a/SiteGenerator/Sources/SiteGenerator/Model/Archive.swift b/SiteGenerator/Sources/SiteGenerator/Model/Archive.swift new file mode 100644 index 0000000..146672a --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Model/Archive.swift @@ -0,0 +1,18 @@ +// +// Archive.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-01. +// + +import Foundation + +struct Archive: Page { + // Page properties + let title: String + let template: String? + let styles: [String] + let scripts: [String] + + // Other properties +} diff --git a/SiteGenerator/Sources/SiteGenerator/Model/DefaultPage.swift b/SiteGenerator/Sources/SiteGenerator/Model/DefaultPage.swift new file mode 100644 index 0000000..db725d3 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Model/DefaultPage.swift @@ -0,0 +1,15 @@ +// +// DefaultPage.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-01. +// + +import Foundation + +struct DefaultPage: Page { + let title: String + let template: String? + let styles: [String] + let scripts: [String] +} diff --git a/SiteGenerator/Sources/SiteGenerator/Model/HumanSite.swift b/SiteGenerator/Sources/SiteGenerator/Model/HumanSite.swift new file mode 100644 index 0000000..ebc1d8a --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Model/HumanSite.swift @@ -0,0 +1,29 @@ +// +// HumanSite.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-01. +// + +import Foundation + +/// This is used to make the JSON simpler to write with optionals. +struct HumanSite: Codable { + let author: String + let title: String + let url: String + let template: String? + let styles: [String]? + let scripts: [String]? +} + +extension Site { + init(humanSite: HumanSite) { + self.author = humanSite.author + self.title = humanSite.title + self.url = humanSite.url + self.template = humanSite.template ?? "site" + self.styles = humanSite.styles ?? [] + self.scripts = humanSite.scripts ?? [] + } +} diff --git a/SiteGenerator/Sources/SiteGenerator/Model/Index.swift b/SiteGenerator/Sources/SiteGenerator/Model/Index.swift new file mode 100644 index 0000000..1f4b5c3 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Model/Index.swift @@ -0,0 +1,23 @@ +// +// Index.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-01. +// + +import Foundation + +struct Index: Page { + // Page properties + let template: String? + let styles: [String] + let scripts: [String] + + var title: String { + assertionFailure("Don't use this. Use Site.title instead.") + return "easter egg" + } + + // Other properties + let recentPosts: [Post] +} diff --git a/SiteGenerator/Sources/SiteGenerator/Model/Page.swift b/SiteGenerator/Sources/SiteGenerator/Model/Page.swift new file mode 100644 index 0000000..8079706 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Model/Page.swift @@ -0,0 +1,15 @@ +// +// Page.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-01. +// + +import Foundation + +protocol Page { + var title: String { get } + var template: String? { get } + var styles: [String] { get } + var scripts: [String] { get } +} diff --git a/SiteGenerator/Sources/SiteGenerator/Model/Post.swift b/SiteGenerator/Sources/SiteGenerator/Model/Post.swift new file mode 100644 index 0000000..ba58c65 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Model/Post.swift @@ -0,0 +1,20 @@ +// +// Post.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-01. +// + +import Foundation + +struct Post: Page { + // Page properties + let title: String + let template: String? + let styles: [String] + let scripts: [String] + + // Other properties + let date: Date + let formattedDate: String +} diff --git a/SiteGenerator/Sources/SiteGenerator/Model/Project.swift b/SiteGenerator/Sources/SiteGenerator/Model/Project.swift new file mode 100644 index 0000000..7b7e9f1 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Model/Project.swift @@ -0,0 +1,18 @@ +// +// Project.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-01. +// + +import Foundation + +struct Project: Page { + // Page properties + let title: String + let template: String? + let styles: [String] + let scripts: [String] + + // Other properties +} diff --git a/SiteGenerator/Sources/SiteGenerator/Model/Projects.swift b/SiteGenerator/Sources/SiteGenerator/Model/Projects.swift new file mode 100644 index 0000000..a4a0a45 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Model/Projects.swift @@ -0,0 +1,19 @@ +// +// Projects.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-01. +// + +import Foundation + +struct Projects: Page { + // Page properties + let title: String + let template: String? + let styles: [String] + let scripts: [String] + + // Other properties + let projects: [Project] +} diff --git a/SiteGenerator/Sources/SiteGenerator/Model/RenderedPage.swift b/SiteGenerator/Sources/SiteGenerator/Model/RenderedPage.swift new file mode 100644 index 0000000..26e9899 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Model/RenderedPage.swift @@ -0,0 +1,13 @@ +// +// RenderedPage.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-01. +// + +import Foundation + +struct RenderedPage { + let page: SomePage + let body: String +} diff --git a/SiteGenerator/Sources/SiteGenerator/Model/Site.swift b/SiteGenerator/Sources/SiteGenerator/Model/Site.swift new file mode 100644 index 0000000..a5240c5 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Model/Site.swift @@ -0,0 +1,25 @@ +// +// Site.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-01. +// + +import Foundation + +struct Site { + let author: String + let title: String + let url: String + let template: String + let styles: [String] + let scripts: [String] +} + +extension Site { + static func decode(from url: URL) throws -> Site { + let json = try Data(contentsOf: url) + let humanSite = try JSONDecoder().decode(HumanSite.self, from: json) + return Site(humanSite: humanSite) + } +} diff --git a/SiteGenerator/Sources/SiteGenerator/Site.swift b/SiteGenerator/Sources/SiteGenerator/Site.swift deleted file mode 100644 index a43e28f..0000000 --- a/SiteGenerator/Sources/SiteGenerator/Site.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Site.swift -// SiteGenerator -// -// Created by Sami Samhuri on 2019-12-01. -// - -import Foundation - -public struct Site: Codable { - public let author: String - public let email: String - public let title: String - public let url: String - public let styles: [String]? -} - -public extension Site { - static func decode(from url: URL) throws -> Site { - let json = try Data(contentsOf: url) - return try JSONDecoder().decode(Site.self, from: json) - } -} diff --git a/SiteGenerator/Sources/SiteGenerator/TemplateContext.swift b/SiteGenerator/Sources/SiteGenerator/TemplateContext.swift new file mode 100644 index 0000000..72a3001 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/TemplateContext.swift @@ -0,0 +1,87 @@ +// +// TemplateContext.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-01. +// + +import Foundation + +enum PageType { + case page(DefaultPage) + + case index(Index) + case archive(Archive) +// case monthPosts(MonthPosts) +// case yearPosts(YearPosts) + case post(Post) + + case projects(Projects) + case project(Project) +} + +struct TemplateContext { + let site: Site + let pageType: PageType + + var page: Page { + switch pageType { + case let .index(page as Page), + let .archive(page as Page), + let .projects(page as Page), + let .project(page as Page), + let .post(page as Page), + let .page(page as Page): + return page + } + } + + var title: String { + if case .index = pageType { + return site.title + } + return "\(site.title): \(page.title)" + } + + var template: String { + page.template ?? site.template + } +} + +// MARK: - Dictionary form + +extension PageType { + var dictionary: [String: Any] { + switch self { + case let .index(index): + return ["index": index] + + case let .archive(archive): + return ["archive": archive] + + case let .projects(projects): + return ["projects": projects] + + case let .project(project): + return ["project": project] + + case let .post(post): + return ["post": post] + + case .page: + return [:] + } + } +} + +extension TemplateContext { + var dictionary: [String: Any] { + [ + "site": site, + "page": page, + "title": title, + "styles": site.styles + page.styles, + "scripts": site.scripts + page.scripts, + ].merging(pageType.dictionary, uniquingKeysWith: { current, _ in current }) + } +} diff --git a/SiteGenerator/Sources/SiteGenerator/main.swift b/SiteGenerator/Sources/SiteGenerator/main.swift index 3f53f72..ed4800c 100644 --- a/SiteGenerator/Sources/SiteGenerator/main.swift +++ b/SiteGenerator/Sources/SiteGenerator/main.swift @@ -17,6 +17,6 @@ func main(sourcePath: String, targetPath: String) throws { let sourcePath = CommandLine.arguments[1] let targetPath = CommandLine.arguments[2] -// TODO: validate args +#warning("TODO: validate args") try! main(sourcePath: sourcePath, targetPath: targetPath) diff --git a/Tests/test-markdown/in/site.json b/Tests/test-markdown/in/site.json index b72b697..4462c23 100644 --- a/Tests/test-markdown/in/site.json +++ b/Tests/test-markdown/in/site.json @@ -1,6 +1,5 @@ { "author": "A man has no name", - "email": "jaqen@hotmail.com", "title": "Valar Morghulis", "url": "http://example.net" } diff --git a/Tests/test-public-subdirs/in/site.json b/Tests/test-public-subdirs/in/site.json index b72b697..4462c23 100644 --- a/Tests/test-public-subdirs/in/site.json +++ b/Tests/test-public-subdirs/in/site.json @@ -1,6 +1,5 @@ { "author": "A man has no name", - "email": "jaqen@hotmail.com", "title": "Valar Morghulis", "url": "http://example.net" } diff --git a/Tests/test-styles/in/site.json b/Tests/test-styles/in/site.json index 88c857d..1a70462 100644 --- a/Tests/test-styles/in/site.json +++ b/Tests/test-styles/in/site.json @@ -1,6 +1,5 @@ { "author": "A man has no name", - "email": "jaqen@hotmail.com", "title": "Valar Morghulis", "url": "http://example.net", "styles": [ diff --git a/Tests/test-styles/in/templates/site.html b/Tests/test-styles/in/templates/site.html index 2fdf52f..8e140a6 100644 --- a/Tests/test-styles/in/templates/site.html +++ b/Tests/test-styles/in/templates/site.html @@ -2,7 +2,7 @@ {{ site.title }} -{% for style in site.styles %} +{% for style in allStyles %} {% endfor %} diff --git a/public/css/style.less b/public/css/style.less index eb3c90f..ff4de40 100644 --- a/public/css/style.less +++ b/public/css/style.less @@ -371,6 +371,8 @@ ul.archive { } body.projects:not(.index) .container { + /* FIXME: find out where this is used and do it differently ... this selector sucks */ + body { background-color: red; } h2, h4 { text-align: center; } diff --git a/site.json b/site.json index ae14b7e..c6ddbcd 100644 --- a/site.json +++ b/site.json @@ -1,6 +1,10 @@ { "title": "samhuri.net", "author": "Sami Samhuri", - "email": "sami@samhuri.net", - "url": "https://samhuri.net" + "url": "https://samhuri.net", + "styles": [ + "/css/normalize.css", + "/css/style.css" + ], + "scripts": [] } diff --git a/templates/samhuri.net.html b/templates/samhuri.net.html index 35029ad..0a956ef 100644 --- a/templates/samhuri.net.html +++ b/templates/samhuri.net.html @@ -5,7 +5,7 @@ - {% block title %}{{ site.title }}{% endblock %} + {% block title %}{{ site.title }}{% endblock %} @@ -15,7 +15,7 @@ -{% for style in styles %} +{% for style in allStyles %} {% endfor %} @@ -45,7 +45,11 @@ +{% if bodyClassNames %} +{% else %} + +{% endif %}

{{ site.title }}

@@ -84,7 +88,7 @@ })(); -{% for script in scripts %} +{% for script in allScripts %} {% endfor %}