A rich-text editor stores your document as plain text plus a list of formatting ranges: "characters 0 through 4 are bold", "characters 2 through 6 are italic". To render or save that document, you serialize it to HTML. The catch is that ranges overlap freely — the user can bold a word and italicize the second half of it — but HTML tags must be properly nested. You cannot emit <b>The <i>quick</b> brown</i>; a browser will silently repair it and your round-trip breaks. Your job is to turn an arbitrary set of overlapping ranges into a single, valid, deterministic HTML string.
Implement richTextToHtml(doc).
// doc: {
// text: string,
// ranges: Array<{ start: number, end: number, tag: string }>
// }
// Each range styles the HALF-OPEN interval [start, end) with `tag`
// (a bare tag name like 'b', 'i', 'u'). [start, end) includes the
// character at `start` and excludes the character at `end`. Ranges may
// overlap, nest, or share bounds arbitrarily, in any array order.
//
// returns: string
// HTML in which every range's styling is applied to exactly its
// characters, all tags are WELL-NESTED, and the text's special
// characters (&, <, >) are escaped.
function richTextToHtml(doc): string;
// Two non-overlapping ranges, each its own tag pair. Half-open intervals
// mean [0,2) covers "ab" and [2,4) covers "cd" — they do not share a char.
richTextToHtml({
text: 'abcd',
ranges: [
{ start: 0, end: 2, tag: 'b' },
{ start: 2, end: 4, tag: 'i' },
],
});
// → "<b>ab</b><i>cd</i>"
// Two OVERLAPPING ranges. bold = [0,4) "abcd", italic = [2,6) "cdef".
// The overlap forces a split at every boundary (0, 2, 4, 6) into three
// segments. In the middle segment both tags are active and must nest;
// bold opened first, so bold is the outer tag.
richTextToHtml({
text: 'abcdef',
ranges: [
{ start: 0, end: 4, tag: 'b' },
{ start: 2, end: 6, tag: 'i' },
],
});
// → "<b>ab</b><b><i>cd</i></b><i>ef</i>"
// |- b only -||-- b + i --||- i only -|
[start, end). The character at start is included; the character at end is excluded. So [0, 3) and [3, 6) are adjacent siblings, not overlapping — they share no character. An off-by-one here is the most common bug.start and end offsets (plus 0 and text.length). Consecutive offsets bound a segment over which the set of active tags is constant.start ascending (the range that opened earliest is the outermost tag); break ties by end descending (the wider range is more enclosing, so it goes outer); break remaining ties alphabetically by tag name; then by original array index. This makes the output deterministic — the same input always produces byte-for-byte the same string, regardless of the order ranges appear in the input array.<b>ab</b><b><i>cd</i></b> re-opens <b> rather than leaving it dangling.&, <, and > in the text (to &, <, >) so literal characters in the document never become markup. Tag names you emit are trusted and not escaped.href), block-level tags, minimizing tag churn across segments, or validating that tag is a real HTML element — see Going further in the solution.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.