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 selects part of the document and copies it, the editor has to carry the styling along: the copied fragment must stay bold where it was bold, italic where it was italic, with the highlighting lined up over exactly the same letters. Your job is to implement that copy: a slice that cuts a window out of a styled document and produces a new, smaller document whose styling is correctly realigned to the cut text.
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: bold [0, 5) and italic [3, 8) both apply to characters 3 and 4. The slice has to keep every style that touches the window, trim the ones that hang over an edge, drop the ones that fall entirely outside, and shift all the survivors so their coordinates count from the start of the new text rather than the old.
type Range = { start: number; end: number; style: string }; // half-open [start, end)
type Doc = { text: string; ranges: Range[] };
// Cut the window [start, end) out of doc. Returns a NEW Doc:
// - text = doc.text.slice(start, end)
// - ranges = each original range intersected with [start, end),
// then shifted left by `start` (subtract start from both ends),
// dropping any range that ends up empty.
// doc is never mutated.
function slice(doc: Doc, start: number, end: number): Doc;
// A slice through two OVERLAPPING ranges. Both survive, clamped and offset.
const doc = {
text: 'abcdefghij',
ranges: [
{ start: 0, end: 5, style: 'bold' }, // covers a b c d e
{ start: 3, end: 8, style: 'italic' }, // covers d e f g h
],
};
slice(doc, 2, 7);
// → {
// text: 'cdefg', // doc.text.slice(2, 7)
// ranges: [
// { start: 0, end: 3, style: 'bold' }, // [0,5)∩[2,7)=[2,5) → −2 → [0,3)
// { start: 1, end: 5, style: 'italic' }, // [3,8)∩[2,7)=[3,7) → −2 → [1,5)
// ],
// }
// A slice that DROPS one range and CLAMPS another.
const doc = {
text: 'hello world',
ranges: [
{ start: 0, end: 3, style: 'bold' }, // "hel" — fully before the window
{ start: 6, end: 11, style: 'link' }, // "world" — overlaps the right edge
],
};
slice(doc, 3, 8);
// → {
// text: 'lo wo', // doc.text.slice(3, 8)
// ranges: [
// { start: 3, end: 5, style: 'link' }, // [6,11)∩[3,8)=[6,8) → −3 → [3,5)
// ], // bold [0,3) is fully before [3,8) → dropped
// }
[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'. Getting the off-by-one right at the boundaries is the crux of the problem.start === end after clamping) carries no characters and must not appear in the output. This includes zero-length ranges that were empty in the input.doc, its ranges array, and every range object inside it must be untouched.0 ≤ start ≤ end ≤ doc.text.length), merging adjacent same-style ranges, or any insert/delete editing operations — 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.