Convert lists to attributed strings
This commit is contained in:
parent
38d57b3f79
commit
97ce18d056
|
@ -99,6 +99,28 @@ struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
|
||||||
styleStack.append(.blockquote)
|
styleStack.append(.blockquote)
|
||||||
case "p":
|
case "p":
|
||||||
startBlockElement()
|
startBlockElement()
|
||||||
|
case "ol":
|
||||||
|
startBlockElement()
|
||||||
|
finishRun()
|
||||||
|
styleStack.append(.orderedList(nextElementOrdinal: 1))
|
||||||
|
case "ul":
|
||||||
|
startBlockElement()
|
||||||
|
finishRun()
|
||||||
|
styleStack.append(.unorderedList)
|
||||||
|
case "li":
|
||||||
|
if str.length != 0 || !currentRun.isEmpty {
|
||||||
|
currentRun.append("\n")
|
||||||
|
}
|
||||||
|
let marker: String
|
||||||
|
if case .orderedList(let nextElementOrdinal) = styleStack.last {
|
||||||
|
marker = orderedTextList.marker(forItemNumber: nextElementOrdinal)
|
||||||
|
styleStack[styleStack.count - 1] = .orderedList(nextElementOrdinal: nextElementOrdinal + 1)
|
||||||
|
} else if case .unorderedList = styleStack.last {
|
||||||
|
marker = unorderedTextList.marker(forItemNumber: 0)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
currentRun.append("\t\(marker)\t")
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -135,6 +157,14 @@ struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
|
||||||
case "blockquote":
|
case "blockquote":
|
||||||
finishRun()
|
finishRun()
|
||||||
removeLastStyle(.blockquote)
|
removeLastStyle(.blockquote)
|
||||||
|
case "ol":
|
||||||
|
finishRun()
|
||||||
|
removeLastStyle(.orderedList)
|
||||||
|
case "ul":
|
||||||
|
finishRun()
|
||||||
|
removeLastStyle(.unorderedList)
|
||||||
|
case "li":
|
||||||
|
finishRun()
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -159,6 +189,19 @@ struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
|
||||||
return style
|
return style
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
private lazy var listParagraphStyle: NSParagraphStyle = {
|
||||||
|
let style = configuration.paragraphStyle.mutableCopy() as! NSMutableParagraphStyle
|
||||||
|
// I don't like that I can't just use paragraphStyle.textLists, because it makes the list markers
|
||||||
|
// not use the monospace digit font (it seems to just use whatever font attribute is set for the whole thing),
|
||||||
|
// and it doesn't right align the list markers.
|
||||||
|
// Unfortunately, doing it manually means the list markers are incldued in the selectable text.
|
||||||
|
style.headIndent = 32
|
||||||
|
style.firstLineHeadIndent = 0
|
||||||
|
// Use 2 tab stops, one for the list marker, the second for the content.
|
||||||
|
style.tabStops = [NSTextTab(textAlignment: .right, location: 28), NSTextTab(textAlignment: .natural, location: 32)]
|
||||||
|
return style
|
||||||
|
}()
|
||||||
|
|
||||||
private mutating func finishRun() {
|
private mutating func finishRun() {
|
||||||
guard !currentRun.isEmpty else {
|
guard !currentRun.isEmpty else {
|
||||||
return
|
return
|
||||||
|
@ -190,6 +233,8 @@ struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
|
||||||
case .blockquote:
|
case .blockquote:
|
||||||
attributes[.paragraphStyle] = blockquoteParagraphStyle
|
attributes[.paragraphStyle] = blockquoteParagraphStyle
|
||||||
currentFontTraits.insert(.italic)
|
currentFontTraits.insert(.italic)
|
||||||
|
case .orderedList, .unorderedList:
|
||||||
|
attributes[.paragraphStyle] = listParagraphStyle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,6 +335,8 @@ private enum Style {
|
||||||
case link(URL?)
|
case link(URL?)
|
||||||
case strikethrough
|
case strikethrough
|
||||||
case blockquote
|
case blockquote
|
||||||
|
case orderedList(nextElementOrdinal: Int)
|
||||||
|
case unorderedList
|
||||||
|
|
||||||
var type: StyleType {
|
var type: StyleType {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -305,11 +352,22 @@ private enum Style {
|
||||||
return .strikethrough
|
return .strikethrough
|
||||||
case .blockquote:
|
case .blockquote:
|
||||||
return .blockquote
|
return .blockquote
|
||||||
|
case .orderedList(nextElementOrdinal: _):
|
||||||
|
return .orderedList
|
||||||
|
case .unorderedList:
|
||||||
|
return .unorderedList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum StyleType {
|
enum StyleType: Equatable {
|
||||||
case bold, italic, monospace, link, strikethrough, blockquote
|
case bold
|
||||||
|
case italic
|
||||||
|
case monospace
|
||||||
|
case link
|
||||||
|
case strikethrough
|
||||||
|
case blockquote
|
||||||
|
case orderedList
|
||||||
|
case unorderedList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,3 +376,12 @@ extension Collection where Element == Attribute {
|
||||||
first(where: { $0.name == name })?.value
|
first(where: { $0.name == name })?.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let orderedTextList = OrderedNumberTextList(markerFormat: .decimal, options: 0)
|
||||||
|
private let unorderedTextList = NSTextList(markerFormat: .disc, options: 0)
|
||||||
|
|
||||||
|
private class OrderedNumberTextList: NSTextList {
|
||||||
|
override func marker(forItemNumber itemNumber: Int) -> String {
|
||||||
|
"\(super.marker(forItemNumber: itemNumber))."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -26,6 +26,13 @@ final class AttributedStringConverterTests: XCTestCase {
|
||||||
style.firstLineHeadIndent = 32
|
style.firstLineHeadIndent = 32
|
||||||
return style
|
return style
|
||||||
}()
|
}()
|
||||||
|
private let listParagraphStyle: NSParagraphStyle = {
|
||||||
|
let style = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
|
||||||
|
style.headIndent = 32
|
||||||
|
style.firstLineHeadIndent = 0
|
||||||
|
style.tabStops = [NSTextTab(textAlignment: .right, location: 28), NSTextTab(textAlignment: .natural, location: 32)]
|
||||||
|
return style
|
||||||
|
}()
|
||||||
|
|
||||||
private func convert(_ html: String) -> NSAttributedString {
|
private func convert(_ html: String) -> NSAttributedString {
|
||||||
convert(html, callbacks: DefaultCallbacks.self)
|
convert(html, callbacks: DefaultCallbacks.self)
|
||||||
|
@ -192,4 +199,12 @@ final class AttributedStringConverterTests: XCTestCase {
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testOrderedList() {
|
||||||
|
let result = convert("<ol><li>a</li><li>b</li></ol>")
|
||||||
|
XCTAssertEqual(result, NSAttributedString(string: "\t1.\ta\n\t2.\tb", attributes: [
|
||||||
|
.font: font,
|
||||||
|
.paragraphStyle: listParagraphStyle,
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue