Skip to content
This repository was archived by the owner on Jul 25, 2024. It is now read-only.

Commit 1c0b924

Browse files
Sam1301kunall17
authored andcommitted
Add support for ordered/undered lists in messages.
1 parent 2b392e4 commit 1c0b924

File tree

2 files changed

+188
-1
lines changed

2 files changed

+188
-1
lines changed

app/src/main/java/com/zulip/android/models/Message.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.zulip.android.ZulipApp;
2323
import com.zulip.android.util.Constants;
2424
import com.zulip.android.util.CustomHtmlToSpannedConverter;
25+
import com.zulip.android.util.ListTagHandler;
2526
import com.zulip.android.util.UrlHelper;
2627
import com.zulip.android.util.ZLog;
2728

@@ -361,7 +362,7 @@ public Drawable getDrawable(String source) {
361362
};
362363

363364
CustomHtmlToSpannedConverter converter = new CustomHtmlToSpannedConverter(
364-
source, null, null, parser, emojiGetter, app.getServerURI(), context);
365+
source, null, new ListTagHandler(), parser, emojiGetter, app.getServerURI(), context);
365366

366367
return CustomHtmlToSpannedConverter.linkifySpanned(converter.convert(), Linkify.ALL);
367368
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package com.zulip.android.util;
2+
3+
import android.text.Editable;
4+
import android.text.Html;
5+
import android.text.Spanned;
6+
import android.text.style.BulletSpan;
7+
import android.text.style.LeadingMarginSpan;
8+
import android.util.Log;
9+
10+
import org.xml.sax.XMLReader;
11+
12+
import java.util.Stack;
13+
14+
/**
15+
* Custom TagHandler {@link Html.TagHandler} to support ordered and unordered list in
16+
* TextView {@link android.widget.TextView}.
17+
*/
18+
19+
public class ListTagHandler implements Html.TagHandler {
20+
private static final String LOG_TAG = ListTagHandler.class.getSimpleName();
21+
22+
private static final String OL_TAG = "ol";
23+
private static final String UL_TAG = "ul";
24+
private static final String LI_TAG = "li";
25+
26+
/**
27+
* List indentation in pixels.
28+
*/
29+
private static final int INDENT_PX = 10;
30+
private static final int LIST_ITEM_INDENT_PX = INDENT_PX * 2;
31+
private static final BulletSpan BULLET_SPAN = new BulletSpan(INDENT_PX);
32+
33+
/**
34+
* Keeps track of lists (ol, ul). On bottom of Stack is the outermost list
35+
* and on top of Stack is the most nested list.
36+
*/
37+
private final Stack<ListTag> lists = new Stack<ListTag>();
38+
39+
@Override
40+
public void handleTag(final boolean opening, final String tag, final Editable output, final XMLReader xmlReader) {
41+
if (UL_TAG.equalsIgnoreCase(tag)) {
42+
if (opening) {
43+
// handle <ul>
44+
lists.push(new Ul());
45+
} else {
46+
// handle </ul>
47+
lists.pop();
48+
}
49+
} else if (OL_TAG.equalsIgnoreCase(tag)) {
50+
if (opening) {
51+
// handle <ol>
52+
// use default start index of 1
53+
lists.push(new Ol());
54+
} else {
55+
// handle </ol>
56+
lists.pop();
57+
}
58+
} else if (LI_TAG.equalsIgnoreCase(tag)) {
59+
if (opening) {
60+
// handle <li>
61+
lists.peek().openItem(output);
62+
} else {
63+
// handle </li>
64+
lists.peek().closeItem(output, lists.size());
65+
}
66+
} else {
67+
Log.d(LOG_TAG, "Found an unsupported tag " + tag);
68+
}
69+
}
70+
71+
/**
72+
* Abstract super class for {@link Ul} and {@link Ol}.
73+
*/
74+
private abstract static class ListTag {
75+
/**
76+
* Opens a new list item.
77+
*
78+
* @param text
79+
*/
80+
public void openItem(final Editable text) {
81+
if (text.length() > 0 && text.charAt(text.length() - 1) != '\n') {
82+
text.append("\n");
83+
}
84+
final int len = text.length();
85+
text.setSpan(this, len, len, Spanned.SPAN_MARK_MARK);
86+
}
87+
88+
/**
89+
* Closes a list item.
90+
*
91+
* @param text
92+
* @param indentation
93+
*/
94+
public final void closeItem(final Editable text, final int indentation) {
95+
if (text.length() > 0 && text.charAt(text.length() - 1) != '\n') {
96+
text.append("\n");
97+
}
98+
final Object[] replaces = getReplaces(text, indentation);
99+
final int len = text.length();
100+
final ListTag listTag = getLast(text);
101+
final int where = text.getSpanStart(listTag);
102+
text.removeSpan(listTag);
103+
if (where != len) {
104+
for (Object replace : replaces) {
105+
text.setSpan(replace, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
106+
}
107+
}
108+
}
109+
110+
protected abstract Object[] getReplaces(final Editable text, final int indentation);
111+
112+
/**
113+
* Note: This knows that the last returned object from getSpans() will be the most recently added.
114+
*/
115+
private ListTag getLast(final Spanned text) {
116+
final ListTag[] listTags = text.getSpans(0, text.length(), ListTag.class);
117+
if (listTags.length == 0) {
118+
return null;
119+
}
120+
return listTags[listTags.length - 1];
121+
}
122+
}
123+
124+
/**
125+
* Class representing the unordered list <ul></ul> HTML tag.
126+
*/
127+
private static class Ul extends ListTag {
128+
129+
@Override
130+
protected Object[] getReplaces(final Editable text, final int indentation) {
131+
// Nested BulletSpans increases distance between BULLET_SPAN and text, so we must prevent it.
132+
int bulletMargin = INDENT_PX;
133+
if (indentation > 1) {
134+
bulletMargin = INDENT_PX - BULLET_SPAN.getLeadingMargin(true);
135+
if (indentation > 2) {
136+
// This get's more complicated when we add a LeadingMarginSpan into the same line:
137+
// we have also counter it's effect to BulletSpan
138+
bulletMargin -= (indentation - 2) * LIST_ITEM_INDENT_PX;
139+
}
140+
}
141+
return new Object[]{
142+
new LeadingMarginSpan.Standard(LIST_ITEM_INDENT_PX * (indentation - 1)),
143+
new BulletSpan(bulletMargin)
144+
};
145+
}
146+
}
147+
148+
/**
149+
* Class representing the ordered list <ol></ol> HTML tag.
150+
*/
151+
private static class Ol extends ListTag {
152+
private int nextIdx;
153+
154+
/**
155+
* Creates a new <ol></ol> with start index of 1.
156+
*/
157+
public Ol() {
158+
this(1); // default start index
159+
}
160+
161+
/**
162+
* Creates a new <ol></ol> with given start index.
163+
*
164+
* @param startIdx
165+
*/
166+
public Ol(final int startIdx) {
167+
this.nextIdx = startIdx;
168+
}
169+
170+
@Override
171+
public void openItem(final Editable text) {
172+
super.openItem(text);
173+
text.append(Integer.toString(nextIdx++)).append(". ");
174+
}
175+
176+
@Override
177+
protected Object[] getReplaces(final Editable text, final int indentation) {
178+
int numberMargin = LIST_ITEM_INDENT_PX * (indentation - 1);
179+
if (indentation > 2) {
180+
// Same as in ordered lists: counter the effect of nested Spans
181+
numberMargin -= (indentation - 2) * LIST_ITEM_INDENT_PX;
182+
}
183+
return new Object[]{new LeadingMarginSpan.Standard(numberMargin)};
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)