Working with text

Comparing, matching, and renaming text are three of the most common things a developer does to a string, and each has a well-defined limit that is not obvious from using the tool. Knowing where those limits are is the difference between a pattern that works and one that works until it does not.

What a diff can and cannot represent

A line diff finds the longest common subsequence of lines: the largest set of lines appearing in the same relative order in both versions. Those are unchanged. Anything else is a deletion from the first or an addition to the second.

This model has no concept of modification. A changed line is a deletion followed by an addition, because line comparison is exact. It also has no concept of movement: relocating a function appears as a large deletion and an identical large addition, with nothing linking them. Some tools detect moves afterwards; the algorithm itself does not.

The practical consequence is that whitespace destroys diffs. Line endings are the worst offender, since CRLF and LF make every line differ at the byte level, and a file that has crossed platforms shows as entirely rewritten. Trailing whitespace does the same invisibly, and converting tabs to spaces rewrites every indented line. This is what .gitattributes, editor configuration, and a shared formatter exist to prevent, and it is why an unrelated reformatting commit mixed with a logic change is so hard to review.

Line granularity suits code and suits prose badly, because a paragraph is often a single very long line. Hard-wrapping one sentence per line makes a line diff behave like a sentence diff, which is why some documentation projects adopt the convention.

Regular expressions: the useful part and the hard limit

Regular expressions match patterns in flat text, and within that scope they are extremely effective. Two behaviours account for most of the confusion.

Quantifiers are greedy: they consume as much as possible, then give characters back until the rest of the pattern matches. The pattern for an angle-bracketed tag using a greedy dot matches from the first bracket to the last, spanning everything between. Adding a question mark makes the quantifier lazy and stops at the first closing bracket. Where possible, a negated character class is better than either, because it cannot cross the boundary at all and does not backtrack.

The g flag makes a regex object stateful. Each call to test or exec resumes from lastIndex and updates it, so the same regex tested twice against the same string returns true and then false. On a module-scope regex this produces a validator that rejects every second identical input, which is genuinely hard to spot in review because the code reads correctly.

The serious risk is catastrophic backtracking. Nested quantifiers over overlapping patterns force the engine to try exponentially many ways of partitioning the input, so a few dozen characters can take seconds and a few more can hang the thread. Because JavaScript regex evaluation is synchronous, that is a denial-of-service vector rather than a performance note. Any pattern that will see user input should be tested against a long non-matching string specifically to confirm it returns promptly.

The hard limit is that regular languages cannot count, so nested structures are out of reach. HTML, JSON, and source code all require a parser. A regex that appears to handle them works on the sample you tried and fails on nesting, attribute reordering, or a comment containing something that looks like markup.

Naming conventions are arbitrary and non-negotiable

Each language community settled on a convention, and code ignoring it reads as written by an outsider. camelCase for identifiers in JavaScript, Java, C#, and Swift. snake_case in Python, Ruby, Rust, and SQL. PascalCase for types nearly everywhere, and for React components, where the capital is functionally significant because JSX uses it to distinguish a component from an element. kebab-case for URLs, CSS, HTML attributes, and package names, where no expression parser is involved to misread the hyphen as subtraction. Uppercase with underscores for constants and environment variables.

Acronyms are genuinely ambiguous and the major style guides disagree. The browser API chose XMLHttpRequest, which is widely regarded as a mistake, because consecutive capitals destroy word boundaries. Treating an acronym as an ordinary word is more defensible and makes automated conversion reliable, since a converter can find the boundaries. The practical implication is that names containing consecutive capitals or digits adjacent to letters may not survive a round trip through a converter.

The less obvious hazard is that case conversion is locale-sensitive. Turkish has dotted and dotless i as separate letters, so uppercasing a lowercase i under a Turkish locale does not produce ASCII I. Code that uppercases a string before comparing it against a literal breaks for those users only, which makes it hard to reproduce and easy to dismiss. Use locale-invariant casing for identifiers and protocol tokens, and reserve locale-aware casing for text shown to a person.

Across an API boundary, convert in exactly one place. A Python backend and a JavaScript frontend must reconcile somewhere, and doing it ad hoc per call site guarantees inconsistency. Either the serialisation layer normalises so the API speaks one convention, or the frontend client normalises so nothing downstream sees the other. Both work; mixing them does not.

Why this work belongs on your own machine

The text people most need to compare, match, and rename is usually the text they should least want to upload: two revisions of a contract, a configuration file with credentials in it, proprietary source, or a data export taken either side of a migration.

Every tool in this category runs in the page. Nothing is transmitted, and the network tab will confirm it while you work.

Frequently asked questions

Why does my diff show every line as changed?

Line endings, almost always. CRLF against LF makes every line differ at the byte level. Trailing whitespace and tab-to-space conversion do the same.

Why is a moved function shown as a delete plus an add?

The algorithm matches lines in order and has no representation for movement. Relocated content is a deletion in one place and an identical addition in another.

Why does my regex test return false every second time?

The g flag makes the regex stateful through lastIndex. Drop g when using test, or reset lastIndex before each call.

Can I parse HTML or JSON with a regular expression?

No. Nested structures require counting, which regular languages cannot do. Use DOMParser for HTML and a real parser for JSON.

Where should I convert between naming conventions?

In exactly one place: either the serialisation layer or the frontend API client. Converting per call site guarantees inconsistency.

Tools referenced in this guide