Skip to content

Find & Replace — JavaScript source

Find and replace text with literal or regular-expression matching, global replace, case sensitivity, whole-word, and capture-group substitution. Live match counter.

This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

/**
 * Find & replace with literal or regex matching, $-substitution
 * ($1 backrefs, $&, $$), case sensitivity, whole-word, and global modes.
 *
 * Language: JavaScript (ES module)
 * CosmoDev polyglot showcase port of the `find-replace` tool.
 * Ported from src/lib/findReplace.ts - display source, part of CosmoDev's
 * polyglot tool pages.
 *
 * Mirrors the live lib exactly. A literal find string is regex-escaped and
 * matched verbatim; an isRegex find is compiled as-is. \b wraps the pattern
 * when wholeWord is set, and the flags string composes g (global), i
 * (case-insensitive), and m (multiline, regex mode only). Invalid patterns
 * are reported as an error string instead of throwing, and an empty find is
 * a no-op. Replacement delegates to String.prototype.replace, so $1, $&,
 * and $$ expand exactly as the engine defines them - this is the reference
 * implementation every other language in the polyglot set is measured
 * against.
 */

const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

const DEFAULTS = {
  isRegex: false,
  caseSensitive: true,
  wholeWord: false,
  global: true,
  multiline: false,
};

/**
 * Compile the find expression with flag + whole-word modifiers.
 * Returns a RegExp on success, or { error } on invalid syntax.
 */
function buildRegex(find, opts) {
  let pattern = opts.isRegex ? find : escapeRegExp(find);
  if (opts.wholeWord) pattern = `\\b${pattern}\\b`;
  let flags = opts.global ? 'g' : '';
  if (!opts.caseSensitive) flags += 'i';
  if (opts.isRegex && opts.multiline) flags += 'm';
  try {
    return new RegExp(pattern, flags);
  } catch (e) {
    return { error: e.message };
  }
}

/**
 * Count occurrences of `find` in `input`.
 * In non-global mode JS String.match returns [whole, g1..gN], so the count
 * is 1 + (capture-group count) when a match exists.
 */
export function countMatches(input, find, optsIn = {}) {
  const opts = { ...DEFAULTS, ...optsIn };
  if (find === '') return { matches: 0, error: null };
  const re = buildRegex(find, opts);
  if (re instanceof RegExp) {
    const m = input.match(re);
    return { matches: m ? m.length : 0, error: null };
  }
  return { matches: 0, error: re.error };
}

/**
 * Replace occurrences of `find` with `replacement`.
 * Never throws: invalid patterns return the input unchanged plus an error.
 */
export function findReplace(input, find, replacement, optsIn = {}) {
  const opts = { ...DEFAULTS, ...optsIn };
  if (find === '') return { result: input, matches: 0, error: null };
  const re = buildRegex(find, opts);
  if (!(re instanceof RegExp)) {
    return { result: input, matches: 0, error: re.error };
  }
  const matches = input.match(re)?.length ?? 0;
  // Rebuild a fresh RegExp: a global match() above advanced lastIndex.
  const re2 = new RegExp(re.source, re.flags);
  const result = input.replace(re2, replacement);
  return { result, matches, error: null };
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →