closetag.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. // CodeMirror, copyright (c) by Marijn Haverbeke and others
  2. // Distributed under an MIT license: http://codemirror.net/LICENSE
  3. /**
  4. * Tag-closer extension for CodeMirror.
  5. *
  6. * This extension adds an "autoCloseTags" option that can be set to
  7. * either true to get the default behavior, or an object to further
  8. * configure its behavior.
  9. *
  10. * These are supported options:
  11. *
  12. * `whenClosing` (default true)
  13. * Whether to autoclose when the '/' of a closing tag is typed.
  14. * `whenOpening` (default true)
  15. * Whether to autoclose the tag when the final '>' of an opening
  16. * tag is typed.
  17. * `dontCloseTags` (default is empty tags for HTML, none for XML)
  18. * An array of tag names that should not be autoclosed.
  19. * `indentTags` (default is block tags for HTML, none for XML)
  20. * An array of tag names that should, when opened, cause a
  21. * blank line to be added inside the tag, and the blank line and
  22. * closing line to be indented.
  23. *
  24. * See demos/closetag.html for a usage example.
  25. */
  26. (function(mod) {
  27. if (typeof exports == "object" && typeof module == "object") // CommonJS
  28. mod(require("../../lib/codemirror"), require("../fold/xml-fold"));
  29. else if (typeof define == "function" && define.amd) // AMD
  30. define(["../../lib/codemirror", "../fold/xml-fold"], mod);
  31. else // Plain browser env
  32. mod(CodeMirror);
  33. })(function(CodeMirror) {
  34. CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) {
  35. if (old != CodeMirror.Init && old)
  36. cm.removeKeyMap("autoCloseTags");
  37. if (!val) return;
  38. var map = {name: "autoCloseTags"};
  39. if (typeof val != "object" || val.whenClosing)
  40. map["'/'"] = function(cm) { return autoCloseSlash(cm); };
  41. if (typeof val != "object" || val.whenOpening)
  42. map["'>'"] = function(cm) { return autoCloseGT(cm); };
  43. cm.addKeyMap(map);
  44. });
  45. var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param",
  46. "source", "track", "wbr"];
  47. var htmlIndent = ["applet", "blockquote", "body", "button", "div", "dl", "fieldset", "form", "frameset", "h1", "h2", "h3", "h4",
  48. "h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"];
  49. function autoCloseGT(cm) {
  50. if (cm.getOption("disableInput")) return CodeMirror.Pass;
  51. var ranges = cm.listSelections(), replacements = [];
  52. for (var i = 0; i < ranges.length; i++) {
  53. if (!ranges[i].empty()) return CodeMirror.Pass;
  54. var pos = ranges[i].head, tok = cm.getTokenAt(pos);
  55. var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
  56. if (inner.mode.name != "xml" || !state.tagName) return CodeMirror.Pass;
  57. var opt = cm.getOption("autoCloseTags"), html = inner.mode.configuration == "html";
  58. var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose);
  59. var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent);
  60. var tagName = state.tagName;
  61. if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch);
  62. var lowerTagName = tagName.toLowerCase();
  63. // Don't process the '>' at the end of an end-tag or self-closing tag
  64. if (!tagName ||
  65. tok.type == "string" && (tok.end != pos.ch || !/[\"\']/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1) ||
  66. tok.type == "tag" && state.type == "closeTag" ||
  67. tok.string.indexOf("/") == (tok.string.length - 1) || // match something like <someTagName />
  68. dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 ||
  69. closingTagExists(cm, tagName, pos, state, true))
  70. return CodeMirror.Pass;
  71. var indent = indentTags && indexOf(indentTags, lowerTagName) > -1;
  72. replacements[i] = {indent: indent,
  73. text: ">" + (indent ? "\n\n" : "") + "</" + tagName + ">",
  74. newPos: indent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1)};
  75. }
  76. for (var i = ranges.length - 1; i >= 0; i--) {
  77. var info = replacements[i];
  78. cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, "+insert");
  79. var sel = cm.listSelections().slice(0);
  80. sel[i] = {head: info.newPos, anchor: info.newPos};
  81. cm.setSelections(sel);
  82. if (info.indent) {
  83. cm.indentLine(info.newPos.line, null, true);
  84. cm.indentLine(info.newPos.line + 1, null, true);
  85. }
  86. }
  87. }
  88. function autoCloseCurrent(cm, typingSlash) {
  89. var ranges = cm.listSelections(), replacements = [];
  90. var head = typingSlash ? "/" : "</";
  91. for (var i = 0; i < ranges.length; i++) {
  92. if (!ranges[i].empty()) return CodeMirror.Pass;
  93. var pos = ranges[i].head, tok = cm.getTokenAt(pos);
  94. var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
  95. if (typingSlash && (tok.type == "string" || tok.string.charAt(0) != "<" ||
  96. tok.start != pos.ch - 1))
  97. return CodeMirror.Pass;
  98. // Kludge to get around the fact that we are not in XML mode
  99. // when completing in JS/CSS snippet in htmlmixed mode. Does not
  100. // work for other XML embedded languages (there is no general
  101. // way to go from a mixed mode to its current XML state).
  102. if (inner.mode.name != "xml") {
  103. if (cm.getMode().name == "htmlmixed" && inner.mode.name == "javascript")
  104. replacements[i] = head + "script>";
  105. else if (cm.getMode().name == "htmlmixed" && inner.mode.name == "css")
  106. replacements[i] = head + "style>";
  107. else
  108. return CodeMirror.Pass;
  109. } else {
  110. if (!state.context || !state.context.tagName ||
  111. closingTagExists(cm, state.context.tagName, pos, state))
  112. return CodeMirror.Pass;
  113. replacements[i] = head + state.context.tagName + ">";
  114. }
  115. }
  116. cm.replaceSelections(replacements);
  117. ranges = cm.listSelections();
  118. for (var i = 0; i < ranges.length; i++)
  119. if (i == ranges.length - 1 || ranges[i].head.line < ranges[i + 1].head.line)
  120. cm.indentLine(ranges[i].head.line);
  121. }
  122. function autoCloseSlash(cm) {
  123. if (cm.getOption("disableInput")) return CodeMirror.Pass;
  124. autoCloseCurrent(cm, true);
  125. }
  126. CodeMirror.commands.closeTag = function(cm) { return autoCloseCurrent(cm); };
  127. function indexOf(collection, elt) {
  128. if (collection.indexOf) return collection.indexOf(elt);
  129. for (var i = 0, e = collection.length; i < e; ++i)
  130. if (collection[i] == elt) return i;
  131. return -1;
  132. }
  133. // If xml-fold is loaded, we use its functionality to try and verify
  134. // whether a given tag is actually unclosed.
  135. function closingTagExists(cm, tagName, pos, state, newTag) {
  136. if (!CodeMirror.scanForClosingTag) return false;
  137. var end = Math.min(cm.lastLine() + 1, pos.line + 500);
  138. var nextClose = CodeMirror.scanForClosingTag(cm, pos, null, end);
  139. if (!nextClose || nextClose.tag != tagName) return false;
  140. var cx = state.context;
  141. // If the immediate wrapping context contains onCx instances of
  142. // the same tag, a closing tag only exists if there are at least
  143. // that many closing tags of that type following.
  144. for (var onCx = newTag ? 1 : 0; cx && cx.tagName == tagName; cx = cx.prev) ++onCx;
  145. pos = nextClose.to;
  146. for (var i = 1; i < onCx; i++) {
  147. var next = CodeMirror.scanForClosingTag(cm, pos, null, end);
  148. if (!next || next.tag != tagName) return false;
  149. pos = next.to;
  150. }
  151. return true;
  152. }
  153. });