xref: /plugin/pagecss/action.php (revision f2a25b8dc0d66b25b74db7510495b2d5d6a554d7)
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