xref: /plugin/bpmnio/syntax/bpmnio.php (revision 738a44ff661500d783f23960888bd3ae2818f44c)
1<?php
2
3use dokuwiki\Extension\SyntaxPlugin;
4use dokuwiki\File\MediaResolver;
5
6/**
7 * @license    See LICENSE file
8 */
9// See help: https://www.dokuwiki.org/devel:syntax_plugins
10// The HTML structure generated by this syntax plugin is:
11//
12// <div class="plugin-bpmnio" id="__(bpmn|dmn)_js_<hash>">
13//   <div class="bpmn_js_data">
14//      ... base64 encoded xml
15//   </div>
16//   <div class="bpmn_js_canvas {$class}">
17//     <div class="bpmn_js_container">... rendered herein</div>
18//   </div>
19// </div>
20class syntax_plugin_bpmnio_bpmnio extends SyntaxPlugin
21{
22    protected string $type = ''; // 'bpmn' or 'dmn'
23    protected string $src = ''; // media file
24    protected string $zoom = ''; // optional scaling factor
25    protected string $lint = ''; // optional bpmnlint mode: on|off|inactive
26
27    private function loadLinkProcessor(): void
28    {
29        require_once __DIR__ . '/../inc/link_processor.php';
30    }
31
32    public function getPType(): string
33    {
34        return 'block';
35    }
36
37    public function getType(): string
38    {
39        return 'protected';
40    }
41
42    public function getSort(): int
43    {
44        return 0;
45    }
46
47    public function connectTo($mode): void
48    {
49        $this->Lexer->addEntryPattern('<bpmnio.*?>(?=.*?</bpmnio>)', $mode, 'plugin_bpmnio_bpmnio');
50    }
51
52    public function postConnect(): void
53    {
54        $this->Lexer->addExitPattern('</bpmnio>', 'plugin_bpmnio_bpmnio');
55    }
56
57    public function handle($match, $state, $pos, Doku_Handler $handler): array
58    {
59        switch ($state) {
60            case DOKU_LEXER_ENTER:
61                $matched = [];
62                preg_match('/<bpmnio\s+([^>]+)>/', $match, $matched);
63
64                $attrs = [];
65                if (!empty($matched[1])) {
66                    $attrs = $this->buildAttributes($matched[1]);
67                }
68
69                $this->type = $attrs['type'] ?? 'bpmn';
70                $this->src = $attrs['src'] ?? '';
71                $this->zoom = $this->normalizeZoom($attrs['zoom'] ?? null) ?? '';
72                $this->lint = $this->resolveLint($attrs['lint'] ?? null, $this->type);
73
74                return [$state, $this->type, '', $pos, '', false, $this->zoom, $this->lint];
75
76            case DOKU_LEXER_UNMATCHED:
77                $posStart = $pos;
78                $posEnd = $pos + strlen($match);
79
80                $inline = empty($this->src);
81                if (!$inline) {
82                    $match = $this->getMedia($this->src);
83                }
84                return [
85                    $state, $this->type, base64_encode(trim($match)),
86                    $posStart, $posEnd, $inline, $this->zoom, $this->lint,
87                ];
88
89            case DOKU_LEXER_EXIT:
90                $this->type = '';
91                $this->src = '';
92                $this->zoom = '';
93                $this->lint = '';
94                return [$state, '', '', '', '', false, '', ''];
95        }
96        return [];
97    }
98
99    private function buildAttributes($string)
100    {
101        $attrs = [];
102        preg_match_all('/(\w+)=["\'](.*?)["\']/', $string, $matches, PREG_SET_ORDER);
103        foreach ($matches as $match) {
104            $attrs[$match[1]] = $match[2];
105        }
106        return $attrs;
107    }
108
109    private function normalizeZoom($zoom): ?string
110    {
111        if ($zoom === null || $zoom === '') {
112            return null;
113        }
114
115        if (!is_numeric($zoom)) {
116            return null;
117        }
118
119        $zoom = (float) $zoom;
120        if ($zoom <= 0) {
121            return null;
122        }
123
124        return rtrim(rtrim(number_format($zoom, 4, '.', ''), '0'), '.');
125    }
126
127    /**
128     * Resolve the effective bpmnlint mode for the diagram.
129     *
130     * Per-diagram `lint` attribute wins when valid; otherwise the global plugin
131     * setting is used. Linting only applies to BPMN diagrams; for any other
132     * type the result is an empty string and no data-lint attribute is emitted.
133     *
134     * @param mixed  $lint Raw attribute value (string|null|other)
135     * @param string $type Diagram type (`bpmn` or `dmn`)
136     */
137    private function resolveLint($lint, string $type): string
138    {
139        if ($type !== 'bpmn') {
140            return '';
141        }
142
143        $allowed = ['on', 'off', 'inactive'];
144
145        if (is_string($lint)) {
146            $normalized = strtolower(trim($lint));
147            if (in_array($normalized, $allowed, true)) {
148                return $normalized;
149            }
150        }
151
152        $default = strtolower((string) $this->getConf('lint'));
153        return in_array($default, $allowed, true) ? $default : 'off';
154    }
155
156    private function getMedia($src)
157    {
158        global $ID;
159
160        $id = (new MediaResolver($ID))->resolveId($src);
161        if (auth_quickaclcheck($id) < AUTH_READ) {
162            return "Error: Access denied for file $src";
163        }
164
165        $file = mediaFN($id);
166        if (!file_exists($file) || !is_readable($file)) {
167            return "Error: Cannot load file $src";
168        }
169
170        return file_get_contents($file);
171    }
172
173    public function render($mode, Doku_Renderer $renderer, $data): bool
174    {
175        [$state, $type, $match, $posStart, $posEnd, $inline, $zoom, $lint] = array_pad($data, 8, '');
176
177        if (is_a($renderer, 'renderer_plugin_dw2pdf')) {
178            if ($state == DOKU_LEXER_EXIT) {
179                $renderer->doc .= <<<HTML
180                    <div class="plugin-bpmnio">
181                        <a href="https://github.com/Color-Of-Code/dokuwiki-plugin-bpmnio/issues/4">
182                            DW2PDF support missing: Help wanted
183                        </a>
184                    </div>
185                    HTML;
186            }
187            return true;
188        }
189
190        if ($mode == 'xhtml' || $mode == 'odt') {
191            switch ($state) {
192                case DOKU_LEXER_ENTER:
193                    $bpmnid = "__{$type}_js_{$posStart}";
194                    $renderer->doc .= <<<HTML
195                        <div class="plugin-bpmnio" id="{$bpmnid}">
196                        HTML;
197                    break;
198
199                case DOKU_LEXER_UNMATCHED:
200                    $xml = base64_decode($match, true);
201                    if ($xml === false) {
202                        $xml = $match;
203                    }
204
205                    $this->loadLinkProcessor();
206                    $payload = plugin_bpmnio_link_processor::buildPayload($xml);
207                    $encodedXml = base64_encode($payload['xml']);
208                    $encodedLinks = base64_encode(json_encode($payload['links']));
209                    $renderer->doc .= <<<HTML
210                        <div class="{$type}_js_data">
211                            {$encodedXml}
212                        </div>
213                        <div class="{$type}_js_links">
214                            {$encodedLinks}
215                        </div>
216                        HTML;
217                    if ($inline) {
218                        $target = "plugin_bpmnio_{$type}";
219                        $sectionEditData = ['target' => $target];
220                        $class = $renderer->startSectionEdit($posStart, $sectionEditData);
221                    } else {
222                        $class = '';
223                    }
224                    $zoomAttr = $zoom !== '' ? " data-zoom=\"{$zoom}\"" : '';
225                    $lintAttr = $lint !== '' ? " data-lint=\"{$lint}\"" : '';
226                    $renderer->doc .= <<<HTML
227                        <div class="{$type}_js_canvas {$class}">
228                            <div class="{$type}_js_container"{$zoomAttr}{$lintAttr}></div>
229                        </div>
230                        HTML;
231                    if ($inline) {
232                        $renderer->finishSectionEdit($posEnd);
233                    }
234                    break;
235
236                case DOKU_LEXER_EXIT:
237                    $renderer->doc .= <<<HTML
238                        </div>
239                        HTML;
240                    break;
241            }
242            return true;
243        }
244        return false;
245    }
246}
247