From 72bbc433eb85c62a52e70c1f81429eafb70817a7 Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Tue, 3 Dec 2019 00:38:11 -0800 Subject: [PATCH] Refactor site generator and add plugin to render projects --- Readme.md | 2 +- .../SiteGenerator/Date+CurrentYear.swift | 14 +++ .../Sources/SiteGenerator/Generator.swift | 108 ------------------ .../SiteGenerator/Generator/Generator.swift | 105 +++++++++++++++++ .../PageContext.swift} | 26 ++--- .../SiteGenerator/Generator/Plugin.swift | 19 +++ .../SiteGenerator/Generator/Renderer.swift | 19 +++ .../SiteGenerator/Generator/SiteContext.swift | 30 +++++ .../Generator/TemplateContext.swift | 14 +++ .../Sources/SiteGenerator/Model/Site.swift | 14 +-- .../SiteGenerator/Projects/Project.swift | 14 +++ .../Projects/ProjectsPlugin.swift | 59 ++++++++++ .../LessRenderer.swift} | 26 +++-- .../Renderers/MarkdownRenderer.swift | 34 ++++++ .../Sources/SiteGenerator/main.swift | 6 +- .../expected/projects/index.html | 15 +++ .../expected/projects/linux.html | 10 ++ Tests/test-projects/in/projects.json | 8 ++ Tests/test-projects/in/site.json | 5 + Tests/test-projects/in/templates/project.html | 10 ++ .../test-projects/in/templates/projects.html | 15 +++ 21 files changed, 413 insertions(+), 140 deletions(-) create mode 100644 SiteGenerator/Sources/SiteGenerator/Date+CurrentYear.swift delete mode 100644 SiteGenerator/Sources/SiteGenerator/Generator.swift create mode 100644 SiteGenerator/Sources/SiteGenerator/Generator/Generator.swift rename SiteGenerator/Sources/SiteGenerator/{TemplateContext.swift => Generator/PageContext.swift} (68%) create mode 100644 SiteGenerator/Sources/SiteGenerator/Generator/Plugin.swift create mode 100644 SiteGenerator/Sources/SiteGenerator/Generator/Renderer.swift create mode 100644 SiteGenerator/Sources/SiteGenerator/Generator/SiteContext.swift create mode 100644 SiteGenerator/Sources/SiteGenerator/Generator/TemplateContext.swift create mode 100644 SiteGenerator/Sources/SiteGenerator/Projects/Project.swift create mode 100644 SiteGenerator/Sources/SiteGenerator/Projects/ProjectsPlugin.swift rename SiteGenerator/Sources/SiteGenerator/{LessParser.swift => Renderers/LessRenderer.swift} (57%) create mode 100644 SiteGenerator/Sources/SiteGenerator/Renderers/MarkdownRenderer.swift create mode 100644 Tests/test-projects/expected/projects/index.html create mode 100644 Tests/test-projects/expected/projects/linux.html create mode 100644 Tests/test-projects/in/projects.json create mode 100644 Tests/test-projects/in/site.json create mode 100644 Tests/test-projects/in/templates/project.html create mode 100644 Tests/test-projects/in/templates/projects.html diff --git a/Readme.md b/Readme.md index 7accc63..8e05b05 100644 --- a/Readme.md +++ b/Readme.md @@ -79,7 +79,7 @@ Execution, trying TDD for the first time: - [ ] Generate JSON feed - - [ ] Munge HTML files to make them available without an extension (index.html hack) + - [ ] Munge HTML files to make them available without an extension (index.html hack, do it in the SiteGenerator) - [ ] Inline CSS? diff --git a/SiteGenerator/Sources/SiteGenerator/Date+CurrentYear.swift b/SiteGenerator/Sources/SiteGenerator/Date+CurrentYear.swift new file mode 100644 index 0000000..c64f30c --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Date+CurrentYear.swift @@ -0,0 +1,14 @@ +// +// Date+CurrentYear.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-02. +// + +import Foundation + +extension Date { + static var currentYear: Int { + Calendar.current.dateComponents([.year], from: Date()).year! + } +} diff --git a/SiteGenerator/Sources/SiteGenerator/Generator.swift b/SiteGenerator/Sources/SiteGenerator/Generator.swift deleted file mode 100644 index 27662af..0000000 --- a/SiteGenerator/Sources/SiteGenerator/Generator.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// Generator.swift -// SiteGenerator -// -// Created by Sami Samhuri on 2019-12-01. -// - -import Foundation -import Ink -import PathKit -import Stencil - -public final class Generator { - private let fileManager: FileManager = .default - - let site: Site - let sourceURL: URL - - private let lessParser: LessParser - private let mdParser: MarkdownParser - private let templateRenderer: Environment - - public init(sourceURL: URL) throws { - let siteURL = sourceURL.appendingPathComponent("site.json") - self.site = try Site.decode(from: siteURL) - self.sourceURL = sourceURL - - let templatesURL = sourceURL.appendingPathComponent("templates") - self.templateRenderer = Environment(loader: FileSystemLoader(paths: [Path(templatesURL.path)])) - - self.lessParser = LessParser() - self.mdParser = MarkdownParser() - } - - public func generate(targetURL: URL) throws { - // Iterate through all files in public recursively and render or copy each one - let publicURL = sourceURL.appendingPathComponent("public") - try renderPath(publicURL.path, to: targetURL) - } - - func renderPath(_ path: String, to targetURL: URL) throws { - for filename in try fileManager.contentsOfDirectory(atPath: path) { - - // Recurse into subdirectories, updating the target directory as well. - let fileURL = URL(fileURLWithPath: path).appendingPathComponent(filename) - var isDir: ObjCBool = false - fileManager.fileExists(atPath: fileURL.path, isDirectory: &isDir) - guard !isDir.boolValue else { - try renderPath(fileURL.path, to: targetURL.appendingPathComponent(filename)) - continue - } - - // Make sure this path exists so we can write to it. - try fileManager.createDirectory(at: targetURL, withIntermediateDirectories: true, attributes: nil) - - // Processes the file, transforming it if necessary. - try renderOrCopyFile(url: fileURL, targetDir: targetURL) - } - } - - func renderOrCopyFile(url fileURL: URL, targetDir: URL) throws { - let filename = fileURL.lastPathComponent - guard filename != ".DS_Store", filename != ".gitkeep" else { - print("Ignoring hidden file \(filename)") - return - } - - let ext = filename.split(separator: ".").last! - switch ext { - case "less": - let cssURL = targetDir.appendingPathComponent(filename.replacingOccurrences(of: ".less", with: ".css")) - try renderLess(from: fileURL, to: cssURL) - - case "md": - let htmlURL = targetDir.appendingPathComponent(filename.replacingOccurrences(of: ".md", with: ".html")) - try renderMarkdown(from: fileURL, to: htmlURL) - - default: - // Who knows. Copy the file unchanged. - let dest = targetDir.appendingPathComponent(filename) - try fileManager.copyItem(at: fileURL, to: dest) - } - } - - func renderLess(from sourceURL: URL, to targetURL: URL) throws { - let less = try String(contentsOf: sourceURL, encoding: .utf8) - let css = try lessParser.parse(less) - try css.write(to: targetURL, atomically: true, encoding: .utf8) - } - - func renderMarkdown( - from sourceURL: URL, - to targetURL: URL - ) throws { - let bodyMarkdown = try String(contentsOf: sourceURL, encoding: .utf8) - let bodyHTML = mdParser.html(from: bodyMarkdown).trimmingCharacters(in: .whitespacesAndNewlines) - let metadata = try markdownMetadata(from: sourceURL) - let page = Page(metadata: metadata) - let context = TemplateContext(site: site, page: page, metadata: metadata, body: bodyHTML) - let pageHTML = try templateRenderer.renderTemplate(name: "\(context.template).html", context: context.dictionary) - try pageHTML.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 - } -} diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/Generator.swift b/SiteGenerator/Sources/SiteGenerator/Generator/Generator.swift new file mode 100644 index 0000000..abb3211 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Generator/Generator.swift @@ -0,0 +1,105 @@ +// +// Generator.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-01. +// + +import Foundation +import PathKit +import Stencil + +public final class Generator: PluginDelegate, RendererDelegate { + // Dependencies + let fileManager: FileManager = .default + let templateRenderer: Environment + + // Site properties + let site: Site + let sourceURL: URL + let plugins: [Plugin] + let renderers: [Renderer] + + public init(sourceURL: URL, plugins: [Plugin], renderers: [Renderer]) throws { + let templatesURL = sourceURL.appendingPathComponent("templates") + let templatesPath = Path(templatesURL.path) + let loader = FileSystemLoader(paths: [templatesPath]) + self.templateRenderer = Environment(loader: loader) + + let siteURL = sourceURL.appendingPathComponent("site.json") + self.site = try Site.decode(from: siteURL) + self.sourceURL = sourceURL + self.plugins = plugins + self.renderers = renderers + + for plugin in plugins { + try plugin.setUp(sourceURL: sourceURL) + } + } + + public func generate(targetURL: URL) throws { + for plugin in plugins { + try plugin.render(targetURL: targetURL, delegate: self) + } + + let publicURL = sourceURL.appendingPathComponent("public") + try renderPath(publicURL.path, to: targetURL) + } + + // Recursively copy or render every file in the given path. + func renderPath(_ path: String, to targetURL: URL) throws { + for filename in try fileManager.contentsOfDirectory(atPath: path) { + + // Recurse into subdirectories, updating the target directory as well. + let fileURL = URL(fileURLWithPath: path).appendingPathComponent(filename) + var isDir: ObjCBool = false + fileManager.fileExists(atPath: fileURL.path, isDirectory: &isDir) + guard !isDir.boolValue else { + try renderPath(fileURL.path, to: targetURL.appendingPathComponent(filename)) + continue + } + + // Make sure this path exists so we can write to it. + try fileManager.createDirectory(at: targetURL, withIntermediateDirectories: true, attributes: nil) + + // Processes the file, transforming it if necessary. + try renderOrCopyFile(url: fileURL, targetDir: targetURL) + } + } + + func renderOrCopyFile(url fileURL: URL, targetDir: URL) throws { + let filename = fileURL.lastPathComponent + guard filename != ".DS_Store", filename != ".gitkeep" else { + print("Ignoring hidden file \(filename)") + return + } + + let ext = String(filename.split(separator: ".").last!) + for renderer in renderers { + if renderer.canRenderFile(named: filename, withExtension: ext) { + try renderer.render(fileURL: fileURL, targetDir: targetDir, delegate: self) + return + } + } + + // Not handled by any renderer. Copy the file unchanged. + let dest = targetDir.appendingPathComponent(filename) + try fileManager.copyItem(at: fileURL, to: dest) + } + + // MARK: - PluginDelegate and RendererDelegate + + public func renderPage(bodyHTML: String, metadata: [String: String]) throws -> String { + let page = Page(metadata: metadata) + let context = PageContext(site: site, body: bodyHTML, page: page, metadata: metadata) + let pageHTML = try templateRenderer.renderTemplate(name: "\(context.template).html", context: context.dictionary) + return pageHTML + } + + public func renderTemplate(name: String?, context: [String: Any]) throws -> String { + let siteContext = SiteContext(site: site, template: name) + let contextDict = siteContext.dictionary.merging(context, uniquingKeysWith: { _, new in new }) + print("Rendering \(siteContext.template) with context \(contextDict)") + return try templateRenderer.renderTemplate(name: "\(siteContext.template).html", context: contextDict) + } +} diff --git a/SiteGenerator/Sources/SiteGenerator/TemplateContext.swift b/SiteGenerator/Sources/SiteGenerator/Generator/PageContext.swift similarity index 68% rename from SiteGenerator/Sources/SiteGenerator/TemplateContext.swift rename to SiteGenerator/Sources/SiteGenerator/Generator/PageContext.swift index d3fbe25..efd1ef4 100644 --- a/SiteGenerator/Sources/SiteGenerator/TemplateContext.swift +++ b/SiteGenerator/Sources/SiteGenerator/Generator/PageContext.swift @@ -1,17 +1,17 @@ // -// TemplateContext.swift -// SiteGenerator +// File.swift +// // -// Created by Sami Samhuri on 2019-12-01. +// Created by Sami Samhuri on 2019-12-02. // import Foundation -struct TemplateContext { +struct PageContext { let site: Site + let body: String let page: Page let metadata: [String: String] - let body: String var title: String { guard !page.title.isEmpty else { @@ -20,29 +20,23 @@ struct TemplateContext { return "\(site.title): \(page.title)" } +} +extension PageContext: TemplateContext { var template: String { page.template ?? site.template } - var currentYear: Int { - Calendar.current.dateComponents([.year], from: Date()).year! - } -} - -// MARK: - Dictionary form - -extension TemplateContext { var dictionary: [String: Any] { [ "site": site, - "page": page, - "metadata": metadata, "title": title, "body": body, + "page": page, + "metadata": metadata, "styles": site.styles + page.styles, "scripts": site.scripts + page.scripts, - "currentYear": currentYear, + "currentYear": Date.currentYear, ] } } diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/Plugin.swift b/SiteGenerator/Sources/SiteGenerator/Generator/Plugin.swift new file mode 100644 index 0000000..e0b499b --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Generator/Plugin.swift @@ -0,0 +1,19 @@ +// +// Plugin.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-02. +// + +import Foundation + +public protocol PluginDelegate: AnyObject { + func renderPage(bodyHTML: String, metadata: [String: String]) throws -> String + func renderTemplate(name: String?, context: [String: Any]) throws -> String +} + +public protocol Plugin { + func setUp(sourceURL: URL) throws + + func render(targetURL: URL, delegate: PluginDelegate) throws +} diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/Renderer.swift b/SiteGenerator/Sources/SiteGenerator/Generator/Renderer.swift new file mode 100644 index 0000000..4252857 --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Generator/Renderer.swift @@ -0,0 +1,19 @@ +// +// Renderer.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-02. +// + +import Foundation + +public protocol RendererDelegate: AnyObject { + func renderPage(bodyHTML: String, metadata: [String: String]) throws -> String + func renderTemplate(name: String?, context: [String: Any]) throws -> String +} + +public protocol Renderer { + func canRenderFile(named filename: String, withExtension ext: String) -> Bool + + func render(fileURL: URL, targetDir: URL, delegate: RendererDelegate) throws +} diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/SiteContext.swift b/SiteGenerator/Sources/SiteGenerator/Generator/SiteContext.swift new file mode 100644 index 0000000..a16158a --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Generator/SiteContext.swift @@ -0,0 +1,30 @@ +// +// SiteContext.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-01. +// + +import Foundation + +struct SiteContext { + let site: Site + let template: String + + init(site: Site, template: String? = nil) { + self.site = site + self.template = template ?? site.template + } +} + +extension SiteContext: TemplateContext { + var dictionary: [String: Any] { + [ + "site": site, + "title": site.title, + "styles": site.styles, + "scripts": site.scripts, + "currentYear": Date.currentYear, + ] + } +} diff --git a/SiteGenerator/Sources/SiteGenerator/Generator/TemplateContext.swift b/SiteGenerator/Sources/SiteGenerator/Generator/TemplateContext.swift new file mode 100644 index 0000000..477b9bc --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Generator/TemplateContext.swift @@ -0,0 +1,14 @@ +// +// TemplateContext.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-02. +// + +import Foundation + +protocol TemplateContext { + var template: String { get } + + var dictionary: [String : Any] { get } +} diff --git a/SiteGenerator/Sources/SiteGenerator/Model/Site.swift b/SiteGenerator/Sources/SiteGenerator/Model/Site.swift index a5240c5..3a1cae8 100644 --- a/SiteGenerator/Sources/SiteGenerator/Model/Site.swift +++ b/SiteGenerator/Sources/SiteGenerator/Model/Site.swift @@ -7,13 +7,13 @@ import Foundation -struct Site { - let author: String - let title: String - let url: String - let template: String - let styles: [String] - let scripts: [String] +public struct Site { + public let author: String + public let title: String + public let url: String + public let template: String + public let styles: [String] + public let scripts: [String] } extension Site { diff --git a/SiteGenerator/Sources/SiteGenerator/Projects/Project.swift b/SiteGenerator/Sources/SiteGenerator/Projects/Project.swift new file mode 100644 index 0000000..de366ba --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Projects/Project.swift @@ -0,0 +1,14 @@ +// +// Project.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-02. +// + +import Foundation + +struct Project: Codable { + let title: String + let description: String + var path: String! +} diff --git a/SiteGenerator/Sources/SiteGenerator/Projects/ProjectsPlugin.swift b/SiteGenerator/Sources/SiteGenerator/Projects/ProjectsPlugin.swift new file mode 100644 index 0000000..7eccefb --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Projects/ProjectsPlugin.swift @@ -0,0 +1,59 @@ +// +// ProjectsPlugin.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-02. +// + +import Foundation + +private struct Projects: Codable { + let projects: [Project] + + static func decode(from url: URL) throws -> Projects { + let json = try Data(contentsOf: url) + let projects = try JSONDecoder().decode(Projects.self, from: json) + return projects + } +} + +final class ProjectsPlugin: Plugin { + let fileManager: FileManager = .default + let path: String + + var projects: [Project] = [] + var sourceURL: URL! + + init(path: String = "projects") { + self.path = path + } + + func setUp(sourceURL: URL) throws { + self.sourceURL = sourceURL + let projectsURL = sourceURL.appendingPathComponent("projects.json") + if fileManager.fileExists(atPath: projectsURL.path) { + self.projects = try Projects.decode(from: projectsURL).projects.map { project in + Project(title: project.title, description: project.description, path: "/\(path)/\(project.title).html") + } + } + } + + func render(targetURL: URL, delegate: PluginDelegate) throws { + guard !projects.isEmpty else { + return + } + + let projectsDir = targetURL.appendingPathComponent(path) + try fileManager.createDirectory(at: projectsDir, withIntermediateDirectories: true, attributes: nil) + let projectsURL = projectsDir.appendingPathComponent("index.html") + let projectsHTML = try delegate.renderTemplate(name: "projects", context: ["projects": projects]) + try projectsHTML.write(to: projectsURL, atomically: true, encoding: .utf8) + + for project in projects { + let filename = "\(project.title).html" + let projectURL = projectsDir.appendingPathComponent(filename) + let projectHTML = try delegate.renderTemplate(name: "project", context: ["project": project]) + try projectHTML.write(to: projectURL, atomically: true, encoding: .utf8) + } + } +} diff --git a/SiteGenerator/Sources/SiteGenerator/LessParser.swift b/SiteGenerator/Sources/SiteGenerator/Renderers/LessRenderer.swift similarity index 57% rename from SiteGenerator/Sources/SiteGenerator/LessParser.swift rename to SiteGenerator/Sources/SiteGenerator/Renderers/LessRenderer.swift index b53e9d6..0e41a0c 100644 --- a/SiteGenerator/Sources/SiteGenerator/LessParser.swift +++ b/SiteGenerator/Sources/SiteGenerator/Renderers/LessRenderer.swift @@ -1,15 +1,26 @@ // -// LessParser.swift +// LessRenderer.swift // SiteGenerator // -// Created by Sami Samhuri on 2019-12-01. +// Created by Sami Samhuri on 2019-12-02. // import Foundation -/// Shells out to lessc on the command line. -final class LessParser { - /// Parses Less and returns CSS. +public final class LessRenderer: Renderer { + public func canRenderFile(named filename: String, withExtension ext: String) -> Bool { + ext == "less" + } + + /// Parse Less and render it as CSS. + public func render(fileURL: URL, targetDir: URL, delegate: RendererDelegate) throws { + let filename = fileURL.lastPathComponent + let cssURL = targetDir.appendingPathComponent(filename.replacingOccurrences(of: ".less", with: ".css")) + let less = try String(contentsOf: fileURL, encoding: .utf8) + let css = try parse(less) + try css.write(to: cssURL, atomically: true, encoding: .utf8) + } + func parse(_ less: String) throws -> String { let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) defer { @@ -24,7 +35,8 @@ final class LessParser { return try String(contentsOf: cssURL, encoding: .utf8) } - private let lesscPath = URL(fileURLWithPath: #file) + let lesscPath = URL(fileURLWithPath: #file) + .deletingLastPathComponent() .deletingLastPathComponent() .appendingPathComponent("node_modules") .appendingPathComponent("less") @@ -33,7 +45,7 @@ final class LessParser { .path @discardableResult - private func shell(_ args: String...) -> Int32 { + func shell(_ args: String...) -> Int32 { let task = Process() task.launchPath = "/usr/bin/env" task.arguments = args diff --git a/SiteGenerator/Sources/SiteGenerator/Renderers/MarkdownRenderer.swift b/SiteGenerator/Sources/SiteGenerator/Renderers/MarkdownRenderer.swift new file mode 100644 index 0000000..040359b --- /dev/null +++ b/SiteGenerator/Sources/SiteGenerator/Renderers/MarkdownRenderer.swift @@ -0,0 +1,34 @@ +// +// MarkdownRenderer.swift +// SiteGenerator +// +// Created by Sami Samhuri on 2019-12-02. +// + +import Foundation +import Ink + +public final class MarkdownRenderer: Renderer { + let mdParser = MarkdownParser() + + public 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(fileURL: URL, targetDir: URL, delegate: RendererDelegate) throws { + let mdFilename = fileURL.lastPathComponent + let htmlFilename = mdFilename.replacingOccurrences(of: ".md", with: ".html") + let htmlURL = targetDir.appendingPathComponent(htmlFilename) + let bodyMarkdown = try String(contentsOf: fileURL, encoding: .utf8) + let bodyHTML = mdParser.html(from: bodyMarkdown).trimmingCharacters(in: .whitespacesAndNewlines) + let metadata = try markdownMetadata(from: fileURL) + let pageHTML = try delegate.renderPage(bodyHTML: bodyHTML, metadata: metadata) + try pageHTML.write(to: htmlURL, 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 + } +} diff --git a/SiteGenerator/Sources/SiteGenerator/main.swift b/SiteGenerator/Sources/SiteGenerator/main.swift index ed4800c..fd740f5 100644 --- a/SiteGenerator/Sources/SiteGenerator/main.swift +++ b/SiteGenerator/Sources/SiteGenerator/main.swift @@ -10,7 +10,11 @@ import Foundation func main(sourcePath: String, targetPath: String) throws { let sourceURL = URL(fileURLWithPath: sourcePath) let targetURL = URL(fileURLWithPath: targetPath) - let generator = try Generator(sourceURL: sourceURL) + let generator = try Generator( + sourceURL: sourceURL, + plugins: [ProjectsPlugin()], + renderers: [LessRenderer(), MarkdownRenderer()] + ) try generator.generate(targetURL: targetURL) } diff --git a/Tests/test-projects/expected/projects/index.html b/Tests/test-projects/expected/projects/index.html new file mode 100644 index 0000000..6caec2d --- /dev/null +++ b/Tests/test-projects/expected/projects/index.html @@ -0,0 +1,15 @@ + + + + Nvidia, fuck you! + + +

Nvidia, fuck you!

+ + + + diff --git a/Tests/test-projects/expected/projects/linux.html b/Tests/test-projects/expected/projects/linux.html new file mode 100644 index 0000000..4781c28 --- /dev/null +++ b/Tests/test-projects/expected/projects/linux.html @@ -0,0 +1,10 @@ + + + + Nvidia, fuck you!: linux + + +

linux

+

a (free) operating system (just a hobby, won't be big and professional like gnu)

+ + diff --git a/Tests/test-projects/in/projects.json b/Tests/test-projects/in/projects.json new file mode 100644 index 0000000..d99c72a --- /dev/null +++ b/Tests/test-projects/in/projects.json @@ -0,0 +1,8 @@ +{ + "projects": [ + { + "title": "linux", + "description": "a (free) operating system (just a hobby, won't be big and professional like gnu)" + } + ] +} \ No newline at end of file diff --git a/Tests/test-projects/in/site.json b/Tests/test-projects/in/site.json new file mode 100644 index 0000000..1aca942 --- /dev/null +++ b/Tests/test-projects/in/site.json @@ -0,0 +1,5 @@ +{ + "author": "Linus Torvalds", + "title": "Nvidia, fuck you!", + "url": "http://example.net" +} diff --git a/Tests/test-projects/in/templates/project.html b/Tests/test-projects/in/templates/project.html new file mode 100644 index 0000000..d0adbfd --- /dev/null +++ b/Tests/test-projects/in/templates/project.html @@ -0,0 +1,10 @@ + + + + {{ site.title }}: {{ project.title }} + + +

{{ project.title }}

+

{{ project.description }}

+ + diff --git a/Tests/test-projects/in/templates/projects.html b/Tests/test-projects/in/templates/projects.html new file mode 100644 index 0000000..ee71b12 --- /dev/null +++ b/Tests/test-projects/in/templates/projects.html @@ -0,0 +1,15 @@ + + + + {{ site.title }} + + +

{{ site.title }}

+ + + +