All questions

Styled Text Ranges IV

Premium

Styled Text Ranges IV

A rich-text editor stores its document as plain text plus a list of style ranges — but this time a range's style is an object, not a single tag: { bold: true, color: 'red' }. When the user selects part of the text and clicks the bold button, you don't want to overwrite the selection's color and italic settings — you only want to toggle bold on, leaving every other style key exactly as it was. Your job is to implement that targeted edit: a patchStyle that lays a small bundle of style keys over a character range and shallow-merges them into whatever was already there, touching only the keys in the patch.

Ranges are stored as half-open intervals [start, end)start included, end excluded, so [0, 5) covers indices 0, 1, 2, 3, 4. Ranges may overlap: bold over [0, 5) and italic over [3, 8) both apply to characters 3 and 4. The patch covers its own window [start, end), and only the characters inside that window change. A range wider than the patch has to split at the patch boundaries so its untouched part keeps its original style. Characters in the window that no range covered get a fresh range carrying just the patch. Afterward you normalize: adjacent ranges whose style objects are equal collapse into one, and empty ranges are dropped.

Signature

type Style = Record<string, unknown>;        // e.g. { bold: true, color: 'red' }
type Range = { start: number; end: number; style: Style }; // half-open [start, end)
type Doc = { text: string; ranges: Range[] };

// Apply `patch` over the window [start, end). Returns a NEW Doc where every
// character in [start, end) has its range's style SHALLOW-MERGED with `patch`
// (patch keys override; unrelated existing keys are preserved). Ranges are
// split at the window boundaries so only the covered slice changes; characters
// in the window with no prior range get a range carrying just `patch`. The
// result is normalized: adjacent ranges with deep-equal styles are merged and
// empty ranges dropped. doc is never mutated.
function patchStyle(doc: Doc, start: number, end: number, patch: Style): Doc;

Examples

// Patch adds a key (bold) while preserving the existing one (color).
const doc = {
  text: 'hello',
  ranges: [{ start: 0, end: 5, style: { color: 'red' } }],
};
patchStyle(doc, 0, 5, { bold: true });
// → {
//     text: 'hello',
//     ranges: [
//       { start: 0, end: 5, style: { color: 'red', bold: true } }, // color kept
//     ],
//   }
// Patch over a PARTIAL slice of a wider range splits it into three pieces.
const doc = {
  text: 'hello',
  ranges: [{ start: 0, end: 5, style: { color: 'red' } }],
};
patchStyle(doc, 1, 3, { bold: true });
// → {
//     text: 'hello',
//     ranges: [
//       { start: 0, end: 1, style: { color: 'red' } },            // before — untouched
//       { start: 1, end: 3, style: { color: 'red', bold: true } }, // patched slice
//       { start: 3, end: 5, style: { color: 'red' } },            // after — untouched
//     ],
//   }

Notes

  • Shallow merge, not replace. The new style for a covered character is { ...oldStyle, ...patch }: patch keys win, but every key the patch doesn't mention is carried over unchanged. Patching { bold: true } onto { color: 'red' } yields { color: 'red', bold: true }, never { bold: true }.
  • Only the [start, end) slice changes. A range wider than the window splits at the boundaries; the parts outside the window keep their original style object untouched. The patch is not applied to whole ranges.
  • Gaps get patch-only ranges. A character inside the window that no range covered has no prior style, so it gets a new range whose style is just a copy of patch.
  • Overlapping ranges are patched independently. Each existing range is split and merged on its own. If two ranges both cover a window character, both get the patch keys merged into their own styles; they are never combined into one.
  • Normalize by value, not reference. After patching, two adjacent ranges whose style objects are deep-equal (same keys, same values) merge into one range. Compare the contents of the style objects, not their identities — freshly built objects are never === even when equal. Empty ranges (start === end) are dropped.
  • No mutation. Return a brand-new document with brand-new range and style objects. The caller's doc, its ranges array, every range, and every nested style object must be left exactly as they were.
  • Don't worry about validating inputs (assume 0 ≤ start ≤ end ≤ doc.text.length and that styles are flat one-level objects), nested/deep merging, or removing keys — 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