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.
- package/README.md +43 -0
- package/cli/help.js +617 -0
- package/cli/index.js +102 -106
- package/cli/utils/file-utils.js +26 -4
- package/package.json +3 -2
- package/runtime/async.js +39 -0
- package/runtime/dom-element.js +107 -0
- package/runtime/index.js +70 -6
- package/runtime/pulse.js +40 -0
- package/runtime/ssr-async.js +229 -0
- package/runtime/ssr-hydrator.js +310 -0
- package/runtime/ssr-serializer.js +266 -0
- package/runtime/ssr.js +463 -0
|
@@ -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, '<')
|
|
49
|
+
.replace(/>/g, '>');
|
|
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, '&')
|
|
61
|
+
.replace(/"/g, '"')
|
|
62
|
+
.replace(/</g, '<')
|
|
63
|
+
.replace(/>/g, '>');
|
|
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
|
+
};
|