From be2aab2ea8b13bfc19d51cee38d773bf996879ab Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Sat, 14 Dec 2024 16:31:40 +0100 Subject: [PATCH] Rework content commands, add audio player --- CHDataManagement.xcodeproj/project.pbxproj | 137 +++++++- .../xcshareddata/swiftpm/Package.resolved | 11 +- .../Generator/GenerationResultsHandler.swift | 24 -- .../Generator/LocalizedWebsiteGenerator.swift | 5 +- .../Page Content/AudioPlayerCommand.swift | 94 ++++++ .../Page Content/ButtonCommand.swift | 103 ++++++ .../Page Content/CommandProcessor.swift | 9 + .../Page Content/LabelsCommand.swift | 30 ++ .../Generator/PageCommandExtractor.swift | 13 +- .../Generator/PageContentAnomaly.swift | 56 ++-- .../Generator/PageContentGenerator.swift | 291 ++++++++-------- .../Generator/PageGenerationResults.swift | 45 ++- .../Generator/PageGenerator.swift | 3 +- .../Generator/PostListPageGenerator.swift | 4 +- .../Generator/RequiredHeaders.swift | 6 + .../Generator/ShorthandMarkdownKey.swift | 27 +- .../Model/Content+Generation.swift | 26 +- CHDataManagement/Model/Content+Load.swift | 1 + CHDataManagement/Model/Content+Save.swift | 1 + .../Model/Content+Validation.swift | 28 ++ CHDataManagement/Model/FileResource.swift | 32 +- CHDataManagement/Model/Item.swift | 18 + CHDataManagement/Model/Page.swift | 40 ++- CHDataManagement/Model/Song.swift | 22 ++ CHDataManagement/Model/Tag.swift | 7 + .../AdditionalPageHeaders.swift | 15 +- .../AudioPlayer/AudioPlayer.swift | 128 +++++++ .../AudioPlayer/AudioPlayerContent.swift | 46 +++ .../ContentElements/ContentButtons.swift | 13 +- .../ContentElements/ContentLabels.swift | 28 ++ .../ContentElements/HikingStatistics.swift | 42 --- .../Page Elements/ContentElements/Icons.swift | 107 ------ .../Icons/AudioPlayerIcons.swift | 55 +++ .../ContentElements/Icons/ButtonIcons.swift | 36 ++ .../ContentElements/Icons/ContentIcon.swift | 18 + .../ContentElements/Icons/PageIcon.swift | 63 ++++ .../Icons/StatisticsIcons.swift | 45 +++ CHDataManagement/Pages/ContentPage.swift | 14 +- .../Preview Content/Page+Mock.swift | 1 + CHDataManagement/Storage/Model/PageFile.swift | 2 + .../Views/Files/FileSelectionView.swift | 28 ++ .../Views/Pages/AddPageView.swift | 1 + .../Pages/LocalizedPageContentView.swift | 34 +- .../Views/Pages/LocalizedPageDetailView.swift | 4 +- .../Views/Pages/PageContentResultsView.swift | 23 +- .../Views/Pages/PageContentView.swift | 19 +- .../Views/Pages/PageDetailView.swift | 12 +- .../Views/Posts/PostDetailView.swift | 21 ++ .../Content/PageSettingsContentView.swift | 288 +--------------- .../Settings/Content/Pages/PageIssue.swift | 49 +++ .../Content/Pages/PageIssueChecker.swift | 84 +++++ .../Content/Pages/PageIssueView.swift | 316 ++++++++++++++++++ 52 files changed, 1758 insertions(+), 767 deletions(-) delete mode 100644 CHDataManagement/Generator/GenerationResultsHandler.swift create mode 100644 CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift create mode 100644 CHDataManagement/Generator/Page Content/ButtonCommand.swift create mode 100644 CHDataManagement/Generator/Page Content/CommandProcessor.swift create mode 100644 CHDataManagement/Generator/Page Content/LabelsCommand.swift create mode 100644 CHDataManagement/Model/Content+Validation.swift create mode 100644 CHDataManagement/Model/Item.swift create mode 100644 CHDataManagement/Model/Song.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/AudioPlayer/AudioPlayer.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/AudioPlayer/AudioPlayerContent.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/ContentLabels.swift delete mode 100644 CHDataManagement/Page Elements/ContentElements/HikingStatistics.swift delete mode 100644 CHDataManagement/Page Elements/ContentElements/Icons.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/Icons/AudioPlayerIcons.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/Icons/ButtonIcons.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/Icons/ContentIcon.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/Icons/PageIcon.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/Icons/StatisticsIcons.swift create mode 100644 CHDataManagement/Views/Files/FileSelectionView.swift create mode 100644 CHDataManagement/Views/Settings/Content/Pages/PageIssue.swift create mode 100644 CHDataManagement/Views/Settings/Content/Pages/PageIssueChecker.swift create mode 100644 CHDataManagement/Views/Settings/Content/Pages/PageIssueView.swift diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index 7d3427d..db3ab0f 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -58,7 +58,6 @@ E25DA5802D01C6AC00AEF16D /* Splash in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA57F2D01C6AC00AEF16D /* Splash */; }; E25DA5832D01C7A400AEF16D /* VideoFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5822D01C7A100AEF16D /* VideoFileType.swift */; }; E25DA5852D01C92700AEF16D /* ShorthandMarkdownKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */; }; - E25DA5872D01CA9300AEF16D /* GenerationResultsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5862D01CA8D00AEF16D /* GenerationResultsHandler.swift */; }; E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5882D01CBCE00AEF16D /* Content+Generation.swift */; }; E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA58A2D020C9200AEF16D /* PageImage.swift */; }; E25DA58D2D021BA400AEF16D /* WebsiteImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA58C2D021BA000AEF16D /* WebsiteImage.swift */; }; @@ -69,7 +68,7 @@ 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 /* HikingStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D311F2D0320E20051B7F4 /* HikingStatistics.swift */; }; + E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D311F2D0320E20051B7F4 /* ContentLabels.swift */; }; E29D31222D0363FD0051B7F4 /* ContentButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31212D0363FA0051B7F4 /* ContentButtons.swift */; }; E29D31242D0366860051B7F4 /* TagList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31232D0366820051B7F4 /* TagList.swift */; }; E29D31262D0370A80051B7F4 /* VideoOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31252D0370A50051B7F4 /* VideoOption.swift */; }; @@ -107,7 +106,7 @@ E29D31712D08234D0051B7F4 /* GenerationDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31702D08234D0051B7F4 /* GenerationDetailView.swift */; }; E29D31792D083DE50051B7F4 /* PageContentResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31782D083DDA0051B7F4 /* PageContentResultsView.swift */; }; E29D317D2D086AB00051B7F4 /* Int+Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D317C2D086AAE0051B7F4 /* Int+Random.swift */; }; - E29D317F2D086F4C0051B7F4 /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D317E2D086F490051B7F4 /* Icons.swift */; }; + E29D317F2D086F4C0051B7F4 /* StatisticsIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */; }; E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */; }; E29D31852D0AE8EE0051B7F4 /* RequiredHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */; }; E29D31872D0AE9DE0051B7F4 /* AdditionalPageHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31862D0AE9D40051B7F4 /* AdditionalPageHeaders.swift */; }; @@ -119,6 +118,24 @@ E29D31942D0B7D280051B7F4 /* SvgImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31932D0B7D250051B7F4 /* SvgImage.swift */; }; E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31952D0C18690051B7F4 /* PathSettings.swift */; }; E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */; }; + E29D319B2D0C452B0051B7F4 /* PageIssue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D319A2D0C452B0051B7F4 /* PageIssue.swift */; }; + E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D319C2D0C45B60051B7F4 /* PageIssueView.swift */; }; + E29D319F2D0C46310051B7F4 /* PageIssueChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D319E2D0C46290051B7F4 /* PageIssueChecker.swift */; }; + E29D31A12D0C75CA0051B7F4 /* Content+Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31A02D0C75C50051B7F4 /* Content+Validation.swift */; }; + E29D31A32D0CC98C0051B7F4 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31A22D0CC98B0051B7F4 /* Item.swift */; }; + E29D31A52D0CD03F0051B7F4 /* FileSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31A42D0CD03A0051B7F4 /* FileSelectionView.swift */; }; + E29D31A82D0CDC5D0051B7F4 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = E29D31A72D0CDC5D0051B7F4 /* SwiftSoup */; }; + E29D31AA2D0CEE3F0051B7F4 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31A92D0CEE3C0051B7F4 /* AudioPlayer.swift */; }; + E29D31AD2D0DA5360051B7F4 /* AudioPlayerIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31AC2D0DA5310051B7F4 /* AudioPlayerIcons.swift */; }; + E29D31B12D0DA5510051B7F4 /* ContentIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31B02D0DA5510051B7F4 /* ContentIcon.swift */; }; + E29D31B32D0DA6E80051B7F4 /* ButtonIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31B22D0DA6E50051B7F4 /* ButtonIcons.swift */; }; + E29D31B52D0DA8490051B7F4 /* PageIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31B42D0DA8490051B7F4 /* PageIcon.swift */; }; + E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31B72D0DAC1D0051B7F4 /* ButtonCommand.swift */; }; + E29D31BA2D0DB5080051B7F4 /* LabelsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31B92D0DB4EF0051B7F4 /* LabelsCommand.swift */; }; + E29D31BC2D0DB5120051B7F4 /* CommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31BB2D0DB5110051B7F4 /* CommandProcessor.swift */; }; + E29D31BE2D0DB85A0051B7F4 /* AudioPlayerCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31BD2D0DB8560051B7F4 /* AudioPlayerCommand.swift */; }; + E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31BF2D0DB9ED0051B7F4 /* AudioPlayerContent.swift */; }; + E29D31C32D0DBEF20051B7F4 /* Song.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31C22D0DBEF00051B7F4 /* Song.swift */; }; E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C022CB16C220060935B /* Environment+Language.swift */; }; E2A21C082CB17B870060935B /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C072CB17B810060935B /* TagView.swift */; }; E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C0D2CB189D70060935B /* Color+RGB.swift */; }; @@ -209,7 +226,6 @@ E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageContentGenerator.swift; sourceTree = ""; }; E25DA5822D01C7A100AEF16D /* VideoFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoFileType.swift; sourceTree = ""; }; E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShorthandMarkdownKey.swift; sourceTree = ""; }; - E25DA5862D01CA8D00AEF16D /* GenerationResultsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResultsHandler.swift; sourceTree = ""; }; E25DA5882D01CBCE00AEF16D /* Content+Generation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Generation.swift"; sourceTree = ""; }; E25DA58A2D020C9200AEF16D /* PageImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageImage.swift; sourceTree = ""; }; E25DA58C2D021BA000AEF16D /* WebsiteImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteImage.swift; sourceTree = ""; }; @@ -220,7 +236,7 @@ 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 /* HikingStatistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HikingStatistics.swift; sourceTree = ""; }; + E29D311F2D0320E20051B7F4 /* ContentLabels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLabels.swift; sourceTree = ""; }; E29D31212D0363FA0051B7F4 /* ContentButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentButtons.swift; sourceTree = ""; }; E29D31232D0366820051B7F4 /* TagList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagList.swift; sourceTree = ""; }; E29D31252D0370A50051B7F4 /* VideoOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoOption.swift; sourceTree = ""; }; @@ -258,7 +274,7 @@ E29D31702D08234D0051B7F4 /* GenerationDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationDetailView.swift; sourceTree = ""; }; E29D31782D083DDA0051B7F4 /* PageContentResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageContentResultsView.swift; sourceTree = ""; }; E29D317C2D086AAE0051B7F4 /* Int+Random.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Random.swift"; sourceTree = ""; }; - E29D317E2D086F490051B7F4 /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = ""; }; + E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsIcons.swift; sourceTree = ""; }; E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedPageLink.swift; sourceTree = ""; }; E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredHeaders.swift; sourceTree = ""; }; E29D31862D0AE9D40051B7F4 /* AdditionalPageHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionalPageHeaders.swift; sourceTree = ""; }; @@ -270,6 +286,23 @@ E29D31932D0B7D250051B7F4 /* SvgImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SvgImage.swift; sourceTree = ""; }; E29D31952D0C18690051B7F4 /* PathSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSettings.swift; sourceTree = ""; }; E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSettingsFile.swift; sourceTree = ""; }; + E29D319A2D0C452B0051B7F4 /* PageIssue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIssue.swift; sourceTree = ""; }; + E29D319C2D0C45B60051B7F4 /* PageIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIssueView.swift; sourceTree = ""; }; + E29D319E2D0C46290051B7F4 /* PageIssueChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIssueChecker.swift; sourceTree = ""; }; + E29D31A02D0C75C50051B7F4 /* Content+Validation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Validation.swift"; sourceTree = ""; }; + E29D31A22D0CC98B0051B7F4 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; + E29D31A42D0CD03A0051B7F4 /* FileSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSelectionView.swift; sourceTree = ""; }; + E29D31A92D0CEE3C0051B7F4 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; + E29D31AC2D0DA5310051B7F4 /* AudioPlayerIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerIcons.swift; sourceTree = ""; }; + E29D31B02D0DA5510051B7F4 /* ContentIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentIcon.swift; sourceTree = ""; }; + E29D31B22D0DA6E50051B7F4 /* ButtonIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcons.swift; sourceTree = ""; }; + E29D31B42D0DA8490051B7F4 /* PageIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIcon.swift; sourceTree = ""; }; + E29D31B72D0DAC1D0051B7F4 /* ButtonCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonCommand.swift; sourceTree = ""; }; + E29D31B92D0DB4EF0051B7F4 /* LabelsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelsCommand.swift; sourceTree = ""; }; + E29D31BB2D0DB5110051B7F4 /* CommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessor.swift; sourceTree = ""; }; + E29D31BD2D0DB8560051B7F4 /* AudioPlayerCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerCommand.swift; sourceTree = ""; }; + E29D31BF2D0DB9ED0051B7F4 /* AudioPlayerContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerContent.swift; sourceTree = ""; }; + E29D31C22D0DBEF00051B7F4 /* Song.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Song.swift; sourceTree = ""; }; E2A21C022CB16C220060935B /* Environment+Language.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Language.swift"; sourceTree = ""; }; E2A21C072CB17B810060935B /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = ""; }; E2A21C0D2CB189D70060935B /* Color+RGB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+RGB.swift"; sourceTree = ""; }; @@ -323,6 +356,7 @@ E25DA52F2CFFC91B00AEF16D /* SDWebImageWebPCoder in Frameworks */, E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */, E25DA5802D01C6AC00AEF16D /* Splash in Frameworks */, + E29D31A82D0CDC5D0051B7F4 /* SwiftSoup in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -370,10 +404,10 @@ E25DA5782D01C56200AEF16D /* Generator */ = { isa = PBXGroup; children = ( + E29D31B62D0DAC030051B7F4 /* Page Content */, E29D31912D0B3EF30051B7F4 /* PageCommandExtractor.swift */, E29D318F2D0B34870051B7F4 /* PageContentAnomaly.swift */, E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */, - E25DA5862D01CA8D00AEF16D /* GenerationResultsHandler.swift */, E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */, E218502E2CFAF6990090B18B /* LocalizedWebsiteGenerator.swift */, E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */, @@ -403,16 +437,17 @@ E29D311E2D0320D90051B7F4 /* ContentElements */ = { isa = PBXGroup; children = ( + E29D31C12D0DBED70051B7F4 /* AudioPlayer */, + E29D31AB2D0DA52C0051B7F4 /* Icons */, E29D31932D0B7D250051B7F4 /* SvgImage.swift */, E29D318A2D0B07E60051B7F4 /* ContentBox.swift */, E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */, E29D31862D0AE9D40051B7F4 /* AdditionalPageHeaders.swift */, E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */, - E29D317E2D086F490051B7F4 /* Icons.swift */, E29D31272D0371870051B7F4 /* ContentPageVideo.swift */, E29D31232D0366820051B7F4 /* TagList.swift */, E29D31212D0363FA0051B7F4 /* ContentButtons.swift */, - E29D311F2D0320E20051B7F4 /* HikingStatistics.swift */, + E29D311F2D0320E20051B7F4 /* ContentLabels.swift */, ); path = ContentElements; sourceTree = ""; @@ -432,11 +467,54 @@ E29D318C2D0B2E5E0051B7F4 /* Content */ = { isa = PBXGroup; children = ( + E29D31992D0C451B0051B7F4 /* Pages */, E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */, ); path = Content; sourceTree = ""; }; + E29D31992D0C451B0051B7F4 /* Pages */ = { + isa = PBXGroup; + children = ( + E29D319E2D0C46290051B7F4 /* PageIssueChecker.swift */, + E29D319C2D0C45B60051B7F4 /* PageIssueView.swift */, + E29D319A2D0C452B0051B7F4 /* PageIssue.swift */, + ); + path = Pages; + sourceTree = ""; + }; + E29D31AB2D0DA52C0051B7F4 /* Icons */ = { + isa = PBXGroup; + children = ( + E29D31B42D0DA8490051B7F4 /* PageIcon.swift */, + E29D31B22D0DA6E50051B7F4 /* ButtonIcons.swift */, + E29D31B02D0DA5510051B7F4 /* ContentIcon.swift */, + E29D31AC2D0DA5310051B7F4 /* AudioPlayerIcons.swift */, + E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */, + ); + path = Icons; + sourceTree = ""; + }; + E29D31B62D0DAC030051B7F4 /* Page Content */ = { + isa = PBXGroup; + children = ( + E29D31BD2D0DB8560051B7F4 /* AudioPlayerCommand.swift */, + E29D31BB2D0DB5110051B7F4 /* CommandProcessor.swift */, + E29D31B92D0DB4EF0051B7F4 /* LabelsCommand.swift */, + E29D31B72D0DAC1D0051B7F4 /* ButtonCommand.swift */, + ); + path = "Page Content"; + sourceTree = ""; + }; + E29D31C12D0DBED70051B7F4 /* AudioPlayer */ = { + isa = PBXGroup; + children = ( + E29D31A92D0CEE3C0051B7F4 /* AudioPlayer.swift */, + E29D31BF2D0DB9ED0051B7F4 /* AudioPlayerContent.swift */, + ); + path = AudioPlayer; + sourceTree = ""; + }; E2A21C322CB5BCAC0060935B /* Pages */ = { isa = PBXGroup; children = ( @@ -493,6 +571,7 @@ E29D31682D0702670051B7F4 /* TextFileContentView.swift */, E29D314A2D04FC940051B7F4 /* FileToAdd.swift */, E29D314C2D04FCBF0051B7F4 /* FileToAddView.swift */, + E29D31A42D0CD03A0051B7F4 /* FileSelectionView.swift */, ); path = Files; sourceTree = ""; @@ -523,9 +602,11 @@ E2B85F392C428F020047CD0C /* Model */ = { isa = PBXGroup; children = ( + E29D31A22D0CC98B0051B7F4 /* Item.swift */, E25DA5812D01C79800AEF16D /* Types */, E25DA53B2D0042EA00AEF16D /* Settings */, E2E06DFA2CA4A6570019C2AF /* Content.swift */, + E29D31A02D0C75C50051B7F4 /* Content+Validation.swift */, E25DA5882D01CBCE00AEF16D /* Content+Generation.swift */, E25DA5162CFF00F200AEF16D /* Content+Save.swift */, E25DA5142CFF00B900AEF16D /* Content+Load.swift */, @@ -538,6 +619,7 @@ E2A37D182CEA36A40000979F /* LocalizedTag.swift */, E2A9CB7D2C7BCF2A005C89CC /* Page.swift */, E25A0B882CE4021400F33674 /* LocalizedPage.swift */, + E29D31C22D0DBEF00051B7F4 /* Song.swift */, ); path = Model; sourceTree = ""; @@ -690,6 +772,7 @@ E25DA52E2CFFC91B00AEF16D /* SDWebImageWebPCoder */, E25DA57C2D01C67900AEF16D /* Ink */, E25DA57F2D01C6AC00AEF16D /* Splash */, + E29D31A72D0CDC5D0051B7F4 /* SwiftSoup */, ); productName = CHDataManagement; productReference = E2DD04702C276F31003BFF1F /* CHDataManagement.app */; @@ -726,6 +809,7 @@ E25DA52D2CFFC91B00AEF16D /* XCRemoteSwiftPackageReference "SDWebImageWebPCoder" */, E25DA57B2D01C67900AEF16D /* XCRemoteSwiftPackageReference "ink" */, E25DA57E2D01C6AC00AEF16D /* XCRemoteSwiftPackageReference "Splash" */, + E29D31A62D0CDC5D0051B7F4 /* XCRemoteSwiftPackageReference "SwiftSoup" */, ); productRefGroup = E2DD04712C276F31003BFF1F /* Products */; projectDirPath = ""; @@ -761,6 +845,7 @@ E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */, E29D313B2D04464A0051B7F4 /* LocalizedTagDetailView.swift in Sources */, E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */, + E29D31BA2D0DB5080051B7F4 /* LabelsCommand.swift in Sources */, E29D31692D0702700051B7F4 /* TextFileContentView.swift in Sources */, E25DA5412D00446C00AEF16D /* PostSettings.swift in Sources */, E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */, @@ -768,11 +853,13 @@ E29D31902D0B34870051B7F4 /* PageContentAnomaly.swift in Sources */, E29D31872D0AE9DE0051B7F4 /* AdditionalPageHeaders.swift in Sources */, E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */, + E29D31AD2D0DA5360051B7F4 /* AudioPlayerIcons.swift in Sources */, E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */, E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */, E21850252CF38BCE0090B18B /* TextEntrySheet.swift in Sources */, E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */, E21850172CEE55FC0090B18B /* FileType.swift in Sources */, + E29D31B12D0DA5510051B7F4 /* ContentIcon.swift in Sources */, E29D31612D06D95C0051B7F4 /* ResourceFileType.swift in Sources */, E2A37D112CE537800000979F /* PageFile.swift in Sources */, E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */, @@ -780,7 +867,7 @@ E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */, E29D31852D0AE8EE0051B7F4 /* RequiredHeaders.swift in Sources */, E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */, - E29D317F2D086F4C0051B7F4 /* Icons.swift in Sources */, + E29D317F2D086F4C0051B7F4 /* StatisticsIcons.swift in Sources */, E2A21C082CB17B870060935B /* TagView.swift in Sources */, E29D313D2D047C1B0051B7F4 /* LocalizedPageContentView.swift in Sources */, E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */, @@ -807,6 +894,7 @@ E29D31922D0B3EFC0051B7F4 /* PageCommandExtractor.swift in Sources */, E29D315B2D06D63E0051B7F4 /* ModelFileType.swift in Sources */, E29D31302D03A2C50051B7F4 /* DescriptionField.swift in Sources */, + E29D31BE2D0DB85A0051B7F4 /* AudioPlayerCommand.swift in Sources */, E29D31552D06D2CE0051B7F4 /* TagListView.swift in Sources */, E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */, E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */, @@ -816,6 +904,7 @@ E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */, E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */, E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */, + E29D31BC2D0DB5120051B7F4 /* CommandProcessor.swift in Sources */, E29D312C2D039DB80051B7F4 /* PageDetailView.swift in Sources */, E29D31432D0488960051B7F4 /* MainContentView.swift in Sources */, E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */, @@ -829,6 +918,7 @@ E21850332CFAFA2F0090B18B /* Settings.swift in Sources */, E29D31892D0AED1F0051B7F4 /* ModelViewer.swift in Sources */, E29D31412D04887F0051B7F4 /* SelectedDetailView.swift in Sources */, + E29D31A32D0CC98C0051B7F4 /* Item.swift in Sources */, E25DA57A2D01C64400AEF16D /* PageContentGenerator.swift in Sources */, E21850192CEE561C0090B18B /* PageOnDisk.swift in Sources */, E2A21C202CB28ED20060935B /* MockImage.swift in Sources */, @@ -836,22 +926,29 @@ E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */, E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */, E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */, + E29D31A12D0C75CA0051B7F4 /* Content+Validation.swift in Sources */, E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */, E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */, + E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */, E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */, E2B85F412C4294790047CD0C /* PageHead.swift in Sources */, E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */, E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */, + E29D31B32D0DA6E80051B7F4 /* ButtonIcons.swift in Sources */, + E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */, E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */, E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */, E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */, - E29D31202D0320E70051B7F4 /* HikingStatistics.swift in Sources */, + E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */, E29D314B2D04FC950051B7F4 /* FileToAdd.swift in Sources */, E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */, + E29D31B52D0DA8490051B7F4 /* PageIcon.swift in Sources */, E25DA51D2CFF135E00AEF16D /* GenericPage.swift in Sources */, + E29D319B2D0C452B0051B7F4 /* PageIssue.swift in Sources */, E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */, E29D31472D04892E0051B7F4 /* FileListView.swift in Sources */, E218503D2CFCFD910090B18B /* LocalizedPostFeedSettingsView.swift in Sources */, + E29D31A52D0CD03F0051B7F4 /* FileSelectionView.swift in Sources */, E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */, E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */, E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */, @@ -859,11 +956,14 @@ E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */, E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */, E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */, + E29D319F2D0C46310051B7F4 /* PageIssueChecker.swift in Sources */, E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */, E29D31322D03B5680051B7F4 /* LocalizedPostDetailView.swift in Sources */, E29D31712D08234D0051B7F4 /* GenerationDetailView.swift in Sources */, E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */, + E29D31C32D0DBEF20051B7F4 /* Song.swift in Sources */, E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */, + E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */, E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */, E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */, E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */, @@ -881,10 +981,10 @@ E25DA5092CFD964E00AEF16D /* TagContentView.swift in Sources */, E29D31342D03B5D50051B7F4 /* IconButton.swift in Sources */, E25DA5712D01015400AEF16D /* GenerationContentView.swift in Sources */, - E25DA5872D01CA9300AEF16D /* GenerationResultsHandler.swift in Sources */, E29D316F2D0822770051B7F4 /* SettingsListView.swift in Sources */, E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */, E25DA5852D01C92700AEF16D /* ShorthandMarkdownKey.swift in Sources */, + E29D31AA2D0CEE3F0051B7F4 /* AudioPlayer.swift in Sources */, E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */, E25DA58F2D02368D00AEF16D /* PageSettings.swift in Sources */, E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */, @@ -1161,6 +1261,14 @@ minimumVersion = 0.16.0; }; }; + E29D31A62D0CDC5D0051B7F4 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/scinfu/SwiftSoup.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.7.6; + }; + }; E2B85F342C426BED0047CD0C /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols"; @@ -1197,6 +1305,11 @@ package = E25DA57E2D01C6AC00AEF16D /* XCRemoteSwiftPackageReference "Splash" */; productName = Splash; }; + E29D31A72D0CDC5D0051B7F4 /* SwiftSoup */ = { + isa = XCSwiftPackageProductDependency; + package = E29D31A62D0CDC5D0051B7F4 /* XCRemoteSwiftPackageReference "SwiftSoup" */; + productName = SwiftSoup; + }; E2B85F352C426BEE0047CD0C /* SFSafeSymbols */ = { isa = XCSwiftPackageProductDependency; package = E2B85F342C426BED0047CD0C /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; diff --git a/CHDataManagement.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CHDataManagement.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7e2873d..41c5283 100644 --- a/CHDataManagement.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CHDataManagement.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "09586c34852addc5d95a3a7234451be33efeecd2b5dbd5ef8607a959add71d3f", + "originHash" : "610a80083aa646fbd77d72ddb7dcc16342884551283091b7b1ebf40042816810", "pins" : [ { "identity" : "highlightedtexteditor", @@ -99,6 +99,15 @@ "revision" : "7f4df436eb78fe64fe2c32c58006e9949fa28ad8", "version" : "0.16.0" } + }, + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup.git", + "state" : { + "revision" : "0837db354faf9c9deb710dc597046edaadf5360f", + "version" : "2.7.6" + } } ], "version" : 3 diff --git a/CHDataManagement/Generator/GenerationResultsHandler.swift b/CHDataManagement/Generator/GenerationResultsHandler.swift deleted file mode 100644 index 1d1dbc8..0000000 --- a/CHDataManagement/Generator/GenerationResultsHandler.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -final class GenerationResultsHandler { - - var requiredVideoFiles: Set = [] - - /// Generic warnings for pages - private var pageWarnings: [(message: String, source: String)] = [] - - private var missingPages: [String : [String]] = [:] - - func warning(_ message: String, page: Page) { - pageWarnings.append((message, page.id)) - print("Page: \(page.id): \(message)") - } - - func addRequiredVideoFile(fileId: String) { - requiredVideoFiles.insert(fileId) - } - - func missing(page: String, linkedBy source: String) { - missingPages[page, default: []].append(source) - } -} diff --git a/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift b/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift index 1e859df..19594ea 100644 --- a/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift +++ b/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift @@ -130,7 +130,7 @@ final class LocalizedWebsiteGenerator { return true } - let path = self.content.absoluteUrlToPage(page, language: language) + ".html" + let path = page.absoluteUrl(for: language) + ".html" guard save(content, to: path) else { print("Failed to save page") return false @@ -151,9 +151,8 @@ final class LocalizedWebsiteGenerator { continue } - let outputPath = content.absoluteUrlToFile(file) do { - try content.storage.copy(file: file.id, to: outputPath) + try content.storage.copy(file: file.id, to: file.absoluteUrl) } catch { print("Failed to copy file \(file.id): \(error)") return false diff --git a/CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift b/CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift new file mode 100644 index 0000000..275f337 --- /dev/null +++ b/CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift @@ -0,0 +1,94 @@ +import Foundation + +struct AudioPlayerCommandProcessor: CommandProcessor { + + let commandType: ShorthandMarkdownKey = .audioPlayer + + let content: Content + + let results: PageGenerationResults + + init(content: Content, results: PageGenerationResults) { + self.content = content + self.results = results + } + + func process(_ arguments: [String], markdown: Substring) -> String { + guard arguments.count == 2 else { + results.invalid(command: .audioPlayer, "Invalid audio player arguments") + return "" + } + let fileId = arguments[0] + let titleText = arguments[1] + + guard content.isValidIdForFile(fileId) else { + results.invalid(command: .audioPlayer, "Invalid file id \(fileId) for audio player") + return "" + } + + guard let file = content.file(fileId) else { + results.missingFiles.insert(fileId) + return "" + } + let songs: [Song] + do { + let data = try file.dataContent() + songs = try JSONDecoder().decode([Song].self, from: data) + } catch { + results.issues.insert(.failedToLoadContent(error)) + return "" + } + + var playlist: [AudioPlayer.PlaylistItem] = [] + var amplitude: [AmplitudeSong] = [] + + for song in songs { + guard let image = content.image(song.cover) else { + results.missing(file: song.cover, markdown: "Missing cover image \(song.cover) in \(file.id)") + continue + } + + guard let audioFile = content.file(song.file) else { + results.missing(file: song.file, markdown: "Missing audio file \(song.file) in \(file.id)") + continue + } + #warning("Check if file is audio") + let coverUrl = image.absoluteUrl + + let playlistItem = AudioPlayer.PlaylistItem( + index: playlist.count, + image: coverUrl, + name: song.name, + album: song.album, + track: song.track, + artist: song.artist) + + let amplitudeSong = AmplitudeSong( + name: song.name, + artist: song.artist, + album: song.album, + track: "\(song.track)", + url: audioFile.absoluteUrl, + cover_art_url: coverUrl) + + playlist.append(playlistItem) + amplitude.append(amplitudeSong) + } + + let footerScript = AudioPlayerScript(items: amplitude).content + results.requiredFooters.insert(footerScript) + results.requiredHeaders.insert(.audioPlayerCss) + results.requiredHeaders.insert(.amplitude) + + results.requiredIcons.formUnion([ + .audioPlayerClose, + .audioPlayerPlaylist, + .audioPlayerNext, + .audioPlayerPrevious, + .audioPlayerPlay, + .audioPlayerPause + ]) + + return AudioPlayer(playingText: titleText, items: playlist).content + } +} diff --git a/CHDataManagement/Generator/Page Content/ButtonCommand.swift b/CHDataManagement/Generator/Page Content/ButtonCommand.swift new file mode 100644 index 0000000..06b5b8b --- /dev/null +++ b/CHDataManagement/Generator/Page Content/ButtonCommand.swift @@ -0,0 +1,103 @@ + +struct ButtonCommandProcessor: CommandProcessor { + + let commandType: ShorthandMarkdownKey = .buttons + + let content: Content + + let results: PageGenerationResults + + init(content: Content, results: PageGenerationResults) { + self.content = content + self.results = results + } + + /** + Format: `![buttons](<,,;...)` + Format: `![buttons](type=;...)` + Types: + - Download: `download=,,` + - External link: `external=,` + - Git: `git=,` + - Play: `play-circle=,` + */ + func process(_ arguments: [String], markdown: Substring) -> String { + let buttons = arguments.compactMap { convert(button: $0, markdown: markdown) } + return ContentButtons(items: buttons).content + } + + private func convert(button: String, markdown: Substring) -> ContentButtons.Item? { + guard let type = PageIcon(rawValue: button.dropAfterFirst("=").trimmed) else { + results.invalid(command: commandType, markdown) + return nil + } + let parts = button.dropBeforeFirst("=").components(separatedBy: ",").map { $0.trimmed } + switch type { + case .buttonDownload: + return download(arguments: parts, markdown: markdown) + case .buttonGitLink: + return link(icon: .buttonGitLink, arguments: parts, markdown: markdown) + case .buttonExternalLink: + return link(icon: .buttonExternalLink, arguments: parts, markdown: markdown) + case .buttonPlay: + return play(arguments: parts, markdown: markdown) + default: + results.invalid(command: commandType, markdown) + return nil + } + } + + private func download(arguments: [String], markdown: Substring) -> ContentButtons.Item? { + guard (2...3).contains(arguments.count) else { + results.invalid(command: commandType, markdown) + return nil + } + let fileId = arguments[0].trimmed + let title = arguments[1].trimmed + let downloadName = arguments.count > 2 ? arguments[2].trimmed : nil + + guard let file = content.file(fileId) else { + results.missing(file: fileId, markdown: markdown) + return nil + } + results.files.insert(file) + results.requiredIcons.insert(.buttonDownload) + return ContentButtons.Item( + icon: .buttonDownload, + filePath: file.absoluteUrl, + text: title, + downloadFileName: downloadName) + } + + private func link(icon: PageIcon, arguments: [String], markdown: Substring) -> ContentButtons.Item? { + guard arguments.count == 2 else { + results.invalid(command: .buttons, markdown) + return nil + } + let rawUrl = arguments[0].trimmed + guard let url = rawUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + results.invalid(command: .buttons, markdown) + return nil + } + + results.externalLinks.insert(rawUrl) + results.requiredIcons.insert(icon) + + let title = arguments[1].trimmed + + return .init(icon: icon, filePath: url, text: title) + } + + private func play(arguments: [String], markdown: Substring) -> ContentButtons.Item? { + guard arguments.count == 2 else { + results.invalid(command: .buttons, markdown) + return nil + } + let text = arguments[0].trimmed + let event = arguments[1].trimmed + + results.requiredIcons.insert(.buttonPlay) + + return .init(icon: .buttonPlay, filePath: nil, text: text, onClickText: event) + } +} diff --git a/CHDataManagement/Generator/Page Content/CommandProcessor.swift b/CHDataManagement/Generator/Page Content/CommandProcessor.swift new file mode 100644 index 0000000..d82fecf --- /dev/null +++ b/CHDataManagement/Generator/Page Content/CommandProcessor.swift @@ -0,0 +1,9 @@ + +protocol CommandProcessor { + + var commandType: ShorthandMarkdownKey { get } + + init(content: Content, results: PageGenerationResults) + + func process(_ arguments: [String], markdown: Substring) -> String +} diff --git a/CHDataManagement/Generator/Page Content/LabelsCommand.swift b/CHDataManagement/Generator/Page Content/LabelsCommand.swift new file mode 100644 index 0000000..71ae0ca --- /dev/null +++ b/CHDataManagement/Generator/Page Content/LabelsCommand.swift @@ -0,0 +1,30 @@ + +struct LabelsCommandProcessor: CommandProcessor { + + let commandType: ShorthandMarkdownKey = .labels + + let content: Content + + let results: PageGenerationResults + + init(content: Content, results: PageGenerationResults) { + self.content = content + self.results = results + } + + func process(_ arguments: [String], markdown: Substring) -> String { + let labels: [ContentLabel] = arguments.compactMap { arg in + let parts = arg.components(separatedBy: "=") + guard parts.count == 2 else { + results.invalid(command: .labels, markdown) + return nil + } + guard let icon = PageIcon(rawValue: parts[0].trimmed) else { + results.invalid(command: .labels, markdown) + return nil + } + return .init(icon: icon, value: parts[1]) + } + return ContentLabels(labels: labels).content + } +} diff --git a/CHDataManagement/Generator/PageCommandExtractor.swift b/CHDataManagement/Generator/PageCommandExtractor.swift index d50e632..6509b1f 100644 --- a/CHDataManagement/Generator/PageCommandExtractor.swift +++ b/CHDataManagement/Generator/PageCommandExtractor.swift @@ -1,20 +1,21 @@ import Ink +#warning("Remove if unused") final class PageCommandExtractor { - private var occurences: [(full: String, command: String, arguments: [String])] = [] + private var occurrences: [(full: String, command: String, arguments: [String])] = [] - func findOccurences(of command: ShorthandMarkdownKey, in content: String) -> [(full: String, arguments: [String])] { - findOccurences(of: command.rawValue, in: content) + func findOccurrences(of command: ShorthandMarkdownKey, in content: String) -> [(full: String, arguments: [String])] { + findOccurrences(of: command.rawValue, in: content) } - func findOccurences(of command: String, in content: String) -> [(full: String, arguments: [String])] { + func findOccurrences(of command: String, in content: String) -> [(full: String, arguments: [String])] { let parser = MarkdownParser(modifiers: [ Modifier(target: .images, closure: processMarkdownImage), ]) _ = parser.html(from: content) - return occurences + return occurrences .filter { $0.command == command } .map { ($0.full, $0.arguments) } } @@ -25,7 +26,7 @@ final class PageCommandExtractor { let command = markdown.between("![", and: "]").trimmed - occurences.append((full: String(markdown), command: command, arguments: arguments)) + occurrences.append((full: String(markdown), command: command, arguments: arguments)) return "" } } diff --git a/CHDataManagement/Generator/PageContentAnomaly.swift b/CHDataManagement/Generator/PageContentAnomaly.swift index dabb6c9..87f85fd 100644 --- a/CHDataManagement/Generator/PageContentAnomaly.swift +++ b/CHDataManagement/Generator/PageContentAnomaly.swift @@ -1,12 +1,10 @@ - enum PageContentAnomaly { case failedToLoadContent(Error) - case missingFile(String) - case missingPage(String) - case missingTag(String) - case unknownCommand(String) - case invalidCommandArguments(command: ShorthandMarkdownKey, arguments: [String]) + case missingFile(file: String, markdown: String) + case missingPage(page: String, markdown: String) + case missingTag(tag: String, markdown: String) + case invalidCommand(command: ShorthandMarkdownKey?, markdown: String) } extension PageContentAnomaly: Identifiable { @@ -15,20 +13,32 @@ extension PageContentAnomaly: Identifiable { switch self { case .failedToLoadContent: return "load-failed" - case .missingFile(let string): + case .missingFile(let string, _): return "missing-file-\(string)" - case .missingPage(let string): + case .missingPage(let string, _): return "missing-page-\(string)" - case .missingTag(let string): + case .missingTag(let string, _): return "missing-tag-\(string)" - case .unknownCommand(let string): - return "unknown-command-\(string)" - case .invalidCommandArguments(let command, let arguments): - return "invalid-arguments-\(command)-\(arguments.joined(separator: "-"))" + case .invalidCommand(_, let markdown): + return "invalid-command-\(markdown)" } } } +extension PageContentAnomaly: Equatable { + + static func == (lhs: PageContentAnomaly, rhs: PageContentAnomaly) -> Bool { + lhs.id == rhs.id + } +} + +extension PageContentAnomaly: Hashable { + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + extension PageContentAnomaly { enum Severity: String, CaseIterable { @@ -40,7 +50,7 @@ extension PageContentAnomaly { switch self { case .failedToLoadContent: return .error - case .missingFile, .missingPage, .missingTag, .unknownCommand, .invalidCommandArguments: + case .missingFile, .missingPage, .missingTag, .invalidCommand: return .warning } } @@ -52,16 +62,14 @@ extension PageContentAnomaly: CustomStringConvertible { switch self { case .failedToLoadContent(let error): return "Failed to load content: \(error)" - case .missingFile(let string): - return "Missing file \(string)" - case .missingPage(let string): - return "Missing page \(string)" - case .missingTag(let string): - return "Missing tag \(string)" - case .unknownCommand(let string): - return "Unknown command \(string)" - case .invalidCommandArguments(let command, let arguments): - return "Invalid command arguments for \(command): \(arguments)" + case .missingFile(let string, _): + return "Missing file: \(string)" + case .missingPage(let string, _): + return "Missing page: \(string)" + case .missingTag(let string, _): + return "Missing tag: \(string)" + case .invalidCommand(_, let markdown): + return "Invalid command: \(markdown)" } } } diff --git a/CHDataManagement/Generator/PageContentGenerator.swift b/CHDataManagement/Generator/PageContentGenerator.swift index ba1406f..81908d9 100644 --- a/CHDataManagement/Generator/PageContentGenerator.swift +++ b/CHDataManagement/Generator/PageContentGenerator.swift @@ -1,6 +1,7 @@ import Foundation import Ink import Splash +import SwiftSoup typealias VideoSource = (url: String, type: VideoFileType) @@ -18,6 +19,12 @@ final class PageContentParser { private let content: Content + private let buttonHandler: ButtonCommandProcessor + + private let labelHandler: LabelsCommandProcessor + + private let audioPlayer: AudioPlayerCommandProcessor + let language: ContentLanguage var largeImageWidth: Int { @@ -31,6 +38,9 @@ final class PageContentParser { init(content: Content, language: ContentLanguage) { self.content = content self.language = language + self.buttonHandler = .init(content: content, results: results) + self.labelHandler = .init(content: content, results: results) + self.audioPlayer = .init(content: content, results: results) } func requestImages(_ generator: ImageGenerator) { @@ -77,7 +87,7 @@ final class PageContentParser { if file.hasPrefix(tagLinkMarker) { return handleTagLink(file: file, html: html, markdown: markdown) } - #warning("Check existence of linked file") + results.externalLinks.insert(file) return html } @@ -86,12 +96,12 @@ final class PageContentParser { let textToChange = file.dropAfterFirst("#") let pageId = textToChange.replacingOccurrences(of: pageLinkMarker, with: "") guard let page = content.page(pageId) else { - results.missingPages.insert(pageId) + results.missing(page: pageId, markdown: markdown) // Remove link since the page can't be found return markdown.between("[", and: "]") } results.linkedPages.insert(page) - let pagePath = content.absoluteUrlToPage(page, language: language) + let pagePath = page.absoluteUrl(for: language) return html.replacingOccurrences(of: textToChange, with: pagePath) } @@ -100,7 +110,7 @@ final class PageContentParser { let textToChange = file.dropAfterFirst("#") let tagId = textToChange.replacingOccurrences(of: tagLinkMarker, with: "") guard let tag = content.tag(tagId) else { - results.missingTags.insert(tagId) + results.missing(tag: tagId, markdown: markdown) // Remove link since the tag can't be found return markdown.between("[", and: "]") } @@ -109,10 +119,71 @@ final class PageContentParser { return html.replacingOccurrences(of: textToChange, with: tagPath) } - private func handleHTML(html: String, markdown: Substring) -> String { + private func handleHTML(_: String, markdown: Substring) -> String { + let result = String(markdown) #warning("Check HTML code in markdown for required resources") + findImages(in: result) + findLinks(in: result) + findSourceSets(in: result) // Things to check: - return html + return result + } + + 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") } + + for src in srcAttributes { + print("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") } + + for src in srcAttributes { + print("Found link in html: \(src)") + } + } 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 srcAttributes = try linkElements.array().compactMap { try $0.attr("srcset") } + + for src in srcAttributes { + print("Found source set in html: \(src)") + } + } catch { + print("Error parsing HTML: \(error)") + } } /** @@ -151,40 +222,38 @@ final class PageContentParser { let rawCommand = percentDecoded(markdown.between("![", and: "]").trimmed) guard rawCommand != "" else { - return handleImage(arguments) + return handleImage(arguments, markdown: markdown) } guard let command = ShorthandMarkdownKey(rawValue: rawCommand) else { // Treat unknown commands as normal links - results.unknownCommands.append(rawCommand) + results.invalid(command: nil, markdown) return html } switch command { case .image: - return handleImage(arguments) - case .hikingStatistics: - return handleHikingStatistics(arguments) - case .downloadButtons: - return handleDownloadButtons(arguments) + return handleImage(arguments, markdown: markdown) + case .labels: + return labelHandler.process(arguments, markdown: markdown) + case .buttons: + return buttonHandler.process(arguments, markdown: markdown) case .video: - return handleVideo(arguments) - case .externalLink: - return handleExternalButtons(arguments) - case .gitLink: - return handleGitButtons(arguments) + return handleVideo(arguments, markdown: markdown) case .pageLink: - return handlePageLink(arguments) + return handlePageLink(arguments, markdown: markdown) case .includedHtml: - return handleExternalHtml(arguments) + return handleExternalHtml(arguments, markdown: markdown) case .box: - return handleSimpleBox(arguments) + return handleSimpleBox(arguments, markdown: markdown) case .model: - return handleModel(arguments) + return handleModel(arguments, markdown: markdown) case .svg: - return handleSvg(arguments) + return handleSvg(arguments, markdown: markdown) + case .audioPlayer: + return audioPlayer.process(arguments, markdown: markdown) default: - results.unknownCommands.append(command.rawValue) + results.invalid(command: nil, markdown) return "" } } @@ -192,15 +261,15 @@ final class PageContentParser { /** Format: `[image](;]` */ - private func handleImage(_ arguments: [String]) -> String { + private func handleImage(_ arguments: [String], markdown: Substring) -> String { guard (1...2).contains(arguments.count) else { - results.invalidCommandArguments.append((.image , arguments)) + results.invalid(command: .image, markdown) return "" } let imageId = arguments[0] guard let image = content.image(imageId) else { - results.missingFiles.insert(imageId) + results.missing(file: imageId, markdown: markdown) return "" } results.files.insert(image) @@ -208,7 +277,7 @@ final class PageContentParser { let caption = arguments.count == 2 ? arguments[1] : nil let altText = image.getDescription(for: language) - let path = content.absoluteUrlToFile(image) + let path = image.absoluteUrl guard !image.type.isSvg else { return SvgImage(imagePath: path, altText: altText).content @@ -235,170 +304,80 @@ final class PageContentParser { caption: caption).content } - /** - Format: `![hiking-stats](