Full generation, file type cleanup
This commit is contained in:
parent
41887a1401
commit
1e4682dad1
@ -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 */,
|
||||
|
@ -6,4 +6,8 @@ extension Array {
|
||||
Array(self[$0..<Swift.min($0 + size, count)])
|
||||
}
|
||||
}
|
||||
|
||||
var nonEmpty: Self? {
|
||||
isEmpty ? nil : self
|
||||
}
|
||||
}
|
||||
|
11
CHDataManagement/Extensions/ConvertThrowing.swift
Normal file
11
CHDataManagement/Extensions/ConvertThrowing.swift
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
prefix operator ~>
|
||||
|
||||
prefix func ~> (operation: () throws -> Void) -> Bool {
|
||||
do {
|
||||
try operation()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
199
CHDataManagement/Generator/GenerationResults.swift
Normal file
199
CHDataManagement/Generator/GenerationResults.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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):
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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: ` -> 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 ""
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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]! } ?? [])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
240
CHDataManagement/Model/FileType.swift
Normal file
240
CHDataManagement/Model/FileType.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ final class Tag: Item {
|
||||
}
|
||||
|
||||
override var itemType: ItemType {
|
||||
.tag
|
||||
.tagPage(self)
|
||||
}
|
||||
|
||||
func contains(urlComponent: String) -> Bool {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
|
||||
enum TextFileType: String {
|
||||
|
||||
case json
|
||||
|
||||
case conf
|
||||
|
||||
case yaml
|
||||
|
||||
init?(fileExtension: String) {
|
||||
self.init(rawValue: fileExtension)
|
||||
}
|
||||
|
||||
var fileExtension: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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?
|
||||
|
@ -1,11 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
struct PageOnDisk {
|
||||
|
||||
let page: PageFile
|
||||
|
||||
let deContentUrl: URL
|
||||
|
||||
let enContentUrl: URL
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
130
CHDataManagement/Views/Files/MultiFileSelectionView.swift
Normal file
130
CHDataManagement/Views/Files/MultiFileSelectionView.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -30,7 +30,7 @@ struct IdPropertyView: View {
|
||||
}
|
||||
|
||||
private var isValid: Bool {
|
||||
validation(id)
|
||||
validation(newId)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,9 @@ struct PostImagesView: View {
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showImagePicker) {
|
||||
MultiFileSelectionView(selectedFiles: $post.images, allowedType: .images)
|
||||
}
|
||||
}
|
||||
|
||||
private func shiftLeft(_ image: FileResource) {
|
||||
|
@ -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")
|
||||
/*
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user