Add navigation settings, fix page generation

This commit is contained in:
Christoph Hagen 2025-01-02 11:56:51 +01:00
parent 922ba4ebe7
commit 4d4275e072
43 changed files with 921 additions and 581 deletions

View File

@ -85,7 +85,6 @@
E25DA5912D023A8400AEF16D /* IntegerField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5902D023A7E00AEF16D /* IntegerField.swift */; }; E25DA5912D023A8400AEF16D /* IntegerField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5902D023A7E00AEF16D /* IntegerField.swift */; };
E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5922D023B3600AEF16D /* PageSettingsFile.swift */; }; E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5922D023B3600AEF16D /* PageSettingsFile.swift */; };
E25DA5952D023BD100AEF16D /* PageSettingsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5942D023BCC00AEF16D /* PageSettingsDetailView.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 */; }; E25DA5992D02401E00AEF16D /* PageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5982D02401A00AEF16D /* PageGenerator.swift */; };
E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA59A2D024A2900AEF16D /* DateItem.swift */; }; E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA59A2D024A2900AEF16D /* DateItem.swift */; };
E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D311F2D0320E20051B7F4 /* ContentLabels.swift */; }; E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D311F2D0320E20051B7F4 /* ContentLabels.swift */; };
@ -196,6 +195,20 @@
E2FE0EF42D1D6D2E002963B7 /* GeneralIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF32D1D6D22002963B7 /* GeneralIcons.swift */; }; E2FE0EF42D1D6D2E002963B7 /* GeneralIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF32D1D6D22002963B7 /* GeneralIcons.swift */; };
E2FE0EF62D1D6DF1002963B7 /* Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF52D1D6DEE002963B7 /* Icon.swift */; }; E2FE0EF62D1D6DF1002963B7 /* Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF52D1D6DEE002963B7 /* Icon.swift */; };
E2FE0EF82D1D8110002963B7 /* IconCommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF72D1D810C002963B7 /* IconCommandProcessor.swift */; }; E2FE0EF82D1D8110002963B7 /* IconCommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF72D1D810C002963B7 /* IconCommandProcessor.swift */; };
E2FE0EFA2D25AFBA002963B7 /* PageHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF92D25AFB5002963B7 /* PageHeader.swift */; };
E2FE0EFC2D266D22002963B7 /* NavigationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EFB2D266D18002963B7 /* NavigationSettings.swift */; };
E2FE0EFE2D266DA5002963B7 /* NavigationSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EFD2D266DA1002963B7 /* NavigationSettingsFile.swift */; };
E2FE0F002D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EFF2D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift */; };
E2FE0F022D266FCB002963B7 /* LocalizedNavigationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F012D266FCB002963B7 /* LocalizedNavigationSettings.swift */; };
E2FE0F042D267206002963B7 /* LocalizedNavigationBarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F032D2671FC002963B7 /* LocalizedNavigationBarSettingsView.swift */; };
E2FE0F062D267350002963B7 /* TextFieldPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F052D26734E002963B7 /* TextFieldPropertyView.swift */; };
E2FE0F092D2689F0002963B7 /* TagPageGeneratorSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F082D2689F0002963B7 /* TagPageGeneratorSource.swift */; };
E2FE0F0B2D2689FF002963B7 /* FeedGeneratorSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F0A2D2689FF002963B7 /* FeedGeneratorSource.swift */; };
E2FE0F0D2D268A09002963B7 /* PostListPageGeneratorSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F0C2D268A09002963B7 /* PostListPageGeneratorSource.swift */; };
E2FE0F0F2D268D4F002963B7 /* BoxCommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F0E2D268D4B002963B7 /* BoxCommandProcessor.swift */; };
E2FE0F112D268E7E002963B7 /* PageCodeProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F102D268E78002963B7 /* PageCodeProcessor.swift */; };
E2FE0F152D26918F002963B7 /* PageHtmlProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F142D269188002963B7 /* PageHtmlProcessor.swift */; };
E2FE0F172D2698D5002963B7 /* LocalizedPageId.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F162D2698D5002963B7 /* LocalizedPageId.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@ -273,7 +286,6 @@
E25DA5902D023A7E00AEF16D /* IntegerField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerField.swift; sourceTree = "<group>"; }; E25DA5902D023A7E00AEF16D /* IntegerField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerField.swift; sourceTree = "<group>"; };
E25DA5922D023B3600AEF16D /* PageSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsFile.swift; sourceTree = "<group>"; }; E25DA5922D023B3600AEF16D /* PageSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsFile.swift; sourceTree = "<group>"; };
E25DA5942D023BCC00AEF16D /* PageSettingsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsDetailView.swift; sourceTree = "<group>"; }; E25DA5942D023BCC00AEF16D /* PageSettingsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsDetailView.swift; sourceTree = "<group>"; };
E25DA5962D023F9900AEF16D /* ContentPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPage.swift; sourceTree = "<group>"; };
E25DA5982D02401A00AEF16D /* PageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageGenerator.swift; sourceTree = "<group>"; }; E25DA5982D02401A00AEF16D /* PageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageGenerator.swift; sourceTree = "<group>"; };
E25DA59A2D024A2900AEF16D /* DateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateItem.swift; sourceTree = "<group>"; }; E25DA59A2D024A2900AEF16D /* DateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateItem.swift; sourceTree = "<group>"; };
E29D311F2D0320E20051B7F4 /* ContentLabels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLabels.swift; sourceTree = "<group>"; }; E29D311F2D0320E20051B7F4 /* ContentLabels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLabels.swift; sourceTree = "<group>"; };
@ -383,6 +395,20 @@
E2FE0EF32D1D6D22002963B7 /* GeneralIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralIcons.swift; sourceTree = "<group>"; }; E2FE0EF32D1D6D22002963B7 /* GeneralIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralIcons.swift; sourceTree = "<group>"; };
E2FE0EF52D1D6DEE002963B7 /* Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icon.swift; sourceTree = "<group>"; }; E2FE0EF52D1D6DEE002963B7 /* Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icon.swift; sourceTree = "<group>"; };
E2FE0EF72D1D810C002963B7 /* IconCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconCommandProcessor.swift; sourceTree = "<group>"; }; E2FE0EF72D1D810C002963B7 /* IconCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconCommandProcessor.swift; sourceTree = "<group>"; };
E2FE0EF92D25AFB5002963B7 /* PageHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHeader.swift; sourceTree = "<group>"; };
E2FE0EFB2D266D18002963B7 /* NavigationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSettings.swift; sourceTree = "<group>"; };
E2FE0EFD2D266DA1002963B7 /* NavigationSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSettingsFile.swift; sourceTree = "<group>"; };
E2FE0EFF2D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedNavigationSettingsFile.swift; sourceTree = "<group>"; };
E2FE0F012D266FCB002963B7 /* LocalizedNavigationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedNavigationSettings.swift; sourceTree = "<group>"; };
E2FE0F032D2671FC002963B7 /* LocalizedNavigationBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedNavigationBarSettingsView.swift; sourceTree = "<group>"; };
E2FE0F052D26734E002963B7 /* TextFieldPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldPropertyView.swift; sourceTree = "<group>"; };
E2FE0F082D2689F0002963B7 /* TagPageGeneratorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagPageGeneratorSource.swift; sourceTree = "<group>"; };
E2FE0F0A2D2689FF002963B7 /* FeedGeneratorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedGeneratorSource.swift; sourceTree = "<group>"; };
E2FE0F0C2D268A09002963B7 /* PostListPageGeneratorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListPageGeneratorSource.swift; sourceTree = "<group>"; };
E2FE0F0E2D268D4B002963B7 /* BoxCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxCommandProcessor.swift; sourceTree = "<group>"; };
E2FE0F102D268E78002963B7 /* PageCodeProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageCodeProcessor.swift; sourceTree = "<group>"; };
E2FE0F142D269188002963B7 /* PageHtmlProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHtmlProcessor.swift; sourceTree = "<group>"; };
E2FE0F162D2698D5002963B7 /* LocalizedPageId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageId.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -406,6 +432,7 @@
E229901A2D0E3F09009F8D77 /* Item */ = { E229901A2D0E3F09009F8D77 /* Item */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E2FE0F162D2698D5002963B7 /* LocalizedPageId.swift */,
E229902B2D0F6FC0009F8D77 /* ItemId.swift */, E229902B2D0F6FC0009F8D77 /* ItemId.swift */,
E229901D2D0E4362009F8D77 /* LocalizedItem.swift */, E229901D2D0E4362009F8D77 /* LocalizedItem.swift */,
E29D31A22D0CC98B0051B7F4 /* Item.swift */, E29D31A22D0CC98B0051B7F4 /* Item.swift */,
@ -431,6 +458,8 @@
E25DA5322D0041C400AEF16D /* Settings */ = { E25DA5322D0041C400AEF16D /* Settings */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E2FE0EFF2D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift */,
E2FE0EFD2D266DA1002963B7 /* NavigationSettingsFile.swift */,
E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */, E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */,
E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */, E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */,
E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */, E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */,
@ -443,10 +472,12 @@
E25DA53B2D0042EA00AEF16D /* Settings */ = { E25DA53B2D0042EA00AEF16D /* Settings */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E29D31952D0C18690051B7F4 /* PathSettings.swift */, E2FE0F012D266FCB002963B7 /* LocalizedNavigationSettings.swift */,
E25DA58E2D02368A00AEF16D /* PageSettings.swift */,
E25DA5402D00446700AEF16D /* PostSettings.swift */,
E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */, E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */,
E2FE0EFB2D266D18002963B7 /* NavigationSettings.swift */,
E25DA58E2D02368A00AEF16D /* PageSettings.swift */,
E29D31952D0C18690051B7F4 /* PathSettings.swift */,
E25DA5402D00446700AEF16D /* PostSettings.swift */,
E21850322CFAFA200090B18B /* Settings.swift */, E21850322CFAFA200090B18B /* Settings.swift */,
); );
path = Settings; path = Settings;
@ -455,6 +486,7 @@
E25DA5782D01C56200AEF16D /* Generator */ = { E25DA5782D01C56200AEF16D /* Generator */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E2FE0F072D2689DC002963B7 /* Post Lists */,
E29D31B62D0DAC030051B7F4 /* Page Content */, E29D31B62D0DAC030051B7F4 /* Page Content */,
E22990232D0EDBD0009F8D77 /* HeaderElement.swift */, E22990232D0EDBD0009F8D77 /* HeaderElement.swift */,
E29D31842D0AE8EE0051B7F4 /* KnownHeaderElement.swift */, E29D31842D0AE8EE0051B7F4 /* KnownHeaderElement.swift */,
@ -465,7 +497,6 @@
E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */, E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */,
E25DA5982D02401A00AEF16D /* PageGenerator.swift */, E25DA5982D02401A00AEF16D /* PageGenerator.swift */,
E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */, E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */,
E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */,
E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */, E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */,
E29D31252D0370A50051B7F4 /* VideoOption.swift */, E29D31252D0370A50051B7F4 /* VideoOption.swift */,
E29D318F2D0B34870051B7F4 /* GenerationAnomaly.swift */, E29D318F2D0B34870051B7F4 /* GenerationAnomaly.swift */,
@ -540,6 +571,9 @@
E29D31B62D0DAC030051B7F4 /* Page Content */ = { E29D31B62D0DAC030051B7F4 /* Page Content */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E2FE0F142D269188002963B7 /* PageHtmlProcessor.swift */,
E2FE0F102D268E78002963B7 /* PageCodeProcessor.swift */,
E2FE0F0E2D268D4B002963B7 /* BoxCommandProcessor.swift */,
E2FE0EF72D1D810C002963B7 /* IconCommandProcessor.swift */, E2FE0EF72D1D810C002963B7 /* IconCommandProcessor.swift */,
E2FE0EED2D1C22EF002963B7 /* InlineLinkProcessor.swift */, E2FE0EED2D1C22EF002963B7 /* InlineLinkProcessor.swift */,
E29D31BD2D0DB8560051B7F4 /* AudioPlayerCommand.swift */, E29D31BD2D0DB8560051B7F4 /* AudioPlayerCommand.swift */,
@ -583,6 +617,7 @@
E25DA5442D00952D00AEF16D /* SettingsSection.swift */, E25DA5442D00952D00AEF16D /* SettingsSection.swift */,
E2A21C352CB9A3D70060935B /* PathSettingsView.swift */, E2A21C352CB9A3D70060935B /* PathSettingsView.swift */,
E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */, E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */,
E2FE0F032D2671FC002963B7 /* LocalizedNavigationBarSettingsView.swift */,
E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */, E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */,
E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */, E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */,
E229901F2D0ECBD4009F8D77 /* TagOverviewDetailView.swift */, E229901F2D0ECBD4009F8D77 /* TagOverviewDetailView.swift */,
@ -593,27 +628,28 @@
E2A21C372CB9A4F10060935B /* Generic */ = { E2A21C372CB9A4F10060935B /* Generic */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E29D31622D06E95A0051B7F4 /* NavigationIcon.swift */,
E29D31332D03B5D30051B7F4 /* IconButton.swift */,
E29D312F2D03A2BD0051B7F4 /* DescriptionField.swift */,
E25DA5902D023A7E00AEF16D /* IntegerField.swift */,
E218501C2CEE6CB30090B18B /* VerticalCenter.swift */,
E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */,
E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */,
E2A21C0F2CB18B390060935B /* FlowHStack.swift */,
E22990292D0F5A10009F8D77 /* DetailTitle.swift */,
E22990392D0F7E44009F8D77 /* GenericPropertyView.swift */,
E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */,
E22990252D0F5822009F8D77 /* FilePropertyView.swift */,
E229902D2D0F7278009F8D77 /* IdPropertyView.swift */,
E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */, E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */,
E22990312D0F7678009F8D77 /* DatePropertyView.swift */, E22990312D0F7678009F8D77 /* DatePropertyView.swift */,
E22990332D0F77E4009F8D77 /* PagePropertyView.swift */, E29D312F2D03A2BD0051B7F4 /* DescriptionField.swift */,
E22990352D0F79CC009F8D77 /* OptionalStringPropertyView.swift */, E22990292D0F5A10009F8D77 /* DetailTitle.swift */,
E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */, E22990252D0F5822009F8D77 /* FilePropertyView.swift */,
E22990372D0F7B2C009F8D77 /* OptionalImagePropertyView.swift */, E2A21C0F2CB18B390060935B /* FlowHStack.swift */,
E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */,
E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */, E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */,
E22990392D0F7E44009F8D77 /* GenericPropertyView.swift */,
E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */,
E29D31332D03B5D30051B7F4 /* IconButton.swift */,
E229902D2D0F7278009F8D77 /* IdPropertyView.swift */,
E25DA5902D023A7E00AEF16D /* IntegerField.swift */,
E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */,
E29D31622D06E95A0051B7F4 /* NavigationIcon.swift */,
E22990372D0F7B2C009F8D77 /* OptionalImagePropertyView.swift */,
E22990352D0F79CC009F8D77 /* OptionalStringPropertyView.swift */,
E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */,
E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */,
E22990332D0F77E4009F8D77 /* PagePropertyView.swift */,
E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */,
E2FE0F052D26734E002963B7 /* TextFieldPropertyView.swift */,
E218501C2CEE6CB30090B18B /* VerticalCenter.swift */,
); );
path = Generic; path = Generic;
sourceTree = "<group>"; sourceTree = "<group>";
@ -686,15 +722,6 @@
path = Model; path = Model;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E2B85F3E2C4293FF0047CD0C /* Pages */ = {
isa = PBXGroup;
children = (
E25DA5962D023F9900AEF16D /* ContentPage.swift */,
E25DA51C2CFF135B00AEF16D /* GenericPage.swift */,
);
path = Pages;
sourceTree = "<group>";
};
E2B85F3F2C42946E0047CD0C /* Page Elements */ = { E2B85F3F2C42946E0047CD0C /* Page Elements */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -702,12 +729,14 @@
E25DA58C2D021BA000AEF16D /* WebsiteImage.swift */, E25DA58C2D021BA000AEF16D /* WebsiteImage.swift */,
E25DA58A2D020C9200AEF16D /* PageImage.swift */, E25DA58A2D020C9200AEF16D /* PageImage.swift */,
E25DA51E2CFF15C100AEF16D /* NavigationBar.swift */, E25DA51E2CFF15C100AEF16D /* NavigationBar.swift */,
E2FE0EF92D25AFB5002963B7 /* PageHeader.swift */,
E25DA51A2CFF08AF00AEF16D /* PostFeedPageNavigation.swift */, E25DA51A2CFF08AF00AEF16D /* PostFeedPageNavigation.swift */,
E2B85F422C4294F60047CD0C /* FeedEntry.swift */, E2B85F422C4294F60047CD0C /* FeedEntry.swift */,
E2A21C452CBAE2E50060935B /* FeedEntryContent.swift */, E2A21C452CBAE2E50060935B /* FeedEntryContent.swift */,
E2A21C272CB29B290060935B /* FeedEntryData.swift */, E2A21C272CB29B290060935B /* FeedEntryData.swift */,
E2B85F442C429ED60047CD0C /* ImageGallery.swift */, E2B85F442C429ED60047CD0C /* ImageGallery.swift */,
E2B85F402C4294790047CD0C /* PageHead.swift */, E2B85F402C4294790047CD0C /* PageHead.swift */,
E25DA51C2CFF135B00AEF16D /* GenericPage.swift */,
); );
path = "Page Elements"; path = "Page Elements";
sourceTree = "<group>"; sourceTree = "<group>";
@ -785,7 +814,6 @@
E2B85F392C428F020047CD0C /* Model */, E2B85F392C428F020047CD0C /* Model */,
E2B85F462C42C7CA0047CD0C /* Views */, E2B85F462C42C7CA0047CD0C /* Views */,
E2B85F3F2C42946E0047CD0C /* Page Elements */, E2B85F3F2C42946E0047CD0C /* Page Elements */,
E2B85F3E2C4293FF0047CD0C /* Pages */,
E2DD04792C276F32003BFF1F /* Assets.xcassets */, E2DD04792C276F32003BFF1F /* Assets.xcassets */,
E2DD047B2C276F32003BFF1F /* CHDataManagement.entitlements */, E2DD047B2C276F32003BFF1F /* CHDataManagement.entitlements */,
E2B85F552C4BD0AD0047CD0C /* Extensions */, E2B85F552C4BD0AD0047CD0C /* Extensions */,
@ -809,6 +837,17 @@
path = "Preview Content"; path = "Preview Content";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E2FE0F072D2689DC002963B7 /* Post Lists */ = {
isa = PBXGroup;
children = (
E2FE0F0C2D268A09002963B7 /* PostListPageGeneratorSource.swift */,
E2FE0F0A2D2689FF002963B7 /* FeedGeneratorSource.swift */,
E2FE0F082D2689F0002963B7 /* TagPageGeneratorSource.swift */,
E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */,
);
path = "Post Lists";
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -899,7 +938,9 @@
files = ( files = (
E29D31242D0366860051B7F4 /* TagList.swift in Sources */, E29D31242D0366860051B7F4 /* TagList.swift in Sources */,
E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */, E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */,
E2FE0EFE2D266DA5002963B7 /* NavigationSettingsFile.swift in Sources */,
E2FE0EE62D15A0B5002963B7 /* GenerationResults.swift in Sources */, E2FE0EE62D15A0B5002963B7 /* GenerationResults.swift in Sources */,
E2FE0F152D26918F002963B7 /* PageHtmlProcessor.swift in Sources */,
E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */, E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */,
E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */, E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */,
E229904C2D10BE5D009F8D77 /* InitialSetupView.swift in Sources */, E229904C2D10BE5D009F8D77 /* InitialSetupView.swift in Sources */,
@ -909,6 +950,7 @@
E29D313B2D04464A0051B7F4 /* LocalizedTagDetailView.swift in Sources */, E29D313B2D04464A0051B7F4 /* LocalizedTagDetailView.swift in Sources */,
E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */, E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */,
E29D31BA2D0DB5080051B7F4 /* LabelsCommand.swift in Sources */, E29D31BA2D0DB5080051B7F4 /* LabelsCommand.swift in Sources */,
E2FE0EFC2D266D22002963B7 /* NavigationSettings.swift in Sources */,
E29D31692D0702700051B7F4 /* TextFileContentView.swift in Sources */, E29D31692D0702700051B7F4 /* TextFileContentView.swift in Sources */,
E25DA5412D00446C00AEF16D /* PostSettings.swift in Sources */, E25DA5412D00446C00AEF16D /* PostSettings.swift in Sources */,
E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */, E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */,
@ -922,6 +964,7 @@
E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */, E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */,
E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */, E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */,
E21850172CEE55FC0090B18B /* FileType.swift in Sources */, E21850172CEE55FC0090B18B /* FileType.swift in Sources */,
E2FE0F042D267206002963B7 /* LocalizedNavigationBarSettingsView.swift in Sources */,
E29D31B12D0DA5510051B7F4 /* ContentIcon.swift in Sources */, E29D31B12D0DA5510051B7F4 /* ContentIcon.swift in Sources */,
E2A37D112CE537800000979F /* PageFile.swift in Sources */, E2A37D112CE537800000979F /* PageFile.swift in Sources */,
E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */, E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */,
@ -947,7 +990,6 @@
E25DA58D2D021BA400AEF16D /* WebsiteImage.swift in Sources */, E25DA58D2D021BA400AEF16D /* WebsiteImage.swift in Sources */,
E25DA5992D02401E00AEF16D /* PageGenerator.swift in Sources */, E25DA5992D02401E00AEF16D /* PageGenerator.swift in Sources */,
E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */, E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */,
E25DA5972D023F9F00AEF16D /* ContentPage.swift in Sources */,
E2B85F3D2C4293F80047CD0C /* FeedPageGenerator.swift in Sources */, E2B85F3D2C4293F80047CD0C /* FeedPageGenerator.swift in Sources */,
E229902C2D0F6FC6009F8D77 /* ItemId.swift in Sources */, E229902C2D0F6FC6009F8D77 /* ItemId.swift in Sources */,
E25DA5952D023BD100AEF16D /* PageSettingsDetailView.swift in Sources */, E25DA5952D023BD100AEF16D /* PageSettingsDetailView.swift in Sources */,
@ -962,6 +1004,7 @@
E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */, E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */,
E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */, E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */,
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */, E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */,
E2FE0F092D2689F0002963B7 /* TagPageGeneratorSource.swift in Sources */,
E22990302D0F75DE009F8D77 /* BoolPropertyView.swift in Sources */, E22990302D0F75DE009F8D77 /* BoolPropertyView.swift in Sources */,
E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */, E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */,
E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */, E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */,
@ -977,6 +1020,8 @@
E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */, E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */,
E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */, E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */,
E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */, E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */,
E2FE0F172D2698D5002963B7 /* LocalizedPageId.swift in Sources */,
E2FE0F0D2D268A09002963B7 /* PostListPageGeneratorSource.swift in Sources */,
E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */, E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */,
E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */, E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */,
E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */, E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */,
@ -988,6 +1033,7 @@
E25DA57A2D01C64400AEF16D /* PageContentGenerator.swift in Sources */, E25DA57A2D01C64400AEF16D /* PageContentGenerator.swift in Sources */,
E2A21C202CB28ED20060935B /* MockImage.swift in Sources */, E2A21C202CB28ED20060935B /* MockImage.swift in Sources */,
E2FE0EEE2D1C22F3002963B7 /* InlineLinkProcessor.swift in Sources */, E2FE0EEE2D1C22F3002963B7 /* InlineLinkProcessor.swift in Sources */,
E2FE0F022D266FCB002963B7 /* LocalizedNavigationSettings.swift in Sources */,
E29D313F2D04822C0051B7F4 /* AddPostView.swift in Sources */, E29D313F2D04822C0051B7F4 /* AddPostView.swift in Sources */,
E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */, E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */,
E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */, E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */,
@ -997,6 +1043,7 @@
E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */, E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */,
E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */, E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */,
E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */, E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */,
E2FE0F062D267350002963B7 /* TextFieldPropertyView.swift in Sources */,
E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */, E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */,
E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */, E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */,
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */, E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
@ -1009,6 +1056,7 @@
E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */, E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */,
E22990222D0ED12E009F8D77 /* TagOverviewFile.swift in Sources */, E22990222D0ED12E009F8D77 /* TagOverviewFile.swift in Sources */,
E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */, E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */,
E2FE0F002D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift in Sources */,
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */, E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */, E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */,
E229904A2D10BB90009F8D77 /* SecurityScopeBookmark.swift in Sources */, E229904A2D10BB90009F8D77 /* SecurityScopeBookmark.swift in Sources */,
@ -1024,8 +1072,10 @@
E29D31A52D0CD03F0051B7F4 /* FileSelectionView.swift in Sources */, E29D31A52D0CD03F0051B7F4 /* FileSelectionView.swift in Sources */,
E22990322D0F767B009F8D77 /* DatePropertyView.swift in Sources */, E22990322D0F767B009F8D77 /* DatePropertyView.swift in Sources */,
E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */, E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */,
E2FE0EFA2D25AFBA002963B7 /* PageHeader.swift in Sources */,
E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */, E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */,
E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */, E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */,
E2FE0F0B2D2689FF002963B7 /* FeedGeneratorSource.swift in Sources */,
E2DD04742C276F31003BFF1F /* MainView.swift in Sources */, E2DD04742C276F31003BFF1F /* MainView.swift in Sources */,
E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */, E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */,
E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */, E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */,
@ -1038,10 +1088,12 @@
E29D31C32D0DBEF20051B7F4 /* Song.swift in Sources */, E29D31C32D0DBEF20051B7F4 /* Song.swift in Sources */,
E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */, E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */,
E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */, E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */,
E2FE0F112D268E7E002963B7 /* PageCodeProcessor.swift in Sources */,
E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */, E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */,
E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */, E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */,
E22990192D0E3546009F8D77 /* ItemType.swift in Sources */, E22990192D0E3546009F8D77 /* ItemType.swift in Sources */,
E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */, E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */,
E2FE0F0F2D268D4F002963B7 /* BoxCommandProcessor.swift in Sources */,
E22990482D10B7B7009F8D77 /* StorageAccessError.swift in Sources */, E22990482D10B7B7009F8D77 /* StorageAccessError.swift in Sources */,
E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */, E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */,
E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */, E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */,

View File

@ -2,28 +2,34 @@ import Foundation
final class FeedPageGenerator { final class FeedPageGenerator {
let results: PageGenerationResults
let content: Content let content: Content
init(content: Content) { init(content: Content, results: PageGenerationResults) {
self.content = content self.content = content
self.results = results
} }
private func includeSwiper(in headers: inout Set<HeaderElement>) { private func includeSwiper(in headers: inout Set<HeaderElement>) {
if let swiperCss = content.settings.posts.swiperCssFile { if let swiperCss = content.settings.posts.swiperCssFile {
headers.insert(.css(file: swiperCss, order: HeaderElement.swiperCssFileOrder)) headers.insert(.css(file: swiperCss, order: HeaderElement.swiperCssFileOrder))
results.require(file: swiperCss)
} }
if let swiperJs = content.settings.posts.swiperJsFile { if let swiperJs = content.settings.posts.swiperJsFile {
headers.insert(.js(file: swiperJs, defer: true)) headers.insert(.js(file: swiperJs, defer: true))
results.require(file: swiperJs)
} }
} }
func generatePage(language: ContentLanguage, func generatePage(language: ContentLanguage,
posts: [FeedEntryData], posts: [FeedEntryData],
title: String, title: String,
description: String, description: String?,
showTitle: Bool, showTitle: Bool,
pageNumber: Int, pageNumber: Int,
totalPages: Int) -> String { totalPages: Int,
languageButtonUrl: String) -> String {
var headers = content.defaultPageHeaders var headers = content.defaultPageHeaders
var footer = "" var footer = ""
if posts.contains(where: { $0.images.count > 1 }) { if posts.contains(where: { $0.images.count > 1 }) {
@ -32,12 +38,23 @@ final class FeedPageGenerator {
footer = swiperInitScript(posts: posts) footer = swiperInitScript(posts: posts)
} }
let page = GenericPage( let iconUrl = content.settings.navigation.localized(in: language).rootUrl
let languageButton = NavigationBar.Link(
text: language.next.rawValue,
url: languageButtonUrl)
let pageHeader = PageHeader(
language: language, language: language,
title: title, title: title,
description: description, description: description,
iconUrl: iconUrl,
languageButton: languageButton,
links: content.navigationBar(in: language), links: content.navigationBar(in: language),
headers: headers, headers: headers,
icons: [])
let page = GenericPage(
header: pageHeader,
additionalFooter: footer) { content in additionalFooter: footer) { content in
if showTitle { if showTitle {
content += "<h1>\(title)</h1>" content += "<h1>\(title)</h1>"
@ -48,7 +65,6 @@ final class FeedPageGenerator {
if totalPages > 1 { if totalPages > 1 {
content += PostFeedPageNavigation(currentPage: pageNumber, numberOfPages: totalPages, language: language).content content += PostFeedPageNavigation(currentPage: pageNumber, numberOfPages: totalPages, language: language).content
} }
} }
return page.content return page.content
} }

View File

@ -1,12 +1,5 @@
import Foundation import Foundation
struct LocalizedPageId: Hashable {
let language: ContentLanguage
let pageId: String
}
final class GenerationResults: ObservableObject { final class GenerationResults: ObservableObject {
/// The files that could not be accessed /// The files that could not be accessed
@ -181,6 +174,10 @@ final class GenerationResults: ObservableObject {
func unsaved(_ path: String) { func unsaved(_ path: String) {
update { self.unsavedOutputFiles.insert(path) } update { self.unsavedOutputFiles.insert(path) }
} }
func empty(_ page: LocalizedPageId) {
update {self.emptyPages.insert(page) }
}
} }
private extension Dictionary where Value == Set<ItemId> { private extension Dictionary where Value == Set<ItemId> {

View File

@ -0,0 +1,24 @@
struct BoxCommandProcessor: CommandProcessor {
let commandType: ShorthandMarkdownKey = .box
let results: PageGenerationResults
init(content: Content, results: PageGenerationResults) {
self.results = results
}
/**
Format: `![box](<title>;<body>)`
*/
func process(_ arguments: [String], markdown: Substring) -> String {
guard arguments.count > 1 else {
results.invalid(command: .box, markdown)
return ""
}
let title = arguments[0]
let text = arguments.dropFirst().joined(separator: ";")
return ContentBox(title: title, text: text).content
}
}

View File

@ -3,7 +3,6 @@ struct IconCommandProcessor: CommandProcessor {
let commandType: ShorthandMarkdownKey = .icons let commandType: ShorthandMarkdownKey = .icons
let results: PageGenerationResults let results: PageGenerationResults
init(content: Content, results: PageGenerationResults) { init(content: Content, results: PageGenerationResults) {

View File

@ -13,7 +13,7 @@ struct InlineLinkProcessor {
let language: ContentLanguage let language: ContentLanguage
func handleLink(html: String, markdown: Substring) -> String { func process(html: String, markdown: Substring) -> String {
let url = markdown.between("(", and: ")") let url = markdown.between("(", and: ")")
if url.hasPrefix(pageLinkMarker) { if url.hasPrefix(pageLinkMarker) {
return handleInlinePageLink(url: url, html: html, markdown: markdown) return handleInlinePageLink(url: url, html: html, markdown: markdown)
@ -56,7 +56,7 @@ struct InlineLinkProcessor {
return markdown.between("[", and: "]") return markdown.between("[", and: "]")
} }
results.linked(to: tag) results.linked(to: tag)
let tagPath = content.absoluteUrlToTag(tag, language: language) let tagPath = tag.absoluteUrl(in: language)
return html.replacingOccurrences(of: textToChange, with: tagPath) return html.replacingOccurrences(of: textToChange, with: tagPath)
} }

View File

@ -0,0 +1,25 @@
import Splash
struct PageCodeProcessor {
private let codeHighlightFooter = "<script>hljs.highlightAll();</script>"
private let swift = SyntaxHighlighter(format: HTMLOutputFormat())
let results: PageGenerationResults
init(results: PageGenerationResults) {
self.results = results
}
func process(_ html: String, markdown: Substring) -> String {
guard markdown.starts(with: "```swift") else {
results.require(header: .codeHightlighting)
results.require(footer: codeHighlightFooter)
return html // Just use normal code highlighting
}
// Highlight swift code using Splash
let code = markdown.between("```swift", and: "```").trimmed
return "<pre><code>" + swift.highlight(code) + "</pre></code>"
}
}

View File

@ -0,0 +1,153 @@
import SwiftSoup
/**
Handles both inline HTML and the external HTML command
*/
struct PageHtmlProcessor: CommandProcessor {
let commandType: ShorthandMarkdownKey = .includedHtml
let results: PageGenerationResults
let content: Content
init(content: Content, results: PageGenerationResults) {
self.content = content
self.results = results
}
/**
Handle the HTML command
Format: `![html](<fileId>)`
*/
func process(_ arguments: [String], markdown: Substring) -> String {
guard arguments.count == 1 else {
results.invalid(command: .includedHtml, markdown)
return ""
}
let fileId = arguments[0]
guard let file = content.file(fileId) else {
results.missing(file: fileId, source: "External HTML command")
return ""
}
let content = file.textContent()
checkResources(in: content)
return content
}
/**
Handle inline HTML
*/
func process(_ html: String, markdown: Substring) -> String {
checkResources(in: html)
return html
}
private func checkResources(in html: String) {
let document: Document
do {
document = try SwiftSoup.parse(html)
} catch {
results.warning("Failed to parse inline HTML: \(error)")
return
}
checkImages(in: document)
checkLinks(in: document)
checkSourceSets(in: document)
}
private func checkImages(in document: Document) {
let srcAttributes: [String]
do {
let imgElements = try document.select("img")
srcAttributes = try imgElements.array()
.compactMap { try $0.attr("src") }
.filter { !$0.trimmed.isEmpty }
} catch {
results.warning("Failed to check 'src' attributes of <img> elements in inline HTML: \(error)")
return
}
for src in srcAttributes {
results.warning("Found image in html: \(src)")
}
}
private func checkLinks(in document: Document) {
let hrefs: [String]
do {
let linkElements = try document.select("a")
hrefs = try linkElements.array()
.compactMap { try $0.attr("href").trimmed }
.filter { !$0.isEmpty }
} catch {
results.warning("Failed to check 'href' attributes of <a> elements in inline HTML: \(error)")
return
}
for url in hrefs {
if url.hasPrefix("http://") || url.hasPrefix("https://") {
results.externalLink(to: url)
} else {
results.warning("Relative link in HTML: \(url)")
}
}
}
private func checkSourceSets(in document: Document) {
let sources: [Element]
do {
sources = try document.select("source").array()
} catch {
results.warning("Failed to find <source> elements in inline HTML: \(error)")
return
}
}
private func checkSourceSetAttributes(sources: [Element]) {
let srcSets: [String]
do {
srcSets = try sources
.compactMap { try $0.attr("srcset") }
.filter { !$0.trimmed.isEmpty }
} catch {
results.warning("Failed to check 'srcset' attributes of <source> elements in inline HTML: \(error)")
return
}
for src in srcSets {
results.warning("Found source set in html: \(src)")
}
}
private func checkSourceAttributes(sources: [Element]) {
let srcAttributes: [String]
do {
srcAttributes = try sources
.compactMap { try $0.attr("src") }
.filter { !$0.trimmed.isEmpty }
} catch {
results.warning("Failed to check 'src' attributes of <source> elements in inline HTML: \(error)")
return
}
for src in srcAttributes {
guard content.isValidIdForFile(src) else {
results.warning("Found source in html: \(src)")
continue
}
guard let file = content.file(src) else {
results.warning("Found source in html: \(src)")
continue
}
#warning("Either find files by their full path, or replace file id with full path")
results.require(file: file)
}
}
}

View File

@ -1,29 +1,33 @@
import Foundation import Foundation
import Ink import Ink
import Splash
import SwiftSoup
final class PageContentParser { final class PageContentParser {
private static let codeHighlightFooter = "<script>hljs.highlightAll();</script>"
private let swift = SyntaxHighlighter(format: HTMLOutputFormat())
private let language: ContentLanguage private let language: ContentLanguage
private let content: Content private let content: Content
private let results: PageGenerationResults private let results: PageGenerationResults
// MARK: Command handlers
private let buttonHandler: ButtonCommandProcessor private let buttonHandler: ButtonCommandProcessor
private let labelHandler: LabelsCommandProcessor private let labelHandler: LabelsCommandProcessor
private let audioPlayer: AudioPlayerCommandProcessor private let audioPlayer: AudioPlayerCommandProcessor
private let icons: IconCommandProcessor
private let box: BoxCommandProcessor
private let html: PageHtmlProcessor
// MARK: Other handlers
private let inlineLink: InlineLinkProcessor private let inlineLink: InlineLinkProcessor
private let icons: IconCommandProcessor private let code: PageCodeProcessor
var largeImageWidth: Int { var largeImageWidth: Int {
content.settings.pages.largeImageWidth content.settings.pages.largeImageWidth
@ -40,127 +44,25 @@ final class PageContentParser {
self.buttonHandler = .init(content: content, results: results) self.buttonHandler = .init(content: content, results: results)
self.labelHandler = .init(content: content, results: results) self.labelHandler = .init(content: content, results: results)
self.audioPlayer = .init(content: content, results: results) self.audioPlayer = .init(content: content, results: results)
self.inlineLink = .init(content: content, results: results, language: language)
self.icons = .init(content: content, results: results) self.icons = .init(content: content, results: results)
self.box = .init(content: content, results: results)
self.html = .init(content: content, results: results)
self.inlineLink = .init(content: content, results: results, language: language)
self.code = .init(results: results)
} }
func generatePage(from content: String) -> String { func generatePage(from content: String) -> String {
let parser = MarkdownParser(modifiers: [ let parser = MarkdownParser(modifiers: [
Modifier(target: .images, closure: processMarkdownImage), Modifier(target: .images, closure: processMarkdownImage),
Modifier(target: .codeBlocks, closure: handleCode), Modifier(target: .codeBlocks, closure: code.process),
Modifier(target: .links, closure: inlineLink.handleLink), Modifier(target: .links, closure: inlineLink.process),
Modifier(target: .html, closure: handleHTML), Modifier(target: .html, closure: html.process),
Modifier(target: .headings, closure: handleHeadlines) Modifier(target: .headings, closure: handleHeadlines)
]) ])
return parser.html(from: content) return parser.html(from: content)
} }
private func handleCode(html: String, markdown: Substring) -> String {
guard markdown.starts(with: "```swift") else {
results.require(header: .codeHightlighting)
results.require(footer: PageContentParser.codeHighlightFooter)
return html // Just use normal code highlighting
}
// Highlight swift code using Splash
let code = markdown.between("```swift", and: "```").trimmed
return "<pre><code>" + swift.highlight(code) + "</pre></code>"
}
private func handleHTML(html: String, _: Substring) -> String {
findResourcesInHtml(html: html)
return html
}
private func findResourcesInHtml(html: String) {
findImages(in: html)
findLinks(in: html)
findSourceSets(in: html)
}
private func findImages(in markdown: String) {
do {
// Parse the HTML string
let document = try SwiftSoup.parse(markdown)
// Select all 'img' elements
let imgElements = try document.select("img")
// Extract the 'src' attributes from each 'img' element
let srcAttributes = try imgElements.array()
.compactMap { try $0.attr("src") }
.filter { !$0.trimmed.isEmpty }
for src in srcAttributes {
results.warning("Found image in html: \(src)")
}
} catch {
print("Error parsing HTML: \(error)")
}
}
private func findLinks(in markdown: String) {
do {
// Parse the HTML string
let document = try SwiftSoup.parse(markdown)
// Select all 'img' elements
let linkElements = try document.select("a")
// Extract the 'src' attributes from each 'img' element
let srcAttributes = try linkElements.array()
.compactMap { try $0.attr("href").trimmed }
.filter { !$0.isEmpty }
for url in srcAttributes {
if url.hasPrefix("http://") || url.hasPrefix("https://") {
results.externalLink(to: url)
} else {
results.warning("Relative link in HTML: \(url)")
}
}
} catch {
print("Error parsing HTML: \(error)")
}
}
private func findSourceSets(in markdown: String) {
do {
// Parse the HTML string
let document = try SwiftSoup.parse(markdown)
// Select all 'img' elements
let linkElements = try document.select("source")
// Extract the 'src' attributes from each 'img' element
let srcsetAttributes = try linkElements.array()
.compactMap { try $0.attr("srcset") }
.filter { !$0.trimmed.isEmpty }
for src in srcsetAttributes {
results.warning("Found source set in html: \(src)")
}
let srcAttributes = try linkElements.array()
.compactMap { try $0.attr("src") }
.filter { !$0.trimmed.isEmpty }
for src in srcAttributes {
guard content.isValidIdForFile(src) else {
results.warning("Found source in html: \(src)")
continue
}
guard let file = content.file(src) else {
results.warning("Found source in html: \(src)")
continue
}
#warning("Either find files by their full path, or replace file id with full path")
results.require(file: file)
}
} catch {
print("Error parsing HTML: \(error)")
}
}
/** /**
Modify headlines by extracting an id from the headline and adding it into the html element Modify headlines by extracting an id from the headline and adding it into the html element
@ -218,9 +120,9 @@ final class PageContentParser {
case .pageLink: case .pageLink:
return handlePageLink(arguments, markdown: markdown) return handlePageLink(arguments, markdown: markdown)
case .includedHtml: case .includedHtml:
return handleExternalHtml(arguments, markdown: markdown) return self.html.process(arguments, markdown: markdown)
case .box: case .box:
return handleSimpleBox(arguments, markdown: markdown) return box.process(arguments, markdown: markdown)
case .model: case .model:
return handleModel(arguments, markdown: markdown) return handleModel(arguments, markdown: markdown)
case .svg: case .svg:
@ -343,37 +245,6 @@ final class PageContentParser {
return option return option
} }
/**
Format: `![html](<fileId>)`
*/
private func handleExternalHtml(_ arguments: [String], markdown: Substring) -> String {
guard arguments.count == 1 else {
results.invalid(command: .includedHtml, markdown)
return ""
}
let fileId = arguments[0]
guard let file = content.file(fileId) else {
results.missing(file: fileId, source: "External HTML command")
return ""
}
let content = file.textContent()
findResourcesInHtml(html: content)
return content
}
/**
Format: `![box](<title>;<body>)`
*/
private func handleSimpleBox(_ arguments: [String], markdown: Substring) -> String {
guard arguments.count > 1 else {
results.invalid(command: .box, markdown)
return ""
}
let title = arguments[0]
let text = arguments.dropFirst().joined(separator: ";")
return ContentBox(title: title, text: text).content
}
/** /**
Format: `![page](<pageId>)` Format: `![page](<pageId>)`
*/ */

View File

@ -73,6 +73,8 @@ final class PageGenerationResults: ObservableObject {
/// The files that could not be saved to the output folder /// The files that could not be saved to the output folder
private(set) var unsavedOutputFiles: [String: Set<ItemType>] = [:] private(set) var unsavedOutputFiles: [String: Set<ItemType>] = [:]
private(set) var pageIsEmpty: Bool
init(itemId: ItemId, delegate: GenerationResults) { init(itemId: ItemId, delegate: GenerationResults) {
self.itemId = itemId self.itemId = itemId
self.delegate = delegate self.delegate = delegate
@ -94,33 +96,7 @@ final class PageGenerationResults: ObservableObject {
invalidCommands = [] invalidCommands = []
warnings = [] warnings = []
unsavedOutputFiles = [:] unsavedOutputFiles = [:]
} pageIsEmpty = false
private init(other: PageGenerationResults) {
self.itemId = other.itemId
self.delegate = other.delegate
inaccessibleFiles = other.inaccessibleFiles
unparsableFiles = other.unparsableFiles
missingFiles = other.missingFiles
missingLinkedFiles = other.missingLinkedFiles
missingLinkedTags = other.missingLinkedTags
missingLinkedPages = other.missingLinkedPages
requiredHeaders = other.requiredHeaders
requiredFooters = other.requiredFooters
requiredIcons = other.requiredIcons
linkedPages = other.linkedPages
linkedTags = other.linkedTags
externalLinks = other.externalLinks
usedFiles = other.usedFiles
requiredFiles = other.requiredFiles
imagesToGenerate = other.imagesToGenerate
invalidCommands = other.invalidCommands
warnings = other.warnings
unsavedOutputFiles = other.unsavedOutputFiles
}
func copy() -> PageGenerationResults {
.init(other: self)
} }
// MARK: Adding entries // MARK: Adding entries
@ -231,5 +207,11 @@ final class PageGenerationResults: ObservableObject {
unsavedOutputFiles[path, default: []].insert(source) unsavedOutputFiles[path, default: []].insert(source)
delegate.unsaved(path) delegate.unsaved(path)
} }
func markPageAsEmpty() {
guard case .page(let page) = itemId.itemType else { return }
pageIsEmpty = true
delegate.empty(.init(language: itemId.language, pageId: page.id))
}
} }

View File

@ -19,13 +19,32 @@ final class PageGenerator {
return result return result
} }
private func makeEmptyPageContent(in language: ContentLanguage) -> String {
switch language {
case .english:
return ContentBox(
title: "Content not available",
text: "This page is not available yet. Try the German version or check back later.")
.content
case .german:
return ContentBox(
title: "Inhalt nicht verfügbar",
text: "Diese Seite ist noch nicht verfügbar. Versuche die englische Version oder komm später hierher zurück.")
.content
}
}
func generate(page: Page, language: ContentLanguage, results: PageGenerationResults) -> String? { func generate(page: Page, language: ContentLanguage, results: PageGenerationResults) -> String? {
let contentGenerator = PageContentParser( let contentGenerator = PageContentParser(
content: content, content: content,
language: language, results: results) language: language, results: results)
guard let rawPageContent = content.storage.pageContent(for: page.id, language: language) else { let rawPageContent: String
return nil if let existing = content.storage.pageContent(for: page.id, language: language) {
rawPageContent = existing
} else {
rawPageContent = makeEmptyPageContent(in: language)
results.markPageAsEmpty()
} }
let pageContent = contentGenerator.generatePage(from: rawPageContent) let pageContent = contentGenerator.generatePage(from: rawPageContent)
@ -34,27 +53,41 @@ final class PageGenerator {
let tags: [FeedEntryData.Tag] = page.tags.map { tag in let tags: [FeedEntryData.Tag] = page.tags.map { tag in
.init(name: tag.localized(in: language).name, .init(name: tag.localized(in: language).name,
url: content.absoluteUrlToTag(tag, language: language)) url: tag.absoluteUrl(in: language))
} }
let headers = makeHeaders(requiredItems: results.requiredHeaders) let headers = makeHeaders(requiredItems: results.requiredHeaders)
results.require(files: headers.compactMap { $0.file }) results.require(files: headers.compactMap { $0.file })
let fullPage = ContentPage( let iconUrl = content.settings.navigation.localized(in: language).rootUrl
language: language, let languageUrl = page.absoluteUrl(in: language.next)
dateString: page.dateText(in: language), let languageButton = NavigationBar.Link(
title: localized.title, text: language.next.rawValue,
showTitle: !localized.hideTitle, url: languageUrl)
tags: tags,
linkTitle: localized.linkPreviewTitle ?? localized.title,
description: localized.linkPreviewDescription ?? "",
navigationBarLinks: content.navigationBar(in: language),
pageContent: pageContent,
headers: headers,
footers: results.requiredFooters.sorted(),
icons: results.requiredIcons)
.content
return fullPage let pageHeader = PageHeader(
language: language,
title: localized.linkPreviewTitle ?? localized.title,
description: localized.linkPreviewDescription,
iconUrl: iconUrl,
languageButton: languageButton,
links: content.navigationBar(in: language),
headers: headers,
icons: results.requiredIcons)
let fullPage = GenericPage(
header: pageHeader,
additionalFooter: results.requiredFooters.sorted().joined()) { content in
content += "<article>"
if !localized.hideTitle {
content += "<h3>\(page.dateText(in: language))</h3>"
content += "<h1>\(localized.title)</h1>"
content += TagList(tags: tags).content
}
content += pageContent
content += "</article>"
}
return fullPage.content
} }
} }

View File

@ -0,0 +1,25 @@
struct FeedGeneratorSource: PostListPageGeneratorSource {
let language: ContentLanguage
let content: Content
let results: PageGenerationResults
var showTitle: Bool {
false
}
var pageTitle: String {
content.settings.localized(in: language).title
}
var pageDescription: String {
content.settings.localized(in: language).description
}
func pageUrlPrefix(for language: ContentLanguage) -> String {
content.settings.localized(in: language).feedUrlPrefix
}
}

View File

@ -2,42 +2,26 @@ import Foundation
final class PostListPageGenerator { final class PostListPageGenerator {
private let language: ContentLanguage let source: PostListPageGeneratorSource
private let content: Content init(source: PostListPageGeneratorSource) {
self.source = source
}
private let results: PageGenerationResults private var language: ContentLanguage {
source.language
private let showTitle: Bool
private let pageTitle: String
private let pageDescription: String
/// The url of the page, excluding the extension
private let pageUrlPrefix: String
init(language: ContentLanguage,
content: Content,
results: PageGenerationResults,
showTitle: Bool, pageTitle: String,
pageDescription: String,
pageUrlPrefix: String) {
self.language = language
self.content = content
self.results = results
self.showTitle = showTitle
self.pageTitle = pageTitle
self.pageDescription = pageDescription
self.pageUrlPrefix = pageUrlPrefix
} }
private var mainContentMaximumWidth: Int { private var mainContentMaximumWidth: Int {
content.settings.posts.contentWidth source.content.settings.posts.contentWidth
} }
private var postsPerPage: Int { private var postsPerPage: Int {
content.settings.posts.postsPerPage source.content.settings.posts.postsPerPage
}
private func pageUrl(in language: ContentLanguage, pageNumber: Int) -> String {
"\(source.pageUrlPrefix(for: language))/\(pageNumber).html"
} }
func createPages(for posts: [Post]) { func createPages(for posts: [Post]) {
@ -67,7 +51,7 @@ final class PostListPageGenerator {
let tags: [FeedEntryData.Tag] = post.tags.filter { $0.isVisible }.map { tag in let tags: [FeedEntryData.Tag] = post.tags.filter { $0.isVisible }.map { tag in
.init(name: tag.localized(in: language).name, .init(name: tag.localized(in: language).name,
url: content.absoluteUrlToTag(tag, language: language)) url: tag.absoluteUrl(in: language))
} }
let images = localized.images.map(createFeedImage) let images = localized.images.map(createFeedImage)
@ -82,25 +66,28 @@ final class PostListPageGenerator {
images: images) images: images)
} }
let feedPageGenerator = FeedPageGenerator(content: content) let feedPageGenerator = FeedPageGenerator(content: source.content, results: source.results)
let languageButtonUrl = pageUrl(in: language.next, pageNumber: pageIndex)
let fileContent = feedPageGenerator.generatePage( let fileContent = feedPageGenerator.generatePage(
language: language, language: language,
posts: posts, posts: posts,
title: pageTitle, title: source.pageTitle,
description: pageDescription, description: source.pageDescription,
showTitle: showTitle, showTitle: source.showTitle,
pageNumber: pageIndex, pageNumber: pageIndex,
totalPages: pageCount) totalPages: pageCount,
let filePath = "\(pageUrlPrefix)/\(pageIndex).html" languageButtonUrl: languageButtonUrl)
let filePath = pageUrl(in: language, pageNumber: pageIndex)
guard save(fileContent, to: filePath) else { guard save(fileContent, to: filePath) else {
results.unsavedOutput(filePath, source: .feed) source.results.unsavedOutput(filePath, source: .feed)
return return
} }
} }
private func createFeedImage(for image: FileResource) -> FeedEntryData.Image { private func createFeedImage(for image: FileResource) -> FeedEntryData.Image {
results.requireImageSet(for: image, size: mainContentMaximumWidth) source.results.requireImageSet(for: image, size: mainContentMaximumWidth)
return .init( return .init(
rawImagePath: image.absoluteUrl, rawImagePath: image.absoluteUrl,
width: mainContentMaximumWidth, width: mainContentMaximumWidth,
@ -109,6 +96,6 @@ final class PostListPageGenerator {
} }
private func save(_ content: String, to relativePath: String) -> Bool { private func save(_ content: String, to relativePath: String) -> Bool {
self.content.storage.write(content, to: relativePath) source.content.storage.write(content, to: relativePath)
} }
} }

View File

@ -0,0 +1,17 @@
protocol PostListPageGeneratorSource {
var language: ContentLanguage { get }
var content: Content { get }
var results: PageGenerationResults { get }
var showTitle: Bool { get }
var pageTitle: String { get }
var pageDescription: String { get }
func pageUrlPrefix(for language: ContentLanguage) -> String
}

View File

@ -0,0 +1,27 @@
struct TagPageGeneratorSource: PostListPageGeneratorSource {
let language: ContentLanguage
let content: Content
let results: PageGenerationResults
let tag: Tag
var showTitle: Bool {
true
}
var pageTitle: String {
tag.localized(in: language).name
}
var pageDescription: String {
tag.localized(in: language).description ?? ""
}
func pageUrlPrefix(for language: ContentLanguage) -> String {
tag.absoluteUrl(in: language)
}
}

View File

@ -37,7 +37,7 @@ enum ShorthandMarkdownKey: String {
/// Format: `![tag](<tagId>)` /// Format: `![tag](<tagId>)`
case tagLink = "tag" case tagLink = "tag"
/// Additional HTML code include verbatim into the page. /// Additional HTML code included verbatim into the page.
/// Format: `![html](<fileId>)` /// Format: `![html](<fileId>)`
case includedHtml = "html" case includedHtml = "html"

View File

@ -5,6 +5,7 @@ import SFSafeSymbols
#warning("Fix CV") #warning("Fix CV")
#warning("Fix endeavor basics (image compare)") #warning("Fix endeavor basics (image compare)")
#warning("Add custom url string to external files (optional)")
#warning("Show all warnings on page content") #warning("Show all warnings on page content")
#warning("Button to delete file") #warning("Button to delete file")
#warning("Add link to other language") #warning("Add link to other language")

View File

@ -106,20 +106,6 @@ extension Content {
} }
} }
// MARK: Paths to items
private func makeCleanAbsolutePath(_ path: String) -> String {
("/" + path).replacingOccurrences(of: "//", with: "/")
}
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"
}
// MARK: Find items by id // MARK: Find items by id
func page(_ pageId: String) -> Page? { func page(_ pageId: String) -> Page? {
@ -145,7 +131,7 @@ extension Content {
// MARK: Generation input // MARK: Generation input
func navigationBar(in language: ContentLanguage) -> [NavigationBar.Link] { func navigationBar(in language: ContentLanguage) -> [NavigationBar.Link] {
settings.navigationItems.map { settings.navigation.navigationItems.map {
.init(text: $0.title(in: language), .init(text: $0.title(in: language),
url: $0.absoluteUrl(in: language)) url: $0.absoluteUrl(in: language))
} }
@ -197,9 +183,6 @@ extension Content {
continue continue
} }
for language in ContentLanguage.allCases { for language in ContentLanguage.allCases {
guard page.hasContent(in: language) else {
continue
}
generateInternal(page, in: language) generateInternal(page, in: language)
} }
} }
@ -213,14 +196,12 @@ extension Content {
for language in ContentLanguage.allCases { for language in ContentLanguage.allCases {
guard shouldGenerateWebsite else { return } guard shouldGenerateWebsite else { return }
let results = results.makeResults(for: .feed, in: language) let results = results.makeResults(for: .feed, in: language)
let generator = PostListPageGenerator( let source = FeedGeneratorSource(
language: language, language: language,
content: self, content: self,
results: results, results: results)
showTitle: false,
pageTitle: settings.localized(in: language).title, let generator = PostListPageGenerator(source: source)
pageDescription: settings.localized(in: language).description,
pageUrlPrefix: settings.localized(in: language).feedUrlPrefix)
generator.createPages(for: posts) generator.createPages(for: posts)
} }
@ -249,16 +230,12 @@ extension Content {
let posts = posts.filter { $0.tags.contains(tag) } let posts = posts.filter { $0.tags.contains(tag) }
guard posts.count > 0 else { continue } guard posts.count > 0 else { continue }
let localized = tag.localized(in: language) let source = TagPageGeneratorSource(
let urlPrefix = absoluteUrlPrefixForTag(tag, language: language)
let generator = PostListPageGenerator(
language: language, language: language,
content: self, content: self,
results: results, results: results,
showTitle: true, tag: tag)
pageTitle: localized.name, let generator = PostListPageGenerator(source: source)
pageDescription: localized.description ?? "",
pageUrlPrefix: urlPrefix)
generator.createPages(for: posts) generator.createPages(for: posts)
} }
} }

View File

@ -146,52 +146,10 @@ extension Content {
self.files = files.values.sorted { $0.id } self.files = files.values.sorted { $0.id }
self.posts = posts.values.sorted(ascending: false) { $0.startDate } self.posts = posts.values.sorted(ascending: false) { $0.startDate }
self.tagOverview = tagOverview self.tagOverview = tagOverview
self.settings = makeSettings(settings, tags: tags, pages: pages, files: files, posts: posts) self.settings = .init(file: settings, tags: tags, pages: pages, files: files, posts: posts, tagOverview: tagOverview)
print("Content loaded") print("Content loaded")
} }
private func makeSettings(_ settings: SettingsFile,
tags: [String : Tag],
pages: [String : Page],
files: [String : FileResource],
posts: [String : Post]) -> Settings {
#warning("Notify about missing links")
let navigationItems: [Item] = settings.navigationItems.compactMap { raw in
guard let type = ItemType(rawValue: raw, posts: posts, pages: pages, tags: tags) else {
return nil
}
switch type {
case .general:
return nil
case .post(let post):
return post
case .feed:
return nil // TODO: Provide feed object
case .page(let page):
return page
case .tagPage(let tag):
return tag
case .tagOverview:
return tagOverview
}
}
let posts = PostSettings(file: settings.posts, files: files)
let pages = PageSettings(file: settings.pages, files: files)
let paths = PathSettings(file: settings.paths)
return Settings(
paths: paths,
navigationItems: navigationItems,
posts: posts,
pages: pages,
german: .init(file: settings.german),
english: .init(file: settings.english))
}
private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], files: [String : FileResource]) -> [String : Page] { private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], files: [String : FileResource]) -> [String : Page] {
pagesData.reduce(into: [:]) { pages, data in pagesData.reduce(into: [:]) { pages, data in
let (pageId, page) = data let (pageId, page) = data

View File

@ -133,37 +133,3 @@ private extension LocalizedTag {
originalURL: originalUrl) originalURL: originalUrl)
} }
} }
extension Settings {
var file: SettingsFile {
.init(
paths: paths.file,
navigationItems: navigationItems.map { $0.itemType.id },
posts: posts.file,
pages: pages.file,
german: german.file,
english: english.file)
}
}
private extension PostSettings {
var file: PostSettingsFile {
.init(postsPerPage: postsPerPage,
contentWidth: contentWidth,
swiperCssFile: swiperCssFile?.id,
swiperJsFile: swiperJsFile?.id,
defaultCssFile: defaultCssFile?.id)
}
}
private extension LocalizedPostSettings {
var file: LocalizedPostSettingsFile {
.init(
feedTitle: title,
feedDescription: description,
feedUrlPrefix: feedUrlPrefix)
}
}

View File

@ -32,3 +32,13 @@ extension ContentLanguage: Comparable {
lhs.rawValue < rhs.rawValue lhs.rawValue < rhs.rawValue
} }
} }
extension ContentLanguage {
var next: ContentLanguage {
switch self {
case .english: return .german
case .german: return .english
}
}
}

View File

@ -67,6 +67,8 @@ enum FileType: String {
case js case js
case ttf
// MARK: Text // MARK: Text
case json case json
@ -143,7 +145,7 @@ enum FileType: String {
return .video return .video
case .mp3, .aac: case .mp3, .aac:
return .audio return .audio
case .js, .css: case .js, .css, .ttf:
return .asset return .asset
case .json, .conf, .yaml: case .json, .conf, .yaml:
return .text return .text
@ -195,7 +197,7 @@ enum FileType: String {
var isAsset: Bool { var isAsset: Bool {
switch self { switch self {
case .js, .css: case .js, .css, .ttf:
return true return true
default: default:
return false return false

View File

@ -0,0 +1,25 @@
struct LocalizedPageId: Hashable {
let language: ContentLanguage
let pageId: String
}
extension LocalizedPageId: Identifiable {
var id: String {
pageId + "-" + language.rawValue
}
}
extension LocalizedPageId: Comparable {
static func < (lhs: LocalizedPageId, rhs: LocalizedPageId) -> Bool {
lhs.id < rhs.id
}
}
extension LocalizedPageId: Equatable {
}

View File

@ -0,0 +1,26 @@
import Foundation
final class LocalizedNavigationSettings: ObservableObject {
@Published
var rootUrl: String
init(rootUrl: String) {
self.rootUrl = rootUrl
}
init(file: LocalizedNavigationSettingsFile) {
self.rootUrl = file.rootUrl
}
var file: LocalizedNavigationSettingsFile {
.init(rootUrl: rootUrl)
}
}
extension LocalizedNavigationSettings {
static var `default`: LocalizedNavigationSettings {
.init(rootUrl: "/")
}
}

View File

@ -17,9 +17,18 @@ final class LocalizedPostSettings: ObservableObject {
self.feedUrlPrefix = feedUrlPrefix self.feedUrlPrefix = feedUrlPrefix
} }
// MARK: Storage
init(file: LocalizedPostSettingsFile) { init(file: LocalizedPostSettingsFile) {
self.title = file.feedTitle self.title = file.feedTitle
self.description = file.feedDescription self.description = file.feedDescription
self.feedUrlPrefix = file.feedUrlPrefix self.feedUrlPrefix = file.feedUrlPrefix
} }
var file: LocalizedPostSettingsFile {
.init(
feedTitle: title,
feedDescription: description,
feedUrlPrefix: feedUrlPrefix)
}
} }

View File

@ -0,0 +1,71 @@
import Foundation
final class NavigationSettings: ObservableObject {
/// The items to show in the navigation bar
@Published
var navigationItems: [Item]
@Published
var german: LocalizedNavigationSettings
@Published
var english: LocalizedNavigationSettings
init(navigationItems: [Item], german: LocalizedNavigationSettings, english: LocalizedNavigationSettings) {
self.navigationItems = navigationItems
self.german = german
self.english = english
}
init(file: NavigationSettingsFile,
tags: [String : Tag],
pages: [String : Page],
files: [String : FileResource],
posts: [String : Post],
tagOverview: TagOverviewPage?) {
#warning("Notify about missing links")
self.navigationItems = file.navigationItems.compactMap { raw in
guard let type = ItemType(rawValue: raw, posts: posts, pages: pages, tags: tags) else {
return nil
}
switch type {
case .general:
return nil
case .post(let post):
return post
case .feed:
return nil // TODO: Provide feed object
case .page(let page):
return page
case .tagPage(let tag):
return tag
case .tagOverview:
return tagOverview
}
}
self.german = LocalizedNavigationSettings(file: file.german)
self.english = LocalizedNavigationSettings(file: file.english)
}
var file: NavigationSettingsFile {
.init(
navigationItems: navigationItems.map { $0.itemType.id },
german: german.file,
english: english.file)
}
}
extension NavigationSettings: LocalizedItem {
}
extension NavigationSettings {
static var `default`: NavigationSettings {
.init(navigationItems: [],
german: .default,
english: .default)
}
}

View File

@ -31,6 +31,8 @@ final class PostSettings: ObservableObject {
self.defaultCssFile = defaultCssFile self.defaultCssFile = defaultCssFile
} }
// MARK: Storage
init(file: PostSettingsFile, files: [String : FileResource]) { init(file: PostSettingsFile, files: [String : FileResource]) {
self.postsPerPage = file.postsPerPage self.postsPerPage = file.postsPerPage
self.contentWidth = file.contentWidth self.contentWidth = file.contentWidth
@ -38,4 +40,12 @@ final class PostSettings: ObservableObject {
self.swiperJsFile = file.swiperJsFile.map { files[$0] } self.swiperJsFile = file.swiperJsFile.map { files[$0] }
self.defaultCssFile = file.defaultCssFile.map { files[$0] } self.defaultCssFile = file.defaultCssFile.map { files[$0] }
} }
var file: PostSettingsFile {
.init(postsPerPage: postsPerPage,
contentWidth: contentWidth,
swiperCssFile: swiperCssFile?.id,
swiperJsFile: swiperJsFile?.id,
defaultCssFile: defaultCssFile?.id)
}
} }

View File

@ -7,7 +7,7 @@ final class Settings: ObservableObject {
/// The items to show in the navigation bar /// The items to show in the navigation bar
@Published @Published
var navigationItems: [Item] var navigation: NavigationSettings
@Published @Published
var posts: PostSettings var posts: PostSettings
@ -21,9 +21,9 @@ final class Settings: ObservableObject {
@Published @Published
var english: LocalizedPostSettings var english: LocalizedPostSettings
init(paths: PathSettings, navigationItems: [Item], posts: PostSettings, pages: PageSettings, german: LocalizedPostSettings, english: LocalizedPostSettings) { init(paths: PathSettings, navigation: NavigationSettings, posts: PostSettings, pages: PageSettings, german: LocalizedPostSettings, english: LocalizedPostSettings) {
self.paths = paths self.paths = paths
self.navigationItems = navigationItems self.navigation = navigation
self.posts = posts self.posts = posts
self.pages = pages self.pages = pages
self.german = german self.german = german
@ -36,4 +36,48 @@ final class Settings: ObservableObject {
case .german: return german case .german: return german
} }
} }
init(file: SettingsFile,
tags: [String : Tag],
pages: [String : Page],
files: [String : FileResource],
posts: [String : Post],
tagOverview: TagOverviewPage?) {
self.navigation = NavigationSettings(
file: file.navigation,
tags: tags,
pages: pages,
files: files,
posts: posts,
tagOverview: tagOverview)
self.posts = PostSettings(file: file.posts, files: files)
self.pages = PageSettings(file: file.pages, files: files)
self.paths = PathSettings(file: file.paths)
self.german = .init(file: file.german)
self.english = .init(file: file.english)
}
var file: SettingsFile {
.init(
paths: paths.file,
navigation: navigation.file,
posts: posts.file,
pages: pages.file,
german: german.file,
english: english.file)
}
}
extension Settings {
static let `default`: Settings = .init(
paths: .default,
navigation: .default,
posts: .default,
pages: .default,
german: .german,
english: .english)
} }

View File

@ -13,7 +13,7 @@ struct FeedEntry {
} }
var content: String { var content: String {
var result = "<div class='card\(cardLinkClassText)'>" var result = "<article><div class='card\(cardLinkClassText)'>"
ImageGallery(id: data.entryId, images: data.images) ImageGallery(id: data.entryId, images: data.images)
.addContent(to: &result) .addContent(to: &result)
@ -34,7 +34,7 @@ struct FeedEntry {
if let url = data.link { if let url = data.link {
result += "<div class='link-center'><div class='link'>\(url.text)</div></div>" result += "<div class='link-center'><div class='link'>\(url.text)</div></div>"
} }
result += "</div></div>" // Closes card-content and card result += "</div></div></article>" // Closes card-content, card, article
return result return result
} }
} }

View File

@ -0,0 +1,28 @@
import Foundation
struct GenericPage {
let header: PageHeader
let additionalFooter: String
let insertedContent: (inout String) -> Void
init(header: PageHeader,
additionalFooter: String,
insertedContent: @escaping (inout String) -> Void) {
self.header = header
self.additionalFooter = additionalFooter
self.insertedContent = insertedContent
}
var content: String {
var result = ""
header.populate(&result) // Opens <html><body><main>
insertedContent(&result)
result += "</main>" // Close <main>
result += additionalFooter
result += "</body></html>" // Close <body><html>
return result
}
}

View File

@ -12,25 +12,29 @@ struct NavigationBar: HtmlProducer {
private let links: [Link] private let links: [Link]
init(links: [Link]) { private let languageButton: Link
private let iconUrl: String
init(links: [Link], languageButton: Link, iconUrl: String) {
self.links = links self.links = links
self.languageButton = languageButton
self.iconUrl = iconUrl
} }
func populate(_ result: inout String) { func populate(_ result: inout String) {
result += "<nav class='navbar'><div class='navbar-fade'></div><div class='nav-center'>" result += "<nav class='navbar'>"
let middleIndex = links.count / 2 result += "<div class='fade'></div>"
let leftNavigationItems = links[..<middleIndex] result += "<div class='centered'>"
let rightNavigationItems = links[middleIndex...] result += "<a class='icon-link' href='\(iconUrl)'><div class='icon'></div></a>"
result += "<div class='buttons'><input type='checkbox' id='menu-toggle'>"
for item in leftNavigationItems { result += "<label for='menu-toggle' class='burger'><div></div><div></div><div></div></label>"
result += "<a class='nav-animate' href='\(item.url)'>\(item.text)</a>" result += "<ul>"
for item in links {
result += "<li><a href='\(item.url)'>\(item.text)</a></li>"
} }
result += "</ul>"
result += "<a id='nav-image' href='/'><div class='icon-ch'></div></a>" result += "<a class='lang-button' href='\(languageButton.url)'>\(languageButton.text)</a>"
result += "</div></div></nav>" // Close buttons, centered, navbar
for item in rightNavigationItems {
result += "<a class='nav-animate' href='\(item.url)'>\(item.text)</a>"
}
result += "</div></nav>" // Close nav-center, navbar
} }
} }

View File

@ -0,0 +1,60 @@
struct PageHeader: HtmlProducer {
let language: ContentLanguage
let iconUrl: String
let languageButton: NavigationBar.Link
let links: [NavigationBar.Link]
let headers: [HeaderElement]
let icons: Set<PageIcon>
init(
language: ContentLanguage,
title: String,
description: String?,
iconUrl: String,
languageButton: NavigationBar.Link,
links: [NavigationBar.Link],
headers: Set<HeaderElement>,
icons: Set<PageIcon>) {
self.language = language
self.iconUrl = iconUrl
self.languageButton = languageButton
self.links = links
self.icons = icons
var headers = headers
headers.insert(.title(title))
if let description {
headers.insert(.description(description))
}
self.headers = headers.sorted()
}
func populate(_ result: inout String) {
result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">"
result += PageHead(items: headers).content
result += "<body>"
result += NavigationBar(links: links, languageButton: languageButton, iconUrl: iconUrl).content
result += symbols // Add the svg images required by the page as hidden elements
result += "<main>"
result += "<div class='navbar-spacer'></div>"
}
private var symbols: String {
guard !icons.isEmpty else {
return ""
}
var result = "<div style='display:none'>"
for icon in icons {
result += icon.icon.content
}
result += "</div>"
return result
}
}

View File

@ -1,87 +0,0 @@
import Foundation
struct ContentPage: HtmlProducer {
private let linkTitle: String
private let description: String
private let language: ContentLanguage
private let dateString: String
private let title: String
private let showTitle: Bool
private let tags: [FeedEntryData.Tag]
private let navigationBarLinks: [NavigationBar.Link]
private let pageContent: String
private let headers: [HeaderElement]
private let footers: String
private let icons: Set<PageIcon>
init(language: ContentLanguage,
dateString: String,
title: String,
showTitle: Bool,
tags: [FeedEntryData.Tag],
linkTitle: String,
description: String,
navigationBarLinks: [NavigationBar.Link],
pageContent: String,
headers: Set<HeaderElement>,
footers: [String], icons: Set<PageIcon>) {
self.language = language
self.dateString = dateString
self.title = title
self.showTitle = showTitle
self.tags = tags
self.linkTitle = linkTitle
self.description = description
self.navigationBarLinks = navigationBarLinks
self.pageContent = pageContent
self.headers = headers.union([.title(title), .description(description)]).sorted()
self.footers = footers.joined()
self.icons = icons
}
func populate(_ result: inout String) {
// TODO: Add headers and footers from page content
result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">"
result += PageHead(items: headers).content
result += "<body>"
result += NavigationBar(links: navigationBarLinks).content
result += "<main><article>"
result += "<div style=\"height: 70px;\"></div>"
if showTitle {
result += "<h3>\(dateString)</h3>"
result += "<h1>\(title)</h1>"
result += TagList(tags: tags).content
}
result += symbols
result += pageContent
result += "</article></main>"
result += footers
result += "</body></html>" // Close content
}
private var symbols: String {
guard !icons.isEmpty else {
return ""
}
var result = "<div style='display:none'>"
for icon in icons {
result += icon.icon.content
}
result += "</div>"
return result
}
}

View File

@ -1,43 +0,0 @@
import Foundation
struct GenericPage {
let language: ContentLanguage
let title: String
let description: String
let links: [NavigationBar.Link]
let headers: [HeaderElement]
let additionalFooter: String
let insertedContent: (inout String) -> Void
init(language: ContentLanguage, title: String, description: String, links: [NavigationBar.Link], headers: Set<HeaderElement>, additionalFooter: String, insertedContent: @escaping (inout String) -> Void) {
self.language = language
self.title = title
self.description = description
self.links = links
self.headers = headers.union([.title(title), .description(description)]).sorted()
self.additionalFooter = additionalFooter
self.insertedContent = insertedContent
}
var content: String {
#warning("Consolidate this code with ContentPage")
var result = ""
result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">"
result += PageHead(items: headers).content
result += "<body>"
result += NavigationBar(links: links).content
result += "<div class=\"content\"><div style=\"height: 70px;\"></div>"
insertedContent(&result)
result += "</div>"
result += additionalFooter
result += "</body></html>" // Close content
return result
}
}

View File

@ -1,16 +1,5 @@
import Foundation import Foundation
extension Settings {
static let `default`: Settings = .init(
paths: .default,
navigationItems: [],
posts: .default,
pages: .default,
german: .german,
english: .english)
}
extension PathSettings { extension PathSettings {
static var `default`: PathSettings { static var `default`: PathSettings {

View File

@ -0,0 +1,17 @@
import Foundation
struct LocalizedNavigationSettingsFile {
let rootUrl: String
}
extension LocalizedNavigationSettingsFile: Codable {
}
extension LocalizedNavigationSettingsFile {
static var `default`: LocalizedNavigationSettingsFile {
.init(rootUrl: "/")
}
}

View File

@ -0,0 +1,25 @@
import Foundation
struct NavigationSettingsFile {
/// The tags to show in the navigation bar
let navigationItems: [String]
let german: LocalizedNavigationSettingsFile
let english: LocalizedNavigationSettingsFile
}
extension NavigationSettingsFile: Codable {
}
extension NavigationSettingsFile {
static var `default`: NavigationSettingsFile {
.init(
navigationItems: [],
german: .default,
english: .default)
}
}

View File

@ -5,7 +5,7 @@ struct SettingsFile {
let paths: PathSettingsFile let paths: PathSettingsFile
/// The tags to show in the navigation bar /// The tags to show in the navigation bar
let navigationItems: [String] let navigation: NavigationSettingsFile
let posts: PostSettingsFile let posts: PostSettingsFile
@ -23,7 +23,7 @@ extension SettingsFile {
static var `default`: SettingsFile { static var `default`: SettingsFile {
.init( .init(
paths: .default, paths: .default,
navigationItems: [], navigation: .default,
posts: .default, posts: .default,
pages: .default, pages: .default,
german: .default, german: .default,

View File

@ -0,0 +1,27 @@
import SwiftUI
struct TextFieldPropertyView: View {
let title: LocalizedStringKey
@Binding
var text: String
let prompt: String?
let footer: LocalizedStringKey
init(title: LocalizedStringKey, text: Binding<String>, prompt: String? = nil, footer: LocalizedStringKey) {
self.title = title
self._text = text
self.prompt = prompt
self.footer = footer
}
var body: some View {
GenericPropertyView(title: title, footer: footer) {
DescriptionField(text: $text)
.textFieldStyle(.roundedBorder)
}
}
}

View File

@ -60,6 +60,11 @@ struct GenerationContentView: View {
Text("\(content.results.requiredFiles.count) files") Text("\(content.results.requiredFiles.count) files")
} }
List { List {
Section("Empty pages") {
ForEach(content.results.emptyPages.sorted()) { id in
Text("\(id.pageId) (\(id.language))")
}
}
Section("Inaccessible files") { Section("Inaccessible files") {
ForEach(content.results.inaccessibleFiles.sorted()) { file in ForEach(content.results.inaccessibleFiles.sorted()) { file in
Text(file.id) Text(file.id)

View File

@ -0,0 +1,16 @@
import SwiftUI
struct LocalizedNavigationBarSettingsView: View {
@ObservedObject
var settings: LocalizedNavigationSettings
var body: some View {
VStack(alignment: .leading) {
StringPropertyView(
title: "Root URL",
text: $settings.rootUrl,
footer: "The url leading to the root page for the language")
}
}
}

View File

@ -7,30 +7,20 @@ struct LocalizedPostFeedSettingsView: View {
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Title") StringPropertyView(
.font(.headline) title: "Title",
TextField("", text: $settings.title) text: $settings.title,
.textFieldStyle(.roundedBorder) footer: "The title of all post feed pages.")
.frame(maxWidth: 400)
Text("The title of all post feed pages.")
.foregroundStyle(.secondary)
.padding(.bottom)
Text("URL prefix") StringPropertyView(
.font(.headline) title: "URL prefix",
TextField("", text: $settings.feedUrlPrefix) text: $settings.feedUrlPrefix,
.textFieldStyle(.roundedBorder) footer: "The prefix to generate the urls for all post feed pages.")
.frame(maxWidth: 400)
Text("The prefix to generate the urls for all post feed pages.")
.foregroundStyle(.secondary)
.padding(.bottom)
Text("Description") TextFieldPropertyView(
.font(.headline) title: "Description",
DescriptionField(text: $settings.description) text: $settings.description,
Text("The description of all post feed pages.") footer: "The description of all post feed pages.")
.foregroundStyle(.secondary)
.padding(.bottom)
} }
} }
} }

View File

@ -35,19 +35,21 @@ struct NavigationBarSettingsView: View {
.buttonStyle(.plain) .buttonStyle(.plain)
} }
ForEach(content.settings.navigationItems) { tag in ForEach(content.settings.navigation.navigationItems) { tag in
TagView(text: tag.title(in: language)) TagView(text: tag.title(in: language))
.foregroundStyle(.white) .foregroundStyle(.white)
} }
Text("Select the tags to show in the navigation bar. The number should be even.") Text("Select the tags to show in the navigation bar. The number should be even.")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
LocalizedNavigationBarSettingsView(settings: content.settings.navigation.localized(in: language))
} }
.padding() .padding()
} }
.sheet(isPresented: $showItemPicker) { .sheet(isPresented: $showItemPicker) {
ItemSelectionView( ItemSelectionView(
isPresented: $showItemPicker, isPresented: $showItemPicker,
selectedItems: $content.settings.navigationItems) selectedItems: $content.settings.navigation.navigationItems)
} }
} }
} }