@codename/deck 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,89 @@
1
+ // A .deck.html opened on its own, in any browser: the Markdown its page holds read and drawn as
2
+ // slides; saved back as the same kind of file, the renderer as it came (linked or held whole);
3
+ // saved as PowerPoint with PptxGenJS loaded only then. Libraries come from `data-base` on the
4
+ // renderer's script, else jsDelivr's npm mirror.
5
+ import { createDeck } from './deck.js'
6
+ import { defaultBlocks, libraryAt } from './blocks.js'
7
+ import { readDeckFile, writeDeckFile, unescape } from './file.js'
8
+ import { toPptx } from './pptx.js'
9
+ import css from './deck.css'
10
+
11
+ const me = /** @type {HTMLScriptElement | null} */ (document.currentScript)
12
+ const base = me?.dataset.base ?? 'https://cdn.jsdelivr.net/npm/'
13
+
14
+ const PAGE = `html, body { margin: 0; } body { min-block-size: 100vh; }
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
+ body > .deck > .deck-stage, body > .deck > .deck-bar { inline-size: min(100%, (100vh - 7rem) * 16 / 9); }
17
+ body > .deck.is-editing { justify-items: stretch; align-content: start; }
18
+ body > .deck.is-editing > .deck-stage, body > .deck.is-editing > .deck-bar, body > .deck.is-presenting > .deck-stage, body > .deck.is-presenting > .deck-bar, body > .deck.is-overview > .deck-stage { inline-size: auto; }
19
+ body > .deck:is(.is-presenting, .is-overview) { justify-items: stretch; align-content: start; }`
20
+
21
+ function boot() {
22
+ const holder = document.getElementById('deck')
23
+ if (!holder) return
24
+ const source = unescape(holder.textContent ?? '').replace(/^\n/, '').replace(/\n$/, '')
25
+ const sheet = new CSSStyleSheet()
26
+ sheet.replaceSync(`${css}\n${PAGE}`)
27
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]
28
+ holder.style.display = 'none'
29
+ const root = document.createElement('main')
30
+ document.body.append(root)
31
+ const blocks = defaultBlocks({ base, root })
32
+ /** @type {any} */
33
+ let handle = null
34
+ const view = createDeck(root, {
35
+ source, blocks, lang: document.documentElement.lang || undefined,
36
+ async onSave(text) {
37
+ const settings = view.deck.settings
38
+ const file = writeDeckFile(text, {
39
+ title: String(settings.title ?? view.deck.slides[0]?.title ?? document.title), lang: view.lang, dir: view.dir,
40
+ renderer: me?.src ? { src: me.src } : { code: me?.textContent ?? '' }, generator: document.querySelector('meta[name=generator]')?.getAttribute('content') ?? 'deck',
41
+ })
42
+ const name = decodeURIComponent(location.pathname.split('/').pop() || 'slides.deck.html')
43
+ const picker = /** @type {any} */ (window).showSaveFilePicker
44
+ if (picker) {
45
+ try {
46
+ handle ??= await picker({ suggestedName: name, types: [{ description: 'Deck', accept: { 'text/html': ['.deck.html', '.html'] } }] })
47
+ const writable = await handle.createWritable()
48
+ await writable.write(file)
49
+ await writable.close()
50
+ return
51
+ } catch (error) { if (/** @type {Error} */ (error).name === 'AbortError') return }
52
+ }
53
+ download(new Blob([file], { type: 'text/html' }), name)
54
+ },
55
+ async onExport(deck) {
56
+ const { PptxGenJS, JSZip } = await libraries()
57
+ const { blob } = await toPptx(deck, { PptxGenJS, JSZip })
58
+ download(blob, `${String(deck.deck.settings.title ?? deck.deck.slides[0]?.title ?? 'slides').replace(/[\\/:*?"<>|]+/g, ' ').trim() || 'slides'}.pptx`)
59
+ },
60
+ })
61
+ const title = view.deck.settings.title ?? view.deck.slides[0]?.title
62
+ if (title) document.title = String(title)
63
+ const stage = /** @type {HTMLElement | null} */ (root.querySelector('.deck-stage'))
64
+ stage?.focus({ preventScroll: true })
65
+ Object.assign(window, { deckView: view, deckTools: { toPptx, libraries, readDeckFile, writeDeckFile } })
66
+ }
67
+
68
+ async function libraries() {
69
+ if (!(/** @type {any} */ (window).PptxGenJS)) await new Promise((resolve, reject) => {
70
+ const tag = document.createElement('script')
71
+ tag.src = libraryAt(base, 'pptxgenjs', '4.0.1', 'dist/pptxgen.bundle.js')
72
+ tag.onload = resolve
73
+ tag.onerror = reject
74
+ document.head.append(tag)
75
+ })
76
+ return { PptxGenJS: /** @type {any} */ (window).PptxGenJS, JSZip: /** @type {any} */ (window).JSZip }
77
+ }
78
+
79
+ /** @param {Blob} blob @param {string} name */
80
+ function download(blob, name) {
81
+ const a = Object.assign(document.createElement('a'), { href: URL.createObjectURL(blob), download: name })
82
+ document.body.append(a)
83
+ a.click()
84
+ a.remove()
85
+ setTimeout(() => URL.revokeObjectURL(a.href), 10_000)
86
+ }
87
+
88
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot)
89
+ else boot()
package/src/strings.js ADDED
@@ -0,0 +1,10 @@
1
+ // The deck's own words, for a deck opened on its own; a host gives its own instead (options.strings).
2
+ const STRINGS = /** @type {Record<string, Record<string, string>>} */ ({
3
+ en: { deck: 'Slides', slide: 'slide', carousel: 'carousel', previous: 'Previous', next: 'Next', present: 'Present full screen', presenter: 'Presenter view', overview: 'All slides', edit: 'Change', scheme: 'Light or dark', export: 'Save as PowerPoint', source: 'The deck as text', notes: 'Notes', noNotes: 'No notes for this slide.', upNext: 'Next', last: 'Last slide', where: 'Slide {n} of {count}', saved: 'Saved' },
4
+ fr: { deck: 'Diapositives', slide: 'diapositive', carousel: 'carrousel', previous: 'Précédente', next: 'Suivante', present: 'Présenter en plein écran', presenter: 'Vue présentateur', overview: 'Toutes les diapositives', edit: 'Modifier', scheme: 'Clair ou sombre', export: 'Enregistrer en PowerPoint', source: 'La présentation en texte', notes: 'Notes', noNotes: 'Pas de notes pour cette diapositive.', upNext: 'Ensuite', last: 'Dernière diapositive', where: 'Diapositive {n} sur {count}', saved: 'Enregistré' },
5
+ ar: { deck: 'الشرائح', slide: 'شريحة', carousel: 'عرض شرائح', previous: 'السابقة', next: 'التالية', present: 'العرض بملء الشاشة', presenter: 'عرض المقدّم', overview: 'كل الشرائح', edit: 'تعديل', scheme: 'فاتح أو داكن', export: 'الحفظ بصيغة PowerPoint', source: 'العرض كنص', notes: 'الملاحظات', noNotes: 'لا ملاحظات لهذه الشريحة.', upNext: 'التالية', last: 'الشريحة الأخيرة', where: 'الشريحة {n} من {count}', saved: 'تم الحفظ' },
6
+ ja: { deck: 'スライド', slide: 'スライド', carousel: 'カルーセル', previous: '前へ', next: '次へ', present: '全画面で発表', presenter: '発表者ビュー', overview: 'すべてのスライド', edit: '編集', scheme: 'ライトまたはダーク', export: 'PowerPoint で保存', source: 'テキストとしてのスライド', notes: 'メモ', noNotes: 'このスライドにメモはありません。', upNext: '次', last: '最後のスライド', where: 'スライド {n} / {count}', saved: '保存しました' },
7
+ })
8
+
9
+ /** @param {string} lang */
10
+ export const stringsFor = (lang) => STRINGS[lang.split('-')[0].toLowerCase()] ?? STRINGS.en
package/src/version.js ADDED
@@ -0,0 +1,2 @@
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'
package/src/yaml.js ADDED
@@ -0,0 +1,137 @@
1
+ // Front matter as brains and Slidev write it: the YAML a deck needs, maps by their indent, lists
2
+ // in blocks and in brackets, quoted and plain scalars, block scalars (| and >) and comments. What
3
+ // it can't read stays a string, so a deck never fails to open over its settings. Pure.
4
+
5
+ /** @param {string} text @returns {Record<string, any>} */
6
+ export function parseYaml(text) {
7
+ const lines = text.replace(/\r\n?/g, '\n').split('\n').map((raw) => ({ indent: raw.length - raw.trimStart().length, text: uncomment(raw).trimEnd(), raw }))
8
+ const at = { i: 0 }
9
+ const value = block(lines, at, 0)
10
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
11
+ }
12
+
13
+ /** A line without its comment: a # at its start or after a space, outside quotes. @param {string} line */
14
+ function uncomment(line) {
15
+ let quote = ''
16
+ for (let i = 0; i < line.length; i++) {
17
+ const c = line[i]
18
+ if (quote) { if (c === quote) quote = '' }
19
+ else if (c === '"' || c === "'") quote = c
20
+ else if (c === '#' && (i === 0 || /\s/.test(line[i - 1]))) return line.slice(0, i)
21
+ }
22
+ return line
23
+ }
24
+
25
+ /** @param {{ indent: number, text: string, raw: string }[]} lines @param {{ i: number }} at @param {number} min @returns {any} */
26
+ function block(lines, at, min) {
27
+ while (at.i < lines.length && !lines[at.i].text.trim()) at.i++
28
+ if (at.i >= lines.length || lines[at.i].indent < min) return null
29
+ const indent = lines[at.i].indent
30
+ if (/^-(\s|$)/.test(lines[at.i].text.trim())) {
31
+ const list = []
32
+ while (at.i < lines.length) {
33
+ const line = lines[at.i]
34
+ if (!line.text.trim()) { at.i++; continue }
35
+ if (line.indent !== indent || !/^-(\s|$)/.test(line.text.trim())) break
36
+ const rest = line.text.trim().slice(1).trim()
37
+ at.i++
38
+ if (!rest) list.push(block(lines, at, indent + 1))
39
+ else if (/^[^'"[{][^:]*:(\s|$)/.test(rest)) {
40
+ // A map that starts on the dash's line: its first key there, the rest below it.
41
+ const width = line.raw.indexOf(rest)
42
+ lines.splice(at.i, 0, { indent: width, text: ' '.repeat(width) + rest, raw: ' '.repeat(width) + rest })
43
+ list.push(block(lines, at, width))
44
+ } else list.push(scalar(rest))
45
+ }
46
+ return list
47
+ }
48
+ /** @type {Record<string, any>} */
49
+ const map = {}
50
+ while (at.i < lines.length) {
51
+ const line = lines[at.i]
52
+ if (!line.text.trim()) { at.i++; continue }
53
+ if (line.indent !== indent) break
54
+ const entry = /^("(?:[^"\\]|\\.)*"|'(?:[^']|'')*'|[^:]+?)\s*:(?:\s+(.*))?$/.exec(line.text.trim())
55
+ at.i++
56
+ if (!entry) continue
57
+ const key = String(scalar(entry[1]))
58
+ const rest = (entry[2] ?? '').trim()
59
+ if (rest === '|' || rest === '>' || /^[|>][+-]$/.test(rest)) map[key] = blockScalar(lines, at, indent, rest[0] === '>')
60
+ else if (rest) map[key] = scalar(rest)
61
+ else map[key] = block(lines, at, indent + 1)
62
+ }
63
+ return map
64
+ }
65
+
66
+ /** @param {{ indent: number, text: string, raw: string }[]} lines @param {{ i: number }} at @param {number} indent @param {boolean} folded */
67
+ function blockScalar(lines, at, indent, folded) {
68
+ const taken = []
69
+ while (at.i < lines.length && (!lines[at.i].raw.trim() || lines[at.i].indent > indent)) taken.push(lines[at.i++].raw)
70
+ while (taken.length && !taken.at(-1)?.trim()) taken.pop()
71
+ const width = Math.min(...taken.filter((line) => line.trim()).map((line) => line.length - line.trimStart().length))
72
+ const text = taken.map((line) => line.slice(width)).join('\n')
73
+ return folded ? text.replace(/([^\n])\n(?=[^\n])/g, '$1 ') : text
74
+ }
75
+
76
+ /** A scalar, or a list or map in brackets. @param {string} text @returns {any} */
77
+ function scalar(text) {
78
+ const t = text.trim()
79
+ if (t.startsWith('[') || t.startsWith('{')) {
80
+ const read = flow(t, { i: 0 })
81
+ if (read !== undefined) return read
82
+ }
83
+ if (/^"(?:[^"\\]|\\.)*"$/.test(t)) {
84
+ try { return JSON.parse(t) } catch { return t.slice(1, -1) }
85
+ }
86
+ if (/^'(?:[^']|'')*'$/.test(t)) return t.slice(1, -1).replace(/''/g, "'")
87
+ if (/^(true|yes|on)$/i.test(t)) return true
88
+ if (/^(false|no|off)$/i.test(t)) return false
89
+ if (/^(null|~)$/i.test(t) || t === '') return null
90
+ if (/^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?$/i.test(t)) return Number(t)
91
+ return t
92
+ }
93
+
94
+ /** A list or map in brackets, or undefined when it isn't one. @param {string} text @param {{ i: number }} at @returns {any} */
95
+ function flow(text, at) {
96
+ const space = () => { while (/\s/.test(text[at.i] ?? '')) at.i++ }
97
+ const item = () => {
98
+ space()
99
+ if (text[at.i] === '[' || text[at.i] === '{') return flow(text, at)
100
+ const start = at.i
101
+ if (text[at.i] === '"' || text[at.i] === "'") {
102
+ const q = text[at.i++]
103
+ while (at.i < text.length && !(text[at.i] === q && text[at.i - 1] !== '\\')) at.i++
104
+ at.i++
105
+ } else while (at.i < text.length && !/[,\]}]/.test(text[at.i]) && !(text[at.i] === ':' && /^[\s,\]}]?$/.test(text[at.i + 1] ?? ''))) at.i++
106
+ return scalar(text.slice(start, at.i))
107
+ }
108
+ const open = text[at.i++]
109
+ space()
110
+ if (open === '[') {
111
+ const list = []
112
+ while (at.i < text.length && text[at.i] !== ']') {
113
+ list.push(item())
114
+ space()
115
+ if (text[at.i] === ',') at.i++
116
+ else if (text[at.i] !== ']') return undefined
117
+ space()
118
+ }
119
+ at.i++
120
+ return list
121
+ }
122
+ /** @type {Record<string, any>} */
123
+ const map = {}
124
+ while (at.i < text.length && text[at.i] !== '}') {
125
+ const key = item()
126
+ space()
127
+ if (text[at.i] !== ':') return undefined
128
+ at.i++
129
+ map[String(key)] = item()
130
+ space()
131
+ if (text[at.i] === ',') at.i++
132
+ else if (text[at.i] !== '}') return undefined
133
+ space()
134
+ }
135
+ at.i++
136
+ return map
137
+ }