Rework content commands, add audio player
This commit is contained in:
parent
b3b8c9a610
commit
be2aab2ea8
@ -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 = "<group>"; };
|
||||
E25DA5822D01C7A100AEF16D /* VideoFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoFileType.swift; sourceTree = "<group>"; };
|
||||
E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShorthandMarkdownKey.swift; sourceTree = "<group>"; };
|
||||
E25DA5862D01CA8D00AEF16D /* GenerationResultsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResultsHandler.swift; sourceTree = "<group>"; };
|
||||
E25DA5882D01CBCE00AEF16D /* Content+Generation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Generation.swift"; sourceTree = "<group>"; };
|
||||
E25DA58A2D020C9200AEF16D /* PageImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageImage.swift; sourceTree = "<group>"; };
|
||||
E25DA58C2D021BA000AEF16D /* WebsiteImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteImage.swift; sourceTree = "<group>"; };
|
||||
@ -220,7 +236,7 @@
|
||||
E25DA5962D023F9900AEF16D /* ContentPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPage.swift; sourceTree = "<group>"; };
|
||||
E25DA5982D02401A00AEF16D /* PageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageGenerator.swift; sourceTree = "<group>"; };
|
||||
E25DA59A2D024A2900AEF16D /* DateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateItem.swift; sourceTree = "<group>"; };
|
||||
E29D311F2D0320E20051B7F4 /* HikingStatistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HikingStatistics.swift; sourceTree = "<group>"; };
|
||||
E29D311F2D0320E20051B7F4 /* ContentLabels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLabels.swift; sourceTree = "<group>"; };
|
||||
E29D31212D0363FA0051B7F4 /* ContentButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentButtons.swift; sourceTree = "<group>"; };
|
||||
E29D31232D0366820051B7F4 /* TagList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagList.swift; sourceTree = "<group>"; };
|
||||
E29D31252D0370A50051B7F4 /* VideoOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoOption.swift; sourceTree = "<group>"; };
|
||||
@ -258,7 +274,7 @@
|
||||
E29D31702D08234D0051B7F4 /* GenerationDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationDetailView.swift; sourceTree = "<group>"; };
|
||||
E29D31782D083DDA0051B7F4 /* PageContentResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageContentResultsView.swift; sourceTree = "<group>"; };
|
||||
E29D317C2D086AAE0051B7F4 /* Int+Random.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Random.swift"; sourceTree = "<group>"; };
|
||||
E29D317E2D086F490051B7F4 /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = "<group>"; };
|
||||
E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsIcons.swift; sourceTree = "<group>"; };
|
||||
E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedPageLink.swift; sourceTree = "<group>"; };
|
||||
E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredHeaders.swift; sourceTree = "<group>"; };
|
||||
E29D31862D0AE9D40051B7F4 /* AdditionalPageHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionalPageHeaders.swift; sourceTree = "<group>"; };
|
||||
@ -270,6 +286,23 @@
|
||||
E29D31932D0B7D250051B7F4 /* SvgImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SvgImage.swift; sourceTree = "<group>"; };
|
||||
E29D31952D0C18690051B7F4 /* PathSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSettings.swift; sourceTree = "<group>"; };
|
||||
E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSettingsFile.swift; sourceTree = "<group>"; };
|
||||
E29D319A2D0C452B0051B7F4 /* PageIssue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIssue.swift; sourceTree = "<group>"; };
|
||||
E29D319C2D0C45B60051B7F4 /* PageIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIssueView.swift; sourceTree = "<group>"; };
|
||||
E29D319E2D0C46290051B7F4 /* PageIssueChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIssueChecker.swift; sourceTree = "<group>"; };
|
||||
E29D31A02D0C75C50051B7F4 /* Content+Validation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Validation.swift"; sourceTree = "<group>"; };
|
||||
E29D31A22D0CC98B0051B7F4 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
|
||||
E29D31A42D0CD03A0051B7F4 /* FileSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSelectionView.swift; sourceTree = "<group>"; };
|
||||
E29D31A92D0CEE3C0051B7F4 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = "<group>"; };
|
||||
E29D31AC2D0DA5310051B7F4 /* AudioPlayerIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerIcons.swift; sourceTree = "<group>"; };
|
||||
E29D31B02D0DA5510051B7F4 /* ContentIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentIcon.swift; sourceTree = "<group>"; };
|
||||
E29D31B22D0DA6E50051B7F4 /* ButtonIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcons.swift; sourceTree = "<group>"; };
|
||||
E29D31B42D0DA8490051B7F4 /* PageIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIcon.swift; sourceTree = "<group>"; };
|
||||
E29D31B72D0DAC1D0051B7F4 /* ButtonCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonCommand.swift; sourceTree = "<group>"; };
|
||||
E29D31B92D0DB4EF0051B7F4 /* LabelsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelsCommand.swift; sourceTree = "<group>"; };
|
||||
E29D31BB2D0DB5110051B7F4 /* CommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessor.swift; sourceTree = "<group>"; };
|
||||
E29D31BD2D0DB8560051B7F4 /* AudioPlayerCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerCommand.swift; sourceTree = "<group>"; };
|
||||
E29D31BF2D0DB9ED0051B7F4 /* AudioPlayerContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerContent.swift; sourceTree = "<group>"; };
|
||||
E29D31C22D0DBEF00051B7F4 /* Song.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Song.swift; sourceTree = "<group>"; };
|
||||
E2A21C022CB16C220060935B /* Environment+Language.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Language.swift"; sourceTree = "<group>"; };
|
||||
E2A21C072CB17B810060935B /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
|
||||
E2A21C0D2CB189D70060935B /* Color+RGB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+RGB.swift"; sourceTree = "<group>"; };
|
||||
@ -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 = "<group>";
|
||||
@ -432,11 +467,54 @@
|
||||
E29D318C2D0B2E5E0051B7F4 /* Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E29D31992D0C451B0051B7F4 /* Pages */,
|
||||
E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */,
|
||||
);
|
||||
path = Content;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E29D31992D0C451B0051B7F4 /* Pages */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E29D319E2D0C46290051B7F4 /* PageIssueChecker.swift */,
|
||||
E29D319C2D0C45B60051B7F4 /* PageIssueView.swift */,
|
||||
E29D319A2D0C452B0051B7F4 /* PageIssue.swift */,
|
||||
);
|
||||
path = Pages;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E29D31AB2D0DA52C0051B7F4 /* Icons */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E29D31B42D0DA8490051B7F4 /* PageIcon.swift */,
|
||||
E29D31B22D0DA6E50051B7F4 /* ButtonIcons.swift */,
|
||||
E29D31B02D0DA5510051B7F4 /* ContentIcon.swift */,
|
||||
E29D31AC2D0DA5310051B7F4 /* AudioPlayerIcons.swift */,
|
||||
E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */,
|
||||
);
|
||||
path = Icons;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E29D31B62D0DAC030051B7F4 /* Page Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E29D31BD2D0DB8560051B7F4 /* AudioPlayerCommand.swift */,
|
||||
E29D31BB2D0DB5110051B7F4 /* CommandProcessor.swift */,
|
||||
E29D31B92D0DB4EF0051B7F4 /* LabelsCommand.swift */,
|
||||
E29D31B72D0DAC1D0051B7F4 /* ButtonCommand.swift */,
|
||||
);
|
||||
path = "Page Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E29D31C12D0DBED70051B7F4 /* AudioPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E29D31A92D0CEE3C0051B7F4 /* AudioPlayer.swift */,
|
||||
E29D31BF2D0DB9ED0051B7F4 /* AudioPlayerContent.swift */,
|
||||
);
|
||||
path = AudioPlayer;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E2A21C322CB5BCAC0060935B /* Pages */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -493,6 +571,7 @@
|
||||
E29D31682D0702670051B7F4 /* TextFileContentView.swift */,
|
||||
E29D314A2D04FC940051B7F4 /* FileToAdd.swift */,
|
||||
E29D314C2D04FCBF0051B7F4 /* FileToAddView.swift */,
|
||||
E29D31A42D0CD03A0051B7F4 /* FileSelectionView.swift */,
|
||||
);
|
||||
path = Files;
|
||||
sourceTree = "<group>";
|
||||
@ -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 = "<group>";
|
||||
@ -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" */;
|
||||
|
@ -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
|
||||
|
@ -1,24 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
final class GenerationResultsHandler {
|
||||
|
||||
var requiredVideoFiles: Set<String> = []
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
103
CHDataManagement/Generator/Page Content/ButtonCommand.swift
Normal file
103
CHDataManagement/Generator/Page Content/ButtonCommand.swift
Normal file
@ -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: ``
|
||||
Format: ``
|
||||
Types:
|
||||
- Download: `download=<fileId>,<text>,<download-filename?>`
|
||||
- External link: `external=<url>,<text>`
|
||||
- Git: `git=<url>,<text>`
|
||||
- Play: `play-circle=<text>,<click-action>`
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
|
||||
protocol CommandProcessor {
|
||||
|
||||
var commandType: ShorthandMarkdownKey { get }
|
||||
|
||||
init(content: Content, results: PageGenerationResults)
|
||||
|
||||
func process(_ arguments: [String], markdown: Substring) -> String
|
||||
}
|
30
CHDataManagement/Generator/Page Content/LabelsCommand.swift
Normal file
30
CHDataManagement/Generator/Page Content/LabelsCommand.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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 ""
|
||||
}
|
||||
}
|
||||
|
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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: <img src= <a href= <source>
|
||||
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](<imageId>;<caption?>]`
|
||||
*/
|
||||
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: ``
|
||||
*/
|
||||
private func handleHikingStatistics(_ arguments: [String]) -> String {
|
||||
#warning("Make statistics more generic using key-value pairs")
|
||||
guard (1...5).contains(arguments.count) else {
|
||||
results.invalidCommandArguments.append((.hikingStatistics, arguments))
|
||||
return ""
|
||||
}
|
||||
|
||||
let time = arguments[0].trimmed
|
||||
let elevationUp = arguments.count > 1 ? arguments[1].trimmed : nil
|
||||
let elevationDown = arguments.count > 2 ? arguments[2].trimmed : nil
|
||||
let distance = arguments.count > 3 ? arguments[3].trimmed : nil
|
||||
let calories = arguments.count > 4 ? arguments[4].trimmed : nil
|
||||
|
||||
return HikingStatistics(
|
||||
time: time,
|
||||
elevationUp: elevationUp,
|
||||
elevationDown: elevationDown,
|
||||
distance: distance,
|
||||
calories: calories)
|
||||
.content
|
||||
}
|
||||
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
private func handleDownloadButtons(_ arguments: [String]) -> String {
|
||||
let buttons = arguments.compactMap(convertButton)
|
||||
return ContentButtons(items: buttons).content
|
||||
}
|
||||
|
||||
private func convertButton(definition button: String) -> ContentButtons.Item? {
|
||||
let parts = button.components(separatedBy: ",")
|
||||
guard (2...3).contains(parts.count) else {
|
||||
results.invalidCommandArguments.append((.downloadButtons, parts))
|
||||
return nil
|
||||
}
|
||||
let fileId = parts[0].trimmed
|
||||
let title = parts[1].trimmed
|
||||
let downloadName = parts.count > 2 ? parts[2].trimmed : nil
|
||||
|
||||
guard let file = content.file(id: fileId) else {
|
||||
results.missingFiles.insert(fileId)
|
||||
return nil
|
||||
}
|
||||
results.files.insert(file)
|
||||
let filePath = content.absoluteUrlToFile(file)
|
||||
return ContentButtons.Item(icon: .download, filePath: filePath, text: title, downloadFileName: downloadName)
|
||||
}
|
||||
|
||||
/**
|
||||
Format: ` -> String {
|
||||
private func handleVideo(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count >= 1 else {
|
||||
results.invalidCommandArguments.append((.video, arguments))
|
||||
results.invalid(command: .video, markdown)
|
||||
return ""
|
||||
}
|
||||
let fileId = arguments[0].trimmed
|
||||
|
||||
let options = arguments.dropFirst().compactMap(convertVideoOption)
|
||||
let options = arguments.dropFirst().compactMap { convertVideoOption($0, markdown: markdown) }
|
||||
|
||||
guard let file = content.file(id: fileId) else {
|
||||
results.missingFiles.insert(fileId)
|
||||
guard let file = content.file(fileId) else {
|
||||
results.missing(file: fileId, markdown: markdown)
|
||||
return ""
|
||||
}
|
||||
results.files.insert(file)
|
||||
|
||||
guard let videoType = file.type.videoType?.htmlType else {
|
||||
results.invalidCommandArguments.append((.video, arguments))
|
||||
results.invalid(command: .video, markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
let filePath = content.absoluteUrlToFile(file)
|
||||
return ContentPageVideo(
|
||||
filePath: filePath,
|
||||
filePath: file.absoluteUrl,
|
||||
videoType: videoType,
|
||||
options: options)
|
||||
.content
|
||||
}
|
||||
|
||||
private func convertVideoOption(_ videoOption: String) -> VideoOption? {
|
||||
private func convertVideoOption(_ videoOption: String, markdown: Substring) -> VideoOption? {
|
||||
guard let optionText = videoOption.trimmed.nonEmpty else {
|
||||
return nil
|
||||
}
|
||||
guard let option = VideoOption(rawValue: optionText) else {
|
||||
results.invalidCommandArguments.append((.video, [optionText]))
|
||||
results.invalid(command: .video, markdown)
|
||||
return nil
|
||||
}
|
||||
if case let .poster(imageId) = option {
|
||||
if let image = content.image(imageId) {
|
||||
results.files.insert(image)
|
||||
let link = content.absoluteUrlToFile(image)
|
||||
let width = 2*thumbnailWidth
|
||||
let fullLink = WebsiteImage.imagePath(source: link, width: width, height: width)
|
||||
let fullLink = WebsiteImage.imagePath(source: image.absoluteUrl, width: width, height: width)
|
||||
return .poster(image: fullLink)
|
||||
} else {
|
||||
results.missingFiles.insert(imageId)
|
||||
results.missing(file: imageId, markdown: markdown)
|
||||
return nil // Image file not present, so skip the option
|
||||
}
|
||||
}
|
||||
if case let .src(videoId) = option {
|
||||
if let video = content.video(videoId) {
|
||||
results.files.insert(video)
|
||||
let link = content.absoluteUrlToFile(video)
|
||||
let link = video.absoluteUrl
|
||||
// TODO: Set correct video path?
|
||||
return .src(link)
|
||||
} else {
|
||||
results.missingFiles.insert(videoId)
|
||||
results.missing(file: videoId, markdown: markdown)
|
||||
return nil // Video file not present, so skip the option
|
||||
}
|
||||
}
|
||||
return option
|
||||
}
|
||||
|
||||
private func handleExternalButtons(_ arguments: [String]) -> String {
|
||||
// 
|
||||
}
|
||||
|
||||
private func handleGitButtons(_ arguments: [String]) -> String {
|
||||
// 
|
||||
}
|
||||
|
||||
private func handleButtons(icon: PageIcon, arguments: [String]) -> String {
|
||||
guard arguments.count >= 1 else {
|
||||
results.invalidCommandArguments.append((.externalLink, arguments))
|
||||
return ""
|
||||
}
|
||||
let buttons: [ContentButtons.Item] = arguments.compactMap { button in
|
||||
let parts = button.components(separatedBy: ",")
|
||||
guard parts.count == 2 else {
|
||||
results.invalidCommandArguments.append((.externalLink, parts))
|
||||
return nil
|
||||
}
|
||||
let rawUrl = parts[0].trimmed
|
||||
guard let url = rawUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
|
||||
results.invalidCommandArguments.append((.externalLink, parts))
|
||||
return nil
|
||||
}
|
||||
let title = parts[1].trimmed
|
||||
|
||||
return .init(
|
||||
icon: icon,
|
||||
filePath: url,
|
||||
text: title)
|
||||
}
|
||||
return ContentButtons(items: buttons).content
|
||||
}
|
||||
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
private func handleExternalHtml(_ arguments: [String]) -> String {
|
||||
private func handleExternalHtml(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 1 else {
|
||||
results.invalidCommandArguments.append((.includedHtml, arguments))
|
||||
results.invalid(command: .includedHtml, markdown)
|
||||
return ""
|
||||
}
|
||||
let fileId = arguments[0]
|
||||
guard let file = content.file(id: fileId) else {
|
||||
results.missingFiles.insert(fileId)
|
||||
guard let file = content.file(fileId) else {
|
||||
results.missing(file: fileId, markdown: markdown)
|
||||
return ""
|
||||
}
|
||||
return file.textContent()
|
||||
@ -407,9 +386,9 @@ final class PageContentParser {
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
private func handleSimpleBox(_ arguments: [String]) -> String {
|
||||
private func handleSimpleBox(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count > 1 else {
|
||||
results.invalidCommandArguments.append((.box, arguments))
|
||||
results.invalid(command: .box, markdown)
|
||||
return ""
|
||||
}
|
||||
let title = arguments[0]
|
||||
@ -420,15 +399,15 @@ final class PageContentParser {
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
private func handlePageLink(_ arguments: [String]) -> String {
|
||||
private func handlePageLink(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 1 else {
|
||||
results.invalidCommandArguments.append((.pageLink, arguments))
|
||||
results.invalid(command: .pageLink, markdown)
|
||||
return ""
|
||||
}
|
||||
let pageId = arguments[0]
|
||||
|
||||
guard let page = content.page(pageId) else {
|
||||
results.missingPages.insert(pageId)
|
||||
results.missing(page: pageId, markdown: markdown)
|
||||
return ""
|
||||
}
|
||||
guard !page.isDraft else {
|
||||
@ -437,7 +416,7 @@ final class PageContentParser {
|
||||
}
|
||||
|
||||
let localized = page.localized(in: language)
|
||||
let url = content.absoluteUrlToPage(page, language: language)
|
||||
let url = page.absoluteUrl(for: language)
|
||||
let title = localized.linkPreviewTitle ?? localized.title
|
||||
let description = localized.linkPreviewDescription ?? ""
|
||||
|
||||
@ -447,7 +426,7 @@ final class PageContentParser {
|
||||
results.imagesToGenerate.insert(.init(size: size, image: image))
|
||||
|
||||
return RelatedPageLink.Image(
|
||||
url: content.absoluteUrlToFile(image),
|
||||
url: image.absoluteUrl,
|
||||
description: image.getDescription(for: language),
|
||||
size: size)
|
||||
}
|
||||
@ -463,32 +442,31 @@ final class PageContentParser {
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
private func handleModel(_ arguments: [String]) -> String {
|
||||
private func handleModel(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 1 else {
|
||||
results.invalidCommandArguments.append((.model, arguments))
|
||||
results.invalid(command: .model, markdown)
|
||||
return ""
|
||||
}
|
||||
let fileId = arguments[0]
|
||||
guard fileId.hasSuffix(".glb") else {
|
||||
results.invalidCommandArguments.append((.model, ["\(fileId) is not a .glb file"]))
|
||||
results.invalid(command: .model, markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
guard let file = content.file(id: fileId) else {
|
||||
results.missingFiles.insert(fileId)
|
||||
guard let file = content.file(fileId) else {
|
||||
results.missing(file: fileId, markdown: markdown)
|
||||
return ""
|
||||
}
|
||||
results.files.insert(file)
|
||||
results.requiredHeaders.insert(.modelViewer)
|
||||
|
||||
let path = content.absoluteUrlToFile(file)
|
||||
let description = file.getDescription(for: language)
|
||||
return ModelViewer(file: path, description: description).content
|
||||
return ModelViewer(file: file.absoluteUrl, description: description).content
|
||||
}
|
||||
|
||||
private func handleSvg(_ arguments: [String]) -> String {
|
||||
private func handleSvg(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 5 else {
|
||||
results.invalidCommandArguments.append((.svg, arguments))
|
||||
results.invalid(command: .svg, markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
@ -496,26 +474,24 @@ final class PageContentParser {
|
||||
let y = Int(arguments[2]),
|
||||
let partWidth = Int(arguments[3]),
|
||||
let partHeight = Int(arguments[4]) else {
|
||||
results.invalidCommandArguments.append((.svg, arguments))
|
||||
results.invalid(command: .svg, markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
let imageId = arguments[0]
|
||||
|
||||
guard let image = content.image(imageId) else {
|
||||
results.missingFiles.insert(imageId)
|
||||
results.missing(file: imageId, markdown: markdown)
|
||||
return ""
|
||||
}
|
||||
guard case .image(let imageType) = image.type,
|
||||
imageType == .svg else {
|
||||
results.invalidCommandArguments.append((.svg, arguments))
|
||||
results.invalid(command: .svg, markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
let path = content.absoluteUrlToFile(image)
|
||||
|
||||
return PartialSvgImage(
|
||||
imagePath: path,
|
||||
imagePath: image.absoluteUrl,
|
||||
altText: image.getDescription(for: language),
|
||||
x: x,
|
||||
y: y,
|
||||
@ -523,7 +499,6 @@ final class PageContentParser {
|
||||
height: partHeight)
|
||||
.content
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -23,6 +23,9 @@ final class PageGenerationResults: ObservableObject {
|
||||
@Published
|
||||
var linkedTags: Set<Tag> = []
|
||||
|
||||
@Published
|
||||
var externalLinks: Set<String> = []
|
||||
|
||||
@Published
|
||||
var files: Set<FileResource> = []
|
||||
|
||||
@ -39,10 +42,7 @@ final class PageGenerationResults: ObservableObject {
|
||||
var missingTags: Set<String> = []
|
||||
|
||||
@Published
|
||||
var unknownCommands: [String] = []
|
||||
|
||||
@Published
|
||||
var invalidCommandArguments: [(command: ShorthandMarkdownKey, arguments: [String])] = []
|
||||
var invalidCommands: [(command: ShorthandMarkdownKey?, markdown: String)] = []
|
||||
|
||||
@Published
|
||||
var requiredHeaders: RequiredHeaders = []
|
||||
@ -50,26 +50,45 @@ final class PageGenerationResults: ObservableObject {
|
||||
@Published
|
||||
var requiredFooters: Set<String> = []
|
||||
|
||||
@Published
|
||||
var requiredIcons: Set<PageIcon> = []
|
||||
|
||||
@Published
|
||||
var issues: Set<PageContentAnomaly> = []
|
||||
|
||||
func reset() {
|
||||
linkedPages = []
|
||||
linkedTags = []
|
||||
externalLinks = []
|
||||
files = []
|
||||
imagesToGenerate = []
|
||||
missingPages = []
|
||||
missingFiles = []
|
||||
missingTags = []
|
||||
unknownCommands = []
|
||||
invalidCommandArguments = []
|
||||
invalidCommands = []
|
||||
requiredHeaders = []
|
||||
requiredFooters = []
|
||||
requiredIcons = []
|
||||
issues = []
|
||||
}
|
||||
|
||||
var convertedWarnings: [PageContentAnomaly] {
|
||||
var result = [PageContentAnomaly]()
|
||||
result += missingPages.map { .missingPage($0) }
|
||||
result += missingFiles.map { .missingFile($0) }
|
||||
result += unknownCommands.map { .unknownCommand($0) }
|
||||
result += invalidCommandArguments.map { .invalidCommandArguments(command: $0.command, arguments: $0.arguments) }
|
||||
return result
|
||||
func invalid(command: ShorthandMarkdownKey?, _ markdown: Substring) {
|
||||
invalidCommands.append((command, String(markdown)))
|
||||
issues.insert(.invalidCommand(command: command, markdown: String(markdown)))
|
||||
}
|
||||
|
||||
func missing(page: String, markdown: Substring) {
|
||||
missingPages.insert(page)
|
||||
issues.insert(.missingPage(page: page, markdown: String(markdown)))
|
||||
}
|
||||
|
||||
func missing(tag: String, markdown: Substring) {
|
||||
missingTags.insert(tag)
|
||||
issues.insert(.missingTag(tag: tag, markdown: String(markdown)))
|
||||
}
|
||||
|
||||
func missing(file: String, markdown: Substring) {
|
||||
missingFiles.insert(file)
|
||||
issues.insert(.missingFile(file: file, markdown: String(markdown)))
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,8 @@ final class PageGenerator {
|
||||
navigationBarLinks: navigationBarLinks,
|
||||
pageContent: pageContent,
|
||||
headers: headers.content,
|
||||
footers: contentGenerator.results.requiredFooters.sorted())
|
||||
footers: contentGenerator.results.requiredFooters.sorted(),
|
||||
icons: contentGenerator.results.requiredIcons)
|
||||
.content
|
||||
|
||||
return (fullPage, contentGenerator.results)
|
||||
|
@ -62,7 +62,7 @@ final class PostListPageGenerator {
|
||||
|
||||
let linkUrl = post.linkedPage.map {
|
||||
FeedEntryData.Link(
|
||||
url: content.absoluteUrlToPage($0, language: language),
|
||||
url: $0.absoluteUrl(for: language),
|
||||
text: language == .english ? "View" : "Anzeigen") // TODO: Add to settings
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@ final class PostListPageGenerator {
|
||||
maxWidth: mainContentMaximumWidth,
|
||||
maxHeight: mainContentMaximumWidth)
|
||||
return .init(
|
||||
rawImagePath: content.absoluteUrlToFile(image),
|
||||
rawImagePath: image.absoluteUrl,
|
||||
width: Int(mainContentMaximumWidth),
|
||||
height: Int(mainContentMaximumWidth),
|
||||
altText: image.getDescription(for: language))
|
||||
|
@ -5,10 +5,16 @@ enum HeaderFile: String {
|
||||
|
||||
case modelViewer = "model-viewer.min.js"
|
||||
|
||||
case audioPlayerCss = "audio-player.css"
|
||||
|
||||
case amplitude = "amplitude.min.js"
|
||||
|
||||
var asModule: Bool {
|
||||
switch self {
|
||||
case .codeHightlighting: return false
|
||||
case .modelViewer: return true
|
||||
case .amplitude: return false
|
||||
case .audioPlayerCss: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,20 +13,17 @@ enum ShorthandMarkdownKey: String {
|
||||
/// Format: ``
|
||||
case labels
|
||||
|
||||
/// A video
|
||||
/// Format: ``
|
||||
|
||||
/// A variable number of download buttons for file downloads
|
||||
/// Format: `[download](<<fileId>,<text>,<download-filename?>;...)`
|
||||
case downloadButtons = "download"
|
||||
/// Format: `[buttons](type=<<fileId>,<text>,<download-filename?>;...)`
|
||||
case buttons
|
||||
|
||||
/// A box with a title and content
|
||||
/// Format: ``
|
||||
@ -40,20 +37,16 @@ enum ShorthandMarkdownKey: String {
|
||||
/// Format: ``
|
||||
case pageLink = "page"
|
||||
|
||||
/// A large button to an external page.
|
||||
/// Format: ``
|
||||
case includedHtml = "html"
|
||||
|
||||
/// SVG Image showing only a part of the image
|
||||
/// Format ``
|
||||
case svg
|
||||
|
||||
/// A player to play audio files
|
||||
/// Format: ``
|
||||
case audioPlayer = "audio-player"
|
||||
|
||||
}
|
||||
|
@ -4,14 +4,6 @@ extension Content {
|
||||
("/" + path).replacingOccurrences(of: "//", with: "/")
|
||||
}
|
||||
|
||||
private func pathPrefix(for file: FileResource) -> String {
|
||||
switch file.type {
|
||||
case .image: return settings.paths.imagesOutputFolderPath
|
||||
case .video: return settings.paths.videosOutputFolderPath
|
||||
default: return settings.paths.filesOutputFolderPath
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Paths to items
|
||||
|
||||
func absoluteUrlPrefixForTag(_ tag: Tag, language: ContentLanguage) -> String {
|
||||
@ -22,20 +14,6 @@ extension Content {
|
||||
absoluteUrlPrefixForTag(tag, language: language) + ".html"
|
||||
}
|
||||
|
||||
func absoluteUrlToPage(_ page: Page, language: ContentLanguage) -> String {
|
||||
// TODO: Record link to trace connections between pages
|
||||
makeCleanAbsolutePath(settings.pages.pageUrlPrefix + "/" + page.localized(in: language).urlString)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the url path to a file in the output folder.
|
||||
The result is an absolute path from the output folder for use in HTML.
|
||||
*/
|
||||
func absoluteUrlToFile(_ file: FileResource) -> String {
|
||||
let path = pathPrefix(for: file) + "/" + file.id
|
||||
return makeCleanAbsolutePath(path)
|
||||
}
|
||||
|
||||
// MARK: Find items by id
|
||||
|
||||
func page(_ pageId: String) -> Page? {
|
||||
@ -50,8 +28,8 @@ extension Content {
|
||||
files.first { $0.id == videoId && $0.type.isVideo }
|
||||
}
|
||||
|
||||
func file(id: String) -> FileResource? {
|
||||
files.first { $0.id == id }
|
||||
func file(_ fileId: String) -> FileResource? {
|
||||
files.first { $0.id == fileId }
|
||||
}
|
||||
|
||||
func tag(_ tagId: String) -> Tag? {
|
||||
|
@ -135,6 +135,7 @@ extension Content {
|
||||
pages[pageId] = Page(
|
||||
content: self,
|
||||
id: pageId,
|
||||
externalLink: page.externalLink,
|
||||
isDraft: page.isDraft,
|
||||
createdDate: page.createdDate,
|
||||
startDate: page.startDate,
|
||||
|
@ -47,6 +47,7 @@ private extension Page {
|
||||
|
||||
var pageFile: PageFile {
|
||||
.init(isDraft: isDraft,
|
||||
externalLink: externalLink,
|
||||
tags: tags.map { $0.id },
|
||||
createdDate: createdDate,
|
||||
startDate: startDate,
|
||||
|
28
CHDataManagement/Model/Content+Validation.swift
Normal file
28
CHDataManagement/Model/Content+Validation.swift
Normal file
@ -0,0 +1,28 @@
|
||||
import Foundation
|
||||
|
||||
extension Content {
|
||||
|
||||
private static let disallowedCharactersInIds = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted
|
||||
|
||||
private static let disallowedCharactersInFileIds = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-.")).inverted
|
||||
|
||||
func isNewIdForTag(_ id: String) -> Bool {
|
||||
!tags.contains { $0.id == id }
|
||||
}
|
||||
|
||||
func isNewIdForPage(_ id: String) -> Bool {
|
||||
!pages.contains { $0.id == id }
|
||||
}
|
||||
|
||||
func isNewIdForPost(_ id: String) -> Bool {
|
||||
!posts.contains { $0.id == id }
|
||||
}
|
||||
|
||||
func isValidIdForTagOrTagOrPost(_ id: String) -> Bool {
|
||||
id.rangeOfCharacter(from: Content.disallowedCharactersInIds) == nil
|
||||
}
|
||||
|
||||
func isValidIdForFile(_ id: String) -> Bool {
|
||||
id.rangeOfCharacter(from: Content.disallowedCharactersInFileIds) == nil
|
||||
}
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class FileResource: ObservableObject {
|
||||
|
||||
unowned let content: Content
|
||||
final class FileResource: Item {
|
||||
|
||||
let type: FileType
|
||||
|
||||
@ -24,24 +22,24 @@ final class FileResource: ObservableObject {
|
||||
var size: CGSize = .zero
|
||||
|
||||
init(content: Content, id: String, isExternallyStored: Bool, en: String, de: String) {
|
||||
self.content = content
|
||||
self.id = id
|
||||
self.type = FileType(fileExtension: id.fileExtension)
|
||||
self.englishDescription = en
|
||||
self.germanDescription = de
|
||||
self.isExternallyStored = isExternallyStored
|
||||
super.init(content: content)
|
||||
}
|
||||
|
||||
/**
|
||||
Only for bundle images
|
||||
*/
|
||||
init(resourceImage: String, type: ImageFileType) {
|
||||
self.content = .mock // TODO: Add images to mock
|
||||
self.type = .image(type)
|
||||
self.id = resourceImage
|
||||
self.englishDescription = "A test image included in the bundle"
|
||||
self.germanDescription = "Ein Testbild aus dem Bundle"
|
||||
self.isExternallyStored = true
|
||||
super.init(content: .mock) // TODO: Add images to mock
|
||||
}
|
||||
|
||||
func getDescription(for language: ContentLanguage) -> String {
|
||||
@ -62,6 +60,10 @@ final class FileResource: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func dataContent() throws -> Data {
|
||||
try content.storage.fileData(for: id)
|
||||
}
|
||||
|
||||
// MARK: Images
|
||||
|
||||
var aspectRatio: CGFloat {
|
||||
@ -94,6 +96,26 @@ final class FileResource: ObservableObject {
|
||||
private var failureImage: Image {
|
||||
Image(systemSymbol: .exclamationmarkTriangle)
|
||||
}
|
||||
|
||||
// MARK: Paths
|
||||
|
||||
/**
|
||||
Get the url path to a file in the output folder.
|
||||
The result is an absolute path from the output folder for use in HTML.
|
||||
*/
|
||||
var absoluteUrl: String {
|
||||
let path = pathPrefix + "/" + id
|
||||
return makeCleanAbsolutePath(path)
|
||||
}
|
||||
|
||||
|
||||
private var pathPrefix: String {
|
||||
switch type {
|
||||
case .image: return content.settings.paths.imagesOutputFolderPath
|
||||
case .video: return content.settings.paths.videosOutputFolderPath
|
||||
default: return content.settings.paths.filesOutputFolderPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FileResource: Identifiable {
|
||||
|
18
CHDataManagement/Model/Item.swift
Normal file
18
CHDataManagement/Model/Item.swift
Normal file
@ -0,0 +1,18 @@
|
||||
import Foundation
|
||||
|
||||
class Item: ObservableObject {
|
||||
|
||||
unowned let content: Content
|
||||
|
||||
init(content: Content) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
func makeCleanAbsolutePath(_ path: String) -> String {
|
||||
"/" + makeCleanRelativePath(path)
|
||||
}
|
||||
|
||||
func makeCleanRelativePath(_ path: String) -> String {
|
||||
path.components(separatedBy: "/").filter { !$0.isEmpty }.joined(separator: "/")
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
final class Page: ObservableObject {
|
||||
|
||||
unowned let content: Content
|
||||
final class Page: Item {
|
||||
|
||||
/**
|
||||
The unique id of the entry
|
||||
@ -10,6 +8,15 @@ final class Page: ObservableObject {
|
||||
@Published
|
||||
var id: String
|
||||
|
||||
/**
|
||||
The external link this page points to.
|
||||
|
||||
If this value is not `nil`, then the page has no content
|
||||
and many other features are disabled.
|
||||
*/
|
||||
@Published
|
||||
var externalLink: String?
|
||||
|
||||
@Published
|
||||
var isDraft: Bool
|
||||
|
||||
@ -44,6 +51,7 @@ final class Page: ObservableObject {
|
||||
|
||||
init(content: Content,
|
||||
id: String,
|
||||
externalLink: String?,
|
||||
isDraft: Bool,
|
||||
createdDate: Date,
|
||||
startDate: Date,
|
||||
@ -51,8 +59,8 @@ final class Page: ObservableObject {
|
||||
german: LocalizedPage,
|
||||
english: LocalizedPage,
|
||||
tags: [Tag]) {
|
||||
self.content = content
|
||||
self.id = id
|
||||
self.externalLink = externalLink
|
||||
self.isDraft = isDraft
|
||||
self.createdDate = createdDate
|
||||
self.startDate = startDate
|
||||
@ -61,6 +69,8 @@ final class Page: ObservableObject {
|
||||
self.german = german
|
||||
self.english = english
|
||||
self.tags = tags
|
||||
|
||||
super.init(content: content)
|
||||
}
|
||||
|
||||
func localized(in language: ContentLanguage) -> LocalizedPage {
|
||||
@ -78,6 +88,28 @@ final class Page: ObservableObject {
|
||||
id = newId
|
||||
return true
|
||||
}
|
||||
|
||||
var isExternalUrl: Bool {
|
||||
externalLink != nil
|
||||
}
|
||||
|
||||
// MARK: Paths
|
||||
|
||||
func absoluteUrl(for language: ContentLanguage) -> String {
|
||||
if let url = externalLink {
|
||||
return url
|
||||
}
|
||||
// TODO: Record link to trace connections between pages
|
||||
return makeCleanAbsolutePath(internalPath(for: language))
|
||||
}
|
||||
|
||||
func filePathRelativeToOutputFolder(for language: ContentLanguage) -> String {
|
||||
makeCleanRelativePath(internalPath(for: language))
|
||||
}
|
||||
|
||||
private func internalPath(for language: ContentLanguage) -> String {
|
||||
content.settings.pages.pageUrlPrefix + "/" + localized(in: language).urlString
|
||||
}
|
||||
}
|
||||
|
||||
extension Page: Identifiable {
|
||||
|
22
CHDataManagement/Model/Song.swift
Normal file
22
CHDataManagement/Model/Song.swift
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
struct Song {
|
||||
|
||||
let name: String
|
||||
|
||||
let artist: String
|
||||
|
||||
let album: String
|
||||
|
||||
let track: Int
|
||||
|
||||
/// The file id of the audio file
|
||||
let file: String
|
||||
|
||||
/// The file id of the cover image
|
||||
let cover: String
|
||||
}
|
||||
|
||||
|
||||
extension Song: Codable {
|
||||
|
||||
}
|
@ -15,6 +15,13 @@ final class Tag: ObservableObject {
|
||||
@Published
|
||||
var english: LocalizedTag
|
||||
|
||||
init(id: String) {
|
||||
self.isVisible = true
|
||||
self.english = .init(urlComponent: id, name: id)
|
||||
let deId = id + "-" + ContentLanguage.german.rawValue
|
||||
self.german = .init(urlComponent: deId, name: deId)
|
||||
}
|
||||
|
||||
init(isVisible: Bool = true, german: LocalizedTag, english: LocalizedTag) {
|
||||
self.isVisible = isVisible
|
||||
self.german = german
|
||||
|
@ -5,10 +5,17 @@ struct AdditionalPageHeaders {
|
||||
|
||||
let assetPath: String
|
||||
|
||||
#warning("Provide paths in settings, import files")
|
||||
var content: String {
|
||||
headers.map { header in
|
||||
let module = header.asModule ? " type='module'" : ""
|
||||
return "<script\(module) src='\(assetPath)/\(header.rawValue)'></script>"
|
||||
}.sorted().joined()
|
||||
headers.map(header).sorted().joined()
|
||||
}
|
||||
|
||||
private func header(for asset: HeaderFile) -> String {
|
||||
let file = asset.rawValue
|
||||
guard file.hasSuffix(".js") else {
|
||||
return "<link rel='stylesheet' type='text/css' href='\(assetPath)/css/\(file)'>"
|
||||
}
|
||||
let module = asset.asModule ? " type='module'" : ""
|
||||
return "<script\(module) src='\(assetPath)/js/\(file)'></script>"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,128 @@
|
||||
|
||||
struct AudioPlayer: HtmlProducer {
|
||||
|
||||
let playingText: String
|
||||
|
||||
let items: [PlaylistItem]
|
||||
|
||||
private var top: String {
|
||||
"""
|
||||
<div class='top'>
|
||||
<div> </div>
|
||||
<div class='top-center'>\(playingText)</div>
|
||||
<div class='show-playlist'><svg><use href='#\(AudioPlayerPlaylistIcon.name)'></use></svg></div>
|
||||
</div>
|
||||
"""
|
||||
}
|
||||
|
||||
private var center: String {
|
||||
"""
|
||||
<div class='center'>
|
||||
<img data-amplitude-song-info='cover_art_url' class='main-album-art'/>
|
||||
<div class='song-meta-data'>
|
||||
<span data-amplitude-song-info='name' class='song-name'></span>
|
||||
<span data-amplitude-song-info='artist' class='song-artist'></span>
|
||||
</div>
|
||||
<div class='time-progress'>
|
||||
<div id='progress-container'>
|
||||
<input type='range' class='amplitude-song-slider'/>
|
||||
<progress id='song-played-progress' class='amplitude-song-played-progress'></progress>
|
||||
<progress id='song-buffered-progress' class='amplitude-buffered-progress' value='0'></progress>
|
||||
</div>
|
||||
<div class='time-container'>
|
||||
<span class='current-time'>
|
||||
<span class='amplitude-current-hours'></span>:<span class='amplitude-current-minutes'></span>:<span class='amplitude-current-seconds'></span>
|
||||
</span>
|
||||
<span class='duration'>
|
||||
<span class='amplitude-duration-hours'></span>:<span class='amplitude-duration-minutes'></span>:<span class='amplitude-duration-seconds'></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
}
|
||||
|
||||
private var controls: String {
|
||||
"""
|
||||
<div id="audio-player-controls">
|
||||
<svg id="previous" class="amplitude-prev"><use href='#\(AudioPlayerPreviousIcon.name)'></use></svg>
|
||||
<div class="amplitude-play-pause" id="play-pause">
|
||||
<svg class="play-icon"><use href='#\(AudioPlayerPlayIcon.name)'></use></svg>
|
||||
<svg class="pause-icon"><use href='#\(AudioPlayerPauseIcon.name)'></use></svg>
|
||||
</div>
|
||||
<svg id="next" class="amplitude-next"><use href='#\(AudioPlayerNextIcon.name)'></use></svg>
|
||||
</div>
|
||||
"""
|
||||
}
|
||||
|
||||
private var playlistStart: String {
|
||||
"""
|
||||
<div id="playlist-container">
|
||||
<div class="top">
|
||||
<div class="queue">Playlist</div>
|
||||
<div class="close-playlist"><svg><use href='#\(AudioPlayerCloseIcon.name)'></use></svg></div>
|
||||
</div>
|
||||
<div class="playlist">
|
||||
"""
|
||||
}
|
||||
|
||||
private var playlistEnd: String {
|
||||
"""
|
||||
</div>
|
||||
|
||||
<div class="white-player-playlist-controls">
|
||||
<img data-amplitude-song-info="cover_art_url" class="playlist-album-art"/>
|
||||
<div class="playlist-controls">
|
||||
<div class="playlist-meta-data">
|
||||
<span data-amplitude-song-info="name" class="song-name"></span>
|
||||
<span data-amplitude-song-info="artist" class="song-artist"></span>
|
||||
</div>
|
||||
<div class="playlist-control-wrapper">
|
||||
<svg class="amplitude-prev" id="playlist-previous"><use href='#\(AudioPlayerPreviousIcon.name)'></use></svg>
|
||||
<div class="amplitude-play-pause" id="playlist-play-pause">
|
||||
<svg class="play-icon"><use href='#\(AudioPlayerPlayIcon.name)'></use></svg>
|
||||
<svg class="pause-icon"><use href='#\(AudioPlayerPauseIcon.name)'></use></svg>
|
||||
</div>
|
||||
<svg class="amplitude-next" id="playlist-next"><use href='#\(AudioPlayerNextIcon.name)'></use></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
}
|
||||
|
||||
func populate(_ result: inout String) {
|
||||
result += "<div class='audio-player'>"
|
||||
result += top
|
||||
result += center
|
||||
result += controls
|
||||
result += playlistStart
|
||||
for item in items {
|
||||
result += item.content
|
||||
}
|
||||
result += playlistEnd
|
||||
result += "</div>"
|
||||
}
|
||||
|
||||
struct PlaylistItem {
|
||||
|
||||
let index: Int
|
||||
|
||||
let image: String
|
||||
|
||||
let name: String
|
||||
|
||||
let album: String
|
||||
|
||||
let track: Int
|
||||
|
||||
let artist: String
|
||||
|
||||
var content: String {
|
||||
"""
|
||||
<div class="playlist-song amplitude-song-container amplitude-play-pause amplitude-paused" data-amplitude-song-index="\(index)"><img src="\(image)"><div class="playlist-song-meta"><span class="playlist-song-name">\(name)</span><span class="playlist-song-artist">\(album) • \(artist)</span></div></div>
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import Foundation
|
||||
|
||||
struct AmplitudeSong: Codable {
|
||||
let name: String
|
||||
let artist: String
|
||||
let album: String
|
||||
let track: String
|
||||
let url: String
|
||||
let cover_art_url: String
|
||||
}
|
||||
|
||||
struct AudioPlayerScript: HtmlProducer {
|
||||
|
||||
let items: [AmplitudeSong]
|
||||
|
||||
init(items: [AmplitudeSong]) {
|
||||
self.items = items
|
||||
}
|
||||
|
||||
func populate(_ result: inout String) {
|
||||
result += "<script>\n"
|
||||
result += "Amplitude.init({ songs: "
|
||||
let songData = try! JSONEncoder().encode(items)
|
||||
result += String(data: songData, encoding: .utf8)!
|
||||
result += "});"
|
||||
result += "function playEntry(index) { Amplitude.playSongAtIndex(index) };"
|
||||
result += animatePlaylist
|
||||
result += "</script>"
|
||||
}
|
||||
|
||||
private var animatePlaylist: String {
|
||||
"""
|
||||
const el = document.getElementById('playlist-container')
|
||||
document.getElementsByClassName('show-playlist')[0].addEventListener('click', function(){
|
||||
el.classList.remove('slide-out-top');
|
||||
el.classList.add('slide-in-top');
|
||||
el.style.display = "block";
|
||||
});
|
||||
document.getElementsByClassName('close-playlist')[0].addEventListener('click', function(){
|
||||
el.classList.remove('slide-in-top');
|
||||
el.classList.add('slide-out-top');
|
||||
el.style.display = "none";
|
||||
});
|
||||
"""
|
||||
}
|
||||
}
|
@ -5,17 +5,20 @@ struct ContentButtons {
|
||||
|
||||
let icon: PageIcon
|
||||
|
||||
let filePath: String
|
||||
let filePath: String?
|
||||
|
||||
let text: String
|
||||
|
||||
let downloadFileName: String?
|
||||
|
||||
init(icon: PageIcon, filePath: String, text: String, downloadFileName: String? = nil) {
|
||||
let onClickText: String?
|
||||
|
||||
init(icon: PageIcon, filePath: String?, text: String, downloadFileName: String? = nil, onClickText: String? = nil) {
|
||||
self.icon = icon
|
||||
self.filePath = filePath
|
||||
self.text = text
|
||||
self.downloadFileName = downloadFileName
|
||||
self.onClickText = onClickText
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,8 +39,10 @@ struct ContentButtons {
|
||||
|
||||
private func addButton(of item: Item, to result: inout String) {
|
||||
let downloadText = item.downloadFileName.map { " download='\($0)'" } ?? ""
|
||||
result += "<a class='tag' href='\(item.filePath)'\(downloadText)>"
|
||||
result += "<svg><use href='#\(item.icon.name)'></use></svg>\(item.text)"
|
||||
let linkText = item.filePath.map { " href='\($0)'" } ?? ""
|
||||
let onClickText = item.onClickText.map { " onClick='\($0)'" } ?? ""
|
||||
result += "<a class='tag'\(linkText)\(downloadText)\(onClickText)>"
|
||||
result += "<svg><use href='#\(item.icon.icon.name)'></use></svg>\(item.text)"
|
||||
result += "</a>"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
|
||||
struct ContentLabel {
|
||||
|
||||
let icon: PageIcon
|
||||
|
||||
let value: String
|
||||
}
|
||||
|
||||
struct ContentLabels {
|
||||
|
||||
private let labels: [ContentLabel]
|
||||
|
||||
init(labels: [ContentLabel]) {
|
||||
self.labels = labels
|
||||
}
|
||||
|
||||
var content: String {
|
||||
guard !labels.isEmpty else {
|
||||
return ""
|
||||
}
|
||||
var result = "<div class='labels-container'>"
|
||||
for label in labels {
|
||||
result += "<div><svg><use href='#\(label.icon.icon.name)'></use></svg>\(label.value)</div>"
|
||||
}
|
||||
result += "</div>"
|
||||
return result
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
|
||||
struct HikingStatistics {
|
||||
|
||||
private let time: String?
|
||||
|
||||
private let elevationUp: String?
|
||||
|
||||
private let elevationDown: String?
|
||||
|
||||
private let distance: String?
|
||||
|
||||
private let calories: String?
|
||||
|
||||
init(time: String?, elevationUp: String?, elevationDown: String?, distance: String?, calories: String?) {
|
||||
self.time = time
|
||||
self.elevationUp = elevationUp
|
||||
self.elevationDown = elevationDown
|
||||
self.distance = distance
|
||||
self.calories = calories
|
||||
}
|
||||
|
||||
var content: String {
|
||||
var result = "<div class='stats-container'>"
|
||||
if let time {
|
||||
result += "<div><svg><use href='#icon-clock'></use></svg>\(time)</div>"
|
||||
}
|
||||
if let elevationUp {
|
||||
result += "<div><svg><use href='#icon-arrow-up'></use></svg>\(elevationUp)</div>"
|
||||
}
|
||||
if let elevationDown {
|
||||
result += "<div><svg><use href='#icon-arrow-down'></use></svg>\(elevationDown)</div>"
|
||||
}
|
||||
if let distance {
|
||||
result += "<div><svg><use href='#icon-sign'></use></svg>\(distance)</div>"
|
||||
}
|
||||
if let calories {
|
||||
result += "<div><svg><use href='#icon-flame'></use></svg>\(calories)</div>"
|
||||
}
|
||||
result += "</div>"
|
||||
return result
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
|
||||
enum PageIcon: CaseIterable {
|
||||
|
||||
case time
|
||||
|
||||
case elevationUp
|
||||
|
||||
case elevationDown
|
||||
|
||||
case distance
|
||||
|
||||
case calories
|
||||
|
||||
case download
|
||||
|
||||
case externalLink
|
||||
|
||||
case gitLink
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .time: return PageIcon.timeIcon
|
||||
case .elevationUp: return PageIcon.elevationUpIcon
|
||||
case .elevationDown: return PageIcon.elevationDownIcon
|
||||
case .distance: return PageIcon.distanceIcon
|
||||
case .calories: return PageIcon.caloriesIcon
|
||||
case .download: return PageIcon.downloadIcon
|
||||
case .externalLink: return PageIcon.externalLinkIcon
|
||||
case .gitLink: return PageIcon.gitLinkIcon
|
||||
}
|
||||
}
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .time: return "icon-clock"
|
||||
case .elevationUp: return "icon-arrow-up"
|
||||
case .elevationDown: return "icon-arrow-down"
|
||||
case .distance: return "icon-sign"
|
||||
case .calories: return "icon-flame"
|
||||
case .download: return "icon-download"
|
||||
case .externalLink: return "icon-external"
|
||||
case .gitLink: return "icon-git"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PageIcon {
|
||||
|
||||
|
||||
private static let timeIcon =
|
||||
"""
|
||||
<svg id="icon-clock" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .3.4l3.5 2a.5.5 0 0 0 .4-.8L8 8.7V3.5z"/>
|
||||
<path fill="currentColor" d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
private static let elevationUpIcon =
|
||||
"""
|
||||
<svg id="icon-arrow-up" width="16" height="16">
|
||||
<path fill="currentColor" d="m14 2.5a.5.5 0 0 0 -.5-.5h-6a.5.5 0 0 0 0 1h4.8l-10.16 10.15a.5.5 0 0 0 .7.7l10.16-10.14v4.79a.5.5 0 0 0 1 0z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
private static let elevationDownIcon =
|
||||
"""
|
||||
<svg id="icon-arrow-down" width="16" height="16">
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M14 13.5a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1 0-1h4.8L2.14 2.85a.5.5 0 1 1 .7-.7L13 12.29V7.5a.5.5 0 0 1 1 0v6z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
private static let distanceIcon =
|
||||
"""
|
||||
<svg id="icon-sign" width="16" height="16">
|
||||
<path fill="currentColor" d="M7 1.4V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.5a1 1 0 0 0 .8-.4l2-2.3a.5.5 0 0 0 0-.6l-2-2.3a1 1 0 0 0-.8-.4H9V1.4a1 1 0 0 0-2 0zM12.5 5l1.7 2-1.7 2H2V5h10.5z"/>
|
||||
</svg>
|
||||
|
||||
"""
|
||||
|
||||
private static let caloriesIcon =
|
||||
"""
|
||||
<svg id="icon-flame" width="16" height="16">
|
||||
<path fill="currentColor" d="M8 16c3.3 0 6-2 6-5.5 0-1.5-.5-4-2.5-6 .3 1.5-1.3 2-1.3 2C11 4 9 .5 6 0c.4 2 .5 4-2 6-1.3 1-2 2.7-2 4.5C2 14 4.7 16 8 16Zm0-1c-1.7 0-3-1-3-2.8 0-.7.3-2 1.3-3-.2.8.7 1.3.7 1.3-.4-1.3.5-3.3 2-3.5-.2 1-.3 2 1 3a3 3 0 0 1 1 2.3C11 14 9.7 15 8 15Z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
private static let downloadIcon: String =
|
||||
"""
|
||||
<svg id="icon-download" viewBox="0 0 40 40">
|
||||
<path fill="currentColor" fill-rule="evenodd" stroke="none" d="M20 40a20 20 0 1 1 20-20 20 20 0 0 1-20 20zm0-36.8A16.8 16.8 0 1 0 36.8 20 16.8 16.8 0 0 0 20 3.2zm.8 27a1 1 0 0 1-1.6 0L12.1 21c-.4-.4 0-1 .7-1H17v-8.7a.8.8 0 0 1 .8-.8h4.4a.8.8 0 0 1 .8.8V20h4.2c.6 0 1.1.5.7 1l-7.1 9.2z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
private static let externalLinkIcon: String =
|
||||
"""
|
||||
<svg id="icon-external" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m7.5-6.923c-.67.204-1.335.82-1.887 1.855A8 8 0 0 0 5.145 4H7.5zM4.09 4a9.3 9.3 0 0 1 .64-1.539 7 7 0 0 1 .597-.933A7.03 7.03 0 0 0 2.255 4zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a7 7 0 0 0-.656 2.5zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5zM8.5 5v2.5h2.99a12.5 12.5 0 0 0-.337-2.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5zM5.145 12q.208.58.468 1.068c.552 1.035 1.218 1.65 1.887 1.855V12zm.182 2.472a7 7 0 0 1-.597-.933A9.3 9.3 0 0 1 4.09 12H2.255a7 7 0 0 0 3.072 2.472M3.82 11a13.7 13.7 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5zm6.853 3.472A7 7 0 0 0 13.745 12H11.91a9.3 9.3 0 0 1-.64 1.539 7 7 0 0 1-.597.933M8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855q.26-.487.468-1.068zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.7 13.7 0 0 1-.312 2.5m2.802-3.5a7 7 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7 7 0 0 0-3.072-2.472c.218.284.418.598.597.933M10.855 4a8 8 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
private static let gitLinkIcon: String =
|
||||
"""
|
||||
<svg id="icon-git" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" d="M15.698 7.287 8.712.302a1.03 1.03 0 0 0-1.457 0l-1.45 1.45 1.84 1.84a1.223 1.223 0 0 1 1.55 1.56l1.773 1.774a1.224 1.224 0 0 1 1.267 2.025 1.226 1.226 0 0 1-2.002-1.334L8.58 5.963v4.353a1.226 1.226 0 1 1-1.008-.036V5.887a1.226 1.226 0 0 1-.666-1.608L5.093 2.465l-4.79 4.79a1.03 1.03 0 0 0 0 1.457l6.986 6.986a1.03 1.03 0 0 0 1.457 0l6.953-6.953a1.03 1.03 0 0 0 0-1.457"/>
|
||||
</svg>
|
||||
"""
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
|
||||
struct AudioPlayerPlaylistIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-playlist"
|
||||
|
||||
static let content = """
|
||||
<svg id='icon-playlist' viewBox="0 0 28 20"><g fill="none"><rect width="15" height="4" x="13" fill="currentColor" rx="2"/><g fill="currentColor"><path d="M0 1.2C0 .7.4.4.8.7l9.3 5.8c.5.3.5.7 0 1L.8 13.3c-.4.2-.8 0-.8-.5z"/><rect width="14" height="4" x="14" y="8" rx="2"/><rect width="28" height="4" y="16" rx="2"/></g></g></svg>
|
||||
"""
|
||||
}
|
||||
|
||||
struct AudioPlayerCloseIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-close"
|
||||
|
||||
static let content = """
|
||||
<svg id='icon-close' viewBox="0 0 18 18"><path fill="currentColor" d="M9 6.194 3.392.586A1.986 1.986 0 0 0 .582.582c-.78.78-.773 2.033.004 2.81L6.194 9 .586 14.608a1.986 1.986 0 0 0-.004 2.81c.78.78 2.033.773 2.81-.004L9 11.806l5.608 5.608a1.986 1.986 0 0 0 2.81.004c.78-.78.773-2.033-.004-2.81L11.806 9l5.608-5.608a1.986 1.986 0 0 0 .004-2.81 1.982 1.982 0 0 0-2.81.004z"/></svg>
|
||||
"""
|
||||
}
|
||||
|
||||
struct AudioPlayerPauseIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-pause"
|
||||
|
||||
static let content = """
|
||||
<svg id='icon-pause' viewBox="0 0 85 85"><g fill="none"><circle cx="42.5" cy="42.5" fill="currentColor" r="42.5"/><path d="m34 55h6v-24h-6zm12 0h6v-24h-6z" fill="#fff" stroke="#fff"/></g></svg>
|
||||
"""
|
||||
}
|
||||
|
||||
struct AudioPlayerPlayIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-play"
|
||||
|
||||
static let content = """
|
||||
<svg id='icon-play' viewBox="0 0 85 85"><g fill="none"><circle cx="42.5" cy="42.5" r="42.5" fill="currentColor"/><path fill="#fff" d="M33.3 31.3c0-2.3 1.5-3.1 3.4-2l18.8 11.5c2 1.1 2 3 0 4.1L36.7 56.3c-1.9 1.2-3.4.3-3.4-1.9z"/></g>
|
||||
</svg>
|
||||
"""
|
||||
}
|
||||
|
||||
struct AudioPlayerPreviousIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-previous"
|
||||
|
||||
static let content = """
|
||||
<svg id='icon-previous' viewBox="0 0 53 53"><g fill="none" transform="matrix(-1 0 0 1 53 0)"><circle cx="26.5" cy="26.5" r="26.5" fill="currentColor"/><g fill="#fff" transform="translate(16 17)"><path d="M.4 1.8C.4.6 1.2.2 2.2.8l12.3 7.5c1 .5 1 1.5 0 2L2.2 17.8c-1 .6-1.8.1-1.8-1z"/><rect width="3" height="17" x="18" y="1" rx="1.5"/></g></g></svg>
|
||||
"""
|
||||
}
|
||||
|
||||
struct AudioPlayerNextIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-next"
|
||||
|
||||
static let content = """
|
||||
<svg id='icon-next' viewBox="0 0 53 53"><g fill="none"><circle cx="26.5" cy="26.5" r="26.5" fill="currentColor"/><g fill="#fff" transform="translate(16 17)"><path d="M.4 1.8C.4.6 1.2.2 2.2.8l12.3 7.5c1 .5 1 1.5 0 2L2.2 17.8c-1 .6-1.8.1-1.8-1z"/><rect width="3" height="17" x="18" y="1" rx="1.5"/></g></g></svg>
|
||||
"""
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
|
||||
struct ButtonDownloadIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-download"
|
||||
|
||||
static let content = """
|
||||
<svg id="icon-download" viewBox="0 0 40 40"><path fill="currentColor" fill-rule="evenodd" stroke="none" d="M20 40a20 20 0 1 1 20-20 20 20 0 0 1-20 20zm0-36.8A16.8 16.8 0 1 0 36.8 20 16.8 16.8 0 0 0 20 3.2zm.8 27a1 1 0 0 1-1.6 0L12.1 21c-.4-.4 0-1 .7-1H17v-8.7a.8.8 0 0 1 .8-.8h4.4a.8.8 0 0 1 .8.8V20h4.2c.6 0 1.1.5.7 1l-7.1 9.2z"/></svg>
|
||||
"""
|
||||
}
|
||||
|
||||
struct ButtonExternalIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-external"
|
||||
|
||||
static let content = """
|
||||
<svg id="icon-external" viewBox="0 0 16 16"><path fill="currentColor" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m7.5-6.923c-.67.204-1.335.82-1.887 1.855A8 8 0 0 0 5.145 4H7.5zM4.09 4a9.3 9.3 0 0 1 .64-1.539 7 7 0 0 1 .597-.933A7.03 7.03 0 0 0 2.255 4zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a7 7 0 0 0-.656 2.5zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5zM8.5 5v2.5h2.99a12.5 12.5 0 0 0-.337-2.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5zM5.145 12q.208.58.468 1.068c.552 1.035 1.218 1.65 1.887 1.855V12zm.182 2.472a7 7 0 0 1-.597-.933A9.3 9.3 0 0 1 4.09 12H2.255a7 7 0 0 0 3.072 2.472M3.82 11a13.7 13.7 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5zm6.853 3.472A7 7 0 0 0 13.745 12H11.91a9.3 9.3 0 0 1-.64 1.539 7 7 0 0 1-.597.933M8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855q.26-.487.468-1.068zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.7 13.7 0 0 1-.312 2.5m2.802-3.5a7 7 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7 7 0 0 0-3.072-2.472c.218.284.418.598.597.933M10.855 4a8 8 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4z"/></svg>
|
||||
"""
|
||||
}
|
||||
|
||||
struct ButtonGitIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-git"
|
||||
|
||||
static let content = """
|
||||
<svg id="icon-git" viewBox="0 0 16 16"><path fill="currentColor" d="M15.698 7.287 8.712.302a1.03 1.03 0 0 0-1.457 0l-1.45 1.45 1.84 1.84a1.223 1.223 0 0 1 1.55 1.56l1.773 1.774a1.224 1.224 0 0 1 1.267 2.025 1.226 1.226 0 0 1-2.002-1.334L8.58 5.963v4.353a1.226 1.226 0 1 1-1.008-.036V5.887a1.226 1.226 0 0 1-.666-1.608L5.093 2.465l-4.79 4.79a1.03 1.03 0 0 0 0 1.457l6.986 6.986a1.03 1.03 0 0 0 1.457 0l6.953-6.953a1.03 1.03 0 0 0 0-1.457"/></svg>
|
||||
"""
|
||||
}
|
||||
|
||||
struct ButtonPlayIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-play-circle"
|
||||
|
||||
static let content = """
|
||||
<svg id='icon-play-circle' viewBox="0 0 1000 1000"><g fill="currentColor"><path d="M452.6 11.2A495 495 0 0 0 90.1 229.8 525.5 525.5 0 0 0 19.8 398c-8.3 40.7-9.8 56.6-9.8 101.6s1.5 60.5 9.8 101.5A529.7 529.7 0 0 0 90 769.5 493.9 493.9 0 0 0 499.6 990c185 0 355.6-106 438.6-272.3a486.8 486.8 0 0 0-46.8-512.3A494.2 494.2 0 0 0 568.6 13.9c-24-3.7-91.5-5.4-116-2.7zm85.5 76.4c31 3.1 59.6 9.2 90.6 19.4a413.4 413.4 0 0 1 263.9 264.2 412 412 0 0 1-100.8 420.6A413.7 413.7 0 0 1 460 911.4 415.2 415.2 0 0 1 87.6 538a416.4 416.4 0 0 1 143.7-353.3 417.2 417.2 0 0 1 306.8-97.2z"/><path d="M375.4 291.7c-4.2 2-7.5 5.4-10 11-3.6 8-3.8 14.1-3.8 196.9 0 183 .2 189 3.8 197 5 10.9 14.4 15.3 26 12.2 11.4-3 320-183.1 329.5-192.3 6.9-6.8 7.6-8.3 7.6-17s-.7-10-7.6-16.8c-7.9-7.6-314-187.2-326.5-191.6-8.3-2.9-11.1-2.9-19 .6z"/></g></svg>
|
||||
"""
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
|
||||
protocol ContentIcon {
|
||||
|
||||
static var name: String { get }
|
||||
|
||||
static var content: String { get }
|
||||
}
|
||||
|
||||
extension ContentIcon {
|
||||
|
||||
var name: String {
|
||||
Self.name
|
||||
}
|
||||
|
||||
var content: String {
|
||||
Self.content
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
|
||||
enum PageIcon: String, CaseIterable {
|
||||
|
||||
// MARK: Statistics
|
||||
|
||||
case statisticsTime = "time"
|
||||
|
||||
case statisticsElevationUp = "elevation-up"
|
||||
|
||||
case statisticsElevationDown = "elevation-down"
|
||||
|
||||
case statisticsDistance = "distance"
|
||||
|
||||
case statisticsEnergy = "energy"
|
||||
|
||||
// MARK: Buttons
|
||||
|
||||
case buttonDownload = "download"
|
||||
|
||||
case buttonExternalLink = "external"
|
||||
|
||||
case buttonGitLink = "git"
|
||||
|
||||
case buttonPlay = "play-circle"
|
||||
|
||||
// MARK: Audio player
|
||||
|
||||
case audioPlayerPlaylist = "playlist"
|
||||
|
||||
case audioPlayerClose = "close"
|
||||
|
||||
case audioPlayerPlay = "play"
|
||||
|
||||
case audioPlayerPause = "pause"
|
||||
|
||||
case audioPlayerPrevious = "previous"
|
||||
|
||||
case audioPlayerNext = "next"
|
||||
|
||||
var icon: ContentIcon.Type {
|
||||
switch self {
|
||||
case .statisticsTime: return StatisticsTimeIcon.self
|
||||
case .statisticsElevationUp: return StatisticsElevationUpIcon.self
|
||||
case .statisticsElevationDown: return StatisticsElevationDownIcon.self
|
||||
case .statisticsDistance: return StatisticsDistanceIcon.self
|
||||
case .statisticsEnergy: return StatisticsEnergyIcon.self
|
||||
case .buttonDownload: return ButtonDownloadIcon.self
|
||||
case .buttonExternalLink: return ButtonExternalIcon.self
|
||||
case .buttonGitLink: return ButtonGitIcon.self
|
||||
case .buttonPlay: return ButtonPlayIcon.self
|
||||
case .audioPlayerPlaylist: return AudioPlayerPlaylistIcon.self
|
||||
case .audioPlayerClose: return AudioPlayerCloseIcon.self
|
||||
case .audioPlayerPlay: return AudioPlayerPlayIcon.self
|
||||
case .audioPlayerPause: return AudioPlayerPauseIcon.self
|
||||
case .audioPlayerPrevious: return AudioPlayerPreviousIcon.self
|
||||
case .audioPlayerNext: return AudioPlayerNextIcon.self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PageIcon: Hashable {
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
|
||||
struct StatisticsTimeIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-time"
|
||||
|
||||
static let content = """
|
||||
<svg id="icon-time" width="16" height="16" viewBox="0 0 16 16"><path fill="currentColor" d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .3.4l3.5 2a.5.5 0 0 0 .4-.8L8 8.7V3.5z"/><path fill="currentColor" d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/></svg>
|
||||
"""
|
||||
}
|
||||
|
||||
struct StatisticsElevationUpIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-elevation-up"
|
||||
|
||||
static let content = """
|
||||
<svg id="icon-elevation-up" width="16" height="16"><path fill="currentColor" d="m14 2.5a.5.5 0 0 0 -.5-.5h-6a.5.5 0 0 0 0 1h4.8l-10.16 10.15a.5.5 0 0 0 .7.7l10.16-10.14v4.79a.5.5 0 0 0 1 0z"/></svg>
|
||||
"""
|
||||
}
|
||||
|
||||
struct StatisticsElevationDownIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-elevation-down"
|
||||
|
||||
static let content = """
|
||||
<svg id="icon-elevation-down" width="16" height="16"><path fill="currentColor" fill-rule="evenodd" d="M14 13.5a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1 0-1h4.8L2.14 2.85a.5.5 0 1 1 .7-.7L13 12.29V7.5a.5.5 0 0 1 1 0v6z"/></svg>
|
||||
"""
|
||||
}
|
||||
|
||||
struct StatisticsDistanceIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-distance"
|
||||
|
||||
static let content = """
|
||||
<svg id="icon-distance" width="16" height="16"><path fill="currentColor" d="M7 1.4V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.5a1 1 0 0 0 .8-.4l2-2.3a.5.5 0 0 0 0-.6l-2-2.3a1 1 0 0 0-.8-.4H9V1.4a1 1 0 0 0-2 0zM12.5 5l1.7 2-1.7 2H2V5h10.5z"/></svg>
|
||||
"""
|
||||
}
|
||||
|
||||
struct StatisticsEnergyIcon: ContentIcon {
|
||||
|
||||
static let name = "icon-energy"
|
||||
|
||||
static let content = """
|
||||
<svg id="icon-energy" width="16" height="16"><path fill="currentColor" d="M8 16c3.3 0 6-2 6-5.5 0-1.5-.5-4-2.5-6 .3 1.5-1.3 2-1.3 2C11 4 9 .5 6 0c.4 2 .5 4-2 6-1.3 1-2 2.7-2 4.5C2 14 4.7 16 8 16Zm0-1c-1.7 0-3-1-3-2.8 0-.7.3-2 1.3-3-.2.8.7 1.3.7 1.3-.4-1.3.5-3.3 2-3.5-.2 1-.3 2 1 3a3 3 0 0 1 1 2.3C11 14 9.7 15 8 15Z"/></svg>
|
||||
"""
|
||||
}
|
@ -22,7 +22,9 @@ struct ContentPage: HtmlProducer {
|
||||
|
||||
private let footers: String
|
||||
|
||||
init(language: ContentLanguage, dateString: String, title: String, tags: [FeedEntryData.Tag], linkTitle: String, description: String, navigationBarLinks: [NavigationBar.Link], pageContent: String, headers: String, footers: [String]) {
|
||||
private let icons: Set<PageIcon>
|
||||
|
||||
init(language: ContentLanguage, dateString: String, title: String, tags: [FeedEntryData.Tag], linkTitle: String, description: String, navigationBarLinks: [NavigationBar.Link], pageContent: String, headers: String, footers: [String], icons: Set<PageIcon>) {
|
||||
self.language = language
|
||||
self.dateString = dateString
|
||||
self.title = title
|
||||
@ -33,6 +35,7 @@ struct ContentPage: HtmlProducer {
|
||||
self.pageContent = pageContent
|
||||
self.headers = headers
|
||||
self.footers = footers.joined()
|
||||
self.icons = icons
|
||||
}
|
||||
|
||||
func populate(_ result: inout String) {
|
||||
@ -55,13 +58,12 @@ struct ContentPage: HtmlProducer {
|
||||
result += "</body></html>" // Close content
|
||||
}
|
||||
|
||||
#warning("Select only required symbols")
|
||||
private let symbols: String = {
|
||||
private var symbols: String {
|
||||
var result = "<div style='display:none'>"
|
||||
for icon in PageIcon.allCases {
|
||||
result += icon.icon
|
||||
for icon in icons {
|
||||
result += icon.icon.content
|
||||
}
|
||||
result += "</div>"
|
||||
return result
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ extension Page {
|
||||
.init(
|
||||
content: .mock,
|
||||
id: "my-id",
|
||||
externalLink: nil,
|
||||
isDraft: true,
|
||||
createdDate: Date(),
|
||||
startDate: Date().addingTimeInterval(-86400),
|
||||
|
@ -4,6 +4,8 @@ struct PageFile {
|
||||
|
||||
let isDraft: Bool
|
||||
|
||||
let externalLink: String?
|
||||
|
||||
let tags: [String]
|
||||
|
||||
let createdDate: Date
|
||||
|
28
CHDataManagement/Views/Files/FileSelectionView.swift
Normal file
28
CHDataManagement/Views/Files/FileSelectionView.swift
Normal file
@ -0,0 +1,28 @@
|
||||
import SwiftUI
|
||||
|
||||
struct FileSelectionView: View {
|
||||
|
||||
@Binding
|
||||
private var selectedFile: FileResource?
|
||||
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss
|
||||
|
||||
init(selectedFile: Binding<FileResource?>) {
|
||||
self._selectedFile = selectedFile
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
FileListView(selectedFile: $selectedFile)
|
||||
.frame(minHeight: 500, idealHeight: 600)
|
||||
HStack {
|
||||
Button("Cancel") {
|
||||
selectedFile = nil
|
||||
dismiss() }
|
||||
Button("Select") { dismiss() }
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
@ -69,6 +69,7 @@ struct AddPageView: View {
|
||||
let page = Page(
|
||||
content: content,
|
||||
id: newPageId,
|
||||
externalLink: nil,
|
||||
isDraft: true,
|
||||
createdDate: .now,
|
||||
startDate: .now,
|
||||
|
@ -6,18 +6,14 @@ struct LocalizedPageContentView: View {
|
||||
|
||||
let pageId: String
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
@ObservedObject
|
||||
var page: LocalizedPage
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
@State
|
||||
private var isGeneratingWebsite = false
|
||||
|
||||
@State
|
||||
private var loadedPageContentLanguage: ContentLanguage?
|
||||
|
||||
@State
|
||||
private var pageContent: String = ""
|
||||
|
||||
@ -27,10 +23,13 @@ struct LocalizedPageContentView: View {
|
||||
@State
|
||||
private var generationResults = PageGenerationResults()
|
||||
|
||||
@State
|
||||
private var didChangeContent = false
|
||||
|
||||
init(pageId: String, page: LocalizedPage) {
|
||||
init(pageId: String, page: LocalizedPage, language: ContentLanguage) {
|
||||
self.pageId = pageId
|
||||
self.page = page
|
||||
self.language = language
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -55,6 +54,9 @@ struct LocalizedPageContentView: View {
|
||||
HighlightedTextEditor(
|
||||
text: $pageContent,
|
||||
highlightRules: .markdown)
|
||||
.onChange(of: pageContent) {
|
||||
didChangeContent = true
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.onAppear(perform: loadContent)
|
||||
@ -68,25 +70,33 @@ struct LocalizedPageContentView: View {
|
||||
|
||||
guard content != "" else {
|
||||
pageContent = "New file"
|
||||
loadedPageContentLanguage = nil
|
||||
DispatchQueue.main.async {
|
||||
didChangeContent = false
|
||||
}
|
||||
return
|
||||
}
|
||||
pageContent = content
|
||||
loadedPageContentLanguage = language
|
||||
checkContent()
|
||||
} catch {
|
||||
print("Failed to load page content: \(error)")
|
||||
pageContent = "Failed to load"
|
||||
loadedPageContentLanguage = nil
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
didChangeContent = false
|
||||
}
|
||||
}
|
||||
|
||||
private func saveContent() {
|
||||
guard let loadedPageContentLanguage else {
|
||||
guard pageContent != "New file", pageContent != "" else {
|
||||
// TODO: Delete file?
|
||||
return
|
||||
}
|
||||
guard didChangeContent else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try page.content.storage.save(pageContent: pageContent, for: pageId, language: loadedPageContentLanguage)
|
||||
try page.content.storage.save(pageContent: pageContent, for: pageId, language: language)
|
||||
didChangeContent = false
|
||||
} catch {
|
||||
print("Failed to save content: \(error)")
|
||||
}
|
||||
|
@ -34,10 +34,12 @@ struct LocalizedPageDetailView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("Page URL String")
|
||||
.font(.headline)
|
||||
TextField("", text: $newUrlString)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Button("Update", action: setNewId)
|
||||
.disabled(newUrlString.isEmpty || containsInvalidCharacters || idExists)
|
||||
.disabled(newUrlString.isEmpty || containsInvalidCharacters || idExists)
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
|
@ -74,11 +74,19 @@ struct PageContentResultsView: View {
|
||||
text: "\(results.files.count + results.missingFiles.count) images and files",
|
||||
items: results.files.sorted().map { $0.id })
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
TextWithPopup(
|
||||
symbol: .docBadgePlus,
|
||||
text: "\(results.linkedPages.count + results.missingPages.count) page links",
|
||||
items: results.linkedPages.sorted().map { $0.localized(in: language).title })
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
TextWithPopup(
|
||||
symbol: .globe,
|
||||
text: "\(results.externalLinks.count) external links",
|
||||
items: results.externalLinks.sorted())
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if !results.missingPages.isEmpty {
|
||||
TextWithPopup(
|
||||
symbol: .exclamationmarkTriangleFill,
|
||||
@ -93,20 +101,11 @@ struct PageContentResultsView: View {
|
||||
items: results.missingFiles.sorted())
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
if !results.unknownCommands.isEmpty {
|
||||
if !results.invalidCommands.isEmpty {
|
||||
TextWithPopup(
|
||||
symbol: .exclamationmarkTriangleFill,
|
||||
text: "\(results.unknownCommands.count) unknown commands",
|
||||
items: results.unknownCommands.sorted())
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
if !results.invalidCommandArguments.isEmpty {
|
||||
TextWithPopup(
|
||||
symbol: .exclamationmarkTriangleFill,
|
||||
text: "\(results.invalidCommandArguments.count) errors",
|
||||
items: results.invalidCommandArguments.map {
|
||||
"\($0.command.rawValue): \($0.arguments.joined(separator: ";"))"
|
||||
})
|
||||
text: "\(results.invalidCommands.count) invalid commands",
|
||||
items: results.invalidCommands.map { $0.markdown }.sorted())
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
|
@ -24,16 +24,25 @@ struct PageContentView: View {
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@State
|
||||
private var isGeneratingWebsite = false
|
||||
|
||||
init(page: Page) {
|
||||
self.page = page
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
LocalizedPageContentView(pageId: page.id, page: page.localized(in: language))
|
||||
.id(page.id + language.rawValue)
|
||||
if page.isExternalUrl {
|
||||
VStack {
|
||||
PageTitleView(page: page.localized(in: language))
|
||||
.id(page.id + language.rawValue)
|
||||
Spacer()
|
||||
Text("No content available for external page")
|
||||
.font(.title)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
}.padding()
|
||||
} else {
|
||||
LocalizedPageContentView(pageId: page.id, page: page.localized(in: language), language: language)
|
||||
.id(page.id + language.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -62,6 +62,13 @@ struct PageDetailView: View {
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
Text("External url")
|
||||
.font(.headline)
|
||||
OptionalTextField("", text: $page.externalLink,
|
||||
prompt: "External url")
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
|
||||
HStack {
|
||||
Text("Draft")
|
||||
.font(.headline)
|
||||
@ -120,19 +127,20 @@ struct PageDetailView: View {
|
||||
return
|
||||
}
|
||||
isGeneratingWebsite = true
|
||||
print("Generating page")
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
var success = true
|
||||
for language in ContentLanguage.allCases {
|
||||
let generator = LocalizedWebsiteGenerator(
|
||||
content: content,
|
||||
language: language)
|
||||
if !generator.generate(page: page) {
|
||||
print("Generation failed")
|
||||
success = false
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
isGeneratingWebsite = false
|
||||
print("Done")
|
||||
didGenerateWebsite = success
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,9 @@ struct PostDetailView: View {
|
||||
@State
|
||||
private var newId: String
|
||||
|
||||
@State
|
||||
private var showLinkedPagePicker = false
|
||||
|
||||
init(post: Post) {
|
||||
self.post = post
|
||||
self.newId = post.id
|
||||
@ -105,11 +108,29 @@ struct PostDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Linked page")
|
||||
.font(.headline)
|
||||
IconButton(symbol: .squareAndPencilCircleFill,
|
||||
size: 22,
|
||||
color: .blue) {
|
||||
showLinkedPagePicker = true
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
Text(post.linkedPage?.localized(in: language).title ?? "No page linked")
|
||||
|
||||
|
||||
LocalizedPostDetailView(post: post.localized(in: language))
|
||||
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.sheet(isPresented: $showLinkedPagePicker) {
|
||||
PagePickerView(
|
||||
showPagePicker: $showLinkedPagePicker,
|
||||
selectedPage: $post.linkedPage)
|
||||
}
|
||||
}
|
||||
|
||||
private func setNewId() {
|
||||
|
@ -1,33 +1,5 @@
|
||||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
private struct PageIssue {
|
||||
|
||||
let id: Int
|
||||
|
||||
let page: Page
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
let message: PageContentAnomaly
|
||||
|
||||
init(page: Page, language: ContentLanguage, message: PageContentAnomaly) {
|
||||
self.id = .random()
|
||||
self.page = page
|
||||
self.language = language
|
||||
self.message = message
|
||||
}
|
||||
|
||||
var title: String {
|
||||
page.localized(in: language).title
|
||||
}
|
||||
}
|
||||
|
||||
extension PageIssue: Identifiable {
|
||||
|
||||
}
|
||||
|
||||
private struct FixSheet: View {
|
||||
|
||||
@Binding
|
||||
@ -72,275 +44,35 @@ private struct FixSheet: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct ErrorSheet: View {
|
||||
@Binding
|
||||
var isPresented: Bool
|
||||
|
||||
@Binding
|
||||
var message: String
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Error")
|
||||
.font(.headline)
|
||||
Text(message)
|
||||
Button("Dismiss", action: { isPresented = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PageSettingsContentView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@State
|
||||
private var isCheckingPages: Bool = false
|
||||
|
||||
|
||||
@State
|
||||
private var issues: [PageIssue] = []
|
||||
|
||||
@State
|
||||
private var message: String = "No fix available"
|
||||
|
||||
@State
|
||||
private var infoItems: [String] = ["No items set"]
|
||||
|
||||
@State
|
||||
private var fixAction: () -> () = {
|
||||
print("No fix action defined")
|
||||
}
|
||||
|
||||
@State
|
||||
private var showFixActionSheet: Bool = false
|
||||
|
||||
@State
|
||||
private var errorMessage: String = ""
|
||||
|
||||
@State
|
||||
private var showErrorAlert: Bool = false
|
||||
@StateObject
|
||||
var checker: PageIssueChecker = .init()
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Button("Check pages", action: checkAllPagesForErrors)
|
||||
.disabled(isCheckingPages)
|
||||
Button("Fix all", action: applyAllEasyFixes)
|
||||
if isCheckingPages {
|
||||
Button("Check pages", action: { checker.check(pages: content.pages) })
|
||||
.disabled(checker.isCheckingPages)
|
||||
if checker.isCheckingPages {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.frame(height: 20)
|
||||
}
|
||||
}
|
||||
Text("\(issues.count) Issues")
|
||||
Text("\(checker.issues.count) Issues")
|
||||
.font(.headline)
|
||||
List(issues) { issue in
|
||||
List(checker.issues.sorted()) { issue in
|
||||
HStack {
|
||||
Button("Attempt Fix", action: { attemptFix(issue: issue) })
|
||||
VStack(alignment: .leading) {
|
||||
Text(issue.message.description)
|
||||
Text("\(issue.title) (\(issue.language.rawValue.uppercased()))")
|
||||
.font(.caption)
|
||||
}
|
||||
PageIssueView(issue: issue)
|
||||
.id(issue.id)
|
||||
}
|
||||
.environmentObject(checker)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.sheet(isPresented: $showFixActionSheet) {
|
||||
FixSheet(isPresented: $showFixActionSheet,
|
||||
message: $message,
|
||||
infoItems: $infoItems) {
|
||||
fixAction()
|
||||
resetFixSheet()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showErrorAlert) {
|
||||
ErrorSheet(isPresented: $showErrorAlert, message: $errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private func checkAllPagesForErrors() {
|
||||
guard !isCheckingPages else {
|
||||
return
|
||||
}
|
||||
isCheckingPages = true
|
||||
issues = []
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
for language in ContentLanguage.allCases {
|
||||
let parser = PageContentParser(
|
||||
content: content,
|
||||
language: language)
|
||||
for page in content.pages {
|
||||
analyze(page: page, parser: parser)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.isCheckingPages = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func analyze(page: Page, parser: PageContentParser) {
|
||||
parser.reset()
|
||||
do {
|
||||
let rawPageContent = try content.storage.pageContent(for: page.id, language: parser.language)
|
||||
_ = parser.generatePage(from: rawPageContent)
|
||||
let results = parser.results.convertedWarnings.map {
|
||||
PageIssue(page: page, language: parser.language, message: $0)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
issues = results + issues
|
||||
}
|
||||
} catch {
|
||||
let message = PageContentAnomaly.failedToLoadContent(error)
|
||||
let error = PageIssue(page: page, language: parser.language, message: message)
|
||||
DispatchQueue.main.async {
|
||||
issues.insert(error, at: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func applyAllEasyFixes() {
|
||||
issues.forEach { issue in
|
||||
switch issue.message {
|
||||
case .missingFile(let file):
|
||||
fix(missingFile: file, in: issue.page, language: issue.language, ask: false)
|
||||
case .unknownCommand(let string):
|
||||
fixUnknownCommand(string, in: issue.page, language: issue.language)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func attemptFix(issue: PageIssue) {
|
||||
switch issue.message {
|
||||
case .failedToLoadContent:
|
||||
show(error: "No fix available for read errors")
|
||||
case .missingFile(let string):
|
||||
fix(missingFile: string, in: issue.page, language: issue.language)
|
||||
case .missingPage(let string):
|
||||
show(error: "No fix available for missing page \(string)")
|
||||
case .unknownCommand(let string):
|
||||
fixUnknownCommand(string, in: issue.page, language: issue.language)
|
||||
case .invalidCommandArguments(let command, let arguments):
|
||||
show(error: "No fix available for invalid arguments to command \(command) (\(arguments))")
|
||||
case .missingTag(let string):
|
||||
show(error: "No fix available for missing tag \(string)")
|
||||
}
|
||||
}
|
||||
|
||||
private func fix(missingFile: String, in page: Page, language: ContentLanguage, ask: Bool = true) {
|
||||
print("Fixing missing file \(missingFile)")
|
||||
let fileId = page.id + "-" + missingFile
|
||||
if let file = content.file(id: fileId) {
|
||||
replace(missingFile, with: file.id, in: page, language: language)
|
||||
// Remove all errors of the page, and generate them new
|
||||
recalculate(page: page, language: language)
|
||||
return
|
||||
}
|
||||
guard ask else {
|
||||
return
|
||||
}
|
||||
let partialMatches = content.files.filter { $0.id.contains(missingFile) }
|
||||
guard partialMatches.count == 1 else {
|
||||
show(error: "Found \(partialMatches.count) matches for file \(missingFile): \(partialMatches.map { $0.id })")
|
||||
return
|
||||
}
|
||||
let file = partialMatches[0]
|
||||
|
||||
// Ask to fix partially matching file
|
||||
let occurences = findOccurences(of: missingFile, in: page, language: language)
|
||||
message = "Found file '\(file.id)' to match \(missingFile) on page '\(page.localized(in: language).title)'. Do you want to replace it?"
|
||||
infoItems = occurences
|
||||
fixAction = {
|
||||
replace(missingFile, with: file.id, in: page, language: language)
|
||||
// Remove all errors of the page, and generate them new
|
||||
recalculate(page: page, language: language)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
showFixActionSheet = true
|
||||
}
|
||||
}
|
||||
|
||||
private func recalculate(page: Page, language: ContentLanguage) {
|
||||
let remaining = issues.filter {
|
||||
$0.language != language || $0.page.id != page.id
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.issues = remaining
|
||||
self.isCheckingPages = true
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let parser = PageContentParser(content: content, language: language)
|
||||
self.analyze(page: page, parser: parser)
|
||||
self.isCheckingPages = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func resetFixSheet() {
|
||||
DispatchQueue.main.async {
|
||||
self.message = "No fix available"
|
||||
self.fixAction = { print("No fix action defined") }
|
||||
self.infoItems = ["No items set"]
|
||||
}
|
||||
}
|
||||
|
||||
private func show(error: String) {
|
||||
DispatchQueue.main.async {
|
||||
errorMessage = error
|
||||
showErrorAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
private func findMatchingFile(with missingFile: String, in page: Page) -> FileResource? {
|
||||
let fileId = page.id + "-" + missingFile
|
||||
if let file = content.file(id: fileId) {
|
||||
return file
|
||||
}
|
||||
let partialMatches = content.files.filter { $0.id.contains(missingFile) }
|
||||
if partialMatches.count == 1 {
|
||||
return partialMatches[0]
|
||||
}
|
||||
show(error: "Found \(partialMatches.count) matches for file \(missingFile): \(partialMatches.map { $0.id })")
|
||||
return nil
|
||||
}
|
||||
|
||||
private func findOccurences(of searchString: String, in page: Page, language: ContentLanguage) -> [String] {
|
||||
let parts: [String]
|
||||
do {
|
||||
parts = try content.storage.pageContent(for: page.id, language: language)
|
||||
.components(separatedBy: searchString)
|
||||
} catch {
|
||||
show(error: "Failed to get page content to find occurences: \(error.localizedDescription)")
|
||||
return []
|
||||
}
|
||||
|
||||
var occurrences: [String] = []
|
||||
for index in parts.indices.dropLast() {
|
||||
let start = parts[index].suffix(10)
|
||||
let end = parts[index+1].prefix(10)
|
||||
let full = "...\(start)\(searchString)\(end)...".replacingOccurrences(of: "\n", with: "\\n")
|
||||
occurrences.append(full)
|
||||
}
|
||||
return occurrences
|
||||
}
|
||||
|
||||
private func replace(_ oldString: String, with newString: String, in page: Page, language: ContentLanguage) {
|
||||
do {
|
||||
let pageContent = try content.storage.pageContent(for: page.id, language: language)
|
||||
.replacingOccurrences(of: oldString, with: newString)
|
||||
try content.storage.save(pageContent: pageContent, for: page.id, language: language)
|
||||
print("Replaced \(oldString) with \(newString) in page \(page.id) (\(language))")
|
||||
} catch {
|
||||
print("Failed to replace in page \(page.id) (\(language)): \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func fixUnknownCommand(_ string: String, in page: Page, language: ContentLanguage) {
|
||||
show(error: "No fix available for command '\(string)'")
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,49 @@
|
||||
|
||||
struct PageIssue {
|
||||
|
||||
let page: Page
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
let message: PageContentAnomaly
|
||||
|
||||
init(page: Page, language: ContentLanguage, message: PageContentAnomaly) {
|
||||
self.page = page
|
||||
self.language = language
|
||||
self.message = message
|
||||
|
||||
print("\(title) (\(language)): \(message)")
|
||||
}
|
||||
|
||||
var title: String {
|
||||
page.localized(in: language).title
|
||||
}
|
||||
}
|
||||
|
||||
extension PageIssue: Identifiable {
|
||||
|
||||
var id: String {
|
||||
page.id + "-" + language.rawValue + "-" + message.id
|
||||
}
|
||||
}
|
||||
|
||||
extension PageIssue: Equatable {
|
||||
|
||||
static func == (lhs: PageIssue, rhs: PageIssue) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
extension PageIssue: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
extension PageIssue: Comparable {
|
||||
|
||||
static func < (lhs: PageIssue, rhs: PageIssue) -> Bool {
|
||||
lhs.id < rhs.id
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
import Foundation
|
||||
|
||||
final class PageIssueChecker: ObservableObject {
|
||||
|
||||
@Published
|
||||
var isCheckingPages: Bool = false
|
||||
|
||||
@Published
|
||||
var issues: Set<PageIssue> = []
|
||||
|
||||
init() {
|
||||
|
||||
}
|
||||
|
||||
func check(pages: [Page], clearListBeforeStart: Bool = true) {
|
||||
guard !isCheckingPages else {
|
||||
return
|
||||
}
|
||||
isCheckingPages = true
|
||||
if clearListBeforeStart {
|
||||
issues = []
|
||||
}
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
for language in ContentLanguage.allCases {
|
||||
self.check(pages: pages, in: language)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.isCheckingPages = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func check(pages: [Page], in language: ContentLanguage) {
|
||||
for page in pages {
|
||||
analyze(page: page, in: language)
|
||||
}
|
||||
}
|
||||
|
||||
func check(page: Page, in language: ContentLanguage) {
|
||||
guard !isCheckingPages else {
|
||||
return
|
||||
}
|
||||
isCheckingPages = true
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.analyze(page: page, in: language)
|
||||
DispatchQueue.main.async {
|
||||
self.isCheckingPages = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func analyze(page: Page, in language: ContentLanguage) {
|
||||
let parser = PageContentParser(content: page.content, language: language)
|
||||
|
||||
let hasPreviousIssues = issues.contains { $0.page == page && $0.language == language }
|
||||
let pageIssues: [PageIssue]
|
||||
do {
|
||||
let rawPageContent = try page.content.storage.pageContent(for: page.id, language: language)
|
||||
_ = parser.generatePage(from: rawPageContent)
|
||||
pageIssues = parser.results.issues.map {
|
||||
PageIssue(page: page, language: language, message: $0)
|
||||
}
|
||||
} catch {
|
||||
let message = PageContentAnomaly.failedToLoadContent(error)
|
||||
let error = PageIssue(page: page, language: language, message: message)
|
||||
pageIssues = [error]
|
||||
}
|
||||
guard hasPreviousIssues || !pageIssues.isEmpty else {
|
||||
return
|
||||
}
|
||||
update(issues: pageIssues, for: page, in: parser.language)
|
||||
}
|
||||
|
||||
private func update(issues: [PageIssue], for page: Page, in language: ContentLanguage) {
|
||||
let newIssues = self.issues
|
||||
.filter { $0.page != page || $0.language != language }
|
||||
.union(issues)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.issues = newIssues
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,316 @@
|
||||
import SwiftUI
|
||||
|
||||
private struct ButtonAction {
|
||||
|
||||
let name: String
|
||||
|
||||
let action: () -> Void
|
||||
}
|
||||
|
||||
private struct PopupSheet: View {
|
||||
|
||||
@Binding
|
||||
var isPresented: Bool
|
||||
|
||||
@Binding
|
||||
var title: String
|
||||
|
||||
@Binding
|
||||
var message: String
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
Text(message)
|
||||
Button("Dismiss") {
|
||||
message = ""
|
||||
isPresented = false
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
|
||||
private struct PageIssueGenericView: View {
|
||||
|
||||
let issue: PageIssue
|
||||
|
||||
let buttons: [ButtonAction]
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(issue.message.description)
|
||||
Text("\(issue.title) (\(issue.language.rawValue.uppercased()))")
|
||||
.font(.caption)
|
||||
}
|
||||
Spacer()
|
||||
ForEach(buttons, id: \.name) { button in
|
||||
Button(button.name, action: button.action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PageIssueView: View {
|
||||
|
||||
let issue: PageIssue
|
||||
|
||||
@EnvironmentObject
|
||||
private var checker: PageIssueChecker
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@State
|
||||
private var showPopupMessage = false
|
||||
|
||||
|
||||
@State
|
||||
private var popupTitle = "Error"
|
||||
|
||||
@State
|
||||
private var popupMessage = ""
|
||||
|
||||
@State
|
||||
private var showPagePicker = false
|
||||
|
||||
@State
|
||||
private var selectedPage: Page?
|
||||
|
||||
@State
|
||||
private var showFilePicker = false
|
||||
|
||||
@State
|
||||
private var selectedFile: FileResource?
|
||||
|
||||
private var buttons: [ButtonAction] {
|
||||
switch issue.message {
|
||||
case .failedToLoadContent:
|
||||
return [.init(name: "Retry", action: retryPageCheck)]
|
||||
case .missingFile(let missing, _):
|
||||
return [
|
||||
.init(name: "Select file", action: { selectFile(missingFile: missing) }),
|
||||
.init(name: "Create external file", action: { createExternalFile(fileId: missing) })
|
||||
]
|
||||
case .missingPage(let missing, _):
|
||||
return [
|
||||
.init(name: "Select page", action: selectPage),
|
||||
.init(name: "Create page", action: { createPage(pageId: missing) })
|
||||
]
|
||||
case .missingTag(let missing, _):
|
||||
return [
|
||||
.init(name: "Select tag", action: { selectTag(missingPage: missing) }),
|
||||
.init(name: "Create tag", action: { createTag(tagId: missing) })
|
||||
]
|
||||
case .invalidCommand(_, let markdown):
|
||||
return [.init(name: "Replace text", action: { replaceCommand(originalText: markdown) })]
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
PageIssueGenericView(issue: issue, buttons: buttons)
|
||||
.sheet(isPresented: $showPopupMessage) {
|
||||
PopupSheet(isPresented: $showPopupMessage, title: $popupTitle, message: $popupMessage)
|
||||
}
|
||||
.sheet(isPresented: $showPagePicker) {
|
||||
if let page = selectedPage {
|
||||
didSelect(page: page)
|
||||
}
|
||||
} content: {
|
||||
PagePickerView(
|
||||
showPagePicker: $showPagePicker,
|
||||
selectedPage: $selectedPage)
|
||||
}
|
||||
.sheet(isPresented: $showFilePicker) {
|
||||
if let file = selectedFile {
|
||||
didSelect(file: file)
|
||||
}
|
||||
} content: {
|
||||
FileSelectionView(selectedFile: $selectedFile)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func show(error: String) {
|
||||
DispatchQueue.main.async {
|
||||
self.popupTitle = "Error"
|
||||
self.popupMessage = error
|
||||
self.showPopupMessage = true
|
||||
}
|
||||
}
|
||||
|
||||
private func show(info: String) {
|
||||
DispatchQueue.main.async {
|
||||
self.popupTitle = "Info"
|
||||
self.popupMessage = info
|
||||
self.showPopupMessage = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func retryPageCheck() {
|
||||
DispatchQueue.main.async {
|
||||
checker.check(pages: content.pages, clearListBeforeStart: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func selectFile(missingFile: String) {
|
||||
selectedFile = nil
|
||||
showFilePicker = true
|
||||
}
|
||||
|
||||
private func didSelect(file newFile: FileResource) {
|
||||
guard case .missingFile(let missingFile, let markdown) = issue.message else {
|
||||
show(error: "Inconsistency: Selected file, but issue is not a missing file")
|
||||
return
|
||||
}
|
||||
replace(missing: missingFile, with: newFile.id, in: markdown)
|
||||
retryPageCheck()
|
||||
DispatchQueue.main.async {
|
||||
selectedFile = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func createExternalFile(fileId: String) {
|
||||
guard content.isValidIdForFile(fileId) else {
|
||||
show(error: "Invalid file id, can't create external file")
|
||||
return
|
||||
}
|
||||
|
||||
let file = FileResource(
|
||||
content: content,
|
||||
id: fileId,
|
||||
isExternallyStored: true,
|
||||
en: "",
|
||||
de: "")
|
||||
content.files.append(file)
|
||||
|
||||
retryPageCheck()
|
||||
}
|
||||
|
||||
private func selectPage() {
|
||||
selectedPage = nil
|
||||
showPagePicker = true
|
||||
}
|
||||
|
||||
private func didSelect(page newPage: Page) {
|
||||
guard case .missingPage(let missingPage, let markdown) = issue.message else {
|
||||
show(error: "Inconsistency: Selected page, but issue is not a missing page")
|
||||
return
|
||||
}
|
||||
|
||||
replace(missing: missingPage, with: newPage.id, in: markdown)
|
||||
retryPageCheck()
|
||||
DispatchQueue.main.async {
|
||||
selectedPage = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func createPage(pageId: String) {
|
||||
guard content.isValidIdForTagOrTagOrPost(pageId) else {
|
||||
show(error: "Invalid page id, can't create page")
|
||||
return
|
||||
}
|
||||
|
||||
let deString = pageId + "-" + ContentLanguage.german.rawValue
|
||||
|
||||
let page = Page(
|
||||
content: content,
|
||||
id: pageId,
|
||||
externalLink: nil,
|
||||
isDraft: true,
|
||||
createdDate: .now,
|
||||
startDate: .now,
|
||||
endDate: nil,
|
||||
german: .init(content: content,
|
||||
urlString: deString,
|
||||
title: pageId),
|
||||
english: .init(content: content,
|
||||
urlString: pageId,
|
||||
title: pageId),
|
||||
tags: [])
|
||||
content.pages.insert(page, at: 0)
|
||||
|
||||
retryPageCheck()
|
||||
}
|
||||
|
||||
private func selectTag(missingPage: String) {
|
||||
// TODO: Show sheet to select a tag
|
||||
// TODO: Replace tag id in page content with new tag id
|
||||
|
||||
retryPageCheck()
|
||||
}
|
||||
|
||||
private func createTag(tagId: String) {
|
||||
guard content.isValidIdForTagOrTagOrPost(tagId) else {
|
||||
show(error: "Invalid tag id, can't create tag")
|
||||
return
|
||||
}
|
||||
|
||||
let tag = Tag(id: tagId)
|
||||
content.tags.append(tag)
|
||||
|
||||
retryPageCheck()
|
||||
}
|
||||
|
||||
private func replaceCommand(originalText: String) {
|
||||
// TODO: Show sheet with text input
|
||||
// TODO: Replace original text in page content with new text
|
||||
|
||||
retryPageCheck()
|
||||
}
|
||||
|
||||
// MARK: Page Content manipulation
|
||||
|
||||
private func replace(missing: String, with newText: String, in markdown: String) {
|
||||
|
||||
let newString = markdown.replacingOccurrences(of: missing, with: newText)
|
||||
guard newString != markdown else {
|
||||
show(error: "No change in content detected trying to perform replacement")
|
||||
return
|
||||
}
|
||||
|
||||
let occurrences = findOccurrences(of: markdown, in: issue.page, language: issue.language)
|
||||
guard !occurrences.isEmpty else {
|
||||
show(error: "No occurrences of '\(markdown)' found in the page")
|
||||
return
|
||||
}
|
||||
replace(markdown, with: newString, in: issue.page, language: issue.language)
|
||||
|
||||
show(info: "Replaced \(occurrences.count) occurrences of '\(missing)' with '\(newText)'")
|
||||
|
||||
retryPageCheck()
|
||||
}
|
||||
|
||||
private func replace(_ oldString: String, with newString: String, in page: Page, language: ContentLanguage) {
|
||||
do {
|
||||
let pageContent = try content.storage.pageContent(for: page.id, language: language)
|
||||
.replacingOccurrences(of: oldString, with: newString)
|
||||
try content.storage.save(pageContent: pageContent, for: page.id, language: language)
|
||||
print("Replaced \(oldString) with \(newString) in page \(page.id) (\(language))")
|
||||
} catch {
|
||||
print("Failed to replace in page \(page.id) (\(language)): \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func findOccurrences(of searchString: String, in page: Page, language: ContentLanguage) -> [String] {
|
||||
let parts: [String]
|
||||
do {
|
||||
parts = try content.storage.pageContent(for: page.id, language: language)
|
||||
.components(separatedBy: searchString)
|
||||
} catch {
|
||||
print("Failed to get page content to find occurrences: \(error.localizedDescription)")
|
||||
return []
|
||||
}
|
||||
|
||||
var occurrences: [String] = []
|
||||
for index in parts.indices.dropLast() {
|
||||
let start = parts[index].suffix(10)
|
||||
let end = parts[index+1].prefix(10)
|
||||
let full = "...\(start)\(searchString)\(end)...".replacingOccurrences(of: "\n", with: "\\n")
|
||||
occurrences.append(full)
|
||||
}
|
||||
return occurrences
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user