@@ -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,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
0 commit comments