gp-dimm-city 1.2.0-alpha.2 → 1.2.0
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 +60 -9
- package/README.md +1 -1
- package/components.yaml +7 -14
- package/package.json +2 -2
- package/plugin.js +80 -256
- package/snippets/callout.md +1 -1
- package/snippets/chapter-start-page.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,10 +7,11 @@ cut a version that has no `## [X.Y.Z]` heading below.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [1.2.0] -
|
|
10
|
+
## [1.2.0] - 2026-10-10
|
|
11
11
|
|
|
12
|
-
Requires Gutterpress 0.11.16
|
|
13
|
-
|
|
12
|
+
Requires Gutterpress 0.11.16 or newer (the desktop app or CLI): older
|
|
13
|
+
versions reject the `section: true` markers below and do not report a
|
|
14
|
+
mistyped `@callout` variant (`unknown_variant`).
|
|
14
15
|
Books pinned to an earlier gp-dimm-city are unaffected until re-pinned.
|
|
15
16
|
|
|
16
17
|
### Changed
|
|
@@ -33,9 +34,10 @@ Books pinned to an earlier gp-dimm-city are unaffected until re-pinned.
|
|
|
33
34
|
- `@specialty-card`'s `data-position="odd|even"` now comes from a small core
|
|
34
35
|
rule that numbers the cards in document order, as before.
|
|
35
36
|
- Snippets for `@sidebar-box`, `@glossary`, `@toc` and `@specialty-intro`.
|
|
36
|
-
The `@block` snippet uses the variant word
|
|
37
|
+
The `@block` snippet uses the variant word, and the chapter-start snippet
|
|
38
|
+
opens its excerpt with `@fiction-excerpt`.
|
|
37
39
|
- **Nine section-styled components are declared as real core sections**
|
|
38
|
-
(Gutterpress 0.11.16
|
|
40
|
+
(Gutterpress 0.11.16, `section: true`): `@column-panel`, `@tabbed`,
|
|
39
41
|
`@card-grid`, `@citizen-walkthrough`, `@fiction-excerpt`, `@npc-stat`,
|
|
40
42
|
`@flaws`, `@ideals` and `@dreams`, each with an `@end-…` closer and a
|
|
41
43
|
snippet. `@npc-stat` is exactly `@section .dc-npc-stat`: same element, same
|
|
@@ -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
|
-
-
|
|
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`.
|
|
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`
|
|
143
|
-
|
|
144
|
-
|
|
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`, `@
|
|
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
|
|
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
|
|
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: "
|
|
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:
|
|
221
|
-
recipe: "
|
|
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
|
|
3
|
+
"version": "1.2.0",
|
|
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
|
|
72
|
+
"gutterpress": "0.11.16",
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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;
|
|
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
|
-
//
|
|
1333
|
-
//
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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
|
|
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'),
|
package/snippets/callout.md
CHANGED