From b3b8c9a610afe07195e3d27ec37edb2902d11a0f Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Fri, 13 Dec 2024 11:26:34 +0100 Subject: [PATCH] Full page content, fixes, cleaner settings --- CHDataManagement.xcodeproj/project.pbxproj | 98 ++-- .../xcdebugger/Breakpoints_v2.xcbkptlist | 6 + .../Generator/LocalizedWebsiteGenerator.swift | 64 +-- .../Generator/PageCommandExtractor.swift | 31 ++ .../Generator/PageContentAnomaly.swift | 67 +++ .../Generator/PageContentGenerator.swift | 472 ++++++++++-------- .../Generator/PageGenerationResults.swift | 47 +- .../Generator/PageGenerator.swift | 17 +- .../Generator/PostListPageGenerator.swift | 20 +- .../Generator/RequiredHeaders.swift | 16 + .../Generator/ShorthandMarkdownKey.swift | 8 + CHDataManagement/Main/MainView.swift | 7 +- .../Model/Content+Generation.swift | 91 ++-- CHDataManagement/Model/Content+Load.swift | 27 +- CHDataManagement/Model/Content+Save.swift | 36 +- .../Settings/LocalizedPostSettings.swift | 6 + .../Model/Settings/LocalizedSettings.swift | 15 - .../Settings/NavigationBarSettings.swift | 17 - .../Model/Settings/PageSettings.swift | 16 +- .../Model/Settings/PathSettings.swift | 31 ++ .../Model/Settings/PostSettings.swift | 5 + .../Model/Settings/Settings.swift | 21 +- CHDataManagement/Model/Types/FileType.swift | 10 + .../AdditionalPageHeaders.swift | 14 + .../ContentElements/ContentBox.swift | 14 + .../ContentElements/ModelViewer.swift | 11 + .../ContentElements/RelatedPageLink.swift | 39 ++ .../ContentElements/SvgImage.swift | 41 ++ .../Page Elements/ImageGallery.swift | 3 +- .../Page Elements/NavigationBar.swift | 49 +- CHDataManagement/Pages/ContentPage.swift | 18 +- CHDataManagement/Pages/GenericPage.swift | 8 +- CHDataManagement/Pages/PageInFeed.swift | 4 +- .../Preview Content/WebsiteData+Mock.swift | 36 +- .../Settings/LocalizedSettingsFile.swift | 20 - .../Settings/NavigationBarSettingsFile.swift | 19 - .../Model/Settings/PageSettingsFile.swift | 8 +- .../Model/Settings/PathSettingsFile.swift | 42 ++ .../Storage/Model/Settings/SettingsFile.swift | 14 +- .../Views/Pages/LocalizedPageDetailView.swift | 2 + .../Views/Pages/PageContentResultsView.swift | 6 +- .../Views/Pages/PageDetailView.swift | 4 +- .../Views/Posts/LocalizedPostDetailView.swift | 2 + .../Content/PageSettingsContentView.swift | 346 +++++++++++++ .../Settings/GenerationContentView.swift | 23 +- .../Views/Settings/GenerationDetailView.swift | 4 +- .../Settings/NavigationBarSettingsView.swift | 31 +- ...iew.swift => PageSettingsDetailView.swift} | 22 +- ...tingsView.swift => PathSettingsView.swift} | 48 +- .../Views/Settings/PostFeedSettingsView.swift | 2 +- 50 files changed, 1351 insertions(+), 607 deletions(-) create mode 100644 CHDataManagement.xcodeproj/xcuserdata/ch.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 CHDataManagement/Generator/PageCommandExtractor.swift create mode 100644 CHDataManagement/Generator/PageContentAnomaly.swift create mode 100644 CHDataManagement/Generator/RequiredHeaders.swift delete mode 100644 CHDataManagement/Model/Settings/LocalizedSettings.swift delete mode 100644 CHDataManagement/Model/Settings/NavigationBarSettings.swift create mode 100644 CHDataManagement/Model/Settings/PathSettings.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/AdditionalPageHeaders.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/ContentBox.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/ModelViewer.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/RelatedPageLink.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/SvgImage.swift delete mode 100644 CHDataManagement/Storage/Model/Settings/LocalizedSettingsFile.swift delete mode 100644 CHDataManagement/Storage/Model/Settings/NavigationBarSettingsFile.swift create mode 100644 CHDataManagement/Storage/Model/Settings/PathSettingsFile.swift create mode 100644 CHDataManagement/Views/Settings/Content/PageSettingsContentView.swift rename CHDataManagement/Views/Settings/{PageSettingsView.swift => PageSettingsDetailView.swift} (64%) rename CHDataManagement/Views/Settings/{FolderSettingsView.swift => PathSettingsView.swift} (55%) diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index 43895a8..7d3427d 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -43,12 +43,8 @@ E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5282CFFBFB800AEF16D /* ImageFileType.swift */; }; E25DA52C2CFFC3EC00AEF16D /* SDWebImageAVIFCoder in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA52B2CFFC3EC00AEF16D /* SDWebImageAVIFCoder */; }; E25DA52F2CFFC91B00AEF16D /* SDWebImageWebPCoder in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA52E2CFFC91B00AEF16D /* SDWebImageWebPCoder */; }; - E25DA5342D0041CB00AEF16D /* NavigationBarSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5332D0041CB00AEF16D /* NavigationBarSettingsFile.swift */; }; E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */; }; E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */; }; - E25DA53A2D00424000AEF16D /* LocalizedSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5392D00423F00AEF16D /* LocalizedSettingsFile.swift */; }; - E25DA53D2D0043E600AEF16D /* LocalizedSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA53C2D0043E200AEF16D /* LocalizedSettings.swift */; }; - E25DA53F2D00441F00AEF16D /* NavigationBarSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA53E2D00441C00AEF16D /* NavigationBarSettings.swift */; }; E25DA5412D00446C00AEF16D /* PostSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5402D00446700AEF16D /* PostSettings.swift */; }; E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5442D00952D00AEF16D /* SettingsSection.swift */; }; E25DA56D2D00EBCF00AEF16D /* NavigationBarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */; }; @@ -69,7 +65,7 @@ E25DA58F2D02368D00AEF16D /* PageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA58E2D02368A00AEF16D /* PageSettings.swift */; }; E25DA5912D023A8400AEF16D /* IntegerField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5902D023A7E00AEF16D /* IntegerField.swift */; }; E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5922D023B3600AEF16D /* PageSettingsFile.swift */; }; - E25DA5952D023BD100AEF16D /* PageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5942D023BCC00AEF16D /* PageSettingsView.swift */; }; + E25DA5952D023BD100AEF16D /* PageSettingsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5942D023BCC00AEF16D /* PageSettingsDetailView.swift */; }; E25DA5972D023F9F00AEF16D /* ContentPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5962D023F9900AEF16D /* ContentPage.swift */; }; E25DA5992D02401E00AEF16D /* PageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5982D02401A00AEF16D /* PageGenerator.swift */; }; E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA59A2D024A2900AEF16D /* DateItem.swift */; }; @@ -112,6 +108,17 @@ E29D31792D083DE50051B7F4 /* PageContentResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31782D083DDA0051B7F4 /* PageContentResultsView.swift */; }; E29D317D2D086AB00051B7F4 /* Int+Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D317C2D086AAE0051B7F4 /* Int+Random.swift */; }; E29D317F2D086F4C0051B7F4 /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D317E2D086F490051B7F4 /* Icons.swift */; }; + E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */; }; + E29D31852D0AE8EE0051B7F4 /* RequiredHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */; }; + E29D31872D0AE9DE0051B7F4 /* AdditionalPageHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31862D0AE9D40051B7F4 /* AdditionalPageHeaders.swift */; }; + E29D31892D0AED1F0051B7F4 /* ModelViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */; }; + E29D318B2D0B07EE0051B7F4 /* ContentBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318A2D0B07E60051B7F4 /* ContentBox.swift */; }; + E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */; }; + E29D31902D0B34870051B7F4 /* PageContentAnomaly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318F2D0B34870051B7F4 /* PageContentAnomaly.swift */; }; + E29D31922D0B3EFC0051B7F4 /* PageCommandExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31912D0B3EF30051B7F4 /* PageCommandExtractor.swift */; }; + E29D31942D0B7D280051B7F4 /* SvgImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31932D0B7D250051B7F4 /* SvgImage.swift */; }; + E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31952D0C18690051B7F4 /* PathSettings.swift */; }; + E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */; }; E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C022CB16C220060935B /* Environment+Language.swift */; }; E2A21C082CB17B870060935B /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C072CB17B810060935B /* TagView.swift */; }; E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C0D2CB189D70060935B /* Color+RGB.swift */; }; @@ -122,7 +129,7 @@ E2A21C2A2CB2AA4F0060935B /* Post+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C292CB2AA4C0060935B /* Post+Mock.swift */; }; E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */; }; E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C312CB5BCAC0060935B /* PageContentView.swift */; }; - E2A21C362CB9A3D70060935B /* FolderSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C352CB9A3D70060935B /* FolderSettingsView.swift */; }; + E2A21C362CB9A3D70060935B /* PathSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C352CB9A3D70060935B /* PathSettingsView.swift */; }; E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C452CBAE2E50060935B /* FeedEntryContent.swift */; }; E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C472CBAF8830060935B /* String+Extensions.swift */; }; E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C502CBBD53C0060935B /* FileResource.swift */; }; @@ -189,12 +196,8 @@ E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSSize+Scaling.swift"; sourceTree = ""; }; E25DA5262CFF745200AEF16D /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; E25DA5282CFFBFB800AEF16D /* ImageFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFileType.swift; sourceTree = ""; }; - E25DA5332D0041CB00AEF16D /* NavigationBarSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettingsFile.swift; sourceTree = ""; }; E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettingsFile.swift; sourceTree = ""; }; E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettingsFile.swift; sourceTree = ""; }; - E25DA5392D00423F00AEF16D /* LocalizedSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedSettingsFile.swift; sourceTree = ""; }; - E25DA53C2D0043E200AEF16D /* LocalizedSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedSettings.swift; sourceTree = ""; }; - E25DA53E2D00441C00AEF16D /* NavigationBarSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettings.swift; sourceTree = ""; }; E25DA5402D00446700AEF16D /* PostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettings.swift; sourceTree = ""; }; E25DA5442D00952D00AEF16D /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = ""; }; E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettingsView.swift; sourceTree = ""; }; @@ -213,7 +216,7 @@ E25DA58E2D02368A00AEF16D /* PageSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettings.swift; sourceTree = ""; }; E25DA5902D023A7E00AEF16D /* IntegerField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerField.swift; sourceTree = ""; }; E25DA5922D023B3600AEF16D /* PageSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsFile.swift; sourceTree = ""; }; - E25DA5942D023BCC00AEF16D /* PageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsView.swift; sourceTree = ""; }; + E25DA5942D023BCC00AEF16D /* PageSettingsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsDetailView.swift; sourceTree = ""; }; E25DA5962D023F9900AEF16D /* ContentPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPage.swift; sourceTree = ""; }; E25DA5982D02401A00AEF16D /* PageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageGenerator.swift; sourceTree = ""; }; E25DA59A2D024A2900AEF16D /* DateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateItem.swift; sourceTree = ""; }; @@ -256,6 +259,17 @@ E29D31782D083DDA0051B7F4 /* PageContentResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageContentResultsView.swift; sourceTree = ""; }; E29D317C2D086AAE0051B7F4 /* Int+Random.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Random.swift"; sourceTree = ""; }; E29D317E2D086F490051B7F4 /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = ""; }; + E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedPageLink.swift; sourceTree = ""; }; + E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredHeaders.swift; sourceTree = ""; }; + E29D31862D0AE9D40051B7F4 /* AdditionalPageHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionalPageHeaders.swift; sourceTree = ""; }; + E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelViewer.swift; sourceTree = ""; }; + E29D318A2D0B07E60051B7F4 /* ContentBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBox.swift; sourceTree = ""; }; + E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsContentView.swift; sourceTree = ""; }; + E29D318F2D0B34870051B7F4 /* PageContentAnomaly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageContentAnomaly.swift; sourceTree = ""; }; + E29D31912D0B3EF30051B7F4 /* PageCommandExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageCommandExtractor.swift; sourceTree = ""; }; + E29D31932D0B7D250051B7F4 /* SvgImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SvgImage.swift; sourceTree = ""; }; + E29D31952D0C18690051B7F4 /* PathSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSettings.swift; sourceTree = ""; }; + E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSettingsFile.swift; sourceTree = ""; }; E2A21C022CB16C220060935B /* Environment+Language.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Language.swift"; sourceTree = ""; }; E2A21C072CB17B810060935B /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = ""; }; E2A21C0D2CB189D70060935B /* Color+RGB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+RGB.swift"; sourceTree = ""; }; @@ -266,7 +280,7 @@ E2A21C292CB2AA4C0060935B /* Post+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post+Mock.swift"; sourceTree = ""; }; E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalCenter.swift; sourceTree = ""; }; E2A21C312CB5BCAC0060935B /* PageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageContentView.swift; sourceTree = ""; }; - E2A21C352CB9A3D70060935B /* FolderSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderSettingsView.swift; sourceTree = ""; }; + E2A21C352CB9A3D70060935B /* PathSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSettingsView.swift; sourceTree = ""; }; E2A21C452CBAE2E50060935B /* FeedEntryContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedEntryContent.swift; sourceTree = ""; }; E2A21C472CBAF8830060935B /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; E2A21C502CBBD53C0060935B /* FileResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileResource.swift; sourceTree = ""; }; @@ -332,9 +346,8 @@ E25DA5322D0041C400AEF16D /* Settings */ = { isa = PBXGroup; children = ( + E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */, E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */, - E25DA5392D00423F00AEF16D /* LocalizedSettingsFile.swift */, - E25DA5332D0041CB00AEF16D /* NavigationBarSettingsFile.swift */, E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */, E25DA5922D023B3600AEF16D /* PageSettingsFile.swift */, E21850342CFAFA570090B18B /* SettingsFile.swift */, @@ -345,10 +358,9 @@ E25DA53B2D0042EA00AEF16D /* Settings */ = { isa = PBXGroup; children = ( + E29D31952D0C18690051B7F4 /* PathSettings.swift */, E25DA58E2D02368A00AEF16D /* PageSettings.swift */, E25DA5402D00446700AEF16D /* PostSettings.swift */, - E25DA53E2D00441C00AEF16D /* NavigationBarSettings.swift */, - E25DA53C2D0043E200AEF16D /* LocalizedSettings.swift */, E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */, E21850322CFAFA200090B18B /* Settings.swift */, ); @@ -358,6 +370,9 @@ E25DA5782D01C56200AEF16D /* Generator */ = { isa = PBXGroup; children = ( + E29D31912D0B3EF30051B7F4 /* PageCommandExtractor.swift */, + E29D318F2D0B34870051B7F4 /* PageContentAnomaly.swift */, + E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */, E25DA5862D01CA8D00AEF16D /* GenerationResultsHandler.swift */, E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */, E218502E2CFAF6990090B18B /* LocalizedWebsiteGenerator.swift */, @@ -388,6 +403,11 @@ E29D311E2D0320D90051B7F4 /* ContentElements */ = { isa = PBXGroup; children = ( + E29D31932D0B7D250051B7F4 /* SvgImage.swift */, + E29D318A2D0B07E60051B7F4 /* ContentBox.swift */, + E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */, + E29D31862D0AE9D40051B7F4 /* AdditionalPageHeaders.swift */, + E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */, E29D317E2D086F490051B7F4 /* Icons.swift */, E29D31272D0371870051B7F4 /* ContentPageVideo.swift */, E29D31232D0366820051B7F4 /* TagList.swift */, @@ -409,6 +429,14 @@ path = Main; sourceTree = ""; }; + E29D318C2D0B2E5E0051B7F4 /* Content */ = { + isa = PBXGroup; + children = ( + E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */, + ); + path = Content; + sourceTree = ""; + }; E2A21C322CB5BCAC0060935B /* Pages */ = { isa = PBXGroup; children = ( @@ -426,12 +454,13 @@ E2A21C342CB9A3CA0060935B /* Settings */ = { isa = PBXGroup; children = ( + E29D318C2D0B2E5E0051B7F4 /* Content */, E29D316E2D0822720051B7F4 /* SettingsListView.swift */, E25DA5702D01015400AEF16D /* GenerationContentView.swift */, E29D31702D08234D0051B7F4 /* GenerationDetailView.swift */, - E25DA5942D023BCC00AEF16D /* PageSettingsView.swift */, + E25DA5942D023BCC00AEF16D /* PageSettingsDetailView.swift */, E25DA5442D00952D00AEF16D /* SettingsSection.swift */, - E2A21C352CB9A3D70060935B /* FolderSettingsView.swift */, + E2A21C352CB9A3D70060935B /* PathSettingsView.swift */, E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */, E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */, E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */, @@ -575,16 +604,16 @@ E2B85F552C4BD0AD0047CD0C /* Extensions */ = { isa = PBXGroup; children = ( - E29D317C2D086AAE0051B7F4 /* Int+Random.swift */, - E25DA5262CFF745200AEF16D /* URL+Extensions.swift */, - E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */, E25DA5182CFF035200AEF16D /* Array+Split.swift */, - E2A37D202CEA94E80000979F /* Sequence+Sorted.swift */, - E2A37D1E2CEA94330000979F /* Optional+Extensions.swift */, - E2A21C472CBAF8830060935B /* String+Extensions.swift */, - E2A21C0D2CB189D70060935B /* Color+RGB.swift */, E2B85F562C4BD0BB0047CD0C /* Binding+Extension.swift */, + E2A21C0D2CB189D70060935B /* Color+RGB.swift */, E2A21C022CB16C220060935B /* Environment+Language.swift */, + E29D317C2D086AAE0051B7F4 /* Int+Random.swift */, + E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */, + E2A37D1E2CEA94330000979F /* Optional+Extensions.swift */, + E2A37D202CEA94E80000979F /* Sequence+Sorted.swift */, + E2A21C472CBAF8830060935B /* String+Extensions.swift */, + E25DA5262CFF745200AEF16D /* URL+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -736,11 +765,12 @@ E25DA5412D00446C00AEF16D /* PostSettings.swift in Sources */, E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */, E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */, + E29D31902D0B34870051B7F4 /* PageContentAnomaly.swift in Sources */, + E29D31872D0AE9DE0051B7F4 /* AdditionalPageHeaders.swift in Sources */, E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */, E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */, E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */, E21850252CF38BCE0090B18B /* TextEntrySheet.swift in Sources */, - E25DA53A2D00424000AEF16D /* LocalizedSettingsFile.swift in Sources */, E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */, E21850172CEE55FC0090B18B /* FileType.swift in Sources */, E29D31612D06D95C0051B7F4 /* ResourceFileType.swift in Sources */, @@ -748,6 +778,7 @@ E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */, E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */, E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */, + E29D31852D0AE8EE0051B7F4 /* RequiredHeaders.swift in Sources */, E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */, E29D317F2D086F4C0051B7F4 /* Icons.swift in Sources */, E2A21C082CB17B870060935B /* TagView.swift in Sources */, @@ -755,8 +786,8 @@ E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */, E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */, E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */, - E25DA53F2D00441F00AEF16D /* NavigationBarSettings.swift in Sources */, E218502F2CFAF69C0090B18B /* LocalizedWebsiteGenerator.swift in Sources */, + E29D31942D0B7D280051B7F4 /* SvgImage.swift in Sources */, E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */, E25DA5832D01C7A400AEF16D /* VideoFileType.swift in Sources */, E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */, @@ -767,22 +798,24 @@ E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */, E25DA5972D023F9F00AEF16D /* ContentPage.swift in Sources */, E2B85F3D2C4293F80047CD0C /* PageInFeed.swift in Sources */, - E25DA5952D023BD100AEF16D /* PageSettingsView.swift in Sources */, + E25DA5952D023BD100AEF16D /* PageSettingsDetailView.swift in Sources */, E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */, E29D31492D0489BB0051B7F4 /* AddFileView.swift in Sources */, E2A21C2A2CB2AA4F0060935B /* Post+Mock.swift in Sources */, E29D312E2D03A0D70051B7F4 /* LocalizedPageDetailView.swift in Sources */, E2581DED2C75202400F1F079 /* Tag.swift in Sources */, + E29D31922D0B3EFC0051B7F4 /* PageCommandExtractor.swift in Sources */, E29D315B2D06D63E0051B7F4 /* ModelFileType.swift in Sources */, E29D31302D03A2C50051B7F4 /* DescriptionField.swift in Sources */, - E25DA53D2D0043E600AEF16D /* LocalizedSettings.swift in Sources */, E29D31552D06D2CE0051B7F4 /* TagListView.swift in Sources */, + E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */, E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */, E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */, E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */, E29D31262D0370A80051B7F4 /* VideoOption.swift in Sources */, E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */, E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */, + E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */, E29D312C2D039DB80051B7F4 /* PageDetailView.swift in Sources */, E29D31432D0488960051B7F4 /* MainContentView.swift in Sources */, E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */, @@ -794,6 +827,7 @@ E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */, E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */, E21850332CFAFA2F0090B18B /* Settings.swift in Sources */, + E29D31892D0AED1F0051B7F4 /* ModelViewer.swift in Sources */, E29D31412D04887F0051B7F4 /* SelectedDetailView.swift in Sources */, E25DA57A2D01C64400AEF16D /* PageContentGenerator.swift in Sources */, E21850192CEE561C0090B18B /* PageOnDisk.swift in Sources */, @@ -804,6 +838,7 @@ E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */, E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */, E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */, + E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */, E2B85F412C4294790047CD0C /* PageHead.swift in Sources */, E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */, E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */, @@ -831,9 +866,9 @@ E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */, E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */, E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */, + E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */, E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */, E29D315F2D06D6F30051B7F4 /* CodeFileType.swift in Sources */, - E25DA5342D0041CB00AEF16D /* NavigationBarSettingsFile.swift in Sources */, E2A37D0E2CE527070000979F /* Storage.swift in Sources */, E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */, E29D314D2D04FCBF0051B7F4 /* FileToAddView.swift in Sources */, @@ -855,12 +890,13 @@ E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */, E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */, E29D31222D0363FD0051B7F4 /* ContentButtons.swift in Sources */, - E2A21C362CB9A3D70060935B /* FolderSettingsView.swift in Sources */, + E2A21C362CB9A3D70060935B /* PathSettingsView.swift in Sources */, E29D31362D0435430051B7F4 /* TabSelection.swift in Sources */, E29D31572D06D38B0051B7F4 /* AddTagView.swift in Sources */, E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */, E29D31792D083DE50051B7F4 /* PageContentResultsView.swift in Sources */, E25DA5912D023A8400AEF16D /* IntegerField.swift in Sources */, + E29D318B2D0B07EE0051B7F4 /* ContentBox.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/CHDataManagement.xcodeproj/xcuserdata/ch.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/CHDataManagement.xcodeproj/xcuserdata/ch.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..9c4188f --- /dev/null +++ b/CHDataManagement.xcodeproj/xcuserdata/ch.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift b/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift index 569f028..1e859df 100644 --- a/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift +++ b/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift @@ -4,20 +4,16 @@ final class LocalizedWebsiteGenerator { let language: ContentLanguage - let localizedSettings: LocalizedSettings + private let localizedPostSettings: LocalizedPostSettings private var outputDirectory: URL { - URL(filePath: content.settings.outputDirectoryPath) + content.settings.outputDirectory } private var postsPerPage: Int { content.settings.posts.postsPerPage } - private var navigationIconPath: String { - content.settings.navigationBar.iconPath - } - private var mainContentMaximumWidth: CGFloat { CGFloat(content.settings.posts.contentWidth) } @@ -26,19 +22,20 @@ final class LocalizedWebsiteGenerator { private let imageGenerator: ImageGenerator - private var navigationBarData: NavigationBarData { - createNavigationBarData( - settings: content.settings.navigationBar, - iconDescription: localizedSettings.navigationBarIconDescription) + private var navigationBarLinks: [NavigationBar.Link] { + content.settings.navigationTags.map { + let localized = $0.localized(in: language) + return .init(text: localized.name, url: content.absoluteUrlToTag($0, language: language)) + } } init(content: Content, language: ContentLanguage) { self.language = language self.content = content - self.localizedSettings = content.settings.localized(in: language) + self.localizedPostSettings = content.settings.localized(in: language) self.imageGenerator = ImageGenerator( storage: content.storage, - relativeImageOutputPath: "images") // TODO: Get from settings + relativeImageOutputPath: content.settings.paths.imagesOutputFolderPath) } func generateWebsite(callback: (String) -> Void) -> Bool { @@ -63,11 +60,11 @@ final class LocalizedWebsiteGenerator { language: language, content: content, imageGenerator: imageGenerator, - navigationBarData: navigationBarData, + navigationBarLinks: navigationBarLinks, showTitle: false, - pageTitle: localizedSettings.posts.title, - pageDescription: localizedSettings.posts.description, - pageUrlPrefix: localizedSettings.posts.feedUrlPrefix) + pageTitle: localizedPostSettings.title, + pageDescription: localizedPostSettings.description, + pageUrlPrefix: localizedPostSettings.feedUrlPrefix) return generator.createPages(for: content.posts) } @@ -78,16 +75,17 @@ final class LocalizedWebsiteGenerator { let localized = tag.localized(in: language) - #warning("Get tag url prefix from settings") + let urlPrefix = content.absoluteUrlPrefixForTag(tag, language: language) + let generator = PostListPageGenerator( language: language, content: content, imageGenerator: imageGenerator, - navigationBarData: navigationBarData, + navigationBarLinks: navigationBarLinks, showTitle: true, pageTitle: localized.name, pageDescription: localized.description ?? "", - pageUrlPrefix: "tags/\(localized.urlComponent)") + pageUrlPrefix: urlPrefix) guard generator.createPages(for: posts) else { return false } @@ -95,17 +93,6 @@ final class LocalizedWebsiteGenerator { return true } - private func createNavigationBarData(settings: NavigationBarSettings, iconDescription: String) -> NavigationBarData { - let navigationItems: [NavigationBarLink] = settings.tags.map { - let localized = $0.localized(in: language) - return .init(text: localized.name, url: localized.urlComponent) - } - return NavigationBarData( - navigationIconPath: navigationIconPath, - iconDescription: iconDescription, - navigationItems: navigationItems) - } - private func generatePagesFolderIfNeeded() -> Bool { let relativePath = content.settings.pages.pageUrlPrefix @@ -125,7 +112,10 @@ final class LocalizedWebsiteGenerator { print("Failed to generate output folder") return false } - let pageGenerator = PageGenerator(content: content, imageGenerator: imageGenerator, navigationBarData: navigationBarData) + let pageGenerator = PageGenerator( + content: content, + imageGenerator: imageGenerator, + navigationBarLinks: navigationBarLinks) let content: String let results: PageGenerationResults @@ -140,7 +130,7 @@ final class LocalizedWebsiteGenerator { return true } - let path = self.content.pageLink(page, language: language) + ".html" + let path = self.content.absoluteUrlToPage(page, language: language) + ".html" guard save(content, to: path) else { print("Failed to save page") return false @@ -161,15 +151,7 @@ final class LocalizedWebsiteGenerator { continue } - let outputPath: String - switch file.type { - case .video: - outputPath = content.pathToVideo(file) - case .image: - outputPath = content.pathToImage(file) - default: - outputPath = content.pathToFile(file) - } + let outputPath = content.absoluteUrlToFile(file) do { try content.storage.copy(file: file.id, to: outputPath) } catch { diff --git a/CHDataManagement/Generator/PageCommandExtractor.swift b/CHDataManagement/Generator/PageCommandExtractor.swift new file mode 100644 index 0000000..d50e632 --- /dev/null +++ b/CHDataManagement/Generator/PageCommandExtractor.swift @@ -0,0 +1,31 @@ +import Ink + +final class PageCommandExtractor { + + private var occurences: [(full: String, command: String, arguments: [String])] = [] + + func findOccurences(of command: ShorthandMarkdownKey, in content: String) -> [(full: String, arguments: [String])] { + findOccurences(of: command.rawValue, in: content) + } + + func findOccurences(of command: String, in content: String) -> [(full: String, arguments: [String])] { + let parser = MarkdownParser(modifiers: [ + Modifier(target: .images, closure: processMarkdownImage), + ]) + _ = parser.html(from: content) + + return occurences + .filter { $0.command == command } + .map { ($0.full, $0.arguments) } + } + + private func processMarkdownImage(html: String, markdown: Substring) -> String { + let argumentList = markdown.between(first: "](", andLast: ")").removingPercentEncoding ?? markdown.between(first: "](", andLast: ")") + let arguments = argumentList.components(separatedBy: ";") + + + let command = markdown.between("![", and: "]").trimmed + occurences.append((full: String(markdown), command: command, arguments: arguments)) + return "" + } +} diff --git a/CHDataManagement/Generator/PageContentAnomaly.swift b/CHDataManagement/Generator/PageContentAnomaly.swift new file mode 100644 index 0000000..dabb6c9 --- /dev/null +++ b/CHDataManagement/Generator/PageContentAnomaly.swift @@ -0,0 +1,67 @@ + + +enum PageContentAnomaly { + case failedToLoadContent(Error) + case missingFile(String) + case missingPage(String) + case missingTag(String) + case unknownCommand(String) + case invalidCommandArguments(command: ShorthandMarkdownKey, arguments: [String]) +} + +extension PageContentAnomaly: Identifiable { + + var id: String { + switch self { + case .failedToLoadContent: + return "load-failed" + case .missingFile(let string): + return "missing-file-\(string)" + case .missingPage(let string): + return "missing-page-\(string)" + case .missingTag(let string): + return "missing-tag-\(string)" + case .unknownCommand(let string): + return "unknown-command-\(string)" + case .invalidCommandArguments(let command, let arguments): + return "invalid-arguments-\(command)-\(arguments.joined(separator: "-"))" + } + } +} + +extension PageContentAnomaly { + + enum Severity: String, CaseIterable { + case warning + case error + } + + var severity: Severity { + switch self { + case .failedToLoadContent: + return .error + case .missingFile, .missingPage, .missingTag, .unknownCommand, .invalidCommandArguments: + return .warning + } + } +} + +extension PageContentAnomaly: CustomStringConvertible { + + var description: String { + switch self { + case .failedToLoadContent(let error): + return "Failed to load content: \(error)" + case .missingFile(let string): + return "Missing file \(string)" + case .missingPage(let string): + return "Missing page \(string)" + case .missingTag(let string): + return "Missing tag \(string)" + case .unknownCommand(let string): + return "Unknown command \(string)" + case .invalidCommandArguments(let command, let arguments): + return "Invalid command arguments for \(command): \(arguments)" + } + } +} diff --git a/CHDataManagement/Generator/PageContentGenerator.swift b/CHDataManagement/Generator/PageContentGenerator.swift index 446cedb..ba1406f 100644 --- a/CHDataManagement/Generator/PageContentGenerator.swift +++ b/CHDataManagement/Generator/PageContentGenerator.swift @@ -8,15 +8,17 @@ final class PageContentParser { private let pageLinkMarker = "page:" + private let tagLinkMarker = "tag:" + + private static let codeHighlightFooter = "" + private let swift = SyntaxHighlighter(format: HTMLOutputFormat()) let results = PageGenerationResults() private let content: Content - private let language: ContentLanguage - - private var largeImageCount: Int = 0 + let language: ContentLanguage var largeImageWidth: Int { content.settings.pages.largeImageWidth @@ -32,26 +34,16 @@ final class PageContentParser { } func requestImages(_ generator: ImageGenerator) { - let thumbnailWidth = CGFloat(thumbnailWidth) - let largeImageWidth = CGFloat(largeImageWidth) - - for image in results.files { - guard case .image = image.type else { - continue - } + for request in results.imagesToGenerate { generator.generateImageSet( - for: image.id, - maxWidth: thumbnailWidth, maxHeight: thumbnailWidth) - - generator.generateImageSet( - for: image.id, - maxWidth: largeImageWidth, maxHeight: largeImageWidth) + for: request.image.id, + maxWidth: CGFloat(request.size), + maxHeight: CGFloat(request.size)) } } func reset() { results.reset() - largeImageCount = 0 } func generatePage(from content: String) -> String { @@ -68,6 +60,8 @@ final class PageContentParser { private func handleCode(html: String, markdown: Substring) -> String { guard markdown.starts(with: "```swift") else { + results.requiredHeaders.insert(.codeHightlighting) + results.requiredFooters.insert(PageContentParser.codeHighlightFooter) return html // Just use normal code highlighting } // Highlight swift code using Splash @@ -78,35 +72,46 @@ final class PageContentParser { private func handleLink(html: String, markdown: Substring) -> String { let file = markdown.between("(", and: ")") if file.hasPrefix(pageLinkMarker) { - // Retain links pointing to elements within a page - let textToChange = file.dropAfterFirst("#") - let pageId = textToChange.replacingOccurrences(of: pageLinkMarker, with: "") - guard let page = content.page(pageId) else { - results.missingPages.insert(pageId) - // Remove link since the page can't be found - return markdown.between("[", and: "]") - } - results.linkedPages.insert(page) - let pagePath = content.pageLink(page, language: language) - return html.replacingOccurrences(of: textToChange, with: pagePath) + return handlePageLink(file: file, html: html, markdown: markdown) } - - // TODO: Check that linked file exists -// if let filePath = page.nonAbsolutePathRelativeToRootForContainedInputFile(file) { -// // The target of the page link must be present after generation is complete -// results.expect(file: filePath, source: page.path) -// } + if file.hasPrefix(tagLinkMarker) { + return handleTagLink(file: file, html: html, markdown: markdown) + } + #warning("Check existence of linked file") return html } + private func handlePageLink(file: String, html: String, markdown: Substring) -> String { + // Retain links pointing to elements within a page + let textToChange = file.dropAfterFirst("#") + let pageId = textToChange.replacingOccurrences(of: pageLinkMarker, with: "") + guard let page = content.page(pageId) else { + results.missingPages.insert(pageId) + // Remove link since the page can't be found + return markdown.between("[", and: "]") + } + results.linkedPages.insert(page) + let pagePath = content.absoluteUrlToPage(page, language: language) + return html.replacingOccurrences(of: textToChange, with: pagePath) + } + + private func handleTagLink(file: String, html: String, markdown: Substring) -> String { + // Retain links pointing to elements within a page + let textToChange = file.dropAfterFirst("#") + let tagId = textToChange.replacingOccurrences(of: tagLinkMarker, with: "") + guard let tag = content.tag(tagId) else { + results.missingTags.insert(tagId) + // Remove link since the tag can't be found + return markdown.between("[", and: "]") + } + results.linkedTags.insert(tag) + let tagPath = content.absoluteUrlToTag(tag, language: language) + return html.replacingOccurrences(of: textToChange, with: tagPath) + } + private func handleHTML(html: String, markdown: Substring) -> String { - // TODO: Check HTML code in markdown for required resources - //print("[HTML] Found in page \(page.path):") - //print(markdown) - // Things to check: - // return html } @@ -130,35 +135,28 @@ final class PageContentParser { return parts[0] + " id=\"\(id)\">" + parts.dropFirst().joined(separator: ">") } - private func processMarkdownImage(html: String, markdown: Substring) -> String { - // First, check the content type, then parse the remaining arguments - // Notation: - // -> Optional argument - // -> Repeated argument (0 or more) - // ![url](;) - // ![image](;] - // ![video](;] - // ![svg](;<;;;?>) - // ![download](<,,;...) - // ![box](;<body>) - // ![model](<file>;<description>) - // ![page](<pageId>) - // ![external](<<url>;<text>...> - // ![html](<fileId>) + private func percentDecoded(_ string: String) -> String { + guard let decoded = string.removingPercentEncoding else { + print("Invalid string: \(string)") + return string + } + return decoded + } - let argumentList = markdown.between(first: "](", andLast: ")").removingPercentEncoding ?? markdown.between(first: "](", andLast: ")") + private func processMarkdownImage(html: String, markdown: Substring) -> String { + // + let argumentList = percentDecoded(markdown.between(first: "](", andLast: ")")) let arguments = argumentList.components(separatedBy: ";") - let rawCommand = markdown.between("![", and: "]").trimmed + let rawCommand = percentDecoded(markdown.between("![", and: "]").trimmed) guard rawCommand != "" else { return handleImage(arguments) } - guard let convertedCommand = rawCommand.removingPercentEncoding, - let command = ShorthandMarkdownKey(rawValue: convertedCommand) else { + guard let command = ShorthandMarkdownKey(rawValue: rawCommand) else { // Treat unknown commands as normal links - results.warnings.append("Unknown markdown command '\(rawCommand)'") + results.unknownCommands.append(rawCommand) return html } @@ -173,25 +171,28 @@ final class PageContentParser { return handleVideo(arguments) case .externalLink: return handleExternalButtons(arguments) - /* - case .includedHtml: - return handleExternalHTML(file: content) - case .box: - return handleSimpleBox(content: content) + case .gitLink: + return handleGitButtons(arguments) case .pageLink: - return handlePageLink(pageId: content) + return handlePageLink(arguments) + case .includedHtml: + return handleExternalHtml(arguments) + case .box: + return handleSimpleBox(arguments) case .model: - return handle3dModel(content: content) - */ + return handleModel(arguments) + case .svg: + return handleSvg(arguments) default: - results.warnings.append("Unhandled command '\(command.rawValue)'") + results.unknownCommands.append(command.rawValue) return "" - } } + /** + Format: `[image](<imageId>;<caption?>]` + */ private func handleImage(_ arguments: [String]) -> String { - // [image](<imageId>;<caption?>] guard (1...2).contains(arguments.count) else { results.invalidCommandArguments.append((.image , arguments)) return "" @@ -207,19 +208,25 @@ final class PageContentParser { let caption = arguments.count == 2 ? arguments[1] : nil let altText = image.getDescription(for: language) - let path = content.pathToImage(image) + let path = content.absoluteUrlToFile(image) + + guard !image.type.isSvg else { + return SvgImage(imagePath: path, altText: altText).content + } let thumbnail = FeedEntryData.Image( rawImagePath: path, width: thumbnailWidth, height: thumbnailWidth, altText: altText) + results.imagesToGenerate.insert(.init(size: thumbnailWidth, image: image)) let largeImage = FeedEntryData.Image( rawImagePath: path, width: largeImageWidth, height: largeImageWidth, altText: altText) + results.imagesToGenerate.insert(.init(size: largeImageWidth, image: image)) return PageImage( imageId: imageId.replacingOccurrences(of: ".", with: "-"), @@ -228,7 +235,11 @@ final class PageContentParser { caption: caption).content } + /** + Format: `![hiking-stats](<time>;<elevation-up>;<elevation-down>;<distance>;<calories>)` + */ private func handleHikingStatistics(_ arguments: [String]) -> String { + #warning("Make statistics more generic using key-value pairs") guard (1...5).contains(arguments.count) else { results.invalidCommandArguments.append((.hikingStatistics, arguments)) return "" @@ -249,30 +260,37 @@ final class PageContentParser { .content } + /** + Format: `![download](<<fileId>,<text>,<download-filename?>;...)` + */ private func handleDownloadButtons(_ arguments: [String]) -> String { - // ![download](<<fileId>,<text>,<download-filename?>;...) - let buttons: [ContentButtons.Item] = arguments.compactMap { button in - let parts = button.components(separatedBy: ",") - guard (2...3).contains(parts.count) else { - results.invalidCommandArguments.append((.downloadButtons, parts)) - return nil - } - let file = parts[0].trimmed - let title = parts[1].trimmed - let downloadName = parts.count > 2 ? parts[2].trimmed : nil - - // Ensure that file is available - guard let filePath = content.pathToFile(file) else { - results.missingFiles.insert(file) - return nil - } - return ContentButtons.Item(icon: .download, filePath: filePath, text: title, downloadFileName: downloadName) - } + let buttons = arguments.compactMap(convertButton) return ContentButtons(items: buttons).content } + private func convertButton(definition button: String) -> ContentButtons.Item? { + let parts = button.components(separatedBy: ",") + guard (2...3).contains(parts.count) else { + results.invalidCommandArguments.append((.downloadButtons, parts)) + return nil + } + let fileId = parts[0].trimmed + let title = parts[1].trimmed + let downloadName = parts.count > 2 ? parts[2].trimmed : nil + + guard let file = content.file(id: fileId) else { + results.missingFiles.insert(fileId) + return nil + } + results.files.insert(file) + let filePath = content.absoluteUrlToFile(file) + return ContentButtons.Item(icon: .download, filePath: filePath, text: title, downloadFileName: downloadName) + } + + /** + Format: `![video](<fileId>;<option1...>]` + */ private func handleVideo(_ arguments: [String]) -> String { - // ![video](<fileId>;<option1...>] guard arguments.count >= 1 else { results.invalidCommandArguments.append((.video, arguments)) return "" @@ -288,11 +306,11 @@ final class PageContentParser { results.files.insert(file) guard let videoType = file.type.videoType?.htmlType else { - results.warnings.append("Unknown video file type for \(fileId)") + results.invalidCommandArguments.append((.video, arguments)) return "" } - let filePath = content.pathToFile(file) + let filePath = content.absoluteUrlToFile(file) return ContentPageVideo( filePath: filePath, videoType: videoType, @@ -311,7 +329,7 @@ final class PageContentParser { if case let .poster(imageId) = option { if let image = content.image(imageId) { results.files.insert(image) - let link = content.pathToImage(image) + let link = content.absoluteUrlToFile(image) let width = 2*thumbnailWidth let fullLink = WebsiteImage.imagePath(source: link, width: width, height: width) return .poster(image: fullLink) @@ -323,7 +341,7 @@ final class PageContentParser { if case let .src(videoId) = option { if let video = content.video(videoId) { results.files.insert(video) - let link = content.pathToVideo(video) + let link = content.absoluteUrlToFile(video) // TODO: Set correct video path? return .src(link) } else { @@ -334,64 +352,17 @@ final class PageContentParser { return option } - /* - - private func handleGif(file: String, altText: String) -> String { - let imagePath = page.pathRelativeToRootForContainedInputFile(file) - results.require(file: imagePath, source: page.path) - - guard let size = results.getImageSize(atPath: imagePath, source: page.path) else { - return "" - } - let width = Int(size.width) - let height = Int(size.height) - return factory.html.image(file: file, width: width, height: height, altText: altText) - } - - private func handleSvg(file: String, area: String?) -> String { - let imagePath = page.pathRelativeToRootForContainedInputFile(file) - results.require(file: imagePath, source: page.path) - - guard let size = results.getImageSize(atPath: imagePath, source: page.path) else { - return "" // Missing image warning already produced - } - let width = Int(size.width) - let height = Int(size.height) - - var altText = "image " + file.lastComponentAfter("/") - guard let area = area else { - return factory.html.image(file: file, width: width, height: height, altText: altText) - } - let parts = area.components(separatedBy: ",").map { $0.trimmed } - switch parts.count { - case 1: - return factory.html.image(file: file, width: width, height: height, altText: parts[0]) - case 4: - break - case 5: - altText = parts[4] - default: - results.warning("Invalid area string for svg image", source: page.path) - return factory.html.image(file: file, width: width, height: height, altText: altText) - } - guard let x = Int(parts[0]), - let y = Int(parts[1]), - let partWidth = Int(parts[2]), - let partHeight = Int(parts[3]) else { - results.warning("Invalid area string for svg image", source: page.path) - return factory.html.image(file: file, width: width, height: height, altText: altText) - } - let part = SVGSelection(x, y, partWidth, partHeight) - return factory.html.svgImage(file: file, part: part, altText: altText) - } - - private func handleFile(file: String, fileExtension: String) -> String { - results.warning("Unhandled file \(file) with extension \(fileExtension)", source: page.path) - return "" - } -*/ private func handleExternalButtons(_ arguments: [String]) -> String { // ![external](<<url>;<text>...> + handleButtons(icon: .externalLink, arguments: arguments) + } + + private func handleGitButtons(_ arguments: [String]) -> String { + // ![git](<<url>;<text>...> + handleButtons(icon: .gitLink, arguments: arguments) + } + + private func handleButtons(icon: PageIcon, arguments: [String]) -> String { guard arguments.count >= 1 else { results.invalidCommandArguments.append((.externalLink, arguments)) return "" @@ -410,98 +381,161 @@ final class PageContentParser { let title = parts[1].trimmed return .init( - icon: .externalLink, + icon: icon, filePath: url, text: title) } return ContentButtons(items: buttons).content } -/* - private func handleExternalHTML(file: String) -> String { - let path = page.pathRelativeToRootForContainedInputFile(file) - return results.getContentOfRequiredFile(at: path, source: page.path) ?? "" - } - private func handleSimpleBox(content: String) -> String { - let parts = content.components(separatedBy: ";") - guard parts.count > 1 else { - results.warning("Invalid box specification", page: page) + /** + Format: `![html](<fileId>)` + */ + private func handleExternalHtml(_ arguments: [String]) -> String { + guard arguments.count == 1 else { + results.invalidCommandArguments.append((.includedHtml, arguments)) return "" } - let title = parts[0] - let text = parts.dropFirst().joined(separator: ";") - return factory.makePlaceholder(title: title, text: text) - } - - private func handlePageLink(pageId: String) -> String { - guard let linkedPage = siteRoot.find(pageId) else { - // Checking the page path will add it to the missing pages - _ = results.getPagePath(for: pageId, source: page.path, language: language) - // Remove link since the page can't be found + let fileId = arguments[0] + guard let file = content.file(id: fileId) else { + results.missingFiles.insert(fileId) return "" } - guard linkedPage.state == .standard else { + return file.textContent() + } + + /** + Format: `![box](<title>;<body>)` + */ + private func handleSimpleBox(_ arguments: [String]) -> String { + guard arguments.count > 1 else { + results.invalidCommandArguments.append((.box, arguments)) + return "" + } + let title = arguments[0] + let text = arguments.dropFirst().joined(separator: ";") + return ContentBox(title: title, text: text).content + } + + /** + Format: `![page](<pageId>)` + */ + private func handlePageLink(_ arguments: [String]) -> String { + guard arguments.count == 1 else { + results.invalidCommandArguments.append((.pageLink, arguments)) + return "" + } + let pageId = arguments[0] + + guard let page = content.page(pageId) else { + results.missingPages.insert(pageId) + return "" + } + guard !page.isDraft else { // Prevent linking to unpublished content return "" } - var content = [PageLinkTemplate.Key: String]() - content[.title] = linkedPage.title(for: language) - content[.altText] = "" + let localized = page.localized(in: language) + let url = content.absoluteUrlToPage(page, language: language) + let title = localized.linkPreviewTitle ?? localized.title + let description = localized.linkPreviewDescription ?? "" - let fullThumbnailPath = linkedPage.thumbnailFilePath(for: language).destination - // Note: Here we assume that the thumbnail was already used elsewhere, so already generated - let relativeImageUrl = page.relativePathToOtherSiteElement(file: fullThumbnailPath) - let metadata = linkedPage.localized(for: language) + let image = localized.linkPreviewImage.map { image in + let size = content.settings.pages.pageLinkImageSize + results.files.insert(image) + results.imagesToGenerate.insert(.init(size: size, image: image)) - if linkedPage.state.hasThumbnailLink { - let fullPageUrl = linkedPage.fullPageUrl(for: language) - let relativePageUrl = page.relativePathToOtherSiteElement(file: fullPageUrl) - content[.url] = "href=\"\(relativePageUrl)\"" + return RelatedPageLink.Image( + url: content.absoluteUrlToFile(image), + description: image.getDescription(for: language), + size: size) } - content[.image] = relativeImageUrl.dropAfterLast(".") - if let suffix = metadata.thumbnailSuffix { - content[.title] = factory.html.make(title: metadata.title, suffix: suffix) - } else { - content[.title] = metadata.title - } - - let path = linkedPage.makePath(language: language, from: siteRoot) - content[.path] = factory.pageLink.makePath(components: path) - - content[.description] = metadata.relatedContentText - if let parent = linkedPage.findParent(from: siteRoot), parent.thumbnailStyle == .large { - content[.className] = " related-page-link-large" - } - - // We assume that the thumbnail images are already required by overview pages. - return factory.pageLink.generate(content) + return RelatedPageLink( + title: title, + description: description, + url: url, + image: image) + .content } - private func handle3dModel(content: String) -> String { - let parts = content.components(separatedBy: ";") - guard parts.count > 1 else { - results.warning("Invalid 3d model specification", page: page) + /** + Format: `![model](<file>)` + */ + private func handleModel(_ arguments: [String]) -> String { + guard arguments.count == 1 else { + results.invalidCommandArguments.append((.model, arguments)) return "" } - let file = parts[0] - guard file.hasSuffix(".glb") else { - results.warning("Invalid 3d model file \(file) (must be .glb)", page: page) + let fileId = arguments[0] + guard fileId.hasSuffix(".glb") else { + results.invalidCommandArguments.append((.model, ["\(fileId) is not a .glb file"])) return "" } - // Ensure that file is available - let filePath = page.pathRelativeToRootForContainedInputFile(file) - results.require(file: filePath, source: page.path) + guard let file = content.file(id: fileId) else { + results.missingFiles.insert(fileId) + return "" + } + results.files.insert(file) + results.requiredHeaders.insert(.modelViewer) - // Add required file to head - headers.insert(.modelViewer) - - let description = parts.dropFirst().joined(separator: ";") - return """ - <model-viewer alt="\(description)" src="\(file)" ar shadow-intensity="1" camera-controls touch-action="pan-y"></model-viewer> - """ + let path = content.absoluteUrlToFile(file) + let description = file.getDescription(for: language) + return ModelViewer(file: path, description: description).content } - */ + + private func handleSvg(_ arguments: [String]) -> String { + guard arguments.count == 5 else { + results.invalidCommandArguments.append((.svg, arguments)) + return "" + } + + guard let x = Int(arguments[1]), + let y = Int(arguments[2]), + let partWidth = Int(arguments[3]), + let partHeight = Int(arguments[4]) else { + results.invalidCommandArguments.append((.svg, arguments)) + return "" + } + + let imageId = arguments[0] + + guard let image = content.image(imageId) else { + results.missingFiles.insert(imageId) + return "" + } + guard case .image(let imageType) = image.type, + imageType == .svg else { + results.invalidCommandArguments.append((.svg, arguments)) + return "" + } + + let path = content.absoluteUrlToFile(image) + + return PartialSvgImage( + imagePath: path, + altText: image.getDescription(for: language), + x: x, + y: y, + width: partWidth, + height: partHeight) + .content + } + } + +/* +private func handleGif(file: String, altText: String) -> String { + let imagePath = page.pathRelativeToRootForContainedInputFile(file) + results.require(file: imagePath, source: page.path) + + guard let size = results.getImageSize(atPath: imagePath, source: page.path) else { + return "" + } + let width = Int(size.width) + let height = Int(size.height) + return factory.html.image(file: file, width: width, height: height, altText: altText) +} +*/ diff --git a/CHDataManagement/Generator/PageGenerationResults.swift b/CHDataManagement/Generator/PageGenerationResults.swift index 4c4a466..06c6266 100644 --- a/CHDataManagement/Generator/PageGenerationResults.swift +++ b/CHDataManagement/Generator/PageGenerationResults.swift @@ -1,32 +1,75 @@ import Foundation +struct ImageToGenerate { + + let size: Int + + let image: FileResource +} + +extension ImageToGenerate: Hashable { + + func hash(into hasher: inout Hasher) { + hasher.combine(size) + hasher.combine(image.id) + } +} + final class PageGenerationResults: ObservableObject { @Published var linkedPages: Set<Page> = [] + @Published + var linkedTags: Set<Tag> = [] + @Published var files: Set<FileResource> = [] + @Published + var imagesToGenerate: Set<ImageToGenerate> = [] + @Published var missingPages: Set<String> = [] @Published var missingFiles: Set<String> = [] + @Published + var missingTags: Set<String> = [] + + @Published + var unknownCommands: [String] = [] + @Published var invalidCommandArguments: [(command: ShorthandMarkdownKey, arguments: [String])] = [] @Published - var warnings: [String] = [] + var requiredHeaders: RequiredHeaders = [] + @Published + var requiredFooters: Set<String> = [] func reset() { linkedPages = [] + linkedTags = [] files = [] + imagesToGenerate = [] missingPages = [] missingFiles = [] + missingTags = [] + unknownCommands = [] invalidCommandArguments = [] - warnings = [] + requiredHeaders = [] + requiredFooters = [] + } + + var convertedWarnings: [PageContentAnomaly] { + var result = [PageContentAnomaly]() + result += missingPages.map { .missingPage($0) } + result += missingFiles.map { .missingFile($0) } + result += unknownCommands.map { .unknownCommand($0) } + result += invalidCommandArguments.map { .invalidCommandArguments(command: $0.command, arguments: $0.arguments) } + return result } } diff --git a/CHDataManagement/Generator/PageGenerator.swift b/CHDataManagement/Generator/PageGenerator.swift index c501407..ceaf13b 100644 --- a/CHDataManagement/Generator/PageGenerator.swift +++ b/CHDataManagement/Generator/PageGenerator.swift @@ -4,12 +4,12 @@ final class PageGenerator { private let imageGenerator: ImageGenerator - private let navigationBarData: NavigationBarData + private let navigationBarLinks: [NavigationBar.Link] - init(content: Content, imageGenerator: ImageGenerator, navigationBarData: NavigationBarData) { + init(content: Content, imageGenerator: ImageGenerator, navigationBarLinks: [NavigationBar.Link]) { self.content = content self.imageGenerator = imageGenerator - self.navigationBarData = navigationBarData + self.navigationBarLinks = navigationBarLinks } func generate(page: Page, language: ContentLanguage) throws -> (page: String, results: PageGenerationResults) { @@ -27,9 +27,12 @@ final class PageGenerator { let tags: [FeedEntryData.Tag] = page.tags.map { tag in .init(name: tag.localized(in: language).name, - url: content.tagLink(tag, language: language)) + url: content.absoluteUrlToTag(tag, language: language)) } + let headers = AdditionalPageHeaders( + headers: contentGenerator.results.requiredHeaders, + assetPath: content.settings.pages.javascriptFilesPath) let fullPage = ContentPage( language: language, dateString: page.dateText(in: language), @@ -37,8 +40,10 @@ final class PageGenerator { tags: tags, linkTitle: localized.linkPreviewTitle ?? localized.title, description: localized.linkPreviewDescription ?? "", - navigationBarData: navigationBarData, - pageContent: pageContent) + navigationBarLinks: navigationBarLinks, + pageContent: pageContent, + headers: headers.content, + footers: contentGenerator.results.requiredFooters.sorted()) .content return (fullPage, contentGenerator.results) diff --git a/CHDataManagement/Generator/PostListPageGenerator.swift b/CHDataManagement/Generator/PostListPageGenerator.swift index f1f5250..3991727 100644 --- a/CHDataManagement/Generator/PostListPageGenerator.swift +++ b/CHDataManagement/Generator/PostListPageGenerator.swift @@ -8,7 +8,7 @@ final class PostListPageGenerator { private let imageGenerator: ImageGenerator - private let navigationBarData: NavigationBarData + private let navigationBarLinks: [NavigationBar.Link] private let showTitle: Bool @@ -19,11 +19,11 @@ final class PostListPageGenerator { /// The url of the page, excluding the extension private let pageUrlPrefix: String - init(language: ContentLanguage, content: Content, imageGenerator: ImageGenerator, navigationBarData: NavigationBarData, showTitle: Bool, pageTitle: String, pageDescription: String, pageUrlPrefix: String) { + init(language: ContentLanguage, content: Content, imageGenerator: ImageGenerator, navigationBarLinks: [NavigationBar.Link], showTitle: Bool, pageTitle: String, pageDescription: String, pageUrlPrefix: String) { self.language = language self.content = content self.imageGenerator = imageGenerator - self.navigationBarData = navigationBarData + self.navigationBarLinks = navigationBarLinks self.showTitle = showTitle self.pageTitle = pageTitle self.pageDescription = pageDescription @@ -49,30 +49,30 @@ final class PostListPageGenerator { let startIndex = (pageIndex - 1) * postsPerPage let endIndex = min(pageIndex * postsPerPage, totalCount) let postsOnPage = posts[startIndex..<endIndex] - guard createPostFeedPage(pageIndex, pageCount: numberOfPages, posts: postsOnPage, bar: navigationBarData) else { + guard createPostFeedPage(pageIndex, pageCount: numberOfPages, posts: postsOnPage, bar: navigationBarLinks) else { return false } } return true } - private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>, bar: NavigationBarData) -> Bool { + private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>, bar: [NavigationBar.Link]) -> Bool { let posts: [FeedEntryData] = posts.map { post in let localized: LocalizedPost = post.localized(in: language) let linkUrl = post.linkedPage.map { FeedEntryData.Link( - url: content.pageLink($0, language: language), + url: content.absoluteUrlToPage($0, language: language), text: language == .english ? "View" : "Anzeigen") // TODO: Add to settings } let tags: [FeedEntryData.Tag] = post.tags.filter { $0.isVisible }.map { tag in .init(name: tag.localized(in: language).name, - url: content.tagLink(tag, language: language)) + url: content.absoluteUrlToTag(tag, language: language)) } return FeedEntryData( - entryId: "\(post.id)", + entryId: post.id, title: localized.title, textAboveTitle: post.dateText(in: language), link: linkUrl, @@ -86,7 +86,7 @@ final class PostListPageGenerator { title: pageTitle, showTitle: showTitle, description: pageDescription, - navigationBarData: bar, + navigationBarLinks: bar, pageNumber: pageIndex, totalPages: pageCount, posts: posts) @@ -104,7 +104,7 @@ final class PostListPageGenerator { maxWidth: mainContentMaximumWidth, maxHeight: mainContentMaximumWidth) return .init( - rawImagePath: content.pathToImage(image), + rawImagePath: content.absoluteUrlToFile(image), width: Int(mainContentMaximumWidth), height: Int(mainContentMaximumWidth), altText: image.getDescription(for: language)) diff --git a/CHDataManagement/Generator/RequiredHeaders.swift b/CHDataManagement/Generator/RequiredHeaders.swift new file mode 100644 index 0000000..7841b0b --- /dev/null +++ b/CHDataManagement/Generator/RequiredHeaders.swift @@ -0,0 +1,16 @@ + +enum HeaderFile: String { + + case codeHightlighting = "highlight.min.js" + + case modelViewer = "model-viewer.min.js" + + var asModule: Bool { + switch self { + case .codeHightlighting: return false + case .modelViewer: return true + } + } +} + +typealias RequiredHeaders = Set<HeaderFile> diff --git a/CHDataManagement/Generator/ShorthandMarkdownKey.swift b/CHDataManagement/Generator/ShorthandMarkdownKey.swift index 9cc0f31..80b019c 100644 --- a/CHDataManagement/Generator/ShorthandMarkdownKey.swift +++ b/CHDataManagement/Generator/ShorthandMarkdownKey.swift @@ -44,8 +44,16 @@ enum ShorthandMarkdownKey: String { /// Format: `![external](<<url>;<text>...>` case externalLink = "external" + /// A large button to a git/github page + /// Format: `![git](<<url>;<text>...>` + case gitLink = "git" + /// Additional HTML code include verbatim into the page. /// Format: `![html](<fileId>)` case includedHtml = "html" + /// SVG Image showing only a part of the image + /// Format `![svg](<fileId>;` + case svg + } diff --git a/CHDataManagement/Main/MainView.swift b/CHDataManagement/Main/MainView.swift index 577e32b..c3a175e 100644 --- a/CHDataManagement/Main/MainView.swift +++ b/CHDataManagement/Main/MainView.swift @@ -4,6 +4,11 @@ import SFSafeSymbols #warning("Allow selection of pages as navigation bar items") #warning("Transfer images of posts to other language") #warning("Show tag selection view for pages") +#warning("Button to replace files") +#warning("Add external pages") +#warning("Convert statistics into key-value pairs") +#warning("Replace links to files inside pages when id changes") +#warning("Calculate file sizes") @main struct MainView: App { @@ -70,7 +75,7 @@ struct MainView: App { case .files: SelectedContentView<FileContentView>(selected: $selectedFile) case .generation: - GenerationContentView() + GenerationContentView(selected: $selectedSection) } } diff --git a/CHDataManagement/Model/Content+Generation.swift b/CHDataManagement/Model/Content+Generation.swift index 55961ed..b5aaa04 100644 --- a/CHDataManagement/Model/Content+Generation.swift +++ b/CHDataManagement/Model/Content+Generation.swift @@ -1,56 +1,47 @@ extension Content { - #warning("Get tag url prefix from settings") - func tagLink(_ tag: Tag, language: ContentLanguage) -> String { - "/tags/\(tag.localized(in: language).urlComponent).html" + private func makeCleanAbsolutePath(_ path: String) -> String { + ("/" + path).replacingOccurrences(of: "//", with: "/") } - func pageLink(_ page: Page, language: ContentLanguage) -> String { + private func pathPrefix(for file: FileResource) -> String { + switch file.type { + case .image: return settings.paths.imagesOutputFolderPath + case .video: return settings.paths.videosOutputFolderPath + default: return settings.paths.filesOutputFolderPath + } + } + + // MARK: Paths to items + + func absoluteUrlPrefixForTag(_ tag: Tag, language: ContentLanguage) -> String { + makeCleanAbsolutePath(settings.paths.tagsOutputFolderPath + "/" + tag.localized(in: language).urlComponent) + } + + func absoluteUrlToTag(_ tag: Tag, language: ContentLanguage) -> String { + absoluteUrlPrefixForTag(tag, language: language) + ".html" + } + + func absoluteUrlToPage(_ page: Page, language: ContentLanguage) -> String { // TODO: Record link to trace connections between pages - var prefix = settings.pages.pageUrlPrefix - if !prefix.hasPrefix("/") { - prefix = "/" + prefix - } - if !prefix.hasSuffix("/") { - prefix.append("/") - } - - return prefix + page.localized(in: language).urlString + makeCleanAbsolutePath(settings.pages.pageUrlPrefix + "/" + page.localized(in: language).urlString) } + /** + Get the url path to a file in the output folder. + The result is an absolute path from the output folder for use in HTML. + */ + func absoluteUrlToFile(_ file: FileResource) -> String { + let path = pathPrefix(for: file) + "/" + file.id + return makeCleanAbsolutePath(path) + } + + // MARK: Find items by id + func page(_ pageId: String) -> Page? { pages.first { $0.id == pageId } } - func pageLink(pageId: String, language: ContentLanguage) -> String? { - guard let page = pages.first(where: { $0.id == pageId }) else { - // TODO: Note missing link - print("Missing page \(pageId) linked") - return nil - } - return pageLink(page, language: language) - } - - func pathToFile(_ fileId: String) -> String? { - guard let file = file(id: fileId) else { - return nil - } - switch file.type { - case .image: return pathToImage(file) - case .video: return pathToVideo(file) - default: return pathToFile(file) - } - } - - func pathToFile(_ file: FileResource) -> String { - #warning("Add files path to settings") - return "/files/\(file.id)" - } - - func pathToImage(_ image: FileResource) -> String { - return "/images/\(image.id)" - } - func image(_ imageId: String) -> FileResource? { files.first { $0.id == imageId && $0.type.isImage } } @@ -59,19 +50,11 @@ extension Content { files.first { $0.id == videoId && $0.type.isVideo } } - func pathToVideo(_ videoId: String) -> String? { - guard let video = video(videoId) else { - return nil - } - return pathToVideo(video) - } - - func pathToVideo(_ video: FileResource) -> String { - "/videos/\(video.id)" - } - - func file(id: String) -> FileResource? { files.first { $0.id == id } } + + func tag(_ tagId: String) -> Tag? { + tags.first { $0.id == tagId } + } } diff --git a/CHDataManagement/Model/Content+Load.swift b/CHDataManagement/Model/Content+Load.swift index 8a10fe5..2d7919a 100644 --- a/CHDataManagement/Model/Content+Load.swift +++ b/CHDataManagement/Model/Content+Load.swift @@ -38,14 +38,6 @@ extension Content { linkPreviewDescription: page.linkPreviewDescription) } - private func convert(_ settings: LocalizedSettingsFile) -> LocalizedSettings { - .init(navigationBarIconDescription: settings.navigationBarIconDescription, - posts: .init( - title: settings.posts.feedTitle, - description: settings.posts.feedDescription, - feedUrlPrefix: settings.posts.feedUrlPrefix)) - } - func loadFromDisk() throws { let storage = Storage(baseFolder: URL(filePath: contentPath)) @@ -118,26 +110,23 @@ extension Content { private func makeSettings(_ settings: SettingsFile, tags: [String : Tag]) -> Settings { - let navigationBar = NavigationBarSettings( - iconPath: settings.navigationBar.navigationIconPath, - tags: settings.navigationBar.navigationTags.map { tags[$0]! }) + let navigationTags = settings.navigationTags.map { tags[$0]! } let posts = PostSettings( postsPerPage: settings.posts.postsPerPage, contentWidth: settings.posts.contentWidth) - let pages = PageSettings( - pageUrlPrefix: settings.pages.pageUrlPrefix, - contentWidth: settings.pages.contentWidth, - largeImageWidth: settings.pages.largeImageWidth) + let pages = PageSettings(file: settings.pages) + + let paths = PathSettings(file: settings.paths) return Settings( - outputDirectoryPath: settings.outputDirectoryPath, - navigationBar: navigationBar, + paths: paths, + navigationTags: navigationTags, posts: posts, pages: pages, - german: convert(settings.german), - english: convert(settings.english)) + german: .init(file: settings.german), + english: .init(file: settings.english)) } private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], images: [String : FileResource]) -> [String : Page] { diff --git a/CHDataManagement/Model/Content+Save.swift b/CHDataManagement/Model/Content+Save.swift index dda1393..5556d64 100644 --- a/CHDataManagement/Model/Content+Save.swift +++ b/CHDataManagement/Model/Content+Save.swift @@ -124,20 +124,12 @@ private extension LocalizedTag { } } -private extension NavigationBarSettings { - - var file: NavigationBarSettingsFile { - .init(navigationIconPath: iconPath, - navigationTags: tags.map { $0.id }) - } -} - extension Settings { var file: SettingsFile { .init( - outputDirectoryPath: outputDirectoryPath, - navigationBar: navigationBar.file, + paths: paths.file, + navigationTags: navigationTags.map { $0.id }, posts: posts.file, pages: pages.file, german: german.file, @@ -145,6 +137,18 @@ extension Settings { } } +private extension PathSettings { + + var file: PathSettingsFile { + .init(outputDirectoryPath: outputDirectoryPath, + pagesOutputFolderPath: pagesOutputFolderPath, + imagesOutputFolderPath: imagesOutputFolderPath, + filesOutputFolderPath: filesOutputFolderPath, + videosOutputFolderPath: videosOutputFolderPath, + tagsOutputFolderPath: tagsOutputFolderPath) + } +} + private extension PostSettings { var file: PostSettingsFile { @@ -158,15 +162,9 @@ private extension PageSettings { var file: PageSettingsFile { .init(pageUrlPrefix: pageUrlPrefix, contentWidth: contentWidth, - largeImageWidth: largeImageWidth) - } -} - -private extension LocalizedSettings { - - var file: LocalizedSettingsFile { - .init(navigationBarIconDescription: navigationBarIconDescription, - posts: posts.file) + largeImageWidth: largeImageWidth, + pageLinkImageSize: pageLinkImageSize, + javascriptFilesPath: javascriptFilesPath) } } diff --git a/CHDataManagement/Model/Settings/LocalizedPostSettings.swift b/CHDataManagement/Model/Settings/LocalizedPostSettings.swift index 7104928..bc21c5f 100644 --- a/CHDataManagement/Model/Settings/LocalizedPostSettings.swift +++ b/CHDataManagement/Model/Settings/LocalizedPostSettings.swift @@ -16,4 +16,10 @@ final class LocalizedPostSettings: ObservableObject { self.description = description self.feedUrlPrefix = feedUrlPrefix } + + init(file: LocalizedPostSettingsFile) { + self.title = file.feedTitle + self.description = file.feedDescription + self.feedUrlPrefix = file.feedUrlPrefix + } } diff --git a/CHDataManagement/Model/Settings/LocalizedSettings.swift b/CHDataManagement/Model/Settings/LocalizedSettings.swift deleted file mode 100644 index c551fe9..0000000 --- a/CHDataManagement/Model/Settings/LocalizedSettings.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -final class LocalizedSettings: ObservableObject { - - @Published - var navigationBarIconDescription: String - - @Published - var posts: LocalizedPostSettings - - init(navigationBarIconDescription: String, posts: LocalizedPostSettings) { - self.navigationBarIconDescription = navigationBarIconDescription - self.posts = posts - } -} diff --git a/CHDataManagement/Model/Settings/NavigationBarSettings.swift b/CHDataManagement/Model/Settings/NavigationBarSettings.swift deleted file mode 100644 index 308a2fb..0000000 --- a/CHDataManagement/Model/Settings/NavigationBarSettings.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation - -final class NavigationBarSettings: ObservableObject { - - /// The path to the main icon in the navigation bar - @Published - var iconPath: String - - /// The tags to show in the navigation bar - @Published - var tags: [Tag] - - init(iconPath: String, tags: [Tag]) { - self.iconPath = iconPath - self.tags = tags - } -} diff --git a/CHDataManagement/Model/Settings/PageSettings.swift b/CHDataManagement/Model/Settings/PageSettings.swift index 0773766..7bc0b29 100644 --- a/CHDataManagement/Model/Settings/PageSettings.swift +++ b/CHDataManagement/Model/Settings/PageSettings.swift @@ -13,9 +13,17 @@ final class PageSettings: ObservableObject { @Published var largeImageWidth: Int - init(pageUrlPrefix: String, contentWidth: Int, largeImageWidth: Int) { - self.pageUrlPrefix = pageUrlPrefix - self.contentWidth = contentWidth - self.largeImageWidth = largeImageWidth + @Published + var pageLinkImageSize: Int + + @Published + var javascriptFilesPath: String + + init(file: PageSettingsFile) { + self.pageUrlPrefix = file.pageUrlPrefix + self.contentWidth = file.contentWidth + self.largeImageWidth = file.largeImageWidth + self.pageLinkImageSize = file.pageLinkImageSize + self.javascriptFilesPath = file.javascriptFilesPath } } diff --git a/CHDataManagement/Model/Settings/PathSettings.swift b/CHDataManagement/Model/Settings/PathSettings.swift new file mode 100644 index 0000000..2588cf6 --- /dev/null +++ b/CHDataManagement/Model/Settings/PathSettings.swift @@ -0,0 +1,31 @@ +import Foundation + +final class PathSettings: ObservableObject { + + @Published + var outputDirectoryPath: String + + @Published + var pagesOutputFolderPath: String + + @Published + var imagesOutputFolderPath: String + + @Published + var filesOutputFolderPath: String + + @Published + var videosOutputFolderPath: String + + @Published + var tagsOutputFolderPath: String + + init(file: PathSettingsFile) { + self.outputDirectoryPath = file.outputDirectoryPath + self.pagesOutputFolderPath = file.pagesOutputFolderPath + self.imagesOutputFolderPath = file.imagesOutputFolderPath + self.filesOutputFolderPath = file.filesOutputFolderPath + self.videosOutputFolderPath = file.videosOutputFolderPath + self.tagsOutputFolderPath = file.tagsOutputFolderPath + } +} diff --git a/CHDataManagement/Model/Settings/PostSettings.swift b/CHDataManagement/Model/Settings/PostSettings.swift index 3b2123b..53030c5 100644 --- a/CHDataManagement/Model/Settings/PostSettings.swift +++ b/CHDataManagement/Model/Settings/PostSettings.swift @@ -14,4 +14,9 @@ final class PostSettings: ObservableObject { self.postsPerPage = postsPerPage self.contentWidth = contentWidth } + + init(file: PostSettingsFile) { + self.postsPerPage = file.postsPerPage + self.contentWidth = file.contentWidth + } } diff --git a/CHDataManagement/Model/Settings/Settings.swift b/CHDataManagement/Model/Settings/Settings.swift index 2d445a9..eab50ad 100644 --- a/CHDataManagement/Model/Settings/Settings.swift +++ b/CHDataManagement/Model/Settings/Settings.swift @@ -3,10 +3,11 @@ import Foundation final class Settings: ObservableObject { @Published - var outputDirectoryPath: String + var paths: PathSettings + /// The tags to show in the navigation bar @Published - var navigationBar: NavigationBarSettings + var navigationTags: [Tag] @Published var posts: PostSettings @@ -15,24 +16,28 @@ final class Settings: ObservableObject { var pages: PageSettings @Published - var german: LocalizedSettings + var german: LocalizedPostSettings @Published - var english: LocalizedSettings + var english: LocalizedPostSettings - init(outputDirectoryPath: String, navigationBar: NavigationBarSettings, posts: PostSettings, pages: PageSettings, german: LocalizedSettings, english: LocalizedSettings) { - self.outputDirectoryPath = outputDirectoryPath - self.navigationBar = navigationBar + init(paths: PathSettings, navigationTags: [Tag], posts: PostSettings, pages: PageSettings, german: LocalizedPostSettings, english: LocalizedPostSettings) { + self.paths = paths + self.navigationTags = navigationTags self.posts = posts self.pages = pages self.german = german self.english = english } - func localized(in language: ContentLanguage) -> LocalizedSettings { + func localized(in language: ContentLanguage) -> LocalizedPostSettings { switch language { case .english: return english case .german: return german } } + + var outputDirectory: URL { + URL(fileURLWithPath: paths.outputDirectoryPath) + } } diff --git a/CHDataManagement/Model/Types/FileType.swift b/CHDataManagement/Model/Types/FileType.swift index 4ff9346..4764488 100644 --- a/CHDataManagement/Model/Types/FileType.swift +++ b/CHDataManagement/Model/Types/FileType.swift @@ -103,4 +103,14 @@ enum FileType { } return nil } + + var isSvg: Bool { + guard case .image(let imageFileType) = self else { + return false + } + guard case .svg = imageFileType else { + return false + } + return true + } } diff --git a/CHDataManagement/Page Elements/ContentElements/AdditionalPageHeaders.swift b/CHDataManagement/Page Elements/ContentElements/AdditionalPageHeaders.swift new file mode 100644 index 0000000..d1824af --- /dev/null +++ b/CHDataManagement/Page Elements/ContentElements/AdditionalPageHeaders.swift @@ -0,0 +1,14 @@ + +struct AdditionalPageHeaders { + + let headers: RequiredHeaders + + let assetPath: String + + var content: String { + headers.map { header in + let module = header.asModule ? " type='module'" : "" + return "<script\(module) src='\(assetPath)/\(header.rawValue)'></script>" + }.sorted().joined() + } +} diff --git a/CHDataManagement/Page Elements/ContentElements/ContentBox.swift b/CHDataManagement/Page Elements/ContentElements/ContentBox.swift new file mode 100644 index 0000000..ddcf88e --- /dev/null +++ b/CHDataManagement/Page Elements/ContentElements/ContentBox.swift @@ -0,0 +1,14 @@ + +struct ContentBox: HtmlProducer { + + let title: String + + let text: String + + func populate(_ result: inout String) { + result += "<div class='box'>" + result += "<span class='title'>\(title)</span>" + result += "<p>\(text)</p>" + result += "</div>" + } +} diff --git a/CHDataManagement/Page Elements/ContentElements/ModelViewer.swift b/CHDataManagement/Page Elements/ContentElements/ModelViewer.swift new file mode 100644 index 0000000..f7e07a3 --- /dev/null +++ b/CHDataManagement/Page Elements/ContentElements/ModelViewer.swift @@ -0,0 +1,11 @@ + +struct ModelViewer { + + let file: String + + let description: String + + var content: String { + "<model-viewer alt='\(description)' src='\(file)' ar shadow-intensity='1' camera-controls touch-action='pan-y'></model-viewer>" + } +} diff --git a/CHDataManagement/Page Elements/ContentElements/RelatedPageLink.swift b/CHDataManagement/Page Elements/ContentElements/RelatedPageLink.swift new file mode 100644 index 0000000..52e24b1 --- /dev/null +++ b/CHDataManagement/Page Elements/ContentElements/RelatedPageLink.swift @@ -0,0 +1,39 @@ + +struct RelatedPageLink { + + struct Image { + + let url: String + + let description: String + + let size: Int + } + + let title: String + + let description: String + + let url: String + + let image: Image? + + var content: String { + var result = "" + result += "<a href='\(url)' class='related-box-wrapper'>" + result += "<div class='related-box'>" + if let image { + result += WebsiteImage( + rawImagePath: image.url, + width: image.size, + height: image.size, + altText: image.description) + .content + } + result += "<div class='related-content'>" + result += "<h3>\(title)</h3>" + result += "<p>\(description)</p>" + result += "</div></div></a>" // Close related-box-wrapper, related-box + return result + } +} diff --git a/CHDataManagement/Page Elements/ContentElements/SvgImage.swift b/CHDataManagement/Page Elements/ContentElements/SvgImage.swift new file mode 100644 index 0000000..19b7f7d --- /dev/null +++ b/CHDataManagement/Page Elements/ContentElements/SvgImage.swift @@ -0,0 +1,41 @@ + +struct PartialSvgImage: HtmlProducer { + + let imagePath: String + + let altText: String + + let x: Int + + let y: Int + + let width: Int + + let height: Int + + private var aspectRatio: Double { + guard height > 1 else { + return 1 + } + return Double(width) / Double(height) + } + + func populate(_ result: inout String) { + result += "<span class='content-image svg-image'>" + result += "<img src='\(imagePath)#svgView(viewBox(\(x), \(y), \(width), \(height)))' loading='lazy' style='aspect-ratio:\(aspectRatio)' alt='\(altText)'/>" + result += "</span>" + } +} + +struct SvgImage: HtmlProducer { + + let imagePath: String + + let altText: String + + func populate(_ result: inout String) { + result += "<div class='content-image svg-image'>" + result += "<img src='\(imagePath)' loading='lazy' alt='\(altText)'/>" + result += "</div>" + } +} diff --git a/CHDataManagement/Page Elements/ImageGallery.swift b/CHDataManagement/Page Elements/ImageGallery.swift index 7f9b2e1..bcc524c 100644 --- a/CHDataManagement/Page Elements/ImageGallery.swift +++ b/CHDataManagement/Page Elements/ImageGallery.swift @@ -23,8 +23,9 @@ struct ImageGallery { result += "<div id='\(htmlSafeId)' class='swiper'><div class='swiper-wrapper'>" guard images.count > 1 else { + result += "<div class='swiper-slide'>" result += WebsiteImage(image: images[0]).content - result += "</div></div>" // Close swiper, swiper-wrapper + result += "</div></div></div>" // Close swiper-slide, swiper, swiper-wrapper return } diff --git a/CHDataManagement/Page Elements/NavigationBar.swift b/CHDataManagement/Page Elements/NavigationBar.swift index cba3e77..59f97a9 100644 --- a/CHDataManagement/Page Elements/NavigationBar.swift +++ b/CHDataManagement/Page Elements/NavigationBar.swift @@ -1,53 +1,36 @@ import Foundation -struct NavigationBarLink { - let text: String +struct NavigationBar: HtmlProducer { - let url: String -} + struct Link { + let text: String -struct NavigationBarData { - - let navigationIconPath: String - - let iconDescription: String - - let navigationItems: [NavigationBarLink] -} - - -struct NavigationBar { - - let data: NavigationBarData - - init(data: NavigationBarData) { - self.data = data + let url: String } - private var items: [NavigationBarLink] { - data.navigationItems + private let links: [Link] + + init(links: [Link]) { + self.links = links } - var content: String { - var result = "<nav class=\"navbar\"><div class=\"navbar-fade\"></div><div class=\"nav-center\">" - let middleIndex = items.count / 2 - let leftNavigationItems = items[..<middleIndex] - let rightNavigationItems = items[middleIndex...] + func populate(_ result: inout String) { + result += "<nav class='navbar'><div class='navbar-fade'></div><div class='nav-center'>" + let middleIndex = links.count / 2 + let leftNavigationItems = links[..<middleIndex] + let rightNavigationItems = links[middleIndex...] for item in leftNavigationItems { - result += "<a class=\"nav-animate\" href=\"\(item.url)\">\(item.text)</a>" + result += "<a class='nav-animate' href='\(item.url)'>\(item.text)</a>" } - result += "<a id=\"nav-image\" href=\"/\">" - result += "<img class=\"navbar-icon\" src=\"\(data.navigationIconPath)\" alt=\"\(data.iconDescription)\">" - result += "</a>" + result += "<a id='nav-image' href='/'><div class='icon-ch'></div></a>" for item in rightNavigationItems { - result += "<a class=\"nav-animate\" href=\"\(item.url)\">\(item.text)</a>" + result += "<a class='nav-animate' href='\(item.url)'>\(item.text)</a>" } result += "</div></nav>" // Close nav-center, navbar - return result } } diff --git a/CHDataManagement/Pages/ContentPage.swift b/CHDataManagement/Pages/ContentPage.swift index 4b250e4..a300f1c 100644 --- a/CHDataManagement/Pages/ContentPage.swift +++ b/CHDataManagement/Pages/ContentPage.swift @@ -14,27 +14,33 @@ struct ContentPage: HtmlProducer { private let tags: [FeedEntryData.Tag] - private let navigationBarData: NavigationBarData + private let navigationBarLinks: [NavigationBar.Link] private let pageContent: String - init(language: ContentLanguage, dateString: String, title: String, tags: [FeedEntryData.Tag], linkTitle: String, description: String, navigationBarData: NavigationBarData, pageContent: String) { + private let headers: String + + private let footers: String + + init(language: ContentLanguage, dateString: String, title: String, tags: [FeedEntryData.Tag], linkTitle: String, description: String, navigationBarLinks: [NavigationBar.Link], pageContent: String, headers: String, footers: [String]) { self.language = language self.dateString = dateString self.title = title self.tags = tags self.linkTitle = linkTitle self.description = description - self.navigationBarData = navigationBarData + self.navigationBarLinks = navigationBarLinks self.pageContent = pageContent + self.headers = headers + self.footers = footers.joined() } func populate(_ result: inout String) { // TODO: Add headers and footers from page content result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">" - result += PageHead(title: title, description: description, additionalHeaders: "").content + result += PageHead(title: title, description: description, additionalHeaders: headers).content result += "<body>" - result += NavigationBar(data: navigationBarData).content + result += NavigationBar(links: navigationBarLinks).content result += "<main><article>" result += "<div style=\"height: 70px;\"></div>" @@ -45,7 +51,7 @@ struct ContentPage: HtmlProducer { result += pageContent result += "</article></main>" - result += "" // TODO: Footer + result += footers result += "</body></html>" // Close content } diff --git a/CHDataManagement/Pages/GenericPage.swift b/CHDataManagement/Pages/GenericPage.swift index 22bd80f..5e9828f 100644 --- a/CHDataManagement/Pages/GenericPage.swift +++ b/CHDataManagement/Pages/GenericPage.swift @@ -8,7 +8,7 @@ struct GenericPage { let description: String - let data: NavigationBarData + let links: [NavigationBar.Link] let additionalHeaders: String @@ -16,11 +16,11 @@ struct GenericPage { let insertedContent: (inout String) -> Void - init(language: ContentLanguage, title: String, description: String, data: NavigationBarData, additionalHeaders: String, additionalFooter: String, insertedContent: @escaping (inout String) -> Void) { + init(language: ContentLanguage, title: String, description: String, links: [NavigationBar.Link], additionalHeaders: String, additionalFooter: String, insertedContent: @escaping (inout String) -> Void) { self.language = language self.title = title self.description = description - self.data = data + self.links = links self.additionalHeaders = additionalHeaders self.additionalFooter = additionalFooter self.insertedContent = insertedContent @@ -30,7 +30,7 @@ struct GenericPage { result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">" result += PageHead(title: title, description: description, additionalHeaders: additionalHeaders).content result += "<body>" - result += NavigationBar(data: data).content + result += NavigationBar(links: links).content result += "<div class=\"content\"><div style=\"height: 70px;\"></div>" insertedContent(&result) result += "</div>" diff --git a/CHDataManagement/Pages/PageInFeed.swift b/CHDataManagement/Pages/PageInFeed.swift index 6869b92..dc4b3d3 100644 --- a/CHDataManagement/Pages/PageInFeed.swift +++ b/CHDataManagement/Pages/PageInFeed.swift @@ -14,7 +14,7 @@ struct PageInFeed { let description: String - let navigationBarData: NavigationBarData + let navigationBarLinks: [NavigationBar.Link] let pageNumber: Int @@ -41,7 +41,7 @@ struct PageInFeed { language: language, title: title, description: description, - data: navigationBarData, + links: navigationBarLinks, additionalHeaders: headers, additionalFooter: footer) { content in if showTitle { diff --git a/CHDataManagement/Preview Content/WebsiteData+Mock.swift b/CHDataManagement/Preview Content/WebsiteData+Mock.swift index d9c92db..ad8a94f 100644 --- a/CHDataManagement/Preview Content/WebsiteData+Mock.swift +++ b/CHDataManagement/Preview Content/WebsiteData+Mock.swift @@ -3,38 +3,32 @@ import Foundation extension Settings { static let mock: Settings = .init( - outputDirectoryPath: "/some/path", - navigationBar: .init(iconPath: "/some/other/path", tags: []), - posts: .mock, - pages: .mock, + paths: .default, + navigationTags: [], + posts: .default, + pages: .default, german: .german, english: .english) } +extension PathSettings { + + static var `default`: PathSettings { + .init(file: .default) + } +} + extension PostSettings { - static var mock: PostSettings { - .init(postsPerPage: 20, contentWidth: 600) + static var `default`: PostSettings { + .init(file: .default) } } extension PageSettings { - static var mock: PageSettings { - .init(pageUrlPrefix: "pages", contentWidth: 600, largeImageWidth: 1200) - } -} - -extension LocalizedSettings { - - static var german: LocalizedSettings { - .init(navigationBarIconDescription: "Ein Symbol", - posts: .german) - } - - static var english: LocalizedSettings { - .init(navigationBarIconDescription: "An icon", - posts: .english) + static var `default`: PageSettings { + .init(file: .default) } } diff --git a/CHDataManagement/Storage/Model/Settings/LocalizedSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/LocalizedSettingsFile.swift deleted file mode 100644 index 5d9db6a..0000000 --- a/CHDataManagement/Storage/Model/Settings/LocalizedSettingsFile.swift +++ /dev/null @@ -1,20 +0,0 @@ - -struct LocalizedSettingsFile { - - let navigationBarIconDescription: String - - let posts: LocalizedPostSettingsFile - -} - -extension LocalizedSettingsFile: Codable { - -} - -extension LocalizedSettingsFile { - - static var `default`: LocalizedSettingsFile { - .init(navigationBarIconDescription: "An icon", - posts: .default) - } -} diff --git a/CHDataManagement/Storage/Model/Settings/NavigationBarSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/NavigationBarSettingsFile.swift deleted file mode 100644 index 042168d..0000000 --- a/CHDataManagement/Storage/Model/Settings/NavigationBarSettingsFile.swift +++ /dev/null @@ -1,19 +0,0 @@ - -struct NavigationBarSettingsFile { - - /// The path to the main icon in the navigation bar - let navigationIconPath: String - - /// The tags to show in the navigation bar - let navigationTags: [String] -} - -extension NavigationBarSettingsFile: Codable { } - -extension NavigationBarSettingsFile { - - static var `default`: NavigationBarSettingsFile { - .init(navigationIconPath: "/assets/icons/icon.svg", - navigationTags: []) - } -} diff --git a/CHDataManagement/Storage/Model/Settings/PageSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/PageSettingsFile.swift index 7bfd32d..9972bf8 100644 --- a/CHDataManagement/Storage/Model/Settings/PageSettingsFile.swift +++ b/CHDataManagement/Storage/Model/Settings/PageSettingsFile.swift @@ -6,6 +6,10 @@ struct PageSettingsFile { let contentWidth: Int let largeImageWidth: Int + + let pageLinkImageSize: Int + + let javascriptFilesPath: String } extension PageSettingsFile: Codable { @@ -17,6 +21,8 @@ extension PageSettingsFile { static var `default`: PageSettingsFile { .init(pageUrlPrefix: "page", contentWidth: 600, - largeImageWidth: 1200) + largeImageWidth: 1200, + pageLinkImageSize: 180, + javascriptFilesPath: "/assets/js") } } diff --git a/CHDataManagement/Storage/Model/Settings/PathSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/PathSettingsFile.swift new file mode 100644 index 0000000..8a554ce --- /dev/null +++ b/CHDataManagement/Storage/Model/Settings/PathSettingsFile.swift @@ -0,0 +1,42 @@ + +struct PathSettingsFile { + + let outputDirectoryPath: String + + let pagesOutputFolderPath: String + + let imagesOutputFolderPath: String + + let filesOutputFolderPath: String + + let videosOutputFolderPath: String + + let tagsOutputFolderPath: String + + init(outputDirectoryPath: String, pagesOutputFolderPath: String, imagesOutputFolderPath: String, filesOutputFolderPath: String, videosOutputFolderPath: String, tagsOutputFolderPath: String) { + self.outputDirectoryPath = outputDirectoryPath + self.pagesOutputFolderPath = pagesOutputFolderPath + self.imagesOutputFolderPath = imagesOutputFolderPath + self.filesOutputFolderPath = filesOutputFolderPath + self.videosOutputFolderPath = videosOutputFolderPath + self.tagsOutputFolderPath = tagsOutputFolderPath + } +} + +extension PathSettingsFile: Codable { + +} + +extension PathSettingsFile { + + static var `default`: PathSettingsFile { + PathSettingsFile( + outputDirectoryPath: "build", + pagesOutputFolderPath: "page", + imagesOutputFolderPath: "image", + filesOutputFolderPath: "file", + videosOutputFolderPath: "video", + tagsOutputFolderPath: "tag") + } + +} diff --git a/CHDataManagement/Storage/Model/Settings/SettingsFile.swift b/CHDataManagement/Storage/Model/Settings/SettingsFile.swift index 2349a55..284a381 100644 --- a/CHDataManagement/Storage/Model/Settings/SettingsFile.swift +++ b/CHDataManagement/Storage/Model/Settings/SettingsFile.swift @@ -2,18 +2,18 @@ import Foundation struct SettingsFile { - /// The file path to the output directory - let outputDirectoryPath: String + let paths: PathSettingsFile - let navigationBar: NavigationBarSettingsFile + /// The tags to show in the navigation bar + let navigationTags: [String] let posts: PostSettingsFile let pages: PageSettingsFile - let german: LocalizedSettingsFile + let german: LocalizedPostSettingsFile - let english: LocalizedSettingsFile + let english: LocalizedPostSettingsFile } extension SettingsFile: Codable { } @@ -22,8 +22,8 @@ extension SettingsFile { static var `default`: SettingsFile { .init( - outputDirectoryPath: "", - navigationBar: .default, + paths: .default, + navigationTags: [], posts: .default, pages: .default, german: .default, diff --git a/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift b/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift index 0157648..01769ac 100644 --- a/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift +++ b/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift @@ -72,6 +72,8 @@ struct LocalizedPageDetailView: View { .aspectRatio(contentMode: .fit) .frame(maxWidth: 400, maxHeight: 300) .cornerRadius(8) + Text(image.id) + .font(.headline) } Text("Link Preview Description") diff --git a/CHDataManagement/Views/Pages/PageContentResultsView.swift b/CHDataManagement/Views/Pages/PageContentResultsView.swift index 14f0750..7d077a4 100644 --- a/CHDataManagement/Views/Pages/PageContentResultsView.swift +++ b/CHDataManagement/Views/Pages/PageContentResultsView.swift @@ -93,11 +93,11 @@ struct PageContentResultsView: View { items: results.missingFiles.sorted()) .foregroundStyle(.red) } - if !results.warnings.isEmpty { + if !results.unknownCommands.isEmpty { TextWithPopup( symbol: .exclamationmarkTriangleFill, - text: "\(results.warnings.count) errors", - items: results.warnings.sorted()) + text: "\(results.unknownCommands.count) unknown commands", + items: results.unknownCommands.sorted()) .foregroundStyle(.red) } if !results.invalidCommandArguments.isEmpty { diff --git a/CHDataManagement/Views/Pages/PageDetailView.swift b/CHDataManagement/Views/Pages/PageDetailView.swift index bd98b4a..739a82b 100644 --- a/CHDataManagement/Views/Pages/PageDetailView.swift +++ b/CHDataManagement/Views/Pages/PageDetailView.swift @@ -109,11 +109,11 @@ struct PageDetailView: View { } private func generate() { - guard content.settings.outputDirectoryPath != "" else { + guard content.settings.paths.outputDirectoryPath != "" else { print("Invalid output path") return } - let url = URL(fileURLWithPath: content.settings.outputDirectoryPath) + let url = content.settings.outputDirectory guard FileManager.default.fileExists(atPath: url.path) else { print("Missing output folder") diff --git a/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift b/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift index d4df67e..0a82b9b 100644 --- a/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift +++ b/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift @@ -46,6 +46,8 @@ struct LocalizedPostDetailView: View { .aspectRatio(contentMode: .fit) .frame(maxWidth: 400, maxHeight: 300) .cornerRadius(8) + Text(image.id) + .font(.headline) } Text("Link Preview Description") diff --git a/CHDataManagement/Views/Settings/Content/PageSettingsContentView.swift b/CHDataManagement/Views/Settings/Content/PageSettingsContentView.swift new file mode 100644 index 0000000..643bace --- /dev/null +++ b/CHDataManagement/Views/Settings/Content/PageSettingsContentView.swift @@ -0,0 +1,346 @@ +import SwiftUI + + + +private struct PageIssue { + + let id: Int + + let page: Page + + let language: ContentLanguage + + let message: PageContentAnomaly + + init(page: Page, language: ContentLanguage, message: PageContentAnomaly) { + self.id = .random() + self.page = page + self.language = language + self.message = message + } + + var title: String { + page.localized(in: language).title + } +} + +extension PageIssue: Identifiable { + +} + +private struct FixSheet: View { + + @Binding + var isPresented: Bool + + @Binding + var message: String + + @Binding + var infoItems: [String] + + let action: () -> Void + + init(isPresented: Binding<Bool>, message: Binding<String>, infoItems: Binding<[String]>, action: @escaping () -> Void) { + self._isPresented = isPresented + self._message = message + self._infoItems = infoItems + self.action = action + } + + var body: some View { + VStack { + Text("Fix issue") + .font(.headline) + Text(message) + .font(.body) + List { + ForEach(infoItems, id: \.self) { item in + Text(item) + } + } + HStack { + Button("Fix", action: { + isPresented = false + action() + }) + Button("Cancel", action: { isPresented = false }) + } + } + .frame(minHeight: 200) + .padding() + } +} + +private struct ErrorSheet: View { + @Binding + var isPresented: Bool + + @Binding + var message: String + + + var body: some View { + VStack { + Text("Error") + .font(.headline) + Text(message) + Button("Dismiss", action: { isPresented = false }) + } + } +} + +struct PageSettingsContentView: View { + + @EnvironmentObject + private var content: Content + + @State + private var isCheckingPages: Bool = false + + + @State + private var issues: [PageIssue] = [] + + @State + private var message: String = "No fix available" + + @State + private var infoItems: [String] = ["No items set"] + + @State + private var fixAction: () -> () = { + print("No fix action defined") + } + + @State + private var showFixActionSheet: Bool = false + + @State + private var errorMessage: String = "" + + @State + private var showErrorAlert: Bool = false + + var body: some View { + VStack(alignment: .leading) { + HStack { + Button("Check pages", action: checkAllPagesForErrors) + .disabled(isCheckingPages) + Button("Fix all", action: applyAllEasyFixes) + if isCheckingPages { + ProgressView() + .progressViewStyle(.circular) + .frame(height: 20) + } + } + Text("\(issues.count) Issues") + .font(.headline) + List(issues) { issue in + HStack { + Button("Attempt Fix", action: { attemptFix(issue: issue) }) + VStack(alignment: .leading) { + Text(issue.message.description) + Text("\(issue.title) (\(issue.language.rawValue.uppercased()))") + .font(.caption) + } + } + } + } + .padding() + .sheet(isPresented: $showFixActionSheet) { + FixSheet(isPresented: $showFixActionSheet, + message: $message, + infoItems: $infoItems) { + fixAction() + resetFixSheet() + } + } + .sheet(isPresented: $showErrorAlert) { + ErrorSheet(isPresented: $showErrorAlert, message: $errorMessage) + } + } + + private func checkAllPagesForErrors() { + guard !isCheckingPages else { + return + } + isCheckingPages = true + issues = [] + DispatchQueue.global(qos: .userInitiated).async { + for language in ContentLanguage.allCases { + let parser = PageContentParser( + content: content, + language: language) + for page in content.pages { + analyze(page: page, parser: parser) + } + } + + DispatchQueue.main.async { + self.isCheckingPages = false + } + } + } + + private func analyze(page: Page, parser: PageContentParser) { + parser.reset() + do { + let rawPageContent = try content.storage.pageContent(for: page.id, language: parser.language) + _ = parser.generatePage(from: rawPageContent) + let results = parser.results.convertedWarnings.map { + PageIssue(page: page, language: parser.language, message: $0) + } + DispatchQueue.main.async { + issues = results + issues + } + } catch { + let message = PageContentAnomaly.failedToLoadContent(error) + let error = PageIssue(page: page, language: parser.language, message: message) + DispatchQueue.main.async { + issues.insert(error, at: 0) + } + } + } + + private func applyAllEasyFixes() { + issues.forEach { issue in + switch issue.message { + case .missingFile(let file): + fix(missingFile: file, in: issue.page, language: issue.language, ask: false) + case .unknownCommand(let string): + fixUnknownCommand(string, in: issue.page, language: issue.language) + default: + return + } + } + } + + private func attemptFix(issue: PageIssue) { + switch issue.message { + case .failedToLoadContent: + show(error: "No fix available for read errors") + case .missingFile(let string): + fix(missingFile: string, in: issue.page, language: issue.language) + case .missingPage(let string): + show(error: "No fix available for missing page \(string)") + case .unknownCommand(let string): + fixUnknownCommand(string, in: issue.page, language: issue.language) + case .invalidCommandArguments(let command, let arguments): + show(error: "No fix available for invalid arguments to command \(command) (\(arguments))") + case .missingTag(let string): + show(error: "No fix available for missing tag \(string)") + } + } + + private func fix(missingFile: String, in page: Page, language: ContentLanguage, ask: Bool = true) { + print("Fixing missing file \(missingFile)") + let fileId = page.id + "-" + missingFile + if let file = content.file(id: fileId) { + replace(missingFile, with: file.id, in: page, language: language) + // Remove all errors of the page, and generate them new + recalculate(page: page, language: language) + return + } + guard ask else { + return + } + let partialMatches = content.files.filter { $0.id.contains(missingFile) } + guard partialMatches.count == 1 else { + show(error: "Found \(partialMatches.count) matches for file \(missingFile): \(partialMatches.map { $0.id })") + return + } + let file = partialMatches[0] + + // Ask to fix partially matching file + let occurences = findOccurences(of: missingFile, in: page, language: language) + message = "Found file '\(file.id)' to match \(missingFile) on page '\(page.localized(in: language).title)'. Do you want to replace it?" + infoItems = occurences + fixAction = { + replace(missingFile, with: file.id, in: page, language: language) + // Remove all errors of the page, and generate them new + recalculate(page: page, language: language) + } + DispatchQueue.main.async { + showFixActionSheet = true + } + } + + private func recalculate(page: Page, language: ContentLanguage) { + let remaining = issues.filter { + $0.language != language || $0.page.id != page.id + } + DispatchQueue.main.async { + self.issues = remaining + self.isCheckingPages = true + DispatchQueue.global(qos: .userInitiated).async { + let parser = PageContentParser(content: content, language: language) + self.analyze(page: page, parser: parser) + self.isCheckingPages = false + } + } + } + + private func resetFixSheet() { + DispatchQueue.main.async { + self.message = "No fix available" + self.fixAction = { print("No fix action defined") } + self.infoItems = ["No items set"] + } + } + + private func show(error: String) { + DispatchQueue.main.async { + errorMessage = error + showErrorAlert = true + } + } + + private func findMatchingFile(with missingFile: String, in page: Page) -> FileResource? { + let fileId = page.id + "-" + missingFile + if let file = content.file(id: fileId) { + return file + } + let partialMatches = content.files.filter { $0.id.contains(missingFile) } + if partialMatches.count == 1 { + return partialMatches[0] + } + show(error: "Found \(partialMatches.count) matches for file \(missingFile): \(partialMatches.map { $0.id })") + return nil + } + + private func findOccurences(of searchString: String, in page: Page, language: ContentLanguage) -> [String] { + let parts: [String] + do { + parts = try content.storage.pageContent(for: page.id, language: language) + .components(separatedBy: searchString) + } catch { + show(error: "Failed to get page content to find occurences: \(error.localizedDescription)") + return [] + } + + var occurrences: [String] = [] + for index in parts.indices.dropLast() { + let start = parts[index].suffix(10) + let end = parts[index+1].prefix(10) + let full = "...\(start)\(searchString)\(end)...".replacingOccurrences(of: "\n", with: "\\n") + occurrences.append(full) + } + return occurrences + } + + private func replace(_ oldString: String, with newString: String, in page: Page, language: ContentLanguage) { + do { + let pageContent = try content.storage.pageContent(for: page.id, language: language) + .replacingOccurrences(of: oldString, with: newString) + try content.storage.save(pageContent: pageContent, for: page.id, language: language) + print("Replaced \(oldString) with \(newString) in page \(page.id) (\(language))") + } catch { + print("Failed to replace in page \(page.id) (\(language)): \(error)") + } + } + + private func fixUnknownCommand(_ string: String, in page: Page, language: ContentLanguage) { + show(error: "No fix available for command '\(string)'") + } +} diff --git a/CHDataManagement/Views/Settings/GenerationContentView.swift b/CHDataManagement/Views/Settings/GenerationContentView.swift index 731fec6..ef00e47 100644 --- a/CHDataManagement/Views/Settings/GenerationContentView.swift +++ b/CHDataManagement/Views/Settings/GenerationContentView.swift @@ -8,6 +8,13 @@ struct GenerationContentView: View { @EnvironmentObject private var content: Content + @Binding + private var selectedSection: SettingsSection + + init(selected: Binding<SettingsSection>) { + self._selectedSection = selected + } + @State private var isGeneratingWebsite = false @@ -15,6 +22,16 @@ struct GenerationContentView: View { private var generatorText: String = "" var body: some View { + switch selectedSection { + case .folders, .navigationBar, .postFeed: + generationView + case .pages: + PageSettingsContentView() + } + } + + @ViewBuilder + private var generationView: some View { ScrollView { VStack(alignment: .leading) { Text("Website Generation") @@ -42,11 +59,11 @@ struct GenerationContentView: View { } private func generateFeed() { - guard content.settings.outputDirectoryPath != "" else { + guard content.settings.paths.outputDirectoryPath != "" else { print("Invalid output path") return } - let url = URL(fileURLWithPath: content.settings.outputDirectoryPath) + let url = content.settings.outputDirectory guard FileManager.default.fileExists(atPath: url.path) else { print("Missing output folder") @@ -71,7 +88,7 @@ struct GenerationContentView: View { } #Preview { - GenerationContentView() + GenerationContentView(selected: .constant(.folders)) .environmentObject(Content.mock) .padding() } diff --git a/CHDataManagement/Views/Settings/GenerationDetailView.swift b/CHDataManagement/Views/Settings/GenerationDetailView.swift index ffcf01a..4837b50 100644 --- a/CHDataManagement/Views/Settings/GenerationDetailView.swift +++ b/CHDataManagement/Views/Settings/GenerationDetailView.swift @@ -10,13 +10,13 @@ struct GenerationDetailView: View { //case .generation: // GenerationSettingsView() case .folders: - FolderSettingsView() + PathSettingsView() case .navigationBar: NavigationBarSettingsView() case .postFeed: PostFeedSettingsView() case .pages: - PageSettingsView() + PageSettingsDetailView() } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) diff --git a/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift b/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift index d7e52ed..b6369b7 100644 --- a/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift +++ b/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift @@ -1,17 +1,5 @@ import SwiftUI -private struct IconDescriptionView: View { - - @ObservedObject - var settings: LocalizedSettings - - var body: some View { - TextField("", text: $settings.navigationBarIconDescription) - .textFieldStyle(.roundedBorder) - .frame(maxWidth: 300) - } -} - struct NavigationBarSettingsView: View { @Environment(\.language) @@ -33,25 +21,10 @@ struct NavigationBarSettingsView: View { .foregroundStyle(.secondary) .padding(.bottom, 30) - Text("Icon Path") - .font(.headline) - TextField("", text: $content.settings.navigationBar.iconPath) - .textFieldStyle(.roundedBorder) - Text("Specify the path to the icon file with regard to the final website folder.") - .foregroundStyle(.secondary) - .padding(.bottom, 30) - - Text("Icon Description") - .font(.headline) - IconDescriptionView(settings: content.settings.localized(in: language)) - Text("Provide a description of the icon for screen readers.") - .foregroundStyle(.secondary) - .padding(.bottom) - Text("Visible Tags") .font(.headline) FlowHStack { - ForEach(content.settings.navigationBar.tags, id: \.id) { tag in + ForEach(content.settings.navigationTags) { tag in TagView(text: tag.localized(in: language).name) .foregroundStyle(.white) } @@ -74,7 +47,7 @@ struct NavigationBarSettingsView: View { .sheet(isPresented: $showTagPicker) { TagSelectionView( presented: $showTagPicker, - selected: $content.settings.navigationBar.tags, + selected: $content.settings.navigationTags, tags: $content.tags) } } diff --git a/CHDataManagement/Views/Settings/PageSettingsView.swift b/CHDataManagement/Views/Settings/PageSettingsDetailView.swift similarity index 64% rename from CHDataManagement/Views/Settings/PageSettingsView.swift rename to CHDataManagement/Views/Settings/PageSettingsDetailView.swift index 03f04b1..b30fe19 100644 --- a/CHDataManagement/Views/Settings/PageSettingsView.swift +++ b/CHDataManagement/Views/Settings/PageSettingsDetailView.swift @@ -1,6 +1,6 @@ import SwiftUI -struct PageSettingsView: View { +struct PageSettingsDetailView: View { @Environment(\.language) private var language @@ -25,7 +25,7 @@ struct PageSettingsView: View { .foregroundStyle(.secondary) .padding(.bottom) - Text("Image Width") + Text("Fullscreen Image Width") .font(.headline) IntegerField("", number: $content.settings.pages.largeImageWidth) .textFieldStyle(.roundedBorder) @@ -33,6 +33,14 @@ struct PageSettingsView: View { .foregroundStyle(.secondary) .padding(.bottom) + Text("Page Link Image Width") + .font(.headline) + IntegerField("", number: $content.settings.pages.pageLinkImageSize) + .textFieldStyle(.roundedBorder) + Text("The maximum width of images diplayed as thumbnails on page links") + .foregroundStyle(.secondary) + .padding(.bottom) + Text("Page URL Prefix") .font(.headline) TextField("", text: $content.settings.pages.pageUrlPrefix) @@ -40,6 +48,14 @@ struct PageSettingsView: View { Text("The URL prefix used for the links to pages") .foregroundStyle(.secondary) .padding(.bottom) + + Text("Javascript Files Path") + .font(.headline) + TextField("", text: $content.settings.pages.javascriptFilesPath) + .textFieldStyle(.roundedBorder) + Text("The path to the javascript files in the output folder") + .foregroundStyle(.secondary) + .padding(.bottom) } } } @@ -47,7 +63,7 @@ struct PageSettingsView: View { #Preview { - PageSettingsView() + PageSettingsDetailView() .environmentObject(Content.mock) .padding() } diff --git a/CHDataManagement/Views/Settings/FolderSettingsView.swift b/CHDataManagement/Views/Settings/PathSettingsView.swift similarity index 55% rename from CHDataManagement/Views/Settings/FolderSettingsView.swift rename to CHDataManagement/Views/Settings/PathSettingsView.swift index 21dbb6f..ad91814 100644 --- a/CHDataManagement/Views/Settings/FolderSettingsView.swift +++ b/CHDataManagement/Views/Settings/PathSettingsView.swift @@ -1,6 +1,6 @@ import SwiftUI -struct FolderSettingsView: View { +struct PathSettingsView: View { @Environment(\.language) private var language @@ -38,13 +38,53 @@ struct FolderSettingsView: View { Text("Output Folder") .font(.headline) .padding(.bottom, 1) - Text(content.settings.outputDirectoryPath) + Text(content.settings.paths.outputDirectoryPath) Button(action: selectOutputFolder) { Text("Select folder") } Text("The folder where the generated website is stored") .foregroundStyle(.secondary) .padding(.bottom) + + Text("Pages output folder") + .font(.headline) + TextField("", text: $content.settings.paths.pagesOutputFolderPath) + .textFieldStyle(.roundedBorder) + Text("The path in the output folder where the generated pages are stored") + .foregroundStyle(.secondary) + .padding(.bottom) + + Text("Tags output folder") + .font(.headline) + TextField("", text: $content.settings.paths.tagsOutputFolderPath) + .textFieldStyle(.roundedBorder) + Text("The path in the output folder where the generated tag pages are stored") + .foregroundStyle(.secondary) + .padding(.bottom) + + Text("Files output folder") + .font(.headline) + TextField("", text: $content.settings.paths.filesOutputFolderPath) + .textFieldStyle(.roundedBorder) + Text("The path in the output folder where the copied files are stored") + .foregroundStyle(.secondary) + .padding(.bottom) + + Text("Images output folder") + .font(.headline) + TextField("", text: $content.settings.paths.imagesOutputFolderPath) + .textFieldStyle(.roundedBorder) + Text("The path in the output folder where the generated images are stored") + .foregroundStyle(.secondary) + .padding(.bottom) + + Text("Videos output folder") + .font(.headline) + TextField("", text: $content.settings.paths.videosOutputFolderPath) + .textFieldStyle(.roundedBorder) + Text("The path in the output folder where the generated videos are stored") + .foregroundStyle(.secondary) + .padding(.bottom) } } } @@ -64,7 +104,7 @@ struct FolderSettingsView: View { guard let url = savePanelUsingOpenPanel() else { return } - content.settings.outputDirectoryPath = url.path() + content.settings.paths.outputDirectoryPath = url.path() } private func savePanelUsingOpenPanel() -> URL? { @@ -91,7 +131,7 @@ struct FolderSettingsView: View { } #Preview { - FolderSettingsView() + PathSettingsView() .environmentObject(Content.mock) .padding() } diff --git a/CHDataManagement/Views/Settings/PostFeedSettingsView.swift b/CHDataManagement/Views/Settings/PostFeedSettingsView.swift index 971ecb0..50fa4fa 100644 --- a/CHDataManagement/Views/Settings/PostFeedSettingsView.swift +++ b/CHDataManagement/Views/Settings/PostFeedSettingsView.swift @@ -36,7 +36,7 @@ struct PostFeedSettingsView: View { .foregroundStyle(.secondary) .padding(.bottom) - LocalizedPostFeedSettingsView(settings: content.settings.localized(in: language).posts) + LocalizedPostFeedSettingsView(settings: content.settings.localized(in: language)) } } }