"use strict";
/**
* @constructor
* @classdesc This class represents an official realization of KozMUL (Kozalo's Mark-Up Language). It lets you use {@link https://daringfireball.net/projects/markdown Markdown}-like syntax to mark-up your documents.
* Requires KozUtils, KozStrUtils and KozExceptions libraries.
* @param {Object=} operations Determines which transformations will be applied to text. See the example below.
* @param {Object=} options Currently, there is only one option: targetBlank. If you set this field to true, all links will be transformed with 'target="_blank"' attribute and, thus, opened in new tabs.
* @throws Either KozExceptions.unresolvedDependency (if it exists in the scope) or a string exception.
*
* @example
* // By default, the following object is used as an "operations" object:
* {
* transformHTMLEntities: true,
* transformBasicStyleTags: true,
* transformTypographicalCharacters: true,
* transformSubscriptAndSuperscriptTags: true,
* transformParagraphs: true,
* transformLists: true,
* transformQuotations: true,
* transformHorizontalLines: false,
* transformHeadings: false,
* transformLinks: false,
* transformImages: false,
* transformTables: false,
* transformCodeTags: false,
* transformSubstitutions: false
* }
*
* // And the following one as "options":
* {
* targetBlank: false,
* tableClasses: ['table', 'bordered-table'],
* listsWithIndentations: true,
* mostImportantHeading: 3
* }
*
* // You are allowed to pass your own object consisting of fields you want to change. For instance, lets create an object performing all transformations and using targetBlank option:
* let processor = new KozMUL({
* transformHorizontalLines: true,
* transformHeadings: true,
* transformLinks: true,
* transformImages: true,
* transformTables: true,
* transformCodeTags: true,
* transformSubstitutions: true
* }, {
* targetBlank: true
* });
*
* @author Kozalo <kozalo@yandex.ru>
* @copyright Leonid Kozarin, 2016
*/
let KozMUL = function(operations, options) {
if (typeof(KozExceptions) == "undefined")
throw "Unresolved dependency: KozExceptions!";
else if (typeof(KozUtils) == "undefined")
throw new KozExceptions.unresolvedDependency("KozUtils");
else if (String.prototype.matchAll === undefined)
throw new KozExceptions.unresolvedDependency("KozStrUtils");
this._operations = KozUtils.initOptions(operations, {
transformHTMLEntities: true,
transformBasicStyleTags: true,
transformTypographicalCharacters: true,
transformSubscriptAndSuperscriptTags: true,
transformParagraphs: true,
transformLists: true,
transformQuotations: true,
transformHorizontalLines: false,
transformHeadings: false,
transformLinks: false,
transformImages: false,
transformTables: false,
transformCodeTags: false,
transformSubstitutions: false
});
this._operations.toSingleLine = true;
this._operations.basicTransformations = true;
this._operations.removeEscapingCharacters = true;
this._options = KozUtils.initOptions(options, {
targetBlank: false,
tableClasses: ['table', 'table-bordered'],
listsWithIndentations: true,
mostImportantHeading: 3
});
};
/**
* Usually, you should use this method to convert your text to HTML. It executes all transformations determined in the constructor.
* @param {string} text
* @returns {string}
* @memberof KozMUL
*/
KozMUL.prototype.toHTML = function(text) {
if (typeof(text) !== "string")
throw new KozExceptions.invalidArgument({name: 'text', value: text}, 'KozMUL.toHTML(): the "text" argument must be a string!');
let commandsOrder = [
"toSingleLine",
"transformHTMLEntities",
"transformHorizontalLines",
"transformLists",
"transformHeadings",
"transformBasicStyleTags",
"transformTypographicalCharacters",
"transformSubscriptAndSuperscriptTags",
"transformQuotations",
"transformTables",
"transformCodeTags",
"transformSubstitutions",
"transformParagraphs",
"basicTransformations",
"transformLinks",
"transformImages",
"removeEscapingCharacters"
];
let self = this;
commandsOrder.forEach(function(command) {
if (self._operations[command])
text = self['_' + command](text);
});
return text;
};
/**
* Removes redundant paragraphs around blockquotes and redundant line break at the end of the text if it exists. Look at the example below to see other transformations which are performed by this method.
*
* @example
* some collocation => some collocation
* [[eol]] => <br/>
*
* @param {string} text
* @returns {string}
* @memberof KozMUL
*/
KozMUL.prototype._basicTransformations = function(text) {
return text
.replace(/([^ ]) {2}([^ ])/g, "$1 $2")
.replace(/\[\[eol]](<\/p>)?$/i, "$1")
.replace(/\[\[eol]]/gi, "<br/>")
.replace(/(<p>)?<blockquote><\/p>/gi, "<blockquote>")
.replace(/<p><\/blockquote>(<\/p>)?/gi, "</blockquote>");
};
/**
* As the name says, this function removes escaping characters (\\).
*
* @param {string} text
* @returns {string}
* @memberof KozMUL
*/
KozMUL.prototype._removeEscapingCharacters = function(text) {
return text.replace(/\\{2}/g, "");
};
/**
* Gets rid of all HTML tags and does some replaces (see the example below).
*
* @example
* & => &
* < и > => < и >
* " => "
* ' => '
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._transformHTMLEntities = function(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
/**
* Look at the example below to see what exactly this method replaces.
*
* @example
* **bold text** => <strong>bold text</strong>
* __italic text__ => <em>italic text</em>
* ~~strikethrough text~~ => <s>strikethrough text</s>
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._transformBasicStyleTags = function(text) {
return text
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
.replace(/__(.*?)__/g, "<em>$1</em>")
.replace(/~~(.*?)~~/g, "<s>$1</s>");
};
/**
* Look at the example below to see what exactly this method replaces.
*
* @example
* -- => — (—)
* << => « («)
* >> => » (»)
* (c) => © (©)
* (r) => ® (®)
* (tm) => ™ (™)
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._transformTypographicalCharacters = function(text) {
return text
.replace(/--/g, "—")
.replace(/<</g, "«")
.replace(/>>/g, "»")
.replace(/\(c\)/gi, "©")
.replace(/\(r\)/gi, "®")
.replace(/\(tm\)/gi, "™");
};
/**
* Look at the example below to see what exactly this method replaces.
*
* @example
* H.{2}O. 3^{2}=9 => H<sub>2</sub>O. 3<sup>2</sup>=9
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._transformSubscriptAndSuperscriptTags = function(text) {
return text
.replace(/\^{(.+?)}/g, "<sup>$1</sup>")
.replace(/\.{(.+?)}/g, "<sub>$1</sub>");
};
/**
* Replaces all line breaks with [[eol]]s.
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._toSingleLine = function(text) {
return text.replace(/\n/g, '[[eol]]');
};
/**
* Wraps text blocks with <p>-tags. The method tries to be as smart as possible: it extracts headings, lists and blockquotes from paragraphs to adhere the HTML specification.
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._transformParagraphs = function(text) {
let normalizedText = text.replace(/(\[\[eol]]){3,}/gi, "[[eol]][[eol]]");
let arr = normalizedText.split('[[eol]][[eol]]');
let newArr = [];
arr.forEach(function(p) {
let heading = '';
let hIndex;
// ~n = -(n+1) => ~-1 = 0
while (~ (hIndex = p.search(/<\/h[3-4]>/gi)) ) {
heading += p.substr(0, hIndex + 5);
p = p.substr(hIndex + 5);
}
if (p !== "")
newArr.push(heading + '<p>' + p + '</p>');
else
newArr.push(heading);
});
// The code below is supposed to extract lists and blockquotes from paragraphs since this is specified by the HTML specification.
let textWithParagraphs = newArr.join('')
.replace(/\[\[eol]](<[ou]l>)/gi, "</p>$1")
.replace(/<p>(<[ou]l>)/gi, "$1")
.replace(/<blockquote>(<\/p>)?/gi, "</p><blockquote>")
.replace(/(<p>)?<\/blockquote>/gi, "</blockquote><p>");
let matchedLists = textWithParagraphs.matchAll(/<\/[ou]l>/g);
let offset = 0;
matchedLists.forEach(function(obj) {
let next4chars = textWithParagraphs.substr(obj.index + offset + 5, 4);
let next7chars = textWithParagraphs.substr(obj.index + offset + 5, 7);
if (next7chars == "[[eol]]") {
let i = obj.index + offset + obj.string.length;
textWithParagraphs = textWithParagraphs.substr(0, i) + "<p>" + textWithParagraphs.substr(i + next7chars.length);
offset -= next7chars.length - 3;
}
else if (next4chars != "<ol>" && next4chars != "<ul>" && next4chars != "<li>") {
let i = obj.index + offset + obj.string.length;
textWithParagraphs = textWithParagraphs.substr(0, i) + "<p>" + textWithParagraphs.substr(i);
offset += 3;
}
});
let openedOverClosed = textWithParagraphs.matchAll("<p>").length - textWithParagraphs.matchAll("</p>").length;
for (let i = 0; i < openedOverClosed; i++)
textWithParagraphs += "</p>";
return textWithParagraphs
.replace(/<p>\[\[eol]]/gi, "<p>")
.replace(/<p><\/p>/gi, "")
.replace(/\[\[eol]]<\/p>/gi, "</p>");
};
/**
* Look at the example below to see what exactly this method replaces.
* By default the most important heading is <h3>. Use the *mostImportantHeading* option while creating an instance of the class to change this.
*
* @example
* == Heading 1[[eol]] => <h3>Heading 1</h3>
* $$ Heading 2[[eol]] => <h4>Heading 2</h4>
* %% Heading 3[[eol]] => <h5>Heading 3</h5>
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._transformHeadings = function(text) {
let N = this._options.mostImportantHeading;
return text
.replace(/== (.*?)( ==)?\[\[eol]]/gi, `<h${N}>$1</h${N}>`)
.replace(/\$\$ (.*?)( \$\$)?\[\[eol]]/gi, `<h${N+1}>$1</h${N+1}>`)
.replace(/%% (.*?)( %%)?\[\[eol]]/gi, `<h${N+2}>$1</h${N+2}>`);
};
/**
* Look at the example below to see what exactly this method replaces.
*
* @example
* ``alert('Hello!')`` => <code>alert('Hello!')</code>
* ```function() { <pre><code>function() {
* console.log('OK') => console.log('OK')</code></pre>
* }```
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._transformCodeTags = function(text) {
return text
.replace(/```(.*?)```(\[\[eol]])?/g, "<pre><code>$1</code></pre>")
.replace(/``(.*?)``/g, "<code>$1</code>");
};
/**
* Look at the example below to see what exactly this method replaces.
*
* @example
* // Due to technical restrictions, "@" in the example below has been changed to <at sign>.
* <at sign>[[http://kozalo.ru]] => <a href="http://kozalo.ru">http://kozalo.ru</a>
* <at sign>[My website][http://kozalo.ru] => <a href="http://kozalo.ru">My website</a>
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._transformLinks = function(text) {
let processedText = text;
if (this._operations.transformLinks && this._operations.transformImages)
processedText = processedText
.replace(new RegExp('@!\\[\\[(.*?)\\]\\]', 'g'), (this._options.targetBlank) ? '<a href="$1" target="_blank"><img src="$1"></a>' : '<a href="$1"><img src="$1"></a>')
.replace(new RegExp('@!\\[(.*?)\\]\\[(.*?)\\]', 'g'), (this._options.targetBlank) ? '<a href="$2" target="_blank"><img src="$2" alt="$1"></a>' : '<a href="$2"><img src="$2" alt="$1"></a>');
return processedText
.replace(new RegExp('@\\[\\[(.*?)\\]\\]', 'g'), (this._options.targetBlank) ? '<a href="$1" target="_blank">$1</a>' : '<a href="$1">$1</a>')
.replace(new RegExp('@\\[(.*?)\\]\\[(.*?)\\]', 'g'), (this._options.targetBlank) ? '<a href="$2" target="_blank">$1</a>' : '<a href="$2">$1</a>');
};
/**
* Look at the example below to see what exactly this method does.
*
* @example
* ![[http://kozalo.ru/images/service/logo.png]] => <img src="http://kozalo.ru/images/service/logo.png">
* ![Logo][http://kozalo.ru/images/service/logo.png] => <img src="http://kozalo.ru/images/service/logo.png" alt="Logo">
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._transformImages = function(text) {
return text
.replace(new RegExp('!\\[\\[(.*?)\\]\\]', 'g'), '<img src="$1">')
.replace(new RegExp('!\\[(.*?)\\]\\[(.*?)\\]', 'g'), '<img src="$2" alt="$1">');
};
/**
* This function has 2 modes. Both are demonstrated in the example below.
*
* @example
* The first mode:
* # One; + One;
* # Two; + Two;
* # Three. + Three.
* # Nested one; # Nested one;
* # Nested two; # Nested two;
* # Nested three. + Four.
* # Four. +
* #
*
* The second one:
* ## One; ++ One;
* # Two; + Two;
* # Three. + Three.
* ## Nested one; ## Nested one;
* # Nested two; # Nested two;
* ### Nested three. #
* ### Four. +++ Four.
*
* Both will be transformed into the following HTML code:
* <ol> <ul>
* <li>One;</li> <li>One;</li>
* <li>Two;</li> <li>Two;</li>
* <li>Three.</li> <li>Three.</li>
* <ol> <ol>
* <li>Nested one;</li> <li>Nested one;</li>
* <li>Nested two;</li> <li>Nested two;</li>
* <li>Nested three.</li> </ol>
* </ol> <li>Four.</li>
* <li>Four.</li> </ul>
* </ol>
*
* Note that the type of a list is determined on the first line. So, you can write something like this:
* ## Ordered list;
* + Still ordered.
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._transformLists = function(text) {
let paragraphsEnabled = this._operations.transformParagraphs;
let li = (paragraphsEnabled) ? '<li><p>' : '<li>';
let _li = (paragraphsEnabled) ? '</p></li>' : '</li>';
return (this._options.listsWithIndentations) ?
transformListsWithIndentations(text) :
transformListsWithoutIndentations(text);
function transformListsWithoutIndentations(text) {
let arr = text.split('[[eol]]');
let closedTags = [];
let newTextArr = [];
let unclosedTag = false;
arr.forEach(function(str) {
if ((str.substr(0, 4) == "### ") || (str.substr(0, 4) == "+++ "))
{
closeItemIfOpened();
if (str.substr(0, 4) == "### ") {
newTextArr.push('<ol>');
closedTags.push('</ol>');
}
else {
newTextArr.push('<ul>');
closedTags.push('</ul>');
}
newTextArr.push(li + str.substr(4));
unclosedTag = true;
}
else if ((str.substr(0, 2) == "# ") || (str.substr(0, 2) == "+ "))
{
if (!closedTags.length) {
if (str.substr(0, 2) == "# ") {
newTextArr.push('<ol>');
closedTags.push('</ol>');
}
else {
newTextArr.push('<ul>');
closedTags.push('</ul>');
}
}
closeItemIfOpened();
newTextArr.push(li + str.substr(2));
unclosedTag = true;
}
else if (closedTags.length && ((str.substr(0, 3) == "## ") || (str.substr(0, 3) == "++ ")))
{
closeItemIfOpened();
newTextArr.push(li + str.substr(3) + _li + closedTags.pop());
}
else if (closedTags.length && ((str.trim() == "#") || (str.trim() == "+")))
{
closeItemIfOpened();
newTextArr.push(closedTags.pop());
}
else if (paragraphsEnabled && unclosedTag && str.trim() == "")
newTextArr.push('</p><p>')
else
{
if (unclosedTag)
newTextArr.push('[[eol]]' + str);
else
newTextArr.push(str + '[[eol]]');
}
});
closeItemIfOpened();
while (closedTags.length)
newTextArr.push(closedTags.pop());
return newTextArr.join('').replace(/\[\[eol]]$/, "");
function closeItemIfOpened() {
if (unclosedTag) {
newTextArr.push(_li);
unclosedTag = false;
}
}
}
function transformListsWithIndentations(text) {
let arr = text.split('[[eol]]');
let closedTags = [];
let newTextArr = [];
let unclosedTag = false;
let tabulationCount = -1;
arr.forEach(function(str) {
let trimmedStr = str.trim();
let tabulations = (str.match(/^\s+/) || [''])[0];
if ((trimmedStr.substr(0, 2) == "# ") || (trimmedStr.substr(0, 2) == "+ ")) {
if (tabulations.length > tabulationCount) {
tabulationCount = tabulations.length;
closeItemIfOpened();
if (trimmedStr.substr(0, 2) == "# ") {
newTextArr.push('<ol>');
closedTags.push('</ol>');
}
else {
newTextArr.push('<ul>');
closedTags.push('</ul>');
}
newTextArr.push(li + trimmedStr.substr(2));
unclosedTag = true;
}
else if (tabulations.length < tabulationCount) {
tabulationCount = tabulations.length;
closeItemIfOpened();
newTextArr.push(closedTags.pop() + li + trimmedStr.substr(2));
unclosedTag = true;
}
else {
closeItemIfOpened();
newTextArr.push(li + trimmedStr.substr(2));
unclosedTag = true;
}
}
else if (closedTags.length && ((trimmedStr == "#") || (trimmedStr == "+")))
{
if (tabulations.length == 0)
tabulationCount = -1;
closeList();
}
else
{
if (tabulations.length < tabulationCount) {
tabulationCount = (tabulations.length == 0) ? -1 : tabulations.length;
closeList();
}
if (paragraphsEnabled && unclosedTag && trimmedStr == "") {
newTextArr.push('</p><p>');
}
else {
if (unclosedTag)
newTextArr.push('[[eol]]' + str);
else
newTextArr.push(str + '[[eol]]');
}
}
});
closeItemIfOpened();
while (closedTags.length)
newTextArr.push(closedTags.pop());
return newTextArr.join('').replace(/\[\[eol]]$/, "");
function closeItemIfOpened() {
if (unclosedTag) {
newTextArr.push(_li);
unclosedTag = false;
}
}
function closeList() {
closeItemIfOpened();
newTextArr.push(closedTags.pop());
}
}
};
/**
* Look at the example below to see what exactly this method does.
*
* @example
* // Transforms the text:
* > Hello!
* >
* > My name is...
*
* // into:
* <blockquote>
* <p>Hello!</p>
* <p>My name is...</p>
* </blockquote>
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._transformQuotations = function(text) {
let arr = text.split('[[eol]]');
let quoteMarked = false;
let newTextArr = [];
let startTag = '<blockquote>';
let endTag = '</blockquote>';
if (this._operations.transformParagraphs) {
startTag += '[[eol]][[eol]]';
endTag = '[[eol]][[eol]]' + endTag;
}
arr.forEach(function(str) {
if ((str.substr(0, 5) == "> ") || (str.trim() == ">"))
{
if (!quoteMarked)
{
newTextArr.push(startTag + str.substr(5));
quoteMarked = true;
}
else
newTextArr.push('[[eol]]' + str.substr(5));
}
else
{
if (quoteMarked)
{
newTextArr.push(endTag);
quoteMarked = false;
}
newTextArr.push(str + '[[eol]]');
}
});
if (quoteMarked)
newTextArr.push(endTag);
let result = newTextArr.join('');
return result
.replace(/\[\[eol]]<blockquote>/gi, "<blockquote>")
.replace(/<\/blockquote>\[\[eol]]/gi, "</blockquote>[[eol]][[eol]]");
};
/**
* Look at the example below to see what exactly this method does.
*
* @example
* // Transforms the text:
* [table]
* [row][header]h1[/header][header]h2[/header][/row]
* [row][cell]1[/cell][cell]2[/cell][/row]
* [row][cell]3[/cell][cell]4[/cell][/row]
* [/table]
*
* // into:
* <table>
* <tr><th>h1</th><th>h2</th></tr>
* <tr><td>1</td><td>2</td></tr>
* <tr><td>3</td><td>4</td></tr>
* </table>
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._transformTables = function(text) {
let tableClasses = this._options.tableClasses.join(' ').trim();
return text
.replace(/\[table](\[\[eol]])?/gi, (tableClasses != "") ? '<table class="' + tableClasses + '">' : '<table>')
.replace(/\[\/table](\[\[eol]])?/gi, '</table>')
.replace(/\[(\/)?header](\[\[eol]])?/gi, '<$1th>')
.replace(/\[(\/)?row](\[\[eol]])?/gi, '<$1tr>')
.replace(/\[(\/)?cell](\[\[eol]])?/gi, '<$1td>');
};
/**
* Look at the example below to see what exactly this method does.
*
* @example
* // Transforms the text:
* {{rf=Russian Federation}}
* I'm from {{rf}}
*
* // into:
* I'm from Russian Federation
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._transformSubstitutions = function(text) {
let regexp = /\{\{([a-zA-Z]+?)=(.+?)}}(\[\[eol]])?/gi;
let substitution;
let substitutions = {};
while (substitution = regexp.exec(text))
substitutions[substitution[1]] = substitution[2];
text = text.replace(regexp, '');
for (let key in substitutions)
{
regexp = new RegExp('\{\{' + key + '}}', 'gi');
text = text.replace(regexp, substitutions[key]);
}
return text;
};
/**
* Replaces 6 or more hypens (-) or 3 or more dashes (—) in the string without other characters to a line (<hr/>).
*
* @example
* // Transforms the following string:
* ------
*
* // into:
* <hr/>
*
* @param {string} text
* @return {string}
* @memberof KozMUL
*/
KozMUL.prototype._transformHorizontalLines = function(text) {
return text
.replace(/\[\[eol]](-{6,}|—{3,})\[\[eol]]/gi, "<hr/>")
.replace(/\[\[eol]](—){3}\[\[eol]]/gi, "<hr/>");
};