diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index 1f3dff4..4cf9e4a 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -85,7 +85,6 @@ E25DA5912D023A8400AEF16D /* IntegerField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5902D023A7E00AEF16D /* IntegerField.swift */; }; E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5922D023B3600AEF16D /* PageSettingsFile.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 */; }; 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 */; }; E2FE0EF62D1D6DF1002963B7 /* Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF52D1D6DEE002963B7 /* Icon.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 */ /* Begin PBXFileReference section */ @@ -273,7 +286,6 @@ 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 /* 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 = ""; }; E29D311F2D0320E20051B7F4 /* ContentLabels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLabels.swift; sourceTree = ""; }; @@ -383,6 +395,20 @@ E2FE0EF32D1D6D22002963B7 /* GeneralIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralIcons.swift; sourceTree = ""; }; E2FE0EF52D1D6DEE002963B7 /* Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icon.swift; sourceTree = ""; }; E2FE0EF72D1D810C002963B7 /* IconCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconCommandProcessor.swift; sourceTree = ""; }; + E2FE0EF92D25AFB5002963B7 /* PageHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHeader.swift; sourceTree = ""; }; + E2FE0EFB2D266D18002963B7 /* NavigationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSettings.swift; sourceTree = ""; }; + E2FE0EFD2D266DA1002963B7 /* NavigationSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSettingsFile.swift; sourceTree = ""; }; + E2FE0EFF2D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedNavigationSettingsFile.swift; sourceTree = ""; }; + E2FE0F012D266FCB002963B7 /* LocalizedNavigationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedNavigationSettings.swift; sourceTree = ""; }; + E2FE0F032D2671FC002963B7 /* LocalizedNavigationBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedNavigationBarSettingsView.swift; sourceTree = ""; }; + E2FE0F052D26734E002963B7 /* TextFieldPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldPropertyView.swift; sourceTree = ""; }; + E2FE0F082D2689F0002963B7 /* TagPageGeneratorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagPageGeneratorSource.swift; sourceTree = ""; }; + E2FE0F0A2D2689FF002963B7 /* FeedGeneratorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedGeneratorSource.swift; sourceTree = ""; }; + E2FE0F0C2D268A09002963B7 /* PostListPageGeneratorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListPageGeneratorSource.swift; sourceTree = ""; }; + E2FE0F0E2D268D4B002963B7 /* BoxCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxCommandProcessor.swift; sourceTree = ""; }; + E2FE0F102D268E78002963B7 /* PageCodeProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageCodeProcessor.swift; sourceTree = ""; }; + E2FE0F142D269188002963B7 /* PageHtmlProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHtmlProcessor.swift; sourceTree = ""; }; + E2FE0F162D2698D5002963B7 /* LocalizedPageId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageId.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -406,6 +432,7 @@ E229901A2D0E3F09009F8D77 /* Item */ = { isa = PBXGroup; children = ( + E2FE0F162D2698D5002963B7 /* LocalizedPageId.swift */, E229902B2D0F6FC0009F8D77 /* ItemId.swift */, E229901D2D0E4362009F8D77 /* LocalizedItem.swift */, E29D31A22D0CC98B0051B7F4 /* Item.swift */, @@ -431,6 +458,8 @@ E25DA5322D0041C400AEF16D /* Settings */ = { isa = PBXGroup; children = ( + E2FE0EFF2D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift */, + E2FE0EFD2D266DA1002963B7 /* NavigationSettingsFile.swift */, E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */, E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */, E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */, @@ -443,10 +472,12 @@ E25DA53B2D0042EA00AEF16D /* Settings */ = { isa = PBXGroup; children = ( - E29D31952D0C18690051B7F4 /* PathSettings.swift */, - E25DA58E2D02368A00AEF16D /* PageSettings.swift */, - E25DA5402D00446700AEF16D /* PostSettings.swift */, + E2FE0F012D266FCB002963B7 /* LocalizedNavigationSettings.swift */, E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */, + E2FE0EFB2D266D18002963B7 /* NavigationSettings.swift */, + E25DA58E2D02368A00AEF16D /* PageSettings.swift */, + E29D31952D0C18690051B7F4 /* PathSettings.swift */, + E25DA5402D00446700AEF16D /* PostSettings.swift */, E21850322CFAFA200090B18B /* Settings.swift */, ); path = Settings; @@ -455,6 +486,7 @@ E25DA5782D01C56200AEF16D /* Generator */ = { isa = PBXGroup; children = ( + E2FE0F072D2689DC002963B7 /* Post Lists */, E29D31B62D0DAC030051B7F4 /* Page Content */, E22990232D0EDBD0009F8D77 /* HeaderElement.swift */, E29D31842D0AE8EE0051B7F4 /* KnownHeaderElement.swift */, @@ -465,7 +497,6 @@ E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */, E25DA5982D02401A00AEF16D /* PageGenerator.swift */, E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */, - E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */, E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */, E29D31252D0370A50051B7F4 /* VideoOption.swift */, E29D318F2D0B34870051B7F4 /* GenerationAnomaly.swift */, @@ -540,6 +571,9 @@ E29D31B62D0DAC030051B7F4 /* Page Content */ = { isa = PBXGroup; children = ( + E2FE0F142D269188002963B7 /* PageHtmlProcessor.swift */, + E2FE0F102D268E78002963B7 /* PageCodeProcessor.swift */, + E2FE0F0E2D268D4B002963B7 /* BoxCommandProcessor.swift */, E2FE0EF72D1D810C002963B7 /* IconCommandProcessor.swift */, E2FE0EED2D1C22EF002963B7 /* InlineLinkProcessor.swift */, E29D31BD2D0DB8560051B7F4 /* AudioPlayerCommand.swift */, @@ -583,6 +617,7 @@ E25DA5442D00952D00AEF16D /* SettingsSection.swift */, E2A21C352CB9A3D70060935B /* PathSettingsView.swift */, E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */, + E2FE0F032D2671FC002963B7 /* LocalizedNavigationBarSettingsView.swift */, E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */, E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */, E229901F2D0ECBD4009F8D77 /* TagOverviewDetailView.swift */, @@ -593,27 +628,28 @@ E2A21C372CB9A4F10060935B /* Generic */ = { isa = PBXGroup; 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 */, E22990312D0F7678009F8D77 /* DatePropertyView.swift */, - E22990332D0F77E4009F8D77 /* PagePropertyView.swift */, - E22990352D0F79CC009F8D77 /* OptionalStringPropertyView.swift */, - E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */, - E22990372D0F7B2C009F8D77 /* OptionalImagePropertyView.swift */, - E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */, + E29D312F2D03A2BD0051B7F4 /* DescriptionField.swift */, + E22990292D0F5A10009F8D77 /* DetailTitle.swift */, + E22990252D0F5822009F8D77 /* FilePropertyView.swift */, + E2A21C0F2CB18B390060935B /* FlowHStack.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; sourceTree = ""; @@ -686,15 +722,6 @@ path = Model; sourceTree = ""; }; - E2B85F3E2C4293FF0047CD0C /* Pages */ = { - isa = PBXGroup; - children = ( - E25DA5962D023F9900AEF16D /* ContentPage.swift */, - E25DA51C2CFF135B00AEF16D /* GenericPage.swift */, - ); - path = Pages; - sourceTree = ""; - }; E2B85F3F2C42946E0047CD0C /* Page Elements */ = { isa = PBXGroup; children = ( @@ -702,12 +729,14 @@ E25DA58C2D021BA000AEF16D /* WebsiteImage.swift */, E25DA58A2D020C9200AEF16D /* PageImage.swift */, E25DA51E2CFF15C100AEF16D /* NavigationBar.swift */, + E2FE0EF92D25AFB5002963B7 /* PageHeader.swift */, E25DA51A2CFF08AF00AEF16D /* PostFeedPageNavigation.swift */, E2B85F422C4294F60047CD0C /* FeedEntry.swift */, E2A21C452CBAE2E50060935B /* FeedEntryContent.swift */, E2A21C272CB29B290060935B /* FeedEntryData.swift */, E2B85F442C429ED60047CD0C /* ImageGallery.swift */, E2B85F402C4294790047CD0C /* PageHead.swift */, + E25DA51C2CFF135B00AEF16D /* GenericPage.swift */, ); path = "Page Elements"; sourceTree = ""; @@ -785,7 +814,6 @@ E2B85F392C428F020047CD0C /* Model */, E2B85F462C42C7CA0047CD0C /* Views */, E2B85F3F2C42946E0047CD0C /* Page Elements */, - E2B85F3E2C4293FF0047CD0C /* Pages */, E2DD04792C276F32003BFF1F /* Assets.xcassets */, E2DD047B2C276F32003BFF1F /* CHDataManagement.entitlements */, E2B85F552C4BD0AD0047CD0C /* Extensions */, @@ -809,6 +837,17 @@ path = "Preview Content"; sourceTree = ""; }; + E2FE0F072D2689DC002963B7 /* Post Lists */ = { + isa = PBXGroup; + children = ( + E2FE0F0C2D268A09002963B7 /* PostListPageGeneratorSource.swift */, + E2FE0F0A2D2689FF002963B7 /* FeedGeneratorSource.swift */, + E2FE0F082D2689F0002963B7 /* TagPageGeneratorSource.swift */, + E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */, + ); + path = "Post Lists"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -899,7 +938,9 @@ files = ( E29D31242D0366860051B7F4 /* TagList.swift in Sources */, E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */, + E2FE0EFE2D266DA5002963B7 /* NavigationSettingsFile.swift in Sources */, E2FE0EE62D15A0B5002963B7 /* GenerationResults.swift in Sources */, + E2FE0F152D26918F002963B7 /* PageHtmlProcessor.swift in Sources */, E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */, E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */, E229904C2D10BE5D009F8D77 /* InitialSetupView.swift in Sources */, @@ -909,6 +950,7 @@ E29D313B2D04464A0051B7F4 /* LocalizedTagDetailView.swift in Sources */, E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */, E29D31BA2D0DB5080051B7F4 /* LabelsCommand.swift in Sources */, + E2FE0EFC2D266D22002963B7 /* NavigationSettings.swift in Sources */, E29D31692D0702700051B7F4 /* TextFileContentView.swift in Sources */, E25DA5412D00446C00AEF16D /* PostSettings.swift in Sources */, E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */, @@ -922,6 +964,7 @@ E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */, E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */, E21850172CEE55FC0090B18B /* FileType.swift in Sources */, + E2FE0F042D267206002963B7 /* LocalizedNavigationBarSettingsView.swift in Sources */, E29D31B12D0DA5510051B7F4 /* ContentIcon.swift in Sources */, E2A37D112CE537800000979F /* PageFile.swift in Sources */, E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */, @@ -947,7 +990,6 @@ E25DA58D2D021BA400AEF16D /* WebsiteImage.swift in Sources */, E25DA5992D02401E00AEF16D /* PageGenerator.swift in Sources */, E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */, - E25DA5972D023F9F00AEF16D /* ContentPage.swift in Sources */, E2B85F3D2C4293F80047CD0C /* FeedPageGenerator.swift in Sources */, E229902C2D0F6FC6009F8D77 /* ItemId.swift in Sources */, E25DA5952D023BD100AEF16D /* PageSettingsDetailView.swift in Sources */, @@ -962,6 +1004,7 @@ E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */, E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */, E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */, + E2FE0F092D2689F0002963B7 /* TagPageGeneratorSource.swift in Sources */, E22990302D0F75DE009F8D77 /* BoolPropertyView.swift in Sources */, E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */, E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */, @@ -977,6 +1020,8 @@ E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */, E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */, E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */, + E2FE0F172D2698D5002963B7 /* LocalizedPageId.swift in Sources */, + E2FE0F0D2D268A09002963B7 /* PostListPageGeneratorSource.swift in Sources */, E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */, E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */, E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */, @@ -988,6 +1033,7 @@ E25DA57A2D01C64400AEF16D /* PageContentGenerator.swift in Sources */, E2A21C202CB28ED20060935B /* MockImage.swift in Sources */, E2FE0EEE2D1C22F3002963B7 /* InlineLinkProcessor.swift in Sources */, + E2FE0F022D266FCB002963B7 /* LocalizedNavigationSettings.swift in Sources */, E29D313F2D04822C0051B7F4 /* AddPostView.swift in Sources */, E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */, E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */, @@ -997,6 +1043,7 @@ E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */, E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */, E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */, + E2FE0F062D267350002963B7 /* TextFieldPropertyView.swift in Sources */, E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */, E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */, E2B85F412C4294790047CD0C /* PageHead.swift in Sources */, @@ -1009,6 +1056,7 @@ E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */, E22990222D0ED12E009F8D77 /* TagOverviewFile.swift in Sources */, E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */, + E2FE0F002D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift in Sources */, E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */, E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */, E229904A2D10BB90009F8D77 /* SecurityScopeBookmark.swift in Sources */, @@ -1024,8 +1072,10 @@ E29D31A52D0CD03F0051B7F4 /* FileSelectionView.swift in Sources */, E22990322D0F767B009F8D77 /* DatePropertyView.swift in Sources */, E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */, + E2FE0EFA2D25AFBA002963B7 /* PageHeader.swift in Sources */, E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */, E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */, + E2FE0F0B2D2689FF002963B7 /* FeedGeneratorSource.swift in Sources */, E2DD04742C276F31003BFF1F /* MainView.swift in Sources */, E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */, E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */, @@ -1038,10 +1088,12 @@ E29D31C32D0DBEF20051B7F4 /* Song.swift in Sources */, E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */, E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */, + E2FE0F112D268E7E002963B7 /* PageCodeProcessor.swift in Sources */, E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */, E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */, E22990192D0E3546009F8D77 /* ItemType.swift in Sources */, E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */, + E2FE0F0F2D268D4F002963B7 /* BoxCommandProcessor.swift in Sources */, E22990482D10B7B7009F8D77 /* StorageAccessError.swift in Sources */, E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */, E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */, diff --git a/CHDataManagement/Generator/FeedPageGenerator.swift b/CHDataManagement/Generator/FeedPageGenerator.swift index 229b71c..92e68e8 100644 --- a/CHDataManagement/Generator/FeedPageGenerator.swift +++ b/CHDataManagement/Generator/FeedPageGenerator.swift @@ -2,28 +2,34 @@ import Foundation final class FeedPageGenerator { + let results: PageGenerationResults + let content: Content - init(content: Content) { + init(content: Content, results: PageGenerationResults) { self.content = content + self.results = results } private func includeSwiper(in headers: inout Set) { if let swiperCss = content.settings.posts.swiperCssFile { headers.insert(.css(file: swiperCss, order: HeaderElement.swiperCssFileOrder)) + results.require(file: swiperCss) } if let swiperJs = content.settings.posts.swiperJsFile { headers.insert(.js(file: swiperJs, defer: true)) + results.require(file: swiperJs) } } func generatePage(language: ContentLanguage, posts: [FeedEntryData], title: String, - description: String, + description: String?, showTitle: Bool, pageNumber: Int, - totalPages: Int) -> String { + totalPages: Int, + languageButtonUrl: String) -> String { var headers = content.defaultPageHeaders var footer = "" if posts.contains(where: { $0.images.count > 1 }) { @@ -32,12 +38,23 @@ final class FeedPageGenerator { 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, title: title, description: description, + iconUrl: iconUrl, + languageButton: languageButton, links: content.navigationBar(in: language), headers: headers, + icons: []) + + let page = GenericPage( + header: pageHeader, additionalFooter: footer) { content in if showTitle { content += "

\(title)

" @@ -48,7 +65,6 @@ final class FeedPageGenerator { if totalPages > 1 { content += PostFeedPageNavigation(currentPage: pageNumber, numberOfPages: totalPages, language: language).content } - } return page.content } diff --git a/CHDataManagement/Generator/GenerationResults.swift b/CHDataManagement/Generator/GenerationResults.swift index 128bf16..d1c7868 100644 --- a/CHDataManagement/Generator/GenerationResults.swift +++ b/CHDataManagement/Generator/GenerationResults.swift @@ -1,12 +1,5 @@ import Foundation -struct LocalizedPageId: Hashable { - - let language: ContentLanguage - - let pageId: String -} - final class GenerationResults: ObservableObject { /// The files that could not be accessed @@ -181,6 +174,10 @@ final class GenerationResults: ObservableObject { func unsaved(_ path: String) { update { self.unsavedOutputFiles.insert(path) } } + + func empty(_ page: LocalizedPageId) { + update {self.emptyPages.insert(page) } + } } private extension Dictionary where Value == Set { diff --git a/CHDataManagement/Generator/Page Content/BoxCommandProcessor.swift b/CHDataManagement/Generator/Page Content/BoxCommandProcessor.swift new file mode 100644 index 0000000..f99ac73 --- /dev/null +++ b/CHDataManagement/Generator/Page Content/BoxCommandProcessor.swift @@ -0,0 +1,24 @@ + +struct BoxCommandProcessor: CommandProcessor { + + let commandType: ShorthandMarkdownKey = .box + + let results: PageGenerationResults + + init(content: Content, results: PageGenerationResults) { + self.results = results + } + + /** + Format: `![box](;<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 + } +} diff --git a/CHDataManagement/Generator/Page Content/IconCommandProcessor.swift b/CHDataManagement/Generator/Page Content/IconCommandProcessor.swift index b52a273..9b8fdef 100644 --- a/CHDataManagement/Generator/Page Content/IconCommandProcessor.swift +++ b/CHDataManagement/Generator/Page Content/IconCommandProcessor.swift @@ -3,7 +3,6 @@ struct IconCommandProcessor: CommandProcessor { let commandType: ShorthandMarkdownKey = .icons - let results: PageGenerationResults init(content: Content, results: PageGenerationResults) { diff --git a/CHDataManagement/Generator/Page Content/InlineLinkProcessor.swift b/CHDataManagement/Generator/Page Content/InlineLinkProcessor.swift index c69ffb7..b980280 100644 --- a/CHDataManagement/Generator/Page Content/InlineLinkProcessor.swift +++ b/CHDataManagement/Generator/Page Content/InlineLinkProcessor.swift @@ -13,7 +13,7 @@ struct InlineLinkProcessor { let language: ContentLanguage - func handleLink(html: String, markdown: Substring) -> String { + func process(html: String, markdown: Substring) -> String { let url = markdown.between("(", and: ")") if url.hasPrefix(pageLinkMarker) { return handleInlinePageLink(url: url, html: html, markdown: markdown) @@ -56,7 +56,7 @@ struct InlineLinkProcessor { return markdown.between("[", and: "]") } results.linked(to: tag) - let tagPath = content.absoluteUrlToTag(tag, language: language) + let tagPath = tag.absoluteUrl(in: language) return html.replacingOccurrences(of: textToChange, with: tagPath) } diff --git a/CHDataManagement/Generator/Page Content/PageCodeProcessor.swift b/CHDataManagement/Generator/Page Content/PageCodeProcessor.swift new file mode 100644 index 0000000..049d94f --- /dev/null +++ b/CHDataManagement/Generator/Page Content/PageCodeProcessor.swift @@ -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>" + } +} diff --git a/CHDataManagement/Generator/Page Content/PageHtmlProcessor.swift b/CHDataManagement/Generator/Page Content/PageHtmlProcessor.swift new file mode 100644 index 0000000..d31d645 --- /dev/null +++ b/CHDataManagement/Generator/Page Content/PageHtmlProcessor.swift @@ -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) + } + } + +} diff --git a/CHDataManagement/Generator/PageContentGenerator.swift b/CHDataManagement/Generator/PageContentGenerator.swift index b69d9ce..f4d1e32 100644 --- a/CHDataManagement/Generator/PageContentGenerator.swift +++ b/CHDataManagement/Generator/PageContentGenerator.swift @@ -1,29 +1,33 @@ import Foundation import Ink -import Splash -import SwiftSoup final class PageContentParser { - private static let codeHighlightFooter = "<script>hljs.highlightAll();</script>" - - private let swift = SyntaxHighlighter(format: HTMLOutputFormat()) - private let language: ContentLanguage private let content: Content private let results: PageGenerationResults + // MARK: Command handlers + private let buttonHandler: ButtonCommandProcessor private let labelHandler: LabelsCommandProcessor 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 icons: IconCommandProcessor + private let code: PageCodeProcessor var largeImageWidth: Int { content.settings.pages.largeImageWidth @@ -40,127 +44,25 @@ final class PageContentParser { self.buttonHandler = .init(content: content, results: results) self.labelHandler = .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.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 { let parser = MarkdownParser(modifiers: [ Modifier(target: .images, closure: processMarkdownImage), - Modifier(target: .codeBlocks, closure: handleCode), - Modifier(target: .links, closure: inlineLink.handleLink), - Modifier(target: .html, closure: handleHTML), + Modifier(target: .codeBlocks, closure: code.process), + Modifier(target: .links, closure: inlineLink.process), + Modifier(target: .html, closure: html.process), Modifier(target: .headings, closure: handleHeadlines) ]) 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 @@ -218,9 +120,9 @@ final class PageContentParser { case .pageLink: return handlePageLink(arguments, markdown: markdown) case .includedHtml: - return handleExternalHtml(arguments, markdown: markdown) + return self.html.process(arguments, markdown: markdown) case .box: - return handleSimpleBox(arguments, markdown: markdown) + return box.process(arguments, markdown: markdown) case .model: return handleModel(arguments, markdown: markdown) case .svg: @@ -343,37 +245,6 @@ final class PageContentParser { 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>)` */ diff --git a/CHDataManagement/Generator/PageGenerationResults.swift b/CHDataManagement/Generator/PageGenerationResults.swift index 39f1419..f7c1c4a 100644 --- a/CHDataManagement/Generator/PageGenerationResults.swift +++ b/CHDataManagement/Generator/PageGenerationResults.swift @@ -73,6 +73,8 @@ final class PageGenerationResults: ObservableObject { /// The files that could not be saved to the output folder private(set) var unsavedOutputFiles: [String: Set<ItemType>] = [:] + private(set) var pageIsEmpty: Bool + init(itemId: ItemId, delegate: GenerationResults) { self.itemId = itemId self.delegate = delegate @@ -94,33 +96,7 @@ final class PageGenerationResults: ObservableObject { invalidCommands = [] warnings = [] unsavedOutputFiles = [:] - } - - 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) + pageIsEmpty = false } // MARK: Adding entries @@ -231,5 +207,11 @@ final class PageGenerationResults: ObservableObject { unsavedOutputFiles[path, default: []].insert(source) 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)) + } } diff --git a/CHDataManagement/Generator/PageGenerator.swift b/CHDataManagement/Generator/PageGenerator.swift index 073a14e..06a97b5 100644 --- a/CHDataManagement/Generator/PageGenerator.swift +++ b/CHDataManagement/Generator/PageGenerator.swift @@ -19,13 +19,32 @@ final class PageGenerator { 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? { let contentGenerator = PageContentParser( content: content, language: language, results: results) - guard let rawPageContent = content.storage.pageContent(for: page.id, language: language) else { - return nil + let rawPageContent: String + 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) @@ -34,27 +53,41 @@ final class PageGenerator { let tags: [FeedEntryData.Tag] = page.tags.map { tag in .init(name: tag.localized(in: language).name, - url: content.absoluteUrlToTag(tag, language: language)) + url: tag.absoluteUrl(in: language)) } let headers = makeHeaders(requiredItems: results.requiredHeaders) results.require(files: headers.compactMap { $0.file }) - let fullPage = ContentPage( - language: language, - dateString: page.dateText(in: language), - title: localized.title, - showTitle: !localized.hideTitle, - 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 + let iconUrl = content.settings.navigation.localized(in: language).rootUrl + let languageUrl = page.absoluteUrl(in: language.next) + let languageButton = NavigationBar.Link( + text: language.next.rawValue, + url: languageUrl) - 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 } } diff --git a/CHDataManagement/Generator/Post Lists/FeedGeneratorSource.swift b/CHDataManagement/Generator/Post Lists/FeedGeneratorSource.swift new file mode 100644 index 0000000..7085fbc --- /dev/null +++ b/CHDataManagement/Generator/Post Lists/FeedGeneratorSource.swift @@ -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 + } +} diff --git a/CHDataManagement/Generator/PostListPageGenerator.swift b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift similarity index 62% rename from CHDataManagement/Generator/PostListPageGenerator.swift rename to CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift index 5f8f8ca..961541b 100644 --- a/CHDataManagement/Generator/PostListPageGenerator.swift +++ b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift @@ -2,42 +2,26 @@ import Foundation 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 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 language: ContentLanguage { + source.language } private var mainContentMaximumWidth: Int { - content.settings.posts.contentWidth + source.content.settings.posts.contentWidth } 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]) { @@ -67,7 +51,7 @@ final class PostListPageGenerator { let tags: [FeedEntryData.Tag] = post.tags.filter { $0.isVisible }.map { tag in .init(name: tag.localized(in: language).name, - url: content.absoluteUrlToTag(tag, language: language)) + url: tag.absoluteUrl(in: language)) } let images = localized.images.map(createFeedImage) @@ -82,25 +66,28 @@ final class PostListPageGenerator { 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( language: language, posts: posts, - title: pageTitle, - description: pageDescription, - showTitle: showTitle, + title: source.pageTitle, + description: source.pageDescription, + showTitle: source.showTitle, pageNumber: pageIndex, - totalPages: pageCount) - let filePath = "\(pageUrlPrefix)/\(pageIndex).html" + totalPages: pageCount, + languageButtonUrl: languageButtonUrl) + let filePath = pageUrl(in: language, pageNumber: pageIndex) guard save(fileContent, to: filePath) else { - results.unsavedOutput(filePath, source: .feed) + source.results.unsavedOutput(filePath, source: .feed) return } } private func createFeedImage(for image: FileResource) -> FeedEntryData.Image { - results.requireImageSet(for: image, size: mainContentMaximumWidth) + source.results.requireImageSet(for: image, size: mainContentMaximumWidth) return .init( rawImagePath: image.absoluteUrl, width: mainContentMaximumWidth, @@ -109,6 +96,6 @@ final class PostListPageGenerator { } private func save(_ content: String, to relativePath: String) -> Bool { - self.content.storage.write(content, to: relativePath) + source.content.storage.write(content, to: relativePath) } } diff --git a/CHDataManagement/Generator/Post Lists/PostListPageGeneratorSource.swift b/CHDataManagement/Generator/Post Lists/PostListPageGeneratorSource.swift new file mode 100644 index 0000000..a5d8b09 --- /dev/null +++ b/CHDataManagement/Generator/Post Lists/PostListPageGeneratorSource.swift @@ -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 +} diff --git a/CHDataManagement/Generator/Post Lists/TagPageGeneratorSource.swift b/CHDataManagement/Generator/Post Lists/TagPageGeneratorSource.swift new file mode 100644 index 0000000..27f9909 --- /dev/null +++ b/CHDataManagement/Generator/Post Lists/TagPageGeneratorSource.swift @@ -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) + } +} diff --git a/CHDataManagement/Generator/ShorthandMarkdownKey.swift b/CHDataManagement/Generator/ShorthandMarkdownKey.swift index 9816292..ab8831c 100644 --- a/CHDataManagement/Generator/ShorthandMarkdownKey.swift +++ b/CHDataManagement/Generator/ShorthandMarkdownKey.swift @@ -37,7 +37,7 @@ enum ShorthandMarkdownKey: String { /// Format: `![tag](<tagId>)` case tagLink = "tag" - /// Additional HTML code include verbatim into the page. + /// Additional HTML code included verbatim into the page. /// Format: `![html](<fileId>)` case includedHtml = "html" diff --git a/CHDataManagement/Main/MainView.swift b/CHDataManagement/Main/MainView.swift index 1f21905..8d59c22 100644 --- a/CHDataManagement/Main/MainView.swift +++ b/CHDataManagement/Main/MainView.swift @@ -5,6 +5,7 @@ import SFSafeSymbols #warning("Fix CV") #warning("Fix endeavor basics (image compare)") +#warning("Add custom url string to external files (optional)") #warning("Show all warnings on page content") #warning("Button to delete file") #warning("Add link to other language") diff --git a/CHDataManagement/Model/Content+Generation.swift b/CHDataManagement/Model/Content+Generation.swift index 370e782..9d78a1a 100644 --- a/CHDataManagement/Model/Content+Generation.swift +++ b/CHDataManagement/Model/Content+Generation.swift @@ -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 func page(_ pageId: String) -> Page? { @@ -145,7 +131,7 @@ extension Content { // MARK: Generation input func navigationBar(in language: ContentLanguage) -> [NavigationBar.Link] { - settings.navigationItems.map { + settings.navigation.navigationItems.map { .init(text: $0.title(in: language), url: $0.absoluteUrl(in: language)) } @@ -197,9 +183,6 @@ extension Content { continue } for language in ContentLanguage.allCases { - guard page.hasContent(in: language) else { - continue - } generateInternal(page, in: language) } } @@ -213,14 +196,12 @@ extension Content { for language in ContentLanguage.allCases { guard shouldGenerateWebsite else { return } let results = results.makeResults(for: .feed, in: language) - let generator = PostListPageGenerator( + let source = FeedGeneratorSource( language: language, content: self, - results: results, - showTitle: false, - pageTitle: settings.localized(in: language).title, - pageDescription: settings.localized(in: language).description, - pageUrlPrefix: settings.localized(in: language).feedUrlPrefix) + results: results) + + let generator = PostListPageGenerator(source: source) generator.createPages(for: posts) } @@ -249,16 +230,12 @@ extension Content { let posts = posts.filter { $0.tags.contains(tag) } guard posts.count > 0 else { continue } - let localized = tag.localized(in: language) - let urlPrefix = absoluteUrlPrefixForTag(tag, language: language) - let generator = PostListPageGenerator( + let source = TagPageGeneratorSource( language: language, content: self, results: results, - showTitle: true, - pageTitle: localized.name, - pageDescription: localized.description ?? "", - pageUrlPrefix: urlPrefix) + tag: tag) + let generator = PostListPageGenerator(source: source) generator.createPages(for: posts) } } diff --git a/CHDataManagement/Model/Content+Load.swift b/CHDataManagement/Model/Content+Load.swift index f7b9f62..8f0b210 100644 --- a/CHDataManagement/Model/Content+Load.swift +++ b/CHDataManagement/Model/Content+Load.swift @@ -146,52 +146,10 @@ extension Content { self.files = files.values.sorted { $0.id } self.posts = posts.values.sorted(ascending: false) { $0.startDate } 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") } - 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] { pagesData.reduce(into: [:]) { pages, data in let (pageId, page) = data diff --git a/CHDataManagement/Model/Content+Save.swift b/CHDataManagement/Model/Content+Save.swift index 19a6b58..4e95a7a 100644 --- a/CHDataManagement/Model/Content+Save.swift +++ b/CHDataManagement/Model/Content+Save.swift @@ -133,37 +133,3 @@ private extension LocalizedTag { 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) - } -} diff --git a/CHDataManagement/Model/ContentLanguage.swift b/CHDataManagement/Model/ContentLanguage.swift index 1caba02..90611f5 100644 --- a/CHDataManagement/Model/ContentLanguage.swift +++ b/CHDataManagement/Model/ContentLanguage.swift @@ -32,3 +32,13 @@ extension ContentLanguage: Comparable { lhs.rawValue < rhs.rawValue } } + +extension ContentLanguage { + + var next: ContentLanguage { + switch self { + case .english: return .german + case .german: return .english + } + } +} diff --git a/CHDataManagement/Model/FileType.swift b/CHDataManagement/Model/FileType.swift index 9604966..d7c582c 100644 --- a/CHDataManagement/Model/FileType.swift +++ b/CHDataManagement/Model/FileType.swift @@ -67,6 +67,8 @@ enum FileType: String { case js + case ttf + // MARK: Text case json @@ -143,7 +145,7 @@ enum FileType: String { return .video case .mp3, .aac: return .audio - case .js, .css: + case .js, .css, .ttf: return .asset case .json, .conf, .yaml: return .text @@ -195,7 +197,7 @@ enum FileType: String { var isAsset: Bool { switch self { - case .js, .css: + case .js, .css, .ttf: return true default: return false diff --git a/CHDataManagement/Model/Item/LocalizedPageId.swift b/CHDataManagement/Model/Item/LocalizedPageId.swift new file mode 100644 index 0000000..195c689 --- /dev/null +++ b/CHDataManagement/Model/Item/LocalizedPageId.swift @@ -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 { + +} diff --git a/CHDataManagement/Model/Settings/LocalizedNavigationSettings.swift b/CHDataManagement/Model/Settings/LocalizedNavigationSettings.swift new file mode 100644 index 0000000..ffc92c2 --- /dev/null +++ b/CHDataManagement/Model/Settings/LocalizedNavigationSettings.swift @@ -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: "/") + } +} diff --git a/CHDataManagement/Model/Settings/LocalizedPostSettings.swift b/CHDataManagement/Model/Settings/LocalizedPostSettings.swift index bc21c5f..8c10414 100644 --- a/CHDataManagement/Model/Settings/LocalizedPostSettings.swift +++ b/CHDataManagement/Model/Settings/LocalizedPostSettings.swift @@ -17,9 +17,18 @@ final class LocalizedPostSettings: ObservableObject { self.feedUrlPrefix = feedUrlPrefix } + // MARK: Storage + init(file: LocalizedPostSettingsFile) { self.title = file.feedTitle self.description = file.feedDescription self.feedUrlPrefix = file.feedUrlPrefix } + + var file: LocalizedPostSettingsFile { + .init( + feedTitle: title, + feedDescription: description, + feedUrlPrefix: feedUrlPrefix) + } } diff --git a/CHDataManagement/Model/Settings/NavigationSettings.swift b/CHDataManagement/Model/Settings/NavigationSettings.swift new file mode 100644 index 0000000..8fc9668 --- /dev/null +++ b/CHDataManagement/Model/Settings/NavigationSettings.swift @@ -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) + } +} diff --git a/CHDataManagement/Model/Settings/PostSettings.swift b/CHDataManagement/Model/Settings/PostSettings.swift index 3656ea6..b159eea 100644 --- a/CHDataManagement/Model/Settings/PostSettings.swift +++ b/CHDataManagement/Model/Settings/PostSettings.swift @@ -31,6 +31,8 @@ final class PostSettings: ObservableObject { self.defaultCssFile = defaultCssFile } + // MARK: Storage + init(file: PostSettingsFile, files: [String : FileResource]) { self.postsPerPage = file.postsPerPage self.contentWidth = file.contentWidth @@ -38,4 +40,12 @@ final class PostSettings: ObservableObject { self.swiperJsFile = file.swiperJsFile.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) + } } diff --git a/CHDataManagement/Model/Settings/Settings.swift b/CHDataManagement/Model/Settings/Settings.swift index 896570e..beea212 100644 --- a/CHDataManagement/Model/Settings/Settings.swift +++ b/CHDataManagement/Model/Settings/Settings.swift @@ -7,7 +7,7 @@ final class Settings: ObservableObject { /// The items to show in the navigation bar @Published - var navigationItems: [Item] + var navigation: NavigationSettings @Published var posts: PostSettings @@ -21,9 +21,9 @@ final class Settings: ObservableObject { @Published 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.navigationItems = navigationItems + self.navigation = navigation self.posts = posts self.pages = pages self.german = german @@ -36,4 +36,48 @@ final class Settings: ObservableObject { 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) } diff --git a/CHDataManagement/Page Elements/FeedEntry.swift b/CHDataManagement/Page Elements/FeedEntry.swift index b48315d..41f8d43 100644 --- a/CHDataManagement/Page Elements/FeedEntry.swift +++ b/CHDataManagement/Page Elements/FeedEntry.swift @@ -13,7 +13,7 @@ struct FeedEntry { } var content: String { - var result = "<div class='card\(cardLinkClassText)'>" + var result = "<article><div class='card\(cardLinkClassText)'>" ImageGallery(id: data.entryId, images: data.images) .addContent(to: &result) @@ -34,7 +34,7 @@ struct FeedEntry { if let url = data.link { 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 } } diff --git a/CHDataManagement/Page Elements/GenericPage.swift b/CHDataManagement/Page Elements/GenericPage.swift new file mode 100644 index 0000000..ca96a82 --- /dev/null +++ b/CHDataManagement/Page Elements/GenericPage.swift @@ -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 + } +} diff --git a/CHDataManagement/Page Elements/NavigationBar.swift b/CHDataManagement/Page Elements/NavigationBar.swift index 59f97a9..8cf532e 100644 --- a/CHDataManagement/Page Elements/NavigationBar.swift +++ b/CHDataManagement/Page Elements/NavigationBar.swift @@ -12,25 +12,29 @@ struct NavigationBar: HtmlProducer { 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.languageButton = languageButton + self.iconUrl = iconUrl } 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 += "<nav class='navbar'>" + result += "<div class='fade'></div>" + result += "<div class='centered'>" + result += "<a class='icon-link' href='\(iconUrl)'><div class='icon'></div></a>" + result += "<div class='buttons'><input type='checkbox' id='menu-toggle'>" + result += "<label for='menu-toggle' class='burger'><div></div><div></div><div></div></label>" + result += "<ul>" + for item in links { + result += "<li><a href='\(item.url)'>\(item.text)</a></li>" } - - 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 += "</div></nav>" // Close nav-center, navbar + result += "</ul>" + result += "<a class='lang-button' href='\(languageButton.url)'>\(languageButton.text)</a>" + result += "</div></div></nav>" // Close buttons, centered, navbar } } diff --git a/CHDataManagement/Page Elements/PageHeader.swift b/CHDataManagement/Page Elements/PageHeader.swift new file mode 100644 index 0000000..8ccd7bb --- /dev/null +++ b/CHDataManagement/Page Elements/PageHeader.swift @@ -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 + } +} diff --git a/CHDataManagement/Pages/ContentPage.swift b/CHDataManagement/Pages/ContentPage.swift deleted file mode 100644 index 221a7c7..0000000 --- a/CHDataManagement/Pages/ContentPage.swift +++ /dev/null @@ -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 - } -} diff --git a/CHDataManagement/Pages/GenericPage.swift b/CHDataManagement/Pages/GenericPage.swift deleted file mode 100644 index ad8bc3c..0000000 --- a/CHDataManagement/Pages/GenericPage.swift +++ /dev/null @@ -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 - } -} diff --git a/CHDataManagement/Preview Content/WebsiteData+Mock.swift b/CHDataManagement/Preview Content/WebsiteData+Mock.swift index f8abcf3..290616c 100644 --- a/CHDataManagement/Preview Content/WebsiteData+Mock.swift +++ b/CHDataManagement/Preview Content/WebsiteData+Mock.swift @@ -1,16 +1,5 @@ import Foundation -extension Settings { - - static let `default`: Settings = .init( - paths: .default, - navigationItems: [], - posts: .default, - pages: .default, - german: .german, - english: .english) -} - extension PathSettings { static var `default`: PathSettings { diff --git a/CHDataManagement/Storage/Model/Settings/LocalizedNavigationSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/LocalizedNavigationSettingsFile.swift new file mode 100644 index 0000000..13a1035 --- /dev/null +++ b/CHDataManagement/Storage/Model/Settings/LocalizedNavigationSettingsFile.swift @@ -0,0 +1,17 @@ +import Foundation + +struct LocalizedNavigationSettingsFile { + + let rootUrl: String +} + +extension LocalizedNavigationSettingsFile: Codable { + +} + +extension LocalizedNavigationSettingsFile { + + static var `default`: LocalizedNavigationSettingsFile { + .init(rootUrl: "/") + } +} diff --git a/CHDataManagement/Storage/Model/Settings/NavigationSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/NavigationSettingsFile.swift new file mode 100644 index 0000000..be5b373 --- /dev/null +++ b/CHDataManagement/Storage/Model/Settings/NavigationSettingsFile.swift @@ -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) + } +} diff --git a/CHDataManagement/Storage/Model/Settings/SettingsFile.swift b/CHDataManagement/Storage/Model/Settings/SettingsFile.swift index 4b82b99..4ce17bf 100644 --- a/CHDataManagement/Storage/Model/Settings/SettingsFile.swift +++ b/CHDataManagement/Storage/Model/Settings/SettingsFile.swift @@ -5,7 +5,7 @@ struct SettingsFile { let paths: PathSettingsFile /// The tags to show in the navigation bar - let navigationItems: [String] + let navigation: NavigationSettingsFile let posts: PostSettingsFile @@ -23,7 +23,7 @@ extension SettingsFile { static var `default`: SettingsFile { .init( paths: .default, - navigationItems: [], + navigation: .default, posts: .default, pages: .default, german: .default, diff --git a/CHDataManagement/Views/Generic/TextFieldPropertyView.swift b/CHDataManagement/Views/Generic/TextFieldPropertyView.swift new file mode 100644 index 0000000..6e7b17f --- /dev/null +++ b/CHDataManagement/Views/Generic/TextFieldPropertyView.swift @@ -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) + } + } +} diff --git a/CHDataManagement/Views/Settings/Content/GenerationContentView.swift b/CHDataManagement/Views/Settings/Content/GenerationContentView.swift index e6b0d99..463dfe0 100644 --- a/CHDataManagement/Views/Settings/Content/GenerationContentView.swift +++ b/CHDataManagement/Views/Settings/Content/GenerationContentView.swift @@ -60,6 +60,11 @@ struct GenerationContentView: View { Text("\(content.results.requiredFiles.count) files") } List { + Section("Empty pages") { + ForEach(content.results.emptyPages.sorted()) { id in + Text("\(id.pageId) (\(id.language))") + } + } Section("Inaccessible files") { ForEach(content.results.inaccessibleFiles.sorted()) { file in Text(file.id) diff --git a/CHDataManagement/Views/Settings/LocalizedNavigationBarSettingsView.swift b/CHDataManagement/Views/Settings/LocalizedNavigationBarSettingsView.swift new file mode 100644 index 0000000..684bc33 --- /dev/null +++ b/CHDataManagement/Views/Settings/LocalizedNavigationBarSettingsView.swift @@ -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") + } + } +} diff --git a/CHDataManagement/Views/Settings/LocalizedPostFeedSettingsView.swift b/CHDataManagement/Views/Settings/LocalizedPostFeedSettingsView.swift index 64609d2..bd524c6 100644 --- a/CHDataManagement/Views/Settings/LocalizedPostFeedSettingsView.swift +++ b/CHDataManagement/Views/Settings/LocalizedPostFeedSettingsView.swift @@ -7,30 +7,20 @@ struct LocalizedPostFeedSettingsView: View { var body: some View { VStack(alignment: .leading) { - Text("Title") - .font(.headline) - TextField("", text: $settings.title) - .textFieldStyle(.roundedBorder) - .frame(maxWidth: 400) - Text("The title of all post feed pages.") - .foregroundStyle(.secondary) - .padding(.bottom) + StringPropertyView( + title: "Title", + text: $settings.title, + footer: "The title of all post feed pages.") - Text("URL prefix") - .font(.headline) - TextField("", text: $settings.feedUrlPrefix) - .textFieldStyle(.roundedBorder) - .frame(maxWidth: 400) - Text("The prefix to generate the urls for all post feed pages.") - .foregroundStyle(.secondary) - .padding(.bottom) + StringPropertyView( + title: "URL prefix", + text: $settings.feedUrlPrefix, + footer: "The prefix to generate the urls for all post feed pages.") - Text("Description") - .font(.headline) - DescriptionField(text: $settings.description) - Text("The description of all post feed pages.") - .foregroundStyle(.secondary) - .padding(.bottom) + TextFieldPropertyView( + title: "Description", + text: $settings.description, + footer: "The description of all post feed pages.") } } } diff --git a/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift b/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift index 0b99b53..e1731ab 100644 --- a/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift +++ b/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift @@ -35,19 +35,21 @@ struct NavigationBarSettingsView: View { .buttonStyle(.plain) } - ForEach(content.settings.navigationItems) { tag in + ForEach(content.settings.navigation.navigationItems) { tag in TagView(text: tag.title(in: language)) .foregroundStyle(.white) } Text("Select the tags to show in the navigation bar. The number should be even.") .foregroundStyle(.secondary) + + LocalizedNavigationBarSettingsView(settings: content.settings.navigation.localized(in: language)) } .padding() } .sheet(isPresented: $showItemPicker) { ItemSelectionView( isPresented: $showItemPicker, - selectedItems: $content.settings.navigationItems) + selectedItems: $content.settings.navigation.navigationItems) } } }