rajdeep / proton

Purely native and extensible rich text editor for iOS and macOS Catalyst apps
Other
1.26k stars 81 forks source link

Lists with formatted text is broken into multiple paragraphs when exporting #246

Closed danielallsopp closed 6 months ago

danielallsopp commented 10 months ago

I'm currently writing an HTML exporter for Proton, using the JSON examples and the ListParserTests as a guide. Everything works great if the text in any of the list items doesn't contain any formatting, such as bold or italic:

The ListParser will return an array containing 3 ListItem values, i.e. the enumerateInlineContents().forEach { content in } block will contain the entire list in the content variable, i.e. I can add the opening tag <ul>, enumerate through the ListItems adding each one to my encoded string, and then finally close the list with </ul>. The encoded string looks like this:

Encoded String: '["<ul><li>​one</li><li>two</li><li>three</li></ul>"]'

However, if I format the text in the list, like this:

Then the ListParser will return a new paragraph for each formatted item, which is making it difficult for me to differentiate when a list should be opened and closed so my encoded string ends up like this:

Encoded String: '["<ul><li><b>one</b></li></ul><ul><li><i>two</i></li></ul><ul><li><i><b>three</b></i></li></ul>"]'

Any ideas, or is this a known issue, or do I need to rethink my strategy for encoding lists?

rajdeep commented 9 months ago

hi @danielallsopp, this is by design in iOS. In attributedString representation, each new line is a start of a new paragraph. I am not sure I understand your question correctly as for both the cases, you should be getting a new paragraph for each line of the text. I verified this again using the following in my tests:

  var list = [ListItem]()

        let indent: CGFloat = 50

        list.append(ListItem(text: NSAttributedString(string: "Item 1"), level: 1, attributeValue: 1))
        list.append(ListItem(text: NSAttributedString(string: "Item 2"), level: 1, attributeValue: 1))
        //         list.append(ListItem(text: NSAttributedString(string: "Item 2", attributes: [.font: UIFont.systemFont(ofSize: 18, weight: .bold)]), level: 1, attributeValue: 1))
        list.append(ListItem(text: NSAttributedString(string: "Item 3"), level: 1, attributeValue: 2))

        let text = ListParser.parse(list: list, indent: indent)
        print(text)

The output in terms of structure is same if you use the commented line for "Item 2" or the uncommented one.

Are you able to provide a test that can help me understand the issue better?

Also, you may consider the following function in combination with what you are using which will still allow you to get the range of text for each of the list item, which you can possibly use to append list tags:

func parse(attributedString: NSAttributedString, indent: CGFloat = 25) -> [(range: NSRange, listItem: ListItem)] 
danielallsopp commented 9 months ago

Hi @rajdeep Thanks for getting back to me.

each new line is a start of a new paragraph

This is not the case from my tests though; if I create a list with no formatting on any of the list items, then the whole list, i.e. each list item, is sent in one paragraph block from the string.enumerateInlineContents().forEach call, like this:

​these
are
passed
in
one
paragraph {
    NSColor = "<UIDynamicCatalogSystemColor: 0x600002b69000; name = labelColor>";
    NSFont = "<UICTFont: 0x7f809e1a8530> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 17.00pt";
    NSParagraphStyle = "Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 25, TailIndent 0, FirstLineHeadIndent 25, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (\n    28L,\n    56L,\n    84L,\n    112L,\n    140L,\n    168L,\n    196L,\n    224L,\n    252L,\n    280L,\n    308L,\n    336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection 0, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint ''";
    "_listItem" = "TextEditorFormat.unorderedList";
}), enclosingRange: Optional({0, 35}))

When I parse the paragraph with ListParser then yes, each item is extracted one at a time. This way I can easily create an unordered list in HTML, because if I know the paragraph is a list I can create opening and closing tags:

<ul>
ParseList -> Create an li for each list item
</ul>

When there is formatting on the list items then I don't get the whole list in one paragraph, i.e. in the string.enumerateInlineContents().forEach block each line item is sent as a new paragraph which makes it impossible to know when to open and close the list with HTML tags. For example:

​these
{
    NSColor = "<UIDynamicCatalogSystemColor: 0x600002b69000; name = labelColor>";
    NSFont = "<UICTFont: 0x7f809e1a8530> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 17.00pt";
    NSParagraphStyle = "Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 25, TailIndent 0, FirstLineHeadIndent 25, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (\n    28L,\n    56L,\n    84L,\n    112L,\n    140L,\n    168L,\n    196L,\n    224L,\n    252L,\n    280L,\n    308L,\n    336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection 0, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint ''";
    "_listItem" = "TextEditorFormat.unorderedList";
}), enclosingRange: Optional({0, 7}))

are{
    NSColor = "<UIDynamicCatalogSystemColor: 0x600002b69000; name = labelColor>";
    NSFont = "<UICTFont: 0x7f809e19e350> font-family: \".SFUI-SemiboldItalic\"; font-weight: bold; font-style: italic; font-size: 17.00pt";
    NSParagraphStyle = "Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 25, TailIndent 0, FirstLineHeadIndent 25, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (\n    28L,\n    56L,\n    84L,\n    112L,\n    140L,\n    168L,\n    196L,\n    224L,\n    252L,\n    280L,\n    308L,\n    336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection 0, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint ''";
    "_listItem" = "TextEditorFormat.unorderedList";
}), enclosingRange: Optional({7, 3}))

passed{
    NSColor = "<UIDynamicCatalogSystemColor: 0x600002b69000; name = labelColor>";
    NSFont = "<UICTFont: 0x7f809d7ca790> font-family: \".SFUI-RegularItalic\"; font-weight: normal; font-style: italic; font-size: 17.00pt";
    NSParagraphStyle = "Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 25, TailIndent 0, FirstLineHeadIndent 25, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (\n    28L,\n    56L,\n    84L,\n    112L,\n    140L,\n    168L,\n    196L,\n    224L,\n    252L,\n    280L,\n    308L,\n    336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection 0, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint ''";
    "_listItem" = "TextEditorFormat.unorderedList";
}), enclosingRange: Optional({11, 6}))

etc.

Let me know if you need any more information and I'll try and put a test project together. Parsing the list isn't a problem, but knowing when to insert the opening and closing list tags is.

rajdeep commented 9 months ago

@danielallsopp, this is by design in case of enumerateInlineContents. How I see this being used is to get fine grain information of the contents, including text, for something like persistence to JSON or similar.

If you notice, in the example you shared, the font for each line is different, even though paragraphStyle is the same:

​these
{
 ...
    NSFont = "<UICTFont: 0x7f809e1a8530> **font-family: \".SFUI-Regular\";** font-weight: normal; font-style: normal; font-size: 17.00pt";
...

are{
 ...
    NSFont = "<UICTFont: 0x7f809e19e350> font-family: \".SFUI-SemiboldItalic\"; font-weight: bold; font-style: italic; font-size: 17.00pt";
    ...
passed{
    ...
    NSFont = "<UICTFont: 0x7f809d7ca790> font-family: \".SFUI-RegularItalic\"; font-weight: normal; font-style: italic; font-size: 17.00pt";
...
etc.

If you would like to get info individually, you may try one of the following:

Also, for your purposes, can you not just use the contents parsed by ListParser and take the ranges from that to append the list start and end tags?

Hope this helps.

If you feel I am still missing the context, I think best would be to provide a failing unit test that I can look at to understand your requirements. 🙂

danielallsopp commented 9 months ago

Thanks for your suggestions @rajdeep, I'm going to go through my code now and see if I can use them to get around my issue. I will get back to you as soon as possible with my findings.

rajdeep commented 6 months ago

please feel to reopen this issue if any further discussion is required.