Skip to content

Canonical text and durable evidence anchors

canonicalVersion: 1 means unnormalized logical-order text. Internal ranges use UTF-16 code units, matching DOM Text/Range offsets. The only alternative accepted unit is codepoint, explicitly converted over the supplied exact text. Missing/unsupported units, empty ranges, negative/non-integer offsets, out-of-bounds endpoints and UTF-16 endpoints splitting surrogate pairs are rejected.

For A🚀é, the string has five UTF-16 code units and four Unicode code points. The rocket is [1,3) in UTF-16 and [1,2) in code points. The final combining mark is preserved; no NFC/NFD normalization occurs. Grapheme clusters are a display concern, not the stored unit. RTL text is indexed in logical source order, never visual screen order.

import { documentId, toUtf16Range, resolveAnchor } from 'entity-viz-kit/core';
const source = { id: documentId('doc:7'), version: 'v1', canonicalVersion: 1 as const, text: 'A🚀é' };
const range = toUtf16Range(source.text, { start: 1, end: 2, unit: 'codepoint' });
// { start: 1, end: 3, unit: 'utf16' }
const outcome = resolveAnchor({
documentId: source.id, documentVersion: 'v1', canonicalVersion: 1,
range, exact: '🚀', prefix: 'A', suffix: 'é'
}, source);
// outcome.status === 'exact'; range is a validated current-text coordinate.

createOffsetConverter(text) returns a reusable range conversion function with one lazy code-point map for that exact string. Use it for batches; it retains no global cache. Validators, segmenting, source windows and DOM mapping use the same implementation.

exact means the source version, offsets, quote and any supplied prefix/suffix validate. Otherwise every exact quote/context candidate is considered; one yields re-anchored, several yield ambiguous, none yield unavailable. There is no fuzzy path. A changed source with unchanged coordinates is still re-anchored, not exact. Wrong document identity cannot resolve.

The result retains original, currentVersion, reason, range (only for exact/re-anchored), candidate ranges and truncation state. At most 1,000 matching candidates are retained. Truncation cannot create false uniqueness. serializeAnchorResolution stores plain versioned data; restoreAnchorResolution(serialized, currentSource) recomputes against the supplied source so a serialized success is not treated as eternal proof.

Evidence includes its original exact quote, document ID/version, source label and snippet annotations relative to that quote. Display those original fields regardless of current-source access or resolution. A current page is a separate versioned object. Never silently rewrite original citation offsets to current offsets.

segmentText sweeps validated intervals into ordered non-overlapping segments with sorted explicit memberships. Joining segment text reconstructs the original string exactly, including Unicode/whitespace. Overlaps are not priority-resolved. segmentPassage projects validated mention IDs/entity IDs, query-match flags, evidence flags and a native source link onto these segments. Query membership and inspection styling are later projections, not alterations of source coordinates.

Text is bounded to two million UTF-16 units; intervals are bounded to 20,000 and emitted interval memberships to 250,000. Validation rejects a pathological allocation rather than silently dropping evidence. The current snapshot is not a claim of a measured interactive highlighter over every maximum-sized input.

import { createDomTextMap } from 'entity-viz-kit/highlighter';
import { documentId } from 'entity-viz-kit/core';
const map = createDomTextMap(articleElement, {
documentId: documentId('doc:7'), version: 'page-revision-3',
maxNodes: 100000, maxCharacters: 2000000,
excludeSelector: '.private-note'
});
const start = map.source.text.indexOf('Alex Morgan'); // A known fixture location, not entity recognition.
if (start >= 0) {
const ranges = map.ranges({ start, end: start + 11, unit: 'utf16' });
// Real Range instances across one or more Text nodes. The library does not paint them here.
}

The example lookup only locates an already-known phrase in a controlled fixture; production hosts supply validated entity annotations, not inferred identities.

Version 1 semantic block elements are ADDRESS, ARTICLE, ASIDE, BLOCKQUOTE, DIV, DL/DT/DD, FIGCAPTION/FIGURE, FOOTER/FORM, H1–H6, HEADER, HR, LI, MAIN, NAV, OL, P, PRE, SECTION, TABLE/TR and UL. BR and excluded subtrees also establish boundaries. A pending boundary inserts exactly one LF before the next nonempty text unless the preceding text ends with LF or the following text begins with LF. It does not normalize existing whitespace. The mapping root itself introduces no synthetic leading/trailing separator.

Each public part is either text or separator. Native ranges include only real Text pieces. Selecting A\nB across two paragraphs yields real ranges for A and B; joining their strings excludes the synthetic LF. The canonical source still contains it. Store canonical offsets against canonical text, never by concatenating returned Range strings.

Traversal excludes script/style/noscript/template, inputs/textareas/select/options/buttons, iframe/object/embed, editable subtrees, data-evk-private/data-evk-exclude, hidden and aria-hidden roots, plus the optional selector. Excluded text is never joined directly to neighboring words. This is a semantic text policy, not a computed-CSS visibility crawler. An invalid selector is rejected.

One explicitly supplied root is one coordinate space. Shadow roots and frame documents require separately permitted maps/IDs; traversal does not bypass browser access restrictions. Maps keep native node references private. isCurrent() compares a fresh bounded snapshot; replaced nodes, inserted/reordered text, changed exclusions or offset shifts invalidate it. ranges() rejects stale maps. The static map adds no mutation observer, injected event handling, network access or automatic repair. The 0.5 live highlighter provides that lifecycle around the same walk; see docs/guides/live-highlighter.md.