Search code examples
swiftnsattributedstringrtfnsrange

Extract table from NSAttributedString


Suppose I have read NSAttributedString from a file (html or rtf) and in that file I clearly see several tables. Is there a way to extract those tables or at least find an NSRange that corresponds to the table? It would be ideal if I could somehow extract (array of) NSTextTableBlock,NSTextTable or NSTextBlock from NSAttributedString. But if that's not possible, then at least there should be a way to find NSRanges of table cells or something similar. Swift (possibly 4) is preferred but obj-c is fine too.

For example imagine such scenario:

let html =
"""
<table style="height: 51px;" width="147">
    <tbody>
        <tr>
            <td style="width: 65.5px;">a</td>
            <td style="width: 65.5px;">b</td>
        </tr>
        <tr>
            <td style="width: 65.5px;">c</td>
            <td style="width: 65.5px;">d</td>
        </tr>
    </tbody>
</table>
"""
var str = NSAttributedString(html: html.data(using: .utf8)!,    options: [:], documentAttributes: nil)!

and then I would like to do something more or less like this:

for table in str{
    for row in table{
        for cell in row{
            //do something
        }
    }
}

Solution

  • I found a bit naive solution to this problem but it works. You basically iterate over all characters in NSAttributedString, query their attributes and then check if among them is a NSParagraphStyle with a table.

    This piece of code extracts array of NSTextTable from given location (remember that tables can be nested)

    extension NSAttributedString{
    
    func paragraphStyle(at index:Int)->NSParagraphStyle?{
        let key = NSAttributedStringKey.paragraphStyle
        return attribute(key, at: index, effectiveRange: nil) as! NSParagraphStyle?
    }
    func textBlocks(at index:Int)->[NSTextBlock]?{
        return paragraphStyle(at: index)?.textBlocks
    }
    func tables(at index:Int)->[NSTextTable]?{
        guard let tbs = textBlocks(at: index) else{
            return nil
        }
        var output = Set<NSTextTable>()
        for tb in tbs{
            if let t = tb as? NSTextTableBlock{
                output.insert(t.table)
            }
        }
        return Array(output)
    }
    
    }
    

    And this could help you to collect all tables (except nested tables - in order to collect them too, you would have to run this function recursively inside every table):

    extension NSAttributedString{
    
    var outterTables:[NSTextTable]{
        var index = 0
        let len = length
        var output:[NSTextTable] = []
        while index < len{
            if let tab = outterTable(at: index){
                output.append(tab)
                index = range(of: tab, at: index).upperBound
            }else{
                index += 1
            }
        }
        return output
    }
    
    }