PageRenderTime 68ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

/XML-DP-1.0/js/DPEditor.js

https://bitbucket.org/dpv140/phplib
JavaScript | 685 lines | 441 code | 70 blank | 174 comment | 191 complexity | 81e97d065b8177fcac81a27692822b34 MD5 | raw file
  1. /*
  2. * A live-editor, based on contentEditable
  3. *
  4. * @author Dayan Paez
  5. * @version 2012-12-15
  6. */
  7. /**
  8. * Creates a new four-way stack
  9. */
  10. function DPContextMap() {
  11. this.env = [];
  12. this.buf = [];
  13. this.sym = [];
  14. this.arg = [];
  15. }
  16. DPContextMap.prototype.count = function() {
  17. return this.env.length;
  18. };
  19. DPContextMap.prototype.unshift = function(env, buf, sym, arg) {
  20. this.env.unshift((env) ? env : null);
  21. this.buf.unshift((buf) ? buf : "");
  22. this.sym.unshift((sym) ? sym : "");
  23. this.arg.unshift((arg) ? arg : 0);
  24. };
  25. DPContextMap.prototype.shift = function() {
  26. return [this.env.shift(), this.buf.shift(), this.sym.shift(), this.arg.shift()];
  27. };
  28. /**
  29. * Similar map for nested lists
  30. */
  31. function DPList() {
  32. this.ul = [];
  33. this.li = [];
  34. this.sym = [];
  35. }
  36. DPList.prototype.count = function() { return this.ul.length; };
  37. DPList.prototype.unshift = function(ul, li, sym) {
  38. this.ul.unshift((ul) ? ul : null);
  39. this.li.unshift((li) ? li : null);
  40. this.sym.unshift((sym) ? sym : "");
  41. };
  42. DPList.prototype.shift = function() {
  43. return [this.ul.shift(), this.li.shift(), this.sym.shift()];
  44. };
  45. /**
  46. * Creates a new editor
  47. *
  48. * @param String ID the ID of the textarea to editorize
  49. * @param boolean doPreview true to create the preview pane
  50. */
  51. function DPEditor(id, doPreview) {
  52. this.myElement = document.getElementById(id);
  53. if (!this.myElement) {
  54. this.log("DPEditor: no element with ID " + id);
  55. return;
  56. }
  57. this.myID = id;
  58. // We strive towards XHTML compliance. As such, we expect the
  59. // source code to also be compliant, but that is the client's
  60. // job. For speed sake, we assume that the textarea can be wrapped
  61. // around a DIV precisely where it lives in the DOM.
  62. this.myContainer = this.newElement("div", {"class":"dpe-parent", "id":id + "_parent"});
  63. this.myElement.parentNode.replaceChild(this.myContainer, this.myElement);
  64. // Add wrapper
  65. this.myWrapper = this.newElement("div", {"class":"dpe-wrapper", "id":id + "_wrapper"}, this.myContainer);
  66. this.myWrapper.appendChild(this.myElement);
  67. // For backwards compatibility, assume preview requested
  68. this.myDisplay = null;
  69. if (doPreview == false) { return; }
  70. // templates
  71. this.oneast_tpl = this.newElement("h1");
  72. this.twoast_tpl = this.newElement("h2");
  73. this.thrast_tpl = this.newElement("h3");
  74. // figures class
  75. this.figure_class = null;
  76. this.myDisplay = this.newElement("div", {"class":"dpe-preview", "id":id + "_display"}, this.myContainer);
  77. var myObj = this;
  78. var listener = function(evt) { myObj.parse(); };
  79. this.myElement.onkeyup = listener;
  80. this.myElement.onfocus = listener;
  81. this.parse();
  82. }
  83. DPEditor.prototype.log = function(mes) {
  84. if (console.log) {
  85. console.log("DPEditor: " + mes);
  86. }
  87. };
  88. /**
  89. * If a paragraph consists of ONLY images, then it will receive the special 'figure' class
  90. *
  91. * @param String cl the class (specify null to bypass feature)
  92. */
  93. DPEditor.prototype.setFigureClass = function(cl) {
  94. this.figure_class = cl;
  95. };
  96. /**
  97. * Helper method to create elements
  98. *
  99. */
  100. DPEditor.prototype.newElement = function(tag, attrs, parentNode) {
  101. var elem = document.createElement(tag);
  102. if (attrs) {
  103. for (var attr in attrs)
  104. elem.setAttribute(attr, attrs[attr]);
  105. }
  106. if (parentNode)
  107. parentNode.appendChild(elem);
  108. return elem;
  109. };
  110. /**
  111. * Return the HTMLElement object for the given resource tag.
  112. *
  113. * The resource tag is the element in {TAG:...[,...]} environments.
  114. * This method allows subclasses to extend the list of such parsed
  115. * environments. The default understood values are 'img' (Ximg), 'a'
  116. * for (XA) and 'e', also for XA, with mailto: auto-prepended.
  117. *
  118. * When overriding this function, it is imperative to also override
  119. * the <pre>setResourceParam</pre> function as well.
  120. *
  121. * @param String tag alphanumeric string representing tag
  122. * @return HTMLElement|null null to indicate no tag recognized
  123. * @see setResourceParam
  124. */
  125. DPEditor.prototype.getResourceTag = function(tag) {
  126. switch (tag) {
  127. case "a":
  128. case "e":
  129. return this.newElement("a");
  130. case "img":
  131. return this.newElement("img");
  132. default:
  133. return null;
  134. }
  135. };
  136. /**
  137. * Set the resource's parameter number using provided content.
  138. *
  139. * @param int num either 0 or 1, at this point
  140. * @param HTMLElement env as returned by <pre>getResourceTag</pre>
  141. * @param String tag the tag used for the object
  142. * @param String cont the content to use
  143. * @param boolean close if this is the last argument
  144. * @see getResourceTag
  145. */
  146. DPEditor.prototype.setResourceParam = function(num, env, tag, cont, close) {
  147. switch (tag) {
  148. case "a":
  149. if (num > 0)
  150. env.appendChild(document.createTextNode(cont));
  151. else {
  152. env.setAttribute("href", cont);
  153. if (close)
  154. env.appendChild(document.createTextNode(cont));
  155. }
  156. return;
  157. case "e":
  158. if (num > 0)
  159. env.appendChild(document.createTextNode(cont));
  160. else {
  161. env.setAttribute("href", "mailto:" + cont);
  162. if (close)
  163. env.appendChild(document.createTextNode(cont));
  164. }
  165. return;
  166. case "img":
  167. if (num > 0)
  168. env.setAttribute("alt", cont);
  169. else {
  170. env.setAttribute("src", cont);
  171. if (close)
  172. env.setAttribute("alt", "Image: " + cont);
  173. }
  174. return;
  175. default:
  176. env.appendChild(document.createTextNode(cont));
  177. }
  178. };
  179. /**
  180. * Toggles parsing on/off (true/false) for resource.
  181. *
  182. * Some resources like images, use the second parameter for
  183. * attributes like alt-text, for which there is no inline
  184. * parsing. Others, like hyperlinks, allow inline elements to appear
  185. * as the second argument.
  186. *
  187. * @param int num the argument number
  188. * @param HTMLElement env the resource in question
  189. * @param String tag the tag used
  190. * @return boolean true if inline parsing should be allowed
  191. * @see getResourceTag
  192. * @see setResourceParam
  193. */
  194. DPEditor.prototype.getParseForParam = function(num, env, tag) {
  195. return !(env instanceof HTMLImageElement);
  196. };
  197. DPEditor.prototype.parse = function(evt) {
  198. // empty the display div
  199. if (this.myDisplay == null) {
  200. this.log("DPEditor: no display detected.");
  201. return;
  202. }
  203. while (this.myDisplay.hasChildNodes())
  204. this.myDisplay.removeChild(this.myDisplay.childNodes[0]);
  205. var input = this.myElement.value;
  206. if (this.preParse)
  207. input = this.preParse(input);
  208. input += "\n\n";
  209. var context = new DPContextMap();
  210. var num_new_lines = 0;
  211. // inside certain environments, the parsing rules are relaxed. For
  212. // instance, in the first argument of an A element (href attr), or
  213. // in both arguments of an IMG element (src and alt attrs).
  214. var do_parse = true;
  215. // index to provide an auto-generated alt text to images
  216. var image_num = 0;
  217. // stack of previous list environments in nested fashion. 'sym'
  218. // contains both the depth and the symbol used, e.g. ' - '
  219. var lists = new DPList();
  220. // table rows must be kept in a queue until they are added either
  221. // to the head or the body of the table
  222. var trows = [];
  223. var table = null;
  224. var row = null;
  225. var td = null;
  226. var env = null;
  227. // gobble up characters
  228. var len = input.length;
  229. var i = 0;
  230. var chr, inlist, buf;
  231. while (i < len) {
  232. chr = input[i];
  233. // beginning of "new" environment
  234. if (context.count() == 0) {
  235. inlist = (lists.count() > 0 && num_new_lines == 1);
  236. // ------------------------------------------------------------
  237. // Headings
  238. if (chr == "*" && !inlist) {
  239. // gobble up to the first non-asterisk
  240. buf = "";
  241. while (++i < len && input[i] == "*")
  242. buf += "*";
  243. if (i < len && input[i] == " ") {
  244. switch (buf.length) {
  245. case 0:
  246. context.unshift(this.oneast_tpl.cloneNode()); break;
  247. case 1:
  248. context.unshift(this.twoast_tpl.cloneNode()); break;
  249. case 2:
  250. context.unshift(this.thrast_tpl.cloneNode()); break;
  251. default:
  252. context.unshift(document.createElement("p"), buf);
  253. }
  254. lists = new DPList();
  255. i++;
  256. continue;
  257. }
  258. else
  259. i--;
  260. }
  261. // ------------------------------------------------------------
  262. // Tables
  263. else if (chr == "|" && !inlist) {
  264. lists = new DPList();
  265. // are we already in a table
  266. if (table == null) {
  267. table = this.newElement("table");
  268. trows = [];
  269. this.myDisplay.appendChild(table);
  270. }
  271. // are we already in a row?
  272. if (row == null) {
  273. row = this.newElement("tr");
  274. trows.push(row);
  275. }
  276. td = this.newElement("td", {}, row);
  277. context.unshift(td);
  278. i++;
  279. continue;
  280. }
  281. else if (chr == '-' && table != null) {
  282. // all previous rows belong in THEAD. All the cells thus far have been TD's,
  283. // but they need to be converted to TH's.
  284. env = this.newElement("thead");
  285. table.appendChild(env);
  286. for (var j = 0; j < trows.length; j++) {
  287. for (var k = 0; k < trows[j].childNodes.length; k++) {
  288. td = this.newElement("th");
  289. while (trows[j].childNodes[k].hasChildNodes())
  290. td.appendChild(trows[j].childNodes[k].childNodes[0]);
  291. env.appendChild(td);
  292. }
  293. }
  294. trows = [];
  295. // consume until the end of the line
  296. do { i++; } while (i < len && input[i] != "\n");
  297. i++;
  298. continue;
  299. }
  300. // ------------------------------------------------------------
  301. // Lists. These are complicated, because they can be nested
  302. // to any depth
  303. // ------------------------------------------------------------
  304. else if (chr == ' ') {
  305. buf = ''; // depth
  306. while (++i < len && input[i] == ' ')
  307. buf += ' ';
  308. if (i < len - 2) {
  309. var sub = input.substring(i, i + 2);
  310. if (sub == "- " || sub == "+ ") {
  311. var sym = (buf + sub);
  312. // if the previous environment is one of the lists,
  313. // then append this list item there. Recall that
  314. // we are more lenient with list items, allowing
  315. // one empty line between successive entries
  316. if (lists.count() == 0) {
  317. lists.unshift((sub == "- ") ? this.newElement("ul") : this.newElement("ol"), null, sym);
  318. this.myDisplay.appendChild(lists.ul[0]);
  319. }
  320. else if (lists.sym[0] == sym) {
  321. // most likely case: just another entry => do nothing here
  322. }
  323. else if (lists.sym[0].length < sym.length) {
  324. env = lists.li[0];
  325. lists.unshift((sub == "- ") ? this.newElement("ul") : this.newElement("ol"), null, sym);
  326. env.appendChild(lists.ul[0]);
  327. }
  328. else {
  329. // find the matching depth
  330. env = null;
  331. var j;
  332. for (j = 0; j < lists.count(); j++) {
  333. if (lists.sym[j] == sym) {
  334. env = lists.li[j];
  335. break;
  336. }
  337. }
  338. if (env != null) {
  339. for (var k = 0; k < j; k++)
  340. lists.shift();
  341. }
  342. else {
  343. // reverse compatibility: not actually a sublist,
  344. // but a misaligned -/+. Treat as regular text
  345. context.unshift(lists.li[0], (" " + sub), "", 0);
  346. i += 2;
  347. continue;
  348. }
  349. }
  350. context.unshift(this.newElement("li"));
  351. lists.ul[0].appendChild(context.env[0]);
  352. lists.li[0] = context.env[0];
  353. i += 2;
  354. continue;
  355. }
  356. }
  357. i -= buf.length;
  358. }
  359. else if (chr == " " || chr == "\t") {
  360. // trim whitespace
  361. i++;
  362. continue;
  363. }
  364. }
  365. // ------------------------------------------------------------
  366. // Table cell endings
  367. // ------------------------------------------------------------
  368. if (chr == "|" && context.env[0] instanceof HTMLTableCellElement) {
  369. // are we at the end of a line? Let the new-line handler do it
  370. if (i + 1 >= len || input[i + 1] == "\n") {
  371. i++;
  372. continue;
  373. }
  374. var cont = '';
  375. for (j = context.count() - 1; j >= 0; j--)
  376. cont += (context.sym[j] + context.buf[j]);
  377. context.env[0].appendChild(document.createTextNode(cont.trimRight()));
  378. context = new DPContextMap();
  379. continue;
  380. }
  381. // ------------------------------------------------------------
  382. // New lines? Are we at the end of some environment?
  383. // ------------------------------------------------------------
  384. if (chr == "\n") {
  385. num_new_lines++;
  386. num_envs = context.count();
  387. if (num_envs > 0) {
  388. env = context.env[num_envs - 1];
  389. if (num_new_lines >= 2 || env instanceof HTMLLIElement || env instanceof HTMLTableCellElement) {
  390. buf = '';
  391. for (j = num_envs - 1; j >= 0; j--)
  392. buf += (context.sym[j] + context.buf[j]);
  393. env.appendChild(document.createTextNode(buf.trimRight()));
  394. if (!(env instanceof HTMLLIElement || env instanceof HTMLTableCellElement)) {
  395. this.myDisplay.appendChild(env);
  396. // ------------------------------------------------------------
  397. // Handle special 'figures' case
  398. // ------------------------------------------------------------
  399. if (this.figure_class != null && env instanceof HTMLParagraphElement) {
  400. var is_figure = true;
  401. for (j = 0; j < env.childNodes.length; j++) {
  402. if (env.childNodes[j] instanceof HTMLImageElement)
  403. continue;
  404. if (env.childNodes[j].nodeType == Node.TEXT_NODE && env.childNodes[j].nodeValue.trim() == "")
  405. continue;
  406. is_figure = false;
  407. break;
  408. }
  409. if (is_figure)
  410. env.setAttribute("class", this.figure_class);
  411. }
  412. }
  413. context = new DPContextMap();
  414. if (env instanceof HTMLTableCellElement)
  415. row = null;
  416. }
  417. else // replace new line with space
  418. context.buf[0] += " ";
  419. }
  420. // hard reset the list
  421. if (num_new_lines >= 3)
  422. lists = new DPList();
  423. // hard reset the table
  424. if (table != null && num_new_lines >= 2) {
  425. var tbody = this.newElement("tbody", {}, table);
  426. for (j = 0; j < trows.length; j++)
  427. tbody.appendChild(trows[j]);
  428. table = null;
  429. }
  430. i++;
  431. continue;
  432. }
  433. // ------------------------------------------------------------
  434. // Create a P element by default
  435. // ------------------------------------------------------------
  436. if (context.count() == 0) {
  437. if (!inlist) {
  438. context.unshift(this.newElement("p"));
  439. lists = new DPList();
  440. }
  441. else {
  442. context.unshift(lists.li[0], ' ');
  443. }
  444. }
  445. // ------------------------------------------------------------
  446. // At this point, we have an environment to work with, now
  447. // consume characters according to inline rules
  448. // ------------------------------------------------------------
  449. if (chr == '\\' && (i + 1) < len) {
  450. var next = input[i + 1];
  451. var num_envs = context.count();
  452. env = context.env[num_envs - 1];
  453. if (next == "\n" && !(env instanceof HTMLTableCellElement)) {
  454. buf = '';
  455. for (j = num_envs - 1; j >= 0; j--)
  456. buf += (context.sym[j] + context.buf[j]);
  457. env.appendChild(document.createTextNode(buf.trimRight()));
  458. this.newElement("br", {}, env);
  459. // remove all but 'env'
  460. while (context.count() > 1)
  461. context.shift();
  462. context.buf[0] = '';
  463. i += 2;
  464. continue;
  465. }
  466. // Escape commas inside {...} elements
  467. else if (next == ",") {
  468. context.buf[0] += next;
  469. i += 2;
  470. continue;
  471. }
  472. }
  473. if (do_parse && (chr == "*" || chr == "/" || chr == "✂")) {
  474. // (possible) start of inline environment
  475. //
  476. // if not the first character, then previous must be word
  477. // boundary; there must be a 'next' character, and it must be
  478. // the beginning of a word; and it must not be the same
  479. // character; and the environment must not already be in use
  480. var a = context.buf[0];
  481. if (context.sym.indexOf(chr) < 0
  482. && (i + 1) < len
  483. && input[i + 1] != chr
  484. && input[i + 1] != " "
  485. && input[i + 1] != "\t"
  486. && (a == "" || /\B/.test(a.charAt(a.length - 1)))) {
  487. env = null;
  488. switch (chr) {
  489. case "*": env = this.newElement("strong"); break;
  490. case "/": env = this.newElement("em"); break;
  491. case "✂": env = this.newElement("del"); break;
  492. }
  493. context.unshift(env, "", chr, 0);
  494. i++;
  495. continue;
  496. }
  497. // (possible) end of inline environment. Check if any inline
  498. // environments in the stack are being closed, not just the
  499. // top one. Viz:
  500. //
  501. // Input: I *bought a /blue pony* mom.
  502. // Output: I <strong>bought a /blue pony</strong> mom.
  503. //
  504. // It would be wrong to wait for the <em> to close before
  505. // closing the <strong>
  506. var closed = false;
  507. for (j = 0; j < context.sym.length; j++) {
  508. if (context.sym[j] == chr) {
  509. closed = true;
  510. break;
  511. }
  512. }
  513. // do the closing by rebuilding j-th buffer with prior buffers
  514. // (if any) and appending j-th to parent
  515. if (closed) {
  516. context.env[j].appendChild(document.createTextNode(context.buf[j]));
  517. for (k = j - 1; k >= 0; k--) {
  518. context.env[j].appendChild(document.createTextNode(context.sym[k]));
  519. context.env[j].appendChild(document.createTextNode(context.buf[k]));
  520. }
  521. for (k = 0; k < j; k++)
  522. context.shift();
  523. // add myself to my parent and reset his buffer
  524. context.env[1].appendChild(document.createTextNode(context.buf[1]));
  525. context.env[1].appendChild(context.env[0]);
  526. context.buf[1] = "";
  527. context.shift();
  528. i++;
  529. continue;
  530. }
  531. } // end of */- inline
  532. // ------------------------------------------------------------
  533. // Opening {} environments
  534. // ------------------------------------------------------------
  535. if (do_parse && chr == "{") {
  536. // Attempt to find the resource tag: alphanumeric characters
  537. // followed by a colon (:), eg.g. "a:", "img:", etc
  538. var colon_i = input.indexOf(":", i);
  539. if (colon_i > (i + 1)) {
  540. var tag = input.substring(i + 1, colon_i);
  541. var xtag;
  542. if (/^[A-Za-z0-9]+$/.test(tag) && (xtag = this.getResourceTag(tag)) != null) {
  543. context.unshift(xtag, "", ("{" + tag + ":"), 0);
  544. i += 2 + tag.length;
  545. do_parse = false;
  546. continue;
  547. }
  548. }
  549. }
  550. // ------------------------------------------------------------
  551. // Closing {} environments?
  552. // ------------------------------------------------------------
  553. if (chr == "}") {
  554. // see note about */- elements
  555. closed = false;
  556. for (j = 0; j < context.sym.length; j++) {
  557. if (context.sym[j].length > 0 && context.sym[j][0] == "{") {
  558. closed = true;
  559. break;
  560. }
  561. }
  562. // do the closing by rebuilding j-th buffer
  563. if (closed) {
  564. cont = context.buf[j];
  565. for (k = j - 1; k >= 0; k--) {
  566. cont += context.sym[k];
  567. cont += context.buf[k];
  568. }
  569. for (k = 0; k < j; k++)
  570. context.shift();
  571. // set the second attribute depending on number of
  572. // arguments for this environment
  573. tag = context.sym[0].substring(1, context.sym[0].length - 1);
  574. this.setResourceParam(context.arg[0], context.env[0], tag, cont, true);
  575. // add myself to my parent and reset his buffer
  576. context.env[1].appendChild(document.createTextNode(context.buf[1]));
  577. context.env[1].appendChild(context.env[0]);
  578. context.buf[1] = "";
  579. context.shift();
  580. i++;
  581. do_parse = true;
  582. continue;
  583. }
  584. } // end closing environments
  585. // ------------------------------------------------------------
  586. // commas are important immediately inside A's and IMG's, as
  587. // they delineate between first and second argument
  588. //
  589. // The last condition limits two arguments per resource
  590. // ------------------------------------------------------------
  591. if (chr == "," && /^\{[A-Za-z0-9]+:$/.test(context.sym[0]) && context.arg[0] == 0) {
  592. tag = context.sym[0].substring(1, context.sym[0].length - 1);
  593. this.setResourceParam(0, context.env[0], tag, context.buf[0].trim(), false);
  594. do_parse = this.getParseForParam(1, context.env[0], tag);
  595. context.buf[0] = "";
  596. i++;
  597. context.arg[0] = 1;
  598. continue;
  599. }
  600. // ------------------------------------------------------------
  601. // empty space at the beginning of block environment have no meaning
  602. // ------------------------------------------------------------
  603. if ((chr == " " || chr == "\t") && context.count() == 0 && context.buf[0] == "") {
  604. i++;
  605. continue;
  606. }
  607. // ------------------------------------------------------------
  608. // Default action: append chr to buffer
  609. // ------------------------------------------------------------
  610. num_new_lines = 0;
  611. context.buf[0] += chr;
  612. i++;
  613. }
  614. // anything left: add it all
  615. };