@miliastry/quasar 1.1.0 → 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/dist/index.mjs CHANGED
@@ -6232,6 +6232,42 @@ var SemanticAnalyzer = class {
6232
6232
  return diagnostics.length > 0 ? diagnostics : null;
6233
6233
  }
6234
6234
  });
6235
+ this.register({
6236
+ code: "box-missing-equals",
6237
+ severity: "warning",
6238
+ kinds: ["box"],
6239
+ validate: (node, ctx) => {
6240
+ if (node.text.startsWith("=")) return null;
6241
+ const openEnd = node.innerStart > node.range.start ? node.innerStart : node.range.start + 5;
6242
+ if (node.range.start < 0 || openEnd > ctx.source.length) return null;
6243
+ const openTag = ctx.source.slice(node.range.start, openEnd);
6244
+ if (openTag.includes("=")) return null;
6245
+ const fixes = [
6246
+ {
6247
+ description: "Add '=' to [box]",
6248
+ isAutomatic: true,
6249
+ operations: [
6250
+ {
6251
+ kind: "replace_text",
6252
+ range: { start: node.range.start, end: openEnd },
6253
+ newText: "[box=]"
6254
+ }
6255
+ ]
6256
+ }
6257
+ ];
6258
+ return createDiagnostic(
6259
+ "box-missing-equals",
6260
+ "[box] without '=' can be previewed in Miliastry, but osu! requires [box=] to parse it correctly",
6261
+ "warning",
6262
+ {
6263
+ nodeId: node.id,
6264
+ nodeKind: node.kind,
6265
+ range: { start: node.range.start, end: openEnd },
6266
+ fixes
6267
+ }
6268
+ );
6269
+ }
6270
+ });
6235
6271
  }
6236
6272
  /**
6237
6273
  * Create a validator for a specific tag/kind.
@@ -8329,10 +8365,29 @@ var KIND_TO_TAG_NAME = {
8329
8365
  function hasAttrValue(value) {
8330
8366
  return value !== void 0 && value !== null && String(value) !== "";
8331
8367
  }
8332
- function normalizeColorToHex(color) {
8368
+ function expandHexForOsu(body) {
8369
+ if (!/^[0-9a-fA-F]+$/.test(body)) return null;
8370
+ switch (body.length) {
8371
+ case 3:
8372
+ return body[0] + body[0] + body[1] + body[1] + body[2] + body[2];
8373
+ case 4:
8374
+ return body[0] + body[0] + body[1] + body[1] + body[2] + body[2];
8375
+ case 6:
8376
+ return body;
8377
+ case 8:
8378
+ return body.slice(0, 6);
8379
+ default:
8380
+ return null;
8381
+ }
8382
+ }
8383
+ function normalizeColorToHex(color, target = "miliastry") {
8333
8384
  if (!color) return color;
8334
8385
  const trimmed = color.trim();
8335
- if (trimmed.startsWith("#")) return trimmed;
8386
+ if (trimmed.startsWith("#")) {
8387
+ if (target !== "osu") return trimmed;
8388
+ const expanded = expandHexForOsu(trimmed.slice(1));
8389
+ return expanded === null ? trimmed : `#${expanded}`;
8390
+ }
8336
8391
  const rgbMatch = trimmed.match(/^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/i);
8337
8392
  if (rgbMatch) {
8338
8393
  const r = parseInt(rgbMatch[1], 10).toString(16).padStart(2, "0");
@@ -8340,6 +8395,10 @@ function normalizeColorToHex(color) {
8340
8395
  const b = parseInt(rgbMatch[3], 10).toString(16).padStart(2, "0");
8341
8396
  return `#${r}${g}${b}`.toLowerCase();
8342
8397
  }
8398
+ if (target === "osu" && /\d/.test(trimmed)) {
8399
+ const expanded = expandHexForOsu(trimmed);
8400
+ if (expanded !== null) return `#${expanded}`;
8401
+ }
8343
8402
  return trimmed;
8344
8403
  }
8345
8404
  var BBCodeExporter = class extends Visitor {
@@ -8464,7 +8523,7 @@ var BBCodeExporter = class extends Visitor {
8464
8523
  if (this.shouldResolveTokens() && col.startsWith("$")) {
8465
8524
  col = resolveTokenValue(col, this.tokenResolver);
8466
8525
  }
8467
- out = `[color=${normalizeColorToHex(col)}]${out}[/color]`;
8526
+ out = `[color=${normalizeColorToHex(col, this.target)}]${out}[/color]`;
8468
8527
  }
8469
8528
  if (style.fontSize) {
8470
8529
  let size = style.fontSize;
@@ -8547,7 +8606,7 @@ var BBCodeExporter = class extends Visitor {
8547
8606
  if (this.shouldResolveTokens() && color.startsWith("$")) {
8548
8607
  color = resolveTokenValue(color, this.tokenResolver);
8549
8608
  }
8550
- return `=${normalizeColorToHex(color)}`;
8609
+ return `=${normalizeColorToHex(color, this.target)}`;
8551
8610
  } else if (node.kind === "font" && hasAttrValue(node.metadata.font)) {
8552
8611
  let font = String(node.metadata.font);
8553
8612
  if (this.shouldResolveTokens() && font.startsWith("$")) {
@@ -8650,7 +8709,7 @@ var BBCodeExporter = class extends Visitor {
8650
8709
  if (this.shouldResolveTokens() && col.startsWith("$")) {
8651
8710
  col = resolveTokenValue(col, this.tokenResolver);
8652
8711
  }
8653
- return `=${normalizeColorToHex(col)}`;
8712
+ return `=${normalizeColorToHex(col, this.target)}`;
8654
8713
  }
8655
8714
  if (this.shouldResolveTokens() && text.startsWith("=")) {
8656
8715
  let val = text.slice(1);
@@ -8975,6 +9034,23 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
8975
9034
  * LYNE's forum renderer applies.
8976
9035
  */
8977
9036
  tableDepth = 0;
9037
+ /**
9038
+ * ¿Estamos emitiendo el vocabulario de clases de osu!?
9039
+ *
9040
+ * osu estiliza box, spoilerbox, notice, imagemap, youtube, los alineados y
9041
+ * los perfiles POR NOMBRE DE CLASE, no por estilo inline. Sobre una userpage
9042
+ * real el HTML de Quasar salía sin estilo porque emitía su propio
9043
+ * vocabulario (`<details>`, `.notice`, `.imagemap-container`…). Bajo
9044
+ * `dialect: 'osu'` se emiten las clases y la estructura de osu; el resto de
9045
+ * dialectos conserva la suya, que es la que sus hojas de estilo esperan.
9046
+ */
9047
+ isOsu() {
9048
+ return this.options.dialect === "osu";
9049
+ }
9050
+ /** osu recorta los saltos pegados a la apertura y al cierre de box/notice. */
9051
+ static trimOsuEdges(html) {
9052
+ return html.replace(/^[\t ]*\r?\n/, "").replace(/\r?\n[\t ]*$/, "");
9053
+ }
8978
9054
  idAttr(node) {
8979
9055
  if (_HTMLRenderer.idMode === "none") return "";
8980
9056
  if (_HTMLRenderer.idMode === "all") return ` data-node-id="${node.id}"`;
@@ -9105,7 +9181,7 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
9105
9181
  case "spoiler":
9106
9182
  return this.wrapInline("span", node, 'class="spoiler"');
9107
9183
  case "color":
9108
- return this.wrapInline("span", node, this.colorStyle(node));
9184
+ return this.renderColor(node);
9109
9185
  case "font_size":
9110
9186
  return this.wrapInline("span", node, this.fontSizeStyle(node));
9111
9187
  case "font":
@@ -9123,11 +9199,11 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
9123
9199
  case "audio":
9124
9200
  return this.renderAudio(node);
9125
9201
  case "center":
9126
- return this.wrapBlock("div", node, 'style="text-align:center;"');
9202
+ return this.renderAlignAs(node, "center");
9127
9203
  case "right":
9128
- return this.wrapBlock("div", node, 'style="text-align:right;"');
9204
+ return this.renderAlignAs(node, "right");
9129
9205
  case "left":
9130
- return this.wrapBlock("div", node, 'style="text-align:left;"');
9206
+ return this.renderAlignAs(node, "left");
9131
9207
  // Sigue siendo siempre `h2`, como antes: el nivel de BBCode no mapea al
9132
9208
  // de HTML y un `[heading=9]` daría un `<h9>` inválido. `data-bare-level`
9133
9209
  // sólo anota que el 2 lo puso el renderer, para que el camino de vuelta
@@ -9260,12 +9336,10 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
9260
9336
  case "paint":
9261
9337
  return this.renderEffectSegments(node, "paint");
9262
9338
  case "spacing":
9263
- if (this.options.osuBehaviour && this.isNextCodeBlock(node)) return "\n";
9264
- if (this.isTrailingBlockBoundary(node)) return "\n";
9265
- return this.isPrevBlockBoundary(node) ? "\n" : `<br${this.idAttr(node)}>`;
9339
+ if (this.isNewlineSwallowed(node)) return "\n";
9340
+ return `<br${this.idAttr(node)}>`;
9266
9341
  case "empty_line":
9267
- if (this.options.osuBehaviour && this.isImmediateEmptyLineBeforeCode(node)) return "\n";
9268
- if (this.isTrailingBlockBoundary(node)) return "\n";
9342
+ if (this.isNewlineSwallowed(node)) return "\n";
9269
9343
  return `<div class="bb-empty-line"${this.idAttr(node)}><br></div>`;
9270
9344
  case "group":
9271
9345
  return this.wrapInline("span", node, 'class="group"');
@@ -9301,58 +9375,241 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
9301
9375
  return this.escapeHtml(node.text || "");
9302
9376
  }
9303
9377
  }
9304
- // ─── Render Helpers ─────────────────────────────────────
9305
- /** osu! quirk: newlines immediately preceding a [code] block are completely ignored */
9306
- isNextCodeBlock(node) {
9307
- let next = node.nextSibling;
9308
- while (next && (next.kind === "spacing" || next.kind === "empty_line")) {
9309
- next = next.nextSibling;
9310
- }
9311
- return next?.kind === "code";
9312
- }
9313
- /** Checks if this is the LAST empty_line right before a code block (skipping only spacing) */
9314
- isImmediateEmptyLineBeforeCode(node) {
9315
- let next = node.nextSibling;
9316
- while (next && next.kind === "spacing") {
9317
- next = next.nextSibling;
9318
- }
9319
- return next?.kind === "code";
9320
- }
9321
- isPrevBlockBoundary(node) {
9322
- let prev = node.previousSibling;
9323
- while (prev) {
9324
- if (prev.kind === "spacing" || prev.kind === "empty_line") {
9325
- prev = prev.previousSibling;
9326
- continue;
9378
+ // ─── Newline swallowing ─────────────────────────────────
9379
+ //
9380
+ // osu! turns newlines into `<br />` with one flat rule at the very end of
9381
+ // `BBCodeFromDB::toHTML` — `str_replace("\n", '<br />')`. Every subtlety
9382
+ // lives BEFORE that line: each block pass is a regex that eats the newlines
9383
+ // touching its own tags, so those newlines are simply gone by the time the
9384
+ // flat rule runs. The amount eaten differs per tag, and the asymmetries are
9385
+ // not decorative:
9386
+ //
9387
+ // parseBox `\[box=…\]\n*` `\n*\[/box\]\n?`
9388
+ // parseCode `\[code\]\n*` `\n*\[/code\]\n?`
9389
+ // parseNotice `\[notice\]\n*` `\n*\[/notice\]\n?`
9390
+ // parseList `\s*\[\*\]` `\s*\[/list\]\n?\n?`
9391
+ // parseQuote `\[quote…\]\s*` `\s*\[/quote\]\n?\n?`
9392
+ // parseHeading — `\[/heading\]\n?`
9393
+ // parseImagemap — `\[/imagemap\]\n?`
9394
+ // parseAlignment strtr of `[centre]\n` and `[/centre]\n` — exactly one
9395
+ //
9396
+ // Quasar used to approximate all of that with two neighbourhood heuristics
9397
+ // (`isPrevBlockBoundary` / `isTrailingBlockBoundary`) that treated every
9398
+ // block alike, so they over-ate at `[centre]`/`[/imagemap]` and under-ate at
9399
+ // `[/list]`/`[/quote]`. This models the real rules instead.
9400
+ //
9401
+ // Deliberately NOT gated on the dialect: Miliastry is "osu with steroids"
9402
+ // and has to break lines the same way. Blocks that only exist in Miliastry
9403
+ // (tables, gallery, columns, scroll, …) have no osu counterpart to copy, so
9404
+ // they keep the legacy behaviour via {@link HTMLRenderer.LEGACY_BLOCK_RULE}.
9405
+ /** How many newlines a construct swallows around its own tags. */
9406
+ static NEWLINE_RULES = {
9407
+ // `\n*` inside both edges, one newline after the close.
9408
+ box: { afterOpen: "all", beforeClose: "all", beforeOpen: "none", afterClose: 1 },
9409
+ boxw: { afterOpen: "all", beforeClose: "all", beforeOpen: "none", afterClose: 1 },
9410
+ spoilerbox: { afterOpen: "all", beforeClose: "all", beforeOpen: "none", afterClose: 1 },
9411
+ notice: { afterOpen: "all", beforeClose: "all", beforeOpen: "none", afterClose: 1 },
9412
+ wnotice: { afterOpen: "all", beforeClose: "all", beforeOpen: "none", afterClose: 1 },
9413
+ code: { afterOpen: "all", beforeClose: "all", beforeOpen: "none", afterClose: 1 },
9414
+ // `\s*` — not just newlines — and TWO newlines after the close.
9415
+ quote: { afterOpen: "whitespace", beforeClose: "whitespace", beforeOpen: "none", afterClose: 2 },
9416
+ // `[list]` itself eats nothing after its opening tag: the pass that eats
9417
+ // is `\s*\[\*\]`, which needs an item to follow. `[list]\n\nloose text`
9418
+ // keeps both newlines; `[list]\n[*]a` loses one to the item, not the list.
9419
+ list: { afterOpen: "none", beforeClose: "whitespace", beforeOpen: "none", afterClose: 2 },
9420
+ // `\s*\[\*\]`. The matching `[/*]` of the table exists only in legacy
9421
+ // phpBB rows — `BBCodeForDB` never emits one — so the item's close is
9422
+ // width-less here and its two-newline budget is unreachable by design;
9423
+ // `[*]a\n\n[*]b` loses both newlines to the NEXT item's `\s*`, which is
9424
+ // the same output by a different route.
9425
+ list_item: { afterOpen: "none", beforeClose: "none", beforeOpen: "whitespace", afterClose: 0 },
9426
+ // strtr with `[centre]\n` / `[/centre]\n`: exactly one on each outer edge,
9427
+ // and nothing before the close — `x\n[/centre]` really does keep its `<br>`.
9428
+ center: { afterOpen: "one", beforeClose: "none", beforeOpen: "none", afterClose: 1 },
9429
+ left: { afterOpen: "one", beforeClose: "none", beforeOpen: "none", afterClose: 1 },
9430
+ right: { afterOpen: "one", beforeClose: "none", beforeOpen: "none", afterClose: 1 },
9431
+ align: { afterOpen: "one", beforeClose: "none", beforeOpen: "none", afterClose: 1 },
9432
+ heading: { afterOpen: "none", beforeClose: "none", beforeOpen: "none", afterClose: 1 },
9433
+ imagemap: { afterOpen: "none", beforeClose: "none", beforeOpen: "none", afterClose: 1 },
9434
+ // `[img]` is inline in osu and swallows nothing at all.
9435
+ image: { afterOpen: "none", beforeClose: "none", beforeOpen: "none", afterClose: 0 },
9436
+ document: { afterOpen: "none", beforeClose: "none", beforeOpen: "none", afterClose: 0 }
9437
+ };
9438
+ /**
9439
+ * What a Miliastry-only block does. This is what the old
9440
+ * `isPrevBlockBoundary` / `isTrailingBlockBoundary` pair did for every block:
9441
+ * eat the first newline after the open, every newline before the close, and
9442
+ * the first newline after the close.
9443
+ */
9444
+ static LEGACY_BLOCK_RULE = {
9445
+ afterOpen: "one",
9446
+ beforeClose: "all",
9447
+ beforeOpen: "none",
9448
+ afterClose: 1
9449
+ };
9450
+ /**
9451
+ * Containers whose opening tag occupies no source text, so a backwards scan
9452
+ * has to walk straight through them.
9453
+ */
9454
+ static WIDTHLESS_OPEN = /* @__PURE__ */ new Set(["paragraph", "group"]);
9455
+ /**
9456
+ * Same for the closing side. `list_item` is here because `[/*]` is never
9457
+ * written: an item ends where the next `[*]` or the `[/list]` begins, so
9458
+ * `\s*\[/list\]` sees the newline that Quasar stores inside the item.
9459
+ */
9460
+ static WIDTHLESS_CLOSE = /* @__PURE__ */ new Set(["paragraph", "group", "list_item"]);
9461
+ newlineRule(kind) {
9462
+ const rule = _HTMLRenderer.NEWLINE_RULES[kind];
9463
+ if (rule) return rule;
9464
+ return this.BLOCK_TAGS.has(kind) ? _HTMLRenderer.LEGACY_BLOCK_RULE : null;
9465
+ }
9466
+ static isNewlineNode(node) {
9467
+ return node.kind === "spacing" || node.kind === "empty_line";
9468
+ }
9469
+ static isBlankText(node) {
9470
+ return node.kind === "text" && node.children.length === 0 && node.text.trim() === "";
9471
+ }
9472
+ /**
9473
+ * Whether this `spacing` / `empty_line` leaf is eaten by a neighbouring tag
9474
+ * and therefore renders nothing.
9475
+ *
9476
+ * Each leaf is exactly ONE source newline (the parser splits a run into one
9477
+ * node per `\n`), so the four scans below can be read straight off the
9478
+ * regexes they mirror. A newline eaten by any of them is eaten: osu's passes
9479
+ * run in a fixed order, but since a consumed newline is consumed whichever
9480
+ * pass claimed it, the union is enough — the per-pass order only matters for
9481
+ * a budget that could be spent elsewhere, and budgets here are counted from
9482
+ * the tag outwards, exactly as `\n?\n?` counts.
9483
+ */
9484
+ isNewlineSwallowed(node) {
9485
+ return this.eatenByOpeningTag(node) || this.eatenByClosingTag(node) || this.eatenAfterClosingTag(node) || this.eatenBeforeOpeningTag(node);
9486
+ }
9487
+ /** `\[box\]\n*`, `\[quote\]\s*`, `[centre]\n`. */
9488
+ eatenByOpeningTag(node) {
9489
+ let cur = node;
9490
+ let newlinesBetween = 0;
9491
+ let blankBetween = false;
9492
+ for (; ; ) {
9493
+ const prev = cur.previousSibling;
9494
+ if (prev) {
9495
+ if (_HTMLRenderer.isNewlineNode(prev)) {
9496
+ newlinesBetween++;
9497
+ cur = prev;
9498
+ continue;
9499
+ }
9500
+ if (_HTMLRenderer.isBlankText(prev)) {
9501
+ blankBetween = true;
9502
+ cur = prev;
9503
+ continue;
9504
+ }
9505
+ return false;
9327
9506
  }
9328
- if (prev.kind === "text" && prev.text.trim() === "") {
9329
- prev = prev.previousSibling;
9507
+ const parent = cur.parent;
9508
+ if (!parent) return false;
9509
+ if (_HTMLRenderer.WIDTHLESS_OPEN.has(parent.kind)) {
9510
+ cur = parent;
9330
9511
  continue;
9331
9512
  }
9332
- break;
9513
+ const rule = this.newlineRule(parent.kind);
9514
+ if (!rule) return false;
9515
+ switch (rule.afterOpen) {
9516
+ case "whitespace":
9517
+ return true;
9518
+ // `\n*` matches newlines only: a stray space breaks the run.
9519
+ case "all":
9520
+ return !blankBetween;
9521
+ case "one":
9522
+ return !blankBetween && newlinesBetween === 0;
9523
+ default:
9524
+ return false;
9525
+ }
9333
9526
  }
9334
- if (prev && this.BLOCK_TAGS.has(prev.kind) && prev.kind !== "image" && prev.kind !== "imagemap") return true;
9335
- if (!prev && node.parent && this.BLOCK_TAGS.has(node.parent.kind) && node.parent.kind !== "image" && node.parent.kind !== "imagemap") return true;
9336
- return false;
9337
9527
  }
9338
- isTrailingBlockBoundary(node) {
9339
- let next = node.nextSibling;
9340
- while (next) {
9341
- if (next.kind === "spacing" || next.kind === "empty_line") {
9342
- next = next.nextSibling;
9528
+ /** `\n*\[/box\]`, `\s*\[/quote\]`, `\s*\[/list\]`. */
9529
+ eatenByClosingTag(node) {
9530
+ let cur = node;
9531
+ let blankBetween = false;
9532
+ for (; ; ) {
9533
+ const next = cur.nextSibling;
9534
+ if (next) {
9535
+ if (_HTMLRenderer.isNewlineNode(next)) {
9536
+ cur = next;
9537
+ continue;
9538
+ }
9539
+ if (_HTMLRenderer.isBlankText(next)) {
9540
+ blankBetween = true;
9541
+ cur = next;
9542
+ continue;
9543
+ }
9544
+ return false;
9545
+ }
9546
+ const parent = cur.parent;
9547
+ if (!parent) return false;
9548
+ if (_HTMLRenderer.WIDTHLESS_CLOSE.has(parent.kind)) {
9549
+ cur = parent;
9343
9550
  continue;
9344
9551
  }
9345
- if (next.kind === "text" && next.text.trim() === "") {
9346
- next = next.nextSibling;
9552
+ const rule = this.newlineRule(parent.kind);
9553
+ if (!rule) return false;
9554
+ switch (rule.beforeClose) {
9555
+ case "whitespace":
9556
+ return true;
9557
+ case "all":
9558
+ return !blankBetween;
9559
+ default:
9560
+ return false;
9561
+ }
9562
+ }
9563
+ }
9564
+ /** `\[/box\]\n?`, `\[/list\]\n?\n?`. */
9565
+ eatenAfterClosingTag(node) {
9566
+ let cur = node;
9567
+ let newlinesBetween = 0;
9568
+ for (; ; ) {
9569
+ const prev = cur.previousSibling;
9570
+ if (!prev) {
9571
+ const parent = cur.parent;
9572
+ if (parent && _HTMLRenderer.WIDTHLESS_OPEN.has(parent.kind)) {
9573
+ cur = parent;
9574
+ continue;
9575
+ }
9576
+ return false;
9577
+ }
9578
+ if (_HTMLRenderer.isNewlineNode(prev)) {
9579
+ newlinesBetween++;
9580
+ cur = prev;
9347
9581
  continue;
9348
9582
  }
9349
- break;
9583
+ let closer = prev;
9584
+ while (_HTMLRenderer.WIDTHLESS_CLOSE.has(closer.kind) && closer.children.length > 0) {
9585
+ closer = closer.children[closer.children.length - 1];
9586
+ }
9587
+ const rule = this.newlineRule(closer.kind);
9588
+ return rule !== null && newlinesBetween < rule.afterClose;
9350
9589
  }
9351
- if (!next && node.parent && this.BLOCK_TAGS.has(node.parent.kind) && node.parent.kind !== "document") {
9352
- return true;
9590
+ }
9591
+ /** `\s*\[\*\]` — the only pass that eats whitespace BEFORE an opening tag. */
9592
+ eatenBeforeOpeningTag(node) {
9593
+ let cur = node;
9594
+ for (; ; ) {
9595
+ const next = cur.nextSibling;
9596
+ if (next) {
9597
+ if (_HTMLRenderer.isNewlineNode(next) || _HTMLRenderer.isBlankText(next)) {
9598
+ cur = next;
9599
+ continue;
9600
+ }
9601
+ return this.newlineRule(next.kind)?.beforeOpen === "whitespace";
9602
+ }
9603
+ const parent = cur.parent;
9604
+ if (!parent) return false;
9605
+ if (_HTMLRenderer.WIDTHLESS_CLOSE.has(parent.kind)) {
9606
+ cur = parent;
9607
+ continue;
9608
+ }
9609
+ return false;
9353
9610
  }
9354
- return false;
9355
9611
  }
9612
+ // ─── Render Helpers ─────────────────────────────────────
9356
9613
  renderError(node) {
9357
9614
  const errorMsg = this.escapeHtml(node.metadata?.message || node.text || "Syntax Error");
9358
9615
  const content = this.renderChildren(node) || this.escapeHtml(node.text || "");
@@ -9404,6 +9661,41 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
9404
9661
  return !!raw && /^[a-z]{2,20}(?: [a-z]{2,20})?$|^[1-9]00$/i.test(raw.trim());
9405
9662
  }
9406
9663
  /** Read a metadata field, falling back to the raw tag attribute. */
9664
+ /**
9665
+ * Lo único que osu! acepta en `[color=…]`.
9666
+ *
9667
+ * Su `BBCodeForDB::parseColour` sella el tag con un uid sólo si el valor
9668
+ * matchea `#[[:xdigit:]]{6}` o `[[:alpha:]]+` — nada más. No valida que el
9669
+ * nombre sea un color CSS de verdad (`banana` pasa), pero `#fff`, `#ffffffff`,
9670
+ * `rgb(...)`, `$token` o un hex sin `#` no pasan. Sin uid, la segunda pasada
9671
+ * no ve el tag y el opener *y* el closer quedan como texto en la página.
9672
+ */
9673
+ static OSU_COLOR_RE = /^(?:#[0-9a-fA-F]{6}|[a-zA-Z]+)$/;
9674
+ /**
9675
+ * `[color]` con el vocabulario de cada dialecto.
9676
+ *
9677
+ * Miliastry (y Lyne) aceptan a propósito más que osu: `#RGB`, `#RGBA`, un
9678
+ * `$token` de diseño, nombres propios. Bajo `dialect: 'osu'` eso es una
9679
+ * mentira: el editor pintaría color donde la página publicada muestra el
9680
+ * BBCode crudo. Así que replicamos lo que hace osu — literal el opener,
9681
+ * literal el closer, y los hijos renderizados normalmente en el medio.
9682
+ *
9683
+ * El chequeo mira el texto crudo del atributo, no el valor saneado: osu
9684
+ * matchea sobre la fuente, así que `[color="#ffffff"]` (con comillas) también
9685
+ * se le escapa.
9686
+ */
9687
+ renderColor(node) {
9688
+ if (this.options.dialect === "osu") {
9689
+ const text = node.text || "";
9690
+ const eq = text.indexOf("=");
9691
+ const raw = eq >= 0 ? text.slice(eq + 1) : text ? "" : nodeAttrValue(node, "color");
9692
+ if (!_HTMLRenderer.OSU_COLOR_RE.test(raw)) {
9693
+ const opener = eq >= 0 ? `[color${text}]` : `[${text || "color"}]`;
9694
+ return this.escapeHtml(opener) + this.renderChildren(node) + this.escapeHtml("[/color]");
9695
+ }
9696
+ }
9697
+ return this.wrapInline("span", node, this.colorStyle(node));
9698
+ }
9407
9699
  colorStyle(node) {
9408
9700
  const color = sanitizeColor(nodeAttrValue(node, "color"), this.tokenResolver);
9409
9701
  return color ? `style="color:${color};"` : "";
@@ -9445,6 +9737,11 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
9445
9737
  return `<strong${entity}><a${this.idAttr(node)} href="${this.escapeHtml(link.href)}"${ext}>${content}</a></strong>`;
9446
9738
  }
9447
9739
  }
9740
+ if (type === "profile" && this.isOsu()) {
9741
+ const key2 = val || `@${this.collectNodeText(node)}`;
9742
+ const href = `https://osu.ppy.sh/users/${encodeURIComponent(key2)}`;
9743
+ return `<a${this.idAttr(node)}${entity} class="user-name js-usercard" data-user-id="${this.escapeHtml(key2)}" href="${href}">${content}</a>`;
9744
+ }
9448
9745
  if (type === "profile") {
9449
9746
  const url = this.options.theme === "lyne" || this.options.dialect === "lyne" ? `/u/${encodeURIComponent(val || content)}` : `https://osu.ppy.sh/users/${this.escapeHtml(val || content)}`;
9450
9747
  return `<strong${entity}><a${this.idAttr(node)} href="${url}" target="_blank" rel="noopener">${content}</a></strong>`;
@@ -9499,13 +9796,25 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
9499
9796
  if (!id) return this.mediaError("youtube", "[youtube] missing video ID");
9500
9797
  const ytMatch = /(?:youtu\.be\/|v=|\/embed\/|\/shorts\/)([\w-]{11})/.exec(id);
9501
9798
  if (ytMatch) id = ytMatch[1];
9502
- return `<iframe${this.idAttr(node)} class="bb-youtube" data-youtube="${this.escapeHtml(id)}" src="https://www.youtube.com/embed/${this.escapeHtml(id)}" frameborder="0" allowfullscreen></iframe>`;
9799
+ const cls = this.isOsu() ? "u-embed-wide u-embed-wide--bbcode" : "bb-youtube";
9800
+ const rel = this.isOsu() ? "?rel=0" : "";
9801
+ return `<iframe${this.idAttr(node)} class="${cls}" data-youtube="${this.escapeHtml(id)}" src="https://www.youtube.com/embed/${this.escapeHtml(id)}${rel}" frameborder="0" allowfullscreen></iframe>`;
9503
9802
  }
9504
9803
  renderAudio(node) {
9505
9804
  let src = String(node.metadata?.src ?? "") || node.text || "";
9506
9805
  if (this.options.mediaProxy && src) {
9507
9806
  src = this.options.mediaProxy(src);
9508
9807
  }
9808
+ const isLyne = this.options.theme === "lyne" || this.options.dialect === "lyne";
9809
+ if (isLyne) {
9810
+ const rawName = src.split("?")[0].split("#")[0].split("/").filter(Boolean).pop() || "audio_track.mp3";
9811
+ let fileName = rawName;
9812
+ try {
9813
+ fileName = decodeURIComponent(rawName);
9814
+ } catch {
9815
+ }
9816
+ return `<div${this.idAttr(node)} class="lx-audio bb-audio" data-src="${this.escapeHtml(src)}"><audio preload="metadata" src="${this.escapeHtml(src)}"></audio><div class="lx-track" role="progressbar" aria-label="Audio progress"><div class="fill"></div></div><div class="lx-row"><button type="button" class="lx-btn" aria-label="Play"><svg viewBox="0 0 16 16"><path d="M3 1.5 14 8 3 14.5z"/></svg></button><div class="lx-title"><div class="name">${this.escapeHtml(fileName)}</div><div class="label"><span class="dot"></span> <span class="status-text">audio</span></div></div><div class="lx-vol"><button type="button" class="lx-vol-btn" aria-label="Mute"><svg viewBox="0 0 16 16"><path d="M10.707 11.182A4.5 4.5 0 0 0 12.025 8a4.5 4.5 0 0 0-1.318-3.182L10 5.525A3.5 3.5 0 0 1 11.025 8 3.5 3.5 0 0 1 10 10.475zM6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06"/></svg></button><input type="range" class="lx-vol-slider" min="0" max="1" step="0.01" value="0.2" aria-label="Volume" style="background:linear-gradient(to right, var(--color-accent, #2EE6E2) 20%, var(--color-inset-well, #080D20) 20%);" /></div><button type="button" class="lx-speed" aria-label="Playback speed">1.0\xD7</button><div class="lx-time"><span class="cur">0:00</span> / <span class="total">\u2013:\u2013\u2013</span></div></div></div>`;
9817
+ }
9509
9818
  return `<audio${this.idAttr(node)} controls src="${this.escapeHtml(src)}" class="bb-audio"></audio>`;
9510
9819
  }
9511
9820
  hexToRgba(hex, alpha) {
@@ -9531,6 +9840,10 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
9531
9840
  const warningIcon = warning ? `<span aria-hidden class="bb-notice-mark"${markStyle}>\u26A0</span>` : "";
9532
9841
  return `<div${this.idAttr(node)} class="notice bb-cut-panel bb-notice${warning ? " bb-wnotice" : ""}" role="note"${styleAttr}>${warningIcon}<div class="bb-notice-body">${content}</div></div>`;
9533
9842
  }
9843
+ if (this.isOsu()) {
9844
+ const content = _HTMLRenderer.trimOsuEdges(this.renderChildren(node));
9845
+ return `<div${this.idAttr(node)} class="well">${content}</div>`;
9846
+ }
9534
9847
  return this.wrapBlock("div", node, 'class="notice"');
9535
9848
  }
9536
9849
  renderTables(node) {
@@ -9636,7 +9949,21 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
9636
9949
  renderAlign(node) {
9637
9950
  const alignVal = (String(node.metadata?.align ?? "") || nodeAttrValue(node) || "center").trim().toLowerCase();
9638
9951
  const validAlign = alignVal === "left" || alignVal === "right" ? alignVal : "center";
9639
- return this.wrapBlock("div", node, `style="text-align:${validAlign};"`);
9952
+ return this.renderAlignAs(node, validAlign);
9953
+ }
9954
+ /**
9955
+ * `[centre]` / `[left]` / `[right]` (y `[align=…]`).
9956
+ *
9957
+ * osu! no usa `text-align` inline: estiliza el bloque por nombre de clase,
9958
+ * con la grafía británica `centre`. Fuera del dialecto osu el estilo inline
9959
+ * se mantiene, porque ni Miliastry ni Lyne traen esas reglas.
9960
+ */
9961
+ renderAlignAs(node, align) {
9962
+ if (this.isOsu()) {
9963
+ const name = align === "center" ? "centre" : align;
9964
+ return this.wrapBlock("div", node, `class="bbcode__align-${name}"`);
9965
+ }
9966
+ return this.wrapBlock("div", node, `style="text-align:${align};"`);
9640
9967
  }
9641
9968
  renderEffect(node) {
9642
9969
  const raw = (String(node.metadata?.effectType ?? "") || nodeAttrValue(node) || "glow").toLowerCase().trim();
@@ -9825,7 +10152,25 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
9825
10152
  }
9826
10153
  return `<blockquote${this.idAttr(node)}>${content}</blockquote>`;
9827
10154
  }
10155
+ /**
10156
+ * La estructura exacta que `bbcode-spoilerbox` de osu-web espera.
10157
+ *
10158
+ * El toggle de osu es JS: `js-spoilerbox__link` es el gancho del click y
10159
+ * `js-spoilerbox__body` el panel que abre. Si falta cualquiera de las dos
10160
+ * clases el box queda mudo, así que la estructura no es decorativa.
10161
+ */
10162
+ renderOsuSpoilerbox(node, title, extra) {
10163
+ const content = _HTMLRenderer.trimOsuEdges(this.renderChildren(node));
10164
+ return `<div${this.idAttr(node)} class="js-spoilerbox bbcode-spoilerbox"${extra}><a class="js-spoilerbox__link bbcode-spoilerbox__link" href="#"><span class="bbcode-spoilerbox__link-icon"></span><span class="bbcode-spoilerbox__link-text">${title}</span></a><div class="js-spoilerbox__body bbcode-spoilerbox__body">${content}</div></div>`;
10165
+ }
10166
+ /** El rótulo de un box bajo osu: el del autor, o `SPOILER` en mayúsculas. */
10167
+ osuBoxTitle(node) {
10168
+ return this.hasOwnTitle(node) ? this.renderTitle(node, "SPOILER") : "SPOILER";
10169
+ }
9828
10170
  renderSpoilerbox(node) {
10171
+ if (this.isOsu()) {
10172
+ return this.renderOsuSpoilerbox(node, this.osuBoxTitle(node), this.bareTitleAttr(node));
10173
+ }
9829
10174
  const title = this.renderTitle(node, "Spoiler");
9830
10175
  const bare = this.bareTitleAttr(node);
9831
10176
  const content = this.renderChildren(node);
@@ -9835,6 +10180,9 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
9835
10180
  return `<details${this.idAttr(node)}${bare}${accent}><summary><span class="bb-box-heading">${title}</span></summary><div class="${bodyCls}">${content}</div></details>`;
9836
10181
  }
9837
10182
  renderBox(node) {
10183
+ if (this.isOsu()) {
10184
+ return this.renderOsuSpoilerbox(node, this.osuBoxTitle(node), this.bareTitleAttr(node));
10185
+ }
9838
10186
  const title = this.renderTitle(node, "Box");
9839
10187
  const bare = this.bareTitleAttr(node);
9840
10188
  const content = this.renderChildren(node);
@@ -9852,9 +10200,18 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
9852
10200
  * escrito y devolvía `[box=Box]`.
9853
10201
  */
9854
10202
  bareTitleAttr(node) {
10203
+ return this.hasOwnTitle(node) ? "" : ' data-bare-title="1"';
10204
+ }
10205
+ /**
10206
+ * ¿El título del box lo escribió el autor, o es el relleno del parser?
10207
+ *
10208
+ * `BBCodeToGreenNode` ya deja `metadata.title = 'Box'`/`'Spoiler'` para un
10209
+ * tag pelado, así que el `fallback` de `renderTitle` nunca llega a usarse:
10210
+ * quien quiera otro rótulo por defecto tiene que preguntar por aquí.
10211
+ */
10212
+ hasOwnTitle(node) {
9855
10213
  const raw = node.metadata?.rawTitle;
9856
- const hasOwnTitle = raw !== void 0 ? String(raw) !== "" : node.metadata?.title !== void 0;
9857
- return hasOwnTitle ? "" : ' data-bare-title="1"';
10214
+ return raw !== void 0 ? String(raw) !== "" : node.metadata?.title !== void 0;
9858
10215
  }
9859
10216
  renderTitle(node, fallback) {
9860
10217
  const titleNodes = node.metadata?.titleNodes;
@@ -9953,8 +10310,17 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
9953
10310
  if (areaUrl && !areaUrl.startsWith("http://") && !areaUrl.startsWith("https://") && !areaUrl.startsWith("mailto:")) {
9954
10311
  areaUrl = "https://" + areaUrl;
9955
10312
  }
10313
+ if (this.isOsu()) {
10314
+ const pos = `left:${x}%;top:${y}%;width:${w}%;height:${h}%;`;
10315
+ const title = ` title="${this.escapeHtml(label)}"`;
10316
+ areas += url === "#" ? `<span class="imagemap__link" style="${pos}"${title}></span>` : `<a class="imagemap__link" href="${this.escapeHtml(areaUrl)}" style="${pos}"${title}></a>`;
10317
+ continue;
10318
+ }
9956
10319
  areas += `<a${this.idAttr(node)} href="${this.escapeHtml(areaUrl)}" target="_blank" rel="noopener" class="imagemap-area bbcode-imap-area" style="position:absolute;left:${x}%;top:${y}%;width:${w}%;height:${h}%;" title="${this.escapeHtml(label || "Link")}"></a>`;
9957
10320
  }
10321
+ if (this.isOsu()) {
10322
+ return `<div${this.idAttr(node)} class="imagemap"><img class="imagemap__image" loading="lazy" src="${this.escapeHtml(imageUrl)}" alt="">${areas}</div>`;
10323
+ }
9958
10324
  return `<div${this.idAttr(node)} class="imagemap-container bbcode-imagemap" style="position:relative;display:inline-block;"><img src="${this.escapeHtml(imageUrl)}" alt="imagemap" style="max-width:100%;height:auto;display:block;">${areas}</div>`;
9959
10325
  }
9960
10326
  collectNodeText(node) {
@@ -10239,38 +10605,39 @@ function morphNodes(parent, newParent) {
10239
10605
  oldNode.nodeValue = newNode.nodeValue;
10240
10606
  }
10241
10607
  } else if (oldNode.nodeType === 1 && newNode.nodeType === 1 && oldNode.tagName === newNode.tagName) {
10242
- const oldEl = oldNode;
10243
- const newEl = newNode;
10244
- if (oldEl.tagName === "DETAILS" && oldEl.hasAttribute("open")) {
10245
- newEl.setAttribute("open", "");
10246
- }
10247
- if (oldEl.classList.contains("open")) {
10248
- newEl.classList.add("open");
10249
- }
10250
- if (oldEl.classList.contains("is-open")) {
10251
- newEl.classList.add("is-open");
10252
- }
10253
- const newAttrs = newEl.attributes;
10254
- for (let i2 = 0; i2 < newAttrs.length; i2++) {
10255
- const attr = newAttrs[i2];
10256
- if (oldEl.getAttribute(attr.name) !== attr.value) {
10257
- oldEl.setAttribute(attr.name, attr.value);
10258
- }
10259
- }
10260
- const oldAttrs = oldEl.attributes;
10261
- for (let i2 = oldAttrs.length - 1; i2 >= 0; i2--) {
10262
- const name = oldAttrs[i2].name;
10263
- if (!newEl.hasAttribute(name)) {
10264
- oldEl.removeAttribute(name);
10265
- }
10266
- }
10267
- morphNodes(oldEl, newEl);
10608
+ morphElement(oldNode, newNode);
10268
10609
  } else {
10269
10610
  oldNode.parentNode.replaceChild(newNode.cloneNode(true), oldNode);
10270
10611
  }
10271
10612
  }
10272
10613
  }
10273
10614
  }
10615
+ function morphElement(oldEl, newEl) {
10616
+ if (oldEl.tagName === "DETAILS" && oldEl.hasAttribute("open")) {
10617
+ newEl.setAttribute("open", "");
10618
+ }
10619
+ if (oldEl.classList.contains("open")) {
10620
+ newEl.classList.add("open");
10621
+ }
10622
+ if (oldEl.classList.contains("is-open")) {
10623
+ newEl.classList.add("is-open");
10624
+ }
10625
+ const newAttrs = newEl.attributes;
10626
+ for (let i = 0; i < newAttrs.length; i++) {
10627
+ const attr = newAttrs[i];
10628
+ if (oldEl.getAttribute(attr.name) !== attr.value) {
10629
+ oldEl.setAttribute(attr.name, attr.value);
10630
+ }
10631
+ }
10632
+ const oldAttrs = oldEl.attributes;
10633
+ for (let i = oldAttrs.length - 1; i >= 0; i--) {
10634
+ const name = oldAttrs[i].name;
10635
+ if (!newEl.hasAttribute(name)) {
10636
+ oldEl.removeAttribute(name);
10637
+ }
10638
+ }
10639
+ morphNodes(oldEl, newEl);
10640
+ }
10274
10641
 
10275
10642
  // src/Visitors/BlockPatcher.ts
10276
10643
  var caches = /* @__PURE__ */ new WeakMap();
@@ -10308,6 +10675,9 @@ function getCache(container) {
10308
10675
  function blockKey(node, index) {
10309
10676
  return node.id ?? `__block_${index}`;
10310
10677
  }
10678
+ function isContextSensitive(node) {
10679
+ return node.kind === "spacing" || node.kind === "empty_line";
10680
+ }
10311
10681
  function nodeFromHtml(html) {
10312
10682
  const t = document.createElement("template");
10313
10683
  t.innerHTML = html;
@@ -10326,7 +10696,7 @@ function renderedTag(html) {
10326
10696
  return m ? m[1].toUpperCase() : null;
10327
10697
  }
10328
10698
  function shouldMorphInPlace(element, run) {
10329
- return run.kind === "element" && !!run.node.id && run.node.children.length > 0 && canMorphInPlace(run.node) && renderedTag(run.html) === element.tagName;
10699
+ return run.kind === "element" && !!run.node.id && canMorphInPlace(run.node) && renderedTag(run.html) === element.tagName;
10330
10700
  }
10331
10701
  function buildRuns(blocks, keys, getHtml, baseStart) {
10332
10702
  const runs = [];
@@ -10392,7 +10762,7 @@ function reconcileKeyed(container, rootNode, keys, renderer, cache, options) {
10392
10762
  let patched = 0;
10393
10763
  try {
10394
10764
  runs = buildRuns(blocks, keys, (node, key) => {
10395
- if (cache.lastNode.get(key) === node) {
10765
+ if (cache.lastNode.get(key) === node && !isContextSensitive(node)) {
10396
10766
  return { html: cache.lastHtml.get(key) ?? "", kind: cache.lastClass.get(key) ?? "none" };
10397
10767
  }
10398
10768
  const html = renderer.render(node);
@@ -10433,7 +10803,12 @@ function reconcileKeyed(container, rootNode, keys, renderer, cache, options) {
10433
10803
  continue;
10434
10804
  }
10435
10805
  if (element.nodeType === 1 && shouldMorphInPlace(element, run)) {
10436
- morphHTML(element, renderer.renderChildren(run.node));
10806
+ const newEl = nodeFromHtml(run.html);
10807
+ if (newEl && newEl.nodeType === 1 && newEl.tagName === element.tagName) {
10808
+ morphElement(element, newEl);
10809
+ } else {
10810
+ container.replaceChild(newEl, element);
10811
+ }
10437
10812
  } else {
10438
10813
  container.replaceChild(nodeFromHtml(run.html), element);
10439
10814
  }
@@ -10500,7 +10875,7 @@ function reconcileWindowed(container, rootNode, change, renderer, cache, options
10500
10875
  let runs;
10501
10876
  try {
10502
10877
  runs = buildRuns(blocks, keys, (node2, key) => {
10503
- if (cache.lastNode.get(key) === node2) {
10878
+ if (cache.lastNode.get(key) === node2 && !isContextSensitive(node2)) {
10504
10879
  return { html: cache.lastHtml.get(key) ?? "", kind: cache.lastClass.get(key) ?? "none" };
10505
10880
  }
10506
10881
  const html = renderer.render(node2);
@@ -10659,7 +11034,12 @@ function reconcileWindowed(container, rootNode, change, renderer, cache, options
10659
11034
  continue;
10660
11035
  }
10661
11036
  if (element.nodeType === 1 && shouldMorphInPlace(element, run)) {
10662
- morphHTML(element, renderer.renderChildren(run.node));
11037
+ const newEl = nodeFromHtml(run.html);
11038
+ if (newEl && newEl.nodeType === 1 && newEl.tagName === element.tagName) {
11039
+ morphElement(element, newEl);
11040
+ } else {
11041
+ container.replaceChild(newEl, element);
11042
+ }
10663
11043
  } else {
10664
11044
  container.replaceChild(nodeFromHtml(run.html), element);
10665
11045
  }
@@ -11881,6 +12261,13 @@ function domToGreenTree(root) {
11881
12261
  children.push(greenLeaf("spacing", ""));
11882
12262
  }
11883
12263
  });
12264
+ } else if (el.classList.contains("lx-audio")) {
12265
+ kind = "audio";
12266
+ const audioSrc = el.getAttribute("data-src") || el.querySelector("audio")?.getAttribute("src") || "";
12267
+ if (audioSrc) {
12268
+ children.push(greenLeaf("text", audioSrc));
12269
+ currentOffset += audioSrc.length;
12270
+ }
11884
12271
  } else if (el.style.textAlign === "center") kind = "center";
11885
12272
  else if (el.style.textAlign === "right") kind = "right";
11886
12273
  else if (el.style.textAlign === "left") kind = "left";
@@ -12614,7 +13001,7 @@ var GreenNodePool = class _GreenNodePool {
12614
13001
  };
12615
13002
 
12616
13003
  // src/BBCode/BBCodeDocumentModel.ts
12617
- var BBCodeDocumentModel = class extends DocumentModel {
13004
+ var BBCodeDocumentModel = class _BBCodeDocumentModel extends DocumentModel {
12618
13005
  _strictMode;
12619
13006
  _dialect;
12620
13007
  /** Per-document interner, or null when interning is off. */
@@ -12650,6 +13037,35 @@ var BBCodeDocumentModel = class extends DocumentModel {
12650
13037
  const r = renderer ?? new HTMLRenderer({ dialect: this._dialect, theme: this._dialect === "lyne" ? "lyne" : "osu" });
12651
13038
  return r.render(this.redRoot);
12652
13039
  }
13040
+ /**
13041
+ * Fast-path read-only rendering for forum posts, comments, and static views.
13042
+ *
13043
+ * Completely bypasses semantic analysis (linter), undo stack allocations, and
13044
+ * editor node ids (idMode: 'none') to maximize throughput and minimize memory.
13045
+ */
13046
+ static renderForum(source, options = {}) {
13047
+ const previousIdMode = HTMLRenderer.idMode;
13048
+ HTMLRenderer.idMode = "none";
13049
+ try {
13050
+ const dialect = options.dialect ?? (options.theme === "lyne" ? "lyne" : "miliastry");
13051
+ const doc = new _BBCodeDocumentModel({
13052
+ source: source || " ",
13053
+ dialect,
13054
+ autoAnalyze: false,
13055
+ maxUndo: 0,
13056
+ incremental: false
13057
+ });
13058
+ if (!doc.redRoot) return "";
13059
+ const renderer = new HTMLRenderer({
13060
+ ...options,
13061
+ dialect,
13062
+ registry: options.registry ?? doc.tagRegistry
13063
+ });
13064
+ return renderer.render(doc.redRoot);
13065
+ } finally {
13066
+ HTMLRenderer.idMode = previousIdMode;
13067
+ }
13068
+ }
12653
13069
  /**
12654
13070
  * Parse BBCode text directly to a GreenNode tree using the
12655
13071
  * DocumentEngine's built-in BBCode Lexer + Parser.
@@ -12700,6 +13116,7 @@ var BBCodeDocumentModel = class extends DocumentModel {
12700
13116
  return new BBCodeExporter(this.tagRegistry, exportTarget).export(root);
12701
13117
  }
12702
13118
  };
13119
+ var renderForumBBCode = BBCodeDocumentModel.renderForum;
12703
13120
 
12704
13121
  // src/Edits/Rules/Rule.ts
12705
13122
  function endOf(p) {
@@ -13262,31 +13679,32 @@ function transformRange(range3, changes) {
13262
13679
  // src/Visuals/BoxDrawer.ts
13263
13680
  var DEFAULT_DURATION_MS = 400;
13264
13681
  var MIN_DURATION_MS = 120;
13682
+ var OSU_OPEN_CLASS = "js-spoilerbox--open";
13265
13683
  var DEFAULT_EASING = "cubic-bezier(0.33, 1, 0.68, 1)";
13266
13684
  var running = /* @__PURE__ */ new WeakMap();
13267
13685
  function prefersReducedMotion() {
13268
13686
  return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
13269
13687
  }
13270
- function toggleBoxWithDrawer(details, options = {}) {
13688
+ function animateDrawer(details, isOpen, setOpen, options = {}) {
13271
13689
  if (typeof details.animate !== "function" || prefersReducedMotion()) {
13272
- details.open = !details.open;
13690
+ setOpen(!isOpen());
13273
13691
  return;
13274
13692
  }
13275
13693
  const from = details.getBoundingClientRect().height;
13276
13694
  const previous = running.get(details);
13277
13695
  const restoreOverflow = previous ? previous.restoreOverflow : details.style.overflow;
13278
- const opening = previous ? !previous.opening : !details.open;
13696
+ const opening = previous ? !previous.opening : !isOpen();
13279
13697
  previous?.animation.cancel();
13280
13698
  let collapsed;
13281
13699
  let expanded;
13282
- if (details.open) {
13700
+ if (isOpen()) {
13283
13701
  expanded = details.getBoundingClientRect().height;
13284
- details.open = false;
13702
+ setOpen(false);
13285
13703
  collapsed = details.getBoundingClientRect().height;
13286
- details.open = true;
13704
+ setOpen(true);
13287
13705
  } else {
13288
13706
  collapsed = details.getBoundingClientRect().height;
13289
- details.open = true;
13707
+ setOpen(true);
13290
13708
  expanded = details.getBoundingClientRect().height;
13291
13709
  }
13292
13710
  const to = opening ? expanded : collapsed;
@@ -13305,15 +13723,36 @@ function toggleBoxWithDrawer(details, options = {}) {
13305
13723
  const settle = (finished) => {
13306
13724
  if (running.get(details)?.animation !== animation) return;
13307
13725
  running.delete(details);
13308
- if (finished) details.open = opening;
13726
+ if (finished) setOpen(opening);
13309
13727
  details.style.overflow = restoreOverflow;
13310
13728
  };
13311
13729
  animation.addEventListener("finish", () => settle(true));
13312
13730
  animation.addEventListener("cancel", () => settle(false));
13313
13731
  }
13732
+ function toggleBoxWithDrawer(details, options = {}) {
13733
+ animateDrawer(details, () => details.open, (v) => {
13734
+ details.open = v;
13735
+ }, options);
13736
+ }
13737
+ function toggleSpoilerboxWithDrawer(box, options = {}) {
13738
+ animateDrawer(
13739
+ box,
13740
+ () => box.classList.contains(OSU_OPEN_CLASS),
13741
+ (v) => box.classList.toggle(OSU_OPEN_CLASS, v),
13742
+ options
13743
+ );
13744
+ }
13314
13745
  function bindBoxDrawer(root, options = {}) {
13315
13746
  const handleClick = (e) => {
13316
13747
  const target = e.target;
13748
+ const osuLink = target?.closest(".js-spoilerbox__link");
13749
+ if (osuLink) {
13750
+ const box = osuLink.closest(".js-spoilerbox");
13751
+ if (!(box instanceof HTMLElement) || !root.contains(box)) return;
13752
+ e.preventDefault();
13753
+ toggleSpoilerboxWithDrawer(box, options);
13754
+ return;
13755
+ }
13317
13756
  const summary = target?.closest("summary");
13318
13757
  if (!summary) return;
13319
13758
  const details = summary.parentElement;
@@ -13326,6 +13765,314 @@ function bindBoxDrawer(root, options = {}) {
13326
13765
  return () => root.removeEventListener("click", handleClick);
13327
13766
  }
13328
13767
 
13768
+ // src/Visuals/LyneAudio.ts
13769
+ function bindLyneAudio(root = typeof document !== "undefined" ? document.documentElement : void 0) {
13770
+ if (!root || typeof root.addEventListener !== "function") {
13771
+ return () => {
13772
+ };
13773
+ }
13774
+ const formatTime = (seconds) => {
13775
+ if (!isFinite(seconds) || isNaN(seconds) || seconds < 0) return "\u2013:\u2013\u2013";
13776
+ const m = Math.floor(seconds / 60);
13777
+ const s = Math.floor(seconds % 60);
13778
+ return `${m}:${s < 10 ? "0" : ""}${s}`;
13779
+ };
13780
+ const PLAY_SVG = '<svg viewBox="0 0 16 16"><path d="M3 1.5 14 8 3 14.5z"/></svg>';
13781
+ const PAUSE_SVG = '<svg viewBox="0 0 16 16"><path d="M3 2h3.6v12H3zM9.4 2H13v12H9.4z"/></svg>';
13782
+ const RETRY_SVG = '<svg viewBox="0 0 16 16"><path d="M8 2a6 6 0 1 0 6 6h-2a4 4 0 1 1-4-4v3l5-4-5-4z"/></svg>';
13783
+ const VOL_LOW_SVG = '<svg viewBox="0 0 16 16"><path d="M10.707 11.182A4.5 4.5 0 0 0 12.025 8a4.5 4.5 0 0 0-1.318-3.182L10 5.525A3.5 3.5 0 0 1 11.025 8 3.5 3.5 0 0 1 10 10.475zM6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06"/></svg>';
13784
+ const VOL_HIGH_SVG = '<svg viewBox="0 0 16 16"><path d="M11.536 14.01A8.47 8.47 0 0 0 14.026 8a8.47 8.47 0 0 0-2.49-6.01l-.708.707A7.48 7.48 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303z"/><path d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.48 5.48 0 0 1 11.025 8a5.48 5.48 0 0 1-1.61 3.89z"/><path d="M8.707 11.182A4.5 4.5 0 0 0 10.025 8a4.5 4.5 0 0 0-1.318-3.182L8 5.525A3.5 3.5 0 0 1 9.025 8 3.5 3.5 0 0 1 8 10.475zM6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06"/></svg>';
13785
+ const VOL_MUTE_SVG = '<svg viewBox="0 0 16 16"><path d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06m7.137 2.096a.5.5 0 0 1 0 .708L12.207 8l1.647 1.646a.5.5 0 0 1-.708.708L11.5 8.707l-1.646 1.647a.5.5 0 0 1-.708-.708L10.793 8 9.146 6.354a.5.5 0 1 1 .708-.708L11.5 7.293l1.646-1.647a.5.5 0 0 1 .708 0"/></svg>';
13786
+ const DEFAULT_VOLUME = 0.2;
13787
+ const applyDefaultVolume = (audio) => {
13788
+ if (audio && audio.dataset.volSet !== "true") {
13789
+ audio.volume = DEFAULT_VOLUME;
13790
+ audio.dataset.volSet = "true";
13791
+ }
13792
+ };
13793
+ const updateVolUI = (player, vol) => {
13794
+ const btn = player.querySelector(".lx-vol-btn");
13795
+ const slider = player.querySelector(".lx-vol-slider");
13796
+ if (slider) {
13797
+ if (Math.abs(parseFloat(slider.value) - vol) > 5e-3) {
13798
+ slider.value = String(vol);
13799
+ }
13800
+ const pct = (vol * 100).toFixed(1);
13801
+ slider.style.background = `linear-gradient(to right, var(--color-accent, #2EE6E2) ${pct}%, var(--color-inset-well, #080D20) ${pct}%)`;
13802
+ }
13803
+ if (btn) {
13804
+ if (vol <= 1e-3) {
13805
+ btn.innerHTML = VOL_MUTE_SVG;
13806
+ btn.setAttribute("aria-label", "Unmute");
13807
+ } else if (vol > 0.5) {
13808
+ btn.innerHTML = VOL_HIGH_SVG;
13809
+ btn.setAttribute("aria-label", "Mute");
13810
+ } else {
13811
+ btn.innerHTML = VOL_LOW_SVG;
13812
+ btn.setAttribute("aria-label", "Mute");
13813
+ }
13814
+ }
13815
+ };
13816
+ const onClick = (e) => {
13817
+ const target = e.target;
13818
+ if (!target) return;
13819
+ const btn = target.closest(".lx-btn");
13820
+ if (btn) {
13821
+ const player = btn.closest(".lx-audio");
13822
+ const audio = player?.querySelector("audio");
13823
+ if (audio && player) {
13824
+ e.preventDefault();
13825
+ e.stopPropagation();
13826
+ if (player.classList.contains("is-error")) {
13827
+ player.classList.remove("is-error");
13828
+ player.classList.add("is-loading");
13829
+ audio.load();
13830
+ const p = audio.play();
13831
+ if (p !== void 0) {
13832
+ p.catch((err) => {
13833
+ console.warn("[LyneAudio] retry playback failed:", err);
13834
+ player.classList.remove("is-loading");
13835
+ player.classList.add("is-error");
13836
+ });
13837
+ }
13838
+ } else if (audio.paused) {
13839
+ const doc = root.ownerDocument || document;
13840
+ doc.querySelectorAll(".lx-audio.is-playing").forEach((other) => {
13841
+ if (other !== player) {
13842
+ const otherAudio = other.querySelector("audio");
13843
+ if (otherAudio && !otherAudio.paused) {
13844
+ otherAudio.pause();
13845
+ }
13846
+ }
13847
+ });
13848
+ player.classList.add("is-loading");
13849
+ const p = audio.play();
13850
+ if (p !== void 0) {
13851
+ p.then(() => {
13852
+ player.classList.remove("is-loading");
13853
+ }).catch((err) => {
13854
+ console.warn("[LyneAudio] play failed:", err);
13855
+ player.classList.remove("is-loading");
13856
+ player.classList.add("is-error");
13857
+ });
13858
+ }
13859
+ } else {
13860
+ audio.pause();
13861
+ }
13862
+ }
13863
+ return;
13864
+ }
13865
+ const track = target.closest(".lx-track");
13866
+ if (track) {
13867
+ const player = track.closest(".lx-audio");
13868
+ const audio = player?.querySelector("audio");
13869
+ if (audio) {
13870
+ e.preventDefault();
13871
+ e.stopPropagation();
13872
+ const rect = track.getBoundingClientRect();
13873
+ const pct = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1);
13874
+ if (audio.duration && isFinite(audio.duration)) {
13875
+ audio.currentTime = pct * audio.duration;
13876
+ }
13877
+ const fill = track.querySelector(".fill");
13878
+ if (fill) fill.style.width = `${pct * 100}%`;
13879
+ }
13880
+ return;
13881
+ }
13882
+ const speed = target.closest(".lx-speed");
13883
+ if (speed) {
13884
+ const player = speed.closest(".lx-audio");
13885
+ const audio = player?.querySelector("audio");
13886
+ if (audio) {
13887
+ e.preventDefault();
13888
+ e.stopPropagation();
13889
+ const rates = [1, 1.25, 1.5, 0.75];
13890
+ const curRate = audio.playbackRate || 1;
13891
+ const curIdx = rates.indexOf(curRate);
13892
+ const nextRate = rates[(curIdx + 1) % rates.length];
13893
+ audio.playbackRate = nextRate;
13894
+ speed.textContent = `${nextRate}\xD7`;
13895
+ speed.classList.toggle("active", nextRate !== 1);
13896
+ }
13897
+ return;
13898
+ }
13899
+ const volBtn = target.closest(".lx-vol-btn");
13900
+ if (volBtn) {
13901
+ const player = volBtn.closest(".lx-audio");
13902
+ const audio = player?.querySelector("audio");
13903
+ if (audio && player) {
13904
+ e.preventDefault();
13905
+ e.stopPropagation();
13906
+ audio.dataset.volSet = "true";
13907
+ if (audio.muted || audio.volume <= 1e-3) {
13908
+ const prev = parseFloat(audio.dataset.prevVol || String(DEFAULT_VOLUME)) || DEFAULT_VOLUME;
13909
+ audio.muted = false;
13910
+ audio.volume = prev;
13911
+ updateVolUI(player, prev);
13912
+ } else {
13913
+ audio.dataset.prevVol = String(audio.volume);
13914
+ audio.muted = true;
13915
+ updateVolUI(player, 0);
13916
+ }
13917
+ }
13918
+ return;
13919
+ }
13920
+ };
13921
+ const onInput = (e) => {
13922
+ const target = e.target;
13923
+ if (!target) return;
13924
+ const slider = target.closest(".lx-vol-slider");
13925
+ if (slider) {
13926
+ const player = slider.closest(".lx-audio");
13927
+ const audio = player?.querySelector("audio");
13928
+ if (audio) {
13929
+ const val = parseFloat(slider.value);
13930
+ const clamped = isNaN(val) ? DEFAULT_VOLUME : Math.max(0, Math.min(1, val));
13931
+ audio.dataset.volSet = "true";
13932
+ audio.muted = clamped === 0;
13933
+ audio.volume = clamped;
13934
+ if (player) {
13935
+ updateVolUI(player, clamped);
13936
+ }
13937
+ }
13938
+ }
13939
+ };
13940
+ const onPlay = (e) => {
13941
+ const audio = e.target;
13942
+ if (audio?.tagName !== "AUDIO") return;
13943
+ applyDefaultVolume(audio);
13944
+ const player = audio.closest(".lx-audio");
13945
+ if (!player) return;
13946
+ player.classList.remove("is-loading");
13947
+ player.classList.add("is-playing");
13948
+ const btn = player.querySelector(".lx-btn");
13949
+ if (btn) {
13950
+ btn.innerHTML = PAUSE_SVG;
13951
+ btn.setAttribute("aria-label", "Pause");
13952
+ }
13953
+ const label = player.querySelector(".lx-title .label .status-text");
13954
+ if (label) label.textContent = "playing";
13955
+ updateVolUI(player, audio.muted ? 0 : audio.volume);
13956
+ };
13957
+ const onPause = (e) => {
13958
+ const audio = e.target;
13959
+ if (audio?.tagName !== "AUDIO") return;
13960
+ const player = audio.closest(".lx-audio");
13961
+ if (!player) return;
13962
+ player.classList.remove("is-playing", "is-loading");
13963
+ const btn = player.querySelector(".lx-btn");
13964
+ if (btn) {
13965
+ btn.innerHTML = PLAY_SVG;
13966
+ btn.setAttribute("aria-label", "Play");
13967
+ }
13968
+ const label = player.querySelector(".lx-title .label .status-text");
13969
+ if (label) label.textContent = "audio";
13970
+ };
13971
+ const onTimeUpdate = (e) => {
13972
+ const audio = e.target;
13973
+ if (audio?.tagName !== "AUDIO") return;
13974
+ const player = audio.closest(".lx-audio");
13975
+ if (!player) return;
13976
+ const cur = audio.currentTime || 0;
13977
+ const dur = audio.duration || 0;
13978
+ const fill = player.querySelector(".lx-track .fill");
13979
+ const curTime = player.querySelector(".lx-time .cur");
13980
+ if (fill && dur > 0 && isFinite(dur)) {
13981
+ fill.style.width = `${cur / dur * 100}%`;
13982
+ }
13983
+ if (curTime) {
13984
+ curTime.textContent = formatTime(cur);
13985
+ }
13986
+ };
13987
+ const onLoadedMetadata = (e) => {
13988
+ const audio = e.target;
13989
+ if (audio?.tagName !== "AUDIO") return;
13990
+ applyDefaultVolume(audio);
13991
+ const player = audio.closest(".lx-audio");
13992
+ if (!player) return;
13993
+ const dur = audio.duration || 0;
13994
+ const totalTime = player.querySelector(".lx-time .total");
13995
+ if (totalTime && dur > 0 && isFinite(dur)) {
13996
+ totalTime.textContent = formatTime(dur);
13997
+ }
13998
+ updateVolUI(player, audio.muted ? 0 : audio.volume);
13999
+ };
14000
+ const onWaiting = (e) => {
14001
+ const audio = e.target;
14002
+ if (audio?.tagName !== "AUDIO") return;
14003
+ const player = audio.closest(".lx-audio");
14004
+ if (!player) return;
14005
+ player.classList.add("is-loading");
14006
+ const label = player.querySelector(".lx-title .label .status-text");
14007
+ if (label) label.textContent = "buffering\u2026";
14008
+ };
14009
+ const onError = (e) => {
14010
+ const audio = e.target;
14011
+ if (audio?.tagName !== "AUDIO") return;
14012
+ const player = audio.closest(".lx-audio");
14013
+ if (!player) return;
14014
+ player.classList.remove("is-playing", "is-loading");
14015
+ player.classList.add("is-error");
14016
+ const label = player.querySelector(".lx-title .label .status-text");
14017
+ if (label) label.textContent = "unavailable \u2014 file error";
14018
+ const btn = player.querySelector(".lx-btn");
14019
+ if (btn) {
14020
+ btn.innerHTML = RETRY_SVG;
14021
+ btn.setAttribute("aria-label", "Retry");
14022
+ btn.style.background = "var(--color-danger, #FF3B5C)";
14023
+ }
14024
+ const curTime = player.querySelector(".lx-time .cur");
14025
+ const totalTime = player.querySelector(".lx-time .total");
14026
+ if (curTime) curTime.textContent = "\u2013:\u2013\u2013";
14027
+ if (totalTime) totalTime.textContent = "\u2013:\u2013\u2013";
14028
+ };
14029
+ const onVolumeChange = (e) => {
14030
+ const audio = e.target;
14031
+ if (audio?.tagName !== "AUDIO") return;
14032
+ const player = audio.closest(".lx-audio");
14033
+ if (!player) return;
14034
+ updateVolUI(player, audio.muted ? 0 : audio.volume);
14035
+ };
14036
+ try {
14037
+ root.querySelectorAll("audio").forEach((audio) => {
14038
+ applyDefaultVolume(audio);
14039
+ const player = audio.closest(".lx-audio");
14040
+ if (player) {
14041
+ updateVolUI(player, audio.muted ? 0 : audio.volume);
14042
+ }
14043
+ });
14044
+ } catch {
14045
+ }
14046
+ root.addEventListener("click", onClick, true);
14047
+ root.addEventListener("input", onInput, true);
14048
+ root.addEventListener("change", onInput, true);
14049
+ root.addEventListener("play", onPlay, true);
14050
+ root.addEventListener("pause", onPause, true);
14051
+ root.addEventListener("timeupdate", onTimeUpdate, true);
14052
+ root.addEventListener("loadedmetadata", onLoadedMetadata, true);
14053
+ root.addEventListener("durationchange", onLoadedMetadata, true);
14054
+ root.addEventListener("waiting", onWaiting, true);
14055
+ root.addEventListener("error", onError, true);
14056
+ root.addEventListener("volumechange", onVolumeChange, true);
14057
+ return () => {
14058
+ root.removeEventListener("click", onClick, true);
14059
+ root.removeEventListener("input", onInput, true);
14060
+ root.removeEventListener("change", onInput, true);
14061
+ root.removeEventListener("play", onPlay, true);
14062
+ root.removeEventListener("pause", onPause, true);
14063
+ root.removeEventListener("timeupdate", onTimeUpdate, true);
14064
+ root.removeEventListener("loadedmetadata", onLoadedMetadata, true);
14065
+ root.removeEventListener("durationchange", onLoadedMetadata, true);
14066
+ root.removeEventListener("waiting", onWaiting, true);
14067
+ root.removeEventListener("error", onError, true);
14068
+ root.removeEventListener("volumechange", onVolumeChange, true);
14069
+ };
14070
+ }
14071
+ var setupLyneAudioRuntime = bindLyneAudio;
14072
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
14073
+ bindLyneAudio(document.documentElement);
14074
+ }
14075
+
13329
14076
  // src/Visuals/index.ts
13330
14077
  var visualThemes = [
13331
14078
  {
@@ -17636,4 +18383,4 @@ var MergeColorsTransform = class {
17636
18383
  }
17637
18384
  };
17638
18385
 
17639
- export { ANIM_TYPES, ASTOptimizer, AXES, BBBlocksExporter, BBCODE_RAW_TAGS, BBCODE_TAG_NAMES, BBCodeDocumentModel, BBCodeExporter, BLEND_MODES, BRIDGES_WHITESPACE, CONTAINER_TYPES, ChangeTracker, CharacterCountAnalyzer, CollapseGradientTransform, ColorUsageAnalyzer, CommandRegistry, ContributionKind, DEFAULT_CELL_ASPECT, DEFAULT_MASK, DEFAULT_SPATIAL, DEFAULT_WAVE_OPTIONS, DROPPABLE_WHEN_EMPTY, DROP_EMPTY_PRIORITY, DROP_REDUNDANT_NESTING_PRIORITY, DefaultDecision, DeleteNode, DocumentEventBus, DocumentModel, DropEmptyTagsRule, DropRedundantNestingRule, EFFECT_PARAM_KEYS, EFFECT_TYPES, EFFECT_UNITS, EXPRESSION_VARS, Formatter, GRADIENT_DEFAULTS, GROW_DEFAULTS, GradientAnalyzer, GradientTransformer, GreenNode, GrowTransformer, HTMLDocumentModel, HTMLRenderer, IMG_MODIFIERS, IncrementalParser, InsertText, JSONExporter, LYNE_ONLY_TAGS, Lexer, Linter, MASK_SHAPES, MERGEABLE_INLINE, MERGE_ADJACENT_PRIORITY, MILIASTRY_ONLY_TAGS, MarkdownDocumentModel, MarkdownExporter, MergeAdjacentRule, MergeColorsTransform, MergeNode, MergeableColorAnalyzer, MilHibriDocumentModel, NEUTRAL_SIZE, NodeFactory, NodeMatcher, PAINT_DEFAULTS, PAINT_MAX_COLORS, PaletteRemapDecision, Pipeline, PipelineBuilder, PipelineMode, PipelineStage, PluginAPI, PluginRegistry, QueryEngine, RAINBOW_DEFAULTS, REORDER_WRAPPERS_PRIORITY, RainbowAnalyzer, RainbowCollapseTransform, RainbowTransformer, RedNode, RedNodeStore, RenderPipeline, RenderTree, ReorderWrappersRule, SEPARATOR_VARIANTS, SHORTEN_HEX_PRIORITY, SPATIAL_AXES, SPATIAL_DEFAULTS, SVGRenderer, SemanticAnalyzer, ShortenHexRule, SineWaveTransformer, SplitNode, SymbolAnalyzer, SymbolTable, TABLE_FLAGS, TagRegistry, TiptapExporter, Transaction, TreeBuilder, TreeDiffer, UNWRAP_INVISIBLE_COLOR_PRIORITY, UndoManager, UnwrapInvisibleColorRule, Visitor, WAVE_KINDS, WaveAnalyzer, WaveCollapseTransform, WrapInTag, adjustHsl, allAttributeVocabularies, allRules, applyCentralGradient, applyEditsToSource, applyEffect, applyGradient, applyGrow, applyMultiGradient, applyRainbow, attributeValue, attributeVocabularyFor, axisValue, bbBlockToGreenNode, bbBlocksToGreenTree, bbBlocksToRedTree, bindBoxDrawer, blendHex, buildRangeScope, buildSampleTable, clamp012 as clamp01, clampRange, classifyOverlap, clearPatchCache, closeRange, coalesceDeletions, compareEditPriority, compileExpression, computeTextDelta, countTextLength, defaultRules, deletion, documentScope, domToSVG, domToSVGResult, ease, editsConflict, effectiveAxis, endOf, evaluateEffect, expressionVars, extractTextContent, fbm, findCollapsibleGradients, formatGradientTag, getBBCodeTagNames, greenLeaf, greenNode, greenToRedNode, gridFromParams, hasBothDelimiters, hashSeed, hexToHsl, hexToOklab, hexToRgb, hslToHex, htmlStringToGreenTree, isBlockKind, isHexColor, isInvisibleWhitespace, markdownAstToGreenTree, markdownAstToRedTree, maskDistance, maskFromParams, maskValue, mergeIdentity, mergeStyledSegments, mixHex, mixHexOklab, mixMultiple, mixMultipleStops, morphHTML, nodeAttrValue, nodeKindToTag, normalizeColorValue, normalizeHex, openRange, optimizeBBCode, optimizeTree, parseColorStops, parseEffectParams, parseImgAttr, parsePaintGrid, patchBlocksInto, perceptualDistance, positionedChildren, posterizeHex, randAt, reconcileVisualDOMToBBCode, repairNesting, resolveEditConflicts, resolveTokenValue, rgbToHex, samplePaintGrid, sanitizeColor, sanitizeFontFamily, sanitizeFontSize, shortenableHex, solveCubicBezierY, spatialFromParams, spatialPoint, stringifyColorStops, stringifyEffectParams, stringifyPaintCells, stringifyPaintPalette, tagToNodeKind, toTokenResolver, toggleBoxWithDrawer, transformOffset, transformRange, validateExpression, valueNoise, visualThemes, waveform };
18386
+ export { ANIM_TYPES, ASTOptimizer, AXES, BBBlocksExporter, BBCODE_RAW_TAGS, BBCODE_TAG_NAMES, BBCodeDocumentModel, BBCodeExporter, BLEND_MODES, BRIDGES_WHITESPACE, CONTAINER_TYPES, ChangeTracker, CharacterCountAnalyzer, CollapseGradientTransform, ColorUsageAnalyzer, CommandRegistry, ContributionKind, DEFAULT_CELL_ASPECT, DEFAULT_MASK, DEFAULT_SPATIAL, DEFAULT_WAVE_OPTIONS, DROPPABLE_WHEN_EMPTY, DROP_EMPTY_PRIORITY, DROP_REDUNDANT_NESTING_PRIORITY, DefaultDecision, DeleteNode, DocumentEventBus, DocumentModel, DropEmptyTagsRule, DropRedundantNestingRule, EFFECT_PARAM_KEYS, EFFECT_TYPES, EFFECT_UNITS, EXPRESSION_VARS, Formatter, GRADIENT_DEFAULTS, GROW_DEFAULTS, GradientAnalyzer, GradientTransformer, GreenNode, GrowTransformer, HTMLDocumentModel, HTMLRenderer, IMG_MODIFIERS, IncrementalParser, InsertText, JSONExporter, LYNE_ONLY_TAGS, Lexer, Linter, MASK_SHAPES, MERGEABLE_INLINE, MERGE_ADJACENT_PRIORITY, MILIASTRY_ONLY_TAGS, MarkdownDocumentModel, MarkdownExporter, MergeAdjacentRule, MergeColorsTransform, MergeNode, MergeableColorAnalyzer, MilHibriDocumentModel, NEUTRAL_SIZE, NodeFactory, NodeMatcher, PAINT_DEFAULTS, PAINT_MAX_COLORS, PaletteRemapDecision, Pipeline, PipelineBuilder, PipelineMode, PipelineStage, PluginAPI, PluginRegistry, QueryEngine, RAINBOW_DEFAULTS, REORDER_WRAPPERS_PRIORITY, RainbowAnalyzer, RainbowCollapseTransform, RainbowTransformer, RedNode, RedNodeStore, RenderPipeline, RenderTree, ReorderWrappersRule, SEPARATOR_VARIANTS, SHORTEN_HEX_PRIORITY, SPATIAL_AXES, SPATIAL_DEFAULTS, SVGRenderer, SemanticAnalyzer, ShortenHexRule, SineWaveTransformer, SplitNode, SymbolAnalyzer, SymbolTable, TABLE_FLAGS, TagRegistry, TiptapExporter, Transaction, TreeBuilder, TreeDiffer, UNWRAP_INVISIBLE_COLOR_PRIORITY, UndoManager, UnwrapInvisibleColorRule, Visitor, WAVE_KINDS, WaveAnalyzer, WaveCollapseTransform, WrapInTag, adjustHsl, allAttributeVocabularies, allRules, applyCentralGradient, applyEditsToSource, applyEffect, applyGradient, applyGrow, applyMultiGradient, applyRainbow, attributeValue, attributeVocabularyFor, axisValue, bbBlockToGreenNode, bbBlocksToGreenTree, bbBlocksToRedTree, bindBoxDrawer, bindLyneAudio, blendHex, buildRangeScope, buildSampleTable, clamp012 as clamp01, clampRange, classifyOverlap, clearPatchCache, closeRange, coalesceDeletions, compareEditPriority, compileExpression, computeTextDelta, countTextLength, defaultRules, deletion, documentScope, domToSVG, domToSVGResult, ease, editsConflict, effectiveAxis, endOf, evaluateEffect, expressionVars, extractTextContent, fbm, findCollapsibleGradients, formatGradientTag, getBBCodeTagNames, greenLeaf, greenNode, greenToRedNode, gridFromParams, hasBothDelimiters, hashSeed, hexToHsl, hexToOklab, hexToRgb, hslToHex, htmlStringToGreenTree, isBlockKind, isHexColor, isInvisibleWhitespace, markdownAstToGreenTree, markdownAstToRedTree, maskDistance, maskFromParams, maskValue, mergeIdentity, mergeStyledSegments, mixHex, mixHexOklab, mixMultiple, mixMultipleStops, morphElement, morphHTML, nodeAttrValue, nodeKindToTag, normalizeColorValue, normalizeHex, openRange, optimizeBBCode, optimizeTree, parseColorStops, parseEffectParams, parseImgAttr, parsePaintGrid, patchBlocksInto, perceptualDistance, positionedChildren, posterizeHex, randAt, reconcileVisualDOMToBBCode, renderForumBBCode, repairNesting, resolveEditConflicts, resolveTokenValue, rgbToHex, samplePaintGrid, sanitizeColor, sanitizeFontFamily, sanitizeFontSize, setupLyneAudioRuntime, shortenableHex, solveCubicBezierY, spatialFromParams, spatialPoint, stringifyColorStops, stringifyEffectParams, stringifyPaintCells, stringifyPaintPalette, tagToNodeKind, toTokenResolver, toggleBoxWithDrawer, toggleSpoilerboxWithDrawer, transformOffset, transformRange, validateExpression, valueNoise, visualThemes, waveform };