{"id":28397926,"url":"https://github.com/splintered-reality/py_trees_js","last_synced_at":"2025-10-12T05:33:24.058Z","repository":{"id":57458042,"uuid":"194470475","full_name":"splintered-reality/py_trees_js","owner":"splintered-reality","description":"Javascript library for behaviour tree visualisation, demo'd with a qt-js hybrid application","archived":false,"fork":false,"pushed_at":"2025-01-14T02:32:39.000Z","size":4298,"stargazers_count":18,"open_issues_count":10,"forks_count":4,"subscribers_count":5,"default_branch":"devel","last_synced_at":"2025-06-01T12:11:50.858Z","etag":null,"topics":["behaviour-trees","jointjs","pyqt5","qt-js-application"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/splintered-reality.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.rst","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2019-06-30T03:24:21.000Z","updated_at":"2025-05-26T01:06:11.000Z","dependencies_parsed_at":"2022-09-06T13:11:23.579Z","dependency_job_id":"df64a5e5-dfa9-4953-9ee9-9b4ea85aa059","html_url":"https://github.com/splintered-reality/py_trees_js","commit_stats":{"total_commits":121,"total_committers":1,"mean_commits":121.0,"dds":0.0,"last_synced_commit":"f74dc3b1ca0d1883f45f5a0c947defcbbdc23dbb"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/splintered-reality%2Fpy_trees_js","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/splintered-reality%2Fpy_trees_js/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/splintered-reality%2Fpy_trees_js/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/splintered-reality%2Fpy_trees_js/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/splintered-reality","download_url":"https://codeload.github.com/splintered-reality/py_trees_js/tar.gz/refs/heads/devel","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/splintered-reality%2Fpy_trees_js/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":258655647,"owners_count":22736643,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["behaviour-trees","jointjs","pyqt5","qt-js-application"],"created_at":"2025-06-01T03:09:38.932Z","updated_at":"2025-10-12T05:33:23.962Z","avatar_url":"https://github.com/splintered-reality.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PyTrees Js\n\n[[About](#about)] [[Features](#features)] [[Preview](#preview)] [[Exmaple - Simple Web App](#example---simple-web-app)] [[Example - PyQt App](#example---pyqt-app)] [[The JSON Specification](#the-json-specification)]\n\n## About\n\nLibraries for visualisation of runtime or replayed behaviour trees.\n\n* [./js](./js) - a self-contained javascript library to build apps around\n* [py_trees_js](./py_trees_js) - a python package that makes the js available as a pyqt resource\n* [py_trees_js.viewer](./py_trees_js/viewer) - a demo pyqtwebengine app\n\nSee [py_trees_ros_viewer](https://github.com/splintered-reality/py_trees_ros_viewer) for a fully fledged pyqt integration that uses [py_trees_js](./py_trees_js).\n\n## Features\n\n* Visualise the runtime state of a behaviour tree\n* Collapsible subtrees\n* Zoom and scale contents to fit\n* Timeline rewind \u0026 resume\n* Blackboard key-value storage view\n* Activity log view\n\nAlthough designed for py_trees, the js libs (in particular, the interfaces) are not dependent on py_trees and could be used for other behaviour tree applications.\n\n## Preview\n\nWith VSCode DevContainers and on a PC with an NVIDIA GPU:\n\n* Install [VSCode](https://code.visualstudio.com/docs/setup/linux#_debian-and-ubuntu-based-distributions) \n* Install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#docker)\n* Clone and launch the demo:\n\n```\n$ git clone https://github.com/splintered-reality/py_trees_js\n$ code .\n$ cd py_trees_js\n\n# Reopen the project in the devcontainer\n$ (use CTRL-SHIFT-P if you miss VSCode's helper dialog)\n\n# Setup and launch\n$ poetry install\n$ poetry run py-trees-demo-viewer\n```\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"images/splash.png\" width=\"80%\"/\u003e\n  \u003cimg src=\"images/screenshot.png\" width=\"100%\"/\u003e\n\u003c/p\u003e\n\nIf you do not have a PC that meets those requirements, some alternative options:\n\n* Install [Poetry](https://python-poetry.org/) and PyQt on your system or in a venv. Clone and launch.\n* If you're just interested in seeing the demo viewer, `pip install --user py_trees_js` and launch the viewer\n* Create your own devcontainer with something like the [desktop-lite](https://github.com/devcontainers/features/tree/main/src/desktop-lite) feature. If this works, send me a PR!\n\n## Example - Simple Web App\n\nBuilding a complete application that can render a behaviour tree stream is an effort that can be decomposed into two tasks:\n\n1. Creating the web app for rendering trees and visualising a timeline\n2. Wrapping the web app in a framework and connecting it to an external stream\n\nThe first stage is purely an exercise with html, css and javascript. The latter will depend on your use case - it could be a qt-js hybrid application (as exemplified here) for developers, an electron application for cross-platform and mobile deployment or a cloud based service.\n\nThis section will walk through how to build a web application with the provided js libraries. An example of wrapping the web app within a Qt-Js application will follow.\n\nTo get started, let's begin with a basic html page with two divs, one for the tree canvas and one for the timeline:\n\n```xhtml\n\u003c!doctype html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"utf-8\"\u003e\n  \u003ctitle\u003ePyTrees Viewer\u003c/title\u003e\n  \u003cstyle\u003e\n    html {\n      height: 100%  /* canvas is intended to fill the screen, cascading heights achieves this */\n    }\n    body {\n      margin: 0;\n      overflow:hidden;  /* no scrollbars */\n      height: 100%  /* canvas is intended to fill the screen, cascading heights achieves this */\n    }\n  \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cdiv id=\"canvas\"\u003e\u003c/div\u003e\n  \u003cdiv id=\"timeline\"\u003e\u003c/div\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\nNext, bring in the javascript libraries. For exemplar purposes, it is assumed here that the libraries\nhave been made available alongside the html page - how is an integration detail depending on the mode\nof deployment (see next section for an example).\n\nNote that the `py_trees-\u003cversion\u003e.js` library has only one dependency, [jointjs](https://resources.jointjs.com/docs/jointjs/v3.0/joint.html),\nbut that in turn has a few dependencies of it's own. The bundled libraries in the `js/jointjs` folder\nof this repository correspond to the requirements for a specific version of jointjs and\nhave been tested to work with the accompany `py_trees-\u003cversion\u003e.js` library.\n\nYou can verify that the libraries have been properly imported by calling `py_trees.hello()` which\nwill print version information of the loaded javascript libraries (if found) to the javascript console.\n\n```xhtml\n\u003c!doctype html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"utf-8\"\u003e\n  \u003ctitle\u003ePyTrees Viewer\u003c/title\u003e\n  \u003clink rel=\"stylesheet\" href=\"js/py_trees-0.6.0.css\"/\u003e\n  \u003clink rel=\"stylesheet\" type=\"text/css\" href=\"js/jointjs/joint-3.0.4.min.css\"/\u003e\n  \u003cscript src=\"js/jointjs/dagre-0.8.4.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/graphlib-2.1.7.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/jquery-3.4.1.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/lodash-4.17.11.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/backbone-1.4.0.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/joint-3.0.4.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/py_trees-0.6.0.js\"\u003e\u003c/script\u003e\n  \u003c!-- Web app integration css here --\u003e\n  \u003cstyle\u003e\n    html {\n      height: 100%  /* canvas is intended to fill the screen, cascading heights achieves this */\n    }\n    body {\n      margin: 0;\n      overflow:hidden;  /* no scrollbars */\n      height: 100%  /* canvas is intended to fill the screen, cascading heights achieves this */\n    }\n  \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cscript type=\"text/javascript\"\u003e\n    py_trees.hello()\n  \u003c/script\u003e\n  \u003cdiv id=\"window\"\u003e\n    \u003cdiv id=\"canvas\"\u003e\u003c/div\u003e\n    \u003cdiv id=\"timeline\"\u003e\u003c/div\u003e\n  \u003c/div\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\nOutput from `py_trees.hello()`:\n\n```\n********************************************************************************\n                               Py Trees JS\n\n A javascript library for visualisation of executing behaviour trees.\n\n Version \u0026 Dependency Info:\n  - py_trees:  0.6.0\n    - jointjs :  3.1.0\n       - backbone:  1.4.0\n       - dagre   :  0.8.4\n       - jquery  :  3.4.1\n       - lodash  :  4.17.11\n********************************************************************************\n```\n\nIn the next iteration, the canvas is initialised and a callback for\naccepting incoming trees from an external source is prepared. To test it,\npass it the demo tree provided by the library.\n\n```xhtml\n\u003c!doctype html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"utf-8\"\u003e\n  \u003ctitle\u003ePyTrees Viewer\u003c/title\u003e\n  \u003clink rel=\"stylesheet\" href=\"js/py_trees-0.6.0.css\"/\u003e\n  \u003clink rel=\"stylesheet\" type=\"text/css\" href=\"js/jointjs/joint-3.0.4.min.css\"/\u003e\n  \u003cscript src=\"js/jointjs/dagre-0.8.4.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/graphlib-2.1.7.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/jquery-3.4.1.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/lodash-4.17.11.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/backbone-1.4.0.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/joint-3.0.4.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/py_trees-0.6.0.js\"\u003e\u003c/script\u003e\n  \u003c!-- Web app integration css here --\u003e\n  \u003cstyle\u003e\n    html {\n      height: 100%  /* canvas is intended to fill the screen, cascading heights achieves this */\n    }\n    body {\n      margin: 0;\n      overflow:hidden;  /* no scrollbars */\n      height: 100%  /* canvas is intended to fill the screen, cascading heights achieves this */\n    }\n  \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cscript type=\"text/javascript\"\u003e\n    py_trees.hello()\n  \u003c/script\u003e\n  \u003cdiv id=\"window\"\u003e\n    \u003cdiv id=\"canvas\"\u003e\u003c/div\u003e\n    \u003cdiv id=\"timeline\"\u003e\u003c/div\u003e\n  \u003c/div\u003e\n  \u003cscript type=\"text/javascript\"\u003e\n    // rendering canvas\n    canvas_graph = py_trees.canvas.create_graph()\n    canvas_paper = py_trees.canvas.create_paper({graph: canvas_graph})\n\n    render_tree = function({tree}) {\n      py_trees.canvas.update_graph({graph: canvas_graph, tree: tree})\n      py_trees.canvas.layout_graph({graph: canvas_graph})\n      if ( canvas_graph.get('scale_content_to_fit') ) {\n        py_trees.canvas.scale_content_to_fit(canvas_paper)\n      }\n      return \"rendered\"\n    }\n    render_tree({tree: py_trees.experimental.create_demo_tree_definition()})\n  \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\nAt this point, your web app should be visualising a single tree and\nzoom/collapse/scale to fit interactions functional. I'm happy, you should be too!\n\nAdding a timeline to the application is optional, but the code does not\nchange significantly and is a very useful feature to have. The built-in demo\napp's [index.html](py_trees_js/viewer/html/index.html) does exactly this. The code is reproduced below for convenience.\n\n```xhtml\n\u003c!doctype html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"utf-8\"\u003e\n  \u003ctitle\u003ePyTrees Viewer\u003c/title\u003e\n  \u003clink rel=\"stylesheet\" href=\"js/py_trees-0.6.0.css\"/\u003e\n  \u003clink rel=\"stylesheet\" type=\"text/css\" href=\"js/jointjs/joint-3.0.4.min.css\"/\u003e\n  \u003cscript src=\"js/jointjs/dagre-0.8.4.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/graphlib-2.1.7.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/jquery-3.4.1.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/lodash-4.17.11.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/backbone-1.4.0.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/jointjs/joint-3.0.4.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"js/py_trees-0.6.0.js\"\u003e\u003c/script\u003e\n  \u003c!-- Web app integration css here --\u003e\n  \u003cstyle\u003e\n    html {\n      height: 100%  /* canvas is intended to fill the screen, cascading heights achieves this */\n    }\n    body {\n      margin: 0;\n      overflow:hidden;  /* no scrollbars */\n      height: 100%  /* canvas is intended to fill the screen, cascading heights achieves this */\n    }\n  \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cscript type=\"text/javascript\"\u003e\n    py_trees.hello()\n  \u003c/script\u003e\n  \u003cdiv id=\"window\"\u003e\n    \u003cdiv id=\"canvas\"\u003e\u003c/div\u003e\n    \u003cdiv id=\"timeline\"\u003e\u003c/div\u003e\n  \u003c/div\u003e\n  \u003cscript type=\"text/javascript\"\u003e\n    // rendering canvas\n    canvas_graph = py_trees.canvas.create_graph()\n    canvas_paper = py_trees.canvas.create_paper({graph: canvas_graph})\n\n    // event timeline\n    timeline_graph = py_trees.timeline.create_graph({event_cache_limit: 100});\n    timeline_paper = py_trees.timeline.create_paper({\n        timeline_graph: timeline_graph,\n        canvas_graph: canvas_graph,\n        canvas_paper: canvas_paper,\n    })\n\n    // react to window resizing events\n    $(window).resize(function() {\n      py_trees.canvas.on_window_resize(canvas_paper)\n      py_trees.timeline.on_window_resize(timeline_paper)\n    })\n\n    render_tree = function({tree}) {\n      py_trees.timeline.add_tree_to_cache({\n          timeline_graph: timeline_graph,\n          canvas_graph: canvas_graph,\n          canvas_paper: canvas_paper,\n          tree: tree\n      })\n      return \"rendered\"\n    }\n  \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n## Example - PyQt App\n\nThe `py-trees-demo-viewer` app is a qt-js hybrid application using `qtwebengine`.\nEvery time a qt button is pressed, an internally generated tree snapshot is sent to `render_tree()` in the embedded web application. From here, it is not too hard to\nimagine connecting the qt application to an actual external source. The qt layer\nthen acts as a shim or relay transferring messages to the internal web app.\n\nHow does it work? \n\n* The js libs are made available as a `.qrc` resource [1]\n* A simple web app is made available as another `.qrc` resource\n* Both resources are consumed by the QWebEngine View to serve the app\n\n[1] This can be made available separately and as a dependency to the actual\npyqt application. For instance, the [py_trees_js](./py_trees_js) package is a dependency of [py_trees_ros_viewer](https://github.com/splintered-reality/py_trees_ros_viewer).\n\nIn more detail...\n\n### The JS Libraries\n\n1. Bundle the javascript resources into a `.qrc` file\n2. Generate the resources as a c++ library / python module\n3. Deploy the c++ library/python module in your development environment\n\nIn this case, the py_trees and jointjs javascript libraries have been listed\nin [py_trees_js/resources.qrc](py_trees_js/resources.qrc), generated using\n[py_trees_js/gen.bash](py_trees_js/gen.bash), resulting in the importable module\n[py_trees_js/resources.py](py_trees_js/resources.py). From this point, any pythonic\nQt application wishing to visualise behaviour trees need only import this module from the `py_trees_js` package.\n\n### The Web App\n\n1. Bundle the `.html`/`.css` pages into a `.qrc` file\n2. Import into directly into designer when building your Qt application\n\nIn this case, our web app ([py_trees_js/viewer/html/index.html](py_trees_js/viewer/html/index.html)) has been rolled into [py_trees_js/viewer/web_app.qrc](py_trees_js/viewer/web_app.qrc) which is directly loaded into [py_trees_js/viewer/web_view.ui](py_trees_js/viewer/web_view.ui) where the URL property of the QWebEngineView widget has been configured with the resources `index.html`.\n\nYou could alternatively, generate a module from the `.qrc` and import that into the\nrelevant python code as was done for the javascript resources.\n\n### The Qt Application\n\nThe Qt application can be designed in whatever way you're most comfortable with - via\nDesigner, pure C++ or python. In this case, Qt's Designer is used to produce the `.ui`\nfiles which are generated into python modules and finally customised and brought together\nas a PyQt5 application. Refer to [py_trees_js/viewer](py_trees_js/viewer) for more details\nor as a reference example from which to start your own Qt-Js hybrid application.\n\nKey elements:\n\n1. Build your Qt application around a QWebEngineView widget\n2. Link/import the javascript module in the web engine view class\n3. Load the html page into the QWebEngineView view\n\nDo not use the QWebView widget - this is deprecating in favour of the QWebEngineView widget. The most notable difference is that QWebView uses Qt's old webkit, while QWebEngineView makes use of Chromium's webkit.\n\nNote that the second step automagically makes available the javascript resources to the application\nwhen it's loaded. It's not terribly fussy about where it gets loaded, see [py_trees_js/viewer/web_view.py](py_trees_js/viewer/web_view.py) for an example:\n\n```\n# This is the module generated by running pyrcc5 on the js libraries .qrc\n# It could have been equivalently deployed in a completely different python package\nimport py_trees_js.resources\n```\n\nLoading the web page can be accomplished in designer. Simply point it at your qresource file\nand set the dynamic URL property on the QWebEngineView widget. Alternatively you can import\nthe resource module and load it via QWebEngineView's `load` api.\n\n#### Qt-Js Interactions\n\nQt and JS can interact directly over snippets of javascript code (via `runJavaScript()`\nor over QWebChannel (a mechanism similar to sigslots) where more complexity is needed.\nThe example application here calls on the `render_tree()` method we created earlier in\nthe web application to send trees to the app. Example code from [py_trees_js/viewer/viewer.py](py_trees_js/viewer/viewer.py) which handles button clicks to cycle through a list of\ndemonstration trees:\n\n```\ndef send_tree_response(reply):\n    console.logdebug(\"reply: '{}' [viewer]\".format(reply))\n\n\n@qt_core.pyqtSlot()\ndef send_tree(web_view_page, demo_trees, unused_checked):\n    demo_trees[send_tree.index]['timestamp'] = time.time()\n    console.logdebug(\"send: tree '{}' [{}][viewer]\".format(\n        send_tree.index, demo_trees[send_tree.index]['timestamp'])\n    )\n    javascript_command = \"render_tree({{tree: {}}})\".format(demo_trees[send_tree.index])\n    web_view_page.runJavaScript(javascript_command, send_tree_response)\n    send_tree.index = 0 if send_tree.index == 2 else send_tree.index + 1\n\nsend_tree.index = 0\n```\n\n## The JSON Specification\n\nTODO: A JSon schema\n\nRoughly, the specification expects json objects of the form:\n\n* timestamp: int\n* behaviours: dict[str, dict]\n* (optional) visited_path: list[str]\n* (optional) blackboard: {\n*    behaviours: dict[str, dict[str, str]],\n*    data: dict[str, str]\n* }\n* (optional) activity: list[str]\n\nwhere each behaviour in the dict has specification:\n\n* id: str\n* status: Union[`INVALID`,`FAILURE`, `RUNNING`, `SUCCESS`]\n* name: str\n* colour: \u003chtml style hex code\u003e\n* (optional) children: List[str]\n* (optional) data: \u003cgeneric key-value dictionary\u003e\n\nIdentification strings (id's) must be unique and are used as both keys for the\nbehaviours dictionary, children and visited_path variables.\n\nAn example (extracted from `py_trees.experimental.create_demo_tree_definition()`):\n\n```\n{\n    timestamp: 1563938995,\n    visited_path: ['1', '2', '3', '4', '5', '7', '8'],\n    behaviours: {\n        '1': {\n            id: '1',\n            status: 'RUNNING',\n            name: 'Selector',\n            colour: '#00FFFF',\n            children: ['2', '3', '4', '6'],\n            data: {\n                Type: 'py_trees.composites.Selector',\n                Feedback: \"Decision maker\",\n            },\n        },\n        '2': {\n            id: '2',\n            status: 'FAILURE',\n            name: 'Worker',\n            colour: '#FFA500',\n            children: ['7', '8', '9'],\n            data: {\n                Type: 'py_trees.composites.Sequence',\n                Feedback: \"Worker\"\n            },\n        },\n    }\n    'blackboard': {\n        'behaviours': {  # key metadata per behaviour\n            '2': {\n                '/parameters/initial_value': 'r',\n                '/state/worker': 'w'\n            },\n        },\n        'data': {\n            '/parameters/initial_value': 'foo',\n            '/state/worker': 'bar',\n        },\n    'activity': [\n        \"\u003ctext style='color: blue;'\u003eWorker initialised with 'foo'\u003c/text\u003e'\",\n        \"\u003ctext style='color: red;'\u003eWorker wrote 'bar'\u003c/text\u003e'\",\n    ]\n}\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsplintered-reality%2Fpy_trees_js","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsplintered-reality%2Fpy_trees_js","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsplintered-reality%2Fpy_trees_js/lists"}