pulse-js-framework 1.7.15 → 1.7.17

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,266 @@
1
+ /**
2
+ * Pulse SSR Serializer - HTML serialization for MockNode trees
3
+ *
4
+ * Converts MockDOMAdapter node trees into HTML strings for server-side rendering.
5
+ * Handles all node types: elements, text, comments, and fragments.
6
+ */
7
+
8
+ // ============================================================================
9
+ // Constants
10
+ // ============================================================================
11
+
12
+ /**
13
+ * Void elements that are self-closing in HTML (no closing tag).
14
+ */
15
+ const VOID_ELEMENTS = new Set([
16
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
17
+ 'link', 'meta', 'param', 'source', 'track', 'wbr'
18
+ ]);
19
+
20
+ /**
21
+ * Boolean attributes that are present or absent (no value needed).
22
+ */
23
+ const BOOLEAN_ATTRS = new Set([
24
+ 'disabled', 'checked', 'readonly', 'required', 'autofocus',
25
+ 'multiple', 'selected', 'hidden', 'open', 'inert', 'novalidate',
26
+ 'async', 'defer', 'formnovalidate', 'allowfullscreen', 'autoplay',
27
+ 'controls', 'loop', 'muted', 'playsinline', 'reversed', 'ismap'
28
+ ]);
29
+
30
+ /**
31
+ * Attributes that should be skipped during serialization.
32
+ */
33
+ const SKIP_ATTRS = new Set(['class', 'id', 'style']);
34
+
35
+ // ============================================================================
36
+ // HTML Escaping
37
+ // ============================================================================
38
+
39
+ /**
40
+ * Escape HTML special characters in text content.
41
+ * @param {string} str - String to escape
42
+ * @returns {string} Escaped string safe for HTML text content
43
+ */
44
+ export function escapeHTML(str) {
45
+ if (str == null) return '';
46
+ return String(str)
47
+ .replace(/&/g, '&')
48
+ .replace(/</g, '&lt;')
49
+ .replace(/>/g, '&gt;');
50
+ }
51
+
52
+ /**
53
+ * Escape special characters for use in HTML attributes.
54
+ * @param {string} str - String to escape
55
+ * @returns {string} Escaped string safe for attribute values
56
+ */
57
+ export function escapeAttr(str) {
58
+ if (str == null) return '';
59
+ return String(str)
60
+ .replace(/&/g, '&amp;')
61
+ .replace(/"/g, '&quot;')
62
+ .replace(/</g, '&lt;')
63
+ .replace(/>/g, '&gt;');
64
+ }
65
+
66
+ // ============================================================================
67
+ // Style Serialization
68
+ // ============================================================================
69
+
70
+ /**
71
+ * Convert camelCase to kebab-case for CSS properties.
72
+ * @param {string} str - camelCase string
73
+ * @returns {string} kebab-case string
74
+ */
75
+ function camelToKebab(str) {
76
+ return str.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);
77
+ }
78
+
79
+ /**
80
+ * Serialize a style object to a CSS string.
81
+ * @param {Object} styleObj - Style object with camelCase properties
82
+ * @returns {string} CSS string like "color: red; font-size: 16px"
83
+ */
84
+ function serializeStyle(styleObj) {
85
+ if (!styleObj || typeof styleObj !== 'object') return '';
86
+
87
+ const styles = [];
88
+ for (const [key, value] of Object.entries(styleObj)) {
89
+ if (value != null && value !== '') {
90
+ styles.push(`${camelToKebab(key)}: ${value}`);
91
+ }
92
+ }
93
+ return styles.join('; ');
94
+ }
95
+
96
+ // ============================================================================
97
+ // Attribute Serialization
98
+ // ============================================================================
99
+
100
+ /**
101
+ * Serialize element attributes to an HTML attribute string.
102
+ * @param {MockElement} element - Element to serialize attributes from
103
+ * @returns {string} Attribute string like ' id="foo" class="bar"'
104
+ */
105
+ function serializeAttributes(element) {
106
+ const parts = [];
107
+
108
+ // ID attribute
109
+ if (element.id) {
110
+ parts.push(`id="${escapeAttr(element.id)}"`);
111
+ }
112
+
113
+ // Class attribute
114
+ if (element.className) {
115
+ parts.push(`class="${escapeAttr(element.className)}"`);
116
+ }
117
+
118
+ // Other attributes from _attributes Map
119
+ if (element._attributes) {
120
+ for (const [name, value] of element._attributes) {
121
+ // Skip already handled attributes
122
+ if (SKIP_ATTRS.has(name)) continue;
123
+
124
+ if (BOOLEAN_ATTRS.has(name)) {
125
+ // Boolean attributes: present if truthy
126
+ if (value === 'true' || value === true || value === name || value === '') {
127
+ parts.push(name);
128
+ }
129
+ } else {
130
+ // Regular attributes
131
+ parts.push(`${name}="${escapeAttr(value)}"`);
132
+ }
133
+ }
134
+ }
135
+
136
+ // Style attribute
137
+ const styleStr = serializeStyle(element._style || element.style);
138
+ if (styleStr) {
139
+ parts.push(`style="${escapeAttr(styleStr)}"`);
140
+ }
141
+
142
+ return parts.length > 0 ? ' ' + parts.join(' ') : '';
143
+ }
144
+
145
+ // ============================================================================
146
+ // Node Serialization
147
+ // ============================================================================
148
+
149
+ /**
150
+ * Serialize a MockNode tree to an HTML string.
151
+ *
152
+ * Supports all node types:
153
+ * - Element nodes (nodeType 1)
154
+ * - Text nodes (nodeType 3)
155
+ * - Comment nodes (nodeType 8)
156
+ * - Document fragments (nodeType 11)
157
+ *
158
+ * @param {MockNode} node - Root node to serialize
159
+ * @param {Object} [options] - Serialization options
160
+ * @param {boolean} [options.pretty=false] - Pretty print with indentation
161
+ * @param {number} [options.indent=0] - Initial indentation level
162
+ * @param {string} [options.indentStr=' '] - Indentation string
163
+ * @returns {string} HTML string
164
+ *
165
+ * @example
166
+ * const adapter = new MockDOMAdapter();
167
+ * const div = adapter.createElement('div');
168
+ * div.id = 'app';
169
+ * div.className = 'container';
170
+ * adapter.appendChild(div, adapter.createTextNode('Hello'));
171
+ *
172
+ * serializeToHTML(div);
173
+ * // '<div id="app" class="container">Hello</div>'
174
+ */
175
+ export function serializeToHTML(node, options = {}) {
176
+ if (!node) return '';
177
+
178
+ const { pretty = false, indent = 0, indentStr = ' ' } = options;
179
+ const prefix = pretty ? indentStr.repeat(indent) : '';
180
+ const newline = pretty ? '\n' : '';
181
+
182
+ // Text node (nodeType 3)
183
+ if (node.nodeType === 3) {
184
+ const text = node.textContent ?? node.data ?? '';
185
+ return escapeHTML(text);
186
+ }
187
+
188
+ // Comment node (nodeType 8)
189
+ if (node.nodeType === 8) {
190
+ const data = node.data ?? node.textContent ?? '';
191
+ return `${prefix}<!--${data}-->`;
192
+ }
193
+
194
+ // Document fragment (nodeType 11)
195
+ if (node.nodeType === 11) {
196
+ return (node.childNodes || [])
197
+ .map(child => serializeToHTML(child, { ...options, indent }))
198
+ .join(newline);
199
+ }
200
+
201
+ // Element node (nodeType 1)
202
+ if (node.nodeType === 1) {
203
+ const tag = (node.tagName || 'div').toLowerCase();
204
+ const attrs = serializeAttributes(node);
205
+
206
+ // Void elements (self-closing)
207
+ if (VOID_ELEMENTS.has(tag)) {
208
+ return `${prefix}<${tag}${attrs}>`;
209
+ }
210
+
211
+ // Elements with children
212
+ const children = node.childNodes || [];
213
+ let childrenHTML = '';
214
+
215
+ if (children.length > 0) {
216
+ if (pretty) {
217
+ childrenHTML = newline +
218
+ children
219
+ .map(child => serializeToHTML(child, { ...options, indent: indent + 1 }))
220
+ .join(newline) +
221
+ newline + prefix;
222
+ } else {
223
+ childrenHTML = children
224
+ .map(child => serializeToHTML(child, options))
225
+ .join('');
226
+ }
227
+ }
228
+
229
+ return `${prefix}<${tag}${attrs}>${childrenHTML}</${tag}>`;
230
+ }
231
+
232
+ // Unknown node type
233
+ return '';
234
+ }
235
+
236
+ /**
237
+ * Serialize only the children of a node (excludes the node itself).
238
+ * Useful for serializing the body content without the body tag.
239
+ *
240
+ * @param {MockNode} node - Node whose children to serialize
241
+ * @param {Object} [options] - Serialization options
242
+ * @returns {string} HTML string of children
243
+ */
244
+ export function serializeChildren(node, options = {}) {
245
+ if (!node || !node.childNodes) return '';
246
+
247
+ const { pretty = false } = options;
248
+ const newline = pretty ? '\n' : '';
249
+
250
+ return node.childNodes
251
+ .map(child => serializeToHTML(child, options))
252
+ .join(newline);
253
+ }
254
+
255
+ // ============================================================================
256
+ // Exports
257
+ // ============================================================================
258
+
259
+ export default {
260
+ serializeToHTML,
261
+ serializeChildren,
262
+ escapeHTML,
263
+ escapeAttr,
264
+ VOID_ELEMENTS,
265
+ BOOLEAN_ATTRS
266
+ };