Skip to content
Snippets Groups Projects
markdown.js 14.7 KiB
Newer Older
echicken's avatar
echicken committed
/*
echicken's avatar
echicken committed
 * Partial DokuWiki markup parser/renderer
echicken's avatar
echicken committed
 * === h4 ===
echicken's avatar
echicken committed
 * ===== h2 =====
 * ====== h1 ======
 * [[http://some.web.site/|some link text]]
 * {{http://some.web.site/image.png|some alt text}}
 * ** bold **
 * // italic //
 * __ underline __
echicken's avatar
echicken committed
 * '' monospaced ''
 * ** // __ '' bold italic underline monospace '' __ // **
echicken's avatar
echicken committed
 * > blockquote
 * * Unordered lists
 *  * With sub-items
 * - Ordered lists
 *  - With sub-items
echicken's avatar
echicken committed
 * Lines\\ with\\ forced\\ line\\ breaks
 * ^this^is^a^table^heading^
 * |this|is|a|table|row|
 * ^you|can|have|headings|anywhere|
echicken's avatar
echicken committed
 * To do:
 *  - nested blockquote in HTML
 *  - image links
 *  - code blocks
 *  - text conversion (HTML only probably)
echicken's avatar
echicken committed
 */
load('sbbsdefs.js');
load('table.js');
echicken's avatar
echicken committed

if (typeof Frame == 'undefined') Frame = false;

function Markdown(target, settings) {

echicken's avatar
echicken committed
  const state = {
    list_level : 0,
    links : [],
    images : [],
echicken's avatar
echicken committed
    footnotes : [],
echicken's avatar
echicken committed
    table : [],
    blockquote : false,
    list_stack : []
  };

  const config = {
    console : {
      bold_style : '\1h',
      italic_style : '\1r',
      underline_style : '\1g',
      heading_underline : true,
      heading_style : '\1h',
      link_style : '\1h\1c',
echicken's avatar
echicken committed
      image_style : '\1h\1m',
      footnote_style : '\1h\1y'
    },
    html : {
      a : '',
      ul : 'list-group',
      ol : 'list-group',
      li : 'list-group-item',
      table : 'table table-striped',
      thead : '',
      tbody : '',
      th : '',
      tr : '',
      td : '',
      img : '',
      hr : '',
      blockquote : 'blockquote'
    }
  };
  if (typeof settings == 'object') {
    if (typeof settings.console == 'object') {
      Object.keys(settings.console).forEach(function (e) {
        config.console[e] = settings.console[e];
      });
    }
    if (typeof settings.html == 'object') {
      Object.keys(settings.html).forEach(function (e) {
        config.html[e] = settings.html[e];
      });
    }
echicken's avatar
echicken committed
  if (Frame && target instanceof Frame) target.word_wrap = true;
echicken's avatar
echicken committed
  this.reset = function () {
    state.list_level = 0;
    state.links = [];
    state.images = [];
echicken's avatar
echicken committed
    state.footnotes = [];
echicken's avatar
echicken committed
    state.table = [];
    state.blockquote = false;
    state.list_stack = [];
  }

echicken's avatar
echicken committed
  Object.defineProperty(this, 'state', { get : function () {
    return state;
  }});
echicken's avatar
echicken committed
  Object.defineProperty(this, 'target', {
    get : function () {
      return target;
    },
    set : function (t) {
echicken's avatar
echicken committed
      this.reset();
echicken's avatar
echicken committed
      if (t == 'html') {
        target = t;
      } else if (Frame && t instanceof Frame) {
        target = t;
      } else if (
        typeof t.screen_columns == 'number' && typeof t.putmsg == 'function'
      ) {
        target = t;
      } else {
        throw 'Invalid output target';
      }
    }
  });
echicken's avatar
echicken committed
  Object.defineProperty(this, 'columns', { get : function () {
    if (target == 'html') {
      return 0;
    } else if (Frame && target instanceof Frame) {
      return target.width;
    } else {
      return target.screen_columns;
    }
  }});

  Object.defineProperty(this, 'config', { value : config });

}

Markdown.prototype.html_tag_format = function (tag, attributes) {
  var ret = '<' + tag;
  if (this.config.html[tag] != '') {
    ret += ' class="' + this.config.html[tag] + '"';
  }
  if (attributes) {
    Object.keys(attributes).forEach(function (e) {
      ret += ' ' + e + '="' + attributes[e] + '"'
    });
  }
  return ret + '>';
echicken's avatar
echicken committed
}

Markdown.prototype.render_text_console = function (text) {
  const self = this;
echicken's avatar
echicken committed
  return text.replace(/\*\*([^\*]+)\*\*/g, function (m, c) {
    return '\1+' + self.config.console.bold_style + c + '\1-';
echicken's avatar
echicken committed
  }).replace(/\/\/([^\/]+)\/\//g, function (m, c) {
    return '\1+' + self.config.console.italic_style + c + '\1-';
echicken's avatar
echicken committed
  }).replace(/__([^_]+)__/g, function (m, c) {
    return '\1+' + self.config.console.underline_style + c + '\1-';
echicken's avatar
echicken committed
  }).replace(/''([^']+)''/g, function (m, c) {
echicken's avatar
echicken committed
  }).replace(/\{\{(.+)\}\}/g, function (m, c) {
    c = c.split('|');
    self.state.images.push({ text : (c[1] || c[0]), link : c[0] });
    return '\1+' + self.config.console.image_style + (c[1] || c[0]) + ' [' + self.state.images.length + ']\1-';
echicken's avatar
echicken committed
  }).replace(/\[\[([^\]]+)\]\]/g, function (m, c) {
    c = c.split('|');
    self.state.links.push({ text : c[1] || c[0], link : c[0] });
    return '\1+' + self.config.console.link_style + (c[1] || c[0]) + ' [' + self.state.links.length + ']\1-';
echicken's avatar
echicken committed
  }).replace(/\(\(([^\)]+)\)\)/g, function (m, c) {
echicken's avatar
echicken committed
    self.state.footnotes.push(c);
    return '\1+' + self.config.console.footnote_style + '[' + self.state.footnotes.length + ']\1-';
echicken's avatar
echicken committed
  }).replace(/\\\\(\s|$)/g, '\r\n');
echicken's avatar
echicken committed
}

Markdown.prototype.render_text_html = function (text) {
  const self = this;
echicken's avatar
echicken committed
  return text.replace(/\\1.(.+)\\1./g, function (m, c) {
echicken's avatar
echicken committed
  }).replace(/\*\*([^\*]+)\*\*/g, function (m, c) {
echicken's avatar
echicken committed
    return '<b>' + c + '</b>';
echicken's avatar
echicken committed
  }).replace(/\/\/([^\/]+)\/\//g, function (m, c) {
echicken's avatar
echicken committed
  }).replace(/__([^_]+)__/g, function (m, c) {
    return '<span style="text-decoration:underline;">' + c + '</span>';
echicken's avatar
echicken committed
  }).replace(/''([^']+)''/g, function (m, c) {
    return '<code>' + c + '</code>';
echicken's avatar
echicken committed
  }).replace(/\{\{(.+)\}\}/g, function (m, c) {
    c = c.split('|');
    return self.html_tag_format('img', { alt : (c[1] || c[0]), src : c[0] });
echicken's avatar
echicken committed
  }).replace(/\[\[([^\]]+)\]\]/g, function (m, c) {
    c = c.split('|');
    return self.html_tag_format('a', { href : c[0] }) + (c[1] || c[0]) + '</a>';
echicken's avatar
echicken committed
  }).replace(/\(\(([^\)]+)\)\)/g, function (m, c) {
echicken's avatar
echicken committed
    self.state.footnotes.push(c);
    return self.html_tag_format('a', { href : '#f' + self.state.footnotes.length }) + ' [' + self.state.footnotes.length + ']</a>';
echicken's avatar
echicken committed
  }).replace(/\\\\(\s|$)/g, '<br>');
echicken's avatar
echicken committed
}

Markdown.prototype.render_table = function () {

  const self = this;
  const columns = []; // Length is number of columns, values are column widths
  this.state.table.forEach(function (e) {
    e.forEach(function (e, i) {
      const raw = strip_ctrl(e);
      const visible = raw ? raw.length : 0;
      if (columns.length < (i + 1)) {
        columns.push(visible);
      } else if (columns[i] < visible) {
        columns[i] = visible;
      }
    });
  });
  if (this.target == 'html') {
    var ret = this.html_tag_format('table');
echicken's avatar
echicken committed
    this.state.table.forEach(function (e, i, a) {
      ret += self.html_tag_format('tr');
echicken's avatar
echicken committed
      if (i == 0) ret += self.html_tag_format('thead');
echicken's avatar
echicken committed
      for (var n = 0; n < columns.length; n++) {
echicken's avatar
echicken committed
        if (e[n] == ':::') continue;
echicken's avatar
echicken committed
        if (e[n] == '') continue;
echicken's avatar
echicken committed
        var attr = {};
echicken's avatar
echicken committed
        var nr = i + 1;
        if (self.state.table[nr] && self.state.table[nr][n] == ':::') {
echicken's avatar
echicken committed
          attr.rowspan = 1;
          while (
echicken's avatar
echicken committed
            typeof self.state.table[nr] !== 'undefined'
            && self.state.table[nr][n] == ':::'
echicken's avatar
echicken committed
          ) {
            attr.rowspan++;
echicken's avatar
echicken committed
            nr++;
echicken's avatar
echicken committed
          }
          if (attr.rowspan < 2) delete attr.rowspan;
        }
echicken's avatar
echicken committed
        var nc = n + 1;
        if (typeof e[nc] != 'undefined' && e[nc] == '') {
          attr.colspan = 1;
          while (typeof e[nc] !== 'undefined' && e[nc] == '') {
            attr.colspan++;
            nc++;
          }
          if (attr.colspan < 2) delete attr.colspan;
        }
echicken's avatar
echicken committed
        if (e[n].search(/^\s\s+/) > -1) attr.style = "text-align:right;";
        if (e[n].search(/^\s\s+(.+)\s\s+$/) > -1) attr.style = "text-align:center;";
echicken's avatar
echicken committed
        var tt = i == 0 ? 'th' : 'td';
        var tag = [self.html_tag_format(tt, attr), '</' + tt + '/>'];
        ret += tag[0] + e[n] + tag[1];
echicken's avatar
echicken committed
      }
      ret += '</tr>';
      if (i == 0) ret += '</thead>';
echicken's avatar
echicken committed
    });
    ret += '</table><br>';
    this.state.table = [];
    return ret;
  } else {
    var ret = table(this.state.table);
echicken's avatar
echicken committed
    this.state.table = [];
    return ret;
echicken's avatar
echicken committed
  }

}

Markdown.prototype.render_line_console = function (line) {

  var match;
  const self = this;
  var ret = this.render_text_console(line);
echicken's avatar
echicken committed

  // Ordered and unordered lists
  match = ret.match(/^(\s*)(\*|-)\s+(.+)$/m);
echicken's avatar
echicken committed
  if (match !== null) {
echicken's avatar
echicken committed
    if (this.state.table.length) ret += this.render_table();
echicken's avatar
echicken committed
    if (match[2] == '*') {
      lt = 'ul';
    } else {
      lt = 'ol';
    }
echicken's avatar
echicken committed
    if (match[1].length > this.state.list_level) {
echicken's avatar
echicken committed
      this.state.list_level = match[1].length;
echicken's avatar
echicken committed
      if (lt == 'ol') this.state.list_stack[this.state.list_level] = 0;
echicken's avatar
echicken committed
    } else if (match[1].length < this.state.list_level) {
echicken's avatar
echicken committed
      if (lt == 'ol') this.state.list_stack.splice(this.state.list_level, 1);
echicken's avatar
echicken committed
      this.state.list_level = match[1].length;
echicken's avatar
echicken committed
    } else if (lt == 'ol') {
      if (typeof this.state.list_stack[this.state.list_level] != 'number') {
        this.state.list_stack[this.state.list_level] = 0;
      } else {
        this.state.list_stack[this.state.list_level]++;
      }
echicken's avatar
echicken committed
    }
    for (var n = 0; n < this.state.list_level; n++) {
      ret += this.config.console.list_indent;
echicken's avatar
echicken committed
    }
    if (lt == 'ul') {
      ret += match[2];
    } else {
echicken's avatar
echicken committed
      ret += (this.state.list_stack[this.state.list_level] + 1) + '.';
echicken's avatar
echicken committed
    return ret;
  }
  if (this.state.list_level) {
    ret += '\r\n';
    this.state.list_level = 0;
echicken's avatar
echicken committed
    this.state.list_stack = [];
  // Table
  const tre = /([|^])([^|^]+)(?=[|^])/g;
  match = tre.exec(ret);
  if (match !== null) {
    const _ret = match.input;
    const row = [];
    do {
      row.push(match[2]);
      match = tre.exec(ret);
    } while (match !== null);
    ret = ret.replace(_ret, '');
    return ret;
echicken's avatar
echicken committed
  } else if (this.state.table.length) {
    ret += this.render_table();
  }

  // Heading
  match = ret.match(/^(==+)([^=]+)==+\s*$/m);
echicken's avatar
echicken committed
  if (match !== null) {
    ret += '\1+';
    ret += this.config.console.heading_style;
    if (this.config.console.heading_underline) {
      ret += '\r\n';
      for (var n = 0; n < match[2].length; n++) {
        ret += user.settings&USER_NO_EXASCII ? '-' : ascii(196);
      }
    }
    ret += '\1-\r\n\r\n';
    return ret;
echicken's avatar
echicken committed
  }

  // Blockquote
  match = ret.match(/^\s*>\s(.+)$/m);
echicken's avatar
echicken committed
  if (match !== null) {
    return ret.replace(
      match[0], quote_msg(word_wrap(match[1]), this.columns - 1)
echicken's avatar
echicken committed
    ) + '\r\n';
  }

  // Horizontal Rule
  match = ret.match(/^----+$/m);
echicken's avatar
echicken committed
  if (match !== null) {
    var s = '';
    while (s.length < this.columns - 1) {
      s += user.settings&USER_NO_EXASCII ? '-' : ascii(196);
    return ret.replace(match[0], s) + '\r\n';
echicken's avatar
echicken committed

}

Markdown.prototype.render_line_html = function (line) {

  var match;
  const self = this;
echicken's avatar
echicken committed

  // Blockquote
  match = ret.match(/^\s*>\s(.+)$/m);
echicken's avatar
echicken committed
  if (match !== null) {
echicken's avatar
echicken committed
    if (this.state.table.length) ret += this.render_table();
    if (!this.state.blockquote) {
      ret += this.html_tag_format('blockquote');
echicken's avatar
echicken committed
      this.state.blockquote = true;
    }
    return ret + match[1];
  } else if (this.state.blockquote) {
    ret += '</blockquote>';
    this.state.blockquote = false;
  }

  // Ordered and unordered lists
  match = ret.match(/^(\s*)(\*|-)\s+(.+)$/m);
echicken's avatar
echicken committed
  if (match !== null) {
echicken's avatar
echicken committed
    if (this.state.table.length) ret += this.render_table();
    var lt = (match[2] == '*' ? 'ul' : 'ol');
    if (!match[1].length) {
      while (this.state.list_stack.length > 1) {
        ret += '</' + this.state.list_stack.pop() + '></li>';
echicken's avatar
echicken committed
      }
      if (this.state.list_stack.length < 1) {
        this.state.list_stack.push(lt);
        ret += this.html_tag_format(lt);
    } else if (match[1].length >= this.state.list_stack.length) {
echicken's avatar
echicken committed
      this.state.list_stack.push(lt);
      ret += this.html_tag_format('li');
      ret += this.html_tag_format(lt);
    ret += this.html_tag_format('li');
    ret += '</li>';
echicken's avatar
echicken committed
    return ret;
  }
  while (this.state.list_stack.length) {
    ret += '</' + this.state.list_stack.pop() + '>';
  }

  // Table
echicken's avatar
echicken committed
  const tre = /([|^])([^|^]*)(?=[|^])/g;
  match = tre.exec(ret);
  if (match !== null) {
    const _ret = match.input;
    const row = [];
    do {
      if (match[1] == '^') {
        // This is lousy, but if you want table headings to look special,
        // then include a 'doku_th' class in your stylesheet.
        // You're welcome.
        row.push('<span class="doku_th">' + match[2] + '</span>');
      } else {
        row.push(match[2]);
      }
      match = tre.exec(ret);
    } while (match !== null);
    ret = ret.replace(_ret, '');
    return ret;
echicken's avatar
echicken committed
  } else if (this.state.table.length) {
    ret += this.render_table();
  }

  // Heading
  match = ret.match(/^(==+)([^=]+)==+\s*$/m);
echicken's avatar
echicken committed
  if (match !== null) {
    ret = ret.replace(match[0], '');
    var lvl = 6 - Math.min(match[1].split(' ')[0].length, 5);
echicken's avatar
echicken committed
    ret += '<h' + lvl + '>';
echicken's avatar
echicken committed
    ret += '</h' + lvl + '>';
    return ret;
  }

  // Horizontal Rule
  match = ret.match(/^----+$/m);
echicken's avatar
echicken committed
  if (match !== null) {
    return ret.replace(match[0], '') + this.html_tag_format('hr');
echicken's avatar
echicken committed

}

Markdown.prototype.render_console = function (text) {
  const self = this;
  text.split(/\n/).forEach(function (e) {
    var line = self.render_line_console(e.replace(/\r$/, ''));
    if (typeof line == 'string') {
      self.target.putmsg(self.target instanceof Frame ? line : word_wrap(line, self.columns));
echicken's avatar
echicken committed
    }
  });
  if (this.state.links.length) {
    this.target.putmsg('\1+' + self.config.console.link_style + 'Links:\1-\r\n');
echicken's avatar
echicken committed
    this.state.links.forEach(function (e, i) {
      self.target.putmsg('\1+' + self.config.console.link_style + '[' + (i + 1) + '] ' + e.link + '\1-\r\n');
echicken's avatar
echicken committed
    });
    this.target.putmsg('\r\n');
  }
  if (this.state.images.length) {
    this.target.putmsg('\1+' + self.config.console.image_style + 'Images:\1-\r\n');
echicken's avatar
echicken committed
    this.state.images.forEach(function (e, i) {
      self.target.putmsg('\1+' + self.config.console.image_style + '[' + (i + 1) + '] ' + e.link + '\1-\r\n');
echicken's avatar
echicken committed
    });
    this.target.putmsg('\r\n');
  }
echicken's avatar
echicken committed
  if (this.state.footnotes.length) {
    this.target.putmsg('\1+' + self.config.console.footnote_style + 'Footnotes:\1-\r\n');
    this.state.footnotes.forEach(function (e, i) {
      self.target.putmsg('\1+' + self.config.console.footnote_style + '[' + (i + 1) + '] ' + e + '\1-\r\n');
    });
  }
echicken's avatar
echicken committed
}

Markdown.prototype.render_html = function (text) {
  const self = this;
  text.split(/\n/).forEach(function (e) {
    var line = self.render_line_html(e.replace(/\r$/, ''));
    if (typeof line == 'string') writeln(line);
  });
echicken's avatar
echicken committed
  if (this.state.footnotes.length) {
    writeln('<hr>Footnotes:<br>');
echicken's avatar
echicken committed
    this.state.footnotes.forEach(function (e, i) {
      writeln('<a id="f' + (i + 1) + '">[' + (i + 1) + '] ' + e + '</a><br>');
echicken's avatar
echicken committed
    });
  }
echicken's avatar
echicken committed
}

Markdown.prototype.render = function (text) {
  if (this.target == 'html') {
    this.render_html(text);
  } else {
    this.render_console(text);
  }
}