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:
15
* See demos/closetag.html for a usage example.
17
* @author Nathan Williams <nathan@nlwillia.net>
18
* Contributed under the same license terms as CodeMirror.
21
/** Option that allows tag closing behavior to be toggled. Default is true. */
22
CodeMirror.defaults['closeTagEnabled'] = true;
24
/** Array of tag names to add indentation after the start tag for. Default is the list of block-level html tags. */
25
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'];
28
* Call during key processing to close tags. Handles the key event if the tag is closed, otherwise throws CodeMirror.Pass.
29
* - cm: The editor instance.
30
* - ch: The character being processed.
31
* - indent: Optional. Omit or pass true to use the default indentation tag list defined in the 'closeTagIndent' option.
32
* Pass false to disable indentation. Pass an array to override the default list of tag names.
34
CodeMirror.defineExtension("closeTag", function(cm, ch, indent) {
35
if (!cm.getOption('closeTagEnabled')) {
36
throw CodeMirror.Pass;
39
var mode = cm.getOption('mode');
41
if (mode == 'text/html') {
44
* Relevant structure of token:
62
var pos = cm.getCursor();
63
var tok = cm.getTokenAt(pos);
64
var state = tok.state;
66
if (state.mode && state.mode != 'html') {
67
throw CodeMirror.Pass; // With htmlmixed, we only care about the html sub-mode.
71
var type = state.htmlState ? state.htmlState.type : state.type; // htmlmixed : xml
73
if (tok.className == 'tag' && type == 'closeTag') {
74
throw CodeMirror.Pass; // Don't process the '>' at the end of an end-tag.
77
cm.replaceSelection('>'); // Mode state won't update until we finish the tag.
78
pos = {line: pos.line, ch: pos.ch + 1};
81
tok = cm.getTokenAt(cm.getCursor());
83
type = state.htmlState ? state.htmlState.type : state.type; // htmlmixed : xml
85
if (tok.className == 'tag' && type != 'selfcloseTag') {
86
var tagName = state.htmlState ? state.htmlState.context.tagName : state.tagName; // htmlmixed : xml
87
if (tagName.length > 0) {
88
insertEndTag(cm, indent, pos, tagName);
93
// Undo the '>' insert and allow cm to handle the key instead.
94
cm.setSelection({line: pos.line, ch: pos.ch - 1}, pos);
95
cm.replaceSelection("");
97
} else if (ch == '/') {
98
if (tok.className == 'tag' && tok.string == '<') {
99
var tagName = state.htmlState ? (state.htmlState.context ? state.htmlState.context.tagName : '') : state.context.tagName; // htmlmixed : xml # extra htmlmized check is for '</' edge case
100
if (tagName.length > 0) {
101
completeEndTag(cm, pos, tagName);
107
} else if (mode == 'xmlpure') {
109
var pos = cm.getCursor();
110
var tok = cm.getTokenAt(pos);
111
var tagName = tok.state.context.tagName;
114
// <foo> tagName=foo, string=foo
115
// <foo /> tagName=foo, string=/ # ignore
116
// <foo></foo> tagName=foo, string=/foo # ignore
117
if (tok.string == tagName) {
118
cm.replaceSelection('>'); // parity w/html modes
119
pos = {line: pos.line, ch: pos.ch + 1};
122
insertEndTag(cm, indent, pos, tagName);
126
} else if (ch == '/') {
127
// <foo / tagName=foo, string= # ignore
128
// <foo></ tagName=foo, string=<
129
if (tok.string == '<') {
130
completeEndTag(cm, pos, tagName);
136
throw CodeMirror.Pass; // Bubble if not handled
139
function insertEndTag(cm, indent, pos, tagName) {
140
if (shouldIndent(cm, indent, tagName)) {
141
cm.replaceSelection('\n\n</' + tagName + '>', 'end');
142
cm.indentLine(pos.line + 1);
143
cm.indentLine(pos.line + 2);
144
cm.setCursor({line: pos.line + 1, ch: cm.getLine(pos.line + 1).length});
146
cm.replaceSelection('</' + tagName + '>');
151
function shouldIndent(cm, indent, tagName) {
152
if (typeof indent == 'undefined' || indent == null || indent == true) {
153
indent = cm.getOption('closeTagIndent');
158
return indexOf(indent, tagName.toLowerCase()) != -1;
161
// C&P from codemirror.js...would be nice if this were visible to utilities.
162
function indexOf(collection, elt) {
163
if (collection.indexOf) return collection.indexOf(elt);
164
for (var i = 0, e = collection.length; i < e; ++i)
165
if (collection[i] == elt) return i;
169
function completeEndTag(cm, pos, tagName) {
170
cm.replaceSelection('/' + tagName + '>');
171
cm.setCursor({line: pos.line, ch: pos.ch + tagName.length + 2 });