Skip to content

Directive body rejects the whole construct when it holds a pre-claimed span #154

Description

@wildthink

Split out of #120 review, as suggested there — it lives in scanLinkFamily's overlap rule, not in the directive files.

Symptom

A directive whose body contains a span claimed by an earlier pass is rejected whole. It doesn't degrade to a directive containing literal text; there is no directive node at all.

@font(size: 18){a `b` c}     → no directive, `b` is a code span
@font(size: 18){a \* c}      → no directive

Scope is narrower than it first looks

Only pre-claimed spans bite — code spans (pass 1) and backslash escapes (pass 2). Constructs claimed in the same pass or later compose normally:

body result
@font(size: 18){a `b` c} rejected
@font(size: 18){a \* c} rejected
@font(size: 18){a $x^2$ c} directive
@font(size: 18){a [l](u) c} directive
@font(size: 18){a *b* c} directive

Worth stating explicitly because the review described $…$ as also rejecting, and it doesn't — it's claimed in pass 3 alongside directives, so it never overlaps a prior claim. That narrows the fix: only the pass-1 and pass-2 claims need an exemption.

The escape half also explains why the argument-splitter escape inconsistency raised in the #120 review is currently unreachable. Any backslash before ASCII punctuation is claimed in pass 2, so the candidate is rejected before splitArguments ever sees it — @font(family: "a \" b"){x} produces no directive rather than a directive with a wrong value. The reachable case is a backslash before a NON-punctuation character ("a \z b"), which is not an escape and passes through raw. So "make the splitters escape-aware to match balanced()" would be fixing a path nothing reaches; deciding what an escape MEANS inside a directive is the real question, and it is this issue.

Suggested fix

The same exemption #118 gave link labels: permit claimed spans lying wholly inside the directive's body range, and keep rejecting partial overlaps and anything crossing the body boundary. ClaimedIndex.overlapping already exists for exactly this after #140, so the machinery is in place:

claimed.overlapping(candidate.range).allSatisfy { rangeContains(bodyRange, $0) }

Two things to decide, which is why this is an issue and not a patch:

  1. Whether the body should then re-parse the claimed span as a child, the way a link label does, or keep it literal. Re-parsing is the consistent answer and means a directive body behaves like any other container.
  2. What an escape means in a directive body. If escapes stay claimed, {a \} b} still can't produce a literal brace, which is the case an author actually wants. Granting the exemption fixes it for free, but only if the body's re-parse keeps the escape node.

Happy to take it once #120/#121 land, since it touches the same overlap rule and I'd rather not stack it under a PR pair that's already waiting.

Current state

Documented rather than silent: the limitation is in the DirectiveScanner header, at the InlineParser hook, in the changelog entry, and pinned by tests in DirectiveParserTests — asserting both that pre-claimed spans reject and that same-pass constructs compose, so whoever lifts this flips the tests deliberately instead of deleting a surprise.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions