@diagrammo/dgmo 0.40.0 → 0.41.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/dist/index.cjs CHANGED
@@ -15099,6 +15099,8 @@ function parseEventLine(content, palette) {
15099
15099
  let currentTagGroup = null;
15100
15100
  let currentEvent = null;
15101
15101
  let currentEra = null;
15102
+ let eraIndent = -1;
15103
+ let currentEventIndent = 0;
15102
15104
  const aliasMap = /* @__PURE__ */ new Map();
15103
15105
  for (let i = 0; i < lines.length; i++) {
15104
15106
  const line11 = lines[i];
@@ -15173,52 +15175,15 @@ function parseEventLine(content, palette) {
15173
15175
  });
15174
15176
  continue;
15175
15177
  }
15176
- if (indent > 0) {
15177
- if (!currentEvent) {
15178
- pushWarning(
15179
- lineNumber,
15180
- `Indented description "${trimmed}" has no event above it \u2014 add an event line first. (\xA728)`
15181
- );
15182
- continue;
15183
- }
15178
+ if (currentEvent && indent > currentEventIndent) {
15184
15179
  const descLine = trimmed.startsWith("- ") ? `\u2022 ${trimmed.substring(2)}` : trimmed;
15185
15180
  currentEvent.description.push(descLine);
15186
15181
  continue;
15187
15182
  }
15188
- currentTagGroup = null;
15189
- if (trimmed.toLowerCase() === "no-scale") {
15190
- options.scale = false;
15191
- continue;
15192
- }
15193
- const sideMatch = trimmed.match(SIDE_RE);
15194
- if (sideMatch) {
15195
- options.side = sideMatch[1].toLowerCase();
15196
- continue;
15197
- }
15198
- if (trimmed.toLowerCase() === "no-box") {
15199
- options.noBox = true;
15200
- continue;
15201
- }
15202
- if (trimmed.toLowerCase() === "no-legend") {
15203
- options.noLegend = true;
15204
- continue;
15205
- }
15206
- const dirMatch = trimmed.match(DIRECTION_RE);
15207
- if (dirMatch) {
15208
- const dir = dirMatch[1].toUpperCase();
15209
- if (dir !== "LR") {
15210
- pushWarning(
15211
- lineNumber,
15212
- `event-line is horizontal-only in v1; \`direction ${dir}\` (vertical orientation) is a fast-follow.`,
15213
- EVENT_LINE_DIAGNOSTIC_CODES.UNSUPPORTED
15214
- );
15215
- }
15216
- continue;
15217
- }
15218
- if (tryParseSharedOption(trimmed, sharedOptions)) continue;
15219
15183
  const eraMatch = trimmed.match(ERA_RE);
15220
15184
  if (eraMatch) {
15221
15185
  contentStarted = true;
15186
+ currentTagGroup = null;
15222
15187
  currentEvent = null;
15223
15188
  const name = eraMatch[1].trim();
15224
15189
  let rest = (eraMatch[2] ?? "").trim();
@@ -15242,16 +15207,52 @@ function parseEventLine(content, palette) {
15242
15207
  const era = { name, color, collapsed, lineNumber };
15243
15208
  result.eras.push(era);
15244
15209
  currentEra = name;
15210
+ eraIndent = indent;
15245
15211
  continue;
15246
15212
  }
15247
- if (SECTION_SEAM_RE.test(trimmed)) {
15248
- pushWarning(
15249
- lineNumber,
15250
- "Group events with `[Name]` era brackets (\xA728.6a), not `section`.",
15251
- EVENT_LINE_DIAGNOSTIC_CODES.UNSUPPORTED
15252
- );
15253
- continue;
15213
+ if (indent === 0) {
15214
+ currentTagGroup = null;
15215
+ if (trimmed.toLowerCase() === "no-scale") {
15216
+ options.scale = false;
15217
+ continue;
15218
+ }
15219
+ const sideMatch = trimmed.match(SIDE_RE);
15220
+ if (sideMatch) {
15221
+ options.side = sideMatch[1].toLowerCase();
15222
+ continue;
15223
+ }
15224
+ if (trimmed.toLowerCase() === "no-box") {
15225
+ options.noBox = true;
15226
+ continue;
15227
+ }
15228
+ if (trimmed.toLowerCase() === "no-legend") {
15229
+ options.noLegend = true;
15230
+ continue;
15231
+ }
15232
+ const dirMatch = trimmed.match(DIRECTION_RE);
15233
+ if (dirMatch) {
15234
+ const dir = dirMatch[1].toUpperCase();
15235
+ if (dir !== "LR") {
15236
+ pushWarning(
15237
+ lineNumber,
15238
+ `event-line is horizontal-only in v1; \`direction ${dir}\` (vertical orientation) is a fast-follow.`,
15239
+ EVENT_LINE_DIAGNOSTIC_CODES.UNSUPPORTED
15240
+ );
15241
+ }
15242
+ continue;
15243
+ }
15244
+ if (tryParseSharedOption(trimmed, sharedOptions)) continue;
15245
+ if (SECTION_SEAM_RE.test(trimmed)) {
15246
+ pushWarning(
15247
+ lineNumber,
15248
+ "Group events with `[Name]` era brackets (\xA728.6a), not `section`.",
15249
+ EVENT_LINE_DIAGNOSTIC_CODES.UNSUPPORTED
15250
+ );
15251
+ continue;
15252
+ }
15254
15253
  }
15254
+ const inEra = currentEra !== null && indent > eraIndent;
15255
+ if (!inEra) currentEra = null;
15255
15256
  contentStarted = true;
15256
15257
  const event = parseEventHeader(
15257
15258
  trimmed,
@@ -15263,6 +15264,7 @@ function parseEventLine(content, palette) {
15263
15264
  );
15264
15265
  result.events.push(event);
15265
15266
  currentEvent = event;
15267
+ currentEventIndent = indent;
15266
15268
  }
15267
15269
  options.noTitle = sharedOptions["no-title"] === "on";
15268
15270
  finalizeAutoTagColors(result.tagGroups, palette);
package/dist/index.js CHANGED
@@ -15115,6 +15115,8 @@ function parseEventLine(content, palette) {
15115
15115
  let currentTagGroup = null;
15116
15116
  let currentEvent = null;
15117
15117
  let currentEra = null;
15118
+ let eraIndent = -1;
15119
+ let currentEventIndent = 0;
15118
15120
  const aliasMap = /* @__PURE__ */ new Map();
15119
15121
  for (let i = 0; i < lines.length; i++) {
15120
15122
  const line11 = lines[i];
@@ -15189,52 +15191,15 @@ function parseEventLine(content, palette) {
15189
15191
  });
15190
15192
  continue;
15191
15193
  }
15192
- if (indent > 0) {
15193
- if (!currentEvent) {
15194
- pushWarning(
15195
- lineNumber,
15196
- `Indented description "${trimmed}" has no event above it \u2014 add an event line first. (\xA728)`
15197
- );
15198
- continue;
15199
- }
15194
+ if (currentEvent && indent > currentEventIndent) {
15200
15195
  const descLine = trimmed.startsWith("- ") ? `\u2022 ${trimmed.substring(2)}` : trimmed;
15201
15196
  currentEvent.description.push(descLine);
15202
15197
  continue;
15203
15198
  }
15204
- currentTagGroup = null;
15205
- if (trimmed.toLowerCase() === "no-scale") {
15206
- options.scale = false;
15207
- continue;
15208
- }
15209
- const sideMatch = trimmed.match(SIDE_RE);
15210
- if (sideMatch) {
15211
- options.side = sideMatch[1].toLowerCase();
15212
- continue;
15213
- }
15214
- if (trimmed.toLowerCase() === "no-box") {
15215
- options.noBox = true;
15216
- continue;
15217
- }
15218
- if (trimmed.toLowerCase() === "no-legend") {
15219
- options.noLegend = true;
15220
- continue;
15221
- }
15222
- const dirMatch = trimmed.match(DIRECTION_RE);
15223
- if (dirMatch) {
15224
- const dir = dirMatch[1].toUpperCase();
15225
- if (dir !== "LR") {
15226
- pushWarning(
15227
- lineNumber,
15228
- `event-line is horizontal-only in v1; \`direction ${dir}\` (vertical orientation) is a fast-follow.`,
15229
- EVENT_LINE_DIAGNOSTIC_CODES.UNSUPPORTED
15230
- );
15231
- }
15232
- continue;
15233
- }
15234
- if (tryParseSharedOption(trimmed, sharedOptions)) continue;
15235
15199
  const eraMatch = trimmed.match(ERA_RE);
15236
15200
  if (eraMatch) {
15237
15201
  contentStarted = true;
15202
+ currentTagGroup = null;
15238
15203
  currentEvent = null;
15239
15204
  const name = eraMatch[1].trim();
15240
15205
  let rest = (eraMatch[2] ?? "").trim();
@@ -15258,16 +15223,52 @@ function parseEventLine(content, palette) {
15258
15223
  const era = { name, color, collapsed, lineNumber };
15259
15224
  result.eras.push(era);
15260
15225
  currentEra = name;
15226
+ eraIndent = indent;
15261
15227
  continue;
15262
15228
  }
15263
- if (SECTION_SEAM_RE.test(trimmed)) {
15264
- pushWarning(
15265
- lineNumber,
15266
- "Group events with `[Name]` era brackets (\xA728.6a), not `section`.",
15267
- EVENT_LINE_DIAGNOSTIC_CODES.UNSUPPORTED
15268
- );
15269
- continue;
15229
+ if (indent === 0) {
15230
+ currentTagGroup = null;
15231
+ if (trimmed.toLowerCase() === "no-scale") {
15232
+ options.scale = false;
15233
+ continue;
15234
+ }
15235
+ const sideMatch = trimmed.match(SIDE_RE);
15236
+ if (sideMatch) {
15237
+ options.side = sideMatch[1].toLowerCase();
15238
+ continue;
15239
+ }
15240
+ if (trimmed.toLowerCase() === "no-box") {
15241
+ options.noBox = true;
15242
+ continue;
15243
+ }
15244
+ if (trimmed.toLowerCase() === "no-legend") {
15245
+ options.noLegend = true;
15246
+ continue;
15247
+ }
15248
+ const dirMatch = trimmed.match(DIRECTION_RE);
15249
+ if (dirMatch) {
15250
+ const dir = dirMatch[1].toUpperCase();
15251
+ if (dir !== "LR") {
15252
+ pushWarning(
15253
+ lineNumber,
15254
+ `event-line is horizontal-only in v1; \`direction ${dir}\` (vertical orientation) is a fast-follow.`,
15255
+ EVENT_LINE_DIAGNOSTIC_CODES.UNSUPPORTED
15256
+ );
15257
+ }
15258
+ continue;
15259
+ }
15260
+ if (tryParseSharedOption(trimmed, sharedOptions)) continue;
15261
+ if (SECTION_SEAM_RE.test(trimmed)) {
15262
+ pushWarning(
15263
+ lineNumber,
15264
+ "Group events with `[Name]` era brackets (\xA728.6a), not `section`.",
15265
+ EVENT_LINE_DIAGNOSTIC_CODES.UNSUPPORTED
15266
+ );
15267
+ continue;
15268
+ }
15270
15269
  }
15270
+ const inEra = currentEra !== null && indent > eraIndent;
15271
+ if (!inEra) currentEra = null;
15271
15272
  contentStarted = true;
15272
15273
  const event = parseEventHeader(
15273
15274
  trimmed,
@@ -15279,6 +15280,7 @@ function parseEventLine(content, palette) {
15279
15280
  );
15280
15281
  result.events.push(event);
15281
15282
  currentEvent = event;
15283
+ currentEventIndent = indent;
15282
15284
  }
15283
15285
  options.noTitle = sharedOptions["no-title"] === "on";
15284
15286
  finalizeAutoTagColors(result.tagGroups, palette);
@@ -2399,6 +2399,23 @@ tag Genre as g
2399
2399
  - **Tag** — trailing same-line metadata (`g: Pop`), like timeline; colors the dot, leader, and card.
2400
2400
  - **Description** — bare indented body lines (like `pyramid`/`ring`); `- ` makes a bullet; inline markdown (`**bold**`, `*italic*`, `` `code` ``) supported.
2401
2401
 
2402
+ #### Eras
2403
+
2404
+ Group a run of events into a labeled section with a `[Name]` bracket, then **indent the events beneath it** (the org / version-control nesting idiom). An event belongs to the era it is indented under; its description sits one level deeper still. An event at indent 0 sits **outside** any era.
2405
+
2406
+ ```
2407
+ [The Early Web]
2408
+ 1991 WorldWideWeb t: Protocol
2409
+ Tim Berners-Lee publishes the first site.
2410
+ 1993 Mosaic t: Browser
2411
+
2412
+ [The App Era] collapsed: true
2413
+ 2005 Ajax t: Platform
2414
+ ```
2415
+
2416
+ - **Trailing metadata** after `]` — optional `collapsed: true` and/or a named color (`[The 1960s] collapsed: true blue`).
2417
+ - **`collapsed: true`** folds the era into a single summary card (its name + a bulleted list of member events) while a bracket stays on the spine; in the app a reader toggles it live.
2418
+
2402
2419
  #### Directives
2403
2420
 
2404
2421
  - `no-scale` — space events evenly instead of by date (dates become captions).
@@ -8,19 +8,19 @@ tag Theme as t
8
8
  Standard orange
9
9
 
10
10
  [The Early Web]
11
- 1991 WorldWideWeb t: Protocol
12
- Tim Berners-Lee publishes the first website at **CERN**.
13
- 1993 Mosaic t: Browser
14
- The first popular *graphical* browser.
11
+ 1991 WorldWideWeb t: Protocol
12
+ Tim Berners-Lee publishes the first website at **CERN**.
13
+ 1993 Mosaic t: Browser
14
+ The first popular *graphical* browser.
15
15
 
16
16
  [The Standards Era] collapsed: true
17
- 1995 JavaScript t: Platform
18
- Brendan Eich writes the language in ten days.
19
- 1996 CSS t: Standard
20
- Styling splits from structure.
17
+ 1995 JavaScript t: Platform
18
+ Brendan Eich writes the language in ten days.
19
+ 1996 CSS t: Standard
20
+ Styling splits from structure.
21
21
 
22
22
  [The App Era]
23
- 2005 Ajax t: Platform
24
- `XMLHttpRequest` updates pages without a reload.
25
- 2014 HTML5 t: Standard
26
- The living standard — video, canvas, semantics.
23
+ 2005 Ajax t: Platform
24
+ `XMLHttpRequest` updates pages without a reload.
25
+ 2014 HTML5 t: Standard
26
+ The living standard — video, canvas, semantics.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -203,15 +203,17 @@ export function parseEventLine(
203
203
  continue;
204
204
  }
205
205
 
206
- // ── Indented line in content phase = description body ──
207
- if (indent > 0) {
208
- if (!currentEvent) {
209
- pushWarning(
210
- lineNumber,
211
- `Indented description "${trimmed}" has no event above it — add an event line first. (§28)`
212
- );
213
- continue;
214
- }
206
+ // ── Content phase: indentation classifies each line. ──
207
+ //
208
+ // Eras are top-level `[Name]` section headers whose member events are
209
+ // INDENTED beneath them (the org §7 / version-control §29 idiom); an event
210
+ // at indent 0 sits OUTSIDE any era. A line indented deeper than its event
211
+ // is that event's description body (§28.4).
212
+
213
+ // Description body — any line indented deeper than its event. Checked
214
+ // before the era bracket so a bracketed prose line (` [aside]`) under an
215
+ // event stays a description rather than opening a spurious era.
216
+ if (currentEvent && indent > currentEventIndent) {
215
217
  const descLine = trimmed.startsWith('- ')
216
218
  ? `• ${trimmed.substring(2)}`
217
219
  : trimmed;
@@ -219,47 +221,14 @@ export function parseEventLine(
219
221
  continue;
220
222
  }
221
223
 
222
- // ── Indent-0 line: directive, reserved seam, or event ──
223
- currentTagGroup = null;
224
-
225
- if (trimmed.toLowerCase() === 'no-scale') {
226
- options.scale = false;
227
- continue;
228
- }
229
- const sideMatch = trimmed.match(SIDE_RE);
230
- if (sideMatch) {
231
- options.side = sideMatch[1]!.toLowerCase() as EventLineOptions['side'];
232
- continue;
233
- }
234
- if (trimmed.toLowerCase() === 'no-box') {
235
- options.noBox = true;
236
- continue;
237
- }
238
- if (trimmed.toLowerCase() === 'no-legend') {
239
- options.noLegend = true;
240
- continue;
241
- }
242
- const dirMatch = trimmed.match(DIRECTION_RE);
243
- if (dirMatch) {
244
- const dir = dirMatch[1]!.toUpperCase();
245
- if (dir !== 'LR') {
246
- pushWarning(
247
- lineNumber,
248
- `event-line is horizontal-only in v1; \`direction ${dir}\` (vertical orientation) is a fast-follow.`,
249
- EVENT_LINE_DIAGNOSTIC_CODES.UNSUPPORTED
250
- );
251
- }
252
- continue;
253
- }
254
- if (tryParseSharedOption(trimmed, sharedOptions)) continue;
255
-
256
- // ── Era run delimiter: `[Name]` opens a section that runs to the next
257
- // `[Name]` or EOF (§28.6a). NOT an indentation container — events stay at
258
- // indent 0, so flat bare-body descriptions are preserved. Optional trailing
259
- // `collapsed: true` and/or a color name tint/fold the era.
224
+ // ── Era run delimiter: `[Name]` (§28.6a) — a section header whose member
225
+ // events are indented beneath it. Dedenting to (or past) the bracket's
226
+ // own indent leaves the era. Optional trailing `collapsed: true` and/or
227
+ // a color name tint/fold the era.
260
228
  const eraMatch = trimmed.match(ERA_RE);
261
229
  if (eraMatch) {
262
230
  contentStarted = true;
231
+ currentTagGroup = null;
263
232
  currentEvent = null;
264
233
  const name = eraMatch[1]!.trim();
265
234
  let rest = (eraMatch[2] ?? '').trim();
@@ -288,19 +257,59 @@ export function parseEventLine(
288
257
  const era: EventLineEra = { name, color, collapsed, lineNumber };
289
258
  result.eras.push(era);
290
259
  currentEra = name;
260
+ eraIndent = indent;
291
261
  continue;
292
262
  }
293
263
 
294
- if (SECTION_SEAM_RE.test(trimmed)) {
295
- pushWarning(
296
- lineNumber,
297
- 'Group events with `[Name]` era brackets (§28.6a), not `section`.',
298
- EVENT_LINE_DIAGNOSTIC_CODES.UNSUPPORTED
299
- );
300
- continue;
264
+ // ── Top-level (indent 0) directives and reserved seams. ──
265
+ if (indent === 0) {
266
+ currentTagGroup = null;
267
+ if (trimmed.toLowerCase() === 'no-scale') {
268
+ options.scale = false;
269
+ continue;
270
+ }
271
+ const sideMatch = trimmed.match(SIDE_RE);
272
+ if (sideMatch) {
273
+ options.side = sideMatch[1]!.toLowerCase() as EventLineOptions['side'];
274
+ continue;
275
+ }
276
+ if (trimmed.toLowerCase() === 'no-box') {
277
+ options.noBox = true;
278
+ continue;
279
+ }
280
+ if (trimmed.toLowerCase() === 'no-legend') {
281
+ options.noLegend = true;
282
+ continue;
283
+ }
284
+ const dirMatch = trimmed.match(DIRECTION_RE);
285
+ if (dirMatch) {
286
+ const dir = dirMatch[1]!.toUpperCase();
287
+ if (dir !== 'LR') {
288
+ pushWarning(
289
+ lineNumber,
290
+ `event-line is horizontal-only in v1; \`direction ${dir}\` (vertical orientation) is a fast-follow.`,
291
+ EVENT_LINE_DIAGNOSTIC_CODES.UNSUPPORTED
292
+ );
293
+ }
294
+ continue;
295
+ }
296
+ if (tryParseSharedOption(trimmed, sharedOptions)) continue;
297
+ if (SECTION_SEAM_RE.test(trimmed)) {
298
+ pushWarning(
299
+ lineNumber,
300
+ 'Group events with `[Name]` era brackets (§28.6a), not `section`.',
301
+ EVENT_LINE_DIAGNOSTIC_CODES.UNSUPPORTED
302
+ );
303
+ continue;
304
+ }
301
305
  }
302
306
 
303
307
  // ── Event line ──
308
+ // An event belongs to the open era only when indented beneath that era's
309
+ // bracket; an indent-0 event sits outside any era and closes the run
310
+ // (events outside eras are fully supported — they simply have `era: null`).
311
+ const inEra = currentEra !== null && indent > eraIndent;
312
+ if (!inEra) currentEra = null;
304
313
  contentStarted = true;
305
314
  const event = parseEventHeader(
306
315
  trimmed,
@@ -312,6 +321,7 @@ export function parseEventLine(
312
321
  );
313
322
  result.events.push(event);
314
323
  currentEvent = event;
324
+ currentEventIndent = indent;
315
325
  }
316
326
 
317
327
  options.noTitle = sharedOptions['no-title'] === 'on';