/** * Tag-closer extension for CodeMirror. * * This extension adds a "closeTag" utility function that can be used with key bindings to * insert a matching end tag after the ">" character of a start tag has been typed. It can * also complete " * Contributed under the same license terms as CodeMirror. */ (function() { /** Option that allows tag closing behavior to be toggled. Default is true. */ CodeMirror.defaults['closeTagEnabled'] = true; /** Array of tag names to add indentation after the start tag for. Default is the list of block-level html tags. */ 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']; /** * Call during key processing to close tags. Handles the key event if the tag is closed, otherwise throws CodeMirror.Pass. * - cm: The editor instance. * - ch: The character being processed. * - indent: Optional. Omit or pass true to use the default indentation tag list defined in the 'closeTagIndent' option. * Pass false to disable indentation. Pass an array to override the default list of tag names. */ CodeMirror.defineExtension("closeTag", function(cm, ch, indent) { if (!cm.getOption('closeTagEnabled')) { throw CodeMirror.Pass; } var mode = cm.getOption('mode'); if (mode == 'text/html') { /* * Relevant structure of token: * * htmlmixed * className * state * htmlState * type * context * tagName * mode * * xml * className * state * tagName * type */ var pos = cm.getCursor(); var tok = cm.getTokenAt(pos); var state = tok.state; if (state.mode && state.mode != 'html') { throw CodeMirror.Pass; // With htmlmixed, we only care about the html sub-mode. } if (ch == '>') { var type = state.htmlState ? state.htmlState.type : state.type; // htmlmixed : xml if (tok.className == 'tag' && type == 'closeTag') { throw CodeMirror.Pass; // Don't process the '>' at the end of an end-tag. } cm.replaceSelection('>'); // Mode state won't update until we finish the tag. pos = {line: pos.line, ch: pos.ch + 1}; cm.setCursor(pos); tok = cm.getTokenAt(cm.getCursor()); state = tok.state; type = state.htmlState ? state.htmlState.type : state.type; // htmlmixed : xml if (tok.className == 'tag' && type != 'selfcloseTag') { var tagName = state.htmlState ? state.htmlState.context.tagName : state.tagName; // htmlmixed : xml if (tagName.length > 0) { insertEndTag(cm, indent, pos, tagName); } return; } // Undo the '>' insert and allow cm to handle the key instead. cm.setSelection({line: pos.line, ch: pos.ch - 1}, pos); cm.replaceSelection(""); } else if (ch == '/') { if (tok.className == 'tag' && tok.string == '<') { var tagName = state.htmlState ? (state.htmlState.context ? state.htmlState.context.tagName : '') : state.context.tagName; // htmlmixed : xml # extra htmlmized check is for ' 0) { completeEndTag(cm, pos, tagName); return; } } } } throw CodeMirror.Pass; // Bubble if not handled }); function insertEndTag(cm, indent, pos, tagName) { if (shouldIndent(cm, indent, tagName)) { cm.replaceSelection('\n\n', 'end'); cm.indentLine(pos.line + 1); cm.indentLine(pos.line + 2); cm.setCursor({line: pos.line + 1, ch: cm.getLine(pos.line + 1).length}); } else { cm.replaceSelection(''); cm.setCursor(pos); } } function shouldIndent(cm, indent, tagName) { if (typeof indent == 'undefined' || indent == null || indent == true) { indent = cm.getOption('closeTagIndent'); } if (!indent) { indent = []; } return indexOf(indent, tagName.toLowerCase()) != -1; } // C&P from codemirror.js...would be nice if this were visible to utilities. function indexOf(collection, elt) { if (collection.indexOf) return collection.indexOf(elt); for (var i = 0, e = collection.length; i < e; ++i) if (collection[i] == elt) return i; return -1; } function completeEndTag(cm, pos, tagName) { cm.replaceSelection('/' + tagName + '>'); cm.setCursor({line: pos.line, ch: pos.ch + tagName.length + 2 }); } })();