From 2c27edc48744118536780754a74390fc4c474b6c Mon Sep 17 00:00:00 2001 From: Falkan Date: Wed, 18 Mar 2026 19:59:33 -0400 Subject: [PATCH] fix: cycle references resolve to Label (symbol) instead of empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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)'. --- data/units.json | 2 +- src/lib/description.ts | 33 ++++++++++++++++++++++----------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/data/units.json b/data/units.json index 5118571..cc8c922 100644 --- a/data/units.json +++ b/data/units.json @@ -7,7 +7,7 @@ "symbol": "lb", "group": "dickloads", "toBase": 0.038, - "description": "{dickload:description}" + "description": "My description is {dickload:description}" }, { "id": "dickload", diff --git a/src/lib/description.ts b/src/lib/description.ts index 9c21f3a..6bf8920 100644 --- a/src/lib/description.ts +++ b/src/lib/description.ts @@ -15,14 +15,13 @@ import type { Unit, Group } from './types'; * Replace {id:description} tokens with a sentinel \x00DESC:id\x00 * (null-byte delimited — can't appear in user-authored text). * 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} - * tokens), this is one substitution deep. Any remaining sentinels after - * Pass 2 (self-references, unresolvable cycles) are stripped to empty. + * Any sentinels that survive after that substitution are cycles — they + * are resolved as "Label (symbol)" (the {id} expansion) rather than + * leaving an empty slot. * - * Result for a mutual reference (id0 ↔ id2): - * - id0's description contains id2's Pass-1 text (with id2's back-reference stripped) - * - id2's description contains id0's Pass-1 text (with id0's back-reference stripped) - * One level of expansion, then stops cleanly. + * Result for a mutual reference (pound ↔ dickload): + * pound's description: "See dickload (dl)" + * dickload's description: "See pound (lb)" */ // 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. - // 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(); for (const [id, p1] of pass1) { result.set( id, p1 .replace(SENTINEL_RE, (_match, refId: string) => pass1.get(refId) ?? '') - // Strip any sentinels that survived (self-references or nested cycles) - .replace(SENTINEL_RE, '') + .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 ''; + }) ); } @@ -127,5 +132,11 @@ export function resolveDescription( const unit = unitMap.get(refId); 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 ''; + }); }