Added
Link Here
|
1 |
/******************************************************************************* |
2 |
* Copyright (c) 2004 John-Mason P. Shackelford and others. |
3 |
* All rights reserved. This program and the accompanying materials |
4 |
* are made available under the terms of the Common Public License v1.0 |
5 |
* which accompanies this distribution, and is available at |
6 |
* http://www.eclipse.org/legal/cpl-v10.html |
7 |
* |
8 |
* Contributors: |
9 |
* John-Mason P. Shackelford - initial API and implementation |
10 |
*******************************************************************************/ |
11 |
package org.eclipse.ant.internal.ui.editor.formatter; |
12 |
|
13 |
import java.text.CharacterIterator; |
14 |
import java.text.StringCharacterIterator; |
15 |
import java.util.ArrayList; |
16 |
import java.util.Arrays; |
17 |
import java.util.List; |
18 |
|
19 |
/** |
20 |
* |
21 |
*/ |
22 |
public class XmlTagFormatter { |
23 |
|
24 |
protected static class AttributePair { |
25 |
|
26 |
private String attribute; |
27 |
|
28 |
private String value; |
29 |
|
30 |
public AttributePair(String attribute, String value) { |
31 |
this.attribute = attribute; |
32 |
this.value = value; |
33 |
} |
34 |
|
35 |
public String getAttribute() { |
36 |
return attribute; |
37 |
} |
38 |
|
39 |
public String getValue() { |
40 |
return value; |
41 |
} |
42 |
} |
43 |
|
44 |
protected static class ParseException extends Exception { |
45 |
|
46 |
public ParseException(String message) { |
47 |
super(message); |
48 |
} |
49 |
} |
50 |
|
51 |
protected static class Tag { |
52 |
|
53 |
private List attributes = new ArrayList(); |
54 |
|
55 |
private boolean closed; |
56 |
|
57 |
private String elementName; |
58 |
|
59 |
public void addAttribute(String attribute, String value) { |
60 |
attributes.add(new AttributePair(attribute, value)); |
61 |
} |
62 |
|
63 |
public int attributeCount() { |
64 |
return attributes.size(); |
65 |
} |
66 |
|
67 |
public AttributePair getAttributePair(int i) { |
68 |
return (AttributePair) attributes.get(i); |
69 |
} |
70 |
|
71 |
public String getElementName() { |
72 |
return this.elementName; |
73 |
} |
74 |
|
75 |
public boolean isClosed() { |
76 |
return closed; |
77 |
} |
78 |
|
79 |
public int minimumLength() { |
80 |
int length = 2; // for the < > |
81 |
if (this.isClosed()) length++; // if we need to add an /> |
82 |
length += this.getElementName().length(); |
83 |
if (this.attributeCount() > 0 || this.isClosed()) length++; |
84 |
for (int i = 0; i < this.attributeCount(); i++) { |
85 |
AttributePair attributePair = this.getAttributePair(i); |
86 |
length += attributePair.getAttribute().length(); |
87 |
length += attributePair.getValue().length(); |
88 |
length += 4; // equals sign, quote characters & trailing space |
89 |
} |
90 |
if (this.attributeCount() > 0 && !this.isClosed()) length--; |
91 |
return length; |
92 |
} |
93 |
|
94 |
public void setAttributes(List attributePair) { |
95 |
attributes.clear(); |
96 |
attributes.addAll(attributePair); |
97 |
} |
98 |
|
99 |
public void setClosed(boolean closed) { |
100 |
this.closed = closed; |
101 |
} |
102 |
|
103 |
public void setElementName(String elementName) { |
104 |
this.elementName = elementName; |
105 |
} |
106 |
|
107 |
public String toString() { |
108 |
StringBuffer sb = new StringBuffer(500); |
109 |
sb.append("<"); //$NON-NLS-1$ |
110 |
sb.append(this.getElementName()); |
111 |
if (this.attributeCount() > 0 || this.isClosed()) sb.append(' '); |
112 |
|
113 |
for (int i = 0; i < this.attributeCount(); i++) { |
114 |
AttributePair attributePair = this.getAttributePair(i); |
115 |
sb.append(attributePair.getAttribute()); |
116 |
sb.append("=\""); //$NON-NLS-1$ |
117 |
sb.append(attributePair.getValue()); |
118 |
sb.append("\""); //$NON-NLS-1$ |
119 |
if (this.isClosed() || i != this.attributeCount() - 1) |
120 |
sb.append(' '); |
121 |
} |
122 |
if (this.isClosed()) sb.append("/"); //$NON-NLS-1$ |
123 |
sb.append(">"); //$NON-NLS-1$ |
124 |
return sb.toString(); |
125 |
} |
126 |
} |
127 |
|
128 |
protected static class TagFormatter { |
129 |
|
130 |
/** |
131 |
* @param searchChar |
132 |
* @param inTargetString |
133 |
* @return |
134 |
*/ |
135 |
private int countChar(char searchChar, String inTargetString) { |
136 |
StringCharacterIterator iter = new StringCharacterIterator( |
137 |
inTargetString); |
138 |
int i = 0; |
139 |
if (iter.first() == searchChar) i++; |
140 |
while (iter.getIndex() < iter.getEndIndex()) { |
141 |
if (iter.next() == searchChar) { |
142 |
i++; |
143 |
} |
144 |
} |
145 |
return i; |
146 |
} |
147 |
|
148 |
/** |
149 |
* @param tagText |
150 |
* @param prefs |
151 |
* @param indent |
152 |
* @return |
153 |
*/ |
154 |
public String format(Tag tag, FormattingPreferences prefs, String indent) { |
155 |
if (prefs.wrapLongTags() |
156 |
&& lineRequiresWrap(indent + tag.toString(), prefs |
157 |
.getMaximumLineWidth(), prefs.getTabWidth())) { |
158 |
return wrapTag(tag, prefs, indent); |
159 |
} else { |
160 |
return tag.toString(); |
161 |
} |
162 |
} |
163 |
|
164 |
/** |
165 |
* @param line |
166 |
* @param lineWidth |
167 |
* @param tabWidth |
168 |
* @return |
169 |
*/ |
170 |
protected boolean lineRequiresWrap(String line, int lineWidth, |
171 |
int tabWidth) { |
172 |
return tabExpandedLineWidth(line, tabWidth) > lineWidth; |
173 |
} |
174 |
|
175 |
/** |
176 |
* @param line |
177 |
* the line in which spaces are to be expanded |
178 |
* @param tabWidth |
179 |
* number of spaces to substitute for a tab |
180 |
* @return length of the line with tabs expanded to spaces |
181 |
*/ |
182 |
protected int tabExpandedLineWidth(String line, int tabWidth) { |
183 |
int tabCount = countChar('\t', line); |
184 |
return (line.length() - tabCount) + (tabCount * tabWidth); |
185 |
} |
186 |
|
187 |
/** |
188 |
* @param tag |
189 |
* @param prefs |
190 |
* @param indent |
191 |
* @return |
192 |
*/ |
193 |
protected String wrapTag(Tag tag, FormattingPreferences prefs, |
194 |
String indent) { |
195 |
StringBuffer sb = new StringBuffer(1024); |
196 |
sb.append('<'); |
197 |
sb.append(tag.getElementName()); |
198 |
sb.append(' '); |
199 |
|
200 |
if (tag.attributeCount() > 0) { |
201 |
sb.append(tag.getAttributePair(0).getAttribute()); |
202 |
sb.append("=\""); //$NON-NLS-1$ |
203 |
sb.append(tag.getAttributePair(0).getValue()); |
204 |
sb.append('"'); |
205 |
} |
206 |
|
207 |
if (tag.attributeCount() > 1) { |
208 |
char[] extraIndent = new char[tag.getElementName().length() + 2]; |
209 |
Arrays.fill(extraIndent, ' '); |
210 |
for (int i = 1; i < tag.attributeCount(); i++) { |
211 |
sb.append('\n'); |
212 |
sb.append(indent); |
213 |
sb.append(extraIndent); |
214 |
sb.append(tag.getAttributePair(i).getAttribute()); |
215 |
sb.append("=\""); //$NON-NLS-1$ |
216 |
sb.append(tag.getAttributePair(i).getValue()); |
217 |
sb.append('"'); |
218 |
} |
219 |
} |
220 |
|
221 |
if (prefs.alignElementCloseChar()) { |
222 |
sb.append("\n"); //$NON-NLS-1$ |
223 |
sb.append(indent); |
224 |
} else if (tag.isClosed()) { |
225 |
sb.append(' '); |
226 |
} |
227 |
|
228 |
if (tag.isClosed()) sb.append("/"); //$NON-NLS-1$ |
229 |
sb.append(">"); //$NON-NLS-1$ |
230 |
return sb.toString(); |
231 |
} |
232 |
} |
233 |
|
234 |
// if object creation is an issue, use static methods or a flyweight |
235 |
// pattern |
236 |
protected static class TagParser { |
237 |
|
238 |
private String elementName; |
239 |
|
240 |
private String parseText; |
241 |
|
242 |
/** |
243 |
* |
244 |
*/ |
245 |
protected List getAttibutes(String elementText) |
246 |
throws ParseException { |
247 |
|
248 |
class Mode { |
249 |
private int mode; |
250 |
public void setAttributeNameSearching() {mode = 0;} |
251 |
public void setAttributeNameFound() {mode = 1;} |
252 |
public void setAttributeValueSearching() {mode = 2;} |
253 |
public void setAttributeValueFound() {mode = 3;} |
254 |
public void setFinished() {mode = 4;} |
255 |
public boolean isAttributeNameSearching() {return mode == 0;} |
256 |
public boolean isAttributeNameFound() {return mode == 1;} |
257 |
public boolean isAttributeValueSearching() {return mode == 2;} |
258 |
public boolean isAttributeValueFound() {return mode == 3;} |
259 |
public boolean isFinished() {return mode == 4;} |
260 |
} |
261 |
|
262 |
List attributePairs = new ArrayList(); |
263 |
|
264 |
CharacterIterator iter = new StringCharacterIterator(elementText |
265 |
.substring(getElementName(elementText).length() + 2)); |
266 |
|
267 |
// state for finding attributes |
268 |
Mode mode = new Mode(); |
269 |
mode.setAttributeNameSearching(); |
270 |
char attributeQuote = '"'; |
271 |
StringBuffer currentAttributeName = null; |
272 |
StringBuffer currentAttributeValue = null; |
273 |
|
274 |
char c = iter.first(); |
275 |
while (!mode.isFinished() && iter.getIndex() < iter.getEndIndex()) { |
276 |
|
277 |
switch (c) { |
278 |
|
279 |
case '"': |
280 |
case '\'': |
281 |
|
282 |
if (mode.isAttributeValueSearching()) { |
283 |
|
284 |
// start of an attribute value |
285 |
attributeQuote = c; |
286 |
mode.setAttributeValueFound(); |
287 |
currentAttributeValue = new StringBuffer(1024); |
288 |
|
289 |
} else if (mode.isAttributeValueFound() |
290 |
&& attributeQuote == c) { |
291 |
|
292 |
// we've completed a pair! |
293 |
AttributePair pair = new AttributePair( |
294 |
currentAttributeName.toString(), |
295 |
currentAttributeValue.toString()); |
296 |
|
297 |
attributePairs.add(pair); |
298 |
|
299 |
// start looking for another attribute |
300 |
mode.setAttributeNameSearching(); |
301 |
|
302 |
} else if (mode.isAttributeValueFound() |
303 |
&& attributeQuote != c) { |
304 |
|
305 |
// this quote character is part of the attribute value |
306 |
currentAttributeValue.append(c); |
307 |
|
308 |
} else { |
309 |
// this is no place for a quote! |
310 |
throw new ParseException("Unexpected '" + c //$NON-NLS-1$ |
311 |
+ "' when parsing:\n\t" + elementText); //$NON-NLS-1$ |
312 |
} |
313 |
break; |
314 |
|
315 |
case '=': |
316 |
|
317 |
if (mode.isAttributeValueFound()) { |
318 |
|
319 |
// this character is part of the attribute value |
320 |
currentAttributeValue.append(c); |
321 |
|
322 |
} else if (mode.isAttributeNameFound()) { |
323 |
|
324 |
// end of the name, now start looking for the value |
325 |
mode.setAttributeValueSearching(); |
326 |
|
327 |
} else { |
328 |
// this is no place for an equals sign! |
329 |
throw new ParseException("Unexpected '" + c //$NON-NLS-1$ |
330 |
+ "' when parsing:\n\t" + elementText); //$NON-NLS-1$ |
331 |
} |
332 |
break; |
333 |
|
334 |
case '/': |
335 |
case '>': |
336 |
if (mode.isAttributeValueFound()) { |
337 |
// attribute values are CDATA, add it all |
338 |
currentAttributeValue.append(c); |
339 |
} else if (mode.isAttributeNameSearching()) { |
340 |
mode.setFinished(); |
341 |
} else { |
342 |
// we aren't ready to be done! |
343 |
throw new ParseException("Unexpected '" + c //$NON-NLS-1$ |
344 |
+ "' when parsing:\n\t" + elementText); //$NON-NLS-1$ |
345 |
} |
346 |
break; |
347 |
|
348 |
default: |
349 |
|
350 |
if (mode.isAttributeValueFound()) { |
351 |
// attribute values are CDATA, add it all |
352 |
currentAttributeValue.append(c); |
353 |
} else { |
354 |
if (!Character.isWhitespace(c)) { |
355 |
if (mode.isAttributeNameSearching()) { |
356 |
// we found the start of an attribute name |
357 |
mode.setAttributeNameFound(); |
358 |
currentAttributeName = new StringBuffer(255); |
359 |
currentAttributeName.append(c); |
360 |
} else if (mode.isAttributeNameFound()) { |
361 |
currentAttributeName.append(c); |
362 |
} |
363 |
} |
364 |
} |
365 |
break; |
366 |
} |
367 |
|
368 |
c = iter.next(); |
369 |
} |
370 |
return attributePairs; |
371 |
} |
372 |
|
373 |
/** |
374 |
* @param tagText |
375 |
* text of an XML tag |
376 |
* @return extracted XML element name |
377 |
*/ |
378 |
protected String getElementName(String tagText) throws ParseException { |
379 |
if (!tagText.equals(this.parseText) || this.elementName == null) { |
380 |
int endOfTag = tagEnd(tagText); |
381 |
if ((tagText.length() > 2) && (endOfTag > 1)) { |
382 |
this.parseText = tagText; |
383 |
this.elementName = tagText.substring(1, endOfTag); |
384 |
} else { |
385 |
throw new ParseException("No element name for the tag:\n\t" //$NON-NLS-1$ |
386 |
+ tagText); |
387 |
} |
388 |
} |
389 |
return elementName; |
390 |
} |
391 |
|
392 |
/** |
393 |
* @param tagText |
394 |
* @return |
395 |
*/ |
396 |
protected boolean isClosed(String tagText) { |
397 |
return tagText.charAt(tagText.lastIndexOf(">") - 1) == '/'; //$NON-NLS-1$ |
398 |
} |
399 |
|
400 |
/** |
401 |
* @param tagText |
402 |
* @return an fully populated tag |
403 |
*/ |
404 |
public Tag parse(String tagText) throws ParseException { |
405 |
Tag tag = new Tag(); |
406 |
tag.setElementName(getElementName(tagText)); |
407 |
tag.setAttributes(getAttibutes(tagText)); |
408 |
tag.setClosed(isClosed(tagText)); |
409 |
return tag; |
410 |
} |
411 |
|
412 |
private int tagEnd(String text) { |
413 |
// This is admittedly a little loose, but we don't want the |
414 |
// formatter to be too strict... |
415 |
// http://www.w3.org/TR/2000/REC-xml-20001006#NT-Name |
416 |
for (int i = 1; i < text.length(); i++) { |
417 |
char c = text.charAt(i); |
418 |
if (!Character.isLetterOrDigit(c) && c != ':' && c != '.' |
419 |
&& c != '-' && c != '_') { return i; } |
420 |
} |
421 |
return -1; |
422 |
} |
423 |
|
424 |
} |
425 |
|
426 |
/** |
427 |
* @param tagText |
428 |
* @param prefs |
429 |
* @param indent |
430 |
* @return |
431 |
*/ |
432 |
public String format(String tagText, FormattingPreferences prefs, |
433 |
String indent) { |
434 |
|
435 |
Tag tag; |
436 |
if (tagText.startsWith("</") || tagText.startsWith("<%") |
437 |
|| tagText.startsWith("<?") || tagText.startsWith("<[")) { |
438 |
return tagText; |
439 |
} else { |
440 |
try { |
441 |
tag = new TagParser().parse(tagText); |
442 |
} catch (ParseException e) { |
443 |
// if we can't parse the tag, give up and leave the text as is. |
444 |
return tagText; |
445 |
} |
446 |
return new TagFormatter().format(tag, prefs, indent); |
447 |
} |
448 |
} |
449 |
|
450 |
} |