contour-deck 0.3.0 → 0.3.2

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 CHANGED
@@ -13,7 +13,7 @@ Outline-first slide decks on real DOM.
13
13
  A deck is blocks, slides, and pages: it opens on an outline of every block, a block opens into a left-right track of slides, and each slide scrolls down one page at a time.
14
14
  The core is plain TypeScript with no runtime dependency (7 KB of JavaScript and 3 KB of CSS, gzipped); the phone remote is a separate module that loads on first use.
15
15
 
16
- Status: 0.3.0 on npm. It holds the deck (M1), the M2 client (phone remote, laser pointer, drawing, tilt pointing, and a presenter window, see [Remote](#remote); the relay it talks to is a separate small service, [docs/PROTOCOL.md](docs/PROTOCOL.md)), and, new in 0.3.0, an opt-in structure for the Markdown plugins that reads blocks, slides, and pages from headings ([docs/PLUGINS.md](docs/PLUGINS.md#headings-mode)). Live demo: https://wayne930242.github.io/contour-deck/
16
+ Status: 0.3.1 on npm. It holds the deck (M1), the M2 client (phone remote, laser pointer, drawing, tilt pointing, and a presenter window, see [Remote](#remote); the relay it talks to is a separate small service, [docs/PROTOCOL.md](docs/PROTOCOL.md)), and, since 0.3.0, an opt-in structure for the Markdown plugins that reads blocks, slides, and pages from headings ([docs/PLUGINS.md](docs/PLUGINS.md#headings-mode)); 0.3.1 lets HTML comments stand anywhere in it and titles a heading-less block by its first slide. Live demo: https://wayne930242.github.io/contour-deck/
17
17
  The design is in [docs/DESIGN.md](docs/DESIGN.md) (Traditional Chinese); the HTML the core reads is in [docs/CONTRACT.md](docs/CONTRACT.md).
18
18
 
19
19
  ## 看看它怎麼動
@@ -116,7 +116,7 @@ deck.prev();
116
116
  deck.open("4.2"); // put a block on the stage
117
117
  deck.go("4.4.2"); // or deck.go("slide-id")
118
118
  deck.overview(); // back to the outline
119
- deck.slideBy(1); // the next slide, as the right arrow key; -1 for the one before
119
+ deck.slideBy(1); // the next slide, as the right arrow key once the page's steps are shown; -1 for the one before
120
120
  deck.pageBy(1); // one stop down inside the slide, as the down arrow key
121
121
  deck.state; // { mode, block, slide, page, step }; ordinals are 1-based
122
122
  deck.on("change", ({ mode, at, step }) => {});
@@ -201,7 +201,7 @@ The phone must be able to open the page's URL, so a deck on `localhost` needs a
201
201
  | --- | --- | --- |
202
202
  | `Space`, `Enter`, `PageDown` | advance one step | open the current slide |
203
203
  | `Shift+Space`, `PageUp`, `Backspace` | back one step | |
204
- | `←` `→` | previous, next slide | move between thumbnails |
204
+ | `←` `→` | hide or show the current page's steps first, then the previous or next slide | move between thumbnails |
205
205
  | `↑` `↓` | previous, next stop | move between thumbnails |
206
206
  | `Home`, `End` | first, last slide of the block | |
207
207
  | `N` | notes panel | |
package/dist/core/deck.js CHANGED
@@ -302,6 +302,15 @@ export function createDeck(root, options = {}) {
302
302
  slide.el.scrollBy({ top: below + 32, behavior: smooth() });
303
303
  render();
304
304
  }
305
+ /** The arrow keys: reveal or hide the current page's steps first, then move to the next or previous slide. */
306
+ function stepOrSlide(direction) {
307
+ readScroll();
308
+ const steps = pageAt().steps;
309
+ const pending = direction > 0 ? steps.some(item => !item.hasAttribute(ON)) : steps.some(item => item.hasAttribute(ON));
310
+ if (pending)
311
+ return direction > 0 ? next() : prev();
312
+ slideBy(direction);
313
+ }
305
314
  function prev() {
306
315
  if (busy || mode !== "stage")
307
316
  return;
@@ -509,8 +518,8 @@ export function createDeck(root, options = {}) {
509
518
  PageDown: next,
510
519
  PageUp: prev,
511
520
  Backspace: prev,
512
- ArrowRight: () => slideBy(1),
513
- ArrowLeft: () => slideBy(-1),
521
+ ArrowRight: () => stepOrSlide(1),
522
+ ArrowLeft: () => stepOrSlide(-1),
514
523
  ArrowDown: () => stepV(1),
515
524
  ArrowUp: () => stepV(-1),
516
525
  Escape: toOverview,
@@ -3,7 +3,8 @@ import { type Group } from "./draft.js";
3
3
  import { type HastNode } from "./hast.js";
4
4
  /**
5
5
  * Reads the blocks out of the flow. A block with a title of its own (`::block` title, else its
6
- * heading) derives its label and the 附錄 kind from it unless `::block` gives them; a block
7
- * without one takes the deck's title and derives nothing.
6
+ * heading) derives its label and the 附錄 kind from it unless `::block` gives them. A block
7
+ * without one derives nothing: in a deck of several blocks it takes its first slide's title, and
8
+ * the only block takes the deck's title, else the deck's first `h1`, else its first slide's title.
8
9
  */
9
10
  export declare function readHeadings(flow: HastNode[], deckTitle: string | undefined, file: DeckFile): Group[];
@@ -3,10 +3,11 @@
3
3
  *
4
4
  * - A top-level `---` starts a block.
5
5
  * - A top-level `##` starts a slide. What stands before a block's first `##` is the block's first
6
- * slide (a title, a `# 拆解` divider), unless it is nothing but a `####` kicker, notes, and
7
- * directives: those open the first `##` slide.
6
+ * slide (a title, a `# 拆解` divider), unless it is nothing but a `####` kicker, notes,
7
+ * `::block`/`::slide`, and HTML comments: those open the first `##` slide.
8
8
  * - A top-level `###` starts a further page of the slide, unless it stands right under the `##`
9
- * with no content between them: then both head the first page.
9
+ * with no content between them (notes, `::block`/`::slide`, and HTML comments are not content):
10
+ * then both head the first page.
10
11
  * - A block is titled by the first `#` or `####` before its first `##` or `###`; the heading stays
11
12
  * where it is. `::block{…}` anywhere at the block's top level overrides what it names.
12
13
  * - Headings inside other elements, `####` and deeper, and a `#` after the first `##` are content.
@@ -15,7 +16,11 @@ import { MARK_BLOCK, MARK_SLIDE, MARK_VERTICAL, NOTES } from "../shared/markers.
15
16
  import { NO_VERTICAL } from "../shared/structure.js";
16
17
  import { blockMarkOf, isRule, leadTitle, ORIGIN, splitAt } from "./draft.js";
17
18
  import { isBlank, isElement, markOf, plainText } from "./hast.js";
18
- import { kindOf, shortOf } from "./titles.js";
19
+ import { kindOf, shortOf, slideTitle } from "./titles.js";
20
+ /** An HTML comment: a `comment` node (after rehype-raw), or a `raw` node of nothing but comments. */
21
+ const isComment = (node) => node.type === "comment" || (node.type === "raw" && /^\s*(<!--(?:(?!-->)[\s\S])*-->\s*)+$/.test(node.value ?? ""));
22
+ /** A slide's pages without their speaker notes, which never reach the audience. */
23
+ const visibleOf = (slide) => slide.pages.map(page => page.filter(node => !(isElement(node) && NOTES in node.properties)));
19
24
  function readBlock(nodes, file) {
20
25
  let slide = { pages: [[]] };
21
26
  const slides = [slide];
@@ -32,7 +37,9 @@ function readBlock(nodes, file) {
32
37
  let trailing;
33
38
  for (const node of nodes) {
34
39
  if (!isElement(node)) {
35
- if (!isBlank(node)) {
40
+ if (isComment(node))
41
+ slide.pages[slide.pages.length - 1].push(node);
42
+ else if (!isBlank(node)) {
36
43
  slide.pages[slide.pages.length - 1].push(node);
37
44
  bare = trailing = undefined;
38
45
  body = true;
@@ -94,7 +101,7 @@ function readBlock(nodes, file) {
94
101
  trailing = undefined;
95
102
  }
96
103
  // Only the opening can be left empty: every other slide starts with its `##`.
97
- if (!slides[0].pages[0].length) {
104
+ if (!slides[0].pages[0].some(node => !isComment(node))) {
98
105
  const marker = idMarkers.get(slides[0]);
99
106
  if (marker)
100
107
  file.fail("::slide without any content in its slide", marker, ORIGIN);
@@ -109,8 +116,9 @@ function readBlock(nodes, file) {
109
116
  }
110
117
  /**
111
118
  * Reads the blocks out of the flow. A block with a title of its own (`::block` title, else its
112
- * heading) derives its label and the 附錄 kind from it unless `::block` gives them; a block
113
- * without one takes the deck's title and derives nothing.
119
+ * heading) derives its label and the 附錄 kind from it unless `::block` gives them. A block
120
+ * without one derives nothing: in a deck of several blocks it takes its first slide's title, and
121
+ * the only block takes the deck's title, else the deck's first `h1`, else its first slide's title.
114
122
  */
115
123
  export function readHeadings(flow, deckTitle, file) {
116
124
  const blocks = splitAt(flow, isRule, () => true)
@@ -121,7 +129,10 @@ export function readHeadings(flow, deckTitle, file) {
121
129
  const own = mark.title ?? heading;
122
130
  const spec = own
123
131
  ? { ...mark, title: own, short: mark.short ?? shortOf(own), kind: mark.kind ?? kindOf(own) }
124
- : { ...mark, title: leadTitle(deckTitle, drafts, slides[0]) };
132
+ : {
133
+ ...mark,
134
+ title: (blocks.length > 1 && slideTitle(visibleOf(slides[0]), "")) || leadTitle(deckTitle, drafts, slides[0]),
135
+ };
125
136
  return { spec, slides };
126
137
  });
127
138
  }
@@ -17,7 +17,7 @@
17
17
  * Run it after `contour-deck/remark`, which writes the markers this plugin reads.
18
18
  */
19
19
  import { MARK_BLOCK, MARK_SLIDE, MARK_VERTICAL } from "../shared/markers.js";
20
- import { structureOf } from "../shared/structure.js";
20
+ import { checkStructure, structureOf } from "../shared/structure.js";
21
21
  import { blockMarkOf, hasContent, isRule, leadTitle, ORIGIN, splitAt, } from "./draft.js";
22
22
  import { element, isElement, markOf, plainText, walkElements } from "./hast.js";
23
23
  import { readHeadings } from "./headings.js";
@@ -200,7 +200,9 @@ export function rehypeDeck(options = {}) {
200
200
  else
201
201
  flow.push(node);
202
202
  }
203
- const groups = structureOf(options.structure, file, ORIGIN) === "headings"
203
+ const structure = structureOf(options.structure, file, ORIGIN);
204
+ checkStructure(file, structure, ORIGIN);
205
+ const groups = structure === "headings"
204
206
  ? readHeadings(flow, deckTitle, file)
205
207
  : groupBlocks(readSlides(flow, file), deckTitle);
206
208
  rejectNestedMarkers(groups.flatMap(group => group.slides), file);
@@ -17,7 +17,7 @@
17
17
  * and `::block` needs no `title`: the block has its heading.
18
18
  */
19
19
  import { MARK, MARK_BLOCK, MARK_BUILD, MARK_SLIDE, MARK_VERTICAL, NOTES } from "../shared/markers.js";
20
- import { NO_VERTICAL, structureOf } from "../shared/structure.js";
20
+ import { NO_VERTICAL, recordStructure, structureOf } from "../shared/structure.js";
21
21
  const ORIGIN = "contour-deck:remark";
22
22
  const DIRECTIVES = {
23
23
  summary: {
@@ -127,6 +127,8 @@ export function remarkDeck(options = {}) {
127
127
  return (tree, file) => {
128
128
  if (options.match && !options.match(file))
129
129
  return;
130
- visit(tree, file, structureOf(options.structure, file, ORIGIN));
130
+ const structure = structureOf(options.structure, file, ORIGIN);
131
+ recordStructure(file, structure);
132
+ visit(tree, file, structure);
131
133
  };
132
134
  }
@@ -24,6 +24,16 @@ const KEYS = {
24
24
  Escape: "overview",
25
25
  o: "overview",
26
26
  };
27
+ /** The arrow keys reveal or hide the current page's steps before they move to another slide, as on the deck. */
28
+ function arrowStep(key, state) {
29
+ if (!state || state.mode !== "stage")
30
+ return undefined;
31
+ if (key === "ArrowRight" && state.step < state.steps)
32
+ return "next";
33
+ if (key === "ArrowLeft" && state.step > 0)
34
+ return "prev";
35
+ return undefined;
36
+ }
27
37
  export function presenterView(root, options = {}) {
28
38
  injectStyle(options.nonce);
29
39
  const labels = { ...defaultRemoteLabels, ...options.labels };
@@ -94,7 +104,7 @@ export function presenterView(root, options = {}) {
94
104
  const onButton = !!event.target.closest?.("button,a");
95
105
  if (onButton && (key === " " || key === "Enter"))
96
106
  return;
97
- const command = key === " " && event.shiftKey ? "prev" : KEYS[key];
107
+ const command = key === " " && event.shiftKey ? "prev" : arrowStep(key, guest.state) ?? KEYS[key];
98
108
  if (!command)
99
109
  return;
100
110
  event.preventDefault();
@@ -21,3 +21,6 @@ export declare const NO_VERTICAL = "::vertical has no meaning with structure: \"
21
21
  * names is an error here, not a silent fallback to the default.
22
22
  */
23
23
  export declare function structureOf(option: StructureOption | undefined, file: DeckFile, origin: string): Structure;
24
+ export declare function recordStructure(file: DeckFile, structure: Structure): void;
25
+ /** Fails when `remarkDeck` read the file by another structure than `rehypeDeck` is about to. */
26
+ export declare function checkStructure(file: DeckFile, structure: Structure, origin: string): void;
@@ -20,3 +20,18 @@ export function structureOf(option, file, origin) {
20
20
  return value;
21
21
  return file.fail(`structure must be "slides" or "headings", got ${JSON.stringify(value)}`, undefined, origin);
22
22
  }
23
+ /** Where `remarkDeck` leaves the structure it read a file by, for `rehypeDeck` to compare. */
24
+ const RECORD = "contourDeckStructure";
25
+ export function recordStructure(file, structure) {
26
+ file.data[RECORD] = structure;
27
+ }
28
+ /** Fails when `remarkDeck` read the file by another structure than `rehypeDeck` is about to. */
29
+ export function checkStructure(file, structure, origin) {
30
+ const data = file.data;
31
+ const recorded = data[RECORD];
32
+ // Read once: a later pass on the same file without remarkDeck must not see a stale record.
33
+ delete data[RECORD];
34
+ if (recorded !== undefined && recorded !== structure) {
35
+ file.fail(`remarkDeck ran with structure "${String(recorded)}" and rehypeDeck with "${structure}": give both the same structure option`, undefined, origin);
36
+ }
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contour-deck",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Outline-first slide decks on real DOM: blocks, slides, pages.",
5
5
  "keywords": [
6
6
  "slides",