@@ -16,6 +16,11 @@ class ThemeService
1616 */
1717 protected array $ listeners = [];
1818
19+ /**
20+ * @var array<string, ThemeModule>
21+ */
22+ protected array $ modules = [];
23+
1924 /**
2025 * Get the currently configured theme.
2126 * Returns an empty string if not configured.
@@ -77,20 +82,94 @@ public function registerCommand(Command $command): void
7782 }
7883
7984 /**
80- * Read any actions from the set theme path if the 'functions.php' file exists .
85+ * Read any actions from the 'functions.php' file of the active theme or its modules .
8186 */
8287 public function readThemeActions (): void
8388 {
84- $ themeActionsFile = theme_path ('functions.php ' );
85- if (!$ themeActionsFile || !file_exists ($ themeActionsFile )) {
89+ $ moduleFunctionFiles = array_map (function (ThemeModule $ module ): string {
90+ return $ module ->path ('functions.php ' );
91+ }, $ this ->modules );
92+ $ allFunctionFiles = array_merge (array_values ($ moduleFunctionFiles ), [theme_path ('functions.php ' )]);
93+ $ filteredFunctionFiles = array_filter ($ allFunctionFiles , function (string $ file ): bool {
94+ return $ file && file_exists ($ file );
95+ });
96+
97+ foreach ($ filteredFunctionFiles as $ functionFile ) {
98+ try {
99+ require $ functionFile ;
100+ } catch (\Error $ exception ) {
101+ throw new ThemeException ("Failed loading theme functions file at \"{$ functionFile }\" with error: {$ exception ->getMessage ()}" );
102+ }
103+ }
104+ }
105+
106+ /**
107+ * Read the modules folder and load in any valid theme modules.
108+ */
109+ public function loadModules (): void
110+ {
111+ $ modulesFolder = theme_path ('modules ' );
112+ if (!$ modulesFolder || !is_dir ($ modulesFolder )) {
86113 return ;
87114 }
88115
89- try {
90- require $ themeActionsFile ;
91- } catch (\Error $ exception ) {
92- throw new ThemeException ("Failed loading theme functions file at \"{$ themeActionsFile }\" with error: {$ exception ->getMessage ()}" );
116+ $ subFolders = array_filter (scandir ($ modulesFolder ), function ($ item ) use ($ modulesFolder ) {
117+ return $ item !== '. ' && $ item !== '.. ' && is_dir ($ modulesFolder . DIRECTORY_SEPARATOR . $ item );
118+ });
119+
120+ foreach ($ subFolders as $ folderName ) {
121+ $ moduleJsonFile = $ modulesFolder . DIRECTORY_SEPARATOR . $ folderName . DIRECTORY_SEPARATOR . 'bookstack-module.json ' ;
122+
123+ if (!file_exists ($ moduleJsonFile )) {
124+ continue ;
125+ }
126+
127+ try {
128+ $ jsonContent = file_get_contents ($ moduleJsonFile );
129+ $ jsonData = json_decode ($ jsonContent , true );
130+
131+ if (json_last_error () !== JSON_ERROR_NONE ) {
132+ throw new ThemeException ("Invalid JSON in module file at \"{$ moduleJsonFile }\": " . json_last_error_msg ());
133+ }
134+
135+ $ module = ThemeModule::fromJson ($ jsonData , $ folderName );
136+ $ this ->modules [$ folderName ] = $ module ;
137+ } catch (ThemeException $ exception ) {
138+ throw $ exception ;
139+ } catch (\Exception $ exception ) {
140+ throw new ThemeException ("Failed loading module from \"{$ moduleJsonFile }\" with error: {$ exception ->getMessage ()}" );
141+ }
142+ }
143+ }
144+
145+ /**
146+ * Get all loaded theme modules.
147+ * @return array<string, ThemeModule>
148+ */
149+ public function getModules (): array
150+ {
151+ return $ this ->modules ;
152+ }
153+
154+ /**
155+ * Look for a specific file within the theme or its modules.
156+ * Returns the first file found or null if not found.
157+ */
158+ public function findFirstFile (string $ path ): ?string
159+ {
160+ $ themePath = theme_path ($ path );
161+ if (file_exists ($ themePath )) {
162+ return $ themePath ;
163+ }
164+
165+ foreach ($ this ->modules as $ module ) {
166+ $ customizedFile = $ module ->path ($ path );
167+ if (file_exists ($ customizedFile )) {
168+ return $ customizedFile ;
169+ }
93170 }
171+
172+ return null ;
94173 }
95174
96175 /**
0 commit comments