@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/render.js
CHANGED
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
// policy can draw a deck. Each block that can be changed by hand says where its words are in the
|
|
4
4
|
// source (data-edit, offsets), so an edit made on the slide goes back into the Markdown. HTML a
|
|
5
5
|
// deck holds is not drawn: Slidev's <v-click> and <v-clicks> become steps, <br> a line break, and
|
|
6
|
-
// any other tag gives up its words alone. ```echarts and ```mermaid go to the host's drawers.
|
|
7
|
-
|
|
6
|
+
// any other tag gives up its words alone. ```echarts and ```mermaid go to the host's drawers. A
|
|
7
|
+
// layout that draws its lists as a mind map has a slot's lists merged into one map (mindmap.js);
|
|
8
|
+
// one that draws them as a figure (cards, numbers, a timeline…), or sets its parts face to face or
|
|
9
|
+
// in a matrix, or shows the deck's contents, has them drawn so (figures.js).
|
|
10
|
+
import { STANDARD, CORNERS, layoutNameOf } from './kit.js'
|
|
11
|
+
import { itemsOf, mergeItems, keyOf, watchMap } from './mindmap.js'
|
|
12
|
+
import { entriesOf, boldLines, listFigure, partHeads, partsFigure, contentsFigure } from './figures.js'
|
|
13
|
+
import { parseYaml } from './yaml.js'
|
|
8
14
|
|
|
9
15
|
/** @typedef {import('./parse.js').Slide} Slide */
|
|
10
16
|
/** @typedef {import('./parse.js').Slot} Slot */
|
|
11
|
-
/** @typedef {(element: HTMLElement, source: string) => void | Promise<void>} Drawer */
|
|
12
|
-
/** @typedef {{ blocks: Record<string, Drawer>, resolve: (src: string) => string | null, headings: 'real' | 'aria', number: (n: number) => string, total: number, kit: import('./kit.js').Kit, lex: (markdown: string) => any[], slide: string }} Context */
|
|
17
|
+
/** @typedef {(element: HTMLElement, source: string, options?: Record<string, any>) => void | Promise<void>} Drawer */
|
|
18
|
+
/** @typedef {{ blocks: Record<string, Drawer>, resolve: (src: string) => string | null, headings: 'real' | 'aria', number: (n: number) => string, total: number, kit: import('./kit.js').Kit, lex: (markdown: string) => any[], slide: string, deck?: import('./parse.js').Deck }} Context */
|
|
13
19
|
|
|
14
20
|
/**
|
|
15
21
|
* @param {string} tag @param {Record<string, string | number | boolean | undefined>} [attributes]
|
|
@@ -36,8 +42,7 @@ export function safeUrl(href) {
|
|
|
36
42
|
*/
|
|
37
43
|
export function renderSlide(slide, context) {
|
|
38
44
|
// The layout the slide names, by its name or a name brains give it, else the one its content suits.
|
|
39
|
-
const
|
|
40
|
-
const layout = context.kit.layouts[asked] ? asked : context.kit.layouts[ALIASES[asked]] ? ALIASES[asked] : inferLayout(slide)
|
|
45
|
+
const layout = layoutNameOf(slide, context.kit)
|
|
41
46
|
const definition = context.kit.layouts[layout] ?? STANDARD.layouts.default
|
|
42
47
|
const classes = String(slide.settings.class ?? '').split(/\s+/).filter((name) => ['invert', 'text-center', 'text-left'].includes(name))
|
|
43
48
|
const counter = { step: 0 }
|
|
@@ -58,7 +63,7 @@ export function renderSlide(slide, context) {
|
|
|
58
63
|
canvas.style.setProperty('grid-template-rows', definition.rows.join(' '))
|
|
59
64
|
section.append(canvas)
|
|
60
65
|
const image = slide.settings.image ?? slide.settings.background
|
|
61
|
-
if (typeof image === 'string' && ['image-left', 'image-right', 'image', 'cover'].includes(layout)) {
|
|
66
|
+
if (typeof image === 'string' && ['image-left', 'image-right', 'image', 'cover', 'quote'].includes(layout)) {
|
|
62
67
|
const url = context.resolve(image)
|
|
63
68
|
if (url) {
|
|
64
69
|
const picture = h('div', { class: 'deck-image', role: 'img', 'aria-label': String(slide.settings.alt ?? '') || undefined })
|
|
@@ -69,10 +74,13 @@ export function renderSlide(slide, context) {
|
|
|
69
74
|
}
|
|
70
75
|
for (const name of names) {
|
|
71
76
|
const slot = slots.get(name)
|
|
72
|
-
const element = h('div', { class: 'deck-slot', 'data-slot': name }, slot ? blocks(slot, context, counter) : [])
|
|
77
|
+
const element = h('div', { class: 'deck-slot', 'data-slot': name }, slot ? blocks(slot, context, counter, definition.lists, definition.parts) : [])
|
|
73
78
|
element.style.setProperty('grid-area', `s-${name}`)
|
|
74
79
|
canvas.append(element)
|
|
75
80
|
}
|
|
81
|
+
// The deck's contents, under the words of the slot they open.
|
|
82
|
+
const contents = definition.contents && contentsFigure(context, slide.index, Number(slide.settings.maxDepth) || undefined)
|
|
83
|
+
if (contents) canvas.querySelector(`.deck-slot[data-slot="${names[0]}"]`)?.append(contents)
|
|
76
84
|
// Slots a layout doesn't have still show, under the rest, rather than being lost.
|
|
77
85
|
for (const slot of slide.slots) if (!names.includes(slot.name)) canvas.append(h('div', { class: 'deck-slot', 'data-slot': slot.name }, blocks(slot, context, counter)))
|
|
78
86
|
if (definition.numbered && slide.settings.paginate !== false) section.append(h('span', { class: 'deck-number', 'aria-hidden': 'true' }, [context.number(slide.index + 1)]))
|
|
@@ -87,15 +95,50 @@ export function renderSlide(slide, context) {
|
|
|
87
95
|
return { element: section, steps: counter.step }
|
|
88
96
|
}
|
|
89
97
|
|
|
90
|
-
/**
|
|
91
|
-
|
|
98
|
+
/**
|
|
99
|
+
* @param {Slot} slot @param {Context} context @param {{ step: number }} counter
|
|
100
|
+
* @param {import('./kit.js').List} [kind] how its lists are drawn @param {'none' | 'versus' | 'matrix'} [parted] how its parts are set @returns {Node[]}
|
|
101
|
+
*/
|
|
102
|
+
function blocks(slot, context, counter, kind = 'bullets', parted = 'none') {
|
|
92
103
|
const tokens = context.lex(slot.text)
|
|
93
104
|
/** @type {Node[]} */
|
|
94
105
|
const out = []
|
|
106
|
+
// Its parts, each from its heading, and where they go; the blocks of the part being drawn go to it.
|
|
107
|
+
const heads = parted === 'none' ? new Set() : partHeads(tokens, parted === 'versus' ? 2 : 4)
|
|
108
|
+
/** @type {Node[][]} */
|
|
109
|
+
const parts = []
|
|
110
|
+
let sink = out, partsAt = -1
|
|
111
|
+
const mapped = kind === 'mindmap' && !heads.size, figured = kind !== 'bullets' && kind !== 'mindmap' && !heads.size
|
|
112
|
+
// A figure's entries, and where it goes.
|
|
113
|
+
/** @type {import('./figures.js').Entry[]} */
|
|
114
|
+
const entries = []
|
|
115
|
+
let figureAt = -1
|
|
116
|
+
// A mind map's lists, each with its heading when one comes just before it, and where the map goes.
|
|
117
|
+
/** @type {{ items: import('./mindmap.js').Item[], step: boolean }[]} */
|
|
118
|
+
const lists = []
|
|
119
|
+
let mapAt = -1, label = /** @type {HTMLElement | null} */ (null)
|
|
95
120
|
let at = slot.start, mode = /** @type {'' | 'click' | 'clicks'} */ ('')
|
|
96
|
-
for (const token of tokens) {
|
|
121
|
+
for (const [i, token] of tokens.entries()) {
|
|
97
122
|
const start = at
|
|
98
123
|
at += token.raw.length
|
|
124
|
+
if (heads.has(i)) { if (partsAt < 0) partsAt = out.length; parts.push(sink = []) }
|
|
125
|
+
if (figured && (token.type === 'list' || boldLines(token))) {
|
|
126
|
+
if (figureAt < 0) figureAt = out.length
|
|
127
|
+
// A click an entry in <v-clicks>, one for them all in <v-click>, numbered where they are.
|
|
128
|
+
const found = entriesOf(token, start, context.lex), once = mode === 'click' ? ++counter.step : 0
|
|
129
|
+
for (const entry of found) entry.step = once || (mode === 'clicks' ? ++counter.step : 0)
|
|
130
|
+
entries.push(...found)
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
if (mapped && (token.type === 'list' || (token.type === 'heading' && out.length && tokens.slice(i + 1).find((/** @type {any} */ t) => t.type !== 'space')?.type === 'list'))) {
|
|
134
|
+
if (mapAt < 0) mapAt = out.length
|
|
135
|
+
if (token.type === 'heading') { label = /** @type {HTMLElement} */ (block(token, start, context, null)); continue }
|
|
136
|
+
const items = itemsOf(token.raw, start)
|
|
137
|
+
for (const item of items) if (mode === 'clicks') item.step = true
|
|
138
|
+
lists.push({ items: label ? [{ text: label.textContent ?? '', start, end: start, children: items, element: label, step: mode === 'clicks' }] : items, step: mode === 'clicks' })
|
|
139
|
+
label = null
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
99
142
|
// Pandoc's fences: `::: incremental` shows its list a point a click, as <v-clicks> does; the others, nothing.
|
|
100
143
|
const fences = token.type === 'paragraph' ? token.raw.trim().split('\n').map((/** @type {string} */ line) => line.trim()) : []
|
|
101
144
|
if (fences.length && fences.every((/** @type {string} */ line) => /^:{3,}/.test(line))) {
|
|
@@ -107,7 +150,7 @@ function blocks(slot, context, counter) {
|
|
|
107
150
|
}
|
|
108
151
|
if (token.type === 'html') {
|
|
109
152
|
const tag = /^\s*<(\/?)(v-clicks?|br)\b[^>]*>\s*$/i.exec(token.raw)
|
|
110
|
-
if (tag && tag[2] === 'br')
|
|
153
|
+
if (tag && tag[2] === 'br') sink.push(h('br'))
|
|
111
154
|
else if (tag) mode = tag[1] ? '' : /** @type {'click' | 'clicks'} */ (tag[2].toLowerCase() === 'v-clicks' ? 'clicks' : 'click')
|
|
112
155
|
else if (/<li\b/i.test(token.raw)) {
|
|
113
156
|
// A list written as HTML, as brains write Slidev's steps (<li v-click>): its items' words, each
|
|
@@ -120,30 +163,79 @@ function blocks(slot, context, counter) {
|
|
|
120
163
|
if (/\bv-click\b/.test(attrs) || mode === 'clicks') step(li, ++counter.step)
|
|
121
164
|
list.append(li)
|
|
122
165
|
}
|
|
123
|
-
if (list.childElementCount)
|
|
166
|
+
if (list.childElementCount) sink.push(list)
|
|
124
167
|
} else {
|
|
125
168
|
const words = token.raw.replace(/<!--[\s\S]*?-->/g, '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
|
|
126
|
-
if (words)
|
|
169
|
+
if (words) sink.push(h('p', {}, [words]))
|
|
127
170
|
}
|
|
128
171
|
continue
|
|
129
172
|
}
|
|
130
173
|
const element = block(token, start, context, mode === 'clicks' ? counter : null)
|
|
131
174
|
if (!element) continue
|
|
132
175
|
if (mode === 'click' || (mode === 'clicks' && token.type !== 'list')) step(element, ++counter.step)
|
|
133
|
-
|
|
176
|
+
sink.push(element)
|
|
134
177
|
}
|
|
178
|
+
if (lists.length) mindmap(out, lists.flatMap((list) => list.items), context, counter, mapAt)
|
|
179
|
+
if (entries.length) out.splice(figureAt, 0, listFigure(kind, entries, context))
|
|
180
|
+
if (parts.length) out.splice(partsAt, 0, partsFigure(parted, parts))
|
|
135
181
|
return out
|
|
136
182
|
}
|
|
137
183
|
|
|
184
|
+
/**
|
|
185
|
+
* A slot's lists as one mind map, put among its other blocks where the first list was: merged, under the item they share, else under the heading the slot opens with, taken
|
|
186
|
+
* from the slot into the map; a heading other than that one stays the slide's. Each branch is a step when its list was
|
|
187
|
+
* one a click. Its words keep where they are in the source, so they change in place as a list's do; a chart or a
|
|
188
|
+
* diagram fenced under an item is drawn in its node, under its words.
|
|
189
|
+
* @param {Node[]} out the slot's other blocks @param {import('./mindmap.js').Item[]} items @param {Context} context
|
|
190
|
+
* @param {{ step: number }} counter @param {number} at where the map goes
|
|
191
|
+
*/
|
|
192
|
+
function mindmap(out, items, context, counter, at) {
|
|
193
|
+
const merged = mergeItems(items)
|
|
194
|
+
const opening = at > 0 && out[0] instanceof HTMLElement && out[0].matches('h1, h2, h3, h4, h5, h6, .deck-h') ? out[0] : null
|
|
195
|
+
/** @type {import('./mindmap.js').Item} */
|
|
196
|
+
let root
|
|
197
|
+
if (merged.length === 1 && (!opening || keyOf(opening.textContent ?? '') === keyOf(merged[0].text))) root = { ...merged[0], element: opening ?? merged[0].element }
|
|
198
|
+
else if (merged.length === 1) root = merged[0]
|
|
199
|
+
else root = { text: opening?.textContent ?? '', start: 0, end: 0, children: merged, element: opening ?? undefined }
|
|
200
|
+
if (opening && root.element === opening) { out.shift(); at-- }
|
|
201
|
+
const node = (/** @type {import('./mindmap.js').Item} */ item, /** @type {number} */ depth) => {
|
|
202
|
+
if (item.element) { item.element.classList.add('deck-map-node'); item.element.setAttribute('data-depth', String(depth)); return item.element }
|
|
203
|
+
const [first] = item.text ? context.lex(item.text) : []
|
|
204
|
+
const words = inline(first?.tokens ?? [{ type: 'text', text: item.text }], context)
|
|
205
|
+
if (!item.blocks?.length) return h('span', { class: 'deck-map-node', 'data-depth': depth, dir: 'auto', 'data-edit': range(item.start, item.end) }, words)
|
|
206
|
+
return h('div', { class: 'deck-map-node has-block', 'data-depth': depth }, [
|
|
207
|
+
item.text ? h('span', { class: 'deck-map-words', dir: 'auto', 'data-edit': range(item.start, item.end) }, words) : null,
|
|
208
|
+
...item.blocks.map((one) => block({ type: 'code', lang: one.lang, text: one.text, raw: one.text }, item.start, context, null)),
|
|
209
|
+
])
|
|
210
|
+
}
|
|
211
|
+
/** @type {(item: import('./mindmap.js').Item, depth: number) => HTMLElement} */
|
|
212
|
+
const tree = (item, depth) => h('li', {}, [node(item, depth), item.children.length ? h('ul', {}, item.children.map((child) => tree(child, depth + 1))) : null])
|
|
213
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
214
|
+
svg.setAttribute('class', 'deck-map-lines')
|
|
215
|
+
svg.setAttribute('aria-hidden', 'true')
|
|
216
|
+
svg.setAttribute('preserveAspectRatio', 'none')
|
|
217
|
+
const branches = root.children.map((child, n) => {
|
|
218
|
+
const li = tree(child, 1), group = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
|
219
|
+
for (const one of [li, group]) one.setAttribute('data-branch', String((n % 6) + 1))
|
|
220
|
+
if (child.step) { const k = ++counter.step; step(li, k); step(group, k) }
|
|
221
|
+
svg.append(group)
|
|
222
|
+
return li
|
|
223
|
+
})
|
|
224
|
+
const top = h('li', {}, [node(root, 0), branches.length ? h('ul', {}, branches) : null])
|
|
225
|
+
const map = h('div', { class: 'deck-mindmap' }, [h('div', { class: 'deck-map-plane' }, [svg, h('ul', { class: 'deck-map-tree' }, [top])])])
|
|
226
|
+
watchMap(map)
|
|
227
|
+
out.splice(at, 0, map)
|
|
228
|
+
}
|
|
229
|
+
|
|
138
230
|
/** @param {Element} element @param {number} n */
|
|
139
|
-
const step = (element, n) => { element.classList.add('deck-step'); element.setAttribute('data-step', String(n)) }
|
|
231
|
+
export const step = (element, n) => { element.classList.add('deck-step'); element.setAttribute('data-step', String(n)) }
|
|
140
232
|
|
|
141
233
|
/**
|
|
142
234
|
* @param {any} token @param {number} start where its raw starts in the source
|
|
143
235
|
* @param {Context} context @param {{ step: number } | null} stepped list items each a step
|
|
144
236
|
* @returns {HTMLElement | null}
|
|
145
237
|
*/
|
|
146
|
-
function block(token, start, context, stepped) {
|
|
238
|
+
export function block(token, start, context, stepped) {
|
|
147
239
|
switch (token.type) {
|
|
148
240
|
case 'heading': {
|
|
149
241
|
const head = /^\s*#{1,6}[ \t]+/.exec(token.raw)
|
|
@@ -185,7 +277,7 @@ function block(token, start, context, stepped) {
|
|
|
185
277
|
if (drawer) {
|
|
186
278
|
const box = h('div', { class: `deck-block deck-${lang}`, 'data-source': token.text })
|
|
187
279
|
box.append(h('pre', { class: 'deck-block-source', dir: 'ltr' }, [h('code', {}, [token.text])]))
|
|
188
|
-
queueMicrotask(() => Promise.resolve(drawer(box, token.text)).catch(() => box.classList.add('is-failed')))
|
|
280
|
+
queueMicrotask(() => Promise.resolve(drawer(box, token.text, optionsOf(token.lang ?? ''))).catch(() => box.classList.add('is-failed')))
|
|
189
281
|
return box
|
|
190
282
|
}
|
|
191
283
|
const marked = lines(info[2] ?? '')
|
|
@@ -214,6 +306,17 @@ function children(tokens, start, raw, context) {
|
|
|
214
306
|
})
|
|
215
307
|
}
|
|
216
308
|
|
|
309
|
+
/**
|
|
310
|
+
* The options after a fence's language, as Slidev writes them (`mermaid {theme: 'neutral', scale: 0.8}`), read as YAML's
|
|
311
|
+
* flow, which JSON is too; none if they don't read. @param {string} info @returns {Record<string, any>}
|
|
312
|
+
*/
|
|
313
|
+
export function optionsOf(info) {
|
|
314
|
+
const braces = /\{[\s\S]*\}/.exec(info)?.[0]
|
|
315
|
+
if (!braces) return {}
|
|
316
|
+
const read = parseYaml(`options: ${braces.replace(/\n/g, ' ')}`).options
|
|
317
|
+
return read && typeof read === 'object' && !Array.isArray(read) ? read : {}
|
|
318
|
+
}
|
|
319
|
+
|
|
217
320
|
/** Line numbers as Slidev marks them, `{2,4-5}`; `|` steps are read as all their lines at once. @param {string} text */
|
|
218
321
|
function lines(text) {
|
|
219
322
|
const marked = new Set()
|
|
@@ -225,7 +328,7 @@ function lines(text) {
|
|
|
225
328
|
}
|
|
226
329
|
|
|
227
330
|
/** @param {number} from @param {number} to */
|
|
228
|
-
const range = (from, to) => (to > from ? `${from}-${to}` : undefined)
|
|
331
|
+
export const range = (from, to) => (to > from ? `${from}-${to}` : undefined)
|
|
229
332
|
|
|
230
333
|
/** @param {any[]} tokens @param {Context} context @returns {Node[]} */
|
|
231
334
|
export function inline(tokens = [], context) {
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// deck's version, which a linked file names in its renderer's address. Kept equal to package.json's by a test.
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.4.0'
|