dbdocs 1.0.0__tar.gz → 1.0.2__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.
- {dbdocs-1.0.0 → dbdocs-1.0.2}/.gitignore +1 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/PKG-INFO +1 -1
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/core/config.py +21 -2
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/extract/nodes.py +39 -14
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/site/builder.py +14 -7
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/site/bundle/assets/app.js +16 -12
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/site/bundle/assets/style.css +6 -10
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/site/bundle/index.html +1 -1
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/site/deploy.py +17 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/site/inject.py +3 -3
- {dbdocs-1.0.0 → dbdocs-1.0.2}/LICENSE +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/README.md +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/__init__.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/__main__.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/cli/__init__.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/cli/main.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/core/__init__.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/core/artifacts.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/core/exceptions.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/core/log.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/extract/__init__.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/extract/_sqlglot_lineage.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/extract/column_lineage.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/extract/erd.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/extract/erd_json.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/extract/graph.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/main.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/site/__init__.py +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/site/bundle/assets/favicon.svg +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/site/bundle/assets/graph/index.css +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/site/bundle/assets/graph/index.js +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/site/bundle/assets/vendor/marked.min.js +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/dbdocs/site/bundle/assets/vendor/minisearch.min.js +0 -0
- {dbdocs-1.0.0 → dbdocs-1.0.2}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbdocs
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Alternative dbt docs site: Catalog + ERD + column-level lineage
|
|
5
5
|
Project-URL: Homepage, https://github.com/datnguye/dbt-docs
|
|
6
6
|
Project-URL: Repository, https://github.com/datnguye/dbt-docs
|
|
@@ -8,6 +8,23 @@ from dbdocs.core.exceptions import DbDocsConfigError
|
|
|
8
8
|
DEFAULT_CONFIG_FILENAME = "dbdocs.yml"
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def _resolve_within_cwd(value: str, field_name: str) -> Path:
|
|
12
|
+
"""Resolve *value* against cwd; reject a relative path that escapes the cwd.
|
|
13
|
+
|
|
14
|
+
An absolute *value* is resolved as-is (the caller's deliberate choice — this
|
|
15
|
+
is documented behaviour and must not be rejected). A relative *value* is
|
|
16
|
+
resolved against ``Path.cwd()`` and then checked: if the result is not
|
|
17
|
+
inside (or equal to) the cwd, ``DbDocsConfigError`` is raised.
|
|
18
|
+
"""
|
|
19
|
+
base = Path.cwd().resolve()
|
|
20
|
+
candidate = Path(value)
|
|
21
|
+
resolved = candidate if candidate.is_absolute() else (base / candidate)
|
|
22
|
+
resolved = resolved.resolve()
|
|
23
|
+
if not candidate.is_absolute() and resolved != base and base not in resolved.parents:
|
|
24
|
+
raise DbDocsConfigError(f"{field_name} {value!r} escapes the project directory ({base}).")
|
|
25
|
+
return resolved
|
|
26
|
+
|
|
27
|
+
|
|
11
28
|
@dataclass
|
|
12
29
|
class DbDocsConfig:
|
|
13
30
|
"""Site configuration for a dbdocs build.
|
|
@@ -103,8 +120,9 @@ class DbDocsConfig:
|
|
|
103
120
|
directory **at access time** — this is intentional and must stay aligned
|
|
104
121
|
with dbterd's ``DbtErd``, which also reads artifacts from ``./target``
|
|
105
122
|
relative to the cwd. An absolute ``target_dir`` is returned unchanged.
|
|
123
|
+
A relative path that would escape the cwd raises ``DbDocsConfigError``.
|
|
106
124
|
"""
|
|
107
|
-
return str(
|
|
125
|
+
return str(_resolve_within_cwd(self.target_dir, "target_dir"))
|
|
108
126
|
|
|
109
127
|
@property
|
|
110
128
|
def output_path(self) -> str:
|
|
@@ -113,5 +131,6 @@ class DbDocsConfig:
|
|
|
113
131
|
Resolved against the cwd at access time, mirroring ``target_path`` — a
|
|
114
132
|
relative ``output_dir`` follows the working directory, an absolute one is
|
|
115
133
|
returned unchanged.
|
|
134
|
+
A relative path that would escape the cwd raises ``DbDocsConfigError``.
|
|
116
135
|
"""
|
|
117
|
-
return str(
|
|
136
|
+
return str(_resolve_within_cwd(self.output_dir, "output_dir"))
|
|
@@ -12,27 +12,52 @@ from typing import Any
|
|
|
12
12
|
from dbdocs.core.artifacts import NODE_PREFIXES, db_schema, node_name
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def _column_entry(name: str, manifest_column: Any, col_type: str) -> dict:
|
|
16
|
+
"""One column record. Newlines in the description become ``<br>`` for HTML."""
|
|
17
|
+
description = getattr(manifest_column, "description", "") or "" if manifest_column else ""
|
|
18
|
+
return {
|
|
19
|
+
"name": name,
|
|
20
|
+
"type": col_type or "",
|
|
21
|
+
"tags": (getattr(manifest_column, "tags", []) or []) if manifest_column else [],
|
|
22
|
+
"description": description.replace("\n", "<br>"),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
15
26
|
def _columns(model: Any, catalog_node: Any) -> list:
|
|
16
|
-
"""
|
|
27
|
+
"""Build a node's columns: the manifest is the source of truth, the catalog
|
|
28
|
+
enriches it.
|
|
29
|
+
|
|
30
|
+
The **manifest** decides which columns are documented and carries their
|
|
31
|
+
metadata (description, tags, ``data_type``); the **catalog** *enriches* — it
|
|
32
|
+
supplies the warehouse-confirmed type and any columns the manifest didn't
|
|
33
|
+
document. It never replaces the manifest, so a model absent from a
|
|
34
|
+
stale/partial ``catalog.json`` still shows every documented column.
|
|
17
35
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
36
|
+
Manifest columns come first, in manifest order, each with its type enriched
|
|
37
|
+
from the catalog when present (falling back to the manifest ``data_type``).
|
|
38
|
+
The catalog keys columns by the warehouse's casing (Snowflake upper-cases
|
|
39
|
+
them) while the manifest keeps the modeled casing, so the catalog type lookup
|
|
40
|
+
is case-insensitive. Catalog-only columns are appended afterwards.
|
|
21
41
|
"""
|
|
22
42
|
manifest_columns = getattr(model, "columns", {}) or {}
|
|
23
43
|
catalog_columns = getattr(catalog_node, "columns", {}) or {} if catalog_node else {}
|
|
44
|
+
catalog_by_lower = {str(name).lower(): col for name, col in catalog_columns.items()}
|
|
45
|
+
|
|
24
46
|
columns = []
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"tags": (getattr(manifest_column, "tags", []) or []) if manifest_column else [],
|
|
33
|
-
"description": description.replace("\n", "<br>"),
|
|
34
|
-
}
|
|
47
|
+
seen_lower = set()
|
|
48
|
+
for name, manifest_column in manifest_columns.items():
|
|
49
|
+
lower = str(name).lower()
|
|
50
|
+
seen_lower.add(lower)
|
|
51
|
+
catalog_column = catalog_by_lower.get(lower)
|
|
52
|
+
col_type = getattr(catalog_column, "type", None) or getattr(
|
|
53
|
+
manifest_column, "data_type", None
|
|
35
54
|
)
|
|
55
|
+
columns.append(_column_entry(name, manifest_column, col_type or ""))
|
|
56
|
+
|
|
57
|
+
for name, catalog_column in catalog_columns.items():
|
|
58
|
+
if str(name).lower() in seen_lower:
|
|
59
|
+
continue
|
|
60
|
+
columns.append(_column_entry(name, None, getattr(catalog_column, "type", "") or ""))
|
|
36
61
|
return columns
|
|
37
62
|
|
|
38
63
|
|
|
@@ -9,11 +9,12 @@ data → write ``index.html`` + a debug ``dbdocs-data.json``.
|
|
|
9
9
|
import json
|
|
10
10
|
from datetime import datetime
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from shutil import copytree
|
|
12
|
+
from shutil import copytree, rmtree
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
15
|
from dbdocs.core.artifacts import adapter_type, load_artifacts
|
|
16
|
-
from dbdocs.core.config import DbDocsConfig
|
|
16
|
+
from dbdocs.core.config import DbDocsConfig, _resolve_within_cwd
|
|
17
|
+
from dbdocs.core.exceptions import DbDocsConfigError
|
|
17
18
|
from dbdocs.core.log import logger
|
|
18
19
|
from dbdocs.extract.column_lineage import ColumnLineageExtractor
|
|
19
20
|
from dbdocs.extract.erd import build_erd, build_erd_data
|
|
@@ -66,12 +67,16 @@ class ReportBuilder:
|
|
|
66
67
|
def _read_readme(self) -> str:
|
|
67
68
|
"""The project README markdown (rendered on the overview), or ``""``.
|
|
68
69
|
|
|
69
|
-
``config.readme`` is a path relative to the cwd; a missing file
|
|
70
|
-
empty config value
|
|
70
|
+
``config.readme`` is a path relative to the cwd; a missing file, an
|
|
71
|
+
empty config value, or a relative path that escapes the cwd all yield
|
|
72
|
+
no README section (fail-soft — a bad README must never sink generate).
|
|
71
73
|
"""
|
|
72
74
|
if not self.config.readme:
|
|
73
75
|
return ""
|
|
74
|
-
|
|
76
|
+
try:
|
|
77
|
+
path = _resolve_within_cwd(self.config.readme, "readme")
|
|
78
|
+
except DbDocsConfigError:
|
|
79
|
+
return ""
|
|
75
80
|
try:
|
|
76
81
|
return path.read_text(encoding="utf-8")
|
|
77
82
|
except OSError:
|
|
@@ -109,7 +114,9 @@ class ReportBuilder:
|
|
|
109
114
|
def generate(self, output_dir: "str | None" = None) -> str:
|
|
110
115
|
"""Render the site into ``output_dir`` (or config's). Returns its path."""
|
|
111
116
|
out = Path(output_dir) if output_dir else Path(self.config.output_path)
|
|
112
|
-
out.
|
|
117
|
+
if out.exists():
|
|
118
|
+
rmtree(out)
|
|
119
|
+
out.mkdir(parents=True)
|
|
113
120
|
copytree(src=BUNDLE_DIR, dst=out, dirs_exist_ok=True)
|
|
114
121
|
|
|
115
122
|
data = self.build_data()
|
|
@@ -117,7 +124,7 @@ class ReportBuilder:
|
|
|
117
124
|
index.write_text(inject(index.read_text(encoding="utf-8"), data), encoding="utf-8")
|
|
118
125
|
# Compact, not indented — keeps the debug dump cheap on large projects.
|
|
119
126
|
(out / "dbdocs-data.json").write_text(
|
|
120
|
-
json.dumps(data, separators=(",", ":"), default=self._json_default),
|
|
127
|
+
json.dumps(data, separators=(",", ":"), sort_keys=True, default=self._json_default),
|
|
121
128
|
encoding="utf-8",
|
|
122
129
|
)
|
|
123
130
|
|
|
@@ -120,12 +120,23 @@
|
|
|
120
120
|
el("span", {}, ["Generated by "]),
|
|
121
121
|
el("a", { href: "https://github.com/datnguye/dbt-docs", target: "_blank", rel: "noopener" }, ["dbdocs"]),
|
|
122
122
|
]);
|
|
123
|
+
var meta = footerMeta();
|
|
124
|
+
if (meta) left.appendChild(el("span", { class: "cf-meta" }, [" · " + meta]));
|
|
123
125
|
var right = el("a", { class: "cf-top", href: "#", onclick: function (e) { e.preventDefault(); app.scrollTop = 0; } }, ["↑ Back to top"]);
|
|
124
126
|
foot.appendChild(left);
|
|
125
127
|
foot.appendChild(right);
|
|
126
128
|
return foot;
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
/* "snowflake · N column-lineage edges" — the build metadata shown in the
|
|
132
|
+
content footer. The "Generated X" part lives in the topbar (see boot). */
|
|
133
|
+
function footerMeta() {
|
|
134
|
+
var parts = [];
|
|
135
|
+
if (META.adapter_type) parts.push(META.adapter_type);
|
|
136
|
+
parts.push(Object.keys(COLLIN).length + " column-lineage edges");
|
|
137
|
+
return parts.join(" · ");
|
|
138
|
+
}
|
|
139
|
+
|
|
129
140
|
var RTYPE_ORDER = { model: 0, snapshot: 1, seed: 2, source: 3 };
|
|
130
141
|
|
|
131
142
|
function buildNav() {
|
|
@@ -456,17 +467,6 @@
|
|
|
456
467
|
link.hidden = false;
|
|
457
468
|
}
|
|
458
469
|
|
|
459
|
-
function initTopbarMeta() {
|
|
460
|
-
var box = document.getElementById("topbar-meta");
|
|
461
|
-
if (!box) return;
|
|
462
|
-
var parts = [];
|
|
463
|
-
if (META.generated_at) parts.push("Generated " + META.generated_at);
|
|
464
|
-
if (META.adapter_type) parts.push(META.adapter_type);
|
|
465
|
-
parts.push(Object.keys(COLLIN).length + " column-lineage edges");
|
|
466
|
-
box.textContent = parts.join(" · ");
|
|
467
|
-
box.hidden = false;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
470
|
function initFooter() {
|
|
471
471
|
var footer = document.getElementById("site-footer");
|
|
472
472
|
if (!footer) return;
|
|
@@ -486,8 +486,12 @@
|
|
|
486
486
|
function boot() {
|
|
487
487
|
document.getElementById("brand-name").textContent = META.site_name || "dbt docs";
|
|
488
488
|
document.title = META.site_name || "dbt docs";
|
|
489
|
+
if (META.generated_at) {
|
|
490
|
+
var gen = document.getElementById("brand-generated");
|
|
491
|
+
gen.textContent = "Generated " + META.generated_at;
|
|
492
|
+
gen.hidden = false;
|
|
493
|
+
}
|
|
489
494
|
initRepo();
|
|
490
|
-
initTopbarMeta();
|
|
491
495
|
initFooter();
|
|
492
496
|
initTheme();
|
|
493
497
|
buildNav();
|
|
@@ -52,8 +52,9 @@ code, pre { font-family: "SF Mono", "Roboto Mono", Menlo, Consolas, monospace; }
|
|
|
52
52
|
background: var(--bg-elev); border-bottom: 1px solid var(--border);
|
|
53
53
|
position: sticky; top: 0; z-index: 30;
|
|
54
54
|
}
|
|
55
|
-
.brand { display: flex; align-items:
|
|
56
|
-
.brand-mark { display: block; width: 24px; height: 24px; border-radius: 6px; }
|
|
55
|
+
.brand { display: flex; align-items: baseline; gap: 8px; font-weight: 600; min-width: 160px; }
|
|
56
|
+
.brand-mark { display: block; width: 24px; height: 24px; border-radius: 6px; align-self: center; }
|
|
57
|
+
.brand-generated { color: var(--text-soft); font-size: 12px; font-weight: 400; white-space: nowrap; }
|
|
57
58
|
.search-wrap { position: relative; flex: 1; max-width: 520px; }
|
|
58
59
|
#search {
|
|
59
60
|
width: 100%; padding: 7px 12px; border-radius: var(--radius);
|
|
@@ -118,21 +119,16 @@ code, pre { font-family: "SF Mono", "Roboto Mono", Menlo, Consolas, monospace; }
|
|
|
118
119
|
}
|
|
119
120
|
.footer-copy { color: var(--text-soft); font-size: 12px; }
|
|
120
121
|
|
|
121
|
-
/* Build metadata in the header (generated / adapter / edges) */
|
|
122
|
-
.topbar-meta {
|
|
123
|
-
color: var(--text-soft); font-size: 11.5px; white-space: nowrap;
|
|
124
|
-
padding-right: 6px; flex-shrink: 0;
|
|
125
|
-
}
|
|
126
|
-
@media (max-width: 1100px) { .topbar-meta { display: none; } }
|
|
127
|
-
|
|
128
122
|
/* End-of-page content footer */
|
|
129
123
|
.content-foot {
|
|
130
124
|
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
131
125
|
margin-top: 40px; padding-top: 16px; border-top: 1px solid var(--border);
|
|
132
126
|
color: var(--text-soft); font-size: 12px;
|
|
133
127
|
}
|
|
134
|
-
.content-foot .cf-left { display: inline-flex; align-items: center; gap: 6px; }
|
|
128
|
+
.content-foot .cf-left { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
135
129
|
.content-foot .cf-left .ic { color: var(--text-soft); }
|
|
130
|
+
/* Build metadata (generated / adapter / edges) appended to the footer's left */
|
|
131
|
+
.content-foot .cf-meta { color: var(--text-soft); }
|
|
136
132
|
.content-foot .cf-top { color: var(--text-soft); }
|
|
137
133
|
.content-foot .cf-top:hover { color: var(--accent); }
|
|
138
134
|
.bmc {
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
<div class="brand">
|
|
15
15
|
<img class="brand-mark" src="assets/favicon.svg" alt="" width="24" height="24" />
|
|
16
16
|
<span class="brand-name" id="brand-name">dbt docs</span>
|
|
17
|
+
<span class="brand-generated" id="brand-generated" hidden></span>
|
|
17
18
|
</div>
|
|
18
|
-
<div class="topbar-meta" id="topbar-meta" hidden></div>
|
|
19
19
|
<div class="search-wrap">
|
|
20
20
|
<input id="search" type="search" placeholder="Search models, sources, columns…" autocomplete="off" />
|
|
21
21
|
<div id="search-results" class="search-results" hidden></div>
|
|
@@ -14,6 +14,7 @@ the output dir onto a ``gh-pages`` branch and pushes it.
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import json
|
|
17
|
+
import re
|
|
17
18
|
import subprocess
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
from shutil import copytree, rmtree
|
|
@@ -25,6 +26,17 @@ from dbdocs.site.builder import ReportBuilder
|
|
|
25
26
|
|
|
26
27
|
VERSIONS_FILE = "versions.json"
|
|
27
28
|
|
|
29
|
+
#: Allows only alphanumerics, dots, underscores, and hyphens — a safe single path segment.
|
|
30
|
+
_SAFE_SEGMENT = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _validate_segment(value: str, kind: str) -> None:
|
|
34
|
+
"""Raise :class:`DeployError` if *value* is not a safe single path segment."""
|
|
35
|
+
if not _SAFE_SEGMENT.match(value) or value in {".", ".."}:
|
|
36
|
+
raise DeployError(
|
|
37
|
+
f"Invalid {kind} {value!r}: must match ^[A-Za-z0-9._-]+$ and not be '.' or '..'."
|
|
38
|
+
)
|
|
39
|
+
|
|
28
40
|
|
|
29
41
|
def _read_versions(output_root: Path) -> list:
|
|
30
42
|
path = output_root / VERSIONS_FILE
|
|
@@ -57,6 +69,9 @@ def deploy(
|
|
|
57
69
|
title: "str | None" = None,
|
|
58
70
|
) -> str:
|
|
59
71
|
"""Generate ``version`` into the output root and update the version index."""
|
|
72
|
+
_validate_segment(version, "version")
|
|
73
|
+
if alias is not None:
|
|
74
|
+
_validate_segment(alias, "alias")
|
|
60
75
|
output_root = Path(config.output_path)
|
|
61
76
|
version_dir = output_root / version
|
|
62
77
|
if version_dir.exists():
|
|
@@ -85,6 +100,7 @@ def delete(config: DbDocsConfig, version: str, push: bool = False) -> None:
|
|
|
85
100
|
|
|
86
101
|
Raises :class:`DeployError` if the version isn't deployed.
|
|
87
102
|
"""
|
|
103
|
+
_validate_segment(version, "version")
|
|
88
104
|
output_root = Path(config.output_path)
|
|
89
105
|
versions = _read_versions(output_root)
|
|
90
106
|
entry = next((v for v in versions if v.get("version") == version), None)
|
|
@@ -95,6 +111,7 @@ def delete(config: DbDocsConfig, version: str, push: bool = False) -> None:
|
|
|
95
111
|
if version_dir.exists():
|
|
96
112
|
rmtree(version_dir)
|
|
97
113
|
for alias in entry.get("aliases", []):
|
|
114
|
+
_validate_segment(alias, "alias")
|
|
98
115
|
alias_dir = output_root / alias
|
|
99
116
|
if alias_dir.exists():
|
|
100
117
|
rmtree(alias_dir)
|
|
@@ -16,9 +16,9 @@ INJECT_MARKER = "<!-- DBDOCS_DATA -->"
|
|
|
16
16
|
|
|
17
17
|
def data_script(data: dict) -> str:
|
|
18
18
|
"""The ``<script>`` tag that sets ``window.dbdocsData`` from ``data``."""
|
|
19
|
-
payload = base64.b64encode(
|
|
20
|
-
"
|
|
21
|
-
)
|
|
19
|
+
payload = base64.b64encode(
|
|
20
|
+
json.dumps(data, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
|
21
|
+
).decode("ascii")
|
|
22
22
|
return f'<script>window.dbdocsData = JSON.parse(atob("{payload}"));</script>'
|
|
23
23
|
|
|
24
24
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|