From 81483ae31912ec38d3737a82ab35dd843e32361a Mon Sep 17 00:00:00 2001 From: Rajdeep Kwatra Date: Tue, 9 Jul 2024 11:04:11 +1000 Subject: [PATCH] Added ability to convert LitItem collection from ListParser to hierarchal representation --- Proton/Sources/Swift/Core/ListParser.swift | 41 +++++++++++++ Proton/Tests/Core/ListParserTests.swift | 71 ++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/Proton/Sources/Swift/Core/ListParser.swift b/Proton/Sources/Swift/Core/ListParser.swift index a4a11c48..cdde9d57 100644 --- a/Proton/Sources/Swift/Core/ListParser.swift +++ b/Proton/Sources/Swift/Core/ListParser.swift @@ -46,6 +46,16 @@ public struct ListItem { } } +public class ListItemNode { + public let item: ListItem + public internal(set) var children: [ListItemNode] + + init(item: ListItem, children: [ListItemNode]) { + self.item = item + self.children = children + } +} + /// Provides helper function to convert between `NSAttributedString` and `[ListItem]` public struct ListParser { @@ -104,6 +114,37 @@ public struct ListParser { } return items } + + /// Creates hierarchical representation of `ListItem` from the provided collection based on levels of each of the items + /// - Parameter listItems: ListItems to convert + /// - Returns: Collection of `ListItemNode` with each node having children nodes based on level of individual list items. + public static func createListItemNodes(from listItems: [ListItem]) -> [ListItemNode] { + var result = [ListItemNode]() + var stack: [(node: ListItemNode, level: Int)] = [] + + for item in listItems { + let newNode = ListItemNode(item: item, children: []) + + // Pop from the stack until the current item's parent is found + while let last = stack.last, last.level >= item.level { + stack.removeLast() + } + + if let last = stack.last { + // If there's a parent, add this node to its children + stack[stack.count - 1].node.children.append(newNode) + } else { + // If there's no parent, this is a root node + result.append(newNode) + } + + // Push the current node onto the stack + stack.append((newNode, item.level)) + } + + // Since we've been directly modifying the nodes in the stack, the `result` array now contains the fully constructed tree + return result + } private static func parseList(in attributedString: NSAttributedString, rangeInOriginalString: NSRange, indent: CGFloat, attributeValue: Any?) -> [(range: NSRange, listItem: ListItem)] { var items = [(range: NSRange, listItem: ListItem)]() diff --git a/Proton/Tests/Core/ListParserTests.swift b/Proton/Tests/Core/ListParserTests.swift index 6bf7e3bc..df599f5f 100644 --- a/Proton/Tests/Core/ListParserTests.swift +++ b/Proton/Tests/Core/ListParserTests.swift @@ -111,6 +111,36 @@ class ListParserTests: XCTestCase { XCTAssertEqual(list[3].listItem.text.string, text2) } + func testParsesMultiLevelListIntoListNodes() { + let paraStyle1 = NSMutableParagraphStyle.forListLevel(1) + let paraStyle2 = NSMutableParagraphStyle.forListLevel(2) + + let text1 = "This is line 1. This is line 1. This is line 1. This is line 1.\n" + let text1a = "Subitem 1 Subitem 1.\n" + let text1b = "SubItem 2 SubItem 2.\n" + let text2 = "This is line 2. This is line 2. This is line 2." + + let attributedString = NSMutableAttributedString(string: text1, attributes: [.paragraphStyle: paraStyle1]) + attributedString.append(NSAttributedString(string: text1a, attributes: [.paragraphStyle: paraStyle2])) + attributedString.append(NSAttributedString(string: text1b, attributes: [.paragraphStyle: paraStyle2])) + attributedString.append(NSAttributedString(string: text2, attributes: [.paragraphStyle: paraStyle1])) + attributedString.addAttribute(.listItem, value: 1, range: attributedString.fullRange) + + let list = ListParser.parse(attributedString: attributedString) + let nodes = ListParser.createListItemNodes(from: list.map { $0.listItem }) + XCTAssertEqual(nodes.count, 2) + + XCTAssertEqual(nodes[0].item.level, 1) + XCTAssertEqual(nodes[0].children[0].item.level, 2) + XCTAssertEqual(nodes[0].children[1].item.level, 2) + XCTAssertEqual(nodes[1].item.level, 1) + + XCTAssertEqual(nodes[0].item.text.string, String(text1.prefix(text1.count - 1))) + XCTAssertEqual(nodes[0].children[0].item.text.string, String(text1a.prefix(text1a.count - 1))) + XCTAssertEqual(nodes[0].children[1].item.text.string, String(text1b.prefix(text1b.count - 1))) + XCTAssertEqual(nodes[1].item.text.string, text2) + } + func testParsesMultiLevelRepeatingList() { let levels = 3 let paraStyles = (1...levels).map { NSMutableParagraphStyle.forListLevel($0) } @@ -123,6 +153,47 @@ class ListParserTests: XCTestCase { attributedString.append(NSAttributedString(string: text, attributes: [.paragraphStyle: style])) } + attributedString.addAttribute(.listItem, value: 1, range: attributedString.fullRange) + let list = ListParser.parse(attributedString: attributedString) + let nodes = ListParser.createListItemNodes(from: list.map { $0.listItem }) + + XCTAssertEqual(nodes.count, 4) + for node in nodes { + XCTAssertEqual(node.item.text.string, "Text") + XCTAssertEqual(node.item.level, 1) + } + + XCTAssertEqual(nodes[0].children.count, 0) + + XCTAssertEqual(nodes[1].children.count, 2) + XCTAssertTrue(nodes[1].children.allSatisfy { $0.item.level == 2 }) + + XCTAssertEqual(nodes[1].children[0].children.count, 0) + + XCTAssertEqual(nodes[1].children[1].children.count, 2) + XCTAssertTrue(nodes[1].children[1].children.allSatisfy { $0.item.level == 3 }) + + + XCTAssertEqual(nodes[2].children.count, 0) + + XCTAssertEqual(nodes[3].children[0].children.count, 0) + + XCTAssertEqual(nodes[3].children[1].children.count, 2) + XCTAssertTrue(nodes[3].children[1].children.allSatisfy { $0.item.level == 3 }) + } + + func testParsesMultiLevelRepeatingListNodes() { + let levels = 3 + let paraStyles = (1...levels).map { NSMutableParagraphStyle.forListLevel($0) } + + let text = "Text\n" + let attributedString = NSMutableAttributedString() + for i in 0..