2
* Tag-closer extension for CodeMirror.
4
* This extension adds a "closeTag" utility function that can be used with key bindings to
5
* insert a matching end tag after the ">" character of a start tag has been typed. It can
6
* also complete "</" if a matching start tag is found. It will correctly ignore signal
7
* characters for empty tags, comments, CDATA, etc.
9
* The function depends on internal parser state to identify tags. It is compatible with the
10
* following CodeMirror modes and will ignore all others:
14
* See demos/closetag.html for a usage example.
16
* @author Nathan Williams <nathan@nlwillia.net>
17
* Contributed under the same license terms as CodeMirror.
20
/** Option that allows tag closing behavior to be toggled. Default is true. */
21
CodeMirror.defaults['closeTagEnabled'] = true;
23
/** Array of tag names to add indentation after the start tag for. Default is the list of block-level html tags. */
24
CodeMirror.defaults['closeTagIndent'] = ['applet', 'blockquote', 'body', 'button', 'div', 'dl', 'fieldset', 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'html', 'iframe', 'layer', 'legend', 'object', 'ol', 'p', 'select', 'table', 'ul'];
26
/** Array of tag names where an end tag is forbidden. */
27
CodeMirror.defaults['closeTagVoid'] = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
29
function innerState(cm, state) {
30
return CodeMirror.innerMode(cm.getMode(), state).state;
35
* Call during key processing to close tags. Handles the key event if the tag is closed, otherwise throws CodeMirror.Pass.
36
* - cm: The editor instance.
37
* - ch: The character being processed.
38
* - indent: Optional. An array of tag names to indent when closing. Omit or pass true to use the default indentation tag list defined in the 'closeTagIndent' option.
39
* Pass false to disable indentation. Pass an array to override the default list of tag names.
40
* - vd: Optional. An array of tag names that should not be closed. Omit to use the default void (end tag forbidden) tag list defined in the 'closeTagVoid' option. Ignored in xml mode.
42
CodeMirror.defineExtension("closeTag", function(cm, ch, indent, vd) {
43
if (!cm.getOption('closeTagEnabled')) {
44
throw CodeMirror.Pass;
48
* Relevant structure of token:
67
var pos = cm.getCursor();
68
var tok = cm.getTokenAt(pos);
69
var state = innerState(cm, tok.state);
74
var type = state.type;
76
if (tok.className == 'tag' && type == 'closeTag') {
77
throw CodeMirror.Pass; // Don't process the '>' at the end of an end-tag.
80
cm.replaceSelection('>'); // Mode state won't update until we finish the tag.
81
pos = {line: pos.line, ch: pos.ch + 1};
84
tok = cm.getTokenAt(cm.getCursor());
85
state = innerState(cm, tok.state);
86
if (!state) throw CodeMirror.Pass;
87
var type = state.type;
89
if (tok.className == 'tag' && type != 'selfcloseTag') {
90
var tagName = state.tagName;
91
if (tagName.length > 0 && shouldClose(cm, vd, tagName)) {
92
insertEndTag(cm, indent, pos, tagName);
97
// Undo the '>' insert and allow cm to handle the key instead.
98
cm.setSelection({line: pos.line, ch: pos.ch - 1}, pos);
99
cm.replaceSelection("");
101
} else if (ch == '/') {
102
if (tok.className == 'tag' && tok.string == '<') {
103
var ctx = state.context, tagName = ctx ? ctx.tagName : '';
104
if (tagName.length > 0) {
105
completeEndTag(cm, pos, tagName);
113
throw CodeMirror.Pass; // Bubble if not handled
116
function insertEndTag(cm, indent, pos, tagName) {
117
if (shouldIndent(cm, indent, tagName)) {
118
cm.replaceSelection('\n\n</' + tagName + '>', 'end');
119
cm.indentLine(pos.line + 1);
120
cm.indentLine(pos.line + 2);
121
cm.setCursor({line: pos.line + 1, ch: cm.getLine(pos.line + 1).length});
123
cm.replaceSelection('</' + tagName + '>');
128
function shouldIndent(cm, indent, tagName) {
129
if (typeof indent == 'undefined' || indent == null || indent == true) {
130
indent = cm.getOption('closeTagIndent');
135
return indexOf(indent, tagName.toLowerCase()) != -1;
138
function shouldClose(cm, vd, tagName) {
139
if (cm.getOption('mode') == 'xml') {
140
return true; // always close xml tags
142
if (typeof vd == 'undefined' || vd == null) {
143
vd = cm.getOption('closeTagVoid');
148
return indexOf(vd, tagName.toLowerCase()) == -1;
151
// C&P from codemirror.js...would be nice if this were visible to utilities.
152
function indexOf(collection, elt) {
153
if (collection.indexOf) return collection.indexOf(elt);
154
for (var i = 0, e = collection.length; i < e; ++i)
155
if (collection[i] == elt) return i;
159
function completeEndTag(cm, pos, tagName) {
160
cm.replaceSelection('/' + tagName + '>');
161
cm.setCursor({line: pos.line, ch: pos.ch + tagName.length + 2 });