001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.netbeans.editor.ext.html;
042:
043: import javax.swing.text.Document;
044: import java.util.*;
045: import javax.swing.text.BadLocationException;
046: import javax.swing.text.JTextComponent;
047: import org.netbeans.api.html.lexer.HTMLTokenId;
048: import org.netbeans.api.lexer.Token;
049: import org.netbeans.api.lexer.TokenHierarchy;
050: import org.netbeans.api.lexer.TokenSequence;
051: import org.netbeans.editor.BaseDocument;
052: import org.netbeans.editor.ext.ExtSyntaxSupport;
053: import org.netbeans.editor.ext.html.dtd.DTD;
054: import org.netbeans.editor.ext.html.dtd.DTD.Element;
055: import org.netbeans.editor.ext.html.dtd.InvalidateEvent;
056: import org.netbeans.editor.ext.html.dtd.InvalidateListener;
057: import org.netbeans.spi.editor.completion.CompletionItem;
058:
059: /**
060: * Support methods for HTML document syntax analyzes
061: *
062: * @author Petr Nejedly
063: * @author Marek Fukala
064: */
065: public class HTMLSyntaxSupport extends ExtSyntaxSupport implements
066: InvalidateListener {
067: private static final String FALLBACK_DOCTYPE = "-//W3C//DTD HTML 4.01 Transitional//EN"; // NOI18N
068:
069: private DTD dtd;
070: private String docType;
071:
072: public static synchronized HTMLSyntaxSupport get(Document doc) {
073: HTMLSyntaxSupport sup = (HTMLSyntaxSupport) doc
074: .getProperty(HTMLSyntaxSupport.class);
075: if (sup == null) {
076: sup = new HTMLSyntaxSupport((BaseDocument) doc);
077: doc.putProperty(HTMLSyntaxSupport.class, sup);
078: }
079: return sup;
080: }
081:
082: /** Creates new HTMLSyntaxSupport */
083: public HTMLSyntaxSupport(BaseDocument doc) {
084: super (doc);
085: }
086:
087: /** Reset our cached DTD if no longer valid.
088: */
089: public void dtdInvalidated(InvalidateEvent evt) {
090: if (dtd != null && evt.isInvalidatedIdentifier(docType)) {
091: dtd = null;
092: }
093: }
094:
095: public DTD getDTD() {
096: String type = getDocType();
097: if (type == null)
098: type = FALLBACK_DOCTYPE;
099:
100: if (dtd != null && type == docType)
101: return dtd;
102:
103: docType = type;
104: dtd = org.netbeans.editor.ext.html.dtd.Registry.getDTD(docType,
105: null);
106:
107: if (dtd == null) {
108: //use default for unknown doctypes
109: dtd = org.netbeans.editor.ext.html.dtd.Registry.getDTD(
110: FALLBACK_DOCTYPE, null);
111: }
112: return dtd;
113: }
114:
115: protected String getDocType() {
116: try {
117: SyntaxElement elem = getElementChain(0);
118:
119: if (elem == null)
120: return null; // empty document
121:
122: int type = elem.getType();
123:
124: while (type != SyntaxElement.TYPE_DECLARATION
125: && type != SyntaxElement.TYPE_TAG) {
126: elem = elem.getNext();
127: if (elem == null)
128: break;
129: type = elem.getType();
130: }
131:
132: if (type == SyntaxElement.TYPE_DECLARATION)
133: return ((SyntaxElement.Declaration) elem)
134: .getPublicIdentifier();
135:
136: return null;
137: } catch (BadLocationException e) {
138: return null;
139: }
140: }
141:
142: /** Find matching tags with the current position.
143: * @param offset position of the starting tag
144: * @param simple whether the search should skip comment and possibly other areas.
145: * This can be useful when the speed is critical, because the simple
146: * search is faster.
147: * @return array of integers containing starting and ending position
148: * of the block in the document. Null is returned if there's
149: * no matching block.
150: */
151: @Override
152: public int[] findMatchingBlock(int offset, boolean simpleSearch)
153: throws BadLocationException {
154: BaseDocument document = getDocument();
155: document.readLock();
156: try {
157: TokenHierarchy hi = TokenHierarchy.get(document);
158: TokenSequence ts = tokenSequence(hi, offset);
159: if (ts == null) {
160: //no suitable token sequence found
161: return null;
162: }
163:
164: ts.move(offset);
165: if (!ts.moveNext() && !ts.movePrevious()) {
166: return null; //no token found
167: }
168:
169: Token token = ts.token();
170:
171: // if the carret is after HTML tag ( after char '>' ), ship inside the tag
172: if (token.id() == HTMLTokenId.TAG_CLOSE_SYMBOL) {
173: ts.moveIndex(ts.index() - 2);
174: if (ts.moveNext()) {
175: token = ts.token();
176: }
177: }
178:
179: boolean isInside = false; // flag, whether the carret is somewhere in a HTML tag
180: if (isTagButNotSymbol(token)) {
181: isInside = true; // the carret is somewhere in '<htmltag' or '</htmltag'
182: } else {
183: if (ts.moveNext()) {
184: token = ts.token();
185: if (token.id() == HTMLTokenId.TAG_OPEN_SYMBOL) {
186: //we are on opening symbol < or </
187: //so go to the next token which should be a TAG
188: //if the token is null or nor TAG there is nothing to match
189: if ((token.id() == HTMLTokenId.TAG_CLOSE)
190: || (token.id() == HTMLTokenId.TAG_OPEN)) {
191: isInside = true; // we found a tag
192: } else {
193: return null;
194: }
195: } else {
196: //we are on closing symbol > or />
197: // find out whether the carret is inside an HTML tag
198: //try to find the beginning of the tag.
199: boolean found = false;
200: while (!isTagButNotSymbol(token)
201: && token.id() != HTMLTokenId.TAG_CLOSE_SYMBOL
202: && ts.movePrevious()) {
203: token = ts.token();
204: }
205:
206: if (ts.index() != -1
207: && isTagButNotSymbol(token)) {
208: isInside = true;
209: }
210: }
211: } else {
212: return null; //no token
213: }
214: }
215:
216: if (ts.index() != -1 && isTagButNotSymbol(token)
217: && isInside) {
218: int start; // possition where the matched tag starts
219: int end; // possition where the matched tag ends
220: int poss = -1; // how many the same tags is inside the mathed tag
221:
222: String tag = token.text().toString().toLowerCase()
223: .trim();
224: //test whether we are in a close tag
225: if (token.id() == HTMLTokenId.TAG_CLOSE) {
226: //we are in a close tag
227: do {
228: token = ts.token();
229: if (isTagButNotSymbol(token)) {
230: String tagName = token.text().toString()
231: .toLowerCase().trim();
232: if (tagName.equals(tag)
233: && (token.id() == HTMLTokenId.TAG_OPEN)
234: && !isSingletonTag(ts)) {
235: //it's an open tag
236: if (poss == 0) {
237: //get offset of previous token: < or </
238: ts.movePrevious();
239: start = ts.token().offset(hi);
240: ts.moveIndex(ts.index() + 2);
241: ts.moveNext();
242: Token tok = ts.token();
243: end = tok.offset(hi)
244: + (tok.id() == HTMLTokenId.TAG_CLOSE_SYMBOL ? tok
245: .text().length()
246: : 0);
247: return new int[] { start, end };
248: } else {
249: poss--;
250: }
251: } else {
252: //test whether the tag is a close tag for the 'tag' tagname
253: if ((tagName.indexOf(tag) > -1)
254: && !isSingletonTag(ts)) {
255: poss++;
256: }
257: }
258: }
259: } while (ts.movePrevious());
260:
261: } else {
262: //we are in an open tag
263: if (tag.charAt(0) == '>')
264: return null;
265:
266: //We need to find out whether the open tag is a singleton tag or not.
267: //In the first case no matching is needed
268: if (isSingletonTag(ts))
269: return null;
270:
271: do {
272: token = ts.token();
273: if (isTagButNotSymbol(token)) {
274: String tagName = token.text().toString()
275: .toLowerCase().trim();
276: if (tagName.equals(tag)
277: && token.id() == HTMLTokenId.TAG_CLOSE) {
278: if (poss == 0) {
279: //get offset of previous token: < or </
280: end = token.offset(hi)
281: + token.text().length() + 1;
282: ts.movePrevious();
283: start = ts.token().offset(hi);
284:
285: do {
286: token = ts.token();
287: } while (ts.moveNext()
288: && token.id() != HTMLTokenId.TAG_CLOSE_SYMBOL);
289:
290: if (ts.index() != -1) {
291: end = token.offset(hi)
292: + token.text().length();
293: }
294: return new int[] { start, end };
295: } else
296: poss--;
297: } else {
298: if (tagName.equals(tag)
299: && !isSingletonTag(ts)) {
300: poss++;
301: }
302: }
303: }
304: } while (ts.moveNext());
305: }
306: }
307:
308: ts.move(offset); //reset the token sequence to the original position
309: if (!(ts.moveNext() || ts.movePrevious())) {
310: return null; //no token
311: }
312: token = ts.token();
313:
314: //match html comments
315: if (ts.index() != -1
316: && token.id() == HTMLTokenId.BLOCK_COMMENT) {
317: String tokenImage = token.text().toString();
318: if (tokenImage.startsWith("<!--")
319: && (offset < (token.offset(hi))
320: + "<!--".length())) { //NOI18N
321: //start html token - we need to find the end token of the html comment
322: do {
323: token = ts.token();
324: tokenImage = token.text().toString();
325: if ((token.id() == HTMLTokenId.BLOCK_COMMENT)) {
326: if (tokenImage.endsWith("-->")) {//NOI18N
327: //found end token
328: int end = token.offset(hi)
329: + tokenImage.length();
330: int start = end - "-->".length(); //NOI18N
331: return new int[] { start, end };
332: }
333: } else
334: break;
335: } while (ts.moveNext());
336: }
337:
338: if (tokenImage.endsWith("-->")
339: && (offset >= (token.offset(hi))
340: + tokenImage.length() - "-->".length())) { //NOI18N
341: //end html token - we need to find the start token of the html comment
342: do {
343: token = ts.token();
344: if ((token.id() == HTMLTokenId.BLOCK_COMMENT)) {
345: if (token.text().toString().startsWith(
346: "<!--")) { //NOI18N
347: //found end token
348: int start = token.offset(hi);
349: int end = start + "<!--".length(); //NOI18N
350: return new int[] { start, end };
351: }
352: } else
353: break;
354:
355: } while (ts.movePrevious());
356: }
357: } //eof match html comments
358:
359: } finally {
360: document.readUnlock();
361: }
362: return null;
363: }
364:
365: /** Finds out whether the given {@link TokenSequence}'s actual token is a part of a singleton tag (e.g. <div style=""/>).
366: * @ts TokenSequence positioned on a token within a tag
367: * @return true is the token is a part of singleton tag
368: */
369: public boolean isSingletonTag(TokenSequence ts) {
370: int tsIndex = ts.index(); //backup ts state
371: if (tsIndex != -1) { //test if we are on a token
372: try {
373: do {
374: Token ti = ts.token();
375: if (ti.id() == HTMLTokenId.TAG_CLOSE_SYMBOL) {
376: if ("/>".equals(ti.text().toString())) { // NOI18N
377: //it is a singleton tag => do not match
378: return true;
379: }
380: if (">".equals(ti.text().toString())) {
381: break; // NOI18N
382: }
383: }
384: //break the loop on TEXT or on another open tag symbol
385: //(just to prevent long loop in case the tag is not closed)
386: if ((ti.id() == HTMLTokenId.TEXT)
387: || (ti.id() == HTMLTokenId.TAG_OPEN_SYMBOL)) {
388: break;
389: }
390: } while (ts.moveNext());
391: } finally {
392: ts.moveIndex(tsIndex); //backup the TokenSequence position
393: ts.moveNext();
394: }
395: } else {
396: //ts is rewinded out of tokens
397: }
398: return false;
399: }
400:
401: /** The way how to get previous SyntaxElement in document. It is not intended
402: * for direct usage, and thus is not public. Usually, it is called from
403: * SyntaxElement's method getPrevious()
404: */
405: public SyntaxElement getPreviousElement(int offset)
406: throws BadLocationException {
407: return offset == 0 ? null : getElementChain(offset - 1);
408: }
409:
410: /** Returns SyntaxElement instance for block of tokens, which is either
411: * surrounding given offset, or is just after the offset.
412: *
413: * @param offset offset in document where to search for SyntaxElement
414: * @return SyntaxElement surrounding or laying after the offset
415: * or <CODE>null</CODE> if there is no element there (end of document)
416: */
417: public SyntaxElement getElementChain(int offset)
418: throws BadLocationException {
419: getDocument().readLock();
420: try {
421: TokenHierarchy hi = TokenHierarchy.get(getDocument());
422: TokenSequence ts = tokenSequence(hi, offset);
423: if (ts == null) {
424: //we are out of html - go back and try to find an html element
425: TokenSequence tseq = hi.tokenSequence();
426: tseq.move(offset);
427: if (!tseq.movePrevious() && !tseq.moveNext()) {
428: //no token on the position
429: return null;
430: }
431: int nonHtmlBlockStart = 0;
432: //go back until we find an html code
433: while (tseq.movePrevious()) {
434: //XXX - just one level embedding
435: TokenSequence htmlTS = tseq.embedded(HTMLTokenId
436: .language());
437: if (htmlTS != null) {
438: if (htmlTS.moveNext() || htmlTS.movePrevious()) {
439: //found html piece
440: nonHtmlBlockStart = htmlTS.offset()
441: + htmlTS.token().length();
442: break;
443: }
444: }
445: }
446: return getNextElement(nonHtmlBlockStart);
447: }
448:
449: //html token found
450: ts.move(offset);
451: if (!ts.moveNext() && !ts.movePrevious())
452: return null; //no token found
453:
454: Token item = ts.token();
455:
456: int beginning = ts.offset();
457:
458: if (item.id() == HTMLTokenId.CHARACTER) {
459: do {
460: item = ts.token();
461: beginning = ts.offset();
462: } while (item.id() == HTMLTokenId.CHARACTER
463: && ts.movePrevious());
464:
465: // now item is either HTMLSyntax.VALUE or we're in text, or at BOF
466: if (item.id() != HTMLTokenId.VALUE
467: && item.id() != HTMLTokenId.TEXT) {
468: return getNextElement(beginning);
469: } // else ( for VALUE or TEXT ) fall through
470: }
471:
472: if (item.id() == HTMLTokenId.WS
473: || item.id() == HTMLTokenId.ARGUMENT
474: || // these are possible only in Tags
475: item.id() == HTMLTokenId.OPERATOR
476: || item.id() == HTMLTokenId.VALUE) { // so find boundary
477: while (ts.movePrevious() && !isTag(item = ts.token()))
478: ;
479: return getNextElement(item.offset(hi)); // TAGC
480: }
481:
482: if (item.id() == HTMLTokenId.TEXT) {
483: do {
484: beginning = ts.offset();
485: } while (ts.movePrevious()
486: && (ts.token().id() == HTMLTokenId.TEXT || ts
487: .token().id() == HTMLTokenId.CHARACTER));
488:
489: return getNextElement(beginning); // from start of Commment
490: }
491:
492: if (item.id() == HTMLTokenId.SCRIPT) {
493: //we have just one big token for script
494: return getNextElement(ts.token().offset(hi));
495: }
496:
497: if (item.id() == HTMLTokenId.STYLE) {
498: //we have just one big token for script
499: return getNextElement(ts.token().offset(hi));
500: }
501:
502: if (isTag(item)) {
503: if (item.id() == HTMLTokenId.TAG_OPEN
504: || item.id() == HTMLTokenId.TAG_OPEN_SYMBOL)
505: return getNextElement(item.offset(hi)); // TAGO/ETAGO // NOI18N
506: else {
507: do {
508: if (!ts.movePrevious()) {
509: return getNextElement(item.offset(hi));
510: }
511: item = ts.token();
512: } while (item.id() != HTMLTokenId.TAG_OPEN_SYMBOL);
513:
514: return getNextElement(item.offset(hi)); // TAGC
515: }
516: }
517:
518: if (item.id() == HTMLTokenId.ERROR)
519: return new SyntaxElement(this , item.offset(hi),
520: getTokenEnd(hi, item), SyntaxElement.TYPE_ERROR);
521:
522: if (item.id() == HTMLTokenId.BLOCK_COMMENT) {
523: while (item.id() == HTMLTokenId.BLOCK_COMMENT
524: && !item.text().toString().startsWith("<!--")
525: && ts.movePrevious()) { // NOI18N
526: item = ts.token();
527: }
528: return getNextElement(item.offset(hi)); // from start of Commment
529: }
530:
531: if (item.id() == HTMLTokenId.DECLARATION
532: || item.id() == HTMLTokenId.SGML_COMMENT) {
533: while ((item.id() != HTMLTokenId.DECLARATION || !item
534: .text().toString().startsWith("<!"))
535: && ts.movePrevious()) { // NOI18N
536: item = ts.token();
537: }
538: return getNextElement(item.offset(hi)); // from start of Commment
539: }
540: } finally {
541: getDocument().readUnlock();
542: }
543: return null;
544: }
545:
546: public SyntaxElement getNextElement(int offset)
547: throws javax.swing.text.BadLocationException {
548: getDocument().readLock();
549: try {
550: TokenHierarchy hi = TokenHierarchy.get(getDocument());
551: TokenSequence ts = tokenSequence(hi, offset);
552: if (ts == null) {
553: //we are out of html - go back and try to find an html element
554: TokenSequence tseq = hi.tokenSequence();
555: tseq.move(offset);
556: if (!tseq.movePrevious() && !tseq.moveNext()) {
557: //no token on the position
558: return null;
559: }
560: int nonHtmlBlockEnd = getDocument().getLength();
561: //find end of the non-html block
562: while (tseq.moveNext()) {
563: //XXX - just one level embedding
564: TokenSequence htmlTS = tseq.embedded(HTMLTokenId
565: .language());
566: if (htmlTS != null) {
567: //found html piece
568: if (!htmlTS.moveNext()) {
569: return null; //no token in the TS!?!?!
570: }
571: nonHtmlBlockEnd = htmlTS.offset();
572: break;
573: }
574: }
575: if (offset == nonHtmlBlockEnd) {
576: return null;
577: }
578:
579: return new SyntaxElement(this , offset, nonHtmlBlockEnd,
580: SyntaxElement.TYPE_UNKNOWN);
581: }
582:
583: ts.move(offset);
584: if (!ts.moveNext())
585: return null;
586: org.netbeans.api.lexer.Token item = ts.token();
587: int lastOffset = getTokenEnd(hi, item);
588:
589: if (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.BLOCK_COMMENT) {
590: do {
591: lastOffset = getTokenEnd(hi, ts.token());
592: } while (ts.token().id() == org.netbeans.api.html.lexer.HTMLTokenId.BLOCK_COMMENT
593: && ts.moveNext());
594: return new SyntaxElement(this , offset, lastOffset,
595: SyntaxElement.TYPE_COMMENT);
596: }
597: if (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.DECLARATION) {
598: java.lang.StringBuffer sb = new java.lang.StringBuffer(
599: item.text());
600:
601: while (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.DECLARATION
602: || item.id() == org.netbeans.api.html.lexer.HTMLTokenId.SGML_COMMENT) {
603: lastOffset = getTokenEnd(hi, item);
604: if (!ts.moveNext()) {
605: break;
606: }
607: item = ts.token();
608: if (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.DECLARATION)
609: sb.append(item.text().toString());
610: }
611: java.lang.String image = sb.toString();
612:
613: if (!image.startsWith("<!DOCTYPE"))
614: return new org.netbeans.editor.ext.html.SyntaxElement.Declaration(
615: this , offset, lastOffset, null, null, null);
616: image = image.substring(9).trim();
617: int index = image.indexOf(' ');
618:
619: if (index < 0)
620: return new org.netbeans.editor.ext.html.SyntaxElement.Declaration(
621: this , offset, lastOffset, null, null, null);
622: java.lang.String rootElem = image.substring(0, index);
623:
624: image = image.substring(index).trim();
625: if (image.startsWith("PUBLIC")) {
626: image = image.substring(6).trim();
627: sb = new java.lang.StringBuffer(image);
628: java.lang.String pi = getQuotedString(sb);
629:
630: if (pi != null) {
631: java.lang.String si = getQuotedString(sb);
632:
633: return new org.netbeans.editor.ext.html.SyntaxElement.Declaration(
634: this , offset, lastOffset, rootElem, pi,
635: si);
636: }
637: } else if (image.startsWith("SYSTEM")) {
638: image = image.substring(6).trim();
639: sb = new java.lang.StringBuffer(image);
640: java.lang.String si = getQuotedString(sb);
641:
642: if (si != null) {
643: return new org.netbeans.editor.ext.html.SyntaxElement.Declaration(
644: this , offset, lastOffset, rootElem,
645: null, si);
646: }
647: }
648: return new org.netbeans.editor.ext.html.SyntaxElement.Declaration(
649: this , offset, lastOffset, null, null, null);
650: }
651: if (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.ERROR)
652: return new SyntaxElement(this , item.offset(hi),
653: lastOffset, SyntaxElement.TYPE_ERROR);
654: if (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.TEXT
655: || item.id() == org.netbeans.api.html.lexer.HTMLTokenId.CHARACTER) {
656: do {
657: lastOffset = getTokenEnd(hi, item);
658: item = ts.token();
659: } while (ts.moveNext()
660: && (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.TEXT || item
661: .id() == org.netbeans.api.html.lexer.HTMLTokenId.CHARACTER));
662: return new SyntaxElement(this , offset, lastOffset,
663: SyntaxElement.TYPE_TEXT);
664: }
665: if (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.SCRIPT) {
666: return new SyntaxElement(this , offset, getTokenEnd(hi,
667: item), SyntaxElement.TYPE_SCRIPT);
668: }
669: if (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.STYLE) {
670: return new SyntaxElement(this , offset, getTokenEnd(hi,
671: item), SyntaxElement.TYPE_STYLE);
672: }
673: if (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.TAG_CLOSE
674: || (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.TAG_OPEN_SYMBOL && item
675: .text().toString().equals("</"))) {
676: java.lang.String name = item.text().toString();
677:
678: if (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.TAG_OPEN_SYMBOL) {
679: ts.moveNext();
680: name = ts.token().text().toString();
681: }
682: ts.moveNext();
683: item = ts.token();
684: do {
685: item = ts.token();
686: lastOffset = getTokenEnd(hi, item);
687: } while (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.WS
688: && ts.moveNext());
689: if (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.TAG_CLOSE_SYMBOL) {
690: return new org.netbeans.editor.ext.html.SyntaxElement.Named(
691: this , offset, getTokenEnd(hi, item),
692: SyntaxElement.TYPE_ENDTAG, name);
693: } else {
694: return new org.netbeans.editor.ext.html.SyntaxElement.Named(
695: this , offset, lastOffset,
696: SyntaxElement.TYPE_ENDTAG, name);
697: }
698: }
699: if (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.TAG_OPEN
700: || (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.TAG_OPEN_SYMBOL && !item
701: .text().toString().equals("</"))) {
702: java.lang.String name = item.text().toString();
703: ArrayList<SyntaxElement.TagAttribute> attrs = new ArrayList<SyntaxElement.TagAttribute>();
704:
705: if (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.TAG_OPEN_SYMBOL) {
706: ts.moveNext();
707: name = ts.token().text().toString();
708: }
709: ts.moveNext();
710: item = ts.token();
711:
712: //find tag attributes
713: Token attrNameToken = null;
714: do {
715: item = ts.token();
716: if (item.id() == HTMLTokenId.ARGUMENT) {
717: //attribute name
718: attrNameToken = item;
719: } else if (item.id() == HTMLTokenId.VALUE
720: && attrNameToken != null) {
721: //found attribute value after attribute name
722:
723: //there may be entity reference inside the attribute value
724: //e.g. onclick="alert('hello world');" which divides the value
725: //into more html tokens
726: StringBuffer value = new StringBuffer();
727: Token t = null;
728: do {
729: t = ts.token();
730: value.append(t.text().toString());
731: } while (ts.moveNext()
732: && (ts.token().id() == HTMLTokenId.VALUE || ts
733: .token().id() == HTMLTokenId.CHARACTER));
734:
735: SyntaxElement.TagAttribute tagAttr = new SyntaxElement.TagAttribute(
736: attrNameToken.text().toString(), value
737: .toString(), attrNameToken
738: .offset(hi), item.offset(hi));
739: attrs.add(tagAttr);
740: attrNameToken = null;
741: }
742: lastOffset = getTokenEnd(hi, item);
743: } while ((item.id() == org.netbeans.api.html.lexer.HTMLTokenId.WS
744: || item.id() == org.netbeans.api.html.lexer.HTMLTokenId.ARGUMENT
745: || item.id() == org.netbeans.api.html.lexer.HTMLTokenId.OPERATOR
746: || item.id() == org.netbeans.api.html.lexer.HTMLTokenId.VALUE || item
747: .id() == org.netbeans.api.html.lexer.HTMLTokenId.CHARACTER)
748: && ts.moveNext());
749:
750: if (item.id() == org.netbeans.api.html.lexer.HTMLTokenId.TAG_CLOSE_SYMBOL) {
751: return new org.netbeans.editor.ext.html.SyntaxElement.Tag(
752: this , offset, getTokenEnd(hi, item), name,
753: attrs, item.text().toString().equals("/>"));
754: } else {
755: return new org.netbeans.editor.ext.html.SyntaxElement.Tag(
756: this , offset, lastOffset, name, attrs);
757: }
758: }
759:
760: } finally {
761: getDocument().readUnlock();
762: }
763: return null;
764: }
765:
766: public static boolean isTag(Token t) {
767: return ((t.id() == HTMLTokenId.TAG_OPEN)
768: || (t.id() == HTMLTokenId.TAG_CLOSE)
769: || (t.id() == HTMLTokenId.TAG_OPEN_SYMBOL) || (t.id() == HTMLTokenId.TAG_CLOSE_SYMBOL));
770: }
771:
772: public static boolean isTagButNotSymbol(Token t) {
773: return ((t.id() == HTMLTokenId.TAG_OPEN) || (t.id() == HTMLTokenId.TAG_CLOSE));
774: }
775:
776: private static int getTokenEnd(TokenHierarchy thi, Token item) {
777: return item.offset(thi) + item.text().length();
778: }
779:
780: /**
781: * Beware, changes data
782: */
783: private static String getQuotedString(StringBuffer data) {
784: int startIndex = 0;
785: if (data == null || data.length() == 0)
786: return null;
787: while (data.charAt(startIndex) == ' ')
788: startIndex++;
789:
790: char stopMark = data.charAt(startIndex++);
791: if (stopMark == '"' || stopMark == '\'') {
792: for (int index = startIndex; index < data.length(); index++)
793: if (data.charAt(index) == stopMark) {
794: String quoted = data.substring(startIndex, index);
795: data.delete(0, index + 1);
796: return quoted;
797: }
798: }
799:
800: return null;
801: }
802:
803: private static TokenSequence tokenSequence(TokenHierarchy hi,
804: int offset) {
805: TokenSequence ts = hi.tokenSequence(HTMLTokenId.language());
806: if (ts == null) {
807: //HTML language is not top level one
808: ts = hi.tokenSequence();
809: ts.move(offset);
810: if (!ts.moveNext() && !ts.movePrevious()) {
811: return null; //no token found
812: } else {
813: ts = ts.embedded(HTMLTokenId.language());
814: }
815: }
816: return ts;
817: }
818:
819: public List getPossibleEndTags(int offset, String prefix)
820: throws BadLocationException {
821: prefix = prefix.toUpperCase();
822: int prefixLen = prefix.length();
823: SyntaxElement elem = getElementChain(offset);
824: Stack stack = new Stack();
825: List result = new ArrayList();
826: Set found = new HashSet();
827: DTD dtd = getDTD();
828:
829: if (elem == null) {
830: if (offset > 0) {
831: elem = getElementChain(offset - 1);
832: if (elem == null)
833: return result;
834: } else
835: return result;
836: }
837:
838: int itemsCount = 0;
839: for (elem = elem.getPrevious(); elem != null; elem = elem
840: .getPrevious()) {
841: if (elem.getType() == SyntaxElement.TYPE_ENDTAG) { // NOI18N
842: DTD.Element tag = dtd
843: .getElement(((SyntaxElement.Named) elem)
844: .getName().toUpperCase());
845: if (tag != null && !tag.isEmpty())
846: stack.push(((SyntaxElement.Named) elem).getName()
847: .toUpperCase());
848: } else if (elem.getType() == SyntaxElement.TYPE_TAG) { //now </ and > are returned as SyntaxElement.TAG so I need to filter them NOI18N
849: DTD.Element tag = dtd
850: .getElement(((SyntaxElement.Tag) elem)
851: .getName().toUpperCase());
852:
853: if (tag == null)
854: continue; // Unknown tag - ignore
855: if (tag.isEmpty())
856: continue; // ignore empty Tags - they are like start and imediate end
857:
858: String name = tag.getName();
859:
860: if (stack.empty()) { // empty stack - we are on the same tree deepnes - can close this tag
861: if (name.startsWith(prefix)
862: && !found.contains(name)) { // add only new items
863: found.add(name);
864: result.add(new HTMLCompletionQuery.EndTagItem(
865: name, offset - 2 - prefixLen,
866: prefixLen + 2, name, itemsCount));
867: }
868: if (!tag.hasOptionalEnd())
869: break; // If this tag have required EndTag, we can't go higher until completing this tag
870: } else { // not empty - we match content of stack
871: if (stack.peek().equals(name)) { // match - close this branch of document tree
872: stack.pop();
873: } else if (!tag.hasOptionalEnd())
874: break; // we reached error in document structure, give up
875: }
876: }
877: }
878:
879: return result;
880: }
881:
882: public List getAutocompletedEndTag(int offset) {
883: List l = new ArrayList();
884: try {
885: SyntaxElement elem = getElementChain(offset - 1);
886: if (elem != null
887: && elem.getType() == SyntaxElement.TYPE_TAG) {
888: String tagName = ((SyntaxElement.Named) elem).getName();
889: //check if the tag has required endtag
890: Element dtdElem = getDTD().getElement(
891: tagName.toUpperCase());
892: if (dtdElem == null || !dtdElem.isEmpty()) {
893: CompletionItem eti = new HTMLCompletionQuery.AutocompleteEndTagItem(
894: tagName, offset);
895: l.add(eti);
896: }
897: }
898: } catch (BadLocationException e) {
899: //just ignore
900: }
901: return l;
902: }
903:
904: public int checkCompletion(JTextComponent target, String typedText,
905: boolean visible) {
906: int retVal = COMPLETION_CANCEL;
907: int dotPos = target.getCaret().getDot();
908: BaseDocument doc = (BaseDocument) target.getDocument();
909: switch (typedText.charAt(typedText.length() - 1)) {
910: case '/':
911: if (dotPos >= 2) { // last char before inserted slash
912: try {
913: String txtBeforeSpace = doc.getText(dotPos - 2, 2);
914: if (txtBeforeSpace.equals("</")) // NOI18N
915: return COMPLETION_POPUP;
916: } catch (BadLocationException e) {
917: }
918: }
919: break;
920: case ' ':
921: doc.readLock();
922: try {
923: TokenHierarchy hi = TokenHierarchy.get(doc);
924: TokenSequence ts = tokenSequence(hi, dotPos - 1);
925: if (ts == null) {
926: //no suitable token sequence found
927: return COMPLETION_POST_REFRESH;
928: }
929:
930: ts.move(dotPos - 1);
931: if (ts.moveNext() || ts.movePrevious()) {
932: if (ts.token().id() == HTMLTokenId.WS) {
933: return COMPLETION_POPUP;
934: }
935: }
936: } finally {
937: doc.readUnlock();
938: }
939: break;
940: case '<':
941: case '&':
942: return COMPLETION_POPUP;
943: case ';':
944: return COMPLETION_HIDE;
945: case '>':
946: try {
947: //check if the cursor is behind an open tag
948: SyntaxElement se = getElementChain(dotPos - 1);
949: if (se != null
950: && se.getType() == SyntaxElement.TYPE_TAG) {
951: return COMPLETION_POPUP;
952: }
953: } catch (BadLocationException e) {
954: //do nothing
955: }
956: return COMPLETION_HIDE;
957:
958: }
959: return COMPLETION_POST_REFRESH;
960:
961: }
962:
963: }
|