-
-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathRenderService.cfc
More file actions
360 lines (324 loc) · 15.5 KB
/
RenderService.cfc
File metadata and controls
360 lines (324 loc) · 15.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
component accessors="true" singleton {
property name="cbwireController" inject="CBWIREController@cbwire";
property name="checksumService" inject="ChecksumService@cbwire";
property name="utilityService" inject="UtilityService@cbwire";
property name="validationService" inject="ValidationService@cbwire";
property name="requestService" inject="coldbox:requestService";
/**
* Renders the HTML for a Livewire component, ensuring it has a single outer element.
* If this is the initial load, it encodes the snapshot and inserts Livewire attributes.
*
* @wire Wire | The wire instance for the component being rendered.
* @baseHtml string | The base HTML content to be processed.
*
* @return string The processed HTML with Livewire attributes.
*/
function render( required wire, required baseHtml ) {
local.trimmedHTML = trim( arguments.baseHtml );
// Validate the HTML content to ensure it has a single outer element
validateSingleOuterElement( local.trimmedHTML );
// If this is the initial load, encode the snapshot and insert Livewire attributes
if ( arguments.wire.get_initialLoad() ) {
// Encode the snapshot for HTML attribute inclusion and process the view content
local.snapshotEncoded = encodeAttribute( checksumService.calculateChecksum( arguments.wire._getSnapshot() ) );
return insertInitialLivewireAttributes( local.trimmedHTML, local.snapshotEncoded, arguments.wire.get_id(), arguments.wire.get_listeners(), arguments.wire.get_scripts() );
} else {
// Return the trimmed HTML content
return insertSubsequentLivewireAttributes( arguments.wire.get_id(), local.trimmedHTML );
}
}
/**
* Renders the content of a view template file.
* This method is used internally by the view method to render the content of a view template.
*
* @wire Wire | The wire instance for the component being rendered.
* @normalizedPath string | The normalized path to the view template file.
* @params struct | The parameters to pass to the view template.
*
* @return The rendered content of the view template.
*/
function renderViewContent(
wire,
normalizedPath,
params = {},
template = "/cbwire/views/RendererEncapsulator.cfm"
){
if ( !wire.get_renderedContent().len() ) {
local.templateReturnValues = {};
// Render our view using an renderer encapsulator
savecontent variable="local.viewContent" {
cfmodule(
template = arguments.template,
cbwireComponent = arguments.wire,
validationService = variables.validationService,
requestService = variables.requestService,
normalizedPath = arguments.normalizedPath,
params = arguments.params,
returnValues = local.templateReturnValues
);
}
captureTemplateReturnValues( arguments.wire, local.templateReturnValues );
wire.set_renderedContent( local.viewContent );
return local.viewContent;
}
return wire.get_renderedContent();
}
/**
* Returns the first outer element from the provided html.
* "<div x-data=""></div>" returns "div";
*
* @return string
*/
function getOuterElement( html ) {
local.outerElements = reMatchNoCase( "<[A-Za-z]+\s*", arguments.html );
if( local.outerElements.len() == 0 ) {
throw(
type = "CBWIREException",
message = "The HTML content of the wire component must contain at least one external element. Wire component contains no HTML elements. It is empty."
);
}
local.outerElement = local.outerElements.first();
local.outerElement = local.outerElement.replaceNoCase( "<", "", "one" );
return local.outerElement.trim();
}
/**
* Take an incoming rendering and determine the outer component tag.
* <div>...</div> would return 'div'
*
* @rendering string | The rendering to parse.
*
* @return string
*/
function getComponentTag( rendering ){
var tag = "";
var regexMatches = reFindNoCase( "^<([a-zA-Z0-9]+)", arguments.rendering.trim(), 1, true );
if ( regexMatches.match.len() == 2 ) {
return regexMatches.match[ 2 ];
}
throw( type="CBWIREException", message="Cannot determine component tag." );
}
/**
* Validates that the HTML content has a single outer element.
* Ensures the first and last tags match and that the total number of tags is even.
*
* @trimmedHtml string | The trimmed HTML content to validate.
* @throws ApplicationException | When the HTML does not meet the single outer element criteria.
*/
function validateSingleOuterElement( trimmedHtml ) {
return; // Skip until we can find a much faster way to validate a single outer element.
// Define void elements
local.voidTags = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"];
// Trim and remove any extra spaces between tags for accurate matching
local.cleanHtml = trim(arguments.trimmedHtml).replaceAll("\s+>", ">");
// Regex to find all tags
local.tags = reMatch("<\/?[a-z]+[^>]*>", local.cleanHtml);
// Ensure there is at least one tag
if (arrayLen(local.tags) == 0) {
throw("ApplicationException", "Template must contain at least one HTML tag.");
}
// Check for single outer element by comparing the first and last tag
local.firstTag = tags.first().replaceAll("<\/?([a-z]+)[^>]*>", "$1");
local.lastTag = tags.last().replaceAll("<\/?([a-z]+)[^>]*>", "$1");
// Check if the first and last tags match and are properly nested
if ( local.firstTag != local.lastTag ) {
throw("CBWIRETemplateException", "Template does not have matching outer tags.");
}
// Additional check to ensure no other top-level tags are present
local.depth = 0;
local.tags.each( function( tag, index ) {
local.tagName = tag.replaceAll("<\/?([a-z]+)[^>]*>", "$1");
// Skip depth modification for void elements
if (arrayFindNoCase(voidTags, local.tagName) && left( arguments.tag, 2) != "</") {
return;
}
if (left( arguments.tag, 2) == "</") {
depth--;
} else {
depth++;
}
// If depth returns to zero before last tag, or if depth is not zero after last tag, throw exception
if (depth == 0 && index != tags.len() || index == tags.len() && depth != 0 ) {
throw("CBWIRETemplateException", "Template has more than one outer element, or is missing an end tag </element>.");
}
});
}
/**
* Normalizes the view path for rendering. This means it will convert the dot notation path
* to a slash notation path, check for the existence of .bxm or .cfm files, and ensure the path is correctly formatted.
*
* @viewPath string | The dot notation path to the view template to be rendered, without the .cfm extension.
* @componentPath string | The component path, used to determine if the normalized path should be prefixed with "wires/".
*
* @return string
*/
function normalizeViewPath( required viewPath, required componentPath ) {
var paths = buildViewPaths( arguments.viewPath );
if ( paths.normalizedPath contains "cbwire/models/tmp/" ) {
if ( utilityService.fileExists( paths.fullBxmPath ) ) {
return "/" & paths.normalizedPath & ".bxm";
} else {
return "/" & paths.normalizedPath & ".cfm";
}
}
if ( utilityService.fileExists( paths.fullBxmPath ) ) {
paths.normalizedPath &= ".bxm";
} else if ( utilityService.fileExists( paths.fullCfmPath ) ) {
paths.normalizedPath &= ".cfm";
} else {
throw( type="CBWIREException", message="A .bxm or .cfm template could not be found for '#arguments.viewPath#'." );
}
if ( !isNull( arguments.componentPath ) && !find( "@", arguments.componentPath ) && left( paths.normalizedPath, 6 ) != "wires/" ) {
paths.normalizedPath = "wires/" & paths.normalizedPath;
}
if ( left( paths.normalizedPath, 1 ) != "/" ) {
paths.normalizedPath = "/" & paths.normalizedPath;
}
return paths.normalizedPath;
}
/**
* Returns the full path to the template file based on the wire and path provided.
* If the path is a module path (contains '@'), it will resolve to the module's root path.
* @wire cbwire.models.Component | The wire instance for the component being rendered.
* @path string | The dot-notation path to the view template.
* @return string | The full path to the template file.
*/
function getTemplatePath( required wire, required path ) {
if ( isModulePath( arguments.path ) ) {
var moduleRoot = cbwireController.getModuleRootPath( wire._getModuleName() );
return moduleRoot & ".wires." & wire._getComponentName().listFirst( "@" );
}
return "wires." & arguments.path;
}
/**
* Returns true if the path contains a module.
*
* @return boolean
*/
function isModulePath( required viewPath ) {
return arguments.viewPath contains "@";
}
/**
* Captures the return values from the RendererEncapsulator like cbwire:script and cbwire:assets tags.
*
* @return void
*/
function captureTemplateReturnValues( required wire, required returnValues ) {
// Parse and track cbwire:script tags
arguments.returnValues.filter( function( key, value ) {
return key.findNoCase( "script" );
} ).each( function( key, value, result ) {
// Extract the counter from the tag name
local.counter = key.replaceNoCase( "script", "" );
// Create script tag id based on compile time id and counter
local.scriptTagId = wire._getCompileTimeKey() & "-" & local.counter;
// Track the script tag
wire._trackScript( local.scriptTagId, value );
} );
// Parse and track cbwire:assets tags
arguments.returnValues.filter( function( key, value ) {
return key.findNoCase( "assets" );
} ).each( function( key, value, result ) {
// Extract the counter from the tag name
local.counter = key.replaceNoCase( "assets", "" );
// Create assets tag id based on hash of assets
local.assetsTagId = hash( value, "MD5" );
// Track the assets tag
wire._trackAsset( local.assetsTagId, value );
local.requestAssets = cbwireController.getRequestAssets();
local.requestAssets[ local.assetsTagId ] = value;
} );
}
/**
* Returns the wire:effects attribute contents.
*
* @return string
*/
function generateWireEffectsAttribute( required struct listeners, required struct scripts ) {
local.effects = {};
local.listenersAsArray = arguments.listeners.reduce( function( acc, key, value ) {
acc.append( key );
return acc;
}, [] );
if ( local.listenersAsArray.len() ) {
local.effects[ "listeners" ] = local.listenersAsArray;
}
if ( arguments.scripts.count() ) {
local.effects[ "scripts" ] = arguments.scripts;
}
if ( local.effects.count() ) {
return encodeAttribute( serializeJson( local.effects ) );
}
return "[]";
}
/**
* Encodes a given string for safe usage within an HTML attribute.
*
* @value string | The string to be encoded.
*
* @return String The encoded string suitable for HTML attribute inclusion.
*/
function encodeAttribute( required value ) {
// return arguments.value.replaceNoCase( '"', """, "all" );
return encodeForHTMLAttribute(arguments.value);
}
/**
* Inserts Livewire-specific attributes into the given HTML content, ensuring Livewire can manage the component.
*
* @html string | The original HTML content to be processed.
* @snapshotEncoded string | The encoded snapshot data for Livewire's consumption.
* @id string | The component's unique identifier.
*
* @return String The HTML content with Livewire attributes properly inserted.
*/
private function insertInitialLivewireAttributes( required html, required snapshotEncoded, required id, required listeners, required scripts ) {
// Trim our html
arguments.html = arguments.html.trim();
local.wireEffectsAttribute = generateWireEffectsAttribute( listeners=arguments.listeners, scripts=arguments.scripts );
// Define the wire attributes to append
local.wireAttributes = 'wire:snapshot="' & arguments.snapshotEncoded & '" wire:effects="#local.wireEffectsAttribute#" wire:id="#arguments.id#"';
// Determine our outer element
local.outerElement = getOuterElement( arguments.html );
// Find the position of the opening tag
local.openingTagStart = findNoCase("<" & local.outerElement, arguments.html);
local.openingTagEnd = find(">", arguments.html, local.openingTagStart);
// Insert attributes into the opening tag
if (local.openingTagStart > 0 && local.openingTagEnd > 0) {
local.openingTag = mid(arguments.html, local.openingTagStart, local.openingTagEnd - local.openingTagStart + 1);
local.newOpeningTag = replace(local.openingTag, "<" & local.outerElement, "<" & local.outerElement & " " & local.wireAttributes, "one");
arguments.html = replace(arguments.html, local.openingTag, local.newOpeningTag, "one");
}
return arguments.html;
}
/**
* Inserts subsequent Livewire-specific attributes into the given HTML content.
*
* @html string | The original HTML content to be processed.
*
* @return String The HTML content with Livewire attributes properly inserted.
*/
private function insertSubsequentLivewireAttributes( required id, required html ) {
// Trim our html
arguments.html = arguments.html.trim();
// Define the wire attributes to append
local.wireAttributes = "wire:id=""#arguments.id#""";
// Determine our outer element
local.outerElement = getOuterElement( arguments.html );
// Insert attributes into the opening tag
return arguments.html.reReplaceNoCase( "<" & local.outerElement & "\s*", "<" & local.outerElement & " " & local.wireAttributes & " ", "one" );
}
/**
* Converts a dot-path into .bxm/.cfm absolute paths.
*
* @viewPath string | A dot-notation view path like "my.view.component"
* @return struct { normalizedPath, fullBxmPath, fullCfmPath }
*/
private function buildViewPaths( required string viewPath ) {
var normalizedPath = replace( arguments.viewPath, ".", "/", "all" );
var base = expandPath( "/" & normalizedPath );
return {
normalizedPath: normalizedPath,
fullBxmPath: base & ".bxm",
fullCfmPath: base & ".cfm"
};
}
}