001: /*
002: * Sun Public License Notice
003: *
004: * The contents of this file are subject to the Sun Public License
005: * Version 1.0 (the "License"). You may not use this file except in
006: * compliance with the License. A copy of the License is available at
007: * http://www.sun.com/
008: *
009: * The Original Code is NetBeans. The Initial Developer of the Original
010: * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun
011: * Microsystems, Inc. All Rights Reserved.
012: */
013: package org.netbeans.editor.ext.html;
014:
015: import java.util.ArrayList;
016: import java.util.HashSet;
017: import java.util.List;
018: import java.util.Set;
019: import java.util.Stack;
020:
021: import javax.swing.text.BadLocationException;
022: import javax.swing.text.JTextComponent;
023:
024: import org.netbeans.editor.BaseDocument;
025: import org.netbeans.editor.TokenID;
026: import org.netbeans.editor.TokenItem;
027: import org.netbeans.editor.ext.ExtSyntaxSupport;
028: import org.netbeans.editor.ext.html.dtd.DTD;
029: import org.netbeans.editor.ext.html.dtd.InvalidateEvent;
030: import org.netbeans.editor.ext.html.dtd.InvalidateListener;
031:
032: /**
033: *
034: * @author Petr Nejedly
035: * @version 0.9
036: */
037: public class HTMLSyntaxSupport extends ExtSyntaxSupport implements
038: InvalidateListener {
039: private static final String FALLBACK_DOCTYPE = "-//W3C//DTD HTML 4.01 Transitional//EN"; // NOI18N
040:
041: private DTD dtd;
042: private String docType;
043:
044: /** Creates new HTMLSyntaxSupport */
045: public HTMLSyntaxSupport(BaseDocument doc) {
046: super (doc);
047: }
048:
049: /**
050: * Reset our cached DTD if no longer valid.
051: */
052: public void dtdInvalidated(InvalidateEvent evt) {
053: if (dtd != null && evt.isInvalidatedIdentifier(docType)) {
054: dtd = null;
055: }
056: }
057:
058: public DTD getDTD() {
059: String type = getDocType();
060: if (type == null)
061: type = FALLBACK_DOCTYPE;
062:
063: if (dtd != null && type == docType)
064: return dtd;
065:
066: docType = type;
067: dtd = org.netbeans.editor.ext.html.dtd.Registry.getDTD(docType,
068: null);
069: return dtd;
070: }
071:
072: protected String getDocType() {
073: try {
074: SyntaxElement elem = getElementChain(0);
075:
076: if (elem == null)
077: return null; // empty document
078:
079: int type = elem.getType();
080:
081: while (type != SyntaxElement.TYPE_DECLARATION
082: && type != SyntaxElement.TYPE_TAG) {
083: elem = elem.getNext();
084: if (elem == null)
085: break;
086: type = elem.getType();
087: }
088:
089: if (type == SyntaxElement.TYPE_DECLARATION)
090: return ((SyntaxElement.Declaration) elem)
091: .getPublicIdentifier();
092:
093: return null;
094: } catch (BadLocationException e) {
095: return null;
096: }
097: }
098:
099: private final int getTokenEnd(TokenItem item) {
100: return item.getOffset() + item.getImage().length();
101: }
102:
103: /**
104: * Returns SyntaxElement instance for block of tokens, which is either
105: * surrounding given offset, or is just after the offset.
106: *
107: * @param offset
108: * offset in document where to search for SyntaxElement
109: * @return SyntaxElement surrounding or laying after the offset or <CODE>null</CODE>
110: * if there is no element there (end of document)
111: */
112: public SyntaxElement getElementChain(int offset)
113: throws BadLocationException {
114: TokenItem first = getTokenChain(offset, Math.min(offset + 10,
115: getDocument().getLength()));
116: TokenItem item = first;
117:
118: if (item == null)
119: return null; // on End of document
120: TokenID id = item.getTokenID();
121:
122: int beginning = item.getOffset();
123:
124: if (id == HTMLTokenContext.CHARACTER) {
125: while (id != null && id == HTMLTokenContext.CHARACTER) {
126: beginning = item.getOffset();
127: item = item.getPrevious();
128: id = item == null ? null : item.getTokenID();
129: }
130:
131: // now item is either HTMLSyntax.VALUE or we're in text, or at BOF
132: if (id != HTMLTokenContext.VALUE
133: && id != HTMLTokenContext.TEXT) {
134: return getNextElement(beginning);
135: } // else ( for VALUE or TEXT ) fall through
136: }
137:
138: if (id == HTMLTokenContext.WS
139: || id == HTMLTokenContext.ARGUMENT
140: || // these
141: // are
142: // possible
143: // only
144: // in
145: // Tags
146: id == HTMLTokenContext.OPERATOR
147: || id == HTMLTokenContext.VALUE) { // so
148: // find
149: // boundary
150: do {
151: item = item.getPrevious(); // Can't get null here, there IS TAG
152: // before WS|ARGUMENT|OPERATOR|VALUE
153: id = item.getTokenID();
154: } while (id != HTMLTokenContext.TAG);
155: return getNextElement(item.getOffset()); // TAGC
156: }
157:
158: if (id == HTMLTokenContext.TEXT) {
159: while (id != null
160: && (id == HTMLTokenContext.TEXT || id == HTMLTokenContext.CHARACTER)) {
161: beginning = item.getOffset();
162: item = item.getPrevious();
163: id = item == null ? null : item.getTokenID();
164: }
165: return getNextElement(beginning); // from start of Commment
166: }
167:
168: if (id == HTMLTokenContext.TAG) {
169: if (item.getImage().startsWith("<"))
170: return getNextElement(item.getOffset()); // TAGO/ETAGO
171: else {
172: do {
173: item = item.getPrevious();
174: id = item.getTokenID();
175: } while (id != HTMLTokenContext.TAG);
176: return getNextElement(item.getOffset()); // TAGC
177: }
178: }
179:
180: if (id == HTMLTokenContext.ERROR)
181: return new SyntaxElement(this , item.getOffset(),
182: getTokenEnd(item), SyntaxElement.TYPE_ERROR);
183:
184: if (id == HTMLTokenContext.BLOCK_COMMENT) {
185: while (id == HTMLTokenContext.BLOCK_COMMENT
186: && !item.getImage().startsWith("<!--")) { // NOI18N
187: item = item.getPrevious();
188: id = item.getTokenID();
189: }
190: return getNextElement(item.getOffset()); // from start of
191: // Commment
192: }
193:
194: if (id == HTMLTokenContext.DECLARATION
195: || id == HTMLTokenContext.SGML_COMMENT) {
196: while (id != HTMLTokenContext.DECLARATION
197: || !item.getImage().startsWith("<!")) { // NOI18N
198: item = item.getPrevious();
199: id = item.getTokenID();
200: }
201: return getNextElement(item.getOffset()); // from start of
202: // Commment
203: }
204: return null;
205: }
206:
207: /**
208: * The way how to get previous SyntaxElement in document. It is not intended
209: * for direct usage, and thus is not public. Usually, it is called from
210: * SyntaxElement's method getPrevious()
211: */
212: SyntaxElement getPreviousElement(int offset)
213: throws BadLocationException {
214: return offset == 0 ? null : getElementChain(offset - 1);
215: }
216:
217: /**
218: * Beware, changes data
219: */
220: private static String getQuotedString(StringBuffer data) {
221: int startIndex = 0;
222: while (data.charAt(startIndex) == ' ')
223: startIndex++;
224:
225: char stopMark = data.charAt(startIndex++);
226: if (stopMark == '"' || stopMark == '\'') {
227: for (int index = startIndex; index < data.length(); index++)
228: if (data.charAt(index) == stopMark) {
229: String quoted = data.substring(startIndex, index);
230: data.delete(0, index + 1);
231: return quoted;
232: }
233: }
234:
235: return null;
236: }
237:
238: /**
239: * Get the next element from given offset. Should only be called from
240: * SyntaxElements obtained by getElementChain, or by getElementChain itself.
241: *
242: * @return SyntaxElement startting at offset, or null, if EoD
243: */
244: public SyntaxElement getNextElement(int offset)
245: throws BadLocationException {
246: TokenItem item = getTokenChain(offset, Math.min(offset + 10,
247: getDocument().getLength()));
248: if (item == null)
249: return null; // on End of Document
250: TokenID id = item.getTokenID();
251:
252: int lastOffset = getTokenEnd(item);
253: if (id == HTMLTokenContext.BLOCK_COMMENT) {
254: while (id == HTMLTokenContext.BLOCK_COMMENT) {
255: lastOffset = getTokenEnd(item);
256: item = item.getNext();
257: if (item == null)
258: break; // EoD
259: id = item.getTokenID();
260: }
261: return new SyntaxElement(this , offset, lastOffset,
262: SyntaxElement.TYPE_COMMENT);
263: }
264:
265: if (id == HTMLTokenContext.DECLARATION) {
266: // Compose whole declaration, leaving out included comments
267: StringBuffer sb = new StringBuffer(item.getImage());
268:
269: while (id == HTMLTokenContext.DECLARATION
270: || id == HTMLTokenContext.SGML_COMMENT) {
271: lastOffset = getTokenEnd(item);
272: item = item.getNext();
273: if (item == null)
274: break; // EoD
275: id = item.getTokenID();
276: if (id == HTMLTokenContext.DECLARATION)
277: sb.append(item.getImage());
278: }
279:
280: String image = sb.toString();
281:
282: // not a DOCTYPE declaration
283: if (!image.startsWith("<!DOCTYPE")) // NOI18N
284: return new SyntaxElement.Declaration(this , offset,
285: lastOffset, null, null, null);
286:
287: // Cut off the <!DOCTYPE substring and possible ws
288: image = image.substring(9).trim();
289:
290: int index = image.indexOf(' ');
291: if (index < 0)
292: return new SyntaxElement.Declaration(this , offset,
293: lastOffset, null, null, null);
294:
295: String rootElem = image.substring(0, index);
296:
297: image = image.substring(index).trim();
298:
299: if (image.startsWith("PUBLIC")) { // NOI18N Public ID
300: image = image.substring(6).trim();
301: sb = new StringBuffer(image);
302: String pi = getQuotedString(sb);
303: if (pi != null) {
304: String si = getQuotedString(sb);
305: return new SyntaxElement.Declaration(this , offset,
306: lastOffset, rootElem, pi, si);
307: }
308: } else if (image.startsWith("SYSTEM")) { // NOI18N System ID
309: image = image.substring(6).trim();
310: sb = new StringBuffer(image);
311: String si = getQuotedString(sb);
312: if (si != null) {
313: return new SyntaxElement.Declaration(this , offset,
314: lastOffset, rootElem, null, si);
315: }
316: }
317: return new SyntaxElement.Declaration(this , offset,
318: lastOffset, null, null, null);
319: }
320:
321: if (id == HTMLTokenContext.ERROR)
322: return new SyntaxElement(this , item.getOffset(),
323: lastOffset, SyntaxElement.TYPE_ERROR);
324:
325: if (id == HTMLTokenContext.TEXT
326: || id == HTMLTokenContext.CHARACTER) {
327: while (id == HTMLTokenContext.TEXT
328: || id == HTMLTokenContext.CHARACTER) {
329: lastOffset = getTokenEnd(item);
330: item = item.getNext();
331: if (item == null)
332: break; // EoD
333: id = item.getTokenID();
334: }
335: return new SyntaxElement(this , offset, lastOffset,
336: SyntaxElement.TYPE_TEXT);
337: }
338:
339: String text = item.getImage();
340: if (id == HTMLTokenContext.TAG) {
341: if (text.startsWith("</")) { // endtag
342: String name = text.substring(2);
343: item = item.getNext();
344: id = item == null ? null : item.getTokenID();
345:
346: while (id == HTMLTokenContext.WS) {
347: lastOffset = getTokenEnd(item);
348: item = item.getNext();
349: id = item == null ? null : item.getTokenID();
350: }
351:
352: if (id == HTMLTokenContext.TAG
353: && item.getImage().equals(">")) { // with
354: // this
355: // tag
356: return new SyntaxElement.Named(this , offset,
357: getTokenEnd(item),
358: SyntaxElement.TYPE_ENDTAG, name);
359: } else { // without this tag
360: return new SyntaxElement.Named(this , offset,
361: lastOffset, SyntaxElement.TYPE_ENDTAG, name);
362: }
363: } else { // starttag
364: String name = text.substring(1);
365: ArrayList attrs = new ArrayList();
366:
367: item = item.getNext();
368: id = item == null ? null : item.getTokenID();
369:
370: while (id == HTMLTokenContext.WS
371: || id == HTMLTokenContext.ARGUMENT
372: || id == HTMLTokenContext.OPERATOR
373: || id == HTMLTokenContext.VALUE
374: || id == HTMLTokenContext.CHARACTER) {
375: if (id == HTMLTokenContext.ARGUMENT)
376: attrs.add(item.getImage()); // log all attributes
377: lastOffset = getTokenEnd(item);
378: item = item.getNext();
379: id = item == null ? null : item.getTokenID();
380: }
381: if (id == HTMLTokenContext.TAG
382: && item.getImage().equals(">")) { // with
383: // this
384: // tag
385: return new SyntaxElement.Tag(this , offset,
386: getTokenEnd(item), name, attrs);
387: } else { // without this tag
388: return new SyntaxElement.Tag(this , offset,
389: lastOffset, name, attrs);
390: }
391:
392: }
393: }
394:
395: throw new BadLocationException("Misuse at " + offset, offset);
396: }
397:
398: public List getPossibleEndTags(int offset, String prefix)
399: throws BadLocationException {
400: prefix = prefix.toUpperCase();
401: int prefixLen = prefix.length();
402: SyntaxElement elem = getElementChain(offset);
403: Stack stack = new Stack();
404: List result = new ArrayList();
405: Set found = new HashSet();
406: DTD dtd = getDTD();
407:
408: if (elem != null) {
409: elem = elem.getPrevious(); // we need smtg. before our </
410: } else { // End of Document
411: if (offset > 0) {
412: elem = getElementChain(offset - 1);
413: } else { // beginning of document too, not much we can do on empty
414: // doc
415: return result;
416: }
417: }
418:
419: for (; elem != null; elem = elem.getPrevious()) {
420: if (elem.getType() == SyntaxElement.TYPE_ENDTAG) {
421: stack.push(((SyntaxElement.Named) elem).getName()
422: .toUpperCase());
423: } else if (elem.getType() == SyntaxElement.TYPE_TAG) {
424: DTD.Element tag = dtd
425: .getElement(((SyntaxElement.Tag) elem)
426: .getName().toUpperCase());
427:
428: if (tag == null)
429: continue; // Unknown tag - ignore
430: if (tag.isEmpty())
431: continue; // ignore empty Tags - they are like start and
432: // imediate end
433:
434: String name = tag.getName();
435:
436: if (stack.empty()) { // empty stack - we are on the same tree
437: // deepnes - can close this tag
438: if (name.startsWith(prefix)
439: && !found.contains(name)) { // add
440: // only
441: // new
442: // items
443: found.add(name);
444: result.add(new HTMLCompletionQuery.EndTagItem(
445: name, offset - 2 - prefixLen,
446: prefixLen + 2));
447: }
448: if (!tag.hasOptionalEnd())
449: break; // If this tag have required EndTag, we can't go
450: // higher until completing this tag
451: } else { // not empty - we match content of stack
452: if (stack.peek().equals(name)) { // match - close this
453: // branch of document
454: // tree
455: stack.pop();
456: } else if (!tag.hasOptionalEnd())
457: break; // we reached error in document structure, give
458: // up
459: }
460: }
461: }
462:
463: return result;
464: }
465:
466: public int checkCompletion(JTextComponent target, String typedText,
467: boolean visible) {
468: if (!visible) {
469: int retVal = COMPLETION_CANCEL;
470: switch (typedText.charAt(typedText.length() - 1)) {
471: case '/':
472: int dotPos = target.getCaret().getDot();
473: BaseDocument doc = (BaseDocument) target.getDocument();
474: if (dotPos >= 2) { // last char before inserted slash
475: try {
476: String txtBeforeSpace = doc.getText(dotPos - 2,
477: 2);
478: if (txtBeforeSpace.equals("</")) // NOI18N
479: retVal = COMPLETION_POPUP;
480: } catch (BadLocationException e) {
481: }
482: }
483: break;
484:
485: case ' ':
486: case '<':
487: case '&':
488: retVal = COMPLETION_POPUP;
489: break;
490: }
491: return retVal;
492: } else { // the pane is already visible
493: switch (typedText.charAt(0)) {
494: case '>':
495: case ';':
496: return COMPLETION_HIDE;
497: }
498: return COMPLETION_POST_REFRESH;
499: }
500: }
501:
502: }
|