MarkHighlighter

Description

Marks every occurrence of a keyword inside rendered content, the DOM equivalent of find-in-page

MarkHighlighter is a static helper rather than an IComponent. It walks every text node under a root element, descending into same-origin iframes, and marks the ones that match a keyword. Adapted from mark.js.

Where the browser supports the CSS Custom Highlight APIMarkHighlighter.IsHighlightApiSupported answers that — matches in the page itself are painted through CSS.highlights as live ranges, with no DOM mutation at all, so components holding references to their own text nodes are untouched. The registry names tss-marked and tss-marked-focused are styled by tss.markhighlighter.css.

Three cases get the classic treatment instead, each match wrapped in a <mark data-marked="true"> element and unwrapped again on unmark: text inside iframes, whose documents do not carry the page stylesheet; browsers without the API; and callers that opt out by setting MarkOptions.UseHighlightApi = false or a custom Element, MarkData or ClassName.

Matching is case-insensitive by default, folds diacritics (searching cafe finds café), and merges whitespace runs, so a keyword typed with one space matches text with many. Text inside script, style, title, head and already-marked elements is skipped.

Passes on the same root are serialized: starting a new mark or unmark cancels the in-flight one and waits for it, so calling on every keystroke is safe without guarding it yourself. Large documents are walked cooperatively — the pass yields to the event loop periodically — so the page stays responsive and cancellation engages mid-pass.

Usage

Method What it does
MarkAsync(ctx, keyword, options, eachCb, cancellationToken) Marks every occurrence of keyword under ctx. eachCb receives one MarkedMatch per match in document order; collect them for navigation.
MarkAsync(ctx, keyword, eachCb, cancellationToken) The element-only legacy shape: forces element wrapping and reports each wrapper element individually.
UnmarkAsync(ctx, options, cancellationToken) Removes every mark under ctx: registry ranges are deregistered, wrapper elements unwrapped and their parents re-normalized. Call it before re-marking with a new keyword.
FocusResult(ctx, match, scrollIntoViewIfNeeded) Moves the focused-match highlight to one match and clears it from the others.
IsHighlightApiSupported Feature check for the CSS Custom Highlight API.

A MarkedMatch carries either Range (highlight registry) or Elements (the wrapper elements, one per crossed text node), plus Text. FocusResult handles both: a registry match joins the tss-marked-focused highlight, which has priority above the base one, and is scrolled into view by nudging its scrollable ancestors; a wrapped match takes the class tss-highlight-focused plus an inline theme danger background, so it also shows inside iframes, and the standard scrollIntoView. An HTMLElement overload remains for element-only callers.

MarkOptions are all optional, and anything unset falls back to the static defaults:

Option Effect
Separator split the keyword into several terms
SeparateWordSearch match each word of the keyword on its own
WholeWord require word boundaries around the match
Wildcards * matches any run of non-space characters, ? one optional non-space character
IgnoreJoiners match across soft hyphens and zero-width joiners
MinLength skip keywords shorter than this
AcrossElements match phrases split by inline tags (<b>web</b> applications). Through the registry a spanning match is one range; wrapped, it becomes one mark element per crossed text node, all in that match's Elements
UseHighlightApi null (default) uses the highlight API when supported and no custom Element / MarkData / ClassName is set; true uses it whenever supported; false always wraps in elements. Iframe text is always wrapped

Because per-call options exist, the static fields (MarkHighlighter.Element, .MarkData, .ClassName, RegExpCreator.CaseSensitive) are only defaults. Pass MarkOptions so independent surfaces never fight over global state.

API reference

class

MarkHighlighter

public static class MarkHighlighter

Marks every occurrence of a keyword inside a DOM subtree (same-origin iframes included) and unmarks it again. Where the browser supports the CSS Custom Highlight API, matches in the page itself are painted through the highlight registry (see tss.markhighlighter.css) with no DOM mutation at all; text inside iframes, browsers without the API, and callers that opt out get the classic wrap-in-mark-element treatment. Passes on the same root are serialized: starting a new mark or unmark cancels and awaits the previous one, so callers can fire on every keystroke without racing themselves. Adapted from mark.js (https://github.com/julkue/mark.js, MIT).

Namespace
Tesserae

Properties

NameDescription
IsHighlightApiSupportedWhether this browser has the CSS Custom Highlight API (CSS.highlights and the Highlight constructor).
Property
MarkHighlighter.IsHighlightApiSupported
public static bool IsHighlightApiSupported { get ; }

Whether this browser has the CSS Custom Highlight API (CSS.highlights and the Highlight constructor).

Methods

NameDescription
ScrollRangeIntoViewBrings a highlight-registry match into view: unlike an element, a range has no scrollIntoView, so each scrollable ancestor is nudged until the range's rect is visible
RunExclusiveAsyncRuns one pass at a time per root: a new pass cancels the in-flight one and waits for it to finish, so a mark can never interleave with the unmark it replaces
Method
MarkHighlighter.ScrollRangeIntoView
private static void ScrollRangeIntoView(Range range)

Brings a highlight-registry match into view: unlike an element, a range has no scrollIntoView, so each scrollable ancestor is nudged until the range's rect is visible

Method
MarkHighlighter.RunExclusiveAsync
private static async Task RunExclusiveAsync(HTMLElement ctx, Func<CancellationToken, Task> pass, CancellationToken cancellationToken)

Runs one pass at a time per root: a new pass cancels the in-flight one and waits for it to finish, so a mark can never interleave with the unmark it replaces

Fields

NameDescription
ElementDefault wrapper tag, used when Element is unset.
MarkDataDefault data-* attribute name, used when MarkData is unset.
ClassNameDefault extra class on each wrapper, used when ClassName is unset.
Field
MarkHighlighter.Element
public static string Element = "mark"

Default wrapper tag, used when Element is unset.

Field
MarkHighlighter.MarkData
public static string MarkData = "marked"

Default data-* attribute name, used when MarkData is unset.

Field
MarkHighlighter.ClassName
public static string ClassName = null

Default extra class on each wrapper, used when ClassName is unset.

class

MarkOptions

public sealed class MarkOptions

Per-call configuration for MarkHighlighter and RegExpCreator. A field left unset falls back to the matching static default, so two surfaces highlighting at the same time never fight over global state.

Namespace
Tesserae

Properties

NameDescription
ElementTag name of the wrapper element. Default: Element ("mark").
MarkDataName of the data-* attribute stamped on wrapped elements. Default: MarkData ("marked").
ClassNameExtra class set on each wrapper element. Default: ClassName.
CaseSensitiveCase-sensitive matching. Default: CaseSensitive.
DiacriticsFold diacritics, so 'cafe' also matches 'café'. Default: true.
WholeWordOnly match the keyword where it is not part of a longer word.
SeparateWordSearchSplit a multi-word keyword on whitespace and mark each word on its own.
WildcardsEnable '*' (any run of non-space characters) and '?' (one optional non-space character) in the keyword.
IgnoreJoinersMatch across soft hyphens and zero-width joiners, which hyphenated or justified documents carry mid-word.
MinLengthKeywords shorter than this are not marked. Default: 0 (everything is marked).
AcrossElementsMatch across element boundaries, so a phrase split by inline tags (boldweb applications) is still found.
UseHighlightApiWhether matches are painted through the CSS Custom Highlight API (CSS.highlights + the ::highlight() rules in tss.markhighlighter.css) instead of wrapping them in mark elements - no DOM mutation at all, so components that hold references to their own text nodes are untouched. Null (default): use it when the browser supports it and no custom Element/MarkData/ClassName is asked for. True: use it whenever supported. False: always wrap in elements. Text inside iframes is always wrapped in elements - a highlight registration is per document and an iframe's document doesn't carry the ::highlight() rules.
Property
MarkOptions.Element
public string Element { get; set; }

Tag name of the wrapper element. Default: Element ("mark").

Property
MarkOptions.MarkData
public string MarkData { get; set; }

Name of the data-* attribute stamped on wrapped elements. Default: MarkData ("marked").

Property
MarkOptions.ClassName
public string ClassName { get; set; }

Extra class set on each wrapper element. Default: ClassName.

Property
MarkOptions.CaseSensitive
public bool? CaseSensitive { get; set; }

Case-sensitive matching. Default: CaseSensitive.

Property
MarkOptions.Diacritics
public bool Diacritics { get; set; }

Fold diacritics, so 'cafe' also matches 'café'. Default: true.

Property
MarkOptions.WholeWord
public bool WholeWord { get; set; }

Only match the keyword where it is not part of a longer word.

Property
MarkOptions.SeparateWordSearch
public bool SeparateWordSearch { get; set; }

Split a multi-word keyword on whitespace and mark each word on its own.

Property
MarkOptions.Wildcards
public bool Wildcards { get; set; }

Enable '*' (any run of non-space characters) and '?' (one optional non-space character) in the keyword.

Property
MarkOptions.IgnoreJoiners
public bool IgnoreJoiners { get; set; }

Match across soft hyphens and zero-width joiners, which hyphenated or justified documents carry mid-word.

Property
MarkOptions.MinLength
public int MinLength { get; set; }

Keywords shorter than this are not marked. Default: 0 (everything is marked).

Property
MarkOptions.AcrossElements
public bool AcrossElements { get; set; }

Match across element boundaries, so a phrase split by inline tags (boldweb applications) is still found.

Property
MarkOptions.UseHighlightApi
public bool? UseHighlightApi { get; set; }

Whether matches are painted through the CSS Custom Highlight API (CSS.highlights + the ::highlight() rules in tss.markhighlighter.css) instead of wrapping them in mark elements - no DOM mutation at all, so components that hold references to their own text nodes are untouched. Null (default): use it when the browser supports it and no custom Element/MarkData/ClassName is asked for. True: use it whenever supported. False: always wrap in elements. Text inside iframes is always wrapped in elements - a highlight registration is per document and an iframe's document doesn't carry the ::highlight() rules.

class

MarkedMatch

public sealed class MarkedMatch

One keyword match reported by MarkAsync. Depending on the backend that painted it, the match is either wrapped in mark elements or registered as a live range in the CSS custom highlight registry - exactly one of Elements and Range is set. Hand the match back to FocusResult to focus it.

Namespace
Tesserae

Properties

NameDescription
ElementsThe wrapper elements of this match, in document order - one per text node the match crosses. Null when the match is painted through the CSS highlight registry.
RangeThe live range painted through the CSS highlight registry. Null when the match is wrapped in elements (iframes, browsers without the API, or opted out).
TextThe matched text.
Property
MarkedMatch.Elements
public HTMLElement[] Elements { get; internal set; }

The wrapper elements of this match, in document order - one per text node the match crosses. Null when the match is painted through the CSS highlight registry.

Property
MarkedMatch.Range
public Range Range { get; internal set; }

The live range painted through the CSS highlight registry. Null when the match is wrapped in elements (iframes, browsers without the API, or opted out).

Property
MarkedMatch.Text
public string Text { get; internal set; }

The matched text.

See also

© 2026 Curiosity. All rights reserved.