@reldens/cms 0.18.0 → 0.20.0

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 (36) hide show
  1. package/README.md +260 -20
  2. package/admin/reldens-admin-client.css +127 -80
  3. package/admin/reldens-admin-client.js +24 -0
  4. package/admin/templates/clear-all-cache-button.html +18 -0
  5. package/lib/admin-manager/contents-builder.js +7 -6
  6. package/lib/admin-manager/router-contents.js +58 -16
  7. package/lib/admin-manager-validator.js +2 -1
  8. package/lib/admin-manager.js +4 -2
  9. package/lib/admin-translations.js +9 -1
  10. package/lib/cache/add-cache-button-subscriber.js +53 -5
  11. package/lib/cache/cache-manager.js +47 -8
  12. package/lib/cache/cache-routes-handler.js +23 -0
  13. package/lib/cms-pages-route-manager.js +16 -4
  14. package/lib/frontend.js +310 -119
  15. package/lib/manager.js +43 -3
  16. package/lib/pagination-handler.js +243 -0
  17. package/lib/search-renderer.js +116 -0
  18. package/lib/search.js +344 -0
  19. package/lib/template-engine/asset-transformer.js +41 -0
  20. package/lib/template-engine/collections-single-transformer.js +70 -0
  21. package/lib/template-engine/collections-transformer-base.js +84 -0
  22. package/lib/template-engine/collections-transformer.js +374 -0
  23. package/lib/template-engine/date-transformer.js +53 -0
  24. package/lib/template-engine/entities-transformer.js +67 -0
  25. package/lib/template-engine/partials-transformer.js +175 -0
  26. package/lib/template-engine/system-variables-provider.js +105 -0
  27. package/lib/template-engine/translate-transformer.js +98 -0
  28. package/lib/template-engine/translation-service.js +104 -0
  29. package/lib/template-engine/url-transformer.js +41 -0
  30. package/lib/template-engine.js +133 -438
  31. package/lib/templates-list.js +1 -0
  32. package/migrations/install.sql +18 -18
  33. package/package.json +4 -4
  34. package/templates/page.html +19 -2
  35. package/templates/partials/entriesListView.html +14 -0
  36. package/templates/partials/pagedCollection.html +33 -0
@@ -0,0 +1,374 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - CollectionsTransformer
4
+ *
5
+ */
6
+
7
+ const { CollectionsTransformerBase } = require('./collections-transformer-base');
8
+ const { PaginationHandler } = require('../pagination-handler');
9
+ const { Logger, sc } = require('@reldens/utils');
10
+
11
+ class CollectionsTransformer extends CollectionsTransformerBase
12
+ {
13
+
14
+ constructor(props)
15
+ {
16
+ super(props);
17
+ this.renderEngine = sc.get(props, 'renderEngine', false);
18
+ this.getPartials = sc.get(props, 'getPartials', false);
19
+ this.findAllPartialTags = sc.get(props, 'findAllPartialTags', false);
20
+ this.loadPartialTemplate = sc.get(props, 'loadPartialTemplate', false);
21
+ this.processAllTemplateFunctions = sc.get(props, 'processAllTemplateFunctions', false);
22
+ this.paginationHandler = new PaginationHandler();
23
+ }
24
+
25
+ getLoopCollectionStartRegex()
26
+ {
27
+ return /<collection\s+([^>]+)>/g;
28
+ }
29
+
30
+ getLoopCollectionEndRegex()
31
+ {
32
+ return new RegExp('<\\/collection>');
33
+ }
34
+
35
+ async transform(template, domain, req, systemVariables)
36
+ {
37
+ let processedTemplate = template;
38
+ let matches = [...template.matchAll(this.getLoopCollectionStartRegex())];
39
+ for(let i = matches.length - 1; i >= 0; i--){
40
+ let startMatch = matches[i];
41
+ let tagContent = startMatch[1];
42
+ if(tagContent.includes('field=')){
43
+ continue;
44
+ }
45
+ let tableName = this.extractAttributeValue(tagContent, 'name');
46
+ let filtersJson = this.extractAttributeValue(tagContent, 'filters');
47
+ let queryOptionsJson = this.extractAttributeValue(tagContent, 'data');
48
+ let relationsString = this.extractAttributeValue(tagContent, 'relations');
49
+ let paginationId = this.extractAttributeValue(tagContent, 'pagination');
50
+ if(paginationId && '' !== paginationId){
51
+ let containerName = this.extractAttributeValue(tagContent, 'container');
52
+ let prevPages = this.extractAttributeValue(tagContent, 'prevPages');
53
+ let nextPages = this.extractAttributeValue(tagContent, 'nextPages');
54
+ let loopResult = await this.processPaginatedCollection(
55
+ processedTemplate,
56
+ startMatch,
57
+ tableName,
58
+ filtersJson,
59
+ relationsString,
60
+ queryOptionsJson,
61
+ paginationId,
62
+ containerName,
63
+ prevPages,
64
+ nextPages,
65
+ domain,
66
+ req,
67
+ systemVariables
68
+ );
69
+ if(loopResult){
70
+ processedTemplate = loopResult;
71
+ }
72
+ continue;
73
+ }
74
+ let loopResult = await this.processLoopCollection(
75
+ processedTemplate,
76
+ startMatch,
77
+ tableName,
78
+ filtersJson,
79
+ relationsString,
80
+ queryOptionsJson,
81
+ domain,
82
+ req,
83
+ systemVariables
84
+ );
85
+ if(loopResult){
86
+ processedTemplate = loopResult;
87
+ }
88
+ }
89
+ return processedTemplate;
90
+ }
91
+
92
+ async processLoopCollection(
93
+ template,
94
+ startMatch,
95
+ tableName,
96
+ filtersJson,
97
+ relationsString,
98
+ queryOptionsJson,
99
+ domain,
100
+ req,
101
+ systemVariables
102
+ ){
103
+ return await this.processCollectionBase(
104
+ template,
105
+ startMatch,
106
+ tableName,
107
+ filtersJson,
108
+ relationsString,
109
+ queryOptionsJson,
110
+ domain,
111
+ false,
112
+ req,
113
+ systemVariables
114
+ );
115
+ }
116
+
117
+ async processPaginatedCollection(
118
+ template,
119
+ startMatch,
120
+ tableName,
121
+ filtersJson,
122
+ relationsString,
123
+ queryOptionsJson,
124
+ paginationId,
125
+ containerName,
126
+ prevPages,
127
+ nextPages,
128
+ domain,
129
+ req,
130
+ systemVariables
131
+ ){
132
+ if(!req){
133
+ Logger.warning('No request provided for pagination, falling back to regular collection');
134
+ return await this.processLoopCollection(
135
+ template,
136
+ startMatch,
137
+ tableName,
138
+ filtersJson,
139
+ relationsString,
140
+ queryOptionsJson,
141
+ domain,
142
+ req,
143
+ systemVariables
144
+ );
145
+ }
146
+ return await this.processCollectionBase(
147
+ template,
148
+ startMatch,
149
+ tableName,
150
+ filtersJson,
151
+ relationsString,
152
+ queryOptionsJson,
153
+ domain,
154
+ {
155
+ paginationId,
156
+ containerName,
157
+ prevPages,
158
+ nextPages,
159
+ req
160
+ },
161
+ req,
162
+ systemVariables
163
+ );
164
+ }
165
+
166
+ async processCollectionBase(
167
+ template,
168
+ startMatch,
169
+ tableName,
170
+ filtersJson,
171
+ relationsString,
172
+ queryOptionsJson,
173
+ domain,
174
+ paginationOptions,
175
+ req,
176
+ systemVariables
177
+ ){
178
+ let startPos = startMatch.index;
179
+ let startEnd = startPos + startMatch[0].length;
180
+ let endMatch = this.getLoopCollectionEndRegex().exec(template.substring(startEnd));
181
+ if(!endMatch){
182
+ Logger.warning('No matching end tag found for collection: '+tableName);
183
+ return false;
184
+ }
185
+ let endPos = startEnd + endMatch.index;
186
+ let loopContent = template.substring(startEnd, endPos);
187
+ let collectionData;
188
+ let finalContent;
189
+ if(paginationOptions){
190
+ let templateParams = this.parseCollectionTemplateParams(filtersJson, queryOptionsJson);
191
+ let requestParams = this.paginationHandler.extractCollectionKeyFromRequest(
192
+ paginationOptions.req,
193
+ paginationOptions.paginationId
194
+ );
195
+ let mergedParams = this.paginationHandler.mergeCollectionParameters(
196
+ templateParams,
197
+ requestParams
198
+ );
199
+ mergedParams.prevPages = paginationOptions.prevPages;
200
+ mergedParams.nextPages = paginationOptions.nextPages;
201
+ let entity = this.dataServer.getEntity(tableName);
202
+ if(!entity){
203
+ Logger.warning('Entity not found in dataServer: '+tableName);
204
+ return template.substring(0, startPos) + '' + template.substring(endPos + endMatch[0].length);
205
+ }
206
+ let paginationData = this.paginationHandler.calculatePaginationData(
207
+ await this.paginationHandler.getCollectionTotal(entity, mergedParams.filters),
208
+ mergedParams.page,
209
+ mergedParams.limit,
210
+ this.getCurrentUrl(paginationOptions.req),
211
+ paginationOptions.paginationId,
212
+ mergedParams,
213
+ templateParams
214
+ );
215
+ let originalState = entity.preserveEntityState();
216
+ collectionData = await entity.loadEntityData(mergedParams.filters, {
217
+ limit: mergedParams.limit,
218
+ offset: paginationData.offset,
219
+ sortBy: mergedParams.sortBy,
220
+ sortDirection: mergedParams.sortDirection
221
+ }, relationsString);
222
+ entity.restoreEntityState(originalState);
223
+ if(this.jsonFieldsParser){
224
+ collectionData = this.jsonFieldsParser.parseJsonFields(
225
+ collectionData,
226
+ this.jsonFieldsParser.getJsonFieldsForEntity(tableName)
227
+ );
228
+ }
229
+ let collectionContent = await this.renderCollectionLoop(loopContent, collectionData, domain, req, systemVariables);
230
+ finalContent = this.renderEngine.render(
231
+ 1 < paginationData.totalPages
232
+ ? this.loadPaginationTemplate(paginationOptions.containerName, domain)
233
+ : '{{&collectionContentForCurrentPage}}',
234
+ Object.assign(
235
+ {},
236
+ paginationData,
237
+ {
238
+ collectionContentForCurrentPage: collectionContent,
239
+ hasResults: collectionData && 0 < collectionData.length,
240
+ noResultsMessage: 'No items found.'
241
+ }
242
+ ),
243
+ this.getPartials(domain)
244
+ );
245
+ }
246
+ if(!paginationOptions){
247
+ collectionData = await this.fetchCollectionForTemplate(
248
+ tableName,
249
+ filtersJson,
250
+ queryOptionsJson,
251
+ relationsString
252
+ );
253
+ finalContent = await this.renderCollectionLoop(loopContent, collectionData, domain, req, systemVariables);
254
+ }
255
+ return template.substring(0, startPos) + finalContent + template.substring(endPos + endMatch[0].length);
256
+ }
257
+
258
+ parseCollectionTemplateParams(filtersJson, queryOptionsJson)
259
+ {
260
+ let filters = {};
261
+ let queryOptions = {};
262
+ if(filtersJson && '' !== filtersJson.trim()){
263
+ let convertedFiltersJson = this.convertJsObjectToJson(filtersJson);
264
+ filters = sc.parseJson(convertedFiltersJson, {});
265
+ if(!filters){
266
+ Logger.warning('Invalid filters JSON: '+filtersJson);
267
+ filters = {};
268
+ }
269
+ }
270
+ if(queryOptionsJson && '' !== queryOptionsJson.trim()){
271
+ let convertedOptionsJson = this.convertJsObjectToJson(queryOptionsJson);
272
+ queryOptions = sc.parseJson(convertedOptionsJson, {});
273
+ if(!queryOptions){
274
+ Logger.warning('Invalid query options JSON: '+queryOptionsJson);
275
+ queryOptions = {};
276
+ }
277
+ }
278
+ return {
279
+ filters,
280
+ limit: sc.get(queryOptions, 'limit', 10),
281
+ sortBy: sc.get(queryOptions, 'sortBy', 'id'),
282
+ sortDirection: sc.get(queryOptions, 'sortDirection', 'asc')
283
+ };
284
+ }
285
+
286
+ loadPaginationTemplate(containerName, domain)
287
+ {
288
+ if(!containerName || '' === containerName){
289
+ containerName = 'pagedCollection';
290
+ }
291
+ let partialContent = this.loadPartialTemplate(containerName, domain);
292
+ if(partialContent){
293
+ return partialContent;
294
+ }
295
+ Logger.critical('Pagination template not found: ' + containerName + '. Please create the template file.');
296
+ return '{{&collectionContentForCurrentPage}}';
297
+ }
298
+
299
+ async renderCollectionLoop(loopContent, collectionData, domain, req, systemVariables)
300
+ {
301
+ if(!collectionData || 0 === collectionData.length){
302
+ return this.renderNoResultsMessage(domain);
303
+ }
304
+ let renderedContent = '';
305
+ for(let row of collectionData){
306
+ renderedContent += await this.processPartialsInLoop(loopContent, row, domain, req, systemVariables);
307
+ }
308
+ return renderedContent;
309
+ }
310
+
311
+ renderNoResultsMessage(domain)
312
+ {
313
+ let noResultsTemplate = this.loadPartialTemplate('noResults', domain);
314
+ if(noResultsTemplate){
315
+ return this.renderEngine.render(
316
+ noResultsTemplate,
317
+ {
318
+ message: 'No results found.',
319
+ cssClass: 'no-results',
320
+ alertClass: 'alert-info'
321
+ },
322
+ this.getPartials(domain)
323
+ );
324
+ }
325
+ return '';
326
+ }
327
+
328
+ async processPartialsInLoop(content, rowData, domain, req, systemVariables)
329
+ {
330
+ let processedContent = content;
331
+ let partialTags = this.findAllPartialTags(content);
332
+ for(let i = partialTags.length - 1; i >= 0; i--){
333
+ let tag = partialTags[i];
334
+ let partialContent = this.loadPartialTemplate(tag.name, domain);
335
+ if(!partialContent){
336
+ Logger.warning('Partial template not found: ' + tag.name);
337
+ processedContent = processedContent.substring(0, tag.start)+''+processedContent.substring(tag.end);
338
+ continue;
339
+ }
340
+ if(this.processAllTemplateFunctions){
341
+ partialContent = await this.processAllTemplateFunctions(partialContent, domain, req, systemVariables);
342
+ }
343
+ if(sc.hasOwn(tag.attributes, 'row')){
344
+ processedContent = processedContent.substring(0, tag.start)
345
+ +this.renderEngine.render(partialContent, {row: rowData}, this.getPartials(domain))
346
+ +processedContent.substring(tag.end);
347
+ continue;
348
+ }
349
+ let wrapperTemplate = '{{#vars}}{{> ' + tag.name + '}}{{/vars}}';
350
+ let renderData = { vars: tag.attributes };
351
+ let partials = {[tag.name]: partialContent};
352
+ processedContent = processedContent.substring(0, tag.start) +
353
+ this.renderEngine.render(wrapperTemplate, renderData, partials) +
354
+ processedContent.substring(tag.end);
355
+ }
356
+ if(this.processAllTemplateFunctions){
357
+ processedContent = await this.processAllTemplateFunctions(processedContent, domain, req, systemVariables);
358
+ }
359
+ return this.renderEngine.render(processedContent, {row: rowData}, this.getPartials(domain));
360
+ }
361
+
362
+ getCurrentUrl(req)
363
+ {
364
+ if(!req){
365
+ return '';
366
+ }
367
+ return sc.get(req, 'protocol', 'http') + '://'
368
+ + (req.get('host') || 'localhost')
369
+ + sc.get(req, 'originalUrl', sc.get(req, 'path', '/')).split('?')[0];
370
+ }
371
+
372
+ }
373
+
374
+ module.exports.CollectionsTransformer = CollectionsTransformer;
@@ -0,0 +1,53 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - DateTransformer
4
+ *
5
+ */
6
+
7
+ const { sc } = require('@reldens/utils');
8
+
9
+ class DateTransformer
10
+ {
11
+
12
+ constructor(props)
13
+ {
14
+ this.defaultFormat = sc.get(props, 'defaultFormat', 'Y-m-d H:i:s');
15
+ }
16
+
17
+ async transform(template, domain, req, systemVariables)
18
+ {
19
+ if(!template){
20
+ return template;
21
+ }
22
+ let datePattern = /\[date\(([^)]*)\)\]/g;
23
+ let matches = [...template.matchAll(datePattern)];
24
+ for(let i = matches.length - 1; i >= 0; i--){
25
+ let match = matches[i];
26
+ let args = match[1] ? match[1].split(',').map(arg => arg.trim().replace(/['"]/g, '')) : [];
27
+ let dateValue = args[0] || '';
28
+ let formatValue = args[1] || this.defaultFormat;
29
+ let formattedDate = this.formatDate(dateValue, formatValue);
30
+ template = template.substring(0, match.index) +
31
+ formattedDate +
32
+ template.substring(match.index + match[0].length);
33
+ }
34
+ return template;
35
+ }
36
+
37
+ formatDate(dateValue, format)
38
+ {
39
+ let date;
40
+ if(!dateValue || '' === dateValue || 'now' === dateValue.toLowerCase()){
41
+ date = new Date();
42
+ } else {
43
+ date = new Date(dateValue);
44
+ if(isNaN(date.getTime())){
45
+ date = new Date();
46
+ }
47
+ }
48
+ return sc.formatDate(date, format);
49
+ }
50
+
51
+ }
52
+
53
+ module.exports.DateTransformer = DateTransformer;
@@ -0,0 +1,67 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - EntitiesTransformer
4
+ *
5
+ */
6
+
7
+ const { Logger, sc } = require('@reldens/utils');
8
+
9
+ class EntitiesTransformer
10
+ {
11
+
12
+ constructor(props)
13
+ {
14
+ this.dataServer = sc.get(props, 'dataServer', false);
15
+ this.jsonFieldsParser = sc.get(props, 'jsonFieldsParser', false);
16
+ this.processAllTemplateFunctions = sc.get(props, 'processAllTemplateFunctions', false);
17
+ }
18
+
19
+ getEntityRegex()
20
+ {
21
+ return /<entity\s+name="([^"]+)"(?:\s+field="([^"]+)"\s+value="([^"]+)"|\s+id="([^"]+)")?\s*\/?>/g;
22
+ }
23
+
24
+ async transform(template, domain, req, systemVariables)
25
+ {
26
+ let processedTemplate = template;
27
+ for(let match of template.matchAll(this.getEntityRegex())){
28
+ let tableName = match[1];
29
+ let field = sc.get(match, '2', 'id');
30
+ let value = sc.get(match, '3', sc.get(match, '4', ''));
31
+ if(!value){
32
+ Logger.warning('Entity tag missing value: '+match[0]);
33
+ continue;
34
+ }
35
+ processedTemplate = processedTemplate.replace(
36
+ match[0],
37
+ await this.processAllTemplateFunctions(
38
+ sc.get(await this.fetchEntityForTemplate(tableName, value, field), 'content', ''),
39
+ domain,
40
+ req,
41
+ systemVariables
42
+ )
43
+ );
44
+ }
45
+ return processedTemplate;
46
+ }
47
+
48
+ async fetchEntityForTemplate(tableName, identifier, identifierField)
49
+ {
50
+ let entity = this.dataServer.getEntity(tableName);
51
+ if(!entity){
52
+ Logger.warning('Entity not found in dataServer: '+tableName);
53
+ return false;
54
+ }
55
+ let result = await entity.loadOneBy(identifierField, identifier);
56
+ if(!result){
57
+ return false;
58
+ }
59
+ return this.jsonFieldsParser.parseJsonFields(
60
+ result,
61
+ this.jsonFieldsParser.getJsonFieldsForEntity(tableName)
62
+ );
63
+ }
64
+
65
+ }
66
+
67
+ module.exports.EntitiesTransformer = EntitiesTransformer;
@@ -0,0 +1,175 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - PartialsTransformer
4
+ *
5
+ */
6
+
7
+ const { Logger, sc } = require('@reldens/utils');
8
+
9
+ class PartialsTransformer
10
+ {
11
+
12
+ constructor(props)
13
+ {
14
+ this.renderEngine = sc.get(props, 'renderEngine', false);
15
+ this.getPartials = sc.get(props, 'getPartials', false);
16
+ this.processAllTemplateFunctions = sc.get(props, 'processAllTemplateFunctions', false);
17
+ }
18
+
19
+ async transform(template, domain, req, systemVariables)
20
+ {
21
+ let processedTemplate = template;
22
+ let partialTags = this.findAllPartialTags(template);
23
+ for(let i = partialTags.length - 1; i >= 0; i--){
24
+ let tag = partialTags[i];
25
+ let partialContent = this.loadPartialTemplate(tag.name, domain);
26
+ if(!partialContent){
27
+ Logger.warning('Partial template not found: ' + tag.name);
28
+ processedTemplate = processedTemplate.substring(0, tag.start)+''+processedTemplate.substring(tag.end);
29
+ continue;
30
+ }
31
+ if(this.processAllTemplateFunctions){
32
+ partialContent = await this.processAllTemplateFunctions(partialContent, domain, req, systemVariables);
33
+ }
34
+ let wrapperTemplate = '{{#vars}}{{> ' + tag.name + '}}{{/vars}}';
35
+ let renderData = { vars: tag.attributes };
36
+ let partials = {[tag.name]: partialContent};
37
+ processedTemplate = processedTemplate.substring(0, tag.start) +
38
+ this.renderEngine.render(wrapperTemplate, renderData, partials) +
39
+ processedTemplate.substring(tag.end);
40
+ }
41
+ return processedTemplate;
42
+ }
43
+
44
+ findAllPartialTags(template)
45
+ {
46
+ let partialTags = [];
47
+ let pos = 0;
48
+ let partial = '<partial';
49
+ for(let tagStart = template.indexOf(partial, pos); -1 !== tagStart; tagStart=template.indexOf(partial, pos)){
50
+ let tagEnd = this.findPartialTagEnd(template, tagStart);
51
+ if(-1 === tagEnd){
52
+ pos = tagStart + partial.length;
53
+ continue;
54
+ }
55
+ let fullTag = template.substring(tagStart, tagEnd);
56
+ let nameMatch = fullTag.match(/name=["']([^"']+)["']/);
57
+ if(!nameMatch){
58
+ pos = tagStart + partial.length;
59
+ continue;
60
+ }
61
+ let partialName = nameMatch[1];
62
+ let attributes = this.parsePartialAttributes(fullTag, partialName);
63
+ partialTags.push({
64
+ start: tagStart,
65
+ end: tagEnd,
66
+ name: partialName,
67
+ attributes: attributes,
68
+ fullTag: fullTag
69
+ });
70
+ pos = tagEnd;
71
+ }
72
+ return partialTags;
73
+ }
74
+
75
+ findPartialTagEnd(template, tagStart)
76
+ {
77
+ let inQuotes = false;
78
+ let quoteChar = '';
79
+ let selfCloseTag = '/>';
80
+ let openCloseTag = '</partial>';
81
+ for(let i = tagStart; i < template.length; i++){
82
+ let char = template[i];
83
+ if(!inQuotes && ('"' === char || "'" === char)){
84
+ inQuotes = true;
85
+ quoteChar = char;
86
+ continue;
87
+ }
88
+ if(inQuotes && char === quoteChar && '\\' !== template[i - 1]){
89
+ inQuotes = false;
90
+ quoteChar = '';
91
+ continue;
92
+ }
93
+ if(!inQuotes){
94
+ if(template.substring(i, i + selfCloseTag.length) === selfCloseTag){
95
+ return i + selfCloseTag.length;
96
+ }
97
+ if('>' === char){
98
+ let closeIndex = template.indexOf(openCloseTag, i);
99
+ if(-1 !== closeIndex){
100
+ return closeIndex + openCloseTag.length;
101
+ }
102
+ return i + 1;
103
+ }
104
+ }
105
+ }
106
+ return -1;
107
+ }
108
+
109
+ parsePartialAttributes(fullTag, partialName)
110
+ {
111
+ let namePattern = 'name=' + this.getQuotePattern(fullTag, partialName);
112
+ let nameIndex = fullTag.indexOf(namePattern);
113
+ if(-1 === nameIndex){
114
+ return {};
115
+ }
116
+ let attributesStart = nameIndex + namePattern.length;
117
+ let attributesEnd = fullTag.lastIndexOf('/>');
118
+ if(-1 === attributesEnd){
119
+ attributesEnd = fullTag.lastIndexOf('</partial>');
120
+ }
121
+ if(-1 === attributesEnd){
122
+ attributesEnd = fullTag.lastIndexOf('>');
123
+ }
124
+ if(-1 === attributesEnd || attributesEnd <= attributesStart){
125
+ return {};
126
+ }
127
+ let attributesString = fullTag.substring(attributesStart, attributesEnd).trim();
128
+ return this.extractAttributesObject(attributesString);
129
+ }
130
+
131
+ getQuotePattern(fullTag, partialName)
132
+ {
133
+ if(fullTag.includes('name="' + partialName + '"')){
134
+ return '"' + partialName + '"';
135
+ }
136
+ if(fullTag.includes("name='" + partialName + "'")){
137
+ return "'" + partialName + "'";
138
+ }
139
+ return '"' + partialName + '"';
140
+ }
141
+
142
+ extractAttributesObject(attributesString)
143
+ {
144
+ if(!attributesString){
145
+ return {};
146
+ }
147
+ let attributes = {};
148
+ let valueRegex = /(\w+)=(['"])((?:(?!\2)[^\\]|\\.)*)(\2)/g;
149
+ for(let match of attributesString.matchAll(valueRegex)){
150
+ attributes[match[1]] = match[3];
151
+ }
152
+ let booleanRegex = /\b(\w+)(?!\s*=)/g;
153
+ for(let match of attributesString.matchAll(booleanRegex)){
154
+ if(!sc.hasOwn(attributes, match[1])){
155
+ attributes[match[1]] = true;
156
+ }
157
+ }
158
+ return attributes;
159
+ }
160
+
161
+ loadPartialTemplate(partialName, domain)
162
+ {
163
+ if(!this.getPartials){
164
+ return false;
165
+ }
166
+ let partials = this.getPartials(domain);
167
+ if(sc.hasOwn(partials, partialName)){
168
+ return partials[partialName];
169
+ }
170
+ return false;
171
+ }
172
+
173
+ }
174
+
175
+ module.exports.PartialsTransformer = PartialsTransformer;