@manny-est/node-red-flowpilot 0.4.1 → 0.5.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.
@@ -0,0 +1,165 @@
1
+ // ---- Markdown rendering -----------------------------------------------
2
+ // Chat bubbles render a small, safe subset of markdown. All text is
3
+ // HTML-escaped before any tags are introduced, and only the fixed set of
4
+ // tags this code itself emits ever reaches the DOM — raw HTML from the
5
+ // model or the user is never interpreted, so no separate HTML sanitizer
6
+ // (e.g. DOMPurify) is needed.
7
+ function escapeHtml(str) {
8
+ return String(str)
9
+ .replace(/&/g, "&")
10
+ .replace(/</g, "&lt;")
11
+ .replace(/>/g, "&gt;")
12
+ .replace(/"/g, "&quot;")
13
+ .replace(/'/g, "&#39;");
14
+ }
15
+
16
+ // Inline markdown within a single (already-escaped) line: `code`,
17
+ // **bold**, *italic*, and [text](http(s) url) links. Code spans are
18
+ // split out first so their contents are immune to further markup.
19
+ function renderInlineMarkdown(escaped) {
20
+ var parts = escaped.split(/(`[^`]+`)/);
21
+ return parts.map(function (part, i) {
22
+ if (i % 2 === 1) {
23
+ return "<code>" + part.slice(1, -1) + "</code>";
24
+ }
25
+ return part
26
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
27
+ .replace(/\*([^*]+)\*/g, "<em>$1</em>")
28
+ .replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
29
+ }).join("");
30
+ }
31
+
32
+ // Unique per-block id so the delegated copy-button handler (bound once,
33
+ // see content.find("#fp-messages").on("click", ".fp-code-copy", ...))
34
+ // can find the right <pre> — chat messages are injected as raw HTML
35
+ // strings via .html(), so a per-element .on() bind at construction time
36
+ // isn't possible here the way it is for the Generate review panel's
37
+ // JSON-tab copy button.
38
+ var nextCodeBlockId = 1;
39
+
40
+ // GFM table helpers, used by renderMarkdown below.
41
+ function isTableSeparatorRow(line) {
42
+ var trimmed = line.trim();
43
+ if (!trimmed) { return false; }
44
+ return /^\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?$/.test(trimmed);
45
+ }
46
+
47
+ function splitTableRow(line) {
48
+ var trimmed = line.trim();
49
+ if (trimmed.charAt(0) === "|") { trimmed = trimmed.slice(1); }
50
+ if (trimmed.charAt(trimmed.length - 1) === "|") { trimmed = trimmed.slice(0, -1); }
51
+ return trimmed.split("|").map(function (c) { return c.trim(); });
52
+ }
53
+
54
+ // Block-level markdown: fenced code blocks, headings, tables,
55
+ // bullet/numbered lists, and paragraphs (consecutive lines joined with
56
+ // <br>).
57
+ function renderMarkdown(raw) {
58
+ var lines = String(raw || "").split("\n");
59
+ var html = "";
60
+ var listType = null;
61
+ var paraLines = [];
62
+
63
+ function flushPara() {
64
+ if (paraLines.length) {
65
+ html += "<p>" + paraLines.map(function (l) {
66
+ return renderInlineMarkdown(escapeHtml(l));
67
+ }).join("<br>") + "</p>";
68
+ paraLines = [];
69
+ }
70
+ }
71
+ function closeList() {
72
+ if (listType) { html += "</" + listType + ">"; listType = null; }
73
+ }
74
+
75
+ var i = 0;
76
+ while (i < lines.length) {
77
+ var line = lines[i];
78
+
79
+ // Leading whitespace allowed: models commonly indent a fenced
80
+ // block nested under a numbered/bulleted list item (e.g. "1. Try
81
+ // this:\n ```bash\n curl ...\n ```"). An anchored-at-column-0
82
+ // regex misses that entirely, so the fence markers and everything
83
+ // inside fall through to plain paragraph text instead of a code
84
+ // block — exactly the "code blocks failed to load" bug.
85
+ var fence = line.match(/^\s*```(\w*)\s*$/);
86
+ if (fence) {
87
+ flushPara(); closeList();
88
+ var codeLines = [];
89
+ i++;
90
+ while (i < lines.length && !/^\s*```\s*$/.test(lines[i])) {
91
+ codeLines.push(lines[i]);
92
+ i++;
93
+ }
94
+ var codeBlockId = "fp-code-" + (nextCodeBlockId++);
95
+ html += "<div class=\"fp-code-toolbar\">" +
96
+ "<button class=\"fp-code-copy red-ui-button red-ui-button-small\" type=\"button\" data-code-id=\"" + codeBlockId + "\">Copy</button>" +
97
+ "</div>" +
98
+ "<pre id=\"" + codeBlockId + "\"><code>" + escapeHtml(codeLines.join("\n")) + "</code></pre>";
99
+ i++;
100
+ continue;
101
+ }
102
+
103
+ var heading = line.match(/^(#{1,6})\s+(.*)$/);
104
+ if (heading) {
105
+ flushPara(); closeList();
106
+ var level = Math.min(6, heading[1].length + 2);
107
+ html += "<h" + level + ">" + renderInlineMarkdown(escapeHtml(heading[2])) + "</h" + level + ">";
108
+ i++;
109
+ continue;
110
+ }
111
+
112
+ // GFM-style pipe table: a header row immediately followed by a
113
+ // separator row (---/:--/--:), then zero or more body rows. Models
114
+ // reach for tables constantly in comparison-style answers; without
115
+ // this, every row just fell through to a paragraph line, showing
116
+ // the raw "| a | b |" syntax verbatim.
117
+ if (line.indexOf("|") !== -1 && i + 1 < lines.length && isTableSeparatorRow(lines[i + 1])) {
118
+ flushPara(); closeList();
119
+ var headerCells = splitTableRow(line);
120
+ i += 2;
121
+ var bodyRows = [];
122
+ while (i < lines.length && lines[i].trim() && lines[i].indexOf("|") !== -1) {
123
+ bodyRows.push(splitTableRow(lines[i]));
124
+ i++;
125
+ }
126
+ html += "<table><thead><tr>" +
127
+ headerCells.map(function (c) {
128
+ return "<th>" + renderInlineMarkdown(escapeHtml(c)) + "</th>";
129
+ }).join("") +
130
+ "</tr></thead><tbody>" +
131
+ bodyRows.map(function (row) {
132
+ return "<tr>" + row.map(function (c) {
133
+ return "<td>" + renderInlineMarkdown(escapeHtml(c)) + "</td>";
134
+ }).join("") + "</tr>";
135
+ }).join("") +
136
+ "</tbody></table>";
137
+ continue;
138
+ }
139
+
140
+ var bullet = line.match(/^\s*[-*]\s+(.*)$/);
141
+ if (bullet) {
142
+ flushPara();
143
+ if (listType !== "ul") { closeList(); html += "<ul>"; listType = "ul"; }
144
+ html += "<li>" + renderInlineMarkdown(escapeHtml(bullet[1])) + "</li>";
145
+ i++;
146
+ continue;
147
+ }
148
+
149
+ var numbered = line.match(/^\s*\d+[.)]\s+(.*)$/);
150
+ if (numbered) {
151
+ flushPara();
152
+ if (listType !== "ol") { closeList(); html += "<ol>"; listType = "ol"; }
153
+ html += "<li>" + renderInlineMarkdown(escapeHtml(numbered[1])) + "</li>";
154
+ i++;
155
+ continue;
156
+ }
157
+
158
+ flushPara(); closeList();
159
+
160
+ if (line.trim()) { paraLines.push(line); }
161
+ i++;
162
+ }
163
+ flushPara(); closeList();
164
+ return html;
165
+ }