PageRenderTime 146ms CodeModel.GetById 30ms app.highlight 93ms RepoModel.GetById 13ms app.codeStats 0ms

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