import Markdown
nonisolated func markdownToHTML(
_ text: String,
codeTheme: SyntaxHighlightTheme = .light,
imageURLResolver: ((String) -> String?)? = nil,
linkURLResolver: ((String) -> String?)? = nil
) -> String {
let document = Document(parsing: text)
var renderer = MarkdownHTMLRenderer(
imageURLResolver: imageURLResolver,
linkURLResolver: linkURLResolver,
highlighter: SyntaxHighlighter(theme: codeTheme)
)
return renderer.visit(document)
}
private struct MarkdownHTMLRenderer: MarkupVisitor {
typealias Result = String
nonisolated(unsafe) let imageURLResolver: ((String) -> String?)?
nonisolated(unsafe) let linkURLResolver: ((String) -> String?)?
nonisolated(unsafe) private let highlighter: SyntaxHighlighter
nonisolated(unsafe) private var isRenderingTableHead = false
nonisolated(unsafe) private var currentTableAlignments: [Markdown.Table.ColumnAlignment?] = []
nonisolated(unsafe) private var currentTableColumnIndex = 0
nonisolated init(
imageURLResolver: ((String) -> String?)?,
linkURLResolver: ((String) -> String?)? = nil,
highlighter: SyntaxHighlighter
) {
self.imageURLResolver = imageURLResolver
self.linkURLResolver = linkURLResolver
self.highlighter = highlighter
}
nonisolated mutating func visit(_ markup: Markup) -> String {
markup.accept(&self)
}
nonisolated mutating func defaultVisit(_ markup: Markup) -> String {
visitChildren(of: markup)
}
nonisolated mutating func visitDocument(_ document: Document) -> String {
visitChildren(of: document)
}
nonisolated mutating func visitHeading(_ heading: Heading) -> String {
"\(visitChildren(of: heading))\n"
}
nonisolated mutating func visitParagraph(_ paragraph: Paragraph) -> String {
"
\(visitChildren(of: paragraph))
\n"
}
nonisolated mutating func visitBlockQuote(_ blockQuote: BlockQuote) -> String {
"\n\(visitChildren(of: blockQuote))
\n"
}
nonisolated mutating func visitUnorderedList(_ unorderedList: UnorderedList) -> String {
"\n\(visitChildren(of: unorderedList))
\n"
}
nonisolated mutating func visitOrderedList(_ orderedList: OrderedList) -> String {
"\n\(visitChildren(of: orderedList))
\n"
}
nonisolated mutating func visitListItem(_ listItem: ListItem) -> String {
if let checkbox = listItem.checkbox,
listItem.childCount == 1,
let paragraph = listItem.child(at: 0) as? Paragraph {
let content = visitChildren(of: paragraph)
return "\(checkboxHTML(for: checkbox)) \(content)\n"
}
var body = visitChildren(of: listItem)
if let checkbox = listItem.checkbox {
body = "\(checkboxHTML(for: checkbox))" + body
}
return "\(body)\n"
}
nonisolated mutating func visitCodeBlock(_ codeBlock: CodeBlock) -> String {
let classAttribute: String
if let language = codeBlock.language, !language.isEmpty {
classAttribute = " class=\"language-\(escapeHTMLAttribute(language))\""
} else {
classAttribute = ""
}
let inner = highlighter.highlightedHTML(for: codeBlock.code, language: codeBlock.language)
?? escapeHTML(codeBlock.code)
return "\(inner)
\n"
}
nonisolated mutating func visitInlineCode(_ inlineCode: InlineCode) -> String {
"\(escapeHTML(inlineCode.code))"
}
nonisolated mutating func visitThematicBreak(_: ThematicBreak) -> String {
"
\n"
}
nonisolated mutating func visitHTMLBlock(_ html: HTMLBlock) -> String {
guard let sanitized = sanitizedMarkdownHTMLBlock(html.rawHTML) else { return "" }
return sanitized + "\n"
}
nonisolated mutating func visitInlineHTML(_ inlineHTML: InlineHTML) -> String {
sanitizedMarkdownHTMLTag(inlineHTML.rawHTML) ?? ""
}
nonisolated mutating func visitLink(_ link: Markdown.Link) -> String {
let content = visitChildren(of: link)
guard let destination = link.destination else {
return content
}
let resolvedDestination = linkURLResolver?(destination) ?? destination
guard let sanitizedDestination = sanitizedReadmeLinkURLString(resolvedDestination) else {
return content
}
let href = escapeHTMLAttribute(decodeHTMLEntities(sanitizedDestination))
return "\(content)"
}
nonisolated mutating func visitImage(_ image: Markdown.Image) -> String {
let altText = plainText(from: image)
guard let source = image.source, !source.isEmpty else {
return escapeHTML(altText)
}
let resolvedSource = imageURLResolver?(source) ?? source
guard let sanitizedSource = sanitizedReadmeImageURLString(resolvedSource) else {
return escapeHTML(altText)
}
let src = escapeHTMLAttribute(decodeHTMLEntities(sanitizedSource))
return "
"
}
nonisolated mutating func visitStrong(_ strong: Strong) -> String {
"\(visitChildren(of: strong))"
}
nonisolated mutating func visitEmphasis(_ emphasis: Emphasis) -> String {
"\(visitChildren(of: emphasis))"
}
nonisolated mutating func visitStrikethrough(_ strikethrough: Strikethrough) -> String {
"\(visitChildren(of: strikethrough))"
}
nonisolated mutating func visitText(_ text: Markdown.Text) -> String {
escapeHTML(text.string)
}
nonisolated mutating func visitSoftBreak(_: SoftBreak) -> String {
" "
}
nonisolated mutating func visitLineBreak(_: LineBreak) -> String {
"
"
}
nonisolated mutating func visitTable(_ table: Markdown.Table) -> String {
let previousAlignments = currentTableAlignments
let previousColumnIndex = currentTableColumnIndex
currentTableAlignments = table.columnAlignments
currentTableColumnIndex = 0
let content = visitChildren(of: table)
currentTableAlignments = previousAlignments
currentTableColumnIndex = previousColumnIndex
return "\n"
}
nonisolated mutating func visitTableHead(_ tableHead: Markdown.Table.Head) -> String {
let previousValue = isRenderingTableHead
isRenderingTableHead = true
let content = visitChildren(of: tableHead)
isRenderingTableHead = previousValue
return "\(content)\n"
}
nonisolated mutating func visitTableBody(_ tableBody: Markdown.Table.Body) -> String {
let previousValue = isRenderingTableHead
isRenderingTableHead = false
let content = visitChildren(of: tableBody)
isRenderingTableHead = previousValue
return "\n\(content)\n"
}
nonisolated mutating func visitTableRow(_ tableRow: Markdown.Table.Row) -> String {
let previousColumnIndex = currentTableColumnIndex
currentTableColumnIndex = 0
let content = visitChildren(of: tableRow)
currentTableColumnIndex = previousColumnIndex
return "\(content)
\n"
}
nonisolated mutating func visitTableCell(_ tableCell: Markdown.Table.Cell) -> String {
let tagName = isRenderingTableHead ? "th" : "td"
let styleAttribute = alignmentStyleAttribute(forColumn: currentTableColumnIndex)
currentTableColumnIndex += 1
return "<\(tagName)\(styleAttribute)>\(visitChildren(of: tableCell))\(tagName)>"
}
nonisolated private mutating func visitChildren(of markup: Markup) -> String {
var html = ""
for child in markup.children {
html += visit(child)
}
return html
}
nonisolated private func plainText(from markup: Markup) -> String {
switch markup {
case let text as Markdown.Text:
return text.string
case let inlineCode as InlineCode:
return inlineCode.code
case is SoftBreak:
return " "
case is LineBreak:
return "\n"
default:
var text = ""
for child in markup.children {
text += plainText(from: child)
}
return text
}
}
nonisolated private func alignmentStyleAttribute(forColumn column: Int) -> String {
guard column < currentTableAlignments.count,
let alignment = currentTableAlignments[column] else {
return ""
}
let textAlignment: String
switch alignment {
case .left:
textAlignment = "left"
case .center:
textAlignment = "center"
case .right:
textAlignment = "right"
}
return " style=\"text-align: \(textAlignment);\""
}
nonisolated private func checkboxHTML(for checkbox: Checkbox) -> String {
switch checkbox {
case .checked:
return ""
case .unchecked:
return ""
}
}
}