wand-decks-kit 0.8.0
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.
- package/PROMPT.md +461 -0
- package/README.md +137 -0
- package/START-HERE.md +98 -0
- package/assets/README.md +40 -0
- package/assets/_icon-contact-sheet.png +0 -0
- package/assets/bg-cover-dark.png +0 -0
- package/assets/bg-overlay-dark.png +0 -0
- package/assets/bg-statement-dark.png +0 -0
- package/assets/glyphs/README.md +29 -0
- package/assets/glyphs/activity.svg +15 -0
- package/assets/glyphs/atom.svg +17 -0
- package/assets/glyphs/badge-check.svg +16 -0
- package/assets/glyphs/bell.svg +16 -0
- package/assets/glyphs/blocks.svg +16 -0
- package/assets/glyphs/book-open.svg +16 -0
- package/assets/glyphs/bot.svg +20 -0
- package/assets/glyphs/boxes.svg +26 -0
- package/assets/glyphs/brain.svg +23 -0
- package/assets/glyphs/briefcase.svg +16 -0
- package/assets/glyphs/building-2.svg +21 -0
- package/assets/glyphs/calendar.svg +18 -0
- package/assets/glyphs/chart-column.svg +18 -0
- package/assets/glyphs/chart-line.svg +16 -0
- package/assets/glyphs/chart-pie.svg +16 -0
- package/assets/glyphs/circle-check.svg +16 -0
- package/assets/glyphs/clipboard-check.svg +17 -0
- package/assets/glyphs/clock.svg +16 -0
- package/assets/glyphs/cloud.svg +15 -0
- package/assets/glyphs/coins.svg +18 -0
- package/assets/glyphs/compass.svg +16 -0
- package/assets/glyphs/cpu.svg +24 -0
- package/assets/glyphs/database.svg +17 -0
- package/assets/glyphs/eye.svg +16 -0
- package/assets/glyphs/file-check.svg +17 -0
- package/assets/glyphs/filter.svg +15 -0
- package/assets/glyphs/fingerprint.svg +23 -0
- package/assets/glyphs/flag.svg +16 -0
- package/assets/glyphs/gauge.svg +16 -0
- package/assets/glyphs/gavel.svg +19 -0
- package/assets/glyphs/git-branch.svg +18 -0
- package/assets/glyphs/globe.svg +17 -0
- package/assets/glyphs/graduation-cap.svg +17 -0
- package/assets/glyphs/handshake.svg +19 -0
- package/assets/glyphs/hexagon.svg +15 -0
- package/assets/glyphs/index.json +175 -0
- package/assets/glyphs/infinity.svg +15 -0
- package/assets/glyphs/key.svg +17 -0
- package/assets/glyphs/landmark.svg +20 -0
- package/assets/glyphs/layers.svg +17 -0
- package/assets/glyphs/lightbulb.svg +17 -0
- package/assets/glyphs/link.svg +16 -0
- package/assets/glyphs/list-checks.svg +19 -0
- package/assets/glyphs/lock.svg +16 -0
- package/assets/glyphs/map.svg +17 -0
- package/assets/glyphs/megaphone.svg +16 -0
- package/assets/glyphs/message-square.svg +15 -0
- package/assets/glyphs/milestone.svg +17 -0
- package/assets/glyphs/network.svg +19 -0
- package/assets/glyphs/orbit.svg +19 -0
- package/assets/glyphs/package.svg +18 -0
- package/assets/glyphs/plug.svg +18 -0
- package/assets/glyphs/puzzle.svg +15 -0
- package/assets/glyphs/radar.svg +22 -0
- package/assets/glyphs/receipt.svg +17 -0
- package/assets/glyphs/refresh-cw.svg +18 -0
- package/assets/glyphs/rocket.svg +18 -0
- package/assets/glyphs/route.svg +17 -0
- package/assets/glyphs/scale.svg +19 -0
- package/assets/glyphs/search.svg +16 -0
- package/assets/glyphs/server.svg +18 -0
- package/assets/glyphs/settings.svg +16 -0
- package/assets/glyphs/share-2.svg +19 -0
- package/assets/glyphs/shield-check.svg +16 -0
- package/assets/glyphs/shuffle.svg +19 -0
- package/assets/glyphs/sparkles.svg +19 -0
- package/assets/glyphs/square-stack.svg +17 -0
- package/assets/glyphs/target.svg +17 -0
- package/assets/glyphs/trending-up.svg +16 -0
- package/assets/glyphs/triangle-alert.svg +17 -0
- package/assets/glyphs/trophy.svg +20 -0
- package/assets/glyphs/users.svg +18 -0
- package/assets/glyphs/wallet.svg +16 -0
- package/assets/glyphs/wand-sparkles.svg +22 -0
- package/assets/glyphs/workflow.svg +17 -0
- package/assets/glyphs/zap.svg +15 -0
- package/assets/icons/icon-11.png +0 -0
- package/assets/icons/icon-12.png +0 -0
- package/assets/icons/icon-13.png +0 -0
- package/assets/icons/icon-14.png +0 -0
- package/assets/icons/icon-15.png +0 -0
- package/assets/icons/icon-16.png +0 -0
- package/assets/icons/icon-17.png +0 -0
- package/assets/icons/icon-18.png +0 -0
- package/assets/icons/icon-20.png +0 -0
- package/assets/icons/icon-23.png +0 -0
- package/assets/icons/icon-24.png +0 -0
- package/assets/icons/icon-25.png +0 -0
- package/assets/icons/icon-27.png +0 -0
- package/assets/icons/icon-28.png +0 -0
- package/assets/icons/icon-30.png +0 -0
- package/assets/icons/icon-31.png +0 -0
- package/assets/icons/icon-32.png +0 -0
- package/assets/icons/icon-33.png +0 -0
- package/assets/icons/icon-34.png +0 -0
- package/assets/icons/icon-35.png +0 -0
- package/assets/icons/icon-36.png +0 -0
- package/assets/icons/icon-37.png +0 -0
- package/assets/icons/icon-39.png +0 -0
- package/assets/icons/icon-40.png +0 -0
- package/assets/icons/icon-41.png +0 -0
- package/assets/icons/icon-43.png +0 -0
- package/assets/icons/icon-44.png +0 -0
- package/assets/icons/icon-45.png +0 -0
- package/assets/icons/icon-46.png +0 -0
- package/assets/icons/icon-48.png +0 -0
- package/assets/icons/icon-49.png +0 -0
- package/assets/icons/icon-50.png +0 -0
- package/assets/icons/icon-51.png +0 -0
- package/assets/icons/icon-52.png +0 -0
- package/assets/icons/icon-53.png +0 -0
- package/assets/icons/icon-9.png +0 -0
- package/assets/mark-dark.png +0 -0
- package/assets/mark-white.png +0 -0
- package/assets/orb-cover.png +0 -0
- package/assets/wordmark-dark.png +0 -0
- package/assets/wordmark-white.png +0 -0
- package/build.js +192 -0
- package/build_example.js +91 -0
- package/build_infinite.js +283 -0
- package/docs/LAYOUTS.md +86 -0
- package/docs/PIPELINE.md +116 -0
- package/docs/SPEC.md +89 -0
- package/embed_fonts.py +193 -0
- package/examples/template.json +164 -0
- package/fonts/Geist-Bold.ttf +0 -0
- package/fonts/Geist-BoldItalic.ttf +0 -0
- package/fonts/Geist-ExtraBold.ttf +0 -0
- package/fonts/Geist-ExtraBoldItalic.ttf +0 -0
- package/fonts/Geist-Italic.ttf +0 -0
- package/fonts/Geist-Light.ttf +0 -0
- package/fonts/Geist-LightItalic.ttf +0 -0
- package/fonts/Geist-Medium.ttf +0 -0
- package/fonts/Geist-MediumItalic.ttf +0 -0
- package/fonts/Geist-Regular.ttf +0 -0
- package/fonts/Geist-SemiBold.ttf +0 -0
- package/fonts/Geist-SemiBoldItalic.ttf +0 -0
- package/lib/README.md +23 -0
- package/lib/brand.js +51 -0
- package/lib/glyphs.js +87 -0
- package/lib/layouts_client.js +212 -0
- package/lib/layouts_consult.js +301 -0
- package/lib/layouts_dense.js +361 -0
- package/lib/measure.js +109 -0
- package/lib/pdf.js +517 -0
- package/lib/plan.js +305 -0
- package/lib/render_pdf.js +203 -0
- package/lib/schema.js +399 -0
- package/lib/shared.js +15 -0
- package/lib/trace.js +88 -0
- package/lib/ttf.py +167 -0
- package/lib/wandxml.py +37 -0
- package/package.json +17 -0
- package/preview/preview.css +112 -0
- package/preview/preview.html +24 -0
- package/preview/preview.js +169 -0
- package/qa_coverage.py +120 -0
- package/requirements.txt +9 -0
- package/tools/extract.py +199 -0
- package/wand.js +397 -0
- package/wand_kit.js +1016 -0
package/lib/wandxml.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
wandxml.py — XML parsing that works with nothing installed.
|
|
3
|
+
|
|
4
|
+
The kit has to run wherever someone unzips it. `defusedxml` is the better
|
|
5
|
+
parser and we use it when it's there, but requiring a pip install to get past
|
|
6
|
+
Phase 0 means the engine is unusable in exactly the situation it was built for:
|
|
7
|
+
a fresh container with a zip and a .pptx in it.
|
|
8
|
+
|
|
9
|
+
The stdlib fallback is guarded against the one attack that matters for a parser
|
|
10
|
+
pointed at an untrusted .pptx — entity expansion — by refusing any document with
|
|
11
|
+
a DOCTYPE. Office files never have one.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from defusedxml import minidom as _md
|
|
16
|
+
HARDENED = True
|
|
17
|
+
except ImportError: # stdlib fallback
|
|
18
|
+
from xml.dom import minidom as _md
|
|
19
|
+
HARDENED = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnsafeXML(ValueError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse(data):
|
|
27
|
+
"""Parse .pptx part bytes (or str) into a minidom Document."""
|
|
28
|
+
probe = data[:8192]
|
|
29
|
+
if isinstance(probe, bytes):
|
|
30
|
+
probe = probe.decode("utf-8", "ignore")
|
|
31
|
+
if not HARDENED and ("<!DOCTYPE" in probe or "<!ENTITY" in probe):
|
|
32
|
+
raise UnsafeXML(
|
|
33
|
+
"document declares a DOCTYPE or ENTITY. Office parts never do. "
|
|
34
|
+
"Refusing to parse it with the stdlib parser — `pip install defusedxml` "
|
|
35
|
+
"if this file is genuinely trusted."
|
|
36
|
+
)
|
|
37
|
+
return _md.parseString(data)
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wand-decks-kit",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Rebuilds or authors a client-ready .pptx on the Wand design system: measured fit, embedded Geist, verbatim content.",
|
|
5
|
+
"license": "SEE LICENSE IN PROMPT.md",
|
|
6
|
+
"bin": {
|
|
7
|
+
"wand-decks-kit": "./wand.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./wand_kit.js",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"fontkit": "^2.0.0",
|
|
12
|
+
"pptxgenjs": "^3.12.0"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"doctor": "node wand.js doctor"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* preview.css — the Wand design system as CSS.
|
|
2
|
+
Tokens mirror wand_kit.js §2 exactly. If you change one, change both. */
|
|
3
|
+
|
|
4
|
+
/* @font-face is injected by preview.js - the path to brand/fonts depends on
|
|
5
|
+
whether the kit is running from the repo or from a packaged zip. */
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
--accent: #8B7CF6;
|
|
9
|
+
--accent-deep: #6C5CE7;
|
|
10
|
+
--accent-soft: #A99CF8;
|
|
11
|
+
--accent-pale: #C4B5F4;
|
|
12
|
+
|
|
13
|
+
--bg-dark: #0F0C26;
|
|
14
|
+
--ink-dark: #F3F1FA;
|
|
15
|
+
--body-dark: #B7B0DC;
|
|
16
|
+
--body-dark-alt: #CCC0F2;
|
|
17
|
+
--card-stroke: #2E2757;
|
|
18
|
+
--card-solid: #2C2460;
|
|
19
|
+
--card-muted: #241D4F;
|
|
20
|
+
--card-muted-stroke: #4A3F8C;
|
|
21
|
+
--card-emph-flat: #352B70;
|
|
22
|
+
|
|
23
|
+
--bg-light: #F8F6FB;
|
|
24
|
+
--ink-light: #1D1A33;
|
|
25
|
+
--body-light: #5C5779;
|
|
26
|
+
--muted-light: #555077;
|
|
27
|
+
--surface: #EBE8F8;
|
|
28
|
+
--track: #E9E6F5;
|
|
29
|
+
--divider: #E4E0F2;
|
|
30
|
+
|
|
31
|
+
/* 1 inch on the 10" canvas, in px. Set by preview.js from the zoom control. */
|
|
32
|
+
--in: 96px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
* { box-sizing: border-box; }
|
|
36
|
+
|
|
37
|
+
body {
|
|
38
|
+
margin: 0;
|
|
39
|
+
padding: 28px 24px 64px;
|
|
40
|
+
background: #14121f;
|
|
41
|
+
color: #cfc9e6;
|
|
42
|
+
font-family: "Geist", system-ui, sans-serif;
|
|
43
|
+
font-size: 13px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
header {
|
|
47
|
+
position: sticky; top: 0; z-index: 20;
|
|
48
|
+
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
|
|
49
|
+
margin: -28px -24px 24px; padding: 12px 24px;
|
|
50
|
+
background: rgba(20, 18, 31, .92);
|
|
51
|
+
backdrop-filter: blur(8px);
|
|
52
|
+
border-bottom: 1px solid #2a2540;
|
|
53
|
+
}
|
|
54
|
+
header h1 { margin: 0; font-family: "Geist SemiBold"; font-size: 14px; color: #fff; letter-spacing: .01em; }
|
|
55
|
+
header .spacer { flex: 1; }
|
|
56
|
+
header label { display: flex; align-items: center; gap: 6px; color: #9b93bd; font-size: 12px; cursor: pointer; }
|
|
57
|
+
header input[type="range"] { width: 120px; accent-color: var(--accent); }
|
|
58
|
+
|
|
59
|
+
.errors {
|
|
60
|
+
margin: 0 0 20px; padding: 10px 14px;
|
|
61
|
+
background: #3a1d2c; border: 1px solid #7a3050; border-radius: 8px;
|
|
62
|
+
color: #ffb3c8; font-size: 12px; white-space: pre-wrap;
|
|
63
|
+
}
|
|
64
|
+
.errors:empty { display: none; }
|
|
65
|
+
|
|
66
|
+
.deck { display: flex; flex-direction: column; align-items: center; gap: 28px; }
|
|
67
|
+
|
|
68
|
+
.slide-wrap { position: relative; }
|
|
69
|
+
|
|
70
|
+
.slide {
|
|
71
|
+
position: relative;
|
|
72
|
+
width: calc(10 * var(--in));
|
|
73
|
+
height: calc(5.625 * var(--in));
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
border-radius: 4px;
|
|
76
|
+
box-shadow: 0 8px 30px rgba(0,0,0,.45);
|
|
77
|
+
}
|
|
78
|
+
.slide.dark { background: var(--bg-dark); }
|
|
79
|
+
.slide.light { background: var(--bg-light); }
|
|
80
|
+
|
|
81
|
+
.slide-no {
|
|
82
|
+
position: absolute; top: 0; left: calc(-1 * var(--in) * .52);
|
|
83
|
+
font-family: "Geist SemiBold"; font-size: 12px; color: #5d5680;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* every traced shape is absolutely positioned in inches */
|
|
87
|
+
.sh { position: absolute; }
|
|
88
|
+
.sh.text { display: flex; white-space: pre-wrap; overflow: visible; }
|
|
89
|
+
.sh.text > span { display: block; width: 100%; }
|
|
90
|
+
|
|
91
|
+
.sh.rect { border-radius: 0; }
|
|
92
|
+
.sh.roundRect { border-radius: calc(var(--in) * .10); }
|
|
93
|
+
.sh.ellipse { border-radius: 50%; }
|
|
94
|
+
|
|
95
|
+
.sh.image { object-fit: contain; }
|
|
96
|
+
|
|
97
|
+
/* overflow flagging, driven by the same measurement the build uses */
|
|
98
|
+
.slide.flag .sh.overflow { outline: 1.5px solid #ff5c8a; outline-offset: 1px; }
|
|
99
|
+
.slide.flag .sh.overflow::after {
|
|
100
|
+
content: "▲"; position: absolute; top: -9px; right: -9px;
|
|
101
|
+
font-size: 10px; color: #ff5c8a;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* the §1 layout rails, toggled on to check alignment */
|
|
105
|
+
.grid-overlay { position: absolute; inset: 0; pointer-events: none; display: none; }
|
|
106
|
+
.slide.grid .grid-overlay { display: block; }
|
|
107
|
+
.grid-overlay i {
|
|
108
|
+
position: absolute; background: rgba(139,124,246,.38);
|
|
109
|
+
}
|
|
110
|
+
.grid-overlay i.v { top: 0; bottom: 0; width: 1px; }
|
|
111
|
+
.grid-overlay i.h { left: 0; right: 0; height: 1px; }
|
|
112
|
+
.grid-overlay i.hot { background: rgba(255,92,138,.55); }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Wand deck preview</title>
|
|
6
|
+
<link rel="stylesheet" href="preview.css">
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
|
|
10
|
+
<header>
|
|
11
|
+
<h1 id="title">Wand deck preview</h1>
|
|
12
|
+
<div class="spacer"></div>
|
|
13
|
+
<label><input type="checkbox" id="flag" checked> flag overflow</label>
|
|
14
|
+
<label><input type="checkbox" id="grid"> §1 rails</label>
|
|
15
|
+
<label>zoom <input type="range" id="zoom" min="60" max="170" value="96"></label>
|
|
16
|
+
</header>
|
|
17
|
+
|
|
18
|
+
<pre class="errors" id="errors"></pre>
|
|
19
|
+
<div class="deck" id="deck"></div>
|
|
20
|
+
|
|
21
|
+
<script src="trace.js"></script>
|
|
22
|
+
<script src="preview.js"></script>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/* preview.js — render a shape trace to the DOM at the 10" x 5.625" grid.
|
|
2
|
+
Knows nothing about layouts: it draws whatever wand_kit recorded, so the
|
|
3
|
+
preview and the .pptx cannot disagree about geometry. */
|
|
4
|
+
|
|
5
|
+
(function () {
|
|
6
|
+
const T = window.__WAND_TRACE__;
|
|
7
|
+
const deck = document.getElementById("deck");
|
|
8
|
+
const errBox = document.getElementById("errors");
|
|
9
|
+
const zoom = document.getElementById("zoom");
|
|
10
|
+
const flag = document.getElementById("flag");
|
|
11
|
+
const grid = document.getElementById("grid");
|
|
12
|
+
|
|
13
|
+
// @font-face lives here, not in preview.css: the path to the faces depends on
|
|
14
|
+
// where brand/ resolved, and CSS cannot be told. One source of truth.
|
|
15
|
+
const FACES = [
|
|
16
|
+
["Geist", "Geist-Regular", 400, "normal"], ["Geist", "Geist-Bold", 700, "normal"],
|
|
17
|
+
["Geist", "Geist-Italic", 400, "italic"], ["Geist", "Geist-BoldItalic", 700, "italic"],
|
|
18
|
+
["Geist Medium", "Geist-Medium", 400, "normal"], ["Geist Medium", "Geist-MediumItalic", 400, "italic"],
|
|
19
|
+
["Geist SemiBold", "Geist-SemiBold", 400, "normal"], ["Geist SemiBold", "Geist-SemiBoldItalic", 400, "italic"],
|
|
20
|
+
["Geist ExtraBold", "Geist-ExtraBold", 400, "normal"], ["Geist ExtraBold", "Geist-ExtraBoldItalic", 400, "italic"],
|
|
21
|
+
["Geist Light", "Geist-Light", 400, "normal"], ["Geist Light", "Geist-LightItalic", 400, "italic"],
|
|
22
|
+
];
|
|
23
|
+
const base = T.fontsBase || "../fonts/";
|
|
24
|
+
document.head.appendChild(Object.assign(document.createElement("style"), {
|
|
25
|
+
textContent: FACES.map(([fam, file, w, st]) =>
|
|
26
|
+
`@font-face{font-family:"${fam}";src:url("${base}${file}.ttf");font-weight:${w};font-style:${st};}`).join("\n"),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
document.getElementById("title").textContent = T.title || "Wand deck preview";
|
|
30
|
+
if (T.errors && T.errors.length) errBox.textContent = T.errors.join("\n");
|
|
31
|
+
|
|
32
|
+
const inch = (v) => `calc(${v} * var(--in))`;
|
|
33
|
+
const hex = (c) => (typeof c === "string" ? "#" + c.replace(/^#/, "") : null);
|
|
34
|
+
|
|
35
|
+
/** pptxgenjs fill/line objects -> CSS */
|
|
36
|
+
function paint(el, o) {
|
|
37
|
+
if (o.fill && o.fill.color) {
|
|
38
|
+
const a = o.fill.transparency ? 1 - o.fill.transparency / 100 : 1;
|
|
39
|
+
el.style.background = a < 1 ? withAlpha(o.fill.color, a) : hex(o.fill.color);
|
|
40
|
+
}
|
|
41
|
+
if (o.line && o.line.width) {
|
|
42
|
+
const style = o.line.dashType === "dash" ? "dashed" : "solid";
|
|
43
|
+
el.style.border = `${o.line.width}px ${style} ${hex(o.line.color || "000000")}`;
|
|
44
|
+
}
|
|
45
|
+
if (o.rectRadius != null) el.style.borderRadius = inch(o.rectRadius);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function withAlpha(c, a) {
|
|
49
|
+
const h = c.replace(/^#/, "");
|
|
50
|
+
const n = parseInt(h, 16);
|
|
51
|
+
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a.toFixed(3)})`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ALIGN = { left: "flex-start", center: "center", right: "flex-end" };
|
|
55
|
+
const VALIGN = { top: "flex-start", middle: "center", bottom: "flex-end" };
|
|
56
|
+
|
|
57
|
+
function textEl(sh) {
|
|
58
|
+
const el = document.createElement("div");
|
|
59
|
+
el.className = "sh text";
|
|
60
|
+
const first = (sh.runs[0] && sh.runs[0].options) || {};
|
|
61
|
+
el.style.justifyContent = ALIGN[sh.align || "left"];
|
|
62
|
+
el.style.alignItems = VALIGN[sh.valign || "top"];
|
|
63
|
+
el.style.lineHeight = String(sh.lineSpacingMultiple || 1.2);
|
|
64
|
+
const span = document.createElement("span");
|
|
65
|
+
span.style.textAlign = sh.align || "left";
|
|
66
|
+
|
|
67
|
+
sh.runs.forEach((r) => {
|
|
68
|
+
const o = r.options || {};
|
|
69
|
+
const s = document.createElement("span");
|
|
70
|
+
s.textContent = r.text;
|
|
71
|
+
s.style.fontFamily = `"${o.fontFace || sh.fontFace || "Geist"}"`;
|
|
72
|
+
s.style.fontSize = inch((o.fontSize || sh.fontSize || 12) / 72);
|
|
73
|
+
s.style.color = hex(o.color || sh.color || "000000");
|
|
74
|
+
if (o.bold ?? sh.bold) s.style.fontWeight = "700";
|
|
75
|
+
if (o.italic ?? sh.italic) s.style.fontStyle = "italic";
|
|
76
|
+
span.appendChild(s);
|
|
77
|
+
if (o.breakLine) span.appendChild(document.createElement("br"));
|
|
78
|
+
if (o.bullet) { s.textContent = "▪ " + r.text; s.style.display = "block"; }
|
|
79
|
+
});
|
|
80
|
+
el.appendChild(span);
|
|
81
|
+
el.dataset.size = String(first.fontSize || sh.fontSize || 12);
|
|
82
|
+
return el;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function render() {
|
|
86
|
+
deck.innerHTML = "";
|
|
87
|
+
T.slides.forEach((sl) => {
|
|
88
|
+
const wrap = document.createElement("div");
|
|
89
|
+
wrap.className = "slide-wrap";
|
|
90
|
+
|
|
91
|
+
const no = document.createElement("div");
|
|
92
|
+
no.className = "slide-no";
|
|
93
|
+
no.textContent = String(sl.index).padStart(2, "0");
|
|
94
|
+
wrap.appendChild(no);
|
|
95
|
+
|
|
96
|
+
const s = document.createElement("div");
|
|
97
|
+
const bg = sl.background && sl.background.color;
|
|
98
|
+
s.className = "slide " + (bg && bg.toUpperCase() === "0F0C26" ? "dark" : "light");
|
|
99
|
+
if (flag.checked) s.classList.add("flag");
|
|
100
|
+
if (grid.checked) s.classList.add("grid");
|
|
101
|
+
s.title = sl.layout;
|
|
102
|
+
|
|
103
|
+
sl.shapes.forEach((sh) => {
|
|
104
|
+
let el;
|
|
105
|
+
if (sh.kind === "image") {
|
|
106
|
+
el = document.createElement("img");
|
|
107
|
+
el.className = "sh image";
|
|
108
|
+
// a named glyph arrives as an inline data URI, a legacy PNG as a path
|
|
109
|
+
el.src = sh.data ? "data:" + sh.data
|
|
110
|
+
: sh.path ? (T.assetsBase || "../assets/") + sh.path.split(/[\\/]assets[\\/]/).pop()
|
|
111
|
+
: "";
|
|
112
|
+
} else if (sh.kind === "text") {
|
|
113
|
+
el = textEl(sh);
|
|
114
|
+
} else {
|
|
115
|
+
el = document.createElement("div");
|
|
116
|
+
el.className = "sh " + (sh.type || "rect");
|
|
117
|
+
paint(el, sh);
|
|
118
|
+
}
|
|
119
|
+
el.style.left = inch(sh.x || 0);
|
|
120
|
+
el.style.top = inch(sh.y || 0);
|
|
121
|
+
el.style.width = inch(sh.w || 0);
|
|
122
|
+
el.style.height = inch(sh.h || 0);
|
|
123
|
+
if (sh.kind === "text" && sh.margin === 0) el.style.padding = "0";
|
|
124
|
+
s.appendChild(el);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// §1 rails
|
|
128
|
+
const g = document.createElement("div");
|
|
129
|
+
g.className = "grid-overlay";
|
|
130
|
+
[[0.37, 1], [9.62, 1], [3.58, 0], [6.78, 0]].forEach(([x, hot]) => {
|
|
131
|
+
const i = document.createElement("i");
|
|
132
|
+
i.className = "v" + (hot ? " hot" : "");
|
|
133
|
+
i.style.left = inch(x); g.appendChild(i);
|
|
134
|
+
});
|
|
135
|
+
[[0.62, 1], [1.60, 0], [5.05, 1], [5.21, 0]].forEach(([y, hot]) => {
|
|
136
|
+
const i = document.createElement("i");
|
|
137
|
+
i.className = "h" + (hot ? " hot" : "");
|
|
138
|
+
i.style.top = inch(y); g.appendChild(i);
|
|
139
|
+
});
|
|
140
|
+
s.appendChild(g);
|
|
141
|
+
|
|
142
|
+
wrap.appendChild(s);
|
|
143
|
+
deck.appendChild(wrap);
|
|
144
|
+
if (flag.checked) markOverflow(s);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Browser-side overflow detection: does the laid-out text exceed its box? */
|
|
149
|
+
function markOverflow(slideEl) {
|
|
150
|
+
slideEl.querySelectorAll(".sh.text").forEach((el) => {
|
|
151
|
+
const span = el.firstElementChild;
|
|
152
|
+
if (!span) return;
|
|
153
|
+
if (span.scrollHeight - el.clientHeight > 1.5 || span.scrollWidth - el.clientWidth > 1.5) {
|
|
154
|
+
el.classList.add("overflow");
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function setZoom() {
|
|
160
|
+
document.documentElement.style.setProperty("--in", zoom.value + "px");
|
|
161
|
+
if (flag.checked) document.querySelectorAll(".slide").forEach(markOverflow);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
zoom.addEventListener("input", setZoom);
|
|
165
|
+
flag.addEventListener("change", render);
|
|
166
|
+
grid.addEventListener("change", render);
|
|
167
|
+
setZoom();
|
|
168
|
+
render();
|
|
169
|
+
})();
|
package/qa_coverage.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
qa_coverage.py — makes "faithful = verbatim" enforceable instead of aspirational.
|
|
4
|
+
|
|
5
|
+
python qa_coverage.py visible_dump.txt output.pptx [--min 4] [--json report.json]
|
|
6
|
+
|
|
7
|
+
Reads every string from the Phase 1 verbatim dump, reads every string out of the
|
|
8
|
+
built deck, and reports what didn't make it. Exit 1 if anything is missing, so it
|
|
9
|
+
can gate a build.
|
|
10
|
+
|
|
11
|
+
Matching is whitespace- and case-normalised, and normalises the typographic
|
|
12
|
+
characters that get swapped in transit (curly quotes, dashes, non-breaking
|
|
13
|
+
spaces). It is deliberately NOT fuzzy beyond that: a string that was reworded is
|
|
14
|
+
supposed to fail.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse, json, os, re, sys, unicodedata, zipfile
|
|
18
|
+
|
|
19
|
+
def _brand_lib():
|
|
20
|
+
"""brand/lib in the repo, or lib/ beside the kit once packaged."""
|
|
21
|
+
import os
|
|
22
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
23
|
+
for d in filter(None, [os.environ.get("WAND_BRAND_DIR"),
|
|
24
|
+
os.path.join(here, "lib"),
|
|
25
|
+
os.path.join(here, "..", "lib"),
|
|
26
|
+
os.path.join(here, "..", "..", "..", "..", "brand", "lib"),
|
|
27
|
+
os.path.join(here, "..", "..", "..", "brand", "lib")]):
|
|
28
|
+
if os.path.isfile(os.path.join(d, "wandxml.py")):
|
|
29
|
+
return os.path.normpath(d)
|
|
30
|
+
raise SystemExit("brand/lib not found")
|
|
31
|
+
|
|
32
|
+
sys.path.insert(0, _brand_lib())
|
|
33
|
+
import wandxml
|
|
34
|
+
|
|
35
|
+
TRANS = str.maketrans({
|
|
36
|
+
"\u2018": "'", "\u2019": "'", "\u201c": '"', "\u201d": '"',
|
|
37
|
+
"\u2013": "-", "\u2014": "-", "\u2212": "-", "\u00a0": " ",
|
|
38
|
+
"\u2026": "...", "\u00b7": "·",
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def norm(s):
|
|
43
|
+
s = unicodedata.normalize("NFKC", s).translate(TRANS)
|
|
44
|
+
return re.sub(r"\s+", " ", s).strip().lower()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def strings_from_pptx(path):
|
|
48
|
+
out = []
|
|
49
|
+
with zipfile.ZipFile(path) as z:
|
|
50
|
+
names = sorted((n for n in z.namelist()
|
|
51
|
+
if re.fullmatch(r"ppt/slides/slide\d+\.xml", n)),
|
|
52
|
+
key=lambda n: int(re.findall(r"\d+", n)[0]))
|
|
53
|
+
for i, n in enumerate(names, 1):
|
|
54
|
+
doc = wandxml.parse(z.read(n))
|
|
55
|
+
for sp in doc.getElementsByTagName("p:txBody"):
|
|
56
|
+
# join runs within a paragraph, paragraphs with a space — a line
|
|
57
|
+
# break in the deck is a space in the source string
|
|
58
|
+
paras = []
|
|
59
|
+
for p in sp.getElementsByTagName("a:p"):
|
|
60
|
+
runs = [t.firstChild.nodeValue for t in p.getElementsByTagName("a:t") if t.firstChild]
|
|
61
|
+
if runs:
|
|
62
|
+
paras.append("".join(runs))
|
|
63
|
+
if paras:
|
|
64
|
+
out.append((i, " ".join(paras)))
|
|
65
|
+
return out
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main():
|
|
69
|
+
ap = argparse.ArgumentParser()
|
|
70
|
+
ap.add_argument("dump")
|
|
71
|
+
ap.add_argument("pptx")
|
|
72
|
+
ap.add_argument("--min", type=int, default=4,
|
|
73
|
+
help="ignore source strings shorter than this (default 4)")
|
|
74
|
+
ap.add_argument("--json")
|
|
75
|
+
a = ap.parse_args()
|
|
76
|
+
|
|
77
|
+
source = [l.rstrip("\n") for l in open(a.dump, encoding="utf-8")]
|
|
78
|
+
short = [s for s in source if 0 < len(s.strip()) < a.min]
|
|
79
|
+
source = [s for s in source if len(s.strip()) >= a.min]
|
|
80
|
+
|
|
81
|
+
built = strings_from_pptx(a.pptx)
|
|
82
|
+
haystack = norm(" \u241e ".join(t for _, t in built))
|
|
83
|
+
per_slide = [(i, norm(t)) for i, t in built]
|
|
84
|
+
|
|
85
|
+
missing, found = [], []
|
|
86
|
+
for s in source:
|
|
87
|
+
n = norm(s)
|
|
88
|
+
if not n:
|
|
89
|
+
continue
|
|
90
|
+
if n in haystack:
|
|
91
|
+
where = next((i for i, t in per_slide if n in t), None)
|
|
92
|
+
found.append((s, where))
|
|
93
|
+
else:
|
|
94
|
+
missing.append(s)
|
|
95
|
+
|
|
96
|
+
total = len(found) + len(missing)
|
|
97
|
+
pct = (100.0 * len(found) / total) if total else 100.0
|
|
98
|
+
print(f"coverage: {len(found)}/{total} strings ({pct:.1f}%) across {len(set(i for i,_ in built))} slides")
|
|
99
|
+
if short:
|
|
100
|
+
# Say this out loud. Stat values and ordinals are often 1-3 characters,
|
|
101
|
+
# so a silent length filter is a coverage number that flatters itself.
|
|
102
|
+
missed = [s for s in short if norm(s) not in haystack]
|
|
103
|
+
print(f" {len(short)} string(s) under {a.min} chars not compared" +
|
|
104
|
+
(f" - {len(missed)} of them are absent from the deck: " +
|
|
105
|
+
", ".join(repr(s.strip()) for s in missed[:8]) if missed else " - all present anyway"))
|
|
106
|
+
|
|
107
|
+
if missing:
|
|
108
|
+
print(f"\nMISSING ({len(missing)}) — each must land on a slide or be listed in the Review Note:")
|
|
109
|
+
for s in missing:
|
|
110
|
+
print(" ·", s[:110] + ("…" if len(s) > 110 else ""))
|
|
111
|
+
|
|
112
|
+
if a.json:
|
|
113
|
+
json.dump({"total": total, "found": len(found), "missing": missing},
|
|
114
|
+
open(a.json, "w"), indent=2, ensure_ascii=False)
|
|
115
|
+
|
|
116
|
+
sys.exit(1 if missing else 0)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
main()
|
package/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Optional. The kit runs with nothing installed.
|
|
2
|
+
#
|
|
3
|
+
# pylib/wandxml.py falls back to stdlib xml.dom.minidom, and pylib/ttf.py reads
|
|
4
|
+
# and rewrites the font tables embed_fonts.py needs without fontTools. Install
|
|
5
|
+
# these only if you want the hardened XML parser or are working on the font code.
|
|
6
|
+
#
|
|
7
|
+
# pip install -r requirements.txt
|
|
8
|
+
defusedxml>=0.7 # tools/extract.py, qa_coverage.py - entity-safe XML
|
|
9
|
+
fonttools>=4.50 # cross-checking pylib/ttf.py output
|