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