diff --git a/lib/ace/mode/folding/html_test.js b/lib/ace/mode/folding/html_test.js
index 3179a39e..2e92af32 100644
--- a/lib/ace/mode/folding/html_test.js
+++ b/lib/ace/mode/folding/html_test.js
@@ -113,8 +113,8 @@ module.exports = {
assert.equal(session.getFoldWidget(1), "");
assert.equal(session.getFoldWidget(2), "end");
- assert.range(session.getFoldWidgetRange(0), 0, 7, 2, 0);
- assert.range(session.getFoldWidgetRange(2), 0, 7, 2, 0);
+ assert.range(session.getFoldWidgetRange(0), 0, 6, 2, 0);
+ assert.range(session.getFoldWidgetRange(2), 0, 6, 2, 0);
},
"test: fold should skip void elements": function() {
@@ -132,8 +132,8 @@ module.exports = {
assert.equal(session.getFoldWidget(1), "");
assert.equal(session.getFoldWidget(2), "end");
- assert.range(session.getFoldWidgetRange(0), 0, 7, 2, 0);
- assert.range(session.getFoldWidgetRange(2), 0, 7, 2, 0);
+ assert.range(session.getFoldWidgetRange(0), 0, 6, 2, 0);
+ assert.range(session.getFoldWidgetRange(2), 0, 6, 2, 0);
},
"test: fold multiple unclosed elements": function() {
@@ -157,8 +157,8 @@ module.exports = {
assert.equal(session.getFoldWidget(4), "");
assert.equal(session.getFoldWidget(5), "end");
- assert.range(session.getFoldWidgetRange(0), 0, 6, 5, 0);
- assert.range(session.getFoldWidgetRange(5), 0, 6, 5, 0);
+ assert.range(session.getFoldWidgetRange(0), 0, 5, 5, 0);
+ assert.range(session.getFoldWidgetRange(5), 0, 5, 5, 0);
}
};
diff --git a/lib/ace/mode/folding/xml.js b/lib/ace/mode/folding/xml.js
index 97e3c79d..e4af4878 100644
--- a/lib/ace/mode/folding/xml.js
+++ b/lib/ace/mode/folding/xml.js
@@ -38,6 +38,7 @@
define(function(require, exports, module) {
var oop = require("../../lib/oop");
+var lang = require("../../lib/lang");
var Range = require("../../range").Range;
var BaseFoldMode = require("./fold_mode").FoldMode;
var TokenIterator = require("../../token_iterator").TokenIterator;
@@ -51,121 +52,211 @@ oop.inherits(FoldMode, BaseFoldMode);
(function() {
this.getFoldWidget = function(session, foldStyle, row) {
- var tags = session.getTokens(row, row)[0].tokens
- .filter(function(token) {
- return token.type === "meta.tag";
- })
- .map(function(token) {
- return token.value;
- }).
- join("")
- .trim()
- .replace(/^<|>$|\s+/g, "")
- .split("><");
- var fold = tags[0];
-
- if (!fold || this.voidElements[fold])
- return "";
-
- if (fold.charAt(0) == "/")
+ var tag = this._getFirstTagInLine(session, row);
+
+ if (tag.closing)
return foldStyle == "markbeginend" ? "end" : "";
-
- if (fold.charAt(fold.length-1) == "/")
+
+ if (!tag.tagName || this.voidElements[tag.tagName.toLowerCase()])
return "";
-
- if (tags.indexOf("/" + fold) !== -1)
+
+ if (tag.selfClosing)
return "";
-
+
+ if (tag.value.indexOf("/" + tag.tagName) !== -1)
+ return "";
+
return "start";
};
-
- this.getFoldWidgetRange = function(session, foldStyle, row) {
- var start, end;
- var stack = [];
- var voidElements = this.voidElements;
-
- var iterator = new TokenIterator(session, row, 0);
- var step = "stepForward";
- var isBack = false;
-
- function pop(stack, tagName) {
- while (stack.length) {
- var top = stack[stack.length-1];
- if (!tagName || top === "" || top == tagName) {
- stack.pop();
- return true;
- }
- if (voidElements[top]) {
- stack.pop();
- continue;
- }
- else
- return false;
- }
- return false;
+
+ this._getFirstTagInLine = function(session, row) {
+ var tokens = session.getTokens(row, row)[0].tokens;
+ var value = "";
+ for (var i = 0; i < tokens.length; i++) {
+ var token = tokens[i];
+ if (token.type.indexOf("meta.tag") === 0)
+ value += token.value;
+ else
+ value += lang.stringRepeat(" ", token.value.length);
}
-
- // limited XML parsing to find matching tag
- do {
- var token = iterator.getCurrentToken();
+
+ return this._parseTag(value);
+ };
- var value = token.value.trim();
- if (token && token.type == "meta.tag" && token.value !== ">") {
- var tagName = value.replace(/^[<\s]*|[\s*>]$/g, "");
- if (!start) {
- if (tagName.charAt(0) == "/") {
- tagName = tagName.slice(1);
- step = "stepBackward";
- isBack = true;
- }
-
- start = {
- row: row,
- column: iterator.getCurrentTokenColumn() + (isBack ? 0 : value.length + 1)
- };
+ this.tagRe = /^(\s*)((\/?)([-_a-zA-Z0-9:!]*)\s*(\/?)>?)/;
+ this._parseTag = function(tag) {
+
+ var match = this.tagRe.exec(tag);
+ var column = this.tagRe.lastIndex || 0;
+ this.tagRe.lastIndex = 0;
- stack.push(tagName);
- }
- else {
- var close;
- if (tagName.charAt(0) == "/") {
- tagName = tagName.slice(1);
- close = !isBack;
- }
- else if (tagName.charAt(tagName.length-1) == "/") {
- tagName = "";
- close = !isBack;
- }
- else
- close = isBack;
-
- if (close) {
- if (pop(stack, tagName)) {
- if (stack.length === 0) {
- end = {
- row: iterator.getCurrentTokenRow(),
- column: iterator.getCurrentTokenColumn() + (isBack ? value.length + 1 : 0)
- };
- if (isBack)
- return Range.fromPoints(end, start);
- else
- return Range.fromPoints(start, end);
- }
- }
- else {
- if (!(isBack && voidElements[tagName]))
- typeof console !== undefined && console.error("unmatched tags!", tagName, stack);
- }
- }
- else {
- stack.push(tagName);
- }
- }
- }
-
- } while(token = iterator[step]());
+ return {
+ value: tag,
+ match: match ? match[2] : "",
+ closing: match ? !!match[3] : false,
+ selfClosing: match ? !!match[5] || match[2] == "/>" : false,
+ tagName: match ? match[4] : "",
+ column: match[1] ? column + match[1].length : column
+ };
};
+ /**
+ * reads a full tag and places the iterator after the tag
+ */
+ this._readTagForward = function(iterator) {
+ var token = iterator.getCurrentToken();
+ if (!token)
+ return null;
+
+ var value = "";
+ var start;
+
+ do {
+ if (token.type.indexOf("meta.tag") === 0) {
+ if (!start) {
+ var start = {
+ row: iterator.getCurrentTokenRow(),
+ column: iterator.getCurrentTokenColumn()
+ };
+ }
+ value += token.value;
+ if (value.indexOf(">") !== -1) {
+ var tag = this._parseTag(value);
+ tag.start = start;
+ tag.end = {
+ row: iterator.getCurrentTokenRow(),
+ column: iterator.getCurrentTokenColumn() + token.value.length
+ };
+ iterator.stepForward();
+ return tag;
+ }
+ }
+ } while(token = iterator.stepForward());
+
+ return null;
+ };
+
+ this._readTagBackward = function(iterator) {
+ var token = iterator.getCurrentToken();
+ if (!token)
+ return null;
+
+ var value = "";
+ var end;
+
+ do {
+ if (token.type.indexOf("meta.tag") === 0) {
+ if (!end) {
+ end = {
+ row: iterator.getCurrentTokenRow(),
+ column: iterator.getCurrentTokenColumn() + token.value.length
+ };
+ }
+ value = token.value + value;
+ if (value.indexOf("<") !== -1) {
+ var tag = this._parseTag(value);
+ tag.end = end;
+ tag.start = {
+ row: iterator.getCurrentTokenRow(),
+ column: iterator.getCurrentTokenColumn()
+ };
+ iterator.stepBackward();
+ return tag;
+ }
+ }
+ } while(token = iterator.stepBackward());
+
+ return null;
+ };
+
+ this._pop = function(stack, tag) {
+ while (stack.length) {
+
+ var top = stack[stack.length-1];
+ if (!tag || top.tagName == tag.tagName) {
+ return stack.pop();
+ }
+ else if (this.voidElements[tag.tagName]) {
+ return;
+ }
+ else if (this.voidElements[top.tagName]) {
+ stack.pop();
+ continue;
+ } else {
+ return null;
+ }
+ }
+ };
+
+ this.getFoldWidgetRange = function(session, foldStyle, row) {
+ var firstTag = this._getFirstTagInLine(session, row);
+
+ if (!firstTag.match)
+ return null;
+
+ var isBackward = firstTag.closing || firstTag.selfClosing;
+ var stack = [];
+ var tag;
+
+ if (!isBackward) {
+ var iterator = new TokenIterator(session, row, firstTag.column);
+ var start = {
+ row: row,
+ column: firstTag.column + firstTag.tagName.length + 2
+ };
+ while (tag = this._readTagForward(iterator)) {
+ if (tag.selfClosing) {
+ if (!stack.length) {
+ tag.start.column += tag.tagName.length + 2;
+ tag.end.column -= 2;
+ return Range.fromPoints(tag.start, tag.end);
+ } else
+ continue;
+ }
+
+ if (tag.closing) {
+ this._pop(stack, tag);
+ if (stack.length == 0)
+ break;
+ }
+ else {
+ stack.push(tag)
+ }
+ }
+ return Range.fromPoints(start, tag.start);
+ }
+ else {
+ var iterator = new TokenIterator(session, row, firstTag.column + firstTag.match.length);
+ var end = {
+ row: row,
+ column: firstTag.column
+ };
+
+ while (tag = this._readTagBackward(iterator)) {
+ if (tag.selfClosing) {
+ if (!stack.length) {
+ tag.start.column += tag.tagName.length + 2;
+ tag.end.column -= 2;
+ return Range.fromPoints(tag.start, tag.end);
+ } else
+ continue;
+ }
+
+ if (!tag.closing) {
+ this._pop(stack, tag);
+ if (stack.length == 0)
+ break;
+ }
+ else {
+ stack.push(tag)
+ }
+ }
+ tag.start.column += tag.tagName.length + 2;
+ return Range.fromPoints(tag.start, end);
+ }
+
+ };
+
}).call(FoldMode.prototype);
});
\ No newline at end of file
diff --git a/lib/ace/mode/folding/xml_test.js b/lib/ace/mode/folding/xml_test.js
index 7af1c201..2a47dfc5 100644
--- a/lib/ace/mode/folding/xml_test.js
+++ b/lib/ace/mode/folding/xml_test.js
@@ -80,8 +80,8 @@ module.exports = {
assert.equal(session.getFoldWidget(1), "");
assert.equal(session.getFoldWidget(2), "end");
- assert.range(session.getFoldWidgetRange(0), 0, 9, 2, 0);
- assert.range(session.getFoldWidgetRange(2), 0, 9, 2, 0);
+ assert.range(session.getFoldWidgetRange(0), 0, 8, 2, 0);
+ assert.range(session.getFoldWidgetRange(2), 0, 8, 2, 0);
},
"test: fold should skip multi line self closing elements": function() {
@@ -103,10 +103,10 @@ module.exports = {
assert.equal(session.getFoldWidget(3), "end");
assert.equal(session.getFoldWidget(4), "end");
- assert.range(session.getFoldWidgetRange(0), 0, 9, 4, 0);
+ assert.range(session.getFoldWidgetRange(0), 0, 8, 4, 0);
assert.range(session.getFoldWidgetRange(1), 1, 9, 3, 19);
assert.range(session.getFoldWidgetRange(3), 1, 9, 3, 19);
- assert.range(session.getFoldWidgetRange(4), 0, 9, 4, 0);
+ assert.range(session.getFoldWidgetRange(4), 0, 8, 4, 0);
}
};
diff --git a/lib/ace/mode/html_tokenizer_test.js b/lib/ace/mode/html_tokenizer_test.js
index ec96dc49..06640b38 100644
--- a/lib/ace/mode/html_tokenizer_test.js
+++ b/lib/ace/mode/html_tokenizer_test.js
@@ -53,16 +53,19 @@ module.exports = {
var line = "'123'";
var tokens = this.tokenizer.getLineTokens(line, "start").tokens;
- assert.equal(9, tokens.length);
+ assert.equal(12, tokens.length);
assert.equal("meta.tag", tokens[0].type);
- assert.equal("text", tokens[1].type);
- assert.equal("entity.other.attribute-name", tokens[2].type);
- assert.equal("keyword.operator", tokens[3].type);
- assert.equal("string", tokens[4].type);
- assert.equal("meta.tag", tokens[5].type);
- assert.equal("keyword.definition", tokens[6].type);
- assert.equal("meta.tag", tokens[7].type);
- assert.equal("text", tokens[8].type);
+ assert.equal("meta.tag.script", tokens[1].type);
+ assert.equal("text", tokens[2].type);
+ assert.equal("entity.other.attribute-name", tokens[3].type);
+ assert.equal("keyword.operator", tokens[4].type);
+ assert.equal("string", tokens[5].type);
+ assert.equal("meta.tag", tokens[6].type);
+ assert.equal("keyword.definition", tokens[7].type);
+ assert.equal("meta.tag", tokens[8].type);
+ assert.equal("meta.tag.script", tokens[9].type);
+ assert.equal("meta.tag", tokens[10].type);
+ assert.equal("text", tokens[11].type);
},
"test: tokenize multiline attribute value with double quotes": function() {