node-red-contrib-markdown-note 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ted Lanham
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # Node-RED Markdown Note
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/node-red-contrib-markdown-note.svg)](https://www.npmjs.com/package/node-red-contrib-markdown-note)
4
+ [![Downloads](https://img.shields.io/npm/dt/node-red-contrib-markdown-note.svg)](https://www.npmjs.com/package/node-red-contrib-markdown-note)
5
+ [![License](https://img.shields.io/npm/l/node-red-contrib-markdown-note.svg)](LICENSE)
6
+
7
+ A Node-RED node for adding **Markdown-formatted notes** directly on the flow canvas.
8
+ Designed for inline documentation, design notes, and contextual explanations that remain visible while editing or reviewing flows.
9
+
10
+ ![Node Preview](screenshots/hero.png)
11
+
12
+ ---
13
+
14
+ ## Why use Markdown Note?
15
+
16
+ The standard Comment node hides content by default. Markdown Note keeps your notes **always visible**, making it easier to:
17
+
18
+ - Document flow logic inline
19
+ - Highlight important information
20
+ - Include structured content with headings, lists, and code blocks
21
+
22
+ | Feature | Comment Node | Markdown Note |
23
+ |---------|-------------|---------------|
24
+ | Visibility | Collapsed by default | Always visible |
25
+ | Formatting | Plain text | Markdown (headers, lists, code, blockquotes) |
26
+ | Structure | Minimal | Suitable for detailed documentation |
27
+
28
+ ---
29
+
30
+ ## Features
31
+
32
+ - **Always-visible notes** – No need to click to expand.
33
+ - **Markdown rendering** – Support for headers, lists, emphasis, code blocks, quotes.
34
+ - **Task lists** – Track TODOs or action items inline.
35
+ - **Resizable & readable layout** – Automatically adjusts to content size.
36
+ - **Developer-focused** – Document payload formats, API contracts, assumptions, or edge cases directly on the flow.
37
+
38
+ ---
39
+
40
+
41
+ ## Known Issues
42
+
43
+ - **Links are not clickable**: Due to Node-RED's security restrictions on SVG content in the flow editor, hyperlinks rendered in the markdown cannot be clicked.
44
+ - **Visual jump on edit**: When closing the edit dialog, the node may appear slightly displaced or "jump" until you click away (deselect the node). This is a rendering artifact of the flow editor's redraw cycle.
45
+
46
+ ---
47
+
48
+ ## Installation
49
+
50
+ Run this in your Node-RED user directory (`~/.node-red`):
51
+
52
+ ```bash
53
+ npm install node-red-contrib-markdown-note
package/markdown.html ADDED
@@ -0,0 +1,361 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("note", {
3
+ category: "common",
4
+ color: "#F3E5AB",
5
+ defaults: {
6
+ content: { value: "" },
7
+ },
8
+ inputs: 0,
9
+ outputs: 0,
10
+ icon: "font-awesome/fa-file-text-o",
11
+ label: function () {
12
+ // Convert actual newlines to literal \n that Node-RED interprets as line breaks
13
+ if (this.content) {
14
+ return this.content.replace(/\n/g, "\\n ");
15
+ }
16
+ return "note";
17
+ },
18
+
19
+ oneditprepare: function () {
20
+ var content = this.content || "";
21
+
22
+ // Capture top-left anchor position before editing
23
+ // This ensures the node stays in place after edits
24
+ if (
25
+ this._noteTopY === undefined &&
26
+ this.y !== undefined &&
27
+ this.h !== undefined
28
+ ) {
29
+ this._noteTopY = this.y - this.h / 2;
30
+ this._noteLastY = this.y;
31
+ }
32
+
33
+ // Create the ACE editor with markdown mode
34
+ this.editor = RED.editor.createEditor({
35
+ id: "node-input-content-editor",
36
+ mode: "ace/mode/markdown",
37
+ value: content,
38
+ });
39
+
40
+ this.editor.focus();
41
+ },
42
+ oneditsave: function () {
43
+ this.content = this.editor.getValue();
44
+ $("#node-input-content").val(this.content);
45
+ this.editor.destroy();
46
+ delete this.editor;
47
+
48
+ // Reposition node after Node-RED finishes its layout
49
+ var node = this;
50
+ if (node._noteTopY !== undefined) {
51
+ setTimeout(function () {
52
+ // Re-render to get correct height
53
+ renderNoteNode(node);
54
+
55
+ // Anchor top edge
56
+ if (node.h !== undefined) {
57
+ node.y = node._noteTopY + node.h / 2;
58
+ node._noteLastY = node.y;
59
+ RED.view.redraw(true);
60
+ }
61
+ }, 0);
62
+ }
63
+ },
64
+ oneditcancel: function () {
65
+ this.editor.destroy();
66
+ delete this.editor;
67
+ },
68
+ oneditresize: function (size) {
69
+ var rows = $("#dialog-form>div:not(.node-text-editor-row)");
70
+ var height = $("#dialog-form").height();
71
+ for (var i = 0; i < rows.length; i++) {
72
+ height -= $(rows[i]).outerHeight(true);
73
+ }
74
+ var editorRow = $("#dialog-form>div.node-text-editor-row");
75
+ height -=
76
+ parseInt(editorRow.css("marginTop")) +
77
+ parseInt(editorRow.css("marginBottom"));
78
+ $(".node-text-editor").css("height", height + "px");
79
+ this.editor.resize();
80
+ },
81
+ });
82
+
83
+ // Render markdown content inside nodes on the workspace
84
+ RED.events.on("nodes:add", function (node) {
85
+ if (node.type === "note") {
86
+ setTimeout(function () {
87
+ renderNoteNode(node);
88
+ }, 100);
89
+ }
90
+ });
91
+
92
+ RED.events.on("nodes:change", function (node) {
93
+ if (node.type === "note") {
94
+ // Delay to allow Node-RED to recalculate size
95
+ setTimeout(function () {
96
+ renderNoteNode(node);
97
+ }, 100);
98
+ }
99
+ });
100
+
101
+ RED.events.on("view:redraw", function () {
102
+ // Single redraw after view settles
103
+ setTimeout(function () {
104
+ RED.nodes.eachNode(function (node) {
105
+ if (node.type === "note") {
106
+ renderNoteNode(node);
107
+ }
108
+ });
109
+ }, 100);
110
+ });
111
+
112
+ function renderNoteNode(node) {
113
+ var nodeEl = document.getElementById(node.id);
114
+ if (!nodeEl) return;
115
+
116
+ var g = d3.select(nodeEl);
117
+
118
+ // Clear any previous custom content FIRST
119
+ g.selectAll(".note-overlay").remove();
120
+
121
+ // CSS-BASED HIDING: Apply class to parent group
122
+ g.classed("note-hidden-content", true);
123
+
124
+ // Get the node's main rect to determine size
125
+ var rect = g.select("rect.node");
126
+ // Fallback if class .node doesn't exist
127
+ if (rect.empty()) rect = g.select("rect");
128
+ if (rect.empty()) return;
129
+
130
+ var bbox = rect.node().getBBox();
131
+ var w = bbox.width;
132
+ var h = bbox.height;
133
+
134
+ // INSET: Render overlay slightly smaller than rect to reveal the border stroke
135
+ // Node-RED standard nodes have rounded corners.
136
+ // By insetting 1px, we sit "inside" the stroke.
137
+ var inset = 1;
138
+
139
+ // Pre-process content: convert single newlines to markdown line breaks
140
+ // In markdown, single \n doesn't create a line break - need " \n" (two spaces + newline)
141
+ //
142
+ // We also need to preserve blank lines intentionally entered.
143
+ // Standard markdown often collapses consecutive blank lines.
144
+ // We replace empty or whitespace-only lines with a Non-Breaking Space (\u00A0).
145
+ // This forces the renderer to draw a line of height, which in turn
146
+ // allows the node to resize correctly for "visual" blank space.
147
+
148
+ var rawContent = node.content || "";
149
+
150
+ // Sanitize FIRST so we don't accidentally escape our injection or leave an opening for injection
151
+ var sanitized = RED.utils.sanitize(rawContent);
152
+
153
+ // Split into lines
154
+ var lines = sanitized.split("\n");
155
+
156
+ // Process each line: if empty/whitespace, replace with &nbsp; (unicode \u00A0)
157
+ var processedLines = lines.map(function (line) {
158
+ if (!line.trim()) {
159
+ return "\u00A0";
160
+ }
161
+ return line;
162
+ });
163
+
164
+ // Join back with markdown hard break (space space newline)
165
+ var finalMarkdown = processedLines.join(" \n");
166
+
167
+ // Render markdown to HTML
168
+ var html = RED.utils.renderMarkdown(finalMarkdown);
169
+
170
+ // Create foreignObject to hold the HTML content
171
+ var fo = g
172
+ .append("foreignObject")
173
+ .attr("class", "note-overlay")
174
+ .attr("x", inset)
175
+ .attr("y", inset)
176
+ .attr("width", w - inset * 2)
177
+ .attr("height", h - inset * 2)
178
+ .attr("pointer-events", "none");
179
+
180
+ var contentDiv = fo
181
+ .append("xhtml:div")
182
+ .attr("class", "note-node-content")
183
+ .attr("xmlns", "http://www.w3.org/1999/xhtml")
184
+ .style("width", w - inset * 2 + "px")
185
+ .style("overflow", "hidden")
186
+ .html(html);
187
+
188
+ // Measure actual content height and resize node to fit
189
+ var contentNode = contentDiv.node();
190
+ if (contentNode) {
191
+ // Minimum height of one line (14px font * 1.5 line-height + padding)
192
+ var minHeight = 28;
193
+ var actualHeight = Math.max(
194
+ contentNode.scrollHeight + inset * 2,
195
+ minHeight,
196
+ );
197
+
198
+ // Use the smaller of Node-RED's allocation or our measured height
199
+ // SNAP TO GRID: Round up to nearest 40px (double grid size)
200
+ // Node-RED nodes align to center.
201
+ // Height 20px (1 unit) -> top edge is at -10px (mid-grid).
202
+ // Height 40px (2 units) -> top edge is at -20px (on-grid).
203
+ // To ensure consistent edge alignment, we enforce 40px increments.
204
+ var snappedHeight = Math.ceil(actualHeight / 40) * 40;
205
+
206
+ // We use snappedHeight directly to allow expansion beyond initial 'h'
207
+ var finalHeight = Math.max(snappedHeight, Math.ceil(minHeight / 40) * 40);
208
+
209
+ // Track top edge position to anchor the node
210
+ // If this is first render OR user has moved the node, recalculate anchor
211
+ var currentTopY = node.y - h / 2;
212
+ if (node._noteTopY === undefined || node._noteLastY === undefined) {
213
+ // First time: store current top edge as anchor
214
+ node._noteTopY = currentTopY;
215
+ } else if (Math.abs(node.y - node._noteLastY) > 1) {
216
+ // User moved the node - update anchor
217
+ node._noteTopY = currentTopY;
218
+ }
219
+
220
+ // Set y so top edge stays at _noteTopY
221
+ var newY = node._noteTopY + finalHeight / 2;
222
+ node.y = newY;
223
+ node._noteLastY = newY;
224
+
225
+ rect.attr("height", finalHeight);
226
+ fo.attr("height", finalHeight - inset * 2);
227
+ contentDiv.style("height", finalHeight - inset * 2 + "px");
228
+
229
+ // Update Node-RED's internal dimensions
230
+ node.h = finalHeight;
231
+ }
232
+ }
233
+ </script>
234
+
235
+ <script type="text/html" data-template-name="note">
236
+ <div class="form-row node-text-editor-row">
237
+ <input type="hidden" id="node-input-content" />
238
+ <div
239
+ style="height: 250px; min-height:150px;"
240
+ class="node-text-editor"
241
+ id="node-input-content-editor"
242
+ ></div>
243
+ </div>
244
+ </script>
245
+
246
+ <script type="text/html" data-help-name="note">
247
+ <p>A node for displaying markdown-formatted notes in your flow.</p>
248
+ <h3>Usage</h3>
249
+ <p>
250
+ Use this node to add documentation, instructions, or annotations directly in
251
+ your Node-RED flows.
252
+ </p>
253
+ <p>
254
+ Double-click to edit. Use the toolbar buttons or type markdown syntax
255
+ directly:
256
+ </p>
257
+ <ul>
258
+ <li><b>Headers:</b> # H1, ## H2, ### H3</li>
259
+ <li><b>Bold:</b> **text**</li>
260
+ <li><b>Italic:</b> _text_</li>
261
+ <li><b>Code:</b> `code`</li>
262
+ <li><b>Lists:</b> - item or 1. item</li>
263
+ <li><b>Links:</b> [text](url)</li>
264
+ <li><b>Quote:</b> > text</li>
265
+ </ul>
266
+
267
+ </script>
268
+
269
+ <style>
270
+ /* Markdown content rendered inside the node */
271
+ .note-node-content {
272
+ font-size: 14px;
273
+ line-height: 1.5;
274
+ color: #333;
275
+ font-family:
276
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
277
+ padding: 0 8px;
278
+ background: #f3e5ab;
279
+ border-radius: 4px;
280
+ }
281
+
282
+ .note-node-content h1 {
283
+ font-size: 18px;
284
+ font-weight: bold;
285
+ margin: 0 0 8px 0;
286
+ border-bottom: 1px solid #ccc;
287
+ padding-bottom: 4px;
288
+ }
289
+
290
+ .note-node-content h2 {
291
+ font-size: 16px;
292
+ font-weight: bold;
293
+ margin: 8px 0 6px 0;
294
+ }
295
+
296
+ .note-node-content h3 {
297
+ font-size: 15px;
298
+ font-weight: bold;
299
+ margin: 6px 0 4px 0;
300
+ }
301
+
302
+ .note-node-content p {
303
+ margin: 4px 0;
304
+ }
305
+
306
+ .note-node-content ul,
307
+ .note-node-content ol {
308
+ margin: 4px 0;
309
+ padding-left: 18px;
310
+ }
311
+
312
+ .note-node-content li {
313
+ margin: 2px 0;
314
+ }
315
+
316
+ .note-node-content code {
317
+ background: rgba(0, 0, 0, 0.08);
318
+ padding: 2px 5px;
319
+ border-radius: 2px;
320
+ font-family: monospace;
321
+ font-size: 13px;
322
+ }
323
+
324
+ .note-node-content pre {
325
+ background: rgba(0, 0, 0, 0.08);
326
+ padding: 3px;
327
+ border-radius: 2px;
328
+ overflow-x: auto;
329
+ margin: 2px 0;
330
+ }
331
+
332
+ .note-node-content pre code {
333
+ background: none;
334
+ padding: 0;
335
+ }
336
+
337
+ .note-node-content a {
338
+ color: #0066cc;
339
+ text-decoration: underline;
340
+ }
341
+
342
+ .note-node-content blockquote {
343
+ border-left: 2px solid #ccc;
344
+ margin: 2px 0;
345
+ padding-left: 6px;
346
+ color: #666;
347
+ font-style: italic;
348
+ }
349
+
350
+ /*
351
+ CSS Hiding Logic for Sticky Note
352
+ Hide Node-RED's default label text, icons, etc.
353
+ */
354
+ g.note-hidden-content text,
355
+ g.note-hidden-content .node_icon_group,
356
+ g.note-hidden-content .node_icon,
357
+ g.note-hidden-content image {
358
+ display: none !important;
359
+ visibility: hidden !important;
360
+ }
361
+ </style>
package/markdown.js ADDED
@@ -0,0 +1,7 @@
1
+ module.exports = function(RED) {
2
+ function NoteNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ // UI-only node - no message processing
5
+ }
6
+ RED.nodes.registerType("note", NoteNode);
7
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "node-red-contrib-markdown-note",
3
+ "version": "1.0.0",
4
+ "description": "A Node-RED node for displaying markdown notes as annotations",
5
+ "keywords": [
6
+ "node-red",
7
+ "markdown",
8
+ "note",
9
+ "comment",
10
+ "annotation",
11
+ "documentation"
12
+ ],
13
+ "author": "Ted Lanham <tedlanham@gmail.com>",
14
+ "license": "MIT",
15
+ "homepage": "https://github.com/Backroads4Me/node-red-contrib-markdown-note",
16
+ "bugs": {
17
+ "url": "https://github.com/Backroads4Me/node-red-contrib-markdown-note/issues"
18
+ },
19
+ "main": "markdown.js",
20
+ "files": [
21
+ "markdown.js",
22
+ "markdown.html",
23
+ "package.json",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "node-red": {
28
+ "version": ">=3.0.0",
29
+ "nodes": {
30
+ "note": "markdown.js"
31
+ }
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/Backroads4Me/node-red-contrib-markdown-note.git"
36
+ },
37
+ "engines": {
38
+ "node": ">=14.0.0"
39
+ }
40
+ }