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

View File

@ -6,4 +6,8 @@ extension Array {
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 failedToParseContent
case missingFile(file: String, markdown: String)
@ -9,7 +9,7 @@ enum PageContentAnomaly {
case warning(String)
}
extension PageContentAnomaly: Identifiable {
extension GenerationAnomaly: Identifiable {
var id: String {
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
}
}
extension PageContentAnomaly: Hashable {
extension GenerationAnomaly: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
extension PageContentAnomaly {
extension GenerationAnomaly {
enum Severity: String, CaseIterable {
case warning
@ -62,7 +62,7 @@ extension PageContentAnomaly {
}
}
extension PageContentAnomaly: CustomStringConvertible {
extension GenerationAnomaly: CustomStringConvertible {
var description: String {
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 {
switch self {
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, _):
return "<link rel='stylesheet' href='\(file.assetUrl)' />"
return "<link rel='stylesheet' href='\(file.absoluteUrl)' />"
case .js(let file, let deferred):
let deferText = deferred ? " defer" : ""
return "<script src='\(file.assetUrl)'\(deferText)></script>"
return "<script src='\(file.absoluteUrl)'\(deferText)></script>"
case .jsModule(let file):
return "<script type='module' src='\(file.assetUrl)'></script>"
return "<script type='module' src='\(file.absoluteUrl)'></script>"
case .author(let author):
return "<meta name='author' content='\(author)'>"
case .title(let title):

View File

@ -11,8 +11,6 @@ final class ImageGenerator {
private var generatedImages: [String : Set<String>] = [:]
private var jobs: [ImageGenerationJob] = []
init(storage: Storage, settings: Settings) {
self.storage = storage
self.settings = settings
@ -23,20 +21,6 @@ final class ImageGenerator {
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 {
guard storage.save(listOfGeneratedImages: generatedImages) else {
print("Failed to save list of generated images")
@ -45,50 +29,6 @@ final class ImageGenerator {
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.
@ -105,6 +45,9 @@ final class ImageGenerator {
}
private func needsToGenerate(version: String, for image: String) -> Bool {
if exists(version) {
return false
}
guard let versions = generatedImages[image] else {
return true
}
@ -143,7 +86,7 @@ final class ImageGenerator {
// MARK: Image operations
private func generate(job: ImageGenerationJob) -> Bool {
func generate(job: ImageGenerationJob) -> Bool {
guard needsToGenerate(version: job.version, for: job.image) else {
return true
}
@ -158,7 +101,7 @@ final class ImageGenerator {
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 {
print("Failed to get data for type \(job.type)")
@ -209,7 +152,7 @@ final class ImageGenerator {
// 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 {
case .jpg:
return image.representation(using: .jpeg, properties: [.compressionFactor: NSNumber(value: 0.6)])
@ -225,6 +168,8 @@ final class ImageGenerator {
return nil
case .tiff:
return nil
default:
return nil
}
}

View File

@ -4,13 +4,70 @@ struct ImageGenerationJob {
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 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 {
results.missingFiles.insert(fileId)
results.missing(file: fileId, source: "Audio player song list")
return ""
}
guard let data = file.dataContent() else {
results.issues.insert(.failedToLoadContent)
results.inaccessibleContent(file: file)
return ""
}
let songs: [Song]
do {
songs = try JSONDecoder().decode([Song].self, from: data)
} catch {
results.issues.insert(.failedToParseContent)
results.invalidFormat(file: file, error: "Not valid JSON containing [Song]: \(error)")
return ""
}
@ -47,12 +47,12 @@ struct AudioPlayerCommandProcessor: CommandProcessor {
for song in songs {
guard let image = content.image(song.cover) else {
results.missing(file: song.cover, markdown: "Missing cover image \(song.cover) in \(file.id)")
results.missing(file: song.cover, containedIn: file)
continue
}
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
}
#warning("Check if file is audio")
@ -79,18 +79,17 @@ struct AudioPlayerCommandProcessor: CommandProcessor {
}
let footerScript = AudioPlayerScript(items: amplitude).content
results.requiredFooters.insert(footerScript)
results.requiredHeaders.insert(.audioPlayerCss)
results.requiredHeaders.insert(.audioPlayerJs)
results.require(footer: footerScript)
results.require(headers: .audioPlayerCss, .audioPlayerJs)
results.requiredIcons.formUnion([
results.require(icons:
.audioPlayerClose,
.audioPlayerPlaylist,
.audioPlayerNext,
.audioPlayerPrevious,
.audioPlayerPlay,
.audioPlayerPause
])
)
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
guard let file = content.file(fileId) else {
results.missing(file: fileId, markdown: markdown)
results.missing(file: fileId, source: "Download button")
return nil
}
results.files.insert(file)
results.requiredIcons.insert(.buttonDownload)
results.require(file: file)
results.require(icon: .buttonDownload)
return ContentButtons.Item(
icon: .buttonDownload,
filePath: file.absoluteUrl,
@ -80,8 +80,8 @@ struct ButtonCommandProcessor: CommandProcessor {
return nil
}
results.externalLinks.insert(rawUrl)
results.requiredIcons.insert(icon)
results.externalLink(to: rawUrl)
results.require(icon: icon)
let title = arguments[1].trimmed
@ -96,7 +96,7 @@ struct ButtonCommandProcessor: CommandProcessor {
let text = arguments[0].trimmed
let event = arguments[1].trimmed
results.requiredIcons.insert(.buttonPlay)
results.require(icon: .buttonPlay)
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)
return nil
}
results.requiredIcons.insert(icon)
results.require(icon: icon)
return .init(icon: icon, value: parts[1])
}
return ContentLabels(labels: labels).content

View File

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

View File

@ -17,82 +17,219 @@ extension ImageToGenerate: Hashable {
final class PageGenerationResults: ObservableObject {
@Published
var linkedPages: Set<Page> = []
let itemId: ItemId
@Published
var linkedTags: Set<Tag> = []
private unowned let delegate: GenerationResults
@Published
var externalLinks: Set<String> = []
/// The files that could not be accessed
private(set) var inaccessibleFiles: Set<FileResource>
@Published
var files: Set<FileResource> = []
/// The files that could not be parsed, with the error message produced
private(set) var unparsableFiles: [FileResource : Set<String>]
@Published
var assets: Set<FileResource> = []
/// The missing files directly used by this page, and the source of the file
private(set) var missingFiles: [String: Set<String>]
@Published
var imagesToGenerate: Set<ImageToGenerate> = []
/// The missing files linked to from other files.
private(set) var missingLinkedFiles: [String : Set<FileResource>]
@Published
var missingPages: Set<String> = []
/// The missing tags linked to by this page, and the source of the link
private(set) var missingLinkedTags: [String : Set<String>]
@Published
var missingFiles: Set<String> = []
/// The missing pages linked to by this page, and the source of the link
private(set) var missingLinkedPages: [String : Set<String>]
@Published
var missingTags: Set<String> = []
/// The footer scripts or html to add to the end of the body
private(set) var requiredFooters: Set<String>
@Published
var invalidCommands: [(command: ShorthandMarkdownKey?, markdown: String)] = []
/// The known header elements to include in the page
private(set) var requiredHeaders: Set<KnownHeaderElement>
@Published
var requiredHeaders: Set<KnownHeaderElement> = []
/// The known icons that need to be included as hidden SVGs
private(set) var requiredIcons: Set<PageIcon>
@Published
var requiredFooters: Set<String> = []
/// The pages linked to by the page
private(set) var linkedPages: Set<Page>
@Published
var requiredIcons: Set<PageIcon> = []
/// The tags linked to by this page
private(set) var linkedTags: Set<Tag>
@Published
var issues: Set<PageContentAnomaly> = []
/// The links to external content in this page
private(set) var externalLinks: Set<String>
func reset() {
linkedPages = []
linkedTags = []
externalLinks = []
files = []
assets = []
imagesToGenerate = []
missingPages = []
missingFiles = []
missingTags = []
invalidCommands = []
/// The files used by this page, but not necessarily required in the output folder
private(set) var usedFiles: Set<FileResource>
/// The files that need to be copied
private(set) var requiredFiles: Set<FileResource>
/// The image versions required for this page
private(set) var imagesToGenerate: Set<ImageGenerationJob>
private(set) var invalidCommands: [(command: ShorthandMarkdownKey?, markdown: String)] = []
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 = []
requiredFooters = []
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) {
invalidCommands.append((command, String(markdown)))
issues.insert(.invalidCommand(command: command, markdown: String(markdown)))
let markdown = String(markdown)
invalidCommands.append((command, markdown))
delegate.invalidCommand(markdown)
}
func missing(page: String, markdown: Substring) {
missingPages.insert(page)
issues.insert(.missingPage(page: page, markdown: String(markdown)))
func missing(page: String, source: String) {
missingLinkedPages[page, default: []].insert(source)
delegate.missing(page: page)
}
func missing(tag: String, markdown: Substring) {
missingTags.insert(tag)
issues.insert(.missingTag(tag: tag, markdown: String(markdown)))
func missing(tag: String, source: String) {
missingLinkedTags[tag, default: []].insert(source)
delegate.missing(tag: tag)
}
func missing(file: String, markdown: Substring) {
missingFiles.insert(file)
issues.insert(.missingFile(file: file, markdown: String(markdown)))
func missing(file: String, source: String) {
missingFiles[file, default: []].insert(source)
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 imageGenerator: ImageGenerator
init(content: Content, imageGenerator: ImageGenerator) {
init(content: Content) {
self.content = content
self.imageGenerator = imageGenerator
}
private func makeHeaders(requiredItems: Set<KnownHeaderElement>) -> Set<HeaderElement> {
@ -22,10 +19,10 @@ final class PageGenerator {
return result
}
func generate(page: Page, language: ContentLanguage) -> (page: String, results: PageGenerationResults)? {
func generate(page: Page, language: ContentLanguage, results: PageGenerationResults) -> String? {
let contentGenerator = PageContentParser(
content: content,
language: language)
language: language, results: results)
guard let rawPageContent = content.storage.pageContent(for: page.id, language: language) else {
return nil
@ -33,8 +30,6 @@ final class PageGenerator {
let pageContent = contentGenerator.generatePage(from: rawPageContent)
contentGenerator.requestImages(imageGenerator)
let localized = page.localized(in: language)
let tags: [FeedEntryData.Tag] = page.tags.map { tag in
@ -42,8 +37,8 @@ final class PageGenerator {
url: content.absoluteUrlToTag(tag, language: language))
}
let headers = makeHeaders(requiredItems: contentGenerator.results.requiredHeaders)
contentGenerator.results.assets.formUnion(headers.compactMap { $0.file })
let headers = makeHeaders(requiredItems: results.requiredHeaders)
results.require(files: headers.compactMap { $0.file })
let fullPage = ContentPage(
language: language,
@ -55,10 +50,10 @@ final class PageGenerator {
navigationBarLinks: content.navigationBar(in: language),
pageContent: pageContent,
headers: headers,
footers: contentGenerator.results.requiredFooters.sorted(),
icons: contentGenerator.results.requiredIcons)
footers: results.requiredFooters.sorted(),
icons: results.requiredIcons)
.content
return (fullPage, contentGenerator.results)
return fullPage
}
}

View File

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

View File

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

View File

@ -2,70 +2,97 @@ import Foundation
extension Content {
func generateFeed() -> Bool {
#warning("Implement feed generation")
return false
func generateWebsiteInAllLanguages() {
performGenerationIfIdle {
self.generatePagesInternal()
self.generatePostFeedPagesInternal()
self.generateTagPagesInternal()
self.generateTagOverviewPagesInternal()
self.copyRequiredFiles()
self.generateRequiredImages()
self.status("Generation completed")
}
}
func generateAllPages() -> Bool {
guard startGenerating() else { return false }
defer { endGenerating() }
func generatePostFeedPages() {
performGenerationIfIdle {
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 {
guard generateInternal(page, in: language) else {
return false
}
self.generateInternal(page, in: language)
}
self.copyRequiredFiles()
self.generateRequiredImages()
}
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
}
func generatePage(_ page: Page, in language: ContentLanguage) {
performGenerationIfIdle {
self.generateInternal(page, in: language)
}
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
@ -121,60 +148,134 @@ extension Content {
return result
}
// MARK: Images
func recalculateGeneratedImages() {
let images = Set(self.images.map { $0.id })
imageGenerator.recalculateGeneratedImages(by: images)
}
// MARK: Generation
private func startGenerating() -> Bool {
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
}
private func performGenerationIfIdle(_ operation: @escaping () -> ()) {
DispatchQueue.main.async {
let id = ItemId(itemId: page.id, language: language, itemType: .page)
self.results[id] = results
guard !self.isGeneratingWebsite else {
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"
guard storage.write(content, to: path) else {
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,
lastModified: page.lastModifiedDate,
originalUrl: page.originalURL,
files: Set(page.files),
externalFiles: Set(page.externalFiles),
requiredFiles: Set(page.requiredFiles),
linkPreviewImage: page.linkPreviewImage.map { images[$0] },
linkPreviewTitle: page.linkPreviewTitle,
linkPreviewDescription: page.linkPreviewDescription)
@ -115,14 +112,15 @@ extension Content {
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 german = convert(post.german, images: images)
let english = convert(post.english, images: images)
return Post(
dict[postId] = Post(
content: self,
id: postId,
isDraft: post.isDraft,
@ -145,25 +143,36 @@ extension Content {
self.tags = tags.values.sorted()
self.pages = pages.values.sorted(ascending: false) { $0.startDate }
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.settings = makeSettings(settings, tags: tags, pages: pages, files: files)
self.settings = makeSettings(settings, tags: tags, pages: pages, files: files, posts: posts)
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")
let navigationItems: [Item] = settings.navigationItems.compactMap {
switch $0.type {
case .tag:
return tags[$0.id]
case .page:
return pages[$0.id]
let navigationItems: [Item] = settings.navigationItems.compactMap { raw in
guard let type = ItemType(rawValue: raw, posts: posts, pages: pages, tags: tags) else {
return nil
}
switch type {
case .general:
return nil
case .post(let post):
return post
case .feed:
return nil // TODO: Provide feed object
case .page(let page):
return page
case .tagPage(let tag):
return tag
case .tagOverview:
return tagOverview
default:
return nil
}
}
@ -182,7 +191,7 @@ extension Content {
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
let (pageId, page) = data
pages[pageId] = Page(
@ -193,9 +202,10 @@ extension Content {
createdDate: page.createdDate,
startDate: page.startDate,
endDate: page.endDate,
german: convert(page.german, images: images),
english: convert(page.english, images: images),
tags: page.tags.map { tags[$0]! })
german: convert(page.german, images: files),
english: convert(page.english, images: files),
tags: page.tags.map { tags[$0]! },
requiredFiles: page.requiredFiles?.map { files[$0]! } ?? [])
}
}

View File

@ -63,7 +63,8 @@ private extension Page {
startDate: startDate,
endDate: hasEndDate ? endDate : nil,
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 {
.init(url: urlString,
files: files.sorted(),
externalFiles: externalFiles.sorted(),
requiredFiles: requiredFiles.sorted(),
title: title,
linkPreviewImage: linkPreviewImage?.id,
linkPreviewTitle: linkPreviewTitle,
@ -140,7 +138,7 @@ extension Settings {
var file: SettingsFile {
.init(
paths: paths.file,
navigationItems: navigationItems.map { .init(type: $0.itemType, id: $0.id) },
navigationItems: navigationItems.map { $0.itemType.id },
posts: posts.file,
pages: pages.file,
german: german.file,

View File

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

View File

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

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 {
let itemId: String
let language: ContentLanguage
let itemType: ItemType
@ -11,16 +9,16 @@ struct ItemId {
extension ItemId: Equatable {
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 {
func hash(into hasher: inout Hasher) {
hasher.combine(itemId)
hasher.combine(language)
hasher.combine(itemType)
hasher.combine(itemType.id)
}
}
@ -30,9 +28,6 @@ extension ItemId: Comparable {
guard lhs.itemType == rhs.itemType else {
return lhs.itemType < rhs.itemType
}
guard lhs.itemId == rhs.itemId else {
return lhs.itemId < rhs.itemId
}
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 file
}
extension ItemType: Equatable {
@ -23,13 +25,52 @@ extension ItemType: Hashable {
extension ItemType: Identifiable {
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 {
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?
/**
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
var linkPreviewImage: FileResource?
@ -71,9 +48,6 @@ final class LocalizedPage: ObservableObject {
title: String,
lastModified: Date? = nil,
originalUrl: String? = nil,
files: Set<String> = [],
externalFiles: Set<String> = [],
requiredFiles: Set<String> = [],
linkPreviewImage: FileResource? = nil,
linkPreviewTitle: String? = nil,
linkPreviewDescription: String? = nil) {
@ -82,9 +56,6 @@ final class LocalizedPage: ObservableObject {
self.title = title
self.lastModified = lastModified
self.originalUrl = originalUrl
self.files = files
self.externalFiles = externalFiles
self.requiredFiles = requiredFiles
self.linkPreviewImage = linkPreviewImage
self.linkPreviewTitle = linkPreviewTitle
self.linkPreviewDescription = linkPreviewDescription

View File

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

View File

@ -1,11 +1,6 @@
import Foundation
final class Post: ObservableObject {
unowned let content: Content
@Published
var id: String
final class Post: Item {
@Published
var isDraft: Bool
@ -45,8 +40,6 @@ final class Post: ObservableObject {
german: LocalizedPost,
english: LocalizedPost,
linkedPage: Page? = nil) {
self.content = content
self.id = id
self.isDraft = isDraft
self.createdDate = createdDate
self.startDate = startDate
@ -56,6 +49,7 @@ final class Post: ObservableObject {
self.german = german
self.english = english
self.linkedPage = linkedPage
super.init(content: content, id: id)
}
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 {
}

View File

@ -53,7 +53,7 @@ final class Tag: Item {
}
override var itemType: ItemType {
.tag
.tagPage(self)
}
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,
german: .german,
english: .english,
tags: [.mock])
tags: [.mock],
requiredFiles: [])
}
}
@ -24,18 +25,12 @@ extension LocalizedPage {
urlString: "my-project",
title: "My First Project",
lastModified: nil,
originalUrl: "projects/electronics/my-first-project/en.html",
files: [],
externalFiles: [],
requiredFiles: [])
originalUrl: "projects/electronics/my-first-project/en.html")
static let german = LocalizedPage(
content: .mock,
urlString: "mein-projekt",
title: "Mein Erstes Projekt",
lastModified: nil,
originalUrl: "projects/electronics/my-first-project/de.html",
files: [],
externalFiles: [],
requiredFiles: [])
originalUrl: "projects/electronics/my-first-project/de.html")
}

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 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 {
@ -30,22 +36,6 @@ struct LocalizedPageFile {
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 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
struct NavigationItemReference: Codable {
let type: ItemType
let id: String
}
struct SettingsFile {
let paths: PathSettingsFile
/// The tags to show in the navigation bar
let navigationItems: [NavigationItemReference]
let navigationItems: [String]
let posts: PostSettingsFile

View File

@ -95,6 +95,12 @@ final class Storage: ObservableObject {
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
- Note: This function requires a security scope for the content path

View File

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

View File

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

View File

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

View File

@ -67,17 +67,22 @@ struct PageContentResultsView: View {
@ObservedObject
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 {
HStack {
TextWithPopup(
symbol: .photoOnRectangleAngled,
text: "\(results.files.count + results.missingFiles.count) images and files",
items: results.files.sorted().map { $0.id })
text: "\(totalFileCount) images and files",
items: results.usedFiles.sorted().map { $0.id })
.foregroundStyle(.secondary)
TextWithPopup(
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 })
.foregroundStyle(.secondary)
@ -87,18 +92,18 @@ struct PageContentResultsView: View {
items: results.externalLinks.sorted())
.foregroundStyle(.secondary)
if !results.missingPages.isEmpty {
if !results.missingLinkedPages.isEmpty {
TextWithPopup(
symbol: .exclamationmarkTriangleFill,
text: "\(results.missingPages.count) missing pages",
items: results.missingPages.sorted())
text: "\(results.missingLinkedPages.count) missing pages",
items: results.missingLinkedPages.keys.sorted())
.foregroundStyle(.red)
}
if !results.missingFiles.isEmpty {
TextWithPopup(
symbol: .exclamationmarkTriangleFill,
text: "\(results.missingFiles.count) missing files",
items: results.missingFiles.sorted())
items: results.missingFiles.keys.sorted())
.foregroundStyle(.red)
}
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
@State
private var didGenerateWebsite: Bool?
private var showFileSelectionSheet = false
init(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 {
ScrollView {
VStack(alignment: .leading) {
@ -30,17 +39,17 @@ struct PageDetailView: View {
Text("Generate")
}
.disabled(content.isGeneratingWebsite)
switch didGenerateWebsite {
case .none:
Image(systemSymbol: .questionmarkCircleFill)
.foregroundStyle(.gray)
case .some(true):
Image(systemSymbol: .checkmarkCircleFill)
.foregroundStyle(.green)
case .some(false):
Image(systemSymbol: .xmarkCircleFill)
.foregroundStyle(.red)
}
// switch didGenerateWebsite {
// case .none:
// Image(systemSymbol: .questionmarkCircleFill)
// .foregroundStyle(.gray)
// case .some(true):
// Image(systemSymbol: .checkmarkCircleFill)
// .foregroundStyle(.green)
// case .some(false):
// Image(systemSymbol: .xmarkCircleFill)
// .foregroundStyle(.red)
// }
}
IdPropertyView(
id: $page.id,
@ -72,6 +81,24 @@ struct PageDetailView: View {
footer: "The date when the page content ended")
.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(
isExternalPage: page.isExternalUrl,
page: page.localized(in: language))
@ -79,14 +106,14 @@ struct PageDetailView: View {
}
.padding()
}
.sheet(isPresented: $showFileSelectionSheet) {
MultiFileSelectionView(selectedFiles: $page.requiredFiles, insertSorted: true)
}
}
private func generate() {
DispatchQueue.global(qos: .userInitiated).async {
let success = content.generateFeed()
DispatchQueue.main.async {
didGenerateWebsite = success
}
content.generatePage(page)
}
}
}

View File

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

View File

@ -32,7 +32,6 @@ struct GenerationContentView: View {
@ViewBuilder
private var generationView: some View {
ScrollView {
VStack(alignment: .leading) {
Text("Website Generation")
.font(.largeTitle)
@ -42,32 +41,75 @@ struct GenerationContentView: View {
.padding(.bottom, 30)
HStack {
Button(action: generateFeed) {
Button(action: generateFullWebsite) {
Text("Generate")
}
.disabled(isGeneratingWebsite)
Text(generatorText)
Spacer()
if isGeneratingWebsite {
ProgressView()
.progressViewStyle(.circular)
.frame(height: 25)
}
Button(action: updateGeneratedImages) {
Text("Update images")
}
.disabled(isGeneratingWebsite)
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)
}
}
Text(generatorText)
Spacer()
}
}.padding()
}
}
private func updateGeneratedImages() {
content.recalculateGeneratedImages()
}
private func generateFeed() {
private func generateFullWebsite() {
DispatchQueue.main.async {
_ = content.generateFeed()
content.generateWebsiteInAllLanguages()
}
#warning("Update feed generation")
/*

View File

@ -5,9 +5,9 @@ struct PageIssue {
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.language = language
self.message = message

View File

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

View File

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