structile 6.1.2__tar.gz → 6.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {structile-6.1.2/structile.egg-info → structile-6.3.0}/PKG-INFO +6 -1
- {structile-6.1.2 → structile-6.3.0}/README.pypi.md +5 -0
- structile-6.3.0/VERSION +1 -0
- {structile-6.1.2 → structile-6.3.0}/structile/__init__.py +9 -0
- {structile-6.1.2 → structile-6.3.0}/structile/_paths.py +54 -1
- structile-6.3.0/structile/custom_html.py +67 -0
- {structile-6.1.2 → structile-6.3.0}/structile/interpreters.py +31 -0
- structile-6.3.0/structile/manifest.py +87 -0
- {structile-6.1.2 → structile-6.3.0}/structile/render.py +4 -21
- structile-6.3.0/structile/static/structile.prod.html +183 -0
- {structile-6.1.2 → structile-6.3.0/structile.egg-info}/PKG-INFO +6 -1
- {structile-6.1.2 → structile-6.3.0}/structile.egg-info/SOURCES.txt +2 -0
- structile-6.1.2/VERSION +0 -1
- structile-6.1.2/structile/static/structile.prod.html +0 -183
- {structile-6.1.2 → structile-6.3.0}/LICENSE +0 -0
- {structile-6.1.2 → structile-6.3.0}/MANIFEST.in +0 -0
- {structile-6.1.2 → structile-6.3.0}/pyproject.toml +0 -0
- {structile-6.1.2 → structile-6.3.0}/setup.cfg +0 -0
- {structile-6.1.2 → structile-6.3.0}/structile/__main__.py +0 -0
- {structile-6.1.2 → structile-6.3.0}/structile/_dom_lite.py +0 -0
- {structile-6.1.2 → structile-6.3.0}/structile/_log.py +0 -0
- {structile-6.1.2 → structile-6.3.0}/structile/_plugins.py +0 -0
- {structile-6.1.2 → structile-6.3.0}/structile/config.py +0 -0
- {structile-6.1.2 → structile-6.3.0}/structile/convert.py +0 -0
- {structile-6.1.2 → structile-6.3.0}/structile/dom_lite.js +0 -0
- {structile-6.1.2 → structile-6.3.0}/structile/serialize.py +0 -0
- {structile-6.1.2 → structile-6.3.0}/structile/static/widget.js +0 -0
- {structile-6.1.2 → structile-6.3.0}/structile/widget.py +0 -0
- {structile-6.1.2 → structile-6.3.0}/structile.egg-info/dependency_links.txt +0 -0
- {structile-6.1.2 → structile-6.3.0}/structile.egg-info/requires.txt +0 -0
- {structile-6.1.2 → structile-6.3.0}/structile.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structile
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.3.0
|
|
4
4
|
Summary: A spatial editor and diff tool for structured data, inline in Jupyter
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -232,6 +232,11 @@ data:
|
|
|
232
232
|
extension): `interpretText(text)` (and optionally `serializeText(value)`),
|
|
233
233
|
given the raw text directly — no DOM involved at all.
|
|
234
234
|
|
|
235
|
+
An interpreter script may also declare two optional top-level string
|
|
236
|
+
constants, `INTERPRETER_NAME`/`INTERPRETER_VERSION`, purely to
|
|
237
|
+
self-identify — the standalone viewer's `[?]` About popover shows them for
|
|
238
|
+
whichever interpreter is currently active, when either is declared.
|
|
239
|
+
|
|
235
240
|
`open()` never needs anything beyond its own dependencies for
|
|
236
241
|
either case: every renderer (including `widget`) forwards the interpreter
|
|
237
242
|
source(s) as-is and lets the browser try them client-side. `convert()`
|
|
@@ -204,6 +204,11 @@ data:
|
|
|
204
204
|
extension): `interpretText(text)` (and optionally `serializeText(value)`),
|
|
205
205
|
given the raw text directly — no DOM involved at all.
|
|
206
206
|
|
|
207
|
+
An interpreter script may also declare two optional top-level string
|
|
208
|
+
constants, `INTERPRETER_NAME`/`INTERPRETER_VERSION`, purely to
|
|
209
|
+
self-identify — the standalone viewer's `[?]` About popover shows them for
|
|
210
|
+
whichever interpreter is currently active, when either is declared.
|
|
211
|
+
|
|
207
212
|
`open()` never needs anything beyond its own dependencies for
|
|
208
213
|
either case: every renderer (including `widget`) forwards the interpreter
|
|
209
214
|
source(s) as-is and lets the browser try them client-side. `convert()`
|
structile-6.3.0/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
6.3.0
|
|
@@ -24,10 +24,12 @@ from ._paths import (
|
|
|
24
24
|
as_path_like as _as_path_like,
|
|
25
25
|
read_interpreter as _read_interpreter,
|
|
26
26
|
read_text as _read_text,
|
|
27
|
+
set_viewer_html,
|
|
27
28
|
strict_json_loads as _strict_json_loads,
|
|
28
29
|
)
|
|
29
30
|
from .config import Options, get_option, options, options_from_dict, replace_options, reset_option, resolve_config, resolve_renderer, set_option, use
|
|
30
31
|
from .convert import convert
|
|
32
|
+
from .custom_html import build_custom_html
|
|
31
33
|
from .interpreters import (
|
|
32
34
|
CANONICAL_EXT_FOR_FORMAT as _CANONICAL_EXT_FOR_FORMAT,
|
|
33
35
|
InterpreterSpec,
|
|
@@ -35,9 +37,11 @@ from .interpreters import (
|
|
|
35
37
|
_normalize_ext,
|
|
36
38
|
get_registered_interpreter,
|
|
37
39
|
register_interpreter,
|
|
40
|
+
register_interpreters_from_manifest,
|
|
38
41
|
resolve_candidates as _resolve_interpreter_candidates,
|
|
39
42
|
unregister_interpreter,
|
|
40
43
|
)
|
|
44
|
+
from .manifest import InterpreterManifestEntry, load_manifest
|
|
41
45
|
from ._plugins import PluginRecord, load_plugins, plugins
|
|
42
46
|
from .render import (
|
|
43
47
|
DiffRenderHandle,
|
|
@@ -85,6 +89,11 @@ __all__ = [
|
|
|
85
89
|
"load_plugins",
|
|
86
90
|
"plugins",
|
|
87
91
|
"PluginRecord",
|
|
92
|
+
"build_custom_html",
|
|
93
|
+
"InterpreterManifestEntry",
|
|
94
|
+
"load_manifest",
|
|
95
|
+
"register_interpreters_from_manifest",
|
|
96
|
+
"set_viewer_html",
|
|
88
97
|
"__version__",
|
|
89
98
|
]
|
|
90
99
|
|
|
@@ -26,7 +26,9 @@ def _viewer_html_override() -> Optional[str]:
|
|
|
26
26
|
def find_viewer_html() -> pathlib.Path:
|
|
27
27
|
"""Locate structile.html. Tries, in order:
|
|
28
28
|
|
|
29
|
-
1. `STRUCTILE_VIEWER_HTML` env var, if set
|
|
29
|
+
1. `STRUCTILE_VIEWER_HTML` env var, if set — a dev/testing override,
|
|
30
|
+
or how a custom-distribution wrapper package (see `set_viewer_html`
|
|
31
|
+
below) points every renderer at its own bundled, customized HTML.
|
|
30
32
|
2. The human-readable dev source at the repo root — present for an
|
|
31
33
|
editable, in-repo install (`pip install -e .`) or when just running
|
|
32
34
|
this repo's own tests/scripts/build_samples.py. Always preferred
|
|
@@ -56,6 +58,26 @@ def find_viewer_html() -> pathlib.Path:
|
|
|
56
58
|
)
|
|
57
59
|
|
|
58
60
|
|
|
61
|
+
def set_viewer_html(path: Optional[Union[str, pathlib.Path]]) -> None:
|
|
62
|
+
"""Point every `structile` renderer (`open()`, `diff()`, the `widget`/
|
|
63
|
+
`browser`/`file` renderers, `.to_html()`) at a specific viewer HTML
|
|
64
|
+
file — `None` reverts to `find_viewer_html()`'s normal resolution.
|
|
65
|
+
|
|
66
|
+
This is the documented way for a thin wrapper package to ship its own
|
|
67
|
+
customized `structile.html` (e.g. one built with interpreters baked in
|
|
68
|
+
via `structile.build_custom_html`) without forking `structile` itself:
|
|
69
|
+
call this once, from the wrapper package's own `__init__.py`, with the
|
|
70
|
+
path to its bundled HTML. It's a thin wrapper around the
|
|
71
|
+
`STRUCTILE_VIEWER_HTML` env var (read fresh by `find_viewer_html()` on
|
|
72
|
+
every call, never cached), just under a name that documents this as a
|
|
73
|
+
supported integration point rather than only a dev/testing knob.
|
|
74
|
+
"""
|
|
75
|
+
if path is None:
|
|
76
|
+
os.environ.pop("STRUCTILE_VIEWER_HTML", None)
|
|
77
|
+
else:
|
|
78
|
+
os.environ["STRUCTILE_VIEWER_HTML"] = str(path)
|
|
79
|
+
|
|
80
|
+
|
|
59
81
|
@dataclass(frozen=True)
|
|
60
82
|
class InterpreterSource:
|
|
61
83
|
"""Interpreter JS source text supplied directly, rather than read from a
|
|
@@ -131,6 +153,37 @@ def read_interpreter(source: Union[str, pathlib.Path, InterpreterSource]) -> str
|
|
|
131
153
|
return read_text(p) if p is not None else source # a bare str that isn't a file is raw JS source text
|
|
132
154
|
|
|
133
155
|
|
|
156
|
+
# The one place this module depends on the exact shape of structile.html's
|
|
157
|
+
# main <script> tag, identified by the "use strict" that immediately
|
|
158
|
+
# follows it on the next line (the bare substring "<script>" alone also
|
|
159
|
+
# appears inside a couple of comments elsewhere in the file — see AGENTS.md's
|
|
160
|
+
# note on that CSP-injection lesson from the VS Code extension). Shared by
|
|
161
|
+
# render.py's `window.__STRUCTILE_SNAPSHOT__` bootstrap and
|
|
162
|
+
# custom_html.py's baked-interpreter registry — both just need an arbitrary
|
|
163
|
+
# JSON-safe value visible to the main script before it runs.
|
|
164
|
+
SNAPSHOT_ANCHOR = '\n<script>\n"use strict";'
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def inject_global_script(html_text: str, var_name: str, value: Any, *, anchor: str = SNAPSHOT_ANCHOR) -> str:
|
|
168
|
+
"""Splice `window.<var_name> = <value as JSON>;` into `html_text`,
|
|
169
|
+
right before `anchor`. Repeated calls compose correctly regardless of
|
|
170
|
+
order: each insertion lands before the same anchor text, which is
|
|
171
|
+
never itself touched, so every injected script still precedes the main
|
|
172
|
+
<script> that reads it."""
|
|
173
|
+
idx = html_text.find(anchor)
|
|
174
|
+
if idx == -1:
|
|
175
|
+
raise RuntimeError(
|
|
176
|
+
"structile: could not find structile.html's main <script> tag "
|
|
177
|
+
"to inject before — has the viewer's structure changed?"
|
|
178
|
+
)
|
|
179
|
+
# `<` -> <, same escaping the VS Code extension's jsonForScript
|
|
180
|
+
# uses, so a "</script"-shaped substring anywhere in the data can't
|
|
181
|
+
# break out of this <script> block.
|
|
182
|
+
value_json = json.dumps(value).replace("<", "\\u003c")
|
|
183
|
+
snippet = "\n<script>\nwindow." + var_name + " = " + value_json + ";\n</script>\n"
|
|
184
|
+
return html_text[:idx] + snippet + html_text[idx:]
|
|
185
|
+
|
|
186
|
+
|
|
134
187
|
def strict_json_loads(text: str) -> Any:
|
|
135
188
|
"""`json.loads`, but rejecting the non-standard `NaN`/`Infinity`/
|
|
136
189
|
`-Infinity` constants CPython's `json` module otherwise accepts by
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Build a customized `structile.html` with a curated set of interpreters
|
|
2
|
+
baked directly in, at build time — see README.md's "Building a custom/
|
|
3
|
+
branded distribution" section for the full story and
|
|
4
|
+
`tests/fixtures/structile_demo_custom_distribution/` for a working
|
|
5
|
+
template.
|
|
6
|
+
|
|
7
|
+
Opening the resulting HTML and dropping only a data file of a baked
|
|
8
|
+
extension resolves it immediately — no companion `.js` file ever needed,
|
|
9
|
+
for the extensions this specific build chose to bundle. Stock
|
|
10
|
+
`structile.html` (no manifest applied) is unaffected: the client-side
|
|
11
|
+
registry this bakes into (`window.__STRUCTILE_BAKED_INTERPRETERS__`) is
|
|
12
|
+
empty by default, and `structile.html`'s own extension list
|
|
13
|
+
(`STRUCTURED_EXT_FORMAT`) is only ever merged with it, never replaced by
|
|
14
|
+
it — see the comment above `BAKED_INTERPRETERS` in structile.html.
|
|
15
|
+
|
|
16
|
+
The trust boundary here is deliberately at build time, not runtime: this
|
|
17
|
+
function reads and embeds whatever JS the manifest points at, verbatim —
|
|
18
|
+
the same trust a user pasting/dropping an interpreter into a running
|
|
19
|
+
session extends to it for that session, just decided once, by whoever
|
|
20
|
+
runs this function, instead of by whoever later opens the file.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import pathlib
|
|
25
|
+
from typing import Dict, Optional, Union
|
|
26
|
+
|
|
27
|
+
from ._paths import find_viewer_html, inject_global_script, read_interpreter
|
|
28
|
+
from .manifest import ManifestSource, load_manifest
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _normalize_ext(ext: str) -> str:
|
|
32
|
+
ext = ext.lower()
|
|
33
|
+
return ext if ext.startswith(".") else "." + ext
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_custom_html(
|
|
37
|
+
manifest: ManifestSource,
|
|
38
|
+
*,
|
|
39
|
+
source_html: Optional[Union[str, pathlib.Path]] = None,
|
|
40
|
+
output: Optional[Union[str, pathlib.Path]] = None,
|
|
41
|
+
) -> str:
|
|
42
|
+
"""Bake `manifest`'s `{extensions, path, name, format}` entries (see
|
|
43
|
+
`structile.manifest.load_manifest` for accepted `manifest` shapes) into
|
|
44
|
+
a copy of `structile.html`, and return the resulting HTML text.
|
|
45
|
+
|
|
46
|
+
`source_html`: the base HTML to bake into — defaults to
|
|
47
|
+
`find_viewer_html()`'s own resolution (the dev source next to this
|
|
48
|
+
repo, or the bundled production build). Pass an explicit path to
|
|
49
|
+
bake into a specific build instead (e.g. an already-minified
|
|
50
|
+
`structile.prod.html`).
|
|
51
|
+
`output`: if given, the resulting HTML is also written there.
|
|
52
|
+
"""
|
|
53
|
+
entries = load_manifest(manifest)
|
|
54
|
+
base_html = (
|
|
55
|
+
pathlib.Path(source_html).read_text(encoding="utf-8")
|
|
56
|
+
if source_html is not None
|
|
57
|
+
else find_viewer_html().read_text(encoding="utf-8")
|
|
58
|
+
)
|
|
59
|
+
baked: Dict[str, Dict[str, str]] = {}
|
|
60
|
+
for entry in entries:
|
|
61
|
+
text = read_interpreter(entry.path)
|
|
62
|
+
for ext in entry.extensions:
|
|
63
|
+
baked[_normalize_ext(ext)] = {"format": entry.format, "source": text, "name": entry.name}
|
|
64
|
+
html_text = inject_global_script(base_html, "__STRUCTILE_BAKED_INTERPRETERS__", baked)
|
|
65
|
+
if output is not None:
|
|
66
|
+
pathlib.Path(output).write_text(html_text, encoding="utf-8")
|
|
67
|
+
return html_text
|
|
@@ -19,6 +19,14 @@ see `select_interpreter` below), it's a much smaller DOM-lite object built
|
|
|
19
19
|
from a Python-parsed tree instead (see dom_lite.py/dom_lite.js) — no Node.js,
|
|
20
20
|
no jsdom, just an embedded JS engine (`mini-racer`).
|
|
21
21
|
|
|
22
|
+
An interpreter script may also declare two optional top-level string
|
|
23
|
+
constants, `INTERPRETER_NAME`/`INTERPRETER_VERSION`, purely for
|
|
24
|
+
self-identification — never read or required by anything in this module
|
|
25
|
+
(name/version resolution happens entirely client-side, in
|
|
26
|
+
structile.html's `buildInterpreterFns`, the same place the
|
|
27
|
+
`interpretXML`/`serializeXML`/etc. functions themselves get pulled out),
|
|
28
|
+
surfaced in the standalone viewer's `[?]` About popover.
|
|
29
|
+
|
|
22
30
|
This module is generic across every extension AND across which contract an
|
|
23
31
|
interpreter uses, on purpose: HTML isn't treated as a different *kind* of
|
|
24
32
|
thing from XML anywhere below, only as a different DOMParser *mode*
|
|
@@ -39,6 +47,7 @@ from typing import Any, Dict, List, Optional, Sequence, Union
|
|
|
39
47
|
from ._dom_lite import build_document_payload
|
|
40
48
|
from ._log import logger
|
|
41
49
|
from ._paths import InterpreterSource, read_interpreter
|
|
50
|
+
from .manifest import ManifestSource, load_manifest
|
|
42
51
|
|
|
43
52
|
# A single candidate: a path, a bare str (a path if it names an existing
|
|
44
53
|
# file, else literal JS source text — see read_interpreter), or an explicit
|
|
@@ -114,6 +123,28 @@ def unregister_interpreter(ext: str) -> None:
|
|
|
114
123
|
register_interpreter(ext, None)
|
|
115
124
|
|
|
116
125
|
|
|
126
|
+
def register_interpreters_from_manifest(manifest: ManifestSource) -> None:
|
|
127
|
+
"""Call `register_interpreter()` once per entry in `manifest` (see
|
|
128
|
+
`structile.manifest.load_manifest` for accepted shapes) — the
|
|
129
|
+
Python-side counterpart to `structile.custom_html.build_custom_html`,
|
|
130
|
+
so a custom distribution can drive both the standalone-HTML baking and
|
|
131
|
+
the Python registry from the exact same manifest instead of
|
|
132
|
+
maintaining two separate lists.
|
|
133
|
+
|
|
134
|
+
A plugin's own `register(registry)` callback can't call this directly
|
|
135
|
+
— the facade it receives only exposes `register_interpreter()` (see
|
|
136
|
+
`_plugins.py`'s `_RegistryFacade`) — so loop over
|
|
137
|
+
`structile.manifest.load_manifest(...)` and call
|
|
138
|
+
`registry.register_interpreter(ext, InterpreterSource(text, name=...))`
|
|
139
|
+
per entry there instead; see README.md's "Building a custom/branded
|
|
140
|
+
distribution" section for a worked example.
|
|
141
|
+
"""
|
|
142
|
+
for entry in load_manifest(manifest):
|
|
143
|
+
text = read_interpreter(entry.path)
|
|
144
|
+
for ext in entry.extensions:
|
|
145
|
+
register_interpreter(ext, InterpreterSource(text, name=entry.name))
|
|
146
|
+
|
|
147
|
+
|
|
117
148
|
def get_registered_interpreter(ext: str) -> Optional[InterpreterSpec]:
|
|
118
149
|
"""The raw spec registered for `ext` — a single candidate, a list, or
|
|
119
150
|
`None` if nothing's registered. See `resolve_candidates` for the
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Shared interpreter manifest — the one list a company's custom
|
|
2
|
+
distribution re-reads for two different things: baking interpreters into a
|
|
3
|
+
standalone `structile.html` (see `custom_html.build_custom_html`) and
|
|
4
|
+
registering them with the Python-side registry, either directly
|
|
5
|
+
(`interpreters.register_interpreters_from_manifest`) or from inside an
|
|
6
|
+
entry-point plugin's own `register(registry)` callback (see
|
|
7
|
+
`_plugins.py`'s `_RegistryFacade` — a plugin only ever gets
|
|
8
|
+
`register_interpreter()`, so it loops over `load_manifest(...)` itself
|
|
9
|
+
rather than calling `register_interpreters_from_manifest` directly).
|
|
10
|
+
|
|
11
|
+
A manifest entry mirrors the VS Code extension's
|
|
12
|
+
`contributes.structileInterpreters` contribution shape (`{extensions,
|
|
13
|
+
path, name, format}` — see
|
|
14
|
+
`vscode-extension/src/interpreterContributions.ts`) so the same mental
|
|
15
|
+
model, and often the same JSON, works across every surface this project
|
|
16
|
+
ships.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import pathlib
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
24
|
+
|
|
25
|
+
from ._paths import InterpreterSource
|
|
26
|
+
|
|
27
|
+
# Anything read_interpreter() already accepts: a path, a bare str (a path
|
|
28
|
+
# if it names an existing file, else literal JS source text), or an
|
|
29
|
+
# explicit InterpreterSource.
|
|
30
|
+
ManifestInterpreterPath = Union[str, pathlib.Path, InterpreterSource]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class InterpreterManifestEntry:
|
|
35
|
+
"""One `{extensions, path, name, format}` entry — see module docstring.
|
|
36
|
+
|
|
37
|
+
`extensions`: file extensions this interpreter handles (e.g. [".axml"]).
|
|
38
|
+
`path`: anything `read_interpreter()` accepts. When loaded from a JSON
|
|
39
|
+
manifest file, a relative path is resolved against that file's own
|
|
40
|
+
directory (see `load_manifest`), not the process's working
|
|
41
|
+
directory.
|
|
42
|
+
`name`: a human label used in logs/error messages when this candidate
|
|
43
|
+
fails during selection.
|
|
44
|
+
`format`: `"xml"`/`"html"` (the DOM contract) or any other string (the
|
|
45
|
+
`interpretText` contract) — same meaning as `structile.open()`'s
|
|
46
|
+
own `format=` argument.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
extensions: List[str]
|
|
50
|
+
path: ManifestInterpreterPath
|
|
51
|
+
name: str
|
|
52
|
+
format: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _entry_from_dict(d: Dict[str, Any], *, base_dir: Optional[pathlib.Path]) -> InterpreterManifestEntry:
|
|
56
|
+
extensions = d["extensions"]
|
|
57
|
+
if isinstance(extensions, str):
|
|
58
|
+
extensions = [extensions]
|
|
59
|
+
path = d["path"]
|
|
60
|
+
if base_dir is not None and isinstance(path, str) and not pathlib.Path(path).is_absolute():
|
|
61
|
+
path = base_dir / path
|
|
62
|
+
return InterpreterManifestEntry(extensions=list(extensions), path=path, name=d["name"], format=d["format"])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
ManifestSource = Union[str, pathlib.Path, Sequence[Union[Dict[str, Any], InterpreterManifestEntry]]]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_manifest(source: ManifestSource) -> List[InterpreterManifestEntry]:
|
|
69
|
+
"""Normalize `source` into a list of `InterpreterManifestEntry`.
|
|
70
|
+
|
|
71
|
+
`source` is either:
|
|
72
|
+
- a path to a JSON manifest file — `{"interpreters": [...]}`, each item
|
|
73
|
+
shaped like `InterpreterManifestEntry`'s fields, with `path` values
|
|
74
|
+
resolved relative to the manifest file's own directory; or
|
|
75
|
+
- a plain list of dicts (that same shape) or `InterpreterManifestEntry`
|
|
76
|
+
instances directly — `path` values are used as-is, since there's no
|
|
77
|
+
manifest file to resolve a relative path against.
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(source, (str, pathlib.Path)):
|
|
80
|
+
manifest_path = pathlib.Path(source)
|
|
81
|
+
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
82
|
+
base_dir = manifest_path.resolve().parent
|
|
83
|
+
return [_entry_from_dict(item, base_dir=base_dir) for item in data["interpreters"]]
|
|
84
|
+
entries: List[InterpreterManifestEntry] = []
|
|
85
|
+
for item in source:
|
|
86
|
+
entries.append(item if isinstance(item, InterpreterManifestEntry) else _entry_from_dict(item, base_dir=None))
|
|
87
|
+
return entries
|
|
@@ -31,17 +31,10 @@ from dataclasses import dataclass
|
|
|
31
31
|
from typing import Any, Dict, List, Optional, Union
|
|
32
32
|
|
|
33
33
|
from ._log import logger
|
|
34
|
-
from ._paths import find_viewer_html
|
|
34
|
+
from ._paths import find_viewer_html, inject_global_script
|
|
35
35
|
|
|
36
36
|
RENDERERS = ("widget", "browser", "file", "none", "text")
|
|
37
37
|
|
|
38
|
-
# The one place in structile.html this module depends on the exact
|
|
39
|
-
# shape of: the main <script> tag, identified by the "use strict" that
|
|
40
|
-
# immediately follows it on the next line (the bare substring "<script>"
|
|
41
|
-
# alone also appears inside a couple of comments elsewhere in the file — see
|
|
42
|
-
# AGENTS.md's note on that CSP-injection lesson from the VS Code extension).
|
|
43
|
-
_SNAPSHOT_ANCHOR = '\n<script>\n"use strict";'
|
|
44
|
-
|
|
45
38
|
_temp_dir: Optional[str] = None
|
|
46
39
|
|
|
47
40
|
|
|
@@ -144,24 +137,14 @@ class Payload:
|
|
|
144
137
|
|
|
145
138
|
def _inject_snapshot(snapshot: Dict[str, Any]) -> str:
|
|
146
139
|
"""Splice an arbitrary `window.__STRUCTILE_SNAPSHOT__` payload into a fresh
|
|
147
|
-
read of structile.html, right before its main <script> tag
|
|
140
|
+
read of structile.html, right before its main <script> tag (see
|
|
141
|
+
`_paths.inject_global_script`, the shared low-level splicer).
|
|
148
142
|
Shared by `build_standalone_html` (single-value) and
|
|
149
143
|
`build_standalone_diff_html` (two-value, below) — both just build a
|
|
150
144
|
JSON-safe dict in the shape `applyInitialPayload`/`loadDiffPayload`
|
|
151
145
|
already know how to consume and hand it here."""
|
|
152
146
|
viewer_html = find_viewer_html().read_text(encoding="utf-8")
|
|
153
|
-
|
|
154
|
-
if idx == -1:
|
|
155
|
-
raise RuntimeError(
|
|
156
|
-
"structile: could not find structile.html's main <script> tag "
|
|
157
|
-
"to inject the snapshot before — has the viewer's structure changed?"
|
|
158
|
-
)
|
|
159
|
-
# `<` -> <, same escaping the VS Code extension's jsonForScript uses,
|
|
160
|
-
# so a "</script"-shaped substring anywhere in the data can't break out
|
|
161
|
-
# of this <script> block.
|
|
162
|
-
snapshot_json = json.dumps(snapshot).replace("<", "\\u003c")
|
|
163
|
-
snippet = "\n<script>\nwindow.__STRUCTILE_SNAPSHOT__ = " + snapshot_json + ";\n</script>\n"
|
|
164
|
-
return viewer_html[:idx] + snippet + viewer_html[idx:]
|
|
147
|
+
return inject_global_script(viewer_html, "__STRUCTILE_SNAPSHOT__", snapshot)
|
|
165
148
|
|
|
166
149
|
|
|
167
150
|
def build_standalone_html(payload: Payload) -> str:
|