From f8e457ce88e6a993e78e9beb37852813b329164c Mon Sep 17 00:00:00 2001 From: J-Sek Date: Sun, 16 Aug 2026 02:53:29 +0200 Subject: [PATCH 1/5] feat(toHighlight): add ignore-accents support --- .changeset/highlight-ignore-accents.md | 9 + .../composables/to-highlight/accents.vue | 78 ++++++++ .../composables/transformers/to-highlight.md | 23 +++ .../src/pages/guide/features/utilities.md | 21 ++- .../src/composables/toHighlight/index.test.ts | 49 ++++++ .../0/src/composables/toHighlight/index.ts | 38 ++-- packages/0/src/maturity.json | 6 + packages/0/src/surface.test.ts | 2 +- packages/0/src/utilities/diacritics.test.ts | 85 +++++++++ packages/0/src/utilities/diacritics.ts | 166 ++++++++++++++++++ packages/0/src/utilities/index.ts | 1 + 11 files changed, 457 insertions(+), 21 deletions(-) create mode 100644 .changeset/highlight-ignore-accents.md create mode 100644 apps/docs/src/examples/composables/to-highlight/accents.vue create mode 100644 packages/0/src/utilities/diacritics.test.ts create mode 100644 packages/0/src/utilities/diacritics.ts diff --git a/.changeset/highlight-ignore-accents.md b/.changeset/highlight-ignore-accents.md new file mode 100644 index 0000000000..e9e9772814 --- /dev/null +++ b/.changeset/highlight-ignore-accents.md @@ -0,0 +1,9 @@ +--- +"@vuetify/v0": minor +--- + +feat(toHighlight): add `ignoreAccents` so a plain query matches accented text + +`toHighlight(text, query, { ignoreAccents })` folds diacritics before matching and maps the ranges back onto the source, so `zurich` highlights *Zürich* with its umlaut intact. It is directional — `'target'` folds only the text, `'query'` only the search term, `true` both sides — and covers the letters NFD leaves alone (`ł`, `ø`, `ß`, `æ`, …). + +The matcher behind it ships as `findMatchRanges(text, query, { ignoreCase, ignoreAccents, matchAll })` for filters that need the same ranges without the chunking. diff --git a/apps/docs/src/examples/composables/to-highlight/accents.vue b/apps/docs/src/examples/composables/to-highlight/accents.vue new file mode 100644 index 0000000000..f3a0267e38 --- /dev/null +++ b/apps/docs/src/examples/composables/to-highlight/accents.vue @@ -0,0 +1,78 @@ + + + diff --git a/apps/docs/src/pages/composables/transformers/to-highlight.md b/apps/docs/src/pages/composables/transformers/to-highlight.md index ee557d2f4d..dc6d098837 100644 --- a/apps/docs/src/pages/composables/transformers/to-highlight.md +++ b/apps/docs/src/pages/composables/transformers/to-highlight.md @@ -130,6 +130,22 @@ before chunking, so unordered or overlapping input is normalized for you. ::: +## Recipes + +### Accent-insensitive search + +`ignoreAccents` folds diacritics before matching, then maps the ranges back onto the source +string — the rendered chunks keep their original characters. It is directional: `'target'` folds +only the text so a plain `zurich` reaches *Zürich*, `'query'` folds only the search term so a +pasted `Trø` reaches plain `Tromso`, and `true` folds both sides. + +Letters that NFD leaves untouched are folded as well (`ł → l`, `ø → o`, `ß → ss`, `æ → ae`), and a +fold that changes length still reports ranges into the original text. + +::: gn-example +/composables/to-highlight/accents +::: + ## Accessibility Wrap matched chunks in the native `` element. It carries the implicit ARIA role @@ -154,6 +170,13 @@ Yes. The source `text` string is sliced at match boundaries, so the original cha Yes. The `matches` option accepts `MatchRange[]` — `[start, end]` pairs. Once `createFilter` exposes positional data, pass the result directly and skip the query path. +??? Does accent folding change the highlighted text? + +No. Folding happens on a working copy; the returned chunks are always slices of the source +`text`, so `Zürich` renders with its umlaut even when the query was `zurich`. A fold that changes +length — `ß → ss`, or a decomposed `e` + combining acute — is mapped back to the characters it +came from. + ??? How does it handle overlapping multi-term matches? Overlapping or adjacent spans are merged before the chunks array is produced. diff --git a/apps/docs/src/pages/guide/features/utilities.md b/apps/docs/src/pages/guide/features/utilities.md index 06e813720b..29a0e3ad3e 100644 --- a/apps/docs/src/pages/guide/features/utilities.md +++ b/apps/docs/src/pages/guide/features/utilities.md @@ -477,6 +477,26 @@ range(5, 10) // [10, 11, 12, 13, 14] range(0) // [] ``` +### findMatchRanges + +Locate a query inside a string and get back `[start, end]` pairs, optionally folding accents before matching. This is what backs the `ignoreAccents` option of [toHighlight](/composables/transformers/to-highlight): + +```ts +import { findMatchRanges } from '@vuetify/v0' + +findMatchRanges('Zürich', 'zurich', { ignoreCase: true, ignoreAccents: true }) // [[0, 6]] +findMatchRanges('Zürich', 'zurich', { ignoreCase: true }) // [] +``` + +`ignoreAccents` is directional. `'target'` folds only the text, so a plain query reaches accented entries; `'query'` folds only the search term, so a pasted `Tromsø` reaches plain `Tromso`; `true` folds both sides. + +```ts +findMatchRanges('Łódź', 'Lo', { ignoreAccents: 'target' }) // [[0, 2]] +findMatchRanges('cafe', 'café', { ignoreAccents: 'query' }) // [[0, 4]] +``` + +Folding covers combining marks plus the letters NFD leaves alone (`ł`, `ø`, `đ`, `ß`, `æ`, `œ`, …). Returned indices always address the original string, even when folding changed its length — `ß` folds to `ss` and the range still spans one source character. Pass `matchAll: true` for every occurrence instead of the first. + ### pxToNumber Parse a CSS pixel length into a number. Built for reading `getComputedStyle` output, where a length that does not apply resolves to `''` or `'auto'` rather than to a number: @@ -534,4 +554,3 @@ const id = useId() // 'v:0' (Vue's format) // Outside component const id = useId() // 'v0-0', 'v0-1', ... ``` - diff --git a/packages/0/src/composables/toHighlight/index.test.ts b/packages/0/src/composables/toHighlight/index.test.ts index 011686d3c1..2af29c3e87 100644 --- a/packages/0/src/composables/toHighlight/index.test.ts +++ b/packages/0/src/composables/toHighlight/index.test.ts @@ -132,6 +132,55 @@ describe('toHighlight', () => { }) }) + describe('accent folding', () => { + function matched (chunks: { text: string, match: boolean }[]) { + return chunks.filter(chunk => chunk.match).map(chunk => chunk.text) + } + + it('should not fold accents by default', () => { + expect(toHighlight('café', 'cafe')).toStrictEqual([{ text: 'café', match: false }]) + }) + + it('should fold both sides when ignoreAccents is true', () => { + expect(matched(toHighlight('a café here', 'cafe', { ignoreAccents: true }))).toStrictEqual(['café']) + }) + + it('should preserve the original text when the source is decomposed', () => { + const decomposed = 'cafe\u0301' + + expect(matched(toHighlight(`a ${decomposed} here`, 'cafe', { ignoreAccents: true }))) + .toStrictEqual([decomposed]) + }) + + it('should fold only the text when target', () => { + expect(matched(toHighlight('café', 'cafe', { ignoreAccents: 'target' }))).toStrictEqual(['café']) + expect(toHighlight('cafe', 'café', { ignoreAccents: 'target' })) + .toStrictEqual([{ text: 'cafe', match: false }]) + }) + + it('should fold only the query when query', () => { + expect(matched(toHighlight('cafe', 'café', { ignoreAccents: 'query' }))).toStrictEqual(['cafe']) + }) + + it('should combine with ignoreCase and matchAll', () => { + expect(matched(toHighlight('Zürich, zurich', 'zurich', { + ignoreAccents: true, + ignoreCase: true, + matchAll: true, + }))).toStrictEqual(['Zürich', 'zurich']) + }) + + it('should accept a getter for ignoreAccents', () => { + const ignoreAccents = shallowRef<'target' | false>(false) + const chunks = computed(() => toHighlight('Tromsø', 'tromso', { ignoreCase: true, ignoreAccents })) + + expect(matched(chunks.value)).toStrictEqual([]) + + ignoreAccents.value = 'target' + expect(matched(chunks.value)).toStrictEqual(['Tromsø']) + }) + }) + describe('priority and fallthrough', () => { it('should use pre-computed matches over query when both are provided', () => { expect(toHighlight('hello', 'hello', { matches: [[1, 3]] })).toStrictEqual([ diff --git a/packages/0/src/composables/toHighlight/index.ts b/packages/0/src/composables/toHighlight/index.ts index f33028a9f5..e1652981f7 100644 --- a/packages/0/src/composables/toHighlight/index.ts +++ b/packages/0/src/composables/toHighlight/index.ts @@ -22,9 +22,11 @@ import { toArray } from '#v0/composables/toArray' // Utilities +import { findMatchRanges } from '#v0/utilities' import { toValue } from 'vue' // Types +import type { IgnoreAccents } from '#v0/utilities' import type { MaybeRefOrGetter } from 'vue' /** @@ -83,6 +85,11 @@ export interface ToHighlightOptions { matchAll?: MaybeRefOrGetter /** Case-insensitive matching. Default `false`. */ ignoreCase?: MaybeRefOrGetter + /** + * Folds accents before matching. `'query'` normalizes only the search term, + * `'target'` only the text, `true` both sides. Default `false`. + */ + ignoreAccents?: MaybeRefOrGetter } function merge (ranges: readonly MatchRange[]): MatchRange[] { @@ -115,25 +122,17 @@ function chunk (text: string, ranges: readonly MatchRange[]): HighlightChunk[] { return chunks } -function find (text: string, query: string | string[], matchAll: boolean, ignoreCase: boolean): MatchRange[] { - const terms = toArray(query).filter(Boolean) - const haystack = ignoreCase ? text.toLocaleLowerCase() : text +function find ( + text: string, + query: string | string[], + matchAll: boolean, + ignoreCase: boolean, + ignoreAccents: IgnoreAccents, +): MatchRange[] { const spans: [number, number][] = [] - for (const term of terms) { - const needle = ignoreCase ? term.toLocaleLowerCase() : term - let index = haystack.indexOf(needle) - - if (index !== -1) { - spans.push([index, index + term.length]) - if (matchAll) { - index = haystack.indexOf(needle, index + term.length) - while (index !== -1) { - spans.push([index, index + term.length]) - index = haystack.indexOf(needle, index + term.length) - } - } - } + for (const term of toArray(query).filter(Boolean)) { + spans.push(...findMatchRanges(text, term, { ignoreCase, ignoreAccents, matchAll })) } return merge(spans) @@ -149,7 +148,7 @@ function find (text: string, query: string | string[], matchAll: boolean, ignore * * @param text The source string to split. * @param query One or more search terms. Empty strings are ignored. Case sensitivity controlled by `options.ignoreCase`. - * @param options Optional `matches`, `matchAll`, `ignoreCase`. + * @param options Optional `matches`, `matchAll`, `ignoreCase`, `ignoreAccents`. * @returns A `HighlightChunk[]` array. * * @see https://0.vuetifyjs.com/composables/transformers/to-highlight @@ -177,11 +176,12 @@ export function toHighlight ( const _matches = toValue(options.matches) const matchAll = toValue(options.matchAll) ?? false const ignoreCase = toValue(options.ignoreCase) ?? false + const ignoreAccents = toValue(options.ignoreAccents) ?? false if (_matches?.length) return chunk(_text, merge(_matches)) if (_query) { - const ranges = find(_text, _query, matchAll, ignoreCase) + const ranges = find(_text, _query, matchAll, ignoreCase, ignoreAccents) return ranges.length > 0 ? chunk(_text, ranges) : [{ text: _text, match: false }] } diff --git a/packages/0/src/maturity.json b/packages/0/src/maturity.json index bd596f84ba..39dfa780d3 100644 --- a/packages/0/src/maturity.json +++ b/packages/0/src/maturity.json @@ -736,6 +736,12 @@ "since": "0.0.15", "category": "utilities" }, + "findMatchRanges": { + "level": "preview", + "since": null, + "category": "utilities", + "description": "Finds match ranges of a query in a string, optionally folding accents on either side." + }, "foreground": { "level": "preview", "since": "0.1.11", diff --git a/packages/0/src/surface.test.ts b/packages/0/src/surface.test.ts index 729efea085..ee65d9e6f5 100644 --- a/packages/0/src/surface.test.ts +++ b/packages/0/src/surface.test.ts @@ -44,7 +44,7 @@ const COMPONENTS = [ ] const UTILITIES = [ - 'UNSAFE_KEYS', 'V0Error', 'apca', 'clamp', 'foreground', 'getActiveElement', 'hexToRgb', 'instanceExists', 'instanceName', 'isArray', 'isBoolean', 'isElement', 'isFunction', 'isNaN', 'isNull', 'isNullOrUndefined', 'isNumber', 'isObject', 'isPrimitive', 'isString', 'isSymbol', 'isThenable', 'isUndefined', 'isV0Error', 'mergeDeep', 'pxToNumber', 'range', 'resolveIds', 'resolveIndexes', 'rgbToHex', 'useId', + 'UNSAFE_KEYS', 'V0Error', 'apca', 'clamp', 'findMatchRanges', 'foreground', 'getActiveElement', 'hexToRgb', 'instanceExists', 'instanceName', 'isArray', 'isBoolean', 'isElement', 'isFunction', 'isNaN', 'isNull', 'isNullOrUndefined', 'isNumber', 'isObject', 'isPrimitive', 'isString', 'isSymbol', 'isThenable', 'isUndefined', 'isV0Error', 'mergeDeep', 'pxToNumber', 'range', 'resolveIds', 'resolveIndexes', 'rgbToHex', 'useId', ] describe('public surface', () => { diff --git a/packages/0/src/utilities/diacritics.test.ts b/packages/0/src/utilities/diacritics.test.ts new file mode 100644 index 0000000000..747e72fc6e --- /dev/null +++ b/packages/0/src/utilities/diacritics.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from 'vitest' + +// Utilities +import { findMatchRanges } from './diacritics' + +describe('diacritics', () => { + describe('findMatchRanges', () => { + it('should match exactly when ignoreAccents is off', () => { + expect(findMatchRanges('café', 'cafe', { matchAll: true })).toStrictEqual([]) + expect(findMatchRanges('cafe', 'cafe', { matchAll: true })).toStrictEqual([[0, 4]]) + }) + + it('should return no ranges for an empty query', () => { + expect(findMatchRanges('café', '', { ignoreAccents: true })).toStrictEqual([]) + }) + + it('should fold both sides when true', () => { + expect(findMatchRanges('café', 'cafe', { ignoreAccents: true })).toStrictEqual([[0, 4]]) + }) + + it('should map ranges back onto a decomposed source', () => { + const decomposed = 'cafe\u0301' // e + combining acute, 5 code units + + expect(decomposed).toHaveLength(5) + expect(findMatchRanges(decomposed, 'cafe', { ignoreAccents: true })).toStrictEqual([[0, 5]]) + }) + + it('should fold only the text when target', () => { + expect(findMatchRanges('café', 'cafe', { ignoreAccents: 'target' })).toStrictEqual([[0, 4]]) + expect(findMatchRanges('cafe', 'café', { ignoreAccents: 'target' })).toStrictEqual([]) + }) + + it('should fold only the query when query', () => { + expect(findMatchRanges('cafe', 'café', { ignoreAccents: 'query' })).toStrictEqual([[0, 4]]) + expect(findMatchRanges('café', 'cafe', { ignoreAccents: 'query' })).toStrictEqual([]) + }) + + it('should find the first occurrence only unless matchAll is set', () => { + expect(findMatchRanges('é é', 'e', { ignoreAccents: true })).toStrictEqual([[0, 1]]) + expect(findMatchRanges('é é', 'e', { ignoreAccents: true, matchAll: true })).toStrictEqual([[0, 1], [2, 3]]) + }) + + it('should combine folding with case-insensitivity', () => { + expect(findMatchRanges('RÉSUMÉ', 'resume', { ignoreAccents: true, ignoreCase: true })) + .toStrictEqual([[0, 6]]) + }) + + it('should stay case-sensitive unless ignoreCase is set', () => { + expect(findMatchRanges('RÉSUMÉ', 'resume', { ignoreAccents: true })).toStrictEqual([]) + }) + + it('should fold letters that NFD leaves untouched', () => { + expect(findMatchRanges('Łódź', 'lodz', { ignoreAccents: true, ignoreCase: true })) + .toStrictEqual([[0, 4]]) + expect(findMatchRanges('Łódź', 'Lo', { ignoreAccents: 'target' })).toStrictEqual([[0, 2]]) + }) + + it('should map ranges back across multi-character folds', () => { + // 'ß' folds to 'ss', so the match spans a single source character + expect(findMatchRanges('straße', 'strasse', { ignoreAccents: true })).toStrictEqual([[0, 6]]) + }) + + describe('astral characters (surrogate pairs)', () => { + // 3 capital Adlam letters (U+1E900..) case-fold to their lowercase forms. + const adlam = String.fromCodePoint(0x1_E9_00, 0x1_E9_01, 0x1_E9_02) + // CJK compatibility ideograph (U+2F800), NFD-decomposes to U+4E3D. + const cjkCompat = String.fromCodePoint(0x2_F8_00) + + it('should case-fold an astral bicameral script', () => { + expect(findMatchRanges(adlam, adlam.toLowerCase(), { ignoreAccents: true, ignoreCase: true })) + .toStrictEqual([[0, adlam.length]]) + }) + + it('should decompose an astral NFD character back to its base', () => { + expect(findMatchRanges(cjkCompat, cjkCompat.normalize('NFD'), { ignoreAccents: true })) + .toStrictEqual([[0, cjkCompat.length]]) + }) + + it('should keep emoji intact and map ranges past them', () => { + expect(findMatchRanges('👍 café', 'cafe', { ignoreAccents: true })).toStrictEqual([[3, 7]]) + expect(findMatchRanges('a👍b', 'b', { ignoreAccents: true })).toStrictEqual([[3, 4]]) + }) + }) + }) +}) diff --git a/packages/0/src/utilities/diacritics.ts b/packages/0/src/utilities/diacritics.ts new file mode 100644 index 0000000000..d977d5e593 --- /dev/null +++ b/packages/0/src/utilities/diacritics.ts @@ -0,0 +1,166 @@ +/** + * @module utilities/diacritics + * + * @remarks + * Accent-folding search primitives. Pure string transforms — no DOM, no state, + * no reactivity. Powers the `ignoreAccents` option of `toHighlight` and any + * filter that wants a plain query to reach accented text. + */ + +/** + * Which side of a comparison has its accents folded before matching. + * + * - `'query'` — only the query, so typing `café` finds plain `cafe` + * - `'target'` — only the text, so typing `cafe` finds accented `café` + * - `true` — both sides + * - `false` — neither + * + * @example + * ```ts + * import type { IgnoreAccents } from '@vuetify/v0' + * + * const ignoreAccents: IgnoreAccents = 'target' + * ``` + */ +export type IgnoreAccents = boolean | 'query' | 'target' + +const COMBINING_MARKS = /[\u0300-\u036F]/g + +// Letters that carry no combining mark, so NFD leaves them untouched. +const SPECIAL_LETTERS: Record = { + ł: 'l', + ø: 'o', + đ: 'd', + ð: 'd', + þ: 'th', + ħ: 'h', + ŧ: 't', + ŋ: 'n', + ß: 'ss', + æ: 'ae', + œ: 'oe', + ı: 'i', + Ł: 'L', + Ø: 'O', + Đ: 'D', + Ð: 'D', + Þ: 'Th', + Ħ: 'H', + Ŧ: 'T', + Ŋ: 'N', + ẞ: 'Ss', + Æ: 'Ae', + Œ: 'Oe', +} + +const SPECIAL_LETTER = /* @__PURE__ */ new RegExp(`[${Object.keys(SPECIAL_LETTERS).join('')}]`, 'g') + +function fold (str: string): string { + return str + .normalize('NFD') + .replace(COMBINING_MARKS, '') + .replace(SPECIAL_LETTER, char => SPECIAL_LETTERS[char]!) +} + +// Folds per code point and records the source index each output unit came from, +// so ranges found in the folded string can be mapped back onto the original. +// Both decomposition and lowercasing change length, so a straight indexOf on the +// folded string would otherwise report misaligned indices. +function foldWithMap (str: string, ignoreCase: boolean) { + let folded = '' + const map: number[] = [] + let index = 0 + + for (const char of str) { + const chunk = fold(ignoreCase ? char.toLocaleLowerCase() : char) + + folded += chunk + + // One entry per code unit, not per code point — an astral char folds to + // itself and still occupies two positions in `folded`. + for (let unit = 0; unit < chunk.length; unit++) map.push(index) + + index += char.length + } + + map.push(str.length) + + return { folded, map } +} + +function collect (haystack: string, needle: string, matchAll: boolean): [number, number][] { + const ranges: [number, number][] = [] + let index = haystack.indexOf(needle) + + while (index !== -1) { + ranges.push([index, index + needle.length]) + + if (!matchAll) break + + index = haystack.indexOf(needle, index + needle.length) + } + + return ranges +} + +/** + * Finds `[start, end]` index pairs where `query` occurs in `text`, optionally + * folding accents on either side. Returned indices always address the original + * `text`, even when folding changed its length. + * + * @param text The string to search. + * @param query The term to look for. An empty query yields no ranges. + * @param options Optional `ignoreCase`, `ignoreAccents`, `matchAll`. + * @returns Ranges into `text`, where `end` is exclusive. + * + * @example + * ```ts + * import { findMatchRanges } from '@vuetify/v0' + * + * findMatchRanges('Zürich', 'zurich', { ignoreCase: true, ignoreAccents: true }) + * // [[0, 6]] + * + * findMatchRanges('Łódź', 'Lo', { ignoreAccents: 'target' }) + * // [[0, 2]] + * ``` + */ +/* #__NO_SIDE_EFFECTS__ */ +export function findMatchRanges ( + text: string, + query: string, + options: { + ignoreCase?: boolean + ignoreAccents?: IgnoreAccents + matchAll?: boolean + } = {}, +): [number, number][] { + const { + ignoreCase = false, + ignoreAccents = false, + matchAll = false, + } = options + + const foldQuery = ignoreAccents === true || ignoreAccents === 'query' + const foldTarget = ignoreAccents === true || ignoreAccents === 'target' + + let needle = foldQuery ? fold(query) : query + + if (ignoreCase) { + needle = needle.toLocaleLowerCase() + } + + if (needle.length === 0) { + return [] + } + + if (!foldTarget) { + const haystack = ignoreCase ? text.toLocaleLowerCase() : text + + return collect(haystack, needle, matchAll) + } + + const { folded, map } = foldWithMap(text, ignoreCase) + + return collect(folded, needle, matchAll) + .map(([start, end]) => [map[start]!, map[end]!]) +} diff --git a/packages/0/src/utilities/index.ts b/packages/0/src/utilities/index.ts index 1bbad61545..beadd72e56 100644 --- a/packages/0/src/utilities/index.ts +++ b/packages/0/src/utilities/index.ts @@ -1,6 +1,7 @@ // Utilities export * from './apca' export * from './color' +export * from './diacritics' export * from './errors' export * from './helpers' export * from './instance' From 5fcf270d6ba5f7bf4e4a3e22c6f81a558955be24 Mon Sep 17 00:00:00 2001 From: John Leider Date: Mon, 17 Aug 2026 17:38:19 -0500 Subject: [PATCH 2/5] fix(toHighlight): remap ignoreCase and half-fold matches onto the source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit foldWithMap now runs whenever ignoreCase or target-folding can change length, so İ / İstanbul no longer highlights the following letter. Half of a multi-char fold (s vs ß) expands to the whole source character instead of a zero-width range. --- .changeset/highlight-ignore-accents.md | 2 +- .../composables/to-highlight/accents.vue | 9 +- .../composables/transformers/to-highlight.md | 7 +- .../src/pages/guide/features/utilities.md | 4 +- .../src/composables/toHighlight/index.test.ts | 16 +++ .../0/src/composables/toHighlight/index.ts | 37 ++----- packages/0/src/utilities/diacritics.test.ts | 18 ++- packages/0/src/utilities/diacritics.ts | 104 ++++++++++++------ 8 files changed, 126 insertions(+), 71 deletions(-) diff --git a/.changeset/highlight-ignore-accents.md b/.changeset/highlight-ignore-accents.md index e9e9772814..2dc7a10220 100644 --- a/.changeset/highlight-ignore-accents.md +++ b/.changeset/highlight-ignore-accents.md @@ -4,6 +4,6 @@ feat(toHighlight): add `ignoreAccents` so a plain query matches accented text -`toHighlight(text, query, { ignoreAccents })` folds diacritics before matching and maps the ranges back onto the source, so `zurich` highlights *Zürich* with its umlaut intact. It is directional — `'target'` folds only the text, `'query'` only the search term, `true` both sides — and covers the letters NFD leaves alone (`ł`, `ø`, `ß`, `æ`, …). +`toHighlight(text, query, { ignoreCase: true, ignoreAccents: true })` folds diacritics before matching and maps the ranges back onto the source, so `zurich` highlights *Zürich* with its umlaut intact. It is directional — `'target'` folds only the text, `'query'` only the search term, `true` both sides — and covers the letters NFD leaves alone (`ł`, `ø`, `ß`, `æ`, …). The matcher behind it ships as `findMatchRanges(text, query, { ignoreCase, ignoreAccents, matchAll })` for filters that need the same ranges without the chunking. diff --git a/apps/docs/src/examples/composables/to-highlight/accents.vue b/apps/docs/src/examples/composables/to-highlight/accents.vue index f3a0267e38..ba754457e4 100644 --- a/apps/docs/src/examples/composables/to-highlight/accents.vue +++ b/apps/docs/src/examples/composables/to-highlight/accents.vue @@ -1,19 +1,19 @@