| 776 |
lars |
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 |
});
|