@zolomedia/bifrost-client 1.7.74

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.
Files changed (140) hide show
  1. package/L1_Foundation/L1_Foundation.js +13 -0
  2. package/L1_Foundation/bootstrap/bootstrap.js +11 -0
  3. package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
  4. package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
  5. package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
  6. package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
  7. package/L1_Foundation/bootstrap/module_registry.js +102 -0
  8. package/L1_Foundation/bootstrap/prism_loader.js +164 -0
  9. package/L1_Foundation/config/client_config.js +110 -0
  10. package/L1_Foundation/config/config.js +7 -0
  11. package/L1_Foundation/connection/connection.js +8 -0
  12. package/L1_Foundation/connection/websocket_connection.js +122 -0
  13. package/L1_Foundation/constants/bifrost_constants.js +284 -0
  14. package/L1_Foundation/constants/constants.js +7 -0
  15. package/L1_Foundation/logger/logger.js +10 -0
  16. package/L2_Handling/L2_Handling.js +15 -0
  17. package/L2_Handling/cache/cache.js +22 -0
  18. package/L2_Handling/cache/cache_constants.js +69 -0
  19. package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
  20. package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
  21. package/L2_Handling/cache/orchestration/orchestration.js +12 -0
  22. package/L2_Handling/cache/storage/session_manager.js +289 -0
  23. package/L2_Handling/cache/storage/storage.js +10 -0
  24. package/L2_Handling/cache/storage/storage_manager.js +590 -0
  25. package/L2_Handling/display/composite/composite.js +13 -0
  26. package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
  27. package/L2_Handling/display/composite/swiper_renderer.js +564 -0
  28. package/L2_Handling/display/composite/terminal_renderer.js +922 -0
  29. package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
  30. package/L2_Handling/display/display.js +30 -0
  31. package/L2_Handling/display/feedback/feedback.js +11 -0
  32. package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
  33. package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
  34. package/L2_Handling/display/inputs/button_renderer.js +634 -0
  35. package/L2_Handling/display/inputs/form_renderer.js +583 -0
  36. package/L2_Handling/display/inputs/input_renderer.js +658 -0
  37. package/L2_Handling/display/inputs/inputs.js +12 -0
  38. package/L2_Handling/display/navigation/menu_renderer.js +206 -0
  39. package/L2_Handling/display/navigation/navigation.js +11 -0
  40. package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
  41. package/L2_Handling/display/orchestration/orchestration.js +11 -0
  42. package/L2_Handling/display/orchestration/renderer.js +430 -0
  43. package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
  44. package/L2_Handling/display/outputs/alert_renderer.js +161 -0
  45. package/L2_Handling/display/outputs/audio_renderer.js +94 -0
  46. package/L2_Handling/display/outputs/card_renderer.js +229 -0
  47. package/L2_Handling/display/outputs/code_renderer.js +66 -0
  48. package/L2_Handling/display/outputs/dl_renderer.js +131 -0
  49. package/L2_Handling/display/outputs/header_renderer.js +162 -0
  50. package/L2_Handling/display/outputs/icon_renderer.js +107 -0
  51. package/L2_Handling/display/outputs/image_renderer.js +145 -0
  52. package/L2_Handling/display/outputs/list_renderer.js +190 -0
  53. package/L2_Handling/display/outputs/outputs.js +19 -0
  54. package/L2_Handling/display/outputs/table_renderer.js +765 -0
  55. package/L2_Handling/display/outputs/text_renderer.js +818 -0
  56. package/L2_Handling/display/outputs/typography_renderer.js +293 -0
  57. package/L2_Handling/display/outputs/video_renderer.js +116 -0
  58. package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
  59. package/L2_Handling/display/primitives/form_primitives.js +526 -0
  60. package/L2_Handling/display/primitives/generic_containers.js +109 -0
  61. package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
  62. package/L2_Handling/display/primitives/link_primitives.js +552 -0
  63. package/L2_Handling/display/primitives/lists_primitives.js +262 -0
  64. package/L2_Handling/display/primitives/media_primitives.js +383 -0
  65. package/L2_Handling/display/primitives/primitives.js +19 -0
  66. package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
  67. package/L2_Handling/display/primitives/table_primitives.js +528 -0
  68. package/L2_Handling/display/primitives/typography_primitives.js +175 -0
  69. package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
  70. package/L2_Handling/display/specialized/specialized.js +10 -0
  71. package/L2_Handling/hooks/hooks.js +9 -0
  72. package/L2_Handling/hooks/menu_integration.js +57 -0
  73. package/L2_Handling/hooks/widget_hook_manager.js +292 -0
  74. package/L2_Handling/message/message.js +8 -0
  75. package/L2_Handling/message/message_handler.js +701 -0
  76. package/L2_Handling/navigation/navigation.js +8 -0
  77. package/L2_Handling/navigation/navigation_manager.js +403 -0
  78. package/L2_Handling/zhooks/features/cache_live.js +287 -0
  79. package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
  80. package/L2_Handling/zhooks/zhooks_manager.js +65 -0
  81. package/L2_Handling/zvaf/zvaf.js +8 -0
  82. package/L2_Handling/zvaf/zvaf_manager.js +334 -0
  83. package/L3_Abstraction/L3_Abstraction.js +12 -0
  84. package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
  85. package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
  86. package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
  87. package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
  88. package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
  89. package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
  90. package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
  91. package/L3_Abstraction/renderer/renderer.js +1 -0
  92. package/L3_Abstraction/session/session.js +1 -0
  93. package/L4_Orchestration/L4_Orchestration.js +11 -0
  94. package/L4_Orchestration/client/client.js +1 -0
  95. package/L4_Orchestration/facade/facade.js +9 -0
  96. package/L4_Orchestration/facade/manager_registry.js +118 -0
  97. package/L4_Orchestration/facade/renderer_registry.js +274 -0
  98. package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
  99. package/L4_Orchestration/lifecycle/initializer.js +135 -0
  100. package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
  101. package/L4_Orchestration/rendering/facade.js +94 -0
  102. package/L4_Orchestration/rendering/rendering.js +7 -0
  103. package/LICENSE +21 -0
  104. package/README.md +82 -0
  105. package/bifrost_client.js +204 -0
  106. package/bifrost_core.js +1686 -0
  107. package/docs/ARCHITECTURE.md +111 -0
  108. package/docs/PROTOCOL.md +106 -0
  109. package/docs/RENDERERS.md +101 -0
  110. package/docs/SECURITY.md +92 -0
  111. package/package.json +24 -0
  112. package/syntax/prism-zconfig.js +41 -0
  113. package/syntax/prism-zenv.js +69 -0
  114. package/syntax/prism-zolo-theme.css +288 -0
  115. package/syntax/prism-zolo.js +380 -0
  116. package/syntax/prism-zschema.js +38 -0
  117. package/syntax/prism-zspark.js +25 -0
  118. package/syntax/prism-zui.js +68 -0
  119. package/zSys/accessibility/accessibility.js +10 -0
  120. package/zSys/accessibility/emoji_accessibility.js +173 -0
  121. package/zSys/dom/block_utils.js +122 -0
  122. package/zSys/dom/container_utils.js +370 -0
  123. package/zSys/dom/dom.js +13 -0
  124. package/zSys/dom/dom_utils.js +328 -0
  125. package/zSys/dom/encoding_utils.js +117 -0
  126. package/zSys/dom/style_utils.js +71 -0
  127. package/zSys/errors/error_display.js +299 -0
  128. package/zSys/errors/errors.js +10 -0
  129. package/zSys/theme/color_utils.js +274 -0
  130. package/zSys/theme/dark_mode_utils.js +272 -0
  131. package/zSys/theme/size_utils.js +256 -0
  132. package/zSys/theme/spacing_utils.js +405 -0
  133. package/zSys/theme/theme.js +14 -0
  134. package/zSys/theme/zbase.css +1735 -0
  135. package/zSys/theme/zbase_inject.js +161 -0
  136. package/zSys/theme/ztheme_utils.js +305 -0
  137. package/zSys/validation/error_boundary.js +201 -0
  138. package/zSys/validation/validation.js +11 -0
  139. package/zSys/validation/validation_utils.js +238 -0
  140. package/zSys/zSys.js +14 -0
@@ -0,0 +1,818 @@
1
+ /**
2
+ *
3
+ * Text Renderer - Plain & Rich Text Display
4
+ *
5
+ *
6
+ * Renders text events from zCLI backend, supporting both plain text
7
+ * and rich text with markdown inline formatting.
8
+ *
9
+ * @module rendering/text_renderer
10
+ * @layer 3
11
+ * @pattern Strategy (single event type)
12
+ *
13
+ * Philosophy:
14
+ * - "Terminal first" - text is the foundation of all zCLI output
15
+ * - Pure rendering (no WebSocket, no state, no side effects)
16
+ * - Uses Layer 2 utilities exclusively (no inline logic)
17
+ *
18
+ * Supported Events:
19
+ * - 'text': Plain text with no formatting
20
+ * - 'rich_text': Text with markdown inline syntax (NEW)
21
+ *
22
+ * Markdown Syntax Supported:
23
+ * - `code` -> <code>
24
+ * - **bold** -> <strong>
25
+ * - *italic* -> <em>
26
+ * - __underline__ -> <u>
27
+ * - ~~strikethrough~~ -> <del>
28
+ * - ==highlight== -> <mark>
29
+ * - [text](url) -> <a href>
30
+ * - \ (backslash + newline) -> <br> (recommended for YAML)
31
+ * - (double-space + newline) -> <br>
32
+ * - <br> literal tag (passes through)
33
+ *
34
+ * Dependencies:
35
+ * - Layer 2: dom_utils.js, ztheme_utils.js, error_boundary.js
36
+ *
37
+ * Exports:
38
+ * - TextRenderer: Class for rendering text and rich_text events
39
+ *
40
+ * Example:
41
+ * ```javascript
42
+ * import { TextRenderer } from './text_renderer.js';
43
+ *
44
+ * const renderer = new TextRenderer(logger);
45
+ *
46
+ * // Plain text (returns element, orchestrator handles appending)
47
+ * const textEl = renderer.render({
48
+ * content: 'Hello, zCLI!',
49
+ * color: 'primary',
50
+ * indent: 1
51
+ * }, 'zVaF');
52
+ *
53
+ * // Rich text with markdown (returns element)
54
+ * const richTextEl = renderer.renderRichText({
55
+ * content: 'Use **bold** and `code` syntax',
56
+ * color: 'info'
57
+ * });
58
+ * ```
59
+ */
60
+
61
+ // ─────────────────────────────────────────────────────────────────
62
+ // Imports
63
+ // ─────────────────────────────────────────────────────────────────
64
+
65
+ // Layer 2: Utilities
66
+ import { createElement, setAttributes } from '../../../zSys/dom/dom_utils.js';
67
+ import { getTextColorClass } from '../../../zSys/theme/ztheme_utils.js';
68
+ import { withErrorBoundary } from '../../../zSys/validation/error_boundary.js';
69
+ import emojiAccessibility from '../../../zSys/accessibility/emoji_accessibility.js';
70
+
71
+ // Link primitives: shared URL conversion and type detection (SSOT)
72
+ import { convertZPathToURL, detectLinkType, LINK_TYPE_EXTERNAL } from '../primitives/link_primitives.js';
73
+ import { escapeHtml, safeHref } from '../../../zSys/dom/encoding_utils.js';
74
+
75
+ //
76
+ // Text Renderer Class
77
+ //
78
+
79
+ /**
80
+ * TextRenderer - Renders plain text events
81
+ *
82
+ * Handles the 'text' zDisplay event, which is the most basic
83
+ * output primitive in zCLI. Renders a paragraph element with
84
+ * optional color and indentation.
85
+ */
86
+ export class TextRenderer {
87
+ /**
88
+ * Create a TextRenderer instance
89
+ * @param {Object} logger - Logger instance for debugging
90
+ */
91
+ constructor(logger) {
92
+ this.logger = logger || console;
93
+ this.logger.debug('[TextRenderer] Initialized');
94
+
95
+ // Wrap render methods with error boundary
96
+ const originalRender = this.render.bind(this);
97
+ this.render = withErrorBoundary(originalRender, {
98
+ component: 'TextRenderer.render',
99
+ logger: this.logger
100
+ });
101
+
102
+ const originalRenderRichText = this.renderRichText.bind(this);
103
+ this.renderRichText = withErrorBoundary(originalRenderRichText, {
104
+ component: 'TextRenderer.renderRichText',
105
+ logger: this.logger
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Parse markdown inline syntax to HTML
111
+ *
112
+ * @param {string} text - Text with markdown syntax
113
+ * @returns {string} HTML string with inline elements
114
+ * @private
115
+ *
116
+ * Supported markdown:
117
+ * - `code` -> <code>
118
+ * - **bold** -> <strong>
119
+ * - *italic* -> <em>
120
+ * - __underline__ -> <u>
121
+ * - ~~strikethrough~~ -> <del>
122
+ * - ==highlight== -> <mark>
123
+ * - [text](url) -> <a href="url">
124
+ * - \ (backslash + newline) -> <br> (YAML-friendly)
125
+ * - (double-space + newline) -> <br> (standard markdown, but YAML may strip spaces)
126
+ * - <br> literal tag -> <br> (passes through)
127
+ */
128
+ _parseMarkdown(text) {
129
+ // STEP 1: Process semantic distinction for zMD
130
+ // Convert \x1F (YAML multilines) to \n temporarily (for list processing)
131
+ // We'll convert remaining \n to <br> after lists are processed
132
+ let html = text.replace(/\x1F/g, '\n');
133
+
134
+ // NOTE: Explicit \n will be handled in renderRichText (split into multiple <p> tags)
135
+
136
+ // Trim trailing newlines to avoid extra <br> at the end
137
+ html = html.replace(/\n+$/, '');
138
+
139
+ // Code blocks: ```language\ncode\n``` -> <pre><code>code</code></pre>
140
+ // Must be processed BEFORE inline code to avoid conflicts
141
+ // Use placeholder to protect code blocks from heading regex
142
+ //
143
+ // Prism.js syntax highlighting for ```zolo blocks:
144
+ // - Fixed: Missing /g (global) flags caused only first occurrences to match
145
+ // - CSS theme (prism-zolo-theme.css) auto-generated from zlsp SSOT colors:
146
+ // * Root-keys (zBlocks): Salmon orange (#ffaf87) - matches IDE
147
+ // * Display events (zH1, zMD, etc.): Orange red (#ff5f00)
148
+ // * Properties (nested keys): Golden yellow (#ffd787)
149
+ // * Metadata (_zClass, etc.): Cyan (#00ffff)
150
+ // See: zlsp/themes/generators/prism.py, zlsp/themes/zolo_default.yaml
151
+ const codeBlockPlaceholders = [];
152
+ html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (match, language, code) => {
153
+ // Escape HTML in code
154
+ const escapedCode = code
155
+ .replace(/&/g, '&amp;')
156
+ .replace(/</g, '&lt;')
157
+ .replace(/>/g, '&gt;')
158
+ .replace(/"/g, '&quot;')
159
+ .replace(/'/g, '&#039;');
160
+
161
+ // Special case: ```pre renders as semantic <pre> without <code> wrapper
162
+ // Parallels zText semantic: pre option
163
+ if (language === 'pre') {
164
+ const placeholder = `___CODE_BLOCK_${codeBlockPlaceholders.length}___`;
165
+ codeBlockPlaceholders.push(`<pre class="zFont-mono">${escapedCode}</pre>`);
166
+ return placeholder;
167
+ }
168
+
169
+ // Apply language class if specified
170
+ const langClass = language ? ` language-${language}` : '';
171
+ const placeholder = `___CODE_BLOCK_${codeBlockPlaceholders.length}___`;
172
+ codeBlockPlaceholders.push(`<pre class="zBg-dark zText-light zp-3 zRounded zOverflow-auto" tabindex="0"><code class="zFont-mono${langClass}">${escapedCode}</code></pre>`);
173
+ return placeholder;
174
+ });
175
+
176
+ // Headings: # H1 through ###### H6
177
+ // Process at line start or after newline, must be before bold/italic to avoid conflicts
178
+ // Accept both "# Title" (standard) and "#Title" (lenient)
179
+ html = html.replace(/(?:^|\n)(#{1,6})\s*(.+?)(?=\n|$)/g, (match, hashes, text) => {
180
+ const level = hashes.length;
181
+ const trimmedText = text.trim();
182
+ return `\n<h${level}>${trimmedText}</h${level}>\n`;
183
+ });
184
+
185
+ // Tables: | Col1 | Col2 | -> <table>...</table>
186
+ // Must be processed BEFORE inline code to preserve code in table cells
187
+ // Pattern: header row, separator row (|---|---|), data rows
188
+ html = html.replace(/(?:^|\n)(\|.+\|\n\|[-:|]+\|\n(?:\|.+\|\n?)+)/g, (match, tableBlock) => {
189
+ const lines = tableBlock.trim().split('\n');
190
+ if (lines.length < 3) return match; // Need at least header, separator, and 1 data row
191
+
192
+ // Extract header
193
+ const headerCells = lines[0].split('|').map(cell => cell.trim()).filter(cell => cell);
194
+
195
+ // Skip separator line (lines[1])
196
+
197
+ // Extract data rows
198
+ const dataRows = lines.slice(2).map(line =>
199
+ line.split('|').map(cell => cell.trim()).filter(cell => cell)
200
+ );
201
+
202
+ // Build HTML table
203
+ let tableHTML = '\n<table class="table zmy-4">\n';
204
+
205
+ // Header
206
+ tableHTML += ' <thead>\n <tr>\n';
207
+ headerCells.forEach(cell => {
208
+ tableHTML += ` <th>${cell}</th>\n`;
209
+ });
210
+ tableHTML += ' </tr>\n </thead>\n';
211
+
212
+ // Body
213
+ tableHTML += ' <tbody>\n';
214
+ dataRows.forEach(row => {
215
+ tableHTML += ' <tr>\n';
216
+ row.forEach(cell => {
217
+ tableHTML += ` <td>${cell}</td>\n`;
218
+ });
219
+ tableHTML += ' </tr>\n';
220
+ });
221
+ tableHTML += ' </tbody>\n</table>\n';
222
+
223
+ return tableHTML;
224
+ });
225
+
226
+ // Inline Code: `code` -> <code>code</code> (after code blocks to avoid conflicts)
227
+ // Use placeholders to protect code content from further markdown processing
228
+ const inlineCodeBlocks = [];
229
+ // Double-backtick spans FIRST: `` `text` `` -> <code>`text`</code>
230
+ // Allows single backticks inside; must precede single-backtick regex
231
+ html = html.replace(/``(.+?)``/g, (match, code) => {
232
+ const escaped = escapeHtml(code);
233
+ const placeholder = `___INLINE_CODE_${inlineCodeBlocks.length}___`;
234
+ inlineCodeBlocks.push(`<code>${escaped}</code>`);
235
+ return placeholder;
236
+ });
237
+ html = html.replace(/`([^`]+)`/g, (match, code) => {
238
+ // Escape HTML entities (SSOT) AND convert special chars to display literally
239
+ const escaped = escapeHtml(code)
240
+ .replace(/\n/g, '\\n') // Convert actual newlines to literal \n for display
241
+ .replace(/\t/g, '\\t'); // Convert actual tabs to literal \t for display
242
+ const placeholder = `___INLINE_CODE_${inlineCodeBlocks.length}___`;
243
+ inlineCodeBlocks.push(`<code>${escaped}</code>`);
244
+ return placeholder;
245
+ });
246
+
247
+ // Links: [text](url){attrs} -> <a href="url" target=… class="classes">text</a>
248
+ // MUST run AFTER inline code extraction so `[text](url)` inside backticks
249
+ // is already shielded by ___INLINE_CODE_N___ placeholders.
250
+ // Uses shared convertZPathToURL + detectLinkType from link_primitives.js (SSOT).
251
+ //
252
+ // The optional {…} brace is an attribute list (kramdown/pandoc-style):
253
+ // • target tokens override how the link opens —
254
+ // _blank | newtab | new-tab → new tab
255
+ // _self | sametab | same-tab → same tab
256
+ // • every other token is treated as a CSS class.
257
+ // With no token, target falls back to link type (external → new tab).
258
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)(?:\{([^}]*)\})?/g, (match, text, url, attrs) => {
259
+ // Harden against DOM-XSS: block dangerous href schemes + attr-escape the
260
+ // resolved URL, and HTML-escape the link label (it can carry user content).
261
+ // Inline markdown markers (**/*/etc.) survive escaping and convert later.
262
+ const href = safeHref(convertZPathToURL(url));
263
+ const label = escapeHtml(text);
264
+ const ltype = detectLinkType(url);
265
+
266
+ // Default target by link type; explicit {…} token wins.
267
+ let target = ltype === LINK_TYPE_EXTERNAL ? '_blank' : '_self';
268
+ const classTokens = [];
269
+ if (attrs && attrs.trim()) {
270
+ for (const tok of attrs.trim().split(/\s+/)) {
271
+ const t = tok.toLowerCase();
272
+ if (t === '_blank' || t === 'newtab' || t === 'new-tab') {
273
+ target = '_blank';
274
+ } else if (t === '_self' || t === 'sametab' || t === 'same-tab') {
275
+ target = '_self';
276
+ } else {
277
+ classTokens.push(tok);
278
+ }
279
+ }
280
+ }
281
+
282
+ // _blank always carries rel="noopener noreferrer" (security), regardless of source.
283
+ const rel = target === '_blank' ? ' rel="noopener noreferrer"' : '';
284
+ let classAttr = '';
285
+ if (classTokens.length) {
286
+ const sanitized = classTokens.join(' ').replace(/[^a-zA-Z0-9\-_ ]/g, '').replace(/\s+/g, ' ').trim();
287
+ if (sanitized) classAttr = ` class="${sanitized}"`;
288
+ }
289
+ return `<a href="${href}" target="${target}"${rel}${classAttr}>${label}</a>`;
290
+ });
291
+
292
+ // Lists -> <ul class="zList"> / <ol class="zList">. The marker is the type:
293
+ // UL - * + (disc / circle / square; - * + alone = empty nesting)
294
+ // OL 1- a- A- i- I- (decimal / alpha / roman) — token = digits |
295
+ // single letter | roman string, space-guarded.
296
+ // Process before bold/italic to avoid conflicts with * markers.
297
+ html = html.replace(
298
+ /(?:^|\n)((?:[ \t]*(?:[-*+](?:[ \t]+[^\n]*|[ \t]*)|(?:\d+|[ivxlcdmIVXLCDM]+|[A-Za-z])-[ \t]+[^\n]*)(?:\n|$))+)/g,
299
+ (match, listBlock) => '\n' + this._parseListBlock(listBlock.trimEnd()) + '\n'
300
+ );
301
+
302
+ // Blockquotes: > text -> <blockquote>text</blockquote>
303
+ // Process before bold/italic to avoid conflicts
304
+ // Updated: Keep empty > lines as line breaks within the same blockquote
305
+ html = html.replace(/(?:^|\n)((?:>.*?(?:\n|$))+)/g, (match, quoteBlock) => {
306
+ const lines = quoteBlock
307
+ .trim()
308
+ .split(/\n/)
309
+ .map(line => {
310
+ // Remove > prefix (and optional space after it)
311
+ const content = line.replace(/^>\s?/, '');
312
+ // If line had just >, it becomes empty string which will become <br>
313
+ return content;
314
+ });
315
+
316
+ // Join lines with <br>, treating empty strings as visual line breaks
317
+ const quoteContent = lines.join('<br>');
318
+ // Clean semantic element — base styling lives in zSys/theme/zbase.css
319
+ // (zTheme base), not hardcoded here, so it themes per-app and per-mode.
320
+ return `\n<blockquote><p>${quoteContent}</p></blockquote>\n`;
321
+ });
322
+
323
+ // Bold: **text** -> <strong>text</strong>
324
+ // Use non-greedy .*? to allow nested italics (e.g., **text with *italic* inside**)
325
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
326
+
327
+ // Italic: *text* -> <em>text</em> (but not ** from bold)
328
+ // Use non-greedy .*? for consistency
329
+ html = html.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
330
+
331
+ // Underline/Strikethrough/Highlight run BEFORE inline code restoration
332
+ // so their syntax inside backtick spans stays shielded by placeholders
333
+
334
+ // Underline: __text__ -> <u>text</u>
335
+ // Negative lookaround prevents matching ___INLINE_CODE_N___ placeholders
336
+ html = html.replace(/(?<!_)__(?!_)([^_\n]+?)(?<!_)__(?!_)/g, '<u>$1</u>');
337
+
338
+ // Strikethrough: ~~text~~ -> <del>text</del>
339
+ html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>');
340
+
341
+ // Highlight: ==text== -> <mark>text</mark>
342
+ html = html.replace(/==([^=]+)==/g, '<mark>$1</mark>');
343
+
344
+ // Restore inline code blocks — must be last so all inline syntax above
345
+ // was shielded by ___INLINE_CODE_N___ placeholders
346
+ html = html.replace(/___INLINE_CODE_(\d+)___/g, (match, index) => {
347
+ return inlineCodeBlocks[parseInt(index)];
348
+ });
349
+
350
+ // Line breaks: backslash + newline -> <br> (won't be stripped by YAML)
351
+ html = html.replace(/\\\n/g, '<br>');
352
+
353
+ // Line breaks: double-space + newline -> <br> (markdown standard, but YAML may strip)
354
+ html = html.replace(/ {2}\n/g, '<br>');
355
+
356
+ // Convert remaining newlines to <br> (but NOT within <pre> tags or <ul>/<ol>)
357
+ // These are from \x1F markers (YAML multilines), not explicit \n (which are handled by renderRichText)
358
+ // Strategy: Extract code blocks and lists, convert newlines, then restore
359
+ const preservedBlocks = [];
360
+ html = html.replace(/(<pre[\s\S]*?<\/pre>|<ul[\s\S]*?<\/ul>|<ol[\s\S]*?<\/ol>)/g, (match) => {
361
+ const placeholder = `___PRESERVED_BLOCK_${preservedBlocks.length}___`;
362
+ preservedBlocks.push(match);
363
+ return placeholder;
364
+ });
365
+
366
+ // Convert remaining newlines to <br> (from \x1F markers for line breaks)
367
+ html = html.replace(/\n/g, '<br>');
368
+
369
+ // Restore preserved blocks
370
+ preservedBlocks.forEach((block, index) => {
371
+ html = html.replace(`___PRESERVED_BLOCK_${index}___`, block);
372
+ });
373
+
374
+ // Restore code block placeholders (from earlier protection against heading regex)
375
+ codeBlockPlaceholders.forEach((block, index) => {
376
+ html = html.replace(`___CODE_BLOCK_${index}___`, block);
377
+ });
378
+
379
+ // Remove leading and trailing <br> tags (caused by newlines around lists/blocks)
380
+ html = html.replace(/^(<br>)+/, ''); // Remove leading <br>
381
+ html = html.replace(/(<br>)+$/, ''); // Remove trailing <br>
382
+
383
+ return html;
384
+ }
385
+
386
+ /**
387
+ * Render a rich_text event with markdown parsing
388
+ *
389
+ * @param {Object} data - Rich text event data
390
+ * @param {string} data.content - Text content with markdown syntax
391
+ * @param {string} [data.color] - Text color (primary, secondary, info, success, warning, error)
392
+ * @param {number} [data.indent=0] - Indentation level (0 = no indent)
393
+ * @param {string} [data._zClass] - Custom CSS class (optional, from YAML)
394
+ * @param {string} [data._id] - Custom element ID (optional)
395
+ * @returns {HTMLElement|null} Created paragraph element or null if failed
396
+ *
397
+ * @example
398
+ * renderer.renderRichText({ content: 'This is **bold** and *italic*' });
399
+ * renderer.renderRichText({ content: 'Use `code` for commands', color: 'info' });
400
+ */
401
+ renderRichText(data) {
402
+ const { content, color, indent = 0, _id } = data;
403
+
404
+ // Validate required parameters
405
+ if (!content) {
406
+ this.logger.error('[TextRenderer] [ERROR] Missing required parameter: content');
407
+ return null;
408
+ }
409
+
410
+ // Build CSS classes array.
411
+ // NOTE: _zClass is applied centrally by the orchestrator (SSOT, append mode)
412
+ // on the returned element — this renderer only owns the contextual color class.
413
+ const classes = [];
414
+
415
+ // Add color class if provided (uses Layer 2 utility)
416
+ if (color) {
417
+ const colorClass = getTextColorClass(color);
418
+ if (colorClass) {
419
+ classes.push(colorClass);
420
+ }
421
+ }
422
+
423
+ // Protect inline code from escape decoding (keep literal \n, \t, etc.)
424
+ // Extract backtick content BEFORE decoding
425
+ const inlineCodeBlocks = [];
426
+ const protectedContent = content.replace(/`([^`]+)`/g, (match, code) => {
427
+ const placeholder = `___INLINE_CODE_${inlineCodeBlocks.length}___`;
428
+ inlineCodeBlocks.push(code); // Store BEFORE decoding (keeps literal \n)
429
+ return placeholder;
430
+ });
431
+
432
+ // Now decode escapes in the text OUTSIDE of inline code
433
+ const decodedContent = this._decodeUnicodeEscapes(protectedContent);
434
+
435
+ // Check if decoded content contains explicit \n (paragraph breaks)
436
+ // If so, split into multiple paragraphs; otherwise, render as single paragraph
437
+ if (decodedContent.includes('\n')) {
438
+ // MULTI-PARAGRAPH MODE: Group content blocks (keep list lines together)
439
+ let paragraphs = this._groupContentBlocks(decodedContent);
440
+
441
+ // Restore inline code in each paragraph (keep literal, no decoding)
442
+ paragraphs = paragraphs.map(para => {
443
+ let restored = para;
444
+ inlineCodeBlocks.forEach((code, i) => {
445
+ // Restore with backticks - markdown parser will handle escaping
446
+ restored = restored.replace(`___INLINE_CODE_${i}___`, `\`${code}\``);
447
+ });
448
+ return restored;
449
+ });
450
+
451
+ // Create a container div for multiple paragraphs
452
+ const container = createElement('div', classes);
453
+
454
+ // Apply attributes to container
455
+ const attributes = {};
456
+ if (_id) {
457
+ attributes.id = _id;
458
+ }
459
+ if (indent > 0) {
460
+ attributes.style = `margin-left: ${indent}rem;`;
461
+ }
462
+ if (Object.keys(attributes).length > 0) {
463
+ setAttributes(container, attributes);
464
+ }
465
+
466
+ // Parse each paragraph and create appropriate elements
467
+ paragraphs.forEach((paragraphContent, index) => {
468
+ const parsedMarkdown = this._parseMarkdown(paragraphContent);
469
+ const accessibleHTML = emojiAccessibility.enhanceText(parsedMarkdown);
470
+
471
+ // Check if parsed content contains block-level elements (headings, ul, ol, pre, etc.)
472
+ // Block elements should NOT be wrapped in <p> tags
473
+ const hasBlockElements = /<(h[1-6]|ul|ol|pre|blockquote|div|table)[\s>]/.test(accessibleHTML);
474
+
475
+ if (hasBlockElements) {
476
+ // Create a temporary container to parse the HTML
477
+ const temp = document.createElement('div');
478
+ temp.innerHTML = accessibleHTML;
479
+
480
+ // Append all children directly (unwrap from paragraph)
481
+ Array.from(temp.childNodes).forEach(child => {
482
+ container.appendChild(child);
483
+ });
484
+ } else {
485
+ // Regular text content - wrap in <p>
486
+ const p = createElement('p', []);
487
+ p.innerHTML = accessibleHTML;
488
+ container.appendChild(p);
489
+ }
490
+
491
+ // Apply syntax highlighting to code blocks
492
+ if (window.Prism) {
493
+ const codeBlocks = container.querySelectorAll('pre code[class*="language-"]');
494
+ codeBlocks.forEach((codeBlock) => {
495
+ Prism.highlightElement(codeBlock);
496
+ });
497
+ }
498
+ });
499
+
500
+ this.logger.debug(`[TextRenderer] Rendered rich_text (%s paragraphs)`, paragraphs.length);
501
+ return container;
502
+
503
+ } else {
504
+ // SINGLE-PARAGRAPH MODE: No explicit \n
505
+
506
+ // Restore inline code before parsing markdown (keep literal, no decoding)
507
+ let restoredContent = decodedContent;
508
+ inlineCodeBlocks.forEach((code, i) => {
509
+ // Restore with backticks - markdown parser will handle escaping
510
+ restoredContent = restoredContent.replace(`___INLINE_CODE_${i}___`, `\`${code}\``);
511
+ });
512
+
513
+ const parsedMarkdown = this._parseMarkdown(restoredContent);
514
+ const accessibleHTML = emojiAccessibility.enhanceText(parsedMarkdown);
515
+
516
+ // Check if parsed content contains block-level elements (headings, lists, etc.)
517
+ const hasBlockElements = /<(h[1-6]|ul|ol|pre|blockquote|div|table)[\s>]/.test(accessibleHTML);
518
+
519
+ let element;
520
+ if (hasBlockElements) {
521
+ // Create a container div for block elements (don't wrap in <p>)
522
+ element = createElement('div', classes);
523
+ element.innerHTML = accessibleHTML;
524
+ } else {
525
+ // Regular text content - wrap in <p>
526
+ element = createElement('p', classes);
527
+ element.innerHTML = accessibleHTML;
528
+ }
529
+
530
+ // Apply syntax highlighting to code blocks (Prism.js)
531
+ if (window.Prism) {
532
+ const codeBlocks = element.querySelectorAll('pre code[class*="language-"]');
533
+ codeBlocks.forEach((codeBlock) => {
534
+ Prism.highlightElement(codeBlock);
535
+ });
536
+ }
537
+
538
+ // Apply attributes
539
+ const attributes = {};
540
+ if (_id) {
541
+ attributes.id = _id;
542
+ }
543
+ if (indent > 0) {
544
+ attributes.style = `margin-left: ${indent}rem;`;
545
+ }
546
+ if (Object.keys(attributes).length > 0) {
547
+ setAttributes(element, attributes);
548
+ }
549
+
550
+ this.logger.debug(`[TextRenderer] Rendered rich_text (single ${hasBlockElements ? 'block' : 'paragraph'})`);
551
+ return element;
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Render a text event
557
+ *
558
+ * @param {Object} data - Text event data
559
+ * @param {string} data.content - Text content to display
560
+ * @param {string} [data.color] - Text color (primary, secondary, info, success, warning, error)
561
+ * @param {number} [data.indent=0] - Indentation level (0 = no indent)
562
+ * @param {string} [data.class] - Custom CSS class (optional)
563
+ * @param {string} zone - Target DOM element ID
564
+ * @returns {HTMLElement|null} Created paragraph element or null if failed
565
+ *
566
+ * @example
567
+ * renderer.render({ content: 'Hello!' }, 'zVaF');
568
+ * renderer.render({ content: 'Success!', color: 'success' }, 'zVaF');
569
+ * renderer.render({ content: 'Indented', indent: 2 }, 'zVaF');
570
+ */
571
+ render(data, zone) {
572
+ const { content, color, indent = 0, class: customClass } = data;
573
+
574
+ // Validate required parameters
575
+ if (!content) {
576
+ this.logger.error('[TextRenderer] [ERROR] Missing required parameter: content');
577
+ return null;
578
+ }
579
+
580
+ // Get target container
581
+ const container = document.getElementById(zone);
582
+ if (!container) {
583
+ this.logger.error(`[TextRenderer] [ERROR] Zone not found: ${zone}`);
584
+ return null;
585
+ }
586
+
587
+ // Build CSS classes array
588
+ const classes = [];
589
+
590
+ // Add custom class if provided (from YAML)
591
+ if (customClass) {
592
+ // Split space-separated classes (e.g., "zText-center zmt-3 zmb-4")
593
+ const customClasses = customClass.split(/\s+/).filter(c => c);
594
+ classes.push(...customClasses);
595
+ }
596
+
597
+ // Add color class if provided (uses Layer 2 utility)
598
+ if (color) {
599
+ const colorClass = getTextColorClass(color);
600
+ if (colorClass) {
601
+ classes.push(colorClass);
602
+ }
603
+ }
604
+
605
+ // Create paragraph element (using Layer 2 utility)
606
+ const p = createElement('p', classes);
607
+ p.textContent = content; // Use textContent for XSS safety
608
+
609
+ // Apply attributes
610
+ const attributes = {};
611
+
612
+ // Apply indent as inline style (zTheme doesn't have indent utilities)
613
+ // Each indent level = 1rem left margin
614
+ if (indent > 0) {
615
+ attributes.style = `margin-left: ${indent}rem;`;
616
+ }
617
+
618
+ if (Object.keys(attributes).length > 0) {
619
+ setAttributes(p, attributes);
620
+ }
621
+
622
+ // Append to container
623
+ container.appendChild(p);
624
+
625
+ // Log success
626
+ this.logger.debug(`[TextRenderer] Rendered text (%s chars, indent: %s)`, content.length, indent);
627
+
628
+ return p;
629
+ }
630
+
631
+ /**
632
+ * Decode Unicode escape sequences to actual characters
633
+ * Supports: \uXXXX (standard) and \UXXXXXXXX (extended) formats
634
+ *
635
+ * Note: Basic escape sequences (\n, \t, etc.) are handled by JSON.parse()
636
+ * automatically when receiving data from backend. We only need to decode
637
+ * custom Unicode formats that JSON doesn't handle.
638
+ *
639
+ * @param {string} text - Text containing Unicode escapes
640
+ * @returns {string} - Decoded text
641
+ * @private
642
+ */
643
+ _decodeUnicodeEscapes(text) {
644
+ if (!text || typeof text !== 'string') return text;
645
+
646
+ // Replace \uXXXX format (standard 4-digit Unicode escape)
647
+ text = text.replace(/\\u([0-9A-Fa-f]{4})/g, (match, hexCode) => {
648
+ return String.fromCodePoint(parseInt(hexCode, 16));
649
+ });
650
+
651
+ // Replace \UXXXXXXXX format (extended 4-8 digit for supplementary characters & emojis)
652
+ text = text.replace(/\\U([0-9A-Fa-f]{4,8})/g, (match, hexCode) => {
653
+ return String.fromCodePoint(parseInt(hexCode, 16));
654
+ });
655
+
656
+ // Replace basic escape sequences (literal strings like \\n, \\t, etc.)
657
+ // These come from JSON where Python sends "\n" which becomes "\\n" in JSON
658
+ text = text
659
+ .replace(/\\n/g, '\n') // Newline
660
+ .replace(/\\t/g, '\t') // Tab
661
+ .replace(/\\r/g, '\r') // Carriage return
662
+ .replace(/\\'/g, "'") // Single quote
663
+ .replace(/\\"/g, '"') // Double quote
664
+ .replace(/\\\\/g, '\\'); // Backslash (must be last!)
665
+
666
+ return text;
667
+ }
668
+
669
+ /**
670
+ * Group multi-line content, keeping consecutive list lines together so they
671
+ * are passed as a single block to _parseMarkdown rather than one line at a time.
672
+ * @param {string} content - Decoded multi-line content
673
+ * @returns {string[]} Array of content groups
674
+ */
675
+ _groupContentBlocks(content) {
676
+ const lines = content.split('\n');
677
+ const groups = [];
678
+ let listBuffer = [];
679
+
680
+ const isListLine = (line) => {
681
+ const trimmed = line.trimStart();
682
+ return /^[-*+][ \t]/.test(trimmed) || // UL: - * + text
683
+ /^(?:\d+|[ivxlcdmIVXLCDM]+|[A-Za-z])-[ \t]/.test(trimmed) || // OL: 1- a- A- i- I- text
684
+ /^[-*+]\s*$/.test(trimmed) || // empty marker: - alone
685
+ (listBuffer.length > 0 && line.length > 0 &&
686
+ (line[0] === ' ' || line[0] === '\t') &&
687
+ /^(?:[-*+]|[A-Za-z0-9])/.test(trimmed)); // indented continuation
688
+ };
689
+
690
+ for (const line of lines) {
691
+ if (!line.trim()) {
692
+ if (listBuffer.length > 0) {
693
+ groups.push(listBuffer.join('\n'));
694
+ listBuffer = [];
695
+ }
696
+ continue;
697
+ }
698
+ if (isListLine(line)) {
699
+ listBuffer.push(line);
700
+ } else {
701
+ if (listBuffer.length > 0) {
702
+ groups.push(listBuffer.join('\n'));
703
+ listBuffer = [];
704
+ }
705
+ groups.push(line);
706
+ }
707
+ }
708
+
709
+ if (listBuffer.length > 0) {
710
+ groups.push(listBuffer.join('\n'));
711
+ }
712
+
713
+ return groups.filter(g => g.trim());
714
+ }
715
+
716
+ /**
717
+ * Parse a multi-line list block into nested <ul/ol class="zList"> HTML.
718
+ *
719
+ * The MARKER is the type (authored explicitly) — SSOT with zLSP str_hint and
720
+ * the zCLI block_extractor:
721
+ * Unordered: - * + → disc / circle / square
722
+ * Ordered: 1- a- A- i- I- → decimal / lower-alpha / upper-alpha /
723
+ * lower-roman / upper-roman
724
+ * A list LEVEL's style is set by the first item at that indent. Nesting is
725
+ * indentation-driven (a deeper indent opens a child list, shallower pops out).
726
+ * Ordered token = digits | single letter | roman string (space-guarded).
727
+ *
728
+ * @param {string} block - Multi-line list content
729
+ * @returns {string} HTML string
730
+ */
731
+ _parseListBlock(block) {
732
+ const rawLines = block.split('\n').filter(l => l !== '');
733
+
734
+ // Marker → { tag: 'ul'|'ol', style: <css list-style-type> }
735
+ const classify = (marker) => {
736
+ if (marker === '-') return { tag: 'ul', style: 'disc' };
737
+ if (marker === '*') return { tag: 'ul', style: 'circle' };
738
+ if (marker === '+') return { tag: 'ul', style: 'square' };
739
+ if (/^\d+$/.test(marker)) return { tag: 'ol', style: 'decimal' };
740
+ const upper = marker === marker.toUpperCase();
741
+ const isRoman = /^[ivxlcdm]+$/.test(marker.toLowerCase());
742
+ if (marker === 'i' || marker === 'I' || (marker.length > 1 && isRoman)) {
743
+ return { tag: 'ol', style: upper ? 'upper-roman' : 'lower-roman' };
744
+ }
745
+ return { tag: 'ol', style: upper ? 'upper-alpha' : 'lower-alpha' };
746
+ };
747
+
748
+ const RE_UL = /^([-*+])[ \t]+(.*)$/;
749
+ const RE_OL = /^(\d+|[ivxlcdmIVXLCDM]+|[A-Za-z])-[ \t]+(.*)$/;
750
+ const RE_EMPTY = /^[-*+]\s*$/;
751
+
752
+ const tokens = rawLines.map(line => {
753
+ const indent = line.length - line.trimStart().length;
754
+ const stripped = line.trimStart();
755
+ if (RE_EMPTY.test(stripped)) return { indent, kind: 'empty' };
756
+ let m = stripped.match(RE_UL);
757
+ if (!m) m = stripped.match(RE_OL);
758
+ if (!m) return null;
759
+ const c = classify(m[1]);
760
+ return { indent, kind: 'item', tag: c.tag, style: c.style, text: m[2].replace(/\s+$/, '') };
761
+ }).filter(Boolean);
762
+
763
+ const items = tokens.filter(t => t.kind === 'item');
764
+ if (!items.length) return '';
765
+
766
+ const root = { tag: items[0].tag, style: items[0].style, items: [] };
767
+ const stack = [{ node: root, indent: -1 }];
768
+
769
+ for (const token of tokens) {
770
+ // Empty markers are nesting no-ops (parity with zCLI which skips them);
771
+ // structure is driven purely by indentation.
772
+ if (token.kind === 'empty') continue;
773
+
774
+ let top = stack[stack.length - 1];
775
+
776
+ if (top.indent < 0) {
777
+ top.indent = token.indent;
778
+ } else if (token.indent > top.indent) {
779
+ const parentItems = top.node.items;
780
+ if (parentItems.length > 0) {
781
+ const lastItem = parentItems[parentItems.length - 1];
782
+ if (!lastItem.children) {
783
+ lastItem.children = { tag: token.tag, style: token.style, items: [] };
784
+ }
785
+ stack.push({ node: lastItem.children, indent: token.indent });
786
+ top = stack[stack.length - 1];
787
+ }
788
+ } else if (token.indent < top.indent) {
789
+ while (stack.length > 1 && token.indent < stack[stack.length - 1].indent) {
790
+ stack.pop();
791
+ }
792
+ top = stack[stack.length - 1];
793
+ }
794
+ top.node.items.push({ text: token.text, children: null });
795
+ }
796
+
797
+ const renderNode = (node) => {
798
+ const styleAttr = node.style ? ` style="list-style-type: ${node.style};"` : '';
799
+ let html = `<${node.tag} class="zList"${styleAttr}>`;
800
+ for (const item of node.items) {
801
+ html += `<li>${item.text || ''}`;
802
+ if (item.children) html += renderNode(item.children);
803
+ html += '</li>';
804
+ }
805
+ html += `</${node.tag}>`;
806
+ return html;
807
+ };
808
+
809
+ return renderNode(root);
810
+ }
811
+
812
+ }
813
+
814
+ //
815
+ // Default Export
816
+ //
817
+ export default TextRenderer;
818
+