pindeck 3.0.0-beta.2 → 3.1.0-beta.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/README.md +94 -16
- package/front.mjs +318 -0
- package/lib/deck-list.mjs +49 -0
- package/lib/deck-target.mjs +25 -0
- package/lib/eids.mjs +101 -8
- package/lib/frameworks.mjs +26 -8
- package/lib/login.mjs +61 -0
- package/lib/usage.mjs +15 -0
- package/package.json +4 -3
- package/public/bridge.js +248 -69
- package/public/editor.css +24 -3
- package/public/editor.js +78 -21
- package/public/front.css +14 -0
- package/public/index.html +9 -1
- package/server.mjs +116 -101
package/lib/eids.mjs
CHANGED
|
@@ -145,8 +145,97 @@ function find(node, pred) {
|
|
|
145
145
|
return hit;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
/*
|
|
148
|
+
/* ---- Decks made without reveal.js ----------------------------------------------------------------
|
|
149
|
+
A deck names its slides with <meta name="pindeck:slides" content="main.deck > section.slide">, a CSS
|
|
150
|
+
selector, and its size with <meta name="pindeck:size" content="1440x1080">. Selectors stay simple, so
|
|
151
|
+
the server and the browser find the same slides: tag names, #id, .class, [attr] and [attr="value"],
|
|
152
|
+
joined by spaces (descendant) or ">" (child), in a comma-separated list. */
|
|
153
|
+
|
|
154
|
+
const metaOf = (tree, name) => {
|
|
155
|
+
const m = find(tree, (c) => c.name === "meta" && c.attrs.name?.value.trim().toLowerCase() === `pindeck:${name}`);
|
|
156
|
+
return m ? decode(m.attrs.content?.value || "").trim() || null : null;
|
|
157
|
+
};
|
|
158
|
+
/* The deck's slide selector, or null for a reveal.js deck */
|
|
159
|
+
export const slideSelector = (tree) => metaOf(tree, "slides");
|
|
160
|
+
/* The deck's slide size from <meta name="pindeck:size">: { width, height } or null */
|
|
161
|
+
export function deckSize(tree) {
|
|
162
|
+
const m = /^(\d{2,5})\s*[x×*,\s]\s*(\d{2,5})$/i.exec(metaOf(tree, "size") || "");
|
|
163
|
+
return m ? { width: +m[1], height: +m[2] } : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const IDENT = "-?[_a-zA-Z\\u00a0-\\uffff][\\w\\u00a0-\\uffff-]*";
|
|
167
|
+
const COMPOUND = new RegExp(`^(?:(\\*|${IDENT})|#(${IDENT})|\\.(${IDENT})|\\[\\s*(${IDENT})\\s*(?:=\\s*(?:"([^"]*)"|'([^']*)'|(${IDENT}))\\s*)?\\])`);
|
|
168
|
+
/* "main.deck > section.slide, aside.extra" -> one list of compounds and combinators per selector.
|
|
169
|
+
Throws with the part it cannot read (pseudo-classes, +, ~ ...). */
|
|
170
|
+
export function parseSelector(sel) {
|
|
171
|
+
const parts = []; // top-level commas only: not inside [attr="a,b"]
|
|
172
|
+
let quote = null, depth = 0, from = 0;
|
|
173
|
+
for (let i = 0; i < sel.length; i++) {
|
|
174
|
+
const c = sel[i];
|
|
175
|
+
if (quote) { if (c === quote) quote = null; }
|
|
176
|
+
else if (c === '"' || c === "'") quote = c;
|
|
177
|
+
else if (c === "[") depth++;
|
|
178
|
+
else if (c === "]") depth--;
|
|
179
|
+
else if (c === "," && !depth) { parts.push(sel.slice(from, i)); from = i + 1; }
|
|
180
|
+
}
|
|
181
|
+
parts.push(sel.slice(from));
|
|
182
|
+
return parts.map((part) => {
|
|
183
|
+
const steps = []; // [{ tag, ids, classes, attrs }, ">" | " ", { ... }, ...]
|
|
184
|
+
let s = part.trim(), cur = null;
|
|
185
|
+
if (!s) throw new Error(`empty selector in "${sel}"`);
|
|
186
|
+
while (s) {
|
|
187
|
+
const ws = /^\s+/.exec(s);
|
|
188
|
+
const gt = /^\s*>\s*/.exec(s);
|
|
189
|
+
if (gt) { if (!cur) throw new Error(`"${part.trim()}" starts with ">"`); steps.push(">"); cur = null; s = s.slice(gt[0].length); continue; }
|
|
190
|
+
if (ws) { steps.push(" "); cur = null; s = s.slice(ws[0].length); continue; }
|
|
191
|
+
const m = COMPOUND.exec(s);
|
|
192
|
+
if (!m) throw new Error(`"${s.slice(0, 20)}" in "${part.trim()}" is not supported (use tags, #id, .class, [attr], spaces and ">")`);
|
|
193
|
+
if (!cur) { cur = { tag: null, ids: [], classes: [], attrs: [] }; steps.push(cur); }
|
|
194
|
+
if (m[1]) {
|
|
195
|
+
if (cur.tag || cur.ids.length || cur.classes.length || cur.attrs.length) throw new Error(`a tag name must come first in "${part.trim()}"`);
|
|
196
|
+
cur.tag = m[1] === "*" ? null : m[1].toLowerCase();
|
|
197
|
+
}
|
|
198
|
+
else if (m[2]) cur.ids.push(m[2]);
|
|
199
|
+
else if (m[3]) cur.classes.push(m[3]);
|
|
200
|
+
else cur.attrs.push({ name: m[4].toLowerCase(), value: m[5] ?? m[6] ?? m[7] ?? null });
|
|
201
|
+
s = s.slice(m[0].length);
|
|
202
|
+
}
|
|
203
|
+
if (typeof steps.at(-1) === "string") throw new Error(`"${part.trim()}" ends with a combinator`);
|
|
204
|
+
return steps;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function compoundMatches(node, c) {
|
|
208
|
+
if (!node.attrs) return false; // the root
|
|
209
|
+
if (c.tag && node.name !== c.tag) return false;
|
|
210
|
+
if (c.ids.some((id) => node.attrs.id?.value !== id)) return false;
|
|
211
|
+
const cls = classesOf(node);
|
|
212
|
+
if (c.classes.some((k) => !cls.includes(k))) return false;
|
|
213
|
+
return c.attrs.every((a) => node.attrs[a.name] && (a.value == null || node.attrs[a.name].value === a.value));
|
|
214
|
+
}
|
|
215
|
+
/* steps[i] matches node, and the steps before it match its ancestors as their combinators say */
|
|
216
|
+
function stepsMatch(node, steps, i = steps.length - 1) {
|
|
217
|
+
if (!compoundMatches(node, steps[i])) return false;
|
|
218
|
+
if (i === 0) return true;
|
|
219
|
+
if (steps[i - 1] === ">") return !!node.parent && stepsMatch(node.parent, steps, i - 2);
|
|
220
|
+
for (let a = node.parent; a; a = a.parent) if (stepsMatch(a, steps, i - 2)) return true;
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
/* Elements matching the selector, in document order; an element holding another match is a stack of
|
|
224
|
+
slides, not a slide (as with reveal.js's vertical stacks) */
|
|
225
|
+
function selectorSlides(tree, sel) {
|
|
226
|
+
let list;
|
|
227
|
+
try { list = parseSelector(sel); } catch { return []; }
|
|
228
|
+
const hits = [];
|
|
229
|
+
walk(tree, (c) => { if (list.some((steps) => stepsMatch(c, steps))) hits.push(c); });
|
|
230
|
+
const inside = (a, b) => { for (let p = b.parent; p; p = p.parent) if (p === a) return true; return false; };
|
|
231
|
+
return hits.filter((h) => !hits.some((o) => o !== h && inside(h, o)));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* Slides are leaf <section>s inside .slides; vertical-stack containers are not slides. A deck that names
|
|
235
|
+
its slides with <meta name="pindeck:slides"> has the elements that selector matches instead. */
|
|
149
236
|
export function findSlides(tree) {
|
|
237
|
+
const sel = slideSelector(tree);
|
|
238
|
+
if (sel) return selectorSlides(tree, sel);
|
|
150
239
|
const slidesEl = find(tree, (c) => classesOf(c).includes("slides"));
|
|
151
240
|
if (!slidesEl) return [];
|
|
152
241
|
const out = [];
|
|
@@ -187,6 +276,7 @@ function elementName(node) {
|
|
|
187
276
|
export function assignEids(html) {
|
|
188
277
|
const tree = parse(html);
|
|
189
278
|
const sections = findSlides(tree);
|
|
279
|
+
const isSlide = slideSelector(tree) ? ((c) => sections.includes(c)) : ((c) => c.name === "section");
|
|
190
280
|
const used = new Set();
|
|
191
281
|
const edits = []; // { at, insert } or { start, end, replace }
|
|
192
282
|
const assigned = [], renamed = [];
|
|
@@ -240,7 +330,7 @@ export function assignEids(html) {
|
|
|
240
330
|
// Pass 2: elements inside the slide
|
|
241
331
|
const visit = (node, inSvg) => {
|
|
242
332
|
for (const c of node.children) {
|
|
243
|
-
if (c
|
|
333
|
+
if (isSlide(c)) continue; // nested slide, handled on its own
|
|
244
334
|
const cls = classesOf(c);
|
|
245
335
|
if (c.name === "aside" && cls.includes("notes")) continue;
|
|
246
336
|
if (c.attrs["data-noeid"]) continue;
|
|
@@ -423,10 +513,11 @@ const slideKind = (n) => classList(n).find((c) => /-slide$/.test(c)) || "content
|
|
|
423
513
|
content title and two placeholder bullets, from a content slide (the current one if it is one)
|
|
424
514
|
section a section divider, from the deck's first divider
|
|
425
515
|
duplicate a copy of the current slide
|
|
426
|
-
Element ids are left out; assignEids() gives the new slide fresh ones.
|
|
516
|
+
Element ids are left out; assignEids() gives the new slide fresh ones. In a deck made without reveal.js,
|
|
517
|
+
the slide's own id is left out too: such decks (and Present) go to slides by their id.
|
|
427
518
|
Returns { html, index } (index of the new slide) or { error }. */
|
|
428
519
|
export function insertSlide(html, kind, afterEid) {
|
|
429
|
-
const
|
|
520
|
+
const tree = parse(html), slides = findSlides(tree);
|
|
430
521
|
if (!slides.length) return { error: "The deck has no slides." };
|
|
431
522
|
const after = slides.find((s) => s.attrs["data-eid"]?.value === afterEid) || slides.at(-1);
|
|
432
523
|
const hasHead = (s) => { let h = false; walk({ children: [s] }, (c) => { if (/^h[1-3]$/.test(c.name)) h = true; }); return h; };
|
|
@@ -437,6 +528,7 @@ export function insertSlide(html, kind, afterEid) {
|
|
|
437
528
|
if (!tpl) return { error: kind === "section" ? "This deck has no section divider to model a new one on." : "This deck has no content slide to model a new one on." };
|
|
438
529
|
|
|
439
530
|
let frag = html.slice(tpl.start, elementEnd(html, tpl)).replace(/\s+data-eid="[^"]*"/g, "");
|
|
531
|
+
if (slideSelector(tree)) frag = frag.replace(/^(<[^>]*?)\s+id=(?:"[^"]*"|'[^']*'|[^\s>]+)/, "$1"); // the opening tag only
|
|
440
532
|
if (kind !== "duplicate") frag = placeholderSlide(frag, kind, slides.filter((s, i) => i <= slides.indexOf(after) && slideKind(s) === "section-slide").length + 1);
|
|
441
533
|
|
|
442
534
|
const indent = lineIndent(html, after.start);
|
|
@@ -500,12 +592,13 @@ function slideBlock(html, s) {
|
|
|
500
592
|
are. Slides inside vertical stacks are not moved. Returns { html, changed, from, index } or { error }. */
|
|
501
593
|
export function moveSlide(html, eid, beforeEid) {
|
|
502
594
|
const byEid = (all, e) => all.find((s) => s.attrs["data-eid"]?.value === e);
|
|
503
|
-
const
|
|
595
|
+
const tree = parse(html), slides = findSlides(tree);
|
|
596
|
+
const bySelector = !!slideSelector(tree); // such decks have no vertical stacks
|
|
504
597
|
const slide = byEid(slides, eid), target = beforeEid == null ? null : byEid(slides, beforeEid);
|
|
505
598
|
if (!slide || (beforeEid != null && !target)) return { error: "Slide not found in the file." };
|
|
506
|
-
if ([slide, target].some((s) => s?.parent.name === "section")) return { error: "Slides inside a vertical stack cannot be moved yet." };
|
|
599
|
+
if (!bySelector && [slide, target].some((s) => s?.parent.name === "section")) return { error: "Slides inside a vertical stack cannot be moved yet." };
|
|
507
600
|
const from = slides.indexOf(slide);
|
|
508
|
-
const top = slide.parent.children.filter((c) => c.name === "section");
|
|
601
|
+
const top = slide.parent.children.filter((c) => (bySelector ? slides.includes(c) : c.name === "section"));
|
|
509
602
|
const next = top[top.indexOf(slide) + 1] || null;
|
|
510
603
|
if (target === slide || target === next) return { html, changed: false, from, index: from };
|
|
511
604
|
|
|
@@ -520,7 +613,7 @@ export function moveSlide(html, eid, beforeEid) {
|
|
|
520
613
|
out = cut.slice(0, at) + text + `\n\n${indent}` + cut.slice(at);
|
|
521
614
|
} else {
|
|
522
615
|
// A final vertical stack is absent from the leaf-slide list, but we must insert after it.
|
|
523
|
-
const last = find(parse(cut), (c) => classesOf(c).includes("slides"))
|
|
616
|
+
const last = bySelector ? rest.at(-1) : find(parse(cut), (c) => classesOf(c).includes("slides"))
|
|
524
617
|
.children.filter((c) => c.name === "section").at(-1);
|
|
525
618
|
const at = elementEnd(cut, last);
|
|
526
619
|
out = cut.slice(0, at) + `\n\n${indent}` + text + cut.slice(at);
|
package/lib/frameworks.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/* Which slide framework a deck uses. PinDeck edits reveal.js decks: <section> slides inside an element
|
|
2
|
-
with class "slides" (inside <div class="reveal">).
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
with class "slides" (inside <div class="reveal">). Any other deck can name its slides itself with
|
|
3
|
+
<meta name="pindeck:slides" content="CSS selector"> (see lib/eids.mjs) and is then edited too. Other
|
|
4
|
+
frameworks are recognised by their markup and scripts, so the user gets a clear "not supported (yet)"
|
|
5
|
+
before PinDeck changes anything, instead of an editor without slides. Only tags and scripts are looked
|
|
6
|
+
at, never the slides' text, so a talk that mentions "impress.js" is not taken for one. */
|
|
6
7
|
|
|
7
|
-
import { parse, findSlides } from "./eids.mjs";
|
|
8
|
+
import { parse, findSlides, slideSelector, deckSize, parseSelector } from "./eids.mjs";
|
|
8
9
|
|
|
9
10
|
const OTHERS = [ // name, markers; the first match wins (Marp before bespoke.js, which it is built on)
|
|
10
11
|
["Marp", /\bmarpit\b|bespoke-marp|data-marpit|marp-cli/i],
|
|
@@ -20,9 +21,13 @@ const OTHERS = [ // name, markers; the first match wins (Marp before bespoke.j
|
|
|
20
21
|
/* Tags and script contents only: the slides' text could name any framework */
|
|
21
22
|
const markup = (html) => [...String(html).matchAll(/<script\b[^>]*>[\s\S]*?<\/script>|<[^>]+>/gi)].map((m) => m[0]).join("\n");
|
|
22
23
|
|
|
23
|
-
/* { name, supported }: reveal.js decks are supported, including one without slides yet
|
|
24
|
+
/* { name, supported }: reveal.js decks are supported, including one without slides yet, and so is a deck
|
|
25
|
+
whose <meta name="pindeck:slides"> selector finds slides and whose <meta name="pindeck:size"> gives their
|
|
26
|
+
size ({ name: "selector", selector, supported }) */
|
|
24
27
|
export function detectFramework(html) {
|
|
25
|
-
|
|
28
|
+
const tree = parse(html), selector = slideSelector(tree);
|
|
29
|
+
if (selector) return { name: "selector", selector, supported: findSlides(tree).length > 0 && !!deckSize(tree) };
|
|
30
|
+
if (findSlides(tree).length) return { name: "reveal.js", supported: true };
|
|
26
31
|
const tags = markup(html);
|
|
27
32
|
const other = OTHERS.find(([, re]) => re.test(tags));
|
|
28
33
|
if (other) return { name: other[0], supported: false };
|
|
@@ -30,12 +35,25 @@ export function detectFramework(html) {
|
|
|
30
35
|
return { name: null, supported: false };
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
const HOW = `A deck made otherwise can name its slides in its <head>: ` +
|
|
39
|
+
`<meta name="pindeck:slides" content="main.deck > section.slide"> (a CSS selector for the slides) and ` +
|
|
40
|
+
`<meta name="pindeck:size" content="1440x1080"> (their size in px).`;
|
|
41
|
+
|
|
33
42
|
/* The message for a deck PinDeck cannot edit, or null */
|
|
34
43
|
export function unsupportedMessage(html, file) {
|
|
35
44
|
const fw = detectFramework(html);
|
|
36
45
|
if (fw.supported) return null;
|
|
46
|
+
if (fw.name === "selector") {
|
|
47
|
+
// the slides are found: the size is what is missing (headless Chrome and the PDF pages need it)
|
|
48
|
+
if (findSlides(parse(html)).length) return `${file}: <meta name="pindeck:size"> is missing or not a size ` +
|
|
49
|
+
`such as content="1440x1080" (the slides' width and height in px). PinDeck opens the slides at that size ` +
|
|
50
|
+
`for screenshots and PDFs. Nothing was changed.`;
|
|
51
|
+
let why = "matches no elements";
|
|
52
|
+
try { parseSelector(fw.selector); } catch (e) { why = `cannot be used: ${e.message}`; }
|
|
53
|
+
return `${file}: the slide selector "${fw.selector}" in <meta name="pindeck:slides"> ${why}. Nothing was changed.`;
|
|
54
|
+
}
|
|
37
55
|
const what = fw.name ? `${file} is a ${fw.name} deck.` :
|
|
38
56
|
`${file} has no reveal.js slides (a <div class="reveal"> with <div class="slides"> holding <section> slides).`;
|
|
39
|
-
return `${what} PinDeck works with reveal.js decks
|
|
57
|
+
return `${what} PinDeck works with reveal.js decks; other frameworks may follow. ${HOW} ` +
|
|
40
58
|
`Nothing was changed.`;
|
|
41
59
|
}
|
package/lib/login.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/* Who may read the editor, the deck and the API: shared by the front (pindeck) and each deck server.
|
|
2
|
+
|
|
3
|
+
The token never appears in a page or on a command line, where other users of a shared server could read
|
|
4
|
+
it (ps, curl). A browser logs in once with a one-time code (printed at start, or by `pindeck login`) and
|
|
5
|
+
then holds the token in an HttpOnly cookie; the CLI, Claude and the front send it in a header. Browsers
|
|
6
|
+
share cookies across all ports of a host, so the cookie name carries the server's port. */
|
|
7
|
+
|
|
8
|
+
import crypto from "node:crypto";
|
|
9
|
+
|
|
10
|
+
/* Pages opened as 127.0.0.1 or localhost only, on any port: an SSH tunnel may use another local port.
|
|
11
|
+
Another site's name pointed at 127.0.0.1 (DNS rebinding) must not read the editor page, its token or the deck. */
|
|
12
|
+
export const goodHost = (h) => /^(127\.0\.0\.1|localhost)(:\d{1,5})?$/.test(h);
|
|
13
|
+
|
|
14
|
+
export const LOGIN_PAGE = `<!doctype html><meta charset="utf-8"><title>PinDeck</title>
|
|
15
|
+
<body style="font:16px system-ui,sans-serif;max-width:36em;margin:4em auto;padding:0 1em">
|
|
16
|
+
<h1>Log in to PinDeck</h1><p>Open the login link that PinDeck printed when it started. Links work once;
|
|
17
|
+
for a new one, run <code>pindeck login</code> in the deck folder (or <code>node front.mjs login</code> in a
|
|
18
|
+
PinDeck checkout).</p>`;
|
|
19
|
+
|
|
20
|
+
export function cookie(req, name) {
|
|
21
|
+
for (const part of String(req.headers.cookie || "").split(";")) {
|
|
22
|
+
const i = part.indexOf("=");
|
|
23
|
+
if (i > 0 && part.slice(0, i).trim() === name) return part.slice(i + 1).trim();
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* port() and origin() are read on use: --port 0 learns the port only once listening */
|
|
29
|
+
export function createLogin({ token, port, origin }) {
|
|
30
|
+
const loginCodes = new Map(); // code -> expiry (ms)
|
|
31
|
+
function loginCode(ttl) {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
for (const [c, exp] of loginCodes) if (exp < now) loginCodes.delete(c);
|
|
34
|
+
const code = crypto.randomBytes(16).toString("hex");
|
|
35
|
+
loginCodes.set(code, now + ttl);
|
|
36
|
+
return code;
|
|
37
|
+
}
|
|
38
|
+
const loginUrl = (code, next = "/") => `${origin()}/login?` + new URLSearchParams({ code, ...(next !== "/" ? { next } : {}) });
|
|
39
|
+
const cookieName = () => `pindeck-${port()}`;
|
|
40
|
+
const sameSecret = (a) => typeof a === "string" && a.length === token.length &&
|
|
41
|
+
crypto.timingSafeEqual(Buffer.from(a), Buffer.from(token));
|
|
42
|
+
const authed = (req) => sameSecret(req.headers["x-editor-token"]) || sameSecret(cookie(req, cookieName()));
|
|
43
|
+
/* A one-time code: set the cookie, then go on with a same-origin navigation (a redirect from a cross-site
|
|
44
|
+
start, such as a file:// page or a link in a terminal, would not carry a SameSite=Strict cookie). */
|
|
45
|
+
function login(res, url) {
|
|
46
|
+
const code = url.searchParams.get("code") || "";
|
|
47
|
+
const exp = loginCodes.get(code);
|
|
48
|
+
loginCodes.delete(code);
|
|
49
|
+
if (!exp || exp < Date.now()) {
|
|
50
|
+
res.writeHead(403, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
51
|
+
return res.end(LOGIN_PAGE.replace("<p>", "<p>This login link has expired or was used already. "));
|
|
52
|
+
}
|
|
53
|
+
let next = url.searchParams.get("next") || "/";
|
|
54
|
+
if (!next.startsWith("/") || next.startsWith("//") || next.includes("\\")) next = "/";
|
|
55
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store",
|
|
56
|
+
"set-cookie": `${cookieName()}=${token}; HttpOnly; SameSite=Strict; Path=/` });
|
|
57
|
+
const to = JSON.stringify(next).replace(/</g, "\\u003c");
|
|
58
|
+
res.end(`<!doctype html><meta charset="utf-8"><title>PinDeck</title><script>location.replace(${to})</script>`);
|
|
59
|
+
}
|
|
60
|
+
return { loginCode, loginUrl, cookieName, authed, login };
|
|
61
|
+
}
|
package/lib/usage.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* pindeck --help, and the hint after a bad argument: the front (front.mjs) and a deck server (server.mjs)
|
|
2
|
+
take the same arguments. */
|
|
3
|
+
export const USAGE = `Usage: pindeck [deck-folder | deck-file.html] [--port 5180] [--provider claude|codex] [--model <name>] [--asset-root <folder>]...
|
|
4
|
+
pindeck login [deck-folder | deck-file.html]
|
|
5
|
+
|
|
6
|
+
Opens a reveal.js deck in PinDeck: a deck file such as talk.html, or a folder (default: the current
|
|
7
|
+
folder), which then opens its index.html; a folder without one opens a list of its decks. A deck made
|
|
8
|
+
otherwise names its slides in its <head> with <meta name="pindeck:slides" content="<CSS selector>"> and
|
|
9
|
+
<meta name="pindeck:size" content="1440x1080">.
|
|
10
|
+
In the editor, the deck's name opens the other decks in the deck's folder and the asset roots (and one
|
|
11
|
+
folder down), each with its own chat and undo, on the same port and login.
|
|
12
|
+
login prints a new one-time login link for the running PinDeck (with a deck: one that opens that deck).
|
|
13
|
+
--asset-root lets the deck use files outside its folder: a folder that contains the deck (paths such as
|
|
14
|
+
../shared/style.css then work), or one that symlinks in the deck may point to. Repeatable; also
|
|
15
|
+
"assetRoots" in ~/.pindeck-v3/config.json.`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pindeck",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0-beta.1",
|
|
4
4
|
"description": "PinDeck: an editor for HTML slide decks on macOS, and on Linux (beta) through an SSH tunnel. Pin elements, describe the change, and Claude Code or OpenAI Codex edits the slides.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"slides",
|
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
"bugs": "https://github.com/ehabets/PinDeck/issues",
|
|
24
24
|
"type": "module",
|
|
25
25
|
"bin": {
|
|
26
|
-
"pindeck": "
|
|
26
|
+
"pindeck": "front.mjs"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
|
+
"front.mjs",
|
|
29
30
|
"server.mjs",
|
|
30
31
|
"cli.mjs",
|
|
31
32
|
"lib/",
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
"LICENSE"
|
|
35
36
|
],
|
|
36
37
|
"scripts": {
|
|
37
|
-
"start": "node
|
|
38
|
+
"start": "node front.mjs",
|
|
38
39
|
"test": "node --test"
|
|
39
40
|
},
|
|
40
41
|
"os": [
|