Skip to content

Commit

Permalink
Added ability to convert LitItem collection from ListParser to hierar…
Browse files Browse the repository at this point in the history
…chal representation
  • Loading branch information
rajdeep committed Jul 9, 2024
1 parent 1698682 commit 81483ae
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 0 deletions.
41 changes: 41 additions & 0 deletions Proton/Sources/Swift/Core/ListParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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)]()
Expand Down
71 changes: 71 additions & 0 deletions Proton/Tests/Core/ListParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand All @@ -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..<levels * 2 {
let style = paraStyles[i % levels]
attributedString.append(NSAttributedString(string: text, attributes: [.paragraphStyle: style]))
attributedString.append(NSAttributedString(string: text, attributes: [.paragraphStyle: style]))
}

attributedString.addAttribute(.listItem, value: 1, range: attributedString.fullRange)
let list = ListParser.parse(attributedString: attributedString)
XCTAssertEqual(list.count, 12)
Expand Down

0 comments on commit 81483ae

Please sign in to comment.