xref: /plugin/combo/syntax/webcode.php (revision 5f891b7e09648e05e78f5882f3fdde1e9df9b0f1)
1<?php
2/**
3 * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved.
4 *
5 * This source code is licensed under the GPL license found in the
6 * COPYING  file in the root directory of this source tree.
7 *
8 * @license  GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
9 * @author   ComboStrap <support@combostrap.com>
10 *
11 */
12
13/**
14 * Plugin Webcode: Show webcode (Css, HTML) in a iframe
15 *
16 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
17 * @author     Nicolas GERARD
18 */
19
20// must be run within Dokuwiki
21use ComboStrap\SnippetManager;
22use ComboStrap\LogUtility;
23use ComboStrap\PluginUtility;
24use ComboStrap\Tag;
25
26if (!defined('DOKU_INC')) die();
27
28/**
29 * Webcode
30 */
31class syntax_plugin_combo_webcode extends DokuWiki_Syntax_Plugin
32{
33
34    const EXTERNAL_RESOURCES_ATTRIBUTE_DISPLAY = 'externalResources'; // In the action bar
35    const EXTERNAL_RESOURCES_ATTRIBUTE_KEY = 'externalresources'; // In the code
36
37    // Simple cache bursting implementation for the webCodeConsole.(js|css) file
38    // They must be incremented manually when they changed
39    const WEB_CSS_VERSION = 1.1;
40    const WEB_CONSOLE_JS_VERSION = 2.1;
41
42    const TAG = 'webcode';
43
44    /**
45     * The tag that have codes
46     */
47    const CODE_TAGS = array("code", "plugin_combo_code");
48
49    /**
50     * The attribute names in the array
51     */
52    const CODES_ATTRIBUTE = "codes";
53    const USE_CONSOLE_ATTRIBUTE = "useConsole";
54
55    /**
56     * @var array that holds the iframe attributes
57     */
58    private $attributes = array();
59
60
61    /**
62     * Syntax Type.
63     *
64     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
65     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
66     *
67     * container because it may contain header in case of how to
68     */
69    public function getType()
70    {
71        return 'container';
72    }
73
74    /**
75     * @return array
76     * Allow which kind of plugin inside
77     *
78     * array('container', 'baseonly','formatting', 'substition', 'protected', 'disabled', 'paragraphs')
79     *
80     */
81    public function getAllowedTypes()
82    {
83        return array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs');
84    }
85
86
87    public function accepts($mode)
88    {
89        if (!$this->getConf(syntax_plugin_combo_preformatted::CONF_PREFORMATTED_ENABLE)) {
90            return PluginUtility::disablePreformatted($mode);
91        } else {
92            return true;
93        }
94    }
95
96    /**
97     * @see Doku_Parser_Mode::getSort()
98     * The mode (plugin) with the lowest sort number will win out
99     *
100     * See {@link Doku_Parser_Mode_code}
101     */
102    public function getSort()
103    {
104        return 99;
105    }
106
107    /**
108     * Called before any calls to ConnectTo
109     * @return void
110     */
111    function preConnect()
112    {
113    }
114
115    /**
116     * Create a pattern that will called this plugin
117     *
118     * @param string $mode
119     *
120     * All dokuwiki mode can be seen in the parser.php file
121     * @see Doku_Parser_Mode::connectTo()
122     */
123    public function connectTo($mode)
124    {
125
126        $pattern = PluginUtility::getContainerTagPattern(self::TAG);
127        $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeForComponent($this->getPluginComponent()));
128
129    }
130
131
132    // This where the addPattern and addExitPattern are defined
133    public function postConnect()
134    {
135        $this->Lexer->addExitPattern('</' . self::TAG . '>', PluginUtility::getModeForComponent($this->getPluginComponent()));
136    }
137
138
139    /**
140     * Handle the match
141     * You get the match for each pattern in the $match variable
142     * $state says if it's an entry, exit or match pattern
143     *
144     * This is an instruction block and is cached apart from the rendering output
145     * There is two caches levels
146     * This cache may be suppressed with the url parameters ?purge=true
147     *
148     * The returned values are cached in an array that will be passed to the render method
149     * The handle function goal is to parse the matched syntax through the pattern function
150     * and to return the result for use in the renderer
151     * This result is always cached until the page is modified.
152     * @param string $match
153     * @param int $state
154     * @param int $pos
155     * @param Doku_Handler $handler
156     * @return array|bool
157     * @throws Exception
158     * @see DokuWiki_Syntax_Plugin::handle()
159     *
160     */
161    public function handle($match, $state, $pos, Doku_Handler $handler)
162    {
163        switch ($state) {
164
165            case DOKU_LEXER_ENTER :
166
167                // We got the first webcode tag and its attributes
168
169                $match = substr($match, 8, -1); //9 = strlen("<webcode")
170
171                // Reset of the attributes
172                // With some framework the php object may be still persisted in memory
173                // And you may get some attributes from other page
174                $attributes = array();
175                $attributes['frameborder'] = 1;
176                $attributes['width'] = '100%';
177
178                $renderingModeKey = 'renderingmode';
179                $attributes[$renderingModeKey] = 'story';
180
181                // config Parameters will get their value in lowercase
182                $configAttributes = [$renderingModeKey];
183
184                // /i not case sensitive
185                $attributePattern = "\s*(\w+)\s*=\s*\"?([^\"\s]+)\"?\\s*";
186                $result = preg_match_all('/' . $attributePattern . '/i', $match, $matches);
187
188
189                if ($result != 0) {
190                    foreach ($matches[1] as $key => $lang) {
191                        $attributeKey = strtolower($lang);
192                        $attributeValue = $matches[2][$key];
193                        if (in_array($attributeKey, $configAttributes)) {
194                            $attributeValue = strtolower($attributeValue);
195                        }
196                        $attributes[$attributeKey] = $attributeValue;
197                    }
198                }
199
200                // We set the attributes on a class scope
201                // to be used in the DOKU_LEXER_UNMATCHED step
202                $this->attributes = $attributes;
203
204                // Cache the values to be used by the render method
205                return array(
206                    PluginUtility::STATE => $state,
207                    PluginUtility::ATTRIBUTES => $attributes
208                );
209
210
211            case DOKU_LEXER_UNMATCHED :
212
213                // Cache the values
214                return array(
215                    PluginUtility::STATE => $state,
216                    PluginUtility::PAYLOAD => $match
217                );
218
219            case DOKU_LEXER_EXIT:
220
221                /**
222                 * Capture all codes
223                 */
224                $codes = array();
225                /**
226                 * Does the javascript contains a console statement
227                 */
228                $useConsole = false;
229                $exitTag = new Tag(self::TAG, array(), $state, $handler);
230                $openingTag = $exitTag->getOpeningTag();
231                if ($openingTag->hasDescendants()) {
232                    $tags = $openingTag->getDescendants();
233                    /**
234                     * Mime and code content are in two differents
235                     * tag. To be able to set the content to the good type
236                     * we keep a trace of it
237                     */
238                    $actualCodeType = "";
239                    foreach ($tags as $tag) {
240                        if (in_array($tag->getName(), self::CODE_TAGS)) {
241
242                            if ($tag->getState() == DOKU_LEXER_ENTER) {
243                                // Get the code (The content between the code nodes)
244                                // We ltrim because the match gives us the \n at the beginning and at the end
245                                $actualCodeType = strtolower(trim($tag->getType()));
246
247                                // Xml is html
248                                if ($actualCodeType == 'xml') {
249                                    $actualCodeType = 'html';
250                                }
251                                // The code for a language may be scattered in mutliple block
252                                if (!isset($codes[$actualCodeType])) {
253                                    $codes[$actualCodeType] = "";
254                                }
255                                continue;
256                            }
257
258                            if ($tag->getState() == DOKU_LEXER_UNMATCHED) {
259
260                                $codeContent = $tag->getData()[PluginUtility::PAYLOAD];
261
262                                if (empty($actualCodeType)) {
263                                    LogUtility::msg("The type of the code should not be null for the code content " . $codeContent, LogUtility::LVL_MSG_WARNING, self::TAG);
264                                    continue;
265                                }
266
267                                // Append it
268                                $codes[$actualCodeType] = $codes[$actualCodeType] . $codeContent;
269
270                                // Check if a javascript console function is used, only if the flag is not set to true
271                                if (!$useConsole == true) {
272                                    if (in_array($actualCodeType, array('babel', 'javascript', 'html', 'xml'))) {
273                                        // if the code contains 'console.'
274                                        $result = preg_match('/' . 'console\.' . '/is', $codeContent);
275                                        if ($result) {
276                                            $useConsole = true;
277                                        }
278                                    }
279                                }
280                                // Reset
281                                $actualCodeType = "";
282                            }
283                        }
284                    }
285                }
286                return array(
287                    PluginUtility::STATE => $state,
288                    self::CODES_ATTRIBUTE => $codes,
289                    self::USE_CONSOLE_ATTRIBUTE => $useConsole
290                );
291
292        }
293        return false;
294
295    }
296
297    /**
298     * Render the output
299     * @param string $mode
300     * @param Doku_Renderer $renderer
301     * @param array $data - what the function handle() return'ed
302     * @return bool - rendered correctly (not used)
303     *
304     * The rendering process
305     * @see DokuWiki_Syntax_Plugin::render()
306     *
307     */
308    public function render($mode, Doku_Renderer $renderer, $data)
309    {
310        // The $data variable comes from the handle() function
311        //
312        // $mode = 'xhtml' means that we output html
313        // There is other mode such as metadata where you can output data for the headers (Not 100% sure)
314        if ($mode == 'xhtml') {
315
316
317            /** @var Doku_Renderer_xhtml $renderer */
318
319            $state = $data[PluginUtility::STATE];
320            switch ($state) {
321
322                case DOKU_LEXER_ENTER :
323
324                    PluginUtility::getSnippetManager()->addJavascriptSnippetIfNeeded(self::TAG);
325
326                    // The extracted data are the attribute of the webcode tag
327                    // We put in a class variable so that we can use in the last step (DOKU_LEXER_EXIT)
328                    $this->attributes = $data[PluginUtility::ATTRIBUTES];
329
330                    break;
331
332                case DOKU_LEXER_UNMATCHED :
333
334                    // Render and escape
335                    $renderer->doc .= $renderer->_xmlEntities($data[PluginUtility::PAYLOAD]);
336                    break;
337
338                case DOKU_LEXER_EXIT :
339                    $codes = $data[self::CODES_ATTRIBUTE];
340                    // Create the real output of webcode
341                    if (sizeof($codes) == 0) {
342                        return false;
343                    }
344
345                    PluginUtility::getSnippetManager()->addCssSnippetOnlyOnce(self::TAG);
346
347                    // Dokuwiki Code ?
348                    if (array_key_exists('dw', $codes)) {
349
350                        $renderer->doc .= PluginUtility::render($codes['dw']);
351
352                    } else {
353
354
355                        // Js, Html, Css
356                        $iframeHtml = '<html><head>';
357                        $iframeHtml .= '<meta http-equiv="content-type" content="text/html; charset=UTF-8">';
358                        $iframeHtml .= '<title>Made by Webcode</title>';
359                        $iframeHtml .= '<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css">';
360
361
362                        // External Resources such as css stylesheet or js
363                        $externalResources = array();
364                        if (array_key_exists(self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY, $this->attributes)) {
365                            $externalResources = explode(",", $this->attributes[self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY]);
366                        }
367
368                        // Babel Preprocessor, if babel is used, add it to the external resources
369                        if (array_key_exists('babel', $codes)) {
370                            $babelMin = "https://unpkg.com/babel-standalone@6/babel.min.js";
371                            // a load of babel invoke it (be sure to not have it twice
372                            if (!(array_key_exists($babelMin, $externalResources))) {
373                                $externalResources[] = $babelMin;
374                            }
375                        }
376
377                        // Add the external resources
378                        foreach ($externalResources as $externalResource) {
379                            $pathInfo = pathinfo($externalResource);
380                            $fileExtension = $pathInfo['extension'];
381                            switch ($fileExtension) {
382                                case 'css':
383                                    $iframeHtml .= '<link rel="stylesheet" type="text/css" href="' . $externalResource . '">';
384                                    break;
385                                case 'js':
386                                    $iframeHtml .= '<script type="text/javascript" src="' . $externalResource . '"></script>';
387                                    break;
388                            }
389                        }
390
391
392                        // WebConsole style sheet
393                        $iframeHtml .= '<link rel="stylesheet" type="text/css" href="' . PluginUtility::getResourceBaseUrl() . '/webcode/webcode-iframe.css?ver=' . self::WEB_CSS_VERSION . '"/>';
394
395                        if (array_key_exists('css', $codes)) {
396                            $iframeHtml .= '<!-- The CSS code -->';
397                            $iframeHtml .= '<style>' . $codes['css'] . '</style>';
398                        };
399                        $iframeHtml .= '</head><body style="margin:10px">';
400                        if (array_key_exists('html', $codes)) {
401                            $iframeHtml .= '<!-- The HTML code -->';
402                            $iframeHtml .= $codes['html'];
403                        }
404                        // The javascript console area is based at the end of the HTML document
405                        $useConsole = $data[self::USE_CONSOLE_ATTRIBUTE];
406                        if ($useConsole) {
407                            $iframeHtml .= '<!-- WebCode Console -->';
408                            $iframeHtml .= '<div><p class=\'webConsoleTitle\'>Console Output:</p>';
409                            $iframeHtml .= '<div id=\'webCodeConsole\'></div>';
410                            $iframeHtml .= '<script type=\'text/javascript\' src=\'' . PluginUtility::getResourceBaseUrl() . '/webcode/webcode-console.js?ver=' . self::WEB_CONSOLE_JS_VERSION . '\'></script>';
411                            $iframeHtml .= '</div>';
412                        }
413                        // The javascript comes at the end because it may want to be applied on previous HTML element
414                        // as the page load in the IO order, javascript must be placed at the end
415                        if (array_key_exists('javascript', $codes)) {
416                            $iframeHtml .= '<!-- The Javascript code -->';
417                            $iframeHtml .= '<script type="text/javascript">' . $codes['javascript'] . '</script>';
418                        }
419                        if (array_key_exists('babel', $codes)) {
420                            $iframeHtml .= '<!-- The Babel code -->';
421                            $iframeHtml .= '<script type="text/babel">' . $codes['babel'] . '</script>';
422                        }
423                        $iframeHtml .= '</body></html>';
424
425                        // Here the magic from the plugin happens
426                        // We add the Iframe and the JsFiddleButton
427                        $iFrameHtml = '<iframe ';
428
429                        // We add the name HTML attribute
430                        $name = "WebCode iFrame";
431                        if (array_key_exists('name', $this->attributes)) {
432                            $name .= ' ' . $this->attributes['name'];
433                        }
434                        $iFrameHtml .= ' name="' . $name . '" ';
435
436                        // The class to be able to select them
437                        $iFrameHtml .= ' class="webCode" ';
438
439                        // We add the others HTML attributes
440                        $iFrameHtmlAttributes = array('width', 'height', 'frameborder', 'scrolling');
441                        foreach ($this->attributes as $attribute => $value) {
442                            if (in_array($attribute, $iFrameHtmlAttributes)) {
443                                $iFrameHtml .= ' ' . $attribute . '=' . $value;
444                            }
445                        }
446                        $iFrameHtml .= ' srcdoc="' . htmlentities($iframeHtml) . '" ></iframe>';//
447
448                        // Credits bar
449                        $bar = '<div class="webcode-bar">';
450                        $bar .= '<div class="webcode-bar-item">' . PluginUtility::getUrl(self::TAG, "Rendered by Webcode",false) . '</div>';
451                        $bar .= '<div class="webcode-bar-item">' . $this->addJsFiddleButton($codes, $this->attributes) . '</div>';
452                        $bar .= '</div>';
453                        $renderer->doc .= '<div class="webcode">' . $iFrameHtml . $bar . '</div>';
454                    }
455
456                    break;
457            }
458
459            return true;
460        }
461        return false;
462    }
463
464    /**
465     * @param array $codes the array containing the codes
466     * @param array $attributes the attributes of a call (for now the externalResources)
467     * @return string the HTML form code
468     *
469     * Specification, see http://doc.jsfiddle.net/api/post.html
470     */
471    public function addJsFiddleButton($codes, $attributes)
472    {
473
474        $postURL = "https://jsfiddle.net/api/post/library/pure/"; //No Framework
475
476        $externalResources = array();
477        if (array_key_exists(self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY, $attributes)) {
478            $externalResources = explode(",", $attributes[self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY]);
479        }
480
481
482        if ($this->useConsole) {
483            // If their is a console.log function, add the Firebug Lite support of JsFiddle
484            // Seems to work only with the Edge version of jQuery
485            // $postURL .= "edge/dependencies/Lite/";
486            // The firebug logging is not working anymore because of 404
487            // Adding them here
488            $externalResources[] = 'The firebug resources for the console.log features';
489            $externalResources[] = PluginUtility::getResourceBaseUrl() . '/firebug/firebug-lite.css';
490            $externalResources[] = PluginUtility::getResourceBaseUrl() . '/firebug/firebug-lite-1.2.js';
491        }
492
493        // The below code is to prevent this JsFiddle bug: https://github.com/jsfiddle/jsfiddle-issues/issues/726
494        // The order of the resources is not guaranteed
495        // We pass then the resources only if their is one resources
496        // Otherwise we pass them as a script element in the HTML.
497        if (count($externalResources) <= 1) {
498            $externalResourcesInput = '<input type="hidden" name="resources" value="' . implode(",", $externalResources) . '">';
499        } else {
500            $codes['html'] .= "\n\n\n\n\n<!-- The resources -->\n";
501            $codes['html'] .= "<!-- They have been added here because their order is not guarantee through the API. -->\n";
502            $codes['html'] .= "<!-- See: https://github.com/jsfiddle/jsfiddle-issues/issues/726 -->\n";
503            foreach ($externalResources as $externalResource) {
504                if ($externalResource != "") {
505                    $extension = pathinfo($externalResource)['extension'];
506                    switch ($extension) {
507                        case "css":
508                            $codes['html'] .= "<link href=\"" . $externalResource . "\" rel=\"stylesheet\">\n";
509                            break;
510                        case "js":
511                            $codes['html'] .= "<script src=\"" . $externalResource . "\"></script>\n";
512                            break;
513                        default:
514                            $codes['html'] .= "<!-- " . $externalResource . " -->\n";
515                    }
516                }
517            }
518        }
519
520        $jsCode = $codes['javascript'];
521        $jsPanel = 0; // language for the js specific panel (0 = JavaScript)
522        if (array_key_exists('babel', $codes)) {
523            $jsCode = $codes['babel'];
524            $jsPanel = 3; // 3 = Babel
525        }
526
527        // Title and description
528        global $ID;
529        $title = $attributes['name'];
530        $pageTitle = tpl_pagetitle($ID, true);
531        if (!$title) {
532
533            $title = "Code from " . $pageTitle;
534        }
535        $description = "Code from the page '" . $pageTitle . "' \n" . wl($ID, $absolute = true);
536        return '<form  method="post" action="' . $postURL . '" target="_blank">' .
537            '<input type="hidden" name="title" value="' . htmlentities($title) . '">' .
538            '<input type="hidden" name="description" value="' . htmlentities($description) . '">' .
539            '<input type="hidden" name="css" value="' . htmlentities($codes['css']) . '">' .
540            '<input type="hidden" name="html" value="' . htmlentities("<!-- The HTML -->" . $codes['html']) . '">' .
541            '<input type="hidden" name="js" value="' . htmlentities($jsCode) . '">' .
542            '<input type="hidden" name="panel_js" value="' . htmlentities($jsPanel) . '">' .
543            '<input type="hidden" name="wrap" value="b">' .  //javascript no wrap in body
544            $externalResourcesInput .
545            '<button>Try the code</button>' .
546            '</form>';
547
548    }
549
550    /**
551     * @param $codes the array containing the codes
552     * @param $attributes the attributes of a call (for now the externalResources)
553     * @return string the HTML form code
554     */
555    public function addCodePenButton($codes, $attributes)
556    {
557        // TODO
558        // http://blog.codepen.io/documentation/api/prefill/
559    }
560
561
562}
563