diff --git a/Sources/HTMLStreamer/AttributedStringConverter.swift b/Sources/HTMLStreamer/AttributedStringConverter.swift index 8459f3d..0fe7609 100644 --- a/Sources/HTMLStreamer/AttributedStringConverter.swift +++ b/Sources/HTMLStreamer/AttributedStringConverter.swift @@ -99,6 +99,28 @@ struct AttributedStringConverter { styleStack.append(.blockquote) case "p": 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: break } @@ -135,6 +157,14 @@ struct AttributedStringConverter { case "blockquote": finishRun() removeLastStyle(.blockquote) + case "ol": + finishRun() + removeLastStyle(.orderedList) + case "ul": + finishRun() + removeLastStyle(.unorderedList) + case "li": + finishRun() default: break } @@ -159,6 +189,19 @@ struct AttributedStringConverter { 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() { guard !currentRun.isEmpty else { return @@ -190,6 +233,8 @@ struct AttributedStringConverter { case .blockquote: attributes[.paragraphStyle] = blockquoteParagraphStyle currentFontTraits.insert(.italic) + case .orderedList, .unorderedList: + attributes[.paragraphStyle] = listParagraphStyle } } @@ -290,6 +335,8 @@ private enum Style { case link(URL?) case strikethrough case blockquote + case orderedList(nextElementOrdinal: Int) + case unorderedList var type: StyleType { switch self { @@ -305,11 +352,22 @@ private enum Style { return .strikethrough case .blockquote: return .blockquote + case .orderedList(nextElementOrdinal: _): + return .orderedList + case .unorderedList: + return .unorderedList } } - enum StyleType { - case bold, italic, monospace, link, strikethrough, blockquote + enum StyleType: Equatable { + 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 } } + +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))." + } +} diff --git a/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift b/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift index 73dbd89..3620b51 100644 --- a/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift +++ b/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift @@ -26,6 +26,13 @@ final class AttributedStringConverterTests: XCTestCase { style.firstLineHeadIndent = 32 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 { convert(html, callbacks: DefaultCallbacks.self) @@ -192,4 +199,12 @@ final class AttributedStringConverterTests: XCTestCase { ])) } + func testOrderedList() { + let result = convert("
  1. a
  2. b
") + XCTAssertEqual(result, NSAttributedString(string: "\t1.\ta\n\t2.\tb", attributes: [ + .font: font, + .paragraphStyle: listParagraphStyle, + ])) + } + }