11package com .swmansion .enriched .common .parser ;
22
33import android .text .Editable ;
4- import android .text .Layout ;
54import android .text .Spannable ;
65import android .text .SpannableStringBuilder ;
76import android .text .Spanned ;
87import android .text .TextUtils ;
9- import android .text .style .AlignmentSpan ;
108import android .text .style .ParagraphStyle ;
119import com .swmansion .enriched .common .EnrichedConstants ;
10+ import com .swmansion .enriched .common .spans .EnrichedAlignmentSpan ;
1211import com .swmansion .enriched .common .spans .EnrichedBoldSpan ;
1312import com .swmansion .enriched .common .spans .EnrichedCheckboxListSpan ;
1413import com .swmansion .enriched .common .spans .EnrichedCodeBlockSpan ;
3029import com .swmansion .enriched .common .spans .interfaces .EnrichedBlockSpan ;
3130import com .swmansion .enriched .common .spans .interfaces .EnrichedInlineSpan ;
3231import com .swmansion .enriched .common .spans .interfaces .EnrichedParagraphSpan ;
32+ import com .swmansion .enriched .common .spans .interfaces .EnrichedSpan ;
3333import com .swmansion .enriched .common .spans .interfaces .EnrichedZeroWidthSpaceSpan ;
3434import java .io .IOException ;
3535import java .io .StringReader ;
3636import java .util .HashMap ;
3737import java .util .Map ;
38+ import java .util .regex .Matcher ;
39+ import java .util .regex .Pattern ;
3840import org .ccil .cowan .tagsoup .HTMLSchema ;
3941import org .ccil .cowan .tagsoup .Parser ;
4042import org .xml .sax .Attributes ;
@@ -86,8 +88,8 @@ public static String toHtml(Spanned text) {
8688 String normalizedBlockQuote =
8789 normalizedCodeBlock .replaceAll ("</blockquote>\\ n<br>" , "</blockquote>" );
8890
89- // replace empty <p></p> into <br> in the very end
90- String normalizedHtml = normalizedBlockQuote .replaceAll ("<p></p>" , "<br>" );
91+ // Replace empty <p> tags (with or without style attributes) with <br>
92+ String normalizedHtml = normalizedBlockQuote .replaceAll ("<p[^>]* ></p>" , "<br>" );
9193
9294 return "<html>\n " + normalizedHtml + "</html>" ;
9395 }
@@ -136,6 +138,14 @@ private static void withinDiv(StringBuilder out, Spanned text, int start, int en
136138 }
137139 }
138140
141+ private static String getAlignmentStyleAttr (Spanned text , int start , int end ) {
142+ EnrichedAlignmentSpan [] spans = text .getSpans (start , end , EnrichedAlignmentSpan .class );
143+ if (spans .length == 0 ) return "" ;
144+ String cssValue = spans [0 ].getCssValue ();
145+ if (cssValue .equals ("auto" )) return "" ;
146+ return " style=\" text-align: " + cssValue + "\" " ;
147+ }
148+
139149 private static String getBlockTag (EnrichedParagraphSpan [] spans ) {
140150 for (EnrichedParagraphSpan span : spans ) {
141151 if (span instanceof EnrichedUnorderedListSpan ) {
@@ -213,15 +223,17 @@ private static void withinBlock(StringBuilder out, Spanned text, int start, int
213223 if (isUlListItem && !isInUlList ) {
214224 // Current paragraph is the first item in a list
215225 isInUlList = true ;
216- out .append ("<ul" ).append (">\n " );
226+ out .append ("<ul" ).append (getAlignmentStyleAttr ( text , i , next )). append ( ">\n " );
217227 } else if (isOlListItem && !isInOlList ) {
218228 // Current paragraph is the first item in a list
219229 isInOlList = true ;
220- out .append ("<ol" ).append (">\n " );
230+ out .append ("<ol" ).append (getAlignmentStyleAttr ( text , i , next )). append ( ">\n " );
221231 } else if (isCheckboxListItem && !isInCheckboxList ) {
222232 // Current paragraph is the first item in a list
223233 isInCheckboxList = true ;
224- out .append ("<ul data-type=\" checkbox\" >\n " );
234+ out .append ("<ul data-type=\" checkbox\" " )
235+ .append (getAlignmentStyleAttr (text , i , next ))
236+ .append (">\n " );
225237 }
226238
227239 boolean isList = isUlListItem || isOlListItem || isCheckboxListItem ;
@@ -230,6 +242,11 @@ private static void withinBlock(StringBuilder out, Spanned text, int start, int
230242 out .append ("<" );
231243 out .append (tagType );
232244
245+ // Add alignment style to non-list paragraph/heading tags
246+ if (!isList ) {
247+ out .append (getAlignmentStyleAttr (text , i , next ));
248+ }
249+
233250 if (isCheckboxListItem ) {
234251 EnrichedCheckboxListSpan [] checkboxSpans =
235252 text .getSpans (i , next , EnrichedCheckboxListSpan .class );
@@ -395,6 +412,24 @@ class HtmlToSpannedConverter<T> implements ContentHandler {
395412 private static Boolean isInOrderedList = false ;
396413 private static Boolean isInCheckboxList = false ;
397414 private static Boolean isEmptyTag = false ;
415+ private static String currentListAlignmentCssValue = null ;
416+
417+ private static final Pattern CSS_ALIGNMENT_PATTERN =
418+ Pattern .compile ("text-align\\ s*:\\ s*(left|center|right)" , Pattern .CASE_INSENSITIVE );
419+
420+ private static String parseCssAlignmentValue (Attributes attributes ) {
421+ String style = attributes .getValue ("" , "style" );
422+ if (style == null ) return null ;
423+ Matcher m = CSS_ALIGNMENT_PATTERN .matcher (style );
424+ return m .find () ? m .group (1 ).toLowerCase () : null ;
425+ }
426+
427+ private static void pushAlignmentMark (Editable text , Attributes attributes ) {
428+ String cssValue = parseCssAlignmentValue (attributes );
429+ if (cssValue != null ) {
430+ start (text , new Alignment (cssValue ));
431+ }
432+ }
398433
399434 public HtmlToSpannedConverter (
400435 String source , T style , Parser parser , EnrichedSpanFactory <T > spanFactory ) {
@@ -448,9 +483,24 @@ public Spanned convert() {
448483 int end = mSpannableStringBuilder .getSpanEnd (zeroWidthSpaceSpan );
449484
450485 if (mSpannableStringBuilder .charAt (start ) != EnrichedConstants .ZWS ) {
451- // Insert zero-width space character at the start if it's not already present.
486+ // Collect spans before inserting ZWS. SPAN_EXCLUSIVE_EXCLUSIVE spans will
487+ // shift to start+1. We must re-anchor them back to `start` to prevent
488+ // the loop from processing them again and inserting duplicate ZWS.
489+ EnrichedSpan [] colocated =
490+ mSpannableStringBuilder .getSpans (start , start + 1 , EnrichedSpan .class );
491+
452492 mSpannableStringBuilder .insert (start , EnrichedConstants .ZWS_STRING );
453- end ++; // Adjust end position due to insertion.
493+ end ++;
494+
495+ for (EnrichedSpan span : colocated ) {
496+ if (span == zeroWidthSpaceSpan ) continue ;
497+ // Only re-anchor spans that actually shifted.
498+ // Skip overlapping or INCLUSIVE spans that kept their original start.
499+ if (mSpannableStringBuilder .getSpanStart (span ) != start + 1 ) continue ;
500+ int spanEnd = mSpannableStringBuilder .getSpanEnd (span );
501+ mSpannableStringBuilder .removeSpan (span );
502+ mSpannableStringBuilder .setSpan (span , start , spanEnd , Spannable .SPAN_EXCLUSIVE_EXCLUSIVE );
503+ }
454504 }
455505
456506 mSpannableStringBuilder .removeSpan (zeroWidthSpaceSpan );
@@ -468,14 +518,17 @@ private void handleStartTag(String tag, Attributes attributes) {
468518 } else if (tag .equalsIgnoreCase ("p" )) {
469519 isEmptyTag = true ;
470520 startBlockElement (mSpannableStringBuilder );
521+ pushAlignmentMark (mSpannableStringBuilder , attributes );
471522 } else if (tag .equalsIgnoreCase ("ul" )) {
472523 isInOrderedList = false ;
473524 String dataType = attributes .getValue ("" , "data-type" );
474525 isInCheckboxList = "checkbox" .equals (dataType );
526+ currentListAlignmentCssValue = parseCssAlignmentValue (attributes );
475527 startBlockElement (mSpannableStringBuilder );
476528 } else if (tag .equalsIgnoreCase ("ol" )) {
477529 isInOrderedList = true ;
478530 currentOrderedListItemIndex = 0 ;
531+ currentListAlignmentCssValue = parseCssAlignmentValue (attributes );
479532 startBlockElement (mSpannableStringBuilder );
480533 } else if (tag .equalsIgnoreCase ("li" )) {
481534 isEmptyTag = true ;
@@ -500,16 +553,22 @@ private void handleStartTag(String tag, Attributes attributes) {
500553 start (mSpannableStringBuilder , new Strikethrough ());
501554 } else if (tag .equalsIgnoreCase ("h1" )) {
502555 startHeading (mSpannableStringBuilder , 1 );
556+ pushAlignmentMark (mSpannableStringBuilder , attributes );
503557 } else if (tag .equalsIgnoreCase ("h2" )) {
504558 startHeading (mSpannableStringBuilder , 2 );
559+ pushAlignmentMark (mSpannableStringBuilder , attributes );
505560 } else if (tag .equalsIgnoreCase ("h3" )) {
506561 startHeading (mSpannableStringBuilder , 3 );
562+ pushAlignmentMark (mSpannableStringBuilder , attributes );
507563 } else if (tag .equalsIgnoreCase ("h4" )) {
508564 startHeading (mSpannableStringBuilder , 4 );
565+ pushAlignmentMark (mSpannableStringBuilder , attributes );
509566 } else if (tag .equalsIgnoreCase ("h5" )) {
510567 startHeading (mSpannableStringBuilder , 5 );
568+ pushAlignmentMark (mSpannableStringBuilder , attributes );
511569 } else if (tag .equalsIgnoreCase ("h6" )) {
512570 startHeading (mSpannableStringBuilder , 6 );
571+ pushAlignmentMark (mSpannableStringBuilder , attributes );
513572 } else if (tag .equalsIgnoreCase ("img" )) {
514573 // Image content means the current tag is not empty (e.g. <li><img .../></li>).
515574 isEmptyTag = false ;
@@ -525,9 +584,13 @@ private void handleEndTag(String tag) {
525584 if (tag .equalsIgnoreCase ("br" )) {
526585 handleBr (mSpannableStringBuilder );
527586 } else if (tag .equalsIgnoreCase ("p" )) {
528- endBlockElement (mSpannableStringBuilder );
587+ endBlockElement (mSpannableStringBuilder , mSpanFactory );
529588 } else if (tag .equalsIgnoreCase ("ul" )) {
530- endBlockElement (mSpannableStringBuilder );
589+ currentListAlignmentCssValue = null ;
590+ endBlockElement (mSpannableStringBuilder , mSpanFactory );
591+ } else if (tag .equalsIgnoreCase ("ol" )) {
592+ currentListAlignmentCssValue = null ;
593+ endBlockElement (mSpannableStringBuilder , mSpanFactory );
531594 } else if (tag .equalsIgnoreCase ("li" )) {
532595 endLi (mSpannableStringBuilder , mStyle , mSpanFactory );
533596 } else if (tag .equalsIgnoreCase ("b" )) {
@@ -585,15 +648,15 @@ private static void startBlockElement(Editable text) {
585648 start (text , new Newline (1 ));
586649 }
587650
588- private static void endBlockElement (Editable text ) {
651+ private static < T > void endBlockElement (Editable text , EnrichedSpanFactory < T > spanFactory ) {
589652 Newline n = getLast (text , Newline .class );
590653 if (n != null ) {
591654 appendNewlines (text , n .mNumNewlines );
592655 text .removeSpan (n );
593656 }
594657 Alignment a = getLast (text , Alignment .class );
595658 if (a != null ) {
596- setSpanFromMark (text , a , new AlignmentSpan . Standard (a .mAlignment ));
659+ setParagraphSpanFromMark (text , a , spanFactory . createAlignmentSpan (a .mCssValue ));
597660 }
598661 }
599662
@@ -604,6 +667,10 @@ private static void handleBr(Editable text) {
604667 private void startLi (Editable text , Attributes attributes ) {
605668 startBlockElement (text );
606669
670+ if (currentListAlignmentCssValue != null ) {
671+ start (text , new Alignment (currentListAlignmentCssValue ));
672+ }
673+
607674 if (isInOrderedList ) {
608675 currentOrderedListItemIndex ++;
609676 start (text , new List ("ordered" , currentOrderedListItemIndex , false ));
@@ -616,7 +683,7 @@ private void startLi(Editable text, Attributes attributes) {
616683 }
617684
618685 private static <T > void endLi (Editable text , T style , EnrichedSpanFactory <T > spanFactory ) {
619- endBlockElement (text );
686+ endBlockElement (text , spanFactory );
620687
621688 List l = getLast (text , List .class );
622689 if (l != null ) {
@@ -629,7 +696,7 @@ private static <T> void endLi(Editable text, T style, EnrichedSpanFactory<T> spa
629696 }
630697 }
631698
632- endBlockElement (text );
699+ endBlockElement (text , spanFactory );
633700 }
634701
635702 private void startBlockquote (Editable text ) {
@@ -639,7 +706,7 @@ private void startBlockquote(Editable text) {
639706
640707 private static <T > void endBlockquote (
641708 Editable text , T style , EnrichedSpanFactory <T > spanFactory ) {
642- endBlockElement (text );
709+ endBlockElement (text , spanFactory );
643710 Blockquote last = getLast (text , Blockquote .class );
644711 setParagraphSpanFromMark (text , last , spanFactory .createBlockQuoteSpan (style ));
645712 }
@@ -650,7 +717,7 @@ private void startCodeBlock(Editable text) {
650717 }
651718
652719 private static <T > void endCodeBlock (Editable text , T style , EnrichedSpanFactory <T > spanFactory ) {
653- endBlockElement (text );
720+ endBlockElement (text , spanFactory );
654721 CodeBlock last = getLast (text , CodeBlock .class );
655722 setParagraphSpanFromMark (text , last , spanFactory .createCodeBlockSpan (style ));
656723 }
@@ -684,7 +751,7 @@ private void startHeading(Editable text, int level) {
684751
685752 private static <T > void endHeading (
686753 Editable text , T style , EnrichedSpanFactory <T > spanFactory , int level ) {
687- endBlockElement (text );
754+ endBlockElement (text , spanFactory );
688755
689756 switch (level ) {
690757 case 1 :
@@ -958,10 +1025,10 @@ public Newline(int numNewlines) {
9581025 }
9591026
9601027 private static class Alignment {
961- private final Layout . Alignment mAlignment ;
1028+ final String mCssValue ;
9621029
963- public Alignment (Layout . Alignment alignment ) {
964- mAlignment = alignment ;
1030+ public Alignment (String cssValue ) {
1031+ this . mCssValue = cssValue ;
9651032 }
9661033 }
9671034}
0 commit comments