@codename/deck 0.2.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 +95 -89
- package/dist/deck.bundle.mjs.map +4 -4
- package/dist/deck.css +118 -0
- package/dist/deck.js +215 -91
- package/dist/deck.js.map +4 -4
- package/dist/deck.mjs +43 -37
- package/dist/deck.mjs.map +4 -4
- package/dist/template.mjs +3 -1
- package/dist/template.mjs.map +4 -4
- package/dist/types/blocks.d.ts +6 -4
- package/dist/types/figures.d.ts +68 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/kit.d.ts +27 -4
- package/dist/types/look.d.ts +6 -2
- 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/template.d.ts +35 -0
- package/dist/types/version.d.ts +1 -1
- package/docs/format.md +85 -4
- package/docs/host.md +4 -4
- package/docs/kits.md +21 -4
- package/package.json +1 -1
- package/src/blocks.js +26 -9
- package/src/deck.css +118 -0
- package/src/deck.js +13 -3
- package/src/figures.js +255 -0
- package/src/index.js +1 -1
- package/src/kit.js +59 -15
- package/src/look.js +22 -16
- package/src/mindmap.js +208 -0
- package/src/pptx.js +157 -26
- package/src/render.js +126 -20
- package/src/template.js +451 -26
- package/src/version.js +1 -1
package/src/pptx.js
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
// slide layout of its own (a master, in PptxGenJS's words) with a placeholder for its title and
|
|
3
3
|
// each of its slots, so a slide added there looks like the deck's; words as PowerPoint's own text,
|
|
4
4
|
// a list's levels as its levels; a table as a table; a chart of bars, lines, areas, pies, rings,
|
|
5
|
-
// points or a radar as PowerPoint's own chart, its numbers in the file to change;
|
|
6
|
-
//
|
|
5
|
+
// points or a radar as PowerPoint's own chart, its numbers in the file to change; a mind map as
|
|
6
|
+
// PowerPoint's own shapes, its words in boxes joined by curves; a figure (cards, key figures, a
|
|
7
|
+
// timeline, chevrons, a programme, a pyramid, a bento, parts face to face or in a matrix, the
|
|
8
|
+
// deck's contents) as PowerPoint's own shapes and boxes of words, where the slide has them; anything else (a
|
|
9
|
+
// diagram, another chart, a picture) as a picture, a layout's art too, on its slide layout; the speaker's notes as notes; each slide's
|
|
7
10
|
// transition as PowerPoint's (Morph where it has one, a fade where it hasn't); the look's colours
|
|
8
11
|
// as the file's theme. Read from the deck as drawn, each box where the browser put it, so the file
|
|
9
12
|
// looks like the slides, in the scheme shown. PptxGenJS (with its JSZip) is the host's to load.
|
|
@@ -14,19 +17,23 @@ const W = 13.333, H = 7.5
|
|
|
14
17
|
// Words: a paragraph that is a picture alone on its line is the picture.
|
|
15
18
|
const TEXT = 'h1, h2, h3, h4, h5, h6, .deck-h, p:not(.deck-picture), ul, ol, blockquote'
|
|
16
19
|
|
|
17
|
-
/** @param {string} value rgb(…) or #… @returns {string} six hex digits */
|
|
20
|
+
/** @param {string} value rgb(…), color(srgb …) or #… @returns {string} six hex digits */
|
|
18
21
|
function hexOf(value) {
|
|
19
22
|
const m = /rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/.exec(value)
|
|
20
23
|
if (m) return [m[1], m[2], m[3]].map((n) => Math.round(Number(n)).toString(16).padStart(2, '0')).join('').toUpperCase()
|
|
24
|
+
const srgb = /color\(srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)/.exec(value)
|
|
25
|
+
if (srgb) return [srgb[1], srgb[2], srgb[3]].map((n) => Math.round(Number(n) * 255).toString(16).padStart(2, '0')).join('').toUpperCase()
|
|
21
26
|
return value.replace('#', '').toUpperCase().slice(0, 6) || '000000'
|
|
22
27
|
}
|
|
23
28
|
/** @param {string} value */
|
|
24
29
|
const opaque = (value) => !/rgba\([^)]*,\s*0\)|transparent/.test(value)
|
|
30
|
+
/** A colour's alpha, from rgba(…, a) or color(srgb … / a), else 1. @param {string} value */
|
|
31
|
+
const alphaOf = (value) => { const m = /rgba\((?:[^,]*,){3}\s*([\d.]+)\s*\)|\/\s*([\d.]+)\s*\)/.exec(value); return value === 'transparent' ? 0 : Number(m?.[1] ?? m?.[2] ?? 1) }
|
|
25
32
|
|
|
26
33
|
/**
|
|
27
34
|
* @param {import('./deck.js').DeckView} view
|
|
28
35
|
* @param {{ PptxGenJS: any, JSZip: any }} libraries
|
|
29
|
-
* @returns {Promise<{ blob: Blob, report: { slides: number, charts: number, pictures: number, tables: number, placeholders: number } }>}
|
|
36
|
+
* @returns {Promise<{ blob: Blob, report: { slides: number, charts: number, pictures: number, tables: number, placeholders: number, maps: number, figures: number } }>}
|
|
30
37
|
*/
|
|
31
38
|
export async function toPptx(view, { PptxGenJS, JSZip }) {
|
|
32
39
|
const pres = new PptxGenJS()
|
|
@@ -36,7 +43,7 @@ export async function toPptx(view, { PptxGenJS, JSZip }) {
|
|
|
36
43
|
const rootStyle = getComputedStyle(view.root)
|
|
37
44
|
const firstFont = (/** @type {string} */ stack) => stack.split(',').map((f) => f.trim().replace(/^["']|["']$/g, '')).find((f) => !/^(ui-|system-ui|-apple|sans-serif|serif|monospace)/.test(f)) ?? 'Arial'
|
|
38
45
|
pres.theme = { headFontFace: firstFont(rootStyle.getPropertyValue('--deck-heading')), bodyFontFace: firstFont(rootStyle.getPropertyValue('--deck-body')) }
|
|
39
|
-
const report = { slides: 0, charts: 0, pictures: 0, tables: 0, placeholders: 0 }
|
|
46
|
+
const report = { slides: 0, charts: 0, pictures: 0, tables: 0, placeholders: 0, maps: 0, figures: 0 }
|
|
40
47
|
const rtl = view.dir === 'rtl'
|
|
41
48
|
const slides = view.slides()
|
|
42
49
|
const masters = new Map()
|
|
@@ -57,6 +64,21 @@ export async function toPptx(view, { PptxGenJS, JSZip }) {
|
|
|
57
64
|
const start = parseFloat(pad.paddingInlineStart) * sx, end = parseFloat(pad.paddingInlineEnd) * sx
|
|
58
65
|
return { x: x + (rtl ? end : start), y, w: w - start - end, h: h * 1.08 }
|
|
59
66
|
}
|
|
67
|
+
// A chart as PowerPoint's own, else as a picture; a diagram or a picture as a picture.
|
|
68
|
+
const figure = async (/** @type {Element} */ block, /** @type {{ x: number, y: number, w: number, h: number }} */ at) => {
|
|
69
|
+
if (block.matches('.deck-echarts') && /** @type {any} */ (block).chart) {
|
|
70
|
+
const chart = /** @type {any} */ (block).chart
|
|
71
|
+
if (nativeChart(pres, slide, chart, at, { pt, rtl, lang: view.lang, ink: hexOf(style.color) })) report.charts++
|
|
72
|
+
else { slide.addImage({ data: await pictureOf(`data:image/svg+xml;charset=utf-8,${encodeURIComponent(chart.renderToSVGString())}`, at.w / sx * 2, at.h / sx * 2), ...at }); report.pictures++ }
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
const img = block.querySelector('img')
|
|
76
|
+
if (!img) return
|
|
77
|
+
// A slide's pictures are read as they come near the screen: one not read yet is read now, so that it has its size.
|
|
78
|
+
if (!img.complete) { img.loading = 'eager'; await img.decode().catch(() => {}) }
|
|
79
|
+
const data = await pictureOf(img.src, img.clientWidth * 2, img.clientHeight * 2)
|
|
80
|
+
if (data) { slide.addImage({ data, ...place(img), altText: img.alt }); report.pictures++ }
|
|
81
|
+
}
|
|
60
82
|
// Where a layout centres its words, PowerPoint's box centres them too.
|
|
61
83
|
const anchor = (/** @type {Element} */ slot) => (getComputedStyle(slot).justifyContent === 'center' ? 'middle' : 'top')
|
|
62
84
|
const pt = (/** @type {number} */ px) => Math.round(px * sx * 72 * 10) / 10
|
|
@@ -72,6 +94,9 @@ export async function toPptx(view, { PptxGenJS, JSZip }) {
|
|
|
72
94
|
const masterName = `${definition ? labelOf(definition, layout, view.lang) : layout}${element.classList.contains('deck-class-invert') ? ' (dark)' : ''}`
|
|
73
95
|
if (!masters.has(masterName)) {
|
|
74
96
|
const objects = []
|
|
97
|
+
// The layout's art, its layers as one picture on its paper behind the rest: a slide added in PowerPoint has it too.
|
|
98
|
+
const art = [...element.querySelectorAll('img.deck-art')].map((one) => /** @type {HTMLImageElement} */ (one).src), backdrop = art.length && await pictureOf(art, W * 150, H * 150, false, style.backgroundColor)
|
|
99
|
+
if (backdrop) { objects.push({ image: { data: backdrop, x: 0, y: 0, w: W, h: H } }); report.pictures++ }
|
|
75
100
|
if (title) objects.push({ placeholder: { options: { name: 'title', type: 'title', ...across(title), valign: 'bottom', margin: 0, fontFace: pres.theme.headFontFace, color: hexOf(getComputedStyle(title).color), align: rtl ? 'right' : 'left' }, text: '' } })
|
|
76
101
|
for (const slot of slots) {
|
|
77
102
|
const r = place(slot)
|
|
@@ -98,6 +123,9 @@ export async function toPptx(view, { PptxGenJS, JSZip }) {
|
|
|
98
123
|
const url = /url\("?(.*?)"?\)/.exec(getComputedStyle(background).backgroundImage)?.[1]
|
|
99
124
|
const data = url && await pictureOf(url, background.clientWidth * 2, background.clientHeight * 2, true)
|
|
100
125
|
if (data) { slide.addImage({ data, ...place(background) }); report.pictures++ }
|
|
126
|
+
// The veil over it, as a quote has: the words read on it.
|
|
127
|
+
const veil = getComputedStyle(background, '::after')
|
|
128
|
+
if (veil.content !== 'none' && alphaOf(veil.backgroundColor) > 0) slide.addShape(pres.shapes.RECTANGLE, { ...place(background), fill: { color: hexOf(veil.backgroundColor), transparency: Math.round((1 - alphaOf(veil.backgroundColor)) * 100) }, line: { type: 'none' } })
|
|
101
129
|
}
|
|
102
130
|
if (title && names.has('title')) {
|
|
103
131
|
slide.addText(runs(title, { pt, rtl, lang: view.lang }), { placeholder: 'title' })
|
|
@@ -119,19 +147,13 @@ export async function toPptx(view, { PptxGenJS, JSZip }) {
|
|
|
119
147
|
} else if (block.matches('.deck-table')) {
|
|
120
148
|
table(slide, block, at, { pt, rtl, lang: view.lang })
|
|
121
149
|
report.tables++
|
|
122
|
-
} else if (block.matches('.deck-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// A slide's pictures are read as they come near the screen: one not read yet is read now, so that it has its size.
|
|
130
|
-
if (!img.complete) { img.loading = 'eager'; await img.decode().catch(() => {}) }
|
|
131
|
-
const data = await pictureOf(img.src, img.clientWidth * 2, img.clientHeight * 2)
|
|
132
|
-
if (data) { slide.addImage({ data, ...place(img), altText: img.alt }); report.pictures++ }
|
|
133
|
-
}
|
|
134
|
-
}
|
|
150
|
+
} else if (block.matches('.deck-mindmap')) {
|
|
151
|
+
await mindmap(pres, slide, /** @type {HTMLElement} */ (block), place, { pt, rtl, lang: view.lang }, figure)
|
|
152
|
+
report.maps++
|
|
153
|
+
} else if (block.matches('.deck-figure')) {
|
|
154
|
+
await drawing(pres, slide, block, place, { pt, rtl, lang: view.lang }, figure)
|
|
155
|
+
report.figures++
|
|
156
|
+
} else await figure(block, at)
|
|
135
157
|
}
|
|
136
158
|
}
|
|
137
159
|
const notes = view.deck.slides[i]?.notes
|
|
@@ -159,6 +181,102 @@ export async function toPptx(view, { PptxGenJS, JSZip }) {
|
|
|
159
181
|
return { blob, report }
|
|
160
182
|
}
|
|
161
183
|
|
|
184
|
+
/**
|
|
185
|
+
* A mind map as PowerPoint's own shapes, where the slide has them: each line a curve in its branch's colour, each of its
|
|
186
|
+
* words a box, filled and outlined as on the slide (the root, the first branches), its words as runs, a chart or a
|
|
187
|
+
* diagram in it as the slide's are.
|
|
188
|
+
* @param {any} pres @param {any} slide @param {HTMLElement} map @param {(el: Element) => { x: number, y: number, w: number, h: number }} place
|
|
189
|
+
* @param {{ pt: (px: number) => number, rtl: boolean, lang: string }} how
|
|
190
|
+
* @param {(block: Element, at: { x: number, y: number, w: number, h: number }) => Promise<void>} figure
|
|
191
|
+
*/
|
|
192
|
+
async function mindmap(pres, slide, map, place, how, figure) {
|
|
193
|
+
const plane = /** @type {HTMLElement} */ (map.querySelector('.deck-map-plane')), svg = /** @type {SVGSVGElement} */ (map.querySelector('.deck-map-lines'))
|
|
194
|
+
// The plane is shrunk to its box: its type and lines are so much smaller on the slide.
|
|
195
|
+
const scale = plane.getBoundingClientRect().width / (plane.offsetWidth || 1)
|
|
196
|
+
const area = place(svg), units = Number(svg.getAttribute('viewBox')?.split(' ')[2]) || 1, inch = area.w / units
|
|
197
|
+
for (const path of svg.querySelectorAll('path')) {
|
|
198
|
+
const [x1, y1, , , , , x2, y2] = (path.getAttribute('d')?.match(/-?[\d.]+/g) ?? []).map((n, i) => (i % 2 ? area.y : area.x) + Number(n) * inch)
|
|
199
|
+
const x = Math.min(x1, x2), y = Math.min(y1, y2), middle = (x1 + x2) / 2 - x
|
|
200
|
+
const stroke = getComputedStyle(path)
|
|
201
|
+
slide.addShape(pres.shapes.CUSTOM_GEOMETRY, {
|
|
202
|
+
x, y, w: Math.max(0.01, Math.abs(x2 - x1)), h: Math.max(0.01, Math.abs(y2 - y1)), line: { color: hexOf(stroke.stroke), width: Math.round(parseFloat(stroke.strokeWidth) * inch * 72 * 10) / 10 },
|
|
203
|
+
points: [{ x: x1 - x, y: y1 - y }, { x: x2 - x, y: y2 - y, curve: { type: 'cubic', x1: middle, y1: y1 - y, x2: middle, y2: y2 - y } }],
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
const small = { ...how, pt: (/** @type {number} */ px) => how.pt(px * scale) }
|
|
207
|
+
for (const node of /** @type {NodeListOf<HTMLElement>} */ (map.querySelectorAll('.deck-map-node'))) {
|
|
208
|
+
const style = getComputedStyle(node), at = place(node)
|
|
209
|
+
const align = style.textAlign === 'left' || style.textAlign === 'right' ? style.textAlign : style.textAlign === 'center' ? 'center' : how.rtl ? 'right' : 'left'
|
|
210
|
+
// A node holding a chart: its words alone, over its box, the chart where it is.
|
|
211
|
+
const label = node.matches('.has-block') ? node.querySelector('.deck-map-words') : node
|
|
212
|
+
const words = label ? runs(label, small).map((run) => ({ ...run, options: { ...run.options, align } })) : []
|
|
213
|
+
const border = parseFloat(style.borderTopWidth) || 0, pad = small.pt(parseFloat(style.paddingInlineStart) || 0) / 72
|
|
214
|
+
const fill = alphaOf(style.backgroundColor) > 0 ? { color: hexOf(style.backgroundColor), transparency: Math.round((1 - alphaOf(style.backgroundColor)) * 100) } : undefined
|
|
215
|
+
// PowerPoint sets type a little wider than a browser: words with no box grow away from their line.
|
|
216
|
+
const wide = fill || border ? at.w * 0.04 : at.w * 0.08
|
|
217
|
+
const x = align === 'left' ? at.x : align === 'right' ? at.x - wide : at.x - wide / 2
|
|
218
|
+
slide.addText(label === node && words.length ? words : '', {
|
|
219
|
+
x, y: at.y, w: at.w + wide, h: at.h, margin: [pad * 72, pad * 72, 0, 0], valign: 'middle', align, wrap: true,
|
|
220
|
+
...(fill || border ? { shape: pres.shapes.ROUNDED_RECTANGLE, rectRadius: Math.min(at.h / 2, small.pt(parseFloat(style.borderTopLeftRadius) || 0) / 72), fill } : {}),
|
|
221
|
+
...(border ? { line: { color: hexOf(style.borderTopColor), width: Math.round(small.pt(border) * 10) / 10 } } : {}),
|
|
222
|
+
})
|
|
223
|
+
if (label === node) continue
|
|
224
|
+
if (label) slide.addText(words, { ...place(label), margin: 0, valign: 'middle', align: 'center', wrap: true })
|
|
225
|
+
for (const block of node.querySelectorAll('.deck-block')) await figure(block, place(block))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A figure as PowerPoint's own shapes, as the slide draws them: each shape (a card, a chevron, a level, a dot, a rule) its
|
|
231
|
+
* box, or its polygon, filled and outlined as on the slide, a side's rule a line; then each of its words a box of words
|
|
232
|
+
* where they are, a table as a table, a chart or a diagram as the slide's are.
|
|
233
|
+
* @param {any} pres @param {any} slide @param {Element} root @param {(el: Element) => { x: number, y: number, w: number, h: number }} place
|
|
234
|
+
* @param {{ pt: (px: number) => number, rtl: boolean, lang: string }} how
|
|
235
|
+
* @param {(block: Element, at: { x: number, y: number, w: number, h: number }) => Promise<void>} figure
|
|
236
|
+
*/
|
|
237
|
+
async function drawing(pres, slide, root, place, how, figure) {
|
|
238
|
+
const inches = (/** @type {number} */ px) => how.pt(px) / 72
|
|
239
|
+
// In the order the slide stacks them: a medallion over the parts it sits between.
|
|
240
|
+
const layer = (/** @type {Element} */ el) => parseInt(getComputedStyle(el).zIndex, 10) || 0
|
|
241
|
+
for (const shape of [...root.querySelectorAll('.deck-shape')].sort((a, b) => layer(a) - layer(b))) {
|
|
242
|
+
const style = getComputedStyle(shape), at = place(shape), box = shape.getBoundingClientRect()
|
|
243
|
+
const fill = alphaOf(style.backgroundColor) > 0 ? { color: hexOf(style.backgroundColor), transparency: Math.round((1 - alphaOf(style.backgroundColor)) * 100) } : undefined
|
|
244
|
+
const sides = /** @type {const} */ (['Top', 'Right', 'Bottom', 'Left']).map((side) => ({ side, width: parseFloat(style[`border${side}Width`]) || 0, colour: style[`border${side}Color`] })).map((one) => ({ ...one, width: alphaOf(one.colour) > 0 ? one.width : 0 }))
|
|
245
|
+
const even = sides.every((one) => Math.abs(one.width - sides[0].width) < 0.1)
|
|
246
|
+
const line = even && sides[0].width > 0 ? { color: hexOf(sides[0].colour), width: Math.round(how.pt(sides[0].width) * 10) / 10 } : { type: 'none' }
|
|
247
|
+
const polygon = /^polygon\((.*)\)$/.exec(style.clipPath)?.[1]
|
|
248
|
+
if (polygon && (fill || even && sides[0].width)) {
|
|
249
|
+
// Its points as the slide clips it: lengths and shares of its box, calc() as CSS writes it.
|
|
250
|
+
const length = (/** @type {string} */ text, /** @type {number} */ size) => [...text.replace(/^calc\(|\)$/g, '').matchAll(/([+-]?)\s*([\d.]+)(%|px)?/g)].reduce((sum, [, sign, n, unit]) => sum + (sign === '-' ? -1 : 1) * (unit === '%' ? (Number(n) / 100) * size : inches(Number(n))), 0)
|
|
251
|
+
const points = polygon.split(/,(?![^(]*\))/).map((pair) => pair.trim().match(/calc\([^)]*\)|[-\d.]+(?:%|px)?/g) ?? []).map(([x = '0', y = '0']) => ({ x: length(x, at.w), y: length(y, at.h) }))
|
|
252
|
+
slide.addShape(pres.shapes.CUSTOM_GEOMETRY, { ...at, fill, line, points: [...points, { close: true }] })
|
|
253
|
+
} else if (fill || line.width) {
|
|
254
|
+
const radius = style.borderTopLeftRadius.endsWith('%') ? (Math.min(box.width, box.height) * parseFloat(style.borderTopLeftRadius)) / 100 : parseFloat(style.borderTopLeftRadius) || 0
|
|
255
|
+
const round = radius >= Math.min(box.width, box.height) / 2 - 0.5
|
|
256
|
+
const kind = round && Math.abs(box.width - box.height) < 1 ? pres.shapes.OVAL : radius > 0 ? pres.shapes.ROUNDED_RECTANGLE : pres.shapes.RECTANGLE
|
|
257
|
+
slide.addShape(kind, { ...at, fill, line, ...(kind === pres.shapes.ROUNDED_RECTANGLE ? { rectRadius: Math.min(at.h / 2, at.w / 2, inches(radius)) } : {}) })
|
|
258
|
+
}
|
|
259
|
+
// A rule on one side alone, as a line.
|
|
260
|
+
if (!even) for (const { side, width, colour } of sides) {
|
|
261
|
+
if (!width) continue
|
|
262
|
+
const thick = inches(width)
|
|
263
|
+
const edge = { Top: { x: at.x, y: at.y + thick / 2, w: at.w, h: 0 }, Bottom: { x: at.x, y: at.y + at.h - thick / 2, w: at.w, h: 0 }, Left: { x: at.x + thick / 2, y: at.y, w: 0, h: at.h }, Right: { x: at.x + at.w - thick / 2, y: at.y, w: 0, h: at.h } }[side]
|
|
264
|
+
slide.addShape(pres.shapes.LINE, { ...edge, line: { color: hexOf(colour), width: Math.round(how.pt(width) * 10) / 10 } })
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const words of root.querySelectorAll('.deck-words')) {
|
|
268
|
+
if (words.parentElement?.closest('.deck-words') || !words.textContent?.trim()) continue
|
|
269
|
+
const style = getComputedStyle(words), at = place(words)
|
|
270
|
+
const align = style.textAlign === 'center' ? 'center' : style.textAlign === 'left' || style.textAlign === 'right' ? style.textAlign : (style.textAlign === 'end') !== how.rtl ? 'right' : 'left'
|
|
271
|
+
// PowerPoint sets type a little wider than a browser: words grow away from where they are aligned.
|
|
272
|
+
const wide = at.w * 0.08, x = align === 'left' ? at.x : align === 'right' ? at.x - wide : at.x - wide / 2
|
|
273
|
+
// Words in a shape of their own (a number in its circle) in its middle; others from their top, as their lines may be fewer.
|
|
274
|
+
slide.addText(runs(words, how).map((run) => ({ ...run, options: { ...run.options, align } })), { x, y: at.y, w: at.w + wide, h: at.h, margin: 0, valign: words.matches('.deck-shape') ? 'middle' : 'top', align, wrap: true })
|
|
275
|
+
}
|
|
276
|
+
for (const one of root.querySelectorAll('.deck-table')) table(slide, one, place(one), how)
|
|
277
|
+
for (const block of root.querySelectorAll('.deck-block')) await figure(block, place(block))
|
|
278
|
+
}
|
|
279
|
+
|
|
162
280
|
/** A slot's paragraphs' spacing, from the space under its first block. @param {HTMLElement} slot */
|
|
163
281
|
function spaced(slot) {
|
|
164
282
|
const first = slot.firstElementChild
|
|
@@ -296,21 +414,34 @@ function transitionXml(kind, rtl) {
|
|
|
296
414
|
return '<p:transition spd="med"><p:fade/></p:transition>'
|
|
297
415
|
}
|
|
298
416
|
|
|
299
|
-
/**
|
|
300
|
-
|
|
417
|
+
/**
|
|
418
|
+
* A picture's data as PNG, drawn at a size, or a layout's art, its layers, as JPEG on its paper; null when it can't be read
|
|
419
|
+
* (another site's, unshared). @param {string | string[]} src @param {number} w @param {number} h @param {boolean} [cover] @param {string} [paper]
|
|
420
|
+
*/
|
|
421
|
+
async function pictureOf(src, w, h, cover = false, paper = '') {
|
|
301
422
|
try {
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
423
|
+
const images = await Promise.all([src].flat().map(async (one) => {
|
|
424
|
+
const img = new Image()
|
|
425
|
+
img.crossOrigin = 'anonymous'
|
|
426
|
+
img.src = one
|
|
427
|
+
await img.decode()
|
|
428
|
+
return img
|
|
429
|
+
}))
|
|
430
|
+
const img = images[0]
|
|
306
431
|
const canvas = document.createElement('canvas')
|
|
307
432
|
canvas.width = Math.max(1, Math.round(w))
|
|
308
433
|
canvas.height = Math.max(1, Math.round(h))
|
|
309
434
|
const ctx = /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'))
|
|
310
435
|
const iw = img.naturalWidth || w, ih = img.naturalHeight || h
|
|
311
436
|
const scale = cover ? Math.max(canvas.width / iw, canvas.height / ih) : Math.min(canvas.width / iw, canvas.height / ih)
|
|
312
|
-
ctx.drawImage(img, (canvas.width - iw * scale) / 2, (canvas.height - ih * scale) / 2, iw * scale, ih * scale)
|
|
313
|
-
|
|
437
|
+
if (!paper) ctx.drawImage(img, (canvas.width - iw * scale) / 2, (canvas.height - ih * scale) / 2, iw * scale, ih * scale)
|
|
438
|
+
else {
|
|
439
|
+
// Art, stretched over the slide as it is drawn there, on its paper, as a JPEG: photographs stay small.
|
|
440
|
+
ctx.fillStyle = paper
|
|
441
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
442
|
+
for (const layer of images) ctx.drawImage(layer, 0, 0, canvas.width, canvas.height)
|
|
443
|
+
}
|
|
444
|
+
return canvas.toDataURL(paper ? 'image/jpeg' : 'image/png', 0.9)
|
|
314
445
|
} catch {
|
|
315
446
|
return null
|
|
316
447
|
}
|
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,18 +42,20 @@ 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 }
|
|
44
49
|
const slots = new Map(slide.slots.map((slot) => [slot.name, slot]))
|
|
45
50
|
const names = definition.slots
|
|
51
|
+
const art = (definition.art ?? []).map((name) => context.kit.pictures?.[name]).filter(Boolean)
|
|
46
52
|
const section = h('section', {
|
|
47
|
-
class: ['deck-slide', `deck-layout-${layout}`, ...classes.map((name) => `deck-class-${name}`)].join(' '),
|
|
53
|
+
class: ['deck-slide', `deck-layout-${layout}`, ...classes.map((name) => `deck-class-${name}`), ...(art.length ? ['has-art'] : [])].join(' '),
|
|
48
54
|
'data-index': slide.index, 'data-layout': layout, 'data-paper': definition.paper, 'data-align': definition.align, 'data-justify': definition.justify, 'data-title': definition.title,
|
|
49
55
|
'aria-roledescription': context.slide, role: 'group',
|
|
50
56
|
})
|
|
57
|
+
// Its art, as its template draws it, layer over layer behind the rest.
|
|
58
|
+
for (const src of art) section.append(h('img', { class: 'deck-art', src, alt: '', 'aria-hidden': 'true' }))
|
|
51
59
|
// Its grid, from the layout's definition, through the CSSOM: areas named s-<slot>, as `default` is no name CSS allows.
|
|
52
60
|
const canvas = h('div', { class: 'deck-canvas' })
|
|
53
61
|
canvas.style.setProperty('grid-template-areas', definition.areas.map((row) => `"${row.map((cell) => (cell === '.' ? '.' : `s-${cell}`)).join(' ')}"`).join(' '))
|
|
@@ -55,7 +63,7 @@ export function renderSlide(slide, context) {
|
|
|
55
63
|
canvas.style.setProperty('grid-template-rows', definition.rows.join(' '))
|
|
56
64
|
section.append(canvas)
|
|
57
65
|
const image = slide.settings.image ?? slide.settings.background
|
|
58
|
-
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)) {
|
|
59
67
|
const url = context.resolve(image)
|
|
60
68
|
if (url) {
|
|
61
69
|
const picture = h('div', { class: 'deck-image', role: 'img', 'aria-label': String(slide.settings.alt ?? '') || undefined })
|
|
@@ -66,10 +74,13 @@ export function renderSlide(slide, context) {
|
|
|
66
74
|
}
|
|
67
75
|
for (const name of names) {
|
|
68
76
|
const slot = slots.get(name)
|
|
69
|
-
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) : [])
|
|
70
78
|
element.style.setProperty('grid-area', `s-${name}`)
|
|
71
79
|
canvas.append(element)
|
|
72
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)
|
|
73
84
|
// Slots a layout doesn't have still show, under the rest, rather than being lost.
|
|
74
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)))
|
|
75
86
|
if (definition.numbered && slide.settings.paginate !== false) section.append(h('span', { class: 'deck-number', 'aria-hidden': 'true' }, [context.number(slide.index + 1)]))
|
|
@@ -84,15 +95,50 @@ export function renderSlide(slide, context) {
|
|
|
84
95
|
return { element: section, steps: counter.step }
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
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') {
|
|
89
103
|
const tokens = context.lex(slot.text)
|
|
90
104
|
/** @type {Node[]} */
|
|
91
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)
|
|
92
120
|
let at = slot.start, mode = /** @type {'' | 'click' | 'clicks'} */ ('')
|
|
93
|
-
for (const token of tokens) {
|
|
121
|
+
for (const [i, token] of tokens.entries()) {
|
|
94
122
|
const start = at
|
|
95
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
|
+
}
|
|
96
142
|
// Pandoc's fences: `::: incremental` shows its list a point a click, as <v-clicks> does; the others, nothing.
|
|
97
143
|
const fences = token.type === 'paragraph' ? token.raw.trim().split('\n').map((/** @type {string} */ line) => line.trim()) : []
|
|
98
144
|
if (fences.length && fences.every((/** @type {string} */ line) => /^:{3,}/.test(line))) {
|
|
@@ -104,7 +150,7 @@ function blocks(slot, context, counter) {
|
|
|
104
150
|
}
|
|
105
151
|
if (token.type === 'html') {
|
|
106
152
|
const tag = /^\s*<(\/?)(v-clicks?|br)\b[^>]*>\s*$/i.exec(token.raw)
|
|
107
|
-
if (tag && tag[2] === 'br')
|
|
153
|
+
if (tag && tag[2] === 'br') sink.push(h('br'))
|
|
108
154
|
else if (tag) mode = tag[1] ? '' : /** @type {'click' | 'clicks'} */ (tag[2].toLowerCase() === 'v-clicks' ? 'clicks' : 'click')
|
|
109
155
|
else if (/<li\b/i.test(token.raw)) {
|
|
110
156
|
// A list written as HTML, as brains write Slidev's steps (<li v-click>): its items' words, each
|
|
@@ -117,30 +163,79 @@ function blocks(slot, context, counter) {
|
|
|
117
163
|
if (/\bv-click\b/.test(attrs) || mode === 'clicks') step(li, ++counter.step)
|
|
118
164
|
list.append(li)
|
|
119
165
|
}
|
|
120
|
-
if (list.childElementCount)
|
|
166
|
+
if (list.childElementCount) sink.push(list)
|
|
121
167
|
} else {
|
|
122
168
|
const words = token.raw.replace(/<!--[\s\S]*?-->/g, '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
|
|
123
|
-
if (words)
|
|
169
|
+
if (words) sink.push(h('p', {}, [words]))
|
|
124
170
|
}
|
|
125
171
|
continue
|
|
126
172
|
}
|
|
127
173
|
const element = block(token, start, context, mode === 'clicks' ? counter : null)
|
|
128
174
|
if (!element) continue
|
|
129
175
|
if (mode === 'click' || (mode === 'clicks' && token.type !== 'list')) step(element, ++counter.step)
|
|
130
|
-
|
|
176
|
+
sink.push(element)
|
|
131
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))
|
|
132
181
|
return out
|
|
133
182
|
}
|
|
134
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
|
+
|
|
135
230
|
/** @param {Element} element @param {number} n */
|
|
136
|
-
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)) }
|
|
137
232
|
|
|
138
233
|
/**
|
|
139
234
|
* @param {any} token @param {number} start where its raw starts in the source
|
|
140
235
|
* @param {Context} context @param {{ step: number } | null} stepped list items each a step
|
|
141
236
|
* @returns {HTMLElement | null}
|
|
142
237
|
*/
|
|
143
|
-
function block(token, start, context, stepped) {
|
|
238
|
+
export function block(token, start, context, stepped) {
|
|
144
239
|
switch (token.type) {
|
|
145
240
|
case 'heading': {
|
|
146
241
|
const head = /^\s*#{1,6}[ \t]+/.exec(token.raw)
|
|
@@ -182,7 +277,7 @@ function block(token, start, context, stepped) {
|
|
|
182
277
|
if (drawer) {
|
|
183
278
|
const box = h('div', { class: `deck-block deck-${lang}`, 'data-source': token.text })
|
|
184
279
|
box.append(h('pre', { class: 'deck-block-source', dir: 'ltr' }, [h('code', {}, [token.text])]))
|
|
185
|
-
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')))
|
|
186
281
|
return box
|
|
187
282
|
}
|
|
188
283
|
const marked = lines(info[2] ?? '')
|
|
@@ -211,6 +306,17 @@ function children(tokens, start, raw, context) {
|
|
|
211
306
|
})
|
|
212
307
|
}
|
|
213
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
|
+
|
|
214
320
|
/** Line numbers as Slidev marks them, `{2,4-5}`; `|` steps are read as all their lines at once. @param {string} text */
|
|
215
321
|
function lines(text) {
|
|
216
322
|
const marked = new Set()
|
|
@@ -222,7 +328,7 @@ function lines(text) {
|
|
|
222
328
|
}
|
|
223
329
|
|
|
224
330
|
/** @param {number} from @param {number} to */
|
|
225
|
-
const range = (from, to) => (to > from ? `${from}-${to}` : undefined)
|
|
331
|
+
export const range = (from, to) => (to > from ? `${from}-${to}` : undefined)
|
|
226
332
|
|
|
227
333
|
/** @param {any[]} tokens @param {Context} context @returns {Node[]} */
|
|
228
334
|
export function inline(tokens = [], context) {
|