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.

185 lines
8.3 KiB

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