DEV Community

hediyeh kianmehr
hediyeh kianmehr

Posted on • Edited on

Markdown-Powered Documentation System

Overview

This document provides step-by-step instructions for creating a website
that enables users to write documents using Markdown and publish them in an HTML design format. Additionally, the website will feature a dashboard where users can edit, delete, and view all their documents.


In this document we cover these topics:

  • 1.Workflow in Node-RED
  • 2.Template Node
  • 3.Uibuilder Node
  • 4.Function Node
  • 5.write file Node

1.Workflow in Node-RED

Image description


Template Node

  • Add a Template Node to your workflow.

  • Property:msg.template

  • Syntax Highlight: mustache

Image description

Note:
The Template Node allows you to write raw HTML code used to design and render your website.

  • Write the HTML structure for your web interface.

The goal of this HTML is to create a user interface where users can input their document, format it using Markdown, and click Send to proceed.

<div>
    <input id="textInput" type="text" placeholder="Type here..." />
    <button id="sendBtn">Send</button>
</div>
Enter fullscreen mode Exit fullscreen mode
  • click Done

line-by-line explanation of the HTML code

<div>
Enter fullscreen mode Exit fullscreen mode

<div>:This is a container element.

<input id="textInput" type="text" placeholder="Type here..." />
Enter fullscreen mode Exit fullscreen mode

<input>: Creates a text box where users can type something.

id="textInput": Gives this input box a unique identifier so we can refer to it later in JavaScript or CSS.

type="text": single-line text input.

placeholder="Type here...": Shows this light gray text inside the box before the user types anything. It helps users know what to do.

<button id="sendBtn">Send</button>
Enter fullscreen mode Exit fullscreen mode

<button>: Creates a clickable button labeled Send.

id="sendBtn": Assigns a unique ID to the button so we can attach a click event or style it.

</div>
Enter fullscreen mode Exit fullscreen mode

Closing </div>: Ends the container started earlier, wrapping both the input and button together.


Uibuilder Node

  • Add a Uibuilder to your workflow.

  • ensure you link it directly to the Template Node

عکس

Note:
The Uibuilder Node allows your frontend (web browser) to exchange data with Node-RED

Purpose:
Uibuilder Node is designed to accept a document from the input provided in the Template Node. It stores the document in a variable and, upon clicking the Send button in the browser, transfers it to the function Node for additional processing.

// @ts-nocheck
import './dynamicsrc/uibuilder.js';
import { createApp } from './dynamicsrc/vue.js';


const app = createApp({

    data() {
        return {
            renderedHtml: "",
            etudesData: [],
        }
    },
    components: {},
    computed: {
    },
    destroyed() { },

    methods: {
        attachScriptToHead(jsCode) {
            const existingScript = document.querySelector(`[data-script-id="${btoa(jsCode)}"]`);
            if (existingScript) {
                return; 
            }
            const script = document.createElement('script');
            script.type = 'text/javascript';
            script.text = jsCode; 
            script.setAttribute('data-script-id', btoa(jsCode));


            document.head.appendChild(script);


        },
        bindEvents() {
            const button = document.getElementById('sendBtn');
            const input = document.getElementById('textInput');

            if (button && input) {

                button.replaceWith(button.cloneNode(true));

                const newButton = document.getElementById('sendBtn');
                newButton.addEventListener('click', () => {
                    const val = input.value;
                    uibuilder.send({ payload: val });
                });
            }
        }
    },
    beforeUnmount() {
    },
    mounted() {
        uibuilder.start();

        var vueApp = this;

        uibuilder.onChange('msg', function (msg) {


            if (msg.javascript) {
                    vueApp.attachScriptToHead(msg.javascript);
            }  

            if (msg.json) {
                vueApp.etudesData = msg.json; 
            }
            if (msg && msg.template) {
                vueApp.renderedHtml = msg.template; 
                vueApp.$nextTick(() => vueApp.bindEvents())
            }


            ;
        });

        document.body.addEventListener("click", (event) => this.handleButtonClick(event));
    },
    beforeDestroy() {
        document.removeEventListener('keydown', this.handleEnterKeyPress);
    },
})

app.mount('#app')
Enter fullscreen mode Exit fullscreen mode

line-by-line explanation of the Uibuilder.js

// @ts-nocheck
Enter fullscreen mode Exit fullscreen mode

Explanation:
This line tells TypeScript to ignore errors in this file.

import './dynamicsrc/uibuilder.js';
import { createApp } from './dynamicsrc/vue.js';
Enter fullscreen mode Exit fullscreen mode

Explanation:
import uibuilder.js likely a helper script for communication between Node-RED and the front-end (used to send/receive messages).

createApp is a function from Vue 3.
It is used to start or create your Vue app.

const app = createApp({
Enter fullscreen mode Exit fullscreen mode

Explanation:
Starts creating a new Vue app.

 data() {
        return {
            renderedHtml: "",    // Will store raw HTML received from backend and display it
            etudesData: [],      // Will store any JSON data (like a list) from backend
        }
    },
Enter fullscreen mode Exit fullscreen mode

Explanation:
This part defines the main memory of your app, which Vue will watch and update on the screen automatically when it changes.

   methods: {

        attachScriptToHead(jsCode) {
            const existingScript = document.querySelector(`[data-script-id="${btoa(jsCode)}"]`);
            if (existingScript) {
                return; 
            }

      const script = document.createElement('script');
            script.type = 'text/javascript';
            script.text = jsCode; 
            script.setAttribute('data-script-id', btoa(jsCode));

            document.head.appendChild(script);
        },
Enter fullscreen mode Exit fullscreen mode

Explanation:
Checks if a script with the same Base64-encoded ID already exists (to avoid duplicates).

btoa(jsCode):
Converts the code into a unique string using Base64 encoding.
Example: "alert('hi')" becomes "YWxlcnQoJ2hpJyk=".

If such a script is already found, the function just stops here and does nothing.

if such a script is not already found Creates a <script> element, injects the JS code as text, and adds it to <head>.

  bindEvents() {
            const button = document.getElementById('sendBtn');
            const input = document.getElementById('textInput');
Enter fullscreen mode Exit fullscreen mode

Explanation:
Gets references to input and button elements in the dynamic HTML.

   if (button && input) {
                button.replaceWith(button.cloneNode(true));  

                const newButton = document.getElementById('sendBtn');
                newButton.addEventListener('click', () => {
                    const val = input.value;
                    uibuilder.send({ payload: val });
                });
            }
        }
Enter fullscreen mode Exit fullscreen mode

Explanation:
Replaces the old button with a clone to clear previous event listeners
(Prevent double-event binding).

Adds a click handler that sends input text as a payload using uibuilder.send().

beforeUnmount() { },
Enter fullscreen mode Exit fullscreen mode

Explanation:
You could use it to clean up resources if needed.

   mounted() {
        uibuilder.start();
Enter fullscreen mode Exit fullscreen mode

Explanation:
Initializes the uibuilder connection (enables communication with Node-RED backend).

        var vueApp = this;
Enter fullscreen mode Exit fullscreen mode

Explanation:
Stores this reference (so it's accessible inside nested functions).

 uibuilder.onChange('msg', function (msg) {
Enter fullscreen mode Exit fullscreen mode

Explanation:
When a new message arrives from backend:

  if (msg.javascript) {
                vueApp.attachScriptToHead(msg.javascript);
            }
Enter fullscreen mode Exit fullscreen mode

Explanation:
If message contains a JS code snippet, dynamically attach it to the document.

  if (msg.json) {
                vueApp.etudesData = msg.json; 
            }
Enter fullscreen mode Exit fullscreen mode

Explanation:
If JSON data is received, store it in etudesData.

        if (msg && msg.template) {
                vueApp.renderedHtml = msg.template; 
                vueApp.$nextTick(() => vueApp.bindEvents())
            }
Enter fullscreen mode Exit fullscreen mode

Explanation:
If HTML template is received:

Store it as renderedHtml to be rendered.

After DOM updates ($nextTick), re-bind event handlers.

        document.body.addEventListener("click", (event) => this.handleButtonClick(event));
    },
Enter fullscreen mode Exit fullscreen mode

Explanation:
Listens for any click on the page and calls a handleButtonClick method .

 beforeDestroy() {
        document.removeEventListener('keydown', this.handleEnterKeyPress);
    },
Enter fullscreen mode Exit fullscreen mode

Explanation:
Removes global keydown listener.

})

app.mount('#app')
Enter fullscreen mode Exit fullscreen mode

Explanation:
Mounts the Vue app to the HTML element with id="app".


Function Node

  • Add a Function Node to your workflow.
  • ensure you link it directly to the Uibuilder Node

Note:
The Function Node in Node-RED lets you write custom JavaScript code to process, modify, or analyze messages (msg) that flow through your workflow.

In the Function Node, we receive a document formatted in Markdown and store it in a variable. Then, we analyze the document for Markdown symbols and replace them with corresponding HTML tags to ensure proper display in another Uibuilder.

if (msg._alreadyProcessed) {
    return null; 
}
msg._alreadyProcessed = true;


let text = msg.payload || ""

text = text.replace(/######(.*?)######/g, '<h6>$1</h6>')
text = text.replace(/#####[^#](.*?)#####/g, '<h5>$1</h5>')
text = text.replace(/####(.*?)####/g, '<h4>$1</h4>')
text = text.replace(/###(.*?)###/g, '<h3>$1</h3>')
text = text.replace(/##(.*?)##/g, '<h2>$1</h2>')
text = text.replace(/#(.*?)#/g, '<h1>$1</h1>')


text = text.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>')


text = text.replace(/_(.*?)_/g, '<i>$1</i>')


text = text.replace(/&&(.*?)&&/g, '<span class="highlight">$1</span>')


text = text.replace(/!\[image]\((.*?)\)/g, '<img src="$1">')

const html = `
<div>
${text}
</div>
`

msg.payload = html

let index = flow.get('fileIndex') || 1
flow.set('fileIndex', index + 1)

const basePath = "/data/user/uibuilder/document/src/output/"
msg.filename = `${basePath}generated_${index}.html`

return msg

Enter fullscreen mode Exit fullscreen mode

line-by-line explanation of the Function Node

if (msg._alreadyProcessed) {
    return null; 
}
Enter fullscreen mode Exit fullscreen mode

Check if this message has already been processed to avoid duplicate processing (maybe the flow sends it multiple times).

If yes (msg._alreadyProcessed is true), stop further processing by returning null.

msg._alreadyProcessed = true;
Enter fullscreen mode Exit fullscreen mode

Mark the message as processed by setting this flag to true so next time it won't be processed again.

let text = msg.payload || ""
Get the input text from msg.payload.
Enter fullscreen mode Exit fullscreen mode

If msg.payload is empty or undefined, use an empty string "" as a fallback.

text = text.replace(/######(.*?)######/g, '<h6>$1</h6>')
Enter fullscreen mode Exit fullscreen mode

Replace any text wrapped in six # on each side, e.g. ######text######, with an <h6> HTML heading tag.

text = text.replace(/#####[^#](.*?)#####/g, '<h5>$1</h5>')
Enter fullscreen mode Exit fullscreen mode

Replace text wrapped with five # (with a non-# character right after the first 5 #) on each side, e.g. #####text#####, with <h5>.

Note: The regex might be slightly off because of [^#] here, intended to avoid matching six #?


Write file Node

  • Add a Write file Node to your workflow.

  • Filename: msg.filename

  • Action: overWrite file

  • Encodeing: default

  • Ensure you link it directly to the Function Node

Image description

we want to create file that document save in it it has file name and path with should make automatically with some numbers so in function node we have functionlity to do that

Top comments (0)