001: /*
002: * HtmlMode.java
003: *
004: * Copyright (C) 1998-2004 Peter Graves
005: * $Id: HtmlMode.java,v 1.2 2004/04/22 14:58:09 piso Exp $
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License
009: * as published by the Free Software Foundation; either version 2
010: * of the License, or (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
020: */
021:
022: package org.armedbear.j;
023:
024: import gnu.regexp.RE;
025: import gnu.regexp.REException;
026: import gnu.regexp.REMatch;
027: import java.awt.event.KeyEvent;
028: import java.io.BufferedReader;
029: import java.io.FileInputStream;
030: import java.io.FileNotFoundException;
031: import java.io.IOException;
032: import java.io.InputStream;
033: import java.io.InputStreamReader;
034: import java.util.List;
035: import javax.swing.undo.CompoundEdit;
036:
037: public final class HtmlMode extends AbstractMode implements Constants,
038: Mode {
039: private static final Mode mode = new HtmlMode();
040: private static List elements;
041: private static RE tagNameRE;
042: private static RE attributeNameRE;
043: private static RE quotedValueRE;
044: private static RE unquotedValueRE;
045:
046: private HtmlMode() {
047: super (HTML_MODE, HTML_MODE_NAME);
048: // Support embedded JavaScript.
049: keywords = new Keywords(JavaScriptMode.getMode());
050: }
051:
052: public static final Mode getMode() {
053: return mode;
054: }
055:
056: public Formatter getFormatter(Buffer buffer) {
057: return new HtmlFormatter(buffer);
058: }
059:
060: protected void setKeyMapDefaults(KeyMap km) {
061: km.mapKey(KeyEvent.VK_TAB, 0, "tab");
062: km.mapKey(KeyEvent.VK_TAB, CTRL_MASK, "insertTab");
063: km.mapKey(KeyEvent.VK_ENTER, 0, "newlineAndIndent");
064: km.mapKey(KeyEvent.VK_ENTER, CTRL_MASK, "newline");
065: km.mapKey(KeyEvent.VK_M, CTRL_MASK, "htmlFindMatch");
066: km.mapKey(KeyEvent.VK_E, CTRL_MASK, "htmlInsertMatchingEndTag");
067: km.mapKey(KeyEvent.VK_B, CTRL_MASK, "htmlBold");
068: km.mapKey('=', "htmlElectricEquals");
069: km.mapKey('>', "electricCloseAngleBracket");
070: km.mapKey(KeyEvent.VK_V, CTRL_MASK | ALT_MASK, "viewPage");
071: km.mapKey(KeyEvent.VK_I, ALT_MASK, "cycleIndentSize");
072:
073: // These are the "normal" mappings.
074: km.mapKey(KeyEvent.VK_COMMA, CTRL_MASK | SHIFT_MASK,
075: "htmlInsertTag");
076: km.mapKey(KeyEvent.VK_PERIOD, CTRL_MASK | SHIFT_MASK,
077: "htmlEndTag");
078:
079: // The "normal" mappings don't work for Linux, but these do.
080: km.mapKey(0x7c, CTRL_MASK | SHIFT_MASK, "htmlInsertTag");
081: km.mapKey(0x7e, CTRL_MASK | SHIFT_MASK, "htmlEndTag");
082: }
083:
084: public boolean canIndent() {
085: return true;
086: }
087:
088: public boolean canIndentPaste() {
089: return false;
090: }
091:
092: public int getCorrectIndentation(Line line, Buffer buffer) {
093: if (line.flags() == STATE_SCRIPT)
094: return JavaScriptMode.getMode().getCorrectIndentation(line,
095: buffer);
096: // Ignore comments.
097: if (line.flags() == STATE_HTML_COMMENT)
098: return buffer.getIndentation(line); // Unchanged.
099: if (line.trim().startsWith("<!--"))
100: return buffer.getIndentation(line); // Unchanged.
101: Line model = getModel(line);
102: if (model == null)
103: return 0;
104: int indent = buffer.getIndentation(model);
105: if (line.trim().startsWith("</")) {
106: Position pos = findMatchingStartTag(line);
107: if (pos != null)
108: return buffer.getIndentation(pos.getLine());
109: indent -= buffer.getIndentSize();
110: if (indent < 0)
111: indent = 0;
112: return indent;
113: }
114: final String trim = model.trim();
115: if (trim.startsWith("<") && !trim.startsWith("</")) {
116: if (trim.startsWith("<!")) // Document type declaration.
117: return indent;
118: // Model starts with start tag.
119: String name = Utilities.getTagName(trim).toLowerCase();
120: if (name.equals("html") || name.equals("head")
121: || name.equals("body") || name.equals("form"))
122: return indent;
123: boolean wantsEndTag = wantsEndTag(name);
124: if (wantsEndTag) {
125: String startTag = "<" + name;
126: String endTag = "</" + name;
127: int count = 1;
128: int limit = trim.length();
129: for (int i = startTag.length(); i < limit; i++) {
130: if (trim.charAt(i) == '<') {
131: if (lookingAtIgnoreCase(trim, i, endTag))
132: --count;
133: else if (lookingAtIgnoreCase(trim, i, startTag))
134: ++count;
135: }
136: }
137: if (count > 0)
138: indent += buffer.getIndentSize();
139: }
140: }
141: return indent;
142: }
143:
144: // Line must start with an end tag.
145: private Position findMatchingStartTag(Line line) {
146: String s = line.trim();
147: if (!s.startsWith("</"))
148: return null;
149: FastStringBuffer sb = new FastStringBuffer();
150: for (int i = 2; i < s.length(); i++) {
151: char c = s.charAt(i);
152: if (c <= ' ')
153: break;
154: if (c == '>')
155: break;
156: sb.append(c);
157: }
158: String name = sb.toString();
159: String toBeMatched = "</" + name + ">";
160: String match = "<" + name;
161: int count = 1;
162: boolean foundIt = false;
163: Position pos = new Position(line, 0);
164: final String commentStart = "<!--";
165: final String commentEnd = "-->";
166: // Search backward.
167: while (!pos.atStart()) {
168: pos.prev();
169: if (pos.lookingAt(commentEnd)) {
170: do {
171: pos.prev();
172: } while (!pos.atStart() && !pos.lookingAt(commentStart));
173: } else if (pos.lookingAtIgnoreCase(toBeMatched)) {
174: ++count;
175: } else if (pos.lookingAtIgnoreCase(match)) {
176: if (pos.lookingAtIgnoreCase(match + ">"))
177: --count;
178: else if (pos.lookingAtIgnoreCase(match + " "))
179: --count;
180: else if (pos.lookingAtIgnoreCase(match + "\t"))
181: --count;
182: if (count == 0) {
183: foundIt = true;
184: break;
185: }
186: }
187: }
188: if (foundIt)
189: return pos;
190: // Not found.
191: return null;
192: }
193:
194: private static final boolean lookingAtIgnoreCase(String s, int i,
195: String pattern) {
196: return s.regionMatches(true, i, pattern, 0, pattern.length());
197: }
198:
199: private static Line getModel(Line line) {
200: Line model = line;
201: while ((model = model.previous()) != null) {
202: if (model.flags() == STATE_HTML_COMMENT)
203: continue;
204: if (model.trim().startsWith("<!--"))
205: continue;
206: if (model.isBlank())
207: continue;
208: break;
209: }
210: return model;
211: }
212:
213: public char fixCase(Editor editor, char c) {
214: if (!editor.getBuffer().getBooleanProperty(Property.FIX_CASE))
215: return c;
216: if (!initRegExps())
217: return c;
218: Position pos = findStartOfTag(editor.getDot());
219: if (pos != null) {
220: int index = pos.getOffset();
221: String text = pos.getLine().getText();
222: REMatch match = tagNameRE.getMatch(text, index);
223: if (match == null)
224: return c;
225: if (match.getEndIndex() >= editor.getDotOffset()) {
226: // Tag name.
227: if (editor.getBuffer().getBooleanProperty(
228: Property.UPPER_CASE_TAG_NAMES))
229: return Character.toUpperCase(c);
230: else
231: return Character.toLowerCase(c);
232: }
233: while (true) {
234: index = match.getEndIndex();
235: match = attributeNameRE.getMatch(text, index);
236: if (match == null)
237: return c;
238: if (match.getEndIndex() >= editor.getDotOffset()) {
239: // Attribute name.
240: if (editor.getBuffer().getBooleanProperty(
241: Property.UPPER_CASE_ATTRIBUTE_NAMES))
242: return Character.toUpperCase(c);
243: else
244: return Character.toLowerCase(c);
245: }
246: index = match.getEndIndex();
247: match = quotedValueRE.getMatch(text, index);
248: if (match == null) {
249: match = unquotedValueRE.getMatch(text, index);
250: if (match == null)
251: return c;
252: }
253: if (match.getEndIndex() >= editor.getDotOffset()) {
254: // Attribute value.
255: return c;
256: }
257: }
258: }
259: return c;
260: }
261:
262: private static boolean checkElectricEquals(Editor editor) {
263: Position pos = findStartOfTag(editor.getDot());
264: if (pos == null)
265: return false;
266: char c = editor.getDotChar();
267: if (c == '>' || c == '/' || Character.isWhitespace(c))
268: return true;
269: return false;
270: }
271:
272: private static Position findStartOfTag(Position pos) {
273: int offset = pos.getOffset();
274: String text = pos.getLine().getText();
275: while (--offset >= 0) {
276: char c = text.charAt(offset);
277: if (c == '>')
278: return null;
279: if (c == '<')
280: return new Position(pos.getLine(), offset);
281: }
282: return null;
283: }
284:
285: public static List elements() {
286: if (elements == null)
287: loadElementList();
288: return elements;
289: }
290:
291: private static boolean wantsEndTag(String elementName) {
292: elementName = elementName.trim().toLowerCase();
293: if (elements == null)
294: loadElementList();
295: if (elements != null) {
296: final int limit = elements.size();
297: for (int i = 0; i < limit; i++) {
298: HtmlElement element = (HtmlElement) elements.get(i);
299: if (element.getName().equals(elementName))
300: return element.wantsEndTag();
301: }
302: }
303: return true; // Default.
304: }
305:
306: private static void loadElementList() {
307: elements = HtmlElement.getDefaultElements();
308: String filename = Editor.preferences().getStringProperty(
309: Property.HTML_MODE_TAGS);
310: if (filename != null && filename.length() > 0) {
311: try {
312: FileInputStream istream = new FileInputStream(filename);
313: loadElementsFromStream(istream);
314: } catch (FileNotFoundException e) {
315: Log.error(e);
316: }
317: }
318: }
319:
320: private static void loadElementsFromStream(InputStream istream) {
321: Debug.assertTrue(elements != null);
322: if (istream != null) {
323: try {
324: BufferedReader in = new BufferedReader(
325: new InputStreamReader(istream));
326: while (true) {
327: String s = in.readLine();
328: if (s == null)
329: break; // Reached end of file.
330: s = s.trim();
331: // Ignore blank lines.
332: if (s.trim().length() == 0)
333: continue;
334: // Ignore comment lines.
335: if (s.charAt(0) == '#')
336: continue;
337: int index = s.indexOf('=');
338: if (index >= 0) {
339: // Element names are always stored in lower case.
340: String name = s.substring(0, index).trim()
341: .toLowerCase();
342: String value = s.substring(index + 1).trim();
343: boolean wantsEndTag = value.equals("1")
344: || value.equals("true");
345: boolean found = false;
346: for (int i = 0; i < elements.size(); i++) {
347: HtmlElement element = (HtmlElement) elements
348: .get(i);
349: if (element.getName().equals(name)) {
350: element.setWantsEndTag(wantsEndTag);
351: found = true;
352: break;
353: }
354: }
355: if (!found)
356: elements.add(new HtmlElement(name,
357: wantsEndTag));
358: }
359: }
360: } catch (IOException e) {
361: Log.error(e);
362: }
363: }
364: }
365:
366: private static boolean initRegExps() {
367: if (tagNameRE == null) {
368: try {
369: tagNameRE = new RE("</?[A-Za-z0-9]*");
370: attributeNameRE = new RE("\\s+[A-Za-z0-9]*");
371: quotedValueRE = new RE("\\s*=\\s*\"[^\"]*");
372: unquotedValueRE = new RE("\\s*=\\s*\\S*");
373: } catch (REException e) {
374: tagNameRE = null;
375: return false;
376: }
377: }
378: return true;
379: }
380:
381: public static void htmlStartTag() {
382: htmlTag(Editor.currentEditor(), false);
383: }
384:
385: public static void htmlEndTag() {
386: htmlTag(Editor.currentEditor(), true);
387: }
388:
389: private static void htmlTag(Editor editor, boolean isEndTag) {
390: if (!editor.checkReadOnly())
391: return;
392: CompoundEdit compoundEdit = editor.beginCompoundEdit();
393: editor.insertChar('<');
394: if (isEndTag)
395: editor.insertChar('/');
396: editor.insertChar('>');
397: editor.addUndo(SimpleEdit.MOVE);
398: editor.getDot().moveLeft();
399: editor.moveCaretToDotCol();
400: editor.endCompoundEdit(compoundEdit);
401: }
402:
403: public static void htmlInsertTag() {
404: final Editor editor = Editor.currentEditor();
405: if (!editor.checkReadOnly())
406: return;
407: InsertTagDialog d = new InsertTagDialog(editor);
408: editor.centerDialog(d);
409: d.show();
410: _htmlInsertTag(editor, d.getInput());
411: }
412:
413: public static void htmlInsertTag(String input) {
414: final Editor editor = Editor.currentEditor();
415: if (!editor.checkReadOnly())
416: return;
417: _htmlInsertTag(editor, input);
418: }
419:
420: private static void _htmlInsertTag(Editor editor, String input) {
421: if (input != null && input.length() > 0) {
422: final String tagName, extra;
423: int index = input.indexOf(' ');
424: if (index >= 0) {
425: tagName = input.substring(0, index);
426: extra = input.substring(index);
427: } else {
428: tagName = input;
429: extra = "";
430: }
431: InsertTagDialog.insertTag(editor, tagName, extra,
432: wantsEndTag(tagName));
433: }
434: }
435:
436: public static void htmlInsertMatchingEndTag() {
437: final Editor editor = Editor.currentEditor();
438: if (!editor.checkReadOnly())
439: return;
440: Position pos = editor.getDotCopy();
441: while (pos.prev()) {
442: // If we find an end tag, we've got nothing to match.
443: if (pos.lookingAt("</"))
444: return;
445:
446: if (pos.getChar() == '<') {
447: if (pos.next()) {
448: FastStringBuffer sb = new FastStringBuffer();
449: char c;
450: while (!Character.isWhitespace(c = pos.getChar())
451: && c != '>') {
452: sb.append(c);
453: if (!pos.next())
454: return;
455: }
456: if (sb.length() == 0)
457: return;
458: final String endTag = "</" + sb.toString() + ">";
459: final Buffer buffer = editor.getBuffer();
460: try {
461: buffer.lockWrite();
462: } catch (InterruptedException e) {
463: Log.error(e);
464: return;
465: }
466: try {
467: CompoundEdit compoundEdit = editor
468: .beginCompoundEdit();
469: editor.fillToCaret();
470: editor.addUndo(SimpleEdit.INSERT_STRING);
471: editor.insertStringInternal(endTag);
472: buffer.modified();
473: editor.addUndo(SimpleEdit.MOVE);
474: editor.moveCaretToDotCol();
475: if (buffer
476: .getBooleanProperty(Property.AUTO_INDENT))
477: editor.indentLine();
478: editor.endCompoundEdit(compoundEdit);
479: } finally {
480: buffer.unlockWrite();
481: }
482: }
483: return;
484: }
485: }
486: }
487:
488: public static void htmlBold() {
489: final Editor editor = Editor.currentEditor();
490: final Buffer buffer = editor.getBuffer();
491: if (!editor.checkReadOnly())
492: return;
493: CompoundEdit compoundEdit = editor.beginCompoundEdit();
494: if (editor.getMark() == null)
495: editor.fillToCaret();
496: boolean upper = buffer
497: .getBooleanProperty(Property.UPPER_CASE_TAG_NAMES);
498: InsertTagDialog.insertTag(editor, upper ? "B" : "b", "", true);
499: editor.endCompoundEdit(compoundEdit);
500: }
501:
502: public static void htmlElectricEquals() {
503: final Editor editor = Editor.currentEditor();
504: if (!editor.checkReadOnly())
505: return;
506: boolean ok = false;
507: if (editor.getModeId() == HTML_MODE) {
508: if (editor.getBuffer().getBooleanProperty(
509: Property.ATTRIBUTES_REQUIRE_QUOTES))
510: if (checkElectricEquals(editor))
511: ok = true;
512: }
513: if (ok) {
514: CompoundEdit compoundEdit = editor.beginCompoundEdit();
515: editor.fillToCaret();
516: editor.addUndo(SimpleEdit.INSERT_STRING);
517: editor.insertStringInternal("=\"\"");
518: editor.addUndo(SimpleEdit.MOVE);
519: editor.getDot().moveLeft();
520: editor.moveCaretToDotCol();
521: editor.endCompoundEdit(compoundEdit);
522: } else
523: editor.insertNormalChar('=');
524: }
525:
526: public static void htmlFindMatch() {
527: final Editor editor = Editor.currentEditor();
528: final String special = "{([})]";
529: final String commentStart = "<!--";
530: final String commentEnd = "-->";
531: Position dot = editor.getDot();
532: char c = dot.getChar();
533: if (special.indexOf(c) >= 0) {
534: editor.findMatchingChar();
535: return;
536: }
537: Position saved = dot.copy();
538: while ((c = dot.getChar()) > ' ' && c != '<'
539: && dot.getOffset() > 0) {
540: if (dot.lookingAt(commentEnd))
541: break;
542: dot.prev();
543: }
544: if (c <= ' ')
545: dot.next();
546: Position start = dot.copy();
547: FastStringBuffer sb = new FastStringBuffer(dot.getChar());
548: dot.next();
549: while ((c = dot.getChar()) > ' ' && c != '>') {
550: sb.append(c);
551: if (!dot.next()) {
552: editor.status("Nothing to match");
553: dot.moveTo(saved);
554: return;
555: }
556: }
557: if (c == '>')
558: sb.append(c);
559: String toBeMatched = sb.toString();
560: String match = null;
561: boolean searchForward = true;
562: if (toBeMatched.equals(commentStart))
563: match = commentEnd;
564: else if (toBeMatched.equals(commentEnd)) {
565: match = commentStart;
566: searchForward = false;
567: } else if (toBeMatched.startsWith("</")) {
568: match = "<".concat(toBeMatched.substring(2));
569: if (match.endsWith(">"))
570: match = match.substring(0, match.length() - 1);
571: searchForward = false;
572: } else if (toBeMatched.startsWith("<")) {
573: if (toBeMatched.endsWith(">"))
574: toBeMatched = toBeMatched.substring(0, toBeMatched
575: .length() - 1);
576: match = "</" + toBeMatched.substring(1);
577: if (!match.endsWith(">"))
578: match += '>';
579: } else {
580: editor.status("Nothing to match");
581: dot.moveTo(saved);
582: return;
583: }
584: editor.setWaitCursor();
585: int count = 1;
586: boolean succeeded = false;
587: dot.moveTo(start);
588: if (searchForward) {
589: dot.skip(toBeMatched.length());
590: if (toBeMatched.equals(commentStart)) {
591: while (!dot.atEnd()) {
592: if (dot.lookingAt(commentEnd)) {
593: succeeded = true;
594: break;
595: }
596: dot.next();
597: }
598: } else {
599: // Find matching end tag.
600: while (!dot.atEnd()) {
601: if (dot.lookingAt(commentStart)) {
602: dot.skip(commentStart.length());
603: while (!dot.atEnd()) {
604: if (dot.lookingAt(commentEnd)) {
605: dot.skip(commentEnd.length());
606: break;
607: }
608: dot.next();
609: }
610: } else if (dot.lookingAtIgnoreCase(toBeMatched)) {
611: dot.skip(toBeMatched.length());
612: c = dot.getChar();
613: if (c <= ' ' || c == '>') {
614: ++count;
615: dot.next();
616: }
617: } else if (dot.lookingAtIgnoreCase(match)) {
618: --count;
619: if (count == 0) {
620: succeeded = true;
621: break;
622: }
623: dot.skip(match.length());
624: } else
625: dot.next();
626: }
627: }
628: } else {
629: // Search backward.
630: while (!dot.atStart()) {
631: dot.prev();
632: if (dot.lookingAt(commentEnd)) {
633: do {
634: dot.prev();
635: } while (!dot.atStart()
636: && !dot.lookingAt(commentStart));
637: } else if (dot.lookingAtIgnoreCase(toBeMatched)) {
638: ++count;
639: } else if (dot.lookingAtIgnoreCase(match)) {
640: if (dot.lookingAtIgnoreCase(match + ">"))
641: --count;
642: else if (dot.lookingAtIgnoreCase(match + " "))
643: --count;
644: else if (dot.lookingAtIgnoreCase(match + "\t"))
645: --count;
646: if (count == 0) {
647: succeeded = true;
648: break;
649: }
650: }
651: }
652: }
653: if (succeeded) {
654: Position matchPos = dot.copy();
655: dot.moveTo(saved);
656: editor.updateDotLine();
657: editor.addUndo(SimpleEdit.MOVE);
658: dot.moveTo(matchPos);
659: editor.updateDotLine();
660: editor.moveCaretToDotCol();
661: } else {
662: dot.moveTo(saved);
663: editor.status("No match");
664: }
665: editor.setDefaultCursor();
666: }
667: }
|