mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
Refactor site generator and add plugin to render projects
This commit is contained in:
parent
9f8c1480ef
commit
72bbc433eb
21 changed files with 413 additions and 140 deletions
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
14
SiteGenerator/Sources/SiteGenerator/Date+CurrentYear.swift
Normal file
14
SiteGenerator/Sources/SiteGenerator/Date+CurrentYear.swift
Normal file
|
|
@ -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!
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
105
SiteGenerator/Sources/SiteGenerator/Generator/Generator.swift
Normal file
105
SiteGenerator/Sources/SiteGenerator/Generator/Generator.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
]
|
||||
}
|
||||
}
|
||||
19
SiteGenerator/Sources/SiteGenerator/Generator/Plugin.swift
Normal file
19
SiteGenerator/Sources/SiteGenerator/Generator/Plugin.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
19
SiteGenerator/Sources/SiteGenerator/Generator/Renderer.swift
Normal file
19
SiteGenerator/Sources/SiteGenerator/Generator/Renderer.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
14
SiteGenerator/Sources/SiteGenerator/Projects/Project.swift
Normal file
14
SiteGenerator/Sources/SiteGenerator/Projects/Project.swift
Normal file
|
|
@ -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!
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
15
Tests/test-projects/expected/projects/index.html
Normal file
15
Tests/test-projects/expected/projects/index.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Nvidia, fuck you!</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="http://example.net">Nvidia, fuck you!</a></h1>
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="/projects/linux.html">linux</a></li>
|
||||
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
10
Tests/test-projects/expected/projects/linux.html
Normal file
10
Tests/test-projects/expected/projects/linux.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Nvidia, fuck you!: linux</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>linux</h1>
|
||||
<h3>a (free) operating system (just a hobby, won't be big and professional like gnu)</h4>
|
||||
</body>
|
||||
</html>
|
||||
8
Tests/test-projects/in/projects.json
Normal file
8
Tests/test-projects/in/projects.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"projects": [
|
||||
{
|
||||
"title": "linux",
|
||||
"description": "a (free) operating system (just a hobby, won't be big and professional like gnu)"
|
||||
}
|
||||
]
|
||||
}
|
||||
5
Tests/test-projects/in/site.json
Normal file
5
Tests/test-projects/in/site.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"author": "Linus Torvalds",
|
||||
"title": "Nvidia, fuck you!",
|
||||
"url": "http://example.net"
|
||||
}
|
||||
10
Tests/test-projects/in/templates/project.html
Normal file
10
Tests/test-projects/in/templates/project.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ site.title }}: {{ project.title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ project.title }}</h1>
|
||||
<h3>{{ project.description }}</h4>
|
||||
</body>
|
||||
</html>
|
||||
15
Tests/test-projects/in/templates/projects.html
Normal file
15
Tests/test-projects/in/templates/projects.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ site.title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="{{ site.url }}">{{ site.title }}</a></h1>
|
||||
|
||||
<ul>
|
||||
{% for project in projects %}
|
||||
<li><a href="{{ project.path }}">{{ project.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue