Source code: com/port80/eclipse/xml/editors/XMLFormatter.java
1 package com.port80.eclipse.xml.editors;
2
3 import java.io.IOException;
4 import java.io.StringReader;
5 import java.util.Stack;
6
7 import org.apache.xerces.impl.XMLDocumentScannerImpl;
8 import org.apache.xerces.parsers.SAXParser;
9 import org.apache.xerces.parsers.StandardParserConfiguration;
10 import org.apache.xerces.xni.XNIException;
11 import org.apache.xerces.xni.parser.XMLComponent;
12 import org.apache.xerces.xni.parser.XMLDocumentScanner;
13 import org.eclipse.jface.preference.IPreferenceStore;
14 import org.eclipse.jface.text.source.ISourceViewer;
15 import org.eclipse.ui.IEditorInput;
16 import org.eclipse.ui.texteditor.ITextEditor;
17 import org.xml.sax.Attributes;
18 import org.xml.sax.InputSource;
19 import org.xml.sax.Locator;
20 import org.xml.sax.SAXException;
21 import org.xml.sax.SAXParseException;
22 import org.xml.sax.ext.DeclHandler;
23 import org.xml.sax.ext.LexicalHandler;
24 import org.xml.sax.helpers.AttributesImpl;
25 import org.xml.sax.helpers.DefaultHandler;
26
27 import com.port80.eclipse.editors.EditorsPlugin;
28 import com.port80.eclipse.editors.IConstants;
29 import com.port80.util.Msg;
30 import com.port80.util.text.TextUtil;
31 import com.port80.util.text.XmlUtil;
32
33 /**
34 * @author chrisl
35 */
36 public class XMLFormatter {
37
38 ////////////////////////////////////////////////////////////////////////
39
40 private static final String NAME = "XMLFormatter";
41 private static final boolean DEBUG = false;
42 private static final boolean TRACE = false;
43
44 ////////////////////////////////////////////////////////////////////////
45
46 private XMLEditorConfiguration fConfig;
47 private ISourceViewer fViewer;
48 private IPreferenceStore fPreferences;
49
50 ////////////////////////////////////////////////////////////////////////
51
52 /**
53 * Constructor for XMLFormatter.
54 */
55 public XMLFormatter(XMLEditorConfiguration cf, ISourceViewer viewer) {
56 fConfig = cf;
57 fViewer = viewer;
58 }
59
60 ////////////////////////////////////////////////////////////////////////
61
62 /**
63 * Formats the String <code>sourceString</code>,
64 * and returns a string containing the formatted version.
65 *
66 * @param string the string to format
67 * @param indentationLevel the initial indentation level, used
68 * to shift left/right the entire source fragment. An initial indentation
69 * level of zero has no effect.
70 * @param positions an array of positions to map. These are
71 * character-based source positions inside the original source,
72 * for which corresponding positions in the formatted source will
73 * be computed (so as to relocate elements associated with the original
74 * source). It updates the positions array with updated positions.
75 * If set to <code>null</code>, then no positions are mapped.
76 * @param lineSeparator the line separator to use in formatted source,
77 * if set to <code>null</code>, then the platform default one will be used.
78 * @return the formatted output string.
79 */
80 // public String domFormat(
81 // String string,
82 // int indentationLevel,
83 // int[] positions,
84 // String line_separator,
85 // boolean compact) {
86 // IEditorInput input = fConfig.getEditor().getEditorInput();
87 // //
88 // EditorsPlugin.deleteProblemMarkers(input, IConstants.FormatActionProblemMarker);
89 // Document xmldoc = Util.customParse(fConfig.getEditor(), (SourceViewer) fViewer, false);
90 // StringBuffer ret = new StringBuffer();
91 // xmlProlog(ret, string, line_separator);
92 // if (format(xmldoc, ret))
93 // return ret.toString();
94 // return string;
95 //
96 // }
97 //
98 // private boolean format(Document node, StringBuffer ret) {
99 // return false;
100 // }
101
102 private void xmlProlog(StringBuffer ret, String input, String line_separator) {
103 xmlDeclaration(ret, input);
104 ret.append(line_separator);
105 }
106
107 /*
108 * Extract the xml declaration from the input string.
109 * //FIXME: How to get this from Xerces. There are no handler method for the xml declaration.
110 */
111 private int xmlDeclaration(StringBuffer ret, String input) {
112 int start = TextUtil.skipWhitespaces(input, 0);
113 if (input.startsWith("<?xml", start)) {
114 char c;
115 int end = start + 5;
116 for (int len = input.length(); end < len; ++end) {
117 c = input.charAt(end);
118 if (c == '?' && end + 1 < len && input.charAt(end + 1) == '>') {
119 ret.append(input.substring(start, end + 2));
120 return end + 2;
121 } else if (c == '<') {
122 break;
123 }
124 }
125 // This should not happend, but just in case there are malformed declaration.
126 ret.append(input.substring(start, end));
127 return end;
128 }
129 ret.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
130 return start;
131 }
132
133 ////////////////////////////////////////////////////////////////////////
134
135 /**
136 * Format routine with custom SAXParser.
137 *
138 * @param string
139 * @param indentationLevel
140 * @param positions
141 * @param line_separator
142 * @param compact
143 * @return String
144 */
145 public String format(
146 String string,
147 int indentationLevel,
148 int[] positions,
149 String line_separator,
150 boolean compact) {
151 ITextEditor editor = fConfig.getEditor();
152 IEditorInput input = editor.getEditorInput();
153 EditorsPlugin.deleteProblemMarkers(input, IConstants.FormatActionProblemMarker);
154 StringBuffer ret = new StringBuffer();
155 xmlProlog(ret, string, line_separator);
156 SAXParser reader;
157 XMLContentHandler handler = new XMLContentHandler(ret, line_separator, compact, fConfig, fViewer);
158 reader = new SAXParser(new CustomXmlParserConfiguration((CustomHandler) handler));
159 try {
160 // reader.setFeature("http://xml.org/sax/features/string-interning", true); //$NON-NLS-1$
161 reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
162 reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
163 reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
164 reader.setFeature("http://xml.org/sax/features/validation", false);
165 reader.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
166 // Doesn't help to ignore entity not defined error.
167 // reader.setEntityResolver(null);
168 // reader.setFeature("http://apache.org/xml/features/dom/create-entity-ref-nodes",false);
169 // reader.setFeature("http://apache.org/xml/features/continue-after-fatal-error", true);
170 } catch (SAXException e) {
171 EditorsPlugin.error("Error setting features", null, input);
172 // In case support for this feature is removed
173
174 }
175 reader.setContentHandler(handler);
176 reader.setErrorHandler(handler);
177 try {
178 reader.setProperty("http://xml.org/sax/properties/lexical-handler", handler);
179 reader.setProperty("http://xml.org/sax/properties/declaration-handler", handler);
180 // reader.setProperty("http://apache.org/xml/properties/internal/entity-manager", new CustomEntityManager());
181 } catch (Exception e) {
182 EditorsPlugin.error("Error initializing XML SAXParser", e, input);
183 return string;
184 }
185 try {
186 reader.parse(new InputSource(new StringReader(string)));
187 } catch (SAXParseException e) {
188 Util.reportError("XMLFormatter", e, editor, fViewer);
189 return string;
190 } catch (SAXException e) {
191 Util.reportError("XMLFormatter", e, editor, fViewer);
192 return string;
193 } catch (IOException e) {
194 EditorsPlugin.error("XMLFormatter: Read error", e, input);
195 return string;
196 } catch (Exception e) {
197 EditorsPlugin.error("XMLFormatter: Parse error", e, input);
198 return string;
199 }
200 if (ret.charAt(ret.length() - 1) != '\n')
201 ret.append('\n');
202 return ret.toString();
203 }
204
205 ////////////////////////////////////////////////////////////////////////
206
207 /**
208 * An XMLContentHanlder that format XML source while parsing.
209 */
210 static class XMLContentHandler extends DefaultHandler implements LexicalHandler, DeclHandler, CustomHandler {
211
212 ////////////////////////////////////////////////////////////////////////
213
214 XMLEditorConfiguration fConfig;
215 ISourceViewer fViewer;
216 //
217 StringBuffer fResult;
218 String fLineSep;
219 Locator fLocator;
220 //
221 StringBuffer fText;
222 Stack fTagStack;
223 int fLineWidth;
224 int fTabWidth;
225 String fTab;
226 boolean isNewLine;
227 boolean isCompact;
228 //
229 int fLine;
230 int fColumn;
231 boolean isEmpty;
232 boolean isPreserveSpace;
233 boolean hasInternalDTD;
234
235 /**
236 * Flag to ignore SAX ContentHandler calls when content is handled by LexicalHandler
237 * (eg. between StartEntity and EndEntity).
238 */
239 boolean isIgnore, isCData;
240
241 ////////////////////////////////////////////////////////////////////////
242
243 public XMLContentHandler(
244 StringBuffer ret,
245 String linesep,
246 boolean compact,
247 XMLEditorConfiguration cf,
248 ISourceViewer viewer) {
249 //
250 fConfig = cf;
251 fViewer = viewer;
252 //
253 fResult = ret;
254 fLineSep = (linesep == null) ? "\n" : linesep;
255 fText = new StringBuffer();
256 fTagStack = new Stack();
257 fLineWidth = cf.getLineWidth(viewer);
258 fTabWidth = cf.getTabWidth(viewer);
259 fTab = cf.getTab(viewer);
260 isCompact = compact;
261 isNewLine = true;
262 fLine = 1;
263 }
264
265 public void reportError(String message, int line, int column) {
266 EditorsPlugin.error(message, null, line, column, fConfig.getEditor().getEditorInput(), fViewer);
267 }
268
269 ////////////////////////////////////////////////////////////////////////
270
271 /**
272 * @see org.xml.sax.helpers.DefaultHandler#setDocumentLocator(Locator)
273 */
274 public void setDocumentLocator(Locator locator) {
275 fLocator = locator;
276 }
277
278 /**
279 * @see org.xml.sax.ContentHandler#startDocument()
280 */
281 public void startDocument() throws SAXException {
282 if (TRACE)
283 Msg.println("startDocument");
284 }
285
286 /**
287 * @see org.xml.sax.ContentHandler#endDocument()
288 */
289 public void endDocument() throws SAXException {
290 // SAX do not pass the "\n" beyond the last end tag, so we have to add one here.
291 fText.append(fLineSep);
292 emitText();
293 }
294
295 /**
296 * @see org.xml.sax.ContentHandler#startElement(String, String, String, Attributes)
297 */
298 public void startElement(String namespaceURI, String localName, String qName, Attributes atts)
299 throws SAXException {
300 if (TRACE)
301 Msg.println(
302 "startElement: "
303 + qName
304 + " (line="
305 + fLocator.getLineNumber()
306 + ", column="
307 + fLocator.getColumnNumber());
308 if (isIgnore || isCData)
309 return;
310 //
311 if (fTagStack.isEmpty()) {
312 if (!isNewLine)
313 startNewLine();
314 } else {
315 Element element = (Element) fTagStack.peek();
316 if (!element.isEmitted) {
317 emitStartTag(element, -1);
318 if (!isPreserveSpace && !isNewLine)
319 startNewLine();
320 }
321 emitText();
322 }
323 Attributes a = new AttributesImpl(atts);
324 fTagStack.push(new Element(qName, a, fLine, fTagStack));
325 isEmpty = true;
326 }
327
328 /**
329 * @see org.xml.sax.ContentHandler#endElement(String, String, String)
330 */
331 public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
332 if (TRACE)
333 Msg.println(
334 "endElement: "
335 + qName
336 + " (line="
337 + fLocator.getLineNumber()
338 + ", column="
339 + fLocator.getColumnNumber());
340 if (isIgnore || isCData)
341 return;
342 //
343 int len = 0;
344 if (!fTagStack.empty()) {
345 Element element = (Element) fTagStack.pop();
346 if (!element.qName.equals(qName)) {
347 Msg.println("End tag not match: end=" + qName + ", start=" + element.qName);
348 fTagStack.push(element);
349 }
350 boolean oneliner = isCompact && !element.isEmitted;
351 if (!element.isEmitted)
352 len = emitStartTag(element, 0);
353 if (isEmpty) {
354 emitEndElement(namespaceURI, localName, element);
355 } else {
356 if (!element.isPreserveSpace)
357 trimText(fText);
358 oneliner
359 &= (len >= 0
360 && (len + fText.length() + qName.length() + 3) < fLineWidth
361 && fText.indexOf("\n") < 0);
362 if (oneliner) {
363 emitText(false, element.isPreserveSpace, 0);
364 emitEndElement(namespaceURI, localName, element);
365 if (!isPreserveSpace && !isNewLine)
366 startNewLine();
367 } else {
368 emitText(true, element.isPreserveSpace, 1);
369 emitEndElement(namespaceURI, localName, element);
370 }
371 }
372 } else {
373 Msg.println("End tag with no matching start tag: end=" + qName);
374 emitText();
375 emitEndElement(namespaceURI, localName, new Element(qName, null, -1, fTagStack));
376 }
377 peekPreserveSpace();
378 isEmpty = false;
379 }
380
381 /**
382 * @see org.xml.sax.ContentHandler#characters(char[], int, int)
383 */
384 public void characters(char[] ch, int start, int length) throws SAXException {
385 if (TRACE)
386 Msg.println("characters: |" + String.valueOf(ch, start, length) + "|");
387 if (isIgnore)
388 return;
389 isEmpty = false;
390 fText.append(ch, start, length);
391 }
392
393 /**
394 * @see org.xml.sax.ContentHandler#ignorableWhitespace(char[], int, int)
395 */
396 public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
397 if (TRACE)
398 Msg.println("ignorableWhitespace: |" + ch.toString() + "|");
399 if (isIgnore)
400 return;
401 isEmpty = false;
402 fText.append(ch, start, length);
403 }
404
405 /**
406 * @see org.xml.sax.ContentHandler#processingInstruction(String, String)
407 */
408 public void processingInstruction(String target, String data) throws SAXException {
409 if (TRACE)
410 Msg.println("processingInstruction: |" + target + "|" + data + "|");
411 if (isIgnore || isCData)
412 return;
413 if (!fTagStack.empty()) {
414 Element element = (Element) fTagStack.peek();
415 if (!element.isEmitted) {
416 emitStartTag(element, -1);
417 if (!isPreserveSpace && !isNewLine)
418 startNewLine();
419 }
420 }
421 isEmpty = false;
422 emitPI(target, data);
423 }
424
425 /**
426 * @see org.xml.sax.ContentHandler#skippedEntity(String)
427 */
428 public void skippedEntity(String name) throws SAXException {
429 System.err.println("skippedEntity: |" + name + "|");
430 isEmpty = false;
431 if (isIgnore || isCData)
432 return;
433 }
434
435 // LexicalHandler //////////////////////////////////////////////////////
436 //
437
438 /**
439 * @see org.xml.sax.ext.LexicalHandler#comment(char[], int, int)
440 */
441 public void comment(char[] ch, int start, int length) throws SAXException {
442 //FIXME: comment is valid in internal DTD.
443 // Hack to fix problem that Xerces do not call endDTD when load-external-DTD is disabled.
444 if (isIgnore)
445 return;
446 if (!fTagStack.empty()) {
447 Element element = (Element) fTagStack.peek();
448 if (!element.isEmitted) {
449 emitStartTag(element, -1);
450 if (!isPreserveSpace && !isNewLine)
451 startNewLine();
452 }
453 }
454 emitText();
455 if (!isPreserveSpace) {
456 if (!isNewLine)
457 startNewLine();
458 emitIndent();
459 }
460 fResult.append("<!--");
461 fText.append(ch, start, length);
462 emitComment();
463 if (isNewLine)
464 emitIndent();
465 fResult.append("-->");
466 if (!isPreserveSpace)
467 startNewLine();
468 }
469
470 /**
471 * @see org.xml.sax.ext.LexicalHandler#startDTD(String, String, String)
472 */
473 public void startDTD(String name, String publicId, String systemId) throws SAXException {
474 if (DEBUG)
475 System.err.println(NAME + ".startDTD()");
476 // <!DOCTYPE article PUBLIC "-//OASIS//DTD DocBook XML V4.1.2//EN"
477 // "/usr/share/sgml/docbook/xml-dtd-4.1.2-1.0-8/docbookx.dtd" []>
478 if (!isNewLine)
479 startNewLine();
480 fResult.append("<!DOCTYPE " + name);
481 startNewLine();
482 if (publicId != null) {
483 emitIndent(1);
484 fResult.append("PUBLIC \"" + publicId + "\"");
485 startNewLine();
486 }
487 if (systemId != null) {
488 emitIndent(1);
489 if (publicId == null)
490 fResult.append("SYSTEM \"" + systemId + "\"");
491 else
492 fResult.append("\"" + systemId + "\"");
493 }
494 }
495
496 /**
497 * @see org.xml.sax.ext.LexicalHandler#endDTD()
498 */
499 public void endDTD() throws SAXException {
500 if (DEBUG)
501 System.err.println(NAME + ".endDTD()");
502 flushDTD();
503 }
504
505 /**
506 * @see org.xml.sax.ext.LexicalHandler#startEntity(String)
507 */
508 public void startEntity(String name) throws SAXException {
509 fText.append("&" + name + ";");
510 isIgnore = true;
511 }
512
513 /**
514 * @see org.xml.sax.ext.LexicalHandler#endEntity(String)
515 */
516 public void endEntity(String name) throws SAXException {
517 isIgnore = false;
518 }
519
520 /**
521 * @see org.xml.sax.ext.LexicalHandler#startCDATA()
522 */
523 public void startCDATA() throws SAXException {
524 if (isIgnore)
525 return;
526 if (!fTagStack.empty()) {
527 Element element = (Element) fTagStack.peek();
528 if (!element.isEmitted) {
529 emitStartTag(element, -1);
530 }
531 }
532 emitText();
533 isCData = true;
534 isEmpty = false;
535 }
536
537 /**
538 * @see org.xml.sax.ext.LexicalHandler#endCDATA()
539 */
540 public void endCDATA() throws SAXException {
541 if (isIgnore)
542 return;
543 if (!isPreserveSpace && !isNewLine)
544 startNewLine();
545 fResult.append("<![CDATA[");
546 emitText(true, true, 1);
547 fResult.append("]]>");
548 isCData = false;
549 if (!isPreserveSpace)
550 startNewLine();
551 }
552
553 // DeclHandler ///////////////////////////////////////////////////////
554
555 /**
556 * @see org.xml.sax.ext.DeclHandler#attributeDecl(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)
557 */
558 public void attributeDecl(String eName, String aName, String type, String valueDefault, String value)
559 throws SAXException {
560 if (!isNewLine)
561 startNewLine();
562 emitIndent();
563 fResult.append("!ATTLIST");
564 if (eName != null)
565 fResult.append(" " + eName);
566 if (aName != null)
567 fResult.append(" " + aName);
568 if (type != null)
569 fResult.append(" " + type);
570 if (valueDefault != null)
571 fResult.append(" " + valueDefault);
572 if (value != null)
573 fResult.append(" " + value);
574 fResult.append(">");
575 }
576
577 /**
578 * @see org.xml.sax.ext.DeclHandler#elementDecl(java.lang.String, java.lang.String)
579 */
580 public void elementDecl(String name, String model) throws SAXException {
581 if (!isNewLine)
582 startNewLine();
583 emitIndent();
584 fResult.append("<!ELEMENT " + name);
585 if (model != null)
586 fResult.append(model);
587 fResult.append(">");
588 }
589
590 /**
591 * @see org.xml.sax.ext.DeclHandler#externalEntityDecl(java.lang.String, java.lang.String, java.lang.String)
592 */
593 public void externalEntityDecl(String name, String publicId, String systemId) throws SAXException {
594 if (DEBUG)
595 System.err.println(
596 NAME
597 + ".externalEntityDecl(): name="
598 + name
599 + ", publicId="
600 + publicId
601 + ", systemId="
602 + systemId);
603 if (!isNewLine)
604 startNewLine();
605 emitIndent();
606 fResult.append("<!ENTITY " + name);
607 if (publicId != null)
608 fResult.append(" PUBLIC \"" + publicId + "\"");
609 if (systemId != null)
610 if (publicId == null)
611 fResult.append(" SYSTEM \"" + systemId + "\"");
612 else
613 fResult.append(" \"" + systemId + "\"");
614 fResult.append(">");
615 }
616
617 /**
618 * @see org.xml.sax.ext.DeclHandler#internalEntityDecl(java.lang.String, java.lang.String)
619 */
620 public void internalEntityDecl(String name, String value) throws SAXException {
621 if (DEBUG)
622 System.err.println(NAME + ".internalEntityDecl(): name=" + name + ", value=" + value);
623 if (!isNewLine)
624 startNewLine();
625 emitIndent();
626 fResult.append("<!ENTITY " + name + " \"" + value + "\">");
627 }
628
629 // CustomHandler interface /////////////////////////////////////////////
630
631 public void startInternalDTD(boolean b) {
632 hasInternalDTD = b;
633 if (hasInternalDTD) {
634 fResult.append(" [");
635 Element e = new Element(null, null, fLine, fTagStack);
636 e.isEmitted = true;
637 fTagStack.push(e);
638 }
639 startNewLine();
640 }
641
642 ////////////////////////////////////////////////////////////////////////
643
644 private void startNewLine() {
645 fResult.append(fLineSep);
646 ++fLine;
647 fColumn = 0;
648 isNewLine = true;
649 }
650
651 private void emitIndent() {
652 emitIndent(0);
653 }
654
655 private void emitIndent(int delta) {
656 for (int i = 0; i < fTagStack.size() + delta; ++i) {
657 fResult.append(fTab);
658 fColumn += fTabWidth;
659 }
660 isNewLine = false;
661 }
662
663 private void flushDTD() {
664 if (!isNewLine)
665 startNewLine();
666 if (hasInternalDTD) {
667 fTagStack.pop();
668 fResult.append(']');
669 }
670 fResult.append(">");
671 startNewLine();
672 startNewLine();
673 }
674
675 private void emitText() {
676 boolean preserve = false;
677 if (!fTagStack.empty())
678 preserve = ((Element) fTagStack.peek()).isPreserveSpace;
679 if (!preserve)
680 trimText(fText);
681 emitText(true, preserve, 0);
682 }
683
684 /**
685 * Wrap given text to the given linewidth. Embedded line break in the text are treated as paragraph
686 * break and would becomes a blank line.
687 *
688 * NOTE: XML parser always return 0x0a as line separator
689 *
690 * @param linebreak False to suppress the linebreak before start of text.
691 * @param preserve True to preserve spaces in the text.
692 * @param delta Extra indent level.
693 */
694 private void emitText(boolean linebreak, boolean preserve, int delta) {
695 if (preserve) {
696 fResult.append(isCData ? fText.toString() : XmlUtil.escText(fText.toString()));
697 fText.setLength(0);
698 return;
699 }
700 if (fText.length() == 0)
701 return;
702 int start = 0;
703 int index = fText.indexOf("\n");
704 while (index >= 0) {
705 if (linebreak) {
706 if (!isNewLine)
707 startNewLine();
708 if (start < index)
709 emitIndent(delta);
710 } else
711 linebreak = true;
712 emitText(fText, start, index, delta);
713 startNewLine();
714 start = index + 1;
715 index = fText.indexOf("\n", start);
716 }
717 if (linebreak) {
718 if (!isNewLine)
719 startNewLine();
720 if (start < fText.length())
721 emitIndent(delta);
722 }
723 emitText(fText, start, fText.length(), delta);
724 fText.setLength(0);
725 }
726
727 /**
728 * Emit the given range of text with line wrap.
729 * @param text
730 * @param start
731 * @param end
732 */
733 private void emitText(StringBuffer text, int start, int end, int delta) {
734 int s, e;
735 while (start < end) {
736 int w = fLineWidth - fColumn;
737 e = start + w;
738 if (e >= end) {
739 e = end;
740 } else {
741 for (--e; e >= start; --e) {
742 if (Character.isWhitespace(text.charAt(e)))
743 break;
744 }
745 ++e;
746 }
747 if (e <= start) {
748 reportError(
749 NAME + ".emitText(): problem in break up the text: text=" + text,
750 fLine,
751 fColumn);
752 e = end;
753 }
754 s = start;
755 start = e;
756 for (--e; e >= s; --e) {
757 if (!Character.isWhitespace(text.charAt(e))) {
758 fResult.append(XmlUtil.escText(text.substring(s, e + 1)));
759 break;
760 }
761 }
762 if (start < end) {
763 startNewLine();
764 emitIndent(delta);
765 }
766 }
767 }
768
769 private void emitComment() {
770 int index = fText.indexOf("\n");
771 while (index >= 0) {
772 if (isNewLine) {
773 emitIndent(1);
774 index -= trimLeadingWhitespace(fText);
775 }
776 fResult.append(fText.substring(0, index));
777 startNewLine();
778 fText.delete(0, index + 1);
779 index = fText.indexOf("\n");
780 }
781 if (isNewLine)
782 trimLeadingWhitespace(fText);
783 if (fText.length() > 0) {
784 if (isNewLine) {
785 emitIndent(1);
786 }
787 fResult.append(fText.toString());
788 fColumn += fText.length();
789 }
790 fText.setLength(0);
791 }
792
793 /** @return The text length of the element if it is all on a single line, else -1. */
794 private int emitStartTag(Element element, int delta) {
795 // Lookahead for text length of the start tag.
796 String s;
797 int len = 0;
798 element.isEmitted = true;
799 for (int i = 0, max = element.atts.getLength(); i < max; ++i) {
800 s = element.atts.getQName(i);
801 len += s.length() + 1;
802 s = XmlUtil.escAttrValue(element.atts.getValue(i));
803 if (s != null)
804 len += s.length() + 3;
805 }
806 if (!isCompact
807 || fTagStack.size() * fTabWidth + element.qName.length() + 2 + len > fLineWidth) {
808 emitMultiLineElement(element, delta);
809 return -1;
810 } else {
811 emitSingleLineElement(element, delta);
812 return fColumn;
813
814 }
815 }
816
817 private void emitSingleLineElement(Element element, int delta) {
818 if (!isPreserveSpace) {
819 if (!isNewLine)
820 startNewLine();
821 emitIndent(delta);
822 }
823 element.line = fLine;
824 isPreserveSpace = element.isPreserveSpace;
825 fResult.append("<" + element.qName);
826 fColumn += element.qName.length() + 1;
827 String s;
828 Attributes atts = element.atts;
829 for (int i = 0; i < atts.getLength(); ++i) {
830 s = atts.getQName(i);
831 fResult.append(" " + s);
832 fColumn += s.length() + 1;
833 s = XmlUtil.escAttrValue(atts.getValue(i));
834 if (s != null) {
835 fResult.append("=\"" + s + "\"");
836 fColumn += s.length() + 3;
837 }
838 }
839 fResult.append(">");
840 ++fColumn;
841 isNewLine = false;
842 }
843
844 private void emitMultiLineElement(Element element, int delta) {
845 if (!isPreserveSpace) {
846 if (!isNewLine)
847 startNewLine();
848 emitIndent(delta);
849 }
850 element.line = fLine;
851 isPreserveSpace = element.isPreserveSpace;
852 fResult.append("<" + element.qName);
853 fColumn += element.qName.length() + 1;
854 String s;
855 Attributes atts = element.atts;
856 for (int i = 0; i < atts.getLength(); ++i) {
857 startNewLine();
858 emitIndent(delta + 1);
859 s = atts.getQName(i);
860 fResult.append(s);
861 fColumn += s.length();
862 s = XmlUtil.escAttrValue(atts.getValue(i));
863 if (s != null) {
864 fResult.append("=\"" + s + "\"");
865 fColumn += s.length() + 3;
866 }
867 }
868 fResult.append(">");
869 ++fColumn;
870 isNewLine = false;
871 }
872
873 private void emitEndElement(String namespaceURI, String localName, Element element) {
874 if (isEmpty) {
875 fResult.insert(fResult.length() - 1, " /");
876 ++fColumn;
877 return;
878 }
879 // Put end tag on same line if it is on the same line as start tag and
880 // overall width <= line width.
881 String name = element.qName;
882 if (!isPreserveSpace
883 && !(fLine == element.line && (fColumn + name.length() + 3) <= fLineWidth)) {
884 if (!isNewLine)
885 startNewLine();
886 emitIndent();
887 }
888 fResult.append("</" + name + ">");
889 fColumn += name.length() + 3;
890 isNewLine = false;
891 }
892
893 private void emitPI(String target, String data) {
894 emitText();
895 if (!isPreserveSpace) {
896 if (!isNewLine)
897 startNewLine();
898 emitIndent();
899 }
900 fResult.append("<?" + target + " " + data + "?>");
901 startNewLine();
902 }
903
904 ////////////////////////////////////////////////////////////////////////
905
906 /** @return Number of characters deleted.
907 */
908 private int trimLeadingWhitespace(StringBuffer buf) {
909 if (isPreserveSpace)
910 return 0;
911 int len = buf.length();
912 char c;
913 while (buf.length() > 0) {
914 c = buf.charAt(0);
915 if (c == ' ' || c == '\t') {
916 buf.deleteCharAt(0);
917 } else
918 break;
919 }
920 return len - buf.length();
921 }
922
923 private boolean trimWhitespace(StringBuffer buf) {
924 if (isPreserveSpace)
925 return false;
926 char c;
927 int len;
928 boolean trimmed = false;
929 len = buf.length();
930 int index = buf.length() - 1;
931 while (index >= 0) {
932 c = buf.charAt(index);
933 if (c == ' ' || c == '\t') {
934 --index;
935 trimmed = true;
936 } else
937 break;
938 }
939 buf.setLength(index + 1);
940 len = buf.length();
941 while (len > 1) {
942 c = buf.charAt(0);
943 if (c == ' ' || c == '\t') {
944 buf.delete(0, 1);
945 --len;
946 } else
947 break;
948 }
949 return trimmed;
950 }
951
952 /**
953 * Trim given StringBuffer such that all leading and trailing whitespaces are eliminated unless there
954 * are two or more continuous breaks. In that case, a break would stay. Similarly all whitespaces
955 * inside the text are reduced to a single space unless there are multple line breaks. In that case,
956 * one line break would stay. Lines without blank line between them is reduced to one continuous line
957 * (represent a paragraph).
958 */
959 private void trimText(StringBuffer text) {
960 int len = text.length();
961 int start = 0;
962 char c;
963 int linebreaks = 0;
964 boolean wasspace = true;
965 String s = text.toString();
966 text.setLength(0);
967 // Skip leading blanks.
968 for (; start < len; ++start) {
969 c = s.charAt(start);
970 if (c == '\n' || !Character.isWhitespace(c))
971 break;
972 }
973 for (; start < len; ++start) {
974 c = s.charAt(start);
975 if (c == '\n') {
976 ++linebreaks;
977 if (linebreaks == 1 && !wasspace) {
978 text.append(' ');
979 } else if (linebreaks == 2) {
980 for (int n = text.length();
981 n > 0 && Character.isWhitespace(text.charAt(n - 1));
982 --n)
983 text.setLength(n - 1);
984 text.append("\n");
985 }
986 // linebreak is counted as space, so that no space would be added after it.
987 wasspace = true;
988 } else if (Character.isWhitespace(c)) {
989 if (!wasspace)
990 text.append(' ');
991 wasspace = true;
992 } else {
993 wasspace = false;
994 linebreaks = 0;
995 text.append(c);
996 }
997 }
998 int end = text.length() - 1;
999 for (; end >= 0; --end) {
1000 c = text.charAt(end);
1001 if (c == '\n' || !Character.isWhitespace(c))
1002 break;
1003 }
1004 text.setLength(end + 1);
1005 }
1006
1007 private void peekPreserveSpace() {
1008 if (fTagStack.isEmpty())
1009 isPreserveSpace = false;
1010 else
1011 isPreserveSpace = ((Element) fTagStack.peek()).isPreserveSpace;
1012 }
1013
1014 private void printBuffer(String m, StringBuffer buf) {
1015 System.err.print(m);
1016 for (int i = 0; i < buf.length(); ++i) {
1017 System.err.print(" " + (int) buf.charAt(i));
1018 }
1019 System.err.println("");
1020 }
1021 }
1022
1023 static class Element {
1024 String qName;
1025 Attributes atts;
1026 int line;
1027 boolean isPreserveSpace;
1028 boolean isEmitted;
1029 public Element(String name, Attributes a, int ln, Stack tagstack) {
1030 qName = name;
1031 atts = a;
1032 line = ln;
1033 String s;
1034 isPreserveSpace = false;
1035 boolean hasPreserveSpace = false;
1036 if (atts != null) {
1037 for (int i = 0, max = atts.getLength(); i < max; ++i) {
1038 s = atts.getQName(i);
1039 if (s.equalsIgnoreCase("xml:space")) {
1040 s = atts.getValue(i);
1041 isPreserveSpace = s.equalsIgnoreCase("preserve");
1042 hasPreserveSpace = true;
1043 break;
1044 }
1045 }
1046 }
1047 if (!hasPreserveSpace && !tagstack.isEmpty()) {
1048 isPreserveSpace = ((Element) tagstack.peek()).isPreserveSpace;
1049 }
1050 }
1051 }
1052
1053 ////////////////////////////////////////////////////////////////////////
1054
1055 // static class Scanner {
1056 //
1057 // public static final int LBRACKET = 1;
1058 // public static final int START_END_TAG = 2;
1059 // public static final int START_PI = 3;
1060 // public static final int END_EMPTY_TAG = 11;
1061 // public static final int RBRACKET = 12;
1062 // public static final int CHAR = 21;
1063 // public static final int STRING = 22;
1064 // public static final int IDENTIFIER=23;
1065 // public static final int UNKNOWN = 99;
1066 // public static final int EOF = -1;
1067 //
1068 // char[] fSource;
1069 // StringBuffer fTokenSource = new StringBuffer();
1070 // int pos = 0;
1071 //
1072 // public Scanner(String source) {
1073 // fSource = source.toCharArray();
1074 // }
1075 //
1076 // public boolean hasMoreToken() {
1077 // return pos < fSource.length;
1078 // }
1079 //
1080 // public int getNextToken() {
1081 // char next;
1082 // char c = getChar();
1083 // switch (c) {
1084 // case ' ':
1085 // case '\t':
1086 // case '\n':
1087 // case '\r':
1088 // return getSpace(c);
1089 // case '<' :
1090 // next = getChar();
1091 // if (next == '!') {
1092 // return START_PI;
1093 // } else if (next == '/') {
1094 // return START_END_TAG;
1095 // } else {
1096 // ungetChar();
1097 // return LBRACKET;
1098 // }
1099 // case '/' :
1100 // next = getChar();
1101 // if (next == '>') {
1102 // return END_EMPTY_TAG;
1103 // } else {
1104 // return CHAR;
1105 // }
1106 // case '>' :
1107 // return RBRACKET;
1108 // case '"' :
1109 // case '\'' :
1110 // if (getStringEnd(c) < 0)
1111 // return EOF;
1112 // else
1113 // return STRING;
1114 // case 0 :
1115 // return EOF;
1116 // default :
1117 // if(Character.isDigit(c)) return getNumber(c);
1118 // else if(Character.isLetter()
1119 // return CHAR;
1120 // }
1121 // }
1122 //
1123 // public String getCurrentTokenSource(int type) {
1124 // switch (type) {
1125 // case START_PI :
1126 // return "<!";
1127 // case LBRACKET :
1128 // return "<";
1129 // case START_END_TAG :
1130 // return "</";
1131 // case RBRACKET :
1132 // return ">";
1133 // case CHAR :
1134 // default :
1135 // return "" + fSource[pos - 1];
1136 // }
1137 // }
1138 //
1139 // private char getChar() {
1140 // if (pos >= fSource.length) {
1141 // return 0;
1142 // }
1143 // return fSource[pos++];
1144 // }
1145 //
1146 // private void ungetChar() {
1147 // --pos;
1148 // }
1149 //
1150 // private char peekChar() {
1151 // return fSource[pos];
1152 // }
1153 //
1154 // private char prevChar() {
1155 // return fSource[pos - 1];
1156 // }
1157 //
1158 // private int getStringEnd(char ch) {
1159 // fTokenSource.setLength(0);
1160 // while (pos < fSource.length) {
1161 // char c = fSource[++pos];
1162 // if (c == '\\') {
1163 // // ignore escaped characters
1164 // pos++;
1165 // } else if (c == ch) {
1166 // return pos;
1167 // }
1168 // fTokenSource.append(c);
1169 // }
1170 // if (fTokenSource.length() > 0)
1171 // return pos - 1;
1172 // else
1173 // return -1;
1174 // }
1175 // }
1176
1177 ////////////////////////////////////////////////////////////////////////
1178
1179 interface CustomHandler {
1180 public void endDTD() throws SAXException;
1181 public void startInternalDTD(boolean yes);
1182 }
1183
1184 /**
1185 * Hack to fix missing endDTD() call.
1186 *
1187 * @author chrisl
1188 */
1189 static class CustomXmlParserConfiguration extends StandardParserConfiguration {
1190 private CustomHandler fHandler;
1191 public CustomXmlParserConfiguration(CustomHandler handler) {
1192 super();
1193 if (handler != null) {
1194 // Recreate document scanner with given lexical handler.
1195 boolean b = fComponents.remove(fScanner);
1196 fHandler = handler;
1197 fScanner = createDocumentScanner();
1198 setProperty(DOCUMENT_SCANNER, fScanner);
1199 if (b && fScanner instanceof XMLComponent) {
1200 addComponent((XMLComponent) fScanner);
1201 }
1202 }
1203 boolean b = fComponents.remove(fEntityManager);
1204 fEntityManager = new CustomEntityManager();
1205 setProperty(ENTITY_MANAGER, fEntityManager);
1206 if (b && fEntityManager instanceof XMLComponent)
1207 addComponent(fEntityManager);
1208 //
1209 fErrorReporter.setDocumentLocator(fEntityManager.getEntityScanner());
1210 }
1211 protected XMLDocumentScanner createDocumentScanner() {
1212 return new CustomDocumentScanner(fHandler);
1213 }
1214
1215 }
1216
1217 static class CustomDocumentScanner extends XMLDocumentScannerImpl {
1218 private CustomHandler fHandler;
1219 private boolean fHasInternalDTD;
1220 public CustomDocumentScanner(CustomHandler handler) {
1221 super();
1222 fHandler = handler;
1223 }
1224
1225 /** Scans a doctype declaration. */
1226 protected boolean scanDoctypeDecl() throws IOException, XNIException {
1227 fHasInternalDTD = super.scanDoctypeDecl();
1228 fHasExternalDTD = fLoadExternalDTD && (fDoctypeSystemId != null);
1229 if (!fHasExternalDTD && !fHasInternalDTD && fHandler != null) {
1230 try {
1231 fHandler.endDTD();
1232 } catch (SAXException e) {
1233 e.printStackTrace();
1234 throw new XNIException(e);
1235 }
1236 }
1237 fHandler.startInternalDTD(fHasInternalDTD);
1238 return fHasInternalDTD;
1239 }
1240 }
1241
1242 ////////////////////////////////////////////////////////////////////////
1243
1244}