@@ -34,6 +34,7 @@ const Promise = require('bluebird');
3434const path = require ( 'path' ) ;
3535const { stripIndent } = require ( 'common-tags' ) ;
3636const { SemanticAttributes } = require ( '@opentelemetry/semantic-conventions' ) ;
37+ const voidElements = require ( 'void-elements' ) ;
3738
3839module . 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,127 @@ 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 ] = 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 objects SHOULD have either `name` or `text` property.
1140+ // A node with `name` can have `attrs` (array of element attributes)
1141+ // and `body` (array of child nodes).
1142+ // `text` nodes are rendered as text (no HTML tags).
1143+ renderNodes ( nodes ) {
1144+ if ( ! Array . isArray ( nodes ) ) {
1145+ self . logError (
1146+ 'render-nodes' ,
1147+ 'Invalid nodes array passed to apos.template.renderNodes()'
1148+ ) ;
1149+ return '' ;
1150+ }
1151+ return nodes . map ( node => {
1152+ if ( node . text ) {
1153+ return self . apos . util . escapeHtml ( node . text ) ;
1154+ }
1155+ if ( node . name ) {
1156+ const name = self . apos . util . escapeHtml ( node . name ) ;
1157+ const attrs = Object . entries ( node . attrs || { } )
1158+ . map ( ( [ key , value ] ) => {
1159+ return ` ${ self . apos . util . escapeHtml ( key ) } ="${ self . apos . util . escapeHtml ( value ) } "` ;
1160+ } )
1161+ . join ( '' )
1162+ . trimEnd ( ) ;
1163+
1164+ if ( ! node . body && voidElements [ name ] ) {
1165+ return `<${ name } ${ attrs } />` ;
1166+ }
1167+
1168+ const body = (
1169+ node . body ? self . renderNodes ( node . body ) : ''
1170+ ) . trim ( ) ;
1171+
1172+ return `<${ name } ${ attrs } >${ body } </${ name } >` ;
1173+ }
1174+ self . logError (
1175+ 'render-nodes' ,
1176+ 'Invalid node object passed to apos.template.renderNodes()' ,
1177+ { node }
1178+ ) ;
1179+ return '' ;
1180+ } )
1181+ . join ( '' )
1182+ . trim ( ) ;
1183+ } ,
1184+
1185+ injectNodes ( req , locationKey ) {
1186+ const nodes = self . runtimeNodes [ locationKey ] || [ ] ;
1187+ if ( ! nodes . length ) {
1188+ return '' ;
1189+ }
1190+ const output = [ ] ;
1191+ for ( const { moduleName, method } of nodes ) {
1192+ const handler = self . apos . modules [ moduleName ] [ method ] ;
1193+ output . push ( self . renderNodes ( handler ( req ) ) ) ;
1194+ }
1195+ return output . join ( '\n' ) ;
1196+ } ,
1197+
10761198 async annotateDataForExternalFront ( req , template , data , moduleName ) {
10771199 const docs = self . getDocsForExternalFront ( req , template , data , moduleName ) ;
10781200 for ( const doc of docs ) {
@@ -1101,6 +1223,12 @@ module.exports = {
11011223 data . bundleMarkup . js . push ( ...Array . from ( modulePreload ) ) ;
11021224 }
11031225
1226+ // `node` injections
1227+ data . prependHead = self . injectNodes ( req , 'prepend-head' ) ;
1228+ data . appendHead = self . injectNodes ( req , 'append-head' ) ;
1229+ data . prependBody = self . injectNodes ( req , 'prepend-body' ) ;
1230+ data . appendBody = self . injectNodes ( req , 'append-body' ) ;
1231+
11041232 return data ;
11051233 } ,
11061234
0 commit comments