gp-dimm-city 1.2.0-alpha.2 → 1.2.0-alpha.3

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/CHANGELOG.md CHANGED
@@ -9,8 +9,10 @@ cut a version that has no `## [X.Y.Z]` heading below.
9
9
 
10
10
  ## [1.2.0] - prerelease
11
11
 
12
- Requires Gutterpress 0.11.16-alpha.2 or newer (the desktop app or CLI at
13
- that version): older versions reject the `section: true` markers below.
12
+ Requires Gutterpress 0.11.16-alpha.5 or newer (the desktop app or CLI at
13
+ that version): older versions reject the `section: true` markers below, and
14
+ alpha.5 adds the `unknown_variant` warning that reports a mistyped `@callout`
15
+ variant.
14
16
  Books pinned to an earlier gp-dimm-city are unaffected until re-pinned.
15
17
 
16
18
  ### Changed
@@ -94,12 +96,49 @@ Books pinned to an earlier gp-dimm-city are unaffected until re-pinned.
94
96
  cause is an `@end-learning-path` above the skills it was meant to close.
95
97
  - Snippets: `specialty` shows the whole specialty > learning path > skill
96
98
  hierarchy, and `learning-path` shows its skills.
97
- - devDependency `gutterpress` is `0.11.16-alpha.2`.
99
+ - **`@callout` and `@dm-note` are declared, and the state machine is gone.**
100
+ `@callout` is a declared wrapper (`div.dc-alert` + the variant's class,
101
+ author classes and `label="…"` as before) and `@dm-note` is its alias with
102
+ the `dm` variant preset, so `@dm-note` is `@callout dm` and `@end-dm-note`
103
+ still closes it. One small core rule (`dcCallouts`) adds the variant's
104
+ default label ("Note", "Warning", "Dream Master Note", "Vibe", "Origin",
105
+ "Visit", "Gear") when there is no `label="…"`; an explicit label wins. A
106
+ missing or unrecognised variant is a plain note, as before. With `@tape`
107
+ and the retired `@roll-table` / `@options-table` stripper gone too,
108
+ `dimm_city_transform` and its `closeAll()` are deleted. What is left are
109
+ four small core rules: the `@continue` bridge into skill cards, `> [!TYPE]`
110
+ alerts, `ROLL THE DIE!`, and the image-only paragraph class (now
111
+ `dcImageParagraphs`). `plugin.js` goes from 1,557 to 1,381 lines.
112
+ The emitted callout is the same element, classes and label span as before.
113
+ Core adds `data-callout="<variant>"` (and `data-label` when labelled) and
114
+ `data-source-range`, and writes no newline between the wrapper and its label.
115
+ - `docs/adding-macros.md` and `docs/macros.md` now describe a plugin with no
116
+ hand-written markers: the "hand-written marker" recipe is gone.
117
+ - devDependency `gutterpress` is `0.11.16-alpha.5` (was `0.11.16-alpha.2`).
98
118
  - `docs/plans/specialty-section-variants.md`: the plan for styling section
99
119
  components per specialty (`@npc-stat wirephreak`), for a later release.
100
120
 
101
121
  ### Removed
102
122
 
123
+ - **`@callout variant=…` is gone**, with no compatibility mapping: the variant
124
+ is a bare word (`@callout note`, `@callout vibe`, `@callout .float-right
125
+ origin label="…"`). Core reads `variant=origin` as a plain attribute, so the
126
+ callout would render as a note; instead every `@callout` or `@dm-note`
127
+ written that way gets one Problems message on its line: `@callout: no longer
128
+ takes variant=origin; write "@callout origin" instead.` Edit the books:
129
+ `variant=note` to `note`, `variant="vibe"` to `vibe`, `variant=origin` or
130
+ `variant="origin"` to `origin`.
131
+ - **`@tape` is removed.** The marker was one line with no body and no
132
+ `@end-tape`, which a declared marker cannot express yet: a `selfClosing`
133
+ marker feature was requested upstream as gutterpress#344, and `@tape` can
134
+ return as a declared marker once it exists. Until then write the raw
135
+ `<div class="dc-tape">Label</div>` (`dc-flush` to run edge to edge), which
136
+ renders the same HTML; the `.dc-tape` CSS stays. The design guide's
137
+ examples use the raw form. A leftover `@tape label="…"` line now prints as
138
+ the text it is.
139
+ - **The `@roll-table` / `@options-table` no-op handling** (deprecated in
140
+ 17.3.0), its `roll-table` entry in `components.yaml` and the design guide's
141
+ note. Neither is a marker any more, so a stray one prints as text.
103
142
  - `@block variant=panel`. No book or guide page used it; write `@block panel`.
104
143
  - `@end-skills` (no book used it; it is plain text now).
105
144
  - `@end-skill` no longer closes the `@learning-path` around the skill. It
@@ -139,13 +178,25 @@ Books pinned to an earlier gp-dimm-city are unaffected until re-pinned.
139
178
  swallow the rest of the file without a word. `@card` still closes silently.
140
179
  - `@end-card`, `@end-outcome` and `@end-procedure` with nothing open now warn
141
180
  (`declared_marker_close_without_open`) instead of being ignored.
142
- - `@callout` and `@dm-note` are still hand-written, so they no longer close a
143
- `@card` or `@procedure` that is open, and a card or procedure no longer
144
- closes them. `@callout`, `@procedure`, `@end-procedure`, `@end-callout` is a
181
+ - `@callout` and `@dm-note` nest like every declared marker: a new one closes
182
+ the one still open (with what is inside it), `@end-callout` and
183
+ `@end-dm-note` close it, and `@page`, `@section`, `@chapter` and `@continue`
184
+ close it (a callout left open at a `@page` used to run on, and the next
185
+ page's `<div>` ended up inside it). `@callout`, `@procedure`, `@end-procedure`, `@end-callout` is a
145
186
  callout with the procedure in it. One book chapter wrote exactly that; it
146
187
  used to close the callout at the `@procedure` and leave the steps and the
147
188
  closing paragraph outside its box, and now renders them inside it, as
148
189
  written.
190
+ - A callout written in a skill (even after its card, with no `@end-skill`)
191
+ stays in the skill's card, as before. A stray `@end-callout` or
192
+ `@end-dm-note` warns (`declared_marker_close_without_open`) instead of being
193
+ ignored, and a bare word that is not a variant (`@callout vibes`) is a note
194
+ with core's `unknown_variant` warning.
195
+ - `@dm-note` now takes author classes and an `#id` like `@callout` (it used to
196
+ ignore them).
197
+ - A callout or `@dm-note` glued to the line above it (no blank line) is now a
198
+ marker, as every declared marker is; it used to be folded into the
199
+ paragraph.
149
200
  - A bare word after `@card`, `@outcome` or `@procedure` is no longer a class
150
201
  (core reads it as a variant, and only `flush` is one): write `@card .wide`.
151
202
  No book or guide page used one.
package/README.md CHANGED
@@ -43,7 +43,7 @@ byte for byte on every build.
43
43
  `@specialty-art` / `@specialty-card`, `@learning-path`, `@skill` (with
44
44
  `@continue` for a card that spans a page break), `@outcome`, `@callout`,
45
45
  `@dm-note`, `@sidebar`, `@sidebar-box`, `@block`, `@card`, `@gear`,
46
- `@definition`, `@procedure`, `@lede`, `@toc`, `@tape`, `@glossary`, GFM
46
+ `@definition`, `@procedure`, `@lede`, `@toc`, `@glossary`, GFM
47
47
  alerts (`> [!NOTE]`, `[!WARNING]`, `[!DM]`, `[!VIBE]`, `[!ORIGIN]`,
48
48
  `[!VISIT]`, `[!GEAR]`, `[!FLAVOR]`, `[!PULLQUOTE]`), and the automatic
49
49
  `ROLL THE DIE!` chip. `docs/macros.md` is the inventory;
package/components.yaml CHANGED
@@ -26,8 +26,7 @@
26
26
  # live at least one class styled; used at least once in a book;
27
27
  # plugin-origin entries must actually be emitted/parsed
28
28
  # unused styled or emitted, but used by no book
29
- # retired no class styled, none emitted, none used (a retired marker
30
- # may still be parsed as a no-op stripper)
29
+ # retired no class styled, none emitted, none used
31
30
  # unimplemented no class styled, none emitted — documented or authored,
32
31
  # never built.
33
32
  #
@@ -72,7 +71,7 @@ components:
72
71
  - name: callout
73
72
  status: live
74
73
  origin: plugin
75
- recipe: "@callout variant=note|warning|dm|vibe|origin|visit|gear label=\"…\" … @end-callout"
74
+ recipe: "@callout [note|warning|dm|vibe|origin|visit|gear] label=\"…\" … @end-callout"
76
75
  emits:
77
76
  - div.dc-alert.dc-note > span.dc-alert-label
78
77
  - div.dc-alert.dc-note.warning
@@ -83,7 +82,7 @@ components:
83
82
  - div.dc-alert.dc-gear-callout
84
83
  css: styles/components/callouts.css
85
84
  breaks: "as alert"
86
- notes: "Same DOM and tokens as alert; the macro form allows multiple paragraphs and a custom label."
85
+ notes: "Same DOM and tokens as alert; the macro form allows multiple paragraphs and a custom label. The variant is a bare word (`@callout vibe`); `variant=` is no longer accepted and is reported as a problem. With no label=\"…\" the variant's name is the label."
87
86
 
88
87
  - name: dm-note
89
88
  status: live
@@ -92,7 +91,7 @@ components:
92
91
  emits: div.dc-alert.dc-dm-note > span.dc-alert-label
93
92
  css: [styles/components/callouts.css, styles/dc-native.css]
94
93
  breaks: "as alert; first child glued to the box start (dc-native.css)"
95
- notes: "Sugar for @callout variant=dm. Used by the design guide only."
94
+ notes: "An alias of @callout with the dm variant preset: `@dm-note` is `@callout dm`. Used by the design guide only."
96
95
 
97
96
  - name: flavor
98
97
  status: live
@@ -217,12 +216,13 @@ components:
217
216
 
218
217
  - name: tape
219
218
  status: live
220
- origin: plugin
221
- recipe: "@tape label=\"…\" (also authored as raw <div class=\"dc-tape dc-flush\">…</div>)"
219
+ origin: author
220
+ recipe: "<div class=\"dc-tape\">…</div> (add dc-flush to run edge to edge)"
222
221
  emits: div.dc-tape
223
222
  css: styles/components/chrome.css
224
223
  tokens: [--dc-tape-bg, --dc-tape-border, --dc-tape-color]
225
224
  breaks: "break-inside: avoid"
225
+ notes: "Raw HTML only. The @tape marker was removed in 1.2.0: a one-line marker with no body and no @end- needs a selfClosing declaration core does not have yet (gutterpress#344)."
226
226
 
227
227
  - name: toc
228
228
  status: live
@@ -771,13 +771,6 @@ components:
771
771
  emits: .dc-callout
772
772
  notes: "Removed 2026-10-08 (dc-native.css): nothing emits .dc-callout; the alert family is .dc-alert."
773
773
 
774
- - name: roll-table
775
- status: retired
776
- origin: plugin
777
- recipe: "@roll-table … @end-roll-table / @options-table … @end-options-table"
778
- emits: []
779
- notes: "Removed in plugin 17.3.0; the markers are still parsed and stripped as no-ops. Use @outcome or a plain table."
780
-
781
774
  - name: rules-definition
782
775
  status: retired
783
776
  origin: author
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gp-dimm-city",
3
- "version": "1.2.0-alpha.2",
3
+ "version": "1.2.0-alpha.3",
4
4
  "description": "Dimm City design system for Gutterpress: the markdown-it macros (@skill, @specialty, @learning-path, @callout, …), print stylesheets, design tokens, fonts and page templates the Dimm City TTRPG books are set in.",
5
5
  "type": "module",
6
6
  "main": "plugin.js",
@@ -69,7 +69,7 @@
69
69
  "pack:check": "npm pack --dry-run"
70
70
  },
71
71
  "devDependencies": {
72
- "gutterpress": "0.11.16-alpha.2",
72
+ "gutterpress": "0.11.16-alpha.5",
73
73
  "markdown-it": "^14.1.0"
74
74
  },
75
75
  "peerDependencies": {
package/plugin.js CHANGED
@@ -37,6 +37,11 @@
37
37
  * per `roll | name | text` line (crit, hit, mixed, miss, fail)
38
38
  * @procedure → ol.dc-steps: each numbered list inside becomes the
39
39
  * zero-padded step list (the wrapper renders nothing)
40
+ * @callout [variant] → div.dc-alert.<variant class> > span.dc-alert-label.
41
+ * Variants (bare word): note, warning, dm, vibe, origin,
42
+ * visit, gear; none is a note. The label is `label="…"`,
43
+ * or the variant's name. `@dm-note` is `@callout dm`
44
+ * (an alias). `variant=` is gone: validate reports it.
40
45
  * Core's nesting rule is the only one: a declared marker that is already
41
46
  * open is closed (with everything inside it) when it opens again;
42
47
  * `@end-<name>` closes that marker and what is inside it; `@page`,
@@ -45,12 +50,7 @@
45
50
  * made of rows, a procedure of a numbered list) are the plugin's:
46
51
  * the `validate` of each declaration.
47
52
  *
48
- * HAND-WRITTEN MARKERS (support optional key="value" attributes; handled by
49
- * the dimm_city_transform below):
50
- * @callout → Start a dc-alert callout (variant=note|warning|dm|vibe|origin|visit|gear)
51
- * @end-callout → End callout wrapper
52
- * @dm-note → Start a Dream Master note (sugar for @callout variant=dm)
53
- * @end-dm-note → End dm-note wrapper
53
+ * THE REST OF WHAT THIS FILE DOES (plain markdown-it rules, no markers):
54
54
  * @continue → Continuation marker — inside a skill's card it emits a
55
55
  * card with a "{name} ▸" tab so an oversized skill card
56
56
  * can be split across pages while keeping a visible link
@@ -58,7 +58,8 @@
58
58
  * before core's layout transform; anywhere else it is
59
59
  * core's section continuation)
60
60
  * (chapter-opener composite is now markup-driven — see CSS notes below)
61
- * @tape → Inline tape divider (`<div class="dc-tape">— § —</div>`)
61
+ * `ROLL THE DIE!` → span.dc-roll-the-die
62
+ * an image-only paragraph gets p.dc-img-wrapper
62
63
  *
63
64
  * GFM ALERT SYNTAX:
64
65
  * `> [!NOTE]` / `[!WARNING]` / `[!DM]` / `[!VIBE]` / `[!ORIGIN]` / `[!VISIT]`
@@ -470,125 +471,6 @@ function parseAbilityFromListItem(html) {
470
471
  }
471
472
 
472
473
 
473
- // Parse key="value", key='value', key=value, bare .class / #id tokens,
474
- // and {.class #id} attribute blocks from the body string following a marker keyword.
475
- //
476
- // Grammar matches parseMarkerLine() in Gutterpress's markers.js — inlined here
477
- // rather than imported, because plugin code cannot resolve Gutterpress at runtime:
478
- // - Quote-aware tokenization: key="a b" and key='a b' preserve spaces inside quotes
479
- // - .classname — shorthand class (multiple allowed)
480
- // - #id — shorthand id
481
- // - key=value — arbitrary attribute; key="class" splits on whitespace/commas
482
- // - bare token — treated as a class (DC markers have no positional "name" slot)
483
- //
484
- // Back-compat: brace blocks {.class #id} are stripped and injected as tokens
485
- // before the main pass so @specialty {.augmerc} continues to work alongside
486
- // the new bare-token form @specialty .augmerc.
487
- function parseAttrs(str) {
488
- // Pre-pass: extract brace blocks {.class1 .class2 #id} for back-compat,
489
- // then remove them from the string so the main tokenizer doesn't see them.
490
- const braceClasses = [];
491
- const braceIds = [];
492
- const strNoBraces = str.replace(/\{([^}]+)\}/g, (_, inside) => {
493
- for (const part of inside.trim().split(/\s+/)) {
494
- if (part.startsWith('.')) braceClasses.push(part.slice(1));
495
- else if (part.startsWith('#')) braceIds.push(part.slice(1));
496
- }
497
- return ' ';
498
- });
499
-
500
- // Tokenize the remaining string using the same quote-aware tokenizer as
501
- // markers.js's parseMarkerLine().
502
- const tokens = [];
503
- let buf = '';
504
- let quote = null;
505
- for (let i = 0; i < strNoBraces.length; i++) {
506
- const ch = strNoBraces[i];
507
- if (quote) {
508
- if (ch === quote) quote = null;
509
- else buf += ch;
510
- continue;
511
- }
512
- if (ch === '"' || ch === "'") {
513
- quote = ch;
514
- continue;
515
- }
516
- if (/\s/.test(ch)) {
517
- if (buf) tokens.push(buf);
518
- buf = '';
519
- continue;
520
- }
521
- buf += ch;
522
- }
523
- if (buf) tokens.push(buf);
524
-
525
- // Process tokens: mirrors parseMarkerLine's attr-accumulation loop.
526
- // DC markers have no positional "name" slot — bare tokens become classes.
527
- const attrs = {};
528
- const classes = [...braceClasses];
529
- if (braceIds.length) attrs['id'] = braceIds[braceIds.length - 1];
530
-
531
- for (const t of tokens) {
532
- if (t.startsWith('.')) {
533
- const c = t.slice(1).trim();
534
- if (c) classes.push(c);
535
- continue;
536
- }
537
- if (t.startsWith('#')) {
538
- const id = t.slice(1).trim();
539
- if (id) attrs['id'] = id;
540
- continue;
541
- }
542
- const eq = t.indexOf('=');
543
- if (eq > 0) {
544
- const key = t.slice(0, eq).trim();
545
- const val = t.slice(eq + 1).trim();
546
- if (!key) continue;
547
- if (key === 'class') {
548
- val.split(/[,\s]+/).filter(Boolean).forEach((c) => classes.push(c));
549
- } else if (key === 'id') {
550
- if (val) attrs['id'] = val;
551
- } else {
552
- attrs[key] = val;
553
- }
554
- continue;
555
- }
556
- // Bare token (no . # =) — becomes a class for DC markers
557
- if (t) classes.push(t);
558
- }
559
-
560
- if (classes.length) attrs['class'] = classes.join(' ');
561
- return attrs;
562
- }
563
-
564
- // Check if a paragraph contains a marker and extract any attributes
565
- // Returns { matched: true, attrs: {...} } or { matched: false }
566
- function parseMarker(tok, tokens, i, marker) {
567
- if (tok.type !== 'paragraph_open') return { matched: false };
568
- const inline = tokens[i + 1];
569
- if (!inline || inline.type !== 'inline') return { matched: false };
570
-
571
- const content = inline.content.trim();
572
-
573
- // Exact match (no attributes)
574
- if (content === marker) {
575
- return { matched: true, attrs: {} };
576
- }
577
-
578
- // Check if starts with marker followed by space
579
- if (content.startsWith(marker + ' ')) {
580
- const rest = content.slice(marker.length + 1).trim();
581
- return { matched: true, attrs: parseAttrs(rest) };
582
- }
583
-
584
- return { matched: false };
585
- }
586
-
587
- // Legacy helper for simple marker checks (no attrs needed)
588
- function isMarker(tok, tokens, i, marker) {
589
- return parseMarker(tok, tokens, i, marker).matched;
590
- }
591
-
592
474
  // Gutterpress owns bare @continue as a generic section marker and parses it
593
475
  // before custom core rules run. Claim the exact bare marker first, then decide
594
476
  // before Gutterpress's layout transform whether it belongs to a skill card or
@@ -836,6 +718,19 @@ function dcAlertsTransform(state) {
836
718
  state.tokens = newTokens;
837
719
  }
838
720
 
721
+ /**
722
+ * Mark image-only paragraphs with `.dc-img-wrapper`, so layout rules can
723
+ * target them without relying on `p:has(img)`.
724
+ */
725
+ function dcImageParagraphs(state) {
726
+ const tokens = state.tokens;
727
+ for (let i = 0; i + 2 < tokens.length; i++) {
728
+ const [open, inline, close] = [tokens[i], tokens[i + 1], tokens[i + 2]];
729
+ if (open.type !== 'paragraph_open' || inline.type !== 'inline' || close.type !== 'paragraph_close') continue;
730
+ if (inline.children?.length === 1 && inline.children[0].type === 'image') open.attrSet('class', 'dc-img-wrapper');
731
+ }
732
+ }
733
+
839
734
  /**
840
735
  * `data-position="odd|even"` on every @specialty-card, counted across the
841
736
  * document in order so the card grid alternates its tilt/offset. The wrapper
@@ -1047,8 +942,8 @@ function dcSkillCards(state) {
1047
942
  continue;
1048
943
  }
1049
944
 
1050
- // A @card, @outcome or @procedure is content of the skill's card; a
1051
- // break or another component (@sidebar, @specialty-art, …) ends it.
945
+ // A @callout, @card, @outcome or @procedure is content of the skill's card;
946
+ // a break or another component (@sidebar, @specialty-art, …) ends it.
1052
947
  if (tok.type === 'layout_component_open' && CARD_CONTENT.has(tok.meta.kind)) {
1053
948
  let end = i;
1054
949
  let depth = 0;
@@ -1129,7 +1024,7 @@ function dcSkillCards(state) {
1129
1024
  // run BEFORE dcLearningPaths and dcSkillCards: a card, outcome or procedure
1130
1025
  // inside a skill must already be in its final form when the skill's rule
1131
1026
  // reads it. They stay inside the skill's card (see CARD_CONTENT).
1132
- const CARD_CONTENT = new Set(['card', 'outcome', 'procedure']);
1027
+ const CARD_CONTENT = new Set(['callout', 'card', 'outcome', 'procedure']);
1133
1028
 
1134
1029
  /**
1135
1030
  * `@card`: the wrapper is `div.dc-card` (the declared token itself). Inside it,
@@ -1225,6 +1120,43 @@ function dcProcedures(state) {
1225
1120
  });
1226
1121
  }
1227
1122
 
1123
+ // ── Declared component: @callout (and its alias @dm-note) ──────────────────
1124
+ //
1125
+ // `@callout note|warning|dm|vibe|origin|visit|gear` is a declared wrapper:
1126
+ // core adds `dc-alert`, the variant's class and any author classes. What core
1127
+ // cannot know is each variant's default label, so a rule adds it when the
1128
+ // author wrote no `label="…"`. A missing or unrecognised variant is a plain
1129
+ // note, as it always was.
1130
+ const CALLOUTS = {
1131
+ note: { class: 'dc-note', label: 'Note' },
1132
+ warning: { class: 'dc-note warning', label: 'Warning' },
1133
+ dm: { class: 'dc-dm-note', label: 'Dream Master Note' },
1134
+ vibe: { class: 'dc-vibe-callout', label: 'Vibe' },
1135
+ origin: { class: 'dc-origin-callout', label: 'Origin' },
1136
+ visit: { class: 'dc-visit-callout', label: 'Visit' },
1137
+ gear: { class: 'dc-gear-callout', label: 'Gear' },
1138
+ };
1139
+
1140
+ function dcCallouts(state) {
1141
+ forEachComponent(state.tokens, 'callout', ([open, ...rest]) => {
1142
+ const variant = Object.hasOwn(CALLOUTS, open.meta.variant) ? open.meta.variant : 'note';
1143
+ if (variant !== open.meta.variant) {
1144
+ // no variant class came from core: the default note keeps its place right after dc-alert
1145
+ open.attrSet('class', open.attrGet('class').replace('dc-alert', 'dc-alert dc-note'));
1146
+ }
1147
+ if (open.meta.labelled) return; // an explicit label="…" wins
1148
+ const label = makeToken('html_block', '<span class="dc-alert-label">' + esc(CALLOUTS[variant].label) + '</span>\n');
1149
+ return [open, label, ...rest];
1150
+ });
1151
+ }
1152
+
1153
+ // `variant=` was the old way to pick one; it now reads as a plain attribute
1154
+ // and would silently render a note, so say so once, on the marker's line.
1155
+ function validateCallout({ attrs }) {
1156
+ if (attrs.variant === undefined) return [];
1157
+ return [`no longer takes variant=${attrs.variant}; write "@callout ${attrs.variant}" instead.`];
1158
+ }
1159
+
1228
1160
  /**
1229
1161
  * Structure checks (core runs these on the declared markers below and reports
1230
1162
  * them as layout warnings). They are the plugin's rules, not core's: core
@@ -1329,133 +1261,15 @@ export default function dimmCityPlugin(md, options = {}) {
1329
1261
  // variant chrome, the chevron-styled h1) is provided by CSS attribute
1330
1262
  // selectors in components/*.css matching this structure.
1331
1263
 
1332
- // The hand-written markers that remain: @callout, @dm-note, @tape and the
1333
- // retired @roll-table / @options-table no-ops, plus the image-only paragraph class.
1334
- md.core.ruler.push('dimm_city_transform', function (state) {
1335
- const tokens = state.tokens;
1336
- const newTokens = [];
1337
- let inCallout = false;
1338
- let inDmNote = false;
1339
-
1340
- // A new @callout or @dm-note closes the one still open.
1341
- function closeAll() {
1342
- if (inCallout) newTokens.push(makeToken('html_block', '</div>\n'));
1343
- if (inDmNote) newTokens.push(makeToken('html_block', '</div>\n'));
1344
- inCallout = inDmNote = false;
1345
- }
1346
-
1347
- for (let i = 0; i < tokens.length; i++) {
1348
- const tok = tokens[i];
1349
-
1350
- // --- @roll-table / @options-table (DEPRECATED 17.3.0) ---
1351
- // These macros emitted tier-colored classes (dc-roll-table*, dc-options-table*)
1352
- // with ZERO backing CSS and had zero usage in the book. They are now no-ops:
1353
- // the markers (and their @end- counterparts) are stripped so any stray
1354
- // occurrence renders nothing. Use standard GFM pipe tables instead — they are
1355
- // auto-styled by dc-core.
1356
- if (isMarker(tok, tokens, i, '@roll-table') ||
1357
- isMarker(tok, tokens, i, '@end-roll-table') ||
1358
- isMarker(tok, tokens, i, '@options-table') ||
1359
- isMarker(tok, tokens, i, '@end-options-table')) {
1360
- i += 2;
1361
- continue;
1362
- }
1363
-
1364
- // --- @callout / @end-callout ---
1365
- // Block-level alert wrapper. variant="note|warning|dm|vibe|origin|visit|gear"
1366
- const calloutMarker = parseMarker(tok, tokens, i, '@callout');
1367
- if (calloutMarker.matched) {
1368
- closeAll();
1369
- const variant = (calloutMarker.attrs['variant'] || 'note').toLowerCase();
1370
- const CALLOUT_VARIANTS = {
1371
- 'note': { classes: 'dc-note', label: 'Note' },
1372
- 'warning': { classes: 'dc-note warning', label: 'Warning' },
1373
- 'dm': { classes: 'dc-dm-note', label: 'Dream Master Note' },
1374
- 'vibe': { classes: 'dc-vibe-callout', label: 'Vibe' },
1375
- 'origin': { classes: 'dc-origin-callout', label: 'Origin' },
1376
- 'visit': { classes: 'dc-visit-callout', label: 'Visit' },
1377
- 'gear': { classes: 'dc-gear-callout', label: 'Gear' },
1378
- };
1379
- const cfg = CALLOUT_VARIANTS[variant] || CALLOUT_VARIANTS['note'];
1380
- const labelOverride = calloutMarker.attrs['label'];
1381
- const labelText = labelOverride ? esc(labelOverride) : cfg.label;
1382
- const calloutClass = 'dc-alert ' + cfg.classes +
1383
- (calloutMarker.attrs['class'] ? ' ' + esc(calloutMarker.attrs['class']) : '');
1384
- newTokens.push(makeToken('html_block',
1385
- '<div class="' + calloutClass + '">\n' +
1386
- '<span class="dc-alert-label">' + labelText + '</span>\n'
1387
- ));
1388
- inCallout = true;
1389
- i += 2;
1390
- continue;
1391
- }
1392
-
1393
- if (isMarker(tok, tokens, i, '@end-callout')) {
1394
- if (inCallout) newTokens.push(makeToken('html_block', '</div>\n'));
1395
- inCallout = false;
1396
- i += 2;
1397
- continue;
1398
- }
1399
-
1400
- // --- @dm-note / @end-dm-note ---
1401
- // Block-level DM note wrapper. Equivalent to > [!DM] but supports multi-paragraph content.
1402
- const dmNoteMarker = parseMarker(tok, tokens, i, '@dm-note');
1403
- if (dmNoteMarker.matched) {
1404
- closeAll();
1405
- const labelOverride = dmNoteMarker.attrs['label'];
1406
- const labelText = labelOverride ? esc(labelOverride) : 'Dream Master Note';
1407
- newTokens.push(makeToken('html_block',
1408
- '<div class="dc-alert dc-dm-note">\n' +
1409
- '<span class="dc-alert-label">' + labelText + '</span>\n'
1410
- ));
1411
- inDmNote = true;
1412
- i += 2;
1413
- continue;
1414
- }
1264
+ // Mark image-only paragraphs so layout rules can target them without
1265
+ // relying on p:has(img). The base CSS rule
1266
+ // (p.dc-img-wrapper { padding:0; margin:0 }) lives in components/data.css.
1267
+ // Per-page rules can further refine position via .page.my-class p.dc-img-wrapper.
1268
+ md.core.ruler.push('dc_image_paragraphs', dcImageParagraphs);
1415
1269
 
1416
- if (isMarker(tok, tokens, i, '@end-dm-note')) {
1417
- if (inDmNote) newTokens.push(makeToken('html_block', '</div>\n'));
1418
- inDmNote = false;
1419
- i += 2;
1420
- continue;
1421
- }
1422
-
1423
- // --- @tape (single-line tape divider) ---
1424
- const tapeMarker = parseMarker(tok, tokens, i, '@tape');
1425
- if (tapeMarker.matched) {
1426
- const labelAttr = tapeMarker.attrs['label'] || '';
1427
- const extraClass = tapeMarker.attrs['class'] ? ' ' + esc(tapeMarker.attrs['class']) : '';
1428
- newTokens.push(makeToken('html_block', '<div class="dc-tape' + extraClass + '">' + esc(labelAttr) + '</div>\n'));
1429
- i += 2; continue;
1430
- }
1431
-
1432
- // Mark image-only paragraphs so layout rules can target them without
1433
- // relying on p:has(img). The base CSS rule
1434
- // (p.dc-img-wrapper { padding:0; margin:0 }) lives in components/data.css.
1435
- // Per-page rules can further refine position via .page.my-class p.dc-img-wrapper.
1436
- if (tok.type === 'paragraph_open') {
1437
- const inlineTok = tokens[i + 1];
1438
- const closeTok = tokens[i + 2];
1439
- if (inlineTok && inlineTok.type === 'inline' && closeTok && closeTok.type === 'paragraph_close') {
1440
- const hasOnlyImage = Array.isArray(inlineTok.children)
1441
- && inlineTok.children.length === 1
1442
- && inlineTok.children[0].type === 'image';
1443
- if (hasOnlyImage) {
1444
- tok.attrSet('class', 'dc-img-wrapper');
1445
- }
1446
- }
1447
- }
1448
-
1449
- newTokens.push(tok);
1450
- }
1451
-
1452
- closeAll(); // a @callout or @dm-note left open closes at the end of the document
1453
-
1454
- state.tokens = newTokens;
1455
- });
1456
-
1457
- // Declared components. Cards, outcomes and procedures go first so one inside
1458
- // a skill is already rewritten when the skill's rule looks at it.
1270
+ // Declared components. Callouts, cards, outcomes and procedures go first so
1271
+ // one inside a skill is already rewritten when the skill's rule looks at it.
1272
+ md.core.ruler.push('dc_callouts', dcCallouts);
1459
1273
  md.core.ruler.push('dc_cards', dcCards);
1460
1274
  md.core.ruler.push('dc_outcomes', dcOutcomes);
1461
1275
  md.core.ruler.push('dc_procedures', dcProcedures);
@@ -1468,7 +1282,7 @@ export default function dimmCityPlugin(md, options = {}) {
1468
1282
  * same relationship `metadata` has). Core parses `@name … @end-name`, merges
1469
1283
  * author classes, threads `data-source-range`, nests containers as a stack and
1470
1284
  * closes them at the next `@page`/`@section`/`@chapter`, so none of this is
1471
- * hand-written in the state machine above. A bare markdown-it instance never
1285
+ * hand-written in a marker parser. A bare markdown-it instance never
1472
1286
  * reads this table: declared markers exist only under Gutterpress.
1473
1287
  *
1474
1288
  * A `section: true` entry declares a section-styled component: core treats it
@@ -1511,6 +1325,16 @@ export const markers = {
1511
1325
  outcome: { class: 'dc-outcomes', variants: { flush: 'dc-flush' }, validate: validateOutcome },
1512
1326
  procedure: { validate: validateProcedure },
1513
1327
 
1328
+ // `@callout` takes its variant as a bare word (`@callout vibe`); `@dm-note` is
1329
+ // the same marker with the `dm` variant preset. A default label comes from
1330
+ // `dcCallouts`, an explicit `label="…"` from core.
1331
+ callout: wrapper('dc-alert', {
1332
+ variants: Object.fromEntries(Object.entries(CALLOUTS).map(([name, { class: cls }]) => [name, cls])),
1333
+ label: { tag: 'span', class: 'dc-alert-label', from: 'attr:label' },
1334
+ validate: validateCallout,
1335
+ }),
1336
+ 'dm-note': { alias: 'callout', preset: { variant: 'dm' } },
1337
+
1514
1338
  sidebar: wrapper('dc-sidebar', { variants: { inset: 'inset' } }),
1515
1339
  'sidebar-box': wrapper('dc-prose-panel dc-sidebar-box'),
1516
1340
  definition: wrapper('dc-prose-panel dc-definition-block'),
@@ -1,4 +1,4 @@
1
- @callout variant={{variant}} label="{{label}}"
1
+ @callout note label="{{label}}"
2
2
 
3
3
  {{body}}
4
4