From 8cc80e01709f91893f30c827f6ff12acae1c5c63 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Fri, 4 Aug 2023 02:07:21 +0300 Subject: [PATCH] fix: lint-issues --- Package.resolved | 4 +- Package.swift | 2 +- .../Decoder/BaseURLSite.swift | 13 +- .../Decoder/PostsExportDecoder.swift | 20 --- .../Decoder/SitesExportDecoder.swift | 20 +++ ...oder.swift => SitesExportSynDecoder.swift} | 32 ++--- .../Decoder/SynDecoder.swift | 34 +---- .../Decoder/WordPressDecoder.swift | 8 +- .../Decoder/WordPressSite.swift | 119 ++++++++++++----- .../Extensions/NSRegularExpression.swift | 28 ---- .../Extensions/RSSChannel.swift | 8 ++ .../Extensions/String.swift | 1 - .../ContributeWordPress/HTMLtoMarkdown.swift | 5 +- .../Images/AssetDownloader.swift | 27 +++- .../Images/AssetImport.swift | 90 +++++++++---- .../Images/Downloader.swift | 4 +- .../MarkdownProcessor.swift | 120 ++++++++++-------- .../ProcessorSettings.swift | 37 ++++-- .../Generators/DynamicRedirectGenerator.swift | 29 +++-- .../Generators/RedirectListGenerator.swift | 8 +- .../Writers/DynamicRedirectFileWriter.swift | 13 +- .../Writers/RedirectFileWriter.swift | 10 +- 22 files changed, 356 insertions(+), 276 deletions(-) delete mode 100644 Sources/ContributeWordPress/Decoder/PostsExportDecoder.swift create mode 100644 Sources/ContributeWordPress/Decoder/SitesExportDecoder.swift rename Sources/ContributeWordPress/Decoder/{PostsExportSynDecoder.swift => SitesExportSynDecoder.swift} (65%) delete mode 100644 Sources/ContributeWordPress/Extensions/NSRegularExpression.swift create mode 100644 Sources/ContributeWordPress/Extensions/RSSChannel.swift delete mode 100644 Sources/ContributeWordPress/Extensions/String.swift diff --git a/Package.resolved b/Package.resolved index f55b0a01..56cce721 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/brightdigit/Contribute.git", "state" : { - "revision" : "2dbdcdc99861f589639f1828b10ec3c459323639", - "version" : "1.0.0-alpha.2" + "branch" : "v1.0.0", + "revision" : "72290ab018d72870cf243fe99366fab63a7ef979" } }, { diff --git a/Package.swift b/Package.swift index 127e1d94..54685976 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/brightdigit/Contribute.git", - from: "1.0.0-alpha.2" + branch: "v1.0.0" ), .package( url: "https://github.com/brightdigit/SyndiKit.git", diff --git a/Sources/ContributeWordPress/Decoder/BaseURLSite.swift b/Sources/ContributeWordPress/Decoder/BaseURLSite.swift index bb8a637f..b71e627b 100644 --- a/Sources/ContributeWordPress/Decoder/BaseURLSite.swift +++ b/Sources/ContributeWordPress/Decoder/BaseURLSite.swift @@ -1,19 +1,18 @@ import Foundation import SyndiKit -protocol BaseURLSite { +/// A protocol representing a site with a base URL. +public protocol BaseURLSite { + /// The main URL of the site. var link: URL { get } + + /// The base URL of the blog, if available. var baseBlogURL: URL? { get } } extension BaseURLSite { + /// The base URL of the site. public var baseURL: URL { baseBlogURL ?? link } } - -extension RSSChannel: BaseURLSite { - var baseBlogURL: URL? { - wpBaseBlogURL - } -} diff --git a/Sources/ContributeWordPress/Decoder/PostsExportDecoder.swift b/Sources/ContributeWordPress/Decoder/PostsExportDecoder.swift deleted file mode 100644 index fee8729a..00000000 --- a/Sources/ContributeWordPress/Decoder/PostsExportDecoder.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import SyndiKit - -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -/// A type to explicitly give a type for section name -public typealias SectionName = String - -/// A protocol for decoding WordPress posts from exports. -public protocol PostsExportDecoder { - /// Returns a dictionary of WordPress posts keyed by the filename of - /// the export file as section name. - /// - /// - Parameter directoryURL: The URL of the directory containing the exports. - /// - Returns: A dictionary of WordPress posts keyed by section name. - /// - Throws: An error if posts couldn't be extracted from any of the export files. - func posts(fromExportsAt directoryURL: URL) throws -> [SectionName: WordPressSite] -} diff --git a/Sources/ContributeWordPress/Decoder/SitesExportDecoder.swift b/Sources/ContributeWordPress/Decoder/SitesExportDecoder.swift new file mode 100644 index 00000000..4cee7d8b --- /dev/null +++ b/Sources/ContributeWordPress/Decoder/SitesExportDecoder.swift @@ -0,0 +1,20 @@ +import Foundation +import SyndiKit + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +/// A typealias that represents the section name for the blog +public typealias SectionName = String + +/// A protocol for decoding WordPress sites from exports. +public protocol SitesExportDecoder { + /// Returns a dictionary of WordPress sites keyed by the filename of + /// the export file as section name. + /// + /// - Parameter directoryURL: The URL of the directory containing the exports. + /// - Returns: A dictionary of WordPress sites keyed by section name. + /// - Throws: An error if sites couldn't be extracted from any of the export files. + func sites(fromExportsAt directoryURL: URL) throws -> [SectionName: WordPressSite] +} diff --git a/Sources/ContributeWordPress/Decoder/PostsExportSynDecoder.swift b/Sources/ContributeWordPress/Decoder/SitesExportSynDecoder.swift similarity index 65% rename from Sources/ContributeWordPress/Decoder/PostsExportSynDecoder.swift rename to Sources/ContributeWordPress/Decoder/SitesExportSynDecoder.swift index 7dea67b2..fb194f0d 100644 --- a/Sources/ContributeWordPress/Decoder/PostsExportSynDecoder.swift +++ b/Sources/ContributeWordPress/Decoder/SitesExportSynDecoder.swift @@ -7,7 +7,7 @@ import SyndiKit #endif /// A type that decodes WordPress export files using the `SynDecoder`. -public struct PostsExportSynDecoder: PostsExportDecoder { +public struct SitesExportSynDecoder: SitesExportDecoder { /// The decoder used to decode the WordPress export file. private let decoder: WordPressDecoder = SynDecoder() @@ -21,48 +21,44 @@ public struct PostsExportSynDecoder: PostsExportDecoder { public init() {} - /// The method decodes posts from the given file URL. + /// The method decodes `WordPressSite` from the given file URL. /// /// - Parameters: - /// - url: The URL of file containing WordPress posts to be decoded. + /// - url: The URL of export file containing the WordPress site with its posts. /// - decoder: The WordPress decoder. - /// - Returns: An array of WordPress posts. - /// - Throws: An error of posts couldn't be decoded from the given file.. - private static func postsFromURL( + /// - Returns: The decoded WordPress site, or nil if decoding failed. + /// - Throws: An error of site couldn't be decoded from the given file.. + private static func siteFromURL( _ url: URL, using decoder: WordPressDecoder ) throws -> WordPressSite? { let data = try Data(contentsOf: url) - return try decoder.decodePosts(fromData: data, allowInvalidCharacters: true) + return try decoder.decodeSites(fromData: data, allowInvalidCharacters: true) } - /// Returns a dictionary of WordPress posts keyed by filename. + /// Returns a dictionary of WordPress sites keyed by filename. /// /// - Parameter directoryURL: The URL of the directory containing the exports. - /// - Returns: A dictionary of WordPress posts keyed by section name. - /// - Throws: An error if posts couldn't be extracted from any of the export files. - public func posts( + /// - Returns: A dictionary of WordPress sites keyed by section name. + /// - Throws: An error if sites couldn't be extracted from any of the export files. + public func sites( fromExportsAt directoryURL: URL ) throws -> [SectionName: WordPressSite] { let files = try fileURLsFromDirectory(directoryURL) let feedPairs = try files.map { url -> (String, WordPressSite?) in - try (self.keyFromURL(url), Self.postsFromURL(url, using: decoder)) + try (self.keyFromURL(url), Self.siteFromURL(url, using: decoder)) } return Dictionary(uniqueKeysWithValues: feedPairs).compactMapValues { $0 } } } -extension PostsExportSynDecoder { +extension SitesExportSynDecoder { /// A default logic for finding all files found at given directory. /// - /// If the `directoryURL` is not valid directory, this method - /// throws `ImportError.directory(:_)` error. - /// /// - Parameter directoryURL: The directory URL. - /// - Returns: An array of export file URLs that can be found in the given directory. - /// - Throws: An error if the directory could not be enumerated. + /// - Returns: An array of URLs for export files found in the given directory. private static func defaultFileURLs(atDirectory directoryURL: URL) -> [URL] { let enumerator = FileManager.default.enumerator( at: directoryURL, diff --git a/Sources/ContributeWordPress/Decoder/SynDecoder.swift b/Sources/ContributeWordPress/Decoder/SynDecoder.swift index 112f71f5..bf83b75e 100644 --- a/Sources/ContributeWordPress/Decoder/SynDecoder.swift +++ b/Sources/ContributeWordPress/Decoder/SynDecoder.swift @@ -5,39 +5,19 @@ import SyndiKit import FoundationNetworking #endif -/// An extension that enables SynDecoder to decode WordPress posts. +/// An extension that enables SynDecoder to decode WordPress sites. extension SynDecoder: WordPressDecoder { - public func decodePosts( - fromData data: Data, - allowInvalidCharacters: Bool - ) throws -> WordPressSite? { - let text = String(bytes: data, encoding: .utf8)? - .replacingOccurrences(of: "\u{10}", with: "") - .data(using: .utf8, allowLossyConversion: true) - - let newData: Data - if let text = text, allowInvalidCharacters { - newData = text - } else { - newData = data - } - - let feed = try decode(newData) - let rss = feed as? RSSFeed - return try rss.map(\.channel).map(WordPressSite.init) - } - - /// Decodes an array of WordPress posts from the given data. + /// Decodes an array of WordPress sites from the given data. /// /// - Parameters: /// - data: The data to decode. - /// - allowInvalidCharacters: Whether to allow invalid characters in decoded data. - /// - Returns: An array of WordPress posts, or nil if decoding failed. + /// - allowInvalidCharacters: Whether to allow invalid characters in the data. + /// - Returns: The decoded WordPress site, or nil if decoding failed. /// - Throws: An error if data couldn't be decoded. - public func decodePosts( + public func decodeSites( fromData data: Data, allowInvalidCharacters: Bool - ) throws -> [WordPressPost]? { + ) throws -> WordPressSite? { let text = String(bytes: data, encoding: .utf8)? .replacingOccurrences(of: "\u{10}", with: "") .data(using: .utf8, allowLossyConversion: true) @@ -51,6 +31,6 @@ extension SynDecoder: WordPressDecoder { let feed = try decode(newData) let rss = feed as? RSSFeed - return rss?.channel.items.compactMap(\.wpPost) + return try rss.map(\.channel).map(WordPressSite.init) } } diff --git a/Sources/ContributeWordPress/Decoder/WordPressDecoder.swift b/Sources/ContributeWordPress/Decoder/WordPressDecoder.swift index 7b882e26..5715ae70 100644 --- a/Sources/ContributeWordPress/Decoder/WordPressDecoder.swift +++ b/Sources/ContributeWordPress/Decoder/WordPressDecoder.swift @@ -5,16 +5,16 @@ import SyndiKit import FoundationNetworking #endif -/// A protocol for decoding WordPress posts from raw data. +/// A protocol for decoding WordPress sites from raw data. public protocol WordPressDecoder { - /// Decodes an array of WordPress posts from the given data. + /// Decodes an array of WordPress sites from the given data. /// /// - Parameters: /// - data: The data to decode. /// - allowInvalidCharacters: Whether to allow invalid characters in the data. - /// - Returns: An array of WordPress posts, or nil if decoding failed. + /// - Returns: An array of WordPress sites, or nil if decoding failed. /// - Throws: An error if data couldn't be decoded. - func decodePosts( + func decodeSites( fromData data: Data, allowInvalidCharacters: Bool ) throws -> WordPressSite? diff --git a/Sources/ContributeWordPress/Decoder/WordPressSite.swift b/Sources/ContributeWordPress/Decoder/WordPressSite.swift index e5f2d8c7..20909dd0 100644 --- a/Sources/ContributeWordPress/Decoder/WordPressSite.swift +++ b/Sources/ContributeWordPress/Decoder/WordPressSite.swift @@ -1,8 +1,53 @@ import Foundation import SyndiKit +/// A struct that represents a WordPress site. public struct WordPressSite: BaseURLSite { public static let wpContentUploadsRelativePath = "wp-content/uploads" + + /// The name of the channel. + public let title: String + + /// The URL to the HTML website corresponding to the channel. + public let link: URL + + /// Phrase or sentence describing the channel. + public let description: String? + + /// The publication date and time of the feed's content. + public let pubDate: Date? + + /// The categories associated with the site. + public let categories: [WordPressElements.Category] + + /// The tags associated with the site. + public let tags: [WordPressElements.Tag] + + /// The URL for site hosting this blog. + public let baseSiteURL: URL? + + /// The blog URL for this WordPress site. + public let baseBlogURL: URL? + + /// The posts associated with the site. + public let posts: [WordPressPost] + + internal let assetURLRegex: NSRegularExpression + + /// Initializes a `WordPressSite` instance. + /// + /// - Parameters: + /// - title: The name of the channel. + /// - link: The URL to the HTML website corresponding to the channel. + /// - description: Phrase or sentence describing the channel. + /// - pubDate: The publication date and time of the feed's content. + /// - categories: The categories associated with the site. + /// - tags: The tags associated with the site. + /// - baseSiteURL: The base site URL. + /// - baseBlogURL: The base blog URL. + /// - posts: The posts associated with the site. + /// - assetURLRegex: The regular expression for matching asset urls. + // swiftlint:disable:next function_default_parameter_at_end public init( title: String, link: URL, @@ -13,7 +58,7 @@ public struct WordPressSite: BaseURLSite { baseSiteURL: URL? = nil, baseBlogURL: URL? = nil, posts: [WordPressPost], - assetsImagesRegex: NSRegularExpression + assetURLRegex: NSRegularExpression ) { self.title = title self.link = link @@ -24,32 +69,23 @@ public struct WordPressSite: BaseURLSite { self.baseSiteURL = baseSiteURL self.baseBlogURL = baseBlogURL self.posts = posts - self.assetsImagesRegex = assetsImagesRegex + self.assetURLRegex = assetURLRegex } - - /// The name of the channel. - public let title: String - - /// The URL to the HTML website corresponding to the channel. - public let link: URL - - /// Phrase or sentence describing the channel. - public let description: String? - - /// indicates the publication date and time of the feed's content - public let pubDate: Date? - - public let categories: [WordPressElements.Category] - public let tags: [WordPressElements.Tag] - public let baseSiteURL: URL? - public let baseBlogURL: URL? - - public let posts: [WordPressPost] - - internal let assetsImagesRegex: NSRegularExpression } extension WordPressSite { + /// Returns the import directory name. + public var importDirectoryName: String { + baseURL.firstHostComponent ?? + baseSiteURL?.firstHostComponent ?? + "default" + } + + /// Initializes a `WordPressSite` instance from an ``RSSChannel``. + /// + /// - Parameters: + /// - channel: The `RSSChannel` instance. + /// - Throws: An error if initialization fails. public init(channel: RSSChannel) throws { try self.init( channel: channel, @@ -57,11 +93,18 @@ extension WordPressSite { ) } + /// Initializes a `WordPressSite` instance from an ``RSSChannel`` with + /// a relative resource path. + /// + /// - Parameters: + /// - channel: The RSSChannel instance. + /// - relativeResourcePath: The relative resource path. + /// - Throws: An error if initialization fails. public init( channel: RSSChannel, relativeResourcePath: String ) throws { - let assetsImagesRegex = try Self.defaultAssetsImagesRegex( + let assetURLRegex = try Self.defaultAssetURLRegex( forSite: channel, relativeResourcePath: relativeResourcePath ) @@ -73,30 +116,38 @@ extension WordPressSite { baseSiteURL: channel.wpBaseSiteURL, baseBlogURL: channel.wpBaseBlogURL, posts: channel.items.compactMap(\.wpPost), - assetsImagesRegex: assetsImagesRegex + assetURLRegex: assetURLRegex ) } - static func defaultAssetsImagesRegex( + /// Returns the default regular expression for matching asset urls. + /// + /// - Parameters: + /// - site: The `BaseURLSite` instance. + /// - relativeResourcePath: The relative resource path where assets would be located. + /// - Returns: The default regular expression for matching asset urls. + /// - Throws: An error if the regular expression cannot be created. + public static func defaultAssetURLRegex( forSite site: BaseURLSite, relativeResourcePath: String = WordPressSite.wpContentUploadsRelativePath ) throws -> NSRegularExpression { - try Self.defaultAssetsImagesRegex( + try Self.defaultAssetURLRegex( forAssetSiteURL: site.baseURL, relativeResourcePath: relativeResourcePath ) } - static func defaultAssetsImagesRegex( + /// Returns the default regular expression for matching asset url. + /// + /// - Parameters: + /// - assetSiteURL: The asset site URL. + /// - relativeResourcePath: The relative resource path. + /// - Returns: The default regular expression for matching asset url. + /// - Throws: An error if the regular expression cannot be created. + public static func defaultAssetURLRegex( forAssetSiteURL assetSiteURL: URL, relativeResourcePath: String = WordPressSite.wpContentUploadsRelativePath ) throws -> NSRegularExpression { try NSRegularExpression(pattern: "\(assetSiteURL)/\(relativeResourcePath)([^\"]+)") } - - var importDirectoryName: String { - baseURL.firstHostComponent ?? - baseSiteURL?.firstHostComponent ?? - "default" - } } diff --git a/Sources/ContributeWordPress/Extensions/NSRegularExpression.swift b/Sources/ContributeWordPress/Extensions/NSRegularExpression.swift deleted file mode 100644 index 5c0bb9a9..00000000 --- a/Sources/ContributeWordPress/Extensions/NSRegularExpression.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import SyndiKit - -extension NSRegularExpression { - internal func matchUrls( - in posts: [WordPressPost] - ) -> [(sourceURL: URL, post: WordPressPost)] { - posts - .flatMap { post in - self - .matches( - in: post.body, - range: NSRange(post.body.startIndex..., in: post.body) - ) - .compactMap { match in - guard let range = Range(match.range, in: post.body) else { - return nil - } - - guard let url = URL(string: String(post.body[range])) else { - return nil - } - - return (sourceURL: url, post: post) - } - } - } -} diff --git a/Sources/ContributeWordPress/Extensions/RSSChannel.swift b/Sources/ContributeWordPress/Extensions/RSSChannel.swift new file mode 100644 index 00000000..d0fe0cdc --- /dev/null +++ b/Sources/ContributeWordPress/Extensions/RSSChannel.swift @@ -0,0 +1,8 @@ +import Foundation +import SyndiKit + +extension RSSChannel: BaseURLSite { + public var baseBlogURL: URL? { + wpBaseBlogURL + } +} diff --git a/Sources/ContributeWordPress/Extensions/String.swift b/Sources/ContributeWordPress/Extensions/String.swift deleted file mode 100644 index fecc4ab4..00000000 --- a/Sources/ContributeWordPress/Extensions/String.swift +++ /dev/null @@ -1 +0,0 @@ -import Foundation diff --git a/Sources/ContributeWordPress/HTMLtoMarkdown.swift b/Sources/ContributeWordPress/HTMLtoMarkdown.swift index 30878700..b2a31b8c 100644 --- a/Sources/ContributeWordPress/HTMLtoMarkdown.swift +++ b/Sources/ContributeWordPress/HTMLtoMarkdown.swift @@ -1,13 +1,14 @@ import Contribute import Foundation -@available(*, deprecated, message: "Use `HTMLtoMarkdown` in `Contrbute`.") +@available(*, deprecated, message: "Use `HTMLtoMarkdown` in `Contribute`.") public struct HTMLtoMarkdown: MarkdownGenerator { + public let markdownFromHTML: (String) throws -> String + public init(_ markdownFromHTML: @escaping (String) throws -> String) { self.markdownFromHTML = markdownFromHTML } - public let markdownFromHTML: (String) throws -> String public func markdown(fromHTML htmlString: String) throws -> String { try markdownFromHTML(htmlString) } diff --git a/Sources/ContributeWordPress/Images/AssetDownloader.swift b/Sources/ContributeWordPress/Images/AssetDownloader.swift index e8823ac1..e4c0a8e7 100644 --- a/Sources/ContributeWordPress/Images/AssetDownloader.swift +++ b/Sources/ContributeWordPress/Images/AssetDownloader.swift @@ -32,10 +32,31 @@ public struct AssetDownloader: Downloader { return } - let group = DispatchGroup() + try downloadUsingGroupDispatch( + assets: assets, + allowsOverwrites: allowsOverwrites + ) { errors in + guard errors.isEmpty else { + throw WordPressError.assetDownloadErrors(errors) + } + } + } + /// A helper function to download assets using DispatchGroup. + /// + /// - Parameters: + /// - assets: The imported assets to be downloaded. + /// - allowsOverwrites: To allow overwriting existing assets. + /// - completion: A completion handler called with errors mapped to asset source url. + private func downloadUsingGroupDispatch( + assets: [AssetImport], + allowsOverwrites: Bool, + completion: (_ errors: [URL: Error]) throws -> Void + ) throws { var errors = [URL: Error]() + let group = DispatchGroup() + for asset in assets { group.enter() @@ -53,8 +74,6 @@ public struct AssetDownloader: Downloader { group.wait() - guard errors.isEmpty else { - throw WordPressError.assetDownloadErrors(errors) - } + try completion(errors) } } diff --git a/Sources/ContributeWordPress/Images/AssetImport.swift b/Sources/ContributeWordPress/Images/AssetImport.swift index 0bb15d32..755e00f8 100644 --- a/Sources/ContributeWordPress/Images/AssetImport.swift +++ b/Sources/ContributeWordPress/Images/AssetImport.swift @@ -5,10 +5,13 @@ import SyndiKit import FoundationNetworking #endif +/// A typealias that represents a factory function for creating asset imports. +public typealias AssetImportFactory = + (_ site: WordPressSite, _ importSettings: ProcessorSettings) -> [AssetImport] + /// A type that holds information about an asset imported from a `WordPressPost`. public struct AssetImport: Hashable { - /// The original URL of the asset. - /// /// The source `URL` from where asset will be imported. + /// The source `URL` from where asset will be imported. public let fromURL: URL /// The destination `URL` to where asset will be imported. @@ -17,15 +20,16 @@ public struct AssetImport: Hashable { /// The featured path for the asset. public let featuredPath: String - /// The id of `WordPressPost` to which the asset belongs. + /// The ID of `WordPressPost` to which the asset belongs. public let parentID: Int - /// Initializes a new `WordPressAssetImport` instance. + /// Initializes a new `AssetImport` instance. /// /// - Parameters: - /// - importFromURL: The original URL of the asset. - /// - parentID: The id of `WordPressPost` to which the asset belongs. - /// - newPath: The new path where the asset will be saved. + /// - importFromURL: The source URL of the asset. + /// - importAtURL: The destination URL to where the asset will be imported. + /// - featuredPath: The featured path for the asset. + /// - parentID: The ID of the `WordPressPost` to which the asset belongs. internal init( importFromURL: URL, importAtURL: URL, @@ -38,21 +42,29 @@ public struct AssetImport: Hashable { self.parentID = parentID } - public init?( + /// Initializes a new `AssetImport` instance for a `WordPressPost`. + /// + /// - Parameters: + /// - post: The `WordPressPost` instance. + /// - sourceURL: The source URL from where the asset will be imported. + /// - assetRoot: The root path for the assets. + /// - resourcesPathURL: The URL of the resources path. + /// - importPathURL: The URL of the import path. + public init( forPost post: WordPressPost, sourceURL: URL, assetRoot: String, - resourcePathURL: URL, + resourcesPathURL: URL, importPathURL: URL? ) { let featuredPath = sourceURL.path .replacingOccurrences( - of: "/wp-content/uploads", + of: ["", WordPressSite.wpContentUploadsRelativePath].joined(separator: "/"), with: assetRoot ) .replacingOccurrences(of: "//", with: "/") - let destinationURL = resourcePathURL.appendingPathComponent(featuredPath) + let destinationURL = resourcesPathURL.appendingPathComponent(featuredPath) self.init( importFromURL: importPathURL?.appendingPathComponent(sourceURL.path) ?? sourceURL, @@ -64,6 +76,12 @@ public struct AssetImport: Hashable { } extension AssetImport { + /// Extracts asset imports from a ``WordPressSite`` using the specified import settings. + /// + /// - Parameters: + /// - site: The WordPressSite instance. + /// - importSettings: The ProcessorSettings instance. + /// - Returns: An array of AssetImport instances. public static func extractAssetImports( from site: WordPressSite, using importSettings: ProcessorSettings @@ -73,19 +91,43 @@ extension AssetImport { importSettings.assetRelativePath, site.importDirectoryName ].joined(separator: "/") - return site.assetsImagesRegex - .matchUrls(in: site.posts) - .compactMap { match in - AssetImport( - forPost: match.post, - sourceURL: match.sourceURL, - assetRoot: assetRoot, - resourcePathURL: importSettings.resourcesPathURL, - importPathURL: importSettings.importAssetPathURL - ) + return matchUrls( + in: site.posts, + using: site.assetURLRegex + ) + .compactMap { match in + AssetImport( + forPost: match.post, + sourceURL: match.sourceURL, + assetRoot: assetRoot, + resourcesPathURL: importSettings.resourcesPathURL, + importPathURL: importSettings.importAssetPathURL + ) + } + } + + private static func matchUrls( + in posts: [WordPressPost], + using regex: NSRegularExpression + ) -> [(sourceURL: URL, post: WordPressPost)] { + posts + .flatMap { post in + regex + .matches( + in: post.body, + range: NSRange(post.body.startIndex..., in: post.body) + ) + .compactMap { match in + guard let range = Range(match.range, in: post.body) else { + return nil + } + + guard let url = URL(string: String(post.body[range])) else { + return nil + } + + return (sourceURL: url, post: post) + } } } } - -public typealias AssetImportFactory = - (WordPressSite, ProcessorSettings) -> [AssetImport] diff --git a/Sources/ContributeWordPress/Images/Downloader.swift b/Sources/ContributeWordPress/Images/Downloader.swift index a87b880f..0f378883 100644 --- a/Sources/ContributeWordPress/Images/Downloader.swift +++ b/Sources/ContributeWordPress/Images/Downloader.swift @@ -1,9 +1,9 @@ import Foundation import SyndiKit -/// A protocol for downloading assets from WordPress posts. +/// A protocol for downloading asset imports. public protocol Downloader { - /// Downloads assets. + /// Downloads asset imports. /// /// - Parameters: /// - assets: The imported assets to be downloaded. diff --git a/Sources/ContributeWordPress/MarkdownProcessor.swift b/Sources/ContributeWordPress/MarkdownProcessor.swift index bab93c8b..497b828d 100644 --- a/Sources/ContributeWordPress/MarkdownProcessor.swift +++ b/Sources/ContributeWordPress/MarkdownProcessor.swift @@ -7,13 +7,13 @@ import Yams import FoundationNetworking #endif -/// A type that processes WordPress posts and generates Markdown files for each. +/// A type that processes WordPress sites and generates Markdowns for their posts. public struct MarkdownProcessor< ContentURLGeneratorType: ContentURLGenerator, MarkdownContentBuilderType: MarkdownContentBuilder > where ContentURLGeneratorType.SourceType == Source, MarkdownContentBuilderType.SourceType == Source { - private let exportDecoder: PostsExportDecoder + private let exportDecoder: SitesExportDecoder private let assetDownloader: Downloader private let redirectWriter: RedirectFileWriter private let destinationURLGenerator: ContentURLGeneratorType @@ -21,15 +21,18 @@ public struct MarkdownProcessor< private let postFilters: [PostFilter] private let assetImportFactory: AssetImportFactory - /// Initializes a new `WordPressMarkdownProcessor` instance. + /// Initializes a new `MarkdownProcessor` instance. /// /// - Parameters: - /// - redirectListGenerator: The redirect list generator. + /// - exportDecoder: The export decoder. + /// - redirectWriter: The redirect file writer. + /// - assetDownloader: The asset downloader. /// - destinationURLGenerator: The content URL generator. /// - contentBuilder: The Markdown content builder. /// - postFilters: The post filters. + /// - assetImportFactory: The asset import factory. public init( - exportDecoder: PostsExportDecoder, + exportDecoder: SitesExportDecoder, redirectWriter: RedirectFileWriter, assetDownloader: Downloader, destinationURLGenerator: ContentURLGeneratorType, @@ -46,27 +49,47 @@ public struct MarkdownProcessor< self.assetImportFactory = assetImportFactory } - /// Writes all posts to the given content path URL. - /// - /// - Parameters: - /// - allPosts: A dictionary of WordPress posts keyed by section name. - /// - assets: An array of assets that were imported from WordPress posts. - /// - assetRoot: The root path of the assets directory. - /// - contentPathURL: The content path URL. - /// - htmlToMarkdown: A function that converts HTML to Markdown. - /// - Throws: An error if an error occurs. + private func decodeExports( + at directoryURL: URL + ) throws -> [SectionName: WordPressSite] { + try exportDecoder.sites(fromExportsAt: directoryURL) + } + + private func writeRedirects( + fromSites sites: [SectionName: WordPressSite], + inDirectory directoryURL: URL + ) throws { + try redirectWriter.writeRedirects(fromSites: sites, inDirectory: directoryURL) + } + + private func retrieveAssetImports( + fromSites sites: [SectionName: WordPressSite], + using importSettings: ProcessorSettings + ) throws -> [AssetImport] { + let assetImports: [AssetImport] = sites.values.flatMap { + assetImportFactory($0, importSettings) + } + + try assetDownloader.download( + assets: assetImports, + dryRun: importSettings.skipDownload, + allowsOverwrites: importSettings.overwriteAssets + ) + + return assetImports + } + private func writeAllPosts( - _ allPosts: [SectionName: WordPressSite], + fromSites sites: [SectionName: WordPressSite], withAssets assets: [AssetImport], - to contentPathURL: URL, + inContentDirectory contentDirectoryURL: URL, using htmlToMarkdown: @escaping (String) throws -> String, htmlFromSitePost: ((WordPressSite) -> ((WordPressPost) -> String))? = nil ) throws { - try allPosts.forEach { sectionName, posts in - try FileManager.createDirectory(withName: sectionName, in: contentPathURL) - let htmlFromPost = htmlFromSitePost?(posts) - try posts - .posts + try sites.forEach { sectionName, site in + try FileManager.createDirectory(withName: sectionName, in: contentDirectoryURL) + let htmlFromPost = htmlFromSitePost?(site) + try site.posts .filter(self.postFilters.postSatisfiesAll) .map { post in (post, assets.first { $0.parentID == post.ID }) } .forEach { post, featuredImage in @@ -77,7 +100,7 @@ public struct MarkdownProcessor< featuredImage: featuredImage.map(\.featuredPath), htmlFromPost: htmlFromPost ), - atContentPathURL: contentPathURL, + atContentPathURL: contentDirectoryURL, basedOn: self.destinationURLGenerator, using: htmlToMarkdown ) @@ -92,34 +115,20 @@ public struct MarkdownProcessor< public func begin( withSettings settings: ProcessorSettings ) throws { - // 1. Decodes WordPress posts from exports directory. - let allPosts = try exportDecoder.posts(fromExportsAt: settings.exportsDirectoryURL) + // 1. Decodes WordPress site from exports directory. + let allSites = try decodeExports(at: settings.exportsDirectoryURL) // 2. Writes redirects for all decoded WordPress posts. - try redirectWriter.writeRedirects( - fromPosts: allPosts, - inDirectory: settings.resourcesPathURL - ) - - #warning("Should this just use `.filter(self.postFilters.postSatisfiesAll)`?") - - // 4. Build asset imports from all posts - let assetsImports: [AssetImport] = allPosts.values.flatMap { - assetImportFactory($0, settings) - } + try writeRedirects(fromSites: allSites, inDirectory: settings.resourcesPathURL) - // 5. Download all assets - try assetDownloader.download( - assets: assetsImports, - dryRun: settings.skipDownload, - allowsOverwrites: settings.overwriteAssets - ) + // 3. Extract and download asset imports for all WordPress sites. + let assetImports = try retrieveAssetImports(fromSites: allSites, using: settings) - // 7. Starts writing the markdown files for all WordPress post, + // 4. Starts writing the markdown files for all WordPress posts for each site. try writeAllPosts( - allPosts, - withAssets: assetsImports, - to: settings.contentPathURL, + fromSites: allSites, + withAssets: assetImports, + inContentDirectory: settings.contentPathURL, using: settings.markdownFrom(html:), htmlFromSitePost: settings.htmlFromPost ) @@ -127,31 +136,32 @@ public struct MarkdownProcessor< } extension MarkdownProcessor { - /// Initializes a new `WordPressMarkdownProcessor` instance with default values for - /// - `PostsExportDecoder` - /// - `Downloader` - /// - `ContentURLGeneratorType` - /// - `MarkdownContentBuilderType`. + /// Initializes a new `WordPressMarkdownProcessor` instance. /// /// It will be created with a `DynamicRedirectFileWriter` instance that makes use of /// `DynamicRedirectGenerator` and the provided `RedirectFormatter` to generate /// the redirects and write them into a file. /// /// - Parameters: - /// - redirectFromatter: The formatter to format redirect items into a file. + /// - redirectFormatter: The formatter used to make redirects. /// - postFilters: The post filters. + /// - exportDecoder: The decoder used to decode posts . + /// - assetDownloader: The asset downloader. + /// - destinationURLGenerator: The destination URL generator. + /// - contentBuilder: The content builder. + /// - assetImportFactory: The asset import factory. public init( redirectFromatter: RedirectFormatter, postFilters: [PostFilter], - exportDecoder: PostsExportDecoder = PostsExportSynDecoder(), - assetImportFactory: @escaping AssetImportFactory = - AssetImport.extractAssetImports(from:using:), + exportDecoder: SitesExportDecoder = SitesExportSynDecoder(), assetDownloader: Downloader = AssetDownloader(), destinationURLGenerator: ContentURLGeneratorType = .init(), contentBuilder: MarkdownContentBuilderType = .init( frontMatterExporter: .init(translator: SpecFrontMatterTranslator()), markdownExtractor: FilteredHTMLMarkdownExtractor() - ) + ), + assetImportFactory: @escaping AssetImportFactory = + AssetImport.extractAssetImports(from:using:) ) where ContentURLGeneratorType == SectionContentURLGenerator, MarkdownContentBuilderType == MarkdownContentYAMLBuilder< Source, diff --git a/Sources/ContributeWordPress/ProcessorSettings.swift b/Sources/ContributeWordPress/ProcessorSettings.swift index 36d6e846..f077cd34 100644 --- a/Sources/ContributeWordPress/ProcessorSettings.swift +++ b/Sources/ContributeWordPress/ProcessorSettings.swift @@ -18,7 +18,7 @@ public protocol ProcessorSettings { /// Example: /..../WordPress/exports/ var exportsDirectoryURL: URL { get } - /// The URL of the directory that the posts assets should be imported. + /// The URL of the directory that the assets should be imported. /// /// Example: /..../WordPress/html/ var importAssetPathURL: URL? { get } @@ -32,28 +32,37 @@ public protocol ProcessorSettings { /// Name of directory to store assets relative to ``resourcesPathURL`` var assetRelativePath: String { get } - /// Converts the given HTML to Markdown. + /// The relative path for uploads. + var uploadsRelativePath: String { get } + + /// Converts the given HTML string to Markdown string. /// /// - Parameter html: The HTML string to convert. - /// - Returns: The Markdown representation of the HTML. + /// - Returns: The Markdown string representation of the HTML. func markdownFrom(html: String) throws -> String - /// Converts the given HTML to Markdown. + /// Converts the given `WordPressSite` to HTML string. /// - /// - Parameter html: The HTML string to convert. - /// - Returns: The Markdown representation of the HTML. + /// - Parameter site: The WordPress site to convert. + /// - Returns: The HTML string representation of the WordPress site. func htmlFromPost(_ site: WordPressSite) -> ((WordPressPost) -> String) - - var uploadsRelativePath: String { - get - } } extension ProcessorSettings { + /// The URL for the asset path located under `resourcesPathURL`. public var resourceAssetPathURL: URL { resourcesPathURL.appendingPathComponent(assetRelativePath) } + /// The relative path for uploads directory of WordPress content. + public var uploadsRelativePath: String { + WordPressSite.wpContentUploadsRelativePath + } + + /// Returns the asset directory path for the given site name. + /// + /// - Parameter siteName: The name of the site. + /// - Returns: The asset directory path. public func assetDirectoryPath(forSiteName siteName: String) -> String { let assetRelative = resourceAssetPathURL.relativePath( from: resourcesPathURL @@ -62,10 +71,10 @@ extension ProcessorSettings { return ["", assetRelative, siteName].joined(separator: "/") } - public var uploadsRelativePath: String { - WordPressSite.wpContentUploadsRelativePath - } - + /// Returns the HTML from the given post. + /// + /// - Parameter site: The WordPress site. + /// - Returns: The HTML representation of the post. public func htmlFromPost(_ site: WordPressSite) -> ((WordPressPost) -> String) { let assetPostURLSearchPrefix = site .baseURL diff --git a/Sources/ContributeWordPress/Redirects/Generators/DynamicRedirectGenerator.swift b/Sources/ContributeWordPress/Redirects/Generators/DynamicRedirectGenerator.swift index e00ed766..b6d3fa9c 100644 --- a/Sources/ContributeWordPress/Redirects/Generators/DynamicRedirectGenerator.swift +++ b/Sources/ContributeWordPress/Redirects/Generators/DynamicRedirectGenerator.swift @@ -3,9 +3,10 @@ import SyndiKit /// A type that provides dynamic implementation for generating redirects. /// /// It allows for flexible and customizable generation of redirects by providing closures -/// for post filtering and URL path generation. It dynamically determines which WordPress -/// posts should be included in the redirects and generates the redirect URL paths based -/// on the provided criteria. +/// for post filtering and URL path generation. +/// +/// It dynamically determines which WordPress posts should be included in the redirects +/// and generates the redirect URL paths based on the provided criteria. /// /// By using the `DynamicRedirectGenerator`, someone can define their own rules /// and logic for including or excluding specific WordPress posts and generating the @@ -40,22 +41,22 @@ public struct DynamicRedirectGenerator: RedirectListGenerator { self.init(postFilter: postFilters.postSatisfiesAll) } - /// Generates redirects from the given WordPress posts. + /// Generates redirects for each post from the given WordPress sites. /// /// - Parameter allPosts: A dictionary of WordPress posts keyed by section name. /// - Returns: An array of `RedirectItem` representing the redirects. public func redirects( - fromWordPressPosts allPosts: [SectionName: WordPressSite] + fromSites sites: [SectionName: WordPressSite] ) -> [RedirectItem] { - allPosts.flatMap { args -> [RedirectItem] in - let (dir, site) = args - - return site.posts.filter(self.postFilter).map { post in - RedirectItem( - fromURLPath: post.link.path, - redirectURLPath: self.urlPathGenerate(dir, post) - ) - } + sites.flatMap { dir, site -> [RedirectItem] in + site.posts + .filter(self.postFilter) + .map { post in + RedirectItem( + fromURLPath: post.link.path, + redirectURLPath: self.urlPathGenerate(dir, post) + ) + } } } } diff --git a/Sources/ContributeWordPress/Redirects/Generators/RedirectListGenerator.swift b/Sources/ContributeWordPress/Redirects/Generators/RedirectListGenerator.swift index b53f7c60..a15b4db9 100644 --- a/Sources/ContributeWordPress/Redirects/Generators/RedirectListGenerator.swift +++ b/Sources/ContributeWordPress/Redirects/Generators/RedirectListGenerator.swift @@ -5,13 +5,13 @@ import SyndiKit import FoundationNetworking #endif -/// A protocol for generating redirect items from WordPress posts. +/// A protocol for generating redirect items for each post of WordPress sites. public protocol RedirectListGenerator { - /// Generates redirects list from the given WordPress posts. + /// Generates redirects list for each post of the given WordPress sites. /// - /// - Parameter allPosts: A dictionary of WordPress posts keyed by section name. + /// - Parameter sites: A dictionary of WordPress sites keyed by section name. /// - Returns: An array of `RedirectItem` representing the redirects. func redirects( - fromWordPressPosts allPosts: [SectionName: WordPressSite] + fromSites sites: [SectionName: WordPressSite] ) -> [RedirectItem] } diff --git a/Sources/ContributeWordPress/Redirects/Writers/DynamicRedirectFileWriter.swift b/Sources/ContributeWordPress/Redirects/Writers/DynamicRedirectFileWriter.swift index bb704437..408e8b40 100644 --- a/Sources/ContributeWordPress/Redirects/Writers/DynamicRedirectFileWriter.swift +++ b/Sources/ContributeWordPress/Redirects/Writers/DynamicRedirectFileWriter.swift @@ -4,7 +4,7 @@ import SyndiKit /// A type that writes redirects from posts into a file. /// /// It uses an instance of `DynamicRedirectGenerator` to generate redirect items, -/// then using the provided `RedirectFormatter` it formats the redirects into a string +/// then uses the provided `RedirectFormatter` to format the redirects into a string /// whtich later will be written into a file in a given directory. public struct DynamicRedirectFileWriter: RedirectFileWriter { private let generator: RedirectListGenerator @@ -35,18 +35,11 @@ public struct DynamicRedirectFileWriter: RedirectFileWriter { formatter = redirectFromatter } - /// Generates redirects list from WordPress posts per section, then writes - /// them into a file at the given directory. - /// - /// - Parameters: - /// - posts: A dictionary of WordPress posts keyed by section name. - /// - directoryURL: The directory URL where the redirects file should be written. - /// - Throws: An error if the writing operation fails. public func writeRedirects( - fromPosts posts: [SectionName: WordPressSite], + fromSites sites: [SectionName: WordPressSite], inDirectory directoryURL: URL ) throws { - let redirects = generator.redirects(fromWordPressPosts: posts) + let redirects = generator.redirects(fromSites: sites) let redirectsText = formatter.formatRedirects(redirects) let redirectsPath = formatter.redirectsURL(basedOnResourcesDirectoryURL: directoryURL) try redirectsText.write(to: redirectsPath, atomically: true, encoding: .utf8) diff --git a/Sources/ContributeWordPress/Redirects/Writers/RedirectFileWriter.swift b/Sources/ContributeWordPress/Redirects/Writers/RedirectFileWriter.swift index 03105660..1e82bdd2 100644 --- a/Sources/ContributeWordPress/Redirects/Writers/RedirectFileWriter.swift +++ b/Sources/ContributeWordPress/Redirects/Writers/RedirectFileWriter.swift @@ -1,17 +1,17 @@ import Foundation import SyndiKit -/// A protocol for writing redirect items from WordPress posts. +/// A protocol for writing redirect items for each post of the WordPress sites. public protocol RedirectFileWriter { - /// Generates redirects list from WordPress posts per section, then writes - /// them into a file at the given directory. + /// Generates redirects list for each post of the WordPress sites, then writes them + /// into a file at the given directory. /// /// - Parameters: - /// - posts: A dictionary of WordPress posts keyed by section name. + /// - sites: A dictionary of WordPress sites keyed by section name. /// - directoryURL: The directory URL where the redirects file should be written. /// - Throws: An error if the writing operation fails. func writeRedirects( - fromPosts posts: [SectionName: WordPressSite], + fromSites sites: [SectionName: WordPressSite], inDirectory directoryURL: URL ) throws }