1+ /* eslint-disable max-classes-per-file */
12/*
23 * Copyright 2022 Adobe. All rights reserved.
34 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
@@ -160,6 +161,22 @@ export function toCamelCase(name) {
160161 return toClassName ( name ) . replace ( / - ( [ a - z ] ) / g, ( g ) => g [ 1 ] . toUpperCase ( ) ) ;
161162}
162163
164+ /**
165+ * Gets all the metadata elements that are in the given scope.
166+ * @param {String } scope The scope/prefix for the metadata
167+ * @returns an array of HTMLElement nodes that match the given scope
168+ */
169+ export function getAllMetadata ( scope ) {
170+ return [ ...document . head . querySelectorAll ( `meta[property^="${ scope } :"],meta[name^="${ scope } -"]` ) ]
171+ . reduce ( ( res , meta ) => {
172+ const id = toClassName ( meta . name
173+ ? meta . name . substring ( scope . length + 1 )
174+ : meta . getAttribute ( 'property' ) . split ( ':' ) [ 1 ] ) ;
175+ res [ id ] = meta . getAttribute ( 'content' ) ;
176+ return res ;
177+ } , { } ) ;
178+ }
179+
163180const ICONS_CACHE = { } ;
164181/**
165182 * Replace icons with inline SVG and prefix with codeBasePath.
@@ -470,6 +487,53 @@ export function buildBlock(blockName, content) {
470487 return ( blockEl ) ;
471488}
472489
490+ /**
491+ * Gets the configuration for the given block, and also passes
492+ * the config through all custom patching helpers added to the project.
493+ *
494+ * @param {Element } block The block element
495+ * @returns {Object } The block config (blockName, cssPath and jsPath)
496+ */
497+ function getBlockConfig ( block ) {
498+ const { blockName } = block . dataset ;
499+ const cssPath = `${ window . hlx . codeBasePath } /blocks/${ blockName } /${ blockName } .css` ;
500+ const jsPath = `${ window . hlx . codeBasePath } /blocks/${ blockName } /${ blockName } .js` ;
501+ const original = { blockName, cssPath, jsPath } ;
502+ return ( window . hlx . patchBlockConfig || [ ] )
503+ . filter ( ( fn ) => typeof fn === 'function' )
504+ . reduce ( ( config , fn ) => fn ( config , original ) , { blockName, cssPath, jsPath } ) ;
505+ }
506+
507+ /**
508+ * Loads JS and CSS for a module and executes it's default export.
509+ * @param {string } name The module name
510+ * @param {string } jsPath The JS file to load
511+ * @param {string } [cssPath] An optional CSS file to load
512+ * @param {object[] } [args] Parameters to be passed to the default export when it is called
513+ */
514+ async function loadModule ( name , jsPath , cssPath , ...args ) {
515+ const cssLoaded = cssPath ? loadCSS ( cssPath ) : Promise . resolve ( ) ;
516+ const decorationComplete = jsPath
517+ ? new Promise ( ( resolve ) => {
518+ ( async ( ) => {
519+ let mod ;
520+ try {
521+ mod = await import ( jsPath ) ;
522+ if ( mod . default ) {
523+ await mod . default . apply ( null , args ) ;
524+ }
525+ } catch ( error ) {
526+ // eslint-disable-next-line no-console
527+ console . log ( `failed to load module for ${ name } ` , error ) ;
528+ }
529+ resolve ( mod ) ;
530+ } ) ( ) ;
531+ } )
532+ : Promise . resolve ( ) ;
533+ return Promise . all ( [ cssLoaded , decorationComplete ] )
534+ . then ( ( [ , api ] ) => api ) ;
535+ }
536+
473537/**
474538 * Loads JS and CSS for a block.
475539 * @param {Element } block The block element
@@ -478,24 +542,9 @@ export async function loadBlock(block) {
478542 const status = block . dataset . blockStatus ;
479543 if ( status !== 'loading' && status !== 'loaded' ) {
480544 block . dataset . blockStatus = 'loading' ;
481- const { blockName } = block . dataset ;
545+ const { blockName, cssPath , jsPath } = getBlockConfig ( block ) ;
482546 try {
483- const cssLoaded = loadCSS ( `${ window . hlx . codeBasePath } /blocks/${ blockName } /${ blockName } .css` ) ;
484- const decorationComplete = new Promise ( ( resolve ) => {
485- ( async ( ) => {
486- try {
487- const mod = await import ( `../blocks/${ blockName } /${ blockName } .js` ) ;
488- if ( mod . default ) {
489- await mod . default ( block ) ;
490- }
491- } catch ( error ) {
492- // eslint-disable-next-line no-console
493- console . log ( `failed to load module for ${ blockName } ` , error ) ;
494- }
495- resolve ( ) ;
496- } ) ( ) ;
497- } ) ;
498- await Promise . all ( [ cssLoaded , decorationComplete ] ) ;
547+ await loadModule ( blockName , jsPath , cssPath , block ) ;
499548 } catch ( error ) {
500549 // eslint-disable-next-line no-console
501550 console . log ( `failed to load block ${ blockName } ` , error ) ;
@@ -729,6 +778,121 @@ export function loadFooter(footer) {
729778 return loadBlock ( footerBlock ) ;
730779}
731780
781+ function parsePluginParams ( id , config ) {
782+ const pluginId = ! config
783+ ? id . split ( '/' ) . splice ( id . endsWith ( '/' ) ? - 2 : - 1 , 1 ) [ 0 ] . replace ( / \. j s / , '' )
784+ : id ;
785+ const pluginConfig = {
786+ load : 'eager' ,
787+ ...( typeof config === 'string' || ! config
788+ ? { url : ( config || id ) . replace ( / \/ $ / , '' ) }
789+ : config ) ,
790+ } ;
791+ pluginConfig . options ||= { } ;
792+ return { id : pluginId , config : pluginConfig } ;
793+ }
794+
795+ // Define an execution context for plugins
796+ export const executionContext = {
797+ createOptimizedPicture,
798+ getAllMetadata,
799+ getMetadata,
800+ decorateBlock,
801+ decorateButtons,
802+ decorateIcons,
803+ loadBlock,
804+ loadCSS,
805+ loadScript,
806+ sampleRUM,
807+ toCamelCase,
808+ toClassName,
809+ } ;
810+
811+ class PluginsRegistry {
812+ #plugins;
813+
814+ constructor ( ) {
815+ this . #plugins = new Map ( ) ;
816+ }
817+
818+ // Register a new plugin
819+ add ( id , config ) {
820+ const { id : pluginId , config : pluginConfig } = parsePluginParams ( id , config ) ;
821+ this . #plugins. set ( pluginId , pluginConfig ) ;
822+ }
823+
824+ // Get the plugin
825+ get ( id ) { return this . #plugins. get ( id ) ; }
826+
827+ // Check if the plugin exists
828+ includes ( id ) { return ! ! this . #plugins. has ( id ) ; }
829+
830+ // Load all plugins that are referenced by URL, and updated their configuration with the
831+ // actual API they expose
832+ async load ( phase ) {
833+ [ ...this . #plugins. entries ( ) ]
834+ . filter ( ( [ , plugin ] ) => plugin . condition
835+ && ! plugin . condition ( document , plugin . options , executionContext ) )
836+ . map ( ( [ id ] ) => this . #plugins. delete ( id ) ) ;
837+ return Promise . all ( [ ...this . #plugins. entries ( ) ]
838+ // Filter plugins that don't match the execution conditions
839+ . filter ( ( [ , plugin ] ) => (
840+ ( ! plugin . condition || plugin . condition ( document , plugin . options , executionContext ) )
841+ && phase === plugin . load && plugin . url
842+ ) )
843+ . map ( async ( [ key , plugin ] ) => {
844+ try {
845+ // If the plugin has a default export, it will be executed immediately
846+ const pluginApi = ( await loadModule (
847+ key ,
848+ ! plugin . url . endsWith ( '.js' ) ? `${ plugin . url } /${ key } .js` : plugin . url ,
849+ ! plugin . url . endsWith ( '.js' ) ? `${ plugin . url } /${ key } .css` : null ,
850+ document ,
851+ plugin . options ,
852+ executionContext ,
853+ ) ) || { } ;
854+ this . #plugins. set ( key , { ...plugin , ...pluginApi } ) ;
855+ } catch ( err ) {
856+ // eslint-disable-next-line no-console
857+ console . error ( 'Could not load specified plugin' , key ) ;
858+ }
859+ } ) ) ;
860+ }
861+
862+ // Run a specific phase in the plugin
863+ async run ( phase ) {
864+ return [ ...this . #plugins. values ( ) ]
865+ . reduce ( ( promise , plugin ) => ( // Using reduce to execute plugins sequencially
866+ plugin [ phase ] && ( ! plugin . condition
867+ || plugin . condition ( document , plugin . options , executionContext ) )
868+ ? promise . then ( ( ) => plugin [ phase ] ( document , plugin . options , executionContext ) )
869+ : promise
870+ ) , Promise . resolve ( ) ) ;
871+ }
872+ }
873+
874+ class TemplatesRegistry {
875+ // Register a new template
876+ // eslint-disable-next-line class-methods-use-this
877+ add ( id , url ) {
878+ if ( Array . isArray ( id ) ) {
879+ id . forEach ( ( i ) => window . hlx . templates . add ( i ) ) ;
880+ return ;
881+ }
882+ const { id : templateId , config : templateConfig } = parsePluginParams ( id , url ) ;
883+ templateConfig . condition = ( ) => toClassName ( getMetadata ( 'template' ) ) === templateId ;
884+ window . hlx . plugins . add ( templateId , templateConfig ) ;
885+ }
886+
887+ // Get the template
888+ // eslint-disable-next-line class-methods-use-this
889+ get ( id ) { return window . hlx . plugins . get ( id ) ; }
890+
891+ // Check if the template exists
892+ // eslint-disable-next-line class-methods-use-this
893+ includes ( id ) { return window . hlx . plugins . includes ( id ) ; }
894+ }
895+
732896/**
733897 * Setup block utils.
734898 */
@@ -737,6 +901,9 @@ export function setup() {
737901 window . hlx . RUM_MASK_URL = 'full' ;
738902 window . hlx . codeBasePath = '' ;
739903 window . hlx . lighthouse = new URLSearchParams ( window . location . search ) . get ( 'lighthouse' ) === 'on' ;
904+ window . hlx . patchBlockConfig = [ ] ;
905+ window . hlx . plugins = new PluginsRegistry ( ) ;
906+ window . hlx . templates = new TemplatesRegistry ( ) ;
740907
741908 const scriptEl = document . querySelector ( 'script[src$="/solutions/scripts/scripts.js"]' ) ;
742909 if ( scriptEl ) {
0 commit comments