diff --git a/rsrc/remarkup-image.css b/rsrc/remarkup-image.css --- a/rsrc/remarkup-image.css +++ b/rsrc/remarkup-image.css @@ -2,7 +2,62 @@ * @provides diagram-remarkup-image-css */ -.diagram-container > .diagram-content { +.diagram-content { + display: flex; +} + +.diagram-content.diagram-center { + justify-content: center; +} + +.diagram-content.diagram-right { + justify-content: right; +} + +.diagram-content.diagram-float-left { + float: left; + margin-right: 1em; max-width: 100%; - cursor: pointer; +} + +.diagram-content.diagram-float-right { + float: right; + margin-left: 1em; + max-width: 100%; +} + +.diagram-content > .diagram-container { + max-width: 100%; + overflow: hidden !important; +} + +.diagram-content > .diagram-container.full { + flex-grow: 1; +} + +/* GraphViewer is only rendering when container has width */ +.diagram-content > .diagram-container:empty { + min-width: 1px; +} + +/* Fixing images in GraphViewer lightbox toolbar */ +.geDiagramContainer + .geAdaptiveAsset + div > span > img { + display: initial; +} + +/* Fixing centering of button in GraphViewer toolbar */ +body > div[style*="align-items:"]:not([class]) { + line-height: initial; + display: flex; +} + +/* reset some styles for correct svg rendering */ + +.diagram-content > .diagram-container, .geDiagramContainer { + line-height: initial; +} + +.diagram-content > .diagram-container td, +.geDiagramContainer td { + padding: revert-layer; } diff --git a/rsrc/remarkup-image.js b/rsrc/remarkup-image.js --- a/rsrc/remarkup-image.js +++ b/rsrc/remarkup-image.js @@ -4,24 +4,58 @@ JX.onload(function() { - var singleClickTimeout = null; - - JX.Stratcom.listen( - 'click', - ['diagram-remarkup-image'], - function(evt) { - var detail = evt.getRawEvent().detail; - - if (detail === 1) { - singleClickTimeout = window.setTimeout(function() { - window.open('/diagram/data/' + evt.getTarget().dataset.diagramVersion); - singleClickTimeout = null; - }, 300); - } else if (detail === 2) { - window.clearTimeout(singleClickTimeout); - window.open('/diagram/DIAG' + evt.getTarget().dataset.diagramId); + var viewerScriptURI = 'https://viewer.diagrams.net/js/viewer-static.min.js'; + var viewerScriptPrepended = false; + var viewerScriptBlocked = false; + + document.addEventListener('securitypolicyviolation', function(e) { + if (viewerScriptURI === e.blockedURI) { + viewerScriptBlocked = true; + processElements(); + } + }); + + function processViewer(viewer) { + viewer.toolbar.appendChild( + viewer.createToolbarButton( + function() { window.open(viewer.graphConfig.edit); }, + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMThweCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMThweCIgZmlsbD0iIzAwMDAwMCI+PHBhdGggZD0iTTAgMGgyNHYyNEgwVjB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTE0LjA2IDkuMDJsLjkyLjkyTDUuOTIgMTlINXYtLjkybDkuMDYtOS4wNk0xNy42NiAzYy0uMjUgMC0uNTEuMS0uNy4yOWwtMS44MyAxLjgzIDMuNzUgMy43NSAxLjgzLTEuODNjLjM5LS4zOS4zOS0xLjAyIDAtMS40MWwtMi4zNC0yLjM0Yy0uMi0uMi0uNDUtLjI5LS43MS0uMjl6bS0zLjYgMy4xOUwzIDE3LjI1VjIxaDMuNzVMMTcuODEgOS45NGwtMy43NS0zLjc1eiIvPjwvc3ZnPg==', + null, + true + ) + ); + } + + function processElements() { + console.log('processElements: ', document.querySelectorAll('.diagram-container:empty')); + document.querySelectorAll('.diagram-container:empty').forEach(function(container) { + if (window.GraphViewer) { + GraphViewer.createViewerForElement(container, processViewer); + } else if (viewerScriptBlocked) { + container.innerHTML = 'Diagram Viewer could not be loaded.'; } + }); + } + + function init() { + if (!document.querySelector('.diagram-container')) { + return; } - ); + if (viewerScriptPrepended || viewerScriptBlocked) { + processElements(); + return; + } + + var viewerScript = document.createElement('script'); + viewerScript.setAttribute('src', viewerScriptURI); + viewerScript.addEventListener('load', processElements); + + document.body.prepend(viewerScript); + + viewerScriptPrepended = true; + } + + init(); + JX.Request.listen('done', function() { setTimeout(init); }); }); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -26,6 +26,7 @@ 'DiagramTransactionType' => 'xaction/DiagramTransactionType.php', 'DiagramUploadConduitAPIMethod' => 'conduit/DiagramUploadConduitAPIMethod.php', 'DiagramVersion' => 'storage/DiagramVersion.php', + 'DrawioPngParser' => 'parser/DrawioPngParser.php', 'PhabricatorDiagramQuery' => 'query/PhabricatorDiagramQuery.php', 'PhabricatorDiagramTransactionQuery' => 'query/PhabricatorDiagramTransactionQuery.php', 'PhabricatorDiagramVersionQuery' => 'query/PhabricatorDiagramVersionQuery.php', @@ -61,6 +62,7 @@ 'PhabricatorDestructibleInterface', 'PhabricatorPolicyInterface', ), + 'DrawioPngParser' => 'Phobject', 'PhabricatorDiagramQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorDiagramTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorDiagramVersionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', diff --git a/src/application/DiagramApplication.php b/src/application/DiagramApplication.php --- a/src/application/DiagramApplication.php +++ b/src/application/DiagramApplication.php @@ -1,6 +1,11 @@ addContentSecurityPolicyURI('script-src', 'https://viewer.diagrams.net'); + } + public function getName() { return pht('Diagrams'); } diff --git a/src/celerity/map.php b/src/celerity/map.php --- a/src/celerity/map.php +++ b/src/celerity/map.php @@ -11,15 +11,15 @@ 'diagram-extension.css' => '7ad39d5d', 'iframe-toolbtn.css' => '35ad6f49', 'iframe-toolbtn.js' => '26d75a35', - 'remarkup-image.css' => '42b46bf1', - 'remarkup-image.js' => '64e7e9e1', + 'remarkup-image.css' => '51ddb637', + 'remarkup-image.js' => '842accb1', ), 'symbols' => array( 'diagram-css-extension' => '7ad39d5d', 'diagram-css-iframe-toolbtn' => '35ad6f49', 'diagram-js-iframe-toolbtn' => '26d75a35', - 'diagram-remarkup-image-css' => '42b46bf1', - 'diagram-remarkup-image-js' => '64e7e9e1', + 'diagram-remarkup-image-css' => '51ddb637', + 'diagram-remarkup-image-js' => '842accb1', 'javelin-behavior-diagram-extension' => 'a0b36dca', ), 'requires' => array(), diff --git a/src/controller/DiagramController.php b/src/controller/DiagramController.php --- a/src/controller/DiagramController.php +++ b/src/controller/DiagramController.php @@ -243,53 +243,19 @@ * They are cut out before the 2 strings are compared. */ public static function equalPngMetaData($base64_1, $base64_2) { - $base64 = array($base64_1, $base64_2); - $textData = array(); - for ($i = 0; $i < 2; $i++) { - $data = base64_decode($base64[$i]); - $fp = fopen('data://text/plain;base64,' . base64_encode($data), 'rb'); - $sig = fread($fp, 8); - if ($sig != "\x89PNG\x0d\x0a\x1a\x0a") { - fclose($fp); - return false; - } - $textData[$i] = array(); - while (!feof($fp)) { - try { - $chunk = unpack('Nlength/a4type', fread($fp, 8)); - } catch (Exception $e) { - // invalid base64 data - return false; - } - if ($chunk['type'] == 'IEND') break; - if ($chunk['type'] == 'tEXt') { - list($key, $val) = explode("\0", fread($fp, $chunk['length'])); - if ($key == 'mxfile') { - // Decode the URL-encoded XML data - $decodedVal = urldecode($val); - // Load the XML and remove the modified and etag attributes - $xml = simplexml_load_string($decodedVal); - unset($xml->attributes()->modified); - unset($xml->attributes()->etag); - // Save the modified XML as the value - $val = $xml->asXML(); - } - $textData[$i][$key] = $val; - fseek($fp, 4, SEEK_CUR); - } else { - fseek($fp, $chunk['length'] + 4, SEEK_CUR); - } - } - fclose($fp); - } + $mxfile1 = DrawioPngParser::getMxfile($base64_1); + $mxfile2 = DrawioPngParser::getMxfile($base64_2); - if (isset($textData[0]['mxfile']) && isset($textData[1]['mxfile'])) { - // Both arrays contain the mxfile key, compare their values - return $textData[0]['mxfile'] == $textData[1]['mxfile']; - } else { - // At least one of the arrays doesn't contain mxfile key, return false + if ($mxfile1 === null || $mxfile2 === null) { return false; } + + unset($mxfile1->attributes()->modified); + unset($mxfile1->attributes()->etag); + unset($mxfile2->attributes()->modified); + unset($mxfile2->attributes()->etag); + + return $mxfile1->asXML() == $mxfile2->asXML(); } /** diff --git a/src/parser/DrawioPngParser.php b/src/parser/DrawioPngParser.php new file mode 100644 --- /dev/null +++ b/src/parser/DrawioPngParser.php @@ -0,0 +1,38 @@ +getEngine()->getConfig('viewer'); @@ -31,70 +27,66 @@ PhabricatorObjectHandle $handle, $options) { - if ($options) { - $params = explode(',', $options); - $params = array_map('trim', $params); - } else { - $params = array(); + $options = $this->getOptions($options); + $fullSize = !!$options['full']; + $contentClass = 'diagram-content'; + $contentStyle = ''; + + switch ($options['layout'] && !$fullSize) { + case 'right': + case 'center': + $contentClass .= ' diagram-' . $options['layout']; + break; + case 'left': + default: + $contentClass .= ' diagram-left'; + break; } - // Generate the appropriate HTML using the data from the Diagram and - // file objects. - $style = ''; - $class = 'diagram-content'; - $alt = ''; - - $has_layout = false; - foreach ($params as $param) { - if (strpos($param, '=') !== false) { - list($key, $value) = explode('=', $param, 2); - } else { - $key = $param; - $value = null; - } - switch ($key) { - case 'layout': - $has_layout = true; - if ($value === 'left') { - $class .= ' phabricator-remarkup-embed-layout-left'; - } else if ($value === 'right') { - $class .= ' phabricator-remarkup-embed-layout-right'; - } + if ($options['float'] && !$fullSize) { + switch ($options['layout']) { + case 'right': + $contentClass .= ' diagram-float-right'; break; - case 'float': - $class .= ' phabricator-remarkup-embed-float-left'; - break; - case 'size': - if ($value === 'full') { - $style .= 'width: 100%;'; - } - break; - case 'alt': - $alt = phutil_escape_html($value); + case 'left': + default: + $contentClass .= ' diagram-float-left'; break; } } - if ($has_layout == false) { - $class .= ' phabricator-remarkup-embed-layout-left'; + $width = $options['width']; + if ($width && !$fullSize && preg_match('/^(?:\d*\\.)?\d+(%|px)?$/', $width)) { + if (is_numeric($width)) { + $contentStyle = 'width: ' . $width . 'px;'; + } else { + $contentStyle = 'width: ' . $width . ';'; + } } + $mxGraphConfig = array( + 'responsive' => $fullSize, + 'resize' => true, + 'editable' => true, + 'edit' => PhabricatorEnv::getURI('/diagram/DIAG' . $diagram->getDiagramID()), + 'lightbox' => true, + 'toolbar' => 'pages' . ($fullSize ? '' : ' zoom'), + 'toolbar-position' => 'inline', + 'page' => $options['page'], + 'xml' => DrawioPngParser::getMxfile($diagram->getBase64Data())->asXML() + ); + $output = phutil_tag( 'div', array( - 'class' => 'diagram-container', + 'class' => $contentClass, + 'style' => $contentStyle, ), phutil_tag( - 'img', + 'div', array( - 'style' => $style, - 'class' => $class, - 'src' => 'data:image/png;base64,' . $diagram->getBase64Data(), - 'alt' => $alt, - 'data-sigil' => 'diagram-remarkup-image', - 'data-diagram-version' => $diagram->getPHID(), - 'data-diagram-id' => $diagram->getDiagramID(), - 'title' => 'Double click to edit...' + 'class' => 'diagram-container' . ($fullSize ? ' full' : ''), + 'data-mxgraph' => json_encode($mxGraphConfig) ) ) ); @@ -102,6 +94,24 @@ return $output; } + private function getOptions($option_string) { + $options = array( + 'full' => false, + 'layout' => 'left', + 'float' => false, + 'width' => '', + 'page' => 0 + ); + + if ($option_string) { + $option_string = trim($option_string, ', '); + $parser = new PhutilSimpleOptions(); + $options = $parser->parse($option_string) + $options; + } + + return $options; + } + public function getDocumentation() { return <<