@codename/deck 0.1.1 → 0.2.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/src/pptx.js CHANGED
@@ -11,7 +11,8 @@
11
11
  import { labelOf } from './kit.js'
12
12
 
13
13
  const W = 13.333, H = 7.5
14
- const TEXT = 'h1, h2, h3, h4, h5, h6, .deck-h, p, ul, ol, blockquote'
14
+ // Words: a paragraph that is a picture alone on its line is the picture.
15
+ const TEXT = 'h1, h2, h3, h4, h5, h6, .deck-h, p:not(.deck-picture), ul, ol, blockquote'
15
16
 
16
17
  /** @param {string} value rgb(…) or #… @returns {string} six hex digits */
17
18
  function hexOf(value) {
@@ -78,11 +79,15 @@ export async function toPptx(view, { PptxGenJS, JSZip }) {
78
79
  const inner = slot.firstElementChild && slot.firstElementChild !== title ? across(slot.firstElementChild) : { x: r.x, w: r.w }
79
80
  objects.push({ placeholder: { options: { name: `body-${slot.dataset.slot}`, type: 'body', x: inner.x, y: top + 0.08, w: inner.w, h: Math.max(0.3, r.y + r.h - top - 0.08), margin: 0, valign: anchor(slot), color: hexOf(getComputedStyle(slot).color) }, text: '' } })
80
81
  }
82
+ // The kit's logo as the slide shows it, the one that reads on its paper, on the master: a slide added in PowerPoint has it too.
83
+ const logo = /** @type {HTMLImageElement | undefined} */ ([...element.querySelectorAll('.deck-logo')].find((one) => getComputedStyle(one).display !== 'none'))
84
+ const mark = logo && await pictureOf(logo.src, logo.clientWidth * 3, logo.clientHeight * 3)
85
+ if (logo && mark) { objects.push({ image: { data: mark, ...place(logo) } }); report.pictures++ }
81
86
  pres.defineSlideMaster({
82
87
  title: masterName, background: { color: hexOf(style.backgroundColor) }, objects,
83
88
  slideNumber: definition?.numbered !== false ? { x: rtl ? 0.3 : W - 0.9, y: H - 0.55, w: 0.6, h: 0.3, fontSize: 10, color: hexOf(rootStyle.getPropertyValue('--deck-muted').trim() || '#666666') } : undefined,
84
89
  })
85
- masters.set(masterName, new Set(objects.map((o) => o.placeholder.options.name)))
90
+ masters.set(masterName, new Set(objects.flatMap((o) => (o.placeholder ? [o.placeholder.options.name] : []))))
86
91
  }
87
92
  const names = masters.get(masterName)
88
93
  const slide = pres.addSlide({ masterName })
@@ -121,6 +126,8 @@ export async function toPptx(view, { PptxGenJS, JSZip }) {
121
126
  } else {
122
127
  const img = block.querySelector('img')
123
128
  if (img) {
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(() => {}) }
124
131
  const data = await pictureOf(img.src, img.clientWidth * 2, img.clientHeight * 2)
125
132
  if (data) { slide.addImage({ data, ...place(img), altText: img.alt }); report.pictures++ }
126
133
  }
@@ -145,7 +152,7 @@ export async function toPptx(view, { PptxGenJS, JSZip }) {
145
152
  if (theme) {
146
153
  const colour = (/** @type {string} */ name) => hexOf(rootStyle.getPropertyValue(`--deck-${name}`).trim())
147
154
  const series = [colour('fill'), ...[2, 3, 4, 5, 6].map((n) => colour(`series-${n}`))]
148
- const scheme = `<a:clrScheme name="Deck"><a:dk1><a:srgbClr val="${colour('ink')}"/></a:dk1><a:lt1><a:srgbClr val="${colour('paper')}"/></a:lt1><a:dk2><a:srgbClr val="${colour('ink')}"/></a:dk2><a:lt2><a:srgbClr val="${colour('soft')}"/></a:lt2>${series.map((c, n) => `<a:accent${n + 1}><a:srgbClr val="${c}"/></a:accent${n + 1}>`).join('')}<a:hlink><a:srgbClr val="${colour('primary')}"/></a:hlink><a:folHlink><a:srgbClr val="${colour('primary')}"/></a:folHlink></a:clrScheme>`
155
+ const scheme = /* xml */ `<a:clrScheme name="Deck"><a:dk1><a:srgbClr val="${colour('ink')}"/></a:dk1><a:lt1><a:srgbClr val="${colour('paper')}"/></a:lt1><a:dk2><a:srgbClr val="${colour('ink')}"/></a:dk2><a:lt2><a:srgbClr val="${colour('soft')}"/></a:lt2>${series.map((c, n) => `<a:accent${n + 1}><a:srgbClr val="${c}"/></a:accent${n + 1}>`).join('')}<a:hlink><a:srgbClr val="${colour('primary')}"/></a:hlink><a:folHlink><a:srgbClr val="${colour('primary')}"/></a:folHlink></a:clrScheme>`
149
156
  zip.file('ppt/theme/theme1.xml', theme.replace(/<a:clrScheme[\s\S]*?<\/a:clrScheme>/, scheme))
150
157
  }
151
158
  const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' })
package/src/render.js CHANGED
@@ -4,7 +4,7 @@
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
6
  // any other tag gives up its words alone. ```echarts and ```mermaid go to the host's drawers.
7
- import { STANDARD, inferLayout, ALIASES } from './kit.js'
7
+ import { STANDARD, CORNERS, inferLayout, ALIASES } from './kit.js'
8
8
 
9
9
  /** @typedef {import('./parse.js').Slide} Slide */
10
10
  /** @typedef {import('./parse.js').Slot} Slot */
@@ -73,6 +73,14 @@ export function renderSlide(slide, context) {
73
73
  // Slots a layout doesn't have still show, under the rest, rather than being lost.
74
74
  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
75
  if (definition.numbered && slide.settings.paginate !== false) section.append(h('span', { class: 'deck-number', 'aria-hidden': 'true' }, [context.number(slide.index + 1)]))
76
+ // The kit's logo in its corner, small, or big where the layout says (a cover, the end); its dark one beside it, which the
77
+ // stylesheet shows on dark paper, where without one the logo sits on a light plate.
78
+ const { logo, logoAt, dark } = context.kit.tokens, at = CORNERS.includes(logoAt) ? logoAt : CORNERS[0]
79
+ if (logo && definition.logo !== 'none') {
80
+ for (const [src, kind] of [[logo, dark?.logo ? 'has-dark' : ''], [dark?.logo, 'is-dark']]) {
81
+ if (src) section.append(h('img', { class: `deck-logo ${kind}`.trim(), src, alt: '', 'aria-hidden': 'true', 'data-size': definition.logo, 'data-at': at }))
82
+ }
83
+ }
76
84
  return { element: section, steps: counter.step }
77
85
  }
78
86
 
package/src/standalone.js CHANGED
@@ -11,7 +11,7 @@ import css from './deck.css'
11
11
  const me = /** @type {HTMLScriptElement | null} */ (document.currentScript)
12
12
  const base = me?.dataset.base ?? 'https://cdn.jsdelivr.net/npm/'
13
13
 
14
- const PAGE = `html, body { margin: 0; } body { min-block-size: 100vh; }
14
+ const PAGE = /* css */ `html, body { margin: 0; } body { min-block-size: 100vh; }
15
15
  body > .deck { min-block-size: 100vh; padding: clamp(0.5rem, 3vw, 2rem); align-content: center; justify-items: center; background: color-mix(in srgb, var(--deck-ink) 7%, var(--deck-paper)); }
16
16
  body > .deck > .deck-stage, body > .deck > .deck-bar { inline-size: min(100%, (100vh - 7rem) * 16 / 9); }
17
17
  body > .deck.is-editing { justify-items: stretch; align-content: start; }
@@ -0,0 +1,161 @@
1
+ // A kit from a company's PowerPoint template (.potx, .pptx, or an Office theme, .thmx), and a kit's tokens out in the W3C's Design Tokens format:
2
+ // a module of its own (@codename/deck/template), which a host loads only when it reads or writes one. From the template,
3
+ // its theme's colours (the first light its paper, the first dark its ink, the first accent its primary, the six accents a
4
+ // chart's colours) and faces (headings', words'); the first picture on its slide master, its logo, in the corner it sits
5
+ // in; and each slide layout, a layout of the kit by its name, its placeholders a grid of slots, on the paper its
6
+ // background is. The file is read with the platform's own DecompressionStream and its XML with regular expressions, as a
7
+ // page under Trusted Types parses no markup. What can't be read is left out, with a reason.
8
+
9
+ import { STANDARD, mergeKits, logoOf } from './kit.js'
10
+
11
+ /** The slide layouts PowerPoint names as deck's own are: a template's take their place. */
12
+ const OWN = /** @type {Record<string, string>} */ ({ 'Title Slide': 'cover', 'Title and Content': 'default', 'Section Header': 'section', 'Two Content': 'two-cols-header', 'Title Only': 'center', Blank: 'none' })
13
+ const PPTX = ['Title Slide', 'Title and Content', 'Section Header', 'Two Content', 'Comparison', 'Title Only', 'Blank', 'Content with Caption', 'Picture with Caption']
14
+ const PICTURES = /** @type {Record<string, string>} */ ({ png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', svg: 'image/svg+xml' })
15
+
16
+ /** A zip's files by name, each read when asked. @param {Uint8Array} bytes */
17
+ function unzip(bytes) {
18
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
19
+ let end = bytes.length - 22
20
+ while (end >= 0 && view.getUint32(end, true) !== 0x06054b50) end--
21
+ if (end < 0) throw new Error('not a PowerPoint file')
22
+ /** @type {Map<string, () => Promise<Uint8Array>>} */
23
+ const files = new Map()
24
+ for (let at = view.getUint32(end + 16, true), n = view.getUint16(end + 10, true); n > 0; n--) {
25
+ const method = view.getUint16(at + 10, true), size = view.getUint32(at + 20, true), name = new TextDecoder().decode(bytes.subarray(at + 46, at + 46 + view.getUint16(at + 28, true)))
26
+ const local = view.getUint32(at + 42, true), start = local + 30 + view.getUint16(local + 26, true) + view.getUint16(local + 28, true), data = bytes.subarray(start, start + size)
27
+ files.set(name, async () => (method === 0 ? data : new Uint8Array(await new Response(new Blob([/** @type {BlobPart} */ (data)]).stream().pipeThrough(new DecompressionStream('deflate-raw'))).arrayBuffer())))
28
+ at += 46 + view.getUint16(at + 28, true) + view.getUint16(at + 30, true) + view.getUint16(at + 32, true)
29
+ }
30
+ return files
31
+ }
32
+
33
+ /** An attribute's value in a piece of XML. @param {string} xml @param {string} name */
34
+ const attr = (xml, name) => new RegExp(`\\b${name}="([^"]*)"`).exec(xml)?.[1]
35
+ /** A part's relationships, by their ids, as paths from the package's root. @param {string} rels @param {string} from the part's folder */
36
+ const targets = (rels, from) => new Map([...rels.matchAll(/<Relationship\b[^>]*>/g)].map(([one]) => [String(attr(one, 'Id')), new URL(String(attr(one, 'Target')), `file:///${from}/`).pathname.slice(1)]))
37
+
38
+ /**
39
+ * A kit read from a PowerPoint template, with what couldn't be read.
40
+ * @param {Uint8Array} bytes a .potx, .pptx or .thmx @param {{ name?: string }} [options]
41
+ * @returns {Promise<{ kit: import('./kit.js').Kit, problems: string[] }>}
42
+ */
43
+ export async function kitFromTemplate(bytes, { name } = {}) {
44
+ const files = unzip(bytes), problems = /** @type {string[]} */ ([])
45
+ const text = async (/** @type {string} */ path) => new TextDecoder().decode(await (files.get(path)?.() ?? new Uint8Array()))
46
+ // A template's parts are under ppt/, an Office theme's under theme/.
47
+ const master = [...files.keys()].filter((path) => /^(ppt|theme)\/slideMasters\/slideMaster\d+\.xml$/.test(path)).sort()[0]
48
+ if (!master) throw new Error('no slide master: not a PowerPoint template')
49
+ const root = master.split('/')[0], masterXml = await text(master), masterRels = targets(await text(master.replace(/([^/]+)$/, '_rels/$1.rels')), `${root}/slideMasters`)
50
+ const themePath = [...masterRels.values()].find((path) => /theme\/theme\d+\.xml$/.test(path)) ?? `${root}/theme/theme1.xml`, theme = await text(themePath)
51
+ const [cx, cy] = ((size) => [Number(attr(size, 'cx') ?? 12192000), Number(attr(size, 'cy') ?? 6858000)])(/<p:sldSz\b[^>]*>/.exec(await text(`${root}/presentation.xml`))?.[0] ?? '')
52
+
53
+ // Its theme: colours by their role, faces for headings and words.
54
+ const scheme = Object.fromEntries([...theme.matchAll(/<a:(dk1|lt1|dk2|lt2|accent[1-6])>\s*<a:(?:srgbClr\b[^>]*\bval|sysClr\b[^>]*\blastClr)="([0-9A-Fa-f]{6})"/g)].map(([, role, value]) => [role, `#${value.toLowerCase()}`]))
55
+ const face = (/** @type {string} */ part) => attr(new RegExp(`<a:${part}>[\\s\\S]*?<a:latin\\b[^>]*>`).exec(theme)?.[0].replace(/^[\s\S]*<a:latin/, '') ?? '', 'typeface')
56
+ const [heading, body] = [face('majorFont'), face('minorFont')].map((one) => (one && !one.startsWith('+') ? [one, 'ui-sans-serif', 'system-ui', 'sans-serif'] : null))
57
+ /** @type {Record<string, any>} */
58
+ const tokens = { ...(scheme.lt1 && { paper: scheme.lt1 }), ...(scheme.dk1 && { ink: scheme.dk1 }), ...(scheme.accent1 && { primary: scheme.accent1 }), ...(scheme.dk2 && { muted: scheme.dk2 }), ...(scheme.lt2 && { soft: scheme.lt2 }) }
59
+ const accents = [1, 2, 3, 4, 5, 6].map((n) => scheme[`accent${n}`]).filter(Boolean)
60
+ if (accents.length) tokens.series = accents
61
+ if (heading || body) tokens.fonts = { heading: heading ?? body, body: body ?? heading }
62
+
63
+ // Its logo: the first picture on its master that a browser draws, in the corner it sits in.
64
+ for (const [pic] of masterXml.matchAll(/<p:pic>[\s\S]*?<\/p:pic>/g)) {
65
+ const path = masterRels.get(String(attr(/<a:blip\b[^>]*>/.exec(pic)?.[0] ?? '', 'r:embed'))), type = PICTURES[/\.(\w+)$/.exec(path ?? '')?.[1].toLowerCase() ?? '']
66
+ if (!path || !type || !files.has(path)) continue
67
+ const data = await files.get(path)?.(), url = data && `data:${type};base64,${base64(data)}`
68
+ if (!logoOf(url)) { problems.push(`the logo ${path.split('/').pop()} is larger than 512 KB`); break }
69
+ const [x, y] = ((off) => [Number(attr(off, 'x') ?? 0), Number(attr(off, 'y') ?? 0)])(/<a:off\b[^>]*>/.exec(pic)?.[0] ?? '')
70
+ Object.assign(tokens, { logo: url, logoAt: `${y < cy / 2 ? 'top' : 'bottom'}-${x < cx / 2 ? 'start' : 'end'}` })
71
+ break
72
+ }
73
+
74
+ // Its layouts: each placeholder where it sits, else where the master's of its kind sits.
75
+ const placed = (/** @type {string} */ xml) => [...xml.matchAll(/<p:sp>[\s\S]*?<\/p:sp>/g)].flatMap(([sp]) => {
76
+ const ph = /<p:ph\b[^>]*>/.exec(sp)?.[0]
77
+ if (!ph) return []
78
+ const off = /<a:off\b[^>]*>/.exec(sp)?.[0], ext = /<a:ext\b[^>]*>/.exec(sp)?.[0]
79
+ return [{ type: attr(ph, 'type') ?? 'body', name: attr(/<p:cNvPr\b[^>]*>/.exec(sp)?.[0] ?? '', 'name') ?? '', box: off && ext ? [Number(attr(off, 'x')), Number(attr(off, 'y')), Number(attr(ext, 'cx')), Number(attr(ext, 'cy'))] : null }]
80
+ })
81
+ const masterBoxes = placed(masterXml)
82
+ const paperOf = (/** @type {string} */ xml) => {
83
+ const fill = /<p:bg>[\s\S]*?<\/p:bg>/.exec(xml)?.[0] ?? '', role = attr(/<a:schemeClr\b[^>]*>/.exec(fill)?.[0] ?? '', 'val'), rgb = attr(/<a:srgbClr\b[^>]*>/.exec(fill)?.[0] ?? '', 'val')?.toLowerCase()
84
+ return role === 'accent1' || (rgb && `#${rgb}` === scheme.accent1) ? 'fill' : role === 'tx1' || role === 'dk1' || (rgb && `#${rgb}` === scheme.dk1) ? 'ink' : role === 'bg2' || role === 'lt2' ? 'soft' : 'paper'
85
+ }
86
+ /** @type {Record<string, any>} */
87
+ const layouts = {}
88
+ const paths = [...masterRels.values()].filter((path) => /slideLayouts\/slideLayout\d+\.xml$/.test(path)).sort((a, b) => Number(/(\d+)\.xml$/.exec(a)?.[1]) - Number(/(\d+)\.xml$/.exec(b)?.[1]))
89
+ for (const path of paths) {
90
+ const xml = await text(path), title = (attr(/<p:cSld\b[^>]*>/.exec(xml)?.[0] ?? '', 'name') ?? '').replace(/&amp;/g, '&').trim()
91
+ const boxes = placed(xml).filter((one) => !['dt', 'ftr', 'sldNum'].includes(one.type)).map((one) => ({ ...one, box: one.box ?? masterBoxes.find((m) => m.type === one.type || (one.type === 'ctrTitle' && m.type === 'title'))?.box ?? null }))
92
+ .filter((one) => one.box)
93
+ const key = OWN[title] ?? (title.toLowerCase().normalize('NFKD').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').replace(/^(?=\d)/, 'l-').slice(0, 32) || `layout-${paths.indexOf(path) + 1}`)
94
+ const layout = gridOf(boxes, cx, cy)
95
+ if (!layout) { problems.push(`layout ${title || key}: its placeholders make no grid deck lays out`); continue }
96
+ layouts[key] = { label: title || key, ...layout, paper: paperOf(xml), title: boxes.some((one) => one.type === 'ctrTitle') ? 'huge' : 'normal', logo: STANDARD.layouts[key]?.logo ?? 'small',
97
+ ...(boxes.some((one) => one.type === 'ctrTitle') && { justify: 'center' }), pptx: PPTX.includes(title) ? title : layout.slots.length > 1 ? 'Comparison' : 'Title and Content',
98
+ numbered: /<p:ph\b[^>]*type="sldNum"/.test(xml) }
99
+ }
100
+ const merged = mergeKits({ name: name ?? 'template', tokens, layouts })
101
+ return { kit: { ...merged.kit, name: name ?? 'template' }, problems: [...problems, ...merged.problems] }
102
+ }
103
+
104
+ /**
105
+ * Placeholders as a grid of slots: the titles the default slot, with the box under them across the same columns; the other
106
+ * boxes by the names deck gave them (`body-right`, as its PowerPoint writes them), else `left`, `middle` and `right` side by
107
+ * side, else `s1`, `s2`…; the margins left to deck. @param {{ type: string, name: string, box: number[] | null }[]} boxes
108
+ * @param {number} cx @param {number} cy
109
+ */
110
+ function gridOf(boxes, cx, cy) {
111
+ const isTitle = (/** @type {{ type: string }} */ one) => ['title', 'ctrTitle', 'subTitle'].includes(one.type)
112
+ const box = (/** @type {{ box: number[] | null }} */ one) => /** @type {number[]} */ (one.box)
113
+ const across = (/** @type {number[]} */ a, /** @type {number[]} */ b) => Math.abs(a[0] - b[0]) < cx * 0.05 && Math.abs(a[2] - b[2]) < cx * 0.05 && a[1] > b[1]
114
+ const heading = boxes.filter(isTitle), own = (/** @type {{ name: string }} */ one) => /^body-([a-z][a-z0-9-]{0,31})$/.exec(one.name)?.[1]
115
+ const sorted = boxes.filter((one) => !isTitle(one)).sort((a, b) => box(a)[0] - box(b)[0] || box(a)[1] - box(b)[1])
116
+ // The heading's slot's own words: the only box, or the one under the heading across its columns.
117
+ const under = !heading.length ? undefined : sorted.length === 1 && !own(sorted[0]) ? sorted[0] : sorted.find((one) => own(one) === 'default' || (!own(one) && across(box(one), box(heading[0]))))
118
+ const titles = under ? [...heading, under] : heading, others = sorted.filter((one) => one !== under)
119
+ const union = (/** @type {number[][]} */ list) => (list.length ? ((xs, ys, xe, ye) => [xs, ys, xe - xs, ye - ys])(Math.min(...list.map((b) => b[0])), Math.min(...list.map((b) => b[1])),
120
+ Math.max(...list.map((b) => b[0] + b[2])), Math.max(...list.map((b) => b[1] + b[3]))) : null)
121
+ // The default slot: the heading and, when it is the only one, the box under it; else the heading, else the first box.
122
+ const rest = titles.length ? others : others.slice(1)
123
+ const first = titles.length ? /** @type {number[]} */ (union(titles.map(box))) : others.length ? box(others[0]) : [0, 0, cx, cy]
124
+ const side = rest.every((one, i) => !i || box(one)[0] >= (box(rest[i - 1])[0] + box(rest[i - 1])[2]) * 0.98)
125
+ const names = side && rest.length === 2 && titles.length ? ['left', 'right'] : side && rest.length === 3 && titles.length ? ['left', 'middle', 'right'] : side && rest.length === 1 ? ['right'] : side && rest.length === 2 ? ['middle', 'right']
126
+ : rest.map((_, i) => `s${i + 1}`)
127
+ /** @type {[string, number[]][]} */
128
+ const slots = [['default', first], ...rest.map((one, i) => /** @type {[string, number[]]} */ ([own(one) ?? names[i], box(one)]))]
129
+ // The grid's lines, near ones as one, then each cell the slot whose box holds its middle.
130
+ const lines = (/** @type {number} */ from, /** @type {number} */ size, /** @type {number} */ total) => [...new Set(slots.flatMap(([, b]) => [b[from], b[from] + b[size]]))].sort((a, b) => a - b)
131
+ .reduce((/** @type {number[]} */ kept, at) => (kept.length && at - kept[kept.length - 1] < total * 0.03 ? kept : [...kept, at]), [])
132
+ const xs = lines(0, 2, cx), ys = lines(1, 3, cy)
133
+ const holds = (/** @type {number[]} */ b, /** @type {number} */ x, /** @type {number} */ y) => x >= b[0] - cx * 0.03 && x <= b[0] + b[2] + cx * 0.03 && y >= b[1] - cy * 0.03 && y <= b[1] + b[3] + cy * 0.03
134
+ const areas = ys.slice(1).map((y, r) => xs.slice(1).map((x, c) => slots.find(([, b]) => holds(b, (xs[c] + x) / 2, (ys[r] + y) / 2))?.[0] ?? '.'))
135
+ if (!areas.length || !areas[0].length) return null
136
+ const fr = (/** @type {number[]} */ edges, /** @type {number} */ total) => (edges.length === 2 ? ['1fr'] : edges.slice(1).map((at, i) => `${Math.max(0.1, Math.round(((at - edges[i]) / total) * 100) / 10)}fr`))
137
+ // A row that holds only the heading takes what it needs.
138
+ const rows = fr(ys, cy).map((track, r) => (slots.length > 1 && areas[r].every((cell) => cell === 'default' || cell === '.') ? 'auto' : track))
139
+ return { slots: slots.map(([one]) => one), areas: areas.map((row) => row.join(' ')).join(' | '), columns: fr(xs, cx).join(' '), rows: rows.join(' ') }
140
+ }
141
+
142
+ /** Bytes as base64, a piece at a time. @param {Uint8Array} bytes */
143
+ function base64(bytes) {
144
+ let binary = ''
145
+ for (let i = 0; i < bytes.length; i += 0x8000) binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000))
146
+ return btoa(binary)
147
+ }
148
+
149
+ /**
150
+ * A kit's tokens in the W3C's Design Tokens format (DTCG 2025.10), as design tools read them: its colours light and
151
+ * dark, its chart colours and its faces; `tokensFrom()` reads them back. @param {Record<string, any>} tokens
152
+ */
153
+ export function tokensToDtcg(tokens) {
154
+ const colour = (/** @type {unknown} */ value) => (typeof value === 'string' ? { $type: 'color', $value: value } : undefined)
155
+ const group = (/** @type {Record<string, any>} */ own) => Object.fromEntries(Object.entries({ bg: own.paper, fg: own.ink, accent: own.primary, muted: own.muted, surface: own.soft }).flatMap(([key, value]) => (colour(value) ? [[key, colour(value)]] : [])))
156
+ const fonts = tokens.fonts ?? {}
157
+ return {
158
+ color: { light: group(tokens), ...(tokens.dark && { dark: group(tokens.dark) }), ...(tokens.series && { series: Object.fromEntries(tokens.series.map((/** @type {string} */ one, /** @type {number} */ i) => [`s${i + 1}`, colour(one)])) }) },
159
+ ...((fonts.heading || fonts.body) && { font: { ...(fonts.heading && { heading: { $type: 'fontFamily', $value: fonts.heading } }), ...(fonts.body && { body: { $type: 'fontFamily', $value: fonts.body } }) } }),
160
+ }
161
+ }
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.1.1'
2
+ export const VERSION = '0.2.0'