1 | /* |
2 | * Tomdroid |
3 | * Tomboy on Android |
4 | * http://www.launchpad.net/tomdroid |
5 | * |
6 | * Copyright 2008, 2009, 2010 Olivier Bilodeau <olivier@bottomlesspit.org> |
7 | * Copyright 2009, Benoit Garret <benoit.garret_launchpad@gadz.org> |
8 | * |
9 | * This file is part of Tomdroid. |
10 | * |
11 | * Tomdroid is free software: you can redistribute it and/or modify |
12 | * it under the terms of the GNU General Public License as published by |
13 | * the Free Software Foundation, either version 3 of the License, or |
14 | * (at your option) any later version. |
15 | * |
16 | * Tomdroid is distributed in the hope that it will be useful, |
17 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
18 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
19 | * GNU General Public License for more details. |
20 | * |
21 | * You should have received a copy of the GNU General Public License |
22 | * along with Tomdroid. If not, see <http://www.gnu.org/licenses/>. |
23 | */ |
24 | package org.tomdroid.xml; |
25 | |
26 | import java.util.ArrayList; |
27 | |
28 | import org.tomdroid.Note; |
29 | import org.xml.sax.Attributes; |
30 | import org.xml.sax.SAXException; |
31 | import org.xml.sax.helpers.DefaultHandler; |
32 | |
33 | import android.text.Spannable; |
34 | import android.text.SpannableStringBuilder; |
35 | import android.text.style.BackgroundColorSpan; |
36 | import android.text.style.BulletSpan; |
37 | import android.text.style.LeadingMarginSpan; |
38 | import android.text.style.RelativeSizeSpan; |
39 | import android.text.style.StrikethroughSpan; |
40 | import android.text.style.StyleSpan; |
41 | import android.text.style.TypefaceSpan; |
42 | |
43 | /* |
44 | * This class is responsible for parsing the xml note content |
45 | * and formatting the contents in a SpannableStringBuilder |
46 | */ |
47 | public class NoteContentHandler extends DefaultHandler { |
48 | |
49 | // position keepers |
50 | private boolean inNoteContentTag = false; |
51 | private boolean inBoldTag = false; |
52 | private boolean inItalicTag = false; |
53 | private boolean inStrikeTag = false; |
54 | private boolean inHighlighTag = false; |
55 | private boolean inMonospaceTag = false; |
56 | private boolean inSizeSmallTag = false; |
57 | private boolean inSizeLargeTag = false; |
58 | private boolean inSizeHugeTag = false; |
59 | private int inListLevel = 0; |
60 | private boolean inListItem = false; |
61 | |
62 | // -- Tomboy's notes XML tags names -- |
63 | // Style related |
64 | private final static String NOTE_CONTENT = "note-content"; |
65 | private final static String BOLD = "bold"; |
66 | private final static String ITALIC = "italic"; |
67 | private final static String STRIKETHROUGH = "strikethrough"; |
68 | private final static String HIGHLIGHT = "highlight"; |
69 | private final static String MONOSPACE = "monospace"; |
70 | private final static String SMALL = "size:small"; |
71 | private final static String LARGE = "size:large"; |
72 | private final static String HUGE = "size:huge"; |
73 | // Bullet list-related |
74 | private final static String LIST = "list"; |
75 | private final static String LIST_ITEM = "list-item"; |
76 | |
77 | // holding state for tags |
78 | private int boldStartPos; |
79 | private int boldEndPos; |
80 | private int italicStartPos; |
81 | private int italicEndPos; |
82 | private int strikethroughStartPos; |
83 | private int strikethroughEndPos; |
84 | private int highlightStartPos; |
85 | private int highlightEndPos; |
86 | private int monospaceStartPos; |
87 | private int monospaceEndPos; |
88 | private int smallStartPos; |
89 | private int smallEndPos; |
90 | private int largeStartPos; |
91 | private int largeEndPos; |
92 | private int hugeStartPos; |
93 | private int hugeEndPos; |
94 | private ArrayList<Integer> listItemStartPos = new ArrayList<Integer>(0); |
95 | private ArrayList<Integer> listItemEndPos = new ArrayList<Integer>(0); |
96 | private ArrayList<Boolean> listItemIsEmpty = new ArrayList<Boolean>(0); |
97 | |
98 | // accumulate note-content in this var since it spans multiple xml tags |
99 | private SpannableStringBuilder ssb; |
100 | |
101 | public NoteContentHandler(SpannableStringBuilder noteContent) { |
102 | |
103 | this.ssb = noteContent; |
104 | } |
105 | |
106 | @Override |
107 | public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException { |
108 | |
109 | if (name.equals(NOTE_CONTENT)) { |
110 | |
111 | // we are under the note-content tag |
112 | // we will append all its nested tags so I create a string builder to do that |
113 | inNoteContentTag = true; |
114 | } |
115 | |
116 | // if we are in note-content, keep and convert formatting tags |
117 | // TODO is XML CaSe SeNsItIve? if not change equals to equalsIgnoreCase and apply to endElement() |
118 | if (inNoteContentTag) { |
119 | if (name.equals(BOLD)) { |
120 | inBoldTag = true; |
121 | } else if (name.equals(ITALIC)) { |
122 | inItalicTag = true; |
123 | } else if (name.equals(STRIKETHROUGH)) { |
124 | inStrikeTag = true; |
125 | } else if (name.equals(HIGHLIGHT)) { |
126 | inHighlighTag = true; |
127 | } else if (name.equals(MONOSPACE)) { |
128 | inMonospaceTag = true; |
129 | } else if (name.equals(SMALL)) { |
130 | inSizeSmallTag = true; |
131 | } else if (name.equals(LARGE)) { |
132 | inSizeLargeTag = true; |
133 | } else if (name.equals(HUGE)) { |
134 | inSizeHugeTag = true; |
135 | } else if (name.equals(LIST)) { |
136 | inListLevel++; |
137 | } else if (name.equals(LIST_ITEM)) { |
138 | // Book keeping of where the list-items started and where they end. |
139 | // we need to do this here because a list-item must always have a start, |
140 | // but it doesn't always have any content--so we must assume that a list-item |
141 | // is empty until characters() gets called and proves otherwise. |
142 | |
143 | if (listItemIsEmpty.size() < inListLevel) { |
144 | listItemIsEmpty.add(new Boolean(true)); |
145 | } |
146 | // if listItem's position not already in tracking array, add it. |
147 | // Otherwise if the start position equals 0 then set |
148 | if (listItemStartPos.size() < inListLevel) { |
149 | listItemStartPos.add(new Integer(ssb.length())); |
150 | } else if (listItemStartPos.get(inListLevel-1) == 0) { |
151 | listItemStartPos.set(inListLevel-1, new Integer(ssb.length())); |
152 | } |
153 | // no matter what, we track the end (we add if array not big enough or set otherwise) |
154 | if (listItemEndPos.size() < inListLevel) { |
155 | listItemEndPos.add(new Integer(ssb.length())); |
156 | } else { |
157 | listItemEndPos.set(inListLevel-1, ssb.length()); |
158 | } |
159 | inListItem = true; |
160 | } |
161 | } |
162 | |
163 | } |
164 | |
165 | @Override |
166 | public void endElement(String uri, String localName, String name) |
167 | throws SAXException { |
168 | |
169 | if (name.equals(NOTE_CONTENT)) { |
170 | inNoteContentTag = false; |
171 | } |
172 | |
173 | // if we are in note-content, keep and convert formatting tags |
174 | if (inNoteContentTag) { |
175 | if (name.equals(BOLD)) { |
176 | inBoldTag = false; |
177 | // apply style and reset position keepers |
178 | ssb.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), boldStartPos, boldEndPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
179 | boldStartPos = 0; |
180 | boldEndPos = 0; |
181 | |
182 | } else if (name.equals(ITALIC)) { |
183 | inItalicTag = false; |
184 | // apply style and reset position keepers |
185 | ssb.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), italicStartPos, italicEndPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
186 | italicStartPos = 0; |
187 | italicEndPos = 0; |
188 | |
189 | } else if (name.equals(STRIKETHROUGH)) { |
190 | inStrikeTag = false; |
191 | // apply style and reset position keepers |
192 | ssb.setSpan(new StrikethroughSpan(), strikethroughStartPos, strikethroughEndPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
193 | strikethroughStartPos = 0; |
194 | strikethroughEndPos = 0; |
195 | |
196 | } else if (name.equals(HIGHLIGHT)) { |
197 | inHighlighTag = false; |
198 | // apply style and reset position keepers |
199 | ssb.setSpan(new BackgroundColorSpan(Note.NOTE_HIGHLIGHT_COLOR), highlightStartPos, highlightEndPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
200 | highlightStartPos = 0; |
201 | highlightEndPos = 0; |
202 | |
203 | } else if (name.equals(MONOSPACE)) { |
204 | inMonospaceTag = false; |
205 | // apply style and reset position keepers |
206 | ssb.setSpan(new TypefaceSpan(Note.NOTE_MONOSPACE_TYPEFACE), monospaceStartPos, monospaceEndPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
207 | monospaceStartPos = 0; |
208 | monospaceEndPos = 0; |
209 | |
210 | } else if (name.equals(SMALL)) { |
211 | inSizeSmallTag = false; |
212 | // apply style and reset position keepers |
213 | ssb.setSpan(new RelativeSizeSpan(Note.NOTE_SIZE_SMALL_FACTOR), smallStartPos, smallEndPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
214 | smallStartPos = 0; |
215 | smallEndPos = 0; |
216 | |
217 | } else if (name.equals(LARGE)) { |
218 | inSizeLargeTag = false; |
219 | // apply style and reset position keepers |
220 | ssb.setSpan(new RelativeSizeSpan(Note.NOTE_SIZE_LARGE_FACTOR), largeStartPos, largeEndPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
221 | largeStartPos = 0; |
222 | largeEndPos = 0; |
223 | |
224 | } else if (name.equals(HUGE)) { |
225 | inSizeHugeTag = false; |
226 | // apply style and reset position keepers |
227 | ssb.setSpan(new RelativeSizeSpan(Note.NOTE_SIZE_HUGE_FACTOR), hugeStartPos, hugeEndPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
228 | hugeStartPos = 0; |
229 | hugeEndPos = 0; |
230 | |
231 | } else if (name.equals(LIST)) { |
232 | inListLevel--; |
233 | } else if (name.equals(LIST_ITEM)) { |
234 | |
235 | // if this list item is "empty" then we don't need to try rendering anything. |
236 | if (!inListItem && listItemIsEmpty.get(inListLevel-1)) |
237 | { |
238 | listItemStartPos.set(inListLevel-1, new Integer(0)); |
239 | listItemEndPos.set(inListLevel-1, new Integer(0)); |
240 | listItemIsEmpty.set(inListLevel-1, new Boolean(true)); |
241 | |
242 | return; |
243 | } |
244 | // here, we apply margin and create a bullet span. Plus, we need to reset position keepers. |
245 | // TODO new sexier bullets? |
246 | // Show a leading margin that is as wide as the nested level we are in |
247 | ssb.setSpan(new LeadingMarginSpan.Standard(30*inListLevel), listItemStartPos.get(inListLevel-1), listItemEndPos.get(inListLevel-1), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
248 | ssb.setSpan(new BulletSpan(), listItemStartPos.get(inListLevel-1), listItemEndPos.get(inListLevel-1), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
249 | listItemStartPos.set(inListLevel-1, new Integer(0)); |
250 | listItemEndPos.set(inListLevel-1, new Integer(0)); |
251 | listItemIsEmpty.set(inListLevel-1, new Boolean(true)); |
252 | |
253 | inListItem = false; |
254 | } |
255 | } |
256 | } |
257 | |
258 | @Override |
259 | public void characters(char[] ch, int start, int length) |
260 | throws SAXException { |
261 | |
262 | String currentString = new String(ch, start, length); |
263 | |
264 | if (inNoteContentTag) { |
265 | // while we are in note-content, append |
266 | ssb.append(currentString, start, length); |
267 | int strLenStart = ssb.length()-length; |
268 | int strLenEnd = ssb.length(); |
269 | |
270 | // apply style if required |
271 | // TODO I haven't tested nested tags yet |
272 | if (inBoldTag) { |
273 | // if tag is not equal to 0 then we are already in it: no need to reset it's position again |
274 | if (boldStartPos == 0) { |
275 | boldStartPos = strLenStart; |
276 | } |
277 | // no matter what, if we are still in the tag, end is now further |
278 | boldEndPos = strLenEnd; |
279 | } |
280 | if (inItalicTag) { |
281 | // if tag is not equal to 0 then we are already in it: no need to reset it's position again |
282 | if (italicStartPos == 0) { |
283 | italicStartPos = strLenStart; |
284 | } |
285 | // no matter what, if we are still in the tag, end is now further |
286 | italicEndPos = strLenEnd; |
287 | } |
288 | if (inStrikeTag) { |
289 | // if tag is not equal to 0 then we are already in it: no need to reset it's position again |
290 | if (strikethroughStartPos == 0) { |
291 | strikethroughStartPos = strLenStart; |
292 | } |
293 | // no matter what, if we are still in the tag, end is now further |
294 | strikethroughEndPos = strLenEnd; |
295 | } |
296 | if (inHighlighTag) { |
297 | // if tag is not equal to 0 then we are already in it: no need to reset it's position again |
298 | if (highlightStartPos == 0) { |
299 | highlightStartPos = strLenStart; |
300 | } |
301 | // no matter what, if we are still in the tag, end is now further |
302 | highlightEndPos = strLenEnd; |
303 | } |
304 | if (inMonospaceTag) { |
305 | // if tag is not equal to 0 then we are already in it: no need to reset it's position again |
306 | if (monospaceStartPos == 0) { |
307 | monospaceStartPos = strLenStart; |
308 | } |
309 | // no matter what, if we are still in the tag, end is now further |
310 | monospaceEndPos = strLenEnd; |
311 | } |
312 | if (inSizeSmallTag) { |
313 | // if tag is not equal to 0 then we are already in it: no need to reset it's position again |
314 | if (smallStartPos == 0) { |
315 | smallStartPos = strLenStart; |
316 | } |
317 | // no matter what, if we are still in the tag, end is now further |
318 | smallEndPos = strLenEnd; |
319 | } |
320 | if (inSizeLargeTag) { |
321 | // if tag is not equal to 0 then we are already in it: no need to reset it's position again |
322 | if (largeStartPos == 0) { |
323 | largeStartPos = strLenStart; |
324 | } |
325 | // no matter what, if we are still in the tag, end is now further |
326 | largeEndPos = strLenEnd; |
327 | } |
328 | if (inSizeHugeTag) { |
329 | // if tag is not equal to 0 then we are already in it: no need to reset it's position again |
330 | if (hugeStartPos == 0) { |
331 | hugeStartPos = strLenStart; |
332 | } |
333 | // no matter what, if we are still in the tag, end is now further |
334 | hugeEndPos = strLenEnd; |
335 | } |
336 | if (inListItem) { |
337 | // this list item is not empty, so we mark it as such. We keep track of this to avoid any |
338 | // problems with list items nested like this: <item><item><item>Content!</item></item></item> |
339 | listItemIsEmpty.set(inListLevel-1, new Boolean(false)); |
340 | |
341 | // no matter what, if we are still in the tag, end is now further |
342 | listItemEndPos.set(inListLevel-1, strLenEnd); |
343 | } |
344 | } |
345 | } |
346 | } |