Sign in to edit tickets from this page.

← all tickets · home

Inline linking via pattern rules + PageContext

resolved 5db770f0-fe31-4a18-bcf2-3ea84e948366

created_at
2026-04-24
updated_at
2026-04-24
code_context
src/linking.rs (new), src/render.rs, src/server.rs
priority
P2
ticket_type
feature
parent
177b04ad
resolved_at
2026-04-24
resolution
accepted

Body

CONTEXT

Child of 177b04ad. See the parent for motivation, the principle (UI chrome derived from context, not payload), and the PageContext struct definition shared with Child B. This ticket is narrowly scoped to inline linking.

================================================================ THE CHANGE

After this ticket: entity ids and turn refs that appear anywhere in rendered content on /w/:slug/... pages are wrapped in <a href> anchors pointing at their respective pages. No payload directives. Linking rules are defined centrally and applied uniformly wherever the shape matches.

================================================================ NEW FILE: src/linking.rs

Approximately 200 lines + tests. Public surface:

pub struct PageContext { pub page_type: PageType, pub world_slug: String, pub turn: Option, pub entity_id: Option, pub chain_range: Option<(u64, u64)>, }

pub enum PageType { Session, Turn, Entity }

pub fn apply_rules( payload: &serde_json::Value, rendered_html: &str, context: &PageContext, ) -> String;

Internal types:

enum PathSeg { Key(&'static str), ArrayAny, AnyValue }

enum LinkRule { KeyPath { path: &'static [PathSeg], target: LinkTarget }, Regex { pattern: &'static str, target: LinkTarget }, }

enum LinkTarget { EntityInWorld, // /w/{slug}/entity/{value} TurnInWorld, // /w/{slug}/turn/{parsed-n-from-turn_NNNNNN} SessionForSlug, // /w/{value} }

Static RULES table:

No more rules in v1. Adding rules later is a small, well-understood change — don't over-populate now.

================================================================ ALGORITHM

apply_rules walks the payload tracking the current key path as a stack, collecting (needle, href) pairs to substitute into the rendered HTML.

  1. Structural walk. Recurse through the payload. When a scalar string value's path matches a KeyPath rule, compute the href from (target, context, value) and push (value, href) onto the pair list.
  2. Regex pass. After the walk, for each scalar string encountered, check each Regex rule. If the pattern matches, compute the href and push the pair.
  3. Sort pairs by needle length descending. Longest first so turn_000010 wraps before turn_000001 can eat its prefix.
  4. Substitute. Feed each pair into the retained helper replace_exact_skipping_tags(html, needle, replacement) from src/render.rs. The helper already handles anchor-skipping, tag-attribute-skipping, word boundaries, and multibyte safety.

If context.world_slug is needed for a target (all three current targets need it), read it from the context. For TurnInWorld, parse the turn number from turn_NNNNNN by stripping the turn_ prefix and parsing the remainder as u64.

Key-path walking iterates serde_json::Map in its native order (preserve_order is already on). When a path segment is AnyValue, it matches any map value (for the entities.<any> rule). When ArrayAny, it matches any array element.

================================================================ LINK-IT-ANYWAY POLICY

Pattern matching is structural. A string that matches the shape gets linked, whether or not the target exists. If an entity was deleted or a turn_ref points at a non-existent turn, the click lands on the existing 404 page (entity_not_found or turn_not_found), which already shows a helpful list of valid targets. Do NOT pre-validate against known_entity_ids or the chain range. Trust 404.

================================================================ MODIFICATIONS

src/render.rs:

src/views.rs:

No change. The three builders stay pure-data. They do not import linking and do not construct PageContext.

src/server.rs:

Each of the three world-UI handlers (w_session, w_turn, w_entity) constructs a PageContext from:

Pass Some(&ctx) to render::render_page in the HTML branch. For the ?format=json branch, no context is needed — JSON responses stay pure data, no chrome involved.

================================================================ TESTS

All in src/linking.rs::tests. No live-router needed.

================================================================ ACCEPTANCE

================================================================ EXPLICITLY OUT OF SCOPE

================================================================ NO OPEN KNOBS

Spec is prescriptive. No judgment calls required from the handler. If a question arises during implementation, leave a comment rather than guessing.

Proposed resolution

Implemented end-to-end per spec.

Source:

Merge history:

Verification:

Production smoke (https://chukwa.benac.dev):

Out of scope, unchanged:

Per standing guidance I am not confirming — only proposing.

History (4 events)

Sign in as a human to drive this ticket from the page, or use the MCP tools.