Skip to content

CustomClassInjection: token offsets corrupted when multiple blocks present #206

@ruttydm

Description

@ruttydm

Bug

CustomClassInjection::parse() produces incorrect token offsets when the input contains more than one {:classname:...:} block. This causes RenderTokens to misalign <span> tags in the rendered output.

Version: 2.17.2

Root cause

CustomClassInjection.php line 40:

$content = str_replace([$startToken, $endToken], '', $content);

$endToken is always :} for every custom-class block. str_replace removes all occurrences at once, so the first iteration strips every :} in the entire string — not just the current match's.

However, $additionalOffset (lines 30, 38) only accounts for one :} per iteration. This means every subsequent token gets an incorrect offset, drifting further with each additional block.

Reproduction

Input:

Hello {:bold:world:} and {:italic:foo:} bar

Trace

preg_match_all captures from the original string:

  • Match 0: world at offset 13, start={:bold: (7 chars), end=:} (2 chars)
  • Match 1: foo at offset 33, start={:italic: (9 chars), end=:} (2 chars)

Iteration 0 ({:bold:world:}):

  • $additionalOffset += 7 (start token) → 7
  • Token: offset = 13 − 7 = 6
  • $additionalOffset += 2 (end token) → 9
  • str_replace(["{:bold:", ":}"], '', ...) removes {:bold: (1×) and both :} (2×) = 11 chars removed, but only 9 tracked

Iteration 1 ({:italic:foo:}):

  • $additionalOffset += 9 → 18
  • Token: offset = 33 − 18 = 15

Actual position of foo in the final content Hello world and foo bar:

H(0)e(1)l(2)l(3)o(4) (5)w(6)o(7)r(8)l(9)d(10) (11)a(12)n(13)d(14) (15)f(16)o(17)o(18)

foo is at position 16, not 15. Off by 1.

Impact

RenderTokens (line 38–41) uses the offset to substr the content and wrap tokens in <span> tags. With the wrong offset, characters adjacent to the highlighted token get swallowed or shifted:

Expected: Hello world and <span class="italic">foo</span> bar
Actual:   Hello world an<span class="italic">d foo</span> bar

The error compounds — with N blocks, the Nth token is off by 2 × (N−1) characters (two for each extra :} removed prematurely).

Suggested fix

Replace the str_replace on line 40 with positional removal (e.g. substr_replace at the known match offsets, adjusted for prior removals), so only the current match's markers are stripped — not all occurrences of :} in the string.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions