@codename/deck 0.3.0 → 0.4.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/README.md +4 -4
- package/dist/deck.bundle.mjs +100 -94
- package/dist/deck.bundle.mjs.map +4 -4
- package/dist/deck.css +111 -0
- package/dist/deck.js +208 -91
- package/dist/deck.js.map +4 -4
- package/dist/deck.mjs +42 -36
- package/dist/deck.mjs.map +4 -4
- package/dist/template.mjs +3 -3
- package/dist/template.mjs.map +3 -3
- package/dist/types/blocks.d.ts +6 -4
- package/dist/types/figures.d.ts +68 -0
- package/dist/types/kit.d.ts +16 -0
- package/dist/types/mindmap.d.ts +69 -0
- package/dist/types/pptx.d.ts +3 -1
- package/dist/types/render.d.ts +21 -3
- package/dist/types/version.d.ts +1 -1
- package/docs/format.md +85 -4
- package/docs/host.md +3 -3
- package/docs/kits.md +10 -2
- package/package.json +1 -1
- package/src/blocks.js +26 -9
- package/src/deck.css +111 -0
- package/src/deck.js +11 -1
- package/src/figures.js +255 -0
- package/src/kit.js +35 -6
- package/src/mindmap.js +208 -0
- package/src/pptx.js +132 -17
- package/src/render.js +122 -19
- package/src/version.js +1 -1
package/src/deck.css
CHANGED
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
.deck-slide [data-align="center"] { text-align: center; }
|
|
44
44
|
.deck-block { position: relative; flex: 1 1 auto; min-block-size: 12cqi; overflow: hidden; }
|
|
45
45
|
.deck-block .deck-chart, .deck-block .deck-diagram { position: absolute; inset: 0; display: grid; place-items: center; }
|
|
46
|
+
/* A diagram's picture held to its box, its track sized so its share of it can be. */
|
|
47
|
+
.deck-block .deck-diagram { grid-template: minmax(0, 1fr) / minmax(0, 1fr); }
|
|
46
48
|
.deck-block .deck-diagram img { max-inline-size: 100%; max-block-size: 100%; }
|
|
47
49
|
.deck-block .deck-block-source { display: none; }
|
|
48
50
|
.deck-block.is-failed .deck-block-source { display: block; }
|
|
@@ -79,9 +81,113 @@
|
|
|
79
81
|
.deck-slide[data-title="huge"] :is(h2, .deck-h2) { font-size: 2.4em; }
|
|
80
82
|
/* What the usual layouts add to their definition: Slidev's looks for them. */
|
|
81
83
|
.deck-layout-section .deck-slot::before { content: ""; display: block; inline-size: 3em; block-size: 0.3em; background: var(--deck-fill); margin-block-end: 0.8em; border-radius: 0.15em; }
|
|
84
|
+
.deck-layout-subsection .deck-slot::before { content: ""; display: block; inline-size: 2em; block-size: 0.2em; background: var(--deck-primary); margin-block-end: 0.7em; border-radius: 0.1em; }
|
|
82
85
|
.deck-layout-statement :is(h1, h2, p, .deck-h1, .deck-h2) { font-size: 2.4em; font-family: var(--deck-heading); font-weight: 700; line-height: 1.2; }
|
|
83
86
|
.deck-slide.deck-layout-fact :is(h1, .deck-h1) { font-size: 5em; color: var(--deck-primary); margin-block-end: 0.15em; }
|
|
84
87
|
.deck-layout-quote blockquote { font-size: 1.7em; border: 0; padding: 0; font-family: var(--deck-heading); }
|
|
88
|
+
/* A mind map (mindmap.js): its words where its layout puts them, in em of its own type, the whole shrunk to its box; its
|
|
89
|
+
root on the fill, each branch in a colour of the charts', its lines and its first words' rim. */
|
|
90
|
+
.deck-mindmap { position: relative; flex: 1 1 auto; align-self: stretch; min-block-size: 12cqi; overflow: hidden; }
|
|
91
|
+
.deck-map-plane { position: absolute; top: 50%; left: 50%; translate: -50% -50%; }
|
|
92
|
+
.deck-map-lines { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; overflow: visible; }
|
|
93
|
+
.deck-map-lines path { fill: none; stroke: var(--deck-map-colour); stroke-width: 0.07; stroke-linecap: round; }
|
|
94
|
+
.deck-map-lines path[data-depth="1"] { stroke-width: 0.15; }
|
|
95
|
+
.deck-slide .deck-map-tree, .deck-slide .deck-map-tree :is(ul, li) { margin: 0; padding: 0; list-style: none; }
|
|
96
|
+
.deck-slide .deck-mindmap .deck-map-node { position: absolute; inline-size: max-content; max-inline-size: 11em; margin: 0; padding: 0.15em 0.35em; font-family: var(--deck-body); font-size: 0.85em; font-weight: 400; line-height: 1.2; letter-spacing: 0; text-align: center; text-wrap: balance; color: inherit; }
|
|
97
|
+
/* A chart or a diagram in a node, under its words, at a size of its own. */
|
|
98
|
+
.deck-slide .deck-mindmap .deck-map-node.has-block[data-depth] { display: flex; flex-direction: column; align-items: center; gap: 0.2em; max-inline-size: none; }
|
|
99
|
+
.deck-slide .deck-mindmap .deck-map-node .deck-block { flex: none; inline-size: 13em; block-size: 7.5em; min-block-size: 0; margin: 0; font-size: 0.85em; }
|
|
100
|
+
/* Words wrapped beyond the first branches keep to the side their line comes from. */
|
|
101
|
+
.deck-slide .deck-mindmap .deck-map-node[data-side="right"]:not([data-depth="1"]) { text-align: left; }
|
|
102
|
+
.deck-slide .deck-mindmap .deck-map-node[data-side="left"]:not([data-depth="1"]) { text-align: right; }
|
|
103
|
+
.deck-slide .deck-mindmap .deck-map-node[data-depth="0"] { max-inline-size: 9em; padding: 0.55em 1em; font-family: var(--deck-heading); font-size: 1.35em; font-weight: 700; background: var(--deck-fill); color: var(--deck-on-fill); border-radius: 1.2em; }
|
|
104
|
+
.deck-slide .deck-mindmap .deck-map-node[data-depth="0"]:empty { inline-size: 0.8em; block-size: 0.8em; padding: 0; border-radius: 50%; }
|
|
105
|
+
.deck-slide .deck-mindmap .deck-map-node[data-depth="1"] { max-inline-size: 9em; padding: 0.3em 0.8em; font-size: 1em; font-weight: 600; background: color-mix(in srgb, var(--deck-map-colour) 14%, transparent); border: 0.12em solid var(--deck-map-colour); border-radius: 1em; }
|
|
106
|
+
.deck-mindmap [data-branch="1"] { --deck-map-colour: var(--deck-series-1); }
|
|
107
|
+
.deck-mindmap [data-branch="2"] { --deck-map-colour: var(--deck-series-2); }
|
|
108
|
+
.deck-mindmap [data-branch="3"] { --deck-map-colour: var(--deck-series-3); }
|
|
109
|
+
.deck-mindmap [data-branch="4"] { --deck-map-colour: var(--deck-series-4); }
|
|
110
|
+
.deck-mindmap [data-branch="5"] { --deck-map-colour: var(--deck-series-5); }
|
|
111
|
+
.deck-mindmap [data-branch="6"] { --deck-map-colour: var(--deck-series-6); }
|
|
112
|
+
/* A branch shown a click fades in where it is: moved, its words would be placed from it. */
|
|
113
|
+
.deck-mindmap .deck-step:not(.is-on) { translate: none; }
|
|
114
|
+
/* Figures (figures.js): a slot's lists as cards, key figures, a timeline, chevrons, a programme, a pyramid or a bento;
|
|
115
|
+
its parts face to face or in a matrix; the deck's contents. In em of the slot's type, so they shrink with it. */
|
|
116
|
+
.deck-figure { flex: 1 1 auto; min-block-size: 0; display: grid; gap: 1em; align-content: center; line-break: strict; }
|
|
117
|
+
.deck-slide .deck-figure :is(p, ul, ol) { margin-block: 0 0.4em; }
|
|
118
|
+
.deck-slide .deck-figure :is(p, ul, ol):last-child { margin-block-end: 0; }
|
|
119
|
+
.deck-words { display: block; }
|
|
120
|
+
.deck-figure .deck-lead { font-family: var(--deck-heading); font-weight: 700; line-height: 1.2; }
|
|
121
|
+
.deck-figure .deck-text { line-height: 1.35; }
|
|
122
|
+
.deck-figure :is(ul, ol).deck-words { font-size: 0.85em; }
|
|
123
|
+
/* On the fill, words are on the fill's ink, their bold and links too. */
|
|
124
|
+
:is(.deck-tile.is-big, .deck-part.is-highlight, .deck-chevron, .deck-level) :is(strong, a, h3, .deck-h3) { color: inherit; }
|
|
125
|
+
:is(.deck-tile.is-big, .deck-part.is-highlight) li::marker { color: inherit; }
|
|
126
|
+
/* A chart or a diagram on the fill would lose its first colour, the fill's: what holds one is on the soft fill, ringed with it. */
|
|
127
|
+
:is(.deck-tile.is-big, .deck-part.is-highlight):has(.deck-block) { background: var(--deck-soft); color: var(--deck-ink); border: 0.15em solid var(--deck-fill); }
|
|
128
|
+
/* Steps: a row of cards, each with its number. */
|
|
129
|
+
.deck-steps { align-items: stretch; }
|
|
130
|
+
.deck-card { display: flex; flex-direction: column; gap: 0.45em; padding: 1.1em 1em; background: var(--deck-soft); border-radius: var(--deck-radius); }
|
|
131
|
+
.deck-badge { display: grid; place-items: center; text-align: center; inline-size: 1.9em; block-size: 1.9em; border-radius: 50%; background: var(--deck-fill); color: var(--deck-on-fill); font-family: var(--deck-heading); font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
132
|
+
.deck-card .deck-lead { font-size: 1.05em; }
|
|
133
|
+
.deck-card .deck-text { font-size: 0.9em; }
|
|
134
|
+
/* Key figures: a row of numbers, each with what it counts, a rule between them. */
|
|
135
|
+
.deck-metrics { gap: 1em 0; text-align: center; }
|
|
136
|
+
.deck-metric { display: flex; flex-direction: column; align-items: center; gap: 0.35em; padding: 0.3em 1em; }
|
|
137
|
+
.deck-metric + .deck-metric { border-inline-start: 1px solid var(--deck-line); }
|
|
138
|
+
.deck-metric .deck-lead { font-size: 3.4em; font-weight: 800; line-height: 1; letter-spacing: -0.02em; color: var(--deck-primary); font-variant-numeric: tabular-nums; }
|
|
139
|
+
/* A timeline: its milestones along a line, each its lead over its dot and its words under it. */
|
|
140
|
+
.deck-timeline { grid-template-rows: auto auto auto; gap: 0.6em 0; justify-items: center; text-align: center; }
|
|
141
|
+
.deck-timeline-line { grid-row: 2; grid-column: 1 / -1; justify-self: stretch; align-self: center; block-size: 0.2em; margin-inline: calc(50% / var(--deck-count)); background: var(--deck-line); border-radius: 0.1em; }
|
|
142
|
+
.deck-timeline .deck-lead { align-self: end; padding-inline: 0.4em; font-size: 1.2em; color: var(--deck-primary); }
|
|
143
|
+
.deck-dot { inline-size: 1.15em; block-size: 1.15em; border-radius: 50%; background: var(--deck-fill); border: 0.22em solid var(--deck-paper); }
|
|
144
|
+
.deck-milestone { align-self: start; padding-inline: 0.4em; font-size: 0.9em; }
|
|
145
|
+
/* A process: chevrons, each holding a step's name, its words under it; they point the way the language reads. */
|
|
146
|
+
.deck-process { gap: 0.7em 0; }
|
|
147
|
+
.deck-chevron { --deck-tip: 0.9em; display: flex; align-items: center; justify-content: center; min-block-size: 3.2em; padding: 0.5em calc(var(--deck-tip) + 0.3em) 0.5em calc(var(--deck-tip) * 2); margin-inline-end: calc(0.3em - var(--deck-tip)); background: var(--deck-fill); color: var(--deck-on-fill); text-align: center;
|
|
148
|
+
clip-path: polygon(0% 0%, calc(100% - var(--deck-tip)) 0%, 100% 50%, calc(100% - var(--deck-tip)) 100%, 0% 100%, var(--deck-tip) 50%); }
|
|
149
|
+
.deck-chevron[data-first] { padding-inline-start: 0.9em; clip-path: polygon(0% 0%, calc(100% - var(--deck-tip)) 0%, 100% 50%, calc(100% - var(--deck-tip)) 100%, 0% 100%); }
|
|
150
|
+
.deck-chevron[data-last] { margin-inline-end: 0; }
|
|
151
|
+
.deck[dir="rtl"] .deck-chevron { clip-path: polygon(100% 0%, var(--deck-tip) 0%, 0% 50%, var(--deck-tip) 100%, 100% 100%, calc(100% - var(--deck-tip)) 50%); }
|
|
152
|
+
.deck[dir="rtl"] .deck-chevron[data-first] { clip-path: polygon(100% 0%, var(--deck-tip) 0%, 0% 50%, var(--deck-tip) 100%, 100% 100%); }
|
|
153
|
+
.deck-under { align-self: start; padding-inline: 0.6em; font-size: 0.9em; text-align: center; }
|
|
154
|
+
/* A programme, and the deck's contents: rows of a time (or a number) and its words, in two columns from seven. */
|
|
155
|
+
.deck-agenda { gap: 0 3em; }
|
|
156
|
+
.deck-agenda-col { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 1.2em; align-content: start; }
|
|
157
|
+
.deck-row { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; align-items: baseline; padding-block: 0.55em; border-block-end: 1px solid var(--deck-line); }
|
|
158
|
+
.deck-row > :not(.deck-lead) { grid-column: 2; }
|
|
159
|
+
.deck-row .deck-lead { color: var(--deck-primary); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
160
|
+
.deck-contents { font-size: 1.1em; }
|
|
161
|
+
.deck-contents .deck-row { cursor: pointer; }
|
|
162
|
+
.deck-contents .deck-row:focus-visible { outline: 0.12em solid var(--deck-primary); outline-offset: 0.1em; }
|
|
163
|
+
.deck-contents .deck-row .deck-text { font-family: var(--deck-heading); font-weight: 600; }
|
|
164
|
+
.deck-contents .deck-row.is-sub { padding-block: 0.3em; border-block-end-style: dotted; font-size: 0.85em; }
|
|
165
|
+
.deck-contents .deck-row.is-sub .deck-text { font-family: var(--deck-body); font-weight: 400; }
|
|
166
|
+
/* A pyramid: its levels stacked, the first the narrowest, on top; what each says beside it. */
|
|
167
|
+
.deck-pyramid { gap: 0.3em 2em; justify-content: center; }
|
|
168
|
+
.deck-level { display: flex; align-items: center; justify-content: center; min-block-size: 2.6em; padding-block: 0.45em; background: var(--deck-fill); color: var(--deck-on-fill); text-align: center; }
|
|
169
|
+
.deck-level .deck-words { font-family: var(--deck-heading); font-weight: 700; }
|
|
170
|
+
.deck-beside { align-self: center; font-size: 0.9em; }
|
|
171
|
+
/* A bento: the first tile big, the others around it. */
|
|
172
|
+
.deck-bento { align-content: stretch; gap: 0.9em; grid-template-columns: 1.4fr repeat(2, minmax(0, 1fr)); grid-auto-rows: 1fr; }
|
|
173
|
+
.deck-tile { display: flex; flex-direction: column; justify-content: safe flex-end; gap: 0.35em; padding: 1em 1.1em; background: var(--deck-soft); border-radius: var(--deck-radius); }
|
|
174
|
+
.deck-tile.is-big { grid-row: span 2; background: var(--deck-fill); color: var(--deck-on-fill); }
|
|
175
|
+
.deck-tile .deck-lead { font-size: 1.15em; }
|
|
176
|
+
.deck-tile.is-big .deck-lead { font-size: 1.9em; }
|
|
177
|
+
.deck-tile .deck-block { min-block-size: 6cqi; }
|
|
178
|
+
.deck-bento[data-count="1"] { grid-template-columns: 1fr; }
|
|
179
|
+
.deck-bento[data-count="2"] { grid-template-columns: 1.3fr 1fr; }
|
|
180
|
+
.deck-bento[data-count="2"] .deck-tile.is-big { grid-row: auto; }
|
|
181
|
+
.deck-bento[data-count="3"] { grid-template-columns: 1.3fr 1fr; }
|
|
182
|
+
.deck-bento[data-count="4"] .deck-tile:nth-child(2) { grid-column: span 2; }
|
|
183
|
+
/* Parts face to face, a medallion between them, the last on the fill; or four in a matrix, the second, top at the end, on the fill. */
|
|
184
|
+
.deck-versus, .deck-matrix { align-content: stretch; }
|
|
185
|
+
.deck-versus { column-gap: 0; grid-auto-rows: 1fr; }
|
|
186
|
+
.deck-matrix { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: 1fr; gap: 0.8em; }
|
|
187
|
+
.deck-part { display: flex; flex-direction: column; padding: 1.1em 1.4em; background: var(--deck-soft); border-radius: var(--deck-radius); }
|
|
188
|
+
.deck-part.is-highlight { background: var(--deck-fill); color: var(--deck-on-fill); }
|
|
189
|
+
.deck-slide .deck-part :is(h1, h2, h3, h4, .deck-h) { font-size: 1.35em; color: inherit; margin-block-end: 0.45em; }
|
|
190
|
+
.deck-vs { position: relative; z-index: 1; align-self: center; display: grid; place-items: center; text-align: center; inline-size: 3.2em; block-size: 3.2em; margin-inline: -1em; border-radius: 50%; background: var(--deck-ink); color: var(--deck-paper); border: 0.25em solid var(--deck-paper); font-family: var(--deck-heading); font-weight: 800; font-size: 0.9em; letter-spacing: 0.02em; }
|
|
85
191
|
/* Scripts without italics keep their upright letters. */
|
|
86
192
|
.deck :is(blockquote, em):is(:lang(ar), :lang(fa), :lang(ur), :lang(he), :lang(ja), :lang(zh), :lang(ko), :lang(hi), :lang(th)) { font-style: normal; }
|
|
87
193
|
.deck-layout-quote blockquote::before { content: "\201C"; display: block; font-size: 2.4em; line-height: 0.6; color: var(--deck-primary); }
|
|
@@ -93,6 +199,11 @@
|
|
|
93
199
|
:is(.deck-layout-image-left, .deck-layout-image-right).has-image .deck-slot { padding: 6cqi 5cqi; justify-content: center; }
|
|
94
200
|
:is(.deck-layout-image, .deck-layout-cover).has-image .deck-image { position: absolute; inset: 0; }
|
|
95
201
|
:is(.deck-layout-image, .deck-layout-cover).has-image .deck-slot { position: relative; color: #fff; text-shadow: 0 0.05em 0.4em rgb(0 0 0 / 0.6); }
|
|
202
|
+
/* A quote over its picture, the whole slide, behind a veil. */
|
|
203
|
+
.deck-layout-quote.has-image .deck-image { position: absolute; inset: 0; }
|
|
204
|
+
.deck-layout-quote.has-image .deck-image::after { content: ""; position: absolute; inset: 0; background-color: rgb(0 0 0 / 0.55); }
|
|
205
|
+
.deck-layout-quote.has-image .deck-slot { position: relative; color: #fff; text-shadow: 0 0.05em 0.4em rgb(0 0 0 / 0.5); }
|
|
206
|
+
.deck-layout-quote.has-image blockquote::before { color: inherit; }
|
|
96
207
|
.deck-layout-cover.has-image .deck-image::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to top, rgb(0 0 0 / 0.65), rgb(0 0 0 / 0.1)); }
|
|
97
208
|
.deck-class-invert { background: var(--deck-ink); color: var(--deck-paper); }
|
|
98
209
|
.deck-class-text-center .deck-slot { text-align: center; align-items: center; }
|
package/src/deck.js
CHANGED
|
@@ -124,6 +124,7 @@ export function createDeck(root, options) {
|
|
|
124
124
|
|
|
125
125
|
function draw() {
|
|
126
126
|
context.total = deck.slides.length
|
|
127
|
+
context.deck = deck
|
|
127
128
|
drawn = deck.slides.map((slide) => renderSlide(slide, context))
|
|
128
129
|
for (const [i, { element }] of drawn.entries()) {
|
|
129
130
|
element.setAttribute('aria-label', words.where.replace('{n}', number(i + 1)).replace('{count}', number(drawn.length)))
|
|
@@ -210,7 +211,8 @@ export function createDeck(root, options) {
|
|
|
210
211
|
*/
|
|
211
212
|
function name(slide, toward) {
|
|
212
213
|
const key = (/** @type {Element} */ el) => `${el.tagName}:${el.tagName === 'IMG' ? el.getAttribute('src') : (el.textContent ?? '').trim().slice(0, 80)}`
|
|
213
|
-
|
|
214
|
+
// A mind map's words, not its items, which hold no box of their own.
|
|
215
|
+
const parts = (/** @type {HTMLElement} */ s) => /** @type {HTMLElement[]} */ ([...s.querySelectorAll('h1, h2, h3, h4, .deck-h, li, p, img, .deck-block, blockquote, .deck-image, .deck-map-node')].filter((el) => !el.matches('.deck-mindmap li')))
|
|
214
216
|
if (toward) {
|
|
215
217
|
const there = new Map(parts(toward).map((el) => [key(el), el]))
|
|
216
218
|
let n = 0
|
|
@@ -296,6 +298,8 @@ export function createDeck(root, options) {
|
|
|
296
298
|
target.addEventListener('blur', blur)
|
|
297
299
|
})
|
|
298
300
|
|
|
301
|
+
// The deck's contents: a part chosen, its slide shown.
|
|
302
|
+
const chosen = (/** @type {Event} */ event) => /** @type {HTMLElement | null} */ (/** @type {Element} */ (event.target).closest('.deck-slide.is-shown [data-go]'))
|
|
299
303
|
// The keys a clicker sends, and the deck's own.
|
|
300
304
|
root.addEventListener('keydown', (event) => {
|
|
301
305
|
const target = /** @type {HTMLElement} */ (event.target)
|
|
@@ -303,6 +307,8 @@ export function createDeck(root, options) {
|
|
|
303
307
|
if ((event.metaKey || event.ctrlKey) && event.key === 's') { event.preventDefault(); options.onSave?.(source) }
|
|
304
308
|
return
|
|
305
309
|
}
|
|
310
|
+
const part = chosen(event)
|
|
311
|
+
if (part && event.key === 'Enter') { event.preventDefault(); go(Number(part.dataset.go)); return }
|
|
306
312
|
const forward = dir === 'rtl' ? 'ArrowLeft' : 'ArrowRight', backward = dir === 'rtl' ? 'ArrowRight' : 'ArrowLeft'
|
|
307
313
|
if (event.altKey || event.ctrlKey || event.metaKey) {
|
|
308
314
|
if ((event.metaKey || event.ctrlKey) && event.key === 's') { event.preventDefault(); options.onSave?.(source) }
|
|
@@ -317,6 +323,10 @@ export function createDeck(root, options) {
|
|
|
317
323
|
event.preventDefault()
|
|
318
324
|
act()
|
|
319
325
|
})
|
|
326
|
+
stage.addEventListener('click', (event) => {
|
|
327
|
+
const part = chosen(event)
|
|
328
|
+
if (part && !root.classList.contains('is-editing') && !root.classList.contains('is-overview')) { event.stopPropagation(); go(Number(part.dataset.go)) }
|
|
329
|
+
}, true)
|
|
320
330
|
stage.addEventListener('click', (event) => {
|
|
321
331
|
if (document.fullscreenElement && !(/** @type {Element} */ (event.target).closest('a, button, input'))) move(1)
|
|
322
332
|
})
|
package/src/figures.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// The figures a layout draws from what a slide holds, as slides usually show it: a slot's lists as
|
|
2
|
+
// a row of numbered cards (steps), of key figures (metrics), a timeline, chevrons (process), a timed
|
|
3
|
+
// programme (agenda), a pyramid or a bento; its `##` parts face to face (versus) or four in a
|
|
4
|
+
// matrix; the deck's contents (summary). An item's lead, the words in bold it starts with (`**Q1**`,
|
|
5
|
+
// `**56%**`, `**6:00 PM**`), is its label and the rest its words, each changed in place as a list's
|
|
6
|
+
// are. What is drawn as a shape says so (.deck-shape), and what is words (.deck-words), for
|
|
7
|
+
// PowerPoint to write them as its own shapes and boxes of words.
|
|
8
|
+
import { h, inline, step, range, block } from './render.js'
|
|
9
|
+
import { layoutNameOf } from './kit.js'
|
|
10
|
+
|
|
11
|
+
/** @typedef {import('./render.js').Context} Context */
|
|
12
|
+
/** @typedef {{ tokens: any[], start: number, end: number, blocks: { token: any, start: number }[], step: number }} Entry */
|
|
13
|
+
/** @typedef {{ lead: any[] | null, leadAt: [number, number] | null, rest: any[], restAt: [number, number] | null }} Split */
|
|
14
|
+
|
|
15
|
+
// What may come between a lead and its words: a colon, a dash, a bar, a dot.
|
|
16
|
+
const SEP = /^\s*(?:[::—–|·-]\s*)?/
|
|
17
|
+
// A lead not in bold: a time starting a programme's line, a number starting a key figure's.
|
|
18
|
+
const TIME = /^\d{1,2}(?:[:h.]\d{2}|h)?(?:\s?[ap]\.?m\.?)?(?:\s?[–-]\s?\d{1,2}(?:[:h.]\d{2}|h)?(?:\s?[ap]\.?m\.?)?)?(?=\s|$)/i
|
|
19
|
+
const NUMBER = /^[+−-]?[^\p{L}\p{N}\s]?\d[\d., ]*\S*(?=\s|$)/u
|
|
20
|
+
const PATTERNS = /** @type {Record<string, RegExp>} */ ({ agenda: TIME, metrics: NUMBER })
|
|
21
|
+
|
|
22
|
+
/** A paragraph whose every line starts in bold, as key figures are written without a list. @param {any} token */
|
|
23
|
+
export const boldLines = (token) => token.type === 'paragraph' && token.raw.trim().split('\n').every((/** @type {string} */ line) => /^\s*(\*\*|__)\S/.test(line))
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A list's items, or a paragraph's lines that start in bold, as a figure's entries: each one's words (its first line's
|
|
27
|
+
* inline tokens) and where they are in the source, `end` -1 when they run over lines; what is under an item (a list, a
|
|
28
|
+
* chart) its blocks. @param {any} token @param {number} start where its raw starts @param {(markdown: string) => any[]} lex
|
|
29
|
+
* @returns {Entry[]}
|
|
30
|
+
*/
|
|
31
|
+
export function entriesOf(token, start, lex) {
|
|
32
|
+
if (token.type === 'paragraph') {
|
|
33
|
+
let at = start
|
|
34
|
+
return token.raw.split('\n').flatMap((/** @type {string} */ line) => {
|
|
35
|
+
const from = at
|
|
36
|
+
at += line.length + 1
|
|
37
|
+
const text = line.trim()
|
|
38
|
+
if (!text) return []
|
|
39
|
+
const [first] = lex(text)
|
|
40
|
+
return [{ tokens: first?.tokens ?? [], start: from + line.length - line.trimStart().length, end: from + line.trimEnd().length, blocks: [], step: 0 }]
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
let from = 0
|
|
44
|
+
return token.items.map((/** @type {any} */ item) => {
|
|
45
|
+
const offset = token.raw.indexOf(item.raw, from)
|
|
46
|
+
from = offset + item.raw.length
|
|
47
|
+
const itemStart = start + Math.max(0, offset)
|
|
48
|
+
const marker = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?/.exec(item.raw)
|
|
49
|
+
const [first, ...rest] = item.tokens
|
|
50
|
+
const words = first && (first.type === 'text' || first.type === 'paragraph') ? first : null
|
|
51
|
+
const at = itemStart + (marker?.[0].length ?? 0)
|
|
52
|
+
return {
|
|
53
|
+
tokens: words ? words.tokens ?? [{ type: 'text', raw: words.text, text: words.text }] : [],
|
|
54
|
+
start: at, end: words && marker && !words.raw.trim().includes('\n') ? at + words.raw.trim().length : -1,
|
|
55
|
+
blocks: (words ? rest : item.tokens).map((/** @type {any} */ inner) => ({ token: inner, start: itemStart + Math.max(0, item.raw.indexOf(inner.raw)) })), step: 0,
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* An entry's lead and the rest of its words, each with where it is in the source: the bold it starts with, else for a
|
|
62
|
+
* programme a time and for key figures a number; what comes between them (a colon, a dash) left out.
|
|
63
|
+
* @param {Entry} entry @param {string} kind @returns {Split}
|
|
64
|
+
*/
|
|
65
|
+
export function leadOf(entry, kind) {
|
|
66
|
+
const [first, ...others] = entry.tokens
|
|
67
|
+
const editable = entry.end >= 0
|
|
68
|
+
const after = (/** @type {any[]} */ tokens, /** @type {number} */ from) => {
|
|
69
|
+
const next = tokens[0], cut = next?.type === 'text' ? /** @type {RegExpExecArray} */ (SEP.exec(next.raw))[0].length : 0
|
|
70
|
+
const rest = (cut ? [{ ...next, raw: next.raw.slice(cut), text: next.text.slice(/** @type {RegExpExecArray} */ (SEP.exec(next.text))[0].length) }, ...tokens.slice(1)] : tokens).filter((t) => t.raw.trim() || t.type !== 'text')
|
|
71
|
+
return { rest, restAt: /** @type {[number, number] | null} */ (editable && rest.length && from + cut < entry.end ? [from + cut, entry.end] : null) }
|
|
72
|
+
}
|
|
73
|
+
if (first?.type === 'strong') return { lead: first.tokens, leadAt: editable ? [entry.start + 2, entry.start + first.raw.length - 2] : null, ...after(others, entry.start + first.raw.length) }
|
|
74
|
+
const found = first?.type === 'text' && PATTERNS[kind]?.exec(first.raw)
|
|
75
|
+
if (found) {
|
|
76
|
+
const tail = first.raw.slice(found[0].length)
|
|
77
|
+
return { lead: [{ type: 'text', raw: found[0], text: found[0] }], leadAt: editable ? [entry.start, entry.start + found[0].length] : null, ...after([...(tail ? [{ type: 'text', raw: tail, text: tail }] : []), ...others], entry.start + found[0].length) }
|
|
78
|
+
}
|
|
79
|
+
return { lead: null, leadAt: null, rest: entry.tokens, restAt: editable ? [entry.start, entry.end] : null }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Where a slot's parts start: at each heading of the level its last heading has, but the heading the slot opens with when
|
|
84
|
+
* there are more of them than the parts asked for (the slide's title, written at their level).
|
|
85
|
+
* @param {any[]} tokens @param {number} count how many parts the layout has @returns {Set<number>}
|
|
86
|
+
*/
|
|
87
|
+
export function partHeads(tokens, count) {
|
|
88
|
+
const heads = tokens.flatMap((token, i) => (token.type === 'heading' ? [{ depth: token.depth, i }] : []))
|
|
89
|
+
if (!heads.length) return new Set()
|
|
90
|
+
const depth = /** @type {{ depth: number }} */ (heads.at(-1)).depth
|
|
91
|
+
const at = heads.filter((one) => one.depth === depth).map((one) => one.i)
|
|
92
|
+
return new Set(at.length > count && at[0] === tokens.findIndex((token) => token.type !== 'space') ? at.slice(1) : at)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The deck's contents, as Slidev's <Toc> reads them: each slide's title and level, but a slide with `hideInToc: true`,
|
|
97
|
+
* the contents themselves, a cover, an introduction, the end and slides with no title. Where the deck has parts (slides
|
|
98
|
+
* on `section` or `subsection`, or with a `level`), the parts alone, a section at level 1 and a subsection at 2; else
|
|
99
|
+
* every slide, at its heading's level. `hideInToc: false` brings a slide back. Levels start at 1, `maxDepth` at most.
|
|
100
|
+
* @param {import('./parse.js').Slide[]} slides @param {import('./kit.js').Kit} kit @param {number} at the contents' own slide
|
|
101
|
+
* @param {number} [maxDepth] @returns {{ index: number, title: string, level: number }[]}
|
|
102
|
+
*/
|
|
103
|
+
export function contentsOf(slides, kit, at, maxDepth = 2) {
|
|
104
|
+
const all = slides.map((slide) => {
|
|
105
|
+
const name = layoutNameOf(slide, kit), given = slide.settings.level, hidden = slide.settings.hideInToc
|
|
106
|
+
const depth = /^(#{1,6})\s/m.exec(slide.slots.map((slot) => slot.text).join('\n').replace(/^(`{3,}|~{3,})[\s\S]*?^\1/gm, ''))?.[1].length ?? 1
|
|
107
|
+
const part = Number.isInteger(given) || name === 'section' || name === 'subsection'
|
|
108
|
+
const level = Number.isInteger(given) ? given : name === 'section' ? 1 : name === 'subsection' ? 2 : depth
|
|
109
|
+
const kept = Boolean(slide.title) && (hidden === false || (hidden !== true && slide.index !== at && !kit.layouts[name]?.contents && !['cover', 'intro', 'end'].includes(name)))
|
|
110
|
+
return { index: slide.index, title: slide.title, level, part, kept, shown: hidden === false }
|
|
111
|
+
}).filter((one) => one.kept)
|
|
112
|
+
const chosen = all.some((one) => one.part) ? all.filter((one) => one.part || one.shown) : all
|
|
113
|
+
const top = Math.min(...chosen.map((one) => one.level))
|
|
114
|
+
return chosen.map(({ index, title, level }) => ({ index, title, level: level - top + 1 })).filter((one) => one.level <= Math.max(1, maxDepth))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Columns for a row of n, wrapping beyond `most`. @param {HTMLElement} figure @param {number} n @param {number} most */
|
|
118
|
+
const columns = (figure, n, most) => figure.style.setProperty('grid-template-columns', `repeat(${n <= most ? n : Math.ceil(n / 2)}, minmax(0, 1fr))`)
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A slot's lists as the figure its layout draws. Entries a click each (<v-clicks>) show their card, milestone, chevron,
|
|
122
|
+
* row, level or tile at their step. @param {string} kind @param {Entry[]} entries @param {Context} context @returns {HTMLElement}
|
|
123
|
+
*/
|
|
124
|
+
export function listFigure(kind, entries, context) {
|
|
125
|
+
const items = entries.map((entry) => ({ entry, ...leadOf(entry, kind) }))
|
|
126
|
+
const n = items.length
|
|
127
|
+
const figure = h('div', { class: `deck-figure deck-${kind}`, 'data-count': n })
|
|
128
|
+
figure.style.setProperty('--deck-count', String(n))
|
|
129
|
+
/** @typedef {(typeof items)[number]} Item */
|
|
130
|
+
const at = (/** @type {[number, number] | null} */ where) => (where ? range(...where) : undefined)
|
|
131
|
+
const lead = (/** @type {Item} */ item) => (item.lead ? h('span', { class: 'deck-words deck-lead', dir: 'auto', 'data-edit': at(item.leadAt) }, inline(item.lead, context)) : null)
|
|
132
|
+
const text = (/** @type {Item} */ item) => (item.rest.length ? h('span', { class: 'deck-words deck-text', dir: 'auto', 'data-edit': at(item.restAt) }, inline(item.rest, context)) : null)
|
|
133
|
+
// What is under an item: a list as words, a chart or a diagram as itself.
|
|
134
|
+
const more = (/** @type {Item} */ item) => item.entry.blocks.map(({ token, start }) => {
|
|
135
|
+
const element = block(token, start, context, null)
|
|
136
|
+
if (element && !element.matches('.deck-block, .deck-table, pre')) element.classList.add('deck-words')
|
|
137
|
+
return element
|
|
138
|
+
})
|
|
139
|
+
const stepped = (/** @type {Item} */ item, /** @type {(Element | null)[]} */ elements) => { if (item.entry.step) for (const one of elements) if (one) step(one, item.entry.step) }
|
|
140
|
+
const cell = (/** @type {HTMLElement} */ element, /** @type {number} */ column, /** @type {number} */ row) => { element.style.setProperty('grid-column', String(column)); element.style.setProperty('grid-row', String(row)); return element }
|
|
141
|
+
// The lead, else the words, as what a chevron or a level says; the words beside or under it when there is a lead.
|
|
142
|
+
const label = (/** @type {Item} */ item) => lead(item) ?? text(item) ?? h('span', { class: 'deck-words deck-lead' })
|
|
143
|
+
const under = (/** @type {Item} */ item, /** @type {string} */ name) => h('div', { class: name }, [item.lead ? text(item) : null, ...more(item)])
|
|
144
|
+
switch (kind) {
|
|
145
|
+
case 'steps':
|
|
146
|
+
columns(figure, n, 5)
|
|
147
|
+
for (const [i, item] of items.entries()) {
|
|
148
|
+
const card = h('div', { class: 'deck-card deck-shape' }, [h('span', { class: 'deck-words deck-badge deck-shape' }, [context.number(i + 1)]), lead(item), text(item), ...more(item)])
|
|
149
|
+
stepped(item, [card])
|
|
150
|
+
figure.append(card)
|
|
151
|
+
}
|
|
152
|
+
break
|
|
153
|
+
case 'metrics':
|
|
154
|
+
columns(figure, n, 4)
|
|
155
|
+
for (const item of items) {
|
|
156
|
+
const metric = h('div', { class: 'deck-metric deck-shape' }, [lead(item), text(item), ...more(item)])
|
|
157
|
+
stepped(item, [metric])
|
|
158
|
+
figure.append(metric)
|
|
159
|
+
}
|
|
160
|
+
break
|
|
161
|
+
case 'timeline':
|
|
162
|
+
columns(figure, n, 99)
|
|
163
|
+
// The line first, under its dots, from the first to the last.
|
|
164
|
+
figure.append(h('span', { class: 'deck-timeline-line deck-shape' }))
|
|
165
|
+
for (const [i, item] of items.entries()) {
|
|
166
|
+
const parts = [cell(lead(item) ?? h('span', { class: 'deck-lead' }), i + 1, 1), cell(h('span', { class: 'deck-dot deck-shape' }), i + 1, 2), cell(h('div', { class: 'deck-milestone' }, [text(item), ...more(item)]), i + 1, 3)]
|
|
167
|
+
stepped(item, parts)
|
|
168
|
+
figure.append(...parts)
|
|
169
|
+
}
|
|
170
|
+
break
|
|
171
|
+
case 'process':
|
|
172
|
+
columns(figure, n, 99)
|
|
173
|
+
for (const [i, item] of items.entries()) {
|
|
174
|
+
const parts = [cell(h('div', { class: 'deck-chevron deck-shape', 'data-first': i === 0 || undefined, 'data-last': i === n - 1 || undefined }, [label(item)]), i + 1, 1), cell(under(item, 'deck-under'), i + 1, 2)]
|
|
175
|
+
stepped(item, parts)
|
|
176
|
+
figure.append(...parts)
|
|
177
|
+
}
|
|
178
|
+
break
|
|
179
|
+
case 'agenda': {
|
|
180
|
+
// In two columns from seven lines, the first holding the more.
|
|
181
|
+
const halves = n >= 7 ? [items.slice(0, Math.ceil(n / 2)), items.slice(Math.ceil(n / 2))] : [items]
|
|
182
|
+
figure.style.setProperty('grid-template-columns', `repeat(${halves.length}, minmax(0, 1fr))`)
|
|
183
|
+
for (const half of halves) {
|
|
184
|
+
figure.append(h('div', { class: 'deck-agenda-col' }, half.map((item) => {
|
|
185
|
+
const row = h('div', { class: 'deck-row deck-shape' }, [lead(item) ?? h('span', { class: 'deck-lead' }), text(item), ...more(item)])
|
|
186
|
+
stepped(item, [row])
|
|
187
|
+
return row
|
|
188
|
+
})))
|
|
189
|
+
}
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
case 'pyramid': {
|
|
193
|
+
// Narrowest at the top, widest at the bottom, each level a band of one triangle; its words held to its top edge.
|
|
194
|
+
const beside = items.some((item) => item.lead && (item.rest.length || item.entry.blocks.length))
|
|
195
|
+
figure.style.setProperty('grid-template-columns', beside ? 'minmax(0, 1.15fr) minmax(0, 1fr)' : 'minmax(0, 30em)')
|
|
196
|
+
const half = (/** @type {number} */ k) => Math.round(5000 * (0.16 + (0.84 * k) / n)) / 100
|
|
197
|
+
for (const [i, item] of items.entries()) {
|
|
198
|
+
const band = cell(h('div', { class: 'deck-level deck-shape' }, [label(item)]), 1, i + 1)
|
|
199
|
+
band.style.setProperty('clip-path', `polygon(${50 - half(i)}% 0%, ${50 + half(i)}% 0%, ${50 + half(i + 1)}% 100%, ${50 - half(i + 1)}% 100%)`)
|
|
200
|
+
band.style.setProperty('padding-inline', `max(0.4em, ${Math.round((50 - half(i)) * 100) / 100}%)`)
|
|
201
|
+
const parts = [band, beside ? cell(under(item, 'deck-beside'), 2, i + 1) : null]
|
|
202
|
+
stepped(item, parts)
|
|
203
|
+
figure.append(...parts.filter((one) => one !== null))
|
|
204
|
+
}
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
case 'bento':
|
|
208
|
+
for (const [i, item] of items.entries()) {
|
|
209
|
+
const tile = h('div', { class: `deck-tile deck-shape${i === 0 ? ' is-big' : ''}` }, [lead(item), text(item), ...more(item)])
|
|
210
|
+
stepped(item, [tile])
|
|
211
|
+
figure.append(tile)
|
|
212
|
+
}
|
|
213
|
+
break
|
|
214
|
+
}
|
|
215
|
+
return figure
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A slot's parts as its layout sets them: face to face, the last on the fill, a medallion between each two (versus), or
|
|
220
|
+
* in a grid of two columns, the second, top at the end, on the fill (matrix). Their words stay as they were drawn.
|
|
221
|
+
* @param {string} kind @param {Node[][]} parts @returns {HTMLElement}
|
|
222
|
+
*/
|
|
223
|
+
export function partsFigure(kind, parts) {
|
|
224
|
+
const figure = h('div', { class: `deck-figure deck-${kind}`, 'data-count': parts.length })
|
|
225
|
+
if (kind === 'versus') figure.style.setProperty('grid-template-columns', parts.map(() => 'minmax(0, 1fr)').join(' auto '))
|
|
226
|
+
for (const [i, children] of parts.entries()) {
|
|
227
|
+
for (const child of children) if (child instanceof HTMLElement && !child.matches('.deck-block, .deck-table, pre, .deck-picture')) child.classList.add('deck-words')
|
|
228
|
+
if (kind === 'versus' && i > 0) figure.append(h('span', { class: 'deck-vs deck-shape deck-words', 'aria-hidden': 'true' }, ['VS']))
|
|
229
|
+
const highlight = parts.length > 1 && (kind === 'versus' ? i === parts.length - 1 : i === 1)
|
|
230
|
+
figure.append(h('div', { class: `deck-part deck-shape${highlight ? ' is-highlight' : ''}` }, children))
|
|
231
|
+
}
|
|
232
|
+
return figure
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The deck's contents as rows, a part's number and its title, each going to its slide when chosen; a subsection under
|
|
237
|
+
* its section, without a number; in two columns from seven rows. @param {Context} context @param {number} at its own slide
|
|
238
|
+
* @param {number} [maxDepth] @returns {HTMLElement | null}
|
|
239
|
+
*/
|
|
240
|
+
export function contentsFigure(context, at, maxDepth) {
|
|
241
|
+
const entries = context.deck ? contentsOf(context.deck.slides, context.kit, at, maxDepth) : []
|
|
242
|
+
if (!entries.length) return null
|
|
243
|
+
let n = 0
|
|
244
|
+
const rows = entries.map(({ index, title, level }) => {
|
|
245
|
+
const [heading] = context.lex(`# ${title}`)
|
|
246
|
+
return h('div', { class: `deck-row deck-shape${level > 1 ? ' is-sub' : ''}`, 'data-go': index, role: 'link', tabindex: 0 }, [
|
|
247
|
+
h('span', { class: 'deck-words deck-lead' }, level > 1 ? [] : [context.number(++n)]),
|
|
248
|
+
h('span', { class: 'deck-words deck-text', dir: 'auto' }, inline(heading?.tokens ?? [{ type: 'text', text: title }], context)),
|
|
249
|
+
])
|
|
250
|
+
})
|
|
251
|
+
const halves = rows.length >= 7 ? [rows.slice(0, Math.ceil(rows.length / 2)), rows.slice(Math.ceil(rows.length / 2))] : [rows]
|
|
252
|
+
const figure = h('div', { class: 'deck-figure deck-agenda deck-contents', 'data-count': rows.length }, halves.map((half) => h('div', { class: 'deck-agenda-col' }, half)))
|
|
253
|
+
figure.style.setProperty('grid-template-columns', `repeat(${halves.length}, minmax(0, 1fr))`)
|
|
254
|
+
return figure
|
|
255
|
+
}
|
package/src/kit.js
CHANGED
|
@@ -3,19 +3,24 @@
|
|
|
3
3
|
// deck's own kit holds the usual layouts; a host's (an organisation's, a project's) and the deck
|
|
4
4
|
// file's own `kit:` go over it, in that order. A layout is a grid: its slots by name, the areas
|
|
5
5
|
// they take, its tracks, how its words sit, what paper it is on, how big its logo shows, and the
|
|
6
|
-
// PowerPoint layout it is nearest to,
|
|
7
|
-
//
|
|
6
|
+
// PowerPoint layout it is nearest to, whether its lists are drawn as a mind map or a figure (cards, numbers, a timeline,
|
|
7
|
+
// chevrons, a programme, a pyramid, a bento), whether its `##` parts face each other or make a matrix, whether it shows
|
|
8
|
+
// the deck's contents, and the art a template draws behind it, layers among the kit's pictures; a kit may turn one of
|
|
9
|
+
// the usual layouts off. Its tokens may hold a logo, a picture inside
|
|
8
10
|
// the kit and never an address, as its pictures are. Everything is checked against a small grammar;
|
|
9
11
|
// what fails is dropped, with a reason. Pure.
|
|
10
12
|
|
|
11
13
|
/** @typedef {{ required?: boolean, max_words?: number, max_chars?: number, items?: [number, number], item_max_words?: number, heading_max_words?: number, kind?: string }} Budget */
|
|
12
|
-
/** @typedef {
|
|
14
|
+
/** @typedef {'bullets' | 'mindmap' | 'steps' | 'metrics' | 'timeline' | 'process' | 'agenda' | 'pyramid' | 'bento'} List */
|
|
15
|
+
/** @typedef {{ label?: Record<string, string> | string, intent?: string, use_when?: string, avoid_when?: string, slots: string[], budgets: Record<string, Budget>, areas: string[][], columns: string[], rows: string[], align: 'start' | 'center' | 'end', justify: 'start' | 'center' | 'end', paper: 'paper' | 'fill' | 'ink' | 'soft', title: 'normal' | 'large' | 'huge', logo: 'big' | 'small' | 'none', lists: List, parts: 'none' | 'versus' | 'matrix', contents: boolean, pptx: string, numbered: boolean, art?: string[] }} Layout */
|
|
13
16
|
/** @typedef {{ name?: string, tokens: Record<string, any>, layouts: Record<string, Layout>, pictures: Record<string, string> }} Kit */
|
|
14
17
|
|
|
15
18
|
const NAME = /^[a-z][a-z0-9-]{0,31}$/
|
|
16
19
|
const TRACK = /^(auto|min-content|max-content|\d{1,2}(\.\d)?fr|\d{1,3}%)$/
|
|
17
20
|
const PPTX = ['Title Slide', 'Title and Content', 'Section Header', 'Two Content', 'Comparison', 'Title Only', 'Blank', 'Content with Caption', 'Picture with Caption']
|
|
18
|
-
const ENUMS = /** @type {const} */ ({
|
|
21
|
+
const ENUMS = /** @type {const} */ ({
|
|
22
|
+
align: ['start', 'center', 'end'], justify: ['start', 'center', 'end'], paper: ['paper', 'fill', 'ink', 'soft'], title: ['normal', 'large', 'huge'], logo: ['small', 'big', 'none'],
|
|
23
|
+
lists: ['bullets', 'mindmap', 'steps', 'metrics', 'timeline', 'process', 'agenda', 'pyramid', 'bento'], parts: ['none', 'versus', 'matrix'] })
|
|
19
24
|
/** The corners a logo sits in, as `logoAt` names them, the first by default. */
|
|
20
25
|
export const CORNERS = ['top-end', 'top-start', 'bottom-end', 'bottom-start']
|
|
21
26
|
|
|
@@ -34,7 +39,10 @@ export const artOf = (value) => (typeof value === 'string' && value.length <= 3_
|
|
|
34
39
|
/** The names brains give the usual layouts, for the ones deck has. */
|
|
35
40
|
export const ALIASES = /** @type {Record<string, string>} */ ({
|
|
36
41
|
title: 'cover', 'title-slide': 'cover', 'two-columns': 'two-cols', 'two-column': 'two-cols', columns: 'two-cols', comparison: 'two-cols-header',
|
|
37
|
-
'three-columns': 'three-cols', 'section-header': 'section', divider: 'section', big: 'fact', number: 'fact', 'image-text': 'image-right', closing: 'end', thanks: 'end',
|
|
42
|
+
'three-columns': 'three-cols', 'section-header': 'section', divider: 'section', big: 'fact', number: 'fact', 'image-text': 'image-right', closing: 'end', thanks: 'end', 'mind-map': 'mindmap', map: 'mindmap',
|
|
43
|
+
toc: 'summary', contents: 'summary', 'table-of-contents': 'summary', outline: 'summary', 'sub-section': 'subsection', 'subsection-header': 'subsection',
|
|
44
|
+
cards: 'steps', kpi: 'metrics', kpis: 'metrics', stats: 'metrics', numbers: 'metrics', roadmap: 'timeline', milestones: 'timeline', chevrons: 'process',
|
|
45
|
+
schedule: 'agenda', programme: 'agenda', program: 'agenda', hierarchy: 'pyramid', mosaic: 'bento', vs: 'versus', 'face-off': 'versus', quadrant: 'matrix', quadrants: 'matrix', '2x2': 'matrix',
|
|
38
46
|
})
|
|
39
47
|
|
|
40
48
|
/** deck's own kit: the usual layouts, the ones Slidev names and the ones PowerPoint has, as data. */
|
|
@@ -47,6 +55,8 @@ export const STANDARD = /** @type {Kit} */ ({
|
|
|
47
55
|
cover: { label: { en: 'Cover', fr: 'Couverture' }, justify: 'center', paper: 'fill', title: 'huge', logo: 'big', pptx: 'Title Slide', numbered: false },
|
|
48
56
|
intro: { label: { en: 'Introduction', fr: 'Introduction' }, justify: 'center', paper: 'fill', title: 'large', logo: 'big', pptx: 'Title Slide', numbered: false },
|
|
49
57
|
section: { label: { en: 'Section', fr: 'Section' }, justify: 'center', paper: 'ink', title: 'huge', pptx: 'Section Header', numbered: false },
|
|
58
|
+
subsection: { label: { en: 'Subsection', fr: 'Sous-section' }, justify: 'center', paper: 'soft', title: 'large', pptx: 'Section Header', numbered: false },
|
|
59
|
+
summary: { label: { en: 'Contents', fr: 'Sommaire' }, contents: true, pptx: 'Title and Content' },
|
|
50
60
|
center: { label: { en: 'Centred', fr: 'Centré' }, justify: 'center', align: 'center', pptx: 'Title Only' },
|
|
51
61
|
statement: { label: { en: 'Statement', fr: 'Affirmation' }, justify: 'center', align: 'center', title: 'large', pptx: 'Title Only' },
|
|
52
62
|
fact: { label: { en: 'Big number', fr: 'Grand chiffre' }, justify: 'center', align: 'center', title: 'huge', pptx: 'Title Only' },
|
|
@@ -60,6 +70,16 @@ export const STANDARD = /** @type {Kit} */ ({
|
|
|
60
70
|
'image-left': { label: { en: 'Picture left', fr: 'Image à gauche' }, pptx: 'Picture with Caption', justify: 'center' },
|
|
61
71
|
'image-right': { label: { en: 'Picture right', fr: 'Image à droite' }, pptx: 'Picture with Caption', justify: 'center' },
|
|
62
72
|
image: { label: { en: 'Picture', fr: 'Image' }, logo: 'none', pptx: 'Blank', numbered: false },
|
|
73
|
+
mindmap: { label: { en: 'Mind map', fr: 'Carte mentale' }, lists: 'mindmap', pptx: 'Title Only' },
|
|
74
|
+
steps: { label: { en: 'Steps', fr: 'Étapes' }, lists: 'steps', pptx: 'Title Only' },
|
|
75
|
+
metrics: { label: { en: 'Key figures', fr: 'Chiffres clés' }, lists: 'metrics', pptx: 'Title Only' },
|
|
76
|
+
timeline: { label: { en: 'Timeline', fr: 'Frise' }, lists: 'timeline', pptx: 'Title Only' },
|
|
77
|
+
process: { label: { en: 'Process', fr: 'Processus' }, lists: 'process', pptx: 'Title Only' },
|
|
78
|
+
agenda: { label: { en: 'Agenda', fr: 'Programme' }, lists: 'agenda', pptx: 'Title Only' },
|
|
79
|
+
pyramid: { label: { en: 'Pyramid', fr: 'Pyramide' }, lists: 'pyramid', pptx: 'Title Only' },
|
|
80
|
+
bento: { label: { en: 'Bento', fr: 'Mosaïque' }, lists: 'bento', pptx: 'Title Only' },
|
|
81
|
+
versus: { label: { en: 'Versus', fr: 'Face à face' }, parts: 'versus', pptx: 'Title Only' },
|
|
82
|
+
matrix: { label: { en: 'Matrix', fr: 'Matrice' }, parts: 'matrix', pptx: 'Title Only' },
|
|
63
83
|
})).map(([name, given]) => [name, /** @type {Layout} */ (layoutOf(given).layout)])),
|
|
64
84
|
})
|
|
65
85
|
|
|
@@ -112,7 +132,7 @@ export function layoutOf(given) {
|
|
|
112
132
|
return {
|
|
113
133
|
layout: /** @type {Layout} */ ({
|
|
114
134
|
label, intent: words(given.intent), use_when: words(given.use_when), avoid_when: words(given.avoid_when), slots, budgets, areas: rows, columns, rows: rowTracks,
|
|
115
|
-
align: pick('align', 'start'), justify: pick('justify', 'start'), paper: pick('paper', 'paper'), title: pick('title', 'normal'), logo: pick('logo', 'small'),
|
|
135
|
+
align: pick('align', 'start'), justify: pick('justify', 'start'), paper: pick('paper', 'paper'), title: pick('title', 'normal'), logo: pick('logo', 'small'), lists: pick('lists', 'bullets'), parts: pick('parts', 'none'), contents: given.contents === true,
|
|
116
136
|
pptx: PPTX.includes(given.pptx) ? given.pptx : slots.length > 1 ? 'Comparison' : 'Title and Content', numbered: given.numbered !== false, art,
|
|
117
137
|
}),
|
|
118
138
|
problems,
|
|
@@ -197,6 +217,15 @@ export function labelOf(layout, name, lang) {
|
|
|
197
217
|
return layout.label?.[lang] ?? layout.label?.[lang.split('-')[0]] ?? layout.label?.en ?? name
|
|
198
218
|
}
|
|
199
219
|
|
|
220
|
+
/**
|
|
221
|
+
* The layout a slide is drawn with: the one it names, by its name or a name brains give it, else the one its content
|
|
222
|
+
* suits. @param {{ index: number, settings: Record<string, any>, slots: { name: string, text: string }[] }} slide @param {Kit} kit
|
|
223
|
+
*/
|
|
224
|
+
export function layoutNameOf(slide, kit) {
|
|
225
|
+
const asked = String(slide.settings.layout ?? '').trim().toLowerCase()
|
|
226
|
+
return kit.layouts[asked] ? asked : kit.layouts[ALIASES[asked]] ? ALIASES[asked] : inferLayout(slide)
|
|
227
|
+
}
|
|
228
|
+
|
|
200
229
|
/**
|
|
201
230
|
* The layout a slide that names none is given, from what it holds, as Pandoc's PowerPoint writer
|
|
202
231
|
* chooses: the first slide is the cover; a heading alone, a section; slots beyond the first, the
|