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 <<