Custom Script to Convert Markdown to Rich Text for Display and Back to Markdown for Storage

Custom Script to Convert Markdown to Rich Text for Display and Back to Markdown for Storage

:rocket: Introduction

In ERPNext, Markdown fields are widely used for storing formatted text data. However, Markdown can be difficult for end users to work with, especially when they need to format text, insert images, or embed videos.

This post explains how to create a Custom Script that converts Markdown fields into a Rich Text Editor (like Microsoft Word) for easier formatting and better user experience. When saving, the rich text content is automatically converted back to Markdown for storage.

:point_right: This approach allows end users to:
:white_check_mark: Easily format text with a rich toolbar (bold, italic, lists, etc.)
:white_check_mark: Paste and insert images directly into the editor
:white_check_mark: Embed YouTube videos with a simple link
:white_check_mark: Maintain Markdown format for consistent data storage


:dart: Goals and Requirements

:white_check_mark: Goals:

  • Convert Markdown fields into a rich text editor for better usability
  • Support pasting and uploading images directly
  • Enable embedding YouTube videos easily
  • Convert rich text back to Markdown for consistent database storage

:white_check_mark: Requirements:

  • ERPNext installed and running
  • Admin access to create Custom Scripts
  • Basic understanding of JavaScript and ERPNext customization

:hammer_and_wrench: Complete Custom Script

Follow these steps to implement the solution:

  1. Open ERPNext → Customization → Custom Script
  2. Create a new Custom Script
  3. Paste the following code (replace doctype_name and markdown_field with your actual document type and field name):
frappe.ui.form.on('doctype_name', {
    refresh: function(frm) {
        // Hide the default Markdown field
        frm.toggle_display('markdown_field', false);

        // Create the Rich Text Editor if it doesn’t exist
        if (!frm.rich_text_editor) {
            // Create the container for the Rich Text Editor
            frm.rich_text_editor = $('<div>').appendTo(frm.fields_dict.markdown_field.wrapper);

            // Initialize the Rich Text Editor
            frm.rich_text_editor_editor = new frappe.ui.Editor({
                parent: frm.rich_text_editor,
                height: '300px',
                value: frappe.markdown(frm.doc.markdown_field || "")
            });

            // ✅ Handle image pasting and uploading
            frm.rich_text_editor_editor.quill.getModule('toolbar').addHandler('image', () => {
                let input = document.createElement('input');
                input.setAttribute('type', 'file');
                input.setAttribute('accept', 'image/*');
                input.click();

                input.onchange = () => {
                    let file = input.files[0];
                    if (file) {
                        let reader = new FileReader();
                        reader.onload = (e) => {
                            let base64Image = e.target.result;
                            let range = frm.rich_text_editor_editor.quill.getSelection(true);
                            frm.rich_text_editor_editor.quill.insertEmbed(range.index, 'image', base64Image);
                        };
                        reader.readAsDataURL(file);
                    }
                };
            });

            // ✅ Handle YouTube video embedding
            frm.rich_text_editor_editor.quill.getModule('toolbar').addHandler('video', () => {
                let url = prompt('Enter YouTube URL:');
                if (url) {
                    let range = frm.rich_text_editor_editor.quill.getSelection(true);
                    let videoEmbed = `
                        <iframe width="560" height="315" 
                                src="${url.replace('watch?v=', 'embed/')}" 
                                frameborder="0" 
                                allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" 
                                allowfullscreen>
                        </iframe>`;
                    frm.rich_text_editor_editor.quill.clipboard.dangerouslyPasteHTML(range.index, videoEmbed);
                }
            });
        } else {
            // If already created, update value
            frm.rich_text_editor_editor.set_value(frappe.markdown(frm.doc.markdown_field || ""));
        }
    },

    // ✅ Convert HTML back to Markdown when saving
    before_save: function(frm) {
        let html_value = frm.rich_text_editor_editor.get_value();
        frm.set_value('markdown_field', html_value);
    }
});

:brain: How the Script Works

:arrow_right: 1. Hide Markdown Field

frm.toggle_display('markdown_field', false);

This hides the default Markdown field to avoid confusion for the end user.


:arrow_right: 2. Initialize Rich Text Editor

frm.rich_text_editor_editor = new frappe.ui.Editor({
    parent: frm.rich_text_editor,
    height: '300px',
    value: frappe.markdown(frm.doc.markdown_field || "")
});
  • The frappe.ui.Editor creates a rich text editor similar to a Word document.
  • It takes the existing Markdown content and converts it into HTML for display.

:arrow_right: 3. Handle Image Uploading

let file = input.files[0];
let reader = new FileReader();
reader.onload = (e) => {
    let base64Image = e.target.result;
    let range = frm.rich_text_editor_editor.quill.getSelection(true);
    frm.rich_text_editor_editor.quill.insertEmbed(range.index, 'image', base64Image);
};
reader.readAsDataURL(file);
  • The image is converted to Base64 format and directly inserted into the document.
  • This allows the image to be stored as part of the Markdown content.

:arrow_right: 4. Handle YouTube Video Embedding

let videoEmbed = `
    <iframe width="560" height="315" 
            src="${url.replace('watch?v=', 'embed/')}" 
            frameborder="0" 
            allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" 
            allowfullscreen>
    </iframe>`;
  • Converts the YouTube URL into an embeddable iframe format.
  • Directly inserts the video into the rich text editor.

:arrow_right: 5. Save as Markdown

before_save: function(frm) {
    let html_value = frm.rich_text_editor_editor.get_value();
    frm.set_value('markdown_field', html_value);
}
  • When the document is saved, the rich text HTML is converted back into Markdown format and stored in the database.
  • This ensures consistency with existing ERPNext data structures.

:wrench: Customizing the Toolbar

You can define a custom toolbar configuration like this:

toolbar: [
    [{ 'header': [1, 2, 3, false] }],
    ['bold', 'italic', 'underline', 'strike'],
    [{ 'list': 'ordered' }, { 'list': 'bullet' }],
    ['link', 'image', 'video'],
    ['clean']
]

This gives you control over which formatting options are available to the user.


:white_check_mark: Expected Outcome

:white_check_mark: Markdown content is displayed as rich text
:white_check_mark: End users can format content visually
:white_check_mark: Copy-paste and upload images directly into the editor
:white_check_mark: Embed YouTube videos with a single link
:white_check_mark: On save, content is stored in Markdown format for consistency


:rocket: Advantages

:white_check_mark: Improved user experience with visual editing
:white_check_mark: Seamless handling of images and videos
:white_check_mark: Maintains compatibility with ERPNext’s Markdown format
:white_check_mark: Easy to extend and customize


:dart: Conclusion

This solution enhances ERPNext’s user experience by replacing Markdown fields with a user-friendly rich text editor. The script handles images, videos, and formatting while maintaining compatibility with ERPNext’s Markdown storage format.

If you need additional help or customization, feel free to ask! :sunglasses:

1 Like