RocketChat/Rocket.Chat

View on GitHub
apps/meteor/app/markdown/lib/markdown.js

Summary

Maintainability
A
1 hr
Test Coverage
/*
 * Markdown is a named function that will parse markdown syntax
 * @param {Object} message - The message object
 */
import { escapeHTML } from '@rocket.chat/string-helpers';
import { Meteor } from 'meteor/meteor';

import { filtered } from './parser/filtered/filtered';
import { code } from './parser/original/code';
import { original } from './parser/original/original';

const parsers = {
    original,
    filtered,
};

class MarkdownClass {
    parse(text) {
        const message = {
            html: escapeHTML(text),
        };
        return this.mountTokensBack(this.parseMessageNotEscaped(message)).html;
    }

    parseNotEscaped(text) {
        const message = {
            html: text,
        };
        return this.mountTokensBack(this.parseMessageNotEscaped(message)).html;
    }

    parseMessageNotEscaped(message) {
        const options = {
            rootUrl: Meteor.absoluteUrl(),
        };

        return parsers.original(message, options);
    }

    mountTokensBackRecursively(message, tokenList, useHtml = true) {
        const missingTokens = [];

        if (tokenList.length > 0) {
            for (const { token, text, noHtml } of tokenList) {
                if (message.html.indexOf(token) >= 0) {
                    message.html = message.html.replace(token, () => (useHtml ? text : noHtml)); // Uses lambda so doesn't need to escape $
                } else {
                    missingTokens.push({ token, text, noHtml });
                }
            }
        }

        // If there are tokens that were missing from the string, but the last iteration replaced at least one token, then go again
        // this is done because one of the tokens may have been hidden by another one
        if (missingTokens.length > 0 && missingTokens.length < tokenList.length) {
            this.mountTokensBackRecursively(message, missingTokens, useHtml);
        }
    }

    mountTokensBack(message, useHtml = true) {
        if (message.tokens) {
            this.mountTokensBackRecursively(message, message.tokens, useHtml);
        }

        return message;
    }

    code(...args) {
        return code(...args);
    }

    /** @param {string} message */
    filterMarkdownFromMessage(message) {
        return parsers.filtered(message);
    }
}

export const Markdown = new MarkdownClass();

/** @param {string} message */
export const filterMarkdown = (message) => Markdown.filterMarkdownFromMessage(message);

export const createMarkdownMessageRenderer = ({ ...options }) => {
    const markedParser = parsers.marked;
    return (message, useMarkedParser = false) => {
        if (!message?.html?.trim()) {
            return message;
        }

        return useMarkedParser ? markedParser(message, options) : parsers.original(message, options);
    };
};

export const createMarkdownNotificationRenderer = () => (message) => parsers.filtered(message);