Enable data URIs for libraries (#81)

pull/91/head v1.2.0
Nicholas Bollweg 2022-01-24 08:02:54 -06:00 zatwierdzone przez GitHub
rodzic ea4f7b88ea
commit 82ab297197
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
10 zmienionych plików z 370 dodań i 8 usunięć

Wyświetl plik

@ -14,13 +14,14 @@
#### @deathbeds/ipydrawio 1.2.0
- adds support for data URI-encoded shape libraries (incompatible with `stealth`) [#80]
- fixes selection in widget diagrams with more than 10 shapes [#85]
#### @deathbeds/ipydrawio-notebook 1.2.0
#### @deathbeds/ipydrawio-webpack 16.4.0
#### @deathbeds/ipydrawio-webpack 16.4.500
- updates to drawio v16.4.0
- updates to drawio v16.4.5
#### @deathbeds/ipydrawio-jupyter-templates 1.2.0
@ -31,6 +32,7 @@
#### @deathbeds/ipydrawio-pdf 1.2.0
[#63]: https://github.com/deathbeds/ipydrawio/issues/63
[#80]: https://github.com/deathbeds/ipydrawio/issues/80
[#85]: https://github.com/deathbeds/ipydrawio/issues/85
[#88]: https://github.com/deathbeds/ipydrawio/issues/88

Wyświetl plik

@ -97,3 +97,6 @@ ${XP DIO PAGE SIZE} //div[contains(@class, "geFormatSection")][contains(., "P
${MIME STDERR} application/vnd.jupyter.stderr
# retro
${CSS RETRO TREE DIO BTN} css:button[title='${CREATE A BLANK}']
# docs
${TUTORIALS} ${ROOT}${/}docs${/}tutorials
${CLIB TUTORIAL} ${TUTORIALS}${/}working-with-custom-libraries${/}index.ipynb

Wyświetl plik

@ -0,0 +1,51 @@
# Copyright 2022 ipydrawio contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
*** Settings ***
Documentation Do the tutorials work?
Resource ../_Keywords.robot
Library OperatingSystem
Force Tags component:tutorials
Suite Setup Set Screenshot Directory ${OUTPUT DIR}${/}screenshots${/}tutorials
*** Variables ***
${XP MY LIBRARY TITLE} xpath://a[starts-with(@title, 'my+library')]
${XP MY LIBRARY SHAPES} ${XP MY LIBRARY TITLE}/following-sibling::div[1]//a[contains(@class, 'geItem')]
${XP SHAPE TOOLTIP} xpath://*[contains(@class, 'geSidebarTooltip')]
@{SHAPE TITLES} Exit Machine Queue Source
*** Test Cases ***
Custom Library URL Hack
[Documentation] Does using a custom library work?
[Tags] component:widget
Copy File ${CLIB TUTORIAL} ${HOME}${/}clib.ipynb
Wait Until Keyword Succeeds 5x 5s Open clib.ipynb in ${MENU NOTEBOOK}
Lab Command Restart Kernel and Run All Cells
Accept Default Dialog Option
Click Element css:.jp-Cell:last-child
Wait Until Keyword Succeeds 5x 5s Wait for a Diagram to be Ready
Wait Until Page Contains Element ${XP MY LIBRARY TITLE}
${shapes} = Get WebElements ${XP MY LIBRARY SHAPES}
Should be Equal as Integers ${shapes.__len__()} 4
FOR ${idx} ${shape} IN ENUMERATE ${shapes}
Mouse Over ${shape}
Wait Until Page Contains Element
... ${XP SHAPE TOOLTIP}\[contains(., '${SHAPE TITLES[${idx}]}')]
END
[Teardown] Tear Down Custom Library Tutorial
*** Keywords ***
Tear Down Custom Library Tutorial
Unselect Frame
Remove File ${HOME}${/}clib.ipynb

Wyświetl plik

@ -3,6 +3,7 @@
```{toctree}
:maxdepth: 2
designing-jupyter-extensions/index
working-with-custom-libraries/index
```
<!--

Wyświetl plik

@ -0,0 +1,300 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "e9738510-4036-46e9-b728-4590dbd2c738",
"metadata": {},
"source": [
"# Working with Custom Shape Libraries\n",
"\n",
"When working with the `ipydrawio` TypeScript API, custom shape libraries can be added by providing a fully-resolved, absolute URL to an `.xml` file in `clibs`. \n",
"\n",
"From the [Widget API](../../Diagram%20Widget.ipynb), this is somewhat more complicated, but might be worth it for certain custom cases such as [issue #80](https://github.com/deathbeds/ipydrawio/issues/80), where a fully-kernel-driven solution is desirable, despite the _gotchas_ (see below)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ff64bdc4-2dd5-4738-affc-36dab7c82584",
"metadata": {},
"outputs": [],
"source": [
"import base64, json, urllib.parse, zlib\n",
"import ipydrawio"
]
},
{
"cell_type": "markdown",
"id": "6f54b3ce-a88a-4a89-bebb-3074ad6dfee4",
"metadata": {},
"source": [
"## A Library\n",
"A library is, at its core, a list of shape descriptions. The best way to learn more about these is investigating the [drawio documentation](https://www.diagrams.net/blog/custom-libraries), which covers building them interactively. When done, you'd end up with some data like this."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8c582ec1-23ca-4e76-8733-cf8d8d7d8b46",
"metadata": {},
"outputs": [],
"source": [
"library = [\n",
" {\n",
" \"w\": 80,\n",
" \"h\": 80,\n",
" \"aspect\": \"fixed\",\n",
" \"title\": \"Source\",\n",
" \"xml\": '<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"\" type=\"Source\" interArrivalTime=\"\" id=\"2\"><mxCell style=\"rhombus;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>',\n",
" },\n",
" {\n",
" \"w\": 80,\n",
" \"h\": 80,\n",
" \"aspect\": \"fixed\",\n",
" \"title\": \"Queue\",\n",
" \"xml\": '<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"\" type=\"Queue\" capacity=\"\" id=\"2\"><mxCell style=\"ellipse;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>',\n",
" },\n",
" {\n",
" \"w\": 80,\n",
" \"h\": 80,\n",
" \"aspect\": \"fixed\",\n",
" \"title\": \"Machine\",\n",
" \"xml\": '<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"\" type=\"Machine\" processingTime=\"\" id=\"2\"><mxCell style=\"whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>',\n",
" },\n",
" {\n",
" \"w\": 80,\n",
" \"h\": 80,\n",
" \"aspect\": \"fixed\",\n",
" \"title\": \"Exit\",\n",
" \"xml\": '<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"\" type=\"Exit\" id=\"2\"><mxCell style=\"rhombus;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>',\n",
" },\n",
"]\n"
]
},
{
"cell_type": "markdown",
"id": "708fb970-bade-40a2-a495-b861ebadfbd3",
"metadata": {},
"source": [
"## JSON Encoding"
]
},
{
"cell_type": "markdown",
"id": "3c5665cf-9a04-4f28-b27d-7bd9cf880b2a",
"metadata": {},
"source": [
"Each element in the list has an `xml` attribute which is, as it suggests, XML, and must be carefully escaped. Because this will go through a URL parser, an XML parser, a JSON parser, and then another XML parser, the recommended approach is to use drawio's semi-convoluted base64/zlib technique."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3e9a519c-a868-4d71-885e-fcafe41915fa",
"metadata": {},
"outputs": [],
"source": [
"zlib_opts = dict(wbits=-15)\n",
"\n",
"def inflate(deflated):\n",
" infl = zlib.decompressobj(**zlib_opts)\n",
" return urllib.parse.unquote(infl.decompress(base64.b64decode(deflated)) + infl.flush())\n",
"\n",
"def deflate(inflated):\n",
" defl = zlib.compressobj(**zlib_opts)\n",
" return base64.b64encode(defl.compress(urllib.parse.quote(inflated).encode(\"utf-8\")) + defl.flush()).decode('utf-8')"
]
},
{
"cell_type": "markdown",
"id": "0a33277f-abd0-4b97-a803-ad2ed82a6222",
"metadata": {},
"source": [
"Again, due to the number of parsers involved, it's best to avoid extra spaces in the data URI."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "63445943-ecea-441e-87f5-7d2f361b7c4d",
"metadata": {},
"outputs": [],
"source": [
"library_json = json.dumps(\n",
" [dict(shape.items(), xml=deflate(shape[\"xml\"])) for shape in library],\n",
" separators=(\",\", \":\")\n",
")\n",
"library_json"
]
},
{
"cell_type": "markdown",
"id": "ad6be53d-0d24-4e5e-99b4-33c9aaf531de",
"metadata": {},
"source": [
"## XML Encoding\n",
"\n",
"The JSON is wrapped inside an XML document, with a top-level tag of `mxlibrary`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "82d193a9-2366-4df4-9263-84b65fa96c68",
"metadata": {},
"outputs": [],
"source": [
"library_xml = f\"\"\"<mxlibrary>{library_json}</mxlibrary><!-- /my library -->\"\"\"\n",
"library_xml"
]
},
{
"cell_type": "markdown",
"id": "76502de9-f2cb-4485-a438-acb6a06a8ca5",
"metadata": {},
"source": [
"This, in turn, must be transformed into a [Data URI](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). The whole thing can't be `base64` encoded, because drawio expects a semicolon-separated list of ids. \n",
"\n",
"> **GOTCHA**: Use of data URIs relies on a **NASTY PATCH** applied when packaging `@deathbeds/jupyterlab-drawio-webpack`: by default, the upstream would rewrite this into a proxied request, which `ipydrawio` don't support. Usually, the name of the library will be derived from the filename, which is usually the last path component after the `/` ... in this case, the _whole document_ is the path, so we make do with some hacks."
]
},
{
"cell_type": "markdown",
"id": "bcd588f0-100e-4dd9-96a7-ea0b16bc6a0f",
"metadata": {},
"source": [
"## URL Encoding"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3f062362-8691-42d9-b9d7-f4a29cf6c405",
"metadata": {},
"outputs": [],
"source": [
"library_data_uri = f\"data:application/xml,{library_xml}\""
]
},
{
"cell_type": "markdown",
"id": "c59eeed7-8599-429e-a869-3964b4709964",
"metadata": {},
"source": []
},
{
"cell_type": "markdown",
"id": "ca0d0acc-7186-4f81-83cf-fd93da83f59c",
"metadata": {},
"source": [
"## URL Params\n",
"Finally, the most reliable means of communicating with drawio is via its [URL parameters](https://www.diagrams.net/doc/faq/supported-url-parameters), exposed on the widget as `url_params`\n",
"\n",
"> **GOTCHA** `url_params` should be set before the widget is displayed to avoid extra dialogs. \n",
"\n",
"The `clibs` parameters accepts a list of \"library keys,\" each with different formats. We are interest in `U` (for `URL`) library.\n",
"\n",
"> Some others that might be worth exploring some time include `L` for `Local`, which works with an `IndexedDB` instance... but is not guaranteed to be configured by the time a document loads.\n",
"\n",
"Note, we also override the `stealth` default... `stealth` isn't _strictly_ going to worsen the privacy posture, as all of the other providers are still disabled."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5a89ad37-caf4-4174-90f0-ba51f7c98c3b",
"metadata": {},
"outputs": [],
"source": [
"url_params = dict(ipydrawio.Diagram._default_url_params(None))\n",
"url_params.update(clibs=f\"U{library_data_uri}\", stealth=\"0\",)\n",
"url_params"
]
},
{
"cell_type": "markdown",
"id": "40668d7d-0d2d-4727-add8-ef8d494f0b23",
"metadata": {},
"source": [
"### More URL Params\n",
"\n",
"A number of other parameters can be useful for custom embedding purposes, such as using a `min`imal `ui`, hiding the default `libs`, disabling additional `p`lugins."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "938af1fe-a2b5-42a0-beee-c93d910b7dfd",
"metadata": {},
"outputs": [],
"source": [
"url_params.update(ui=\"min\", libs=\"0\", p=\"\")"
]
},
{
"cell_type": "markdown",
"id": "7b7169ce-668f-426c-8146-1c27aa4ef66d",
"metadata": {},
"source": [
"## The Widget"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "91835331-2f68-48af-9ca1-c2512f25ded3",
"metadata": {},
"outputs": [],
"source": [
"d = ipydrawio.Diagram(url_params=url_params, layout=dict(height=\"800px\"))\n",
"d"
]
},
{
"cell_type": "markdown",
"id": "bbb0e474-649d-4e33-96c3-5f4f3bad56bd",
"metadata": {},
"source": [
"## Use The Source\n",
"\n",
"We should now be able to use the desired shapes in the diagram. Unlike `url_params`, the `value` of the diagram's `source` can be updated immediately."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5daa299b-08d8-4ad2-b35e-d599157612e4",
"metadata": {},
"outputs": [],
"source": [
"d.source.value = '''<mxfile version=\"15.8.7\" type=\"embed\">\n",
" <diagram id=\"x\" name=\"My Diagram\">\n",
" <mxGraphModel dx=\"1687\" dy=\"681\" grid=\"1\" gridSize=\"10\" guides=\"1\" tooltips=\"1\" connect=\"1\" arrows=\"1\" fold=\"1\" page=\"1\" pageScale=\"1\" pageWidth=\"850\" pageHeight=\"1100\" math=\"0\" shadow=\"0\"><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"apple\" type=\"Source\" interArrivalTime=\"\" id=\"2\"><mxCell style=\"rhombus;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry x=\"100\" y=\"90\" width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object><object label=\"banana\" type=\"Queue\" capacity=\"\" id=\"3\"><mxCell style=\"ellipse;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry x=\"250\" y=\"90\" width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object><object label=\"cherry\" type=\"Machine\" processingTime=\"\" id=\"4\"><mxCell style=\"whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry x=\"385\" y=\"90\" width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object><object label=\"date\" type=\"Exit\" id=\"5\"><mxCell style=\"rhombus;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry x=\"510\" y=\"90\" width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object></root>\n",
" </mxGraphModel>\n",
" </diagram>\n",
"</mxfile>'''"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

@ -1 +1 @@
Subproject commit bbf17d5f5a1d73cb6c955b579cd3691fc55ebae7
Subproject commit bdfb2a3944aaef1a9f95097a6c30d98fbefce1c4

Wyświetl plik

@ -43,5 +43,5 @@
"build:pre": "python scripts/patch.py && python scripts/static.py"
},
"types": "lib/index.d.ts",
"version": "16.4.0"
"version": "16.4.500"
}

Wyświetl plik

@ -29,7 +29,12 @@ PATCHES = {
"name": "global ref so we can get at the App at runtime",
"before": "new App(new Editor",
"after": "window.IPYDRAWIO_APP = new App(new Editor",
}
},
{
"name": "patch cors check for data URI",
"before": "this.editor.isCorsEnabledForUrl(n)||(n=",
"after": "(n.match(/^data:/) || this.editor.isCorsEnabledForUrl(n))||(n=",
},
]
}

Wyświetl plik

@ -8,7 +8,7 @@
"@jupyterlab/application": "^3.1.0",
"@jupyterlab/launcher": "^3.1.0",
"@jupyterlab/mainmenu": "^3.1.0",
"@deathbeds/ipydrawio-webpack": "^16.4.0"
"@deathbeds/ipydrawio-webpack": "^16.4.500"
},
"description": "A JupyterLab extension for embedding interactive drawio / mxgraph diagrams.",
"devDependencies": {

Wyświetl plik

@ -106,14 +106,14 @@
"@jupyterlab/application" "^3.1.0"
"@deathbeds/ipydrawio-webpack@file:packages/ipydrawio-webpack":
version "16.4.0"
version "16.4.500"
dependencies:
"@jupyterlab/application" "^3.1.0"
"@deathbeds/ipydrawio@file:packages/ipydrawio":
version "1.2.0"
dependencies:
"@deathbeds/ipydrawio-webpack" "^16.4.0"
"@deathbeds/ipydrawio-webpack" "^16.4.500"
"@jupyterlab/application" "^3.1.0"
"@jupyterlab/launcher" "^3.1.0"
"@jupyterlab/mainmenu" "^3.1.0"