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