Add command for pretty page links

This commit is contained in:
Christoph Hagen 2022-09-25 17:19:07 +02:00
parent 66dcd43082
commit f2ee06b1d7
7 changed files with 144 additions and 7 deletions

View File

@ -119,6 +119,13 @@ extension Element {
It can also be set to manually write a page. It can also be set to manually write a page.
*/ */
let externalUrl: String? let externalUrl: String?
/**
The text to display for content related to the current page.
This property is mandatory at root level, and is propagated to child elements.
*/
let relatedContentText: String
} }
} }
@ -162,6 +169,8 @@ extension Element.LocalizedMetadata {
self.thumbnailSuffix = log.unused(data.thumbnailSuffix, "thumbnailSuffix", source: source) self.thumbnailSuffix = log.unused(data.thumbnailSuffix, "thumbnailSuffix", source: source)
self.cornerText = log.unused(data.cornerText, "cornerText", source: source) self.cornerText = log.unused(data.cornerText, "cornerText", source: source)
self.externalUrl = log.unexpected(data.externalUrl, name: "externalUrl", source: source) self.externalUrl = log.unexpected(data.externalUrl, name: "externalUrl", source: source)
self.relatedContentText = log
.required(data.relatedContentText, name: "relatedContentText", source: source) ?? ""
guard isComplete else { guard isComplete else {
return nil return nil
@ -197,6 +206,7 @@ extension Element.LocalizedMetadata {
self.thumbnailSuffix = data.thumbnailSuffix self.thumbnailSuffix = data.thumbnailSuffix
self.cornerText = data.cornerText self.cornerText = data.cornerText
self.externalUrl = data.externalUrl self.externalUrl = data.externalUrl
self.relatedContentText = data.relatedContentText ?? parent.relatedContentText
guard isComplete else { guard isComplete else {
return nil return nil

View File

@ -421,6 +421,10 @@ extension Element {
return pathRelativeToRootForContainedInputFile(thumbnailFile) return pathRelativeToRootForContainedInputFile(thumbnailFile)
} }
/**
The full url (relative to root) for the localized page
- Parameter language: The language of the page where the url should point
*/
func fullPageUrl(for language: String) -> String { func fullPageUrl(for language: String) -> String {
localized(for: language).externalUrl ?? localizedPath(for: language) localized(for: language).externalUrl ?? localizedPath(for: language)
} }
@ -611,4 +615,42 @@ extension Element {
} }
return nil return nil
} }
var pathComponents: [String] {
path.components(separatedBy: "/")
}
var lastPathComponent: String {
pathComponents.last!
}
func find(elementWithFolder folder: String) -> Element? {
elements.first { $0.lastPathComponent == folder }
}
func makePath(language: String, from root: Element) -> [String] {
let parts = pathComponents.dropLast()
var result = [String]()
var node = root
for part in parts {
guard let child = node.find(elementWithFolder: part) else {
return result
}
result.append(child.title(for: language))
node = child
}
return result
}
func findParent(from root: Element) -> Element? {
let parts = pathComponents.dropLast()
var node = root
for part in pathComponents.dropLast() {
guard let child = node.find(elementWithFolder: part) else {
return node
}
node = child
}
return node
}
} }

View File

@ -112,6 +112,13 @@ extension GenericMetadata {
It can also be set to manually write a page. It can also be set to manually write a page.
*/ */
let externalUrl: String? let externalUrl: String?
/**
The text to display for content related to the current page.
This property is mandatory at root level, and is propagated to child elements.
*/
let relatedContentText: String?
} }
} }
@ -134,6 +141,7 @@ extension GenericMetadata.LocalizedMetadata: Codable {
.thumbnailSuffix, .thumbnailSuffix,
.cornerText, .cornerText,
.externalUrl, .externalUrl,
.relatedContentText,
] ]
} }
@ -163,7 +171,8 @@ extension GenericMetadata.LocalizedMetadata {
titleSuffix: nil, titleSuffix: nil,
thumbnailSuffix: nil, thumbnailSuffix: nil,
cornerText: nil, cornerText: nil,
externalUrl: nil) externalUrl: nil,
relatedContentText: nil)
} }
/** /**
@ -184,7 +193,8 @@ extension GenericMetadata.LocalizedMetadata {
titleSuffix: nil, titleSuffix: nil,
thumbnailSuffix: nil, thumbnailSuffix: nil,
cornerText: nil, cornerText: nil,
externalUrl: nil) externalUrl: nil,
relatedContentText: "")
} }
static var full: GenericMetadata.LocalizedMetadata { static var full: GenericMetadata.LocalizedMetadata {
@ -202,6 +212,7 @@ extension GenericMetadata.LocalizedMetadata {
titleSuffix: "", titleSuffix: "",
thumbnailSuffix: "", thumbnailSuffix: "",
cornerText: "", cornerText: "",
externalUrl: "") externalUrl: "",
relatedContentText: "")
} }
} }

View File

@ -16,7 +16,7 @@ struct PageContentGenerator {
var hasCodeContent = false var hasCodeContent = false
let imageModifier = Modifier(target: .images) { html, markdown in let imageModifier = Modifier(target: .images) { html, markdown in
processMarkdownImage(markdown: markdown, html: html, page: page) processMarkdownImage(markdown: markdown, html: html, page: page, language: language)
} }
let codeModifier = Modifier(target: .codeBlocks) { html, markdown in let codeModifier = Modifier(target: .codeBlocks) { html, markdown in
if markdown.starts(with: "```swift") { if markdown.starts(with: "```swift") {
@ -70,7 +70,7 @@ struct PageContentGenerator {
return html return html
} }
private func processMarkdownImage(markdown: Substring, html: String, page: Element) -> String { private func processMarkdownImage(markdown: Substring, html: String, page: Element, language: String) -> String {
// Split the markdown ![alt](file title) // Split the markdown ![alt](file title)
// There are several known shorthand commands // There are several known shorthand commands
// For images: ![left_title](file right_title) // For images: ![left_title](file right_title)
@ -78,11 +78,12 @@ struct PageContentGenerator {
// For svg with custom area: ![x,y,width,height](file.svg) // For svg with custom area: ![x,y,width,height](file.svg)
// For downloads: ![download](file1, text1; file2, text2, ...) // For downloads: ![download](file1, text1; file2, text2, ...)
// For a simple boxes: ![box](title;body) // For a simple boxes: ![box](title;body)
// A fancy page link: ![page](page_id)
// External pages: ![external](url1, text1; url2, text2, ...) // External pages: ![external](url1, text1; url2, text2, ...)
let fileAndTitle = markdown.between("(", and: ")") let fileAndTitle = markdown.between("(", and: ")")
let alt = markdown.between("[", and: "]").nonEmpty let alt = markdown.between("[", and: "]").nonEmpty
if let alt = alt, let command = ShorthandMarkdownKey(rawValue: alt) { if let alt = alt, let command = ShorthandMarkdownKey(rawValue: alt) {
return handleShortHandCommand(command, page: page, content: fileAndTitle) return handleShortHandCommand(command, page: page, language: language, content: fileAndTitle)
} }
let file = fileAndTitle.dropAfterFirst(" ") let file = fileAndTitle.dropAfterFirst(" ")
@ -101,7 +102,7 @@ struct PageContentGenerator {
return handleFile(page: page, file: file, fileExtension: fileExtension) return handleFile(page: page, file: file, fileExtension: fileExtension)
} }
private func handleShortHandCommand(_ command: ShorthandMarkdownKey, page: Element, content: String) -> String { private func handleShortHandCommand(_ command: ShorthandMarkdownKey, page: Element, language: String, content: String) -> String {
switch command { switch command {
case .downloadButtons: case .downloadButtons:
return handleDownloadButtons(page: page, content: content) return handleDownloadButtons(page: page, content: content)
@ -111,6 +112,8 @@ struct PageContentGenerator {
return handleExternalHTML(page: page, file: content) return handleExternalHTML(page: page, file: content)
case .box: case .box:
return handleSimpleBox(page: page, content: content) return handleSimpleBox(page: page, content: content)
case .pageLink:
return handlePageLink(page: page, language: language, pageId: content)
} }
} }
@ -250,4 +253,45 @@ struct PageContentGenerator {
let text = parts.dropFirst().joined(separator: ";") let text = parts.dropFirst().joined(separator: ";")
return factory.makePlaceholder(title: title, text: text) return factory.makePlaceholder(title: title, text: text)
} }
private func handlePageLink(page: Element, language: String, pageId: String) -> String {
guard let linkedPage = siteRoot.find(pageId) else {
log.add(warning: "Page id '\(pageId)' not found", source: page.path)
// Remove link since the page can't be found
return ""
}
var content = [PageLinkTemplate.Key: String]()
content[.url] = page.relativePathToOtherSiteElement(file: linkedPage.fullPageUrl(for: language))
content[.title] = linkedPage.title(for: language)
let fullThumbnailPath = linkedPage.thumbnailFilePath(for: language)
let relativeImageUrl = page.relativePathToOtherSiteElement(file: fullThumbnailPath)
let metadata = linkedPage.localized(for: language)
if linkedPage.state.hasThumbnailLink {
let fullPageUrl = linkedPage.fullPageUrl(for: language)
let relativePageUrl = page.relativePathToOtherSiteElement(file: fullPageUrl)
content[.url] = "href=\"\(relativePageUrl)\""
}
content[.image] = relativeImageUrl
if 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: ".")
let path = linkedPage.makePath(language: language, from: siteRoot)
content[.path] = factory.pageLink.makePath(components: path)
content[.description] = metadata.relatedContentText
if let parent = linkedPage.findParent(from: siteRoot), parent.thumbnailStyle == .large {
content[.className] = " related-page-link-large"
}
// We assume that the thumbnail images are already required by overview pages.
return factory.pageLink.generate(content)
}
} }

View File

@ -21,4 +21,9 @@ enum ShorthandMarkdownKey: String {
A box with a heading and a text description A box with a heading and a text description
*/ */
case box = "box" case box = "box"
/**
A pretty link to another page on the site.
*/
case pageLink = "page"
} }

View File

@ -0,0 +1,22 @@
import Foundation
struct PageLinkTemplate: Template {
enum Key: String, CaseIterable {
case url = "URL"
case image = "IMAGE"
case image2x = "IMAGE_2X"
case title = "TITLE"
case path = "PATH"
case description = "DESCRIPTION"
case className = "CLASS"
}
static let templateName = "page-link.html"
let raw: String
func makePath(components: [String]) -> String {
components.joined(separator: " » ") //  » ")
}
}

View File

@ -16,6 +16,8 @@ final class TemplateFactory {
let overviewSectionClean: OverviewSectionCleanTemplate let overviewSectionClean: OverviewSectionCleanTemplate
let pageLink: PageLinkTemplate
let box: BoxTemplate let box: BoxTemplate
// MARK: Thumbnails // MARK: Thumbnails
@ -65,6 +67,7 @@ final class TemplateFactory {
self.overviewSection = try .init(in: templateFolder) self.overviewSection = try .init(in: templateFolder)
self.overviewSectionClean = try .init(in: templateFolder) self.overviewSectionClean = try .init(in: templateFolder)
self.box = try .init(in: templateFolder) self.box = try .init(in: templateFolder)
self.pageLink = try .init(in: templateFolder)
self.largeThumbnail = try .init(in: templateFolder) self.largeThumbnail = try .init(in: templateFolder)
self.squareThumbnail = try .init(in: templateFolder) self.squareThumbnail = try .init(in: templateFolder)
self.smallThumbnail = try .init(in: templateFolder) self.smallThumbnail = try .init(in: templateFolder)