114 lines
4.8 KiB
Swift
114 lines
4.8 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import MarkdownParser
|
|
|
|
nonisolated public enum TermindMarkdownParser {
|
|
public static func parse(_ source: String) -> [MarkdownBlockNode] {
|
|
MarkdownParser().parse(source).document
|
|
}
|
|
|
|
public static func plainText(_ source: String) -> String {
|
|
parse(source)
|
|
.flatMap(plainText)
|
|
.joined(separator: " ")
|
|
.split(whereSeparator: \.isWhitespace)
|
|
.joined(separator: " ")
|
|
}
|
|
|
|
/// A portable single-line title that keeps inline emphasis and links.
|
|
public static func inlineTitle(_ source: String) -> AttributedString {
|
|
func inlines(_ nodes: [MarkdownInlineNode]) -> AttributedString {
|
|
nodes.reduce(into: AttributedString()) { result, node in
|
|
var value: AttributedString
|
|
switch node {
|
|
case .text(let text), .html(let text), .math(let text, _):
|
|
value = AttributedString(text)
|
|
case .code(let text):
|
|
value = AttributedString(text)
|
|
value.inlinePresentationIntent = .code
|
|
case .softBreak, .lineBreak:
|
|
value = AttributedString(" ")
|
|
case .emphasis(let children):
|
|
value = inlines(children)
|
|
for run in value.runs {
|
|
value[run.range].inlinePresentationIntent = (run.inlinePresentationIntent ?? []).union(
|
|
.emphasized)
|
|
}
|
|
case .strong(let children):
|
|
value = inlines(children)
|
|
for run in value.runs {
|
|
value[run.range].inlinePresentationIntent = (run.inlinePresentationIntent ?? []).union(
|
|
.stronglyEmphasized)
|
|
}
|
|
case .strikethrough(let children):
|
|
value = inlines(children)
|
|
for run in value.runs {
|
|
value[run.range].inlinePresentationIntent = (run.inlinePresentationIntent ?? []).union(
|
|
.strikethrough)
|
|
}
|
|
case .link(let destination, let children):
|
|
value = inlines(children)
|
|
value.link = URL(string: destination)
|
|
case .image(_, let children):
|
|
value = inlines(children)
|
|
}
|
|
result += value
|
|
}
|
|
}
|
|
func blocks(_ nodes: [MarkdownBlockNode]) -> [AttributedString] {
|
|
nodes.flatMap { node -> [AttributedString] in
|
|
switch node {
|
|
case .paragraph(let content), .heading(_, let content): [inlines(content)]
|
|
case .codeBlock(_, let content):
|
|
[inlines([.code(content.trimmingCharacters(in: .whitespacesAndNewlines))])]
|
|
case .blockquote(let children): blocks(children)
|
|
case .bulletedList(_, let items), .numberedList(_, _, let items): items.flatMap { blocks($0.children) }
|
|
case .taskList(_, let items): items.flatMap { blocks($0.children) }
|
|
case .table(_, let rows): rows.flatMap { $0.cells.map { inlines($0.content) } }
|
|
case .thematicBreak: []
|
|
}
|
|
}
|
|
}
|
|
return blocks(parse(source)).enumerated().reduce(into: AttributedString()) { result, item in
|
|
if item.offset > 0 { result += AttributedString(" ") }
|
|
result += item.element
|
|
}
|
|
}
|
|
|
|
private static func plainText(_ block: MarkdownBlockNode) -> [String] {
|
|
switch block {
|
|
case .paragraph(let content), .heading(_, let content):
|
|
[plainText(content)]
|
|
case .codeBlock(_, let content):
|
|
[content]
|
|
case .blockquote(let children):
|
|
children.flatMap(plainText)
|
|
case .bulletedList(_, let items), .numberedList(_, _, let items):
|
|
items.flatMap { $0.children.flatMap(plainText) }
|
|
case .taskList(_, let items):
|
|
items.flatMap { $0.children.flatMap(plainText) }
|
|
case .table(_, let rows):
|
|
rows.flatMap { row in row.cells.map { plainText($0.content) } }
|
|
case .thematicBreak:
|
|
[]
|
|
}
|
|
}
|
|
|
|
private static func plainText(_ inlines: [MarkdownInlineNode]) -> String {
|
|
inlines.map { inline in
|
|
switch inline {
|
|
case .text(let text), .code(let text), .html(let text), .math(let text, _):
|
|
text
|
|
case .softBreak, .lineBreak:
|
|
" "
|
|
case .emphasis(let children), .strong(let children),
|
|
.strikethrough(let children), .link(_, let children),
|
|
.image(_, let children):
|
|
plainText(children)
|
|
}
|
|
}.joined()
|
|
}
|
|
}
|