@entropicwarrior/sdoc 0.2.19 → 0.2.21
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/LICENSE +1 -1
- package/docs/reference/slide-authoring.sdoc +130 -5
- package/package.json +1 -1
- package/src/slide-layouts.js +21 -0
- package/src/slide-renderer.js +43 -6
package/LICENSE
CHANGED
|
@@ -107,6 +107,18 @@
|
|
|
107
107
|
reserved. \`--pptx\` runs the same check for free, because it has
|
|
108
108
|
already measured the deck.
|
|
109
109
|
|
|
110
|
+
To override what a format does with the optional slides:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
node tools/build-slides.js deck.sdoc --pdf --with-optional
|
|
114
|
+
node tools/build-slides.js deck.sdoc --no-optional
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A slide marked \`optional: true\` is kept in the HTML build and
|
|
118
|
+
left out of \`--pdf\` and \`--pptx\`. Either default can be
|
|
119
|
+
overridden in any format: \`--with-optional\` keeps them,
|
|
120
|
+
\`--no-optional\` drops them. See [Optional Slides](#optional).
|
|
121
|
+
|
|
110
122
|
To use a custom theme:
|
|
111
123
|
|
|
112
124
|
```
|
|
@@ -138,6 +150,7 @@
|
|
|
138
150
|
\`footnote:\` | Small print pinned under the body: provenance, caveats, sources.
|
|
139
151
|
\`status:\` | A second line under the kicker on a title slide, for a distribution notice.
|
|
140
152
|
\`accent:\` | \`primary\`, \`secondary\`, \`tertiary\` or \`neutral\`. An accent names a role; the theme chooses the colour.
|
|
153
|
+
\`optional:\` | \`true\` holds the slide back from the exports, and from any build passed \`--no-optional\`. See [Optional Slides](#optional).
|
|
141
154
|
\`numbered:\` | \`columns\` and \`rows\`. \`true\` numbers the children 01, 02, 03.
|
|
142
155
|
\`variant:\` | \`columns\` and \`rows\`. A named treatment, such as \`panel\` or \`mono\`.
|
|
143
156
|
\`weights:\` | \`split\` only. The ratio between panes, as in \`48 52\`.
|
|
@@ -150,11 +163,11 @@
|
|
|
150
163
|
}
|
|
151
164
|
|
|
152
165
|
A key is only configuration where it means something. \`config:\`,
|
|
153
|
-
\`kicker:\`, \`lede:\`, \`footnote:\`, \`accent
|
|
154
|
-
understood by every scope; the rest are understood
|
|
155
|
-
layout named against them above. Anywhere else they are
|
|
156
|
-
text — a slide opening "Value: the customer keeps their
|
|
157
|
-
renders that sentence, it does not silently swallow it. A
|
|
166
|
+
\`kicker:\`, \`lede:\`, \`footnote:\`, \`accent:\`, \`status:\` and
|
|
167
|
+
\`optional:\` are understood by every scope; the rest are understood
|
|
168
|
+
only by the layout named against them above. Anywhere else they are
|
|
169
|
+
ordinary text — a slide opening "Value: the customer keeps their
|
|
170
|
+
data." renders that sentence, it does not silently swallow it. A
|
|
158
171
|
paragraph spanning more than one source line is never
|
|
159
172
|
configuration.
|
|
160
173
|
|
|
@@ -526,6 +539,114 @@
|
|
|
526
539
|
spine 2, its details in order, and so on. This is the same order
|
|
527
540
|
slides appear in the HTML, so PDF export needs no special case —
|
|
528
541
|
every slide is just one page.
|
|
542
|
+
|
|
543
|
+
Every detail is a page, then, including the ones the audience
|
|
544
|
+
never saw. Mark a detail `optional: true` to keep it out of the
|
|
545
|
+
exported file — see [Optional Slides](#optional).
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
# Optional Slides @optional
|
|
550
|
+
{
|
|
551
|
+
Some slides exist for the room and not for the file that gets sent
|
|
552
|
+
on: the answer to a question that may not be asked, the figure
|
|
553
|
+
behind a figure, the rebuttal you hope to leave unused. Set
|
|
554
|
+
\`optional: true\` on the slide and it stays in the HTML build, which
|
|
555
|
+
is what you present from, and leaves the PDF and the PPTX, which are
|
|
556
|
+
what you send.
|
|
557
|
+
|
|
558
|
+
# Syntax @optional-syntax
|
|
559
|
+
{
|
|
560
|
+
```
|
|
561
|
+
# Unit economics @unit-economics {
|
|
562
|
+
Headline numbers.
|
|
563
|
+
|
|
564
|
+
# Gross margin by cohort @margin :detail {
|
|
565
|
+
optional: true
|
|
566
|
+
|
|
567
|
+
The cohort table, if anyone asks for it.
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
# Pricing objections @objections {
|
|
572
|
+
optional: true
|
|
573
|
+
|
|
574
|
+
Held back unless pricing comes up.
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
\`optional:\` is a slide property, not a scope annotation, and that
|
|
579
|
+
is deliberate: a heading carries one \`:type\`, and a drilldown has
|
|
580
|
+
already spent it on \`:detail\`. Reading optional-ness off the
|
|
581
|
+
configuration run keeps the two independent, so all four
|
|
582
|
+
combinations are available — a required spine, an optional spine,
|
|
583
|
+
a required detail, an optional detail.
|
|
584
|
+
|
|
585
|
+
A slide is the unit. Setting it on a cell inside a structured
|
|
586
|
+
layout — a column, a pipeline row, a pane of a \`split\` — has no
|
|
587
|
+
effect: the whole slide travels or none of it does. Mark the slide
|
|
588
|
+
that holds the cell.
|
|
589
|
+
|
|
590
|
+
It is set the way the other boolean properties are: \`true\`,
|
|
591
|
+
\`yes\`, \`on\` or \`1\` mark the slide; \`false\`, \`no\`, \`off\` or \`0\`
|
|
592
|
+
leave it alone. Like every slide property it must come before any
|
|
593
|
+
content.
|
|
594
|
+
|
|
595
|
+
A value that is none of those words is not configuration at all.
|
|
596
|
+
A slide whose first line is "Optional: a second seat costs
|
|
597
|
+
nothing" renders that sentence — a boolean key discards what it
|
|
598
|
+
cannot read, so it declines the line rather than swallowing it.
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
# What each format does @optional-formats
|
|
602
|
+
{
|
|
603
|
+
{[table]
|
|
604
|
+
Format | Default | Why
|
|
605
|
+
HTML | Included | It is the format you present from, and an optional slide is one you may want in the room.
|
|
606
|
+
PDF | Excluded | It is a format you send on.
|
|
607
|
+
PPTX | Excluded | The same.
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
The default is a guess about intent, not a property of the format,
|
|
611
|
+
so either can be overridden anywhere. An HTML deck is also
|
|
612
|
+
something you send someone:
|
|
613
|
+
|
|
614
|
+
```
|
|
615
|
+
node tools/build-slides.js deck.sdoc --pdf --with-optional
|
|
616
|
+
node tools/build-slides.js deck.sdoc --no-optional
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
When both are given the last one wins, as with \`--fit\`.
|
|
620
|
+
|
|
621
|
+
An optional spine takes its details with it. A detail belongs to
|
|
622
|
+
its spine, and there is nowhere for it to stand once the spine is
|
|
623
|
+
gone, so it goes whether or not it is marked itself.
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
# Numbering @optional-numbering
|
|
627
|
+
{
|
|
628
|
+
Optional slides are dropped before the deck is numbered, not
|
|
629
|
+
hidden afterwards, so the slide indicator counts what the reader
|
|
630
|
+
actually holds. A deck of four spines with one of them optional
|
|
631
|
+
reads \`3 / 3\` on its last page in the exported file and \`4 / 4\`
|
|
632
|
+
in the HTML build. The pages are consecutive either way; there is
|
|
633
|
+
no gap where a slide was removed.
|
|
634
|
+
|
|
635
|
+
A spine whose only details are optional loses its
|
|
636
|
+
\`slide-has-details\` class in the export too, so no theme draws a
|
|
637
|
+
drilldown chevron pointing at nothing.
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
# Themes @optional-theme
|
|
641
|
+
{
|
|
642
|
+
An optional slide carries the \`slide-optional\` class wherever it
|
|
643
|
+
is kept. A theme can use it to mark the slide on screen — a corner
|
|
644
|
+
tab, a tinted rule — so the presenter can see which slides are off
|
|
645
|
+
the main path. The built-in theme does not style it, so an optional
|
|
646
|
+
slide looks like any other until a theme says otherwise.
|
|
647
|
+
|
|
648
|
+
Nothing marked optional reaches a build it was excluded from, so
|
|
649
|
+
the class cannot appear in one.
|
|
529
650
|
}
|
|
530
651
|
}
|
|
531
652
|
|
|
@@ -768,6 +889,10 @@
|
|
|
768
889
|
rather than an outlined box, so a hairline under a heading is a
|
|
769
890
|
hairline rather than a rectangle with three invisible edges.
|
|
770
891
|
|
|
892
|
+
What is not in the file at all: slides marked \`optional: true\`,
|
|
893
|
+
unless \`--with-optional\` was passed. See
|
|
894
|
+
[Optional Slides](#optional).
|
|
895
|
+
|
|
771
896
|
What does not carry: web fonts are named, not embedded, so the
|
|
772
897
|
fonts a theme uses must be available to the viewer. Choosing faces
|
|
773
898
|
that are on Google Fonts is the reliable route, because Slides
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@entropicwarrior/sdoc",
|
|
3
3
|
"displayName": "SDOC - Docs for Human/Agent Teams",
|
|
4
4
|
"description": "A plain-text documentation format with explicit brace scoping — deterministic parsing, AI-agent efficiency, and 10-50x token savings vs Markdown.",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.21",
|
|
6
6
|
"publisher": "entropicwarrior-msenfin",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
package/src/slide-layouts.js
CHANGED
|
@@ -32,6 +32,7 @@ const COMMON_KEYS = new Set([
|
|
|
32
32
|
"footnote",
|
|
33
33
|
"accent",
|
|
34
34
|
"status",
|
|
35
|
+
"optional",
|
|
35
36
|
]);
|
|
36
37
|
|
|
37
38
|
const LAYOUT_KEYS = {
|
|
@@ -53,6 +54,17 @@ const CELL_KEYS = {
|
|
|
53
54
|
bars: ["value", "fill"],
|
|
54
55
|
};
|
|
55
56
|
|
|
57
|
+
// Keys whose value is a boolean. Every other key renders the text it is given,
|
|
58
|
+
// so consuming the line is visible in the output; a boolean key discards
|
|
59
|
+
// anything it does not recognise, which would make an opening sentence
|
|
60
|
+
// disappear. These therefore only take the line when the value is a word they
|
|
61
|
+
// actually understand — "Optional: a second seat costs nothing" is prose.
|
|
62
|
+
const BOOLEAN_KEYS = new Set(["optional", "numbered", "rule"]);
|
|
63
|
+
const BOOLEAN_WORDS = new Set([
|
|
64
|
+
"true", "yes", "on", "1",
|
|
65
|
+
"false", "no", "off", "0",
|
|
66
|
+
]);
|
|
67
|
+
|
|
56
68
|
// Every key any scope might understand. A leading paragraph opening with one
|
|
57
69
|
// of these is a configuration *candidate*; whether it is kept depends on the
|
|
58
70
|
// context resolved below.
|
|
@@ -136,6 +148,13 @@ function extractConfig(children, parentLayout) {
|
|
|
136
148
|
rejected.push(candidate.node);
|
|
137
149
|
continue;
|
|
138
150
|
}
|
|
151
|
+
if (
|
|
152
|
+
BOOLEAN_KEYS.has(candidate.key) &&
|
|
153
|
+
!BOOLEAN_WORDS.has(candidate.value.trim().toLowerCase())
|
|
154
|
+
) {
|
|
155
|
+
rejected.push(candidate.node);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
139
158
|
if (candidate.key === "config" || candidate.key === "layout") {
|
|
140
159
|
config.layout = candidate.value.toLowerCase();
|
|
141
160
|
} else {
|
|
@@ -505,10 +524,12 @@ function buildBody(layout, cell, ctx) {
|
|
|
505
524
|
}
|
|
506
525
|
|
|
507
526
|
module.exports = {
|
|
527
|
+
BOOLEAN_KEYS,
|
|
508
528
|
CONFIG_KEYS,
|
|
509
529
|
STRUCTURED_LAYOUTS,
|
|
510
530
|
extractConfig,
|
|
511
531
|
buildBody,
|
|
512
532
|
accentClass,
|
|
513
533
|
slug,
|
|
534
|
+
truthy,
|
|
514
535
|
};
|
package/src/slide-renderer.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// const html = renderSlides(nodes, { meta, themeCss, themeJs });
|
|
9
9
|
|
|
10
10
|
const { parseInline, renderKatex, escapeHtml, escapeAttr, sanitizeSvg, colorSwatchHtml } = require("./sdoc");
|
|
11
|
-
const { extractConfig, buildBody, accentClass, slug } = require("./slide-layouts");
|
|
11
|
+
const { extractConfig, buildBody, accentClass, slug, truthy } = require("./slide-layouts");
|
|
12
12
|
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// Inline rendering — produces clean HTML without sdoc-* classes
|
|
@@ -211,6 +211,23 @@ function extractDetails(children) {
|
|
|
211
211
|
return { details, contentNodes: rest };
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
// Whether a slide scope carries `optional: true`.
|
|
215
|
+
//
|
|
216
|
+
// Optional-ness is a slide *property*, not a scope type: a heading carries one
|
|
217
|
+
// `:type` annotation and a drilldown has already spent it on `:detail`, so a
|
|
218
|
+
// detail slide could not also be annotated optional. Reading it off the
|
|
219
|
+
// configuration run keeps the two orthogonal — every combination of
|
|
220
|
+
// spine/detail and required/optional is expressible.
|
|
221
|
+
//
|
|
222
|
+
// The details are pulled out first and the config read without a parent
|
|
223
|
+
// layout, exactly as renderSlide() does it, so this answer and the one the
|
|
224
|
+
// slide renders under can never disagree.
|
|
225
|
+
function isOptionalSlide(scope) {
|
|
226
|
+
const { contentNodes } = extractDetails(scope.children || []);
|
|
227
|
+
const { config } = extractConfig(contentNodes);
|
|
228
|
+
return truthy(config.optional);
|
|
229
|
+
}
|
|
230
|
+
|
|
214
231
|
// ---------------------------------------------------------------------------
|
|
215
232
|
// Slide rendering
|
|
216
233
|
// ---------------------------------------------------------------------------
|
|
@@ -246,6 +263,11 @@ function renderSlide(scope, slideIndex, overlayHtml, position) {
|
|
|
246
263
|
if (position && position.detail > 0) {
|
|
247
264
|
classes.push("slide-detail");
|
|
248
265
|
}
|
|
266
|
+
// Present in the HTML build, which is the presenting format; dropped before
|
|
267
|
+
// this point when the deck is rendered for PDF or PPTX export.
|
|
268
|
+
if (truthy(config.optional)) {
|
|
269
|
+
classes.push("slide-optional");
|
|
270
|
+
}
|
|
249
271
|
|
|
250
272
|
// On a title slide the kicker sits beneath the statement rather than above
|
|
251
273
|
// it, so the eye lands on the name first.
|
|
@@ -312,7 +334,8 @@ function renderSlides(nodes, options = {}) {
|
|
|
312
334
|
themeJs = "",
|
|
313
335
|
darkMode = false,
|
|
314
336
|
themeConfig = {},
|
|
315
|
-
fit = null
|
|
337
|
+
fit = null,
|
|
338
|
+
includeOptional = true
|
|
316
339
|
} = options;
|
|
317
340
|
|
|
318
341
|
// The design box and print page come from the theme (themes/<name>/theme.json).
|
|
@@ -373,14 +396,28 @@ function renderSlides(nodes, options = {}) {
|
|
|
373
396
|
footerParts.push(`<span class="nav-next">›</span>`);
|
|
374
397
|
const overlayHtml = `\n<div class="slide-footer">${footerParts.join("")}</div>`;
|
|
375
398
|
|
|
399
|
+
// Optional slides are for the room, not the file that gets sent on. They are
|
|
400
|
+
// kept in the HTML build (includeOptional defaults to true, so every existing
|
|
401
|
+
// caller sees the deck it saw before) and dropped here when a deck is
|
|
402
|
+
// rendered for export. Dropping them before numbering — rather than hiding
|
|
403
|
+
// them later in print CSS — is what keeps the indicator honest: the
|
|
404
|
+
// denominator counts the spine slides the reader actually has.
|
|
405
|
+
//
|
|
406
|
+
// An optional spine takes its details with it. A detail belongs to its spine;
|
|
407
|
+
// there is nowhere for it to go once the spine is gone.
|
|
408
|
+
const kept = includeOptional ? slides : slides.filter((s) => !isOptionalSlide(s));
|
|
409
|
+
|
|
376
410
|
// Build a flat emission order: each spine slide, followed immediately by its
|
|
377
411
|
// :detail children in source order. The flat order matches what we want for
|
|
378
412
|
// PDF export, so PDF needs no special case.
|
|
379
|
-
const totalSpines =
|
|
413
|
+
const totalSpines = kept.length;
|
|
380
414
|
const emitted = [];
|
|
381
|
-
|
|
415
|
+
kept.forEach((scope, i) => {
|
|
382
416
|
const spineIndex = i + 1; // 1-based
|
|
383
|
-
const { details } = extractDetails(scope.children);
|
|
417
|
+
const { details: allDetails } = extractDetails(scope.children);
|
|
418
|
+
const details = includeOptional
|
|
419
|
+
? allDetails
|
|
420
|
+
: allDetails.filter((d) => !isOptionalSlide(d));
|
|
384
421
|
emitted.push({
|
|
385
422
|
scope,
|
|
386
423
|
position: {
|
|
@@ -573,4 +610,4 @@ ${jsTag}${mermaidTag}
|
|
|
573
610
|
</html>`;
|
|
574
611
|
}
|
|
575
612
|
|
|
576
|
-
module.exports = { renderSlides, renderSlide, renderNode, renderInline };
|
|
613
|
+
module.exports = { renderSlides, renderSlide, renderNode, renderInline, isOptionalSlide };
|