/**
* Module to help converting commonmark to html for the editor.
* All methods are exclusively used by the FocusEditorCore class.
*
* ** Not for public use: any function name may change at any time. **
* @module md2html
*/
import * as helper from "./helper.mjs";
export const EMPTY_LINE_HTML_PLACEHOLDER = `<br>`;
export function innerTextToHtml(text, document) {
text = text.replace(/\r/g, "\n");
const lines = text.split("\n").map((l) => {
// strip trailing whitespace
l = l.replace(/\s+$/, "");
if (l.trim() === "") {
return `<div class="block">${EMPTY_LINE_HTML_PLACEHOLDER}</div>`;
}
const el = document.createElement("div");
el.textContent = l;
return el.outerHTML.replace(/ {2}/g, " ");
});
let div = document.createElement("div");
div.innerHTML = lines.join("\n");
return div.innerHTML;
}
export function addCodeBlockClasses(elements, document) {
let isCodeBlock = false;
let codeBlocks = [];
const allCodeBlocksCount = [...elements].filter((l) =>
/^```/.test(l.textContent),
).length;
let codeBlockIndex = 0;
elements.forEach((el) => {
if (el.tagName === "BR") {
const div = document.createElement("div");
div.classList.add("block");
if (isCodeBlock) {
div.classList.add("code-block");
}
div.innerHTML = EMPTY_LINE_HTML_PLACEHOLDER;
el.replaceWith(div);
return;
}
el.classList.add("block");
["code-block", "code-block-start", "code-block-end"].forEach((className) =>
el.classList.remove(className),
);
const l = el.textContent;
if (l.trim() === "") {
el.innerHTML = EMPTY_LINE_HTML_PLACEHOLDER;
}
if (
allCodeBlocksCount % 2 === 1 &&
codeBlockIndex >= allCodeBlocksCount - 1
) {
/* only add codeblock if it has also a closing ``` */
} else {
if (l.match(/^```/) && !isCodeBlock) {
isCodeBlock = true;
el.classList.add("code-block-start");
codeBlockIndex++;
codeBlocks = [];
} else if (l.match(/^```$/) && isCodeBlock) {
isCodeBlock = false;
codeBlockIndex++;
el.classList.add("code-block");
el.classList.add("code-block-end");
}
if (isCodeBlock) {
el.innerHTML = el.innerHTML.replace(/\s{2}/g, " ");
el.classList.add("code-block");
if (el.innerHTML.match(/[<>]/)) {
// remove html tags
el.textContent = String(el.textContent);
}
codeBlocks.push(el);
}
}
if (el.innerHTML.trim() === "") {
el.innerHTML = "<br>";
}
return el;
});
}
function inlineMarkdown(text) {
if (
text.startsWith("`") &&
text.endsWith("`") &&
text[1] != "`" &&
text !== "`"
) {
return `<code>${text}</code>`;
}
let html = helper.escapeHTMLEntities(text);
let htmlBeforeLinkCheck = html;
// find links
html = html.replace(/(!)*\[(.+?)\]\((.*)\)/g, (...matches) => {
let classes = ["link", matches[1] ? "image" : ""]
.filter((v) => !!v)
.join(" ");
let url = helper.stripHtml(matches[3].split(/\s+/)[0]);
let linkText = matches[2];
linkText = inlineMarkdown(linkText);
return `<a href="${url}" style="--url: url(${url})" class="${classes}">${matches[1] || ""}[${linkText}]<span>(${url})</span></a>`;
});
if (htmlBeforeLinkCheck !== html) {
// if a link was found, do not parse more markdown inside the link
return html;
}
// find bold+italic
html = html
.replace(/([*\\]*)(\*{3}[^\s*]+.*?\*{3})([*]*)/g, (...matches) => {
if (matches[1] || matches[3]) {
return matches[0];
}
return `<strong><em>${matches[2]}</em></strong>`;
})
.replace(/([_\\]*)(_{3}[^\s_]+.*?_{3})([_]*)/g, (...matches) => {
if (matches[1] || matches[3]) {
return matches[0];
}
return `<strong class="underscore"><em>${matches[2]}</em></strong>`;
});
// find *italic* / _italic_
html = html
.replace(/([*\\]*)(\*{1}[^\s*]+.*?\*{1})([*]*)/g, (...matches) => {
if (matches[1] || matches[3]) {
return matches[0];
}
return `<em>${matches[2]}</em>`;
})
.replace(/([_\\]*)(_{1}[^\s_]+.*?_{1})([_]*)/g, (...matches) => {
if (matches[1] || matches[3]) {
return matches[0];
}
return `<em class="underscore">${matches[2]}</em>`;
});
// find **bold text** / __bold text__
html = html
.replace(/([*\\]*)(\*\*[^\s*]+.*?\*\*)([*\\]*)/g, (...matches) => {
if (matches[1] || matches[3]) {
return matches[0];
}
return `<strong>${matches[2]}</strong>`;
})
.replace(/([_\\]*)(__[^\s_]+.*?__)([_\\]*)/g, (...matches) => {
if (matches[1] || matches[3]) {
return matches[0];
}
return `<strong>${matches[2]}</strong>`;
});
// find strike through text
html = html.replace(/([~\\])*(~~[^~][^\s]+.*?~~)([~])*/g, (...matches) => {
if (matches[1] || matches[3]) {
return matches[0];
}
return `<s>${matches[2]}</s>`;
});
return html;
}
export function addParagraphClasses(elements, document) {
// TODO: check for trim
elements.forEach((el) => {
helper.removeStyleAttributeRecursively(el);
// remove new line at the beginning
el.textContent = el.textContent.replace(/^\n+/, "");
// add whitespace workaround
if (el.textContent.trim() === "") {
el.innerHTML = helper.whiteSpaceWorkaround();
}
["h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "hr"].forEach(
(className) => el.classList.remove(className),
);
if (el.classList.contains("code-block")) {
return;
}
if (!el.innerHTML) {
return;
}
if (el.textContent.match(/^#{1,6}\s/)) {
let id = helper.slugify(el.textContent.replace(/^#{1,6}\s/, "").trim());
if (!document.getElementById(id)) {
el.id = id;
}
el.innerHTML = inlineMarkdown(el.textContent);
el.classList.add(`h${el.textContent.match(/^(#{1,6})\s/)[1].length}`);
return;
}
if (el.textContent.match(/^>{1,3}/)) {
el.classList.add(`blockquote`);
el.classList.add(
`blockquote-${el.textContent.match(/^(>{1,3})/)[1].length}`,
);
}
// <hr>
if (el.textContent.match(/^(-{3,}|\*{3,})\s*$/)) {
el.classList.add("hr");
}
let html = el.innerHTML;
html = html.replace(/(!)*\[(.+?)\]\((.*)\)/g, (...matches) => {
let classes = ["link", matches[1] ? "image" : ""]
.filter((v) => !!v)
.join(" ");
return `<a href="${matches[3]}" style="--url: url(${matches[3]})" class="${classes}">${matches[1] || ""}[${matches[2]}]<span>(${matches[3]})</span></a>`;
});
// previous way:
html = el.textContent
.split(/(`+[^`]+?`+)/g)
.map(inlineMarkdown)
.join("");
// previous way
if (html !== el.innerHTML) {
el.innerHTML = html;
}
if (el.textContent.trim() === "" && EMPTY_LINE_HTML_PLACEHOLDER) {
el.innerHTML = EMPTY_LINE_HTML_PLACEHOLDER;
}
if (el.textContent.trim() === "") {
el.innerHTML = EMPTY_LINE_HTML_PLACEHOLDER;
}
});
return;
}
export function convertElementsWithMarkdownTablesToVisualTables(elements) {
let tableRows = [];
function columnsToVisualTable(columns) {
const ul = document.createElement("div");
columns.forEach((row, i) => {
const li = document.createElement("span");
li.innerHTML = (i === 0 ? "|" : "") + row + "|";
ul.appendChild(li);
});
return ul;
}
elements.forEach((el, i) => {
if (tableRows.length === 0) {
if (!/^\|.+?\|$/.test(el.textContent)) {
return;
}
if (
/^\|.+?\|/.test(el.textContent) &&
elements[i + 1] &&
/^\|\s*:*[-]+:*\s*\|/.test(elements[i + 1].textContent)
) {
if (!elements[i + 1].textContent.trim().endsWith("|")) {
// if the next line is not a valid table line, stop here
return;
}
if (
!elements[i + 2] ||
!elements[i + 2].textContent.trim().endsWith("|")
) {
// if the line after next line is not a valid table line, stop here
return;
}
// table header found
let columns = el.textContent.split(/\|/);
let tableExtraCssClass = null;
if (
columns.at(-2).startsWith(".") &&
elements[i + 1].textContent.split(/\|/).length + 1 === columns.length
) {
tableExtraCssClass = columns.at(-2).replace(/^\./, "").trim();
}
columns = columns.slice(1, -1);
const table = columnsToVisualTable(columns);
table.classList.add("table-header-text");
table.classList.add("table");
if (tableExtraCssClass) {
table.classList.add("table-with-extra-class");
table.classList.add(`table-${tableExtraCssClass}`);
}
el.innerHTML = "";
el.append(table);
tableRows.push(columns);
}
} else if (/^\|.+?\|$/.test(el.textContent)) {
const columns = columnsToVisualTable(
el.textContent.split(/\|/).slice(1, -1),
);
if (tableRows.length === 1) {
columns.classList.add("table-header-separator");
columns.classList.add("table");
} else {
columns.classList.add("table");
columns.classList.add("table-row");
columns.classList.add(
tableRows.length % 2 === 0 ? "table-row-odd" : "table-row-even",
);
if (
!elements[i + 1] ||
!elements[i + 1].textContent.trim().endsWith("|")
) {
columns.classList.add("table-row-last");
}
}
el.innerHTML = "";
el.append(columns);
tableRows.push(columns);
} else {
tableRows = [];
}
});
}
export function unifyTableCells(elements) {
let isInsideTable = false;
let maxCellWidths = [];
elements.forEach((e) => {
if (e.querySelector(".table-header-text.table")) {
isInsideTable = true;
}
if (!isInsideTable) {
return;
}
if (e.querySelector(".table.table-row-last")) {
isInsideTable = false;
let previousElement = e;
while (previousElement.querySelector(".table")) {
previousElement.querySelectorAll(".table > span").forEach((el, i) => {
el.style.setProperty("--max-cell-width", `${maxCellWidths[i]}em`);
});
previousElement = previousElement.previousElementSibling;
}
}
});
}
source