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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions