Convert Xcode project to swift package

This commit is contained in:
Christoph Hagen
2022-09-09 11:18:32 +02:00
parent 64db75fb44
commit 2a9061c1d6
54 changed files with 30 additions and 724 deletions

View File

@@ -0,0 +1,113 @@
import Foundation
struct HTMLElementsGenerator {
init() {
}
func make(title: String, suffix: String) -> String {
"\(title)<span class=\"suffix\">\(suffix)</span>"
}
func topBarWebsiteTitle(language: String, from page: Element) -> String {
guard let pathToRoot = page.pathToRoot else {
return Element.htmlPageName(for: language)
}
return pathToRoot + Element.htmlPagePathAddition(for: language)
}
func topBarLanguageButton(_ language: String) -> String {
"<a href=\"\(Element.htmlPageName(for: language))\">\(language)</a>"
}
func topBarNavigationLink(url: String, text: String, isActive: Bool) -> String {
"<a\(isActive ? " class=\"active\"" : "") href=\"/\(url)\">\(text)</a>"
}
func linkPreviewImage(file: String) -> String {
"<meta property=\"og:image\" content=\"\(file)\" />"
}
func makePrevText(_ text: String) -> String {
"<span class=\"icon-back\"></span>\(text)"
}
func makeNextText(_ text: String) -> String {
"\(text)<span class=\"icon-next\"></span>"
}
func svgImage(file: String) -> String {
"""
<span class="image">
<img src="\(file)"/>
</span>
"""
}
func svgImage(file: String, x: Int, y: Int, width: Int, height: Int) -> String {
"""
<span class="image">
<img src="\(file)#svgView(viewBox(\(x), \(y), \(width), \(height)))" style="aspect-ratio:\(Float(width)/Float(height))"/>
</span>
"""
}
func downloadButtons(_ buttons: [(file: String, text: String, downloadName: String?)]) -> String {
let content = buttons.map {
if let download = $0.downloadName {
return button(file: $0.file, text: $0.text, downloadName: download)
} else {
return button(file: $0.file, text: $0.text)
}
}.joined(separator: "\n")
return flexParagraph(content)
}
func externalButtons(_ buttons: [(url: String, text: String)]) -> String {
let content = buttons
.map { externalLink(url: $0.url, text: $0.text) }
.joined(separator: "\n")
return flexParagraph(content)
}
private func flexParagraph(_ content: String) -> String {
"""
<p style="display: flex">
\(content)
</p>
"""
}
private func button(file: String, text: String) -> String {
"""
<a class="download-button" href="\(file)">
\(text)<span class="icon icon-download"></span>
</a>
"""
}
private func button(file: String, text: String, downloadName: String) -> String {
"""
<a class="download-button" href="\(file)" download="\(downloadName)">
\(text)<span class="icon icon-download"></span>
</a>
"""
}
private func externalLink(url: String, text: String) -> String {
"""
<a class="download-button" href="\(url)">
\(text)<span class="icon icon-download icon-rotate"></span>
</a>
"""
}
func scriptInclude(path: String) -> String {
"<script src=\"\(path)\"></script>"
}
func codeHighlightFooter() -> String {
"<script>hljs.highlightAll();</script>"
}
}

View File

@@ -0,0 +1,226 @@
import Foundation
import Ink
import Splash
struct PageContentGenerator {
private let factory: TemplateFactory
private let swift = SyntaxHighlighter(format: HTMLOutputFormat())
init(factory: TemplateFactory) {
self.factory = factory
}
func generate(page: Element, language: String, content: String) -> (content: String, includesCode: Bool) {
var hasCodeContent = false
let imageModifier = Modifier(target: .images) { html, markdown in
processMarkdownImage(markdown: markdown, html: html, page: page)
}
let codeModifier = Modifier(target: .codeBlocks) { html, markdown in
if markdown.starts(with: "```swift") {
let code = markdown.between("```swift", and: "```").trimmed
return "<pre><code>" + swift.highlight(code) + "</pre></code>"
}
hasCodeContent = true
return html
}
let linkModifier = Modifier(target: .links) { html, markdown in
handleLink(page: page, language: language, html: html, markdown: markdown)
}
let htmlModifier = Modifier(target: .html) { html, markdown in
handleHTML(page: page, language: language, html: html, markdown: markdown)
}
let parser = MarkdownParser(modifiers: [imageModifier, codeModifier, linkModifier, htmlModifier])
return (parser.html(from: content), hasCodeContent)
}
private func handleLink(page: Element, language: String, html: String, markdown: Substring) -> String {
let file = markdown.between("(", and: ")")
if file.hasPrefix("page:") {
let pageId = file.replacingOccurrences(of: "page:", with: "")
guard let pagePath = files.getPage(for: pageId) else {
log.add(warning: "Page id '\(pageId)' not found", source: page.path)
// Remove link since the page can't be found
return markdown.between("[", and: "]")
}
let fullPath = pagePath + Element.htmlPagePathAddition(for: language)
// Adjust file path to get the page url
let url = page.relativePathToOtherSiteElement(file: fullPath)
return html.replacingOccurrences(of: file, with: url)
}
if let filePath = page.nonAbsolutePathRelativeToRootForContainedInputFile(file) {
// The target of the page link must be present after generation is complete
files.expect(file: filePath, source: page.path)
}
return html
}
private func handleHTML(page: Element, language: String, html: String, markdown: Substring) -> String {
#warning("Check HTML code in markdown for required resources")
//print("[HTML] Found in page \(page.path):")
//print(markdown)
// Things to check:
// <img src=
// <a href=
//
return html
}
private func processMarkdownImage(markdown: Substring, html: String, page: Element) -> String {
// Split the markdown ![alt](file title)
// For images: ![left_title](file right_title)
// For videos: ![option1,option2,...](file)
// For svg with custom area: ![x,y,width,height](file.svg)
// For downloads: ![download](file1, text1; file2, text2, ...)
// External pages: ![external](url1, text1; url2, text2, ...)
let fileAndTitle = markdown.between("(", and: ")")
let alt = markdown.between("[", and: "]").nonEmpty
switch alt {
case "download":
return handleDownloadButtons(page: page, content: fileAndTitle)
case "external":
return handleExternalButtons(page: page, content: fileAndTitle)
case "html":
return handleExternalHTML(page: page, file: fileAndTitle)
default:
break
}
let file = fileAndTitle.dropAfterFirst(" ")
let title = fileAndTitle.contains(" ") ? fileAndTitle.dropBeforeFirst(" ").nonEmpty : nil
let fileExtension = file.lastComponentAfter(".").lowercased()
if let _ = ImageType(fileExtension: fileExtension) {
return handleImage(page: page, file: file, rightTitle: title, leftTitle: alt)
}
if let _ = VideoType(rawValue: fileExtension) {
return handleVideo(page: page, file: file, optionString: alt)
}
if fileExtension == "svg" {
return handleSvg(page: page, file: file, area: alt)
}
return handleFile(page: page, file: file, fileExtension: fileExtension)
}
private func handleImage(page: Element, file: String, rightTitle: String?, leftTitle: String?) -> String {
let imagePath = page.pathRelativeToRootForContainedInputFile(file)
let size = files.requireImage(source: imagePath, destination: imagePath, width: configuration.pageImageWidth)
let imagePath2x = imagePath.insert("@2x", beforeLast: ".")
let file2x = file.insert("@2x", beforeLast: ".")
files.requireImage(source: imagePath, destination: imagePath2x, width: 2 * configuration.pageImageWidth)
let content: [PageImageTemplate.Key : String] = [
.image: file,
.image2x: file2x,
.width: "\(Int(size.width))",
.height: "\(Int(size.height))",
.leftText: leftTitle ?? "",
.rightText: rightTitle ?? ""]
return factory.image.generate(content)
}
private func handleVideo(page: Element, file: String, optionString: String?) -> String {
let options: [PageVideoTemplate.VideoOption] = optionString.unwrapped { string in
string.components(separatedBy: " ").compactMap { optionText in
guard let optionText = optionText.trimmed.nonEmpty else {
return nil
}
guard let option = PageVideoTemplate.VideoOption(rawValue: optionText) else {
log.add(warning: "Unknown video option \(optionText)", source: page.path)
return nil
}
return option
}
} ?? []
#warning("Check page folder for alternative video versions")
let sources: [PageVideoTemplate.VideoSource] = [(url: file, type: .mp4)]
let filePath = page.pathRelativeToRootForContainedInputFile(file)
files.require(file: filePath)
return factory.video.generate(sources: sources, options: options)
}
private func handleSvg(page: Element, file: String, area: String?) -> String {
let imagePath = page.pathRelativeToRootForContainedInputFile(file)
files.require(file: imagePath)
guard let area = area else {
return factory.html.svgImage(file: file)
}
let parts = area.components(separatedBy: ",").map { $0.trimmed }
guard parts.count == 4,
let x = Int(parts[0].trimmed),
let y = Int(parts[1].trimmed),
let width = Int(parts[2].trimmed),
let height = Int(parts[3].trimmed) else {
log.add(warning: "Invalid area string for svg image", source: page.path)
return factory.html.svgImage(file: file)
}
return factory.html.svgImage(file: file, x: x, y: y, width: width, height: height)
}
private func handleFile(page: Element, file: String, fileExtension: String) -> String {
log.add(warning: "Unhandled file \(file) with extension \(fileExtension)", source: page.path)
return ""
}
private func handleDownloadButtons(page: Element, content: String) -> String {
let buttons = content
.components(separatedBy: ";")
.compactMap { button -> (file: String, text: String, downloadName: String?)? in
let parts = button.components(separatedBy: ",")
guard parts.count == 2 || parts.count == 3 else {
log.add(warning: "Invalid button definition", source: page.path)
return nil
}
let file = parts[0].trimmed
let title = parts[1].trimmed
let downloadName = parts.count == 3 ? parts[2].trimmed : nil
// Ensure that file is available
let filePath = page.pathRelativeToRootForContainedInputFile(file)
files.require(file: filePath)
return (file, title, downloadName)
}
return factory.html.downloadButtons(buttons)
}
private func handleExternalButtons(page: Element, content: String) -> String {
let buttons = content
.components(separatedBy: ";")
.compactMap { button -> (url: String, text: String)? in
let parts = button.components(separatedBy: ",")
guard parts.count == 2 else {
log.add(warning: "Invalid external link definition", source: page.path)
return nil
}
let url = parts[0].trimmed
let title = parts[1].trimmed
return (url, title)
}
return factory.html.externalButtons(buttons)
}
private func handleExternalHTML(page: Element, file: String) -> String {
let url = page.inputFolder.appendingPathComponent(file)
guard url.exists else {
log.add(error: "File \(file) not found", source: page.path)
return ""
}
do {
return try String(contentsOf: url)
} catch {
log.add(error: "File \(file) could not be read", source: page.path, error: error)
return ""
}
}
}

View File

@@ -0,0 +1,52 @@
import Foundation
struct OverviewPageGenerator {
private let factory: LocalizedSiteTemplate
init(factory: LocalizedSiteTemplate) {
self.factory = factory
}
func generate(
section: Element,
language: String) {
let path = section.localizedPath(for: language)
let url = files.urlInOutputFolder(path)
let metadata = section.localized(for: language)
var content = [PageTemplate.Key : String]()
content[.head] = factory.pageHead.generate(page: section, language: language)
let languageButton = section.nextLanguage(for: language)
content[.topBar] = factory.topBar.generate(
sectionUrl: section.sectionUrl(for: language),
languageButton: languageButton,
page: section)
content[.contentClass] = "overview"
content[.header] = makeHeader(page: section, metadata: metadata, language: language)
content[.content] = makeContent(section: section, language: language)
content[.footer] = section.customFooterContent()
guard factory.page.generate(content, to: url) else {
return
}
files.generated(page: path)
}
private func makeContent(section: Element, language: String) -> String {
if section.hasNestingElements {
return factory.overviewSection.generate(
sections: section.sortedItems,
in: section,
language: language,
sectionItemCount: section.overviewItemCount)
} else {
return factory.overviewSection.generate(section: section, language: language)
}
}
private func makeHeader(page: Element, metadata: Element.LocalizedMetadata, language: String) -> String {
let content = factory.makeHeaderContent(page: page, metadata: metadata, language: language)
return factory.factory.centeredHeader.generate(content)
}
}

View File

@@ -0,0 +1,47 @@
import Foundation
struct OverviewSectionGenerator {
private let multipleSectionsTemplate: OverviewSectionTemplate
private let singleSectionsTemplate: OverviewSectionCleanTemplate
private let generator: ThumbnailListGenerator
init(factory: TemplateFactory) {
self.multipleSectionsTemplate = factory.overviewSection
self.singleSectionsTemplate = factory.overviewSectionClean
self.generator = ThumbnailListGenerator(factory: factory)
}
func generate(sections: [Element], in parent: Element, language: String, sectionItemCount: Int) -> String {
sections.map { section in
let metadata = section.localized(for: language)
let fullUrl = section.fullPageUrl(for: language)
let relativeUrl = parent.relativePathToFileWithPath(fullUrl)
var content = [OverviewSectionTemplate.Key : String]()
content[.url] = relativeUrl
content[.title] = metadata.title
content[.items] = generator.generateContent(
items: section.itemsForOverview(sectionItemCount),
parent: parent,
language: language,
style: section.thumbnailStyle)
content[.more] = metadata.moreLinkText
return multipleSectionsTemplate.generate(content)
}
.joined(separator: "\n")
}
func generate(section: Element, language: String) -> String {
var content = [OverviewSectionCleanTemplate.Key : String]()
content[.items] = generator.generateContent(
items: section.itemsForOverview(),
parent: section,
language: language,
style: section.thumbnailStyle)
return singleSectionsTemplate.generate(content)
}
}

View File

@@ -0,0 +1,88 @@
import Foundation
import Ink
struct PageGenerator {
struct NavigationLink {
let link: String
let text: String
}
private let factory: LocalizedSiteTemplate
init(factory: LocalizedSiteTemplate) {
self.factory = factory
}
func generate(page: Element, language: String, nextPage: NavigationLink?, previousPage: NavigationLink?) {
guard !page.isExternalPage else {
return
}
let path = page.fullPageUrl(for: language)
let inputContentPath = page.path + "/\(language).md"
let metadata = page.localized(for: language)
let nextLanguage = page.nextLanguage(for: language)
let (pageContent, pageIncludesCode, pageIsEmpty) = makeContent(
page: page, metadata: metadata, language: language, path: inputContentPath)
var content = [PageTemplate.Key : String]()
content[.head] = factory.pageHead.generate(page: page, language: language, includesCode: pageIncludesCode)
let sectionUrl = page.sectionUrl(for: language)
content[.topBar] = factory.topBar.generate(sectionUrl: sectionUrl, languageButton: nextLanguage, page: page)
content[.contentClass] = "content"
content[.header] = makeHeader(page: page, metadata: metadata, language: language)
content[.content] = pageContent
content[.previousPageLinkText] = previousPage.unwrapped { factory.factory.html.makePrevText($0.text) }
content[.previousPageUrl] = previousPage?.link
content[.nextPageLinkText] = nextPage.unwrapped { factory.factory.html.makeNextText($0.text) }
content[.nextPageUrl] = nextPage?.link
content[.footer] = page.customFooterContent()
if pageIncludesCode {
let highlightCode = factory.factory.html.codeHighlightFooter()
content[.footer] = (content[.footer].unwrapped { $0 + "\n" } ?? "") + highlightCode
}
let url = files.urlInOutputFolder(path)
if page.state == .draft {
files.isDraft(path: page.path)
} else if pageIsEmpty, page.state != .hidden {
files.isEmpty(page: path)
}
guard factory.page.generate(content, to: url) else {
return
}
files.generated(page: path)
}
private func makeContent(page: Element, metadata: Element.LocalizedMetadata, language: String, path: String) -> (content: String, includesCode: Bool, isEmpty: Bool) {
let create = configuration.createMdFilesIfMissing
if let raw = files.contentOfOptionalFile(atPath: path, source: page.path, createEmptyFileIfMissing: create)?
.trimmed.nonEmpty {
let (content, includesCode) = PageContentGenerator(factory: factory.factory)
.generate(page: page, language: language, content: raw)
return (content, includesCode, false)
} else {
let (content, includesCode) = PageContentGenerator(factory: factory.factory)
.generate(page: page, language: language, content: metadata.placeholderText)
let placeholder = factory.makePlaceholder(title: metadata.placeholderTitle, text: content)
return (placeholder, includesCode, true)
}
}
private func makeHeader(page: Element, metadata: Element.LocalizedMetadata, language: String) -> String? {
let content = factory.makeHeaderContent(page: page, metadata: metadata, language: language)
switch page.headerType {
case .none:
return nil
case .left:
return factory.factory.leftHeader.generate(content)
case .center:
return factory.factory.centeredHeader.generate(content)
}
}
}

View File

@@ -0,0 +1,47 @@
import Foundation
struct PageHeadGenerator {
static let linkPreviewDesiredImageWidth = 1600
let factory: TemplateFactory
init(factory: TemplateFactory) {
self.factory = factory
}
func generate(page: Element, language: String, includesCode: Bool = false) -> String {
let metadata = page.localized(for: language)
var content = [PageHeadTemplate.Key : String]()
content[.author] = page.author
content[.title] = metadata.linkPreviewTitle
content[.description] = metadata.linkPreviewDescription
if let image = page.linkPreviewImage(for: language) {
// Note: Generate separate destination link for the image,
// since we don't want a single large image for thumbnails.
// Warning: Link preview source path must be relative to root
let linkPreviewImageName = image.insert("-link", beforeLast: ".")
let sourceImagePath = page.pathRelativeToRootForContainedInputFile(image)
let destinationImagePath = page.pathRelativeToRootForContainedInputFile(linkPreviewImageName)
files.requireImage(
source: sourceImagePath,
destination: destinationImagePath,
width: PageHeadGenerator.linkPreviewDesiredImageWidth)
content[.image] = factory.html.linkPreviewImage(file: linkPreviewImageName)
}
content[.customPageContent] = page.customHeadContent()
if includesCode {
let scriptPath = "assets/js/highlight.js"
let relative = page.relativePathToOtherSiteElement(file: scriptPath)
let includeText = factory.html.scriptInclude(path: relative)
if let head = content[.customPageContent] {
content[.customPageContent] = head + "\n" + includeText
} else {
content[.customPageContent] = includeText
}
}
return factory.pageHead.generate(content)
}
}

View File

@@ -0,0 +1,60 @@
import Foundation
struct SiteGenerator {
let templates: TemplateFactory
init() throws {
let templatesFolder = files.urlInContentFolder("templates")
self.templates = try TemplateFactory(templateFolder: templatesFolder)
}
func generate(site: Element) {
site.languages.forEach {
generate(site: site, metadata: $0)
}
}
private func generate(site: Element, metadata: Element.LocalizedMetadata) {
let language = metadata.language
let template = LocalizedSiteTemplate(
factory: templates,
language: language,
site: site)
// Generate sections
let overviewGenerator = OverviewPageGenerator(factory: template)
let pageGenerator = PageGenerator(factory: template)
var elementsToProcess: [Element] = [site]
while let element = elementsToProcess.popLast() {
// Move recursively down to all pages
elementsToProcess.append(contentsOf: element.elements)
processAllFiles(for: element)
if !element.elements.isEmpty {
overviewGenerator.generate(section: element, language: language)
} else {
#warning("Determine previous and next pages (with relative links)")
pageGenerator.generate(
page: element,
language: language,
nextPage: nil,
previousPage: nil)
}
}
}
private func processAllFiles(for element: Element) {
element.requiredFiles.forEach(files.require)
element.externalFiles.forEach(files.exclude)
element.images.forEach {
files.requireImage(
source: $0.sourcePath,
destination: $0.destinationPath,
width: $0.desiredWidth,
desiredHeight: $0.desiredHeight)
}
}
}

View File

@@ -0,0 +1,54 @@
import Foundation
struct ThumbnailListGenerator {
private let factory: TemplateFactory
init(factory: TemplateFactory) {
self.factory = factory
}
func generateContent(items: [Element], parent: Element, language: String, style: ThumbnailStyle) -> String {
items.map { itemContent($0, parent: parent, language: language, style: style) }
.joined(separator: "\n")
}
private func itemContent(_ item: Element, parent: Element, language: String, style: ThumbnailStyle) -> String {
let fullThumbnailPath = item.thumbnailFilePath(for: language)
let relativeImageUrl = parent.relativePathToFileWithPath(fullThumbnailPath)
let metadata = item.localized(for: language)
var content = [ThumbnailKey : String]()
if item.state.hasThumbnailLink {
let fullPageUrl = item.fullPageUrl(for: language)
let relativePageUrl = parent.relativePathToFileWithPath(fullPageUrl)
content[.url] = "href=\"\(relativePageUrl)\""
}
content[.image] = relativeImageUrl
if style == .large, let suffix = metadata.thumbnailSuffix {
content[.title] = factory.html.make(title: metadata.title, suffix: suffix)
} else {
content[.title] = metadata.title
}
content[.image2x] = relativeImageUrl.insert("@2x", beforeLast: ".")
content[.corner] = item.cornerText(for: language).unwrapped {
factory.largeThumbnail.makeCorner(text: $0)
}
files.requireImage(
source: fullThumbnailPath,
destination: fullThumbnailPath,
width: style.width,
desiredHeight: style.height)
// Create image version for high-resolution screens
files.requireImage(
source: fullThumbnailPath,
destination: fullThumbnailPath.insert("@2x", beforeLast: "."),
width: style.width * 2,
desiredHeight: style.height * 2)
return factory.thumbnail(style: style).generate(content, shouldIndent: false)
}
}