xref: /plugin/combo/syntax/webcode.php (revision 32b85071e019dd3646a67c17fac4051338e495eb)
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                return PluginUtility::handleAndReturnUnmatchedData(self::TAG,$match,$handler);
214
215
216            case DOKU_LEXER_EXIT:
217
218                /**
219                 * Capture all codes
220                 */
221                $codes = array();
222                /**
223                 * Does the javascript contains a console statement
224                 */
225                $useConsole = false;
226                $exitTag = new Tag(self::TAG, array(), $state, $handler);
227                $openingTag = $exitTag->getOpeningTag();
228                if ($openingTag->hasDescendants()) {
229                    $tags = $openingTag->getDescendants();
230                    /**
231                     * Mime and code content are in two differents
232                     * tag. To be able to set the content to the good type
233                     * we keep a trace of it
234                     */
235                    $actualCodeType = "";
236                    foreach ($tags as $tag) {
237                        if (in_array($tag->getName(), self::CODE_TAGS)) {
238
239                            if ($tag->getState() == DOKU_LEXER_ENTER) {
240                                // Get the code (The content between the code nodes)
241                                // We ltrim because the match gives us the \n at the beginning and at the end
242                                $actualCodeType = strtolower(trim($tag->getType()));
243
244                                // Xml is html
245                                if ($actualCodeType == 'xml') {
246                                    $actualCodeType = 'html';
247                                }
248                                // The code for a language may be scattered in mutliple block
249                                if (!isset($codes[$actualCodeType])) {
250                                    $codes[$actualCodeType] = "";
251                                }
252                                continue;
253                            }
254
255                            if ($tag->getState() == DOKU_LEXER_UNMATCHED) {
256
257                                $codeContent = $tag->getData()[PluginUtility::PAYLOAD];
258
259                                if (empty($actualCodeType)) {
260                                    LogUtility::msg("The type of the code should not be null for the code content " . $codeContent, LogUtility::LVL_MSG_WARNING, self::TAG);
261                                    continue;
262                                }
263
264                                // Append it
265                                $codes[$actualCodeType] = $codes[$actualCodeType] . $codeContent;
266
267                                // Check if a javascript console function is used, only if the flag is not set to true
268                                if (!$useConsole == true) {
269                                    if (in_array($actualCodeType, array('babel', 'javascript', 'html', 'xml'))) {
270                                        // if the code contains 'console.'
271                                        $result = preg_match('/' . 'console\.' . '/is', $codeContent);
272                                        if ($result) {
273                                            $useConsole = true;
274                                        }
275                                    }
276                                }
277                                // Reset
278                                $actualCodeType = "";
279                            }
280                        }
281                    }
282                }
283                return array(
284                    PluginUtility::STATE => $state,
285                    self::CODES_ATTRIBUTE => $codes,
286                    self::USE_CONSOLE_ATTRIBUTE => $useConsole
287                );
288
289        }
290        return false;
291
292    }
293
294    /**
295     * Render the output
296     * @param string $mode
297     * @param Doku_Renderer $renderer
298     * @param array $data - what the function handle() return'ed
299     * @return bool - rendered correctly (not used)
300     *
301     * The rendering process
302     * @see DokuWiki_Syntax_Plugin::render()
303     *
304     */
305    public function render($mode, Doku_Renderer $renderer, $data)
306    {
307        // The $data variable comes from the handle() function
308        //
309        // $mode = 'xhtml' means that we output html
310        // There is other mode such as metadata where you can output data for the headers (Not 100% sure)
311        if ($mode == 'xhtml') {
312
313
314            /** @var Doku_Renderer_xhtml $renderer */
315
316            $state = $data[PluginUtility::STATE];
317            switch ($state) {
318
319                case DOKU_LEXER_ENTER :
320
321                    PluginUtility::getSnippetManager()->upsertJavascriptForBar(self::TAG);
322
323                    // The extracted data are the attribute of the webcode tag
324                    // We put in a class variable so that we can use in the last step (DOKU_LEXER_EXIT)
325                    $this->attributes = $data[PluginUtility::ATTRIBUTES];
326
327                    break;
328
329                case DOKU_LEXER_UNMATCHED :
330
331                    $renderer->doc .= PluginUtility::renderUnmatched($data);
332                    break;
333
334                case DOKU_LEXER_EXIT :
335                    $codes = $data[self::CODES_ATTRIBUTE];
336                    // Create the real output of webcode
337                    if (sizeof($codes) == 0) {
338                        return false;
339                    }
340
341                    PluginUtility::getSnippetManager()->upsertCssSnippetForBar(self::TAG);
342
343                    // Dokuwiki Code ?
344                    if (array_key_exists('dw', $codes)) {
345
346                        $renderer->doc .= PluginUtility::render($codes['dw']);
347
348                    } else {
349
350
351                        // Js, Html, Css
352                        $iframeHtml = '<html><head>';
353                        $iframeHtml .= '<meta http-equiv="content-type" content="text/html; charset=UTF-8">';
354                        $iframeHtml .= '<title>Made by Webcode</title>';
355                        $iframeHtml .= '<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css">';
356
357
358                        // External Resources such as css stylesheet or js
359                        $externalResources = array();
360                        if (array_key_exists(self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY, $this->attributes)) {
361                            $externalResources = explode(",", $this->attributes[self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY]);
362                        }
363
364                        // Babel Preprocessor, if babel is used, add it to the external resources
365                        if (array_key_exists('babel', $codes)) {
366                            $babelMin = "https://unpkg.com/babel-standalone@6/babel.min.js";
367                            // a load of babel invoke it (be sure to not have it twice
368                            if (!(array_key_exists($babelMin, $externalResources))) {
369                                $externalResources[] = $babelMin;
370                            }
371                        }
372
373                        // Add the external resources
374                        foreach ($externalResources as $externalResource) {
375                            $pathInfo = pathinfo($externalResource);
376                            $fileExtension = $pathInfo['extension'];
377                            switch ($fileExtension) {
378                                case 'css':
379                                    $iframeHtml .= '<link rel="stylesheet" type="text/css" href="' . $externalResource . '">';
380                                    break;
381                                case 'js':
382                                    $iframeHtml .= '<script type="text/javascript" src="' . $externalResource . '"></script>';
383                                    break;
384                            }
385                        }
386
387
388                        // WebConsole style sheet
389                        $iframeHtml .= '<link rel="stylesheet" type="text/css" href="' . PluginUtility::getResourceBaseUrl() . '/webcode/webcode-iframe.css?ver=' . self::WEB_CSS_VERSION . '"/>';
390
391                        if (array_key_exists('css', $codes)) {
392                            $iframeHtml .= '<!-- The CSS code -->';
393                            $iframeHtml .= '<style>' . $codes['css'] . '</style>';
394                        };
395                        $iframeHtml .= '</head><body style="margin:10px">';
396                        if (array_key_exists('html', $codes)) {
397                            $iframeHtml .= '<!-- The HTML code -->';
398                            $iframeHtml .= $codes['html'];
399                        }
400                        // The javascript console area is based at the end of the HTML document
401                        $useConsole = $data[self::USE_CONSOLE_ATTRIBUTE];
402                        if ($useConsole) {
403                            $iframeHtml .= '<!-- WebCode Console -->';
404                            $iframeHtml .= '<div><p class=\'webConsoleTitle\'>Console Output:</p>';
405                            $iframeHtml .= '<div id=\'webCodeConsole\'></div>';
406                            $iframeHtml .= '<script type=\'text/javascript\' src=\'' . PluginUtility::getResourceBaseUrl() . '/webcode/webcode-console.js?ver=' . self::WEB_CONSOLE_JS_VERSION . '\'></script>';
407                            $iframeHtml .= '</div>';
408                        }
409                        // The javascript comes at the end because it may want to be applied on previous HTML element
410                        // as the page load in the IO order, javascript must be placed at the end
411                        if (array_key_exists('javascript', $codes)) {
412                            $iframeHtml .= '<!-- The Javascript code -->';
413                            $iframeHtml .= '<script type="text/javascript">' . $codes['javascript'] . '</script>';
414                        }
415                        if (array_key_exists('babel', $codes)) {
416                            $iframeHtml .= '<!-- The Babel code -->';
417                            $iframeHtml .= '<script type="text/babel">' . $codes['babel'] . '</script>';
418                        }
419                        $iframeHtml .= '</body></html>';
420
421                        // Here the magic from the plugin happens
422                        // We add the Iframe and the JsFiddleButton
423                        $iFrameHtml = '<iframe ';
424
425                        // We add the name HTML attribute
426                        $name = "WebCode iFrame";
427                        if (array_key_exists('name', $this->attributes)) {
428                            $name .= ' ' . $this->attributes['name'];
429                        }
430                        $iFrameHtml .= ' name="' . $name . '" ';
431
432                        // The class to be able to select them
433                        $iFrameHtml .= ' class="webCode" ';
434
435                        // We add the others HTML attributes
436                        $iFrameHtmlAttributes = array('width', 'height', 'frameborder', 'scrolling');
437                        foreach ($this->attributes as $attribute => $value) {
438                            if (in_array($attribute, $iFrameHtmlAttributes)) {
439                                $iFrameHtml .= ' ' . $attribute . '=' . $value;
440                            }
441                        }
442                        $iFrameHtml .= ' srcdoc="' . htmlentities($iframeHtml) . '" ></iframe>';//
443
444                        // Credits bar
445                        $bar = '<div class="webcode-bar">';
446                        $bar .= '<div class="webcode-bar-item">' . PluginUtility::getUrl(self::TAG, "Rendered by Webcode",false) . '</div>';
447                        $bar .= '<div class="webcode-bar-item">' . $this->addJsFiddleButton($codes, $this->attributes) . '</div>';
448                        $bar .= '</div>';
449                        $renderer->doc .= '<div class="webcode">' . $iFrameHtml . $bar . '</div>';
450                    }
451
452                    break;
453            }
454
455            return true;
456        }
457        return false;
458    }
459
460    /**
461     * @param array $codes the array containing the codes
462     * @param array $attributes the attributes of a call (for now the externalResources)
463     * @return string the HTML form code
464     *
465     * Specification, see http://doc.jsfiddle.net/api/post.html
466     */
467    public function addJsFiddleButton($codes, $attributes)
468    {
469
470        $postURL = "https://jsfiddle.net/api/post/library/pure/"; //No Framework
471
472        $externalResources = array();
473        if (array_key_exists(self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY, $attributes)) {
474            $externalResources = explode(",", $attributes[self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY]);
475        }
476
477
478        if ($this->useConsole) {
479            // If their is a console.log function, add the Firebug Lite support of JsFiddle
480            // Seems to work only with the Edge version of jQuery
481            // $postURL .= "edge/dependencies/Lite/";
482            // The firebug logging is not working anymore because of 404
483            // Adding them here
484            $externalResources[] = 'The firebug resources for the console.log features';
485            $externalResources[] = PluginUtility::getResourceBaseUrl() . '/firebug/firebug-lite.css';
486            $externalResources[] = PluginUtility::getResourceBaseUrl() . '/firebug/firebug-lite-1.2.js';
487        }
488
489        // The below code is to prevent this JsFiddle bug: https://github.com/jsfiddle/jsfiddle-issues/issues/726
490        // The order of the resources is not guaranteed
491        // We pass then the resources only if their is one resources
492        // Otherwise we pass them as a script element in the HTML.
493        if (count($externalResources) <= 1) {
494            $externalResourcesInput = '<input type="hidden" name="resources" value="' . implode(",", $externalResources) . '">';
495        } else {
496            $codes['html'] .= "\n\n\n\n\n<!-- The resources -->\n";
497            $codes['html'] .= "<!-- They have been added here because their order is not guarantee through the API. -->\n";
498            $codes['html'] .= "<!-- See: https://github.com/jsfiddle/jsfiddle-issues/issues/726 -->\n";
499            foreach ($externalResources as $externalResource) {
500                if ($externalResource != "") {
501                    $extension = pathinfo($externalResource)['extension'];
502                    switch ($extension) {
503                        case "css":
504                            $codes['html'] .= "<link href=\"" . $externalResource . "\" rel=\"stylesheet\">\n";
505                            break;
506                        case "js":
507                            $codes['html'] .= "<script src=\"" . $externalResource . "\"></script>\n";
508                            break;
509                        default:
510                            $codes['html'] .= "<!-- " . $externalResource . " -->\n";
511                    }
512                }
513            }
514        }
515
516        $jsCode = $codes['javascript'];
517        $jsPanel = 0; // language for the js specific panel (0 = JavaScript)
518        if (array_key_exists('babel', $codes)) {
519            $jsCode = $codes['babel'];
520            $jsPanel = 3; // 3 = Babel
521        }
522
523        // Title and description
524        global $ID;
525        $title = $attributes['name'];
526        $pageTitle = tpl_pagetitle($ID, true);
527        if (!$title) {
528
529            $title = "Code from " . $pageTitle;
530        }
531        $description = "Code from the page '" . $pageTitle . "' \n" . wl($ID, $absolute = true);
532        return '<form  method="post" action="' . $postURL . '" target="_blank">' .
533            '<input type="hidden" name="title" value="' . htmlentities($title) . '">' .
534            '<input type="hidden" name="description" value="' . htmlentities($description) . '">' .
535            '<input type="hidden" name="css" value="' . htmlentities($codes['css']) . '">' .
536            '<input type="hidden" name="html" value="' . htmlentities("<!-- The HTML -->" . $codes['html']) . '">' .
537            '<input type="hidden" name="js" value="' . htmlentities($jsCode) . '">' .
538            '<input type="hidden" name="panel_js" value="' . htmlentities($jsPanel) . '">' .
539            '<input type="hidden" name="wrap" value="b">' .  //javascript no wrap in body
540            $externalResourcesInput .
541            '<button>Try the code</button>' .
542            '</form>';
543
544    }
545
546    /**
547     * @param $codes the array containing the codes
548     * @param $attributes the attributes of a call (for now the externalResources)
549     * @return string the HTML form code
550     */
551    public function addCodePenButton($codes, $attributes)
552    {
553        // TODO
554        // http://blog.codepen.io/documentation/api/prefill/
555    }
556
557
558}
559