@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.
- package/L1_Foundation/L1_Foundation.js +13 -0
- package/L1_Foundation/bootstrap/bootstrap.js +11 -0
- package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
- package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
- package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
- package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
- package/L1_Foundation/bootstrap/module_registry.js +102 -0
- package/L1_Foundation/bootstrap/prism_loader.js +164 -0
- package/L1_Foundation/config/client_config.js +110 -0
- package/L1_Foundation/config/config.js +7 -0
- package/L1_Foundation/connection/connection.js +8 -0
- package/L1_Foundation/connection/websocket_connection.js +122 -0
- package/L1_Foundation/constants/bifrost_constants.js +284 -0
- package/L1_Foundation/constants/constants.js +7 -0
- package/L1_Foundation/logger/logger.js +10 -0
- package/L2_Handling/L2_Handling.js +15 -0
- package/L2_Handling/cache/cache.js +22 -0
- package/L2_Handling/cache/cache_constants.js +69 -0
- package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
- package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
- package/L2_Handling/cache/orchestration/orchestration.js +12 -0
- package/L2_Handling/cache/storage/session_manager.js +289 -0
- package/L2_Handling/cache/storage/storage.js +10 -0
- package/L2_Handling/cache/storage/storage_manager.js +590 -0
- package/L2_Handling/display/composite/composite.js +13 -0
- package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
- package/L2_Handling/display/composite/swiper_renderer.js +564 -0
- package/L2_Handling/display/composite/terminal_renderer.js +922 -0
- package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
- package/L2_Handling/display/display.js +30 -0
- package/L2_Handling/display/feedback/feedback.js +11 -0
- package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
- package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
- package/L2_Handling/display/inputs/button_renderer.js +634 -0
- package/L2_Handling/display/inputs/form_renderer.js +583 -0
- package/L2_Handling/display/inputs/input_renderer.js +658 -0
- package/L2_Handling/display/inputs/inputs.js +12 -0
- package/L2_Handling/display/navigation/menu_renderer.js +206 -0
- package/L2_Handling/display/navigation/navigation.js +11 -0
- package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
- package/L2_Handling/display/orchestration/orchestration.js +11 -0
- package/L2_Handling/display/orchestration/renderer.js +430 -0
- package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
- package/L2_Handling/display/outputs/alert_renderer.js +161 -0
- package/L2_Handling/display/outputs/audio_renderer.js +94 -0
- package/L2_Handling/display/outputs/card_renderer.js +229 -0
- package/L2_Handling/display/outputs/code_renderer.js +66 -0
- package/L2_Handling/display/outputs/dl_renderer.js +131 -0
- package/L2_Handling/display/outputs/header_renderer.js +162 -0
- package/L2_Handling/display/outputs/icon_renderer.js +107 -0
- package/L2_Handling/display/outputs/image_renderer.js +145 -0
- package/L2_Handling/display/outputs/list_renderer.js +190 -0
- package/L2_Handling/display/outputs/outputs.js +19 -0
- package/L2_Handling/display/outputs/table_renderer.js +765 -0
- package/L2_Handling/display/outputs/text_renderer.js +818 -0
- package/L2_Handling/display/outputs/typography_renderer.js +293 -0
- package/L2_Handling/display/outputs/video_renderer.js +116 -0
- package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
- package/L2_Handling/display/primitives/form_primitives.js +526 -0
- package/L2_Handling/display/primitives/generic_containers.js +109 -0
- package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
- package/L2_Handling/display/primitives/link_primitives.js +552 -0
- package/L2_Handling/display/primitives/lists_primitives.js +262 -0
- package/L2_Handling/display/primitives/media_primitives.js +383 -0
- package/L2_Handling/display/primitives/primitives.js +19 -0
- package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
- package/L2_Handling/display/primitives/table_primitives.js +528 -0
- package/L2_Handling/display/primitives/typography_primitives.js +175 -0
- package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
- package/L2_Handling/display/specialized/specialized.js +10 -0
- package/L2_Handling/hooks/hooks.js +9 -0
- package/L2_Handling/hooks/menu_integration.js +57 -0
- package/L2_Handling/hooks/widget_hook_manager.js +292 -0
- package/L2_Handling/message/message.js +8 -0
- package/L2_Handling/message/message_handler.js +701 -0
- package/L2_Handling/navigation/navigation.js +8 -0
- package/L2_Handling/navigation/navigation_manager.js +403 -0
- package/L2_Handling/zhooks/features/cache_live.js +287 -0
- package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
- package/L2_Handling/zhooks/zhooks_manager.js +65 -0
- package/L2_Handling/zvaf/zvaf.js +8 -0
- package/L2_Handling/zvaf/zvaf_manager.js +334 -0
- package/L3_Abstraction/L3_Abstraction.js +12 -0
- package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
- package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
- package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
- package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
- package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
- package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
- package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
- package/L3_Abstraction/renderer/renderer.js +1 -0
- package/L3_Abstraction/session/session.js +1 -0
- package/L4_Orchestration/L4_Orchestration.js +11 -0
- package/L4_Orchestration/client/client.js +1 -0
- package/L4_Orchestration/facade/facade.js +9 -0
- package/L4_Orchestration/facade/manager_registry.js +118 -0
- package/L4_Orchestration/facade/renderer_registry.js +274 -0
- package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
- package/L4_Orchestration/lifecycle/initializer.js +135 -0
- package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
- package/L4_Orchestration/rendering/facade.js +94 -0
- package/L4_Orchestration/rendering/rendering.js +7 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/bifrost_client.js +204 -0
- package/bifrost_core.js +1686 -0
- package/docs/ARCHITECTURE.md +111 -0
- package/docs/PROTOCOL.md +106 -0
- package/docs/RENDERERS.md +101 -0
- package/docs/SECURITY.md +92 -0
- package/package.json +24 -0
- package/syntax/prism-zconfig.js +41 -0
- package/syntax/prism-zenv.js +69 -0
- package/syntax/prism-zolo-theme.css +288 -0
- package/syntax/prism-zolo.js +380 -0
- package/syntax/prism-zschema.js +38 -0
- package/syntax/prism-zspark.js +25 -0
- package/syntax/prism-zui.js +68 -0
- package/zSys/accessibility/accessibility.js +10 -0
- package/zSys/accessibility/emoji_accessibility.js +173 -0
- package/zSys/dom/block_utils.js +122 -0
- package/zSys/dom/container_utils.js +370 -0
- package/zSys/dom/dom.js +13 -0
- package/zSys/dom/dom_utils.js +328 -0
- package/zSys/dom/encoding_utils.js +117 -0
- package/zSys/dom/style_utils.js +71 -0
- package/zSys/errors/error_display.js +299 -0
- package/zSys/errors/errors.js +10 -0
- package/zSys/theme/color_utils.js +274 -0
- package/zSys/theme/dark_mode_utils.js +272 -0
- package/zSys/theme/size_utils.js +256 -0
- package/zSys/theme/spacing_utils.js +405 -0
- package/zSys/theme/theme.js +14 -0
- package/zSys/theme/zbase.css +1735 -0
- package/zSys/theme/zbase_inject.js +161 -0
- package/zSys/theme/ztheme_utils.js +305 -0
- package/zSys/validation/error_boundary.js +201 -0
- package/zSys/validation/validation.js +11 -0
- package/zSys/validation/validation_utils.js +238 -0
- 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, '&')
|
|
156
|
+
.replace(/</g, '<')
|
|
157
|
+
.replace(/>/g, '>')
|
|
158
|
+
.replace(/"/g, '"')
|
|
159
|
+
.replace(/'/g, ''');
|
|
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
|
+
|