The same rule, two rulers inside one replacement-count box
Feed in cat cat cat cat cat and apply one rule that turns cat into dog: the statistics box reports 5. Tick "replace first match only" and run again, it reports 1, and the output really has only the leading occurrence changed. Write the replacement back to cat itself and the two reads become 5 and 0 - not a single character of the string moves, yet the full pass still reports 5. The replacement count on this page is not one ruler but three algorithms sharing a single cell.
This article measures only which segment each of the three checkboxes of batch find and replace changes: the template measures 6240 bytes and 158 lines, none of the three checkboxes carries a checked attribute, so all default to off. The queue order of rule rows and the dollar-sign expansions inside replacements were already measured in another article; this one does not touch them.
The literal mode's escape table names 14 metacharacters
With "use regex" unticked, the find term first passes through the character class /[.*+?^${}()|[\]\\]/g: every character it hits gets a backslash inserted in front, and the result is handed to new RegExp. The class names 14 metacharacters one by one - . * + ? ^ $ { } ( ) | [ ] \. Both { and } are in the table: measured, a{2} comes out as a\{2\}. The -, the / and the space sit outside it, so a-b, a/b and a b all come back unchanged.
Fill the replacement with X every time, leave the other two switches off, and run the same find term once through each mode:
| Find term | Input text | Literal mode | Regex mode |
|---|---|---|---|
a.b | a.b aXb Y.b a.b | 2 | 3 |
a(b) | a(b) a(b)x ab | 2 | 1 |
a{2} | aa a{2} aaa | 1 | 2 |
a|b | a|b a b c | 1 | 4 |
a?b | a?b ab b | 1 | 3 |
Literal mode only recognizes the two spots where a.b matches character for character and prints X aXb Y.b X; in regex mode the dot eats any character, so even aXb counts as a hit. The widest gap is a|b, 1 versus 4: the bar is just a symbol inside the string in literal mode, but an alternation in regex mode.
Case sensitivity only adds or deletes one letter in flags
What the template writes literally is caseSensitive ? 'g' : 'gi': unticked (the default) you get the gi carrying its i, ticked you get g. Input Hello hello HELLO heLLo, find hello, replace with X: unticked it reports 4, raw characters go from 23 to result characters 7; ticked it reports 1, only the pure-lowercase word gets replaced, 23 becomes 19, and the two modes differ by 3 hits.
Replace first match only drops g and swaps the ruler
This mode does two things. First flags = flags.replace('g', '') removes the g, so the replacement lands only on the first spot; then the counting branch switches from "how many rules hit" to comparing the whole string before and after the replacement - if (before !== result) adds one, so each rule contributes at most 1 in this mode, and whether it counts depends on whether the string changed at all.
Figure 1: the reported count against the places actually changed, under the same batch of samples
Zero-width matches split the counting cell from the landing run
In non-firstOnly mode the number does not come from the return value of the replacement: a regex rebuilt from pattern.source with g runs match() and takes the length, then a separate replacement pass lands the edits, and the two objects carry no state to each other. Once a pattern can hit at zero length, the reported count and "how many places changed" part ways right here:
x*onabc(the string holds no x) counts 4; fill the replacement withXand you getXaXbXcX, result characters rise from 3 to 7; fill it with the empty string and the outputabcis untouched while that cell still reports 4.\bona bccounts 4 and yieldsXaX XbcX, 4 rising to 8.(?=a)onabacounts 2 and yieldsXabXa, 3 rising to 5.- The anchors
^and$onabceach count 1, and each inserts just 1 extra character. a*onbaacounts 3; the replacement A yieldsAbAA, 3 becoming 4.
Why so many empty positions: with g on, a match that lands at zero length forces progress by bumping lastIndex up by one, so /x*/g against abx collects 4 items - three empty strings plus one x. In this mode the counted times and the edits the replacement actually performs match case by case; what really disagrees is the count against the change in result characters.
An empty find term drops the rule, a broken regex stays silent
The rule-collection line reads if (inputs[0].value): a rule whose find box is empty is thrown away during collection, so the rule count shows the filtered number, not the rows on screen. Of three rows, leave the find of row 2 empty and the rule count reports 2, one below the row count; fill the find with 0 and the rule survives, since it is a truthy string; leave all three finds empty and the rule count is 0, the replacement count is 0, and the output comes back unchanged.
A regex that fails to compile walks a different path: the throw from new RegExp is swallowed by the empty branch catch (e) {}, no prompt text anywhere, and that rule enters the list yet never runs. Three rules - a→X, b→Y, a lone opening parenthesis→Z - against a b c: the third throws, the output is X Y c keeping the first two results, the replacement count is 2 and the rule count still reports 3. Those same three rules in literal mode are escaped into literals, throw nothing, and the count becomes 3. The only clue is a count that comes out below what you expected.
The raw and result characters cells count UTF-16 code units
Those two cells read raw.length and result.length: neither code points nor bytes. One emoji reports 2, one CJK character 中 reports 1, a supplementary-plane CJK character (U+20000) reports 2; 中文测试 reports 4 while its UTF-8 encoding is 12 bytes. The three preset lines of Hello on the first screen are pure ASCII, 43 code units and 43 bytes; the one Hello→Hi rule that init() seeds in runs out as three lines of Hi with result characters 34 - each spot saves 3 code units, the three save 9 in total.
How the attribute value of a rule row gets assembled
When addRule(find, replace) generates a rule row it splices the find term straight into an HTML attribute; line 71 reads, character by character, 'value="' + (find || '') + '" oninput="process()"', with no quote handling on either side. Inside a string stub the assembled fragment is compared with the value the attribute reads back: find term Hello assembles value="Hello" and reads back unchanged; a'b, which holds a single quote, assembles value="a'b" - a single quote inside a double-quoted attribute needs no escaping, so it reads back unchanged too; say "hi" now, which holds two bare double quotes, assembles a fragment reading value="say "hi" now" character for character, while attribute reading runs from value=" to the next bare double quote, so the value actually read back stops at say and the rest falls outside the attribute. The replacement box uses the very same line of code.
FAQ
Q: Can the replacement count stand for how many places changed?
Not directly. In non-firstOnly mode it equals the accumulated hits of all the rules, and zero-width empty positions enter the tally too; firstOnly mode switches to whether the whole string changed, at most 1 per rule. On the same input the two modes read 5 and 1; with an identity replacement one reports 5 and the other reports 0.
Q: With regex ticked the hits grow, so what is broken?
Nothing is. Literal mode walks that escape table naming 14 metacharacters, regex mode reads the same string as syntax, and the positions they hit differ from the start. To check a pattern writing itself, another article recorded a way to verify it alone; no repeat here.
Q: The statistics area vanished, did the job not run?
With empty input all four cells go blank together with their labels - that is the behavior of the branch itself; feed in a run of spaces and everything shows as usual. When a number looks wrong, first tell which mode it belongs to, then check whether any row left its find box empty.