diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index aeeffa9..74af446 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -77,7 +77,7 @@ E25DA57A2D01C64400AEF16D /* PageContentGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */; }; E25DA57D2D01C67900AEF16D /* Ink in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA57C2D01C67900AEF16D /* Ink */; }; E25DA5802D01C6AC00AEF16D /* Splash in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA57F2D01C6AC00AEF16D /* Splash */; }; - E25DA5852D01C92700AEF16D /* ShorthandMarkdownKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */; }; + E25DA5852D01C92700AEF16D /* CommandType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5842D01C92600AEF16D /* CommandType.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 */; }; E25DA58F2D02368D00AEF16D /* PageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA58E2D02368A00AEF16D /* PageSettings.swift */; }; @@ -89,7 +89,7 @@ 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 */; }; + E29D31262D0370A80051B7F4 /* VideoCommand+Option.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31252D0370A50051B7F4 /* VideoCommand+Option.swift */; }; E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31272D0371870051B7F4 /* ContentPageVideo.swift */; }; E29D312A2D039B090051B7F4 /* FileDescriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31292D039B050051B7F4 /* FileDescriptions.swift */; }; E29D312C2D039DB80051B7F4 /* PageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D312B2D039DB30051B7F4 /* PageDetailView.swift */; }; @@ -190,10 +190,10 @@ E2FE0EE62D15A0B5002963B7 /* GenerationResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */; }; E2FE0EE82D16D4A3002963B7 /* ConvertThrowing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */; }; E2FE0EEC2D1C1253002963B7 /* MultiFileSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */; }; - E2FE0EEE2D1C22F3002963B7 /* InlineLinkProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EED2D1C22EF002963B7 /* InlineLinkProcessor.swift */; }; + E2FE0EEE2D1C22F3002963B7 /* MarkdownLinkProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EED2D1C22EF002963B7 /* MarkdownLinkProcessor.swift */; }; E2FE0EF42D1D6D2E002963B7 /* GeneralIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF32D1D6D22002963B7 /* GeneralIcons.swift */; }; E2FE0EF62D1D6DF1002963B7 /* Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF52D1D6DEE002963B7 /* Icon.swift */; }; - E2FE0EF82D1D8110002963B7 /* IconCommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF72D1D810C002963B7 /* IconCommandProcessor.swift */; }; + E2FE0EF82D1D8110002963B7 /* IconCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF72D1D810C002963B7 /* IconCommand.swift */; }; E2FE0EFA2D25AFBA002963B7 /* PageHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF92D25AFB5002963B7 /* PageHeader.swift */; }; E2FE0EFC2D266D22002963B7 /* NavigationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EFB2D266D18002963B7 /* NavigationSettings.swift */; }; E2FE0EFE2D266DA5002963B7 /* NavigationSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EFD2D266DA1002963B7 /* NavigationSettingsFile.swift */; }; @@ -204,20 +204,20 @@ E2FE0F092D2689F0002963B7 /* TagPageGeneratorSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F082D2689F0002963B7 /* TagPageGeneratorSource.swift */; }; E2FE0F0B2D2689FF002963B7 /* FeedGeneratorSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F0A2D2689FF002963B7 /* FeedGeneratorSource.swift */; }; E2FE0F0D2D268A09002963B7 /* PostListPageGeneratorSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F0C2D268A09002963B7 /* PostListPageGeneratorSource.swift */; }; - E2FE0F0F2D268D4F002963B7 /* BoxCommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F0E2D268D4B002963B7 /* BoxCommandProcessor.swift */; }; - E2FE0F112D268E7E002963B7 /* CodeBlockProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F102D268E78002963B7 /* CodeBlockProcessor.swift */; }; - E2FE0F152D26918F002963B7 /* PageHtmlProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F142D269188002963B7 /* PageHtmlProcessor.swift */; }; + E2FE0F0F2D268D4F002963B7 /* BoxCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F0E2D268D4B002963B7 /* BoxCommand.swift */; }; + E2FE0F112D268E7E002963B7 /* MarkdownCodeProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F102D268E78002963B7 /* MarkdownCodeProcessor.swift */; }; + E2FE0F152D26918F002963B7 /* HtmlCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F142D269188002963B7 /* HtmlCommand.swift */; }; E2FE0F172D2698D5002963B7 /* LocalizedPageId.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F162D2698D5002963B7 /* LocalizedPageId.swift */; }; E2FE0F192D2723E3002963B7 /* ImageSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F182D2723E3002963B7 /* ImageSet.swift */; }; E2FE0F1B2D274FDF002963B7 /* LinkPreviewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1A2D274FDA002963B7 /* LinkPreviewItem.swift */; }; E2FE0F1E2D281AE1002963B7 /* TagOverviewGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */; }; E2FE0F202D29A70E002963B7 /* Array+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */; }; - E2FE0F222D2A84A0002963B7 /* VideoCommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */; }; + E2FE0F222D2A84A0002963B7 /* VideoCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F212D2A849B002963B7 /* VideoCommand.swift */; }; E2FE0F242D2A8C21002963B7 /* TagDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F232D2A8C1A002963B7 /* TagDisplayView.swift */; }; E2FE0F262D2AF9B0002963B7 /* ImageCompareCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F252D2AF9AA002963B7 /* ImageCompareCommand.swift */; }; E2FE0F282D2AFB11002963B7 /* ImageCompare.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F272D2AFB0A002963B7 /* ImageCompare.swift */; }; E2FE0F2A2D2AFBE6002963B7 /* ImageCompareIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F292D2AFBE3002963B7 /* ImageCompareIcons.swift */; }; - E2FE0F2C2D2B119A002963B7 /* ImageCommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F2B2D2B1196002963B7 /* ImageCommandProcessor.swift */; }; + E2FE0F2C2D2B119A002963B7 /* ImageCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F2B2D2B1196002963B7 /* ImageCommand.swift */; }; E2FE0F312D2B1952002963B7 /* PartialSvgImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F302D2B1952002963B7 /* PartialSvgImage.swift */; }; E2FE0F332D2B2665002963B7 /* AudioBlockProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F322D2B265F002963B7 /* AudioBlockProcessor.swift */; }; E2FE0F362D2B27F9002963B7 /* BlockProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F352D2B27F6002963B7 /* BlockProcessor.swift */; }; @@ -227,6 +227,17 @@ E2FE0F3E2D2B4225002963B7 /* AudioSettingsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F3D2D2B4225002963B7 /* AudioSettingsDetailView.swift */; }; E2FE0F402D2B45D3002963B7 /* SwiftProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F3F2D2B45CD002963B7 /* SwiftProcessor.swift */; }; E2FE0F422D2B4821002963B7 /* OtherCodeProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F412D2B480B002963B7 /* OtherCodeProcessor.swift */; }; + E2FE0F462D2BC777002963B7 /* MarkdownImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F452D2BC772002963B7 /* MarkdownImageProcessor.swift */; }; + E2FE0F482D2BC7D1002963B7 /* MarkdownProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F472D2BC7CD002963B7 /* MarkdownProcessor.swift */; }; + E2FE0F4B2D2BCCAA002963B7 /* MarkdownHeadlineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F4A2D2BCCA5002963B7 /* MarkdownHeadlineProcessor.swift */; }; + E2FE0F4D2D2BCD30002963B7 /* PageLinkCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F4C2D2BCD2D002963B7 /* PageLinkCommand.swift */; }; + E2FE0F4F2D2BCD80002963B7 /* TagLinkCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F4E2D2BCD7D002963B7 /* TagLinkCommand.swift */; }; + E2FE0F512D2BCDC8002963B7 /* ModelCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F502D2BCDC4002963B7 /* ModelCommand.swift */; }; + E2FE0F532D2BCE17002963B7 /* SvgCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F522D2BCE15002963B7 /* SvgCommand.swift */; }; + E2FE0F552D2BCFC4002963B7 /* ContentBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F542D2BCFC4002963B7 /* ContentBlock.swift */; }; + E2FE0F572D2BCFD4002963B7 /* BlockLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F562D2BCFD4002963B7 /* BlockLineProcessor.swift */; }; + E2FE0F592D2BCFE4002963B7 /* OrderedKeyBlockProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F582D2BCFE4002963B7 /* OrderedKeyBlockProcessor.swift */; }; + E2FE0F5B2D2BCFF2002963B7 /* KeyedBlockProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F5A2D2BCFF2002963B7 /* KeyedBlockProcessor.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -296,7 +307,7 @@ E25DA5742D018B6100AEF16D /* FileDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDetailView.swift; sourceTree = ""; }; E25DA5762D018B9500AEF16D /* File+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "File+Mock.swift"; sourceTree = ""; }; E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageContentGenerator.swift; sourceTree = ""; }; - E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShorthandMarkdownKey.swift; sourceTree = ""; }; + E25DA5842D01C92600AEF16D /* CommandType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandType.swift; sourceTree = ""; }; E25DA5882D01CBCE00AEF16D /* Content+Generation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Generation.swift"; sourceTree = ""; }; E25DA58A2D020C9200AEF16D /* PageImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageImage.swift; sourceTree = ""; }; E25DA58E2D02368A00AEF16D /* PageSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettings.swift; sourceTree = ""; }; @@ -308,7 +319,7 @@ E29D311F2D0320E20051B7F4 /* ContentLabels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLabels.swift; sourceTree = ""; }; E29D31212D0363FA0051B7F4 /* ContentButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentButtons.swift; sourceTree = ""; }; E29D31232D0366820051B7F4 /* TagList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagList.swift; sourceTree = ""; }; - E29D31252D0370A50051B7F4 /* VideoOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoOption.swift; sourceTree = ""; }; + E29D31252D0370A50051B7F4 /* VideoCommand+Option.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VideoCommand+Option.swift"; sourceTree = ""; }; E29D31272D0371870051B7F4 /* ContentPageVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPageVideo.swift; sourceTree = ""; }; E29D31292D039B050051B7F4 /* FileDescriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDescriptions.swift; sourceTree = ""; }; E29D312B2D039DB30051B7F4 /* PageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageDetailView.swift; sourceTree = ""; }; @@ -408,10 +419,10 @@ E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResults.swift; sourceTree = ""; }; E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertThrowing.swift; sourceTree = ""; }; E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFileSelectionView.swift; sourceTree = ""; }; - E2FE0EED2D1C22EF002963B7 /* InlineLinkProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineLinkProcessor.swift; sourceTree = ""; }; + E2FE0EED2D1C22EF002963B7 /* MarkdownLinkProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownLinkProcessor.swift; sourceTree = ""; }; E2FE0EF32D1D6D22002963B7 /* GeneralIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralIcons.swift; sourceTree = ""; }; E2FE0EF52D1D6DEE002963B7 /* Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icon.swift; sourceTree = ""; }; - E2FE0EF72D1D810C002963B7 /* IconCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconCommandProcessor.swift; sourceTree = ""; }; + E2FE0EF72D1D810C002963B7 /* IconCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconCommand.swift; sourceTree = ""; }; E2FE0EF92D25AFB5002963B7 /* PageHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHeader.swift; sourceTree = ""; }; E2FE0EFB2D266D18002963B7 /* NavigationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSettings.swift; sourceTree = ""; }; E2FE0EFD2D266DA1002963B7 /* NavigationSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSettingsFile.swift; sourceTree = ""; }; @@ -422,20 +433,20 @@ E2FE0F082D2689F0002963B7 /* TagPageGeneratorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagPageGeneratorSource.swift; sourceTree = ""; }; E2FE0F0A2D2689FF002963B7 /* FeedGeneratorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedGeneratorSource.swift; sourceTree = ""; }; E2FE0F0C2D268A09002963B7 /* PostListPageGeneratorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListPageGeneratorSource.swift; sourceTree = ""; }; - E2FE0F0E2D268D4B002963B7 /* BoxCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxCommandProcessor.swift; sourceTree = ""; }; - E2FE0F102D268E78002963B7 /* CodeBlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeBlockProcessor.swift; sourceTree = ""; }; - E2FE0F142D269188002963B7 /* PageHtmlProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHtmlProcessor.swift; sourceTree = ""; }; + E2FE0F0E2D268D4B002963B7 /* BoxCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxCommand.swift; sourceTree = ""; }; + E2FE0F102D268E78002963B7 /* MarkdownCodeProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownCodeProcessor.swift; sourceTree = ""; }; + E2FE0F142D269188002963B7 /* HtmlCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlCommand.swift; sourceTree = ""; }; E2FE0F162D2698D5002963B7 /* LocalizedPageId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageId.swift; sourceTree = ""; }; E2FE0F182D2723E3002963B7 /* ImageSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSet.swift; sourceTree = ""; }; E2FE0F1A2D274FDA002963B7 /* LinkPreviewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewItem.swift; sourceTree = ""; }; E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewGenerator.swift; sourceTree = ""; }; E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Remove.swift"; sourceTree = ""; }; - E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCommandProcessor.swift; sourceTree = ""; }; + E2FE0F212D2A849B002963B7 /* VideoCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCommand.swift; sourceTree = ""; }; E2FE0F232D2A8C1A002963B7 /* TagDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagDisplayView.swift; sourceTree = ""; }; E2FE0F252D2AF9AA002963B7 /* ImageCompareCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompareCommand.swift; sourceTree = ""; }; E2FE0F272D2AFB0A002963B7 /* ImageCompare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompare.swift; sourceTree = ""; }; E2FE0F292D2AFBE3002963B7 /* ImageCompareIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompareIcons.swift; sourceTree = ""; }; - E2FE0F2B2D2B1196002963B7 /* ImageCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCommandProcessor.swift; sourceTree = ""; }; + E2FE0F2B2D2B1196002963B7 /* ImageCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCommand.swift; sourceTree = ""; }; E2FE0F302D2B1952002963B7 /* PartialSvgImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialSvgImage.swift; sourceTree = ""; }; E2FE0F322D2B265F002963B7 /* AudioBlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioBlockProcessor.swift; sourceTree = ""; }; E2FE0F352D2B27F6002963B7 /* BlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockProcessor.swift; sourceTree = ""; }; @@ -445,6 +456,17 @@ E2FE0F3D2D2B4225002963B7 /* AudioSettingsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSettingsDetailView.swift; sourceTree = ""; }; E2FE0F3F2D2B45CD002963B7 /* SwiftProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftProcessor.swift; sourceTree = ""; }; E2FE0F412D2B480B002963B7 /* OtherCodeProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherCodeProcessor.swift; sourceTree = ""; }; + E2FE0F452D2BC772002963B7 /* MarkdownImageProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownImageProcessor.swift; sourceTree = ""; }; + E2FE0F472D2BC7CD002963B7 /* MarkdownProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownProcessor.swift; sourceTree = ""; }; + E2FE0F4A2D2BCCA5002963B7 /* MarkdownHeadlineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownHeadlineProcessor.swift; sourceTree = ""; }; + E2FE0F4C2D2BCD2D002963B7 /* PageLinkCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageLinkCommand.swift; sourceTree = ""; }; + E2FE0F4E2D2BCD7D002963B7 /* TagLinkCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagLinkCommand.swift; sourceTree = ""; }; + E2FE0F502D2BCDC4002963B7 /* ModelCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelCommand.swift; sourceTree = ""; }; + E2FE0F522D2BCE15002963B7 /* SvgCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SvgCommand.swift; sourceTree = ""; }; + E2FE0F542D2BCFC4002963B7 /* ContentBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBlock.swift; sourceTree = ""; }; + E2FE0F562D2BCFD4002963B7 /* BlockLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockLineProcessor.swift; sourceTree = ""; }; + E2FE0F582D2BCFE4002963B7 /* OrderedKeyBlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedKeyBlockProcessor.swift; sourceTree = ""; }; + E2FE0F5A2D2BCFF2002963B7 /* KeyedBlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedBlockProcessor.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -525,9 +547,11 @@ E25DA5782D01C56200AEF16D /* Generator */ = { isa = PBXGroup; children = ( + E2FE0F492D2BC9EF002963B7 /* Commands */, + E2FE0F442D2BC76C002963B7 /* Markdown */, + E2FE0F432D2BC718002963B7 /* Results */, E2FE0F342D2B27E6002963B7 /* Blocks */, E2FE0F072D2689DC002963B7 /* Post Lists */, - E29D31B62D0DAC030051B7F4 /* Page Content */, E2FE0F1C2D281A7B002963B7 /* Page Generators */, E22990232D0EDBD0009F8D77 /* HeaderElement.swift */, E29D31842D0AE8EE0051B7F4 /* KnownHeaderElement.swift */, @@ -535,11 +559,6 @@ E22990412D107A94009F8D77 /* ImageVersion.swift */, E2FE0F182D2723E3002963B7 /* ImageSet.swift */, E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */, - E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */, - E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */, - E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */, - E29D31252D0370A50051B7F4 /* VideoOption.swift */, - E29D318F2D0B34870051B7F4 /* GenerationAnomaly.swift */, ); path = Generator; sourceTree = ""; @@ -610,24 +629,6 @@ path = Icons; sourceTree = ""; }; - E29D31B62D0DAC030051B7F4 /* Page Content */ = { - isa = PBXGroup; - children = ( - E29D31BD2D0DB8560051B7F4 /* AudioPlayerCommand.swift */, - E2FE0F0E2D268D4B002963B7 /* BoxCommandProcessor.swift */, - E29D31B72D0DAC1D0051B7F4 /* ButtonCommand.swift */, - E29D31BB2D0DB5110051B7F4 /* CommandProcessor.swift */, - E2FE0EF72D1D810C002963B7 /* IconCommandProcessor.swift */, - E2FE0F2B2D2B1196002963B7 /* ImageCommandProcessor.swift */, - E2FE0F252D2AF9AA002963B7 /* ImageCompareCommand.swift */, - E2FE0EED2D1C22EF002963B7 /* InlineLinkProcessor.swift */, - E29D31B92D0DB4EF0051B7F4 /* LabelsCommand.swift */, - E2FE0F142D269188002963B7 /* PageHtmlProcessor.swift */, - E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */, - ); - path = "Page Content"; - sourceTree = ""; - }; E29D31C12D0DBED70051B7F4 /* AudioPlayer */ = { isa = PBXGroup; children = ( @@ -917,15 +918,71 @@ E2FE0F342D2B27E6002963B7 /* Blocks */ = { isa = PBXGroup; children = ( + E2FE0F5C2D2BD006002963B7 /* Types */, + E2FE0F322D2B265F002963B7 /* AudioBlockProcessor.swift */, + E2FE0F542D2BCFC4002963B7 /* ContentBlock.swift */, E2FE0F412D2B480B002963B7 /* OtherCodeProcessor.swift */, E2FE0F3F2D2B45CD002963B7 /* SwiftProcessor.swift */, - E2FE0F352D2B27F6002963B7 /* BlockProcessor.swift */, - E2FE0F322D2B265F002963B7 /* AudioBlockProcessor.swift */, - E2FE0F102D268E78002963B7 /* CodeBlockProcessor.swift */, ); path = Blocks; sourceTree = ""; }; + E2FE0F432D2BC718002963B7 /* Results */ = { + isa = PBXGroup; + children = ( + E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */, + E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */, + E29D318F2D0B34870051B7F4 /* GenerationAnomaly.swift */, + ); + path = Results; + sourceTree = ""; + }; + E2FE0F442D2BC76C002963B7 /* Markdown */ = { + isa = PBXGroup; + children = ( + E2FE0F102D268E78002963B7 /* MarkdownCodeProcessor.swift */, + E2FE0F4A2D2BCCA5002963B7 /* MarkdownHeadlineProcessor.swift */, + E2FE0F452D2BC772002963B7 /* MarkdownImageProcessor.swift */, + E2FE0EED2D1C22EF002963B7 /* MarkdownLinkProcessor.swift */, + E2FE0F472D2BC7CD002963B7 /* MarkdownProcessor.swift */, + ); + path = Markdown; + sourceTree = ""; + }; + E2FE0F492D2BC9EF002963B7 /* Commands */ = { + isa = PBXGroup; + children = ( + E29D31BD2D0DB8560051B7F4 /* AudioPlayerCommand.swift */, + E2FE0F0E2D268D4B002963B7 /* BoxCommand.swift */, + E29D31B72D0DAC1D0051B7F4 /* ButtonCommand.swift */, + E29D31BB2D0DB5110051B7F4 /* CommandProcessor.swift */, + E25DA5842D01C92600AEF16D /* CommandType.swift */, + E2FE0F142D269188002963B7 /* HtmlCommand.swift */, + E2FE0EF72D1D810C002963B7 /* IconCommand.swift */, + E2FE0F2B2D2B1196002963B7 /* ImageCommand.swift */, + E2FE0F252D2AF9AA002963B7 /* ImageCompareCommand.swift */, + E29D31B92D0DB4EF0051B7F4 /* LabelsCommand.swift */, + E2FE0F502D2BCDC4002963B7 /* ModelCommand.swift */, + E2FE0F4C2D2BCD2D002963B7 /* PageLinkCommand.swift */, + E2FE0F522D2BCE15002963B7 /* SvgCommand.swift */, + E2FE0F4E2D2BCD7D002963B7 /* TagLinkCommand.swift */, + E2FE0F212D2A849B002963B7 /* VideoCommand.swift */, + E29D31252D0370A50051B7F4 /* VideoCommand+Option.swift */, + ); + path = Commands; + sourceTree = ""; + }; + E2FE0F5C2D2BD006002963B7 /* Types */ = { + isa = PBXGroup; + children = ( + E2FE0F562D2BCFD4002963B7 /* BlockLineProcessor.swift */, + E2FE0F352D2B27F6002963B7 /* BlockProcessor.swift */, + E2FE0F5A2D2BCFF2002963B7 /* KeyedBlockProcessor.swift */, + E2FE0F582D2BCFE4002963B7 /* OrderedKeyBlockProcessor.swift */, + ); + path = Types; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1016,9 +1073,10 @@ files = ( E29D31242D0366860051B7F4 /* TagList.swift in Sources */, E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */, + E2FE0F482D2BC7D1002963B7 /* MarkdownProcessor.swift in Sources */, E2FE0EFE2D266DA5002963B7 /* NavigationSettingsFile.swift in Sources */, E2FE0EE62D15A0B5002963B7 /* GenerationResults.swift in Sources */, - E2FE0F152D26918F002963B7 /* PageHtmlProcessor.swift in Sources */, + E2FE0F152D26918F002963B7 /* HtmlCommand.swift in Sources */, E2FE0F202D29A70E002963B7 /* Array+Remove.swift in Sources */, E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */, E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */, @@ -1027,6 +1085,7 @@ E29D317D2D086AB00051B7F4 /* Int+Random.swift in Sources */, E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */, E29D313B2D04464A0051B7F4 /* LocalizedTagDetailView.swift in Sources */, + E2FE0F552D2BCFC4002963B7 /* ContentBlock.swift in Sources */, E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */, E29D31BA2D0DB5080051B7F4 /* LabelsCommand.swift in Sources */, E2FE0EFC2D266D22002963B7 /* NavigationSettings.swift in Sources */, @@ -1039,6 +1098,7 @@ E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */, E229902E2D0F7280009F8D77 /* IdPropertyView.swift in Sources */, E2FE0F3C2D2B3F45002963B7 /* AudioPlayerSettingsFile.swift in Sources */, + E2FE0F462D2BC777002963B7 /* MarkdownImageProcessor.swift in Sources */, E29D31AD2D0DA5360051B7F4 /* AudioPlayerIcons.swift in Sources */, E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */, E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */, @@ -1068,6 +1128,7 @@ E2FE0F362D2B27F9002963B7 /* BlockProcessor.swift in Sources */, E22990462D10B7A7009F8D77 /* SecurityScopeStatus.swift in Sources */, E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */, + E2FE0F4F2D2BCD80002963B7 /* TagLinkCommand.swift in Sources */, E22990342D0F77E9009F8D77 /* PagePropertyView.swift in Sources */, E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */, E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */, @@ -1092,8 +1153,8 @@ E22990302D0F75DE009F8D77 /* BoolPropertyView.swift in Sources */, E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */, E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */, - E29D31262D0370A80051B7F4 /* VideoOption.swift in Sources */, - E2FE0EF82D1D8110002963B7 /* IconCommandProcessor.swift in Sources */, + E29D31262D0370A80051B7F4 /* VideoCommand+Option.swift in Sources */, + E2FE0EF82D1D8110002963B7 /* IconCommand.swift in Sources */, E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */, E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */, E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */, @@ -1118,7 +1179,7 @@ E29D31A32D0CC98C0051B7F4 /* Item.swift in Sources */, E25DA57A2D01C64400AEF16D /* PageContentGenerator.swift in Sources */, E2A21C202CB28ED20060935B /* MockImage.swift in Sources */, - E2FE0EEE2D1C22F3002963B7 /* InlineLinkProcessor.swift in Sources */, + E2FE0EEE2D1C22F3002963B7 /* MarkdownLinkProcessor.swift in Sources */, E2FE0F022D266FCB002963B7 /* LocalizedNavigationSettings.swift in Sources */, E29D313F2D04822C0051B7F4 /* AddPostView.swift in Sources */, E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */, @@ -1138,6 +1199,8 @@ E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */, E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */, E22990382D0F7B32009F8D77 /* OptionalImagePropertyView.swift in Sources */, + E2FE0F512D2BCDC8002963B7 /* ModelCommand.swift in Sources */, + E2FE0F592D2BCFE4002963B7 /* OrderedKeyBlockProcessor.swift in Sources */, E2FE0EEC2D1C1253002963B7 /* MultiFileSelectionView.swift in Sources */, E29D31B32D0DA6E80051B7F4 /* ButtonIcons.swift in Sources */, E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */, @@ -1147,6 +1210,7 @@ E2FE0F002D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift in Sources */, E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */, E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */, + E2FE0F572D2BCFD4002963B7 /* BlockLineProcessor.swift in Sources */, E229904A2D10BB90009F8D77 /* SecurityScopeBookmark.swift in Sources */, E29D314B2D04FC950051B7F4 /* FileToAdd.swift in Sources */, E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */, @@ -1177,13 +1241,13 @@ E29D31C32D0DBEF20051B7F4 /* Song.swift in Sources */, E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */, E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */, - E2FE0F2C2D2B119A002963B7 /* ImageCommandProcessor.swift in Sources */, - E2FE0F112D268E7E002963B7 /* CodeBlockProcessor.swift in Sources */, + E2FE0F2C2D2B119A002963B7 /* ImageCommand.swift in Sources */, + E2FE0F112D268E7E002963B7 /* MarkdownCodeProcessor.swift in Sources */, E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */, E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */, E22990192D0E3546009F8D77 /* ItemType.swift in Sources */, E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */, - E2FE0F0F2D268D4F002963B7 /* BoxCommandProcessor.swift in Sources */, + E2FE0F0F2D268D4F002963B7 /* BoxCommand.swift in Sources */, E22990482D10B7B7009F8D77 /* StorageAccessError.swift in Sources */, E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */, E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */, @@ -1202,9 +1266,11 @@ E25DA5712D01015400AEF16D /* GenerationContentView.swift in Sources */, E29D316F2D0822770051B7F4 /* SettingsListView.swift in Sources */, E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */, - E25DA5852D01C92700AEF16D /* ShorthandMarkdownKey.swift in Sources */, + E25DA5852D01C92700AEF16D /* CommandType.swift in Sources */, E229903A2D0F7E48009F8D77 /* GenericPropertyView.swift in Sources */, E29D31AA2D0CEE3F0051B7F4 /* AudioPlayer.swift in Sources */, + E2FE0F4B2D2BCCAA002963B7 /* MarkdownHeadlineProcessor.swift in Sources */, + E2FE0F532D2BCE17002963B7 /* SvgCommand.swift in Sources */, E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */, E2FE0F3E2D2B4225002963B7 /* AudioSettingsDetailView.swift in Sources */, E25DA58F2D02368D00AEF16D /* PageSettings.swift in Sources */, @@ -1213,9 +1279,10 @@ E229903C2D0F8A7B009F8D77 /* OptionalTextFieldPropertyView.swift in Sources */, E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */, E29D31222D0363FD0051B7F4 /* ContentButtons.swift in Sources */, - E2FE0F222D2A84A0002963B7 /* VideoCommandProcessor.swift in Sources */, + E2FE0F222D2A84A0002963B7 /* VideoCommand.swift in Sources */, E2FE0F192D2723E3002963B7 /* ImageSet.swift in Sources */, E2A21C362CB9A3D70060935B /* PathSettingsView.swift in Sources */, + E2FE0F5B2D2BCFF2002963B7 /* KeyedBlockProcessor.swift in Sources */, E29D31362D0435430051B7F4 /* TabSelection.swift in Sources */, E2FE0F312D2B1952002963B7 /* PartialSvgImage.swift in Sources */, E2FE0F262D2AF9B0002963B7 /* ImageCompareCommand.swift in Sources */, @@ -1225,6 +1292,7 @@ E29D31792D083DE50051B7F4 /* PageContentResultsView.swift in Sources */, E25DA5912D023A8400AEF16D /* IntegerField.swift in Sources */, E29D318B2D0B07EE0051B7F4 /* ContentBox.swift in Sources */, + E2FE0F4D2D2BCD30002963B7 /* PageLinkCommand.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/CHDataManagement/Generator/Blocks/BlockProcessor.swift b/CHDataManagement/Generator/Blocks/BlockProcessor.swift deleted file mode 100644 index e005f63..0000000 --- a/CHDataManagement/Generator/Blocks/BlockProcessor.swift +++ /dev/null @@ -1,86 +0,0 @@ - -enum ContentBlock: String, CaseIterable { - - case audio - - case swift - - var processor: BlockProcessor.Type { - switch self { - case .audio: return AudioBlockProcessor.self - case .swift: return SwiftBlockProcessor.self - } - } -} - -protocol BlockProcessor { - - static var blockId: ContentBlock { get } - - var results: PageGenerationResults { get } - - init(content: Content, results: PageGenerationResults, language: ContentLanguage) - - func process(_ markdown: Substring) -> String -} - -extension BlockProcessor { - - func invalid(_ markdown: Substring) { - results.invalid(block: Self.blockId, markdown) - } -} - -protocol BlockLineProcessor: BlockProcessor { - - func process(_ lines: [String], markdown: Substring) -> String -} - -extension BlockLineProcessor { - - func process(_ markdown: Substring) -> String { - let lines = markdown - .between("```\(Self.blockId.self)", and: "```") - .components(separatedBy: "\n") - return process(lines, markdown: markdown) - } -} - -protocol OrderedKeyBlockProcessor: BlockLineProcessor { - - associatedtype Key: Hashable, RawRepresentable where Key.RawValue == String - - func process(_ arguments: [(key: Key, value: String)], markdown: Substring) -> String -} - -extension OrderedKeyBlockProcessor { - - func process(_ lines: [String], markdown: Substring) -> String { - let result: [(key: Key, value: String)] = lines.compactMap { line in - guard line.trimmed != "" else { - return nil - } - let (rawKey, rawValue) = line.splitAtFirst(":") - guard let key = Key(rawValue: rawKey.trimmed) else { - print("Invalid key \(rawKey)") - invalid(markdown) - return nil - } - return (key, rawValue.trimmed) - } - return process(result, markdown: markdown) - } -} - -protocol KeyedBlockProcessor: OrderedKeyBlockProcessor { - - func process(_ arguments: [Key : String], markdown: Substring) -> String -} - -extension KeyedBlockProcessor { - - func process(_ arguments: [(key: Key, value: String)], markdown: Substring) -> String { - let result = arguments.reduce(into: [:]) { $0[$1.key] = $1.value } - return process(result, markdown: markdown) - } -} diff --git a/CHDataManagement/Generator/Blocks/ContentBlock.swift b/CHDataManagement/Generator/Blocks/ContentBlock.swift new file mode 100644 index 0000000..506c155 --- /dev/null +++ b/CHDataManagement/Generator/Blocks/ContentBlock.swift @@ -0,0 +1,14 @@ + +enum ContentBlock: String, CaseIterable { + + case audio + + case swift + + var processor: BlockProcessor.Type { + switch self { + case .audio: return AudioBlockProcessor.self + case .swift: return SwiftBlockProcessor.self + } + } +} diff --git a/CHDataManagement/Generator/Blocks/Types/BlockLineProcessor.swift b/CHDataManagement/Generator/Blocks/Types/BlockLineProcessor.swift new file mode 100644 index 0000000..4081a28 --- /dev/null +++ b/CHDataManagement/Generator/Blocks/Types/BlockLineProcessor.swift @@ -0,0 +1,15 @@ + +protocol BlockLineProcessor: BlockProcessor { + + func process(_ lines: [String], markdown: Substring) -> String +} + +extension BlockLineProcessor { + + func process(_ markdown: Substring) -> String { + let lines = markdown + .between("```\(Self.blockId.self)", and: "```") + .components(separatedBy: "\n") + return process(lines, markdown: markdown) + } +} diff --git a/CHDataManagement/Generator/Blocks/Types/BlockProcessor.swift b/CHDataManagement/Generator/Blocks/Types/BlockProcessor.swift new file mode 100644 index 0000000..73fc57a --- /dev/null +++ b/CHDataManagement/Generator/Blocks/Types/BlockProcessor.swift @@ -0,0 +1,18 @@ + +protocol BlockProcessor { + + static var blockId: ContentBlock { get } + + var results: PageGenerationResults { get } + + init(content: Content, results: PageGenerationResults, language: ContentLanguage) + + func process(_ markdown: Substring) -> String +} + +extension BlockProcessor { + + func invalid(_ markdown: Substring) { + results.invalid(block: Self.blockId, markdown) + } +} diff --git a/CHDataManagement/Generator/Blocks/Types/KeyedBlockProcessor.swift b/CHDataManagement/Generator/Blocks/Types/KeyedBlockProcessor.swift new file mode 100644 index 0000000..9647b3b --- /dev/null +++ b/CHDataManagement/Generator/Blocks/Types/KeyedBlockProcessor.swift @@ -0,0 +1,13 @@ + +protocol KeyedBlockProcessor: OrderedKeyBlockProcessor { + + func process(_ arguments: [Key : String], markdown: Substring) -> String +} + +extension KeyedBlockProcessor { + + func process(_ arguments: [(key: Key, value: String)], markdown: Substring) -> String { + let result = arguments.reduce(into: [:]) { $0[$1.key] = $1.value } + return process(result, markdown: markdown) + } +} diff --git a/CHDataManagement/Generator/Blocks/Types/OrderedKeyBlockProcessor.swift b/CHDataManagement/Generator/Blocks/Types/OrderedKeyBlockProcessor.swift new file mode 100644 index 0000000..c243ff2 --- /dev/null +++ b/CHDataManagement/Generator/Blocks/Types/OrderedKeyBlockProcessor.swift @@ -0,0 +1,26 @@ + +protocol OrderedKeyBlockProcessor: BlockLineProcessor { + + associatedtype Key: Hashable, RawRepresentable where Key.RawValue == String + + func process(_ arguments: [(key: Key, value: String)], markdown: Substring) -> String +} + +extension OrderedKeyBlockProcessor { + + func process(_ lines: [String], markdown: Substring) -> String { + let result: [(key: Key, value: String)] = lines.compactMap { line in + guard line.trimmed != "" else { + return nil + } + let (rawKey, rawValue) = line.splitAtFirst(":") + guard let key = Key(rawValue: rawKey.trimmed) else { + print("Invalid key \(rawKey)") + invalid(markdown) + return nil + } + return (key, rawValue.trimmed) + } + return process(result, markdown: markdown) + } +} diff --git a/CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift b/CHDataManagement/Generator/Commands/AudioPlayerCommand.swift similarity index 91% rename from CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift rename to CHDataManagement/Generator/Commands/AudioPlayerCommand.swift index 95d2d01..1962975 100644 --- a/CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift +++ b/CHDataManagement/Generator/Commands/AudioPlayerCommand.swift @@ -1,8 +1,8 @@ import Foundation -struct AudioPlayerCommandProcessor: CommandProcessor { +struct AudioPlayerCommand: CommandProcessor { - let commandType: ShorthandMarkdownKey = .audioPlayer + static let commandType: CommandType = .audioPlayer let content: Content @@ -15,14 +15,14 @@ struct AudioPlayerCommandProcessor: CommandProcessor { func process(_ arguments: [String], markdown: Substring) -> String { guard arguments.count == 2 else { - results.invalid(command: .audioPlayer, "Invalid audio player arguments") + invalid("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") + invalid("Invalid file id \(fileId) for audio player") return "" } diff --git a/CHDataManagement/Generator/Page Content/BoxCommandProcessor.swift b/CHDataManagement/Generator/Commands/BoxCommand.swift similarity index 78% rename from CHDataManagement/Generator/Page Content/BoxCommandProcessor.swift rename to CHDataManagement/Generator/Commands/BoxCommand.swift index 747622c..7590215 100644 --- a/CHDataManagement/Generator/Page Content/BoxCommandProcessor.swift +++ b/CHDataManagement/Generator/Commands/BoxCommand.swift @@ -1,7 +1,7 @@ -struct BoxCommandProcessor: CommandProcessor { +struct BoxCommand: CommandProcessor { - let commandType: ShorthandMarkdownKey = .box + static let commandType: CommandType = .box let results: PageGenerationResults @@ -14,7 +14,7 @@ struct BoxCommandProcessor: CommandProcessor { */ func process(_ arguments: [String], markdown: Substring) -> String { guard arguments.count > 1 else { - results.invalid(command: .box, markdown) + invalid(markdown) return "" } let title = arguments[0] diff --git a/CHDataManagement/Generator/Page Content/ButtonCommand.swift b/CHDataManagement/Generator/Commands/ButtonCommand.swift similarity index 92% rename from CHDataManagement/Generator/Page Content/ButtonCommand.swift rename to CHDataManagement/Generator/Commands/ButtonCommand.swift index 1f3dec2..172aec1 100644 --- a/CHDataManagement/Generator/Page Content/ButtonCommand.swift +++ b/CHDataManagement/Generator/Commands/ButtonCommand.swift @@ -1,7 +1,7 @@ -struct ButtonCommandProcessor: CommandProcessor { +struct ButtonCommand: CommandProcessor { - let commandType: ShorthandMarkdownKey = .buttons + static let commandType: CommandType = .buttons let content: Content @@ -27,7 +27,7 @@ struct ButtonCommandProcessor: CommandProcessor { private func convert(button: String, markdown: Substring) -> ContentButtons.Item? { guard let type = PageIcon(rawValue: button.dropAfterFirst("=").trimmed) else { - results.invalid(command: commandType, markdown) + invalid(markdown) return nil } let parts = button.dropBeforeFirst("=").components(separatedBy: ",").map { $0.trimmed } @@ -41,14 +41,14 @@ struct ButtonCommandProcessor: CommandProcessor { case .buttonPlay: return play(arguments: parts, markdown: markdown) default: - results.invalid(command: commandType, markdown) + invalid(markdown) return nil } } private func download(arguments: [String], markdown: Substring) -> ContentButtons.Item? { guard (2...3).contains(arguments.count) else { - results.invalid(command: commandType, markdown) + invalid(markdown) return nil } let fileId = arguments[0].trimmed diff --git a/CHDataManagement/Generator/Commands/CommandProcessor.swift b/CHDataManagement/Generator/Commands/CommandProcessor.swift new file mode 100644 index 0000000..a0f3821 --- /dev/null +++ b/CHDataManagement/Generator/Commands/CommandProcessor.swift @@ -0,0 +1,18 @@ + +protocol CommandProcessor { + + static var commandType: CommandType { get } + + var results: PageGenerationResults { get } + + init(content: Content, results: PageGenerationResults, language: ContentLanguage) + + func process(_ arguments: [String], markdown: Substring) -> String +} + +extension CommandProcessor { + + func invalid(_ markdown: Substring) { + results.invalid(command: Self.commandType, markdown) + } +} diff --git a/CHDataManagement/Generator/ShorthandMarkdownKey.swift b/CHDataManagement/Generator/Commands/CommandType.swift similarity index 69% rename from CHDataManagement/Generator/ShorthandMarkdownKey.swift rename to CHDataManagement/Generator/Commands/CommandType.swift index 1241dac..3d110b1 100644 --- a/CHDataManagement/Generator/ShorthandMarkdownKey.swift +++ b/CHDataManagement/Generator/Commands/CommandType.swift @@ -3,7 +3,7 @@ import Foundation /** A string key used in markdown to indicate special elements */ -enum ShorthandMarkdownKey: String { +enum CommandType: String { /// An image /// Format: `![image](;]` @@ -67,4 +67,26 @@ enum ShorthandMarkdownKey: String { */ case imageCompare = "compare" + + var processor: CommandProcessor.Type { + switch self { + case .image: return ImageCommand.self + case .labels: return LabelsCommand.self + case .video: return VideoCommand.self + case .buttons: return ButtonCommand.self + case .box: return BoxCommand.self + case .model: return ModelCommand.self + case .pageLink: return PageLinkCommand.self + case .tagLink: return TagLinkCommand.self + case .includedHtml: return HtmlCommand.self + case .svg: return SvgCommand.self + case .audioPlayer: return AudioPlayerCommand.self + case .icons: return IconCommand.self + case .imageCompare: return ImageCompareCommand.self + } + } +} + +extension CommandType: CaseIterable { + } diff --git a/CHDataManagement/Generator/Page Content/PageHtmlProcessor.swift b/CHDataManagement/Generator/Commands/HtmlCommand.swift similarity index 96% rename from CHDataManagement/Generator/Page Content/PageHtmlProcessor.swift rename to CHDataManagement/Generator/Commands/HtmlCommand.swift index 5a8fe57..9a1c3de 100644 --- a/CHDataManagement/Generator/Page Content/PageHtmlProcessor.swift +++ b/CHDataManagement/Generator/Commands/HtmlCommand.swift @@ -3,9 +3,9 @@ import SwiftSoup /** Handles both inline HTML and the external HTML command */ -struct PageHtmlProcessor: CommandProcessor { +struct HtmlCommand: CommandProcessor { - let commandType: ShorthandMarkdownKey = .includedHtml + static let commandType: CommandType = .includedHtml let results: PageGenerationResults @@ -23,7 +23,7 @@ struct PageHtmlProcessor: CommandProcessor { */ func process(_ arguments: [String], markdown: Substring) -> String { guard arguments.count == 1 else { - results.invalid(command: .includedHtml, markdown) + invalid(markdown) return "" } let fileId = arguments[0] diff --git a/CHDataManagement/Generator/Page Content/IconCommandProcessor.swift b/CHDataManagement/Generator/Commands/IconCommand.swift similarity index 76% rename from CHDataManagement/Generator/Page Content/IconCommandProcessor.swift rename to CHDataManagement/Generator/Commands/IconCommand.swift index 8a2e96f..52677c0 100644 --- a/CHDataManagement/Generator/Page Content/IconCommandProcessor.swift +++ b/CHDataManagement/Generator/Commands/IconCommand.swift @@ -1,7 +1,7 @@ -struct IconCommandProcessor: CommandProcessor { +struct IconCommand: CommandProcessor { - let commandType: ShorthandMarkdownKey = .icons + static let commandType: CommandType = .icons let results: PageGenerationResults @@ -13,7 +13,7 @@ struct IconCommandProcessor: CommandProcessor { var icons = [PageIcon]() for argument in arguments { guard let icon = PageIcon(rawValue: argument) else { - results.invalid(command: .icons, markdown) + invalid(markdown) continue } icons.append(icon) diff --git a/CHDataManagement/Generator/Page Content/ImageCommandProcessor.swift b/CHDataManagement/Generator/Commands/ImageCommand.swift similarity index 92% rename from CHDataManagement/Generator/Page Content/ImageCommandProcessor.swift rename to CHDataManagement/Generator/Commands/ImageCommand.swift index 33247e2..47672e6 100644 --- a/CHDataManagement/Generator/Page Content/ImageCommandProcessor.swift +++ b/CHDataManagement/Generator/Commands/ImageCommand.swift @@ -1,6 +1,7 @@ -struct ImageCommandProcessor: CommandProcessor { - let commandType: ShorthandMarkdownKey = .image +struct ImageCommand: CommandProcessor { + + static let commandType: CommandType = .image let content: Content @@ -27,7 +28,7 @@ struct ImageCommandProcessor: CommandProcessor { */ func process(_ arguments: [String], markdown: Substring) -> String { guard (1...2).contains(arguments.count) else { - results.invalid(command: .image, markdown) + invalid(markdown) return "" } let imageId = arguments[0] diff --git a/CHDataManagement/Generator/Page Content/ImageCompareCommand.swift b/CHDataManagement/Generator/Commands/ImageCompareCommand.swift similarity index 89% rename from CHDataManagement/Generator/Page Content/ImageCompareCommand.swift rename to CHDataManagement/Generator/Commands/ImageCompareCommand.swift index f1073e0..19ebca5 100644 --- a/CHDataManagement/Generator/Page Content/ImageCompareCommand.swift +++ b/CHDataManagement/Generator/Commands/ImageCompareCommand.swift @@ -1,7 +1,7 @@ -struct ImageCompareCommandProcessor: CommandProcessor { +struct ImageCompareCommand: CommandProcessor { - let commandType: ShorthandMarkdownKey = .imageCompare + static let commandType: CommandType = .imageCompare let content: Content @@ -17,7 +17,7 @@ struct ImageCompareCommandProcessor: CommandProcessor { func process(_ arguments: [String], markdown: Substring) -> String { guard arguments.count == 2 else { - results.invalid(command: .imageCompare, markdown) + invalid(markdown) return "" } let leftImageId = arguments[0] diff --git a/CHDataManagement/Generator/Page Content/LabelsCommand.swift b/CHDataManagement/Generator/Commands/LabelsCommand.swift similarity index 77% rename from CHDataManagement/Generator/Page Content/LabelsCommand.swift rename to CHDataManagement/Generator/Commands/LabelsCommand.swift index 38949f3..e6594fa 100644 --- a/CHDataManagement/Generator/Page Content/LabelsCommand.swift +++ b/CHDataManagement/Generator/Commands/LabelsCommand.swift @@ -1,7 +1,7 @@ -struct LabelsCommandProcessor: CommandProcessor { +struct LabelsCommand: CommandProcessor { - let commandType: ShorthandMarkdownKey = .labels + static let commandType: CommandType = .labels let content: Content @@ -16,11 +16,11 @@ struct LabelsCommandProcessor: CommandProcessor { let labels: [ContentLabel] = arguments.compactMap { arg in let parts = arg.components(separatedBy: "=") guard parts.count == 2 else { - results.invalid(command: .labels, markdown) + invalid(markdown) return nil } guard let icon = PageIcon(rawValue: parts[0].trimmed) else { - results.invalid(command: .labels, markdown) + invalid(markdown) return nil } results.require(icon: icon) diff --git a/CHDataManagement/Generator/Commands/ModelCommand.swift b/CHDataManagement/Generator/Commands/ModelCommand.swift new file mode 100644 index 0000000..15f7a6b --- /dev/null +++ b/CHDataManagement/Generator/Commands/ModelCommand.swift @@ -0,0 +1,44 @@ + +struct ModelCommand: CommandProcessor { + + static let commandType: CommandType = .model + + let content: Content + + let results: PageGenerationResults + + let language: ContentLanguage + + init(content: Content, results: PageGenerationResults, language: ContentLanguage) { + self.content = content + self.results = results + self.language = language + } + + /** + Format: `![model]()` + */ + func process(_ arguments: [String], markdown: Substring) -> String { + guard arguments.count == 1 else { + invalid(markdown) + return "" + } + let fileId = arguments[0] + guard fileId.hasSuffix(".glb") else { + invalid(markdown) + return "" + } + + guard let file = content.file(fileId) else { + results.missing(file: fileId, source: "Model command") + return "" + } + results.require(file: file) + results.require(header: .modelViewer) + + let description = file.localized(in: language) + return ModelViewer(file: file.absoluteUrl, description: description).content + } + + +} diff --git a/CHDataManagement/Generator/Commands/PageLinkCommand.swift b/CHDataManagement/Generator/Commands/PageLinkCommand.swift new file mode 100644 index 0000000..27f1427 --- /dev/null +++ b/CHDataManagement/Generator/Commands/PageLinkCommand.swift @@ -0,0 +1,61 @@ + +struct PageLinkCommand: CommandProcessor { + + static let commandType: CommandType = .pageLink + + let content: Content + + let results: PageGenerationResults + + let language: ContentLanguage + + init(content: Content, results: PageGenerationResults, language: ContentLanguage) { + self.content = content + self.results = results + self.language = language + } + + /** + Format: `![page]()` + */ + func process(_ arguments: [String], markdown: Substring) -> String { + guard arguments.count == 1 else { + invalid(markdown) + return "" + } + let pageId = arguments[0] + + guard let page = content.page(pageId) else { + results.missing(page: pageId, source: "Page link command") + return "" + } + guard !page.isDraft else { + // Prevent linking to unpublished content + return "" + } + + results.linked(to: page) + + let localized = page.localized(in: language) + let url = page.absoluteUrl(in: language) + let title = localized.linkPreviewTitle ?? localized.title + let description = localized.linkPreviewDescription ?? "" + let image = makePageImage(item: localized) + + return RelatedPageLink( + title: title, + description: description, + url: url, + image: image) + .content + } + + private func makePageImage(item: LinkPreviewItem) -> ImageSet? { + item.linkPreviewImage.map { image in + let size = content.settings.pages.pageLinkImageSize + let imageSet = image.imageSet(width: size, height: size, language: language) + results.require(imageSet: imageSet) + return imageSet + } + } +} diff --git a/CHDataManagement/Generator/Commands/SvgCommand.swift b/CHDataManagement/Generator/Commands/SvgCommand.swift new file mode 100644 index 0000000..1a58dc9 --- /dev/null +++ b/CHDataManagement/Generator/Commands/SvgCommand.swift @@ -0,0 +1,52 @@ + +struct SvgCommand: CommandProcessor { + + static let commandType: CommandType = .svg + + let content: Content + + let results: PageGenerationResults + + let language: ContentLanguage + + init(content: Content, results: PageGenerationResults, language: ContentLanguage) { + self.content = content + self.results = results + self.language = language + } + + func process(_ arguments: [String], markdown: Substring) -> String { + guard arguments.count == 5 else { + invalid(markdown) + return "" + } + + guard let x = Int(arguments[1]), + let y = Int(arguments[2]), + let partWidth = Int(arguments[3]), + let partHeight = Int(arguments[4]) else { + invalid(markdown) + return "" + } + + let imageId = arguments[0] + + guard let image = content.image(imageId) else { + results.missing(file: imageId, source: "SVG command") + return "" + } + guard image.type == .svg else { + invalid(markdown) + return "" + } + + return PartialSvgImage( + imagePath: image.absoluteUrl, + altText: image.localized(in: language), + x: x, + y: y, + width: partWidth, + height: partHeight) + .content + } +} diff --git a/CHDataManagement/Generator/Commands/TagLinkCommand.swift b/CHDataManagement/Generator/Commands/TagLinkCommand.swift new file mode 100644 index 0000000..29d30db --- /dev/null +++ b/CHDataManagement/Generator/Commands/TagLinkCommand.swift @@ -0,0 +1,55 @@ + +struct TagLinkCommand: CommandProcessor { + + static let commandType: CommandType = .tagLink + + let content: Content + + let results: PageGenerationResults + + let language: ContentLanguage + + init(content: Content, results: PageGenerationResults, language: ContentLanguage) { + self.content = content + self.results = results + self.language = language + } + + /** + Format: `![tag]()` + */ + func process(_ arguments: [String], markdown: Substring) -> String { + guard arguments.count == 1 else { + invalid(markdown) + return "" + } + let tagId = arguments[0] + + guard let tag = content.tag(tagId) else { + results.missing(tag: tagId, source: "Tag link command") + return "" + } + + let localized = tag.localized(in: language) + let url = tag.absoluteUrl(in: language) + let title = localized.name + let description = localized.linkPreviewDescription ?? "" + let image = makePageImage(item: localized) + + return RelatedPageLink( + title: title, + description: description, + url: url, + image: image) + .content + } + + private func makePageImage(item: LinkPreviewItem) -> ImageSet? { + item.linkPreviewImage.map { image in + let size = content.settings.pages.pageLinkImageSize + let imageSet = image.imageSet(width: size, height: size, language: language) + results.require(imageSet: imageSet) + return imageSet + } + } +} diff --git a/CHDataManagement/Generator/Commands/VideoCommand+Option.swift b/CHDataManagement/Generator/Commands/VideoCommand+Option.swift new file mode 100644 index 0000000..5de8ff0 --- /dev/null +++ b/CHDataManagement/Generator/Commands/VideoCommand+Option.swift @@ -0,0 +1,129 @@ + +extension VideoCommand { + + /// HTML video options + enum Option { + + /// Specifies that video controls should be displayed (such as a play/pause button etc). + case controls + + /// Specifies that the video will start playing as soon as it is ready + case autoplay + + /// Specifies that the video will start over again, every time it is finished + case loop + + /// Specifies that the audio output of the video should be muted + case muted + + /// Mobile browsers will play the video right where it is instead of the default, which is to open it up fullscreen while it plays + case playsinline + + /// Sets the height of the video player + case height(Int) + + /// Sets the width of the video player + case width(Int) + + /// Specifies if and how the author thinks the video should be loaded when the page loads + case preload(Preload) + + /// Specifies an image to be shown while the video is downloading, or until the user hits the play button + case poster(image: String) + + /// Specifies the URL of the video file + case src(String) + + init?(rawValue: String) { + switch rawValue { + case "controls": + self = .controls + return + case "autoplay": + self = .autoplay + return + case "muted": + self = .muted + return + case "loop": + self = .loop + return + case "playsinline": + self = .playsinline + return + default: break + } + + let parts = rawValue.components(separatedBy: "=") + guard parts.count == 2 else { + return nil + } + + let optionName = parts[0] + let value = parts[1].removingSurroundingQuotes + + switch optionName { + case "height": + guard let height = Int(value) else { + return nil + } + self = .height(height) + case "width": + guard let width = Int(value) else { + return nil + } + self = .width(width) + case "preload": + guard let preloadOption = Preload(rawValue: value) else { + return nil + } + self = .preload(preloadOption) + case "poster": + self = .poster(image: value) + case "src": + self = .src(value) + default: + return nil + } + return + } + + var rawValue: String { + switch self { + case .controls: return "controls" + case .autoplay: return "autoplay" + case .muted: return "muted" + case .loop: return "loop" + case .playsinline: return "playsinline" + case .height(let height): return "height='\(height)'" + case .width(let width): return "width='\(width)'" + case .preload(let option): return "preload='\(option)'" + case .poster(let image): return "poster='\(image)'" + case .src(let url): return "src='\(url)'" + } + } + } +} + +extension VideoCommand.Option { + + /** + The `preload` attribute specifies if and how the author thinks that the video should be loaded when the page loads. + + The `preload` attribute allows the author to provide a hint to the browser about what he/she thinks will lead to the best user experience. + This attribute may be ignored in some instances. + + Note: The `preload` attribute is ignored if `autoplay` is present. + */ + enum Preload: String { + + /// The author thinks that the browser should load the entire video when the page loads + case auto + + /// The author thinks that the browser should load only metadata when the page loads + case metadata + + /// The author thinks that the browser should NOT load the video when the page loads + case none + } +} diff --git a/CHDataManagement/Generator/Page Content/VideoCommandProcessor.swift b/CHDataManagement/Generator/Commands/VideoCommand.swift similarity index 84% rename from CHDataManagement/Generator/Page Content/VideoCommandProcessor.swift rename to CHDataManagement/Generator/Commands/VideoCommand.swift index b88f254..aade462 100644 --- a/CHDataManagement/Generator/Page Content/VideoCommandProcessor.swift +++ b/CHDataManagement/Generator/Commands/VideoCommand.swift @@ -1,7 +1,7 @@ -struct VideoCommandProcessor: CommandProcessor { +struct VideoCommand: CommandProcessor { - let commandType: ShorthandMarkdownKey = .video + static let commandType: CommandType = .video let content: Content @@ -17,7 +17,7 @@ struct VideoCommandProcessor: CommandProcessor { */ func process(_ arguments: [String], markdown: Substring) -> String { guard arguments.count >= 1 else { - results.invalid(command: .video, markdown) + invalid(markdown) return "" } let fileId = arguments[0].trimmed @@ -28,11 +28,10 @@ struct VideoCommandProcessor: CommandProcessor { results.missing(file: fileId, source: "Video command") return "" } - #warning("Create/specify video alternatives") results.require(file: file) guard let videoType = file.type.htmlType else { - results.invalid(command: .video, markdown) + invalid(markdown) return "" } @@ -43,12 +42,12 @@ struct VideoCommandProcessor: CommandProcessor { .content } - private func convertVideoOption(_ videoOption: String, markdown: Substring) -> VideoOption? { + private func convertVideoOption(_ videoOption: String, markdown: Substring) -> Option? { guard let optionText = videoOption.trimmed.nonEmpty else { return nil } - guard let option = VideoOption(rawValue: optionText) else { - results.invalid(command: .video, markdown) + guard let option = Option(rawValue: optionText) else { + invalid(markdown) return nil } switch option { diff --git a/CHDataManagement/Generator/Blocks/CodeBlockProcessor.swift b/CHDataManagement/Generator/Markdown/MarkdownCodeProcessor.swift similarity index 71% rename from CHDataManagement/Generator/Blocks/CodeBlockProcessor.swift rename to CHDataManagement/Generator/Markdown/MarkdownCodeProcessor.swift index 018b94f..a032ceb 100644 --- a/CHDataManagement/Generator/Blocks/CodeBlockProcessor.swift +++ b/CHDataManagement/Generator/Markdown/MarkdownCodeProcessor.swift @@ -1,9 +1,10 @@ +import Ink -struct CodeBlockProcessor { +struct MarkdownCodeProcessor: MarkdownProcessor { - private let codeHighlightFooter = "" + static let modifier: Modifier.Target = .codeBlocks - let results: PageGenerationResults + private let results: PageGenerationResults private let blocks: [ContentBlock : BlockProcessor] @@ -18,16 +19,14 @@ struct CodeBlockProcessor { } } - func process(_ html: String, markdown: Substring) -> String { + private let codeHighlightFooter = "" + + func process(html: String, markdown: Substring) -> String { let input = String(markdown) let rawBlockId = input.dropAfterFirst("\n").dropBeforeFirst("```").trimmed guard let blockId = ContentBlock(rawValue: rawBlockId) else { return other.process(html: html) } - guard let processor = self.blocks[blockId] else { - results.invalid(block: blockId, markdown) - return "" - } - return processor.process(markdown) + return blocks[blockId]!.process(markdown) } } diff --git a/CHDataManagement/Generator/Markdown/MarkdownHeadlineProcessor.swift b/CHDataManagement/Generator/Markdown/MarkdownHeadlineProcessor.swift new file mode 100644 index 0000000..b66ed60 --- /dev/null +++ b/CHDataManagement/Generator/Markdown/MarkdownHeadlineProcessor.swift @@ -0,0 +1,38 @@ +import Ink + +struct MarkdownHeadlineProcessor: MarkdownProcessor { + + static let modifier: Modifier.Target = .headings + + let content: Content + + let results: PageGenerationResults + + let language: ContentLanguage + + init(content: Content, results: PageGenerationResults, language: ContentLanguage) { + self.content = content + self.results = results + self.language = language + } + + /** + Modify headlines by extracting an id from the headline and adding it into the html element + + Format: ###<id> + + The id is created by lowercasing the string, removing all special characters, and replacing spaces with scores + */ + func process(html: String, markdown: Substring) -> String { + let id = markdown + .last(after: "#") + .trimmed + .filter { $0.isNumber || $0.isLetter || $0 == " " } + .lowercased() + .components(separatedBy: " ") + .filter { $0 != "" } + .joined(separator: "-") + let parts = html.components(separatedBy: ">") + return parts[0] + " id=\"\(id)\">" + parts.dropFirst().joined(separator: ">") + } +} diff --git a/CHDataManagement/Generator/Markdown/MarkdownImageProcessor.swift b/CHDataManagement/Generator/Markdown/MarkdownImageProcessor.swift new file mode 100644 index 0000000..0298f86 --- /dev/null +++ b/CHDataManagement/Generator/Markdown/MarkdownImageProcessor.swift @@ -0,0 +1,45 @@ +import Ink + +struct MarkdownImageProcessor: MarkdownProcessor { + + static var modifier: Modifier.Target = .images + + private let content: Content + + private let results: PageGenerationResults + + private let language: ContentLanguage + + private let commands: [CommandType : CommandProcessor] + + init(content: Content, results: PageGenerationResults, language: ContentLanguage) { + self.content = content + self.results = results + self.language = language + + self.commands = CommandType.allCases.reduce(into: [:]) { commands, command in + commands[command] = command.processor.init(content: content, results: results, language: language) + } + } + + var html: HtmlCommand { + commands[.includedHtml] as! HtmlCommand + } + + func process(html: String, markdown: Substring) -> String { + let argumentList = markdown.between(first: "](", andLast: ")").percentDecoded() + let arguments = argumentList.components(separatedBy: ";") + + let rawCommand = markdown.between("![", and: "]").trimmed.percentDecoded() + guard rawCommand != "" else { + return commands[.image]!.process(arguments, markdown: markdown) + } + + guard let command = CommandType(rawValue: rawCommand) else { + // Treat unknown commands as normal links + results.invalid(command: nil, markdown) + return html + } + return commands[command]!.process(arguments, markdown: markdown) + } +} diff --git a/CHDataManagement/Generator/Page Content/InlineLinkProcessor.swift b/CHDataManagement/Generator/Markdown/MarkdownLinkProcessor.swift similarity index 86% rename from CHDataManagement/Generator/Page Content/InlineLinkProcessor.swift rename to CHDataManagement/Generator/Markdown/MarkdownLinkProcessor.swift index b980280..45e08f1 100644 --- a/CHDataManagement/Generator/Page Content/InlineLinkProcessor.swift +++ b/CHDataManagement/Generator/Markdown/MarkdownLinkProcessor.swift @@ -1,5 +1,20 @@ +import Ink -struct InlineLinkProcessor { +struct MarkdownLinkProcessor: MarkdownProcessor { + + static let modifier: Modifier.Target = .links + + private let content: Content + + private let results: PageGenerationResults + + private let language: ContentLanguage + + init(content: Content, results: PageGenerationResults, language: ContentLanguage) { + self.content = content + self.results = results + self.language = language + } private let pageLinkMarker = "page:" @@ -7,12 +22,6 @@ struct InlineLinkProcessor { private let fileLinkMarker = "file:" - let content: Content - - let results: PageGenerationResults - - let language: ContentLanguage - func process(html: String, markdown: Substring) -> String { let url = markdown.between("(", and: ")") if url.hasPrefix(pageLinkMarker) { diff --git a/CHDataManagement/Generator/Markdown/MarkdownProcessor.swift b/CHDataManagement/Generator/Markdown/MarkdownProcessor.swift new file mode 100644 index 0000000..797db93 --- /dev/null +++ b/CHDataManagement/Generator/Markdown/MarkdownProcessor.swift @@ -0,0 +1,10 @@ +import Ink + +protocol MarkdownProcessor { + + static var modifier: Modifier.Target { get } + + init(content: Content, results: PageGenerationResults, language: ContentLanguage) + + func process(html: String, markdown: Substring) -> String +} diff --git a/CHDataManagement/Generator/Page Content/CommandProcessor.swift b/CHDataManagement/Generator/Page Content/CommandProcessor.swift deleted file mode 100644 index 953ae9a..0000000 --- a/CHDataManagement/Generator/Page Content/CommandProcessor.swift +++ /dev/null @@ -1,9 +0,0 @@ - -protocol CommandProcessor { - - var commandType: ShorthandMarkdownKey { get } - - init(content: Content, results: PageGenerationResults, language: ContentLanguage) - - func process(_ arguments: [String], markdown: Substring) -> String -} diff --git a/CHDataManagement/Generator/PageContentGenerator.swift b/CHDataManagement/Generator/PageContentGenerator.swift index b297bbe..c5344fe 100644 --- a/CHDataManagement/Generator/PageContentGenerator.swift +++ b/CHDataManagement/Generator/PageContentGenerator.swift @@ -9,257 +9,35 @@ final class PageContentParser { private let results: PageGenerationResults - // MARK: Command handlers + // MARK: Markdown handlers - private let buttonHandler: ButtonCommandProcessor + private let code: MarkdownCodeProcessor - private let labelHandler: LabelsCommandProcessor + private let headlines: MarkdownHeadlineProcessor - private let audioPlayer: AudioPlayerCommandProcessor + private let image: MarkdownImageProcessor - private let icons: IconCommandProcessor - - private let box: BoxCommandProcessor - - private let html: PageHtmlProcessor - - private let video: VideoCommandProcessor - - private let imageCompare: ImageCompareCommandProcessor - - private let images: ImageCommandProcessor - - // MARK: Other handlers - - private let inlineLink: InlineLinkProcessor - - private let code: CodeBlockProcessor + private let link: MarkdownLinkProcessor init(content: Content, language: ContentLanguage, results: PageGenerationResults) { self.content = content self.results = results self.language = language - self.buttonHandler = .init(content: content, results: results, language: language) - self.labelHandler = .init(content: content, results: results, language: language) - self.audioPlayer = .init(content: content, results: results, language: language) - self.icons = .init(content: content, results: results, language: language) - self.box = .init(content: content, results: results, language: language) - self.html = .init(content: content, results: results, language: language) - self.video = .init(content: content, results: results, language: language) - self.imageCompare = .init(content: content, results: results, language: language) - self.images = .init(content: content, results: results, language: language) - self.inlineLink = .init(content: content, results: results, language: language) self.code = .init(content: content, results: results, language: language) + self.headlines = .init(content: content, results: results, language: language) + self.image = .init(content: content, results: results, language: language) + self.link = .init(content: content, results: results, language: language) } func generatePage(from content: String) -> String { let parser = MarkdownParser(modifiers: [ - Modifier(target: .images, closure: processMarkdownImage), + Modifier(target: .images, closure: image.process), Modifier(target: .codeBlocks, closure: code.process), - Modifier(target: .links, closure: inlineLink.process), - Modifier(target: .html, closure: html.process), - Modifier(target: .headings, closure: handleHeadlines) + Modifier(target: .links, closure: link.process), + Modifier(target: .html, closure: image.html.process), + Modifier(target: .headings, closure: headlines.process) ]) return parser.html(from: content) } - - /** - Modify headlines by extracting an id from the headline and adding it into the html element - - Format: ##<title>#<id> - - The id is created by lowercasing the string, removing all special characters, and replacing spaces with scores - */ - private func handleHeadlines(html: String, markdown: Substring) -> String { - let id = markdown - .last(after: "#") - .trimmed - .filter { $0.isNumber || $0.isLetter || $0 == " " } - .lowercased() - .components(separatedBy: " ") - .filter { $0 != "" } - .joined(separator: "-") - let parts = html.components(separatedBy: ">") - return parts[0] + " id=\"\(id)\">" + parts.dropFirst().joined(separator: ">") - } - - private func processMarkdownImage(html: String, markdown: Substring) -> String { - // - let argumentList = markdown.between(first: "](", andLast: ")").percentDecoded() - let arguments = argumentList.components(separatedBy: ";") - - let rawCommand = markdown.between("![", and: "]").trimmed.percentDecoded() - guard rawCommand != "" else { - return images.process(arguments, markdown: markdown) - } - - guard let command = ShorthandMarkdownKey(rawValue: rawCommand) else { - // Treat unknown commands as normal links - results.invalid(command: nil, markdown) - return html - } - - switch command { - case .image: - return images.process(arguments, markdown: markdown) - case .labels: - return labelHandler.process(arguments, markdown: markdown) - case .buttons: - return buttonHandler.process(arguments, markdown: markdown) - case .video: - return video.process(arguments, markdown: markdown) - case .pageLink: - return handlePageLink(arguments, markdown: markdown) - case .includedHtml: - return self.html.process(arguments, markdown: markdown) - case .box: - return box.process(arguments, markdown: markdown) - case .model: - return handleModel(arguments, markdown: markdown) - case .svg: - return handleSvg(arguments, markdown: markdown) - case .audioPlayer: - return audioPlayer.process(arguments, markdown: markdown) - case .tagLink: - return handleTagLink(arguments, markdown: markdown) - case .icons: - return icons.process(arguments, markdown: markdown) - case .imageCompare: - return imageCompare.process(arguments, markdown: markdown) - } - } - - /** - Format: `![page](<pageId>)` - */ - private func handlePageLink(_ arguments: [String], markdown: Substring) -> String { - guard arguments.count == 1 else { - results.invalid(command: .pageLink, markdown) - return "" - } - let pageId = arguments[0] - - guard let page = content.page(pageId) else { - results.missing(page: pageId, source: "Page link command") - return "" - } - guard !page.isDraft else { - // Prevent linking to unpublished content - return "" - } - - results.linked(to: page) - - let localized = page.localized(in: language) - let url = page.absoluteUrl(in: language) - let title = localized.linkPreviewTitle ?? localized.title - let description = localized.linkPreviewDescription ?? "" - let image = makePageImage(item: localized) - - return RelatedPageLink( - title: title, - description: description, - url: url, - image: image) - .content - } - - /** - Format: `![tag](<tagId>)` - */ - private func handleTagLink(_ arguments: [String], markdown: Substring) -> String { - guard arguments.count == 1 else { - results.invalid(command: .tagLink, markdown) - return "" - } - let tagId = arguments[0] - - guard let tag = content.tag(tagId) else { - results.missing(tag: tagId, source: "Tag link command") - return "" - } - - let localized = tag.localized(in: language) - let url = tag.absoluteUrl(in: language) - let title = localized.name - let description = localized.linkPreviewDescription ?? "" - let image = makePageImage(item: localized) - - return RelatedPageLink( - title: title, - description: description, - url: url, - image: image) - .content - } - - private func makePageImage(item: LinkPreviewItem) -> ImageSet? { - item.linkPreviewImage.map { image in - let size = content.settings.pages.pageLinkImageSize - let imageSet = image.imageSet(width: size, height: size, language: language) - results.require(imageSet: imageSet) - return imageSet - } - } - - /** - Format: `![model](<file>)` - */ - private func handleModel(_ arguments: [String], markdown: Substring) -> String { - guard arguments.count == 1 else { - results.invalid(command: .model, markdown) - return "" - } - let fileId = arguments[0] - guard fileId.hasSuffix(".glb") else { - results.invalid(command: .model, markdown) - return "" - } - - guard let file = content.file(fileId) else { - results.missing(file: fileId, source: "Model command") - return "" - } - results.require(file: file) - results.require(header: .modelViewer) - - let description = file.localized(in: language) - return ModelViewer(file: file.absoluteUrl, description: description).content - } - - private func handleSvg(_ arguments: [String], markdown: Substring) -> String { - guard arguments.count == 5 else { - results.invalid(command: .svg, markdown) - return "" - } - - guard let x = Int(arguments[1]), - let y = Int(arguments[2]), - let partWidth = Int(arguments[3]), - let partHeight = Int(arguments[4]) else { - results.invalid(command: .svg, markdown) - return "" - } - - let imageId = arguments[0] - - guard let image = content.image(imageId) else { - results.missing(file: imageId, source: "SVG command") - return "" - } - guard image.type == .svg else { - results.invalid(command: .svg, markdown) - return "" - } - - return PartialSvgImage( - imagePath: image.absoluteUrl, - altText: image.localized(in: language), - x: x, - y: y, - width: partWidth, - height: partHeight) - .content - } } diff --git a/CHDataManagement/Generator/GenerationAnomaly.swift b/CHDataManagement/Generator/Results/GenerationAnomaly.swift similarity index 96% rename from CHDataManagement/Generator/GenerationAnomaly.swift rename to CHDataManagement/Generator/Results/GenerationAnomaly.swift index 125a9ae..633024b 100644 --- a/CHDataManagement/Generator/GenerationAnomaly.swift +++ b/CHDataManagement/Generator/Results/GenerationAnomaly.swift @@ -5,7 +5,7 @@ enum GenerationAnomaly { case missingFile(file: String, markdown: String) case missingPage(page: String, markdown: String) case missingTag(tag: String, markdown: String) - case invalidCommand(command: ShorthandMarkdownKey?, markdown: String) + case invalidCommand(command: CommandType?, markdown: String) case warning(String) } diff --git a/CHDataManagement/Generator/GenerationResults.swift b/CHDataManagement/Generator/Results/GenerationResults.swift similarity index 100% rename from CHDataManagement/Generator/GenerationResults.swift rename to CHDataManagement/Generator/Results/GenerationResults.swift diff --git a/CHDataManagement/Generator/PageGenerationResults.swift b/CHDataManagement/Generator/Results/PageGenerationResults.swift similarity index 97% rename from CHDataManagement/Generator/PageGenerationResults.swift rename to CHDataManagement/Generator/Results/PageGenerationResults.swift index 2d68bae..e920857 100644 --- a/CHDataManagement/Generator/PageGenerationResults.swift +++ b/CHDataManagement/Generator/Results/PageGenerationResults.swift @@ -66,7 +66,7 @@ final class PageGenerationResults: ObservableObject { /// The image versions required for this page private(set) var imagesToGenerate: Set<ImageVersion> - private(set) var invalidCommands: [(command: ShorthandMarkdownKey?, markdown: String)] + private(set) var invalidCommands: [(command: CommandType?, markdown: String)] private(set) var invalidBlocks: [(block: ContentBlock?, markdown: String)] @@ -132,7 +132,7 @@ final class PageGenerationResults: ObservableObject { delegate.inaccessibleContent(file: file) } - func invalid(command: ShorthandMarkdownKey?, _ markdown: Substring) { + func invalid(command: CommandType?, _ markdown: Substring) { let markdown = String(markdown) invalidCommands.append((command, markdown)) delegate.invalidCommand(markdown) diff --git a/CHDataManagement/Generator/VideoOption.swift b/CHDataManagement/Generator/VideoOption.swift deleted file mode 100644 index 9e329a8..0000000 --- a/CHDataManagement/Generator/VideoOption.swift +++ /dev/null @@ -1,123 +0,0 @@ - -/// HTML video options -enum VideoOption { - - /// Specifies that video controls should be displayed (such as a play/pause button etc). - case controls - - /// Specifies that the video will start playing as soon as it is ready - case autoplay - - /// Specifies that the video will start over again, every time it is finished - case loop - - /// Specifies that the audio output of the video should be muted - case muted - - /// Mobile browsers will play the video right where it is instead of the default, which is to open it up fullscreen while it plays - case playsinline - - /// Sets the height of the video player - case height(Int) - - /// Sets the width of the video player - case width(Int) - - /// Specifies if and how the author thinks the video should be loaded when the page loads - case preload(VideoPreloadOption) - - /// Specifies an image to be shown while the video is downloading, or until the user hits the play button - case poster(image: String) - - /// Specifies the URL of the video file - case src(String) - - init?(rawValue: String) { - switch rawValue { - case "controls": - self = .controls - return - case "autoplay": - self = .autoplay - return - case "muted": - self = .muted - return - case "loop": - self = .loop - return - case "playsinline": - self = .playsinline - return - default: break - } - - let parts = rawValue.components(separatedBy: "=") - guard parts.count == 2 else { - return nil - } - - let optionName = parts[0] - let value = parts[1].removingSurroundingQuotes - - switch optionName { - case "height": - guard let height = Int(value) else { - return nil - } - self = .height(height) - case "width": - guard let width = Int(value) else { - return nil - } - self = .width(width) - case "preload": - guard let preloadOption = VideoPreloadOption(rawValue: value) else { - return nil - } - self = .preload(preloadOption) - case "poster": - self = .poster(image: value) - case "src": - self = .src(value) - default: - return nil - } - return - } - - var rawValue: String { - switch self { - case .controls: return "controls" - case .autoplay: return "autoplay" - case .muted: return "muted" - case .loop: return "loop" - case .playsinline: return "playsinline" - case .height(let height): return "height='\(height)'" - case .width(let width): return "width='\(width)'" - case .preload(let option): return "preload='\(option)'" - case .poster(let image): return "poster='\(image)'" - case .src(let url): return "src='\(url)'" - } - } -} - -/** - The `preload` attribute specifies if and how the author thinks that the video should be loaded when the page loads. - - The `preload` attribute allows the author to provide a hint to the browser about what he/she thinks will lead to the best user experience. - This attribute may be ignored in some instances. - - Note: The `preload` attribute is ignored if `autoplay` is present. - */ -enum VideoPreloadOption: String { - - /// The author thinks that the browser should load the entire video when the page loads - case auto - - /// The author thinks that the browser should load only metadata when the page loads - case metadata - - /// The author thinks that the browser should NOT load the video when the page loads - case none -} diff --git a/CHDataManagement/Main/MainView.swift b/CHDataManagement/Main/MainView.swift index a78ffbd..e58346c 100644 --- a/CHDataManagement/Main/MainView.swift +++ b/CHDataManagement/Main/MainView.swift @@ -3,8 +3,6 @@ import SFSafeSymbols /** **Content** - - Podcast: Fix audio player, preview image - - Article Cap Mosaic: -> GIF feature - iPhone Backgrounds: Add page, html **UI** @@ -20,7 +18,7 @@ import SFSafeSymbols - Posts: Generate separate pages for posts to link to - Settings: Introduce `Authors` (`name`, `image`, `description`) - Page: Property `author` - - Video: Specify versions + - Video: Specify versions -> Block **Generation** - ImageSet: Specify image aspect ratio (width, height) to prevent page jumps diff --git a/CHDataManagement/Page Elements/ContentElements/ContentPageVideo.swift b/CHDataManagement/Page Elements/ContentElements/ContentPageVideo.swift index 3276f6f..28a2b23 100644 --- a/CHDataManagement/Page Elements/ContentElements/ContentPageVideo.swift +++ b/CHDataManagement/Page Elements/ContentElements/ContentPageVideo.swift @@ -5,7 +5,7 @@ struct ContentPageVideo: HtmlProducer { let videoType: String - let options: [VideoOption] + let options: [VideoCommand.Option] func populate(_ result: inout String) { result += "<video\(optionString)>Video not supported."