contour-deck 0.3.0 → 0.3.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 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
  ## 看看它怎麼動
@@ -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
  }
@@ -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.1",
4
4
  "description": "Outline-first slide decks on real DOM: blocks, slides, pages.",
5
5
  "keywords": [
6
6
  "slides",