sdocs-dev 1.14.1 → 1.18.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.
@@ -0,0 +1,258 @@
1
+ // sdocs-slide-resolve.js — template resolution for slide blocks (UMD).
2
+ // Shared by the browser slide pipeline and Node unit tests.
3
+ //
4
+ // Input: an array of raw slide DSL strings (the text between ~~~slide
5
+ // fences), in document order.
6
+ // Output: an array of { dsl, skip, errors } in the same order, where:
7
+ // - dsl: the DSL text to render (merged for consumers; unchanged
8
+ // for plain slides; the template body for template slides)
9
+ // - skip: true for @template slides (they register but don't render)
10
+ // - errors: [{ message }] of resolve-time problems (unknown
11
+ // template, malformed directive, etc.). Empty array when clean.
12
+ //
13
+ // Directives (must be the first non-blank line inside the fence):
14
+ // @template NAME — register this slide's DSL under NAME; don't render it.
15
+ // @extends NAME — replace slot contents in template NAME with the
16
+ // `#id: value` blocks that follow.
17
+ //
18
+ // Slot block syntax inside @extends bodies:
19
+ // #id: inline value (single-line)
20
+ // #id: (multi-line; content on following
21
+ // content line one lines, until the next `#id:` or EOF)
22
+ // content line two
23
+ //
24
+ // The resolver is deliberately pure — it takes strings, returns strings,
25
+ // and does not touch the DOM, parse shape geometry, or know anything
26
+ // about CSS. All heavy lifting (DSL parsing, rendering) happens downstream
27
+ // on the resolved text, so templates re-use every existing code path.
28
+
29
+ (function (exports) {
30
+ 'use strict';
31
+
32
+ var DIRECTIVE_RE = /^\s*@(template|extends)\s+([A-Za-z][\w-]*)\s*$/;
33
+ // Consumer slot lines optionally accept a trailing `!` after the id - the
34
+ // required marker belongs in the template, but accepting it here means a
35
+ // copy-paste from a template definition doesn't silently break.
36
+ var SLOT_RE = /^#([A-Za-z][\w-]*)!?\s*:\s?(.*)$/;
37
+
38
+ function splitDirective(raw) {
39
+ var lines = String(raw == null ? '' : raw).split('\n');
40
+ var i = 0;
41
+ while (i < lines.length && lines[i].trim() === '') i++;
42
+ if (i >= lines.length) return { kind: null };
43
+ var m = lines[i].match(DIRECTIVE_RE);
44
+ if (!m) return { kind: null };
45
+ return {
46
+ kind: m[1],
47
+ name: m[2],
48
+ body: lines.slice(i + 1).join('\n'),
49
+ };
50
+ }
51
+
52
+ function parseSlots(body) {
53
+ var lines = body.split('\n');
54
+ var slots = {};
55
+ var currentId = null;
56
+ var buf = [];
57
+ var inline = false; // inline-only slot (body on the directive line)
58
+ for (var i = 0; i < lines.length; i++) {
59
+ var ln = lines[i];
60
+ var m = ln.match(SLOT_RE);
61
+ if (m) {
62
+ if (currentId !== null) slots[currentId] = finalizeBuf(buf, inline);
63
+ currentId = m[1];
64
+ // A bare `|` after the colon is YAML-style block-scalar sugar: the
65
+ // user signalled "multi-line body follows", same as an empty inline.
66
+ // Dropping it prevents the literal `|` from leaking into the rendered
67
+ // markdown as a stray paragraph.
68
+ var inlineVal = m[2] === '|' ? '' : m[2];
69
+ inline = inlineVal.length > 0;
70
+ buf = inlineVal ? [inlineVal] : [];
71
+ } else if (currentId !== null) {
72
+ buf.push(ln);
73
+ inline = false; // as soon as we collect body lines, treat as block
74
+ }
75
+ // Lines before the first #id: are ignored (allows authors to leave
76
+ // notes or blank space between @extends and the first slot).
77
+ }
78
+ if (currentId !== null) slots[currentId] = finalizeBuf(buf, inline);
79
+ return slots;
80
+ }
81
+
82
+ // Inline slots (single-line `#id: value`) round-trip as-is. Block slots get
83
+ // trailing blank lines trimmed and common leading indent stripped, so an
84
+ // author who wrote:
85
+ //
86
+ // #body:
87
+ // - one
88
+ // - two
89
+ //
90
+ // gets `- one\n- two` in their shape content (not ` - one\n - two`, which
91
+ // markdown may render with an extra indent level or misread as code).
92
+ function finalizeBuf(lines, isInline) {
93
+ if (isInline) return lines.join('\n');
94
+ return dedent(trimTrailingBlank(lines)).join('\n');
95
+ }
96
+
97
+ function trimTrailingBlank(lines) {
98
+ var end = lines.length;
99
+ while (end > 0 && lines[end - 1].trim() === '') end--;
100
+ return lines.slice(0, end);
101
+ }
102
+
103
+ function dedent(lines) {
104
+ var min = Infinity;
105
+ for (var i = 0; i < lines.length; i++) {
106
+ if (lines[i].trim() === '') continue;
107
+ var lead = /^ */.exec(lines[i])[0].length;
108
+ if (lead < min) min = lead;
109
+ }
110
+ if (!isFinite(min) || min === 0) return lines;
111
+ return lines.map(function (l) {
112
+ return l.length >= min ? l.slice(min) : l;
113
+ });
114
+ }
115
+
116
+ // Merge the template's shapes with the consumer's slots by #id match.
117
+ // Relies on the SDocShapes parse/serialize pair, so any DSL the parser
118
+ // accepts round-trips correctly. Shapes whose id doesn't appear in the
119
+ // consumer's slots keep their template-provided content — that's how
120
+ // "partial fills render the layout's placeholder" works.
121
+ function mergeTemplate(templateDsl, slots, SDocShapes) {
122
+ var parsed = SDocShapes.parse(templateDsl);
123
+ var shapes = parsed.shapes.map(function (s) {
124
+ if (s.id && Object.prototype.hasOwnProperty.call(slots, s.id)) {
125
+ var copy = {};
126
+ for (var k in s) if (Object.prototype.hasOwnProperty.call(s, k)) copy[k] = s[k];
127
+ copy.content = slots[s.id];
128
+ return copy;
129
+ }
130
+ return s;
131
+ });
132
+ return SDocShapes.serialize(shapes, parsed.grid);
133
+ }
134
+
135
+ function resolveSlides(rawDsls, SDocShapes, opts) {
136
+ if (!SDocShapes) {
137
+ throw new Error('resolveSlides: SDocShapes is required');
138
+ }
139
+ opts = opts || {};
140
+ var stdlib = opts.stdlib || {};
141
+
142
+ var templates = {};
143
+ var shadowSlideByName = {};
144
+ var parsed = rawDsls.map(splitDirective);
145
+
146
+ // Pass 1: register every user @template. Doing this ahead of pass 2
147
+ // means author ordering in the document is unconstrained - a consumer
148
+ // can appear before its template. A user template shadowing a stdlib
149
+ // name is allowed but flagged on the template's own slide entry, so
150
+ // accidental shadowing is visible at render time.
151
+ for (var i = 0; i < parsed.length; i++) {
152
+ var p = parsed[i];
153
+ if (p.kind === 'template') {
154
+ templates[p.name] = p.body;
155
+ if (Object.prototype.hasOwnProperty.call(stdlib, p.name)) {
156
+ shadowSlideByName[p.name] = i;
157
+ }
158
+ }
159
+ }
160
+
161
+ // Pass 2: build a result per slide.
162
+ return parsed.map(function (p, idx) {
163
+ var raw = rawDsls[idx];
164
+ if (p.kind === 'template') {
165
+ var tErrs = [];
166
+ if (shadowSlideByName[p.name] === idx) {
167
+ tErrs.push({
168
+ line: 1,
169
+ message: 'template "' + p.name + '" shadows the stdlib template of the same name',
170
+ });
171
+ }
172
+ return { dsl: p.body, skip: true, errors: tErrs };
173
+ }
174
+ if (p.kind === 'extends') {
175
+ var tplBody = Object.prototype.hasOwnProperty.call(templates, p.name)
176
+ ? templates[p.name]
177
+ : (Object.prototype.hasOwnProperty.call(stdlib, p.name) ? stdlib[p.name] : null);
178
+ if (!tplBody) {
179
+ return {
180
+ dsl: raw,
181
+ skip: false,
182
+ errors: [{ line: 1, message: 'unknown template "' + p.name + '"' }],
183
+ };
184
+ }
185
+
186
+ var slots = parseSlots(p.body);
187
+ var errors = [];
188
+
189
+ // Parse the template once so we can introspect shape ids + required
190
+ // markers. The same parse work happens inside mergeTemplate; we eat
191
+ // the duplication for clarity (resolver stays pure DSL-string-in,
192
+ // DSL-string-out at the boundary).
193
+ var parsedTpl;
194
+ try {
195
+ parsedTpl = SDocShapes.parse(tplBody);
196
+ } catch (e) {
197
+ return {
198
+ dsl: raw,
199
+ skip: false,
200
+ errors: [{ line: 1, message: 'template parse failed: ' + e.message }],
201
+ };
202
+ }
203
+
204
+ var templateIds = {};
205
+ for (var s = 0; s < parsedTpl.shapes.length; s++) {
206
+ var sid = parsedTpl.shapes[s].id;
207
+ if (sid) templateIds[sid] = parsedTpl.shapes[s];
208
+ }
209
+
210
+ // Unknown-slot check: consumer named a slot the template doesn't
211
+ // define. Silently no-op was the #1 cause of "template feels
212
+ // broken" confusion - surface it as an error per the DSL-design
213
+ // review.
214
+ for (var slotName in slots) {
215
+ if (!Object.prototype.hasOwnProperty.call(slots, slotName)) continue;
216
+ if (!Object.prototype.hasOwnProperty.call(templateIds, slotName)) {
217
+ errors.push({
218
+ line: 1,
219
+ message: 'unknown slot "#' + slotName + '" (template "' + p.name + '" has no shape with that id)',
220
+ });
221
+ }
222
+ }
223
+
224
+ // Required-slot check: template marked the shape with #id! but
225
+ // the consumer didn't pass that slot.
226
+ for (var tid in templateIds) {
227
+ if (!Object.prototype.hasOwnProperty.call(templateIds, tid)) continue;
228
+ var shp = templateIds[tid];
229
+ if (shp.required && !Object.prototype.hasOwnProperty.call(slots, tid)) {
230
+ errors.push({
231
+ line: 1,
232
+ message: 'missing required slot "#' + tid + '" for template "' + p.name + '"',
233
+ });
234
+ }
235
+ }
236
+
237
+ var merged;
238
+ try {
239
+ merged = mergeTemplate(tplBody, slots, SDocShapes);
240
+ } catch (e) {
241
+ return {
242
+ dsl: raw,
243
+ skip: false,
244
+ errors: [{ line: 1, message: 'template merge failed: ' + e.message }],
245
+ };
246
+ }
247
+ return { dsl: merged, skip: false, errors: errors };
248
+ }
249
+ // Plain slide - untouched.
250
+ return { dsl: raw, skip: false, errors: [] };
251
+ });
252
+ }
253
+
254
+ exports.resolveSlides = resolveSlides;
255
+ exports.splitDirective = splitDirective;
256
+ exports.parseSlots = parseSlots;
257
+
258
+ })(typeof module !== 'undefined' && module.exports ? module.exports : (window.SDocSlideResolve = {}));
@@ -0,0 +1,180 @@
1
+ // sdocs-slide-stdlib.js - built-in slide templates (UMD).
2
+ //
3
+ // Each entry is a DSL string the resolver registers under the given name,
4
+ // alongside user `@template` definitions. User-defined templates shadow
5
+ // stdlib names (with a warning surfaced through the slide error badge),
6
+ // so authors can override any built-in without touching this file.
7
+ //
8
+ // Design constants applied across every template:
9
+ // - 1-unit safe margin on all four sides of a 16x9 grid (roughly 6%
10
+ // horizontal, 5-7% vertical, matching the standard slide safe area).
11
+ // - No shape fill colours for shapes containing title/body text. The
12
+ // section divider uses `grid bg=` for full-bleed contrast instead.
13
+ // - Title role (64px) reserved for cover + quote + section. In-deck
14
+ // content slides use subtitle role (40px) so action titles can wrap
15
+ // to two lines without filling half the page.
16
+ // - Caption role (14px) reserved for eyebrows / footers / attributions
17
+ // - never for load-bearing content (it renders as ~3px in a 240px
18
+ // thumbnail, illegible).
19
+ // - Optional slots default to empty content, so omitting them renders
20
+ // nothing visible. Required slots (#name!) emit a resolver error
21
+ // when the consumer doesn't fill them.
22
+ //
23
+ // All templates assume a 16x9 grid. Authors who need a different aspect
24
+ // ratio should copy a template into a user `@template` and edit the grid.
25
+
26
+ (function (exports) {
27
+ 'use strict';
28
+
29
+ var TEMPLATES = {
30
+
31
+ // Opening slide. Once per deck, sets the tone before anything else.
32
+ cover: [
33
+ 'grid 16 9',
34
+ 'r 1 1 14 0.7 #eyebrow text=caption color=$color opacity=0.6 align=left |',
35
+ 'r 1 3.2 14 2.2 #title! text=title align=left | (required: cover title)',
36
+ 'r 1 5.8 14 1.0 #subtitle text=subtitle color=$color opacity=0.75 align=left |',
37
+ 'r 1 7.7 14 0.6 #meta text=caption color=$color opacity=0.6 align=left |',
38
+ ].join('\n'),
39
+
40
+ // The workhorse. 60-70% of body slides. Title at top, body filling the
41
+ // safe area below, optional footer for source / page / context. Title
42
+ // uses subtitle role so an action title can wrap to two lines without
43
+ // crowding the body.
44
+ 'title-body': [
45
+ 'grid 16 9',
46
+ 'r 1 0.7 14 1.1 #title! text=subtitle align=left | (required: slide title)',
47
+ 'r 1 2.2 14 5.7 #body! text=body align=left valign=top | (required: slide body)',
48
+ 'r 1 8.1 14 0.4 #footer text=caption color=$color opacity=0.55 align=left |',
49
+ ].join('\n'),
50
+
51
+ // Two equal columns with a 1-unit gutter, both bodies anchored top so
52
+ // matched-length content reads as parallel. Optional column headers in
53
+ // caption role above each body.
54
+ 'two-column': [
55
+ 'grid 16 9',
56
+ 'r 1 0.7 14 1.1 #title! text=subtitle align=left | (required: slide title)',
57
+ 'r 1 2.4 6.5 0.5 #left-header text=caption color=$color opacity=0.75 align=left |',
58
+ 'r 1 3.0 6.5 5.0 #left! text=body align=left valign=top | (required: left column)',
59
+ 'r 8.5 2.4 6.5 0.5 #right-header text=caption color=$color opacity=0.75 align=left |',
60
+ 'r 8.5 3.0 6.5 5.0 #right! text=body align=left valign=top | (required: right column)',
61
+ ].join('\n'),
62
+
63
+ // Three-way compare. A/B/C variants, before/during/after, three
64
+ // perspectives on the same question. Three equal columns (4.3 wide
65
+ // each) separated by 0.55-unit gutters, optional headers above each.
66
+ // Bias to keeping all three columns roughly the same length - if one
67
+ // is half-empty, drop it and use two-column.
68
+ 'three-column': [
69
+ 'grid 16 9',
70
+ 'r 1 0.7 14 1.1 #title! text=subtitle align=left | (required: slide title)',
71
+ 'r 1 2.4 4.3 0.5 #left-header text=caption color=$color opacity=0.75 align=left |',
72
+ 'r 1 3.0 4.3 5.0 #left! text=body align=left valign=top | (required: left column)',
73
+ 'r 5.85 2.4 4.3 0.5 #mid-header text=caption color=$color opacity=0.75 align=left |',
74
+ 'r 5.85 3.0 4.3 5.0 #mid! text=body align=left valign=top | (required: middle column)',
75
+ 'r 10.7 2.4 4.3 0.5 #right-header text=caption color=$color opacity=0.75 align=left |',
76
+ 'r 10.7 3.0 4.3 5.0 #right! text=body align=left valign=top | (required: right column)',
77
+ ].join('\n'),
78
+
79
+ // Exhibit. Chart on the left (~56% wide), takeaway column on the
80
+ // right (~28% wide). The chart is the evidence; the takeaway tells
81
+ // the audience what to see. Optional source caption at the bottom.
82
+ // Reserve for business decks where the audience needs a verbal
83
+ // handle on the chart under time pressure - if your audience can
84
+ // read the chart themselves in 5 seconds, use figure-hero instead.
85
+ exhibit: [
86
+ 'grid 16 9',
87
+ 'r 1 0.7 14 1.1 #title! text=subtitle align=left | (required: action title)',
88
+ 'r 1 2 9 6 #chart! align=center valign=center | (required: ![alt](url) or chart)',
89
+ 'r 10.5 2 4.5 6 #takeaway! text=body align=left valign=center | (required: takeaway)',
90
+ 'r 1 8.1 14 0.4 #source text=caption color=$color opacity=0.55 align=left |',
91
+ ].join('\n'),
92
+
93
+ // Image-and-text. Image fills the left half of the safe area, body
94
+ // sits on the right. The image slot accepts markdown image syntax:
95
+ // `#image: ![alt](url)`. Optional small title above both columns.
96
+ 'image-and-text': [
97
+ 'grid 16 9',
98
+ 'r 1 0.7 14 0.9 #title text=caption color=$color opacity=0.75 align=left |',
99
+ 'r 1 1.9 7.5 6.2 #image! align=center valign=center | (required: ![alt](url))',
100
+ 'r 9 1.9 6 6.2 #body! text=body align=left valign=top | (required: body)',
101
+ ].join('\n'),
102
+
103
+ // Image-dominant slide. The figure carries the slide; a small
104
+ // caption sits below. Use when the image IS the argument (a chart,
105
+ // a screenshot, a product shot, a photo). If you want supporting
106
+ // body text alongside the image, use `image-and-text` instead.
107
+ 'figure-hero': [
108
+ 'grid 16 9',
109
+ 'r 1 0.6 14 7.0 #image! align=center valign=center | (required: ![alt](url))',
110
+ 'r 1 7.9 14 0.6 #caption text=caption color=$color opacity=0.75 align=center valign=top |',
111
+ ].join('\n'),
112
+
113
+ // Single big idea. Quote, customer voice, or a callout sentence that
114
+ // deserves the whole slide. Lead is centered both ways so short content
115
+ // sits visually balanced rather than top-left adrift.
116
+ quote: [
117
+ 'grid 16 9',
118
+ 'r 2 2 12 4 #lead! text=title align=center valign=center | (required: the lead)',
119
+ 'r 2 6.5 12 0.7 #attribution text=caption color=$color opacity=0.6 align=center valign=top |',
120
+ ].join('\n'),
121
+
122
+ // One big number + a line of context. The hero metric slide.
123
+ // `size=fit` lets the number balloon to fill its shape, so a 3-char
124
+ // value (`87%`) lands much larger than a long one (`$4,231,889`).
125
+ // `maxfont=300px` lifts the default 12%-of-stage cap so the number
126
+ // can actually feel hero-sized; without it, autofit would cap at
127
+ // ~86px on a 720-tall stage and the slide reads as "a small number
128
+ // floating in a big box" rather than "stop the room".
129
+ // Use sparingly - one metric slide per deck is the rule, not three.
130
+ metric: [
131
+ 'grid 16 9',
132
+ 'r 1 1.5 14 4.8 #metric! size=fit maxfont=300px align=center valign=center | (required: the number)',
133
+ 'r 1 6.7 14 1.4 #context text=body color=$color opacity=0.75 align=center valign=top |',
134
+ ].join('\n'),
135
+
136
+ // Section divider. The one template where the slide background gets a
137
+ // saturated fill - the contrast against content slides is the point.
138
+ // Text shapes themselves have no fill (Consultant-2 rule preserved);
139
+ // the grid's bg= provides the colour underneath.
140
+ section: [
141
+ 'grid 16 9 bg=#0f172a',
142
+ 'r 1 1 14 0.7 #kicker text=caption color=#94a3b8 align=left |',
143
+ 'r 1 3 14 3 #title! text=title color=#f8fafc align=left valign=center | (required: section title)',
144
+ 'r 1 6 14 1 #subtitle text=subtitle color=#cbd5e1 align=left valign=center |',
145
+ ].join('\n'),
146
+
147
+ // Closing slide. Symmetric bookend with `cover`. Center-aligned and
148
+ // minimal - one quiet message + optional contact line. Don't write
149
+ // "Thanks for listening" here; pick something the audience will
150
+ // remember instead.
151
+ closing: [
152
+ 'grid 16 9',
153
+ 'r 1 3.5 14 2.0 #lead! text=subtitle align=center valign=center | (required: closing line)',
154
+ 'r 1 6 14 0.6 #contact text=caption color=$color opacity=0.6 align=center valign=center |',
155
+ ].join('\n'),
156
+
157
+ };
158
+
159
+ // Per-template slot summary for `sdoc slides list` and the resolver's
160
+ // unknown-slot check. Required slots end with `!` here too, matching the
161
+ // DSL marker. Order matters - it controls how the listing reads.
162
+ var SLOT_DOCS = {
163
+ cover: ['eyebrow', 'title!', 'subtitle', 'meta'],
164
+ 'title-body': ['title!', 'body!', 'footer'],
165
+ 'two-column': ['title!', 'left-header', 'left!', 'right-header', 'right!'],
166
+ 'three-column': ['title!', 'left-header', 'left!', 'mid-header', 'mid!', 'right-header', 'right!'],
167
+ exhibit: ['title!', 'chart!', 'takeaway!', 'source'],
168
+ 'image-and-text':['title', 'image!', 'body!'],
169
+ 'figure-hero': ['image!', 'caption'],
170
+ quote: ['lead!', 'attribution'],
171
+ metric: ['metric!', 'context'],
172
+ section: ['kicker', 'title!', 'subtitle'],
173
+ closing: ['lead!', 'contact'],
174
+ };
175
+
176
+ exports.templates = TEMPLATES;
177
+ exports.slots = SLOT_DOCS;
178
+ exports.names = Object.keys(TEMPLATES);
179
+
180
+ })(typeof module !== 'undefined' && module.exports ? module.exports : (window.SDocSlideStdlib = {}));
@@ -536,7 +536,7 @@ var STYLE_DEFAULTS = {
536
536
  p: { lineHeight: 1.75, marginBottom: 1.1 },
537
537
  link: { color: '#2563eb', decoration: 'underline' },
538
538
  code: { font: 'JetBrains Mono' },
539
- blockquote: { borderColor: '#2563eb', borderWidth: 3, fontSize: 1.0 },
539
+ blockquote: { borderColor: '#2563eb', borderWidth: 3, fontSize: 1.05 },
540
540
  list: { spacing: 0.3, indent: 1.6 },
541
541
  };
542
542