bigtest.html 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296
  1. <html xmlns="http://www.w3.org/1999/xhtml">
  2. <head>
  3. <script src="js/codemirror.js" type="text/javascript"></script>
  4. <title>CodeMirror: JavaScript demonstration</title>
  5. <link rel="stylesheet" type="text/css" href="css/docs.css"/>
  6. </head>
  7. <body style="padding: 20px;">
  8. <p>This page demonstrates <a href="index.html">CodeMirror</a>'s
  9. JavaScript parser. Note that the ugly buttons at the top are not are
  10. not part of CodeMirror proper -- they demonstrate the way it can be
  11. embedded in a web-application.</p>
  12. <div class="border">
  13. <textarea id="code" cols="120" rows="30">
  14. /* The Editor object manages the content of the editable frame. It
  15. * catches events, colours nodes, and indents lines. This file also
  16. * holds some functions for transforming arbitrary DOM structures into
  17. * plain sequences of &lt;span> and &lt;br> elements
  18. */
  19. // Make sure a string does not contain two consecutive 'collapseable'
  20. // whitespace characters.
  21. function makeWhiteSpace(n) {
  22. var buffer = [], nb = true;
  23. for (; n > 0; n--) {
  24. buffer.push((nb || n == 1) ? nbsp : " ");
  25. nb = !nb;
  26. }
  27. return buffer.join("");
  28. }
  29. // Create a set of white-space characters that will not be collapsed
  30. // by the browser, but will not break text-wrapping either.
  31. function fixSpaces(string) {
  32. if (string.charAt(0) == " ") string = nbsp + string.slice(1);
  33. return string.replace(/[\t \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);});
  34. }
  35. function cleanText(text) {
  36. return text.replace(/\u00a0/g, " ").replace(/\u200b/g, "");
  37. }
  38. // Create a SPAN node with the expected properties for document part
  39. // spans.
  40. function makePartSpan(value, doc) {
  41. var text = value;
  42. if (value.nodeType == 3) text = value.nodeValue;
  43. else value = doc.createTextNode(text);
  44. var span = doc.createElement("SPAN");
  45. span.isPart = true;
  46. span.appendChild(value);
  47. span.currentText = text;
  48. return span;
  49. }
  50. // On webkit, when the last BR of the document does not have text
  51. // behind it, the cursor can not be put on the line after it. This
  52. // makes pressing enter at the end of the document occasionally do
  53. // nothing (or at least seem to do nothing). To work around it, this
  54. // function makes sure the document ends with a span containing a
  55. // zero-width space character. The traverseDOM iterator filters such
  56. // character out again, so that the parsers won't see them. This
  57. // function is called from a few strategic places to make sure the
  58. // zwsp is restored after the highlighting process eats it.
  59. var webkitLastLineHack = webkit ?
  60. function(container) {
  61. var last = container.lastChild;
  62. if (!last || !last.isPart || last.textContent != "\u200b")
  63. container.appendChild(makePartSpan("\u200b", container.ownerDocument));
  64. } : function() {};
  65. var Editor = (function(){
  66. // The HTML elements whose content should be suffixed by a newline
  67. // when converting them to flat text.
  68. var newlineElements = {"P": true, "DIV": true, "LI": true};
  69. function asEditorLines(string) {
  70. return fixSpaces(string.replace(/\t/g, " ").replace(/\u00a0/g, " ")).replace(/\r\n?/g, "\n").split("\n");
  71. }
  72. // Helper function for traverseDOM. Flattens an arbitrary DOM node
  73. // into an array of textnodes and &lt;br> tags.
  74. function simplifyDOM(root) {
  75. var doc = root.ownerDocument;
  76. var result = [];
  77. var leaving = true;
  78. function simplifyNode(node) {
  79. if (node.nodeType == 3) {
  80. var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/[\r\u200b]/g, "").replace(/\n/g, " "));
  81. if (text.length) leaving = false;
  82. result.push(node);
  83. }
  84. else if (node.nodeName == "BR" &amp;&amp; node.childNodes.length == 0) {
  85. leaving = true;
  86. result.push(node);
  87. }
  88. else {
  89. forEach(node.childNodes, simplifyNode);
  90. if (!leaving &amp;&amp; newlineElements.hasOwnProperty(node.nodeName)) {
  91. leaving = true;
  92. result.push(doc.createElement("BR"));
  93. }
  94. }
  95. }
  96. simplifyNode(root);
  97. return result;
  98. }
  99. // Creates a MochiKit-style iterator that goes over a series of DOM
  100. // nodes. The values it yields are strings, the textual content of
  101. // the nodes. It makes sure that all nodes up to and including the
  102. // one whose text is being yielded have been 'normalized' to be just
  103. // &lt;span> and &lt;br> elements.
  104. // See the story.html file for some short remarks about the use of
  105. // continuation-passing style in this iterator.
  106. function traverseDOM(start){
  107. function yield(value, c){cc = c; return value;}
  108. function push(fun, arg, c){return function(){return fun(arg, c);};}
  109. function stop(){cc = stop; throw StopIteration;};
  110. var cc = push(scanNode, start, stop);
  111. var owner = start.ownerDocument;
  112. var nodeQueue = [];
  113. // Create a function that can be used to insert nodes after the
  114. // one given as argument.
  115. function pointAt(node){
  116. var parent = node.parentNode;
  117. var next = node.nextSibling;
  118. return function(newnode) {
  119. parent.insertBefore(newnode, next);
  120. };
  121. }
  122. var point = null;
  123. // Insert a normalized node at the current point. If it is a text
  124. // node, wrap it in a &lt;span>, and give that span a currentText
  125. // property -- this is used to cache the nodeValue, because
  126. // directly accessing nodeValue is horribly slow on some browsers.
  127. // The dirty property is used by the highlighter to determine
  128. // which parts of the document have to be re-highlighted.
  129. function insertPart(part){
  130. var text = "\n";
  131. if (part.nodeType == 3) {
  132. select.snapshotChanged();
  133. part = makePartSpan(part, owner);
  134. text = part.currentText;
  135. }
  136. part.dirty = true;
  137. nodeQueue.push(part);
  138. point(part);
  139. return text;
  140. }
  141. // Extract the text and newlines from a DOM node, insert them into
  142. // the document, and yield the textual content. Used to replace
  143. // non-normalized nodes.
  144. function writeNode(node, c){
  145. var toYield = [];
  146. forEach(simplifyDOM(node), function(part) {
  147. toYield.push(insertPart(part));
  148. });
  149. return yield(toYield.join(""), c);
  150. }
  151. // Check whether a node is a normalized &lt;span> element.
  152. function partNode(node){
  153. if (node.isPart &amp;&amp; node.childNodes.length == 1 &amp;&amp; node.firstChild.nodeType == 3) {
  154. node.currentText = node.firstChild.nodeValue;
  155. return !/[\n\t\r]/.test(node.currentText);
  156. }
  157. return false;
  158. }
  159. // Handle a node. Add its successor to the continuation if there
  160. // is one, find out whether the node is normalized. If it is,
  161. // yield its content, otherwise, normalize it (writeNode will take
  162. // care of yielding).
  163. function scanNode(node, c){
  164. if (node.nextSibling)
  165. c = push(scanNode, node.nextSibling, c);
  166. if (partNode(node)){
  167. nodeQueue.push(node);
  168. return yield(node.currentText, c);
  169. }
  170. else if (node.nodeName == "BR") {
  171. nodeQueue.push(node);
  172. return yield("\n", c);
  173. }
  174. else {
  175. point = pointAt(node);
  176. removeElement(node);
  177. return writeNode(node, c);
  178. }
  179. }
  180. // MochiKit iterators are objects with a next function that
  181. // returns the next value or throws StopIteration when there are
  182. // no more values.
  183. return {next: function(){return cc();}, nodes: nodeQueue};
  184. }
  185. // Determine the text size of a processed node.
  186. function nodeSize(node) {
  187. if (node.nodeName == "BR")
  188. return 1;
  189. else
  190. return node.currentText.length;
  191. }
  192. // Search backwards through the top-level nodes until the next BR or
  193. // the start of the frame.
  194. function startOfLine(node) {
  195. while (node &amp;&amp; node.nodeName != "BR") node = node.previousSibling;
  196. return node;
  197. }
  198. function endOfLine(node, container) {
  199. if (!node) node = container.firstChild;
  200. else if (node.nodeName == "BR") node = node.nextSibling;
  201. while (node &amp;&amp; node.nodeName != "BR") node = node.nextSibling;
  202. return node;
  203. }
  204. // Replace all DOM nodes in the current selection with new ones.
  205. // Needed to prevent issues in IE where the old DOM nodes can be
  206. // pasted back into the document, still holding their old undo
  207. // information.
  208. function scrubPasted(container, start, start2) {
  209. var end = select.selectionTopNode(container, true),
  210. doc = container.ownerDocument;
  211. if (start != null &amp;&amp; start.parentNode != container) start = start2;
  212. if (start === false) start = null;
  213. if (start == end || !end || !container.firstChild) return;
  214. var clear = traverseDOM(start ? start.nextSibling : container.firstChild);
  215. while (end.parentNode == container) try{clear.next();}catch(e){break;}
  216. forEach(clear.nodes, function(node) {
  217. var newNode = node.nodeName == "BR" ? doc.createElement("BR") : makePartSpan(node.currentText, doc);
  218. container.replaceChild(newNode, node);
  219. });
  220. }
  221. // Client interface for searching the content of the editor. Create
  222. // these by calling CodeMirror.getSearchCursor. To use, call
  223. // findNext on the resulting object -- this returns a boolean
  224. // indicating whether anything was found, and can be called again to
  225. // skip to the next find. Use the select and replace methods to
  226. // actually do something with the found locations.
  227. function SearchCursor(editor, string, fromCursor) {
  228. this.editor = editor;
  229. this.history = editor.history;
  230. this.history.commit();
  231. // Are we currently at an occurrence of the search string?
  232. this.atOccurrence = false;
  233. // The object stores a set of nodes coming after its current
  234. // position, so that when the current point is taken out of the
  235. // DOM tree, we can still try to continue.
  236. this.fallbackSize = 15;
  237. var cursor;
  238. // Start from the cursor when specified and a cursor can be found.
  239. if (fromCursor &amp;&amp; (cursor = select.cursorPos(this.editor.container))) {
  240. this.line = cursor.node;
  241. this.offset = cursor.offset;
  242. }
  243. else {
  244. this.line = null;
  245. this.offset = 0;
  246. }
  247. this.valid = !!string;
  248. // Create a matcher function based on the kind of string we have.
  249. var target = string.split("\n"), self = this;;
  250. this.matches = (target.length == 1) ?
  251. // For one-line strings, searching can be done simply by calling
  252. // indexOf on the current line.
  253. function() {
  254. var match = cleanText(self.history.textAfter(self.line).slice(self.offset)).indexOf(string);
  255. if (match > -1)
  256. return {from: {node: self.line, offset: self.offset + match},
  257. to: {node: self.line, offset: self.offset + match + string.length}};
  258. } :
  259. // Multi-line strings require internal iteration over lines, and
  260. // some clunky checks to make sure the first match ends at the
  261. // end of the line and the last match starts at the start.
  262. function() {
  263. var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset));
  264. var match = firstLine.lastIndexOf(target[0]);
  265. if (match == -1 || match != firstLine.length - target[0].length)
  266. return false;
  267. var startOffset = self.offset + match;
  268. var line = self.history.nodeAfter(self.line);
  269. for (var i = 1; i &lt; target.length - 1; i++) {
  270. if (cleanText(self.history.textAfter(line)) != target[i])
  271. return false;
  272. line = self.history.nodeAfter(line);
  273. }
  274. if (cleanText(self.history.textAfter(line)).indexOf(target[target.length - 1]) != 0)
  275. return false;
  276. return {from: {node: self.line, offset: startOffset},
  277. to: {node: line, offset: target[target.length - 1].length}};
  278. };
  279. }
  280. SearchCursor.prototype = {
  281. findNext: function() {
  282. if (!this.valid) return false;
  283. this.atOccurrence = false;
  284. var self = this;
  285. // Go back to the start of the document if the current line is
  286. // no longer in the DOM tree.
  287. if (this.line &amp;&amp; !this.line.parentNode) {
  288. this.line = null;
  289. this.offset = 0;
  290. }
  291. // Set the cursor's position one character after the given
  292. // position.
  293. function saveAfter(pos) {
  294. if (self.history.textAfter(pos.node).length &lt; pos.offset) {
  295. self.line = pos.node;
  296. self.offset = pos.offset + 1;
  297. }
  298. else {
  299. self.line = self.history.nodeAfter(pos.node);
  300. self.offset = 0;
  301. }
  302. }
  303. while (true) {
  304. var match = this.matches();
  305. // Found the search string.
  306. if (match) {
  307. this.atOccurrence = match;
  308. saveAfter(match.from);
  309. return true;
  310. }
  311. this.line = this.history.nodeAfter(this.line);
  312. this.offset = 0;
  313. // End of document.
  314. if (!this.line) {
  315. this.valid = false;
  316. return false;
  317. }
  318. }
  319. },
  320. select: function() {
  321. if (this.atOccurrence) {
  322. select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to);
  323. select.scrollToCursor(this.editor.container);
  324. }
  325. },
  326. replace: function(string) {
  327. if (this.atOccurrence) {
  328. var end = this.editor.replaceRange(this.atOccurrence.from, this.atOccurrence.to, string);
  329. this.line = end.node;
  330. this.offset = end.offset;
  331. this.atOccurrence = false;
  332. }
  333. }
  334. };
  335. // The Editor object is the main inside-the-iframe interface.
  336. function Editor(options) {
  337. this.options = options;
  338. window.indentUnit = options.indentUnit;
  339. this.parent = parent;
  340. this.doc = document;
  341. var container = this.container = this.doc.body;
  342. this.win = window;
  343. this.history = new History(container, options.undoDepth, options.undoDelay,
  344. this, options.onChange);
  345. var self = this;
  346. if (!Editor.Parser)
  347. throw "No parser loaded.";
  348. if (options.parserConfig &amp;&amp; Editor.Parser.configure)
  349. Editor.Parser.configure(options.parserConfig);
  350. if (!options.readOnly)
  351. select.setCursorPos(container, {node: null, offset: 0});
  352. this.dirty = [];
  353. if (options.content)
  354. this.importCode(options.content);
  355. else // FF acts weird when the editable document is completely empty
  356. container.appendChild(this.doc.createElement("BR"));
  357. if (!options.readOnly) {
  358. if (options.continuousScanning !== false) {
  359. this.scanner = this.documentScanner(options.linesPerPass);
  360. this.delayScanning();
  361. }
  362. function setEditable() {
  363. // In IE, designMode frames can not run any scripts, so we use
  364. // contentEditable instead.
  365. if (document.body.contentEditable != undefined &amp;&amp; internetExplorer)
  366. document.body.contentEditable = "true";
  367. else
  368. document.designMode = "on";
  369. document.documentElement.style.borderWidth = "0";
  370. if (!options.textWrapping)
  371. container.style.whiteSpace = "nowrap";
  372. }
  373. // If setting the frame editable fails, try again when the user
  374. // focus it (happens when the frame is not visible on
  375. // initialisation, in Firefox).
  376. try {
  377. setEditable();
  378. }
  379. catch(e) {
  380. var focusEvent = addEventHandler(document, "focus", function() {
  381. focusEvent();
  382. setEditable();
  383. }, true);
  384. }
  385. addEventHandler(document, "keydown", method(this, "keyDown"));
  386. addEventHandler(document, "keypress", method(this, "keyPress"));
  387. addEventHandler(document, "keyup", method(this, "keyUp"));
  388. function cursorActivity() {self.cursorActivity(false);}
  389. addEventHandler(document.body, "mouseup", cursorActivity);
  390. addEventHandler(document.body, "paste", function(event) {
  391. cursorActivity();
  392. if (internetExplorer) {
  393. var text = null;
  394. try {text = window.clipboardData.getData("Text");}catch(e){}
  395. if (text != null) {
  396. self.replaceSelection(text);
  397. event.stop();
  398. }
  399. else {
  400. var start = select.selectionTopNode(self.container, true),
  401. start2 = start &amp;&amp; start.previousSibling;
  402. setTimeout(function(){scrubPasted(self.container, start, start2);}, 0);
  403. }
  404. }
  405. });
  406. addEventHandler(document.body, "cut", cursorActivity);
  407. if (this.options.autoMatchParens)
  408. addEventHandler(document.body, "click", method(this, "scheduleParenBlink"));
  409. }
  410. }
  411. function isSafeKey(code) {
  412. return (code >= 16 &amp;&amp; code &lt;= 18) || // shift, control, alt
  413. (code >= 33 &amp;&amp; code &lt;= 40); // arrows, home, end
  414. }
  415. Editor.prototype = {
  416. // Import a piece of code into the editor.
  417. importCode: function(code) {
  418. this.history.push(null, null, asEditorLines(code));
  419. this.history.reset();
  420. },
  421. // Extract the code from the editor.
  422. getCode: function() {
  423. if (!this.container.firstChild)
  424. return "";
  425. var accum = [];
  426. select.markSelection(this.win);
  427. forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
  428. webkitLastLineHack(this.container);
  429. select.selectMarked();
  430. return cleanText(accum.join(""));
  431. },
  432. checkLine: function(node) {
  433. if (node === false || !(node == null || node.parentNode == this.container))
  434. throw parent.CodeMirror.InvalidLineHandle;
  435. },
  436. cursorPosition: function(start) {
  437. if (start == null) start = true;
  438. var pos = select.cursorPos(this.container, start);
  439. if (pos) return {line: pos.node, character: pos.offset};
  440. else return {line: null, character: 0};
  441. },
  442. firstLine: function() {
  443. return null;
  444. },
  445. lastLine: function() {
  446. if (this.container.lastChild) return startOfLine(this.container.lastChild);
  447. else return null;
  448. },
  449. nextLine: function(line) {
  450. this.checkLine(line);
  451. var end = endOfLine(line, this.container);
  452. return end || false;
  453. },
  454. prevLine: function(line) {
  455. this.checkLine(line);
  456. if (line == null) return false;
  457. return startOfLine(line.previousSibling);
  458. },
  459. selectLines: function(startLine, startOffset, endLine, endOffset) {
  460. this.checkLine(startLine);
  461. var start = {node: startLine, offset: startOffset}, end = null;
  462. if (endOffset !== undefined) {
  463. this.checkLine(endLine);
  464. end = {node: endLine, offset: endOffset};
  465. }
  466. select.setCursorPos(this.container, start, end);
  467. select.scrollToCursor(this.container);
  468. },
  469. lineContent: function(line) {
  470. this.checkLine(line);
  471. var accum = [];
  472. for (line = line ? line.nextSibling : this.container.firstChild;
  473. line &amp;&amp; line.nodeName != "BR"; line = line.nextSibling)
  474. accum.push(nodeText(line));
  475. return cleanText(accum.join(""));
  476. },
  477. setLineContent: function(line, content) {
  478. this.history.commit();
  479. this.replaceRange({node: line, offset: 0},
  480. {node: line, offset: this.history.textAfter(line).length},
  481. content);
  482. this.addDirtyNode(line);
  483. this.scheduleHighlight();
  484. },
  485. insertIntoLine: function(line, position, content) {
  486. var before = null;
  487. if (position == "end") {
  488. before = endOfLine(line, this.container);
  489. }
  490. else {
  491. for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) {
  492. if (position == 0) {
  493. before = cur;
  494. break;
  495. }
  496. var text = (cur.innerText || cur.textContent || cur.nodeValue || "");
  497. if (text.length > position) {
  498. before = cur.nextSibling;
  499. content = text.slice(0, position) + content + text.slice(position);
  500. removeElement(cur);
  501. break;
  502. }
  503. position -= text.length;
  504. }
  505. }
  506. var lines = asEditorLines(content), doc = this.container.ownerDocument;
  507. for (var i = 0; i &lt; lines.length; i++) {
  508. if (i > 0) this.container.insertBefore(doc.createElement("BR"), before);
  509. this.container.insertBefore(makePartSpan(lines[i], doc), before);
  510. }
  511. this.addDirtyNode(line);
  512. this.scheduleHighlight();
  513. },
  514. // Retrieve the selected text.
  515. selectedText: function() {
  516. var h = this.history;
  517. h.commit();
  518. var start = select.cursorPos(this.container, true),
  519. end = select.cursorPos(this.container, false);
  520. if (!start || !end) return "";
  521. if (start.node == end.node)
  522. return h.textAfter(start.node).slice(start.offset, end.offset);
  523. var text = [h.textAfter(start.node).slice(start.offset)];
  524. for (pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos))
  525. text.push(h.textAfter(pos));
  526. text.push(h.textAfter(end.node).slice(0, end.offset));
  527. return cleanText(text.join("\n"));
  528. },
  529. // Replace the selection with another piece of text.
  530. replaceSelection: function(text) {
  531. this.history.commit();
  532. var start = select.cursorPos(this.container, true),
  533. end = select.cursorPos(this.container, false);
  534. if (!start || !end) return;
  535. end = this.replaceRange(start, end, text);
  536. select.setCursorPos(this.container, start, end);
  537. },
  538. replaceRange: function(from, to, text) {
  539. var lines = asEditorLines(text);
  540. lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0];
  541. var lastLine = lines[lines.length - 1];
  542. lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset);
  543. var end = this.history.nodeAfter(to.node);
  544. this.history.push(from.node, end, lines);
  545. return {node: this.history.nodeBefore(end),
  546. offset: lastLine.length};
  547. },
  548. getSearchCursor: function(string, fromCursor) {
  549. return new SearchCursor(this, string, fromCursor);
  550. },
  551. // Re-indent the whole buffer
  552. reindent: function() {
  553. if (this.container.firstChild)
  554. this.indentRegion(null, this.container.lastChild);
  555. },
  556. grabKeys: function(eventHandler, filter) {
  557. this.frozen = eventHandler;
  558. this.keyFilter = filter;
  559. },
  560. ungrabKeys: function() {
  561. this.frozen = "leave";
  562. this.keyFilter = null;
  563. },
  564. // Intercept enter and tab, and assign their new functions.
  565. keyDown: function(event) {
  566. if (this.frozen == "leave") this.frozen = null;
  567. if (this.frozen &amp;&amp; (!this.keyFilter || this.keyFilter(event.keyCode))) {
  568. event.stop();
  569. this.frozen(event);
  570. return;
  571. }
  572. var code = event.keyCode;
  573. // Don't scan when the user is typing.
  574. this.delayScanning();
  575. // Schedule a paren-highlight event, if configured.
  576. if (this.options.autoMatchParens)
  577. this.scheduleParenBlink();
  578. if (code == 13) { // enter
  579. if (event.ctrlKey) {
  580. this.reparseBuffer();
  581. }
  582. else {
  583. select.insertNewlineAtCursor(this.win);
  584. this.indentAtCursor();
  585. select.scrollToCursor(this.container);
  586. }
  587. event.stop();
  588. }
  589. else if (code == 9 &amp;&amp; this.options.tabMode != "default") { // tab
  590. this.handleTab(!event.ctrlKey &amp;&amp; !event.shiftKey);
  591. event.stop();
  592. }
  593. else if (code == 32 &amp;&amp; event.shiftKey &amp;&amp; this.options.tabMode == "default") { // space
  594. this.handleTab(true);
  595. event.stop();
  596. }
  597. else if ((code == 219 || code == 221) &amp;&amp; event.ctrlKey) {
  598. this.blinkParens(event.shiftKey);
  599. event.stop();
  600. }
  601. else if (event.metaKey &amp;&amp; (code == 37 || code == 39)) { // Meta-left/right
  602. var cursor = select.selectionTopNode(this.container);
  603. if (cursor === false || !this.container.firstChild) return;
  604. if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
  605. else {
  606. end = endOfLine(cursor, this.container);
  607. select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
  608. }
  609. event.stop();
  610. }
  611. else if (event.ctrlKey || event.metaKey) {
  612. if ((event.shiftKey &amp;&amp; code == 90) || code == 89) { // shift-Z, Y
  613. select.scrollToNode(this.history.redo());
  614. event.stop();
  615. }
  616. else if (code == 90 || code == 8) { // Z, backspace
  617. select.scrollToNode(this.history.undo());
  618. event.stop();
  619. }
  620. else if (code == 83 &amp;&amp; this.options.saveFunction) { // S
  621. this.options.saveFunction();
  622. event.stop();
  623. }
  624. }
  625. },
  626. // Check for characters that should re-indent the current line,
  627. // and prevent Opera from handling enter and tab anyway.
  628. keyPress: function(event) {
  629. var electric = /indent|default/.test(this.options.tabMode) &amp;&amp; Editor.Parser.electricChars;
  630. // Hack for Opera, and Firefox on OS X, in which stopping a
  631. // keydown event does not prevent the associated keypress event
  632. // from happening, so we have to cancel enter and tab again
  633. // here.
  634. if ((this.frozen &amp;&amp; (!this.keyFilter || this.keyFilter(event.keyCode))) ||
  635. event.code == 13 || (event.code == 9 &amp;&amp; this.options.tabMode != "default") ||
  636. (event.keyCode == 32 &amp;&amp; event.shiftKey &amp;&amp; this.options.tabMode == "default"))
  637. event.stop();
  638. else if (electric &amp;&amp; electric.indexOf(event.character) != -1)
  639. this.parent.setTimeout(method(this, "indentAtCursor"), 0);
  640. },
  641. // Mark the node at the cursor dirty when a non-safe key is
  642. // released.
  643. keyUp: function(event) {
  644. this.cursorActivity(isSafeKey(event.keyCode));
  645. },
  646. // Indent the line following a given &lt;br>, or null for the first
  647. // line. If given a &lt;br> element, this must have been highlighted
  648. // so that it has an indentation method. Returns the whitespace
  649. // element that has been modified or created (if any).
  650. indentLineAfter: function(start, direction) {
  651. // whiteSpace is the whitespace span at the start of the line,
  652. // or null if there is no such node.
  653. var whiteSpace = start ? start.nextSibling : this.container.firstChild;
  654. if (whiteSpace &amp;&amp; !hasClass(whiteSpace, "whitespace"))
  655. whiteSpace = null;
  656. // Sometimes the start of the line can influence the correct
  657. // indentation, so we retrieve it.
  658. var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
  659. var nextChars = (start &amp;&amp; firstText &amp;&amp; firstText.currentText) ? firstText.currentText : "";
  660. // Ask the lexical context for the correct indentation, and
  661. // compute how much this differs from the current indentation.
  662. var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0;
  663. if (direction != null &amp;&amp; this.options.tabMode == "shift")
  664. newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit)
  665. else if (start)
  666. newIndent = start.indentation(nextChars, curIndent, direction);
  667. else if (Editor.Parser.firstIndentation)
  668. newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction);
  669. var indentDiff = newIndent - curIndent;
  670. // If there is too much, this is just a matter of shrinking a span.
  671. if (indentDiff &lt; 0) {
  672. if (newIndent == 0) {
  673. if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild, 0);
  674. removeElement(whiteSpace);
  675. whiteSpace = null;
  676. }
  677. else {
  678. select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
  679. whiteSpace.currentText = makeWhiteSpace(newIndent);
  680. whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
  681. }
  682. }
  683. // Not enough...
  684. else if (indentDiff > 0) {
  685. // If there is whitespace, we grow it.
  686. if (whiteSpace) {
  687. whiteSpace.currentText = makeWhiteSpace(newIndent);
  688. whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
  689. }
  690. // Otherwise, we have to add a new whitespace node.
  691. else {
  692. whiteSpace = makePartSpan(makeWhiteSpace(newIndent), this.doc);
  693. whiteSpace.className = "whitespace";
  694. if (start) insertAfter(whiteSpace, start);
  695. else this.container.insertBefore(whiteSpace, this.container.firstChild);
  696. }
  697. if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true);
  698. }
  699. if (indentDiff != 0) this.addDirtyNode(start);
  700. return whiteSpace;
  701. },
  702. // Re-highlight the selected part of the document.
  703. highlightAtCursor: function() {
  704. var pos = select.selectionTopNode(this.container, true);
  705. var to = select.selectionTopNode(this.container, false);
  706. if (pos === false || to === false) return;
  707. select.markSelection(this.win);
  708. if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
  709. return false;
  710. select.selectMarked();
  711. return true;
  712. },
  713. // When tab is pressed with text selected, the whole selection is
  714. // re-indented, when nothing is selected, the line with the cursor
  715. // is re-indented.
  716. handleTab: function(direction) {
  717. if (this.options.tabMode == "spaces") {
  718. select.insertTabAtCursor(this.win);
  719. }
  720. else if (!select.somethingSelected(this.win)) {
  721. this.indentAtCursor(direction);
  722. }
  723. else {
  724. var start = select.selectionTopNode(this.container, true),
  725. end = select.selectionTopNode(this.container, false);
  726. if (start === false || end === false) return;
  727. this.indentRegion(start, end, direction);
  728. }
  729. },
  730. // Delay (or initiate) the next paren blink event.
  731. scheduleParenBlink: function() {
  732. if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
  733. var self = this;
  734. this.parenEvent = this.parent.setTimeout(function(){self.blinkParens();}, 300);
  735. },
  736. // Take the token before the cursor. If it contains a character in
  737. // '()[]{}', search for the matching paren/brace/bracket, and
  738. // highlight them in green for a moment, or red if no proper match
  739. // was found.
  740. blinkParens: function(jump) {
  741. // Clear the event property.
  742. if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
  743. this.parenEvent = null;
  744. // Extract a 'paren' from a piece of text.
  745. function paren(node) {
  746. if (node.currentText) {
  747. var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/);
  748. return match &amp;&amp; match[1];
  749. }
  750. }
  751. // Determine the direction a paren is facing.
  752. function forward(ch) {
  753. return /[\(\[\{]/.test(ch);
  754. }
  755. var ch, self = this, cursor = select.selectionTopNode(this.container, true);
  756. if (!cursor || !this.highlightAtCursor()) return;
  757. cursor = select.selectionTopNode(this.container, true);
  758. if (!(cursor &amp;&amp; ((ch = paren(cursor)) || (cursor = cursor.nextSibling) &amp;&amp; (ch = paren(cursor)))))
  759. return;
  760. // We only look for tokens with the same className.
  761. var className = cursor.className, dir = forward(ch), match = matching[ch];
  762. // Since parts of the document might not have been properly
  763. // highlighted, and it is hard to know in advance which part we
  764. // have to scan, we just try, and when we find dirty nodes we
  765. // abort, parse them, and re-try.
  766. function tryFindMatch() {
  767. var stack = [], ch, ok = true;;
  768. for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
  769. if (runner.className == className &amp;&amp; runner.nodeName == "SPAN" &amp;&amp; (ch = paren(runner))) {
  770. if (forward(ch) == dir)
  771. stack.push(ch);
  772. else if (!stack.length)
  773. ok = false;
  774. else if (stack.pop() != matching[ch])
  775. ok = false;
  776. if (!stack.length) break;
  777. }
  778. else if (runner.dirty || runner.nodeName != "SPAN" &amp;&amp; runner.nodeName != "BR") {
  779. return {node: runner, status: "dirty"};
  780. }
  781. }
  782. return {node: runner, status: runner &amp;&amp; ok};
  783. }
  784. // Temporarily give the relevant nodes a colour.
  785. function blink(node, ok) {
  786. node.style.fontWeight = "bold";
  787. node.style.color = ok ? "#8F8" : "#F88";
  788. self.parent.setTimeout(function() {node.style.fontWeight = ""; node.style.color = "";}, 500);
  789. }
  790. while (true) {
  791. var found = tryFindMatch();
  792. if (found.status == "dirty") {
  793. this.highlight(found.node, 1);
  794. // Needed because in some corner cases a highlight does not
  795. // reach a node.
  796. found.node.dirty = false;
  797. continue;
  798. }
  799. else {
  800. blink(cursor, found.status);
  801. if (found.node) {
  802. blink(found.node, found.status);
  803. if (jump) select.focusAfterNode(found.node.previousSibling, this.container);
  804. }
  805. break;
  806. }
  807. }
  808. },
  809. // Adjust the amount of whitespace at the start of the line that
  810. // the cursor is on so that it is indented properly.
  811. indentAtCursor: function(direction) {
  812. if (!this.container.firstChild) return;
  813. // The line has to have up-to-date lexical information, so we
  814. // highlight it first.
  815. if (!this.highlightAtCursor()) return;
  816. var cursor = select.selectionTopNode(this.container, false);
  817. // If we couldn't determine the place of the cursor,
  818. // there's nothing to indent.
  819. if (cursor === false)
  820. return;
  821. var lineStart = startOfLine(cursor);
  822. var whiteSpace = this.indentLineAfter(lineStart, direction);
  823. if (cursor == lineStart &amp;&amp; whiteSpace)
  824. cursor = whiteSpace;
  825. // This means the indentation has probably messed up the cursor.
  826. if (cursor == whiteSpace)
  827. select.focusAfterNode(cursor, this.container);
  828. },
  829. // Indent all lines whose start falls inside of the current
  830. // selection.
  831. indentRegion: function(start, end, direction) {
  832. var current = (start = startOfLine(start)), before = start &amp;&amp; startOfLine(start.previousSibling);
  833. if (end.nodeName != "BR") end = endOfLine(end, this.container);
  834. do {
  835. if (current) this.highlight(before, current, true);
  836. this.indentLineAfter(current, direction);
  837. before = current;
  838. current = endOfLine(current, this.container);
  839. } while (current != end);
  840. select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0});
  841. },
  842. // Find the node that the cursor is in, mark it as dirty, and make
  843. // sure a highlight pass is scheduled.
  844. cursorActivity: function(safe) {
  845. if (internetExplorer) {
  846. this.container.createTextRange().execCommand("unlink");
  847. this.selectionSnapshot = select.selectionCoords(this.win);
  848. }
  849. var activity = this.options.cursorActivity;
  850. if (!safe || activity) {
  851. var cursor = select.selectionTopNode(this.container, false);
  852. if (cursor === false || !this.container.firstChild) return;
  853. cursor = cursor || this.container.firstChild;
  854. if (activity) activity(cursor);
  855. if (!safe) {
  856. this.scheduleHighlight();
  857. this.addDirtyNode(cursor);
  858. }
  859. }
  860. },
  861. reparseBuffer: function() {
  862. forEach(this.container.childNodes, function(node) {node.dirty = true;});
  863. if (this.container.firstChild)
  864. this.addDirtyNode(this.container.firstChild);
  865. },
  866. // Add a node to the set of dirty nodes, if it isn't already in
  867. // there.
  868. addDirtyNode: function(node) {
  869. node = node || this.container.firstChild;
  870. if (!node) return;
  871. for (var i = 0; i &lt; this.dirty.length; i++)
  872. if (this.dirty[i] == node) return;
  873. if (node.nodeType != 3)
  874. node.dirty = true;
  875. this.dirty.push(node);
  876. },
  877. // Cause a highlight pass to happen in options.passDelay
  878. // milliseconds. Clear the existing timeout, if one exists. This
  879. // way, the passes do not happen while the user is typing, and
  880. // should as unobtrusive as possible.
  881. scheduleHighlight: function() {
  882. // Timeouts are routed through the parent window, because on
  883. // some browsers designMode windows do not fire timeouts.
  884. var self = this;
  885. this.parent.clearTimeout(this.highlightTimeout);
  886. this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
  887. },
  888. // Fetch one dirty node, and remove it from the dirty set.
  889. getDirtyNode: function() {
  890. while (this.dirty.length > 0) {
  891. var found = this.dirty.pop();
  892. // IE8 sometimes throws an unexplainable 'invalid argument'
  893. // exception for found.parentNode
  894. try {
  895. // If the node has been coloured in the meantime, or is no
  896. // longer in the document, it should not be returned.
  897. while (found &amp;&amp; found.parentNode != this.container)
  898. found = found.parentNode
  899. if (found &amp;&amp; (found.dirty || found.nodeType == 3))
  900. return found;
  901. } catch (e) {}
  902. }
  903. return null;
  904. },
  905. // Pick dirty nodes, and highlight them, until
  906. // options.linesPerPass lines have been highlighted. The highlight
  907. // method will continue to next lines as long as it finds dirty
  908. // nodes. It returns an object indicating the amount of lines
  909. // left, and information about the place where it stopped. If
  910. // there are dirty nodes left after this function has spent all
  911. // its lines, it shedules another highlight to finish the job.
  912. highlightDirty: function(force) {
  913. // Prevent FF from raising an error when it is firing timeouts
  914. // on a page that's no longer loaded.
  915. if (!window.select) return;
  916. var lines = force ? Infinity : this.options.linesPerPass;
  917. if (!this.options.readOnly) select.markSelection(this.win);
  918. var start;
  919. while (lines > 0 &amp;&amp; (start = this.getDirtyNode())){
  920. var result = this.highlight(start, lines);
  921. if (result) {
  922. lines = result.left;
  923. if (result.node &amp;&amp; result.dirty)
  924. this.addDirtyNode(result.node);
  925. }
  926. }
  927. if (!this.options.readOnly) select.selectMarked();
  928. if (start)
  929. this.scheduleHighlight();
  930. return this.dirty.length == 0;
  931. },
  932. // Creates a function that, when called through a timeout, will
  933. // continuously re-parse the document.
  934. documentScanner: function(linesPer) {
  935. var self = this, pos = null;
  936. return function() {
  937. // If the current node is no longer in the document... oh
  938. // well, we start over.
  939. if (pos &amp;&amp; pos.parentNode != self.container)
  940. pos = null;
  941. select.markSelection(self.win);
  942. var result = self.highlight(pos, linesPer, true);
  943. select.selectMarked();
  944. var newPos = result ? (result.node &amp;&amp; result.node.nextSibling) : null;
  945. pos = (pos == newPos) ? null : newPos;
  946. self.delayScanning();
  947. };
  948. },
  949. // Starts the continuous scanning process for this document after
  950. // a given interval.
  951. delayScanning: function() {
  952. if (this.scanner) {
  953. this.parent.clearTimeout(this.documentScan);
  954. this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
  955. }
  956. },
  957. // The function that does the actual highlighting/colouring (with
  958. // help from the parser and the DOM normalizer). Its interface is
  959. // rather overcomplicated, because it is used in different
  960. // situations: ensuring that a certain line is highlighted, or
  961. // highlighting up to X lines starting from a certain point. The
  962. // 'from' argument gives the node at which it should start. If
  963. // this is null, it will start at the beginning of the document.
  964. // When a number of lines is given with the 'target' argument, it
  965. // will highlight no more than that amount of lines. If this
  966. // argument holds a DOM node, it will highlight until it reaches
  967. // that node. If at any time it comes across a 'clean' line (no
  968. // dirty nodes), it will stop, except when 'cleanLines' is true.
  969. highlight: function(from, target, cleanLines, maxBacktrack){
  970. var container = this.container, self = this, active = this.options.activeTokens;
  971. var lines = (typeof target == "number" ? target : null);
  972. if (!container.firstChild)
  973. return;
  974. // Backtrack to the first node before from that has a partial
  975. // parse stored.
  976. while (from &amp;&amp; (!from.parserFromHere || from.dirty)) {
  977. from = from.previousSibling;
  978. if (maxBacktrack != null &amp;&amp; from.nodeName == "BR" &amp;&amp; (--maxBacktrack) &lt; 0)
  979. return false;
  980. }
  981. // If we are at the end of the document, do nothing.
  982. if (from &amp;&amp; !from.nextSibling)
  983. return;
  984. // Check whether a part (&lt;span> node) and the corresponding token
  985. // match.
  986. function correctPart(token, part){
  987. return !part.reduced &amp;&amp; part.currentText == token.value &amp;&amp; part.className == token.style;
  988. }
  989. // Shorten the text associated with a part by chopping off
  990. // characters from the front. Note that only the currentText
  991. // property gets changed. For efficiency reasons, we leave the
  992. // nodeValue alone -- we set the reduced flag to indicate that
  993. // this part must be replaced.
  994. function shortenPart(part, minus){
  995. part.currentText = part.currentText.substring(minus);
  996. part.reduced = true;
  997. }
  998. // Create a part corresponding to a given token.
  999. function tokenPart(token){
  1000. var part = makePartSpan(token.value, self.doc);
  1001. part.className = token.style;
  1002. return part;
  1003. }
  1004. function maybeTouch(node) {
  1005. if (node) {
  1006. if (node.nextSibling != node.oldNextSibling) {
  1007. self.history.touch(node);
  1008. node.oldNextSibling = node.nextSibling;
  1009. }
  1010. }
  1011. else {
  1012. if (self.container.firstChild != self.container.oldFirstChild) {
  1013. self.history.touch(node);
  1014. self.container.oldFirstChild = self.container.firstChild;
  1015. }
  1016. }
  1017. }
  1018. // Get the token stream. If from is null, we start with a new
  1019. // parser from the start of the frame, otherwise a partial parse
  1020. // is resumed.
  1021. var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
  1022. stream = stringStream(traversal),
  1023. parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
  1024. // parts is an interface to make it possible to 'delay' fetching
  1025. // the next DOM node until we are completely done with the one
  1026. // before it. This is necessary because often the next node is
  1027. // not yet available when we want to proceed past the current
  1028. // one.
  1029. var parts = {
  1030. current: null,
  1031. // Fetch current node.
  1032. get: function(){
  1033. if (!this.current)
  1034. this.current = traversal.nodes.shift();
  1035. return this.current;
  1036. },
  1037. // Advance to the next part (do not fetch it yet).
  1038. next: function(){
  1039. this.current = null;
  1040. },
  1041. // Remove the current part from the DOM tree, and move to the
  1042. // next.
  1043. remove: function(){
  1044. container.removeChild(this.get());
  1045. this.current = null;
  1046. },
  1047. // Advance to the next part that is not empty, discarding empty
  1048. // parts.
  1049. getNonEmpty: function(){
  1050. var part = this.get();
  1051. // Allow empty nodes when they are alone on a line, needed
  1052. // for the FF cursor bug workaround (see select.js,
  1053. // insertNewlineAtCursor).
  1054. while (part &amp;&amp; part.nodeName == "SPAN" &amp;&amp; part.currentText == "") {
  1055. var old = part;
  1056. this.remove();
  1057. part = this.get();
  1058. // Adjust selection information, if any. See select.js for details.
  1059. select.snapshotMove(old.firstChild, part &amp;&amp; (part.firstChild || part), 0);
  1060. }
  1061. return part;
  1062. }
  1063. };
  1064. var lineDirty = false, prevLineDirty = true, lineNodes = 0;
  1065. // This forEach loops over the tokens from the parsed stream, and
  1066. // at the same time uses the parts object to proceed through the
  1067. // corresponding DOM nodes.
  1068. forEach(parsed, function(token){
  1069. var part = parts.getNonEmpty();
  1070. if (token.value == "\n"){
  1071. // The idea of the two streams actually staying synchronized
  1072. // is such a long shot that we explicitly check.
  1073. if (part.nodeName != "BR")
  1074. throw "Parser out of sync. Expected BR.";
  1075. if (part.dirty || !part.indentation) lineDirty = true;
  1076. maybeTouch(from);
  1077. from = part;
  1078. // Every &lt;br> gets a copy of the parser state and a lexical
  1079. // context assigned to it. The first is used to be able to
  1080. // later resume parsing from this point, the second is used
  1081. // for indentation.
  1082. part.parserFromHere = parsed.copy();
  1083. part.indentation = token.indentation;
  1084. part.dirty = false;
  1085. // If the target argument wasn't an integer, go at least
  1086. // until that node.
  1087. if (lines == null &amp;&amp; part == target) throw StopIteration;
  1088. // A clean line with more than one node means we are done.
  1089. // Throwing a StopIteration is the way to break out of a
  1090. // MochiKit forEach loop.
  1091. if ((lines != null &amp;&amp; --lines &lt;= 0) || (!lineDirty &amp;&amp; !prevLineDirty &amp;&amp; lineNodes > 1 &amp;&amp; !cleanLines))
  1092. throw StopIteration;
  1093. prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0;
  1094. parts.next();
  1095. }
  1096. else {
  1097. if (part.nodeName != "SPAN")
  1098. throw "Parser out of sync. Expected SPAN.";
  1099. if (part.dirty)
  1100. lineDirty = true;
  1101. lineNodes++;
  1102. // If the part matches the token, we can leave it alone.
  1103. if (correctPart(token, part)){
  1104. part.dirty = false;
  1105. parts.next();
  1106. }
  1107. // Otherwise, we have to fix it.
  1108. else {
  1109. lineDirty = true;
  1110. // Insert the correct part.
  1111. var newPart = tokenPart(token);
  1112. container.insertBefore(newPart, part);
  1113. if (active) active(newPart, token, self);
  1114. var tokensize = token.value.length;
  1115. var offset = 0;
  1116. // Eat up parts until the text for this token has been
  1117. // removed, adjusting the stored selection info (see
  1118. // select.js) in the process.
  1119. while (tokensize > 0) {
  1120. part = parts.get();
  1121. var partsize = part.currentText.length;
  1122. select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
  1123. if (partsize > tokensize){
  1124. shortenPart(part, tokensize);
  1125. tokensize = 0;
  1126. }
  1127. else {
  1128. tokensize -= partsize;
  1129. offset += partsize;
  1130. parts.remove();
  1131. }
  1132. }
  1133. }
  1134. }
  1135. });
  1136. maybeTouch(from);
  1137. webkitLastLineHack(this.container);
  1138. // The function returns some status information that is used by
  1139. // hightlightDirty to determine whether and where it has to
  1140. // continue.
  1141. return {left: lines,
  1142. node: parts.getNonEmpty(),
  1143. dirty: lineDirty};
  1144. }
  1145. };
  1146. return Editor;
  1147. })();
  1148. addEventHandler(window, "load", function() {
  1149. var CodeMirror = window.frameElement.CodeMirror;
  1150. CodeMirror.editor = new Editor(CodeMirror.options);
  1151. this.parent.setTimeout(method(CodeMirror, "init"), 0);
  1152. });
  1153. </textarea>
  1154. </div>
  1155. <script type="text/javascript">
  1156. var textarea = document.getElementById('code');
  1157. var editor = new CodeMirror(CodeMirror.replace(textarea), {
  1158. height: "750px",
  1159. width: "100%",
  1160. content: textarea.value,
  1161. parserfile: ["tokenizejavascript.js", "parsejavascript.js"],
  1162. stylesheet: "css/jscolors.css",
  1163. path: "js/",
  1164. autoMatchParens: true,
  1165. initCallback: function(editor){editor.win.document.body.lastChild.scrollIntoView();}
  1166. });
  1167. </script>
  1168. </body>
  1169. </html>