Update generation

- Move to global objects for files and validation
- Only write changed files
- Check images for changes before scaling
- Simplify code
This commit is contained in:
Christoph Hagen
2022-08-26 17:40:51 +02:00
parent 91d5bcb66d
commit 80d3c08a93
54 changed files with 1344 additions and 2419 deletions

View File

@ -1,21 +1,5 @@
import Foundation
private struct AboveRootDummy: SiteElement {
var sortIndex: Int? { nil }
var sortDate: Date? { nil }
var path: String { "" }
let inputFolder: URL
func title(for language: String) -> String { "" }
func cornerText(for language: String) -> String? { nil }
var elements: [SiteElement] { [] }
}
struct IndexPageGenerator {
private let factory: LocalizedSiteTemplate
@ -25,41 +9,34 @@ struct IndexPageGenerator {
}
func generate(
site: Site,
site: Element,
language: String,
languageButton: String?,
sectionItemCount: Int,
to url: URL) throws {
to url: URL) {
let localized = site.localized(for: language)
var content = [PageTemplate.Key : String]()
content[.head] = try makeHead(site: site, language: language)
content[.topBar] = factory.topBar.generate(section: nil, languageButton: languageButton)
content[.head] = factory.pageHead.generate(page: site, language: language)
content[.topBar] = factory.topBar.generate(sectionUrl: nil, languageButton: languageButton)
content[.contentClass] = "overview"
content[.header] = makeHeader(localized: localized)
let sections = site.elements.compactMap { $0 as? Section }
content[.content] = try factory.overviewSection.generate(
sections: sections,
content[.content] = factory.overviewSection.generate(
sections: site.elements,
in: site,
language: language,
sectionItemCount: sectionItemCount)
content[.footer] = try site.customFooterContent()
try factory.page.generate(content, to: url)
content[.footer] = site.customFooterContent()
guard factory.page.generate(content, to: url) else {
return
}
log.add(info: "Generated \(url.lastPathComponent)", source: site.path)
}
private func makeHead(site: Site, language: String) throws -> String {
let localized = site.localized(for: language)
return try factory.pageHead.generate(page: PageHeadInfo(
author: site.metadata.author,
linkPreviewTitle: localized.linkPreviewTitle,
linkPreviewDescription: localized.linkPreviewDescription,
linkPreviewImage: site.linkPreviewImage(for: language),
customHeadContent: try site.customHeadContent()))
}
private func makeHeader(localized: Site.LocalizedMetadata) -> String {
private func makeHeader(localized: Element.LocalizedMetadata) -> String {
var content = [HeaderKey : String]()
content[.title] = localized.title
#warning("Add title suffix")
content[.subtitle] = localized.subtitle
content[.titleText] = localized.description
return factory.factory.centeredHeader.generate(content)

View File

@ -9,31 +9,18 @@ struct PageContentGenerator {
private let factory: TemplateFactory
private let files: FileProcessor
private let swift = SyntaxHighlighter(format: HTMLOutputFormat())
init(factory: TemplateFactory, files: FileProcessor) {
init(factory: TemplateFactory) {
self.factory = factory
self.files = files
}
func generate(page: Page, language: String, at url: URL) throws -> String {
var errorToThrow: Error? = nil
let content = try wrap(.missingPage(page: url.path, language: language)) {
try String(contentsOf: url)
}
func generate(page: Element, language: String, content: String) -> String {
var hasCodeContent = false
let imageModifier = Modifier(target: .images) { html, markdown in
do {
return try processMarkdownImage(markdown: markdown, html: html, page: page)
} catch {
errorToThrow = error
return ""
}
processMarkdownImage(markdown: markdown, html: html, page: page)
}
let codeModifier = Modifier(target: .codeBlocks) { html, markdown in
if markdown.starts(with: "```swift") {
@ -51,17 +38,13 @@ struct PageContentGenerator {
let parser = MarkdownParser(modifiers: [imageModifier, codeModifier, linkModifier])
if hasCodeContent {
#warning("Automatically add hljs hightlighting if code samples are found")
#warning("Automatically add hljs highlighting if code samples are found")
}
let result = parser.html(from: content)
if let error = errorToThrow {
throw error
}
return result
return parser.html(from: content)
}
private func processMarkdownImage(markdown: Substring, html: String, page: Page) throws -> String {
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: ![option...](file)
@ -72,27 +55,27 @@ struct PageContentGenerator {
let alt = markdown.between("[", and: "]").nonEmpty
let fileExtension = file.lastComponentAfter(".").lowercased()
switch files.mediaType(forExtension: fileExtension) {
switch MediaType(fileExtension: fileExtension) {
case .image:
return try handleImage(page: page, file: file, rightTitle: title, leftTitle: alt)
return handleImage(page: page, file: file, rightTitle: title, leftTitle: alt)
case .video:
return try handleVideo(page: page, file: file, optionString: alt)
return handleVideo(page: page, file: file, optionString: alt)
case .file:
if fileExtension == "svg" {
return try handleSvg(page: page, file: file)
return handleSvg(page: page, file: file)
}
return try handleFile(page: page, file: file, fileExtension: fileExtension)
return handleFile(page: page, file: file, fileExtension: fileExtension)
}
}
private func handleImage(page: Page, file: String, rightTitle: String?, leftTitle: String?) throws -> String {
private func handleImage(page: Element, file: String, rightTitle: String?, leftTitle: String?) -> String {
let imagePath = page.pathRelativeToRootForContainedInputFile(file)
let size = try files.requireImage(source: imagePath, destination: imagePath, width: pageImageWidth)
let size = files.requireImage(source: imagePath, destination: imagePath, width: pageImageWidth)
let imagePath2x = imagePath.insert("@2x", beforeLast: ".")
let file2x = file.insert("@2x", beforeLast: ".")
try files.requireImage(source: imagePath, destination: imagePath2x, width: 2 * pageImageWidth)
files.requireImage(source: imagePath, destination: imagePath2x, width: 2 * pageImageWidth)
let content: [PageImageTemplate.Key : String] = [
.image: file,
@ -104,7 +87,7 @@ struct PageContentGenerator {
return factory.image.generate(content)
}
private func handleVideo(page: Page, file: String, optionString: String?) throws -> String {
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 {
@ -125,7 +108,7 @@ struct PageContentGenerator {
return factory.video.generate(sources: sources, options: options)
}
private func handleSvg(page: Page, file: String) throws -> String {
private func handleSvg(page: Element, file: String) -> String {
let imagePath = page.pathRelativeToRootForContainedInputFile(file)
files.require(file: imagePath)
@ -136,7 +119,7 @@ struct PageContentGenerator {
"""
}
private func handleFile(page: Page, file: String, fileExtension: String) throws -> String {
private func handleFile(page: Element, file: String, fileExtension: String) -> String {
#warning("Handle other files in markdown")
print("[WARN] Unhandled file \(file) with extension \(fileExtension)")
return ""

View File

@ -4,65 +4,53 @@ struct OverviewPageGenerator {
private let factory: LocalizedSiteTemplate
let outputFolder: URL
init(factory: LocalizedSiteTemplate, files: FileProcessor) {
init(factory: LocalizedSiteTemplate) {
self.factory = factory
self.outputFolder = files.outputFolder
}
func generate(
section: Section,
section: Element,
language: String,
backText: String?) throws {
let url = outputFolder.appendingPathComponent(section.localizedPath(for: language))
backText: 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] = try makeHead(section: section, language: language)
content[.head] = factory.pageHead.generate(page: section, language: language)
let languageButton = section.nextLanguage(for: language)
content[.topBar] = factory.topBar.generate(
section: section.sectionId,
sectionUrl: section.sectionUrl(for: language),
languageButton: languageButton)
content[.contentClass] = "overview"
content[.header] = makeHeader(metadata: metadata, language: language, backText: backText)
content[.content] = try makeContent(section: section, language: language)
content[.footer] = try section.customFooterContent()
try factory.page.generate(content, to: url)
content[.content] = makeContent(section: section, language: language)
content[.footer] = section.customFooterContent()
guard factory.page.generate(content, to: url) else {
return
}
log.add(info: "Generated \(path)", source: section.path)
}
private func makeContent(section: Section, language: String) throws -> String {
private func makeContent(section: Element, language: String) -> String {
if section.hasNestingElements {
let sections = section.elements.compactMap { $0 as? Section }
return try factory.overviewSection.generate(
sections: sections,
return factory.overviewSection.generate(
sections: section.elements,
in: section,
language: language,
sectionItemCount: section.metadata.sectionOverviewItemCount)
sectionItemCount: section.overviewItemCount)
} else {
return try factory.overviewSection.generate(section: section, language: language)
return factory.overviewSection.generate(section: section, language: language)
}
}
private func makeHead(section: Section, language: String) throws -> String {
let localized = section.localized(for: language)
let image = section.linkPreviewImage(for: language)
let info = PageHeadInfo(
author: factory.author,
linkPreviewTitle: localized.title,
linkPreviewDescription: localized.linkPreviewDescription,
linkPreviewImage: image,
customHeadContent: try section.customHeadContent())
return try factory.pageHead.generate(page: info)
}
private func makeHeader(metadata: Section.LocalizedMetadata,
private func makeHeader(metadata: Element.LocalizedMetadata,
language: String,
backText: String?) -> String {
var content = [HeaderKey : String]()
content[.title] = metadata.title
#warning("Add title suffix")
content[.subtitle] = metadata.subtitle
content[.titleText] = metadata.description
content[.backLink] = backText.unwrapped { factory.makeBackLink(text: $0, language: language) }

View File

@ -6,19 +6,16 @@ struct OverviewSectionGenerator {
private let singleSectionsTemplate: OverviewSectionCleanTemplate
let files: FileProcessor
private let generator: ThumbnailListGenerator
init(factory: TemplateFactory, files: FileProcessor) {
init(factory: TemplateFactory) {
self.multipleSectionsTemplate = factory.overviewSection
self.singleSectionsTemplate = factory.overviewSectionClean
self.files = files
self.generator = ThumbnailListGenerator(factory: factory, files: files)
self.generator = ThumbnailListGenerator(factory: factory)
}
func generate(sections: [Section], in parent: SiteElement, language: String, sectionItemCount: Int) throws -> String {
try sections.map { section in
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)
@ -26,44 +23,31 @@ struct OverviewSectionGenerator {
var content = [OverviewSectionTemplate.Key : String]()
content[.url] = relativeUrl
content[.title] = metadata.title
content[.items] = try sectionContent(section: section, in: parent, language: language, shownItemCount: sectionItemCount)
content[.more] = metadata.moreLinkTitle
content[.items] = sectionContent(section: section, in: parent, language: language, shownItemCount: sectionItemCount)
content[.more] = metadata.moreLinkText
return multipleSectionsTemplate.generate(content)
}
.joined(separator: "\n")
}
func generate(section: Section, language: String) throws -> String {
func generate(section: Element, language: String) -> String {
var content = [OverviewSectionCleanTemplate.Key : String]()
content[.items] = try sectionContent(section: section, in: section, language: language, shownItemCount: nil)
content[.items] = sectionContent(section: section, in: section, language: language, shownItemCount: nil)
return singleSectionsTemplate.generate(content)
}
private func sectionContent(section: Section, in parent: SiteElement, language: String, shownItemCount: Int?) throws -> String {
let sectionItems: [SiteElement]
private func sectionContent(section: Element, in parent: Element, language: String, shownItemCount: Int?) -> String {
let sectionItems: [Element]
if let shownItemCount = shownItemCount {
sectionItems = Array(section.sortedItems.prefix(shownItemCount))
} else {
sectionItems = section.sortedItems
}
let items: [ThumbnailInfo] = sectionItems.map { item in
#warning("Check if page exists for the language")
let fullPageUrl = item.fullPageUrl(for: language)
let relativePageUrl = parent.relativePathToFileWithPath(fullPageUrl)
let fullThumbnailPath = item.thumbnailFilePath(for: language)
let relativeImageUrl = parent.relativePathToFileWithPath(fullThumbnailPath)
return ThumbnailInfo(
url: relativePageUrl,
imageFilePath: fullThumbnailPath,
imageHtmlUrl: relativeImageUrl,
title: item.title(for: language),
cornerText: item.cornerText(for: language))
}
return try generator.generateContent(
items: items,
style: section.metadata.thumbnailStyle)
return generator.generateContent(
items: sectionItems,
parent: parent,
language: language,
style: section.thumbnailStyle)
}
}

View File

@ -12,70 +12,67 @@ struct PageGenerator {
private let factory: LocalizedSiteTemplate
private let files: FileProcessor
init(factory: LocalizedSiteTemplate, files: FileProcessor) {
init(factory: LocalizedSiteTemplate) {
self.factory = factory
self.files = files
}
func generate(page: Page, language: String, backText: String, nextPage: NavigationLink?, previousPage: NavigationLink?) throws {
func generate(page: Element, language: String, backText: String, nextPage: NavigationLink?, previousPage: NavigationLink?) {
guard !page.isExternalPage else {
return
}
guard !page.metadata.isDraft else {
guard page.state != .draft else {
return
}
let path = page.fullPageUrl(for: language)
let inputContentUrl = page.inputFolder.appendingPathComponent("\(language).md")
let inputContentPath = page.path + "/\(language).md"
#warning("Make prev and next navigation relative")
let metadata = page.localized(for: language)
let nextLanguage = page.nextLanguage(for: language)
var content = [PageTemplate.Key : String]()
content[.head] = try makeHead(page: page, language: language)
content[.topBar] = factory.topBar.generate(section: page.sectionId, languageButton: nextLanguage)
content[.head] = factory.pageHead.generate(page: page, language: language)
let sectionUrl = page.sectionUrl(for: language)
content[.topBar] = factory.topBar.generate(sectionUrl: sectionUrl, languageButton: nextLanguage)
content[.contentClass] = "content"
if !page.metadata.useCustomHeader {
content[.header] = makeHeader(page: page.metadata, metadata: metadata, language: language, backText: backText)
if !page.useCustomHeader {
content[.header] = makeHeader(page: page, metadata: metadata, language: language, backText: backText)
}
content[.content] = try makeContent(page: page, language: language, url: inputContentUrl)
let pageContent = makeContent(page: page, language: language, path: inputContentPath)
content[.content] = pageContent ?? factory.placeholder
content[.previousPageLinkText] = previousPage.unwrapped { factory.makePrevText($0.text) }
content[.previousPageUrl] = previousPage?.link
content[.nextPageLinkText] = nextPage.unwrapped { factory.makeNextText($0.text) }
content[.nextPageUrl] = nextPage?.link
content[.footer] = try page.customFooterContent()
content[.footer] = page.customFooterContent()
let url = files.outputFolder.appendingPathComponent(path)
try factory.page.generate(content, to: url)
}
private func makeContent(page: Page, language: String, url: URL) throws -> String {
guard url.exists else {
print("Generated empty page \(page.path)")
return factory.placeholder
let url = files.urlInOutputFolder(path)
guard factory.page.generate(content, to: url) else {
return
}
print("Generated page \(page.path)")
return try PageContentGenerator(factory: factory.factory, files: files)
.generate(page: page, language: language, at: url)
log.add(info: "Generated \(pageContent == nil ? "empty page " : "")\(path)", source: page.path)
}
private func makeHead(page: Page, language: String) throws -> String {
let metadata = page.localized(for: language)
let info = PageHeadInfo(
author: page.metadata.author ?? factory.author,
linkPreviewTitle: metadata.linkPreviewTitle,
linkPreviewDescription: metadata.linkPreviewDescription,
linkPreviewImage: page.linkPreviewImage(for: language),
customHeadContent: try page.customHeadContent())
return try factory.pageHead.generate(page: info)
private func makeContent(page: Element, language: String, path: String) -> String? {
guard let content = files.contentOfOptionalFile(atPath: path, source: page.path) else {
return nil
}
return PageContentGenerator(factory: factory.factory)
.generate(page: page, language: language, content: content)
}
private func makeHeader(page: Page.Metadata, metadata: Page.LocalizedMetadata, language: String, backText: String) -> String {
private func makeHeader(page: Element, metadata: Element.LocalizedMetadata, language: String, backText: String) -> String {
var content = [HeaderKey : String]()
content[.backLink] = factory.makeBackLink(text: backText, language: language)
content[.title] = metadata.title
if let suffix = metadata.titleSuffix {
content[.title] = make(title: metadata.title, suffix: suffix)
} else {
content[.title] = metadata.title
}
content[.subtitle] = metadata.subtitle
content[.date] = factory.makeDateString(start: page.date, end: page.endDate)
return factory.factory.leftHeader.generate(content)
}
private func make(title: String, suffix: String) -> String {
"\(title)<span class=\"suffix\">\(suffix)</span>"
}
}

View File

@ -1,60 +1,36 @@
import Foundation
protocol PageHeadInfoProvider {
var author: String { get }
var linkPreviewTitle: String { get }
var linkPreviewDescription: String { get }
var linkPreviewImage: String? { get }
var customHeadContent: String? { get }
}
struct PageHeadInfo: PageHeadInfoProvider {
let author: String
let linkPreviewTitle: String
let linkPreviewDescription: String
let linkPreviewImage: String?
let customHeadContent: String?
}
struct PageHeadGenerator {
static let linkPreviewDesiredImageWidth = 1600
let template: PageHeadTemplate
let files: FileProcessor
init(factory: TemplateFactory, files: FileProcessor) {
init(factory: TemplateFactory) {
self.template = factory.pageHead
self.files = files
}
func generate(page: PageHeadInfoProvider) throws -> String {
func generate(page: Element, language: String) -> String {
let metadata = page.localized(for: language)
var content = [PageHeadTemplate.Key : String]()
content[.author] = page.author
content[.title] = page.linkPreviewTitle
content[.description] = page.linkPreviewDescription
if let image = page.linkPreviewImage {
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 linkPreviewImagePath = image.insert("-link", beforeLast: ".")
try files.requireImage(
source: image,
destination: linkPreviewImagePath,
width: Site.linkPreviewDesiredImageWidth)
#warning("Make link preview image path absolute")
content[.image] = "<meta property=\"og:image\" content=\"\(linkPreviewImagePath)\" />"
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] = "<meta property=\"og:image\" content=\"\(linkPreviewImageName)\" />"
}
content[.customPageContent] = page.customHeadContent
content[.customPageContent] = page.customHeadContent()
return template.generate(content)
}

View File

@ -2,60 +2,45 @@ import Foundation
struct SiteGenerator {
let site: Site
let templates: TemplateFactory
private let files: FileProcessor
private var outputFolder: URL {
files.outputFolder
}
init(site: Site, files: FileProcessor) throws {
self.site = site
let templatesFolder = site.inputFolder.appendingPathComponent("templates")
init() throws {
let templatesFolder = files.urlInContentFolder("templates")
self.templates = try TemplateFactory(templateFolder: templatesFolder)
self.files = files
}
func generate() throws {
try site.metadata.languages.forEach { metadata in
let language = metadata.languageIdentifier
func generate(site: Element) throws {
try site.languages.forEach { metadata in
let language = metadata.language
let template = try LocalizedSiteTemplate(
factory: templates,
language: language,
site: site,
files: files)
site: site)
// Generate sections
let overviewGenerator = OverviewPageGenerator(factory: template, files: files)
let pageGenerator = PageGenerator(factory: template, files: files)
let backLinkText = try site.backLinkText(for: language)
var elementsToProcess: [(element: SiteElement, backText: String?)] = site.elements.map { ($0, backLinkText) }
while let (element, backText) = elementsToProcess.popLast() {
if let section = element as? Section {
try overviewGenerator.generate(
section: section,
let overviewGenerator = OverviewPageGenerator(factory: template)
let pageGenerator = PageGenerator(factory: template)
var elementsToProcess: [Element] = site.elements
while let element = elementsToProcess.popLast() {
// Move recursively down to all pages
elementsToProcess.append(contentsOf: element.elements)
element.requiredFiles.forEach(files.require)
let backLinkText = element.backLinkText(for: language)
if !element.elements.isEmpty {
overviewGenerator.generate(
section: element,
language: language,
backText: backText)
let elementBackText = try element.backLinkText(for: language)
let nestedElements = section.elements.map { ($0, elementBackText) }
elementsToProcess.append(contentsOf: nestedElements)
}
if let page = element as? Page {
backText: backLinkText)
} else {
#warning("Determine previous and next pages")
try pageGenerator.generate(
page: page,
pageGenerator.generate(
page: element,
language: language,
backText: backText ?? metadata.defaultBackLinkText,
backText: backLinkText,
nextPage: nil,
previousPage: nil)
for file in page.metadata.requiredFiles {
let relativePath = page.path + "/" + file
files.require(file: relativePath)
}
}
}
@ -63,9 +48,9 @@ struct SiteGenerator {
// Generate front page
let relativeUrl = site.localizedPath(for: language)
let indexPageUrl = outputFolder.appendingPathComponent(relativeUrl)
let indexPageUrl = files.urlInOutputFolder(relativeUrl)
let button = site.nextLanguage(for: language)
try generator.generate(
generator.generate(
site: site,
language: language,
languageButton: button,

View File

@ -1,14 +0,0 @@
import Foundation
struct ThumbnailInfo {
let url: String?
let imageFilePath: String
let imageHtmlUrl: String
let title: String
let cornerText: String?
}

View File

@ -4,35 +4,37 @@ struct ThumbnailListGenerator {
private let factory: TemplateFactory
let files: FileProcessor
init(factory: TemplateFactory, files: FileProcessor) {
init(factory: TemplateFactory) {
self.factory = factory
self.files = files
}
func generateContent(items: [ThumbnailInfo], style: ThumbnailStyle) throws -> String {
try items.map { try itemContent($0, style: style) }
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(_ thumbnail: ThumbnailInfo, style: ThumbnailStyle) throws -> String {
private func itemContent(_ item: Element, parent: Element, language: String, style: ThumbnailStyle) -> String {
let fullPageUrl = item.fullPageUrl(for: language)
let relativePageUrl = parent.relativePathToFileWithPath(fullPageUrl)
let fullThumbnailPath = item.thumbnailFilePath(for: language)
let relativeImageUrl = parent.relativePathToFileWithPath(fullThumbnailPath)
var content = [ThumbnailKey : String]()
content[.url] = thumbnail.url.unwrapped { "href=\"\($0)\"" }
content[.image] = thumbnail.imageHtmlUrl
content[.title] = thumbnail.title
content[.image2x] = thumbnail.imageHtmlUrl.insert("@2x", beforeLast: ".")
content[.corner] = thumbnail.cornerText.unwrapped {
content[.url] = "href=\"\(relativePageUrl)\""
content[.image] = relativeImageUrl
content[.title] = item.title(for: language)
#warning("Generate thumbnail suffix")
content[.image2x] = relativeImageUrl.insert("@2x", beforeLast: ".")
content[.corner] = item.cornerText(for: language).unwrapped {
factory.largeThumbnail.makeCorner(text: $0)
}
try files.requireImage(
source: thumbnail.imageFilePath,
destination: thumbnail.imageFilePath,
files.requireImage(
source: fullThumbnailPath,
destination: fullThumbnailPath,
width: style.width,
desiredHeight: style.height,
createDoubleVersion: true)
return try factory.thumbnail(style: style).generate(content, shouldIndent: false)
return factory.thumbnail(style: style).generate(content, shouldIndent: false)
}
}