@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.
- package/LICENSE +21 -0
- package/README.md +96 -2
- package/dist/deck.bundle.mjs +114 -0
- package/dist/deck.bundle.mjs.map +7 -0
- package/dist/deck.css +164 -0
- package/dist/deck.js +275 -0
- package/dist/deck.js.map +7 -0
- package/dist/deck.mjs +50 -0
- package/dist/deck.mjs.map +7 -0
- package/dist/types/blocks.d.ts +25 -0
- package/dist/types/deck.d.ts +42 -0
- package/dist/types/file.d.ts +38 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/kit.d.ts +73 -0
- package/dist/types/look.d.ts +55 -0
- package/dist/types/parse.d.ts +20 -0
- package/dist/types/pictures.d.ts +19 -0
- package/dist/types/pptx.d.ts +18 -0
- package/dist/types/render.d.ts +34 -0
- package/dist/types/repair.d.ts +5 -0
- package/dist/types/strings.d.ts +2 -0
- package/dist/types/version.d.ts +1 -0
- package/dist/types/yaml.d.ts +2 -0
- package/docs/findings.md +57 -0
- package/docs/format.md +70 -0
- package/docs/host.md +50 -0
- package/docs/kits.md +76 -0
- package/package.json +60 -4
- package/src/blocks.js +99 -0
- package/src/css.d.ts +5 -0
- package/src/deck.css +164 -0
- package/src/deck.js +359 -0
- package/src/file.js +74 -0
- package/src/index.js +12 -0
- package/src/kit.js +185 -0
- package/src/look.js +124 -0
- package/src/parse.js +180 -0
- package/src/pictures.js +47 -0
- package/src/pptx.js +310 -0
- package/src/render.js +241 -0
- package/src/repair.js +51 -0
- package/src/standalone.js +89 -0
- package/src/strings.js +10 -0
- package/src/version.js +2 -0
- package/src/yaml.js +137 -0
|
@@ -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
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
|
+
}
|