@reldens/cms 0.19.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.
@@ -0,0 +1,105 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - SystemVariablesProvider
4
+ *
5
+ */
6
+
7
+ const { sc } = require('@reldens/utils');
8
+
9
+ class SystemVariablesProvider
10
+ {
11
+
12
+ constructor(props)
13
+ {
14
+ this.defaultDomain = sc.get(props, 'defaultDomain', 'default');
15
+ this.projectRoot = sc.get(props, 'projectRoot', './');
16
+ this.publicPath = sc.get(props, 'publicPath', './public');
17
+ }
18
+
19
+ buildSystemVariables(req, route, domain)
20
+ {
21
+ let currentRequest = this.buildCurrentRequestData(req, domain);
22
+ let currentRoute = this.buildCurrentRouteData(route);
23
+ let currentDomain = this.buildCurrentDomainData(domain);
24
+ let systemInfo = this.buildSystemInfo();
25
+ return {
26
+ currentRequest,
27
+ currentRoute,
28
+ currentDomain,
29
+ systemInfo
30
+ };
31
+ }
32
+
33
+ buildCurrentRequestData(req, domain)
34
+ {
35
+ if(!req){
36
+ return {};
37
+ }
38
+ let protocol = sc.get(req, 'protocol', 'http');
39
+ let host = req.get('host') || 'localhost';
40
+ let originalUrl = sc.get(req, 'originalUrl', sc.get(req, 'path', '/'));
41
+ let fullUrl = protocol+'://'+host+originalUrl;
42
+ return {
43
+ method: sc.get(req, 'method', 'GET'),
44
+ path: sc.get(req, 'path', '/'),
45
+ originalUrl,
46
+ fullUrl,
47
+ protocol,
48
+ host,
49
+ domain: domain || host.split(':')[0],
50
+ query: sc.get(req, 'query', {}),
51
+ params: sc.get(req, 'params', {}),
52
+ headers: sc.get(req, 'headers', {}),
53
+ userAgent: req.get('user-agent') || '',
54
+ ip: sc.get(req, 'ip', ''),
55
+ baseUrl: protocol+'://'+host,
56
+ timestamp: sc.getCurrentDate(),
57
+ isSecure: 'https' === protocol
58
+ };
59
+ }
60
+
61
+ buildCurrentRouteData(route)
62
+ {
63
+ if(!route){
64
+ return null;
65
+ }
66
+ return {
67
+ id: sc.get(route, 'id', null),
68
+ path: sc.get(route, 'path', ''),
69
+ router: sc.get(route, 'router', ''),
70
+ domain: sc.get(route, 'domain', null),
71
+ enabled: sc.get(route, 'enabled', false),
72
+ title: sc.get(route, 'title', ''),
73
+ template: sc.get(route, 'template', ''),
74
+ layout: sc.get(route, 'layout', ''),
75
+ meta_title: sc.get(route, 'meta_title', ''),
76
+ meta_description: sc.get(route, 'meta_description', ''),
77
+ sort_order: sc.get(route, 'sort_order', 0)
78
+ };
79
+ }
80
+
81
+ buildCurrentDomainData(domain)
82
+ {
83
+ return {
84
+ current: domain || this.defaultDomain,
85
+ default: this.defaultDomain,
86
+ resolved: domain || this.defaultDomain
87
+ };
88
+ }
89
+
90
+ buildSystemInfo()
91
+ {
92
+ return {
93
+ projectRoot: this.projectRoot,
94
+ publicPath: this.publicPath,
95
+ nodeVersion: process.version,
96
+ platform: process.platform,
97
+ environment: process.env.NODE_ENV || 'development',
98
+ timestamp: sc.getCurrentDate(),
99
+ uptime: process.uptime()
100
+ };
101
+ }
102
+
103
+ }
104
+
105
+ module.exports.SystemVariablesProvider = SystemVariablesProvider;
@@ -0,0 +1,98 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - TranslateTransformer
4
+ *
5
+ */
6
+
7
+ const { TranslationService } = require('./translation-service');
8
+ const { FileHandler } = require('@reldens/server-utils');
9
+ const { sc } = require('@reldens/utils');
10
+
11
+ class TranslateTransformer
12
+ {
13
+
14
+ constructor(props)
15
+ {
16
+ let projectRoot = sc.get(props, 'projectRoot', './');
17
+ let translationsPath = FileHandler.joinPaths(projectRoot, 'translations');
18
+ this.translationService = new TranslationService({
19
+ translationsPath,
20
+ defaultLocale: sc.get(props, 'defaultLocale', 'en'),
21
+ fallbackLocale: sc.get(props, 'fallbackLocale', 'en')
22
+ });
23
+ }
24
+
25
+ async transform(template, domain, req, systemVariables)
26
+ {
27
+ if(!template){
28
+ return template;
29
+ }
30
+ let currentLocale = this.extractLocaleFromSystemVariables(systemVariables);
31
+ let translatePattern = /\[(?:translate|t)\(([^)]+)\)\]/g;
32
+ let matches = [...template.matchAll(translatePattern)];
33
+ for(let i = matches.length - 1; i >= 0; i--){
34
+ let match = matches[i];
35
+ let args = this.parseTranslateArgs(match[1]);
36
+ let translatedText = await this.translationService.translate(
37
+ args.key,
38
+ currentLocale,
39
+ args.defaultValue,
40
+ args.interpolations
41
+ );
42
+ template = template.substring(0, match.index) +
43
+ translatedText +
44
+ template.substring(match.index + match[0].length);
45
+ }
46
+ return template;
47
+ }
48
+
49
+ parseTranslateArgs(argsString)
50
+ {
51
+ let args = argsString.split(',').map(arg => arg.trim().replace(/['"]/g, ''));
52
+ return {
53
+ key: args[0] || '',
54
+ defaultValue: args[1] || '',
55
+ interpolations: this.parseInterpolations(args[2])
56
+ };
57
+ }
58
+
59
+ parseInterpolations(interpolationsStr)
60
+ {
61
+ if(!interpolationsStr){
62
+ return {};
63
+ }
64
+ let interpolations = {};
65
+ let pairs = interpolationsStr.replace(/[{}]/g, '').split(',');
66
+ for(let pair of pairs){
67
+ let keyValue = pair.split(':');
68
+ if(2 === keyValue.length){
69
+ interpolations[keyValue[0].trim()] = keyValue[1].trim();
70
+ }
71
+ }
72
+ return interpolations;
73
+ }
74
+
75
+ extractLocaleFromSystemVariables(systemVariables)
76
+ {
77
+ let currentRequest = sc.get(systemVariables, 'currentRequest', {});
78
+ let headers = sc.get(currentRequest, 'headers', {});
79
+ let acceptLanguage = sc.get(headers, 'accept-language', '');
80
+ if(acceptLanguage){
81
+ let primaryLanguage = acceptLanguage.split(',')[0];
82
+ if(primaryLanguage && primaryLanguage.includes('-')){
83
+ return primaryLanguage.split('-')[0];
84
+ }
85
+ if(primaryLanguage){
86
+ return primaryLanguage;
87
+ }
88
+ }
89
+ let locale = sc.get(currentRequest, 'query', {}).locale;
90
+ if(locale){
91
+ return locale;
92
+ }
93
+ return this.translationService.defaultLocale;
94
+ }
95
+
96
+ }
97
+
98
+ module.exports.TranslateTransformer = TranslateTransformer;
@@ -0,0 +1,104 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - TranslationService
4
+ *
5
+ */
6
+
7
+ const { FileHandler } = require('@reldens/server-utils');
8
+ const { Logger, sc } = require('@reldens/utils');
9
+
10
+ class TranslationService
11
+ {
12
+
13
+ constructor(props)
14
+ {
15
+ this.translationsPath = sc.get(props, 'translationsPath', './translations');
16
+ this.defaultLocale = sc.get(props, 'defaultLocale', 'en');
17
+ this.fallbackLocale = sc.get(props, 'fallbackLocale', 'en');
18
+ this.translations = new Map();
19
+ this.loadedLocales = new Set();
20
+ }
21
+
22
+ async loadTranslations(locale)
23
+ {
24
+ if(this.loadedLocales.has(locale)){
25
+ return true;
26
+ }
27
+ let translationFile = FileHandler.joinPaths(this.translationsPath, locale + '.json');
28
+ if(!FileHandler.exists(translationFile)){
29
+ if(locale !== this.fallbackLocale){
30
+ Logger.warning('Translation file not found for locale: '+locale+', loading fallback');
31
+ return await this.loadTranslations(this.fallbackLocale);
32
+ }
33
+ Logger.warning('Translation file not found: '+translationFile);
34
+ return false;
35
+ }
36
+ let translationContent = FileHandler.readFile(translationFile);
37
+ if(!translationContent){
38
+ Logger.error('Failed to read translation file: '+translationFile);
39
+ return false;
40
+ }
41
+ let translationData = sc.toJson(translationContent, {});
42
+ if(!translationData){
43
+ Logger.error('Invalid JSON in translation file: '+translationFile);
44
+ return false;
45
+ }
46
+ this.translations.set(locale, translationData);
47
+ this.loadedLocales.add(locale);
48
+ return true;
49
+ }
50
+
51
+ async translate(key, locale, defaultValue, interpolations)
52
+ {
53
+ let targetLocale = locale || this.defaultLocale;
54
+ if(!this.loadedLocales.has(targetLocale)){
55
+ await this.loadTranslations(targetLocale);
56
+ }
57
+ let translations = this.translations.get(targetLocale);
58
+ let translatedText = this.getNestedTranslation(translations, key);
59
+ if(!translatedText && targetLocale !== this.fallbackLocale){
60
+ if(!this.loadedLocales.has(this.fallbackLocale)){
61
+ await this.loadTranslations(this.fallbackLocale);
62
+ }
63
+ let fallbackTranslations = this.translations.get(this.fallbackLocale);
64
+ translatedText = this.getNestedTranslation(fallbackTranslations, key);
65
+ }
66
+ if(!translatedText){
67
+ translatedText = defaultValue || key;
68
+ }
69
+ if(interpolations && sc.isObject(interpolations)){
70
+ translatedText = this.interpolateString(translatedText, interpolations);
71
+ }
72
+ return translatedText;
73
+ }
74
+
75
+ getNestedTranslation(translations, key)
76
+ {
77
+ if(!translations || !key){
78
+ return null;
79
+ }
80
+ let keys = key.split('.');
81
+ let current = translations;
82
+ for(let keyPart of keys){
83
+ if(!sc.hasOwn(current, keyPart)){
84
+ return null;
85
+ }
86
+ current = current[keyPart];
87
+ }
88
+ return sc.isString(current) ? current : null;
89
+ }
90
+
91
+ interpolateString(text, interpolations)
92
+ {
93
+ let result = text;
94
+ for(let key of Object.keys(interpolations)){
95
+ let placeholder = '{' + key + '}';
96
+ let value = interpolations[key];
97
+ result = result.replace(new RegExp(sc.sanitize(placeholder), 'g'), value);
98
+ }
99
+ return result;
100
+ }
101
+
102
+ }
103
+
104
+ module.exports.TranslationService = TranslationService;
@@ -0,0 +1,41 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - UrlTransformer
4
+ *
5
+ */
6
+
7
+ const { sc } = require('@reldens/utils');
8
+
9
+ class UrlTransformer
10
+ {
11
+
12
+ async transform(template, domain, req, systemVariables)
13
+ {
14
+ if(!template){
15
+ return template;
16
+ }
17
+ let currentRequest = sc.get(systemVariables, 'currentRequest', {});
18
+ let urlPattern = /\[url\(([^)]+)\)\]/g;
19
+ let matches = [...template.matchAll(urlPattern)];
20
+ for(let i = matches.length - 1; i >= 0; i--){
21
+ let match = matches[i];
22
+ let urlPath = match[1].replace(/['"]/g, '');
23
+ let absoluteUrl = this.buildAbsoluteUrl(urlPath, currentRequest);
24
+ template = template.substring(0, match.index) +
25
+ absoluteUrl +
26
+ template.substring(match.index + match[0].length);
27
+ }
28
+ return template;
29
+ }
30
+
31
+ buildAbsoluteUrl(relativePath, currentRequest)
32
+ {
33
+ if(!relativePath || relativePath.startsWith('http')){
34
+ return relativePath;
35
+ }
36
+ return sc.get(currentRequest, 'baseUrl', '') + relativePath;
37
+ }
38
+
39
+ }
40
+
41
+ module.exports.UrlTransformer = UrlTransformer;
@@ -9,6 +9,11 @@ const { EntitiesTransformer } = require('./template-engine/entities-transformer'
9
9
  const { CollectionsTransformer } = require('./template-engine/collections-transformer');
10
10
  const { CollectionsSingleTransformer } = require('./template-engine/collections-single-transformer');
11
11
  const { PartialsTransformer } = require('./template-engine/partials-transformer');
12
+ const { UrlTransformer } = require('./template-engine/url-transformer');
13
+ const { AssetTransformer } = require('./template-engine/asset-transformer');
14
+ const { DateTransformer } = require('./template-engine/date-transformer');
15
+ const { TranslateTransformer } = require('./template-engine/translate-transformer');
16
+ const { SystemVariablesProvider } = require('./template-engine/system-variables-provider');
12
17
  const { Logger, sc } = require('@reldens/utils');
13
18
 
14
19
  class TemplateEngine
@@ -19,7 +24,16 @@ class TemplateEngine
19
24
  this.renderEngine = sc.get(props, 'renderEngine', false);
20
25
  this.dataServer = sc.get(props, 'dataServer', false);
21
26
  this.getPartials = sc.get(props, 'getPartials', false);
27
+ this.events = sc.get(props, 'events', false);
28
+ this.defaultDomain = sc.get(props, 'defaultDomain', 'default');
29
+ this.projectRoot = sc.get(props, 'projectRoot', './');
30
+ this.publicPath = sc.get(props, 'publicPath', './public');
22
31
  this.jsonFieldsParser = new JsonFieldsParser({entitiesConfig: sc.get(props, 'entitiesConfig', {})});
32
+ this.systemVariablesProvider = new SystemVariablesProvider({
33
+ defaultDomain: this.defaultDomain,
34
+ projectRoot: this.projectRoot,
35
+ publicPath: this.publicPath
36
+ });
23
37
  this.entitiesTransformer = new EntitiesTransformer({
24
38
  dataServer: this.dataServer,
25
39
  jsonFieldsParser: this.jsonFieldsParser,
@@ -31,7 +45,8 @@ class TemplateEngine
31
45
  renderEngine: this.renderEngine,
32
46
  getPartials: this.getPartials,
33
47
  findAllPartialTags: this.findAllPartialTags.bind(this),
34
- loadPartialTemplate: this.loadPartialTemplate.bind(this)
48
+ loadPartialTemplate: this.loadPartialTemplate.bind(this),
49
+ processAllTemplateFunctions: this.processAllTemplateFunctions.bind(this)
35
50
  });
36
51
  this.collectionsTransformer = new CollectionsTransformer({
37
52
  dataServer: this.dataServer,
@@ -39,48 +54,114 @@ class TemplateEngine
39
54
  renderEngine: this.renderEngine,
40
55
  getPartials: this.getPartials,
41
56
  findAllPartialTags: this.findAllPartialTags.bind(this),
42
- loadPartialTemplate: this.loadPartialTemplate.bind(this)
57
+ loadPartialTemplate: this.loadPartialTemplate.bind(this),
58
+ processAllTemplateFunctions: this.processAllTemplateFunctions.bind(this)
43
59
  });
44
60
  this.partialsTransformer = new PartialsTransformer({
45
61
  renderEngine: this.renderEngine,
46
62
  getPartials: this.getPartials
47
63
  });
64
+ this.urlTransformer = new UrlTransformer();
65
+ this.assetTransformer = new AssetTransformer();
66
+ this.dateTransformer = new DateTransformer({
67
+ defaultFormat: sc.get(props, 'defaultDateFormat', 'Y-m-d H:i:s')
68
+ });
69
+ this.translateTransformer = new TranslateTransformer({
70
+ projectRoot: this.projectRoot,
71
+ defaultLocale: sc.get(props, 'defaultLocale', 'en'),
72
+ fallbackLocale: sc.get(props, 'fallbackLocale', 'en')
73
+ });
48
74
  this.transformers = [
49
75
  this.entitiesTransformer,
50
76
  this.collectionsSingleTransformer,
51
77
  this.collectionsTransformer,
52
- this.partialsTransformer
78
+ this.partialsTransformer,
79
+ this.urlTransformer,
80
+ this.assetTransformer,
81
+ this.dateTransformer,
82
+ this.translateTransformer
53
83
  ];
54
84
  }
55
85
 
56
- async processAllTemplateFunctions(template, domain, req)
86
+ async processAllTemplateFunctions(template, domain, req, systemVariables)
57
87
  {
58
88
  let processedTemplate = template;
59
89
  for(let transformer of this.transformers){
60
90
  if(sc.isFunction(transformer.transform)){
61
- processedTemplate = await transformer.transform(processedTemplate, domain, req);
91
+ processedTemplate = await transformer.transform(processedTemplate, domain, req, systemVariables);
62
92
  }
63
93
  }
64
94
  return processedTemplate;
65
95
  }
66
96
 
67
- async render(template, data, partials, domain, req)
97
+ async render(template, data, partials, domain, req, route, currentEntityData)
68
98
  {
69
99
  if(!this.renderEngine){
70
- Logger.error('Render engine not provided');
100
+ Logger.critical('Render engine not provided');
71
101
  return '';
72
102
  }
73
103
  if(!sc.isFunction(this.renderEngine.render)){
74
- Logger.error('Render engine does not contain a render method');
104
+ Logger.critical('Render engine does not contain a render method');
75
105
  return '';
76
106
  }
77
- return this.renderEngine.render(
78
- this.unescapeHtml(await this.processAllTemplateFunctions(template, domain, req)),
107
+ if(!this.events){
108
+ Logger.critical('Events manager not provided');
109
+ return '';
110
+ }
111
+ let systemVariables = this.systemVariablesProvider.buildSystemVariables(req, route, domain);
112
+ let renderContext = {
113
+ template,
79
114
  data,
115
+ partials,
116
+ domain,
117
+ req,
118
+ route,
119
+ currentEntityData
120
+ };
121
+ let eventData = {
122
+ variables: systemVariables,
123
+ renderContext
124
+ };
125
+ await this.events.emit('reldens.afterVariablesCreated', eventData);
126
+ let enhancedData = this.buildEnhancedRenderData(data, eventData.variables, currentEntityData);
127
+ let beforeProcessData = {
128
+ content: template,
129
+ variables: enhancedData,
130
+ renderContext
131
+ };
132
+ await this.events.emit('reldens.beforeContentProcess', beforeProcessData);
133
+ let processedTemplate = await this.processAllTemplateFunctions(
134
+ beforeProcessData.content,
135
+ domain,
136
+ req,
137
+ eventData.variables
138
+ );
139
+ let afterProcessData = {
140
+ processedContent: processedTemplate,
141
+ variables: enhancedData,
142
+ renderContext
143
+ };
144
+ await this.events.emit('reldens.afterContentProcess', afterProcessData);
145
+ return this.renderEngine.render(
146
+ this.unescapeHtml(afterProcessData.processedContent),
147
+ enhancedData,
80
148
  partials
81
149
  );
82
150
  }
83
151
 
152
+ buildEnhancedRenderData(originalData, systemVariables, currentEntityData)
153
+ {
154
+ let enhancedData = Object.assign({}, originalData);
155
+ enhancedData.currentRequest = systemVariables.currentRequest;
156
+ enhancedData.currentRoute = systemVariables.currentRoute;
157
+ enhancedData.currentDomain = systemVariables.currentDomain;
158
+ enhancedData.systemInfo = systemVariables.systemInfo;
159
+ if(currentEntityData){
160
+ enhancedData.currentEntity = currentEntityData;
161
+ }
162
+ return enhancedData;
163
+ }
164
+
84
165
  unescapeHtml(text)
85
166
  {
86
167
  return text
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/cms",
3
3
  "scope": "@reldens",
4
- "version": "0.19.0",
4
+ "version": "0.20.0",
5
5
  "description": "Reldens - CMS",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -33,10 +33,10 @@
33
33
  "url": "https://github.com/damian-pastorini/reldens-cms/issues"
34
34
  },
35
35
  "dependencies": {
36
- "@reldens/server-utils": "^0.20.0",
37
- "@reldens/storage": "^0.60.0",
36
+ "@reldens/server-utils": "^0.21.0",
37
+ "@reldens/storage": "^0.61.0",
38
38
  "@reldens/utils": "^0.50.0",
39
- "dotenv": "^17.0.0",
39
+ "dotenv": "^17.0.1",
40
40
  "mustache": "^4.2.0"
41
41
  }
42
42
  }