fix: cycle references resolve to Label (symbol) instead of empty

When a {id:description} reference forms a cycle, the back-reference
now expands to 'Label (symbol)' rather than being stripped.
So pound ↔ dickload renders as 'See dickload (dl)' and 'See pound (lb)'.
This commit is contained in:
Falkan
2026-03-18 19:59:33 -04:00
parent d332cc135e
commit 2c27edc487
2 changed files with 23 additions and 12 deletions

View File

@@ -7,7 +7,7 @@
"symbol": "lb", "symbol": "lb",
"group": "dickloads", "group": "dickloads",
"toBase": 0.038, "toBase": 0.038,
"description": "{dickload:description}" "description": "My description is {dickload:description}"
}, },
{ {
"id": "dickload", "id": "dickload",

View File

@@ -15,14 +15,13 @@ import type { Unit, Group } from './types';
* Replace {id:description} tokens with a sentinel \x00DESC:id\x00 * Replace {id:description} tokens with a sentinel \x00DESC:id\x00
* (null-byte delimited — can't appear in user-authored text). * (null-byte delimited — can't appear in user-authored text).
* Pass 2: replace each sentinel with the Pass 1 output for the referenced id. * Pass 2: replace each sentinel with the Pass 1 output for the referenced id.
* Since Pass 1 output contains only sentinels (not live {id:description} * Any sentinels that survive after that substitution are cycles — they
* tokens), this is one substitution deep. Any remaining sentinels after * are resolved as "Label (symbol)" (the {id} expansion) rather than
* Pass 2 (self-references, unresolvable cycles) are stripped to empty. * leaving an empty slot.
* *
* Result for a mutual reference (id0 ↔ id2): * Result for a mutual reference (pound ↔ dickload):
* - id0's description contains id2's Pass-1 text (with id2's back-reference stripped) * pound's description: "See dickload (dl)"
* - id2's description contains id0's Pass-1 text (with id0's back-reference stripped) * dickload's description: "See pound (lb)"
* One level of expansion, then stops cleanly.
*/ */
// Null-byte delimiters can't appear in user-authored strings. // Null-byte delimiters can't appear in user-authored strings.
@@ -94,15 +93,21 @@ export function resolveAllDescriptions(
} }
// Pass 2: replace sentinels with Pass 1 output of the referenced id. // Pass 2: replace sentinels with Pass 1 output of the referenced id.
// Strip any remaining sentinels (self-refs, unresolvable cycles) → empty string. // Any sentinels surviving after that substitution are cycles — resolve them
// as Label (symbol) instead of leaving an empty slot.
const result = new Map<string, string>(); const result = new Map<string, string>();
for (const [id, p1] of pass1) { for (const [id, p1] of pass1) {
result.set( result.set(
id, id,
p1 p1
.replace(SENTINEL_RE, (_match, refId: string) => pass1.get(refId) ?? '') .replace(SENTINEL_RE, (_match, refId: string) => pass1.get(refId) ?? '')
// Strip any sentinels that survived (self-references or nested cycles) .replace(SENTINEL_RE, (_match, refId: string) => {
.replace(SENTINEL_RE, '') const u = unitMap.get(refId);
if (u) return `${u.label} (${u.symbol})`;
const g = groupMap.get(refId);
if (g) return g.label;
return '';
})
); );
} }
@@ -127,5 +132,11 @@ export function resolveDescription(
const unit = unitMap.get(refId); const unit = unitMap.get(refId);
return unit?.description ? resolveFields(unit.description, unitMap, groupMap) : ''; return unit?.description ? resolveFields(unit.description, unitMap, groupMap) : '';
}) })
.replace(SENTINEL_RE, ''); // strip surviving sentinels .replace(SENTINEL_RE, (_match, refId: string) => {
const u = unitMap.get(refId);
if (u) return `${u.label} (${u.symbol})`;
const g = groupMap.get(refId);
if (g) return g.label;
return '';
});
} }