diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index 5420807..0a62408 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -44,6 +44,8 @@ E229904E2D13535C009F8D77 /* SecurityBookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229904D2D135349009F8D77 /* SecurityBookmark.swift */; }; E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; }; E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252092C52C9260029FF16 /* ContentLanguage.swift */; }; + E2521DFC2D5020BE00C56662 /* PostContentGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2521DFB2D501DAE00C56662 /* PostContentGenerator.swift */; }; + E2521E002D50BB6E00C56662 /* ItemLinkResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2521DFF2D50BB6E00C56662 /* ItemLinkResults.swift */; }; E2581DED2C75202400F1F079 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DEC2C75202400F1F079 /* Tag.swift */; }; E25DA5092CFD964E00AEF16D /* TagContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5082CFD964E00AEF16D /* TagContentView.swift */; }; E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */; }; @@ -298,6 +300,8 @@ E229904B2D10BE59009F8D77 /* InitialSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialSetupView.swift; sourceTree = ""; }; E229904D2D135349009F8D77 /* SecurityBookmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityBookmark.swift; sourceTree = ""; }; E24252092C52C9260029FF16 /* ContentLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLanguage.swift; sourceTree = ""; }; + E2521DFB2D501DAE00C56662 /* PostContentGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostContentGenerator.swift; sourceTree = ""; }; + E2521DFF2D50BB6E00C56662 /* ItemLinkResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLinkResults.swift; sourceTree = ""; }; E2581DEC2C75202400F1F079 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; E25A0B882CE4021400F33674 /* LocalizedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPage.swift; sourceTree = ""; }; E25DA5082CFD964E00AEF16D /* TagContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagContentView.swift; sourceTree = ""; }; @@ -575,7 +579,6 @@ E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */, E22990412D107A94009F8D77 /* ImageVersion.swift */, E2FE0F182D2723E3002963B7 /* ImageSet.swift */, - E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */, ); path = Generator; sourceTree = ""; @@ -945,6 +948,7 @@ E2FE0F072D2689DC002963B7 /* Post Lists */ = { isa = PBXGroup; children = ( + E2521DFB2D501DAE00C56662 /* PostContentGenerator.swift */, E2FE0F0C2D268A09002963B7 /* PostListPageGeneratorSource.swift */, E2FE0F0A2D2689FF002963B7 /* FeedGeneratorSource.swift */, E2FE0F082D2689F0002963B7 /* TagPageGeneratorSource.swift */, @@ -957,8 +961,9 @@ isa = PBXGroup; children = ( E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */, - E25DA5982D02401A00AEF16D /* PageGenerator.swift */, E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */, + E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */, + E25DA5982D02401A00AEF16D /* PageGenerator.swift */, ); path = "Page Generators"; sourceTree = ""; @@ -1002,6 +1007,7 @@ E2FE0F442D2BC76C002963B7 /* Markdown */ = { isa = PBXGroup; children = ( + E2521DFF2D50BB6E00C56662 /* ItemLinkResults.swift */, E2FE0F102D268E78002963B7 /* MarkdownCodeProcessor.swift */, E2FE0F4A2D2BCCA5002963B7 /* MarkdownHeadlineProcessor.swift */, E2FE0F452D2BC772002963B7 /* MarkdownImageProcessor.swift */, @@ -1237,8 +1243,10 @@ E2FD1D2E2D37180900B48627 /* GeneralSettings.swift in Sources */, E2FD1D542D46577700B48627 /* HtmlProducer.swift in Sources */, E2FE0F0D2D268A09002963B7 /* PostListPageGeneratorSource.swift in Sources */, + E2521E002D50BB6E00C56662 /* ItemLinkResults.swift in Sources */, E2FE0F402D2B45D3002963B7 /* SwiftBlock.swift in Sources */, E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */, + E2521DFC2D5020BE00C56662 /* PostContentGenerator.swift in Sources */, E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */, E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */, E22990402D0F95EC009F8D77 /* FolderOnDiskPropertyView.swift in Sources */, diff --git a/CHDataManagement/Generator/Markdown/ItemLinkResults.swift b/CHDataManagement/Generator/Markdown/ItemLinkResults.swift new file mode 100644 index 0000000..9d9daf9 --- /dev/null +++ b/CHDataManagement/Generator/Markdown/ItemLinkResults.swift @@ -0,0 +1,18 @@ + +protocol ItemLinkResults { + + func externalLink(to url: String) + + func missing(page: String, source: String) + + func linked(to page: Page) + + func missing(tag: String, source: String) + + func linked(to tag: Tag) + + func missing(file: String, source: String) + + func require(file: FileResource) +} + diff --git a/CHDataManagement/Generator/Markdown/MarkdownLinkProcessor.swift b/CHDataManagement/Generator/Markdown/MarkdownLinkProcessor.swift index 45e08f1..2567243 100644 --- a/CHDataManagement/Generator/Markdown/MarkdownLinkProcessor.swift +++ b/CHDataManagement/Generator/Markdown/MarkdownLinkProcessor.swift @@ -1,84 +1,110 @@ import Ink +extension PageGenerationResults: ItemLinkResults { + +} + struct MarkdownLinkProcessor: MarkdownProcessor { static let modifier: Modifier.Target = .links private let content: Content - private let results: PageGenerationResults + private let results: ItemLinkResults private let language: ContentLanguage + init(content: Content, results: ItemLinkResults, language: ContentLanguage) { + self.content = content + self.results = results + self.language = language + } + init(content: Content, results: PageGenerationResults, language: ContentLanguage) { self.content = content self.results = results self.language = language } + func process(html: String, markdown: Substring) -> String { + let markdownUrl = markdown.between("(", and: ")") + guard let (textToReplace, url) = convert(markdownUrl: markdownUrl) else { + // Keep original code, no changes needed + return html + } + guard let convertedUrl = url else { + // Remove link since the target can't be found + return markdown.between("[", and: "]") + } + return html.replacingOccurrences(of: textToReplace, with: convertedUrl) + } + private let pageLinkMarker = "page:" private let tagLinkMarker = "tag:" private let fileLinkMarker = "file:" - func process(html: String, markdown: Substring) -> String { - let url = markdown.between("(", and: ")") - if url.hasPrefix(pageLinkMarker) { - return handleInlinePageLink(url: url, html: html, markdown: markdown) + func convert(markdownUrl: String) -> (textToChange: String, url: String?)? { + if let result = handle(prefix: pageLinkMarker, in: markdownUrl, handler: convert(inlinePageLink:)) { + return result } - if url.hasPrefix(tagLinkMarker) { - return handleInlineTagLink(url: url, html: html, markdown: markdown) + if let result = handle(prefix: tagLinkMarker, in: markdownUrl, handler: convert(inlineTagLink:)) { + return result } - if url.hasPrefix(fileLinkMarker) { - return handleInlineFileLink(url: url, html: html, markdown: markdown) + if let result = handle(prefix: fileLinkMarker, in: markdownUrl, handler: convert(inlineFileLink:)) { + return result } - results.externalLink(to: url) - return html + results.externalLink(to: markdownUrl) + // No need to change anything + return nil } - 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: "") + private func handle(prefix: String, in markdownUrl: String, handler: (String) -> String?) -> (textToChange: String, url: String?)? { + guard markdownUrl.hasPrefix(prefix) else { + // Continue search + return nil + } + let inlineLink = markdownUrl.replacingOccurrences(of: prefix, with: "") + let itemId = inlineLink.dropAfterFirst("#") + let url = handler(itemId) + return (textToChange: prefix + itemId, url: url) + } + + private func convert(inlinePageLink pageId: String) -> String? { 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: "]") + return nil } - guard !page.isDraft else { - return markdown.between("[", and: "]") - } - + // Note link even for draft pages results.linked(to: page) - let pagePath = page.absoluteUrl(in: language) - return html.replacingOccurrences(of: textToChange, with: pagePath) + + guard !page.isDraft else { + // TODO: Report links to draft pages + return nil + } + return page.absoluteUrl(in: language) } - 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: "") + private func convert(inlineTagLink: String) -> String? { + let tagId = inlineTagLink.dropAfterFirst("#") 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: "]") + return nil } results.linked(to: tag) - let tagPath = tag.absoluteUrl(in: language) - return html.replacingOccurrences(of: textToChange, with: tagPath) + return tag.absoluteUrl(in: language) } - 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: "") + private func convert(inlineFileLink fileId: String) -> String? { 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: "]") + return nil } results.require(file: file) - let filePath = file.absoluteUrl - return html.replacingOccurrences(of: url, with: filePath) + return file.absoluteUrl } } diff --git a/CHDataManagement/Generator/PageContentGenerator.swift b/CHDataManagement/Generator/Page Generators/PageContentGenerator.swift similarity index 100% rename from CHDataManagement/Generator/PageContentGenerator.swift rename to CHDataManagement/Generator/Page Generators/PageContentGenerator.swift diff --git a/CHDataManagement/Generator/Post Lists/PostContentGenerator.swift b/CHDataManagement/Generator/Post Lists/PostContentGenerator.swift new file mode 100644 index 0000000..c67b894 --- /dev/null +++ b/CHDataManagement/Generator/Post Lists/PostContentGenerator.swift @@ -0,0 +1,93 @@ +import Foundation +import Ink + +struct PostContentGenerator { + + private let content: Content + + private let results: PageGenerationResults + + private let language: ContentLanguage + + private let post: Post + + init(content: Content, results: PageGenerationResults, language: ContentLanguage, post: Post) { + self.content = content + self.results = results + self.language = language + self.post = post + } + + func generate() -> String { + let parser = MarkdownParser(modifiers: [ + Modifier(target: .images, closure: handleImage), + Modifier(target: .codeBlocks, closure: handleCode), + Modifier(target: .links, closure: handleLink) + ]) + return parser.html(from: post.localized(in: language).text) + } + + private func handleImage( + html: String, + markdown: Substring + ) -> String { + results.warning("Image in \(postDescription)") + return "" + } + + private func handleCode( + html: String, + markdown: Substring + ) -> String { + results.warning("Code in \(postDescription)") + return "" + } + + private var postDescription: String { + "content of post \(post.id) (\(language.shortText))" + } + + private func handleLink( + html: String, + markdown: Substring + ) -> String { + let converter = MarkdownLinkProcessor(content: content, results: self, language: language) + let markdownUrl = markdown.between("(", and: ")") + let text = markdown.between("[", and: "]") + guard let url = converter.convert(markdownUrl: markdownUrl)?.url else { + return text + } + return "\(text)" + } +} + +extension PostContentGenerator: ItemLinkResults { + + func externalLink(to url: String) { + results.externalLink(to: url) + } + + func missing(page: String, source: String) { + results.missing(page: page, source: postDescription) + } + + func linked(to page: Page) { + results.linked(to: page) + } + + func missing(tag: String, source: String) { + results.missing(tag: tag, source: postDescription) + } + + func linked(to tag: Tag) { + results.linked(to: tag) + } + + func missing(file: String, source: String) { + results.missing(file: file, source: postDescription) + } + + func require(file: FileResource) { + results.require(file: file) + } +} diff --git a/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift index 32e078e..f779354 100644 --- a/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift +++ b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift @@ -58,7 +58,7 @@ final class PostListPageGenerator { } } - private func makePostData(post: Post) -> FeedEntryData { + private func makePostData(post: Post, results: PageGenerationResults) -> FeedEntryData { let localized: LocalizedPost = post.localized(in: language) let linkUrl: FeedEntryData.Link? = post.linkedPage.map { @@ -88,6 +88,12 @@ final class PostListPageGenerator { media = nil } + let text = PostContentGenerator( + content: source.content, + results: source.results, + language: language, + post: post).generate() + return FeedEntryData( entryId: post.id, title: localized.title, @@ -95,13 +101,12 @@ final class PostListPageGenerator { link: linkUrl, tags: tags, labels: localized.labels, - text: localized.text.components(separatedBy: "\n\n"), + text: text, media: media) - #warning("Treat post text as markdown") } private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice) { - let posts: [FeedEntryData] = posts.map(makePostData) + let posts: [FeedEntryData] = posts.map { makePostData(post: $0, results: source.results) } let feedPageGenerator = FeedPageGenerator(content: source.content, results: source.results) diff --git a/CHDataManagement/Page Elements/FeedEntry.swift b/CHDataManagement/Page Elements/FeedEntry.swift index 3e94e09..6ff7853 100644 --- a/CHDataManagement/Page Elements/FeedEntry.swift +++ b/CHDataManagement/Page Elements/FeedEntry.swift @@ -35,9 +35,7 @@ struct FeedEntry: HtmlProducer { TagList(tags: data.tags).populate(&result) ContentLabels(labels: data.labels).populate(&result) - for paragraph in data.text { - result += "

\(paragraph)

" - } + result += data.text if let url = data.link { result += "" } diff --git a/CHDataManagement/Page Elements/FeedEntryData.swift b/CHDataManagement/Page Elements/FeedEntryData.swift index a814a6b..7a7937a 100644 --- a/CHDataManagement/Page Elements/FeedEntryData.swift +++ b/CHDataManagement/Page Elements/FeedEntryData.swift @@ -13,11 +13,11 @@ struct FeedEntryData { let labels: [ContentLabel] - let text: [String] + let text: String let media: Media? - init(entryId: String, title: String?, textAboveTitle: String, link: Link?, tags: [Tag], labels: [ContentLabel], text: [String], media: Media?) { + init(entryId: String, title: String?, textAboveTitle: String, link: Link?, tags: [Tag], labels: [ContentLabel], text: String, media: Media?) { self.entryId = entryId self.title = title self.textAboveTitle = textAboveTitle