Skip to content

Commit 61e963d

Browse files
authored
PRO-8061: Inject nodes (#5010)
1 parent 00cda14 commit 61e963d

6 files changed

Lines changed: 383 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
### Adds
66

7-
- Adds any alt text found in an attribute to the media library attachment during import of rich text inline images by API
7+
* Adds any alt text found in an attribute to the media library attachment during import of rich text inline images by API
8+
* Adds `prependNodes` and `appendNodes` methods to every module. These methods allow you to inject HTML to every page using a `node` declaration.
89

910
### Changes
1011

modules/@apostrophecms/module/index.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,77 @@ module.exports = {
333333
});
334334
},
335335

336+
// Prepepnd/append nodes, rendered to HTML, to a given location. Supports
337+
// the same locations as `apos.template.prepend()` and `apos.template.append()`.
338+
// The rendered markup is automatically escaped and injected into the
339+
// appropriate location in the HTML document.
340+
// The `method` argument is the name of existing method in the current module.
341+
// It should be string, passing a function will throw an error.
342+
// Example:
343+
// ```
344+
// self.prependNodes('head', 'myMethod');
345+
// self.appendNodes('body', 'anotherMethod');
346+
// ```
347+
// In the example above, `myMethod` and `anotherMethod` should be defined
348+
// in the current module, and they should return an array of node objects.
349+
// ```
350+
// methods(self) {
351+
// return {
352+
// myMethod(req) {
353+
// return [
354+
// {
355+
// name: 'meta',
356+
// attributes: {
357+
// name: 'my-meta',
358+
// content: 'my content'
359+
// }
360+
// }
361+
// ];
362+
// },
363+
// anotherMethod(req) {
364+
// return [
365+
// {
366+
// tag: 'h4',
367+
// body: [
368+
// {
369+
// comment: 'Start Heading text'
370+
// },
371+
// {
372+
// text: 'Heading text'
373+
// }
374+
// {
375+
// comment: 'End Heading text'
376+
// }
377+
// {
378+
// name: 'script`,
379+
// body: [
380+
// {
381+
// raw: 'console.log("This is not escaped, be careful!");'
382+
// }
383+
// ]
384+
// }
385+
// ]
386+
// }
387+
// ];
388+
// }
389+
// };
390+
// }
391+
// ```
392+
// Node object SHOULD have either `name`, `text`, `raw` or `comment` property.
393+
// A node with `name` can have `attrs` (array of element attributes)
394+
// and `body` (array of child nodes, recursion).
395+
// `text` nodes are rendered as text (no HTML tags), the value is always a string.
396+
// `comment` nodes are rendered as HTML comments, the value is always a string.
397+
// `raw` nodes are rendered as is, no escaping, the value is always a string.
398+
prependNodes(location, method) {
399+
return self.apos.template
400+
.prependNodes(location, self.__meta.name, method);
401+
},
402+
appendNodes(location, method) {
403+
return self.apos.template
404+
.appendNodes(location, self.__meta.name, method);
405+
},
406+
336407
// Render a template. Template overrides are respected; the
337408
// project level modules/modulename/views folder wins if
338409
// it has such a template, followed by the npm module,

modules/@apostrophecms/template/index.js

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const Promise = require('bluebird');
3434
const path = require('path');
3535
const { stripIndent } = require('common-tags');
3636
const { SemanticAttributes } = require('@opentelemetry/semantic-conventions');
37+
const voidElements = require('void-elements');
3738

3839
module.exports = {
3940
options: { alias: 'template' },
@@ -50,9 +51,11 @@ module.exports = {
5051
async inject(req, data) {
5152
const key = `${data.end}-${data.where}`;
5253
const components = self.getInjectedComponents(key, data);
54+
const html = data.when ? '' : self.injectNodes(req, key);
5355

5456
return {
55-
components
57+
components,
58+
html
5659
};
5760
}
5861
};
@@ -67,12 +70,10 @@ module.exports = {
6770
};
6871

6972
self.envs = {};
70-
7173
self.filters = {};
72-
7374
self.nunjucks = self.options.language || require('nunjucks');
74-
7575
self.insertions = {};
76+
self.runtimeNodes = {};
7677

7778
},
7879
handlers(self) {
@@ -1073,6 +1074,141 @@ module.exports = {
10731074
};
10741075
},
10751076

1077+
prependNodes(location, moduleName, method) {
1078+
self.registerRuntimeNodes('prepend', location, moduleName, method);
1079+
},
1080+
1081+
appendNodes(location, moduleName, method) {
1082+
self.registerRuntimeNodes('append', location, moduleName, method);
1083+
},
1084+
1085+
registerRuntimeNodes(end, location, moduleName, method) {
1086+
if (typeof method !== 'string') {
1087+
throw new Error(
1088+
`Do not pass a function to "apos.template.${end}Nodes()". ` +
1089+
'Pass a string with the name of the method to call on the module, ' +
1090+
'e.g. "myMethod"'
1091+
);
1092+
}
1093+
if (typeof moduleName !== 'string') {
1094+
throw new Error(
1095+
`Invalid "moduleName" detected in "apos.template.${end}Nodes()". ` +
1096+
'Pass a string with the name of the module, e.g. "some-module"'
1097+
);
1098+
}
1099+
1100+
if (typeof self.apos.modules[moduleName] === 'undefined') {
1101+
throw new Error(
1102+
`Invalid module "${moduleName}"detected in "apos.template.${end}Nodes()". ` +
1103+
'Make sure the module is registered in your "app.js" file.'
1104+
);
1105+
}
1106+
1107+
if (!self.apos.modules[moduleName][method]) {
1108+
throw new Error(
1109+
`Invalid method "${method}" detected in "apos.template.${end}Nodes()". ` +
1110+
`Make sure the module "${moduleName}" has a method named "${method}".`
1111+
);
1112+
}
1113+
1114+
const key = end + '-' + location;
1115+
self.runtimeNodes[key] ||= [];
1116+
self.runtimeNodes[key].push({
1117+
moduleName,
1118+
method
1119+
});
1120+
},
1121+
1122+
// Accepts array of node objects and returns a string - HTML
1123+
// representation of the nodes. Example nodes:
1124+
// [
1125+
// {
1126+
// name: 'div',
1127+
// attrs: { class: 'my-class' },
1128+
// body: [
1129+
// {
1130+
// text: 'Hello world'
1131+
// }
1132+
// ]
1133+
// },
1134+
// {
1135+
// name: 'link',
1136+
// attrs: { href: '/some/path', rel: 'stylesheet' }
1137+
// }
1138+
// ]
1139+
// Node object SHOULD have either `name`, `text`, `raw` or `comment` property.
1140+
// A node with `name` can have `attrs` (array of element attributes)
1141+
// and `body` (array of child nodes, recursion).
1142+
// `text` nodes are rendered as text (no HTML tags), the value is always a string.
1143+
// `comment` nodes are rendered as HTML comments, the value is always a string.
1144+
// `raw` nodes are rendered as is, no escaping, the value is always a string.
1145+
renderNodes(nodes) {
1146+
if (!Array.isArray(nodes)) {
1147+
self.logError(
1148+
'render-nodes',
1149+
'Invalid nodes array passed to apos.template.renderNodes()'
1150+
);
1151+
return '';
1152+
}
1153+
return nodes.map(node => {
1154+
if (node.text) {
1155+
return self.apos.util.escapeHtml(node.text);
1156+
}
1157+
if (node.comment) {
1158+
return `\n<!-- ${self.apos.util.escapeHtml(node.comment)} -->\n`;
1159+
}
1160+
if (node.raw) {
1161+
return node.raw;
1162+
}
1163+
if (node.name) {
1164+
const name = self.apos.util.escapeHtml(node.name);
1165+
const attrs = Object.entries(node.attrs || {})
1166+
.map(([ key, value ]) => {
1167+
if (value === false || value === null || value === undefined) {
1168+
return '';
1169+
}
1170+
if (value === true) {
1171+
return ` ${self.apos.util.escapeHtml(key)}`;
1172+
}
1173+
return ` ${self.apos.util.escapeHtml(key)}="${self.apos.util.escapeHtml(value)}"`;
1174+
})
1175+
.join('')
1176+
.trimEnd();
1177+
1178+
if (!node.body && voidElements[name]) {
1179+
return `<${name}${attrs} />`;
1180+
}
1181+
1182+
const body = (
1183+
node.body ? self.renderNodes(node.body) : ''
1184+
).trim();
1185+
1186+
return `<${name}${attrs}>${body}</${name}>`;
1187+
}
1188+
self.logError(
1189+
'render-nodes',
1190+
'Invalid node object passed to apos.template.renderNodes()',
1191+
{ node }
1192+
);
1193+
return '';
1194+
})
1195+
.join('')
1196+
.trim();
1197+
},
1198+
1199+
injectNodes(req, locationKey) {
1200+
const nodes = self.runtimeNodes[locationKey] || [];
1201+
if (!nodes.length) {
1202+
return '';
1203+
}
1204+
const output = [];
1205+
for (const { moduleName, method } of nodes) {
1206+
const handler = self.apos.modules[moduleName][method];
1207+
output.push(self.renderNodes(handler(req)));
1208+
}
1209+
return output.join('\n');
1210+
},
1211+
10761212
async annotateDataForExternalFront(req, template, data, moduleName) {
10771213
const docs = self.getDocsForExternalFront(req, template, data, moduleName);
10781214
for (const doc of docs) {
@@ -1101,6 +1237,12 @@ module.exports = {
11011237
data.bundleMarkup.js.push(...Array.from(modulePreload));
11021238
}
11031239

1240+
// `node` injections
1241+
data.prependHead = self.injectNodes(req, 'prepend-head');
1242+
data.appendHead = self.injectNodes(req, 'append-head');
1243+
data.prependBody = self.injectNodes(req, 'prepend-body');
1244+
data.appendBody = self.injectNodes(req, 'append-body');
1245+
11041246
return data;
11051247
},
11061248

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{% for c in data.components %}
22
{% component c %}
33
{% endfor %}
4+
{{ (data.html | safe) if data.html }}

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
"express": "^4.16.4",
7676
"express-bearer-token": "^3.0.0",
7777
"express-cache-on-demand": "^1.0.3",
78-
"express-session": "^1.17.1",
78+
"express-session": "^1.18.2",
7979
"form-data": "^4.0.0",
8080
"fs-extra": "^7.0.1",
8181
"glob": "^10.4.5",
@@ -123,6 +123,7 @@
123123
"tough-cookie": "^4.0.0",
124124
"underscore.string": "^3.3.4",
125125
"uploadfs": "^1.24.3",
126+
"void-elements": "^3.1.0",
126127
"vue": "^3.3.8",
127128
"vue-advanced-cropper": "^2.8.8",
128129
"vue-loader": "^17.1.0",
@@ -142,4 +143,4 @@
142143
"browserslist": [
143144
"ie >= 10"
144145
]
145-
}
146+
}

0 commit comments

Comments
 (0)