@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/README.md +1 -1
- package/dist/Visuals/lyne.css +257 -3
- package/dist/Visuals/osu.css +357 -6
- package/dist/index.js +850 -98
- package/dist/index.mjs +846 -99
- package/dist/types/BBCode/BBCodeDocumentModel.d.ts +11 -1
- package/dist/types/Visitors/DOMMorpher.d.ts +6 -0
- package/dist/types/Visitors/HTMLRenderer.d.ts +120 -8
- package/dist/types/Visitors/index.d.ts +1 -1
- package/dist/types/Visuals/BoxDrawer.d.ts +10 -5
- package/dist/types/Visuals/LyneAudio.d.ts +8 -0
- package/dist/types/Visuals/index.d.ts +1 -0
- package/dist/types/index.d.ts +4 -3
- package/package.json +1 -1
- package/src/BBCode/BBCodeDocumentModel.ts +36 -1
- package/src/HTML/HTMLToGreenNode.ts +8 -0
- package/src/Semantic/SemanticAnalyzer.ts +43 -0
- package/src/Visitors/BBCodeExporter.ts +51 -5
- package/src/Visitors/BlockPatcher.ts +36 -11
- package/src/Visitors/DOMMorpher.ts +44 -38
- package/src/Visitors/HTMLRenderer.ts +402 -59
- package/src/Visitors/index.ts +1 -1
- package/src/Visuals/BoxDrawer.ts +56 -10
- package/src/Visuals/LyneAudio.ts +346 -0
- package/src/Visuals/index.ts +1 -0
- package/src/Visuals/lyne.css +257 -3
- package/src/Visuals/osu.css +357 -6
- package/src/index.ts +6 -3
package/dist/index.js
CHANGED
|
@@ -6234,6 +6234,42 @@ var SemanticAnalyzer = class {
|
|
|
6234
6234
|
return diagnostics.length > 0 ? diagnostics : null;
|
|
6235
6235
|
}
|
|
6236
6236
|
});
|
|
6237
|
+
this.register({
|
|
6238
|
+
code: "box-missing-equals",
|
|
6239
|
+
severity: "warning",
|
|
6240
|
+
kinds: ["box"],
|
|
6241
|
+
validate: (node, ctx) => {
|
|
6242
|
+
if (node.text.startsWith("=")) return null;
|
|
6243
|
+
const openEnd = node.innerStart > node.range.start ? node.innerStart : node.range.start + 5;
|
|
6244
|
+
if (node.range.start < 0 || openEnd > ctx.source.length) return null;
|
|
6245
|
+
const openTag = ctx.source.slice(node.range.start, openEnd);
|
|
6246
|
+
if (openTag.includes("=")) return null;
|
|
6247
|
+
const fixes = [
|
|
6248
|
+
{
|
|
6249
|
+
description: "Add '=' to [box]",
|
|
6250
|
+
isAutomatic: true,
|
|
6251
|
+
operations: [
|
|
6252
|
+
{
|
|
6253
|
+
kind: "replace_text",
|
|
6254
|
+
range: { start: node.range.start, end: openEnd },
|
|
6255
|
+
newText: "[box=]"
|
|
6256
|
+
}
|
|
6257
|
+
]
|
|
6258
|
+
}
|
|
6259
|
+
];
|
|
6260
|
+
return createDiagnostic(
|
|
6261
|
+
"box-missing-equals",
|
|
6262
|
+
"[box] without '=' can be previewed in Miliastry, but osu! requires [box=] to parse it correctly",
|
|
6263
|
+
"warning",
|
|
6264
|
+
{
|
|
6265
|
+
nodeId: node.id,
|
|
6266
|
+
nodeKind: node.kind,
|
|
6267
|
+
range: { start: node.range.start, end: openEnd },
|
|
6268
|
+
fixes
|
|
6269
|
+
}
|
|
6270
|
+
);
|
|
6271
|
+
}
|
|
6272
|
+
});
|
|
6237
6273
|
}
|
|
6238
6274
|
/**
|
|
6239
6275
|
* Create a validator for a specific tag/kind.
|
|
@@ -8331,10 +8367,29 @@ var KIND_TO_TAG_NAME = {
|
|
|
8331
8367
|
function hasAttrValue(value) {
|
|
8332
8368
|
return value !== void 0 && value !== null && String(value) !== "";
|
|
8333
8369
|
}
|
|
8334
|
-
function
|
|
8370
|
+
function expandHexForOsu(body) {
|
|
8371
|
+
if (!/^[0-9a-fA-F]+$/.test(body)) return null;
|
|
8372
|
+
switch (body.length) {
|
|
8373
|
+
case 3:
|
|
8374
|
+
return body[0] + body[0] + body[1] + body[1] + body[2] + body[2];
|
|
8375
|
+
case 4:
|
|
8376
|
+
return body[0] + body[0] + body[1] + body[1] + body[2] + body[2];
|
|
8377
|
+
case 6:
|
|
8378
|
+
return body;
|
|
8379
|
+
case 8:
|
|
8380
|
+
return body.slice(0, 6);
|
|
8381
|
+
default:
|
|
8382
|
+
return null;
|
|
8383
|
+
}
|
|
8384
|
+
}
|
|
8385
|
+
function normalizeColorToHex(color, target = "miliastry") {
|
|
8335
8386
|
if (!color) return color;
|
|
8336
8387
|
const trimmed = color.trim();
|
|
8337
|
-
if (trimmed.startsWith("#"))
|
|
8388
|
+
if (trimmed.startsWith("#")) {
|
|
8389
|
+
if (target !== "osu") return trimmed;
|
|
8390
|
+
const expanded = expandHexForOsu(trimmed.slice(1));
|
|
8391
|
+
return expanded === null ? trimmed : `#${expanded}`;
|
|
8392
|
+
}
|
|
8338
8393
|
const rgbMatch = trimmed.match(/^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/i);
|
|
8339
8394
|
if (rgbMatch) {
|
|
8340
8395
|
const r = parseInt(rgbMatch[1], 10).toString(16).padStart(2, "0");
|
|
@@ -8342,6 +8397,10 @@ function normalizeColorToHex(color) {
|
|
|
8342
8397
|
const b = parseInt(rgbMatch[3], 10).toString(16).padStart(2, "0");
|
|
8343
8398
|
return `#${r}${g}${b}`.toLowerCase();
|
|
8344
8399
|
}
|
|
8400
|
+
if (target === "osu" && /\d/.test(trimmed)) {
|
|
8401
|
+
const expanded = expandHexForOsu(trimmed);
|
|
8402
|
+
if (expanded !== null) return `#${expanded}`;
|
|
8403
|
+
}
|
|
8345
8404
|
return trimmed;
|
|
8346
8405
|
}
|
|
8347
8406
|
var BBCodeExporter = class extends Visitor {
|
|
@@ -8466,7 +8525,7 @@ var BBCodeExporter = class extends Visitor {
|
|
|
8466
8525
|
if (this.shouldResolveTokens() && col.startsWith("$")) {
|
|
8467
8526
|
col = resolveTokenValue(col, this.tokenResolver);
|
|
8468
8527
|
}
|
|
8469
|
-
out = `[color=${normalizeColorToHex(col)}]${out}[/color]`;
|
|
8528
|
+
out = `[color=${normalizeColorToHex(col, this.target)}]${out}[/color]`;
|
|
8470
8529
|
}
|
|
8471
8530
|
if (style.fontSize) {
|
|
8472
8531
|
let size = style.fontSize;
|
|
@@ -8549,7 +8608,7 @@ var BBCodeExporter = class extends Visitor {
|
|
|
8549
8608
|
if (this.shouldResolveTokens() && color.startsWith("$")) {
|
|
8550
8609
|
color = resolveTokenValue(color, this.tokenResolver);
|
|
8551
8610
|
}
|
|
8552
|
-
return `=${normalizeColorToHex(color)}`;
|
|
8611
|
+
return `=${normalizeColorToHex(color, this.target)}`;
|
|
8553
8612
|
} else if (node.kind === "font" && hasAttrValue(node.metadata.font)) {
|
|
8554
8613
|
let font = String(node.metadata.font);
|
|
8555
8614
|
if (this.shouldResolveTokens() && font.startsWith("$")) {
|
|
@@ -8652,7 +8711,7 @@ var BBCodeExporter = class extends Visitor {
|
|
|
8652
8711
|
if (this.shouldResolveTokens() && col.startsWith("$")) {
|
|
8653
8712
|
col = resolveTokenValue(col, this.tokenResolver);
|
|
8654
8713
|
}
|
|
8655
|
-
return `=${normalizeColorToHex(col)}`;
|
|
8714
|
+
return `=${normalizeColorToHex(col, this.target)}`;
|
|
8656
8715
|
}
|
|
8657
8716
|
if (this.shouldResolveTokens() && text.startsWith("=")) {
|
|
8658
8717
|
let val = text.slice(1);
|
|
@@ -8977,6 +9036,23 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
|
|
|
8977
9036
|
* LYNE's forum renderer applies.
|
|
8978
9037
|
*/
|
|
8979
9038
|
tableDepth = 0;
|
|
9039
|
+
/**
|
|
9040
|
+
* ¿Estamos emitiendo el vocabulario de clases de osu!?
|
|
9041
|
+
*
|
|
9042
|
+
* osu estiliza box, spoilerbox, notice, imagemap, youtube, los alineados y
|
|
9043
|
+
* los perfiles POR NOMBRE DE CLASE, no por estilo inline. Sobre una userpage
|
|
9044
|
+
* real el HTML de Quasar salía sin estilo porque emitía su propio
|
|
9045
|
+
* vocabulario (`<details>`, `.notice`, `.imagemap-container`…). Bajo
|
|
9046
|
+
* `dialect: 'osu'` se emiten las clases y la estructura de osu; el resto de
|
|
9047
|
+
* dialectos conserva la suya, que es la que sus hojas de estilo esperan.
|
|
9048
|
+
*/
|
|
9049
|
+
isOsu() {
|
|
9050
|
+
return this.options.dialect === "osu";
|
|
9051
|
+
}
|
|
9052
|
+
/** osu recorta los saltos pegados a la apertura y al cierre de box/notice. */
|
|
9053
|
+
static trimOsuEdges(html) {
|
|
9054
|
+
return html.replace(/^[\t ]*\r?\n/, "").replace(/\r?\n[\t ]*$/, "");
|
|
9055
|
+
}
|
|
8980
9056
|
idAttr(node) {
|
|
8981
9057
|
if (_HTMLRenderer.idMode === "none") return "";
|
|
8982
9058
|
if (_HTMLRenderer.idMode === "all") return ` data-node-id="${node.id}"`;
|
|
@@ -9107,7 +9183,7 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
|
|
|
9107
9183
|
case "spoiler":
|
|
9108
9184
|
return this.wrapInline("span", node, 'class="spoiler"');
|
|
9109
9185
|
case "color":
|
|
9110
|
-
return this.
|
|
9186
|
+
return this.renderColor(node);
|
|
9111
9187
|
case "font_size":
|
|
9112
9188
|
return this.wrapInline("span", node, this.fontSizeStyle(node));
|
|
9113
9189
|
case "font":
|
|
@@ -9125,11 +9201,11 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
|
|
|
9125
9201
|
case "audio":
|
|
9126
9202
|
return this.renderAudio(node);
|
|
9127
9203
|
case "center":
|
|
9128
|
-
return this.
|
|
9204
|
+
return this.renderAlignAs(node, "center");
|
|
9129
9205
|
case "right":
|
|
9130
|
-
return this.
|
|
9206
|
+
return this.renderAlignAs(node, "right");
|
|
9131
9207
|
case "left":
|
|
9132
|
-
return this.
|
|
9208
|
+
return this.renderAlignAs(node, "left");
|
|
9133
9209
|
// Sigue siendo siempre `h2`, como antes: el nivel de BBCode no mapea al
|
|
9134
9210
|
// de HTML y un `[heading=9]` daría un `<h9>` inválido. `data-bare-level`
|
|
9135
9211
|
// sólo anota que el 2 lo puso el renderer, para que el camino de vuelta
|
|
@@ -9262,12 +9338,10 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
|
|
|
9262
9338
|
case "paint":
|
|
9263
9339
|
return this.renderEffectSegments(node, "paint");
|
|
9264
9340
|
case "spacing":
|
|
9265
|
-
if (this.
|
|
9266
|
-
|
|
9267
|
-
return this.isPrevBlockBoundary(node) ? "\n" : `<br${this.idAttr(node)}>`;
|
|
9341
|
+
if (this.isNewlineSwallowed(node)) return "\n";
|
|
9342
|
+
return `<br${this.idAttr(node)}>`;
|
|
9268
9343
|
case "empty_line":
|
|
9269
|
-
if (this.
|
|
9270
|
-
if (this.isTrailingBlockBoundary(node)) return "\n";
|
|
9344
|
+
if (this.isNewlineSwallowed(node)) return "\n";
|
|
9271
9345
|
return `<div class="bb-empty-line"${this.idAttr(node)}><br></div>`;
|
|
9272
9346
|
case "group":
|
|
9273
9347
|
return this.wrapInline("span", node, 'class="group"');
|
|
@@ -9303,58 +9377,241 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
|
|
|
9303
9377
|
return this.escapeHtml(node.text || "");
|
|
9304
9378
|
}
|
|
9305
9379
|
}
|
|
9306
|
-
// ───
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9380
|
+
// ─── Newline swallowing ─────────────────────────────────
|
|
9381
|
+
//
|
|
9382
|
+
// osu! turns newlines into `<br />` with one flat rule at the very end of
|
|
9383
|
+
// `BBCodeFromDB::toHTML` — `str_replace("\n", '<br />')`. Every subtlety
|
|
9384
|
+
// lives BEFORE that line: each block pass is a regex that eats the newlines
|
|
9385
|
+
// touching its own tags, so those newlines are simply gone by the time the
|
|
9386
|
+
// flat rule runs. The amount eaten differs per tag, and the asymmetries are
|
|
9387
|
+
// not decorative:
|
|
9388
|
+
//
|
|
9389
|
+
// parseBox `\[box=…\]\n*` `\n*\[/box\]\n?`
|
|
9390
|
+
// parseCode `\[code\]\n*` `\n*\[/code\]\n?`
|
|
9391
|
+
// parseNotice `\[notice\]\n*` `\n*\[/notice\]\n?`
|
|
9392
|
+
// parseList `\s*\[\*\]` `\s*\[/list\]\n?\n?`
|
|
9393
|
+
// parseQuote `\[quote…\]\s*` `\s*\[/quote\]\n?\n?`
|
|
9394
|
+
// parseHeading — `\[/heading\]\n?`
|
|
9395
|
+
// parseImagemap — `\[/imagemap\]\n?`
|
|
9396
|
+
// parseAlignment strtr of `[centre]\n` and `[/centre]\n` — exactly one
|
|
9397
|
+
//
|
|
9398
|
+
// Quasar used to approximate all of that with two neighbourhood heuristics
|
|
9399
|
+
// (`isPrevBlockBoundary` / `isTrailingBlockBoundary`) that treated every
|
|
9400
|
+
// block alike, so they over-ate at `[centre]`/`[/imagemap]` and under-ate at
|
|
9401
|
+
// `[/list]`/`[/quote]`. This models the real rules instead.
|
|
9402
|
+
//
|
|
9403
|
+
// Deliberately NOT gated on the dialect: Miliastry is "osu with steroids"
|
|
9404
|
+
// and has to break lines the same way. Blocks that only exist in Miliastry
|
|
9405
|
+
// (tables, gallery, columns, scroll, …) have no osu counterpart to copy, so
|
|
9406
|
+
// they keep the legacy behaviour via {@link HTMLRenderer.LEGACY_BLOCK_RULE}.
|
|
9407
|
+
/** How many newlines a construct swallows around its own tags. */
|
|
9408
|
+
static NEWLINE_RULES = {
|
|
9409
|
+
// `\n*` inside both edges, one newline after the close.
|
|
9410
|
+
box: { afterOpen: "all", beforeClose: "all", beforeOpen: "none", afterClose: 1 },
|
|
9411
|
+
boxw: { afterOpen: "all", beforeClose: "all", beforeOpen: "none", afterClose: 1 },
|
|
9412
|
+
spoilerbox: { afterOpen: "all", beforeClose: "all", beforeOpen: "none", afterClose: 1 },
|
|
9413
|
+
notice: { afterOpen: "all", beforeClose: "all", beforeOpen: "none", afterClose: 1 },
|
|
9414
|
+
wnotice: { afterOpen: "all", beforeClose: "all", beforeOpen: "none", afterClose: 1 },
|
|
9415
|
+
code: { afterOpen: "all", beforeClose: "all", beforeOpen: "none", afterClose: 1 },
|
|
9416
|
+
// `\s*` — not just newlines — and TWO newlines after the close.
|
|
9417
|
+
quote: { afterOpen: "whitespace", beforeClose: "whitespace", beforeOpen: "none", afterClose: 2 },
|
|
9418
|
+
// `[list]` itself eats nothing after its opening tag: the pass that eats
|
|
9419
|
+
// is `\s*\[\*\]`, which needs an item to follow. `[list]\n\nloose text`
|
|
9420
|
+
// keeps both newlines; `[list]\n[*]a` loses one to the item, not the list.
|
|
9421
|
+
list: { afterOpen: "none", beforeClose: "whitespace", beforeOpen: "none", afterClose: 2 },
|
|
9422
|
+
// `\s*\[\*\]`. The matching `[/*]` of the table exists only in legacy
|
|
9423
|
+
// phpBB rows — `BBCodeForDB` never emits one — so the item's close is
|
|
9424
|
+
// width-less here and its two-newline budget is unreachable by design;
|
|
9425
|
+
// `[*]a\n\n[*]b` loses both newlines to the NEXT item's `\s*`, which is
|
|
9426
|
+
// the same output by a different route.
|
|
9427
|
+
list_item: { afterOpen: "none", beforeClose: "none", beforeOpen: "whitespace", afterClose: 0 },
|
|
9428
|
+
// strtr with `[centre]\n` / `[/centre]\n`: exactly one on each outer edge,
|
|
9429
|
+
// and nothing before the close — `x\n[/centre]` really does keep its `<br>`.
|
|
9430
|
+
center: { afterOpen: "one", beforeClose: "none", beforeOpen: "none", afterClose: 1 },
|
|
9431
|
+
left: { afterOpen: "one", beforeClose: "none", beforeOpen: "none", afterClose: 1 },
|
|
9432
|
+
right: { afterOpen: "one", beforeClose: "none", beforeOpen: "none", afterClose: 1 },
|
|
9433
|
+
align: { afterOpen: "one", beforeClose: "none", beforeOpen: "none", afterClose: 1 },
|
|
9434
|
+
heading: { afterOpen: "none", beforeClose: "none", beforeOpen: "none", afterClose: 1 },
|
|
9435
|
+
imagemap: { afterOpen: "none", beforeClose: "none", beforeOpen: "none", afterClose: 1 },
|
|
9436
|
+
// `[img]` is inline in osu and swallows nothing at all.
|
|
9437
|
+
image: { afterOpen: "none", beforeClose: "none", beforeOpen: "none", afterClose: 0 },
|
|
9438
|
+
document: { afterOpen: "none", beforeClose: "none", beforeOpen: "none", afterClose: 0 }
|
|
9439
|
+
};
|
|
9440
|
+
/**
|
|
9441
|
+
* What a Miliastry-only block does. This is what the old
|
|
9442
|
+
* `isPrevBlockBoundary` / `isTrailingBlockBoundary` pair did for every block:
|
|
9443
|
+
* eat the first newline after the open, every newline before the close, and
|
|
9444
|
+
* the first newline after the close.
|
|
9445
|
+
*/
|
|
9446
|
+
static LEGACY_BLOCK_RULE = {
|
|
9447
|
+
afterOpen: "one",
|
|
9448
|
+
beforeClose: "all",
|
|
9449
|
+
beforeOpen: "none",
|
|
9450
|
+
afterClose: 1
|
|
9451
|
+
};
|
|
9452
|
+
/**
|
|
9453
|
+
* Containers whose opening tag occupies no source text, so a backwards scan
|
|
9454
|
+
* has to walk straight through them.
|
|
9455
|
+
*/
|
|
9456
|
+
static WIDTHLESS_OPEN = /* @__PURE__ */ new Set(["paragraph", "group"]);
|
|
9457
|
+
/**
|
|
9458
|
+
* Same for the closing side. `list_item` is here because `[/*]` is never
|
|
9459
|
+
* written: an item ends where the next `[*]` or the `[/list]` begins, so
|
|
9460
|
+
* `\s*\[/list\]` sees the newline that Quasar stores inside the item.
|
|
9461
|
+
*/
|
|
9462
|
+
static WIDTHLESS_CLOSE = /* @__PURE__ */ new Set(["paragraph", "group", "list_item"]);
|
|
9463
|
+
newlineRule(kind) {
|
|
9464
|
+
const rule = _HTMLRenderer.NEWLINE_RULES[kind];
|
|
9465
|
+
if (rule) return rule;
|
|
9466
|
+
return this.BLOCK_TAGS.has(kind) ? _HTMLRenderer.LEGACY_BLOCK_RULE : null;
|
|
9467
|
+
}
|
|
9468
|
+
static isNewlineNode(node) {
|
|
9469
|
+
return node.kind === "spacing" || node.kind === "empty_line";
|
|
9470
|
+
}
|
|
9471
|
+
static isBlankText(node) {
|
|
9472
|
+
return node.kind === "text" && node.children.length === 0 && node.text.trim() === "";
|
|
9473
|
+
}
|
|
9474
|
+
/**
|
|
9475
|
+
* Whether this `spacing` / `empty_line` leaf is eaten by a neighbouring tag
|
|
9476
|
+
* and therefore renders nothing.
|
|
9477
|
+
*
|
|
9478
|
+
* Each leaf is exactly ONE source newline (the parser splits a run into one
|
|
9479
|
+
* node per `\n`), so the four scans below can be read straight off the
|
|
9480
|
+
* regexes they mirror. A newline eaten by any of them is eaten: osu's passes
|
|
9481
|
+
* run in a fixed order, but since a consumed newline is consumed whichever
|
|
9482
|
+
* pass claimed it, the union is enough — the per-pass order only matters for
|
|
9483
|
+
* a budget that could be spent elsewhere, and budgets here are counted from
|
|
9484
|
+
* the tag outwards, exactly as `\n?\n?` counts.
|
|
9485
|
+
*/
|
|
9486
|
+
isNewlineSwallowed(node) {
|
|
9487
|
+
return this.eatenByOpeningTag(node) || this.eatenByClosingTag(node) || this.eatenAfterClosingTag(node) || this.eatenBeforeOpeningTag(node);
|
|
9488
|
+
}
|
|
9489
|
+
/** `\[box\]\n*`, `\[quote\]\s*`, `[centre]\n`. */
|
|
9490
|
+
eatenByOpeningTag(node) {
|
|
9491
|
+
let cur = node;
|
|
9492
|
+
let newlinesBetween = 0;
|
|
9493
|
+
let blankBetween = false;
|
|
9494
|
+
for (; ; ) {
|
|
9495
|
+
const prev = cur.previousSibling;
|
|
9496
|
+
if (prev) {
|
|
9497
|
+
if (_HTMLRenderer.isNewlineNode(prev)) {
|
|
9498
|
+
newlinesBetween++;
|
|
9499
|
+
cur = prev;
|
|
9500
|
+
continue;
|
|
9501
|
+
}
|
|
9502
|
+
if (_HTMLRenderer.isBlankText(prev)) {
|
|
9503
|
+
blankBetween = true;
|
|
9504
|
+
cur = prev;
|
|
9505
|
+
continue;
|
|
9506
|
+
}
|
|
9507
|
+
return false;
|
|
9329
9508
|
}
|
|
9330
|
-
|
|
9331
|
-
|
|
9509
|
+
const parent = cur.parent;
|
|
9510
|
+
if (!parent) return false;
|
|
9511
|
+
if (_HTMLRenderer.WIDTHLESS_OPEN.has(parent.kind)) {
|
|
9512
|
+
cur = parent;
|
|
9332
9513
|
continue;
|
|
9333
9514
|
}
|
|
9334
|
-
|
|
9515
|
+
const rule = this.newlineRule(parent.kind);
|
|
9516
|
+
if (!rule) return false;
|
|
9517
|
+
switch (rule.afterOpen) {
|
|
9518
|
+
case "whitespace":
|
|
9519
|
+
return true;
|
|
9520
|
+
// `\n*` matches newlines only: a stray space breaks the run.
|
|
9521
|
+
case "all":
|
|
9522
|
+
return !blankBetween;
|
|
9523
|
+
case "one":
|
|
9524
|
+
return !blankBetween && newlinesBetween === 0;
|
|
9525
|
+
default:
|
|
9526
|
+
return false;
|
|
9527
|
+
}
|
|
9335
9528
|
}
|
|
9336
|
-
if (prev && this.BLOCK_TAGS.has(prev.kind) && prev.kind !== "image" && prev.kind !== "imagemap") return true;
|
|
9337
|
-
if (!prev && node.parent && this.BLOCK_TAGS.has(node.parent.kind) && node.parent.kind !== "image" && node.parent.kind !== "imagemap") return true;
|
|
9338
|
-
return false;
|
|
9339
9529
|
}
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9530
|
+
/** `\n*\[/box\]`, `\s*\[/quote\]`, `\s*\[/list\]`. */
|
|
9531
|
+
eatenByClosingTag(node) {
|
|
9532
|
+
let cur = node;
|
|
9533
|
+
let blankBetween = false;
|
|
9534
|
+
for (; ; ) {
|
|
9535
|
+
const next = cur.nextSibling;
|
|
9536
|
+
if (next) {
|
|
9537
|
+
if (_HTMLRenderer.isNewlineNode(next)) {
|
|
9538
|
+
cur = next;
|
|
9539
|
+
continue;
|
|
9540
|
+
}
|
|
9541
|
+
if (_HTMLRenderer.isBlankText(next)) {
|
|
9542
|
+
blankBetween = true;
|
|
9543
|
+
cur = next;
|
|
9544
|
+
continue;
|
|
9545
|
+
}
|
|
9546
|
+
return false;
|
|
9547
|
+
}
|
|
9548
|
+
const parent = cur.parent;
|
|
9549
|
+
if (!parent) return false;
|
|
9550
|
+
if (_HTMLRenderer.WIDTHLESS_CLOSE.has(parent.kind)) {
|
|
9551
|
+
cur = parent;
|
|
9345
9552
|
continue;
|
|
9346
9553
|
}
|
|
9347
|
-
|
|
9348
|
-
|
|
9554
|
+
const rule = this.newlineRule(parent.kind);
|
|
9555
|
+
if (!rule) return false;
|
|
9556
|
+
switch (rule.beforeClose) {
|
|
9557
|
+
case "whitespace":
|
|
9558
|
+
return true;
|
|
9559
|
+
case "all":
|
|
9560
|
+
return !blankBetween;
|
|
9561
|
+
default:
|
|
9562
|
+
return false;
|
|
9563
|
+
}
|
|
9564
|
+
}
|
|
9565
|
+
}
|
|
9566
|
+
/** `\[/box\]\n?`, `\[/list\]\n?\n?`. */
|
|
9567
|
+
eatenAfterClosingTag(node) {
|
|
9568
|
+
let cur = node;
|
|
9569
|
+
let newlinesBetween = 0;
|
|
9570
|
+
for (; ; ) {
|
|
9571
|
+
const prev = cur.previousSibling;
|
|
9572
|
+
if (!prev) {
|
|
9573
|
+
const parent = cur.parent;
|
|
9574
|
+
if (parent && _HTMLRenderer.WIDTHLESS_OPEN.has(parent.kind)) {
|
|
9575
|
+
cur = parent;
|
|
9576
|
+
continue;
|
|
9577
|
+
}
|
|
9578
|
+
return false;
|
|
9579
|
+
}
|
|
9580
|
+
if (_HTMLRenderer.isNewlineNode(prev)) {
|
|
9581
|
+
newlinesBetween++;
|
|
9582
|
+
cur = prev;
|
|
9349
9583
|
continue;
|
|
9350
9584
|
}
|
|
9351
|
-
|
|
9585
|
+
let closer = prev;
|
|
9586
|
+
while (_HTMLRenderer.WIDTHLESS_CLOSE.has(closer.kind) && closer.children.length > 0) {
|
|
9587
|
+
closer = closer.children[closer.children.length - 1];
|
|
9588
|
+
}
|
|
9589
|
+
const rule = this.newlineRule(closer.kind);
|
|
9590
|
+
return rule !== null && newlinesBetween < rule.afterClose;
|
|
9352
9591
|
}
|
|
9353
|
-
|
|
9354
|
-
|
|
9592
|
+
}
|
|
9593
|
+
/** `\s*\[\*\]` — the only pass that eats whitespace BEFORE an opening tag. */
|
|
9594
|
+
eatenBeforeOpeningTag(node) {
|
|
9595
|
+
let cur = node;
|
|
9596
|
+
for (; ; ) {
|
|
9597
|
+
const next = cur.nextSibling;
|
|
9598
|
+
if (next) {
|
|
9599
|
+
if (_HTMLRenderer.isNewlineNode(next) || _HTMLRenderer.isBlankText(next)) {
|
|
9600
|
+
cur = next;
|
|
9601
|
+
continue;
|
|
9602
|
+
}
|
|
9603
|
+
return this.newlineRule(next.kind)?.beforeOpen === "whitespace";
|
|
9604
|
+
}
|
|
9605
|
+
const parent = cur.parent;
|
|
9606
|
+
if (!parent) return false;
|
|
9607
|
+
if (_HTMLRenderer.WIDTHLESS_CLOSE.has(parent.kind)) {
|
|
9608
|
+
cur = parent;
|
|
9609
|
+
continue;
|
|
9610
|
+
}
|
|
9611
|
+
return false;
|
|
9355
9612
|
}
|
|
9356
|
-
return false;
|
|
9357
9613
|
}
|
|
9614
|
+
// ─── Render Helpers ─────────────────────────────────────
|
|
9358
9615
|
renderError(node) {
|
|
9359
9616
|
const errorMsg = this.escapeHtml(node.metadata?.message || node.text || "Syntax Error");
|
|
9360
9617
|
const content = this.renderChildren(node) || this.escapeHtml(node.text || "");
|
|
@@ -9406,6 +9663,41 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
|
|
|
9406
9663
|
return !!raw && /^[a-z]{2,20}(?: [a-z]{2,20})?$|^[1-9]00$/i.test(raw.trim());
|
|
9407
9664
|
}
|
|
9408
9665
|
/** Read a metadata field, falling back to the raw tag attribute. */
|
|
9666
|
+
/**
|
|
9667
|
+
* Lo único que osu! acepta en `[color=…]`.
|
|
9668
|
+
*
|
|
9669
|
+
* Su `BBCodeForDB::parseColour` sella el tag con un uid sólo si el valor
|
|
9670
|
+
* matchea `#[[:xdigit:]]{6}` o `[[:alpha:]]+` — nada más. No valida que el
|
|
9671
|
+
* nombre sea un color CSS de verdad (`banana` pasa), pero `#fff`, `#ffffffff`,
|
|
9672
|
+
* `rgb(...)`, `$token` o un hex sin `#` no pasan. Sin uid, la segunda pasada
|
|
9673
|
+
* no ve el tag y el opener *y* el closer quedan como texto en la página.
|
|
9674
|
+
*/
|
|
9675
|
+
static OSU_COLOR_RE = /^(?:#[0-9a-fA-F]{6}|[a-zA-Z]+)$/;
|
|
9676
|
+
/**
|
|
9677
|
+
* `[color]` con el vocabulario de cada dialecto.
|
|
9678
|
+
*
|
|
9679
|
+
* Miliastry (y Lyne) aceptan a propósito más que osu: `#RGB`, `#RGBA`, un
|
|
9680
|
+
* `$token` de diseño, nombres propios. Bajo `dialect: 'osu'` eso es una
|
|
9681
|
+
* mentira: el editor pintaría color donde la página publicada muestra el
|
|
9682
|
+
* BBCode crudo. Así que replicamos lo que hace osu — literal el opener,
|
|
9683
|
+
* literal el closer, y los hijos renderizados normalmente en el medio.
|
|
9684
|
+
*
|
|
9685
|
+
* El chequeo mira el texto crudo del atributo, no el valor saneado: osu
|
|
9686
|
+
* matchea sobre la fuente, así que `[color="#ffffff"]` (con comillas) también
|
|
9687
|
+
* se le escapa.
|
|
9688
|
+
*/
|
|
9689
|
+
renderColor(node) {
|
|
9690
|
+
if (this.options.dialect === "osu") {
|
|
9691
|
+
const text = node.text || "";
|
|
9692
|
+
const eq = text.indexOf("=");
|
|
9693
|
+
const raw = eq >= 0 ? text.slice(eq + 1) : text ? "" : nodeAttrValue(node, "color");
|
|
9694
|
+
if (!_HTMLRenderer.OSU_COLOR_RE.test(raw)) {
|
|
9695
|
+
const opener = eq >= 0 ? `[color${text}]` : `[${text || "color"}]`;
|
|
9696
|
+
return this.escapeHtml(opener) + this.renderChildren(node) + this.escapeHtml("[/color]");
|
|
9697
|
+
}
|
|
9698
|
+
}
|
|
9699
|
+
return this.wrapInline("span", node, this.colorStyle(node));
|
|
9700
|
+
}
|
|
9409
9701
|
colorStyle(node) {
|
|
9410
9702
|
const color = sanitizeColor(nodeAttrValue(node, "color"), this.tokenResolver);
|
|
9411
9703
|
return color ? `style="color:${color};"` : "";
|
|
@@ -9447,6 +9739,11 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
|
|
|
9447
9739
|
return `<strong${entity}><a${this.idAttr(node)} href="${this.escapeHtml(link.href)}"${ext}>${content}</a></strong>`;
|
|
9448
9740
|
}
|
|
9449
9741
|
}
|
|
9742
|
+
if (type === "profile" && this.isOsu()) {
|
|
9743
|
+
const key2 = val || `@${this.collectNodeText(node)}`;
|
|
9744
|
+
const href = `https://osu.ppy.sh/users/${encodeURIComponent(key2)}`;
|
|
9745
|
+
return `<a${this.idAttr(node)}${entity} class="user-name js-usercard" data-user-id="${this.escapeHtml(key2)}" href="${href}">${content}</a>`;
|
|
9746
|
+
}
|
|
9450
9747
|
if (type === "profile") {
|
|
9451
9748
|
const url = this.options.theme === "lyne" || this.options.dialect === "lyne" ? `/u/${encodeURIComponent(val || content)}` : `https://osu.ppy.sh/users/${this.escapeHtml(val || content)}`;
|
|
9452
9749
|
return `<strong${entity}><a${this.idAttr(node)} href="${url}" target="_blank" rel="noopener">${content}</a></strong>`;
|
|
@@ -9501,13 +9798,25 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
|
|
|
9501
9798
|
if (!id) return this.mediaError("youtube", "[youtube] missing video ID");
|
|
9502
9799
|
const ytMatch = /(?:youtu\.be\/|v=|\/embed\/|\/shorts\/)([\w-]{11})/.exec(id);
|
|
9503
9800
|
if (ytMatch) id = ytMatch[1];
|
|
9504
|
-
|
|
9801
|
+
const cls = this.isOsu() ? "u-embed-wide u-embed-wide--bbcode" : "bb-youtube";
|
|
9802
|
+
const rel = this.isOsu() ? "?rel=0" : "";
|
|
9803
|
+
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>`;
|
|
9505
9804
|
}
|
|
9506
9805
|
renderAudio(node) {
|
|
9507
9806
|
let src = String(node.metadata?.src ?? "") || node.text || "";
|
|
9508
9807
|
if (this.options.mediaProxy && src) {
|
|
9509
9808
|
src = this.options.mediaProxy(src);
|
|
9510
9809
|
}
|
|
9810
|
+
const isLyne = this.options.theme === "lyne" || this.options.dialect === "lyne";
|
|
9811
|
+
if (isLyne) {
|
|
9812
|
+
const rawName = src.split("?")[0].split("#")[0].split("/").filter(Boolean).pop() || "audio_track.mp3";
|
|
9813
|
+
let fileName = rawName;
|
|
9814
|
+
try {
|
|
9815
|
+
fileName = decodeURIComponent(rawName);
|
|
9816
|
+
} catch {
|
|
9817
|
+
}
|
|
9818
|
+
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>`;
|
|
9819
|
+
}
|
|
9511
9820
|
return `<audio${this.idAttr(node)} controls src="${this.escapeHtml(src)}" class="bb-audio"></audio>`;
|
|
9512
9821
|
}
|
|
9513
9822
|
hexToRgba(hex, alpha) {
|
|
@@ -9533,6 +9842,10 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
|
|
|
9533
9842
|
const warningIcon = warning ? `<span aria-hidden class="bb-notice-mark"${markStyle}>\u26A0</span>` : "";
|
|
9534
9843
|
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>`;
|
|
9535
9844
|
}
|
|
9845
|
+
if (this.isOsu()) {
|
|
9846
|
+
const content = _HTMLRenderer.trimOsuEdges(this.renderChildren(node));
|
|
9847
|
+
return `<div${this.idAttr(node)} class="well">${content}</div>`;
|
|
9848
|
+
}
|
|
9536
9849
|
return this.wrapBlock("div", node, 'class="notice"');
|
|
9537
9850
|
}
|
|
9538
9851
|
renderTables(node) {
|
|
@@ -9638,7 +9951,21 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
|
|
|
9638
9951
|
renderAlign(node) {
|
|
9639
9952
|
const alignVal = (String(node.metadata?.align ?? "") || nodeAttrValue(node) || "center").trim().toLowerCase();
|
|
9640
9953
|
const validAlign = alignVal === "left" || alignVal === "right" ? alignVal : "center";
|
|
9641
|
-
return this.
|
|
9954
|
+
return this.renderAlignAs(node, validAlign);
|
|
9955
|
+
}
|
|
9956
|
+
/**
|
|
9957
|
+
* `[centre]` / `[left]` / `[right]` (y `[align=…]`).
|
|
9958
|
+
*
|
|
9959
|
+
* osu! no usa `text-align` inline: estiliza el bloque por nombre de clase,
|
|
9960
|
+
* con la grafía británica `centre`. Fuera del dialecto osu el estilo inline
|
|
9961
|
+
* se mantiene, porque ni Miliastry ni Lyne traen esas reglas.
|
|
9962
|
+
*/
|
|
9963
|
+
renderAlignAs(node, align) {
|
|
9964
|
+
if (this.isOsu()) {
|
|
9965
|
+
const name = align === "center" ? "centre" : align;
|
|
9966
|
+
return this.wrapBlock("div", node, `class="bbcode__align-${name}"`);
|
|
9967
|
+
}
|
|
9968
|
+
return this.wrapBlock("div", node, `style="text-align:${align};"`);
|
|
9642
9969
|
}
|
|
9643
9970
|
renderEffect(node) {
|
|
9644
9971
|
const raw = (String(node.metadata?.effectType ?? "") || nodeAttrValue(node) || "glow").toLowerCase().trim();
|
|
@@ -9827,7 +10154,25 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
|
|
|
9827
10154
|
}
|
|
9828
10155
|
return `<blockquote${this.idAttr(node)}>${content}</blockquote>`;
|
|
9829
10156
|
}
|
|
10157
|
+
/**
|
|
10158
|
+
* La estructura exacta que `bbcode-spoilerbox` de osu-web espera.
|
|
10159
|
+
*
|
|
10160
|
+
* El toggle de osu es JS: `js-spoilerbox__link` es el gancho del click y
|
|
10161
|
+
* `js-spoilerbox__body` el panel que abre. Si falta cualquiera de las dos
|
|
10162
|
+
* clases el box queda mudo, así que la estructura no es decorativa.
|
|
10163
|
+
*/
|
|
10164
|
+
renderOsuSpoilerbox(node, title, extra) {
|
|
10165
|
+
const content = _HTMLRenderer.trimOsuEdges(this.renderChildren(node));
|
|
10166
|
+
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>`;
|
|
10167
|
+
}
|
|
10168
|
+
/** El rótulo de un box bajo osu: el del autor, o `SPOILER` en mayúsculas. */
|
|
10169
|
+
osuBoxTitle(node) {
|
|
10170
|
+
return this.hasOwnTitle(node) ? this.renderTitle(node, "SPOILER") : "SPOILER";
|
|
10171
|
+
}
|
|
9830
10172
|
renderSpoilerbox(node) {
|
|
10173
|
+
if (this.isOsu()) {
|
|
10174
|
+
return this.renderOsuSpoilerbox(node, this.osuBoxTitle(node), this.bareTitleAttr(node));
|
|
10175
|
+
}
|
|
9831
10176
|
const title = this.renderTitle(node, "Spoiler");
|
|
9832
10177
|
const bare = this.bareTitleAttr(node);
|
|
9833
10178
|
const content = this.renderChildren(node);
|
|
@@ -9837,6 +10182,9 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
|
|
|
9837
10182
|
return `<details${this.idAttr(node)}${bare}${accent}><summary><span class="bb-box-heading">${title}</span></summary><div class="${bodyCls}">${content}</div></details>`;
|
|
9838
10183
|
}
|
|
9839
10184
|
renderBox(node) {
|
|
10185
|
+
if (this.isOsu()) {
|
|
10186
|
+
return this.renderOsuSpoilerbox(node, this.osuBoxTitle(node), this.bareTitleAttr(node));
|
|
10187
|
+
}
|
|
9840
10188
|
const title = this.renderTitle(node, "Box");
|
|
9841
10189
|
const bare = this.bareTitleAttr(node);
|
|
9842
10190
|
const content = this.renderChildren(node);
|
|
@@ -9854,9 +10202,18 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
|
|
|
9854
10202
|
* escrito y devolvía `[box=Box]`.
|
|
9855
10203
|
*/
|
|
9856
10204
|
bareTitleAttr(node) {
|
|
10205
|
+
return this.hasOwnTitle(node) ? "" : ' data-bare-title="1"';
|
|
10206
|
+
}
|
|
10207
|
+
/**
|
|
10208
|
+
* ¿El título del box lo escribió el autor, o es el relleno del parser?
|
|
10209
|
+
*
|
|
10210
|
+
* `BBCodeToGreenNode` ya deja `metadata.title = 'Box'`/`'Spoiler'` para un
|
|
10211
|
+
* tag pelado, así que el `fallback` de `renderTitle` nunca llega a usarse:
|
|
10212
|
+
* quien quiera otro rótulo por defecto tiene que preguntar por aquí.
|
|
10213
|
+
*/
|
|
10214
|
+
hasOwnTitle(node) {
|
|
9857
10215
|
const raw = node.metadata?.rawTitle;
|
|
9858
|
-
|
|
9859
|
-
return hasOwnTitle ? "" : ' data-bare-title="1"';
|
|
10216
|
+
return raw !== void 0 ? String(raw) !== "" : node.metadata?.title !== void 0;
|
|
9860
10217
|
}
|
|
9861
10218
|
renderTitle(node, fallback) {
|
|
9862
10219
|
const titleNodes = node.metadata?.titleNodes;
|
|
@@ -9955,8 +10312,17 @@ var HTMLRenderer = class _HTMLRenderer extends Visitor {
|
|
|
9955
10312
|
if (areaUrl && !areaUrl.startsWith("http://") && !areaUrl.startsWith("https://") && !areaUrl.startsWith("mailto:")) {
|
|
9956
10313
|
areaUrl = "https://" + areaUrl;
|
|
9957
10314
|
}
|
|
10315
|
+
if (this.isOsu()) {
|
|
10316
|
+
const pos = `left:${x}%;top:${y}%;width:${w}%;height:${h}%;`;
|
|
10317
|
+
const title = ` title="${this.escapeHtml(label)}"`;
|
|
10318
|
+
areas += url === "#" ? `<span class="imagemap__link" style="${pos}"${title}></span>` : `<a class="imagemap__link" href="${this.escapeHtml(areaUrl)}" style="${pos}"${title}></a>`;
|
|
10319
|
+
continue;
|
|
10320
|
+
}
|
|
9958
10321
|
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>`;
|
|
9959
10322
|
}
|
|
10323
|
+
if (this.isOsu()) {
|
|
10324
|
+
return `<div${this.idAttr(node)} class="imagemap"><img class="imagemap__image" loading="lazy" src="${this.escapeHtml(imageUrl)}" alt="">${areas}</div>`;
|
|
10325
|
+
}
|
|
9960
10326
|
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>`;
|
|
9961
10327
|
}
|
|
9962
10328
|
collectNodeText(node) {
|
|
@@ -10241,38 +10607,39 @@ function morphNodes(parent, newParent) {
|
|
|
10241
10607
|
oldNode.nodeValue = newNode.nodeValue;
|
|
10242
10608
|
}
|
|
10243
10609
|
} else if (oldNode.nodeType === 1 && newNode.nodeType === 1 && oldNode.tagName === newNode.tagName) {
|
|
10244
|
-
|
|
10245
|
-
const newEl = newNode;
|
|
10246
|
-
if (oldEl.tagName === "DETAILS" && oldEl.hasAttribute("open")) {
|
|
10247
|
-
newEl.setAttribute("open", "");
|
|
10248
|
-
}
|
|
10249
|
-
if (oldEl.classList.contains("open")) {
|
|
10250
|
-
newEl.classList.add("open");
|
|
10251
|
-
}
|
|
10252
|
-
if (oldEl.classList.contains("is-open")) {
|
|
10253
|
-
newEl.classList.add("is-open");
|
|
10254
|
-
}
|
|
10255
|
-
const newAttrs = newEl.attributes;
|
|
10256
|
-
for (let i2 = 0; i2 < newAttrs.length; i2++) {
|
|
10257
|
-
const attr = newAttrs[i2];
|
|
10258
|
-
if (oldEl.getAttribute(attr.name) !== attr.value) {
|
|
10259
|
-
oldEl.setAttribute(attr.name, attr.value);
|
|
10260
|
-
}
|
|
10261
|
-
}
|
|
10262
|
-
const oldAttrs = oldEl.attributes;
|
|
10263
|
-
for (let i2 = oldAttrs.length - 1; i2 >= 0; i2--) {
|
|
10264
|
-
const name = oldAttrs[i2].name;
|
|
10265
|
-
if (!newEl.hasAttribute(name)) {
|
|
10266
|
-
oldEl.removeAttribute(name);
|
|
10267
|
-
}
|
|
10268
|
-
}
|
|
10269
|
-
morphNodes(oldEl, newEl);
|
|
10610
|
+
morphElement(oldNode, newNode);
|
|
10270
10611
|
} else {
|
|
10271
10612
|
oldNode.parentNode.replaceChild(newNode.cloneNode(true), oldNode);
|
|
10272
10613
|
}
|
|
10273
10614
|
}
|
|
10274
10615
|
}
|
|
10275
10616
|
}
|
|
10617
|
+
function morphElement(oldEl, newEl) {
|
|
10618
|
+
if (oldEl.tagName === "DETAILS" && oldEl.hasAttribute("open")) {
|
|
10619
|
+
newEl.setAttribute("open", "");
|
|
10620
|
+
}
|
|
10621
|
+
if (oldEl.classList.contains("open")) {
|
|
10622
|
+
newEl.classList.add("open");
|
|
10623
|
+
}
|
|
10624
|
+
if (oldEl.classList.contains("is-open")) {
|
|
10625
|
+
newEl.classList.add("is-open");
|
|
10626
|
+
}
|
|
10627
|
+
const newAttrs = newEl.attributes;
|
|
10628
|
+
for (let i = 0; i < newAttrs.length; i++) {
|
|
10629
|
+
const attr = newAttrs[i];
|
|
10630
|
+
if (oldEl.getAttribute(attr.name) !== attr.value) {
|
|
10631
|
+
oldEl.setAttribute(attr.name, attr.value);
|
|
10632
|
+
}
|
|
10633
|
+
}
|
|
10634
|
+
const oldAttrs = oldEl.attributes;
|
|
10635
|
+
for (let i = oldAttrs.length - 1; i >= 0; i--) {
|
|
10636
|
+
const name = oldAttrs[i].name;
|
|
10637
|
+
if (!newEl.hasAttribute(name)) {
|
|
10638
|
+
oldEl.removeAttribute(name);
|
|
10639
|
+
}
|
|
10640
|
+
}
|
|
10641
|
+
morphNodes(oldEl, newEl);
|
|
10642
|
+
}
|
|
10276
10643
|
|
|
10277
10644
|
// src/Visitors/BlockPatcher.ts
|
|
10278
10645
|
var caches = /* @__PURE__ */ new WeakMap();
|
|
@@ -10310,6 +10677,9 @@ function getCache(container) {
|
|
|
10310
10677
|
function blockKey(node, index) {
|
|
10311
10678
|
return node.id ?? `__block_${index}`;
|
|
10312
10679
|
}
|
|
10680
|
+
function isContextSensitive(node) {
|
|
10681
|
+
return node.kind === "spacing" || node.kind === "empty_line";
|
|
10682
|
+
}
|
|
10313
10683
|
function nodeFromHtml(html) {
|
|
10314
10684
|
const t = document.createElement("template");
|
|
10315
10685
|
t.innerHTML = html;
|
|
@@ -10328,7 +10698,7 @@ function renderedTag(html) {
|
|
|
10328
10698
|
return m ? m[1].toUpperCase() : null;
|
|
10329
10699
|
}
|
|
10330
10700
|
function shouldMorphInPlace(element, run) {
|
|
10331
|
-
return run.kind === "element" && !!run.node.id &&
|
|
10701
|
+
return run.kind === "element" && !!run.node.id && canMorphInPlace(run.node) && renderedTag(run.html) === element.tagName;
|
|
10332
10702
|
}
|
|
10333
10703
|
function buildRuns(blocks, keys, getHtml, baseStart) {
|
|
10334
10704
|
const runs = [];
|
|
@@ -10394,7 +10764,7 @@ function reconcileKeyed(container, rootNode, keys, renderer, cache, options) {
|
|
|
10394
10764
|
let patched = 0;
|
|
10395
10765
|
try {
|
|
10396
10766
|
runs = buildRuns(blocks, keys, (node, key) => {
|
|
10397
|
-
if (cache.lastNode.get(key) === node) {
|
|
10767
|
+
if (cache.lastNode.get(key) === node && !isContextSensitive(node)) {
|
|
10398
10768
|
return { html: cache.lastHtml.get(key) ?? "", kind: cache.lastClass.get(key) ?? "none" };
|
|
10399
10769
|
}
|
|
10400
10770
|
const html = renderer.render(node);
|
|
@@ -10435,7 +10805,12 @@ function reconcileKeyed(container, rootNode, keys, renderer, cache, options) {
|
|
|
10435
10805
|
continue;
|
|
10436
10806
|
}
|
|
10437
10807
|
if (element.nodeType === 1 && shouldMorphInPlace(element, run)) {
|
|
10438
|
-
|
|
10808
|
+
const newEl = nodeFromHtml(run.html);
|
|
10809
|
+
if (newEl && newEl.nodeType === 1 && newEl.tagName === element.tagName) {
|
|
10810
|
+
morphElement(element, newEl);
|
|
10811
|
+
} else {
|
|
10812
|
+
container.replaceChild(newEl, element);
|
|
10813
|
+
}
|
|
10439
10814
|
} else {
|
|
10440
10815
|
container.replaceChild(nodeFromHtml(run.html), element);
|
|
10441
10816
|
}
|
|
@@ -10502,7 +10877,7 @@ function reconcileWindowed(container, rootNode, change, renderer, cache, options
|
|
|
10502
10877
|
let runs;
|
|
10503
10878
|
try {
|
|
10504
10879
|
runs = buildRuns(blocks, keys, (node2, key) => {
|
|
10505
|
-
if (cache.lastNode.get(key) === node2) {
|
|
10880
|
+
if (cache.lastNode.get(key) === node2 && !isContextSensitive(node2)) {
|
|
10506
10881
|
return { html: cache.lastHtml.get(key) ?? "", kind: cache.lastClass.get(key) ?? "none" };
|
|
10507
10882
|
}
|
|
10508
10883
|
const html = renderer.render(node2);
|
|
@@ -10661,7 +11036,12 @@ function reconcileWindowed(container, rootNode, change, renderer, cache, options
|
|
|
10661
11036
|
continue;
|
|
10662
11037
|
}
|
|
10663
11038
|
if (element.nodeType === 1 && shouldMorphInPlace(element, run)) {
|
|
10664
|
-
|
|
11039
|
+
const newEl = nodeFromHtml(run.html);
|
|
11040
|
+
if (newEl && newEl.nodeType === 1 && newEl.tagName === element.tagName) {
|
|
11041
|
+
morphElement(element, newEl);
|
|
11042
|
+
} else {
|
|
11043
|
+
container.replaceChild(newEl, element);
|
|
11044
|
+
}
|
|
10665
11045
|
} else {
|
|
10666
11046
|
container.replaceChild(nodeFromHtml(run.html), element);
|
|
10667
11047
|
}
|
|
@@ -11883,6 +12263,13 @@ function domToGreenTree(root) {
|
|
|
11883
12263
|
children.push(greenLeaf("spacing", ""));
|
|
11884
12264
|
}
|
|
11885
12265
|
});
|
|
12266
|
+
} else if (el.classList.contains("lx-audio")) {
|
|
12267
|
+
kind = "audio";
|
|
12268
|
+
const audioSrc = el.getAttribute("data-src") || el.querySelector("audio")?.getAttribute("src") || "";
|
|
12269
|
+
if (audioSrc) {
|
|
12270
|
+
children.push(greenLeaf("text", audioSrc));
|
|
12271
|
+
currentOffset += audioSrc.length;
|
|
12272
|
+
}
|
|
11886
12273
|
} else if (el.style.textAlign === "center") kind = "center";
|
|
11887
12274
|
else if (el.style.textAlign === "right") kind = "right";
|
|
11888
12275
|
else if (el.style.textAlign === "left") kind = "left";
|
|
@@ -12616,7 +13003,7 @@ var GreenNodePool = class _GreenNodePool {
|
|
|
12616
13003
|
};
|
|
12617
13004
|
|
|
12618
13005
|
// src/BBCode/BBCodeDocumentModel.ts
|
|
12619
|
-
var BBCodeDocumentModel = class extends DocumentModel {
|
|
13006
|
+
var BBCodeDocumentModel = class _BBCodeDocumentModel extends DocumentModel {
|
|
12620
13007
|
_strictMode;
|
|
12621
13008
|
_dialect;
|
|
12622
13009
|
/** Per-document interner, or null when interning is off. */
|
|
@@ -12652,6 +13039,35 @@ var BBCodeDocumentModel = class extends DocumentModel {
|
|
|
12652
13039
|
const r = renderer ?? new HTMLRenderer({ dialect: this._dialect, theme: this._dialect === "lyne" ? "lyne" : "osu" });
|
|
12653
13040
|
return r.render(this.redRoot);
|
|
12654
13041
|
}
|
|
13042
|
+
/**
|
|
13043
|
+
* Fast-path read-only rendering for forum posts, comments, and static views.
|
|
13044
|
+
*
|
|
13045
|
+
* Completely bypasses semantic analysis (linter), undo stack allocations, and
|
|
13046
|
+
* editor node ids (idMode: 'none') to maximize throughput and minimize memory.
|
|
13047
|
+
*/
|
|
13048
|
+
static renderForum(source, options = {}) {
|
|
13049
|
+
const previousIdMode = HTMLRenderer.idMode;
|
|
13050
|
+
HTMLRenderer.idMode = "none";
|
|
13051
|
+
try {
|
|
13052
|
+
const dialect = options.dialect ?? (options.theme === "lyne" ? "lyne" : "miliastry");
|
|
13053
|
+
const doc = new _BBCodeDocumentModel({
|
|
13054
|
+
source: source || " ",
|
|
13055
|
+
dialect,
|
|
13056
|
+
autoAnalyze: false,
|
|
13057
|
+
maxUndo: 0,
|
|
13058
|
+
incremental: false
|
|
13059
|
+
});
|
|
13060
|
+
if (!doc.redRoot) return "";
|
|
13061
|
+
const renderer = new HTMLRenderer({
|
|
13062
|
+
...options,
|
|
13063
|
+
dialect,
|
|
13064
|
+
registry: options.registry ?? doc.tagRegistry
|
|
13065
|
+
});
|
|
13066
|
+
return renderer.render(doc.redRoot);
|
|
13067
|
+
} finally {
|
|
13068
|
+
HTMLRenderer.idMode = previousIdMode;
|
|
13069
|
+
}
|
|
13070
|
+
}
|
|
12655
13071
|
/**
|
|
12656
13072
|
* Parse BBCode text directly to a GreenNode tree using the
|
|
12657
13073
|
* DocumentEngine's built-in BBCode Lexer + Parser.
|
|
@@ -12702,6 +13118,7 @@ var BBCodeDocumentModel = class extends DocumentModel {
|
|
|
12702
13118
|
return new BBCodeExporter(this.tagRegistry, exportTarget).export(root);
|
|
12703
13119
|
}
|
|
12704
13120
|
};
|
|
13121
|
+
var renderForumBBCode = BBCodeDocumentModel.renderForum;
|
|
12705
13122
|
|
|
12706
13123
|
// src/Edits/Rules/Rule.ts
|
|
12707
13124
|
function endOf(p) {
|
|
@@ -13264,31 +13681,32 @@ function transformRange(range3, changes) {
|
|
|
13264
13681
|
// src/Visuals/BoxDrawer.ts
|
|
13265
13682
|
var DEFAULT_DURATION_MS = 400;
|
|
13266
13683
|
var MIN_DURATION_MS = 120;
|
|
13684
|
+
var OSU_OPEN_CLASS = "js-spoilerbox--open";
|
|
13267
13685
|
var DEFAULT_EASING = "cubic-bezier(0.33, 1, 0.68, 1)";
|
|
13268
13686
|
var running = /* @__PURE__ */ new WeakMap();
|
|
13269
13687
|
function prefersReducedMotion() {
|
|
13270
13688
|
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
13271
13689
|
}
|
|
13272
|
-
function
|
|
13690
|
+
function animateDrawer(details, isOpen, setOpen, options = {}) {
|
|
13273
13691
|
if (typeof details.animate !== "function" || prefersReducedMotion()) {
|
|
13274
|
-
|
|
13692
|
+
setOpen(!isOpen());
|
|
13275
13693
|
return;
|
|
13276
13694
|
}
|
|
13277
13695
|
const from = details.getBoundingClientRect().height;
|
|
13278
13696
|
const previous = running.get(details);
|
|
13279
13697
|
const restoreOverflow = previous ? previous.restoreOverflow : details.style.overflow;
|
|
13280
|
-
const opening = previous ? !previous.opening : !
|
|
13698
|
+
const opening = previous ? !previous.opening : !isOpen();
|
|
13281
13699
|
previous?.animation.cancel();
|
|
13282
13700
|
let collapsed;
|
|
13283
13701
|
let expanded;
|
|
13284
|
-
if (
|
|
13702
|
+
if (isOpen()) {
|
|
13285
13703
|
expanded = details.getBoundingClientRect().height;
|
|
13286
|
-
|
|
13704
|
+
setOpen(false);
|
|
13287
13705
|
collapsed = details.getBoundingClientRect().height;
|
|
13288
|
-
|
|
13706
|
+
setOpen(true);
|
|
13289
13707
|
} else {
|
|
13290
13708
|
collapsed = details.getBoundingClientRect().height;
|
|
13291
|
-
|
|
13709
|
+
setOpen(true);
|
|
13292
13710
|
expanded = details.getBoundingClientRect().height;
|
|
13293
13711
|
}
|
|
13294
13712
|
const to = opening ? expanded : collapsed;
|
|
@@ -13307,15 +13725,36 @@ function toggleBoxWithDrawer(details, options = {}) {
|
|
|
13307
13725
|
const settle = (finished) => {
|
|
13308
13726
|
if (running.get(details)?.animation !== animation) return;
|
|
13309
13727
|
running.delete(details);
|
|
13310
|
-
if (finished)
|
|
13728
|
+
if (finished) setOpen(opening);
|
|
13311
13729
|
details.style.overflow = restoreOverflow;
|
|
13312
13730
|
};
|
|
13313
13731
|
animation.addEventListener("finish", () => settle(true));
|
|
13314
13732
|
animation.addEventListener("cancel", () => settle(false));
|
|
13315
13733
|
}
|
|
13734
|
+
function toggleBoxWithDrawer(details, options = {}) {
|
|
13735
|
+
animateDrawer(details, () => details.open, (v) => {
|
|
13736
|
+
details.open = v;
|
|
13737
|
+
}, options);
|
|
13738
|
+
}
|
|
13739
|
+
function toggleSpoilerboxWithDrawer(box, options = {}) {
|
|
13740
|
+
animateDrawer(
|
|
13741
|
+
box,
|
|
13742
|
+
() => box.classList.contains(OSU_OPEN_CLASS),
|
|
13743
|
+
(v) => box.classList.toggle(OSU_OPEN_CLASS, v),
|
|
13744
|
+
options
|
|
13745
|
+
);
|
|
13746
|
+
}
|
|
13316
13747
|
function bindBoxDrawer(root, options = {}) {
|
|
13317
13748
|
const handleClick = (e) => {
|
|
13318
13749
|
const target = e.target;
|
|
13750
|
+
const osuLink = target?.closest(".js-spoilerbox__link");
|
|
13751
|
+
if (osuLink) {
|
|
13752
|
+
const box = osuLink.closest(".js-spoilerbox");
|
|
13753
|
+
if (!(box instanceof HTMLElement) || !root.contains(box)) return;
|
|
13754
|
+
e.preventDefault();
|
|
13755
|
+
toggleSpoilerboxWithDrawer(box, options);
|
|
13756
|
+
return;
|
|
13757
|
+
}
|
|
13319
13758
|
const summary = target?.closest("summary");
|
|
13320
13759
|
if (!summary) return;
|
|
13321
13760
|
const details = summary.parentElement;
|
|
@@ -13328,6 +13767,314 @@ function bindBoxDrawer(root, options = {}) {
|
|
|
13328
13767
|
return () => root.removeEventListener("click", handleClick);
|
|
13329
13768
|
}
|
|
13330
13769
|
|
|
13770
|
+
// src/Visuals/LyneAudio.ts
|
|
13771
|
+
function bindLyneAudio(root = typeof document !== "undefined" ? document.documentElement : void 0) {
|
|
13772
|
+
if (!root || typeof root.addEventListener !== "function") {
|
|
13773
|
+
return () => {
|
|
13774
|
+
};
|
|
13775
|
+
}
|
|
13776
|
+
const formatTime = (seconds) => {
|
|
13777
|
+
if (!isFinite(seconds) || isNaN(seconds) || seconds < 0) return "\u2013:\u2013\u2013";
|
|
13778
|
+
const m = Math.floor(seconds / 60);
|
|
13779
|
+
const s = Math.floor(seconds % 60);
|
|
13780
|
+
return `${m}:${s < 10 ? "0" : ""}${s}`;
|
|
13781
|
+
};
|
|
13782
|
+
const PLAY_SVG = '<svg viewBox="0 0 16 16"><path d="M3 1.5 14 8 3 14.5z"/></svg>';
|
|
13783
|
+
const PAUSE_SVG = '<svg viewBox="0 0 16 16"><path d="M3 2h3.6v12H3zM9.4 2H13v12H9.4z"/></svg>';
|
|
13784
|
+
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>';
|
|
13785
|
+
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>';
|
|
13786
|
+
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>';
|
|
13787
|
+
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>';
|
|
13788
|
+
const DEFAULT_VOLUME = 0.2;
|
|
13789
|
+
const applyDefaultVolume = (audio) => {
|
|
13790
|
+
if (audio && audio.dataset.volSet !== "true") {
|
|
13791
|
+
audio.volume = DEFAULT_VOLUME;
|
|
13792
|
+
audio.dataset.volSet = "true";
|
|
13793
|
+
}
|
|
13794
|
+
};
|
|
13795
|
+
const updateVolUI = (player, vol) => {
|
|
13796
|
+
const btn = player.querySelector(".lx-vol-btn");
|
|
13797
|
+
const slider = player.querySelector(".lx-vol-slider");
|
|
13798
|
+
if (slider) {
|
|
13799
|
+
if (Math.abs(parseFloat(slider.value) - vol) > 5e-3) {
|
|
13800
|
+
slider.value = String(vol);
|
|
13801
|
+
}
|
|
13802
|
+
const pct = (vol * 100).toFixed(1);
|
|
13803
|
+
slider.style.background = `linear-gradient(to right, var(--color-accent, #2EE6E2) ${pct}%, var(--color-inset-well, #080D20) ${pct}%)`;
|
|
13804
|
+
}
|
|
13805
|
+
if (btn) {
|
|
13806
|
+
if (vol <= 1e-3) {
|
|
13807
|
+
btn.innerHTML = VOL_MUTE_SVG;
|
|
13808
|
+
btn.setAttribute("aria-label", "Unmute");
|
|
13809
|
+
} else if (vol > 0.5) {
|
|
13810
|
+
btn.innerHTML = VOL_HIGH_SVG;
|
|
13811
|
+
btn.setAttribute("aria-label", "Mute");
|
|
13812
|
+
} else {
|
|
13813
|
+
btn.innerHTML = VOL_LOW_SVG;
|
|
13814
|
+
btn.setAttribute("aria-label", "Mute");
|
|
13815
|
+
}
|
|
13816
|
+
}
|
|
13817
|
+
};
|
|
13818
|
+
const onClick = (e) => {
|
|
13819
|
+
const target = e.target;
|
|
13820
|
+
if (!target) return;
|
|
13821
|
+
const btn = target.closest(".lx-btn");
|
|
13822
|
+
if (btn) {
|
|
13823
|
+
const player = btn.closest(".lx-audio");
|
|
13824
|
+
const audio = player?.querySelector("audio");
|
|
13825
|
+
if (audio && player) {
|
|
13826
|
+
e.preventDefault();
|
|
13827
|
+
e.stopPropagation();
|
|
13828
|
+
if (player.classList.contains("is-error")) {
|
|
13829
|
+
player.classList.remove("is-error");
|
|
13830
|
+
player.classList.add("is-loading");
|
|
13831
|
+
audio.load();
|
|
13832
|
+
const p = audio.play();
|
|
13833
|
+
if (p !== void 0) {
|
|
13834
|
+
p.catch((err) => {
|
|
13835
|
+
console.warn("[LyneAudio] retry playback failed:", err);
|
|
13836
|
+
player.classList.remove("is-loading");
|
|
13837
|
+
player.classList.add("is-error");
|
|
13838
|
+
});
|
|
13839
|
+
}
|
|
13840
|
+
} else if (audio.paused) {
|
|
13841
|
+
const doc = root.ownerDocument || document;
|
|
13842
|
+
doc.querySelectorAll(".lx-audio.is-playing").forEach((other) => {
|
|
13843
|
+
if (other !== player) {
|
|
13844
|
+
const otherAudio = other.querySelector("audio");
|
|
13845
|
+
if (otherAudio && !otherAudio.paused) {
|
|
13846
|
+
otherAudio.pause();
|
|
13847
|
+
}
|
|
13848
|
+
}
|
|
13849
|
+
});
|
|
13850
|
+
player.classList.add("is-loading");
|
|
13851
|
+
const p = audio.play();
|
|
13852
|
+
if (p !== void 0) {
|
|
13853
|
+
p.then(() => {
|
|
13854
|
+
player.classList.remove("is-loading");
|
|
13855
|
+
}).catch((err) => {
|
|
13856
|
+
console.warn("[LyneAudio] play failed:", err);
|
|
13857
|
+
player.classList.remove("is-loading");
|
|
13858
|
+
player.classList.add("is-error");
|
|
13859
|
+
});
|
|
13860
|
+
}
|
|
13861
|
+
} else {
|
|
13862
|
+
audio.pause();
|
|
13863
|
+
}
|
|
13864
|
+
}
|
|
13865
|
+
return;
|
|
13866
|
+
}
|
|
13867
|
+
const track = target.closest(".lx-track");
|
|
13868
|
+
if (track) {
|
|
13869
|
+
const player = track.closest(".lx-audio");
|
|
13870
|
+
const audio = player?.querySelector("audio");
|
|
13871
|
+
if (audio) {
|
|
13872
|
+
e.preventDefault();
|
|
13873
|
+
e.stopPropagation();
|
|
13874
|
+
const rect = track.getBoundingClientRect();
|
|
13875
|
+
const pct = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1);
|
|
13876
|
+
if (audio.duration && isFinite(audio.duration)) {
|
|
13877
|
+
audio.currentTime = pct * audio.duration;
|
|
13878
|
+
}
|
|
13879
|
+
const fill = track.querySelector(".fill");
|
|
13880
|
+
if (fill) fill.style.width = `${pct * 100}%`;
|
|
13881
|
+
}
|
|
13882
|
+
return;
|
|
13883
|
+
}
|
|
13884
|
+
const speed = target.closest(".lx-speed");
|
|
13885
|
+
if (speed) {
|
|
13886
|
+
const player = speed.closest(".lx-audio");
|
|
13887
|
+
const audio = player?.querySelector("audio");
|
|
13888
|
+
if (audio) {
|
|
13889
|
+
e.preventDefault();
|
|
13890
|
+
e.stopPropagation();
|
|
13891
|
+
const rates = [1, 1.25, 1.5, 0.75];
|
|
13892
|
+
const curRate = audio.playbackRate || 1;
|
|
13893
|
+
const curIdx = rates.indexOf(curRate);
|
|
13894
|
+
const nextRate = rates[(curIdx + 1) % rates.length];
|
|
13895
|
+
audio.playbackRate = nextRate;
|
|
13896
|
+
speed.textContent = `${nextRate}\xD7`;
|
|
13897
|
+
speed.classList.toggle("active", nextRate !== 1);
|
|
13898
|
+
}
|
|
13899
|
+
return;
|
|
13900
|
+
}
|
|
13901
|
+
const volBtn = target.closest(".lx-vol-btn");
|
|
13902
|
+
if (volBtn) {
|
|
13903
|
+
const player = volBtn.closest(".lx-audio");
|
|
13904
|
+
const audio = player?.querySelector("audio");
|
|
13905
|
+
if (audio && player) {
|
|
13906
|
+
e.preventDefault();
|
|
13907
|
+
e.stopPropagation();
|
|
13908
|
+
audio.dataset.volSet = "true";
|
|
13909
|
+
if (audio.muted || audio.volume <= 1e-3) {
|
|
13910
|
+
const prev = parseFloat(audio.dataset.prevVol || String(DEFAULT_VOLUME)) || DEFAULT_VOLUME;
|
|
13911
|
+
audio.muted = false;
|
|
13912
|
+
audio.volume = prev;
|
|
13913
|
+
updateVolUI(player, prev);
|
|
13914
|
+
} else {
|
|
13915
|
+
audio.dataset.prevVol = String(audio.volume);
|
|
13916
|
+
audio.muted = true;
|
|
13917
|
+
updateVolUI(player, 0);
|
|
13918
|
+
}
|
|
13919
|
+
}
|
|
13920
|
+
return;
|
|
13921
|
+
}
|
|
13922
|
+
};
|
|
13923
|
+
const onInput = (e) => {
|
|
13924
|
+
const target = e.target;
|
|
13925
|
+
if (!target) return;
|
|
13926
|
+
const slider = target.closest(".lx-vol-slider");
|
|
13927
|
+
if (slider) {
|
|
13928
|
+
const player = slider.closest(".lx-audio");
|
|
13929
|
+
const audio = player?.querySelector("audio");
|
|
13930
|
+
if (audio) {
|
|
13931
|
+
const val = parseFloat(slider.value);
|
|
13932
|
+
const clamped = isNaN(val) ? DEFAULT_VOLUME : Math.max(0, Math.min(1, val));
|
|
13933
|
+
audio.dataset.volSet = "true";
|
|
13934
|
+
audio.muted = clamped === 0;
|
|
13935
|
+
audio.volume = clamped;
|
|
13936
|
+
if (player) {
|
|
13937
|
+
updateVolUI(player, clamped);
|
|
13938
|
+
}
|
|
13939
|
+
}
|
|
13940
|
+
}
|
|
13941
|
+
};
|
|
13942
|
+
const onPlay = (e) => {
|
|
13943
|
+
const audio = e.target;
|
|
13944
|
+
if (audio?.tagName !== "AUDIO") return;
|
|
13945
|
+
applyDefaultVolume(audio);
|
|
13946
|
+
const player = audio.closest(".lx-audio");
|
|
13947
|
+
if (!player) return;
|
|
13948
|
+
player.classList.remove("is-loading");
|
|
13949
|
+
player.classList.add("is-playing");
|
|
13950
|
+
const btn = player.querySelector(".lx-btn");
|
|
13951
|
+
if (btn) {
|
|
13952
|
+
btn.innerHTML = PAUSE_SVG;
|
|
13953
|
+
btn.setAttribute("aria-label", "Pause");
|
|
13954
|
+
}
|
|
13955
|
+
const label = player.querySelector(".lx-title .label .status-text");
|
|
13956
|
+
if (label) label.textContent = "playing";
|
|
13957
|
+
updateVolUI(player, audio.muted ? 0 : audio.volume);
|
|
13958
|
+
};
|
|
13959
|
+
const onPause = (e) => {
|
|
13960
|
+
const audio = e.target;
|
|
13961
|
+
if (audio?.tagName !== "AUDIO") return;
|
|
13962
|
+
const player = audio.closest(".lx-audio");
|
|
13963
|
+
if (!player) return;
|
|
13964
|
+
player.classList.remove("is-playing", "is-loading");
|
|
13965
|
+
const btn = player.querySelector(".lx-btn");
|
|
13966
|
+
if (btn) {
|
|
13967
|
+
btn.innerHTML = PLAY_SVG;
|
|
13968
|
+
btn.setAttribute("aria-label", "Play");
|
|
13969
|
+
}
|
|
13970
|
+
const label = player.querySelector(".lx-title .label .status-text");
|
|
13971
|
+
if (label) label.textContent = "audio";
|
|
13972
|
+
};
|
|
13973
|
+
const onTimeUpdate = (e) => {
|
|
13974
|
+
const audio = e.target;
|
|
13975
|
+
if (audio?.tagName !== "AUDIO") return;
|
|
13976
|
+
const player = audio.closest(".lx-audio");
|
|
13977
|
+
if (!player) return;
|
|
13978
|
+
const cur = audio.currentTime || 0;
|
|
13979
|
+
const dur = audio.duration || 0;
|
|
13980
|
+
const fill = player.querySelector(".lx-track .fill");
|
|
13981
|
+
const curTime = player.querySelector(".lx-time .cur");
|
|
13982
|
+
if (fill && dur > 0 && isFinite(dur)) {
|
|
13983
|
+
fill.style.width = `${cur / dur * 100}%`;
|
|
13984
|
+
}
|
|
13985
|
+
if (curTime) {
|
|
13986
|
+
curTime.textContent = formatTime(cur);
|
|
13987
|
+
}
|
|
13988
|
+
};
|
|
13989
|
+
const onLoadedMetadata = (e) => {
|
|
13990
|
+
const audio = e.target;
|
|
13991
|
+
if (audio?.tagName !== "AUDIO") return;
|
|
13992
|
+
applyDefaultVolume(audio);
|
|
13993
|
+
const player = audio.closest(".lx-audio");
|
|
13994
|
+
if (!player) return;
|
|
13995
|
+
const dur = audio.duration || 0;
|
|
13996
|
+
const totalTime = player.querySelector(".lx-time .total");
|
|
13997
|
+
if (totalTime && dur > 0 && isFinite(dur)) {
|
|
13998
|
+
totalTime.textContent = formatTime(dur);
|
|
13999
|
+
}
|
|
14000
|
+
updateVolUI(player, audio.muted ? 0 : audio.volume);
|
|
14001
|
+
};
|
|
14002
|
+
const onWaiting = (e) => {
|
|
14003
|
+
const audio = e.target;
|
|
14004
|
+
if (audio?.tagName !== "AUDIO") return;
|
|
14005
|
+
const player = audio.closest(".lx-audio");
|
|
14006
|
+
if (!player) return;
|
|
14007
|
+
player.classList.add("is-loading");
|
|
14008
|
+
const label = player.querySelector(".lx-title .label .status-text");
|
|
14009
|
+
if (label) label.textContent = "buffering\u2026";
|
|
14010
|
+
};
|
|
14011
|
+
const onError = (e) => {
|
|
14012
|
+
const audio = e.target;
|
|
14013
|
+
if (audio?.tagName !== "AUDIO") return;
|
|
14014
|
+
const player = audio.closest(".lx-audio");
|
|
14015
|
+
if (!player) return;
|
|
14016
|
+
player.classList.remove("is-playing", "is-loading");
|
|
14017
|
+
player.classList.add("is-error");
|
|
14018
|
+
const label = player.querySelector(".lx-title .label .status-text");
|
|
14019
|
+
if (label) label.textContent = "unavailable \u2014 file error";
|
|
14020
|
+
const btn = player.querySelector(".lx-btn");
|
|
14021
|
+
if (btn) {
|
|
14022
|
+
btn.innerHTML = RETRY_SVG;
|
|
14023
|
+
btn.setAttribute("aria-label", "Retry");
|
|
14024
|
+
btn.style.background = "var(--color-danger, #FF3B5C)";
|
|
14025
|
+
}
|
|
14026
|
+
const curTime = player.querySelector(".lx-time .cur");
|
|
14027
|
+
const totalTime = player.querySelector(".lx-time .total");
|
|
14028
|
+
if (curTime) curTime.textContent = "\u2013:\u2013\u2013";
|
|
14029
|
+
if (totalTime) totalTime.textContent = "\u2013:\u2013\u2013";
|
|
14030
|
+
};
|
|
14031
|
+
const onVolumeChange = (e) => {
|
|
14032
|
+
const audio = e.target;
|
|
14033
|
+
if (audio?.tagName !== "AUDIO") return;
|
|
14034
|
+
const player = audio.closest(".lx-audio");
|
|
14035
|
+
if (!player) return;
|
|
14036
|
+
updateVolUI(player, audio.muted ? 0 : audio.volume);
|
|
14037
|
+
};
|
|
14038
|
+
try {
|
|
14039
|
+
root.querySelectorAll("audio").forEach((audio) => {
|
|
14040
|
+
applyDefaultVolume(audio);
|
|
14041
|
+
const player = audio.closest(".lx-audio");
|
|
14042
|
+
if (player) {
|
|
14043
|
+
updateVolUI(player, audio.muted ? 0 : audio.volume);
|
|
14044
|
+
}
|
|
14045
|
+
});
|
|
14046
|
+
} catch {
|
|
14047
|
+
}
|
|
14048
|
+
root.addEventListener("click", onClick, true);
|
|
14049
|
+
root.addEventListener("input", onInput, true);
|
|
14050
|
+
root.addEventListener("change", onInput, true);
|
|
14051
|
+
root.addEventListener("play", onPlay, true);
|
|
14052
|
+
root.addEventListener("pause", onPause, true);
|
|
14053
|
+
root.addEventListener("timeupdate", onTimeUpdate, true);
|
|
14054
|
+
root.addEventListener("loadedmetadata", onLoadedMetadata, true);
|
|
14055
|
+
root.addEventListener("durationchange", onLoadedMetadata, true);
|
|
14056
|
+
root.addEventListener("waiting", onWaiting, true);
|
|
14057
|
+
root.addEventListener("error", onError, true);
|
|
14058
|
+
root.addEventListener("volumechange", onVolumeChange, true);
|
|
14059
|
+
return () => {
|
|
14060
|
+
root.removeEventListener("click", onClick, true);
|
|
14061
|
+
root.removeEventListener("input", onInput, true);
|
|
14062
|
+
root.removeEventListener("change", onInput, true);
|
|
14063
|
+
root.removeEventListener("play", onPlay, true);
|
|
14064
|
+
root.removeEventListener("pause", onPause, true);
|
|
14065
|
+
root.removeEventListener("timeupdate", onTimeUpdate, true);
|
|
14066
|
+
root.removeEventListener("loadedmetadata", onLoadedMetadata, true);
|
|
14067
|
+
root.removeEventListener("durationchange", onLoadedMetadata, true);
|
|
14068
|
+
root.removeEventListener("waiting", onWaiting, true);
|
|
14069
|
+
root.removeEventListener("error", onError, true);
|
|
14070
|
+
root.removeEventListener("volumechange", onVolumeChange, true);
|
|
14071
|
+
};
|
|
14072
|
+
}
|
|
14073
|
+
var setupLyneAudioRuntime = bindLyneAudio;
|
|
14074
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
14075
|
+
bindLyneAudio(document.documentElement);
|
|
14076
|
+
}
|
|
14077
|
+
|
|
13331
14078
|
// src/Visuals/index.ts
|
|
13332
14079
|
var visualThemes = [
|
|
13333
14080
|
{
|
|
@@ -17764,6 +18511,7 @@ exports.bbBlockToGreenNode = bbBlockToGreenNode;
|
|
|
17764
18511
|
exports.bbBlocksToGreenTree = bbBlocksToGreenTree;
|
|
17765
18512
|
exports.bbBlocksToRedTree = bbBlocksToRedTree;
|
|
17766
18513
|
exports.bindBoxDrawer = bindBoxDrawer;
|
|
18514
|
+
exports.bindLyneAudio = bindLyneAudio;
|
|
17767
18515
|
exports.blendHex = blendHex;
|
|
17768
18516
|
exports.buildRangeScope = buildRangeScope;
|
|
17769
18517
|
exports.buildSampleTable = buildSampleTable;
|
|
@@ -17818,6 +18566,7 @@ exports.mixHex = mixHex;
|
|
|
17818
18566
|
exports.mixHexOklab = mixHexOklab;
|
|
17819
18567
|
exports.mixMultiple = mixMultiple;
|
|
17820
18568
|
exports.mixMultipleStops = mixMultipleStops;
|
|
18569
|
+
exports.morphElement = morphElement;
|
|
17821
18570
|
exports.morphHTML = morphHTML;
|
|
17822
18571
|
exports.nodeAttrValue = nodeAttrValue;
|
|
17823
18572
|
exports.nodeKindToTag = nodeKindToTag;
|
|
@@ -17836,6 +18585,7 @@ exports.positionedChildren = positionedChildren;
|
|
|
17836
18585
|
exports.posterizeHex = posterizeHex;
|
|
17837
18586
|
exports.randAt = randAt;
|
|
17838
18587
|
exports.reconcileVisualDOMToBBCode = reconcileVisualDOMToBBCode;
|
|
18588
|
+
exports.renderForumBBCode = renderForumBBCode;
|
|
17839
18589
|
exports.repairNesting = repairNesting;
|
|
17840
18590
|
exports.resolveEditConflicts = resolveEditConflicts;
|
|
17841
18591
|
exports.resolveTokenValue = resolveTokenValue;
|
|
@@ -17844,6 +18594,7 @@ exports.samplePaintGrid = samplePaintGrid;
|
|
|
17844
18594
|
exports.sanitizeColor = sanitizeColor;
|
|
17845
18595
|
exports.sanitizeFontFamily = sanitizeFontFamily;
|
|
17846
18596
|
exports.sanitizeFontSize = sanitizeFontSize;
|
|
18597
|
+
exports.setupLyneAudioRuntime = setupLyneAudioRuntime;
|
|
17847
18598
|
exports.shortenableHex = shortenableHex;
|
|
17848
18599
|
exports.solveCubicBezierY = solveCubicBezierY;
|
|
17849
18600
|
exports.spatialFromParams = spatialFromParams;
|
|
@@ -17855,6 +18606,7 @@ exports.stringifyPaintPalette = stringifyPaintPalette;
|
|
|
17855
18606
|
exports.tagToNodeKind = tagToNodeKind;
|
|
17856
18607
|
exports.toTokenResolver = toTokenResolver;
|
|
17857
18608
|
exports.toggleBoxWithDrawer = toggleBoxWithDrawer;
|
|
18609
|
+
exports.toggleSpoilerboxWithDrawer = toggleSpoilerboxWithDrawer;
|
|
17858
18610
|
exports.transformOffset = transformOffset;
|
|
17859
18611
|
exports.transformRange = transformRange;
|
|
17860
18612
|
exports.validateExpression = validateExpression;
|