mirror of https://github.com/avecms/AVE.cms
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
175 lines
7.8 KiB
175 lines
7.8 KiB
// CodeMirror, copyright (c) by Marijn Haverbeke and others |
|
// Distributed under an MIT license: http://codemirror.net/LICENSE |
|
|
|
/** |
|
* Tag-closer extension for CodeMirror. |
|
* |
|
* This extension adds an "autoCloseTags" option that can be set to |
|
* either true to get the default behavior, or an object to further |
|
* configure its behavior. |
|
* |
|
* These are supported options: |
|
* |
|
* `whenClosing` (default true) |
|
* Whether to autoclose when the '/' of a closing tag is typed. |
|
* `whenOpening` (default true) |
|
* Whether to autoclose the tag when the final '>' of an opening |
|
* tag is typed. |
|
* `dontCloseTags` (default is empty tags for HTML, none for XML) |
|
* An array of tag names that should not be autoclosed. |
|
* `indentTags` (default is block tags for HTML, none for XML) |
|
* An array of tag names that should, when opened, cause a |
|
* blank line to be added inside the tag, and the blank line and |
|
* closing line to be indented. |
|
* |
|
* See demos/closetag.html for a usage example. |
|
*/ |
|
|
|
(function(mod) { |
|
if (typeof exports == "object" && typeof module == "object") // CommonJS |
|
mod(require("../../lib/codemirror"), require("../fold/xml-fold")); |
|
else if (typeof define == "function" && define.amd) // AMD |
|
define(["../../lib/codemirror", "../fold/xml-fold"], mod); |
|
else // Plain browser env |
|
mod(CodeMirror); |
|
})(function(CodeMirror) { |
|
CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) { |
|
if (old != CodeMirror.Init && old) |
|
cm.removeKeyMap("autoCloseTags"); |
|
if (!val) return; |
|
var map = {name: "autoCloseTags"}; |
|
if (typeof val != "object" || val.whenClosing) |
|
map["'/'"] = function(cm) { return autoCloseSlash(cm); }; |
|
if (typeof val != "object" || val.whenOpening) |
|
map["'>'"] = function(cm) { return autoCloseGT(cm); }; |
|
cm.addKeyMap(map); |
|
}); |
|
|
|
var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", |
|
"source", "track", "wbr"]; |
|
var htmlIndent = ["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"]; |
|
|
|
function autoCloseGT(cm) { |
|
if (cm.getOption("disableInput")) return CodeMirror.Pass; |
|
var ranges = cm.listSelections(), replacements = []; |
|
var opt = cm.getOption("autoCloseTags"); |
|
for (var i = 0; i < ranges.length; i++) { |
|
if (!ranges[i].empty()) return CodeMirror.Pass; |
|
var pos = ranges[i].head, tok = cm.getTokenAt(pos); |
|
var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state; |
|
if (inner.mode.name != "xml" || !state.tagName) return CodeMirror.Pass; |
|
|
|
var html = inner.mode.configuration == "html"; |
|
var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose); |
|
var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent); |
|
|
|
var tagName = state.tagName; |
|
if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch); |
|
var lowerTagName = tagName.toLowerCase(); |
|
// Don't process the '>' at the end of an end-tag or self-closing tag |
|
if (!tagName || |
|
tok.type == "string" && (tok.end != pos.ch || !/[\"\']/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1) || |
|
tok.type == "tag" && state.type == "closeTag" || |
|
tok.string.indexOf("/") == (tok.string.length - 1) || // match something like <someTagName /> |
|
dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 || |
|
closingTagExists(cm, tagName, pos, state, true)) |
|
return CodeMirror.Pass; |
|
|
|
var indent = indentTags && indexOf(indentTags, lowerTagName) > -1; |
|
replacements[i] = {indent: indent, |
|
text: ">" + (indent ? "\n\n" : "") + "</" + tagName + ">", |
|
newPos: indent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1)}; |
|
} |
|
|
|
var dontIndentOnAutoClose = (typeof opt == "object" && opt.dontIndentOnAutoClose); |
|
for (var i = ranges.length - 1; i >= 0; i--) { |
|
var info = replacements[i]; |
|
cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, "+insert"); |
|
var sel = cm.listSelections().slice(0); |
|
sel[i] = {head: info.newPos, anchor: info.newPos}; |
|
cm.setSelections(sel); |
|
if (!dontIndentOnAutoClose && info.indent) { |
|
cm.indentLine(info.newPos.line, null, true); |
|
cm.indentLine(info.newPos.line + 1, null, true); |
|
} |
|
} |
|
} |
|
|
|
function autoCloseCurrent(cm, typingSlash) { |
|
var ranges = cm.listSelections(), replacements = []; |
|
var head = typingSlash ? "/" : "</"; |
|
var opt = cm.getOption("autoCloseTags"); |
|
var dontIndentOnAutoClose = (typeof opt == "object" && opt.dontIndentOnSlash); |
|
for (var i = 0; i < ranges.length; i++) { |
|
if (!ranges[i].empty()) return CodeMirror.Pass; |
|
var pos = ranges[i].head, tok = cm.getTokenAt(pos); |
|
var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state; |
|
if (typingSlash && (tok.type == "string" || tok.string.charAt(0) != "<" || |
|
tok.start != pos.ch - 1)) |
|
return CodeMirror.Pass; |
|
// Kludge to get around the fact that we are not in XML mode |
|
// when completing in JS/CSS snippet in htmlmixed mode. Does not |
|
// work for other XML embedded languages (there is no general |
|
// way to go from a mixed mode to its current XML state). |
|
var replacement; |
|
if (inner.mode.name != "xml") { |
|
if (cm.getMode().name == "htmlmixed" && inner.mode.name == "javascript") |
|
replacement = head + "script"; |
|
else if (cm.getMode().name == "htmlmixed" && inner.mode.name == "css") |
|
replacement = head + "style"; |
|
else |
|
return CodeMirror.Pass; |
|
} else { |
|
if (!state.context || !state.context.tagName || |
|
closingTagExists(cm, state.context.tagName, pos, state)) |
|
return CodeMirror.Pass; |
|
replacement = head + state.context.tagName; |
|
} |
|
if (cm.getLine(pos.line).charAt(tok.end) != ">") replacement += ">"; |
|
replacements[i] = replacement; |
|
} |
|
cm.replaceSelections(replacements); |
|
ranges = cm.listSelections(); |
|
if (!dontIndentOnAutoClose) { |
|
for (var i = 0; i < ranges.length; i++) |
|
if (i == ranges.length - 1 || ranges[i].head.line < ranges[i + 1].head.line) |
|
cm.indentLine(ranges[i].head.line); |
|
} |
|
} |
|
|
|
function autoCloseSlash(cm) { |
|
if (cm.getOption("disableInput")) return CodeMirror.Pass; |
|
return autoCloseCurrent(cm, true); |
|
} |
|
|
|
CodeMirror.commands.closeTag = function(cm) { return autoCloseCurrent(cm); }; |
|
|
|
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; |
|
} |
|
|
|
// If xml-fold is loaded, we use its functionality to try and verify |
|
// whether a given tag is actually unclosed. |
|
function closingTagExists(cm, tagName, pos, state, newTag) { |
|
if (!CodeMirror.scanForClosingTag) return false; |
|
var end = Math.min(cm.lastLine() + 1, pos.line + 500); |
|
var nextClose = CodeMirror.scanForClosingTag(cm, pos, null, end); |
|
if (!nextClose || nextClose.tag != tagName) return false; |
|
var cx = state.context; |
|
// If the immediate wrapping context contains onCx instances of |
|
// the same tag, a closing tag only exists if there are at least |
|
// that many closing tags of that type following. |
|
for (var onCx = newTag ? 1 : 0; cx && cx.tagName == tagName; cx = cx.prev) ++onCx; |
|
pos = nextClose.to; |
|
for (var i = 1; i < onCx; i++) { |
|
var next = CodeMirror.scanForClosingTag(cm, pos, null, end); |
|
if (!next || next.tag != tagName) return false; |
|
pos = next.to; |
|
} |
|
return true; |
|
} |
|
});
|
|
|