Full generation, file type cleanup

This commit is contained in:
Christoph Hagen 2024-12-25 18:06:05 +01:00
parent 41887a1401
commit 1e4682dad1
56 changed files with 1577 additions and 1103 deletions

View File

@ -9,15 +9,12 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850082CEE01BF0090B18B /* PagePickerView.swift */; }; E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850082CEE01BF0090B18B /* PagePickerView.swift */; };
E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218500A2CEE02FA0090B18B /* Content+Mock.swift */; }; E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218500A2CEE02FA0090B18B /* Content+Mock.swift */; };
E21850152CEE55D40090B18B /* FileOnDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850142CEE55D40090B18B /* FileOnDisk.swift */; };
E21850172CEE55FC0090B18B /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850162CEE55FB0090B18B /* FileType.swift */; }; E21850172CEE55FC0090B18B /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850162CEE55FB0090B18B /* FileType.swift */; };
E21850192CEE561C0090B18B /* PageOnDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850182CEE561B0090B18B /* PageOnDisk.swift */; };
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218501C2CEE6CB30090B18B /* VerticalCenter.swift */; }; E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218501C2CEE6CB30090B18B /* VerticalCenter.swift */; };
E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850222CF10C840090B18B /* TagSelectionView.swift */; }; E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850222CF10C840090B18B /* TagSelectionView.swift */; };
E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850262CF3B42D0090B18B /* PostDetailView.swift */; }; E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850262CF3B42D0090B18B /* PostDetailView.swift */; };
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502A2CF790AC0090B18B /* PostContentView.swift */; }; E218502B2CF790B30090B18B /* PostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502A2CF790AC0090B18B /* PostContentView.swift */; };
E218502D2CF791440090B18B /* PostImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502C2CF791440090B18B /* PostImagesView.swift */; }; E218502D2CF791440090B18B /* PostImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502C2CF791440090B18B /* PostImagesView.swift */; };
E218502F2CFAF69C0090B18B /* LocalizedWebsiteGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502E2CFAF6990090B18B /* LocalizedWebsiteGenerator.swift */; };
E21850332CFAFA2F0090B18B /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850322CFAFA200090B18B /* Settings.swift */; }; E21850332CFAFA2F0090B18B /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850322CFAFA200090B18B /* Settings.swift */; };
E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850342CFAFA570090B18B /* SettingsFile.swift */; }; E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850342CFAFA570090B18B /* SettingsFile.swift */; };
E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */; }; E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */; };
@ -65,7 +62,6 @@
E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */; }; E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */; };
E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */; }; E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */; };
E25DA5272CFF745700AEF16D /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5262CFF745200AEF16D /* URL+Extensions.swift */; }; E25DA5272CFF745700AEF16D /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5262CFF745200AEF16D /* URL+Extensions.swift */; };
E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5282CFFBFB800AEF16D /* ImageFileType.swift */; };
E25DA52C2CFFC3EC00AEF16D /* SDWebImageAVIFCoder in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA52B2CFFC3EC00AEF16D /* SDWebImageAVIFCoder */; }; E25DA52C2CFFC3EC00AEF16D /* SDWebImageAVIFCoder in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA52B2CFFC3EC00AEF16D /* SDWebImageAVIFCoder */; };
E25DA52F2CFFC91B00AEF16D /* SDWebImageWebPCoder in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA52E2CFFC91B00AEF16D /* SDWebImageWebPCoder */; }; E25DA52F2CFFC91B00AEF16D /* SDWebImageWebPCoder in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA52E2CFFC91B00AEF16D /* SDWebImageWebPCoder */; };
E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */; }; E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */; };
@ -81,7 +77,6 @@
E25DA57A2D01C64400AEF16D /* PageContentGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */; }; E25DA57A2D01C64400AEF16D /* PageContentGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */; };
E25DA57D2D01C67900AEF16D /* Ink in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA57C2D01C67900AEF16D /* Ink */; }; E25DA57D2D01C67900AEF16D /* Ink in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA57C2D01C67900AEF16D /* Ink */; };
E25DA5802D01C6AC00AEF16D /* Splash in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA57F2D01C6AC00AEF16D /* Splash */; }; 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 */; }; E25DA5852D01C92700AEF16D /* ShorthandMarkdownKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */; };
E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5882D01CBCE00AEF16D /* Content+Generation.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 */; }; E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA58A2D020C9200AEF16D /* PageImage.swift */; };
@ -119,10 +114,6 @@
E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31522D0618700051B7F4 /* AddPageView.swift */; }; E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31522D0618700051B7F4 /* AddPageView.swift */; };
E29D31552D06D2CE0051B7F4 /* TagListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31542D06D2CB0051B7F4 /* TagListView.swift */; }; E29D31552D06D2CE0051B7F4 /* TagListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31542D06D2CB0051B7F4 /* TagListView.swift */; };
E29D31572D06D38B0051B7F4 /* AddTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31562D06D3880051B7F4 /* AddTagView.swift */; }; E29D31572D06D38B0051B7F4 /* AddTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31562D06D3880051B7F4 /* AddTagView.swift */; };
E29D315B2D06D63E0051B7F4 /* ModelFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D315A2D06D63C0051B7F4 /* ModelFileType.swift */; };
E29D315D2D06D6EA0051B7F4 /* TextFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D315C2D06D6EA0051B7F4 /* TextFileType.swift */; };
E29D315F2D06D6F30051B7F4 /* CodeFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D315E2D06D6F30051B7F4 /* CodeFileType.swift */; };
E29D31612D06D95C0051B7F4 /* ResourceFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31602D06D9570051B7F4 /* ResourceFileType.swift */; };
E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31622D06E95A0051B7F4 /* NavigationIcon.swift */; }; E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31622D06E95A0051B7F4 /* NavigationIcon.swift */; };
E29D31692D0702700051B7F4 /* TextFileContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31682D0702670051B7F4 /* TextFileContentView.swift */; }; E29D31692D0702700051B7F4 /* TextFileContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31682D0702670051B7F4 /* TextFileContentView.swift */; };
E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */; }; E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */; };
@ -137,7 +128,7 @@
E29D31892D0AED1F0051B7F4 /* ModelViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */; }; E29D31892D0AED1F0051B7F4 /* ModelViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */; };
E29D318B2D0B07EE0051B7F4 /* ContentBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318A2D0B07E60051B7F4 /* ContentBox.swift */; }; E29D318B2D0B07EE0051B7F4 /* ContentBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318A2D0B07E60051B7F4 /* ContentBox.swift */; };
E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */; }; E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */; };
E29D31902D0B34870051B7F4 /* PageContentAnomaly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318F2D0B34870051B7F4 /* PageContentAnomaly.swift */; }; E29D31902D0B34870051B7F4 /* GenerationAnomaly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318F2D0B34870051B7F4 /* GenerationAnomaly.swift */; };
E29D31942D0B7D280051B7F4 /* SvgImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31932D0B7D250051B7F4 /* SvgImage.swift */; }; E29D31942D0B7D280051B7F4 /* SvgImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31932D0B7D250051B7F4 /* SvgImage.swift */; };
E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31952D0C18690051B7F4 /* PathSettings.swift */; }; E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31952D0C18690051B7F4 /* PathSettings.swift */; };
E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */; }; E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */; };
@ -198,20 +189,21 @@
E2DD047E2C276F32003BFF1F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2DD047D2C276F32003BFF1F /* Preview Assets.xcassets */; }; E2DD047E2C276F32003BFF1F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2DD047D2C276F32003BFF1F /* Preview Assets.xcassets */; };
E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E06DFA2CA4A6570019C2AF /* Content.swift */; }; E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E06DFA2CA4A6570019C2AF /* Content.swift */; };
E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E06DFF2CA4A8EB0019C2AF /* Page+Mock.swift */; }; E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E06DFF2CA4A8EB0019C2AF /* Page+Mock.swift */; };
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 */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
E21850082CEE01BF0090B18B /* PagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePickerView.swift; sourceTree = "<group>"; }; E21850082CEE01BF0090B18B /* PagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePickerView.swift; sourceTree = "<group>"; };
E218500A2CEE02FA0090B18B /* Content+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Mock.swift"; sourceTree = "<group>"; }; E218500A2CEE02FA0090B18B /* Content+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Mock.swift"; sourceTree = "<group>"; };
E21850142CEE55D40090B18B /* FileOnDisk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOnDisk.swift; sourceTree = "<group>"; };
E21850162CEE55FB0090B18B /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = "<group>"; }; E21850162CEE55FB0090B18B /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = "<group>"; };
E21850182CEE561B0090B18B /* PageOnDisk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageOnDisk.swift; sourceTree = "<group>"; };
E218501C2CEE6CB30090B18B /* VerticalCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCenter.swift; sourceTree = "<group>"; }; E218501C2CEE6CB30090B18B /* VerticalCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCenter.swift; sourceTree = "<group>"; };
E21850222CF10C840090B18B /* TagSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSelectionView.swift; sourceTree = "<group>"; }; E21850222CF10C840090B18B /* TagSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSelectionView.swift; sourceTree = "<group>"; };
E21850262CF3B42D0090B18B /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = "<group>"; }; E21850262CF3B42D0090B18B /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = "<group>"; };
E218502A2CF790AC0090B18B /* PostContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostContentView.swift; sourceTree = "<group>"; }; E218502A2CF790AC0090B18B /* PostContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostContentView.swift; sourceTree = "<group>"; };
E218502C2CF791440090B18B /* PostImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImagesView.swift; sourceTree = "<group>"; }; E218502C2CF791440090B18B /* PostImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImagesView.swift; sourceTree = "<group>"; };
E218502E2CFAF6990090B18B /* LocalizedWebsiteGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedWebsiteGenerator.swift; sourceTree = "<group>"; };
E21850322CFAFA200090B18B /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; }; E21850322CFAFA200090B18B /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
E21850342CFAFA570090B18B /* SettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFile.swift; sourceTree = "<group>"; }; E21850342CFAFA570090B18B /* SettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFile.swift; sourceTree = "<group>"; };
E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettings.swift; sourceTree = "<group>"; }; E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettings.swift; sourceTree = "<group>"; };
@ -259,7 +251,6 @@
E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGenerator.swift; sourceTree = "<group>"; }; E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGenerator.swift; sourceTree = "<group>"; };
E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSSize+Scaling.swift"; sourceTree = "<group>"; }; E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSSize+Scaling.swift"; sourceTree = "<group>"; };
E25DA5262CFF745200AEF16D /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = "<group>"; }; E25DA5262CFF745200AEF16D /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = "<group>"; };
E25DA5282CFFBFB800AEF16D /* ImageFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFileType.swift; sourceTree = "<group>"; };
E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettingsFile.swift; sourceTree = "<group>"; }; E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettingsFile.swift; sourceTree = "<group>"; };
E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettingsFile.swift; sourceTree = "<group>"; }; E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettingsFile.swift; sourceTree = "<group>"; };
E25DA5402D00446700AEF16D /* PostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettings.swift; sourceTree = "<group>"; }; E25DA5402D00446700AEF16D /* PostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettings.swift; sourceTree = "<group>"; };
@ -271,7 +262,6 @@
E25DA5742D018B6100AEF16D /* FileDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDetailView.swift; sourceTree = "<group>"; }; E25DA5742D018B6100AEF16D /* FileDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDetailView.swift; sourceTree = "<group>"; };
E25DA5762D018B9500AEF16D /* File+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "File+Mock.swift"; sourceTree = "<group>"; }; E25DA5762D018B9500AEF16D /* File+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "File+Mock.swift"; sourceTree = "<group>"; };
E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageContentGenerator.swift; sourceTree = "<group>"; }; 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>"; }; E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShorthandMarkdownKey.swift; sourceTree = "<group>"; };
E25DA5882D01CBCE00AEF16D /* Content+Generation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Generation.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>"; }; E25DA58A2D020C9200AEF16D /* PageImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageImage.swift; sourceTree = "<group>"; };
@ -309,10 +299,6 @@
E29D31522D0618700051B7F4 /* AddPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPageView.swift; sourceTree = "<group>"; }; E29D31522D0618700051B7F4 /* AddPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPageView.swift; sourceTree = "<group>"; };
E29D31542D06D2CB0051B7F4 /* TagListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagListView.swift; sourceTree = "<group>"; }; E29D31542D06D2CB0051B7F4 /* TagListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagListView.swift; sourceTree = "<group>"; };
E29D31562D06D3880051B7F4 /* AddTagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTagView.swift; sourceTree = "<group>"; }; E29D31562D06D3880051B7F4 /* AddTagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTagView.swift; sourceTree = "<group>"; };
E29D315A2D06D63C0051B7F4 /* ModelFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelFileType.swift; sourceTree = "<group>"; };
E29D315C2D06D6EA0051B7F4 /* TextFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFileType.swift; sourceTree = "<group>"; };
E29D315E2D06D6F30051B7F4 /* CodeFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeFileType.swift; sourceTree = "<group>"; };
E29D31602D06D9570051B7F4 /* ResourceFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceFileType.swift; sourceTree = "<group>"; };
E29D31622D06E95A0051B7F4 /* NavigationIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationIcon.swift; sourceTree = "<group>"; }; E29D31622D06E95A0051B7F4 /* NavigationIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationIcon.swift; sourceTree = "<group>"; };
E29D31682D0702670051B7F4 /* TextFileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFileContentView.swift; sourceTree = "<group>"; }; E29D31682D0702670051B7F4 /* TextFileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFileContentView.swift; sourceTree = "<group>"; };
E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListPageGenerator.swift; sourceTree = "<group>"; }; E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListPageGenerator.swift; sourceTree = "<group>"; };
@ -327,7 +313,7 @@
E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelViewer.swift; sourceTree = "<group>"; }; E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelViewer.swift; sourceTree = "<group>"; };
E29D318A2D0B07E60051B7F4 /* ContentBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBox.swift; sourceTree = "<group>"; }; E29D318A2D0B07E60051B7F4 /* ContentBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBox.swift; sourceTree = "<group>"; };
E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsContentView.swift; sourceTree = "<group>"; }; E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsContentView.swift; sourceTree = "<group>"; };
E29D318F2D0B34870051B7F4 /* PageContentAnomaly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageContentAnomaly.swift; sourceTree = "<group>"; }; E29D318F2D0B34870051B7F4 /* GenerationAnomaly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationAnomaly.swift; sourceTree = "<group>"; };
E29D31932D0B7D250051B7F4 /* SvgImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SvgImage.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSettingsFile.swift; sourceTree = "<group>"; };
@ -387,6 +373,10 @@
E2DD047D2C276F32003BFF1F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; E2DD047D2C276F32003BFF1F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
E2E06DFA2CA4A6570019C2AF /* Content.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Content.swift; sourceTree = "<group>"; }; E2E06DFA2CA4A6570019C2AF /* Content.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Content.swift; sourceTree = "<group>"; };
E2E06DFF2CA4A8EB0019C2AF /* Page+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Page+Mock.swift"; sourceTree = "<group>"; }; E2E06DFF2CA4A8EB0019C2AF /* Page+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Page+Mock.swift"; sourceTree = "<group>"; };
E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResults.swift; sourceTree = "<group>"; };
E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertThrowing.swift; sourceTree = "<group>"; };
E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFileSelectionView.swift; sourceTree = "<group>"; };
E2FE0EED2D1C22EF002963B7 /* InlineLinkProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineLinkProcessor.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -424,9 +414,7 @@
children = ( children = (
E29D31292D039B050051B7F4 /* FileDescriptions.swift */, E29D31292D039B050051B7F4 /* FileDescriptions.swift */,
E25DA5322D0041C400AEF16D /* Settings */, E25DA5322D0041C400AEF16D /* Settings */,
E21850142CEE55D40090B18B /* FileOnDisk.swift */,
E2A37D102CE537670000979F /* PageFile.swift */, E2A37D102CE537670000979F /* PageFile.swift */,
E21850182CEE561B0090B18B /* PageOnDisk.swift */,
E2A37D142CE68BEA0000979F /* PostFile.swift */, E2A37D142CE68BEA0000979F /* PostFile.swift */,
E2A37D162CE73F170000979F /* TagFile.swift */, E2A37D162CE73F170000979F /* TagFile.swift */,
E22990212D0ED129009F8D77 /* TagOverviewFile.swift */, E22990212D0ED129009F8D77 /* TagOverviewFile.swift */,
@ -461,38 +449,24 @@
E25DA5782D01C56200AEF16D /* Generator */ = { E25DA5782D01C56200AEF16D /* Generator */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E22990232D0EDBD0009F8D77 /* HeaderElement.swift */,
E29D31B62D0DAC030051B7F4 /* Page Content */, E29D31B62D0DAC030051B7F4 /* Page Content */,
E29D318F2D0B34870051B7F4 /* PageContentAnomaly.swift */, E22990232D0EDBD0009F8D77 /* HeaderElement.swift */,
E29D31842D0AE8EE0051B7F4 /* KnownHeaderElement.swift */, E29D31842D0AE8EE0051B7F4 /* KnownHeaderElement.swift */,
E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */, E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */,
E22990412D107A94009F8D77 /* ImageJob.swift */, E22990412D107A94009F8D77 /* ImageJob.swift */,
E218502E2CFAF6990090B18B /* LocalizedWebsiteGenerator.swift */,
E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */, E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */,
E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */, E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */,
E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */,
E25DA5982D02401A00AEF16D /* PageGenerator.swift */, E25DA5982D02401A00AEF16D /* PageGenerator.swift */,
E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */, E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */,
E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */, E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */,
E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */, E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */,
E29D31252D0370A50051B7F4 /* VideoOption.swift */, E29D31252D0370A50051B7F4 /* VideoOption.swift */,
E29D318F2D0B34870051B7F4 /* GenerationAnomaly.swift */,
); );
path = Generator; path = Generator;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E25DA5812D01C79800AEF16D /* Types */ = {
isa = PBXGroup;
children = (
E29D31602D06D9570051B7F4 /* ResourceFileType.swift */,
E29D315E2D06D6F30051B7F4 /* CodeFileType.swift */,
E29D315C2D06D6EA0051B7F4 /* TextFileType.swift */,
E29D315A2D06D63C0051B7F4 /* ModelFileType.swift */,
E21850162CEE55FB0090B18B /* FileType.swift */,
E25DA5282CFFBFB800AEF16D /* ImageFileType.swift */,
E25DA5822D01C7A100AEF16D /* VideoFileType.swift */,
);
path = Types;
sourceTree = "<group>";
};
E29D311E2D0320D90051B7F4 /* ContentElements */ = { E29D311E2D0320D90051B7F4 /* ContentElements */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -527,6 +501,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E29D31992D0C451B0051B7F4 /* Pages */, E29D31992D0C451B0051B7F4 /* Pages */,
E25DA5702D01015400AEF16D /* GenerationContentView.swift */,
E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */, E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */,
); );
path = Content; path = Content;
@ -557,6 +532,7 @@
E29D31B62D0DAC030051B7F4 /* Page Content */ = { E29D31B62D0DAC030051B7F4 /* Page Content */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E2FE0EED2D1C22EF002963B7 /* InlineLinkProcessor.swift */,
E29D31BD2D0DB8560051B7F4 /* AudioPlayerCommand.swift */, E29D31BD2D0DB8560051B7F4 /* AudioPlayerCommand.swift */,
E29D31BB2D0DB5110051B7F4 /* CommandProcessor.swift */, E29D31BB2D0DB5110051B7F4 /* CommandProcessor.swift */,
E29D31B92D0DB4EF0051B7F4 /* LabelsCommand.swift */, E29D31B92D0DB4EF0051B7F4 /* LabelsCommand.swift */,
@ -593,7 +569,6 @@
children = ( children = (
E29D318C2D0B2E5E0051B7F4 /* Content */, E29D318C2D0B2E5E0051B7F4 /* Content */,
E29D316E2D0822720051B7F4 /* SettingsListView.swift */, E29D316E2D0822720051B7F4 /* SettingsListView.swift */,
E25DA5702D01015400AEF16D /* GenerationContentView.swift */,
E29D31702D08234D0051B7F4 /* GenerationDetailView.swift */, E29D31702D08234D0051B7F4 /* GenerationDetailView.swift */,
E25DA5942D023BCC00AEF16D /* PageSettingsDetailView.swift */, E25DA5942D023BCC00AEF16D /* PageSettingsDetailView.swift */,
E25DA5442D00952D00AEF16D /* SettingsSection.swift */, E25DA5442D00952D00AEF16D /* SettingsSection.swift */,
@ -645,6 +620,7 @@
E29D314A2D04FC940051B7F4 /* FileToAdd.swift */, E29D314A2D04FC940051B7F4 /* FileToAdd.swift */,
E29D314C2D04FCBF0051B7F4 /* FileToAddView.swift */, E29D314C2D04FCBF0051B7F4 /* FileToAddView.swift */,
E29D31A42D0CD03A0051B7F4 /* FileSelectionView.swift */, E29D31A42D0CD03A0051B7F4 /* FileSelectionView.swift */,
E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */,
); );
path = Files; path = Files;
sourceTree = "<group>"; sourceTree = "<group>";
@ -680,7 +656,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E229901A2D0E3F09009F8D77 /* Item */, E229901A2D0E3F09009F8D77 /* Item */,
E25DA5812D01C79800AEF16D /* Types */,
E25DA53B2D0042EA00AEF16D /* Settings */, E25DA53B2D0042EA00AEF16D /* Settings */,
E2E06DFA2CA4A6570019C2AF /* Content.swift */, E2E06DFA2CA4A6570019C2AF /* Content.swift */,
E29D31A02D0C75C50051B7F4 /* Content+Validation.swift */, E29D31A02D0C75C50051B7F4 /* Content+Validation.swift */,
@ -689,6 +664,7 @@
E25DA5142CFF00B900AEF16D /* Content+Load.swift */, E25DA5142CFF00B900AEF16D /* Content+Load.swift */,
E24252092C52C9260029FF16 /* ContentLanguage.swift */, E24252092C52C9260029FF16 /* ContentLanguage.swift */,
E25DA59A2D024A2900AEF16D /* DateItem.swift */, E25DA59A2D024A2900AEF16D /* DateItem.swift */,
E21850162CEE55FB0090B18B /* FileType.swift */,
E2A21C502CBBD53C0060935B /* FileResource.swift */, E2A21C502CBBD53C0060935B /* FileResource.swift */,
E2B85F3A2C428F0D0047CD0C /* Post.swift */, E2B85F3A2C428F0D0047CD0C /* Post.swift */,
E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */, E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */,
@ -760,6 +736,7 @@
E2B85F552C4BD0AD0047CD0C /* Extensions */ = { E2B85F552C4BD0AD0047CD0C /* Extensions */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */,
E25DA5182CFF035200AEF16D /* Array+Split.swift */, E25DA5182CFF035200AEF16D /* Array+Split.swift */,
E2B85F562C4BD0BB0047CD0C /* Binding+Extension.swift */, E2B85F562C4BD0BB0047CD0C /* Binding+Extension.swift */,
E2A21C0D2CB189D70060935B /* Color+RGB.swift */, E2A21C0D2CB189D70060935B /* Color+RGB.swift */,
@ -913,6 +890,7 @@
files = ( files = (
E29D31242D0366860051B7F4 /* TagList.swift in Sources */, E29D31242D0366860051B7F4 /* TagList.swift in Sources */,
E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */, E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */,
E2FE0EE62D15A0B5002963B7 /* GenerationResults.swift in Sources */,
E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */, E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */,
E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */, E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */,
E229904C2D10BE5D009F8D77 /* InitialSetupView.swift in Sources */, E229904C2D10BE5D009F8D77 /* InitialSetupView.swift in Sources */,
@ -926,7 +904,7 @@
E25DA5412D00446C00AEF16D /* PostSettings.swift in Sources */, E25DA5412D00446C00AEF16D /* PostSettings.swift in Sources */,
E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */, E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */,
E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */, E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */,
E29D31902D0B34870051B7F4 /* PageContentAnomaly.swift in Sources */, E29D31902D0B34870051B7F4 /* GenerationAnomaly.swift in Sources */,
E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */, E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */,
E229902E2D0F7280009F8D77 /* IdPropertyView.swift in Sources */, E229902E2D0F7280009F8D77 /* IdPropertyView.swift in Sources */,
E29D31AD2D0DA5360051B7F4 /* AudioPlayerIcons.swift in Sources */, E29D31AD2D0DA5360051B7F4 /* AudioPlayerIcons.swift in Sources */,
@ -935,7 +913,6 @@
E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */, E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */,
E21850172CEE55FC0090B18B /* FileType.swift in Sources */, E21850172CEE55FC0090B18B /* FileType.swift in Sources */,
E29D31B12D0DA5510051B7F4 /* ContentIcon.swift in Sources */, E29D31B12D0DA5510051B7F4 /* ContentIcon.swift in Sources */,
E29D31612D06D95C0051B7F4 /* ResourceFileType.swift in Sources */,
E2A37D112CE537800000979F /* PageFile.swift in Sources */, E2A37D112CE537800000979F /* PageFile.swift in Sources */,
E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */, E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */,
E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */, E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */,
@ -950,10 +927,8 @@
E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */, E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */,
E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */, E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */,
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */, E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */,
E218502F2CFAF69C0090B18B /* LocalizedWebsiteGenerator.swift in Sources */,
E29D31942D0B7D280051B7F4 /* SvgImage.swift in Sources */, E29D31942D0B7D280051B7F4 /* SvgImage.swift in Sources */,
E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */, E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */,
E25DA5832D01C7A400AEF16D /* VideoFileType.swift in Sources */,
E22990462D10B7A7009F8D77 /* SecurityScopeStatus.swift in Sources */, E22990462D10B7A7009F8D77 /* SecurityScopeStatus.swift in Sources */,
E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */, E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */,
E22990342D0F77E9009F8D77 /* PagePropertyView.swift in Sources */, E22990342D0F77E9009F8D77 /* PagePropertyView.swift in Sources */,
@ -971,7 +946,6 @@
E2A21C2A2CB2AA4F0060935B /* Post+Mock.swift in Sources */, E2A21C2A2CB2AA4F0060935B /* Post+Mock.swift in Sources */,
E29D312E2D03A0D70051B7F4 /* LocalizedPageDetailView.swift in Sources */, E29D312E2D03A0D70051B7F4 /* LocalizedPageDetailView.swift in Sources */,
E2581DED2C75202400F1F079 /* Tag.swift in Sources */, E2581DED2C75202400F1F079 /* Tag.swift in Sources */,
E29D315B2D06D63E0051B7F4 /* ModelFileType.swift in Sources */,
E29D31302D03A2C50051B7F4 /* DescriptionField.swift in Sources */, E29D31302D03A2C50051B7F4 /* DescriptionField.swift in Sources */,
E29D31BE2D0DB85A0051B7F4 /* AudioPlayerCommand.swift in Sources */, E29D31BE2D0DB85A0051B7F4 /* AudioPlayerCommand.swift in Sources */,
E29D31552D06D2CE0051B7F4 /* TagListView.swift in Sources */, E29D31552D06D2CE0051B7F4 /* TagListView.swift in Sources */,
@ -994,8 +968,6 @@
E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */, E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */,
E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */, E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */,
E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */, E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */,
E29D315D2D06D6EA0051B7F4 /* TextFileType.swift in Sources */,
E21850152CEE55D40090B18B /* FileOnDisk.swift in Sources */,
E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */, E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */,
E22990402D0F95EC009F8D77 /* FolderOnDiskPropertyView.swift in Sources */, E22990402D0F95EC009F8D77 /* FolderOnDiskPropertyView.swift in Sources */,
E21850332CFAFA2F0090B18B /* Settings.swift in Sources */, E21850332CFAFA2F0090B18B /* Settings.swift in Sources */,
@ -1003,8 +975,8 @@
E29D31412D04887F0051B7F4 /* SelectedDetailView.swift in Sources */, E29D31412D04887F0051B7F4 /* SelectedDetailView.swift in Sources */,
E29D31A32D0CC98C0051B7F4 /* Item.swift in Sources */, E29D31A32D0CC98C0051B7F4 /* Item.swift in Sources */,
E25DA57A2D01C64400AEF16D /* PageContentGenerator.swift in Sources */, E25DA57A2D01C64400AEF16D /* PageContentGenerator.swift in Sources */,
E21850192CEE561C0090B18B /* PageOnDisk.swift in Sources */,
E2A21C202CB28ED20060935B /* MockImage.swift in Sources */, E2A21C202CB28ED20060935B /* MockImage.swift in Sources */,
E2FE0EEE2D1C22F3002963B7 /* InlineLinkProcessor.swift in Sources */,
E29D313F2D04822C0051B7F4 /* AddPostView.swift in Sources */, E29D313F2D04822C0051B7F4 /* AddPostView.swift in Sources */,
E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */, E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */,
E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */, E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */,
@ -1020,6 +992,7 @@
E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */, E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */,
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */, E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */,
E22990382D0F7B32009F8D77 /* OptionalImagePropertyView.swift in Sources */, E22990382D0F7B32009F8D77 /* OptionalImagePropertyView.swift in Sources */,
E2FE0EEC2D1C1253002963B7 /* MultiFileSelectionView.swift in Sources */,
E29D31B32D0DA6E80051B7F4 /* ButtonIcons.swift in Sources */, E29D31B32D0DA6E80051B7F4 /* ButtonIcons.swift in Sources */,
E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */, E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */,
E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */, E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */,
@ -1047,6 +1020,7 @@
E29D319F2D0C46310051B7F4 /* PageIssueChecker.swift in Sources */, E29D319F2D0C46310051B7F4 /* PageIssueChecker.swift in Sources */,
E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */, E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */,
E29D31322D03B5680051B7F4 /* LocalizedPostDetailView.swift in Sources */, E29D31322D03B5680051B7F4 /* LocalizedPostDetailView.swift in Sources */,
E2FE0EE82D16D4A3002963B7 /* ConvertThrowing.swift in Sources */,
E29D31712D08234D0051B7F4 /* GenerationDetailView.swift in Sources */, E29D31712D08234D0051B7F4 /* GenerationDetailView.swift in Sources */,
E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */, E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */,
E29D31C32D0DBEF20051B7F4 /* Song.swift in Sources */, E29D31C32D0DBEF20051B7F4 /* Song.swift in Sources */,
@ -1054,14 +1028,12 @@
E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */, E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */,
E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */, E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */,
E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */, E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */,
E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */,
E22990192D0E3546009F8D77 /* ItemType.swift in Sources */, E22990192D0E3546009F8D77 /* ItemType.swift in Sources */,
E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */, E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */,
E22990482D10B7B7009F8D77 /* StorageAccessError.swift in Sources */, E22990482D10B7B7009F8D77 /* StorageAccessError.swift in Sources */,
E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */, E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */,
E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */, E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */,
E22990152D0E2B7F009F8D77 /* ItemSelectionView.swift in Sources */, E22990152D0E2B7F009F8D77 /* ItemSelectionView.swift in Sources */,
E29D315F2D06D6F30051B7F4 /* CodeFileType.swift in Sources */,
E2A37D0E2CE527070000979F /* Storage.swift in Sources */, E2A37D0E2CE527070000979F /* Storage.swift in Sources */,
E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */, E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */,
E29D314D2D04FCBF0051B7F4 /* FileToAddView.swift in Sources */, E29D314D2D04FCBF0051B7F4 /* FileToAddView.swift in Sources */,

View File

@ -6,4 +6,8 @@ extension Array {
Array(self[$0..<Swift.min($0 + size, count)]) Array(self[$0..<Swift.min($0 + size, count)])
} }
} }
var nonEmpty: Self? {
isEmpty ? nil : self
}
} }

View File

@ -0,0 +1,11 @@
prefix operator ~>
prefix func ~> (operation: () throws -> Void) -> Bool {
do {
try operation()
return true
} catch {
return false
}
}

View File

@ -27,3 +27,20 @@ extension Collection {
} }
} }
extension Collection where Element: Collection, Element.Element: Hashable {
func union() -> Set<Element.Element> {
reduce(into: []) { $0.formUnion($1) }
}
}
extension RangeReplaceableCollection where Element: Comparable {
mutating func insertSorted(_ element: Element) {
let index = firstIndex(where: { $0 > element }) ?? endIndex
insert(element, at: index)
}
}

View File

@ -1,5 +1,5 @@
enum PageContentAnomaly { enum GenerationAnomaly {
case failedToLoadContent case failedToLoadContent
case failedToParseContent case failedToParseContent
case missingFile(file: String, markdown: String) case missingFile(file: String, markdown: String)
@ -9,7 +9,7 @@ enum PageContentAnomaly {
case warning(String) case warning(String)
} }
extension PageContentAnomaly: Identifiable { extension GenerationAnomaly: Identifiable {
var id: String { var id: String {
switch self { switch self {
@ -31,21 +31,21 @@ extension PageContentAnomaly: Identifiable {
} }
} }
extension PageContentAnomaly: Equatable { extension GenerationAnomaly: Equatable {
static func == (lhs: PageContentAnomaly, rhs: PageContentAnomaly) -> Bool { static func == (lhs: GenerationAnomaly, rhs: GenerationAnomaly) -> Bool {
lhs.id == rhs.id lhs.id == rhs.id
} }
} }
extension PageContentAnomaly: Hashable { extension GenerationAnomaly: Hashable {
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
hasher.combine(id) hasher.combine(id)
} }
} }
extension PageContentAnomaly { extension GenerationAnomaly {
enum Severity: String, CaseIterable { enum Severity: String, CaseIterable {
case warning case warning
@ -62,7 +62,7 @@ extension PageContentAnomaly {
} }
} }
extension PageContentAnomaly: CustomStringConvertible { extension GenerationAnomaly: CustomStringConvertible {
var description: String { var description: String {
switch self { switch self {

View File

@ -0,0 +1,199 @@
import Foundation
struct LocalizedPageId: Hashable {
let language: ContentLanguage
let pageId: String
}
final class GenerationResults: ObservableObject {
/// The files that could not be accessed
@Published
var inaccessibleFiles: Set<FileResource> = []
/// The files that could not be parsed, with the error message produced
@Published
var unparsableFiles: Set<FileResource> = []
@Published
var missingFiles: Set<String> = []
@Published
var missingTags: Set<String> = []
@Published
var missingPages: Set<String> = []
@Published
var externalLinks: Set<String> = []
@Published
var requiredFiles: Set<FileResource> = []
@Published
var imagesToGenerate: Set<ImageGenerationJob> = []
@Published
var invalidCommands: Set<String> = []
@Published
var warnings: Set<String> = []
@Published
var unsavedOutputFiles: Set<String> = []
@Published
var failedImages: Set<ImageGenerationJob> = []
@Published
var emptyPages: Set<LocalizedPageId> = []
/// The cache of previously used GenerationResults
private var cache: [ItemId : PageGenerationResults] = [:]
private(set) var general: PageGenerationResults!
var resultCount: Int {
cache.count
}
// MARK: Life cycle
init() {
let id = ItemId(language: .english, itemType: .general)
let general = PageGenerationResults(itemId: id, delegate: self)
self.general = general
cache[id] = general
}
func makeResults(_ itemId: ItemId) -> PageGenerationResults {
guard let result = cache[itemId] else {
let result = PageGenerationResults(itemId: itemId, delegate: self)
cache[itemId] = result
return result
}
return result
}
func makeResults(for type: ItemType, in language: ContentLanguage) -> PageGenerationResults {
let itemId = ItemId(language: language, itemType: type)
return makeResults(itemId)
}
func makeResults(for page: Page, in language: ContentLanguage) -> PageGenerationResults {
let itemId = ItemId(language: language, itemType: .page(page))
return makeResults(itemId)
}
func makeResults(for tag: Tag, in language: ContentLanguage) -> PageGenerationResults {
let itemId = ItemId(language: language, itemType: .tagPage(tag))
return makeResults(itemId)
}
func recalculate() {
let inaccessibleFiles = cache.values.map { $0.inaccessibleFiles }.union()
update { self.inaccessibleFiles = inaccessibleFiles }
let unparsableFiles = cache.values.map { $0.unparsableFiles.keys }.union()
update { self.unparsableFiles = unparsableFiles }
let missingFiles = cache.values.map { $0.missingFiles.keys }.union()
update { self.missingFiles = missingFiles }
let missingTags = cache.values.map { $0.missingLinkedTags.keys }.union()
update { self.missingTags = missingTags }
let missingPages = cache.values.map { $0.missingLinkedPages.keys }.union()
update { self.missingPages = missingPages }
let externalLinks = cache.values.map { $0.externalLinks }.union()
update { self.externalLinks = externalLinks }
let requiredFiles = cache.values.map { $0.requiredFiles }.union()
update { self.requiredFiles = requiredFiles }
let imagesToGenerate = cache.values.map { $0.imagesToGenerate }.union()
update { self.imagesToGenerate = imagesToGenerate }
let invalidCommands = cache.values.map { $0.invalidCommands.map { $0.markdown }}.union()
update { self.invalidCommands = invalidCommands }
let warnings = cache.values.map { $0.warnings }.union()
update { self.warnings = warnings }
let unsavedOutputFiles = cache.values.map { $0.unsavedOutputFiles.keys }.union()
update { self.unsavedOutputFiles = unsavedOutputFiles }
}
private func update(_ operation: @escaping () -> Void) {
DispatchQueue.main.async {
operation()
}
}
// MARK: Adding entries
func inaccessibleContent(file: FileResource) {
update { self.inaccessibleFiles.insert(file) }
}
func unparsable(file: FileResource) {
update { self.unparsableFiles.insert(file) }
}
func missing(file: String) {
update { self.missingFiles.insert(file) }
}
func missing(tag: String) {
update { self.missingTags.insert(tag) }
}
func missing(page: String) {
update { self.missingPages.insert(page) }
}
func externalLink(_ url: String) {
update { self.externalLinks.insert(url) }
}
func require(file: FileResource) {
update { self.requiredFiles.insert(file) }
}
func require<S>(files: S) where S: Sequence, S.Element == FileResource {
update { self.requiredFiles.formUnion(files) }
}
func generate(_ image: ImageGenerationJob) {
update { self.imagesToGenerate.insert(image) }
}
func generate<S>(_ images: S) where S: Sequence, S.Element == ImageGenerationJob {
update { self.imagesToGenerate.formUnion(images) }
}
func invalidCommand(_ markdown: String) {
update { self.invalidCommands.insert(markdown) }
}
func warning(_ warning: String) {
update { self.warnings.insert(warning) }
}
func failed(image: ImageGenerationJob) {
update { self.failedImages.insert(image) }
}
func unsaved(_ path: String) {
update { self.unsavedOutputFiles.insert(path) }
}
}
private extension Dictionary where Value == Set<ItemId> {
mutating func remove<S>(keys: S, of item: ItemId) where S: Sequence, S.Element == Key {
for key in keys {
guard var value = self[key] else { continue }
value.remove(item)
if value.isEmpty {
self[key] = nil
} else {
self[key] = value
}
}
}
}

View File

@ -97,14 +97,14 @@ extension HeaderElement {
var content: String { var content: String {
switch self { switch self {
case .icon(let file, let size, let rel): case .icon(let file, let size, let rel):
return "<link rel='\(rel)' sizes='\(size)x\(size)' href='\(file.assetUrl)'>" return "<link rel='\(rel)' sizes='\(size)x\(size)' href='\(file.absoluteUrl)'>"
case .css(let file, _): case .css(let file, _):
return "<link rel='stylesheet' href='\(file.assetUrl)' />" return "<link rel='stylesheet' href='\(file.absoluteUrl)' />"
case .js(let file, let deferred): case .js(let file, let deferred):
let deferText = deferred ? " defer" : "" let deferText = deferred ? " defer" : ""
return "<script src='\(file.assetUrl)'\(deferText)></script>" return "<script src='\(file.absoluteUrl)'\(deferText)></script>"
case .jsModule(let file): case .jsModule(let file):
return "<script type='module' src='\(file.assetUrl)'></script>" return "<script type='module' src='\(file.absoluteUrl)'></script>"
case .author(let author): case .author(let author):
return "<meta name='author' content='\(author)'>" return "<meta name='author' content='\(author)'>"
case .title(let title): case .title(let title):

View File

@ -11,8 +11,6 @@ final class ImageGenerator {
private var generatedImages: [String : Set<String>] = [:] private var generatedImages: [String : Set<String>] = [:]
private var jobs: [ImageGenerationJob] = []
init(storage: Storage, settings: Settings) { init(storage: Storage, settings: Settings) {
self.storage = storage self.storage = storage
self.settings = settings self.settings = settings
@ -23,20 +21,6 @@ final class ImageGenerator {
settings.paths.imagesOutputFolderPath settings.paths.imagesOutputFolderPath
} }
func runJobs(callback: (String) -> Void) -> Bool {
guard !jobs.isEmpty else {
return true
}
print("Generating \(jobs.count) images...")
while let job = jobs.popLast() {
callback("Generating image \(job.version)")
guard generate(job: job) else {
return false
}
}
return true
}
func save() -> Bool { func save() -> Bool {
guard storage.save(listOfGeneratedImages: generatedImages) else { guard storage.save(listOfGeneratedImages: generatedImages) else {
print("Failed to save list of generated images") print("Failed to save list of generated images")
@ -45,50 +29,6 @@ final class ImageGenerator {
return true return true
} }
private func versionFileName(image: String, type: ImageFileType, width: CGFloat, height: CGFloat) -> String {
let fileName = image.fileNameAndExtension.fileName
let prefix = "\(fileName)@\(Int(width))x\(Int(height))"
return "\(prefix).\(type.fileExtension)"
}
func generateImageSet(for image: String, maxWidth: CGFloat, maxHeight: CGFloat) {
let type = ImageFileType(fileExtension: image.fileExtension!)!
let width2x = maxWidth * 2
let height2x = maxHeight * 2
generateVersion(for: image, type: .avif, maximumWidth: maxWidth, maximumHeight: maxHeight)
generateVersion(for: image, type: .avif, maximumWidth: width2x, maximumHeight: height2x)
generateVersion(for: image, type: .webp, maximumWidth: maxWidth, maximumHeight: maxHeight)
generateVersion(for: image, type: .webp, maximumWidth: width2x, maximumHeight: height2x)
generateVersion(for: image, type: type, maximumWidth: maxWidth, maximumHeight: maxHeight)
generateVersion(for: image, type: type, maximumWidth: width2x, maximumHeight: height2x)
}
func generateVersion(for image: String, type: ImageFileType, maximumWidth: CGFloat, maximumHeight: CGFloat) {
let version = versionFileName(image: image, type: type, width: maximumWidth, height: maximumHeight)
guard needsToGenerate(version: version, for: image) else {
// Image already present
return
}
guard !jobs.contains(where: { $0.version == version }) else {
// Job already in queue
return
}
let job = ImageGenerationJob(
image: image,
version: version,
maximumWidth: maximumWidth,
maximumHeight: maximumHeight,
quality: 0.7,
type: type)
jobs.append(job)
}
/** /**
Remove all versions of an image, so that they will be recreated on the next run. Remove all versions of an image, so that they will be recreated on the next run.
@ -105,6 +45,9 @@ final class ImageGenerator {
} }
private func needsToGenerate(version: String, for image: String) -> Bool { private func needsToGenerate(version: String, for image: String) -> Bool {
if exists(version) {
return false
}
guard let versions = generatedImages[image] else { guard let versions = generatedImages[image] else {
return true return true
} }
@ -143,7 +86,7 @@ final class ImageGenerator {
// MARK: Image operations // MARK: Image operations
private func generate(job: ImageGenerationJob) -> Bool { func generate(job: ImageGenerationJob) -> Bool {
guard needsToGenerate(version: job.version, for: job.image) else { guard needsToGenerate(version: job.version, for: job.image) else {
return true return true
} }
@ -158,7 +101,7 @@ final class ImageGenerator {
return false return false
} }
let representation = create(image: originalImage, width: job.maximumWidth, height: job.maximumHeight) let representation = create(image: originalImage, width: CGFloat(job.maximumWidth), height: CGFloat(job.maximumHeight))
guard let data = create(image: representation, type: job.type, quality: job.quality) else { guard let data = create(image: representation, type: job.type, quality: job.quality) else {
print("Failed to get data for type \(job.type)") print("Failed to get data for type \(job.type)")
@ -209,7 +152,7 @@ final class ImageGenerator {
// MARK: Avif images // MARK: Avif images
private func create(image: NSBitmapImageRep, type: ImageFileType, quality: CGFloat) -> Data? { private func create(image: NSBitmapImageRep, type: FileType, quality: CGFloat) -> Data? {
switch type { switch type {
case .jpg: case .jpg:
return image.representation(using: .jpeg, properties: [.compressionFactor: NSNumber(value: 0.6)]) return image.representation(using: .jpeg, properties: [.compressionFactor: NSNumber(value: 0.6)])
@ -225,6 +168,8 @@ final class ImageGenerator {
return nil return nil
case .tiff: case .tiff:
return nil return nil
default:
return nil
} }
} }

View File

@ -4,13 +4,70 @@ struct ImageGenerationJob {
let image: String let image: String
let version: String let type: FileType
let maximumWidth: CGFloat let maximumWidth: Int
let maximumHeight: CGFloat let maximumHeight: Int
let quality: CGFloat let quality: CGFloat
let type: ImageFileType init(image: String, type: FileType, maximumWidth: CGFloat, maximumHeight: CGFloat, quality: CGFloat = 0.7) {
self.image = image
self.type = type
self.maximumWidth = Int(maximumWidth)
self.maximumHeight = Int(maximumHeight)
self.quality = quality
}
init(image: String, type: FileType, maximumWidth: Int, maximumHeight: Int, quality: CGFloat = 0.7) {
self.image = image
self.type = type
self.maximumWidth = maximumWidth
self.maximumHeight = maximumHeight
self.quality = quality
}
var version: String {
let fileName = image.fileNameAndExtension.fileName
let prefix = "\(fileName)@\(maximumWidth)x\(maximumHeight)"
return "\(prefix).\(type.fileExtension)"
}
static func imageSet(for image: String, maxWidth: Int, maxHeight: Int, quality: CGFloat = 0.7) -> [ImageGenerationJob] {
let type = FileType(fileExtension: image.fileExtension)
let width2x = maxWidth * 2
let height2x = maxHeight * 2
return [
.init(image: image, type: .avif, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
.init(image: image, type: .avif, maximumWidth: width2x, maximumHeight: height2x, quality: quality),
.init(image: image, type: .webp, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
.init(image: image, type: .webp, maximumWidth: width2x, maximumHeight: height2x, quality: quality),
.init(image: image, type: type, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
.init(image: image, type: type, maximumWidth: width2x, maximumHeight: height2x, quality: quality)
]
}
}
extension ImageGenerationJob: Equatable {
static func == (lhs: ImageGenerationJob, rhs: ImageGenerationJob) -> Bool {
lhs.version == rhs.version
}
}
extension ImageGenerationJob: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(version)
}
}
extension ImageGenerationJob: Comparable {
static func < (lhs: ImageGenerationJob, rhs: ImageGenerationJob) -> Bool {
lhs.version < rhs.version
}
} }

View File

@ -1,97 +0,0 @@
import Foundation
final class LocalizedWebsiteGenerator {
private let content: Content
let language: ContentLanguage
private let imageGenerator: ImageGenerator
private let localizedPostSettings: LocalizedPostSettings
init(content: Content, language: ContentLanguage) {
self.language = language
self.content = content
self.localizedPostSettings = content.settings.localized(in: language)
self.imageGenerator = ImageGenerator(
storage: content.storage,
settings: content.settings)
}
private var postsPerPage: Int {
content.settings.posts.postsPerPage
}
private var mainContentMaximumWidth: CGFloat {
CGFloat(content.settings.posts.contentWidth)
}
func generateWebsite(callback: (String) -> Void) -> Bool {
guard createMainPostFeedPages() else {
return false
}
#warning("Generate content pages")
#warning("Generate tag overview page")
guard generateTagPages() else {
return false
}
guard imageGenerator.runJobs(callback: callback) else {
return false
}
return imageGenerator.save()
}
private func createMainPostFeedPages() -> Bool {
let generator = PostListPageGenerator(
language: language,
content: content,
imageGenerator: imageGenerator,
showTitle: false,
pageTitle: localizedPostSettings.title,
pageDescription: localizedPostSettings.description,
pageUrlPrefix: localizedPostSettings.feedUrlPrefix)
return generator.createPages(for: content.posts)
}
private func generateTagPages() -> Bool {
for tag in content.tags {
let posts = content.posts.filter { $0.tags.contains(tag) }
guard posts.count > 0 else { continue }
let localized = tag.localized(in: language)
let urlPrefix = content.absoluteUrlPrefixForTag(tag, language: language)
let generator = PostListPageGenerator(
language: language,
content: content,
imageGenerator: imageGenerator,
showTitle: true,
pageTitle: localized.name,
pageDescription: localized.description ?? "",
pageUrlPrefix: urlPrefix)
guard generator.createPages(for: posts) else {
return false
}
}
return true
}
private func copy(requiredFiles: Set<FileResource>) -> Bool {
//print("Copying \(requiredVideoFiles.count) files...")
for file in requiredFiles {
guard !file.isExternallyStored else {
continue
}
guard content.storage.copy(file: file.id, to: file.absoluteUrl) else {
return false
}
}
return true
}
private func save(_ content: String, to relativePath: String) -> Bool {
self.content.storage.write(content, to: relativePath)
}
}

View File

@ -27,18 +27,18 @@ struct AudioPlayerCommandProcessor: CommandProcessor {
} }
guard let file = content.file(fileId) else { guard let file = content.file(fileId) else {
results.missingFiles.insert(fileId) results.missing(file: fileId, source: "Audio player song list")
return "" return ""
} }
guard let data = file.dataContent() else { guard let data = file.dataContent() else {
results.issues.insert(.failedToLoadContent) results.inaccessibleContent(file: file)
return "" return ""
} }
let songs: [Song] let songs: [Song]
do { do {
songs = try JSONDecoder().decode([Song].self, from: data) songs = try JSONDecoder().decode([Song].self, from: data)
} catch { } catch {
results.issues.insert(.failedToParseContent) results.invalidFormat(file: file, error: "Not valid JSON containing [Song]: \(error)")
return "" return ""
} }
@ -47,12 +47,12 @@ struct AudioPlayerCommandProcessor: CommandProcessor {
for song in songs { for song in songs {
guard let image = content.image(song.cover) else { guard let image = content.image(song.cover) else {
results.missing(file: song.cover, markdown: "Missing cover image \(song.cover) in \(file.id)") results.missing(file: song.cover, containedIn: file)
continue continue
} }
guard let audioFile = content.file(song.file) else { guard let audioFile = content.file(song.file) else {
results.missing(file: song.file, markdown: "Missing audio file \(song.file) in \(file.id)") results.missing(file: song.cover, containedIn: file)
continue continue
} }
#warning("Check if file is audio") #warning("Check if file is audio")
@ -79,18 +79,17 @@ struct AudioPlayerCommandProcessor: CommandProcessor {
} }
let footerScript = AudioPlayerScript(items: amplitude).content let footerScript = AudioPlayerScript(items: amplitude).content
results.requiredFooters.insert(footerScript) results.require(footer: footerScript)
results.requiredHeaders.insert(.audioPlayerCss) results.require(headers: .audioPlayerCss, .audioPlayerJs)
results.requiredHeaders.insert(.audioPlayerJs)
results.requiredIcons.formUnion([ results.require(icons:
.audioPlayerClose, .audioPlayerClose,
.audioPlayerPlaylist, .audioPlayerPlaylist,
.audioPlayerNext, .audioPlayerNext,
.audioPlayerPrevious, .audioPlayerPrevious,
.audioPlayerPlay, .audioPlayerPlay,
.audioPlayerPause .audioPlayerPause
]) )
return AudioPlayer(playingText: titleText, items: playlist).content return AudioPlayer(playingText: titleText, items: playlist).content
} }

View File

@ -57,11 +57,11 @@ struct ButtonCommandProcessor: CommandProcessor {
let downloadName = arguments.count > 2 ? arguments[2].trimmed : nil let downloadName = arguments.count > 2 ? arguments[2].trimmed : nil
guard let file = content.file(fileId) else { guard let file = content.file(fileId) else {
results.missing(file: fileId, markdown: markdown) results.missing(file: fileId, source: "Download button")
return nil return nil
} }
results.files.insert(file) results.require(file: file)
results.requiredIcons.insert(.buttonDownload) results.require(icon: .buttonDownload)
return ContentButtons.Item( return ContentButtons.Item(
icon: .buttonDownload, icon: .buttonDownload,
filePath: file.absoluteUrl, filePath: file.absoluteUrl,
@ -80,8 +80,8 @@ struct ButtonCommandProcessor: CommandProcessor {
return nil return nil
} }
results.externalLinks.insert(rawUrl) results.externalLink(to: rawUrl)
results.requiredIcons.insert(icon) results.require(icon: icon)
let title = arguments[1].trimmed let title = arguments[1].trimmed
@ -96,7 +96,7 @@ struct ButtonCommandProcessor: CommandProcessor {
let text = arguments[0].trimmed let text = arguments[0].trimmed
let event = arguments[1].trimmed let event = arguments[1].trimmed
results.requiredIcons.insert(.buttonPlay) results.require(icon: .buttonPlay)
return .init(icon: .buttonPlay, filePath: nil, text: text, onClickText: event) return .init(icon: .buttonPlay, filePath: nil, text: text, onClickText: event)
} }

View File

@ -0,0 +1,75 @@
struct InlineLinkProcessor {
private let pageLinkMarker = "page:"
private let tagLinkMarker = "tag:"
private let fileLinkMarker = "file:"
let content: Content
let results: PageGenerationResults
let language: ContentLanguage
func handleLink(html: String, markdown: Substring) -> String {
let url = markdown.between("(", and: ")")
if url.hasPrefix(pageLinkMarker) {
return handleInlinePageLink(url: url, html: html, markdown: markdown)
}
if url.hasPrefix(tagLinkMarker) {
return handleInlineTagLink(url: url, html: html, markdown: markdown)
}
if url.hasPrefix(fileLinkMarker) {
return handleInlineFileLink(url: url, html: html, markdown: markdown)
}
results.externalLink(to: url)
return html
}
private func handleInlinePageLink(url: String, html: String, markdown: Substring) -> String {
// Retain links pointing to elements within a page
let textToChange = url.dropAfterFirst("#")
let pageId = textToChange.replacingOccurrences(of: pageLinkMarker, with: "")
guard let page = content.page(pageId) else {
results.missing(page: pageId, source: "Inline page link")
// Remove link since the page can't be found
return markdown.between("[", and: "]")
}
guard !page.isDraft else {
return markdown.between("[", and: "]")
}
results.linked(to: page)
let pagePath = page.absoluteUrl(in: language)
return html.replacingOccurrences(of: textToChange, with: pagePath)
}
private func handleInlineTagLink(url: String, html: String, markdown: Substring) -> String {
// Retain links pointing to elements within a page
let textToChange = url.dropAfterFirst("#")
let tagId = textToChange.replacingOccurrences(of: tagLinkMarker, with: "")
guard let tag = content.tag(tagId) else {
results.missing(tag: tagId, source: "Inline tag link")
// Remove link since the tag can't be found
return markdown.between("[", and: "]")
}
results.linked(to: tag)
let tagPath = content.absoluteUrlToTag(tag, language: language)
return html.replacingOccurrences(of: textToChange, with: tagPath)
}
private func handleInlineFileLink(url: String, html: String, markdown: Substring) -> String {
// Retain links pointing to elements within a page
let fileId = url.replacingOccurrences(of: fileLinkMarker, with: "")
guard let file = content.file(fileId) else {
results.missing(file: fileId, source: "Inline file link")
// Remove link since the file can't be found
return markdown.between("[", and: "]")
}
results.require(file: file)
let filePath = file.absoluteUrl
return html.replacingOccurrences(of: url, with: filePath)
}
}

View File

@ -23,7 +23,7 @@ struct LabelsCommandProcessor: CommandProcessor {
results.invalid(command: .labels, markdown) results.invalid(command: .labels, markdown)
return nil return nil
} }
results.requiredIcons.insert(icon) results.require(icon: icon)
return .init(icon: icon, value: parts[1]) return .init(icon: icon, value: parts[1])
} }
return ContentLabels(labels: labels).content return ContentLabels(labels: labels).content

View File

@ -3,29 +3,25 @@ import Ink
import Splash import Splash
import SwiftSoup import SwiftSoup
typealias VideoSource = (url: String, type: VideoFileType)
final class PageContentParser { final class PageContentParser {
private let pageLinkMarker = "page:"
private let tagLinkMarker = "tag:"
private static let codeHighlightFooter = "<script>hljs.highlightAll();</script>" private static let codeHighlightFooter = "<script>hljs.highlightAll();</script>"
private let swift = SyntaxHighlighter(format: HTMLOutputFormat()) private let swift = SyntaxHighlighter(format: HTMLOutputFormat())
let results = PageGenerationResults() private let language: ContentLanguage
private let content: Content private let content: Content
private let results: PageGenerationResults
private let buttonHandler: ButtonCommandProcessor private let buttonHandler: ButtonCommandProcessor
private let labelHandler: LabelsCommandProcessor private let labelHandler: LabelsCommandProcessor
private let audioPlayer: AudioPlayerCommandProcessor private let audioPlayer: AudioPlayerCommandProcessor
let language: ContentLanguage private let inlineLink: InlineLinkProcessor
var largeImageWidth: Int { var largeImageWidth: Int {
content.settings.pages.largeImageWidth content.settings.pages.largeImageWidth
@ -35,33 +31,21 @@ final class PageContentParser {
content.settings.pages.contentWidth content.settings.pages.contentWidth
} }
init(content: Content, language: ContentLanguage) { init(content: Content, language: ContentLanguage, results: PageGenerationResults) {
self.content = content self.content = content
self.results = results
self.language = language self.language = language
self.buttonHandler = .init(content: content, results: results) self.buttonHandler = .init(content: content, results: results)
self.labelHandler = .init(content: content, results: results) self.labelHandler = .init(content: content, results: results)
self.audioPlayer = .init(content: content, results: results) self.audioPlayer = .init(content: content, results: results)
} self.inlineLink = .init(content: content, results: results, language: language)
func requestImages(_ generator: ImageGenerator) {
for request in results.imagesToGenerate {
generator.generateImageSet(
for: request.image.id,
maxWidth: CGFloat(request.size),
maxHeight: CGFloat(request.size))
}
}
func reset() {
results.reset()
} }
func generatePage(from content: String) -> String { func generatePage(from content: String) -> String {
reset()
let parser = MarkdownParser(modifiers: [ let parser = MarkdownParser(modifiers: [
Modifier(target: .images, closure: processMarkdownImage), Modifier(target: .images, closure: processMarkdownImage),
Modifier(target: .codeBlocks, closure: handleCode), Modifier(target: .codeBlocks, closure: handleCode),
Modifier(target: .links, closure: handleLink), Modifier(target: .links, closure: inlineLink.handleLink),
Modifier(target: .html, closure: handleHTML), Modifier(target: .html, closure: handleHTML),
Modifier(target: .headings, closure: handleHeadlines) Modifier(target: .headings, closure: handleHeadlines)
]) ])
@ -70,8 +54,8 @@ final class PageContentParser {
private func handleCode(html: String, markdown: Substring) -> String { private func handleCode(html: String, markdown: Substring) -> String {
guard markdown.starts(with: "```swift") else { guard markdown.starts(with: "```swift") else {
results.requiredHeaders.insert(.codeHightlighting) results.require(header: .codeHightlighting)
results.requiredFooters.insert(PageContentParser.codeHighlightFooter) results.require(footer: PageContentParser.codeHighlightFooter)
return html // Just use normal code highlighting return html // Just use normal code highlighting
} }
// Highlight swift code using Splash // Highlight swift code using Splash
@ -79,46 +63,6 @@ final class PageContentParser {
return "<pre><code>" + swift.highlight(code) + "</pre></code>" return "<pre><code>" + swift.highlight(code) + "</pre></code>"
} }
private func handleLink(html: String, markdown: Substring) -> String {
let file = markdown.between("(", and: ")")
if file.hasPrefix(pageLinkMarker) {
return handlePageLink(file: file, html: html, markdown: markdown)
}
if file.hasPrefix(tagLinkMarker) {
return handleTagLink(file: file, html: html, markdown: markdown)
}
results.externalLinks.insert(file)
return html
}
private func handlePageLink(file: String, html: String, markdown: Substring) -> String {
// Retain links pointing to elements within a page
let textToChange = file.dropAfterFirst("#")
let pageId = textToChange.replacingOccurrences(of: pageLinkMarker, with: "")
guard let page = content.page(pageId) else {
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 = page.absoluteUrl(in: language)
return html.replacingOccurrences(of: textToChange, with: pagePath)
}
private func handleTagLink(file: String, html: String, markdown: Substring) -> String {
// Retain links pointing to elements within a page
let textToChange = file.dropAfterFirst("#")
let tagId = textToChange.replacingOccurrences(of: tagLinkMarker, with: "")
guard let tag = content.tag(tagId) else {
results.missing(tag: tagId, markdown: markdown)
// Remove link since the tag can't be found
return markdown.between("[", and: "]")
}
results.linkedTags.insert(tag)
let tagPath = content.absoluteUrlToTag(tag, language: language)
return html.replacingOccurrences(of: textToChange, with: tagPath)
}
private func handleHTML(html: String, _: Substring) -> String { private func handleHTML(html: String, _: Substring) -> String {
findResourcesInHtml(html: html) findResourcesInHtml(html: html)
return html return html
@ -144,7 +88,7 @@ final class PageContentParser {
.filter { !$0.trimmed.isEmpty } .filter { !$0.trimmed.isEmpty }
for src in srcAttributes { for src in srcAttributes {
results.issues.insert(.warning("Found image in html: \(src)")) results.warning("Found image in html: \(src)")
} }
} catch { } catch {
print("Error parsing HTML: \(error)") print("Error parsing HTML: \(error)")
@ -166,9 +110,9 @@ final class PageContentParser {
for url in srcAttributes { for url in srcAttributes {
if url.hasPrefix("http://") || url.hasPrefix("https://") { if url.hasPrefix("http://") || url.hasPrefix("https://") {
results.externalLinks.insert(url) results.externalLink(to: url)
} else { } else {
results.issues.insert(.warning("Relative link in HTML: \(url)")) results.warning("Relative link in HTML: \(url)")
} }
} }
} catch { } catch {
@ -190,7 +134,7 @@ final class PageContentParser {
.filter { !$0.trimmed.isEmpty } .filter { !$0.trimmed.isEmpty }
for src in srcsetAttributes { for src in srcsetAttributes {
results.issues.insert(.warning("Found source set in html: \(src)")) results.warning("Found source set in html: \(src)")
} }
let srcAttributes = try linkElements.array() let srcAttributes = try linkElements.array()
@ -199,14 +143,15 @@ final class PageContentParser {
for src in srcAttributes { for src in srcAttributes {
guard content.isValidIdForFile(src) else { guard content.isValidIdForFile(src) else {
results.issues.insert(.warning("Found source in html: \(src)")) results.warning("Found source in html: \(src)")
continue continue
} }
guard let file = content.file(src) else { guard let file = content.file(src) else {
results.issues.insert(.warning("Found source in html: \(src)")) results.warning("Found source in html: \(src)")
continue continue
} }
results.files.insert(file) #warning("Either find files by their full path, or replace file id with full path")
results.require(file: file)
} }
} catch { } catch {
print("Error parsing HTML: \(error)") print("Error parsing HTML: \(error)")
@ -285,7 +230,7 @@ final class PageContentParser {
} }
/** /**
Format: `[image](<imageId>;<caption?>]` Format: `![image](<imageId>;<caption?>]`
*/ */
private func handleImage(_ arguments: [String], markdown: Substring) -> String { private func handleImage(_ arguments: [String], markdown: Substring) -> String {
guard (1...2).contains(arguments.count) else { guard (1...2).contains(arguments.count) else {
@ -295,10 +240,10 @@ final class PageContentParser {
let imageId = arguments[0] let imageId = arguments[0]
guard let image = content.image(imageId) else { guard let image = content.image(imageId) else {
results.missing(file: imageId, markdown: markdown) results.missing(file: imageId, source: "Image command")
return "" return ""
} }
results.files.insert(image) results.used(file: image)
let caption = arguments.count == 2 ? arguments[1] : nil let caption = arguments.count == 2 ? arguments[1] : nil
let altText = image.localized(in: language) let altText = image.localized(in: language)
@ -314,14 +259,14 @@ final class PageContentParser {
width: thumbnailWidth, width: thumbnailWidth,
height: thumbnailWidth, height: thumbnailWidth,
altText: altText) altText: altText)
results.imagesToGenerate.insert(.init(size: thumbnailWidth, image: image)) results.requireImageSet(for: image, size: thumbnailWidth)
let largeImage = FeedEntryData.Image( let largeImage = FeedEntryData.Image(
rawImagePath: path, rawImagePath: path,
width: largeImageWidth, width: largeImageWidth,
height: largeImageWidth, height: largeImageWidth,
altText: altText) altText: altText)
results.imagesToGenerate.insert(.init(size: largeImageWidth, image: image)) results.requireImageSet(for: image, size: largeImageWidth)
return PageImage( return PageImage(
imageId: imageId.replacingOccurrences(of: ".", with: "-"), imageId: imageId.replacingOccurrences(of: ".", with: "-"),
@ -343,12 +288,13 @@ final class PageContentParser {
let options = arguments.dropFirst().compactMap { convertVideoOption($0, markdown: markdown) } let options = arguments.dropFirst().compactMap { convertVideoOption($0, markdown: markdown) }
guard let file = content.file(fileId) else { guard let file = content.file(fileId) else {
results.missing(file: fileId, markdown: markdown) results.missing(file: fileId, source: "Video command")
return "" return ""
} }
results.files.insert(file) #warning("Create/specify video alternatives")
results.require(file: file)
guard let videoType = file.type.videoType?.htmlType else { guard let videoType = file.type.htmlType else {
results.invalid(command: .video, markdown) results.invalid(command: .video, markdown)
return "" return ""
} }
@ -370,23 +316,22 @@ final class PageContentParser {
} }
if case let .poster(imageId) = option { if case let .poster(imageId) = option {
if let image = content.image(imageId) { if let image = content.image(imageId) {
results.files.insert(image) results.used(file: image)
let width = 2*thumbnailWidth let width = 2*thumbnailWidth
let fullLink = WebsiteImage.imagePath(source: image.absoluteUrl, width: width, height: width) let fullLink = WebsiteImage.imagePath(source: image.absoluteUrl, width: width, height: width)
return .poster(image: fullLink) return .poster(image: fullLink)
} else { } else {
results.missing(file: imageId, markdown: markdown) results.missing(file: imageId, source: "Video command poster")
return nil // Image file not present, so skip the option return nil // Image file not present, so skip the option
} }
} }
if case let .src(videoId) = option { if case let .src(videoId) = option {
if let video = content.video(videoId) { if let video = content.video(videoId) {
results.files.insert(video) results.used(file: video)
let link = video.absoluteUrl let link = video.absoluteUrl
// TODO: Set correct video path?
return .src(link) return .src(link)
} else { } else {
results.missing(file: videoId, markdown: markdown) results.missing(file: videoId, source: "Video command source")
return nil // Video file not present, so skip the option return nil // Video file not present, so skip the option
} }
} }
@ -403,7 +348,7 @@ final class PageContentParser {
} }
let fileId = arguments[0] let fileId = arguments[0]
guard let file = content.file(fileId) else { guard let file = content.file(fileId) else {
results.missing(file: fileId, markdown: markdown) results.missing(file: fileId, source: "External HTML command")
return "" return ""
} }
let content = file.textContent() let content = file.textContent()
@ -435,7 +380,7 @@ final class PageContentParser {
let pageId = arguments[0] let pageId = arguments[0]
guard let page = content.page(pageId) else { guard let page = content.page(pageId) else {
results.missing(page: pageId, markdown: markdown) results.missing(page: pageId, source: "Page link command")
return "" return ""
} }
guard !page.isDraft else { guard !page.isDraft else {
@ -443,6 +388,8 @@ final class PageContentParser {
return "" return ""
} }
results.linked(to: page)
let localized = page.localized(in: language) let localized = page.localized(in: language)
let url = page.absoluteUrl(in: language) let url = page.absoluteUrl(in: language)
let title = localized.linkPreviewTitle ?? localized.title let title = localized.linkPreviewTitle ?? localized.title
@ -450,8 +397,8 @@ final class PageContentParser {
let image = localized.linkPreviewImage.map { image in let image = localized.linkPreviewImage.map { image in
let size = content.settings.pages.pageLinkImageSize let size = content.settings.pages.pageLinkImageSize
results.files.insert(image) results.used(file: image)
results.imagesToGenerate.insert(.init(size: size, image: image)) results.requireImageSet(for: image, size: size)
return RelatedPageLink.Image( return RelatedPageLink.Image(
url: image.absoluteUrl, url: image.absoluteUrl,
@ -478,7 +425,7 @@ final class PageContentParser {
let tagId = arguments[0] let tagId = arguments[0]
guard let tag = content.tag(tagId) else { guard let tag = content.tag(tagId) else {
results.missing(tag: tagId, markdown: markdown) results.missing(tag: tagId, source: "Tag link command")
return "" return ""
} }
@ -489,8 +436,7 @@ final class PageContentParser {
let image = localized.linkPreviewImage.map { image in let image = localized.linkPreviewImage.map { image in
let size = content.settings.pages.pageLinkImageSize let size = content.settings.pages.pageLinkImageSize
results.files.insert(image) results.requireImageSet(for: image, size: size)
results.imagesToGenerate.insert(.init(size: size, image: image))
return RelatedPageLink.Image( return RelatedPageLink.Image(
url: image.absoluteUrl, url: image.absoluteUrl,
@ -521,11 +467,11 @@ final class PageContentParser {
} }
guard let file = content.file(fileId) else { guard let file = content.file(fileId) else {
results.missing(file: fileId, markdown: markdown) results.missing(file: fileId, source: "Model command")
return "" return ""
} }
results.files.insert(file) results.require(file: file)
results.requiredHeaders.insert(.modelViewer) results.require(header: .modelViewer)
let description = file.localized(in: language) let description = file.localized(in: language)
return ModelViewer(file: file.absoluteUrl, description: description).content return ModelViewer(file: file.absoluteUrl, description: description).content
@ -548,11 +494,10 @@ final class PageContentParser {
let imageId = arguments[0] let imageId = arguments[0]
guard let image = content.image(imageId) else { guard let image = content.image(imageId) else {
results.missing(file: imageId, markdown: markdown) results.missing(file: imageId, source: "SVG command")
return "" return ""
} }
guard case .image(let imageType) = image.type, guard image.type.isSvg else {
imageType == .svg else {
results.invalid(command: .svg, markdown) results.invalid(command: .svg, markdown)
return "" return ""
} }

View File

@ -17,82 +17,219 @@ extension ImageToGenerate: Hashable {
final class PageGenerationResults: ObservableObject { final class PageGenerationResults: ObservableObject {
@Published let itemId: ItemId
var linkedPages: Set<Page> = []
@Published private unowned let delegate: GenerationResults
var linkedTags: Set<Tag> = []
@Published /// The files that could not be accessed
var externalLinks: Set<String> = [] private(set) var inaccessibleFiles: Set<FileResource>
@Published /// The files that could not be parsed, with the error message produced
var files: Set<FileResource> = [] private(set) var unparsableFiles: [FileResource : Set<String>]
@Published /// The missing files directly used by this page, and the source of the file
var assets: Set<FileResource> = [] private(set) var missingFiles: [String: Set<String>]
@Published /// The missing files linked to from other files.
var imagesToGenerate: Set<ImageToGenerate> = [] private(set) var missingLinkedFiles: [String : Set<FileResource>]
@Published /// The missing tags linked to by this page, and the source of the link
var missingPages: Set<String> = [] private(set) var missingLinkedTags: [String : Set<String>]
@Published /// The missing pages linked to by this page, and the source of the link
var missingFiles: Set<String> = [] private(set) var missingLinkedPages: [String : Set<String>]
@Published /// The footer scripts or html to add to the end of the body
var missingTags: Set<String> = [] private(set) var requiredFooters: Set<String>
@Published /// The known header elements to include in the page
var invalidCommands: [(command: ShorthandMarkdownKey?, markdown: String)] = [] private(set) var requiredHeaders: Set<KnownHeaderElement>
@Published /// The known icons that need to be included as hidden SVGs
var requiredHeaders: Set<KnownHeaderElement> = [] private(set) var requiredIcons: Set<PageIcon>
@Published /// The pages linked to by the page
var requiredFooters: Set<String> = [] private(set) var linkedPages: Set<Page>
@Published /// The tags linked to by this page
var requiredIcons: Set<PageIcon> = [] private(set) var linkedTags: Set<Tag>
@Published /// The links to external content in this page
var issues: Set<PageContentAnomaly> = [] private(set) var externalLinks: Set<String>
func reset() { /// The files used by this page, but not necessarily required in the output folder
linkedPages = [] private(set) var usedFiles: Set<FileResource>
linkedTags = []
externalLinks = [] /// The files that need to be copied
files = [] private(set) var requiredFiles: Set<FileResource>
assets = []
imagesToGenerate = [] /// The image versions required for this page
missingPages = [] private(set) var imagesToGenerate: Set<ImageGenerationJob>
missingFiles = []
missingTags = [] private(set) var invalidCommands: [(command: ShorthandMarkdownKey?, markdown: String)] = []
invalidCommands = []
private(set) var warnings: Set<String>
/// The files that could not be saved to the output folder
private(set) var unsavedOutputFiles: [String: Set<ItemType>] = [:]
init(itemId: ItemId, delegate: GenerationResults) {
self.itemId = itemId
self.delegate = delegate
inaccessibleFiles = []
unparsableFiles = [:]
missingFiles = [:]
missingLinkedFiles = [:]
missingLinkedTags = [:]
missingLinkedPages = [:]
requiredHeaders = [] requiredHeaders = []
requiredFooters = [] requiredFooters = []
requiredIcons = [] requiredIcons = []
issues = [] linkedPages = []
linkedTags = []
externalLinks = []
usedFiles = []
requiredFiles = []
imagesToGenerate = []
invalidCommands = []
warnings = []
unsavedOutputFiles = [:]
}
private init(other: PageGenerationResults) {
self.itemId = other.itemId
self.delegate = other.delegate
inaccessibleFiles = other.inaccessibleFiles
unparsableFiles = other.unparsableFiles
missingFiles = other.missingFiles
missingLinkedFiles = other.missingLinkedFiles
missingLinkedTags = other.missingLinkedTags
missingLinkedPages = other.missingLinkedPages
requiredHeaders = other.requiredHeaders
requiredFooters = other.requiredFooters
requiredIcons = other.requiredIcons
linkedPages = other.linkedPages
linkedTags = other.linkedTags
externalLinks = other.externalLinks
usedFiles = other.usedFiles
requiredFiles = other.requiredFiles
imagesToGenerate = other.imagesToGenerate
invalidCommands = other.invalidCommands
warnings = other.warnings
unsavedOutputFiles = other.unsavedOutputFiles
}
func copy() -> PageGenerationResults {
.init(other: self)
}
// MARK: Adding entries
func inaccessibleContent(file: FileResource) {
inaccessibleFiles.insert(file)
delegate.inaccessibleContent(file: file)
} }
func invalid(command: ShorthandMarkdownKey?, _ markdown: Substring) { func invalid(command: ShorthandMarkdownKey?, _ markdown: Substring) {
invalidCommands.append((command, String(markdown))) let markdown = String(markdown)
issues.insert(.invalidCommand(command: command, markdown: String(markdown))) invalidCommands.append((command, markdown))
delegate.invalidCommand(markdown)
} }
func missing(page: String, markdown: Substring) { func missing(page: String, source: String) {
missingPages.insert(page) missingLinkedPages[page, default: []].insert(source)
issues.insert(.missingPage(page: page, markdown: String(markdown))) delegate.missing(page: page)
} }
func missing(tag: String, markdown: Substring) { func missing(tag: String, source: String) {
missingTags.insert(tag) missingLinkedTags[tag, default: []].insert(source)
issues.insert(.missingTag(tag: tag, markdown: String(markdown))) delegate.missing(tag: tag)
} }
func missing(file: String, markdown: Substring) { func missing(file: String, source: String) {
missingFiles.insert(file) missingFiles[file, default: []].insert(source)
issues.insert(.missingFile(file: file, markdown: String(markdown))) delegate.missing(file: file)
}
func requireImageSet(for image: FileResource, size: Int) {
let jobs = ImageGenerationJob.imageSet(for: image.id, maxWidth: size, maxHeight: size)
imagesToGenerate.formUnion(jobs)
used(file: image)
delegate.generate(jobs)
}
func invalidFormat(file: FileResource, error: String) {
unparsableFiles[file, default: []].insert(error)
delegate.unparsable(file: file)
}
func missing(file: String, containedIn sourceFile: FileResource) {
missingLinkedFiles[file, default: []].insert(sourceFile)
delegate.missing(file: file)
}
func used(file: FileResource) {
usedFiles.insert(file)
// TODO: Notify delegate
}
func require(file: FileResource) {
requiredFiles.insert(file)
usedFiles.insert(file)
delegate.require(file: file)
}
func require(files: [FileResource]) {
requiredFiles.formUnion(files)
usedFiles.formUnion(files)
delegate.require(files: files)
}
func require(footer: String) {
requiredFooters.insert(footer)
}
func require(header: KnownHeaderElement) {
requiredHeaders.insert(header)
}
func require(headers: KnownHeaderElement...) {
requiredHeaders.formUnion(headers)
}
func require(icon: PageIcon) {
requiredIcons.insert(icon)
}
func require(icons: PageIcon...) {
requiredIcons.formUnion(icons)
}
func require(icons: [PageIcon]) {
requiredIcons.formUnion(icons)
}
func linked(to page: Page) {
linkedPages.insert(page)
}
func linked(to tag: Tag) {
linkedTags.insert(tag)
}
func externalLink(to url: String) {
externalLinks.insert(url)
delegate.externalLink(url)
}
func warning(_ warning: String) {
warnings.insert(warning)
delegate.warning(warning)
}
func unsavedOutput(_ path: String, source: ItemType) {
unsavedOutputFiles[path, default: []].insert(source)
delegate.unsaved(path)
} }
} }

View File

@ -2,11 +2,8 @@ final class PageGenerator {
private let content: Content private let content: Content
private let imageGenerator: ImageGenerator init(content: Content) {
init(content: Content, imageGenerator: ImageGenerator) {
self.content = content self.content = content
self.imageGenerator = imageGenerator
} }
private func makeHeaders(requiredItems: Set<KnownHeaderElement>) -> Set<HeaderElement> { private func makeHeaders(requiredItems: Set<KnownHeaderElement>) -> Set<HeaderElement> {
@ -22,10 +19,10 @@ final class PageGenerator {
return result return result
} }
func generate(page: Page, language: ContentLanguage) -> (page: String, results: PageGenerationResults)? { func generate(page: Page, language: ContentLanguage, results: PageGenerationResults) -> String? {
let contentGenerator = PageContentParser( let contentGenerator = PageContentParser(
content: content, content: content,
language: language) language: language, results: results)
guard let rawPageContent = content.storage.pageContent(for: page.id, language: language) else { guard let rawPageContent = content.storage.pageContent(for: page.id, language: language) else {
return nil return nil
@ -33,8 +30,6 @@ final class PageGenerator {
let pageContent = contentGenerator.generatePage(from: rawPageContent) let pageContent = contentGenerator.generatePage(from: rawPageContent)
contentGenerator.requestImages(imageGenerator)
let localized = page.localized(in: language) let localized = page.localized(in: language)
let tags: [FeedEntryData.Tag] = page.tags.map { tag in let tags: [FeedEntryData.Tag] = page.tags.map { tag in
@ -42,8 +37,8 @@ final class PageGenerator {
url: content.absoluteUrlToTag(tag, language: language)) url: content.absoluteUrlToTag(tag, language: language))
} }
let headers = makeHeaders(requiredItems: contentGenerator.results.requiredHeaders) let headers = makeHeaders(requiredItems: results.requiredHeaders)
contentGenerator.results.assets.formUnion(headers.compactMap { $0.file }) results.require(files: headers.compactMap { $0.file })
let fullPage = ContentPage( let fullPage = ContentPage(
language: language, language: language,
@ -55,10 +50,10 @@ final class PageGenerator {
navigationBarLinks: content.navigationBar(in: language), navigationBarLinks: content.navigationBar(in: language),
pageContent: pageContent, pageContent: pageContent,
headers: headers, headers: headers,
footers: contentGenerator.results.requiredFooters.sorted(), footers: results.requiredFooters.sorted(),
icons: contentGenerator.results.requiredIcons) icons: results.requiredIcons)
.content .content
return (fullPage, contentGenerator.results) return fullPage
} }
} }

View File

@ -6,7 +6,7 @@ final class PostListPageGenerator {
private let content: Content private let content: Content
private let imageGenerator: ImageGenerator private let results: PageGenerationResults
private let showTitle: Bool private let showTitle: Bool
@ -17,28 +17,33 @@ final class PostListPageGenerator {
/// The url of the page, excluding the extension /// The url of the page, excluding the extension
private let pageUrlPrefix: String private let pageUrlPrefix: String
init(language: ContentLanguage, content: Content, imageGenerator: ImageGenerator, showTitle: Bool, pageTitle: String, pageDescription: String, pageUrlPrefix: String) { init(language: ContentLanguage,
content: Content,
results: PageGenerationResults,
showTitle: Bool, pageTitle: String,
pageDescription: String,
pageUrlPrefix: String) {
self.language = language self.language = language
self.content = content self.content = content
self.imageGenerator = imageGenerator self.results = results
self.showTitle = showTitle self.showTitle = showTitle
self.pageTitle = pageTitle self.pageTitle = pageTitle
self.pageDescription = pageDescription self.pageDescription = pageDescription
self.pageUrlPrefix = pageUrlPrefix self.pageUrlPrefix = pageUrlPrefix
} }
private var mainContentMaximumWidth: CGFloat { private var mainContentMaximumWidth: Int {
CGFloat(content.settings.posts.contentWidth) content.settings.posts.contentWidth
} }
private var postsPerPage: Int { private var postsPerPage: Int {
content.settings.posts.postsPerPage content.settings.posts.postsPerPage
} }
func createPages(for posts: [Post]) -> Bool { func createPages(for posts: [Post]) {
let totalCount = posts.count let totalCount = posts.count
guard totalCount > 0 else { guard totalCount > 0 else {
return true return
} }
let numberOfPages = (totalCount + postsPerPage - 1) / postsPerPage // Round up let numberOfPages = (totalCount + postsPerPage - 1) / postsPerPage // Round up
@ -46,14 +51,11 @@ final class PostListPageGenerator {
let startIndex = (pageIndex - 1) * postsPerPage let startIndex = (pageIndex - 1) * postsPerPage
let endIndex = min(pageIndex * postsPerPage, totalCount) let endIndex = min(pageIndex * postsPerPage, totalCount)
let postsOnPage = posts[startIndex..<endIndex] let postsOnPage = posts[startIndex..<endIndex]
guard createPostFeedPage(pageIndex, pageCount: numberOfPages, posts: postsOnPage) else { createPostFeedPage(pageIndex, pageCount: numberOfPages, posts: postsOnPage)
return false
} }
} }
return true
}
private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>) -> Bool { private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>) {
let posts: [FeedEntryData] = posts.map { post in let posts: [FeedEntryData] = posts.map { post in
let localized: LocalizedPost = post.localized(in: language) let localized: LocalizedPost = post.localized(in: language)
@ -68,6 +70,8 @@ final class PostListPageGenerator {
url: content.absoluteUrlToTag(tag, language: language)) url: content.absoluteUrlToTag(tag, language: language))
} }
let images = localized.images.map(createFeedImage)
return FeedEntryData( return FeedEntryData(
entryId: post.id, entryId: post.id,
title: localized.title, title: localized.title,
@ -75,7 +79,7 @@ final class PostListPageGenerator {
link: linkUrl, link: linkUrl,
tags: tags, tags: tags,
text: localized.text.components(separatedBy: "\n"), text: localized.text.components(separatedBy: "\n"),
images: localized.images.map(createImageSet)) images: images)
} }
let feedPageGenerator = FeedPageGenerator(content: content) let feedPageGenerator = FeedPageGenerator(content: content)
@ -88,23 +92,19 @@ final class PostListPageGenerator {
showTitle: showTitle, showTitle: showTitle,
pageNumber: pageIndex, pageNumber: pageIndex,
totalPages: pageCount) totalPages: pageCount)
let filePath = "\(pageUrlPrefix)/\(pageIndex).html"
if pageIndex == 1 { guard save(fileContent, to: filePath) else {
return save(fileContent, to: "\(pageUrlPrefix).html") results.unsavedOutput(filePath, source: .feed)
} else { return
return save(fileContent, to: "\(pageUrlPrefix)-\(pageIndex).html")
} }
} }
private func createImageSet(for image: FileResource) -> FeedEntryData.Image { private func createFeedImage(for image: FileResource) -> FeedEntryData.Image {
imageGenerator.generateImageSet( results.requireImageSet(for: image, size: mainContentMaximumWidth)
for: image.id,
maxWidth: mainContentMaximumWidth,
maxHeight: mainContentMaximumWidth)
return .init( return .init(
rawImagePath: image.absoluteUrl, rawImagePath: image.absoluteUrl,
width: Int(mainContentMaximumWidth), width: mainContentMaximumWidth,
height: Int(mainContentMaximumWidth), height: mainContentMaximumWidth,
altText: image.localized(in: language)) altText: image.localized(in: language))
} }

View File

@ -1,9 +1,12 @@
import SwiftUI import SwiftUI
import SFSafeSymbols import SFSafeSymbols
#warning("Fix podcast")
#warning("Fix CV")
#warning("Fix endeavor basics (image compare)")
#warning("Show all warnings on page content") #warning("Show all warnings on page content")
#warning("Button to delete file") #warning("Button to delete file")
#warning("Fix podcast")
#warning("Add link to other language") #warning("Add link to other language")
#warning("Transfer images of posts to other language") #warning("Transfer images of posts to other language")
#warning("Show tag selection view for pages") #warning("Show tag selection view for pages")
@ -17,6 +20,7 @@ import SFSafeSymbols
#warning("Clean up mock content") #warning("Clean up mock content")
#warning("Show posts linking to a page") #warning("Show posts linking to a page")
#warning("Add author to settings and page headers") #warning("Add author to settings and page headers")
#warning("Mark changed images for generation")
@main @main
struct MainView: App { struct MainView: App {

View File

@ -2,70 +2,97 @@ import Foundation
extension Content { extension Content {
func generateFeed() -> Bool { func generateWebsiteInAllLanguages() {
#warning("Implement feed generation") performGenerationIfIdle {
return false self.generatePagesInternal()
self.generatePostFeedPagesInternal()
self.generateTagPagesInternal()
self.generateTagOverviewPagesInternal()
self.copyRequiredFiles()
self.generateRequiredImages()
self.status("Generation completed")
}
} }
func generateAllPages() -> Bool { func generatePostFeedPages() {
guard startGenerating() else { return false } performGenerationIfIdle {
defer { endGenerating() } self.generatePostFeedPagesInternal()
}
}
for page in pages { func check(content: String, of page: Page, for language: ContentLanguage, onComplete: @escaping (PageGenerationResults) -> Void) {
performGenerationIfIdle {
let results = self.results.makeResults(for: page, in: language)
let generator = PageContentParser(content: page.content, language: language, results: results)
_ = generator.generatePage(from: content)
DispatchQueue.main.async {
onComplete(results)
}
}
}
private func copyRequiredFiles() {
let count = results.requiredFiles.count
var completed = 0
for file in results.requiredFiles {
defer {
completed += 1
status("Copying required files: \(completed) / \(count)")
}
guard !file.isExternallyStored else {
continue
}
let path = file.absoluteUrl
if !storage.copy(file: file.id, to: path) {
results.general.unsavedOutput(path, source: .general)
}
}
}
private func generateRequiredImages() {
let imageGenerator = ImageGenerator(
storage: storage,
settings: settings)
let images = results.imagesToGenerate.sorted()
let count = images.count
var completed = 0
for image in images {
defer {
completed += 1
status("Generating required images: \(completed) / \(count)")
}
if imageGenerator.generate(job: image) {
continue
}
results.failed(image: image)
}
//let images = Set(self.images.map { $0.id })
//imageGenerator.recalculateGeneratedImages(by: images)
}
func generateAllPages() {
performGenerationIfIdle {
self.generatePagesInternal()
}
}
func generatePage(_ page: Page) {
performGenerationIfIdle {
for language in ContentLanguage.allCases { for language in ContentLanguage.allCases {
guard generateInternal(page, in: language) else { self.generateInternal(page, in: language)
return false }
self.copyRequiredFiles()
self.generateRequiredImages()
} }
} }
func generatePage(_ page: Page, in language: ContentLanguage) {
performGenerationIfIdle {
self.generateInternal(page, in: language)
} }
let failedAssetCopies = results.values
.reduce(Set()) { $0.union($1.assets) }
.filter { !$0.isExternallyStored }
.filter { !storage.copy(file: $0.id, to: $0.assetUrl) }
let failedFileCopies = results.values
.reduce(Set()) { $0.union($1.files) }
.filter { !$0.isExternallyStored }
.filter { !storage.copy(file: $0.id, to: $0.absoluteUrl) }
guard imageGenerator.runJobs(callback: { _ in }) else {
return false
}
return true
}
func generatePage(_ page: Page) -> Bool {
guard startGenerating() else { return false }
defer { endGenerating() }
for language in ContentLanguage.allCases {
guard generateInternal(page, in: language) else {
return false
}
}
guard imageGenerator.runJobs(callback: { _ in }) else {
return false
}
let failedAssetCopies = results.values
.reduce(Set()) { $0.union($1.assets) }
.filter { !$0.isExternallyStored }
.filter { !storage.copy(file: $0.id, to: $0.assetUrl) }
let failedFileCopies = results.values
.reduce(Set()) { $0.union($1.files) }
.filter { !$0.isExternallyStored }
.filter { !storage.copy(file: $0.id, to: $0.absoluteUrl) }
return true
}
func generatePage(_ page: Page, in language: ContentLanguage) -> Bool {
guard startGenerating() else { return false }
defer { endGenerating() }
return generateInternal(page, in: language)
} }
// MARK: Paths to items // MARK: Paths to items
@ -121,60 +148,134 @@ extension Content {
return result return result
} }
// MARK: Images
func recalculateGeneratedImages() {
let images = Set(self.images.map { $0.id })
imageGenerator.recalculateGeneratedImages(by: images)
}
// MARK: Generation // MARK: Generation
private func startGenerating() -> Bool { private func performGenerationIfIdle(_ operation: @escaping () -> ()) {
guard !isGeneratingWebsite else {
return false
}
// TODO: Fix bug where multiple generating operations can be started
// due to dispatch of locking property on main queue
self.set(isGenerating: true)
return true
}
private func endGenerating() {
set(isGenerating: false)
}
private func generateInternal(_ page: Page, in language: ContentLanguage) -> Bool {
let pageGenerator = PageGenerator(
content: self,
imageGenerator: imageGenerator)
guard let (content, results) = pageGenerator.generate(page: page, language: language) else {
print("Failed to generate page \(page.id) in language \(language)")
return false
}
DispatchQueue.main.async { DispatchQueue.main.async {
let id = ItemId(itemId: page.id, language: language, itemType: .page) guard !self.isGeneratingWebsite else {
self.results[id] = results return
}
self.set(isGenerating: true)
DispatchQueue.global(qos: .userInitiated).async {
operation()
DispatchQueue.main.async {
self.set(isGenerating: false)
}
}
}
}
private func status(_ message: String) {
DispatchQueue.main.async {
self.generationStatus = message
}
}
/**
- Note: Run on background thread
*/
private func generatePagesInternal() {
let count = pages.count
for index in pages.indices {
let page = pages[index]
status("Generating pages: \(index) / \(count)")
guard !page.isExternalUrl else {
continue
}
for language in ContentLanguage.allCases {
guard page.hasContent(in: language) else {
continue
}
generateInternal(page, in: language)
}
}
}
/**
- Note: Run on background thread
*/
private func generatePostFeedPagesInternal() {
status("Generating post feed")
for language in ContentLanguage.allCases {
let results = results.makeResults(for: .feed, in: language)
let generator = PostListPageGenerator(
language: language,
content: self,
results: results,
showTitle: false,
pageTitle: settings.localized(in: language).title,
pageDescription: settings.localized(in: language).description,
pageUrlPrefix: settings.localized(in: language).feedUrlPrefix)
generator.createPages(for: posts)
}
}
/**
- Note: Run on background thread
*/
private func generateTagPagesInternal() {
let count = tags.count
for index in tags.indices {
let tag = tags[index]
status("Generating tag pages: \(index) / \(count)")
generatePagesInternal(for: tag)
}
}
/**
- Note: Run on background thread
*/
private func generatePagesInternal(for tag: Tag) {
for language in ContentLanguage.allCases {
let results = results.makeResults(for: tag, in: language)
let posts = posts.filter { $0.tags.contains(tag) }
guard posts.count > 0 else { continue }
let localized = tag.localized(in: language)
let urlPrefix = absoluteUrlPrefixForTag(tag, language: language)
let generator = PostListPageGenerator(
language: language,
content: self,
results: results,
showTitle: true,
pageTitle: localized.name,
pageDescription: localized.description ?? "",
pageUrlPrefix: urlPrefix)
generator.createPages(for: posts)
}
}
/**
- Note: Run on background thread
*/
private func generateTagOverviewPagesInternal() {
status("Generating tag overview page")
for language in ContentLanguage.allCases {
let results = results.makeResults(for: .tagOverview, in: language)
#warning("Create layout for tag overview page")
}
}
/**
- Note: Run on background thread
*/
private func generateInternal(_ page: Page, in language: ContentLanguage) {
let results = results.makeResults(for: page, in: language)
let pageGenerator = PageGenerator(content: self)
results.require(files: page.requiredFiles)
guard let content = pageGenerator.generate(page: page, language: language, results: results) else {
print("Failed to generate page \(page.id) in language \(language)")
return
} }
let path = page.absoluteUrl(in: language) + ".html" let path = page.absoluteUrl(in: language) + ".html"
guard storage.write(content, to: path) else { guard storage.write(content, to: path) else {
print("Failed to save page \(page.id)") print("Failed to save page \(page.id)")
return false return
}
return true
} }
} }
prefix operator ~>
prefix func ~> (operation: () throws -> Void) -> Bool {
do {
try operation()
return true
} catch {
return false
}
} }

View File

@ -32,9 +32,6 @@ extension Content {
title: page.title, title: page.title,
lastModified: page.lastModifiedDate, lastModified: page.lastModifiedDate,
originalUrl: page.originalURL, originalUrl: page.originalURL,
files: Set(page.files),
externalFiles: Set(page.externalFiles),
requiredFiles: Set(page.requiredFiles),
linkPreviewImage: page.linkPreviewImage.map { images[$0] }, linkPreviewImage: page.linkPreviewImage.map { images[$0] },
linkPreviewTitle: page.linkPreviewTitle, linkPreviewTitle: page.linkPreviewTitle,
linkPreviewDescription: page.linkPreviewDescription) linkPreviewDescription: page.linkPreviewDescription)
@ -115,14 +112,15 @@ extension Content {
english: convert(data.value.english, images: images)) english: convert(data.value.english, images: images))
} }
let pages: [String : Page] = loadPages(pagesData, tags: tags, images: images) let pages: [String : Page] = loadPages(pagesData, tags: tags, files: files)
let posts = postsData.map { postId, post in let posts: [String : Post] = postsData.reduce(into: [:]) { dict, data in
let (postId, post) = data
let linkedPage = post.linkedPageId.map { pages[$0] } let linkedPage = post.linkedPageId.map { pages[$0] }
let german = convert(post.german, images: images) let german = convert(post.german, images: images)
let english = convert(post.english, images: images) let english = convert(post.english, images: images)
return Post( dict[postId] = Post(
content: self, content: self,
id: postId, id: postId,
isDraft: post.isDraft, isDraft: post.isDraft,
@ -145,25 +143,36 @@ extension Content {
self.tags = tags.values.sorted() self.tags = tags.values.sorted()
self.pages = pages.values.sorted(ascending: false) { $0.startDate } self.pages = pages.values.sorted(ascending: false) { $0.startDate }
self.files = files.values.sorted { $0.id } self.files = files.values.sorted { $0.id }
self.posts = posts.sorted(ascending: false) { $0.startDate } self.posts = posts.values.sorted(ascending: false) { $0.startDate }
self.tagOverview = tagOverview self.tagOverview = tagOverview
self.settings = makeSettings(settings, tags: tags, pages: pages, files: files) self.settings = makeSettings(settings, tags: tags, pages: pages, files: files, posts: posts)
print("Content loaded") print("Content loaded")
} }
private func makeSettings(_ settings: SettingsFile, tags: [String : Tag], pages: [String : Page], files: [String : FileResource]) -> Settings { private func makeSettings(_ settings: SettingsFile,
tags: [String : Tag],
pages: [String : Page],
files: [String : FileResource],
posts: [String : Post]) -> Settings {
#warning("Notify about missing links") #warning("Notify about missing links")
let navigationItems: [Item] = settings.navigationItems.compactMap { let navigationItems: [Item] = settings.navigationItems.compactMap { raw in
switch $0.type { guard let type = ItemType(rawValue: raw, posts: posts, pages: pages, tags: tags) else {
case .tag: return nil
return tags[$0.id] }
case .page: switch type {
return pages[$0.id] case .general:
return nil
case .post(let post):
return post
case .feed:
return nil // TODO: Provide feed object
case .page(let page):
return page
case .tagPage(let tag):
return tag
case .tagOverview: case .tagOverview:
return tagOverview return tagOverview
default:
return nil
} }
} }
@ -182,7 +191,7 @@ extension Content {
english: .init(file: settings.english)) english: .init(file: settings.english))
} }
private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], images: [String : FileResource]) -> [String : Page] { private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], files: [String : FileResource]) -> [String : Page] {
pagesData.reduce(into: [:]) { pages, data in pagesData.reduce(into: [:]) { pages, data in
let (pageId, page) = data let (pageId, page) = data
pages[pageId] = Page( pages[pageId] = Page(
@ -193,9 +202,10 @@ extension Content {
createdDate: page.createdDate, createdDate: page.createdDate,
startDate: page.startDate, startDate: page.startDate,
endDate: page.endDate, endDate: page.endDate,
german: convert(page.german, images: images), german: convert(page.german, images: files),
english: convert(page.english, images: images), english: convert(page.english, images: files),
tags: page.tags.map { tags[$0]! }) tags: page.tags.map { tags[$0]! },
requiredFiles: page.requiredFiles?.map { files[$0]! } ?? [])
} }
} }

View File

@ -63,7 +63,8 @@ private extension Page {
startDate: startDate, startDate: startDate,
endDate: hasEndDate ? endDate : nil, endDate: hasEndDate ? endDate : nil,
german: german.pageFile, german: german.pageFile,
english: english.pageFile) english: english.pageFile,
requiredFiles: requiredFiles.nonEmpty?.map { $0.id }.sorted())
} }
} }
@ -71,9 +72,6 @@ private extension LocalizedPage {
var pageFile: LocalizedPageFile { var pageFile: LocalizedPageFile {
.init(url: urlString, .init(url: urlString,
files: files.sorted(),
externalFiles: externalFiles.sorted(),
requiredFiles: requiredFiles.sorted(),
title: title, title: title,
linkPreviewImage: linkPreviewImage?.id, linkPreviewImage: linkPreviewImage?.id,
linkPreviewTitle: linkPreviewTitle, linkPreviewTitle: linkPreviewTitle,
@ -140,7 +138,7 @@ extension Settings {
var file: SettingsFile { var file: SettingsFile {
.init( .init(
paths: paths.file, paths: paths.file,
navigationItems: navigationItems.map { .init(type: $0.itemType, id: $0.id) }, navigationItems: navigationItems.map { $0.itemType.id },
posts: posts.file, posts: posts.file,
pages: pages.file, pages: pages.file,
german: german.file, german: german.file,

View File

@ -26,13 +26,14 @@ final class Content: ObservableObject {
var tagOverview: TagOverviewPage? var tagOverview: TagOverviewPage?
@Published @Published
var results: [ItemId : PageGenerationResults] var results: GenerationResults
@Published
var generationStatus: String = "Ready to generate"
@Published @Published
private(set) var isGeneratingWebsite = false private(set) var isGeneratingWebsite = false
let imageGenerator: ImageGenerator
init(settings: Settings, init(settings: Settings,
posts: [Post], posts: [Post],
pages: [Page], pages: [Page],
@ -45,13 +46,10 @@ final class Content: ObservableObject {
self.tags = tags self.tags = tags
self.files = files self.files = files
self.tagOverview = tagOverview self.tagOverview = tagOverview
self.results = [:] self.results = .init()
let storage = Storage() let storage = Storage()
self.storage = storage self.storage = storage
self.imageGenerator = ImageGenerator(
storage: storage,
settings: settings)
} }
init() { init() {
@ -62,13 +60,10 @@ final class Content: ObservableObject {
self.tags = [] self.tags = []
self.files = [] self.files = []
self.tagOverview = nil self.tagOverview = nil
self.results = [:] self.results = .init()
let storage = Storage() let storage = Storage()
self.storage = storage self.storage = storage
self.imageGenerator = ImageGenerator(
storage: storage,
settings: settings)
} }
private func clear() { private func clear() {
@ -78,7 +73,7 @@ final class Content: ObservableObject {
self.tags = [] self.tags = []
self.files = [] self.files = []
self.tagOverview = nil self.tagOverview = nil
self.results = [:] self.results = .init()
} }
var images: [FileResource] { var images: [FileResource] {
@ -86,10 +81,8 @@ final class Content: ObservableObject {
} }
func set(isGenerating: Bool) { func set(isGenerating: Bool) {
DispatchQueue.main.async {
self.isGeneratingWebsite = isGenerating self.isGeneratingWebsite = isGenerating
} }
}
func add(_ file: FileResource) { func add(_ file: FileResource) {
// TODO: Insert at correct index? // TODO: Insert at correct index?

View File

@ -28,8 +28,8 @@ final class FileResource: Item {
/** /**
Only for bundle images Only for bundle images
*/ */
init(resourceImage: String, type: ImageFileType) { init(resourceImage: String, type: FileType) {
self.type = .image(type) self.type = type
self.english = "A test image included in the bundle" self.english = "A test image included in the bundle"
self.german = "Ein Testbild aus dem Bundle" self.german = "Ein Testbild aus dem Bundle"
self.isExternallyStored = true self.isExternallyStored = true
@ -87,18 +87,20 @@ final class FileResource: Item {
return makeCleanAbsolutePath(path) return makeCleanAbsolutePath(path)
} }
var assetUrl: String {
let path = content.settings.paths.assetsOutputFolderPath + "/" + id
return makeCleanAbsolutePath(path)
}
private var pathPrefix: String { private var pathPrefix: String {
switch type { if type.isImage {
case .image: return content.settings.paths.imagesOutputFolderPath return content.settings.paths.imagesOutputFolderPath
case .video: return content.settings.paths.videosOutputFolderPath
default: return content.settings.paths.filesOutputFolderPath
} }
if type.isVideo {
return content.settings.paths.videosOutputFolderPath
}
if type.isAudio {
}
if type.isAsset {
return content.settings.paths.assetsOutputFolderPath
}
return content.settings.paths.filesOutputFolderPath
} }
// MARK: File // MARK: File

View File

@ -0,0 +1,240 @@
import Foundation
enum FileTypeCategory: String, CaseIterable {
case image
case code
case model
case text
case video
case resource
case asset
case audio
var text: String {
switch self {
case .image: return "Images"
case .code: return "Code"
case .model: return "Models"
case .text: return "Text"
case .video: return "Videos"
case .asset: return "Assets"
case .resource: return "Other"
case .audio: return "Audio"
}
}
}
extension FileTypeCategory: Hashable {
}
extension FileTypeCategory: Identifiable {
var id: String {
rawValue
}
}
enum FileType: String {
// MARK: Images
case jpg
case png
case avif
case webp
case gif
case svg
case tiff
// MARK: Code
case html
case cpp
case swift
// MARK: Assets
case css
case js
// MARK: Text
case json
case conf
case yaml
// MARK: Model
case stl
case f3d
case step
case glb
case f3z
// MARK: Video
case mp4
case m4v
case webm
// MARK: Audio
case mp3
case aac
// MARK: Other
case noExtension
case zip
case cddx
case pdf
case key
case psd
// MARK: Unknown
case unknown
init(fileExtension: String?) {
guard let lower = fileExtension?.lowercased() else {
self = .noExtension
return
}
if lower == "jpeg" {
self = .jpg
return
}
guard let type = FileType(rawValue: lower) else {
self = .unknown
return
}
self = type
}
var category: FileTypeCategory {
switch self {
case .jpg, .png, .avif, .webp, .gif, .svg, .tiff:
return .image
case .mp4, .m4v, .webm:
return .video
case .mp3, .aac:
return .audio
case .js, .css:
return .asset
case .json, .conf, .yaml:
return .text
case .html, .cpp, .swift:
return .code
case .stl, .f3d, .step, .glb, .f3z:
return .model
case .zip, .cddx, .pdf, .key, .psd:
return .resource
case .noExtension, .unknown:
return .resource
}
}
var fileExtension: String {
switch self {
case .noExtension, .unknown: return ""
default:
return rawValue
}
}
var isImage: Bool {
switch self {
case .jpg, .png, .avif, .webp, .gif, .svg, .tiff:
return true
default:
return false
}
}
var isVideo: Bool {
switch self {
case .mp4, .m4v, .webm:
return true
default:
return false
}
}
var isAudio: Bool {
switch self {
case .mp3, .aac:
return true
default:
return false
}
}
var isAsset: Bool {
switch self {
case .js, .css:
return true
default:
return false
}
}
var isTextFile: Bool {
switch self {
case .html, .cpp, .swift, .css, .js, .json, .conf, .yaml:
return true
default:
return false
}
}
var isOtherFile: Bool {
switch self {
case .noExtension, .zip, .cddx, .pdf, .key, .psd:
return true
default:
return false
}
}
var htmlType: String? {
switch self {
case .mp4, .m4v:
return "video/mp4"
case .webm:
return "video/webm"
default:
return nil
}
}
var isSvg: Bool {
guard case .svg = self else {
return false
}
return true
}
}

View File

@ -1,8 +1,6 @@
struct ItemId { struct ItemId {
let itemId: String
let language: ContentLanguage let language: ContentLanguage
let itemType: ItemType let itemType: ItemType
@ -11,16 +9,16 @@ struct ItemId {
extension ItemId: Equatable { extension ItemId: Equatable {
static func == (lhs: ItemId, rhs: ItemId) -> Bool { static func == (lhs: ItemId, rhs: ItemId) -> Bool {
lhs.itemId == rhs.itemId && lhs.language == rhs.language && lhs.itemType == rhs.itemType lhs.language == rhs.language &&
lhs.itemType == rhs.itemType
} }
} }
extension ItemId: Hashable { extension ItemId: Hashable {
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
hasher.combine(itemId)
hasher.combine(language) hasher.combine(language)
hasher.combine(itemType) hasher.combine(itemType.id)
} }
} }
@ -30,9 +28,6 @@ extension ItemId: Comparable {
guard lhs.itemType == rhs.itemType else { guard lhs.itemType == rhs.itemType else {
return lhs.itemType < rhs.itemType return lhs.itemType < rhs.itemType
} }
guard lhs.itemId == rhs.itemId else {
return lhs.itemId < rhs.itemId
}
return lhs.language < rhs.language return lhs.language < rhs.language
} }
} }

View File

@ -1,15 +1,17 @@
enum ItemType: String, Codable { enum ItemType {
case post case general
case tag case post(Post)
case page case feed
case page(Page)
case tagPage(Tag)
case tagOverview case tagOverview
case file
} }
extension ItemType: Equatable { extension ItemType: Equatable {
@ -23,13 +25,52 @@ extension ItemType: Hashable {
extension ItemType: Identifiable { extension ItemType: Identifiable {
var id: String { var id: String {
rawValue switch self {
case .general:
return "0-general"
case .feed:
return "1-feed"
case .post(let post):
return "2-post-\(post.id)"
case .page(let page):
return "3-page-\(page.id)"
case .tagPage(let tag):
return "5-tag-\(tag.id)"
case .tagOverview:
return "4-tag-overview"
}
}
init?(rawValue: String, posts: [String : Post], pages: [String : Page], tags: [String : Tag]) {
if rawValue == "0-general" {
self = .general
} else if rawValue == "1-feed" {
self = .feed
} else if rawValue == "4-tag-overview" {
self = .tagOverview
} else if let id = rawValue.removingPrefix("3-page-"), let page = pages[id] {
self = .page(page)
} else if let id = rawValue.removingPrefix("2-post-"), let post = posts[id] {
self = .post(post)
} else if let id = rawValue.removingPrefix("5-tag-"), let tag = tags[id] {
self = .tagPage(tag)
} else {
return nil
}
} }
} }
extension ItemType: Comparable { extension ItemType: Comparable {
static func < (lhs: ItemType, rhs: ItemType) -> Bool { static func < (lhs: ItemType, rhs: ItemType) -> Bool {
lhs.rawValue < rhs.rawValue lhs.id < rhs.id
}
}
extension String {
func removingPrefix(_ prefix: String) -> String? {
guard self.hasPrefix(prefix) else { return nil }
return String(self.dropFirst(prefix.count))
} }
} }

View File

@ -34,29 +34,6 @@ final class LocalizedPage: ObservableObject {
*/ */
let originalUrl: String? let originalUrl: String?
/**
All files which occur in the content and are stored.
- Note: This property defaults to an empty set.
*/
@Published
var files: Set<String> = []
/**
All files which may occur in the content but are stored externally.
Missing files which would otherwise produce a warning are ignored when included here.
- Note: This property defaults to an empty set.
*/
@Published
var externalFiles: Set<String> = []
/**
Specifies additional files which should be copied to the destination when generating the content.
- Note: This property defaults to an empty set.
*/
@Published
var requiredFiles: Set<String> = []
@Published @Published
var linkPreviewImage: FileResource? var linkPreviewImage: FileResource?
@ -71,9 +48,6 @@ final class LocalizedPage: ObservableObject {
title: String, title: String,
lastModified: Date? = nil, lastModified: Date? = nil,
originalUrl: String? = nil, originalUrl: String? = nil,
files: Set<String> = [],
externalFiles: Set<String> = [],
requiredFiles: Set<String> = [],
linkPreviewImage: FileResource? = nil, linkPreviewImage: FileResource? = nil,
linkPreviewTitle: String? = nil, linkPreviewTitle: String? = nil,
linkPreviewDescription: String? = nil) { linkPreviewDescription: String? = nil) {
@ -82,9 +56,6 @@ final class LocalizedPage: ObservableObject {
self.title = title self.title = title
self.lastModified = lastModified self.lastModified = lastModified
self.originalUrl = originalUrl self.originalUrl = originalUrl
self.files = files
self.externalFiles = externalFiles
self.requiredFiles = requiredFiles
self.linkPreviewImage = linkPreviewImage self.linkPreviewImage = linkPreviewImage
self.linkPreviewTitle = linkPreviewTitle self.linkPreviewTitle = linkPreviewTitle
self.linkPreviewDescription = linkPreviewDescription self.linkPreviewDescription = linkPreviewDescription

View File

@ -36,12 +36,10 @@ final class Page: Item {
var tags: [Tag] var tags: [Tag]
/** /**
Additional images required by the element. Additional files to copy, because the page content references them
These images are specified as: `source_name destination_name width (height)`.
*/ */
@Published @Published
var images: Set<String> = [] var requiredFiles: [FileResource]
init(content: Content, init(content: Content,
id: String, id: String,
@ -52,7 +50,8 @@ final class Page: Item {
endDate: Date?, endDate: Date?,
german: LocalizedPage, german: LocalizedPage,
english: LocalizedPage, english: LocalizedPage,
tags: [Tag]) { tags: [Tag],
requiredFiles: [FileResource]) {
self.externalLink = externalLink self.externalLink = externalLink
self.isDraft = isDraft self.isDraft = isDraft
self.createdDate = createdDate self.createdDate = createdDate
@ -62,6 +61,7 @@ final class Page: Item {
self.german = german self.german = german
self.english = english self.english = english
self.tags = tags self.tags = tags
self.requiredFiles = requiredFiles
super.init(content: content, id: id) super.init(content: content, id: id)
} }
@ -109,12 +109,20 @@ final class Page: Item {
} }
override var itemType: ItemType { override var itemType: ItemType {
.page .page(self)
} }
func contains(urlComponent: String) -> Bool { func contains(urlComponent: String) -> Bool {
english.urlString == urlComponent || german.urlString == urlComponent english.urlString == urlComponent || german.urlString == urlComponent
} }
func pageContent(in language: ContentLanguage) -> String? {
content.storage.pageContent(for: id, language: language)
}
func hasContent(in language: ContentLanguage) -> Bool {
content.storage.hasPageContent(for: id, language: language)
}
} }
extension Page: DateItem { extension Page: DateItem {

View File

@ -1,11 +1,6 @@
import Foundation import Foundation
final class Post: ObservableObject { final class Post: Item {
unowned let content: Content
@Published
var id: String
@Published @Published
var isDraft: Bool var isDraft: Bool
@ -45,8 +40,6 @@ final class Post: ObservableObject {
german: LocalizedPost, german: LocalizedPost,
english: LocalizedPost, english: LocalizedPost,
linkedPage: Page? = nil) { linkedPage: Page? = nil) {
self.content = content
self.id = id
self.isDraft = isDraft self.isDraft = isDraft
self.createdDate = createdDate self.createdDate = createdDate
self.startDate = startDate self.startDate = startDate
@ -56,6 +49,7 @@ final class Post: ObservableObject {
self.german = german self.german = german
self.english = english self.english = english
self.linkedPage = linkedPage self.linkedPage = linkedPage
super.init(content: content, id: id)
} }
func localized(in language: ContentLanguage) -> LocalizedPost { func localized(in language: ContentLanguage) -> LocalizedPost {
@ -82,24 +76,6 @@ final class Post: ObservableObject {
} }
} }
extension Post: Identifiable {
}
extension Post: Equatable {
static func == (lhs: Post, rhs: Post) -> Bool {
lhs.id == rhs.id
}
}
extension Post: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
extension Post: DateItem { extension Post: DateItem {
} }

View File

@ -53,7 +53,7 @@ final class Tag: Item {
} }
override var itemType: ItemType { override var itemType: ItemType {
.tag .tagPage(self)
} }
func contains(urlComponent: String) -> Bool { func contains(urlComponent: String) -> Bool {

View File

@ -1,21 +0,0 @@
enum CodeFileType: String {
case html
case css
case js
case cpp
case swift
init?(fileExtension: String) {
self.init(rawValue: fileExtension)
}
var fileExtension: String {
rawValue
}
}

View File

@ -1,116 +0,0 @@
import Foundation
enum FileTypeCategory: String, CaseIterable {
case image
case code
case model
case text
case video
case resource
var text: String {
switch self {
case .image: return "Images"
case .code: return "Code"
case .model: return "Models"
case .text: return "Text"
case .video: return "Videos"
case .resource: return "Other"
}
}
}
extension FileTypeCategory: Hashable {
}
extension FileTypeCategory: Identifiable {
var id: String {
rawValue
}
}
enum FileType {
case image(ImageFileType)
case code(CodeFileType)
case model(ModelFileType)
case text(TextFileType)
case video(VideoFileType)
case other(ResourceFileType)
init(fileExtension: String?) {
let ext = fileExtension?.lowercased() ?? ""
if let image = ImageFileType(fileExtension: ext) {
self = .image(image)
} else if let code = CodeFileType(fileExtension: ext) {
self = .code(code)
} else if let model = ModelFileType(fileExtension: ext) {
self = .model(model)
} else if let text = TextFileType(fileExtension: ext) {
self = .text(text)
} else if let video = VideoFileType(fileExtension: ext) {
self = .video(video)
} else {
let resource = ResourceFileType(fileExtension: ext)
self = .other(resource)
}
}
var fileExtension: String {
switch self {
case .image(let type): return type.fileExtension
case .code(let type): return type.fileExtension
case .model(let type): return type.fileExtension
case .text(let type): return type.fileExtension
case .video(let type): return type.fileExtension
case .other(let type): return type.fileExtension
}
}
var isImage: Bool {
if case .image = self {
return true
}
return false
}
var isVideo: Bool {
if case .video = self {
return true
}
return false
}
var isTextFile: Bool {
switch self {
case .code, .text: return true
default: return false
}
}
var isOtherFile: Bool {
switch self {
case .model, .other: return true
default: return false
}
}
var videoType: VideoFileType? {
if case .video(let videoType) = self {
return videoType
}
return nil
}
var isSvg: Bool {
guard case .image(let imageFileType) = self else {
return false
}
guard case .svg = imageFileType else {
return false
}
return true
}
}

View File

@ -1,41 +0,0 @@
import Foundation
import AppKit
enum ImageFileType: String {
case jpg
case png
case avif
case webp
case gif
case svg
case tiff
init?(fileExtension: String) {
if fileExtension == "jpeg" {
self = .jpg
return
}
self.init(rawValue: fileExtension)
}
var fileExtension: String {
rawValue
}
var fileType: NSBitmapImageRep.FileType? {
switch self {
case .jpg:
return .jpeg
case .png, .avif, .webp:
return .png
case .gif: return .gif
case .tiff: return .tiff
case .svg: return nil
}
}
}
extension ImageFileType: CaseIterable {
}

View File

@ -1,21 +0,0 @@
enum ModelFileType: String {
case stl
case f3d
case step
case glb
case f3z
init?(fileExtension: String) {
self.init(rawValue: fileExtension)
}
var fileExtension: String {
rawValue
}
}

View File

@ -1,54 +0,0 @@
enum ResourceFileType {
case noExtension
case zip
case cddx
case mp3
case pdf
case key
case psd
case other(String)
init(fileExtension: String) {
switch fileExtension {
case "": self = .noExtension
case "zip": self = .zip
case "cddx": self = .cddx
case "mp3": self = .mp3
case "pdf": self = .pdf
case "key": self = .key
case "psd": self = .psd
default:
self = .other(fileExtension)
}
}
var fileExtension: String {
switch self {
case .noExtension:
return ""
case .zip:
return "zip"
case .cddx:
return "cddx"
case .mp3:
return "mp3"
case .pdf:
return "pdf"
case .key:
return "key"
case .psd:
return "psd"
case .other(let ext):
return ext
}
}
}

View File

@ -1,18 +0,0 @@
enum TextFileType: String {
case json
case conf
case yaml
init?(fileExtension: String) {
self.init(rawValue: fileExtension)
}
var fileExtension: String {
rawValue
}
}

View File

@ -1,30 +0,0 @@
enum VideoFileType: String {
case mp4
case m4v
case webm
init?(fileExtension: String) {
self.init(rawValue: fileExtension)
}
var fileExtension: String {
rawValue
}
var htmlType: String {
switch self {
case .mp4, .m4v:
return "video/mp4"
case .webm:
return "video/webm"
}
}
}
extension VideoFileType: CaseIterable {
}

View File

@ -13,7 +13,8 @@ extension Page {
endDate: nil, endDate: nil,
german: .german, german: .german,
english: .english, english: .english,
tags: [.mock]) tags: [.mock],
requiredFiles: [])
} }
} }
@ -24,18 +25,12 @@ extension LocalizedPage {
urlString: "my-project", urlString: "my-project",
title: "My First Project", title: "My First Project",
lastModified: nil, lastModified: nil,
originalUrl: "projects/electronics/my-first-project/en.html", originalUrl: "projects/electronics/my-first-project/en.html")
files: [],
externalFiles: [],
requiredFiles: [])
static let german = LocalizedPage( static let german = LocalizedPage(
content: .mock, content: .mock,
urlString: "mein-projekt", urlString: "mein-projekt",
title: "Mein Erstes Projekt", title: "Mein Erstes Projekt",
lastModified: nil, lastModified: nil,
originalUrl: "projects/electronics/my-first-project/de.html", originalUrl: "projects/electronics/my-first-project/de.html")
files: [],
externalFiles: [],
requiredFiles: [])
} }

View File

@ -1,25 +0,0 @@
import Foundation
struct FileOnDisk {
let type: FileType
let url: URL
let name: String
init(image: String, url: URL) {
let ext = image.fileExtension!
let type = ImageFileType(fileExtension: ext)!
self.type = .image(type)
self.url = url
self.name = image
}
init(type: FileType, url: URL, name: String) {
self.type = type
self.url = url
self.name = name
}
}

View File

@ -17,6 +17,12 @@ struct PageFile {
let german: LocalizedPageFile let german: LocalizedPageFile
let english: LocalizedPageFile let english: LocalizedPageFile
/**
Specifies additional files which should be copied to the destination when generating the content.
- Note: This property defaults to an empty set.
*/
let requiredFiles: [String]?
} }
extension PageFile: Codable { extension PageFile: Codable {
@ -30,22 +36,6 @@ struct LocalizedPageFile {
let url: String let url: String
/**
The files (images, videos, other files) used in the page.
*/
let files: [String]
/**
The additional files required for the page to function correctly, but which are not stored with the content.
*/
let externalFiles: [String]
/**
Specifies additional files which should be copied to the destination when generating the content.
- Note: This property defaults to an empty set.
*/
let requiredFiles: [String]
let title: String let title: String
let linkPreviewImage: String? let linkPreviewImage: String?

View File

@ -1,11 +0,0 @@
import Foundation
struct PageOnDisk {
let page: PageFile
let deContentUrl: URL
let enContentUrl: URL
}

View File

@ -1,18 +1,11 @@
import Foundation import Foundation
struct NavigationItemReference: Codable {
let type: ItemType
let id: String
}
struct SettingsFile { struct SettingsFile {
let paths: PathSettingsFile let paths: PathSettingsFile
/// The tags to show in the navigation bar /// The tags to show in the navigation bar
let navigationItems: [NavigationItemReference] let navigationItems: [String]
let posts: PostSettingsFile let posts: PostSettingsFile

View File

@ -95,6 +95,12 @@ final class Storage: ObservableObject {
return contentScope.readString(at: path) return contentScope.readString(at: path)
} }
func hasPageContent(for pageId: String, language: ContentLanguage) -> Bool {
guard let contentScope else { return false }
let path = pageContentPath(page: pageId, language: language)
return contentScope.hasFile(at: path)
}
/** /**
Delete all files associated with pages that are not in the given set Delete all files associated with pages that are not in the given set
- Note: This function requires a security scope for the content path - Note: This function requires a security scope for the content path

View File

@ -24,7 +24,7 @@ struct FileContentView: View {
} }
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} else { } else {
switch file.type { switch file.type.category {
case .image: case .image:
file.imageToDisplay file.imageToDisplay
.resizable() .resizable()
@ -39,7 +39,7 @@ struct FileContentView: View {
.font(.title) .font(.title)
} }
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
case .text, .code: case .text, .code, .asset:
TextFileContentView(file: file) TextFileContentView(file: file)
.id(file.id) .id(file.id)
case .video: case .video:
@ -52,7 +52,7 @@ struct FileContentView: View {
.font(.title) .font(.title)
} }
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
case .other: case .resource:
VStack { VStack {
Image(systemSymbol: .docQuestionmark) Image(systemSymbol: .docQuestionmark)
.resizable() .resizable()
@ -62,6 +62,16 @@ struct FileContentView: View {
.font(.title) .font(.title)
} }
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
case .audio:
VStack {
Image(systemSymbol: .waveformPath)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: iconSize)
Text("No preview available")
.font(.title)
}
.foregroundStyle(.secondary)
} }
} }
}.padding() }.padding()

View File

@ -0,0 +1,130 @@
import SwiftUI
struct MultiFileSelectionView: View {
@Environment(\.dismiss)
private var dismiss
@EnvironmentObject
private var content: Content
@Binding
private var selectedFiles: [FileResource]
let allowedType: FileFilterType?
let insertSorted: Bool
@State
private var selectedFileType: FileFilterType
@State
private var searchString = ""
@State
private var newSelection: [FileResource]
init(selectedFiles: Binding<[FileResource]>, allowedType: FileFilterType? = nil, insertSorted: Bool = false) {
self._selectedFiles = selectedFiles
self.newSelection = selectedFiles.wrappedValue
self.allowedType = allowedType
self.selectedFileType = allowedType ?? .images
self.insertSorted = insertSorted
}
private var filesBySelectedType: [FileResource] {
content.files.filter { selectedFileType.matches($0.type) }
}
private var filteredFiles: [FileResource] {
guard !searchString.isEmpty else {
return filesBySelectedType
}
return filesBySelectedType.filter { $0.id.contains(searchString) }
}
var body: some View {
HStack {
VStack {
Text("Selected files")
.font(.title)
List {
ForEach(newSelection) { file in
HStack {
Image(systemSymbol: .minusCircleFill)
.foregroundStyle(.red)
.contentShape(Rectangle())
.onTapGesture { deselect(file: file) }
Text(file.id)
Spacer()
}
}
.onMove(perform: moveSelectedFile)
}
HStack {
Button("Cancel") {
DispatchQueue.main.async {
dismiss()
}
}
Button("Save") {
selectedFiles = newSelection
dismiss()
}
}
}
VStack {
Picker("", selection: $selectedFileType) {
ForEach(FileFilterType.allCases) { type in
Text(type.text).tag(type)
}
}
.pickerStyle(.segmented)
.padding(.trailing, 7)
.disabled(allowedType != nil)
TextField("", text: $searchString, prompt: Text("Search"))
.textFieldStyle(.roundedBorder)
.padding(.horizontal, 8)
List(filteredFiles) { file in
HStack {
if newSelection.contains(file) {
Image(systemSymbol: .checkmarkCircleFill)
.foregroundStyle(.gray)
} else {
Image(systemSymbol: .plusCircleFill)
.foregroundStyle(.green)
}
Text(file.id)
Spacer()
}
.contentShape(Rectangle())
.onTapGesture { select(file: file) }
}
}
}
.frame(minHeight: 500, idealHeight: 600)
.padding()
}
private func deselect(file: FileResource) {
guard let index = newSelection.firstIndex(of: file) else {
return
}
newSelection.remove(at: index)
}
private func select(file: FileResource) {
guard !newSelection.contains(file) else {
return
}
guard insertSorted else {
newSelection.append(file)
return
}
newSelection.insertSorted(file)
}
private func moveSelectedFile(from source: IndexSet, to destination: Int) {
newSelection.move(fromOffsets: source, toOffset: destination)
}
}

View File

@ -30,7 +30,7 @@ struct IdPropertyView: View {
} }
private var isValid: Bool { private var isValid: Bool {
validation(id) validation(newId)
} }
var body: some View { var body: some View {

View File

@ -80,7 +80,8 @@ struct AddPageView: View {
english: .init(content: content, english: .init(content: content,
urlString: "page", urlString: "page",
title: "A Title"), title: "A Title"),
tags: []) tags: [],
requiredFiles: [])
content.add(page) content.add(page)
selectedPage = page selectedPage = page
dismissSheet() dismissSheet()

View File

@ -4,6 +4,9 @@ import HighlightedTextEditor
struct LocalizedPageContentView: View { struct LocalizedPageContentView: View {
@EnvironmentObject
var content: Content
let pageId: String let pageId: String
let language: ContentLanguage let language: ContentLanguage
@ -11,9 +14,6 @@ struct LocalizedPageContentView: View {
@ObservedObject @ObservedObject
var page: LocalizedPage var page: LocalizedPage
@State
private var isGeneratingWebsite = false
@State @State
private var pageContent: String = "" private var pageContent: String = ""
@ -21,7 +21,7 @@ struct LocalizedPageContentView: View {
private var pageContentUsedForGeneration: String = "" private var pageContentUsedForGeneration: String = ""
@State @State
private var generationResults = PageGenerationResults() private var generationResults: PageGenerationResults?
@State @State
private var didChangeContent = false private var didChangeContent = false
@ -47,10 +47,16 @@ struct LocalizedPageContentView: View {
} }
Button(action: checkContent) { Button(action: checkContent) {
Text("Check") Text("Check")
}.disabled(content.isGeneratingWebsite)
if content.isGeneratingWebsite {
ProgressView()
.frame(height: 15)
} }
Spacer() Spacer()
} }
if let generationResults {
PageContentResultsView(results: generationResults) PageContentResultsView(results: generationResults)
}
HighlightedTextEditor( HighlightedTextEditor(
text: $pageContent, text: $pageContent,
highlightRules: .markdown) highlightRules: .markdown)
@ -65,9 +71,19 @@ struct LocalizedPageContentView: View {
private func loadContent() { private func loadContent() {
let language = language let language = language
guard page.content.storage.hasPageContent(for: pageId, language: language) else {
pageContent = "New file"
DispatchQueue.main.async {
didChangeContent = false
}
return
}
guard let content = page.content.storage.pageContent(for: pageId, language: language) else { guard let content = page.content.storage.pageContent(for: pageId, language: language) else {
print("Failed to load page content") print("Failed to load page content")
pageContent = "Failed to load" pageContent = "Failed to load"
DispatchQueue.main.async {
didChangeContent = false
}
return return
} }
guard content != "" else { guard content != "" else {
@ -105,15 +121,14 @@ struct LocalizedPageContentView: View {
guard content != pageContentUsedForGeneration else { guard content != pageContentUsedForGeneration else {
return return
} }
isGeneratingWebsite = true guard let page = self.content.page(pageId) else {
DispatchQueue.global(qos: .background).async { return
let generator = PageContentParser(content: page.content, language: language) }
_ = generator.generatePage(from: content) guard !self.content.isGeneratingWebsite else {
return
DispatchQueue.main.async { }
self.generationResults = generator.results self.content.check(content: content, of: page, for: language) {
isGeneratingWebsite = false self.generationResults = $0
}
} }
} }
} }

View File

@ -67,17 +67,22 @@ struct PageContentResultsView: View {
@ObservedObject @ObservedObject
var results: PageGenerationResults var results: PageGenerationResults
#warning("Rework to only show a single popup will all files, and indicate missing ones")
private var totalFileCount: Int {
results.usedFiles.count + results.missingFiles.count + results.missingLinkedFiles.count
}
var body: some View { var body: some View {
HStack { HStack {
TextWithPopup( TextWithPopup(
symbol: .photoOnRectangleAngled, symbol: .photoOnRectangleAngled,
text: "\(results.files.count + results.missingFiles.count) images and files", text: "\(totalFileCount) images and files",
items: results.files.sorted().map { $0.id }) items: results.usedFiles.sorted().map { $0.id })
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
TextWithPopup( TextWithPopup(
symbol: .docBadgePlus, symbol: .docBadgePlus,
text: "\(results.linkedPages.count + results.missingPages.count) page links", text: "\(results.linkedPages.count + results.missingLinkedPages.count) page links",
items: results.linkedPages.sorted().map { $0.localized(in: language).title }) items: results.linkedPages.sorted().map { $0.localized(in: language).title })
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@ -87,18 +92,18 @@ struct PageContentResultsView: View {
items: results.externalLinks.sorted()) items: results.externalLinks.sorted())
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
if !results.missingPages.isEmpty { if !results.missingLinkedPages.isEmpty {
TextWithPopup( TextWithPopup(
symbol: .exclamationmarkTriangleFill, symbol: .exclamationmarkTriangleFill,
text: "\(results.missingPages.count) missing pages", text: "\(results.missingLinkedPages.count) missing pages",
items: results.missingPages.sorted()) items: results.missingLinkedPages.keys.sorted())
.foregroundStyle(.red) .foregroundStyle(.red)
} }
if !results.missingFiles.isEmpty { if !results.missingFiles.isEmpty {
TextWithPopup( TextWithPopup(
symbol: .exclamationmarkTriangleFill, symbol: .exclamationmarkTriangleFill,
text: "\(results.missingFiles.count) missing files", text: "\(results.missingFiles.count) missing files",
items: results.missingFiles.sorted()) items: results.missingFiles.keys.sorted())
.foregroundStyle(.red) .foregroundStyle(.red)
} }
if !results.invalidCommands.isEmpty { if !results.invalidCommands.isEmpty {
@ -111,7 +116,3 @@ struct PageContentResultsView: View {
} }
} }
} }
#Preview {
PageContentResultsView(results: .init())
}

View File

@ -13,12 +13,21 @@ struct PageDetailView: View {
private var page: Page private var page: Page
@State @State
private var didGenerateWebsite: Bool? private var showFileSelectionSheet = false
init(page: Page) { init(page: Page) {
self.page = page self.page = page
} }
private var requiredFilesText: String {
switch page.requiredFiles.count {
case 0: return "No files"
case 1: return "1 file"
default: return "\(page.requiredFiles.count) files"
}
}
#warning("Show info on page generation")
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
@ -30,17 +39,17 @@ struct PageDetailView: View {
Text("Generate") Text("Generate")
} }
.disabled(content.isGeneratingWebsite) .disabled(content.isGeneratingWebsite)
switch didGenerateWebsite { // switch didGenerateWebsite {
case .none: // case .none:
Image(systemSymbol: .questionmarkCircleFill) // Image(systemSymbol: .questionmarkCircleFill)
.foregroundStyle(.gray) // .foregroundStyle(.gray)
case .some(true): // case .some(true):
Image(systemSymbol: .checkmarkCircleFill) // Image(systemSymbol: .checkmarkCircleFill)
.foregroundStyle(.green) // .foregroundStyle(.green)
case .some(false): // case .some(false):
Image(systemSymbol: .xmarkCircleFill) // Image(systemSymbol: .xmarkCircleFill)
.foregroundStyle(.red) // .foregroundStyle(.red)
} // }
} }
IdPropertyView( IdPropertyView(
id: $page.id, id: $page.id,
@ -72,6 +81,24 @@ struct PageDetailView: View {
footer: "The date when the page content ended") footer: "The date when the page content ended")
.disabled(page.isExternalUrl) .disabled(page.isExternalUrl)
GenericPropertyView(
title: "Required files",
footer: "The additional files required by the page") {
HStack {
Image(systemSymbol: .squareAndPencilCircleFill)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 20)
Text(requiredFilesText)
Spacer()
}
.padding(.vertical, 8)
.contentShape(Rectangle())
.onTapGesture {
showFileSelectionSheet = true
}
}
LocalizedPageDetailView( LocalizedPageDetailView(
isExternalPage: page.isExternalUrl, isExternalPage: page.isExternalUrl,
page: page.localized(in: language)) page: page.localized(in: language))
@ -79,14 +106,14 @@ struct PageDetailView: View {
} }
.padding() .padding()
} }
.sheet(isPresented: $showFileSelectionSheet) {
MultiFileSelectionView(selectedFiles: $page.requiredFiles, insertSorted: true)
}
} }
private func generate() { private func generate() {
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
let success = content.generateFeed() content.generatePage(page)
DispatchQueue.main.async {
didGenerateWebsite = success
}
} }
} }
} }

View File

@ -50,6 +50,9 @@ struct PostImagesView: View {
.padding() .padding()
} }
} }
.sheet(isPresented: $showImagePicker) {
MultiFileSelectionView(selectedFiles: $post.images, allowedType: .images)
}
} }
private func shiftLeft(_ image: FileResource) { private func shiftLeft(_ image: FileResource) {

View File

@ -32,7 +32,6 @@ struct GenerationContentView: View {
@ViewBuilder @ViewBuilder
private var generationView: some View { private var generationView: some View {
ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Website Generation") Text("Website Generation")
.font(.largeTitle) .font(.largeTitle)
@ -42,32 +41,75 @@ struct GenerationContentView: View {
.padding(.bottom, 30) .padding(.bottom, 30)
HStack { HStack {
Button(action: generateFeed) { Button(action: generateFullWebsite) {
Text("Generate") Text("Generate")
} }
.disabled(isGeneratingWebsite) Text(generatorText)
Spacer()
if isGeneratingWebsite { if isGeneratingWebsite {
ProgressView() ProgressView()
.progressViewStyle(.circular) .progressViewStyle(.circular)
.frame(height: 25) .frame(height: 25)
} }
Button(action: updateGeneratedImages) {
Text("Update images")
} }
Text(generatorText) .disabled(isGeneratingWebsite)
Spacer() Text(content.generationStatus)
.font(.subheadline)
.padding()
HStack(spacing: 8) {
Text("\(content.results.imagesToGenerate.count) images")
Text("\(content.results.externalLinks.count) external links")
Text("\(content.results.resultCount) items processed")
Text("\(content.results.requiredFiles.count) files")
}
List {
Section("Inaccessible files") {
ForEach(content.results.inaccessibleFiles.sorted()) { file in
Text(file.id)
}
}
Section("Unparsable files") {
ForEach(content.results.unparsableFiles.sorted()) { file in
Text(file.id)
}
}
Section("Missing files") {
ForEach(content.results.missingFiles.sorted(), id: \.self) { file in
Text(file)
}
}
Section("Missing tags") {
ForEach(content.results.missingTags.sorted(), id: \.self) { tag in
Text(tag)
}
}
Section("Missing pages") {
ForEach(content.results.missingPages.sorted(), id: \.self) { page in
Text(page)
}
}
Section("Invalid commands") {
ForEach(content.results.invalidCommands.sorted(), id: \.self) { markdown in
Text(markdown)
}
}
Section("Warnings") {
ForEach(content.results.warnings.sorted(), id: \.self) { warning in
Text(warning)
}
}
Section("Unsaved output files") {
ForEach(content.results.unsavedOutputFiles.sorted(), id: \.self) { file in
Text(file)
}
}
} }
}.padding() }.padding()
} }
}
private func updateGeneratedImages() { private func generateFullWebsite() {
content.recalculateGeneratedImages()
}
private func generateFeed() {
DispatchQueue.main.async { DispatchQueue.main.async {
_ = content.generateFeed() content.generateWebsiteInAllLanguages()
} }
#warning("Update feed generation") #warning("Update feed generation")
/* /*

View File

@ -5,9 +5,9 @@ struct PageIssue {
let language: ContentLanguage let language: ContentLanguage
let message: PageContentAnomaly let message: GenerationAnomaly
init(page: Page, language: ContentLanguage, message: PageContentAnomaly) { init(page: Page, language: ContentLanguage, message: GenerationAnomaly) {
self.page = page self.page = page
self.language = language self.language = language
self.message = message self.message = message

View File

@ -50,24 +50,23 @@ final class PageIssueChecker: ObservableObject {
} }
private func analyze(page: Page, in language: ContentLanguage) { private func analyze(page: Page, in language: ContentLanguage) {
let parser = PageContentParser(content: page.content, language: language) let results = page.content.results.makeResults(for: page, in: language)
let parser = PageContentParser(content: page.content, language: language, results: results)
let hasPreviousIssues = issues.contains { $0.page == page && $0.language == language } let hasPreviousIssues = issues.contains { $0.page == page && $0.language == language }
let pageIssues: [PageIssue] let pageIssues: [PageIssue]
if let rawPageContent = page.content.storage.pageContent(for: page.id, language: language) { if let rawPageContent = page.content.storage.pageContent(for: page.id, language: language) {
_ = parser.generatePage(from: rawPageContent) _ = parser.generatePage(from: rawPageContent)
pageIssues = parser.results.issues.map { pageIssues = []
PageIssue(page: page, language: language, message: $0)
}
} else { } else {
let message = PageContentAnomaly.failedToLoadContent let message = GenerationAnomaly.failedToLoadContent
let error = PageIssue(page: page, language: language, message: message) let error = PageIssue(page: page, language: language, message: message)
pageIssues = [error] pageIssues = [error]
} }
guard hasPreviousIssues || !pageIssues.isEmpty else { guard hasPreviousIssues || !pageIssues.isEmpty else {
return return
} }
update(issues: pageIssues, for: page, in: parser.language) update(issues: pageIssues, for: page, in: language)
} }
private func update(issues: [PageIssue], for page: Page, in language: ContentLanguage) { private func update(issues: [PageIssue], for page: Page, in language: ContentLanguage) {

View File

@ -231,7 +231,8 @@ struct PageIssueView: View {
english: .init(content: content, english: .init(content: content,
urlString: pageId, urlString: pageId,
title: pageId), title: pageId),
tags: []) tags: [],
requiredFiles: [])
content.pages.insert(page, at: 0) content.pages.insert(page, at: 0)
retryPageCheck() retryPageCheck()