mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-04-27 14:57:40 +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
|
- [ ] 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?
|
- [ ] 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
|
// File.swift
|
||||||
// SiteGenerator
|
//
|
||||||
//
|
//
|
||||||
// Created by Sami Samhuri on 2019-12-01.
|
// Created by Sami Samhuri on 2019-12-02.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct TemplateContext {
|
struct PageContext {
|
||||||
let site: Site
|
let site: Site
|
||||||
|
let body: String
|
||||||
let page: Page
|
let page: Page
|
||||||
let metadata: [String: String]
|
let metadata: [String: String]
|
||||||
let body: String
|
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
guard !page.title.isEmpty else {
|
guard !page.title.isEmpty else {
|
||||||
|
|
@ -20,29 +20,23 @@ struct TemplateContext {
|
||||||
|
|
||||||
return "\(site.title): \(page.title)"
|
return "\(site.title): \(page.title)"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PageContext: TemplateContext {
|
||||||
var template: String {
|
var template: String {
|
||||||
page.template ?? site.template
|
page.template ?? site.template
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentYear: Int {
|
|
||||||
Calendar.current.dateComponents([.year], from: Date()).year!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Dictionary form
|
|
||||||
|
|
||||||
extension TemplateContext {
|
|
||||||
var dictionary: [String: Any] {
|
var dictionary: [String: Any] {
|
||||||
[
|
[
|
||||||
"site": site,
|
"site": site,
|
||||||
"page": page,
|
|
||||||
"metadata": metadata,
|
|
||||||
"title": title,
|
"title": title,
|
||||||
"body": body,
|
"body": body,
|
||||||
|
"page": page,
|
||||||
|
"metadata": metadata,
|
||||||
"styles": site.styles + page.styles,
|
"styles": site.styles + page.styles,
|
||||||
"scripts": site.scripts + page.scripts,
|
"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
|
import Foundation
|
||||||
|
|
||||||
struct Site {
|
public struct Site {
|
||||||
let author: String
|
public let author: String
|
||||||
let title: String
|
public let title: String
|
||||||
let url: String
|
public let url: String
|
||||||
let template: String
|
public let template: String
|
||||||
let styles: [String]
|
public let styles: [String]
|
||||||
let scripts: [String]
|
public let scripts: [String]
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Site {
|
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
|
// SiteGenerator
|
||||||
//
|
//
|
||||||
// Created by Sami Samhuri on 2019-12-01.
|
// Created by Sami Samhuri on 2019-12-02.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Shells out to lessc on the command line.
|
public final class LessRenderer: Renderer {
|
||||||
final class LessParser {
|
public func canRenderFile(named filename: String, withExtension ext: String) -> Bool {
|
||||||
/// Parses Less and returns CSS.
|
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 {
|
func parse(_ less: String) throws -> String {
|
||||||
let tempDir = URL(fileURLWithPath: NSTemporaryDirectory())
|
let tempDir = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||||
defer {
|
defer {
|
||||||
|
|
@ -24,7 +35,8 @@ final class LessParser {
|
||||||
return try String(contentsOf: cssURL, encoding: .utf8)
|
return try String(contentsOf: cssURL, encoding: .utf8)
|
||||||
}
|
}
|
||||||
|
|
||||||
private let lesscPath = URL(fileURLWithPath: #file)
|
let lesscPath = URL(fileURLWithPath: #file)
|
||||||
|
.deletingLastPathComponent()
|
||||||
.deletingLastPathComponent()
|
.deletingLastPathComponent()
|
||||||
.appendingPathComponent("node_modules")
|
.appendingPathComponent("node_modules")
|
||||||
.appendingPathComponent("less")
|
.appendingPathComponent("less")
|
||||||
|
|
@ -33,7 +45,7 @@ final class LessParser {
|
||||||
.path
|
.path
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
private func shell(_ args: String...) -> Int32 {
|
func shell(_ args: String...) -> Int32 {
|
||||||
let task = Process()
|
let task = Process()
|
||||||
task.launchPath = "/usr/bin/env"
|
task.launchPath = "/usr/bin/env"
|
||||||
task.arguments = args
|
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 {
|
func main(sourcePath: String, targetPath: String) throws {
|
||||||
let sourceURL = URL(fileURLWithPath: sourcePath)
|
let sourceURL = URL(fileURLWithPath: sourcePath)
|
||||||
let targetURL = URL(fileURLWithPath: targetPath)
|
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)
|
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