import Placekeeper from '../Utils/Placekeeper.js';
/**
* Represents a markdown engine for parsing and converting markdown content to HTML.
* @class
*/
export default class MarkdownEngine {
#templater = undefined;
#regex = new Map([
['bold', new RegExp('\\*\\*(.+?)\\*\\*', 'g')],
['italic', new RegExp('\\*(.+?)\\*', 'g')],
['inline-code', new RegExp('`(.+?)`', 'g')],
['link', new RegExp('\\[(.+?)\\]\\((.+?)\\)', 'g')],
['image', new RegExp('!\\[(.+?)\\]\\((.+?)\\)', 'g')],
['table', new RegExp('\\|(.*?)\\|', 'g')],
['tasks', new RegExp('\\[([ xX])\\](.+)', 'g')],
['footnote', new RegExp('\\[\\^(.+?)\\]', 'g')],
]);
/**
* Creates an instance of HtmlEngine.
*
* @param {Object} templateEngine - The template engine to use for parsing.
*/
constructor(templater) {
this.#templater = templater;
}
/**
* Handles the specified file by loading its content, parsing sections and variables,
* and caching the result.
*
* @param {string} filepath - The path of the file to handle.
* @returns {Object} - An object with a `parse` method that can be used to parse the file.
* @throws {Error} - If the file is not found.
*/
async handle(filepath) {
const file = Bun.file(filepath);
if (!(await file.exists())) throw new Error('file not found');
if (!file) throw new Error('something went wrong');
const rawContent = await file.text();
let { metadata = {}, content = '' } = await this.#extractFrontMatter(rawContent);
content = this.#toHtml(content);
await this.#templater.add(filepath, content);
return {
mimeType: 'text/html',
parse: async (data = null, options = {}) => {
data = (
!data ||
typeof(data) != 'object' ||
data.constructor.name != 'Object'
) ? metadata : {
...metadata,
...data
};
const parsedContent = await this.#templater.parse(
filepath, data,
{ content }
);
return {
data,
content: parsedContent
};
}
};
}
/**
* Extracts the front matter from the content.
*
* @param {string} content - The content to extract the front matter from.
* @returns {Object} - An object containing the metadata and content.
*/
async #extractFrontMatter(content) {
function parseValue(value) {
value = value.trim();
if (value.startsWith('"') && value.endsWith('"'))
return value.substring(1, value.length - 1);
if (value === 'true' || value === 'false') return value === 'true';
if (!isNaN(value)) return Number(value);
if (value.startsWith('[') && value.endsWith(']'))
return value
.substring(1, value.length - 1)
.split(',')
.map((v) => parseValue(v.trim()));
return value;
}
let lines = content.split('\n');
let isFrontMatter = false;
const variables = {};
let idx = 0;
while (lines.length > 0) {
const line = lines.shift().toString().trim();
if (
line === '' || // ignore empty lines
line.startsWith('#') // ignore comments
) {
idx++;
continue;
}
if (line === '---') {
if (isFrontMatter) break;
isFrontMatter = true;
idx++;
continue;
}
if (isFrontMatter && line.indexOf(':') === -1) continue; // ignore invalid lines
const key = line.substring(0, line.indexOf(':')).trim();
let value = line.substring(line.indexOf(':') + 1).trim();
if (value == '') {
idx++;
continue;
}
value = parseValue(value);
variables[key] = value;
idx++;
}
return {
metadata: variables,
content: lines.join("\n"),
};
}
/**
* Escapes special characters in the given text to prevent XSS attacks.
*
* @param {string} text - The text to escape.
* @returns {string} - The escaped text.
*/
#escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}
/**
* Processes inline formatting in the given text.
*
* @param {string} text - The text to process.
* @returns {string} - The processed text with inline formatting applied.
*/
#processInlineFormatting(text) {
text = text.replace(this.#regex.get('bold'), '$1');
text = text.replace(this.#regex.get('italic'), '$1');
text = text.replace(this.#regex.get('inline-code'), '$1');
text = text.replace(this.#regex.get('link'), '$1');
text = text.replace(this.#regex.get('image'), '');
return text;
}
/**
* Converts the given markdown content to HTML.
*
* @param {string} content - The markdown content to convert.
* @returns {string} - The converted HTML.
*/
#toHtml(content) {
let html = '';
let lines = content.split('\n');
while (lines.length > 0) {
let line = lines.shift().toString().trim();
if (line === '') continue;
if (line.startsWith('#')) {
const level = line.indexOf(' ');
const headingText = line.substring(level).trim();
html += `
${this.#escapeHtml(code.trim())}`;
} else if (line.startsWith('---')) {
html += ''; while (lines.length > 0 && lines[0].startsWith('>')) { const blockquoteText = lines.shift().toString().trim().substring(2).trim(); html += `'; } else if (line.startsWith('|')) { // Table parsing let headers = line.match(this.#regex.get('table')); let align = lines.shift().toString().trim().match(this.#regex.get('table')); let cells = []; while (lines.length > 0 && lines[0].startsWith('|')) { cells.push(lines.shift().toString().trim().match(this.#regex.get('table'))); } html += '${this.#processInlineFormatting(blockquoteText)}
`; } html += '
| ${this.#processInlineFormatting(header.slice(1, -1))} | `; }); html += '
|---|
| ${this.#processInlineFormatting(cell.slice(1, -1))} | `; }); html += '
${this.#processInlineFormatting(line)}
`; } } return html; } }