All questions

Styled Text Ranges III

Premium

Styled Text Ranges III

A rich-text editor stores its content as plain text plus a list of style ranges — "characters 0 through 5 are bold," "characters 3 through 8 are italic." When the user runs find-and-replace — swapping one word for another, or deleting a selection — the text shifts, and every style range has to move with it or it ends up marking the wrong letters. Bold that covered "world" must still cover whatever the user typed in its place; styling after the edit has to slide along by however much the text grew or shrank. Your job is to implement that replace: cut out a span of text, drop in new text, and return a new document whose style ranges are correctly re-aligned and normalized.

Styles are stored as half-open intervals [start, end)start is included, end is excluded, so a range [0, 5) covers characters at indices 0, 1, 2, 3, 4. Ranges may overlap freely. After an edit you must also keep the range list tidy: drop any range that no longer covers a character, and merge ranges of the same style that have become adjacent or overlapping into one.

Signature

type Range = { start: number; end: number; style: string }; // half-open [start, end)
type Doc = { text: string; ranges: Range[] };

// Replace doc.text[start, end) with newText. Returns a NEW Doc:
//   - text = doc.text.slice(0, start) + newText + doc.text.slice(end)
//   - let delta = newText.length - (end - start)
//   - ranges entirely before `start` are unchanged
//   - ranges entirely at/after `end` are shifted by delta (both ends)
//   - ranges overlapping [start, end) are clipped to the surviving text on
//     each side; the inserted text carries NO style, so a range that strictly
//     spanned the cut is split into a left part and a (shifted) right part
//   - then NORMALIZE: drop empty ranges, and merge adjacent/overlapping
//     ranges of identical style into one
// doc is never mutated.
function replaceRange(doc: Doc, start: number, end: number, newText: string): Doc;

Examples

// A replace that SHIFTS a trailing range. "lo" (length 2) becomes "LONG"
// (length 4), so delta = +2 and the link range slides right by 2.
const doc = {
  text: 'hello world',
  ranges: [{ start: 6, end: 11, style: 'link' }], // "world"
};
replaceRange(doc, 3, 5, 'LONG');
// → {
//     text: 'helLONG world',
//     ranges: [{ start: 8, end: 13, style: 'link' }], // [6,11) + 2
//   }
// A replace that SPLITS a containing range and MERGES neighbors.
// bold [0,8) strictly contains the cut [3,5); the inserted text is unstyled,
// so bold splits into [0,3) and [5,8)+delta. With newText='' (deletion),
// delta = -2, the right half lands at [3,6) — touching the left half — and
// the two identical-style ranges merge back into one.
const doc = {
  text: 'abcdefgh',
  ranges: [{ start: 0, end: 8, style: 'bold' }],
};
replaceRange(doc, 3, 5, '');
// → {
//     text: 'abcfgh',
//     ranges: [{ start: 0, end: 6, style: 'bold' }], // split then re-merged
//   }

Notes

  • Half-open intervals. [start, end) includes start, excludes end. A range [3, 6) covers three characters: 3, 4, 5. This is the same convention as String.prototype.slice'abcdef'.slice(3, 6) === 'def'. A range whose end equals the cut's start shares no character with the cut, so it counts as "before" and does not move.
  • The delta shifts trailing text. delta = newText.length - (end - start). Everything at or after the cut's end moves by delta — positive when the replacement is longer, negative when it's shorter, exactly -(end - start) for a pure deletion (newText === '').
  • Inserted text is unstyled. The new text carries no style of its own. A range that strictly contained the replaced span therefore comes out as two pieces — the part before the cut and the part after — not one range stretched across the insertion.
  • Normalize the result. Drop every range that ends up empty (start === end). Then merge any two ranges of the same style whose intervals touch or overlap into a single range. Ranges of different styles never merge, even when they cover the same characters.
  • No mutation. Return a brand-new document with brand-new range objects. The caller's original doc, its ranges array, and every range object inside it must be untouched.
  • Don't worry about validating inputs (assume 0 ≤ start ≤ end ≤ doc.text.length), inheriting style onto the inserted text, or batching multiple edits — those are out of scope (see the solution's Going further).

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium