@codename/deck 0.2.0 → 0.3.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 +1 -1
- package/dist/deck.bundle.mjs +74 -74
- package/dist/deck.bundle.mjs.map +3 -3
- package/dist/deck.css +7 -0
- package/dist/deck.js +67 -60
- package/dist/deck.js.map +3 -3
- package/dist/deck.mjs +33 -33
- package/dist/deck.mjs.map +3 -3
- package/dist/template.mjs +3 -1
- package/dist/template.mjs.map +4 -4
- package/dist/types/index.d.ts +1 -1
- package/dist/types/kit.d.ts +11 -4
- package/dist/types/look.d.ts +6 -2
- package/dist/types/template.d.ts +35 -0
- package/dist/types/version.d.ts +1 -1
- package/docs/host.md +2 -2
- package/docs/kits.md +11 -2
- package/package.json +1 -1
- package/src/deck.css +7 -0
- package/src/deck.js +2 -2
- package/src/index.js +1 -1
- package/src/kit.js +27 -12
- package/src/look.js +22 -16
- package/src/pptx.js +25 -9
- package/src/render.js +4 -1
- package/src/template.js +451 -26
- package/src/version.js +1 -1
package/dist/types/template.d.ts
CHANGED
|
@@ -9,6 +9,41 @@ export declare function kitFromTemplate(bytes: Uint8Array, { name }?: {
|
|
|
9
9
|
kit: import('./kit.js').Kit;
|
|
10
10
|
problems: string[];
|
|
11
11
|
}>;
|
|
12
|
+
export type Art = {
|
|
13
|
+
scheme: Record<string, number[] | null>;
|
|
14
|
+
map: Record<string, string>;
|
|
15
|
+
rels: Map<string, string>;
|
|
16
|
+
themeRels: Map<string, string>;
|
|
17
|
+
files: Map<string, () => Promise<Uint8Array>>;
|
|
18
|
+
size: number[];
|
|
19
|
+
styles: {
|
|
20
|
+
fill: string[];
|
|
21
|
+
line: string[];
|
|
22
|
+
bg: string[];
|
|
23
|
+
};
|
|
24
|
+
faces: {
|
|
25
|
+
major: string;
|
|
26
|
+
minor: string;
|
|
27
|
+
};
|
|
28
|
+
pictures: Map<string, Promise<string | null>>;
|
|
29
|
+
used: Set<string>;
|
|
30
|
+
noted: Set<string>;
|
|
31
|
+
ids: {
|
|
32
|
+
n: number;
|
|
33
|
+
};
|
|
34
|
+
defs: string[];
|
|
35
|
+
group?: string;
|
|
36
|
+
};
|
|
37
|
+
export type Part = {
|
|
38
|
+
name: string;
|
|
39
|
+
tag: string;
|
|
40
|
+
inner: string;
|
|
41
|
+
xml: string;
|
|
42
|
+
};
|
|
43
|
+
export type Colour = {
|
|
44
|
+
rgb: number[];
|
|
45
|
+
alpha: number;
|
|
46
|
+
};
|
|
12
47
|
/**
|
|
13
48
|
* A kit's tokens in the W3C's Design Tokens format (DTCG 2025.10), as design tools read them: its colours light and
|
|
14
49
|
* dark, its chart colours and its faces; `tokensFrom()` reads them back. @param {Record<string, any>} tokens
|
package/dist/types/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.3.0";
|
package/docs/host.md
CHANGED
|
@@ -40,9 +40,9 @@ Every block that can be changed by hand carries `data-edit="from-to"`, the offse
|
|
|
40
40
|
const { blob, report } = await toPptx(view, { PptxGenJS, JSZip })
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
A kit from a PowerPoint template, and a kit's tokens out, are `@codename/deck/template`'s (`kitFromTemplate`, `tokensToDtcg`, [kits.md](kits.md)), which a host imports only when it reads or writes one; `logoOf` and `CORNERS` check a kit's logo and its corner before it is kept.
|
|
43
|
+
A kit from a PowerPoint template, and a kit's tokens out, are `@codename/deck/template`'s (`kitFromTemplate`, `tokensToDtcg`, [kits.md](kits.md)), which a host imports only when it reads or writes one; `logoOf` and `CORNERS` check a kit's logo and its corner before it is kept, and `rgbOf` a colour as deck reads it.
|
|
44
44
|
|
|
45
|
-
PptxGenJS 4 (its bundle carries JSZip) is the host's to load, on demand. The file is made from the deck as drawn: a slide layout per deck layout, named by its label in the deck's language, with a placeholder for its title and each slot; lists as lists with their levels; tables; bars, lines, areas, pies, rings, points and radars as native charts with their numbers, other charts and diagrams as pictures, and pictures, alone on their line or beside the words, as pictures; notes; transitions; the kit's colours as the theme. `report` counts slides, charts, tables, pictures and placeholders.
|
|
45
|
+
PptxGenJS 4 (its bundle carries JSZip) is the host's to load, on demand. The file is made from the deck as drawn: a slide layout per deck layout, named by its label in the deck's language, with a placeholder for its title and each slot; lists as lists with their levels; tables; bars, lines, areas, pies, rings, points and radars as native charts with their numbers, other charts and diagrams as pictures, and pictures, alone on their line or beside the words, as pictures; a layout's art as one picture on its slide layout; notes; transitions; the kit's colours as the theme. `report` counts slides, charts, tables, pictures and placeholders.
|
|
46
46
|
|
|
47
47
|
## Files
|
|
48
48
|
|
package/docs/kits.md
CHANGED
|
@@ -27,7 +27,7 @@ Flat:
|
|
|
27
27
|
|
|
28
28
|
| Token | What it is |
|
|
29
29
|
|---|---|
|
|
30
|
-
| `paper`, `ink`, `primary` | Colours, as `#rgb` or `#
|
|
30
|
+
| `paper`, `ink`, `primary` | Colours, as `#rgb`, `#rrggbb`, `rgb()` or `rgba()` (`rgb(0 85 164 / 50%)` too, or `#rrggbbaa`); one see-through is seen over the paper it is on |
|
|
31
31
|
| `muted`, `soft` | Optional: secondary words, a pale fill |
|
|
32
32
|
| `voice` | `calm`, `elegant`, `bold`, `friendly`: the device's own faces for headings and text |
|
|
33
33
|
| `fonts` | Optional: `{ heading: [...], body: [...] }`, face stacks, over the voice |
|
|
@@ -57,15 +57,24 @@ A logo shows in its corner, small, or big where a layout says (`logo`: a cover,
|
|
|
57
57
|
| `logo` | `small`, `big`, `none`: how the kit's logo shows | `small`; `big` on `cover`, `intro`, `end`; `none` on `image`, `full`, `none` |
|
|
58
58
|
| `pptx` | Title Slide, Title and Content, Section Header, Two Content, Comparison, Title Only, Blank, Content with Caption, Picture with Caption | by its slots |
|
|
59
59
|
| `numbered` | `false` for no slide number | `true` |
|
|
60
|
+
| `art` | Up to 8 names of the kit's `pictures`, drawn over the whole slide, layer over layer, behind the words, as a template draws them | none |
|
|
60
61
|
| `label` | Words, or words by language: `{ en: …, fr: … }`; PowerPoint's layout is named so | the name |
|
|
61
62
|
| `intent`, `use_when`, `avoid_when` | Words for the brain choosing a layout | none |
|
|
62
63
|
| budgets | `required`, `max_words`, `max_chars`, `items: [min, max]`, `item_max_words`, `heading_max_words`, `kind` | none |
|
|
63
64
|
|
|
65
|
+
A layout with art is laid out as its template is: its grid edge to edge (its margins are `.` tracks), left to right whatever the language, its number too, the words keeping theirs. In PowerPoint, its art is one picture on its slide layout.
|
|
66
|
+
|
|
64
67
|
A catalog's `types` are read as `layouts`. A layout given as `false` turns one of deck's own off (`image: false`), but `default`. Budgets are kept with each layout for a linter and for the brain; deck does not check them yet.
|
|
65
68
|
|
|
69
|
+
## Pictures
|
|
70
|
+
|
|
71
|
+
A kit's `pictures` are names (`a-z`, `0-9`, `-`) of pictures inside the kit: PNG, JPEG, GIF, WebP or SVG as a `data:` address, base64, or an SVG written out (`data:image/svg+xml,…`), 3 MB each at most, never an address to fetch. A layout's `art` names them, so a layer many layouts draw (a template's master) is kept once.
|
|
72
|
+
|
|
66
73
|
## From a PowerPoint template, and out
|
|
67
74
|
|
|
68
|
-
`@codename/deck/template`, a module of its own that a host loads only when it needs it, reads a company's template (`.potx`, `.pptx`, or an Office theme, `.thmx`) into a kit: `kitFromTemplate(bytes, { name })` gives `{ kit, problems }`. Its theme's first light is `paper`, its first dark `ink`, its first accent `primary`, its second dark and light `muted` and `soft`, its six accents `series`, its heading and body faces `fonts`; the first picture on its slide master a browser draws is its `logo`, in the corner it sits in. Each slide layout is a layout of the kit by its name: PowerPoint's Title Slide, Title and Content, Section Header, Two Content, Title Only and Blank take the place of `cover`, `default`, `section`, `two-cols-header`, `center` and `none`; the others are added, slugged (`content-with-caption`). Its placeholders become a grid: the title, with the box under it across its columns, the `default` slot; the other boxes `left`, `middle` and `right` side by side, else `s1`, `s2`…, or the names deck gave them when deck wrote the file (`body-right`); the margins are deck's. Its paper is its background's colour by its role, its title `huge` for a title slide's; a layout whose placeholders make no grid is left out, with a reason.
|
|
75
|
+
`@codename/deck/template`, a module of its own that a host loads only when it needs it, reads a company's template (`.potx`, `.pptx`, or an Office theme, `.thmx`) into a kit: `kitFromTemplate(bytes, { name })` gives `{ kit, problems }`. Its theme's first light is `paper`, its first dark `ink`, its first accent `primary`, its second dark and light `muted` and `soft`, its six accents `series`, its heading and body faces `fonts`; the first picture on its slide master a browser draws is its `logo`, in the corner it sits in. Each slide layout is a layout of the kit by its name: PowerPoint's Title Slide, Title and Content, Section Header, Two Content, Title Only and Blank take the place of `cover`, `default`, `section`, `two-cols-header`, `center` and `none`; the others are added, slugged (`content-with-caption`). Its placeholders become a grid: the title, with the box under it across its columns, the `default` slot; the other boxes `left`, `middle` and `right` side by side, else `s1`, `s2`…, or the names deck gave them when deck wrote the file (`body-right`); the margins are deck's. Its paper is its background's colour by its role, its title `huge` for a title slide's, its words aligned as its title's; a layout whose placeholders make no grid is left out, with a reason.
|
|
76
|
+
|
|
77
|
+
Each layout's art is what a slide on it shows besides its placeholders, as SVG layers among the kit's `pictures`: its background where it isn't a colour of the theme (another colour, a gradient, a picture), the master's shapes unless the layout hides them, then its own, in their order. Shapes are PowerPoint's usual ones and any of its own geometry, filled with a colour, a gradient or a picture and outlined as the template says (their style's fill and line where they give none); pictures cropped, see-through and recoloured (duotone, brightness, contrast) as PowerPoint draws them, an SVG beside a picture in its stead; groups scaled into place; a shape's words in its own size, weight, colour and face, in capitals where it says, aligned and wrapped in its box; fields (a slide's number, its date) are deck's. A layout with art keeps its words where the template has them; light words over it are on `ink`; where the master's logo is in its art, deck draws none of its own; and the kit's paper and words by night are its own by day, as the template has one look. Pictures browsers don't draw (EMF, WMF, TIFF) and shapes deck doesn't know are left out, with a reason; so is a picture over 2 MB.
|
|
69
78
|
|
|
70
79
|
`tokensToDtcg(tokens)` writes a kit's tokens in the W3C's Design Tokens format, which `tokensFrom()` reads back.
|
|
71
80
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codename/deck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Slide decks as one web page: Markdown in Slidev's form inside a .deck.html file any browser presents, a renderer that runs anywhere, kits of layouts and looks, and an editable PowerPoint out",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/deck.css
CHANGED
|
@@ -58,6 +58,13 @@
|
|
|
58
58
|
:is(.deck[data-scheme="light"] :is(.deck-slide[data-paper="ink"], .deck-class-invert), .deck[data-scheme="dark"] .deck-slide:is([data-paper="paper"], [data-paper="soft"]):not(.deck-class-invert), .deck[data-fill="dark"] .deck-slide[data-paper="fill"]) .deck-logo.is-dark { display: block; }
|
|
59
59
|
:is(.deck[data-scheme="light"] :is(.deck-slide[data-paper="ink"], .deck-class-invert), .deck[data-scheme="dark"] .deck-slide:is([data-paper="paper"], [data-paper="soft"]):not(.deck-class-invert), .deck[data-fill="dark"] .deck-slide[data-paper="fill"]) .deck-logo:not(.has-dark, .is-dark) { background: #fff; padding: 0.5cqi; border-radius: 0.5cqi; }
|
|
60
60
|
|
|
61
|
+
/* A layout's art (docs/kits.md), as its template draws it, behind words that sit where the template has them: its grid edge to edge, the slide laid out left to right, its number too. */
|
|
62
|
+
.deck-art { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; pointer-events: none; }
|
|
63
|
+
.deck-slide.has-art { direction: ltr; }
|
|
64
|
+
.has-art .deck-canvas { padding: 0; gap: 0; }
|
|
65
|
+
.has-art .deck-slot { padding: 0.5cqi 1cqi; }
|
|
66
|
+
.deck[dir="rtl"] .has-art .deck-slot { direction: rtl; }
|
|
67
|
+
|
|
61
68
|
/* Layouts from their kit's definition: the paper they are on, where their words sit, how big their heading is. */
|
|
62
69
|
.deck-slide[data-paper="fill"] { background: var(--deck-fill); color: var(--deck-on-fill); }
|
|
63
70
|
.deck-slide[data-paper="ink"] { background: var(--deck-ink); color: var(--deck-paper); }
|
package/src/deck.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// the deck changes or is saved.
|
|
9
9
|
import { parseDeck } from './parse.js'
|
|
10
10
|
import { renderSlide, h } from './render.js'
|
|
11
|
-
import { tokensOf, schemeOf, VOICES,
|
|
11
|
+
import { tokensOf, schemeOf, VOICES, rgbOf, contrast } from './look.js'
|
|
12
12
|
import { mergeKits } from './kit.js'
|
|
13
13
|
import { stringsFor } from './strings.js'
|
|
14
14
|
import { Lexer } from 'marked'
|
|
@@ -108,7 +108,7 @@ export function createDeck(root, options) {
|
|
|
108
108
|
})) root.style.setProperty(`--deck-${name}`, value)
|
|
109
109
|
root.dataset.scheme = scheme
|
|
110
110
|
// Whether the primary as a fill is dark, for a logo's dark one there.
|
|
111
|
-
root.dataset.fill = ((fill) => (contrast(fill, [255, 255, 255]) > contrast(fill, [0, 0, 0]) ? 'dark' : 'light'))(
|
|
111
|
+
root.dataset.fill = ((fill) => (contrast(fill, [255, 255, 255]) > contrast(fill, [0, 0, 0]) ? 'dark' : 'light'))(rgbOf(tokens.primary) ?? [247, 197, 49])
|
|
112
112
|
root.dispatchEvent(new CustomEvent('deck-scheme', { detail: { scheme, colours, tokens } }))
|
|
113
113
|
}
|
|
114
114
|
const chooseScheme = () => {
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { createDeck, markdownOf } from './deck.js'
|
|
|
3
3
|
export { parseDeck } from './parse.js'
|
|
4
4
|
export { renderSlide } from './render.js'
|
|
5
5
|
export { STANDARD, CORNERS, mergeKits, layoutOf, logoOf, inferLayout, labelOf, tokensFrom } from './kit.js'
|
|
6
|
-
export { tokensOf, schemeOf, LOOKS, readable, contrast } from './look.js'
|
|
6
|
+
export { tokensOf, schemeOf, LOOKS, readable, contrast, rgbOf } from './look.js'
|
|
7
7
|
export { readDeckFile, writeDeckFile, rendererUrl, escape, unescape } from './file.js'
|
|
8
8
|
export { toPptx } from './pptx.js'
|
|
9
9
|
export { defaultBlocks, libraryAt } from './blocks.js'
|
package/src/kit.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
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
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// PowerPoint layout it is nearest to, and the art a template draws behind it, layers among the kit's
|
|
7
|
+
// pictures; a kit may turn one of the usual layouts off. Its tokens may hold a logo, a picture inside
|
|
8
|
+
// the kit and never an address, as its pictures are. Everything is checked against a small grammar;
|
|
9
|
+
// what fails is dropped, with a reason. Pure.
|
|
9
10
|
|
|
10
11
|
/** @typedef {{ required?: boolean, max_words?: number, max_chars?: number, items?: [number, number], item_max_words?: number, heading_max_words?: number, kind?: string }} Budget */
|
|
11
|
-
/** @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', pptx: string, numbered: boolean }} Layout */
|
|
12
|
-
/** @typedef {{ name?: string, tokens: Record<string, any>, layouts: Record<string, Layout> }} Kit */
|
|
12
|
+
/** @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', pptx: string, numbered: boolean, art?: string[] }} Layout */
|
|
13
|
+
/** @typedef {{ name?: string, tokens: Record<string, any>, layouts: Record<string, Layout>, pictures: Record<string, string> }} Kit */
|
|
13
14
|
|
|
14
15
|
const NAME = /^[a-z][a-z0-9-]{0,31}$/
|
|
15
16
|
const TRACK = /^(auto|min-content|max-content|\d{1,2}(\.\d)?fr|\d{1,3}%)$/
|
|
@@ -24,6 +25,12 @@ export const CORNERS = ['top-end', 'top-start', 'bottom-end', 'bottom-start']
|
|
|
24
25
|
*/
|
|
25
26
|
export const logoOf = (value) => (typeof value === 'string' && value.length <= 700_000 && /^data:image\/(png|jpeg|gif|webp|svg\+xml);base64,[a-z0-9+/]+=*$/i.test(value) ? value : null)
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* A picture of a kit's, a layer of a layout's art drawn behind its words over the whole slide: a picture inside the kit as a
|
|
30
|
+
* logo is, or an SVG written out (`data:image/svg+xml,…`), of 3 MB at most, never an address to fetch; else null. @param {unknown} value
|
|
31
|
+
*/
|
|
32
|
+
export const artOf = (value) => (typeof value === 'string' && value.length <= 3_145_728 && /^data:image\/(?:(?:png|jpeg|gif|webp|svg\+xml);base64,[a-z0-9+/]+=*|svg\+xml,[\w.!~*'()%/+=-]*)$/i.test(value) ? value : null)
|
|
33
|
+
|
|
27
34
|
/** The names brains give the usual layouts, for the ones deck has. */
|
|
28
35
|
export const ALIASES = /** @type {Record<string, string>} */ ({
|
|
29
36
|
title: 'cover', 'title-slide': 'cover', 'two-columns': 'two-cols', 'two-column': 'two-cols', columns: 'two-cols', comparison: 'two-cols-header',
|
|
@@ -34,6 +41,7 @@ export const ALIASES = /** @type {Record<string, string>} */ ({
|
|
|
34
41
|
export const STANDARD = /** @type {Kit} */ ({
|
|
35
42
|
name: 'standard',
|
|
36
43
|
tokens: {},
|
|
44
|
+
pictures: {},
|
|
37
45
|
layouts: Object.fromEntries(Object.entries(/** @type {Record<string, any>} */ ({
|
|
38
46
|
default: { label: { en: 'Title and content', fr: 'Titre et contenu' }, pptx: 'Title and Content' },
|
|
39
47
|
cover: { label: { en: 'Cover', fr: 'Couverture' }, justify: 'center', paper: 'fill', title: 'huge', logo: 'big', pptx: 'Title Slide', numbered: false },
|
|
@@ -98,12 +106,14 @@ export function layoutOf(given) {
|
|
|
98
106
|
return value
|
|
99
107
|
}
|
|
100
108
|
const label = typeof given.label === 'string' || (given.label && typeof given.label === 'object') ? given.label : undefined
|
|
109
|
+
const art = Array.isArray(given.art) && given.art.length <= 8 && given.art.every((/** @type {unknown} */ one) => typeof one === 'string' && NAME.test(one)) ? given.art : undefined
|
|
110
|
+
if (given.art !== undefined && !art) problems.push('art is not 8 names at most')
|
|
101
111
|
const words = (/** @type {any} */ v) => (typeof v === 'string' ? v.slice(0, 300) : undefined)
|
|
102
112
|
return {
|
|
103
113
|
layout: /** @type {Layout} */ ({
|
|
104
114
|
label, intent: words(given.intent), use_when: words(given.use_when), avoid_when: words(given.avoid_when), slots, budgets, areas: rows, columns, rows: rowTracks,
|
|
105
115
|
align: pick('align', 'start'), justify: pick('justify', 'start'), paper: pick('paper', 'paper'), title: pick('title', 'normal'), logo: pick('logo', 'small'),
|
|
106
|
-
pptx: PPTX.includes(given.pptx) ? given.pptx : slots.length > 1 ? 'Comparison' : 'Title and Content', numbered: given.numbered !== false,
|
|
116
|
+
pptx: PPTX.includes(given.pptx) ? given.pptx : slots.length > 1 ? 'Comparison' : 'Title and Content', numbered: given.numbered !== false, art,
|
|
107
117
|
}),
|
|
108
118
|
problems,
|
|
109
119
|
}
|
|
@@ -123,14 +133,15 @@ function clean(given) {
|
|
|
123
133
|
/**
|
|
124
134
|
* Design tokens as the W3C's Design Tokens format (DTCG 2025.10) writes them, read into a kit's:
|
|
125
135
|
* `color.light` and `color.dark` groups (bg, surface, fg, muted, accent) or flat colours, `font.sans`
|
|
126
|
-
* and the like; each `$value` a
|
|
136
|
+
* and the like; each `$value` a colour string or a colour object with its `hex` and `alpha`. Tokens already flat
|
|
127
137
|
* pass through. @param {any} given @returns {Record<string, any>}
|
|
128
138
|
*/
|
|
129
139
|
export function tokensFrom(given) {
|
|
130
140
|
if (!given || typeof given !== 'object') return {}
|
|
131
141
|
if (!JSON.stringify(given).includes('"$value"')) return given
|
|
132
142
|
const value = (/** @type {any} */ t) => (t && typeof t === 'object' && '$value' in t ? t.$value : t)
|
|
133
|
-
|
|
143
|
+
// A colour object's alpha kept, as #rrggbbaa.
|
|
144
|
+
const colour = (/** @type {any} */ t) => { const v = value(t); return typeof v === 'string' ? v : typeof v?.hex === 'string' ? `${v.hex}${typeof v.alpha === 'number' && v.alpha < 1 ? Math.round(Math.max(0, v.alpha) * 255).toString(16).padStart(2, '0') : ''}` : undefined }
|
|
134
145
|
const face = (/** @type {any} */ t) => { const v = value(t); return Array.isArray(v) ? v.map(String) : typeof v === 'string' ? [v] : undefined }
|
|
135
146
|
const NAMES = /** @type {const} */ ({ bg: 'paper', background: 'paper', paper: 'paper', fg: 'ink', text: 'ink', ink: 'ink', accent: 'primary', primary: 'primary', muted: 'muted', surface: 'soft', soft: 'soft' })
|
|
136
147
|
const group = (/** @type {any} */ g) => Object.fromEntries(Object.entries(g ?? {}).filter(([k]) => k in NAMES).map(([k, t]) => [NAMES[/** @type {keyof typeof NAMES} */ (k)], colour(t)]).filter(([, v]) => v))
|
|
@@ -146,13 +157,13 @@ export function tokensFrom(given) {
|
|
|
146
157
|
}
|
|
147
158
|
|
|
148
159
|
/**
|
|
149
|
-
* Kits merged, the later over the earlier: their tokens, their dark ones within theirs,
|
|
150
|
-
* checked; a layout given as false is taken away, but the default. A logo that isn't a picture inside the
|
|
151
|
-
* @param {...any} kits @returns {{ kit: Kit, problems: string[] }}
|
|
160
|
+
* Kits merged, the later over the earlier: their tokens, their dark ones within theirs, their layouts by name and their
|
|
161
|
+
* pictures, each checked; a layout given as false is taken away, but the default. A logo that isn't a picture inside the
|
|
162
|
+
* kit is dropped. @param {...any} kits @returns {{ kit: Kit, problems: string[] }}
|
|
152
163
|
*/
|
|
153
164
|
export function mergeKits(...kits) {
|
|
154
165
|
/** @type {Kit} */
|
|
155
|
-
const kit = { name: 'standard', tokens: {}, layouts: { ...STANDARD.layouts } }
|
|
166
|
+
const kit = { name: 'standard', tokens: {}, layouts: { ...STANDARD.layouts }, pictures: {} }
|
|
156
167
|
const problems = []
|
|
157
168
|
for (const given of kits) {
|
|
158
169
|
if (!given || typeof given !== 'object') continue
|
|
@@ -161,6 +172,10 @@ export function mergeKits(...kits) {
|
|
|
161
172
|
const next = tokensFrom(given.tokens), dark = next.dark && typeof next.dark === 'object' ? { ...kit.tokens.dark, ...next.dark } : kit.tokens.dark
|
|
162
173
|
kit.tokens = { ...kit.tokens, ...next, ...(dark && { dark }) }
|
|
163
174
|
}
|
|
175
|
+
for (const [name, picture] of Object.entries(given.pictures ?? {})) {
|
|
176
|
+
if (NAME.test(name) && artOf(picture)) kit.pictures[name] = picture
|
|
177
|
+
else problems.push(`picture ${name}: not a picture inside the kit`)
|
|
178
|
+
}
|
|
164
179
|
for (const [name, definition] of Object.entries(given.layouts ?? given.types ?? {})) {
|
|
165
180
|
if (!NAME.test(name)) { problems.push(`layout ${name}: not a-z, 0-9 and -`); continue }
|
|
166
181
|
if (definition === false) { if (name !== 'default') delete kit.layouts[name]; continue }
|
package/src/look.js
CHANGED
|
@@ -41,12 +41,12 @@ export function tokensOf(settings, project = {}) {
|
|
|
41
41
|
function clean(given) {
|
|
42
42
|
/** @type {Partial<Tokens>} */
|
|
43
43
|
const kept = {}
|
|
44
|
-
for (const key of /** @type {const} */ (['paper', 'ink', 'primary', 'muted', 'soft'])) if (typeof given[key] === 'string' &&
|
|
45
|
-
if (given.dark && typeof given.dark === 'object') kept.dark = Object.fromEntries(Object.entries(given.dark).filter(([, v]) => typeof v === 'string' &&
|
|
44
|
+
for (const key of /** @type {const} */ (['paper', 'ink', 'primary', 'muted', 'soft'])) if (typeof given[key] === 'string' && rgbOf(given[key])) kept[key] = given[key]
|
|
45
|
+
if (given.dark && typeof given.dark === 'object') kept.dark = Object.fromEntries(Object.entries(given.dark).filter(([, v]) => typeof v === 'string' && rgbOf(v)))
|
|
46
46
|
if (given.fonts && Array.isArray(given.fonts.heading) && Array.isArray(given.fonts.body)) kept.fonts = { heading: given.fonts.heading.map(String), body: given.fonts.body.map(String) }
|
|
47
47
|
if (typeof given.voice === 'string') kept.voice = given.voice
|
|
48
48
|
if (typeof given.radius === 'number') kept.radius = given.radius
|
|
49
|
-
if (Array.isArray(given.series) && given.series.length && given.series.every((/** @type {unknown} */ c) => typeof c === 'string' &&
|
|
49
|
+
if (Array.isArray(given.series) && given.series.length && given.series.every((/** @type {unknown} */ c) => typeof c === 'string' && rgbOf(c))) kept.series = given.series.slice(0, 6)
|
|
50
50
|
return kept
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -55,30 +55,36 @@ function clean(given) {
|
|
|
55
55
|
* @param {Tokens} tokens @param {'light' | 'dark'} scheme @returns {Scheme}
|
|
56
56
|
*/
|
|
57
57
|
export function schemeOf(tokens, scheme) {
|
|
58
|
-
|
|
58
|
+
// A see-through colour is seen over the paper it is on.
|
|
59
|
+
const light = rgbOf(tokens.paper) ?? [255, 253, 248], dark = rgbOf(tokens.ink, light) ?? [29, 27, 22]
|
|
59
60
|
// A kit's own dark colours where it gives them (a design system's dark mode), else worked out.
|
|
60
61
|
const own = scheme === 'dark' ? tokens.dark ?? {} : {}
|
|
61
|
-
const paper = round(
|
|
62
|
-
const ink = round(
|
|
63
|
-
const primary =
|
|
62
|
+
const paper = round(rgbOf(own.paper ?? '', scheme === 'dark' ? [0, 0, 0] : [255, 255, 255]) ?? (scheme === 'dark' ? mix(dark, [0, 0, 0], 0.35) : light))
|
|
63
|
+
const ink = round(rgbOf(own.ink ?? '', paper) ?? (scheme === 'dark' ? mix(light, [255, 255, 255], 0.2) : dark))
|
|
64
|
+
const primary = rgbOf(own.primary ?? tokens.primary, paper) ?? [247, 197, 49]
|
|
64
65
|
// The primary as a fill keeps its own colour; as words or marks it is made to read on the paper.
|
|
65
66
|
const text = readable(primary, paper, 4.5)
|
|
66
67
|
const onPrimary = contrast(ink, primary) >= contrast(paper, primary) ? ink : paper
|
|
67
68
|
// A chart's colours: the kit's own (a template's accents), else the primary turned round the wheel; each made to read at 3:1.
|
|
68
|
-
const given = (tokens.series ?? []).map(
|
|
69
|
+
const given = (tokens.series ?? []).map((c) => rgbOf(c, paper)).filter((c) => c !== null)
|
|
69
70
|
const series = [0, 1, 2, 3, 4, 5].map((n) => readable(/** @type {number[]} */ (given[n % (given.length || 1)] ?? rotate(primary, n * 57)), paper, 3))
|
|
70
71
|
return {
|
|
71
|
-
paper: css(paper), ink: css(ink), muted: css(readable(round(
|
|
72
|
-
primary: css(text), onPrimary: css(onPrimary), soft: css(
|
|
72
|
+
paper: css(paper), ink: css(ink), muted: css(readable(round(rgbOf(own.muted ?? (scheme === 'light' ? tokens.muted ?? '' : ''), paper) ?? mix(ink, paper, 0.38)), paper, 4.5)), line: css(mix(ink, paper, 0.82)),
|
|
73
|
+
primary: css(text), onPrimary: css(onPrimary), soft: css(rgbOf(own.soft ?? (scheme === 'light' ? tokens.soft ?? '' : ''), paper) ?? mix(primary, paper, 0.78)), series: series.map(css),
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
/**
|
|
78
|
+
* A colour as deck reads one, as red, green and blue: `#rgb` or `#rrggbb` (`#rgba`, `#rrggbbaa` with an alpha), `rgb()` or
|
|
79
|
+
* `rgba()` as CSS writes them, by commas or spaces, the alpha after a comma or a slash; one see-through is seen over `under`,
|
|
80
|
+
* white unless said. @param {string} value @param {number[]} [under] @returns {[number, number, number] | null}
|
|
81
|
+
*/
|
|
82
|
+
export function rgbOf(value, under = [255, 255, 255]) {
|
|
83
|
+
const text = String(value).trim(), digits = /^#?([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i.exec(text)?.[1], css = /^rgba?\(([^)]*)\)$/i.exec(text)?.[1].split(/[\s,/]+/).filter(Boolean)
|
|
84
|
+
const share = (/** @type {string} */ part, /** @type {number} */ whole) => (part.endsWith('%') ? (parseFloat(part) / 100) * whole : Number(part))
|
|
85
|
+
const [r, g, b, a = 1] = digits ? (digits.length < 5 ? [...digits].map((c) => c + c) : /** @type {string[]} */ (digits.match(/../g))).map((h, i) => parseInt(h, 16) / (i > 2 ? 255 : 1))
|
|
86
|
+
: css?.length === 3 || css?.length === 4 ? css.map((part, i) => share(part, i > 2 ? 1 : 255)) : []
|
|
87
|
+
return r !== undefined && [r, g, b].every((c) => c >= 0 && c <= 255) && a >= 0 && a <= 1 ? round(mix(under, [r, g, b], a)) : null
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
/** @param {number[]} rgb */
|
package/src/pptx.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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
5
|
// points or a radar as PowerPoint's own chart, its numbers in the file to change; anything else (a
|
|
6
|
-
// diagram, another chart, a picture) as a picture; the speaker's notes as notes; each slide's
|
|
6
|
+
// 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
7
|
// transition as PowerPoint's (Morph where it has one, a fade where it hasn't); the look's colours
|
|
8
8
|
// as the file's theme. Read from the deck as drawn, each box where the browser put it, so the file
|
|
9
9
|
// looks like the slides, in the scheme shown. PptxGenJS (with its JSZip) is the host's to load.
|
|
@@ -72,6 +72,9 @@ export async function toPptx(view, { PptxGenJS, JSZip }) {
|
|
|
72
72
|
const masterName = `${definition ? labelOf(definition, layout, view.lang) : layout}${element.classList.contains('deck-class-invert') ? ' (dark)' : ''}`
|
|
73
73
|
if (!masters.has(masterName)) {
|
|
74
74
|
const objects = []
|
|
75
|
+
// The layout's art, its layers as one picture on its paper behind the rest: a slide added in PowerPoint has it too.
|
|
76
|
+
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)
|
|
77
|
+
if (backdrop) { objects.push({ image: { data: backdrop, x: 0, y: 0, w: W, h: H } }); report.pictures++ }
|
|
75
78
|
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
79
|
for (const slot of slots) {
|
|
77
80
|
const r = place(slot)
|
|
@@ -296,21 +299,34 @@ function transitionXml(kind, rtl) {
|
|
|
296
299
|
return '<p:transition spd="med"><p:fade/></p:transition>'
|
|
297
300
|
}
|
|
298
301
|
|
|
299
|
-
/**
|
|
300
|
-
|
|
302
|
+
/**
|
|
303
|
+
* 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
|
|
304
|
+
* (another site's, unshared). @param {string | string[]} src @param {number} w @param {number} h @param {boolean} [cover] @param {string} [paper]
|
|
305
|
+
*/
|
|
306
|
+
async function pictureOf(src, w, h, cover = false, paper = '') {
|
|
301
307
|
try {
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
308
|
+
const images = await Promise.all([src].flat().map(async (one) => {
|
|
309
|
+
const img = new Image()
|
|
310
|
+
img.crossOrigin = 'anonymous'
|
|
311
|
+
img.src = one
|
|
312
|
+
await img.decode()
|
|
313
|
+
return img
|
|
314
|
+
}))
|
|
315
|
+
const img = images[0]
|
|
306
316
|
const canvas = document.createElement('canvas')
|
|
307
317
|
canvas.width = Math.max(1, Math.round(w))
|
|
308
318
|
canvas.height = Math.max(1, Math.round(h))
|
|
309
319
|
const ctx = /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'))
|
|
310
320
|
const iw = img.naturalWidth || w, ih = img.naturalHeight || h
|
|
311
321
|
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
|
-
|
|
322
|
+
if (!paper) ctx.drawImage(img, (canvas.width - iw * scale) / 2, (canvas.height - ih * scale) / 2, iw * scale, ih * scale)
|
|
323
|
+
else {
|
|
324
|
+
// Art, stretched over the slide as it is drawn there, on its paper, as a JPEG: photographs stay small.
|
|
325
|
+
ctx.fillStyle = paper
|
|
326
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
327
|
+
for (const layer of images) ctx.drawImage(layer, 0, 0, canvas.width, canvas.height)
|
|
328
|
+
}
|
|
329
|
+
return canvas.toDataURL(paper ? 'image/jpeg' : 'image/png', 0.9)
|
|
314
330
|
} catch {
|
|
315
331
|
return null
|
|
316
332
|
}
|
package/src/render.js
CHANGED
|
@@ -43,11 +43,14 @@ export function renderSlide(slide, context) {
|
|
|
43
43
|
const counter = { step: 0 }
|
|
44
44
|
const slots = new Map(slide.slots.map((slot) => [slot.name, slot]))
|
|
45
45
|
const names = definition.slots
|
|
46
|
+
const art = (definition.art ?? []).map((name) => context.kit.pictures?.[name]).filter(Boolean)
|
|
46
47
|
const section = h('section', {
|
|
47
|
-
class: ['deck-slide', `deck-layout-${layout}`, ...classes.map((name) => `deck-class-${name}`)].join(' '),
|
|
48
|
+
class: ['deck-slide', `deck-layout-${layout}`, ...classes.map((name) => `deck-class-${name}`), ...(art.length ? ['has-art'] : [])].join(' '),
|
|
48
49
|
'data-index': slide.index, 'data-layout': layout, 'data-paper': definition.paper, 'data-align': definition.align, 'data-justify': definition.justify, 'data-title': definition.title,
|
|
49
50
|
'aria-roledescription': context.slide, role: 'group',
|
|
50
51
|
})
|
|
52
|
+
// Its art, as its template draws it, layer over layer behind the rest.
|
|
53
|
+
for (const src of art) section.append(h('img', { class: 'deck-art', src, alt: '', 'aria-hidden': 'true' }))
|
|
51
54
|
// Its grid, from the layout's definition, through the CSSOM: areas named s-<slot>, as `default` is no name CSS allows.
|
|
52
55
|
const canvas = h('div', { class: 'deck-canvas' })
|
|
53
56
|
canvas.style.setProperty('grid-template-areas', definition.areas.map((row) => `"${row.map((cell) => (cell === '.' ? '.' : `s-${cell}`)).join(' ')}"`).join(' '))
|