deckrun 1.3.0 → 1.6.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/dist/lint.js ADDED
@@ -0,0 +1,218 @@
1
+ export function lintMarkdown(markdown) {
2
+ const issues = [];
3
+ const trimmed = markdown.trim();
4
+ if (!trimmed) {
5
+ issues.push({
6
+ rule: "empty-deck",
7
+ severity: "error",
8
+ message: "The deck is empty.",
9
+ line: 1,
10
+ column: 1,
11
+ });
12
+ return {
13
+ slides: 0,
14
+ errors: 1,
15
+ warnings: 0,
16
+ issues,
17
+ };
18
+ }
19
+ const lines = markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
20
+ const slides = [];
21
+ let curSlideLines = [];
22
+ let curStartLine = 1;
23
+ let slideIndex = 1;
24
+ for (let i = 0; i < lines.length; i++) {
25
+ const line = lines[i];
26
+ if (/^[ \t]*---[ \t]*$/.test(line)) {
27
+ slides.push({
28
+ slideIndex,
29
+ startLine: curStartLine,
30
+ endLine: i,
31
+ lines: curSlideLines,
32
+ });
33
+ slideIndex++;
34
+ curStartLine = i + 2;
35
+ curSlideLines = [];
36
+ }
37
+ else {
38
+ curSlideLines.push(line);
39
+ }
40
+ }
41
+ slides.push({
42
+ slideIndex,
43
+ startLine: curStartLine,
44
+ endLine: lines.length,
45
+ lines: curSlideLines,
46
+ });
47
+ // Global & Slide checks
48
+ let inCodeFence = false;
49
+ let fenceStartLine = 1;
50
+ let fenceStartCol = 1;
51
+ let inDisplayMath = false;
52
+ let mathStartLine = 1;
53
+ let mathStartCol = 1;
54
+ for (let i = 0; i < lines.length; i++) {
55
+ const lineNum = i + 1;
56
+ const line = lines[i];
57
+ // Determine current slide number
58
+ const currentSlide = slides.find((s) => lineNum >= s.startLine && lineNum <= s.endLine)?.slideIndex ??
59
+ 1;
60
+ // Check code fences
61
+ const fenceMatch = line.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
62
+ if (fenceMatch) {
63
+ if (!inCodeFence) {
64
+ inCodeFence = true;
65
+ fenceStartLine = lineNum;
66
+ fenceStartCol = fenceMatch[1].length + 1;
67
+ const tag = fenceMatch[3].trim();
68
+ if (!tag) {
69
+ issues.push({
70
+ rule: "untagged-code-fence",
71
+ severity: "warning",
72
+ message: "Code fence has no language tag for syntax highlighting.",
73
+ line: lineNum,
74
+ column: fenceStartCol,
75
+ slide: currentSlide,
76
+ });
77
+ }
78
+ }
79
+ else {
80
+ inCodeFence = false;
81
+ }
82
+ }
83
+ // Check display math
84
+ if (!inCodeFence) {
85
+ if (/^\s*\$\$\s*$/.test(line) || /^\s*\\\[\s*$/.test(line)) {
86
+ if (!inDisplayMath) {
87
+ inDisplayMath = true;
88
+ mathStartLine = lineNum;
89
+ mathStartCol = 1;
90
+ }
91
+ else {
92
+ inDisplayMath = false;
93
+ }
94
+ }
95
+ else if (line.includes("$$")) {
96
+ const occurrences = (line.match(/\$\$/g) || []).length;
97
+ if (occurrences % 2 !== 0) {
98
+ inDisplayMath = !inDisplayMath;
99
+ if (inDisplayMath) {
100
+ mathStartLine = lineNum;
101
+ mathStartCol = line.indexOf("$$") + 1;
102
+ }
103
+ }
104
+ }
105
+ // Check headings
106
+ const headingMatch = line.match(/^(\s*#{1,6}\s+)(.*)$/);
107
+ if (headingMatch && headingMatch[2].length > 80) {
108
+ issues.push({
109
+ rule: "long-heading",
110
+ severity: "warning",
111
+ message: `Heading is ${headingMatch[2].length} characters long; consider shortening for presentation readability.`,
112
+ line: lineNum,
113
+ column: headingMatch[1].length + 1,
114
+ slide: currentSlide,
115
+ });
116
+ }
117
+ // Check image directives
118
+ const imgRegex = /!\[([^\]]*)\]\(([^\s)]+)(?:\s+"([^"]*)")?\)/g;
119
+ let imgMatch;
120
+ while ((imgMatch = imgRegex.exec(line)) !== null) {
121
+ const alt = imgMatch[1].trim();
122
+ const title = imgMatch[3] ?? "";
123
+ const col = imgMatch.index + 1;
124
+ if (!alt) {
125
+ issues.push({
126
+ rule: "missing-image-alt",
127
+ severity: "warning",
128
+ message: "Image is missing alt text.",
129
+ line: lineNum,
130
+ column: col,
131
+ slide: currentSlide,
132
+ });
133
+ }
134
+ if (title) {
135
+ const opMatch = title.toLowerCase().match(/opacity[=:]?\s*([^\s"]+)/);
136
+ if (opMatch) {
137
+ const val = parseFloat(opMatch[1]);
138
+ if (isNaN(val) || val < 0 || val > 1) {
139
+ issues.push({
140
+ rule: "invalid-image-opacity",
141
+ severity: "warning",
142
+ message: `Invalid image opacity '${opMatch[1]}'; expected a number between 0 and 1.`,
143
+ line: lineNum,
144
+ column: col,
145
+ slide: currentSlide,
146
+ });
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ if (inCodeFence) {
154
+ issues.push({
155
+ rule: "unclosed-code-fence",
156
+ severity: "error",
157
+ message: "Code fence was opened but never closed.",
158
+ line: fenceStartLine,
159
+ column: fenceStartCol,
160
+ });
161
+ }
162
+ if (inDisplayMath) {
163
+ issues.push({
164
+ rule: "unclosed-math",
165
+ severity: "error",
166
+ message: "Display math block was opened but never closed.",
167
+ line: mathStartLine,
168
+ column: mathStartCol,
169
+ });
170
+ }
171
+ // Per-slide checks
172
+ for (const s of slides) {
173
+ const slideContent = s.lines.join("\n").trim();
174
+ if (!slideContent) {
175
+ issues.push({
176
+ rule: "empty-slide",
177
+ severity: "warning",
178
+ message: `Slide ${s.slideIndex} is empty.`,
179
+ line: s.startLine,
180
+ column: 1,
181
+ slide: s.slideIndex,
182
+ });
183
+ continue;
184
+ }
185
+ // Check bullet density
186
+ const bullets = s.lines.filter((l) => /^\s*([-*+]|\d+[.)])\s+/.test(l));
187
+ if (bullets.length > 8) {
188
+ issues.push({
189
+ rule: "dense-slide",
190
+ severity: "warning",
191
+ message: `Slide ${s.slideIndex} has ${bullets.length} bullets (recommended maximum is 8).`,
192
+ line: s.startLine,
193
+ column: 1,
194
+ slide: s.slideIndex,
195
+ });
196
+ }
197
+ // Check reveal markers
198
+ const revealCount = (slideContent.match(/\{reveal\}/g) || []).length;
199
+ if (revealCount > 10) {
200
+ issues.push({
201
+ rule: "reveal-excessive",
202
+ severity: "warning",
203
+ message: `Slide ${s.slideIndex} has ${revealCount} reveal markers (recommended maximum is 10).`,
204
+ line: s.startLine,
205
+ column: 1,
206
+ slide: s.slideIndex,
207
+ });
208
+ }
209
+ }
210
+ const errors = issues.filter((i) => i.severity === "error").length;
211
+ const warnings = issues.filter((i) => i.severity === "warning").length;
212
+ return {
213
+ slides: slides.length,
214
+ errors,
215
+ warnings,
216
+ issues,
217
+ };
218
+ }
package/dist/parser.js CHANGED
@@ -1,4 +1,89 @@
1
1
  import { marked } from "marked";
2
+ function escapeHtml(value) {
3
+ return value
4
+ .replace(/&/g, "&amp;")
5
+ .replace(/</g, "&lt;")
6
+ .replace(/>/g, "&gt;")
7
+ .replace(/"/g, "&quot;");
8
+ }
9
+ /**
10
+ * Capture TeX before the regular Markdown tokenizer sees it. This keeps
11
+ * operators such as `*` and `_` inside a formula instead of turning them into
12
+ * emphasis. The browser can then render these deliberately marked nodes with
13
+ * KaTeX after fonts and layout styles are available.
14
+ */
15
+ marked.use({
16
+ extensions: [
17
+ {
18
+ name: "deckrunBlockMath",
19
+ level: "block",
20
+ tokenizer(src) {
21
+ const dollars = /^\$\$[ \t]*\n?([\s\S]+?)\n?[ \t]*\$\$(?:[ \t]*(?:\n|$))/.exec(src);
22
+ const brackets = /^\\\[[ \t]*\n?([\s\S]+?)\n?[ \t]*\\\](?:[ \t]*(?:\n|$))/.exec(src);
23
+ const match = dollars ?? brackets;
24
+ if (!match)
25
+ return;
26
+ return {
27
+ type: "deckrunBlockMath",
28
+ raw: match[0],
29
+ text: match[1].trim(),
30
+ display: true,
31
+ };
32
+ },
33
+ renderer(token) {
34
+ return `<div class="math-source" data-display="true">${escapeHtml(String(token.text))}</div>\n`;
35
+ },
36
+ },
37
+ {
38
+ name: "deckrunInlineMath",
39
+ level: "inline",
40
+ start(src) {
41
+ const dollar = src.indexOf("$");
42
+ const paren = src.indexOf("\\(");
43
+ if (dollar < 0)
44
+ return paren < 0 ? undefined : paren;
45
+ if (paren < 0)
46
+ return dollar;
47
+ return Math.min(dollar, paren);
48
+ },
49
+ tokenizer(src) {
50
+ // A closing dollar followed by a digit is treated as currency rather
51
+ // than math, so ordinary prose like "$5 and $10" stays untouched.
52
+ const dollars = /^\$(?!\s|\$)((?:\\.|[^\\$\n])*?[^\\$\s])\$(?!\$|\d)/.exec(src);
53
+ const parens = /^\\\(((?:\\.|[^\\\n])*?)\\\)/.exec(src);
54
+ const match = dollars ?? parens;
55
+ if (!match)
56
+ return;
57
+ return {
58
+ type: "deckrunInlineMath",
59
+ raw: match[0],
60
+ text: match[1],
61
+ display: false,
62
+ };
63
+ },
64
+ renderer(token) {
65
+ return `<span class="math-source" data-display="false">${escapeHtml(String(token.text))}</span>`;
66
+ },
67
+ },
68
+ {
69
+ name: "deckrunRevealMarker",
70
+ level: "inline",
71
+ start(src) {
72
+ const at = src.indexOf("{reveal}");
73
+ return at < 0 ? undefined : at;
74
+ },
75
+ tokenizer(src) {
76
+ const match = /^\{reveal\}/.exec(src);
77
+ if (!match)
78
+ return;
79
+ return { type: "deckrunRevealMarker", raw: match[0] };
80
+ },
81
+ renderer() {
82
+ return '<span class="deckrun-fragment-marker" aria-hidden="true"></span>';
83
+ },
84
+ },
85
+ ],
86
+ });
2
87
  function parseImageDirective(title) {
3
88
  if (!title)
4
89
  return { position: "inline", opacity: 1 };
package/dist/pdf.js CHANGED
@@ -161,7 +161,11 @@ export async function renderPdf(url, browser) {
161
161
  "--mute-audio",
162
162
  // Never touch the browser profile the person is actually using.
163
163
  `--user-data-dir=${join(dir, "profile")}`,
164
- "--virtual-time-budget=5000",
164
+ // Mermaid performs an asynchronous layout pass after its local script has
165
+ // loaded. Give that pass room to settle and flush every compositor stage
166
+ // before Chrome snapshots the pages.
167
+ "--virtual-time-budget=10000",
168
+ "--run-all-compositor-stages-before-draw",
165
169
  // Header/footer flag names differ across versions; unknown switches are ignored.
166
170
  "--no-pdf-header-footer",
167
171
  "--print-to-pdf-no-header",
@@ -0,0 +1,287 @@
1
+ export const DEFAULT_TEMPLATE = "classic";
2
+ export const DEFAULT_TRANSITION = "slide";
3
+ export const TEMPLATE_IDS = [
4
+ "classic",
5
+ "minimal",
6
+ "editorial",
7
+ "spotlight",
8
+ ];
9
+ export const TRANSITION_IDS = [
10
+ "slide",
11
+ "fade",
12
+ "zoom",
13
+ "lift",
14
+ "none",
15
+ ];
16
+ export const TEMPLATE_SPECS = {
17
+ classic: {
18
+ label: "Classic",
19
+ blurb: "The original balanced deckrun layout",
20
+ },
21
+ minimal: {
22
+ label: "Minimal",
23
+ blurb: "Quiet surfaces, wider margins, fewer decorative treatments",
24
+ },
25
+ editorial: {
26
+ label: "Editorial",
27
+ blurb: "Strong rules and magazine-like reading rhythm",
28
+ },
29
+ spotlight: {
30
+ label: "Spotlight",
31
+ blurb: "Centered, high-impact composition for concise keynote slides",
32
+ },
33
+ };
34
+ export const TRANSITION_SPECS = {
35
+ slide: {
36
+ label: "Slide",
37
+ blurb: "Horizontal sliding transition between slides",
38
+ },
39
+ fade: {
40
+ label: "Fade",
41
+ blurb: "Cross-fade between slides",
42
+ },
43
+ zoom: {
44
+ label: "Zoom",
45
+ blurb: "Scale up and down between slides",
46
+ },
47
+ lift: {
48
+ label: "Lift",
49
+ blurb: "Vertical rising transition between slides",
50
+ },
51
+ none: {
52
+ label: "None",
53
+ blurb: "Instant cut between slides",
54
+ },
55
+ };
56
+ export function findTemplate(input) {
57
+ if (!input)
58
+ return null;
59
+ const key = String(input).trim().toLowerCase();
60
+ for (const id of TEMPLATE_IDS) {
61
+ if (id === key)
62
+ return id;
63
+ if (TEMPLATE_SPECS[id].label.toLowerCase() === key)
64
+ return id;
65
+ }
66
+ return null;
67
+ }
68
+ export function resolveTemplateName(input) {
69
+ return findTemplate(input) ?? DEFAULT_TEMPLATE;
70
+ }
71
+ export function templateSummaries() {
72
+ return TEMPLATE_IDS.map((id) => ({
73
+ id,
74
+ label: TEMPLATE_SPECS[id].label,
75
+ blurb: TEMPLATE_SPECS[id].blurb,
76
+ }));
77
+ }
78
+ export function templateListing() {
79
+ const pad = Math.max(...TEMPLATE_IDS.map((id) => id.length));
80
+ return TEMPLATE_IDS.map((id) => {
81
+ const s = TEMPLATE_SPECS[id];
82
+ return `${id.padEnd(pad)} ${s.label.padEnd(10)} ${s.blurb}`;
83
+ });
84
+ }
85
+ export function findTransition(input) {
86
+ if (!input)
87
+ return null;
88
+ const key = String(input).trim().toLowerCase();
89
+ for (const id of TRANSITION_IDS) {
90
+ if (id === key)
91
+ return id;
92
+ if (TRANSITION_SPECS[id].label.toLowerCase() === key)
93
+ return id;
94
+ }
95
+ return null;
96
+ }
97
+ export function resolveTransitionName(input) {
98
+ return findTransition(input) ?? DEFAULT_TRANSITION;
99
+ }
100
+ export function transitionSummaries() {
101
+ return TRANSITION_IDS.map((id) => ({
102
+ id,
103
+ label: TRANSITION_SPECS[id].label,
104
+ blurb: TRANSITION_SPECS[id].blurb,
105
+ }));
106
+ }
107
+ export function transitionListing() {
108
+ const pad = Math.max(...TRANSITION_IDS.map((id) => id.length));
109
+ return TRANSITION_IDS.map((id) => {
110
+ const s = TRANSITION_SPECS[id];
111
+ return `${id.padEnd(pad)} ${s.label.padEnd(8)} ${s.blurb}`;
112
+ });
113
+ }
114
+ export const TEMPLATE_CSS = `/* ── Templates ──────────────────────────────────────────────────────────── */
115
+
116
+ /* Minimal */
117
+ :root[data-template="minimal"] {
118
+ --slide-pad-x: 10vw;
119
+ --slide-pad-y: 8vh;
120
+ }
121
+ :root[data-template="minimal"] #backdrop {
122
+ opacity: 0.15;
123
+ }
124
+ :root[data-template="minimal"] .slide__content h1 {
125
+ letter-spacing: -0.02em;
126
+ }
127
+ :root[data-template="minimal"] blockquote {
128
+ border-left-width: 2px;
129
+ background: transparent;
130
+ }
131
+ :root[data-template="minimal"] pre {
132
+ border: 1px solid var(--surface0);
133
+ }
134
+
135
+ /* Editorial */
136
+ :root[data-template="editorial"] {
137
+ --slide-pad-x: 7vw;
138
+ }
139
+ :root[data-template="editorial"] .slide__content h1 {
140
+ border-bottom: 3px solid var(--accent);
141
+ padding-bottom: 0.3em;
142
+ margin-bottom: 0.6em;
143
+ }
144
+ :root[data-template="editorial"] .slide__content h2 {
145
+ border-bottom: 1px solid var(--surface1);
146
+ padding-bottom: 0.2em;
147
+ }
148
+ :root[data-template="editorial"] blockquote {
149
+ border-left: 4px solid var(--accent);
150
+ font-style: italic;
151
+ background: var(--surface0);
152
+ }
153
+ :root[data-template="editorial"] table th {
154
+ border-bottom: 2px solid var(--accent);
155
+ }
156
+ :root[data-template="editorial"] hr {
157
+ border: none;
158
+ border-top: 2px solid var(--surface2);
159
+ margin: 2em 0;
160
+ }
161
+
162
+ /* Spotlight */
163
+ :root[data-template="spotlight"] .slide {
164
+ justify-content: center;
165
+ align-items: center;
166
+ text-align: center;
167
+ }
168
+ :root[data-template="spotlight"] .slide__content {
169
+ display: flex;
170
+ flex-direction: column;
171
+ align-items: center;
172
+ justify-content: center;
173
+ text-align: center;
174
+ }
175
+ :root[data-template="spotlight"] .slide__content h1 {
176
+ font-size: calc(var(--type-display) * 1.15);
177
+ text-align: center;
178
+ }
179
+ :root[data-template="spotlight"] .slide__content p {
180
+ max-width: 80%;
181
+ margin-left: auto;
182
+ margin-right: auto;
183
+ }
184
+ :root[data-template="spotlight"] .slide__content ul,
185
+ :root[data-template="spotlight"] .slide__content ol {
186
+ text-align: left;
187
+ display: inline-block;
188
+ margin-left: auto;
189
+ margin-right: auto;
190
+ }
191
+ :root[data-template="spotlight"] .slide__content blockquote {
192
+ text-align: center;
193
+ border-left: none;
194
+ border-top: 2px solid var(--accent);
195
+ border-bottom: 2px solid var(--accent);
196
+ padding: 1em 2em;
197
+ background: transparent;
198
+ }
199
+ :root[data-template="spotlight"] .slide__content pre {
200
+ text-align: left;
201
+ }
202
+ `;
203
+ export const TRANSITION_CSS = `/* ── Transitions ────────────────────────────────────────────────────────── */
204
+
205
+ /* Fade */
206
+ :root[data-transition="fade"] .slide {
207
+ transition: opacity 0.3s ease;
208
+ transform: none !important;
209
+ }
210
+ :root[data-transition="fade"] .slide.exit-left,
211
+ :root[data-transition="fade"] .slide.exit-right,
212
+ :root[data-transition="fade"] .slide.enter-from-left,
213
+ :root[data-transition="fade"] .slide.enter-from-right {
214
+ transform: none !important;
215
+ opacity: 0;
216
+ }
217
+ :root[data-transition="fade"] .slide.is-active {
218
+ opacity: 1;
219
+ transform: none !important;
220
+ }
221
+
222
+ /* Zoom */
223
+ :root[data-transition="zoom"] .slide {
224
+ transform: scale(0.92);
225
+ transition: opacity 0.35s ease, transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
226
+ }
227
+ :root[data-transition="zoom"] .slide.is-active {
228
+ transform: scale(1);
229
+ opacity: 1;
230
+ }
231
+ :root[data-transition="zoom"] .slide.exit-left,
232
+ :root[data-transition="zoom"] .slide.exit-right {
233
+ transform: scale(1.08);
234
+ opacity: 0;
235
+ }
236
+ :root[data-transition="zoom"] .slide.enter-from-left,
237
+ :root[data-transition="zoom"] .slide.enter-from-right {
238
+ transform: scale(0.92);
239
+ opacity: 0;
240
+ }
241
+
242
+ /* Lift */
243
+ :root[data-transition="lift"] .slide {
244
+ transform: translateY(48px);
245
+ transition: opacity 0.35s ease, transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
246
+ }
247
+ :root[data-transition="lift"] .slide.is-active {
248
+ transform: translateY(0);
249
+ opacity: 1;
250
+ }
251
+ :root[data-transition="lift"] .slide.exit-left,
252
+ :root[data-transition="lift"] .slide.exit-right {
253
+ transform: translateY(-48px);
254
+ opacity: 0;
255
+ }
256
+ :root[data-transition="lift"] .slide.enter-from-left,
257
+ :root[data-transition="lift"] .slide.enter-from-right {
258
+ transform: translateY(48px);
259
+ opacity: 0;
260
+ }
261
+
262
+ /* None */
263
+ :root[data-transition="none"] .slide,
264
+ :root[data-transition="none"] .slide.is-active,
265
+ :root[data-transition="none"] .slide.exit-left,
266
+ :root[data-transition="none"] .slide.exit-right,
267
+ :root[data-transition="none"] .slide.enter-from-left,
268
+ :root[data-transition="none"] .slide.enter-from-right {
269
+ transition: none !important;
270
+ transform: none !important;
271
+ }
272
+
273
+ @media (prefers-reduced-motion: reduce) {
274
+ :root[data-transition] .slide {
275
+ transition: opacity 0.2s linear !important;
276
+ transform: none !important;
277
+ }
278
+ }
279
+
280
+ @media print {
281
+ :root[data-transition] .slide {
282
+ transition: none !important;
283
+ transform: none !important;
284
+ opacity: 1 !important;
285
+ }
286
+ }
287
+ `;