diff --git a/src/Views/Markdown.js b/src/Views/Markdown.js new file mode 100644 index 0000000..a532098 --- /dev/null +++ b/src/Views/Markdown.js @@ -0,0 +1,208 @@ +import Placekeeper from '../Utils/Placekeeper.js'; + +/** + * symbolic markdown engine implementation for illustrational purposes + * @note at this point its preferable to avoid dependencies for as long as possible + * @class + */ +export default class MarkdownEngine { + #internalCache = new Map(); + constructor() {} + + /** + * 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); + const { sections, variables } = Placekeeper.init(content); + + this.#internalCache.set( + filepath, + new Map([ + ['content', content], + ['sections', sections], + ['variables', variables], + ['metadata', metadata], + ['mimeType', file.type || 'text/html'], + ['size', file.size], + ]) + ); + + console.log(filepath, this.#internalCache.get(filepath)); + + return { + parse: (data = null, options = {}) => + this.parse(filepath, data, options), + }; + } + + /** + * Parses the specified identifier using the provided data and options. + * @param {string} identifier - The identifier of the file to parse. + * @param {Object} [data={}] - The data object to replace placeholders in the file content. + * @param {Object} [options={}] - The options object. + * @returns {Response} - The parsed response object. + * @throws {Error} - If the file is not found. + */ + async parse(identifier, data = {}, options = {}) { + if (!this.#internalCache.has(identifier)) { + throw new Error('file not found'); + } + const view = this.#internalCache.get(identifier); + let parsed = Placekeeper.parse( + view.get('content'), + view.get('sections') + ); + for (const [key, value] of Object.entries(data)) { + parsed = parsed.replace( + new RegExp(`{{${key}}}`, 'g'), + value.toString() + ); + } + + return new Response(view.get('content'), { + ...(options || {}), + headers: { + ...(options.headers || {}), + 'Content-Type': 'text/html', //view.get('mimeType'), + 'Content-Length': view.get(), + }, + }); + } + + async extractFrontMatter(content) { + /** + * Parses a value and returns the appropriate data type. + * @param {string} value - The value to be parsed. + * @returns {string|boolean|number|array} - The parsed value. + */ + function parseValue(value) { + console.log(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 = new Map(); + 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.set(key, value); + idx++; + } + + return { + metadata: variables, + content: lines.join('\n'), + }; + } + + /** + * retrieves the content of a markdown file and returns it as html + */ + toHtml(content) { + let html = ''; + let lines = content.split('\n'); + while (lines.length > 0) { + const line = lines.shift().toString().trim(); + if (line === '') continue; + if (line.startsWith('#')) { + const level = line.indexOf(' '); + html += `${line + .substring(level) + .trim()}`; + } else { + if (line.startsWith('- ') || line.startsWith('1.')) { + const numerical = line.startsWith('1.'); + html += numerical ? '
    ' : '
' : ''; + } else if (line.startsWith('```')) { + const lang = line.substring(3).trim(); + let code = ''; + while (lines.length > 0 && !lines[0].startsWith('```')) { + code += lines.shift().toString().trim() + '\n'; + } + let attr = ''; + if (lang) attr = ` class="language-${lang}"`; + html += `
${code}
`; + } else if (line.startsWith('---')) { + html += '

'; + } else if (line.startsWith('> ')) { + let item = line; + html += '
'; + while (lines.length > 0 && lines[0].startsWith('>')) { + html += lines + .shift() + .toString() + .trim() + .substring(2) + .trim(); + } + html += '
'; + } else { + html += `

${line}

`; + } + } + } + return html; + } +}