16a458d92SdWiGhT<?php 2*f2a25b8dSdwightmulcahy/** 3*f2a25b8dSdwightmulcahy * DokuWiki Plugin: pagecss 4*f2a25b8dSdwightmulcahy * 5*f2a25b8dSdwightmulcahy * This plugin allows DokuWiki users to embed custom CSS directly within their 6*f2a25b8dSdwightmulcahy * wiki pages using `<pagecss>...</pagecss>` blocks. The CSS defined within 7*f2a25b8dSdwightmulcahy * these blocks is then extracted, processed, and injected into the `<head>` 8*f2a25b8dSdwightmulcahy * section of the generated HTML page. 9*f2a25b8dSdwightmulcahy * 10*f2a25b8dSdwightmulcahy * It also provides a feature to automatically wrap CSS rules for classes 11*f2a25b8dSdwightmulcahy * found within the `<pagecss>` block (e.g., `.myclass { ... }`) with a 12*f2a25b8dSdwightmulcahy * `.wrap_myclass { ... }` equivalent. This is useful for styling elements 13*f2a25b8dSdwightmulcahy * that are automatically wrapped by DokuWiki's `.wrap` classes. 14*f2a25b8dSdwightmulcahy * 15*f2a25b8dSdwightmulcahy * Author: Your Name/Entity (or original author if known) 16*f2a25b8dSdwightmulcahy * Date: 2023-10-27 (or original creation date) 17*f2a25b8dSdwightmulcahy * 18*f2a25b8dSdwightmulcahy * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 19*f2a25b8dSdwightmulcahy */ 20*f2a25b8dSdwightmulcahy 21*f2a25b8dSdwightmulcahy// Import necessary DokuWiki extension classes 226a458d92SdWiGhTuse dokuwiki\Extension\ActionPlugin; 236a458d92SdWiGhTuse dokuwiki\Extension\EventHandler; 246a458d92SdWiGhTuse dokuwiki\Extension\Event; 256a458d92SdWiGhT 26*f2a25b8dSdwightmulcahy/** 27*f2a25b8dSdwightmulcahy * Class action_plugin_pagecss 28*f2a25b8dSdwightmulcahy * 29*f2a25b8dSdwightmulcahy * This class extends DokuWiki's ActionPlugin to hook into specific DokuWiki 30*f2a25b8dSdwightmulcahy * events for processing and injecting custom page-specific CSS. 31*f2a25b8dSdwightmulcahy */ 326a458d92SdWiGhTclass action_plugin_pagecss extends ActionPlugin { 336a458d92SdWiGhT 34*f2a25b8dSdwightmulcahy /** 35*f2a25b8dSdwightmulcahy * Registers the plugin's hooks with the DokuWiki event handler. 36*f2a25b8dSdwightmulcahy * 37*f2a25b8dSdwightmulcahy * This method is called by DokuWiki during plugin initialization. 38*f2a25b8dSdwightmulcahy * It sets up which DokuWiki events this plugin will listen for and 39*f2a25b8dSdwightmulcahy * which methods will handle those events. 40*f2a25b8dSdwightmulcahy * 41*f2a25b8dSdwightmulcahy * @param EventHandler $controller The DokuWiki event handler instance. 42*f2a25b8dSdwightmulcahy */ 436a458d92SdWiGhT public function register(EventHandler $controller) { 44*f2a25b8dSdwightmulcahy // Register a hook to inject custom CSS into the HTML header. 45*f2a25b8dSdwightmulcahy // 'TPL_METAHEADER_OUTPUT' is triggered just before the <head> section is closed. 46*f2a25b8dSdwightmulcahy // 'BEFORE' ensures our CSS is added before other elements that might rely on it. 47*f2a25b8dSdwightmulcahy // '$this' refers to the current plugin instance. 48*f2a25b8dSdwightmulcahy // 'inject_css' is the method that will be called when this event fires. 496a458d92SdWiGhT $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'inject_css'); 50*f2a25b8dSdwightmulcahy 51*f2a25b8dSdwightmulcahy // Register a hook to handle metadata caching and extraction of CSS. 52*f2a25b8dSdwightmulcahy // 'PARSER_CACHE_USE' is triggered before DokuWiki attempts to use its parser cache. 53*f2a25b8dSdwightmulcahy // 'BEFORE' allows us to modify the metadata before the page is rendered or cached. 54*f2a25b8dSdwightmulcahy // 'handle_metadata' is the method that will be called. 556a458d92SdWiGhT $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'handle_metadata'); 566a458d92SdWiGhT } 576a458d92SdWiGhT 58*f2a25b8dSdwightmulcahy /** 59*f2a25b8dSdwightmulcahy * Extracts CSS content from `<pagecss>...</pagecss>` blocks within a DokuWiki page. 60*f2a25b8dSdwightmulcahy * 61*f2a25b8dSdwightmulcahy * This method is triggered by the 'PARSER_CACHE_USE' event. It reads the raw 62*f2a25b8dSdwightmulcahy * content of the current wiki page, finds all `<pagecss>` blocks, combines 63*f2a25b8dSdwightmulcahy * their content, and stores it in the page's metadata. It also generates 64*f2a25b8dSdwightmulcahy * `.wrap_classname` rules for any classes found in the embedded CSS. 65*f2a25b8dSdwightmulcahy * 66*f2a25b8dSdwightmulcahy * @param \Doku_Event $event The DokuWiki event object, containing page data. 67*f2a25b8dSdwightmulcahy */ 686a458d92SdWiGhT public function handle_metadata(\Doku_Event $event) { 69*f2a25b8dSdwightmulcahy global $ID; // Global variable holding the current DokuWiki page ID. 706a458d92SdWiGhT 71*f2a25b8dSdwightmulcahy // Sanitize the page ID to ensure it's safe for file system operations and metadata. 72*f2a25b8dSdwightmulcahy $id = cleanID($ID); 73*f2a25b8dSdwightmulcahy // Get the raw content of the current wiki page. This includes all wiki syntax. 74*f2a25b8dSdwightmulcahy $text = rawWiki($id); 75*f2a25b8dSdwightmulcahy 76*f2a25b8dSdwightmulcahy // Use a regular expression to find all occurrences of <pagecss>...</pagecss> blocks. 77*f2a25b8dSdwightmulcahy // The /s modifier makes the dot (.) match newlines as well, allowing multiline CSS. 78*f2a25b8dSdwightmulcahy // The (.*?) captures the content between the tags non-greedily. 796a458d92SdWiGhT preg_match_all('/<pagecss>(.*?)<\/pagecss>/s', $text, $matches); 80*f2a25b8dSdwightmulcahy 81*f2a25b8dSdwightmulcahy // Check if any <pagecss> blocks were found. 826a458d92SdWiGhT if (!empty($matches[1])) { 83*f2a25b8dSdwightmulcahy // If blocks are found, combine all captured CSS content into a single string. 84*f2a25b8dSdwightmulcahy // trim() is used to remove leading/trailing whitespace from each block. 856a458d92SdWiGhT $styles = implode(" ", array_map('trim', $matches[1])); 86*f2a25b8dSdwightmulcahy 87*f2a25b8dSdwightmulcahy // If there's actual CSS content after trimming and combining. 886a458d92SdWiGhT if ($styles) { 89*f2a25b8dSdwightmulcahy $extra = ''; // Initialize a variable to hold the generated .wrap_classname styles. 90*f2a25b8dSdwightmulcahy 91*f2a25b8dSdwightmulcahy // Find all CSS class selectors (e.g., .myclass) within the extracted styles. 92*f2a25b8dSdwightmulcahy // This regex captures the class name (e.g., 'myclass'). 936a458d92SdWiGhT preg_match_all('/\.([a-zA-Z0-9_-]+)\s*\{[^}]*\}/', $styles, $class_matches); 94*f2a25b8dSdwightmulcahy 95*f2a25b8dSdwightmulcahy // Iterate over each found class name. 966a458d92SdWiGhT foreach ($class_matches[1] as $classname) { 97*f2a25b8dSdwightmulcahy // Construct a regex pattern to find the full CSS rule for the current class. 986a458d92SdWiGhT $pattern = '/\.' . preg_quote($classname, '/') . '\s*\{([^}]*)\}/'; 99*f2a25b8dSdwightmulcahy // Match the specific class rule in the combined styles. 1006a458d92SdWiGhT if (preg_match($pattern, $styles, $style_block)) { 101*f2a25b8dSdwightmulcahy // Extract the content of the CSS rule (e.g., "color: red; font-size: 1em;"). 102*f2a25b8dSdwightmulcahy $css_properties = $style_block[1]; 103*f2a25b8dSdwightmulcahy 104*f2a25b8dSdwightmulcahy // Basic check to avoid malformed or incomplete styles that might contain 105*f2a25b8dSdwightmulcahy // unclosed braces, which could lead to invalid CSS. 106*f2a25b8dSdwightmulcahy if (strpos($css_properties, '{') === false && strpos($css_properties, '}') === false) { 107*f2a25b8dSdwightmulcahy // Append the generated .wrap_classname rule to the $extra string. 108*f2a25b8dSdwightmulcahy // DokuWiki often wraps user content in divs with classes like .wrap_someclass. 109*f2a25b8dSdwightmulcahy // This ensures that custom CSS can target these wrapped elements. 110*f2a25b8dSdwightmulcahy $extra .= ".wrap_$classname {{$css_properties}}\n"; 1116a458d92SdWiGhT } 1126a458d92SdWiGhT } 113*f2a25b8dSdwightmulcahy } 114*f2a25b8dSdwightmulcahy 115*f2a25b8dSdwightmulcahy // Append the generated .wrap_classname styles to the main $styles string. 1166a458d92SdWiGhT $styles .= "\n" . trim($extra); 117*f2a25b8dSdwightmulcahy 118*f2a25b8dSdwightmulcahy // IMPORTANT: Prevent premature closing of the <style> tag in the HTML output. 119*f2a25b8dSdwightmulcahy // If a user accidentally or maliciously types `</style>` inside `<pagecss>`, 120*f2a25b8dSdwightmulcahy // this replaces it with `<\style>` which is still valid CSS but doesn't close the tag. 121*f2a25b8dSdwightmulcahy $styles = str_replace('</', '<\/', $styles); 122*f2a25b8dSdwightmulcahy 123*f2a25b8dSdwightmulcahy // Store the processed CSS styles in the page's metadata. 124*f2a25b8dSdwightmulcahy // This makes the styles available later when the HTML header is generated. 125*f2a25b8dSdwightmulcahy p_set_metadata($id, ['pagecss' => ['styles' => $styles]]); 126*f2a25b8dSdwightmulcahy 127*f2a25b8dSdwightmulcahy // Invalidate the DokuWiki parser cache for this page whenever its content changes. 128*f2a25b8dSdwightmulcahy // This ensures that if the <pagecss> blocks are modified, the metadata is re-extracted. 129*f2a25b8dSdwightmulcahy $event->data['depends']['page'][] = $id; 130*f2a25b8dSdwightmulcahy 131*f2a25b8dSdwightmulcahy return; // Exit the function as styles were found and processed. 1326a458d92SdWiGhT } 1336a458d92SdWiGhT } 1346a458d92SdWiGhT 135*f2a25b8dSdwightmulcahy // If no <pagecss> blocks were found or they were empty, 136*f2a25b8dSdwightmulcahy // ensure the 'pagecss' metadata entry is reset to an empty string. 137*f2a25b8dSdwightmulcahy // This prevents old CSS from being injected if the blocks are removed. 138*f2a25b8dSdwightmulcahy p_set_metadata($id, ['pagecss' => ['styles' => '']]); 1396a458d92SdWiGhT } 1406a458d92SdWiGhT 141*f2a25b8dSdwightmulcahy /** 142*f2a25b8dSdwightmulcahy * Injects the extracted CSS into the HTML `<head>` section of the DokuWiki page. 143*f2a25b8dSdwightmulcahy * 144*f2a25b8dSdwightmulcahy * This method is triggered by the 'TPL_METAHEADER_OUTPUT' event. It retrieves 145*f2a25b8dSdwightmulcahy * the stored CSS from the page's metadata and adds it to the event data, 146*f2a25b8dSdwightmulcahy * which DokuWiki then uses to build the `<head>` section. 147*f2a25b8dSdwightmulcahy * 148*f2a25b8dSdwightmulcahy * @param Doku_Event $event The DokuWiki event object, specifically for metaheader output. 149*f2a25b8dSdwightmulcahy */ 1506a458d92SdWiGhT public function inject_css(Doku_Event $event) { 151*f2a25b8dSdwightmulcahy global $ID; // Global variable holding the current DokuWiki page ID. 1526a458d92SdWiGhT 153*f2a25b8dSdwightmulcahy // Sanitize the page ID. 154*f2a25b8dSdwightmulcahy $id = cleanID($ID); 155*f2a25b8dSdwightmulcahy 156*f2a25b8dSdwightmulcahy // Retrieve the 'pagecss' metadata for the current page. 157*f2a25b8dSdwightmulcahy $data = p_get_metadata($id, 'pagecss'); 158*f2a25b8dSdwightmulcahy // Extract the 'styles' content from the metadata, defaulting to an empty string if not set. 159*f2a25b8dSdwightmulcahy $styles = isset($data['styles']) ? $data['styles'] : ''; 160*f2a25b8dSdwightmulcahy 161*f2a25b8dSdwightmulcahy // Check if there are valid styles to inject and ensure it's a string. 1626a458d92SdWiGhT if ($styles && is_string($styles)) { 163*f2a25b8dSdwightmulcahy // Add the custom CSS to the event's 'style' array. 164*f2a25b8dSdwightmulcahy // DokuWiki's template system will then automatically render this 165*f2a25b8dSdwightmulcahy // as a <style> block within the HTML <head>. 1666a458d92SdWiGhT $event->data['style'][] = [ 167*f2a25b8dSdwightmulcahy 'type' => 'text/css', // Specifies the content type. 168*f2a25b8dSdwightmulcahy 'media' => 'screen', // Specifies the media type for the CSS (e.g., 'screen', 'print'). 169*f2a25b8dSdwightmulcahy '_data' => $styles, // The actual CSS content. 1706a458d92SdWiGhT ]; 1716a458d92SdWiGhT } 1726a458d92SdWiGhT } 173*f2a25b8dSdwightmulcahy 1746a458d92SdWiGhT} 175