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:
- 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.
- 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.
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.
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:
@font(size: 18){a `b` c}@font(size: 18){a \* c}@font(size: 18){a $x^2$ c}@font(size: 18){a [l](u) c}@font(size: 18){a *b* c}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
splitArgumentsever 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 matchbalanced()" 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.overlappingalready exists for exactly this after #140, so the machinery is in place:Two things to decide, which is why this is an issue and not a patch:
{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
DirectiveScannerheader, at theInlineParserhook, in the changelog entry, and pinned by tests inDirectiveParserTests— 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.