closetag.js (7705B)
1 // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 // Distributed under an MIT license: http://codemirror.net/LICENSE 3 4 /** 5 * Tag-closer extension for CodeMirror. 6 * 7 * This extension adds an "autoCloseTags" option that can be set to 8 * either true to get the default behavior, or an object to further 9 * configure its behavior. 10 * 11 * These are supported options: 12 * 13 * `whenClosing` (default true) 14 * Whether to autoclose when the '/' of a closing tag is typed. 15 * `whenOpening` (default true) 16 * Whether to autoclose the tag when the final '>' of an opening 17 * tag is typed. 18 * `dontCloseTags` (default is empty tags for HTML, none for XML) 19 * An array of tag names that should not be autoclosed. 20 * `indentTags` (default is block tags for HTML, none for XML) 21 * An array of tag names that should, when opened, cause a 22 * blank line to be added inside the tag, and the blank line and 23 * closing line to be indented. 24 * 25 * See demos/closetag.html for a usage example. 26 */ 27 28 (function(mod) { 29 if (typeof exports == "object" && typeof module == "object") // CommonJS 30 mod(require("../../lib/codemirror"), require("../fold/xml-fold")); 31 else if (typeof define == "function" && define.amd) // AMD 32 define(["../../lib/codemirror", "../fold/xml-fold"], mod); 33 else // Plain browser env 34 mod(CodeMirror); 35 })(function(CodeMirror) { 36 CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) { 37 if (old != CodeMirror.Init && old) 38 cm.removeKeyMap("autoCloseTags"); 39 if (!val) return; 40 var map = {name: "autoCloseTags"}; 41 if (typeof val != "object" || val.whenClosing) 42 map["'/'"] = function(cm) { return autoCloseSlash(cm); }; 43 if (typeof val != "object" || val.whenOpening) 44 map["'>'"] = function(cm) { return autoCloseGT(cm); }; 45 cm.addKeyMap(map); 46 }); 47 48 var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", 49 "source", "track", "wbr"]; 50 var htmlIndent = ["applet", "blockquote", "body", "button", "div", "dl", "fieldset", "form", "frameset", "h1", "h2", "h3", "h4", 51 "h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"]; 52 53 function autoCloseGT(cm) { 54 if (cm.getOption("disableInput")) return CodeMirror.Pass; 55 var ranges = cm.listSelections(), replacements = []; 56 for (var i = 0; i < ranges.length; i++) { 57 if (!ranges[i].empty()) return CodeMirror.Pass; 58 var pos = ranges[i].head, tok = cm.getTokenAt(pos); 59 var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state; 60 if (inner.mode.name != "xml" || !state.tagName) return CodeMirror.Pass; 61 62 var opt = cm.getOption("autoCloseTags"), html = inner.mode.configuration == "html"; 63 var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose); 64 var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent); 65 66 var tagName = state.tagName; 67 if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch); 68 var lowerTagName = tagName.toLowerCase(); 69 // Don't process the '>' at the end of an end-tag or self-closing tag 70 if (!tagName || 71 tok.type == "string" && (tok.end != pos.ch || !/[\"\']/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1) || 72 tok.type == "tag" && state.type == "closeTag" || 73 tok.string.indexOf("/") == (tok.string.length - 1) || // match something like <someTagName /> 74 dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 || 75 closingTagExists(cm, tagName, pos, state, true)) 76 return CodeMirror.Pass; 77 78 var indent = indentTags && indexOf(indentTags, lowerTagName) > -1; 79 replacements[i] = {indent: indent, 80 text: ">" + (indent ? "\n\n" : "") + "</" + tagName + ">", 81 newPos: indent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1)}; 82 } 83 84 for (var i = ranges.length - 1; i >= 0; i--) { 85 var info = replacements[i]; 86 cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, "+insert"); 87 var sel = cm.listSelections().slice(0); 88 sel[i] = {head: info.newPos, anchor: info.newPos}; 89 cm.setSelections(sel); 90 if (info.indent) { 91 cm.indentLine(info.newPos.line, null, true); 92 cm.indentLine(info.newPos.line + 1, null, true); 93 } 94 } 95 } 96 97 function autoCloseCurrent(cm, typingSlash) { 98 var ranges = cm.listSelections(), replacements = []; 99 var head = typingSlash ? "/" : "</"; 100 for (var i = 0; i < ranges.length; i++) { 101 if (!ranges[i].empty()) return CodeMirror.Pass; 102 var pos = ranges[i].head, tok = cm.getTokenAt(pos); 103 var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state; 104 if (typingSlash && (tok.type == "string" || tok.string.charAt(0) != "<" || 105 tok.start != pos.ch - 1)) 106 return CodeMirror.Pass; 107 // Kludge to get around the fact that we are not in XML mode 108 // when completing in JS/CSS snippet in htmlmixed mode. Does not 109 // work for other XML embedded languages (there is no general 110 // way to go from a mixed mode to its current XML state). 111 var replacement; 112 if (inner.mode.name != "xml") { 113 if (cm.getMode().name == "htmlmixed" && inner.mode.name == "javascript") 114 replacement = head + "script"; 115 else if (cm.getMode().name == "htmlmixed" && inner.mode.name == "css") 116 replacement = head + "style"; 117 else 118 return CodeMirror.Pass; 119 } else { 120 if (!state.context || !state.context.tagName || 121 closingTagExists(cm, state.context.tagName, pos, state)) 122 return CodeMirror.Pass; 123 replacement = head + state.context.tagName; 124 } 125 if (cm.getLine(pos.line).charAt(tok.end) != ">") replacement += ">"; 126 replacements[i] = replacement; 127 } 128 cm.replaceSelections(replacements); 129 ranges = cm.listSelections(); 130 for (var i = 0; i < ranges.length; i++) 131 if (i == ranges.length - 1 || ranges[i].head.line < ranges[i + 1].head.line) 132 cm.indentLine(ranges[i].head.line); 133 } 134 135 function autoCloseSlash(cm) { 136 if (cm.getOption("disableInput")) return CodeMirror.Pass; 137 return autoCloseCurrent(cm, true); 138 } 139 140 CodeMirror.commands.closeTag = function(cm) { return autoCloseCurrent(cm); }; 141 142 function indexOf(collection, elt) { 143 if (collection.indexOf) return collection.indexOf(elt); 144 for (var i = 0, e = collection.length; i < e; ++i) 145 if (collection[i] == elt) return i; 146 return -1; 147 } 148 149 // If xml-fold is loaded, we use its functionality to try and verify 150 // whether a given tag is actually unclosed. 151 function closingTagExists(cm, tagName, pos, state, newTag) { 152 if (!CodeMirror.scanForClosingTag) return false; 153 var end = Math.min(cm.lastLine() + 1, pos.line + 500); 154 var nextClose = CodeMirror.scanForClosingTag(cm, pos, null, end); 155 if (!nextClose || nextClose.tag != tagName) return false; 156 var cx = state.context; 157 // If the immediate wrapping context contains onCx instances of 158 // the same tag, a closing tag only exists if there are at least 159 // that many closing tags of that type following. 160 for (var onCx = newTag ? 1 : 0; cx && cx.tagName == tagName; cx = cx.prev) ++onCx; 161 pos = nextClose.to; 162 for (var i = 1; i < onCx; i++) { 163 var next = CodeMirror.scanForClosingTag(cm, pos, null, end); 164 if (!next || next.tag != tagName) return false; 165 pos = next.to; 166 } 167 return true; 168 } 169 });