text.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. /* ***** BEGIN LICENSE BLOCK *****
  2. * Distributed under the BSD license:
  3. *
  4. * Copyright (c) 2010, Ajax.org B.V.
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. * * Redistributions in binary form must reproduce the above copyright
  12. * notice, this list of conditions and the following disclaimer in the
  13. * documentation and/or other materials provided with the distribution.
  14. * * Neither the name of Ajax.org B.V. nor the
  15. * names of its contributors may be used to endorse or promote products
  16. * derived from this software without specific prior written permission.
  17. *
  18. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  19. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  24. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *
  29. * ***** END LICENSE BLOCK ***** */
  30. define(function(require, exports, module) {
  31. "use strict";
  32. var oop = require("../lib/oop");
  33. var dom = require("../lib/dom");
  34. var lang = require("../lib/lang");
  35. var useragent = require("../lib/useragent");
  36. var EventEmitter = require("../lib/event_emitter").EventEmitter;
  37. var Text = function(parentEl) {
  38. this.element = dom.createElement("div");
  39. this.element.className = "ace_layer ace_text-layer";
  40. parentEl.appendChild(this.element);
  41. this.$updateEolChar = this.$updateEolChar.bind(this);
  42. };
  43. (function() {
  44. oop.implement(this, EventEmitter);
  45. this.EOF_CHAR = "\xB6";
  46. this.EOL_CHAR_LF = "\xAC";
  47. this.EOL_CHAR_CRLF = "\xa4";
  48. this.EOL_CHAR = this.EOL_CHAR_LF;
  49. this.TAB_CHAR = "\u2014"; //"\u21E5";
  50. this.SPACE_CHAR = "\xB7";
  51. this.$padding = 0;
  52. this.$updateEolChar = function() {
  53. var EOL_CHAR = this.session.doc.getNewLineCharacter() == "\n"
  54. ? this.EOL_CHAR_LF
  55. : this.EOL_CHAR_CRLF;
  56. if (this.EOL_CHAR != EOL_CHAR) {
  57. this.EOL_CHAR = EOL_CHAR;
  58. return true;
  59. }
  60. }
  61. this.setPadding = function(padding) {
  62. this.$padding = padding;
  63. this.element.style.padding = "0 " + padding + "px";
  64. };
  65. this.getLineHeight = function() {
  66. return this.$fontMetrics.$characterSize.height || 0;
  67. };
  68. this.getCharacterWidth = function() {
  69. return this.$fontMetrics.$characterSize.width || 0;
  70. };
  71. this.$setFontMetrics = function(measure) {
  72. this.$fontMetrics = measure;
  73. this.$fontMetrics.on("changeCharacterSize", function(e) {
  74. this._signal("changeCharacterSize", e);
  75. }.bind(this));
  76. this.$pollSizeChanges();
  77. }
  78. this.checkForSizeChanges = function() {
  79. this.$fontMetrics.checkForSizeChanges();
  80. };
  81. this.$pollSizeChanges = function() {
  82. return this.$pollSizeChangesTimer = this.$fontMetrics.$pollSizeChanges();
  83. };
  84. this.setSession = function(session) {
  85. this.session = session;
  86. if (session)
  87. this.$computeTabString();
  88. };
  89. this.showInvisibles = false;
  90. this.setShowInvisibles = function(showInvisibles) {
  91. if (this.showInvisibles == showInvisibles)
  92. return false;
  93. this.showInvisibles = showInvisibles;
  94. this.$computeTabString();
  95. return true;
  96. };
  97. this.displayIndentGuides = true;
  98. this.setDisplayIndentGuides = function(display) {
  99. if (this.displayIndentGuides == display)
  100. return false;
  101. this.displayIndentGuides = display;
  102. this.$computeTabString();
  103. return true;
  104. };
  105. this.$tabStrings = [];
  106. this.onChangeTabSize =
  107. this.$computeTabString = function() {
  108. var tabSize = this.session.getTabSize();
  109. this.tabSize = tabSize;
  110. var tabStr = this.$tabStrings = [0];
  111. for (var i = 1; i < tabSize + 1; i++) {
  112. if (this.showInvisibles) {
  113. tabStr.push("<span class='ace_invisible ace_invisible_tab'>"
  114. + lang.stringRepeat(this.TAB_CHAR, i)
  115. + "</span>");
  116. } else {
  117. tabStr.push(lang.stringRepeat(" ", i));
  118. }
  119. }
  120. if (this.displayIndentGuides) {
  121. this.$indentGuideRe = /\s\S| \t|\t |\s$/;
  122. var className = "ace_indent-guide";
  123. var spaceClass = "";
  124. var tabClass = "";
  125. if (this.showInvisibles) {
  126. className += " ace_invisible";
  127. spaceClass = " ace_invisible_space";
  128. tabClass = " ace_invisible_tab";
  129. var spaceContent = lang.stringRepeat(this.SPACE_CHAR, this.tabSize);
  130. var tabContent = lang.stringRepeat(this.TAB_CHAR, this.tabSize);
  131. } else{
  132. var spaceContent = lang.stringRepeat(" ", this.tabSize);
  133. var tabContent = spaceContent;
  134. }
  135. this.$tabStrings[" "] = "<span class='" + className + spaceClass + "'>" + spaceContent + "</span>";
  136. this.$tabStrings["\t"] = "<span class='" + className + tabClass + "'>" + tabContent + "</span>";
  137. }
  138. };
  139. this.updateLines = function(config, firstRow, lastRow) {
  140. // Due to wrap line changes there can be new lines if e.g.
  141. // the line to updated wrapped in the meantime.
  142. if (this.config.lastRow != config.lastRow ||
  143. this.config.firstRow != config.firstRow) {
  144. this.scrollLines(config);
  145. }
  146. this.config = config;
  147. var first = Math.max(firstRow, config.firstRow);
  148. var last = Math.min(lastRow, config.lastRow);
  149. var lineElements = this.element.childNodes;
  150. var lineElementsIdx = 0;
  151. for (var row = config.firstRow; row < first; row++) {
  152. var foldLine = this.session.getFoldLine(row);
  153. if (foldLine) {
  154. if (foldLine.containsRow(first)) {
  155. first = foldLine.start.row;
  156. break;
  157. } else {
  158. row = foldLine.end.row;
  159. }
  160. }
  161. lineElementsIdx ++;
  162. }
  163. var row = first;
  164. var foldLine = this.session.getNextFoldLine(row);
  165. var foldStart = foldLine ? foldLine.start.row : Infinity;
  166. while (true) {
  167. if (row > foldStart) {
  168. row = foldLine.end.row+1;
  169. foldLine = this.session.getNextFoldLine(row, foldLine);
  170. foldStart = foldLine ? foldLine.start.row :Infinity;
  171. }
  172. if (row > last)
  173. break;
  174. var lineElement = lineElements[lineElementsIdx++];
  175. if (lineElement) {
  176. var html = [];
  177. this.$renderLine(
  178. html, row, !this.$useLineGroups(), row == foldStart ? foldLine : false
  179. );
  180. lineElement.style.height = config.lineHeight * this.session.getRowLength(row) + "px";
  181. lineElement.innerHTML = html.join("");
  182. }
  183. row++;
  184. }
  185. };
  186. this.scrollLines = function(config) {
  187. var oldConfig = this.config;
  188. this.config = config;
  189. if (!oldConfig || oldConfig.lastRow < config.firstRow)
  190. return this.update(config);
  191. if (config.lastRow < oldConfig.firstRow)
  192. return this.update(config);
  193. var el = this.element;
  194. if (oldConfig.firstRow < config.firstRow)
  195. for (var row=this.session.getFoldedRowCount(oldConfig.firstRow, config.firstRow - 1); row>0; row--)
  196. el.removeChild(el.firstChild);
  197. if (oldConfig.lastRow > config.lastRow)
  198. for (var row=this.session.getFoldedRowCount(config.lastRow + 1, oldConfig.lastRow); row>0; row--)
  199. el.removeChild(el.lastChild);
  200. if (config.firstRow < oldConfig.firstRow) {
  201. var fragment = this.$renderLinesFragment(config, config.firstRow, oldConfig.firstRow - 1);
  202. if (el.firstChild)
  203. el.insertBefore(fragment, el.firstChild);
  204. else
  205. el.appendChild(fragment);
  206. }
  207. if (config.lastRow > oldConfig.lastRow) {
  208. var fragment = this.$renderLinesFragment(config, oldConfig.lastRow + 1, config.lastRow);
  209. el.appendChild(fragment);
  210. }
  211. };
  212. this.$renderLinesFragment = function(config, firstRow, lastRow) {
  213. var fragment = this.element.ownerDocument.createDocumentFragment();
  214. var row = firstRow;
  215. var foldLine = this.session.getNextFoldLine(row);
  216. var foldStart = foldLine ? foldLine.start.row : Infinity;
  217. while (true) {
  218. if (row > foldStart) {
  219. row = foldLine.end.row+1;
  220. foldLine = this.session.getNextFoldLine(row, foldLine);
  221. foldStart = foldLine ? foldLine.start.row : Infinity;
  222. }
  223. if (row > lastRow)
  224. break;
  225. var container = dom.createElement("div");
  226. var html = [];
  227. // Get the tokens per line as there might be some lines in between
  228. // beeing folded.
  229. this.$renderLine(html, row, false, row == foldStart ? foldLine : false);
  230. // don't use setInnerHtml since we are working with an empty DIV
  231. container.innerHTML = html.join("");
  232. if (this.$useLineGroups()) {
  233. container.className = 'ace_line_group';
  234. fragment.appendChild(container);
  235. container.style.height = config.lineHeight * this.session.getRowLength(row) + "px";
  236. } else {
  237. while(container.firstChild)
  238. fragment.appendChild(container.firstChild);
  239. }
  240. row++;
  241. }
  242. return fragment;
  243. };
  244. this.update = function(config) {
  245. this.config = config;
  246. var html = [];
  247. var firstRow = config.firstRow, lastRow = config.lastRow;
  248. var row = firstRow;
  249. var foldLine = this.session.getNextFoldLine(row);
  250. var foldStart = foldLine ? foldLine.start.row : Infinity;
  251. while (true) {
  252. if (row > foldStart) {
  253. row = foldLine.end.row+1;
  254. foldLine = this.session.getNextFoldLine(row, foldLine);
  255. foldStart = foldLine ? foldLine.start.row :Infinity;
  256. }
  257. if (row > lastRow)
  258. break;
  259. if (this.$useLineGroups())
  260. html.push("<div class='ace_line_group' style='height:", config.lineHeight*this.session.getRowLength(row), "px'>")
  261. this.$renderLine(html, row, false, row == foldStart ? foldLine : false);
  262. if (this.$useLineGroups())
  263. html.push("</div>"); // end the line group
  264. row++;
  265. }
  266. this.element.innerHTML = html.join("");
  267. };
  268. this.$textToken = {
  269. "text": true,
  270. "rparen": true,
  271. "lparen": true
  272. };
  273. this.$renderToken = function(stringBuilder, screenColumn, token, value) {
  274. var self = this;
  275. var replaceReg = /\t|&|<|>|( +)|([\x00-\x1f\x80-\xa0\xad\u1680\u180E\u2000-\u200f\u2028\u2029\u202F\u205F\u3000\uFEFF\uFFF9-\uFFFC])|[\u1100-\u115F\u11A3-\u11A7\u11FA-\u11FF\u2329-\u232A\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3000-\u303E\u3041-\u3096\u3099-\u30FF\u3105-\u312D\u3131-\u318E\u3190-\u31BA\u31C0-\u31E3\u31F0-\u321E\u3220-\u3247\u3250-\u32FE\u3300-\u4DBF\u4E00-\uA48C\uA490-\uA4C6\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFF01-\uFF60\uFFE0-\uFFE6]/g;
  276. var replaceFunc = function(c, a, b, tabIdx, idx4) {
  277. if (a) {
  278. return self.showInvisibles
  279. ? "<span class='ace_invisible ace_invisible_space'>" + lang.stringRepeat(self.SPACE_CHAR, c.length) + "</span>"
  280. : c;
  281. } else if (c == "&") {
  282. return "&#38;";
  283. } else if (c == "<") {
  284. return "&#60;";
  285. } else if (c == ">") {
  286. // normally escaping this is not needed, but xml documents throw error when setting innerHTML to ]]>
  287. return "&#62;";
  288. } else if (c == "\t") {
  289. if (typeof self.session !== 'undefined') {
  290. var tabSize = self.session.getScreenTabSize(screenColumn + tabIdx);
  291. screenColumn += tabSize - 1;
  292. return self.$tabStrings[tabSize];
  293. }
  294. return "<span class='ace_invisible ace_invisible_tab'>&#9;</span>";
  295. } else if (c == "\u3000") {
  296. // U+3000 is both invisible AND full-width, so must be handled uniquely
  297. var classToUse = self.showInvisibles ? "ace_cjk ace_invisible ace_invisible_space" : "ace_cjk";
  298. var space = self.showInvisibles ? self.SPACE_CHAR : "";
  299. screenColumn += 1;
  300. return "<span class='" + classToUse + "' style='width:" +
  301. (self.config.characterWidth * 2) +
  302. "px'>" + space + "</span>";
  303. } else if (b) {
  304. return "<span class='ace_invisible ace_invisible_space ace_invalid'>" + self.SPACE_CHAR + "</span>";
  305. } else {
  306. screenColumn += 1;
  307. return "<span class='ace_cjk' style='width:" +
  308. (self.config.characterWidth * 2) +
  309. "px'>" + c + "</span>";
  310. }
  311. };
  312. var output = value.replace(replaceReg, replaceFunc);
  313. if (!this.$textToken[token.type]) {
  314. var classes = "ace_" + token.type.replace(/\./g, " ace_");
  315. var style = "";
  316. if (token.type == "fold")
  317. style = " style='width:" + (token.value.length * this.config.characterWidth) + "px;' ";
  318. stringBuilder.push("<span class='", classes, "'", style, ">", output, "</span>");
  319. }
  320. else {
  321. stringBuilder.push(output);
  322. }
  323. return screenColumn + value.length;
  324. };
  325. this.renderIndentGuide = function(stringBuilder, value, max) {
  326. var cols = value.search(this.$indentGuideRe);
  327. if (cols <= 0 || cols >= max)
  328. return value;
  329. if (value[0] == " ") {
  330. cols -= cols % this.tabSize;
  331. stringBuilder.push(lang.stringRepeat(this.$tabStrings[" "], cols/this.tabSize));
  332. return value.substr(cols);
  333. } else if (value[0] == "\t") {
  334. stringBuilder.push(lang.stringRepeat(this.$tabStrings["\t"], cols));
  335. return value.substr(cols);
  336. }
  337. return value;
  338. };
  339. this.$renderWrappedLine = function(stringBuilder, tokens, splits, onlyContents) {
  340. var chars = 0;
  341. var split = 0;
  342. var splitChars = splits[0];
  343. var screenColumn = 0;
  344. for (var i = 0; i < tokens.length; i++) {
  345. var token = tokens[i];
  346. var value = token.value;
  347. if (i == 0 && this.displayIndentGuides) {
  348. chars = value.length;
  349. value = this.renderIndentGuide(stringBuilder, value, splitChars);
  350. if (!value)
  351. continue;
  352. chars -= value.length;
  353. }
  354. if (chars + value.length < splitChars) {
  355. screenColumn = this.$renderToken(stringBuilder, screenColumn, token, value);
  356. chars += value.length;
  357. } else {
  358. while (chars + value.length >= splitChars) {
  359. screenColumn = this.$renderToken(
  360. stringBuilder, screenColumn,
  361. token, value.substring(0, splitChars - chars)
  362. );
  363. value = value.substring(splitChars - chars);
  364. chars = splitChars;
  365. if (!onlyContents) {
  366. stringBuilder.push("</div>",
  367. "<div class='ace_line' style='height:",
  368. this.config.lineHeight, "px'>"
  369. );
  370. }
  371. stringBuilder.push(lang.stringRepeat("\xa0", splits.indent));
  372. split ++;
  373. screenColumn = 0;
  374. splitChars = splits[split] || Number.MAX_VALUE;
  375. }
  376. if (value.length != 0) {
  377. chars += value.length;
  378. screenColumn = this.$renderToken(
  379. stringBuilder, screenColumn, token, value
  380. );
  381. }
  382. }
  383. }
  384. };
  385. this.$renderSimpleLine = function(stringBuilder, tokens) {
  386. var screenColumn = 0;
  387. var token = tokens[0];
  388. var value = token.value;
  389. if (this.displayIndentGuides)
  390. value = this.renderIndentGuide(stringBuilder, value);
  391. if (value)
  392. screenColumn = this.$renderToken(stringBuilder, screenColumn, token, value);
  393. for (var i = 1; i < tokens.length; i++) {
  394. token = tokens[i];
  395. value = token.value;
  396. screenColumn = this.$renderToken(stringBuilder, screenColumn, token, value);
  397. }
  398. };
  399. // row is either first row of foldline or not in fold
  400. this.$renderLine = function(stringBuilder, row, onlyContents, foldLine) {
  401. if (!foldLine && foldLine != false)
  402. foldLine = this.session.getFoldLine(row);
  403. if (foldLine)
  404. var tokens = this.$getFoldLineTokens(row, foldLine);
  405. else
  406. var tokens = this.session.getTokens(row);
  407. if (!onlyContents) {
  408. stringBuilder.push(
  409. "<div class='ace_line' style='height:",
  410. this.config.lineHeight * (
  411. this.$useLineGroups() ? 1 :this.session.getRowLength(row)
  412. ), "px'>"
  413. );
  414. }
  415. if (tokens.length) {
  416. var splits = this.session.getRowSplitData(row);
  417. if (splits && splits.length)
  418. this.$renderWrappedLine(stringBuilder, tokens, splits, onlyContents);
  419. else
  420. this.$renderSimpleLine(stringBuilder, tokens);
  421. }
  422. if (this.showInvisibles) {
  423. if (foldLine)
  424. row = foldLine.end.row
  425. stringBuilder.push(
  426. "<span class='ace_invisible ace_invisible_eol'>",
  427. row == this.session.getLength() - 1 ? this.EOF_CHAR : this.EOL_CHAR,
  428. "</span>"
  429. );
  430. }
  431. if (!onlyContents)
  432. stringBuilder.push("</div>");
  433. };
  434. this.$getFoldLineTokens = function(row, foldLine) {
  435. var session = this.session;
  436. var renderTokens = [];
  437. function addTokens(tokens, from, to) {
  438. var idx = 0, col = 0;
  439. while ((col + tokens[idx].value.length) < from) {
  440. col += tokens[idx].value.length;
  441. idx++;
  442. if (idx == tokens.length)
  443. return;
  444. }
  445. if (col != from) {
  446. var value = tokens[idx].value.substring(from - col);
  447. // Check if the token value is longer then the from...to spacing.
  448. if (value.length > (to - from))
  449. value = value.substring(0, to - from);
  450. renderTokens.push({
  451. type: tokens[idx].type,
  452. value: value
  453. });
  454. col = from + value.length;
  455. idx += 1;
  456. }
  457. while (col < to && idx < tokens.length) {
  458. var value = tokens[idx].value;
  459. if (value.length + col > to) {
  460. renderTokens.push({
  461. type: tokens[idx].type,
  462. value: value.substring(0, to - col)
  463. });
  464. } else
  465. renderTokens.push(tokens[idx]);
  466. col += value.length;
  467. idx += 1;
  468. }
  469. }
  470. var tokens = session.getTokens(row);
  471. foldLine.walk(function(placeholder, row, column, lastColumn, isNewRow) {
  472. if (placeholder != null) {
  473. renderTokens.push({
  474. type: "fold",
  475. value: placeholder
  476. });
  477. } else {
  478. if (isNewRow)
  479. tokens = session.getTokens(row);
  480. if (tokens.length)
  481. addTokens(tokens, lastColumn, column);
  482. }
  483. }, foldLine.end.row, this.session.getLine(foldLine.end.row).length);
  484. return renderTokens;
  485. };
  486. this.$useLineGroups = function() {
  487. // For the updateLines function to work correctly, it's important that the
  488. // child nodes of this.element correspond on a 1-to-1 basis to rows in the
  489. // document (as distinct from lines on the screen). For sessions that are
  490. // wrapped, this means we need to add a layer to the node hierarchy (tagged
  491. // with the class name ace_line_group).
  492. return this.session.getUseWrapMode();
  493. };
  494. this.destroy = function() {
  495. clearInterval(this.$pollSizeChangesTimer);
  496. if (this.$measureNode)
  497. this.$measureNode.parentNode.removeChild(this.$measureNode);
  498. delete this.$measureNode;
  499. };
  500. }).call(Text.prototype);
  501. exports.Text = Text;
  502. });