Compare commits

...

6 Commits

Author SHA1 Message Date
Christoph Hagen
3d9551117d Ignore config file 2022-09-08 13:01:55 +02:00
Christoph Hagen
1405fa5ee7 Add installation script for minifiers 2022-09-08 13:01:45 +02:00
Christoph Hagen
abd42e4909 Specify required images in metadata 2022-09-08 13:01:32 +02:00
Christoph Hagen
570cebb5d0 Read generator configuration from file 2022-09-08 09:38:35 +02:00
Christoph Hagen
81b373fb5a Correctly label non-throwing functions
Remove throws from Element constructor
2022-09-08 09:38:35 +02:00
Christoph Hagen
28623d1209 Add paths to configuration 2022-09-05 16:08:06 +02:00
10 changed files with 151 additions and 36 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
config.json

View File

@ -78,6 +78,13 @@ struct Element {
*/ */
let requiredFiles: Set<String> let requiredFiles: Set<String>
/**
Additional images required by the element.
These images are specified as: `source_name destination_name width (height)`.
*/
let images: [ManualImage]
/** /**
The style of thumbnail to use when generating overviews. The style of thumbnail to use when generating overviews.
@ -141,7 +148,7 @@ struct Element {
- Parameter folder: The root folder of the site content. - Parameter folder: The root folder of the site content.
- Note: Uses global objects. - Note: Uses global objects.
*/ */
init?(atRoot folder: URL) throws { init?(atRoot folder: URL) {
self.inputFolder = folder self.inputFolder = folder
self.path = "" self.path = ""
@ -160,6 +167,7 @@ struct Element {
self.sortIndex = log.unused(metadata.sortIndex, "sortIndex", source: source) self.sortIndex = log.unused(metadata.sortIndex, "sortIndex", source: source)
self.externalFiles = metadata.externalFiles ?? [] self.externalFiles = metadata.externalFiles ?? []
self.requiredFiles = metadata.requiredFiles ?? [] // Paths are already relative to root self.requiredFiles = metadata.requiredFiles ?? [] // Paths are already relative to root
self.images = metadata.images?.compactMap { ManualImage(input: $0, path: "") } ?? []
self.thumbnailStyle = log.unused(metadata.thumbnailStyle, "thumbnailStyle", source: source) ?? .large self.thumbnailStyle = log.unused(metadata.thumbnailStyle, "thumbnailStyle", source: source) ?? .large
self.useManualSorting = log.unused(metadata.useManualSorting, "useManualSorting", source: source) ?? true self.useManualSorting = log.unused(metadata.useManualSorting, "useManualSorting", source: source) ?? true
self.overviewItemCount = metadata.overviewItemCount ?? Element.overviewItemCountDefault self.overviewItemCount = metadata.overviewItemCount ?? Element.overviewItemCountDefault
@ -175,10 +183,10 @@ struct Element {
} }
files.add(page: path, id: id) files.add(page: path, id: id)
try self.readElements(in: folder, source: nil) self.readElements(in: folder, source: nil)
} }
mutating func readElements(in folder: URL, source: String?) throws { mutating func readElements(in folder: URL, source: String?) {
let subFolders: [URL] let subFolders: [URL]
do { do {
subFolders = try FileManager.default subFolders = try FileManager.default
@ -188,13 +196,13 @@ struct Element {
log.add(error: "Failed to read subfolders", source: source ?? "root", error: error) log.add(error: "Failed to read subfolders", source: source ?? "root", error: error)
return return
} }
self.elements = try subFolders.compactMap { subFolder in self.elements = subFolders.compactMap { subFolder in
let s = (source.unwrapped { $0 + "/" } ?? "") + subFolder.lastPathComponent let s = (source.unwrapped { $0 + "/" } ?? "") + subFolder.lastPathComponent
return try Element(parent: self, folder: subFolder, path: s) return Element(parent: self, folder: subFolder, path: s)
} }
} }
init?(parent: Element, folder: URL, path: String) throws { init?(parent: Element, folder: URL, path: String) {
self.inputFolder = folder self.inputFolder = folder
self.path = path self.path = path
@ -228,6 +236,7 @@ struct Element {
// TODO: Propagate external files from the parent if subpath matches? // TODO: Propagate external files from the parent if subpath matches?
self.externalFiles = Element.rootPaths(for: metadata.externalFiles, path: path) self.externalFiles = Element.rootPaths(for: metadata.externalFiles, path: path)
self.requiredFiles = Element.rootPaths(for: metadata.requiredFiles, path: path) self.requiredFiles = Element.rootPaths(for: metadata.requiredFiles, path: path)
self.images = metadata.images?.compactMap { ManualImage(input: $0, path: path) } ?? []
self.thumbnailStyle = log.thumbnailStyle(metadata.thumbnailStyle, source: source) self.thumbnailStyle = log.thumbnailStyle(metadata.thumbnailStyle, source: source)
self.useManualSorting = metadata.useManualSorting ?? false self.useManualSorting = metadata.useManualSorting ?? false
self.overviewItemCount = metadata.overviewItemCount ?? parent.overviewItemCount self.overviewItemCount = metadata.overviewItemCount ?? parent.overviewItemCount
@ -252,7 +261,7 @@ struct Element {
// All properties initialized // All properties initialized
files.add(page: path, id: id) files.add(page: path, id: id)
try self.readElements(in: folder, source: path) self.readElements(in: folder, source: path)
} }
} }
@ -365,12 +374,20 @@ extension Element {
} }
static func containedFileRelativeToRoot(filePath: String, folder path: String) -> String? { static func containedFileRelativeToRoot(filePath: String, folder path: String) -> String? {
if path == "" {
return filePath
}
if filePath.hasPrefix("/") || filePath.hasPrefix("http") || filePath.hasPrefix("mailto:") { if filePath.hasPrefix("/") || filePath.hasPrefix("http") || filePath.hasPrefix("mailto:") {
return nil return nil
} }
return "\(path)/\(filePath)" return "\(path)/\(filePath)"
} }
/**
Convert a set of relative paths to paths that are relative to the root element.
- Parameter input: The set of paths to convert.
- Parameter path: The path to the folder where the paths are currently relative to.
*/
static func rootPaths(for input: Set<String>?, path: String) -> Set<String> { static func rootPaths(for input: Set<String>?, path: String) -> Set<String> {
guard let input = input else { guard let input = input else {
return [] return []
@ -536,3 +553,44 @@ extension Element {
elements.forEach { $0.printTree(indentation: indentation + " ") } elements.forEach { $0.printTree(indentation: indentation + " ") }
} }
} }
// MARK: Images
extension Element {
struct ManualImage {
let sourcePath: String
let destinationPath: String
let desiredWidth: Int
let desiredHeight: Int?
init?(input: String, path: String) {
let parts = input.components(separatedBy: " ").filter { !$0.isEmpty }
guard parts.count == 3 || parts.count == 4 else {
log.add(error: "Invalid image specification, expected 'source dest width (height)", source: path)
return nil
}
guard let width = Int(parts[2]) else {
log.add(error: "Invalid width for image \(parts[0])", source: path)
return nil
}
self.sourcePath = Element.relativeToRoot(filePath: parts[0], folder: path)
self.destinationPath = Element.relativeToRoot(filePath: parts[1], folder: path)
self.desiredWidth = width
guard parts.count == 4 else {
self.desiredHeight = nil
return
}
guard let height = Int(parts[3]) else {
log.add(error: "Invalid height for image \(parts[0])", source: path)
return nil
}
self.desiredHeight = height
}
}
}

View File

@ -77,6 +77,13 @@ struct GenericMetadata {
*/ */
let requiredFiles: Set<String>? let requiredFiles: Set<String>?
/**
Additional images required by the element.
These images are specified as: `source_name destination_name width (height)`.
*/
let images: Set<String>?
/** /**
The style of thumbnail to use when generating overviews. The style of thumbnail to use when generating overviews.
@ -128,6 +135,7 @@ extension GenericMetadata: Codable {
.sortIndex, .sortIndex,
.externalFiles, .externalFiles,
.requiredFiles, .requiredFiles,
.images,
.thumbnailStyle, .thumbnailStyle,
.useManualSorting, .useManualSorting,
.overviewItemCount, .overviewItemCount,
@ -198,6 +206,7 @@ extension GenericMetadata {
sortIndex: 1, sortIndex: 1,
externalFiles: [], externalFiles: [],
requiredFiles: [], requiredFiles: [],
images: [],
thumbnailStyle: "", thumbnailStyle: "",
useManualSorting: false, useManualSorting: false,
overviewItemCount: 6, overviewItemCount: 6,

View File

@ -1,8 +1,20 @@
import Foundation import Foundation
struct Configuration { struct Configuration: Codable {
let pageImageWidth: Int let pageImageWidth: Int
let minifyCSSandJS: Bool let minifyCSSandJS: Bool
let contentPath: String
let outputPath: String
var contentDirectory: URL {
.init(fileURLWithPath: contentPath)
}
var outputDirectory: URL {
.init(fileURLWithPath: outputPath)
}
} }

View File

@ -9,17 +9,15 @@ struct SiteGenerator {
self.templates = try TemplateFactory(templateFolder: templatesFolder) self.templates = try TemplateFactory(templateFolder: templatesFolder)
} }
func generate(site: Element) throws { func generate(site: Element) {
site.requiredFiles.forEach(files.require) site.languages.forEach {
site.externalFiles.forEach(files.exclude) generate(site: site, metadata: $0)
try site.languages.forEach {
try generate(site: site, metadata: $0)
} }
} }
private func generate(site: Element, metadata: Element.LocalizedMetadata) throws { private func generate(site: Element, metadata: Element.LocalizedMetadata) {
let language = metadata.language let language = metadata.language
let template = try LocalizedSiteTemplate( let template = LocalizedSiteTemplate(
factory: templates, factory: templates,
language: language, language: language,
site: site) site: site)
@ -33,8 +31,7 @@ struct SiteGenerator {
// Move recursively down to all pages // Move recursively down to all pages
elementsToProcess.append(contentsOf: element.elements) elementsToProcess.append(contentsOf: element.elements)
element.requiredFiles.forEach(files.require) processAllFiles(for: element)
element.externalFiles.forEach(files.exclude)
if !element.elements.isEmpty { if !element.elements.isEmpty {
overviewGenerator.generate(section: element, language: language) overviewGenerator.generate(section: element, language: language)
@ -48,4 +45,16 @@ struct SiteGenerator {
} }
} }
} }
private func processAllFiles(for element: Element) {
element.requiredFiles.forEach(files.require)
element.externalFiles.forEach(files.exclude)
element.images.forEach {
files.requireImage(
source: $0.sourcePath,
destination: $0.destinationPath,
width: $0.desiredWidth,
desiredHeight: $0.desiredHeight)
}
}
} }

View File

@ -35,7 +35,7 @@ struct LocalizedSiteTemplate {
factory.page factory.page
} }
init(factory: TemplateFactory, language: String, site: Element) throws { init(factory: TemplateFactory, language: String, site: Element) {
self.author = site.author self.author = site.author
self.factory = factory self.factory = factory
@ -61,7 +61,7 @@ struct LocalizedSiteTemplate {
url: $0.path + Element.htmlPagePathAddition(for: language)) url: $0.path + Element.htmlPagePathAddition(for: language))
} }
self.topBar = try .init( self.topBar = .init(
factory: factory, factory: factory,
language: language, language: language,
sections: sections, sections: sections,

View File

@ -10,7 +10,7 @@ struct PrefilledTopBarTemplate {
private let factory: TemplateFactory private let factory: TemplateFactory
init(factory: TemplateFactory, language: String, sections: [SectionInfo], topBarWebsiteTitle: String) throws { init(factory: TemplateFactory, language: String, sections: [SectionInfo], topBarWebsiteTitle: String) {
self.factory = factory self.factory = factory
self.language = language self.language = language
self.sections = sections self.sections = sections

View File

@ -1,28 +1,33 @@
import Foundation import Foundation
private let contentDirectory = URL(fileURLWithPath: "/Users/ch/Projects/MakerSpace")
private let outputDirectory = URL(fileURLWithPath: "/Users/ch/Projects/MakerSpace/Site")
let configuration = Configuration( let configUrl = URL(fileURLWithPath: "/Users/ch/Projects/MakerSpace/CHGenerator/config.json")
pageImageWidth: 748,
minifyCSSandJS: true) let configuration: Configuration
do {
let data = try Data(contentsOf: configUrl)
configuration = try JSONDecoder().decode(from: data)
} catch {
print("Failed to read configuration: \(error)")
exit(1)
}
let log = ValidationLog() let log = ValidationLog()
let files = FileSystem(in: contentDirectory, to: outputDirectory) let files = FileSystem(
in: configuration.contentDirectory,
to: configuration.outputDirectory)
private let siteData: Element guard let siteData = Element(atRoot: configuration.contentDirectory) else {
do {
guard let element = try Element(atRoot: contentDirectory) else {
exit(0)
}
siteData = element
} catch {
print(error)
exit(0) exit(0)
} }
private let siteGenerator = try SiteGenerator() do {
try siteGenerator.generate(site: siteData) let siteGenerator = try SiteGenerator()
siteGenerator.generate(site: siteData)
} catch {
print("Failed to generate website: \(error)")
exit(2)
}
files.printGeneratedPages() files.printGeneratedPages()
files.printEmptyPages() files.printEmptyPages()
@ -31,4 +36,5 @@ files.printDraftPages()
files.createImages() files.createImages()
print("Images generated") print("Images generated")
files.copyRequiredFiles() files.copyRequiredFiles()
files.printExternalFiles()
files.writeHashes() files.writeHashes()

6
config_example.json Normal file
View File

@ -0,0 +1,6 @@
{
"pageImageWidth" : 748,
"minifyCSSandJS" : true,
"contentPath" : "/path/to/content/folder",
"outputPath" : "/path/to/output/folder")
}

13
install.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
# Install the required dependencies for CHGenerator
# Note: The following is only required if `minifyCSSandJS=True`
# is set in the generator configuration
# Install the Javascript minifier
# https://github.com/mishoo/UglifyJS
npm install uglify-js -g
# Install the clean-css minifier
# https://github.com/clean-css/clean-css-cli
npm install clean-css-cli -g