Full generation, file type cleanup
This commit is contained in:
@@ -2,70 +2,97 @@ import Foundation
|
||||
|
||||
extension Content {
|
||||
|
||||
func generateFeed() -> Bool {
|
||||
#warning("Implement feed generation")
|
||||
return false
|
||||
func generateWebsiteInAllLanguages() {
|
||||
performGenerationIfIdle {
|
||||
self.generatePagesInternal()
|
||||
self.generatePostFeedPagesInternal()
|
||||
self.generateTagPagesInternal()
|
||||
self.generateTagOverviewPagesInternal()
|
||||
|
||||
self.copyRequiredFiles()
|
||||
self.generateRequiredImages()
|
||||
self.status("Generation completed")
|
||||
}
|
||||
}
|
||||
|
||||
func generateAllPages() -> Bool {
|
||||
guard startGenerating() else { return false }
|
||||
defer { endGenerating() }
|
||||
func generatePostFeedPages() {
|
||||
performGenerationIfIdle {
|
||||
self.generatePostFeedPagesInternal()
|
||||
}
|
||||
}
|
||||
|
||||
for page in pages {
|
||||
func check(content: String, of page: Page, for language: ContentLanguage, onComplete: @escaping (PageGenerationResults) -> Void) {
|
||||
performGenerationIfIdle {
|
||||
let results = self.results.makeResults(for: page, in: language)
|
||||
let generator = PageContentParser(content: page.content, language: language, results: results)
|
||||
_ = generator.generatePage(from: content)
|
||||
DispatchQueue.main.async {
|
||||
onComplete(results)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func copyRequiredFiles() {
|
||||
let count = results.requiredFiles.count
|
||||
var completed = 0
|
||||
for file in results.requiredFiles {
|
||||
defer {
|
||||
completed += 1
|
||||
status("Copying required files: \(completed) / \(count)")
|
||||
}
|
||||
guard !file.isExternallyStored else {
|
||||
continue
|
||||
}
|
||||
let path = file.absoluteUrl
|
||||
if !storage.copy(file: file.id, to: path) {
|
||||
results.general.unsavedOutput(path, source: .general)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func generateRequiredImages() {
|
||||
let imageGenerator = ImageGenerator(
|
||||
storage: storage,
|
||||
settings: settings)
|
||||
|
||||
let images = results.imagesToGenerate.sorted()
|
||||
let count = images.count
|
||||
var completed = 0
|
||||
for image in images {
|
||||
defer {
|
||||
completed += 1
|
||||
status("Generating required images: \(completed) / \(count)")
|
||||
}
|
||||
if imageGenerator.generate(job: image) {
|
||||
continue
|
||||
}
|
||||
results.failed(image: image)
|
||||
}
|
||||
|
||||
//let images = Set(self.images.map { $0.id })
|
||||
//imageGenerator.recalculateGeneratedImages(by: images)
|
||||
}
|
||||
|
||||
func generateAllPages() {
|
||||
performGenerationIfIdle {
|
||||
self.generatePagesInternal()
|
||||
}
|
||||
}
|
||||
|
||||
func generatePage(_ page: Page) {
|
||||
performGenerationIfIdle {
|
||||
for language in ContentLanguage.allCases {
|
||||
guard generateInternal(page, in: language) else {
|
||||
return false
|
||||
}
|
||||
self.generateInternal(page, in: language)
|
||||
}
|
||||
self.copyRequiredFiles()
|
||||
self.generateRequiredImages()
|
||||
}
|
||||
|
||||
let failedAssetCopies = results.values
|
||||
.reduce(Set()) { $0.union($1.assets) }
|
||||
.filter { !$0.isExternallyStored }
|
||||
.filter { !storage.copy(file: $0.id, to: $0.assetUrl) }
|
||||
|
||||
let failedFileCopies = results.values
|
||||
.reduce(Set()) { $0.union($1.files) }
|
||||
.filter { !$0.isExternallyStored }
|
||||
.filter { !storage.copy(file: $0.id, to: $0.absoluteUrl) }
|
||||
|
||||
|
||||
guard imageGenerator.runJobs(callback: { _ in }) else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func generatePage(_ page: Page) -> Bool {
|
||||
guard startGenerating() else { return false }
|
||||
defer { endGenerating() }
|
||||
|
||||
for language in ContentLanguage.allCases {
|
||||
guard generateInternal(page, in: language) else {
|
||||
return false
|
||||
}
|
||||
func generatePage(_ page: Page, in language: ContentLanguage) {
|
||||
performGenerationIfIdle {
|
||||
self.generateInternal(page, in: language)
|
||||
}
|
||||
guard imageGenerator.runJobs(callback: { _ in }) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let failedAssetCopies = results.values
|
||||
.reduce(Set()) { $0.union($1.assets) }
|
||||
.filter { !$0.isExternallyStored }
|
||||
.filter { !storage.copy(file: $0.id, to: $0.assetUrl) }
|
||||
|
||||
let failedFileCopies = results.values
|
||||
.reduce(Set()) { $0.union($1.files) }
|
||||
.filter { !$0.isExternallyStored }
|
||||
.filter { !storage.copy(file: $0.id, to: $0.absoluteUrl) }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func generatePage(_ page: Page, in language: ContentLanguage) -> Bool {
|
||||
guard startGenerating() else { return false }
|
||||
defer { endGenerating() }
|
||||
return generateInternal(page, in: language)
|
||||
}
|
||||
|
||||
// MARK: Paths to items
|
||||
@@ -121,60 +148,134 @@ extension Content {
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: Images
|
||||
|
||||
func recalculateGeneratedImages() {
|
||||
let images = Set(self.images.map { $0.id })
|
||||
imageGenerator.recalculateGeneratedImages(by: images)
|
||||
}
|
||||
|
||||
// MARK: Generation
|
||||
|
||||
private func startGenerating() -> Bool {
|
||||
guard !isGeneratingWebsite else {
|
||||
return false
|
||||
}
|
||||
// TODO: Fix bug where multiple generating operations can be started
|
||||
// due to dispatch of locking property on main queue
|
||||
self.set(isGenerating: true)
|
||||
return true
|
||||
}
|
||||
|
||||
private func endGenerating() {
|
||||
set(isGenerating: false)
|
||||
}
|
||||
|
||||
private func generateInternal(_ page: Page, in language: ContentLanguage) -> Bool {
|
||||
let pageGenerator = PageGenerator(
|
||||
content: self,
|
||||
imageGenerator: imageGenerator)
|
||||
|
||||
guard let (content, results) = pageGenerator.generate(page: page, language: language) else {
|
||||
print("Failed to generate page \(page.id) in language \(language)")
|
||||
return false
|
||||
}
|
||||
|
||||
private func performGenerationIfIdle(_ operation: @escaping () -> ()) {
|
||||
DispatchQueue.main.async {
|
||||
let id = ItemId(itemId: page.id, language: language, itemType: .page)
|
||||
self.results[id] = results
|
||||
guard !self.isGeneratingWebsite else {
|
||||
return
|
||||
}
|
||||
self.set(isGenerating: true)
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
operation()
|
||||
DispatchQueue.main.async {
|
||||
self.set(isGenerating: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func status(_ message: String) {
|
||||
DispatchQueue.main.async {
|
||||
self.generationStatus = message
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- Note: Run on background thread
|
||||
*/
|
||||
private func generatePagesInternal() {
|
||||
let count = pages.count
|
||||
for index in pages.indices {
|
||||
let page = pages[index]
|
||||
status("Generating pages: \(index) / \(count)")
|
||||
guard !page.isExternalUrl else {
|
||||
continue
|
||||
}
|
||||
for language in ContentLanguage.allCases {
|
||||
guard page.hasContent(in: language) else {
|
||||
continue
|
||||
}
|
||||
generateInternal(page, in: language)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- Note: Run on background thread
|
||||
*/
|
||||
private func generatePostFeedPagesInternal() {
|
||||
status("Generating post feed")
|
||||
for language in ContentLanguage.allCases {
|
||||
let results = results.makeResults(for: .feed, in: language)
|
||||
let generator = PostListPageGenerator(
|
||||
language: language,
|
||||
content: self,
|
||||
results: results,
|
||||
showTitle: false,
|
||||
pageTitle: settings.localized(in: language).title,
|
||||
pageDescription: settings.localized(in: language).description,
|
||||
pageUrlPrefix: settings.localized(in: language).feedUrlPrefix)
|
||||
generator.createPages(for: posts)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
- Note: Run on background thread
|
||||
*/
|
||||
private func generateTagPagesInternal() {
|
||||
let count = tags.count
|
||||
for index in tags.indices {
|
||||
let tag = tags[index]
|
||||
status("Generating tag pages: \(index) / \(count)")
|
||||
generatePagesInternal(for: tag)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- Note: Run on background thread
|
||||
*/
|
||||
private func generatePagesInternal(for tag: Tag) {
|
||||
for language in ContentLanguage.allCases {
|
||||
let results = results.makeResults(for: tag, in: language)
|
||||
|
||||
let posts = posts.filter { $0.tags.contains(tag) }
|
||||
guard posts.count > 0 else { continue }
|
||||
|
||||
let localized = tag.localized(in: language)
|
||||
let urlPrefix = absoluteUrlPrefixForTag(tag, language: language)
|
||||
let generator = PostListPageGenerator(
|
||||
language: language,
|
||||
content: self,
|
||||
results: results,
|
||||
showTitle: true,
|
||||
pageTitle: localized.name,
|
||||
pageDescription: localized.description ?? "",
|
||||
pageUrlPrefix: urlPrefix)
|
||||
generator.createPages(for: posts)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- Note: Run on background thread
|
||||
*/
|
||||
private func generateTagOverviewPagesInternal() {
|
||||
status("Generating tag overview page")
|
||||
for language in ContentLanguage.allCases {
|
||||
let results = results.makeResults(for: .tagOverview, in: language)
|
||||
#warning("Create layout for tag overview page")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- Note: Run on background thread
|
||||
*/
|
||||
private func generateInternal(_ page: Page, in language: ContentLanguage) {
|
||||
let results = results.makeResults(for: page, in: language)
|
||||
let pageGenerator = PageGenerator(content: self)
|
||||
|
||||
results.require(files: page.requiredFiles)
|
||||
|
||||
guard let content = pageGenerator.generate(page: page, language: language, results: results) else {
|
||||
print("Failed to generate page \(page.id) in language \(language)")
|
||||
return
|
||||
}
|
||||
|
||||
let path = page.absoluteUrl(in: language) + ".html"
|
||||
guard storage.write(content, to: path) else {
|
||||
print("Failed to save page \(page.id)")
|
||||
return false
|
||||
return
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
prefix operator ~>
|
||||
|
||||
prefix func ~> (operation: () throws -> Void) -> Bool {
|
||||
do {
|
||||
try operation()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@@ -32,9 +32,6 @@ extension Content {
|
||||
title: page.title,
|
||||
lastModified: page.lastModifiedDate,
|
||||
originalUrl: page.originalURL,
|
||||
files: Set(page.files),
|
||||
externalFiles: Set(page.externalFiles),
|
||||
requiredFiles: Set(page.requiredFiles),
|
||||
linkPreviewImage: page.linkPreviewImage.map { images[$0] },
|
||||
linkPreviewTitle: page.linkPreviewTitle,
|
||||
linkPreviewDescription: page.linkPreviewDescription)
|
||||
@@ -115,14 +112,15 @@ extension Content {
|
||||
english: convert(data.value.english, images: images))
|
||||
}
|
||||
|
||||
let pages: [String : Page] = loadPages(pagesData, tags: tags, images: images)
|
||||
let pages: [String : Page] = loadPages(pagesData, tags: tags, files: files)
|
||||
|
||||
let posts = postsData.map { postId, post in
|
||||
let posts: [String : Post] = postsData.reduce(into: [:]) { dict, data in
|
||||
let (postId, post) = data
|
||||
let linkedPage = post.linkedPageId.map { pages[$0] }
|
||||
let german = convert(post.german, images: images)
|
||||
let english = convert(post.english, images: images)
|
||||
|
||||
return Post(
|
||||
dict[postId] = Post(
|
||||
content: self,
|
||||
id: postId,
|
||||
isDraft: post.isDraft,
|
||||
@@ -145,25 +143,36 @@ extension Content {
|
||||
self.tags = tags.values.sorted()
|
||||
self.pages = pages.values.sorted(ascending: false) { $0.startDate }
|
||||
self.files = files.values.sorted { $0.id }
|
||||
self.posts = posts.sorted(ascending: false) { $0.startDate }
|
||||
self.posts = posts.values.sorted(ascending: false) { $0.startDate }
|
||||
self.tagOverview = tagOverview
|
||||
self.settings = makeSettings(settings, tags: tags, pages: pages, files: files)
|
||||
self.settings = makeSettings(settings, tags: tags, pages: pages, files: files, posts: posts)
|
||||
print("Content loaded")
|
||||
}
|
||||
|
||||
private func makeSettings(_ settings: SettingsFile, tags: [String : Tag], pages: [String : Page], files: [String : FileResource]) -> Settings {
|
||||
private func makeSettings(_ settings: SettingsFile,
|
||||
tags: [String : Tag],
|
||||
pages: [String : Page],
|
||||
files: [String : FileResource],
|
||||
posts: [String : Post]) -> Settings {
|
||||
|
||||
#warning("Notify about missing links")
|
||||
let navigationItems: [Item] = settings.navigationItems.compactMap {
|
||||
switch $0.type {
|
||||
case .tag:
|
||||
return tags[$0.id]
|
||||
case .page:
|
||||
return pages[$0.id]
|
||||
let navigationItems: [Item] = settings.navigationItems.compactMap { raw in
|
||||
guard let type = ItemType(rawValue: raw, posts: posts, pages: pages, tags: tags) else {
|
||||
return nil
|
||||
}
|
||||
switch type {
|
||||
case .general:
|
||||
return nil
|
||||
case .post(let post):
|
||||
return post
|
||||
case .feed:
|
||||
return nil // TODO: Provide feed object
|
||||
case .page(let page):
|
||||
return page
|
||||
case .tagPage(let tag):
|
||||
return tag
|
||||
case .tagOverview:
|
||||
return tagOverview
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +191,7 @@ extension Content {
|
||||
english: .init(file: settings.english))
|
||||
}
|
||||
|
||||
private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], images: [String : FileResource]) -> [String : Page] {
|
||||
private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], files: [String : FileResource]) -> [String : Page] {
|
||||
pagesData.reduce(into: [:]) { pages, data in
|
||||
let (pageId, page) = data
|
||||
pages[pageId] = Page(
|
||||
@@ -193,9 +202,10 @@ extension Content {
|
||||
createdDate: page.createdDate,
|
||||
startDate: page.startDate,
|
||||
endDate: page.endDate,
|
||||
german: convert(page.german, images: images),
|
||||
english: convert(page.english, images: images),
|
||||
tags: page.tags.map { tags[$0]! })
|
||||
german: convert(page.german, images: files),
|
||||
english: convert(page.english, images: files),
|
||||
tags: page.tags.map { tags[$0]! },
|
||||
requiredFiles: page.requiredFiles?.map { files[$0]! } ?? [])
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -63,7 +63,8 @@ private extension Page {
|
||||
startDate: startDate,
|
||||
endDate: hasEndDate ? endDate : nil,
|
||||
german: german.pageFile,
|
||||
english: english.pageFile)
|
||||
english: english.pageFile,
|
||||
requiredFiles: requiredFiles.nonEmpty?.map { $0.id }.sorted())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,9 +72,6 @@ private extension LocalizedPage {
|
||||
|
||||
var pageFile: LocalizedPageFile {
|
||||
.init(url: urlString,
|
||||
files: files.sorted(),
|
||||
externalFiles: externalFiles.sorted(),
|
||||
requiredFiles: requiredFiles.sorted(),
|
||||
title: title,
|
||||
linkPreviewImage: linkPreviewImage?.id,
|
||||
linkPreviewTitle: linkPreviewTitle,
|
||||
@@ -140,7 +138,7 @@ extension Settings {
|
||||
var file: SettingsFile {
|
||||
.init(
|
||||
paths: paths.file,
|
||||
navigationItems: navigationItems.map { .init(type: $0.itemType, id: $0.id) },
|
||||
navigationItems: navigationItems.map { $0.itemType.id },
|
||||
posts: posts.file,
|
||||
pages: pages.file,
|
||||
german: german.file,
|
||||
|
@@ -26,13 +26,14 @@ final class Content: ObservableObject {
|
||||
var tagOverview: TagOverviewPage?
|
||||
|
||||
@Published
|
||||
var results: [ItemId : PageGenerationResults]
|
||||
var results: GenerationResults
|
||||
|
||||
@Published
|
||||
var generationStatus: String = "Ready to generate"
|
||||
|
||||
@Published
|
||||
private(set) var isGeneratingWebsite = false
|
||||
|
||||
let imageGenerator: ImageGenerator
|
||||
|
||||
init(settings: Settings,
|
||||
posts: [Post],
|
||||
pages: [Page],
|
||||
@@ -45,13 +46,10 @@ final class Content: ObservableObject {
|
||||
self.tags = tags
|
||||
self.files = files
|
||||
self.tagOverview = tagOverview
|
||||
self.results = [:]
|
||||
self.results = .init()
|
||||
|
||||
let storage = Storage()
|
||||
self.storage = storage
|
||||
self.imageGenerator = ImageGenerator(
|
||||
storage: storage,
|
||||
settings: settings)
|
||||
}
|
||||
|
||||
init() {
|
||||
@@ -62,13 +60,10 @@ final class Content: ObservableObject {
|
||||
self.tags = []
|
||||
self.files = []
|
||||
self.tagOverview = nil
|
||||
self.results = [:]
|
||||
self.results = .init()
|
||||
|
||||
let storage = Storage()
|
||||
self.storage = storage
|
||||
self.imageGenerator = ImageGenerator(
|
||||
storage: storage,
|
||||
settings: settings)
|
||||
}
|
||||
|
||||
private func clear() {
|
||||
@@ -78,7 +73,7 @@ final class Content: ObservableObject {
|
||||
self.tags = []
|
||||
self.files = []
|
||||
self.tagOverview = nil
|
||||
self.results = [:]
|
||||
self.results = .init()
|
||||
}
|
||||
|
||||
var images: [FileResource] {
|
||||
@@ -86,9 +81,7 @@ final class Content: ObservableObject {
|
||||
}
|
||||
|
||||
func set(isGenerating: Bool) {
|
||||
DispatchQueue.main.async {
|
||||
self.isGeneratingWebsite = isGenerating
|
||||
}
|
||||
self.isGeneratingWebsite = isGenerating
|
||||
}
|
||||
|
||||
func add(_ file: FileResource) {
|
||||
|
@@ -28,8 +28,8 @@ final class FileResource: Item {
|
||||
/**
|
||||
Only for bundle images
|
||||
*/
|
||||
init(resourceImage: String, type: ImageFileType) {
|
||||
self.type = .image(type)
|
||||
init(resourceImage: String, type: FileType) {
|
||||
self.type = type
|
||||
self.english = "A test image included in the bundle"
|
||||
self.german = "Ein Testbild aus dem Bundle"
|
||||
self.isExternallyStored = true
|
||||
@@ -87,18 +87,20 @@ final class FileResource: Item {
|
||||
return makeCleanAbsolutePath(path)
|
||||
}
|
||||
|
||||
var assetUrl: String {
|
||||
let path = content.settings.paths.assetsOutputFolderPath + "/" + id
|
||||
return makeCleanAbsolutePath(path)
|
||||
}
|
||||
|
||||
|
||||
private var pathPrefix: String {
|
||||
switch type {
|
||||
case .image: return content.settings.paths.imagesOutputFolderPath
|
||||
case .video: return content.settings.paths.videosOutputFolderPath
|
||||
default: return content.settings.paths.filesOutputFolderPath
|
||||
if type.isImage {
|
||||
return content.settings.paths.imagesOutputFolderPath
|
||||
}
|
||||
if type.isVideo {
|
||||
return content.settings.paths.videosOutputFolderPath
|
||||
}
|
||||
if type.isAudio {
|
||||
|
||||
}
|
||||
if type.isAsset {
|
||||
return content.settings.paths.assetsOutputFolderPath
|
||||
}
|
||||
return content.settings.paths.filesOutputFolderPath
|
||||
}
|
||||
|
||||
// MARK: File
|
||||
|
240
CHDataManagement/Model/FileType.swift
Normal file
240
CHDataManagement/Model/FileType.swift
Normal file
@@ -0,0 +1,240 @@
|
||||
import Foundation
|
||||
|
||||
enum FileTypeCategory: String, CaseIterable {
|
||||
case image
|
||||
case code
|
||||
case model
|
||||
case text
|
||||
case video
|
||||
case resource
|
||||
case asset
|
||||
case audio
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case .image: return "Images"
|
||||
case .code: return "Code"
|
||||
case .model: return "Models"
|
||||
case .text: return "Text"
|
||||
case .video: return "Videos"
|
||||
case .asset: return "Assets"
|
||||
case .resource: return "Other"
|
||||
case .audio: return "Audio"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FileTypeCategory: Hashable {
|
||||
|
||||
}
|
||||
|
||||
extension FileTypeCategory: Identifiable {
|
||||
|
||||
var id: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
|
||||
enum FileType: String {
|
||||
|
||||
// MARK: Images
|
||||
|
||||
case jpg
|
||||
|
||||
case png
|
||||
|
||||
case avif
|
||||
|
||||
case webp
|
||||
|
||||
case gif
|
||||
|
||||
case svg
|
||||
|
||||
case tiff
|
||||
|
||||
// MARK: Code
|
||||
|
||||
case html
|
||||
|
||||
case cpp
|
||||
|
||||
case swift
|
||||
|
||||
// MARK: Assets
|
||||
|
||||
case css
|
||||
|
||||
case js
|
||||
|
||||
// MARK: Text
|
||||
|
||||
case json
|
||||
|
||||
case conf
|
||||
|
||||
case yaml
|
||||
|
||||
// MARK: Model
|
||||
|
||||
case stl
|
||||
|
||||
case f3d
|
||||
|
||||
case step
|
||||
|
||||
case glb
|
||||
|
||||
case f3z
|
||||
|
||||
// MARK: Video
|
||||
|
||||
case mp4
|
||||
|
||||
case m4v
|
||||
|
||||
case webm
|
||||
|
||||
// MARK: Audio
|
||||
|
||||
case mp3
|
||||
|
||||
case aac
|
||||
|
||||
// MARK: Other
|
||||
|
||||
case noExtension
|
||||
|
||||
case zip
|
||||
|
||||
case cddx
|
||||
|
||||
case pdf
|
||||
|
||||
case key
|
||||
|
||||
case psd
|
||||
|
||||
// MARK: Unknown
|
||||
|
||||
case unknown
|
||||
|
||||
init(fileExtension: String?) {
|
||||
guard let lower = fileExtension?.lowercased() else {
|
||||
self = .noExtension
|
||||
return
|
||||
}
|
||||
if lower == "jpeg" {
|
||||
self = .jpg
|
||||
return
|
||||
}
|
||||
guard let type = FileType(rawValue: lower) else {
|
||||
self = .unknown
|
||||
return
|
||||
}
|
||||
self = type
|
||||
}
|
||||
|
||||
var category: FileTypeCategory {
|
||||
switch self {
|
||||
case .jpg, .png, .avif, .webp, .gif, .svg, .tiff:
|
||||
return .image
|
||||
case .mp4, .m4v, .webm:
|
||||
return .video
|
||||
case .mp3, .aac:
|
||||
return .audio
|
||||
case .js, .css:
|
||||
return .asset
|
||||
case .json, .conf, .yaml:
|
||||
return .text
|
||||
case .html, .cpp, .swift:
|
||||
return .code
|
||||
case .stl, .f3d, .step, .glb, .f3z:
|
||||
return .model
|
||||
case .zip, .cddx, .pdf, .key, .psd:
|
||||
return .resource
|
||||
case .noExtension, .unknown:
|
||||
return .resource
|
||||
}
|
||||
}
|
||||
|
||||
var fileExtension: String {
|
||||
switch self {
|
||||
case .noExtension, .unknown: return ""
|
||||
default:
|
||||
return rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var isImage: Bool {
|
||||
switch self {
|
||||
case .jpg, .png, .avif, .webp, .gif, .svg, .tiff:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isVideo: Bool {
|
||||
switch self {
|
||||
case .mp4, .m4v, .webm:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isAudio: Bool {
|
||||
switch self {
|
||||
case .mp3, .aac:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isAsset: Bool {
|
||||
switch self {
|
||||
case .js, .css:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isTextFile: Bool {
|
||||
switch self {
|
||||
case .html, .cpp, .swift, .css, .js, .json, .conf, .yaml:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isOtherFile: Bool {
|
||||
switch self {
|
||||
case .noExtension, .zip, .cddx, .pdf, .key, .psd:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var htmlType: String? {
|
||||
switch self {
|
||||
case .mp4, .m4v:
|
||||
return "video/mp4"
|
||||
case .webm:
|
||||
return "video/webm"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var isSvg: Bool {
|
||||
guard case .svg = self else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
@@ -1,8 +1,6 @@
|
||||
|
||||
struct ItemId {
|
||||
|
||||
let itemId: String
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
let itemType: ItemType
|
||||
@@ -11,16 +9,16 @@ struct ItemId {
|
||||
extension ItemId: Equatable {
|
||||
|
||||
static func == (lhs: ItemId, rhs: ItemId) -> Bool {
|
||||
lhs.itemId == rhs.itemId && lhs.language == rhs.language && lhs.itemType == rhs.itemType
|
||||
lhs.language == rhs.language &&
|
||||
lhs.itemType == rhs.itemType
|
||||
}
|
||||
}
|
||||
|
||||
extension ItemId: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(itemId)
|
||||
hasher.combine(language)
|
||||
hasher.combine(itemType)
|
||||
hasher.combine(itemType.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +28,6 @@ extension ItemId: Comparable {
|
||||
guard lhs.itemType == rhs.itemType else {
|
||||
return lhs.itemType < rhs.itemType
|
||||
}
|
||||
guard lhs.itemId == rhs.itemId else {
|
||||
return lhs.itemId < rhs.itemId
|
||||
}
|
||||
return lhs.language < rhs.language
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,17 @@
|
||||
|
||||
enum ItemType: String, Codable {
|
||||
enum ItemType {
|
||||
|
||||
case post
|
||||
case general
|
||||
|
||||
case tag
|
||||
case post(Post)
|
||||
|
||||
case page
|
||||
case feed
|
||||
|
||||
case page(Page)
|
||||
|
||||
case tagPage(Tag)
|
||||
|
||||
case tagOverview
|
||||
|
||||
case file
|
||||
}
|
||||
|
||||
extension ItemType: Equatable {
|
||||
@@ -23,13 +25,52 @@ extension ItemType: Hashable {
|
||||
extension ItemType: Identifiable {
|
||||
|
||||
var id: String {
|
||||
rawValue
|
||||
switch self {
|
||||
case .general:
|
||||
return "0-general"
|
||||
case .feed:
|
||||
return "1-feed"
|
||||
case .post(let post):
|
||||
return "2-post-\(post.id)"
|
||||
case .page(let page):
|
||||
return "3-page-\(page.id)"
|
||||
case .tagPage(let tag):
|
||||
return "5-tag-\(tag.id)"
|
||||
case .tagOverview:
|
||||
return "4-tag-overview"
|
||||
}
|
||||
}
|
||||
|
||||
init?(rawValue: String, posts: [String : Post], pages: [String : Page], tags: [String : Tag]) {
|
||||
if rawValue == "0-general" {
|
||||
self = .general
|
||||
} else if rawValue == "1-feed" {
|
||||
self = .feed
|
||||
} else if rawValue == "4-tag-overview" {
|
||||
self = .tagOverview
|
||||
} else if let id = rawValue.removingPrefix("3-page-"), let page = pages[id] {
|
||||
self = .page(page)
|
||||
} else if let id = rawValue.removingPrefix("2-post-"), let post = posts[id] {
|
||||
self = .post(post)
|
||||
} else if let id = rawValue.removingPrefix("5-tag-"), let tag = tags[id] {
|
||||
self = .tagPage(tag)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ItemType: Comparable {
|
||||
|
||||
static func < (lhs: ItemType, rhs: ItemType) -> Bool {
|
||||
lhs.rawValue < rhs.rawValue
|
||||
lhs.id < rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
func removingPrefix(_ prefix: String) -> String? {
|
||||
guard self.hasPrefix(prefix) else { return nil }
|
||||
return String(self.dropFirst(prefix.count))
|
||||
}
|
||||
}
|
||||
|
@@ -34,29 +34,6 @@ final class LocalizedPage: ObservableObject {
|
||||
*/
|
||||
let originalUrl: String?
|
||||
|
||||
/**
|
||||
All files which occur in the content and are stored.
|
||||
- Note: This property defaults to an empty set.
|
||||
*/
|
||||
@Published
|
||||
var files: Set<String> = []
|
||||
|
||||
/**
|
||||
All files which may occur in the content but are stored externally.
|
||||
|
||||
Missing files which would otherwise produce a warning are ignored when included here.
|
||||
- Note: This property defaults to an empty set.
|
||||
*/
|
||||
@Published
|
||||
var externalFiles: Set<String> = []
|
||||
|
||||
/**
|
||||
Specifies additional files which should be copied to the destination when generating the content.
|
||||
- Note: This property defaults to an empty set.
|
||||
*/
|
||||
@Published
|
||||
var requiredFiles: Set<String> = []
|
||||
|
||||
@Published
|
||||
var linkPreviewImage: FileResource?
|
||||
|
||||
@@ -71,9 +48,6 @@ final class LocalizedPage: ObservableObject {
|
||||
title: String,
|
||||
lastModified: Date? = nil,
|
||||
originalUrl: String? = nil,
|
||||
files: Set<String> = [],
|
||||
externalFiles: Set<String> = [],
|
||||
requiredFiles: Set<String> = [],
|
||||
linkPreviewImage: FileResource? = nil,
|
||||
linkPreviewTitle: String? = nil,
|
||||
linkPreviewDescription: String? = nil) {
|
||||
@@ -82,9 +56,6 @@ final class LocalizedPage: ObservableObject {
|
||||
self.title = title
|
||||
self.lastModified = lastModified
|
||||
self.originalUrl = originalUrl
|
||||
self.files = files
|
||||
self.externalFiles = externalFiles
|
||||
self.requiredFiles = requiredFiles
|
||||
self.linkPreviewImage = linkPreviewImage
|
||||
self.linkPreviewTitle = linkPreviewTitle
|
||||
self.linkPreviewDescription = linkPreviewDescription
|
||||
|
@@ -36,12 +36,10 @@ final class Page: Item {
|
||||
var tags: [Tag]
|
||||
|
||||
/**
|
||||
Additional images required by the element.
|
||||
|
||||
These images are specified as: `source_name destination_name width (height)`.
|
||||
Additional files to copy, because the page content references them
|
||||
*/
|
||||
@Published
|
||||
var images: Set<String> = []
|
||||
var requiredFiles: [FileResource]
|
||||
|
||||
init(content: Content,
|
||||
id: String,
|
||||
@@ -52,7 +50,8 @@ final class Page: Item {
|
||||
endDate: Date?,
|
||||
german: LocalizedPage,
|
||||
english: LocalizedPage,
|
||||
tags: [Tag]) {
|
||||
tags: [Tag],
|
||||
requiredFiles: [FileResource]) {
|
||||
self.externalLink = externalLink
|
||||
self.isDraft = isDraft
|
||||
self.createdDate = createdDate
|
||||
@@ -62,6 +61,7 @@ final class Page: Item {
|
||||
self.german = german
|
||||
self.english = english
|
||||
self.tags = tags
|
||||
self.requiredFiles = requiredFiles
|
||||
|
||||
super.init(content: content, id: id)
|
||||
}
|
||||
@@ -109,12 +109,20 @@ final class Page: Item {
|
||||
}
|
||||
|
||||
override var itemType: ItemType {
|
||||
.page
|
||||
.page(self)
|
||||
}
|
||||
|
||||
func contains(urlComponent: String) -> Bool {
|
||||
english.urlString == urlComponent || german.urlString == urlComponent
|
||||
}
|
||||
|
||||
func pageContent(in language: ContentLanguage) -> String? {
|
||||
content.storage.pageContent(for: id, language: language)
|
||||
}
|
||||
|
||||
func hasContent(in language: ContentLanguage) -> Bool {
|
||||
content.storage.hasPageContent(for: id, language: language)
|
||||
}
|
||||
}
|
||||
|
||||
extension Page: DateItem {
|
||||
|
@@ -1,11 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
final class Post: ObservableObject {
|
||||
|
||||
unowned let content: Content
|
||||
|
||||
@Published
|
||||
var id: String
|
||||
final class Post: Item {
|
||||
|
||||
@Published
|
||||
var isDraft: Bool
|
||||
@@ -45,8 +40,6 @@ final class Post: ObservableObject {
|
||||
german: LocalizedPost,
|
||||
english: LocalizedPost,
|
||||
linkedPage: Page? = nil) {
|
||||
self.content = content
|
||||
self.id = id
|
||||
self.isDraft = isDraft
|
||||
self.createdDate = createdDate
|
||||
self.startDate = startDate
|
||||
@@ -56,6 +49,7 @@ final class Post: ObservableObject {
|
||||
self.german = german
|
||||
self.english = english
|
||||
self.linkedPage = linkedPage
|
||||
super.init(content: content, id: id)
|
||||
}
|
||||
|
||||
func localized(in language: ContentLanguage) -> LocalizedPost {
|
||||
@@ -82,24 +76,6 @@ final class Post: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
extension Post: Identifiable {
|
||||
|
||||
}
|
||||
|
||||
extension Post: Equatable {
|
||||
|
||||
static func == (lhs: Post, rhs: Post) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
extension Post: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
extension Post: DateItem {
|
||||
|
||||
}
|
||||
|
@@ -53,7 +53,7 @@ final class Tag: Item {
|
||||
}
|
||||
|
||||
override var itemType: ItemType {
|
||||
.tag
|
||||
.tagPage(self)
|
||||
}
|
||||
|
||||
func contains(urlComponent: String) -> Bool {
|
||||
|
@@ -1,21 +0,0 @@
|
||||
|
||||
enum CodeFileType: String {
|
||||
|
||||
case html
|
||||
|
||||
case css
|
||||
|
||||
case js
|
||||
|
||||
case cpp
|
||||
|
||||
case swift
|
||||
|
||||
init?(fileExtension: String) {
|
||||
self.init(rawValue: fileExtension)
|
||||
}
|
||||
|
||||
var fileExtension: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
@@ -1,116 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
enum FileTypeCategory: String, CaseIterable {
|
||||
case image
|
||||
case code
|
||||
case model
|
||||
case text
|
||||
case video
|
||||
case resource
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case .image: return "Images"
|
||||
case .code: return "Code"
|
||||
case .model: return "Models"
|
||||
case .text: return "Text"
|
||||
case .video: return "Videos"
|
||||
case .resource: return "Other"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FileTypeCategory: Hashable {
|
||||
|
||||
}
|
||||
|
||||
extension FileTypeCategory: Identifiable {
|
||||
|
||||
var id: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
|
||||
enum FileType {
|
||||
case image(ImageFileType)
|
||||
case code(CodeFileType)
|
||||
case model(ModelFileType)
|
||||
case text(TextFileType)
|
||||
case video(VideoFileType)
|
||||
case other(ResourceFileType)
|
||||
|
||||
init(fileExtension: String?) {
|
||||
let ext = fileExtension?.lowercased() ?? ""
|
||||
|
||||
if let image = ImageFileType(fileExtension: ext) {
|
||||
self = .image(image)
|
||||
} else if let code = CodeFileType(fileExtension: ext) {
|
||||
self = .code(code)
|
||||
} else if let model = ModelFileType(fileExtension: ext) {
|
||||
self = .model(model)
|
||||
} else if let text = TextFileType(fileExtension: ext) {
|
||||
self = .text(text)
|
||||
} else if let video = VideoFileType(fileExtension: ext) {
|
||||
self = .video(video)
|
||||
} else {
|
||||
let resource = ResourceFileType(fileExtension: ext)
|
||||
self = .other(resource)
|
||||
}
|
||||
}
|
||||
|
||||
var fileExtension: String {
|
||||
switch self {
|
||||
case .image(let type): return type.fileExtension
|
||||
case .code(let type): return type.fileExtension
|
||||
case .model(let type): return type.fileExtension
|
||||
case .text(let type): return type.fileExtension
|
||||
case .video(let type): return type.fileExtension
|
||||
case .other(let type): return type.fileExtension
|
||||
}
|
||||
}
|
||||
|
||||
var isImage: Bool {
|
||||
if case .image = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var isVideo: Bool {
|
||||
if case .video = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var isTextFile: Bool {
|
||||
switch self {
|
||||
case .code, .text: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var isOtherFile: Bool {
|
||||
switch self {
|
||||
case .model, .other: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var videoType: VideoFileType? {
|
||||
if case .video(let videoType) = self {
|
||||
return videoType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var isSvg: Bool {
|
||||
guard case .image(let imageFileType) = self else {
|
||||
return false
|
||||
}
|
||||
guard case .svg = imageFileType else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
@@ -1,41 +0,0 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
enum ImageFileType: String {
|
||||
|
||||
case jpg
|
||||
case png
|
||||
case avif
|
||||
case webp
|
||||
case gif
|
||||
case svg
|
||||
case tiff
|
||||
|
||||
init?(fileExtension: String) {
|
||||
if fileExtension == "jpeg" {
|
||||
self = .jpg
|
||||
return
|
||||
}
|
||||
self.init(rawValue: fileExtension)
|
||||
}
|
||||
|
||||
var fileExtension: String {
|
||||
rawValue
|
||||
}
|
||||
|
||||
var fileType: NSBitmapImageRep.FileType? {
|
||||
switch self {
|
||||
case .jpg:
|
||||
return .jpeg
|
||||
case .png, .avif, .webp:
|
||||
return .png
|
||||
case .gif: return .gif
|
||||
case .tiff: return .tiff
|
||||
case .svg: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageFileType: CaseIterable {
|
||||
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
|
||||
enum ModelFileType: String {
|
||||
|
||||
case stl
|
||||
|
||||
case f3d
|
||||
|
||||
case step
|
||||
|
||||
case glb
|
||||
|
||||
case f3z
|
||||
|
||||
init?(fileExtension: String) {
|
||||
self.init(rawValue: fileExtension)
|
||||
}
|
||||
|
||||
var fileExtension: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
@@ -1,54 +0,0 @@
|
||||
|
||||
enum ResourceFileType {
|
||||
|
||||
case noExtension
|
||||
|
||||
case zip
|
||||
|
||||
case cddx
|
||||
|
||||
case mp3
|
||||
|
||||
case pdf
|
||||
|
||||
case key
|
||||
|
||||
case psd
|
||||
|
||||
case other(String)
|
||||
|
||||
init(fileExtension: String) {
|
||||
switch fileExtension {
|
||||
case "": self = .noExtension
|
||||
case "zip": self = .zip
|
||||
case "cddx": self = .cddx
|
||||
case "mp3": self = .mp3
|
||||
case "pdf": self = .pdf
|
||||
case "key": self = .key
|
||||
case "psd": self = .psd
|
||||
default:
|
||||
self = .other(fileExtension)
|
||||
}
|
||||
}
|
||||
|
||||
var fileExtension: String {
|
||||
switch self {
|
||||
case .noExtension:
|
||||
return ""
|
||||
case .zip:
|
||||
return "zip"
|
||||
case .cddx:
|
||||
return "cddx"
|
||||
case .mp3:
|
||||
return "mp3"
|
||||
case .pdf:
|
||||
return "pdf"
|
||||
case .key:
|
||||
return "key"
|
||||
case .psd:
|
||||
return "psd"
|
||||
case .other(let ext):
|
||||
return ext
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
|
||||
enum TextFileType: String {
|
||||
|
||||
case json
|
||||
|
||||
case conf
|
||||
|
||||
case yaml
|
||||
|
||||
init?(fileExtension: String) {
|
||||
self.init(rawValue: fileExtension)
|
||||
}
|
||||
|
||||
var fileExtension: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
|
@@ -1,30 +0,0 @@
|
||||
|
||||
enum VideoFileType: String {
|
||||
|
||||
case mp4
|
||||
|
||||
case m4v
|
||||
|
||||
case webm
|
||||
|
||||
init?(fileExtension: String) {
|
||||
self.init(rawValue: fileExtension)
|
||||
}
|
||||
|
||||
var fileExtension: String {
|
||||
rawValue
|
||||
}
|
||||
|
||||
var htmlType: String {
|
||||
switch self {
|
||||
case .mp4, .m4v:
|
||||
return "video/mp4"
|
||||
case .webm:
|
||||
return "video/webm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension VideoFileType: CaseIterable {
|
||||
|
||||
}
|
Reference in New Issue
Block a user