text-slicer 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["export type SplitMode = 'words' | 'chars' | 'both';\n\nexport interface TextSlicerOptions {\n container?: HTMLElement | string;\n splitMode?: SplitMode;\n cssVariables?: boolean;\n dataAttributes?: boolean;\n keepWhitespaceNodes?: boolean;\n containerHeightVar?: boolean;\n}\n\nexport interface TextSlicerMetrics {\n wordTotal: number;\n charTotal: number;\n renderedAt: number;\n}\n\nexport interface TextSlicerCallbacks {\n onAfterRender?: (metrics: TextSlicerMetrics) => void;\n}\n\ntype RuntimeOptions = Required<Omit<TextSlicerOptions, 'container'>>;\n\nconst DEFAULT_OPTIONS: RuntimeOptions = {\n splitMode: 'both',\n cssVariables: false,\n dataAttributes: false,\n keepWhitespaceNodes: true,\n containerHeightVar: false,\n};\n\nexport const CLASSNAMES = Object.freeze({\n word: 'ts-word',\n char: 'ts-char',\n whitespace: 'ts-whitespace',\n});\n\nconst SPACE = ' ';\nconst CSS_VAR_WORD_TOTAL = '--word-total';\nconst CSS_VAR_CHAR_TOTAL = '--char-total';\nconst CSS_VAR_WORD_INDEX = '--word-index';\nconst CSS_VAR_CHAR_INDEX = '--char-index';\nconst CSS_VAR_CONTAINER_HEIGHT = '--container-height';\nconst MEASURING_CLASS = 'ts-measuring';\n\nconst canUseDOM = (): boolean => typeof window !== 'undefined' && typeof document !== 'undefined';\n\nconst resolveContainer = (container?: HTMLElement | string): HTMLElement | null => {\n if (!canUseDOM()) return null;\n if (!container) return null;\n\n return typeof container === 'string' ? document.querySelector(container) : container;\n};\n\ntype IntlWithSegmenter = typeof Intl & {\n Segmenter?: new (locales?: string | string[], options?: Intl.SegmenterOptions) => Intl.Segmenter;\n};\n\nconst splitIntoGraphemes = (text: string): string[] => {\n const Seg = (Intl as IntlWithSegmenter).Segmenter;\n\n if (typeof Seg === 'function') {\n const segmenter = new Seg('en', { granularity: 'grapheme' });\n return Array.from(segmenter.segment(text), (s: Intl.SegmentData) => s.segment);\n }\n\n return Array.from(text);\n};\n\nconst splitIntoWords = (text: string): string[] => text.split(SPACE);\n\nconst emptyElement = (el: HTMLElement): void => {\n el.replaceChildren();\n};\n\nconst isHTMLElement = (el: unknown): el is HTMLElement =>\n !!el && typeof HTMLElement !== 'undefined' && el instanceof HTMLElement;\n\ntype NonUndefined<T> = T extends undefined ? never : T;\ntype OmitUndefined<T extends Record<string, unknown>> = {\n [K in keyof T as T[K] extends undefined ? never : K]: NonUndefined<T[K]>;\n};\n\nconst omitUndefined = <T extends Record<string, unknown>>(obj: T): OmitUndefined<T> => {\n const out = {} as OmitUndefined<T>;\n\n (Object.keys(obj) as Array<keyof T>).forEach((key) => {\n const val = obj[key];\n\n if (val !== undefined) {\n (out as Record<string, unknown>)[key as string] = val as unknown;\n }\n });\n\n return out;\n};\n\nexport class TextSlicer {\n private readonly el: HTMLElement | null;\n private original: string;\n private opts: RuntimeOptions;\n private callbacks: TextSlicerCallbacks | undefined;\n private charIndex: number;\n private mounted: boolean;\n private resizeObserver?: ResizeObserver;\n\n constructor(options: TextSlicerOptions = {}, callbacks?: TextSlicerCallbacks) {\n const el = resolveContainer(options.container);\n this.el = el;\n this.original = isHTMLElement(el) ? (el.textContent?.toString() ?? '') : '';\n this.opts = {\n ...DEFAULT_OPTIONS,\n ...omitUndefined<Omit<TextSlicerOptions, 'container'>>(options),\n } as RuntimeOptions;\n\n this.callbacks = callbacks;\n this.charIndex = 0;\n this.mounted = false;\n }\n\n get metrics(): TextSlicerMetrics {\n const text = this.original;\n\n return {\n wordTotal: text.length ? splitIntoWords(text).length : 0,\n charTotal: text.length,\n renderedAt: Date.now(),\n };\n }\n\n init(): void {\n if (!this.el) return;\n\n this.mounted = true;\n this.split();\n\n if (this.opts.containerHeightVar) {\n this.initHeightObserver();\n }\n }\n\n reinit(newText?: string, next?: Partial<TextSlicerOptions>): void {\n if (!this.el) return;\n if (typeof newText === 'string') this.original = newText;\n if (next) this.opts = { ...this.opts, ...omitUndefined(next) } as RuntimeOptions;\n\n this.split();\n }\n\n clear(): void {\n if (!this.el) return;\n\n emptyElement(this.el);\n }\n\n split(): void {\n if (!this.el) return;\n\n this.clear();\n this.charIndex = 0;\n\n const text = this.original;\n const fragment = document.createDocumentFragment();\n const words = splitIntoWords(text);\n\n if (this.opts.splitMode === 'chars') {\n this.appendChars(fragment, text);\n } else {\n this.appendWords(fragment, words);\n }\n\n this.el.appendChild(fragment);\n\n if (this.opts.cssVariables) {\n this.el.style.setProperty(CSS_VAR_WORD_TOTAL, String(words.length));\n this.el.style.setProperty(CSS_VAR_CHAR_TOTAL, String(text.length));\n }\n\n this.callbacks?.onAfterRender?.(this.metrics);\n }\n\n destroy(): void {\n if (!this.el) return;\n\n this.clear();\n this.unlockHeight();\n\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = undefined;\n }\n\n if (this.opts.containerHeightVar) {\n this.el.style.removeProperty(CSS_VAR_CONTAINER_HEIGHT);\n }\n\n this.mounted = false;\n }\n\n updateOptions(next: Partial<TextSlicerOptions>): void {\n this.opts = { ...this.opts, ...omitUndefined(next) } as RuntimeOptions;\n\n if (this.mounted) this.split();\n }\n\n lockHeight(): void {\n if (!this.el) return;\n\n const h = this.measureHeight();\n\n if (h > 0) {\n this.el.style.height = `${h}px`;\n }\n }\n\n unlockHeight(): void {\n if (!this.el) return;\n\n this.el.style.removeProperty('height');\n }\n\n private appendWords(fragment: DocumentFragment, words: string[]): void {\n words.forEach((word, wordIndex) => {\n if (this.opts.splitMode === 'both') {\n const wordSpan = this.createWordSpan(wordIndex, word);\n\n for (const ch of splitIntoGraphemes(word)) {\n const charSpan = this.createCharSpan(ch);\n\n wordSpan.append(charSpan);\n }\n\n fragment.append(wordSpan);\n } else {\n const wordSpan = this.createWordSpan(wordIndex);\n\n wordSpan.append(document.createTextNode(word));\n fragment.append(wordSpan);\n }\n\n if (wordIndex < words.length - 1) {\n fragment.append(this.createSpaceSpan());\n }\n });\n }\n\n private appendChars(fragment: DocumentFragment, text: string): void {\n for (const ch of splitIntoGraphemes(text)) {\n const span = this.createCharSpan(ch);\n\n fragment.append(span);\n }\n }\n\n private createWordSpan(index: number, word: string = ''): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.classList.add(CLASSNAMES.word);\n\n if (this.opts.dataAttributes && word) {\n span.setAttribute('data-word', word);\n }\n\n if (this.opts.cssVariables) {\n span.style.setProperty(CSS_VAR_WORD_INDEX, String(index));\n }\n\n return span;\n }\n\n private createCharSpan(ch: string): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.textContent = ch;\n\n if (this.opts.dataAttributes) span.setAttribute('data-char', ch);\n\n if (ch === SPACE) {\n span.classList.add(CLASSNAMES.whitespace);\n\n if (!this.opts.keepWhitespaceNodes) span.textContent = SPACE;\n } else {\n span.classList.add(CLASSNAMES.char);\n\n if (this.opts.cssVariables) {\n span.style.setProperty(CSS_VAR_CHAR_INDEX, String(this.charIndex));\n }\n\n this.charIndex += 1;\n }\n\n return span;\n }\n\n private createSpaceSpan(): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.classList.add(CLASSNAMES.whitespace);\n span.textContent = SPACE;\n\n return span;\n }\n\n private measureHeight(): number {\n if (!this.el) return 0;\n\n this.el.classList.add(MEASURING_CLASS);\n void this.el.offsetHeight;\n\n let h = this.el.offsetHeight || this.el.clientHeight || 0;\n\n if (!h) {\n h = Math.round(this.el.getBoundingClientRect().height);\n }\n\n this.el.classList.remove(MEASURING_CLASS);\n\n return Math.max(0, Math.ceil(h));\n }\n\n private initHeightObserver(): void {\n if (!this.el) return;\n\n this.resizeObserver = new ResizeObserver(() => {\n const h = this.measureHeight();\n\n if (h > 0) {\n this.el?.style.setProperty(CSS_VAR_CONTAINER_HEIGHT, `${h}px`);\n }\n });\n\n this.resizeObserver.observe(this.el);\n }\n}\n"],"mappings":";;;AAuBA,MAAM,kBAAkC;CACtC,WAAW;CACX,cAAc;CACd,gBAAgB;CAChB,qBAAqB;CACrB,oBAAoB;AACtB;AAEA,MAAa,aAAa,OAAO,OAAO;CACtC,MAAM;CACN,MAAM;CACN,YAAY;AACd,CAAC;AAED,MAAM,QAAQ;AACd,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAC3B,MAAM,2BAA2B;AACjC,MAAM,kBAAkB;AAExB,MAAM,kBAA2B,OAAO,WAAW,eAAe,OAAO,aAAa;AAEtF,MAAM,oBAAoB,cAAyD;CACjF,IAAI,CAAC,UAAU,GAAG,OAAO;CACzB,IAAI,CAAC,WAAW,OAAO;CAEvB,OAAO,OAAO,cAAc,WAAW,SAAS,cAAc,SAAS,IAAI;AAC7E;AAMA,MAAM,sBAAsB,SAA2B;CACrD,MAAM,MAAO,KAA2B;CAExC,IAAI,OAAO,QAAQ,YAAY;EAC7B,MAAM,YAAY,IAAI,IAAI,MAAM,EAAE,aAAa,WAAW,CAAC;EAC3D,OAAO,MAAM,KAAK,UAAU,QAAQ,IAAI,IAAI,MAAwB,EAAE,OAAO;CAC/E;CAEA,OAAO,MAAM,KAAK,IAAI;AACxB;AAEA,MAAM,kBAAkB,SAA2B,KAAK,MAAM,KAAK;AAEnE,MAAM,gBAAgB,OAA0B;CAC9C,GAAG,gBAAgB;AACrB;AAEA,MAAM,iBAAiB,OACrB,CAAC,CAAC,MAAM,OAAO,gBAAgB,eAAe,cAAc;AAO9D,MAAM,iBAAoD,QAA6B;CACrF,MAAM,MAAM,CAAC;CAEb,AAAC,OAAO,KAAK,GAAG,CAAC,CAAoB,SAAS,QAAQ;EACpD,MAAM,MAAM,IAAI;EAEhB,IAAI,QAAQ,QACV,AAAC,IAAgC,OAAiB;CAEtD,CAAC;CAED,OAAO;AACT;AAEA,IAAa,aAAb,MAAwB;CACtB,AAAiB;CACjB,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,YAAY,UAA6B,CAAC,GAAG,WAAiC;EAC5E,MAAM,KAAK,iBAAiB,QAAQ,SAAS;EAC7C,KAAK,KAAK;EACV,KAAK,WAAW,cAAc,EAAE,IAAK,GAAG,aAAa,SAAS,KAAK,KAAM;EACzE,KAAK,OAAO;GACV,GAAG;GACH,GAAG,cAAoD,OAAO;EAChE;EAEA,KAAK,YAAY;EACjB,KAAK,YAAY;EACjB,KAAK,UAAU;CACjB;CAEA,IAAI,UAA6B;EAC/B,MAAM,OAAO,KAAK;EAElB,OAAO;GACL,WAAW,KAAK,SAAS,eAAe,IAAI,CAAC,CAAC,SAAS;GACvD,WAAW,KAAK;GAChB,YAAY,KAAK,IAAI;EACvB;CACF;CAEA,OAAa;EACX,IAAI,CAAC,KAAK,IAAI;EAEd,KAAK,UAAU;EACf,KAAK,MAAM;EAEX,IAAI,KAAK,KAAK,oBACZ,KAAK,mBAAmB;CAE5B;CAEA,OAAO,SAAkB,MAAyC;EAChE,IAAI,CAAC,KAAK,IAAI;EACd,IAAI,OAAO,YAAY,UAAU,KAAK,WAAW;EACjD,IAAI,MAAM,KAAK,OAAO;GAAE,GAAG,KAAK;GAAM,GAAG,cAAc,IAAI;EAAE;EAE7D,KAAK,MAAM;CACb;CAEA,QAAc;EACZ,IAAI,CAAC,KAAK,IAAI;EAEd,aAAa,KAAK,EAAE;CACtB;CAEA,QAAc;EACZ,IAAI,CAAC,KAAK,IAAI;EAEd,KAAK,MAAM;EACX,KAAK,YAAY;EAEjB,MAAM,OAAO,KAAK;EAClB,MAAM,WAAW,SAAS,uBAAuB;EACjD,MAAM,QAAQ,eAAe,IAAI;EAEjC,IAAI,KAAK,KAAK,cAAc,SAC1B,KAAK,YAAY,UAAU,IAAI;OAE/B,KAAK,YAAY,UAAU,KAAK;EAGlC,KAAK,GAAG,YAAY,QAAQ;EAE5B,IAAI,KAAK,KAAK,cAAc;GAC1B,KAAK,GAAG,MAAM,YAAY,oBAAoB,OAAO,MAAM,MAAM,CAAC;GAClE,KAAK,GAAG,MAAM,YAAY,oBAAoB,OAAO,KAAK,MAAM,CAAC;EACnE;EAEA,KAAK,WAAW,gBAAgB,KAAK,OAAO;CAC9C;CAEA,UAAgB;EACd,IAAI,CAAC,KAAK,IAAI;EAEd,KAAK,MAAM;EACX,KAAK,aAAa;EAElB,IAAI,KAAK,gBAAgB;GACvB,KAAK,eAAe,WAAW;GAC/B,KAAK,iBAAiB;EACxB;EAEA,IAAI,KAAK,KAAK,oBACZ,KAAK,GAAG,MAAM,eAAe,wBAAwB;EAGvD,KAAK,UAAU;CACjB;CAEA,cAAc,MAAwC;EACpD,KAAK,OAAO;GAAE,GAAG,KAAK;GAAM,GAAG,cAAc,IAAI;EAAE;EAEnD,IAAI,KAAK,SAAS,KAAK,MAAM;CAC/B;CAEA,aAAmB;EACjB,IAAI,CAAC,KAAK,IAAI;EAEd,MAAM,IAAI,KAAK,cAAc;EAE7B,IAAI,IAAI,GACN,KAAK,GAAG,MAAM,SAAS,GAAG,EAAE;CAEhC;CAEA,eAAqB;EACnB,IAAI,CAAC,KAAK,IAAI;EAEd,KAAK,GAAG,MAAM,eAAe,QAAQ;CACvC;CAEA,AAAQ,YAAY,UAA4B,OAAuB;EACrE,MAAM,SAAS,MAAM,cAAc;GACjC,IAAI,KAAK,KAAK,cAAc,QAAQ;IAClC,MAAM,WAAW,KAAK,eAAe,WAAW,IAAI;IAEpD,KAAK,MAAM,MAAM,mBAAmB,IAAI,GAAG;KACzC,MAAM,WAAW,KAAK,eAAe,EAAE;KAEvC,SAAS,OAAO,QAAQ;IAC1B;IAEA,SAAS,OAAO,QAAQ;GAC1B,OAAO;IACL,MAAM,WAAW,KAAK,eAAe,SAAS;IAE9C,SAAS,OAAO,SAAS,eAAe,IAAI,CAAC;IAC7C,SAAS,OAAO,QAAQ;GAC1B;GAEA,IAAI,YAAY,MAAM,SAAS,GAC7B,SAAS,OAAO,KAAK,gBAAgB,CAAC;EAE1C,CAAC;CACH;CAEA,AAAQ,YAAY,UAA4B,MAAoB;EAClE,KAAK,MAAM,MAAM,mBAAmB,IAAI,GAAG;GACzC,MAAM,OAAO,KAAK,eAAe,EAAE;GAEnC,SAAS,OAAO,IAAI;EACtB;CACF;CAEA,AAAQ,eAAe,OAAe,OAAe,IAAqB;EACxE,MAAM,OAAO,SAAS,cAAc,MAAM;EAE1C,KAAK,UAAU,IAAI,WAAW,IAAI;EAElC,IAAI,KAAK,KAAK,kBAAkB,MAC9B,KAAK,aAAa,aAAa,IAAI;EAGrC,IAAI,KAAK,KAAK,cACZ,KAAK,MAAM,YAAY,oBAAoB,OAAO,KAAK,CAAC;EAG1D,OAAO;CACT;CAEA,AAAQ,eAAe,IAA6B;EAClD,MAAM,OAAO,SAAS,cAAc,MAAM;EAE1C,KAAK,cAAc;EAEnB,IAAI,KAAK,KAAK,gBAAgB,KAAK,aAAa,aAAa,EAAE;EAE/D,IAAI,OAAO,OAAO;GAChB,KAAK,UAAU,IAAI,WAAW,UAAU;GAExC,IAAI,CAAC,KAAK,KAAK,qBAAqB,KAAK,cAAc;EACzD,OAAO;GACL,KAAK,UAAU,IAAI,WAAW,IAAI;GAElC,IAAI,KAAK,KAAK,cACZ,KAAK,MAAM,YAAY,oBAAoB,OAAO,KAAK,SAAS,CAAC;GAGnE,KAAK,aAAa;EACpB;EAEA,OAAO;CACT;CAEA,AAAQ,kBAAmC;EACzC,MAAM,OAAO,SAAS,cAAc,MAAM;EAE1C,KAAK,UAAU,IAAI,WAAW,UAAU;EACxC,KAAK,cAAc;EAEnB,OAAO;CACT;CAEA,AAAQ,gBAAwB;EAC9B,IAAI,CAAC,KAAK,IAAI,OAAO;EAErB,KAAK,GAAG,UAAU,IAAI,eAAe;EACrC,AAAK,KAAK,GAAG;EAEb,IAAI,IAAI,KAAK,GAAG,gBAAgB,KAAK,GAAG,gBAAgB;EAExD,IAAI,CAAC,GACH,IAAI,KAAK,MAAM,KAAK,GAAG,sBAAsB,CAAC,CAAC,MAAM;EAGvD,KAAK,GAAG,UAAU,OAAO,eAAe;EAExC,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC;CACjC;CAEA,AAAQ,qBAA2B;EACjC,IAAI,CAAC,KAAK,IAAI;EAEd,KAAK,iBAAiB,IAAI,qBAAqB;GAC7C,MAAM,IAAI,KAAK,cAAc;GAE7B,IAAI,IAAI,GACN,KAAK,IAAI,MAAM,YAAY,0BAA0B,GAAG,EAAE,GAAG;EAEjE,CAAC;EAED,KAAK,eAAe,QAAQ,KAAK,EAAE;CACrC;AACF"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["export type SplitMode = 'words' | 'chars' | 'both';\n\nexport interface TextSlicerOptions {\n container?: HTMLElement | string;\n splitMode?: SplitMode;\n cssVariables?: boolean;\n dataAttributes?: boolean;\n keepWhitespaceNodes?: boolean;\n containerHeightVar?: boolean;\n locale?: string | string[];\n}\n\nexport interface TextSlicerMetrics {\n wordTotal: number;\n charTotal: number;\n renderedAt: number;\n}\n\nexport interface TextSlicerCallbacks {\n onAfterRender?: (metrics: TextSlicerMetrics) => void;\n}\n\ntype RuntimeOptions = Required<Omit<TextSlicerOptions, 'container' | 'locale'>> &\n Pick<TextSlicerOptions, 'locale'>;\n\nconst DEFAULT_OPTIONS: RuntimeOptions = {\n splitMode: 'both',\n cssVariables: false,\n dataAttributes: false,\n keepWhitespaceNodes: true,\n containerHeightVar: false,\n locale: undefined,\n};\n\nexport const CLASSNAMES = Object.freeze({\n word: 'ts-word',\n char: 'ts-char',\n whitespace: 'ts-whitespace',\n});\n\nconst CSS_VAR_WORD_TOTAL = '--word-total';\nconst CSS_VAR_CHAR_TOTAL = '--char-total';\nconst CSS_VAR_WORD_INDEX = '--word-index';\nconst CSS_VAR_CHAR_INDEX = '--char-index';\nconst CSS_VAR_CONTAINER_HEIGHT = '--container-height';\nconst MEASURING_CLASS = 'ts-measuring';\n\nconst canUseDOM = (): boolean => typeof window !== 'undefined' && typeof document !== 'undefined';\n\nconst resolveContainer = (container?: HTMLElement | string): HTMLElement | null => {\n if (!canUseDOM()) return null;\n if (!container) return null;\n\n return typeof container === 'string' ? document.querySelector(container) : container;\n};\n\ntype IntlWithSegmenter = typeof Intl & {\n Segmenter?: new (locales?: string | string[], options?: Intl.SegmenterOptions) => Intl.Segmenter;\n};\n\nconst localeKey = (locale: string | string[] | undefined): string =>\n locale === undefined ? '' : Array.isArray(locale) ? locale.join('\\0') : locale;\n\nconst segmenterCache = new Map<string, Intl.Segmenter>();\n\nconst getSegmenter = (\n locale: string | string[] | undefined,\n granularity: 'grapheme' | 'word',\n): Intl.Segmenter | null => {\n if (typeof Intl === 'undefined') return null;\n\n const Seg = (Intl as IntlWithSegmenter).Segmenter;\n\n if (typeof Seg !== 'function') return null;\n\n const key = `${granularity}:${localeKey(locale)}`;\n let segmenter = segmenterCache.get(key);\n\n if (!segmenter) {\n segmenter = new Seg(locale, { granularity });\n segmenterCache.set(key, segmenter);\n }\n\n return segmenter;\n};\n\nconst splitIntoGraphemes = (text: string, locale?: string | string[]): string[] => {\n const segmenter = getSegmenter(locale, 'grapheme');\n\n if (segmenter) {\n return Array.from(segmenter.segment(text), (s: Intl.SegmentData) => s.segment);\n }\n\n // Fallback: Array.from does not preserve all ZWJ/emoji grapheme clusters.\n // Remove when the browser baseline requires Intl.Segmenter.\n return Array.from(text);\n};\n\nconst isWhitespaceGrapheme = (ch: string): boolean => /^\\s+$/u.test(ch);\n\nconst countCharGraphemes = (text: string, locale?: string | string[]): number =>\n splitIntoGraphemes(text, locale).filter((ch) => !isWhitespaceGrapheme(ch)).length;\n\ntype WordSegment = Intl.SegmentData;\n\nconst segmentWords = (text: string, locale?: string | string[]): WordSegment[] => {\n const segmenter = getSegmenter(locale, 'word');\n\n if (segmenter) {\n return Array.from(segmenter.segment(text));\n }\n\n // Fallback: whitespace splitting is not locale-aware and keeps punctuation attached to tokens.\n // Remove when the browser baseline requires Intl.Segmenter.\n const words = text.split(/\\s+/u).filter(Boolean);\n const segments: WordSegment[] = [];\n\n let cursor = 0;\n\n for (const word of words) {\n const start = text.indexOf(word, cursor);\n\n if (start > cursor) {\n segments.push({\n segment: text.slice(cursor, start),\n index: cursor,\n input: text,\n isWordLike: false,\n });\n }\n\n segments.push({ segment: word, index: start, input: text, isWordLike: true });\n cursor = start + word.length;\n }\n\n if (cursor < text.length) {\n segments.push({ segment: text.slice(cursor), index: cursor, input: text, isWordLike: false });\n }\n\n return segments;\n};\n\nconst countWords = (text: string, locale?: string | string[]): number =>\n segmentWords(text, locale).filter((s) => s.isWordLike).length;\n\nconst emptyElement = (el: HTMLElement): void => {\n el.replaceChildren();\n};\n\nconst isHTMLElement = (el: unknown): el is HTMLElement =>\n !!el && typeof HTMLElement !== 'undefined' && el instanceof HTMLElement;\n\ntype NonUndefined<T> = T extends undefined ? never : T;\ntype OmitUndefined<T extends Record<string, unknown>> = {\n [K in keyof T as T[K] extends undefined ? never : K]: NonUndefined<T[K]>;\n};\n\nconst omitUndefined = <T extends Record<string, unknown>>(obj: T): OmitUndefined<T> => {\n const out = {} as OmitUndefined<T>;\n\n (Object.keys(obj) as Array<keyof T>).forEach((key) => {\n const val = obj[key];\n\n if (val !== undefined) {\n (out as Record<string, unknown>)[key as string] = val as unknown;\n }\n });\n\n return out;\n};\n\ntype RuntimeOptionPatch = Partial<Omit<TextSlicerOptions, 'container'>>;\n\nconst mergeRuntimeOptions = (base: RuntimeOptions, patch: RuntimeOptionPatch): RuntimeOptions => {\n const { container: _container, ...runtimeOptions } = patch as TextSlicerOptions;\n\n return {\n ...base,\n ...omitUndefined(runtimeOptions as Record<string, unknown>),\n };\n};\n\ntype ManagedStyle = { value: string; priority: string };\n\nexport class TextSlicer {\n private readonly el: HTMLElement | null;\n private original: string;\n private opts: RuntimeOptions;\n private callbacks: TextSlicerCallbacks | undefined;\n private charIndex: number;\n private mounted: boolean;\n private resizeObserver?: ResizeObserver;\n private managedStyles = new Map<string, ManagedStyle>();\n private managedAttributes = new Map<string, string | null>();\n\n constructor(options: TextSlicerOptions = {}, callbacks?: TextSlicerCallbacks) {\n const { container, ...runtimeOptions } = options;\n const el = resolveContainer(container);\n this.el = el;\n // Plain-text mode intentionally discards nested markup. Upgrade path:\n // walk text nodes and preserve element boundaries in a DOM-preserving mode.\n this.original = isHTMLElement(el) ? (el.textContent?.toString() ?? '') : '';\n this.opts = mergeRuntimeOptions(DEFAULT_OPTIONS, runtimeOptions);\n\n this.callbacks = callbacks;\n this.charIndex = 0;\n this.mounted = false;\n }\n\n get metrics(): TextSlicerMetrics {\n const text = this.original;\n const { locale } = this.opts;\n\n return {\n wordTotal: text.length ? countWords(text, locale) : 0,\n charTotal: text.length ? countCharGraphemes(text, locale) : 0,\n renderedAt: Date.now(),\n };\n }\n\n init(): void {\n if (!this.el) return;\n\n this.mounted = true;\n this.split();\n this.syncHeightObserver();\n }\n\n reinit(newText?: string, next?: RuntimeOptionPatch): void {\n if (!this.el) return;\n if (typeof newText === 'string') this.original = newText;\n if (next) this.opts = mergeRuntimeOptions(this.opts, next);\n\n this.mounted = true;\n this.split();\n this.syncHeightObserver();\n }\n\n clear(): void {\n if (!this.el) return;\n\n emptyElement(this.el);\n this.restoreManagedAttribute('aria-label');\n }\n\n split(): void {\n if (!this.el) return;\n\n emptyElement(this.el);\n this.charIndex = 0;\n\n const text = this.original;\n const fragment = document.createDocumentFragment();\n\n if (this.opts.splitMode === 'chars') {\n this.appendChars(fragment, text);\n } else {\n this.appendWords(fragment, text);\n }\n\n this.el.appendChild(fragment);\n this.syncAccessibleLabel(text);\n\n const metrics = this.metrics;\n\n if (this.opts.cssVariables) {\n this.setManagedStyle(CSS_VAR_WORD_TOTAL, String(metrics.wordTotal));\n this.setManagedStyle(CSS_VAR_CHAR_TOTAL, String(metrics.charTotal));\n } else {\n this.restoreManagedStyle(CSS_VAR_WORD_TOTAL);\n this.restoreManagedStyle(CSS_VAR_CHAR_TOTAL);\n }\n\n this.callbacks?.onAfterRender?.(metrics);\n }\n\n destroy(): void {\n if (!this.el) return;\n\n this.syncHeightObserver(false);\n this.restoreManagedStyles();\n this.restoreManagedAttribute('aria-label');\n this.el.textContent = this.original;\n this.mounted = false;\n }\n\n updateOptions(next: RuntimeOptionPatch): void {\n this.opts = mergeRuntimeOptions(this.opts, next);\n\n if (this.mounted) {\n this.split();\n this.syncHeightObserver();\n }\n }\n\n lockHeight(): void {\n if (!this.el) return;\n\n const h = this.measureHeight();\n\n if (h > 0) {\n this.setManagedStyle('height', `${h}px`);\n }\n }\n\n unlockHeight(): void {\n this.restoreManagedStyle('height');\n }\n\n private appendWords(fragment: DocumentFragment, text: string): void {\n const segments = segmentWords(text, this.opts.locale);\n let wordIndex = 0;\n\n for (const { segment, isWordLike } of segments) {\n if (isWordLike) {\n const wordSpan = this.createWordSpan(wordIndex, segment);\n\n if (this.opts.splitMode === 'both') {\n this.appendGraphemes(wordSpan, segment);\n } else {\n wordSpan.append(document.createTextNode(segment));\n }\n\n fragment.append(wordSpan);\n wordIndex += 1;\n } else if (isWhitespaceGrapheme(segment)) {\n if (this.opts.splitMode === 'both') {\n this.appendGraphemes(fragment, segment);\n } else {\n fragment.append(this.createWhitespaceSpan(segment));\n }\n } else if (segment.length > 0) {\n if (this.opts.splitMode === 'both') {\n this.appendGraphemes(fragment, segment);\n } else {\n fragment.append(this.createHiddenTextSpan(segment));\n }\n }\n }\n }\n\n private appendChars(fragment: DocumentFragment, text: string): void {\n for (const ch of splitIntoGraphemes(text, this.opts.locale)) {\n this.appendGrapheme(fragment, ch);\n }\n }\n\n private appendGraphemes(parent: DocumentFragment | HTMLElement, text: string): void {\n for (const ch of splitIntoGraphemes(text, this.opts.locale)) {\n this.appendGrapheme(parent, ch);\n }\n }\n\n private appendGrapheme(parent: DocumentFragment | HTMLElement, ch: string): void {\n if (isWhitespaceGrapheme(ch)) {\n if (this.opts.keepWhitespaceNodes) {\n parent.append(this.createWhitespaceSpan(ch));\n } else {\n parent.append(document.createTextNode(ch));\n }\n\n return;\n }\n\n parent.append(this.createCharSpan(ch));\n }\n\n private createWordSpan(index: number, word: string): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.classList.add(CLASSNAMES.word);\n span.setAttribute('aria-hidden', 'true');\n\n if (this.opts.dataAttributes) {\n span.setAttribute('data-word', word);\n }\n\n if (this.opts.cssVariables) {\n span.style.setProperty(CSS_VAR_WORD_INDEX, String(index));\n }\n\n return span;\n }\n\n private createCharSpan(ch: string): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.textContent = ch;\n span.setAttribute('aria-hidden', 'true');\n span.classList.add(CLASSNAMES.char);\n\n if (this.opts.dataAttributes) span.setAttribute('data-char', ch);\n\n if (this.opts.cssVariables) {\n span.style.setProperty(CSS_VAR_CHAR_INDEX, String(this.charIndex));\n }\n\n this.charIndex += 1;\n\n return span;\n }\n\n private createWhitespaceSpan(text: string): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.classList.add(CLASSNAMES.whitespace);\n span.textContent = text;\n span.setAttribute('aria-hidden', 'true');\n\n return span;\n }\n\n private createHiddenTextSpan(text: string): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.textContent = text;\n span.setAttribute('aria-hidden', 'true');\n\n return span;\n }\n\n private syncAccessibleLabel(text: string): void {\n if (!this.el) return;\n\n if (this.managedAttributes.has('aria-label')) {\n this.setManagedAttribute('aria-label', text);\n return;\n }\n\n if (this.el.hasAttribute('aria-label') || this.el.hasAttribute('aria-labelledby')) {\n return;\n }\n\n this.setManagedAttribute('aria-label', text);\n }\n\n private measureHeight(): number {\n if (!this.el) return 0;\n\n this.el.classList.add(MEASURING_CLASS);\n\n try {\n void this.el.offsetHeight;\n\n let h = this.el.offsetHeight || this.el.clientHeight || 0;\n\n if (!h) {\n h = Math.round(this.el.getBoundingClientRect().height);\n }\n\n return Math.max(0, Math.ceil(h));\n } finally {\n this.el.classList.remove(MEASURING_CLASS);\n }\n }\n\n private syncHeightObserver(enable: boolean = this.opts.containerHeightVar): void {\n if (!this.el) return;\n\n if (enable) {\n if (typeof ResizeObserver !== 'function') {\n this.restoreManagedStyle(CSS_VAR_CONTAINER_HEIGHT);\n return;\n }\n\n if (!this.resizeObserver) {\n this.resizeObserver = new ResizeObserver((entries) => {\n const entry = entries[0];\n\n if (!entry || !this.el) return;\n\n const boxSize = entry.borderBoxSize?.[0]?.blockSize;\n const h = Math.max(0, Math.ceil(boxSize ?? entry.contentRect.height));\n\n if (h > 0) {\n this.setManagedStyle(CSS_VAR_CONTAINER_HEIGHT, `${h}px`);\n }\n });\n\n this.resizeObserver.observe(this.el);\n }\n } else if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = undefined;\n this.restoreManagedStyle(CSS_VAR_CONTAINER_HEIGHT);\n }\n }\n\n private setManagedStyle(property: string, value: string): void {\n if (!this.el) return;\n\n if (!this.managedStyles.has(property)) {\n this.managedStyles.set(property, {\n value: this.el.style.getPropertyValue(property),\n priority: this.el.style.getPropertyPriority(property),\n });\n }\n\n this.el.style.setProperty(property, value);\n }\n\n private restoreManagedStyle(property: string): void {\n if (!this.el) return;\n\n const previous = this.managedStyles.get(property);\n\n if (previous === undefined) return;\n\n if (previous.value) {\n this.el.style.setProperty(property, previous.value, previous.priority);\n } else {\n this.el.style.removeProperty(property);\n }\n\n this.managedStyles.delete(property);\n }\n\n private restoreManagedStyles(): void {\n for (const property of [...this.managedStyles.keys()]) {\n this.restoreManagedStyle(property);\n }\n }\n\n private setManagedAttribute(name: string, value: string): void {\n if (!this.el) return;\n\n if (!this.managedAttributes.has(name)) {\n this.managedAttributes.set(name, this.el.getAttribute(name));\n }\n\n this.el.setAttribute(name, value);\n }\n\n private restoreManagedAttribute(name: string): void {\n if (!this.el || !this.managedAttributes.has(name)) return;\n\n const previous = this.managedAttributes.get(name);\n\n if (previous === null) {\n this.el.removeAttribute(name);\n } else if (previous !== undefined) {\n this.el.setAttribute(name, previous);\n }\n\n this.managedAttributes.delete(name);\n }\n}\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -7,6 +7,7 @@ interface TextSlicerOptions {
7
7
  dataAttributes?: boolean;
8
8
  keepWhitespaceNodes?: boolean;
9
9
  containerHeightVar?: boolean;
10
+ locale?: string | string[];
10
11
  }
11
12
  interface TextSlicerMetrics {
12
13
  wordTotal: number;
@@ -21,6 +22,7 @@ declare const CLASSNAMES: Readonly<{
21
22
  char: "ts-char";
22
23
  whitespace: "ts-whitespace";
23
24
  }>;
25
+ type RuntimeOptionPatch = Partial<Omit<TextSlicerOptions, 'container'>>;
24
26
  declare class TextSlicer {
25
27
  private readonly el;
26
28
  private original;
@@ -29,23 +31,34 @@ declare class TextSlicer {
29
31
  private charIndex;
30
32
  private mounted;
31
33
  private resizeObserver?;
34
+ private managedStyles;
35
+ private managedAttributes;
32
36
  constructor(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks);
33
37
  get metrics(): TextSlicerMetrics;
34
38
  init(): void;
35
- reinit(newText?: string, next?: Partial<TextSlicerOptions>): void;
39
+ reinit(newText?: string, next?: RuntimeOptionPatch): void;
36
40
  clear(): void;
37
41
  split(): void;
38
42
  destroy(): void;
39
- updateOptions(next: Partial<TextSlicerOptions>): void;
43
+ updateOptions(next: RuntimeOptionPatch): void;
40
44
  lockHeight(): void;
41
45
  unlockHeight(): void;
42
46
  private appendWords;
43
47
  private appendChars;
48
+ private appendGraphemes;
49
+ private appendGrapheme;
44
50
  private createWordSpan;
45
51
  private createCharSpan;
46
- private createSpaceSpan;
52
+ private createWhitespaceSpan;
53
+ private createHiddenTextSpan;
54
+ private syncAccessibleLabel;
47
55
  private measureHeight;
48
- private initHeightObserver;
56
+ private syncHeightObserver;
57
+ private setManagedStyle;
58
+ private restoreManagedStyle;
59
+ private restoreManagedStyles;
60
+ private setManagedAttribute;
61
+ private restoreManagedAttribute;
49
62
  }
50
63
  //#endregion
51
64
  export { CLASSNAMES, SplitMode, TextSlicer, TextSlicerCallbacks, TextSlicerMetrics, TextSlicerOptions };
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ interface TextSlicerOptions {
7
7
  dataAttributes?: boolean;
8
8
  keepWhitespaceNodes?: boolean;
9
9
  containerHeightVar?: boolean;
10
+ locale?: string | string[];
10
11
  }
11
12
  interface TextSlicerMetrics {
12
13
  wordTotal: number;
@@ -21,6 +22,7 @@ declare const CLASSNAMES: Readonly<{
21
22
  char: "ts-char";
22
23
  whitespace: "ts-whitespace";
23
24
  }>;
25
+ type RuntimeOptionPatch = Partial<Omit<TextSlicerOptions, 'container'>>;
24
26
  declare class TextSlicer {
25
27
  private readonly el;
26
28
  private original;
@@ -29,23 +31,34 @@ declare class TextSlicer {
29
31
  private charIndex;
30
32
  private mounted;
31
33
  private resizeObserver?;
34
+ private managedStyles;
35
+ private managedAttributes;
32
36
  constructor(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks);
33
37
  get metrics(): TextSlicerMetrics;
34
38
  init(): void;
35
- reinit(newText?: string, next?: Partial<TextSlicerOptions>): void;
39
+ reinit(newText?: string, next?: RuntimeOptionPatch): void;
36
40
  clear(): void;
37
41
  split(): void;
38
42
  destroy(): void;
39
- updateOptions(next: Partial<TextSlicerOptions>): void;
43
+ updateOptions(next: RuntimeOptionPatch): void;
40
44
  lockHeight(): void;
41
45
  unlockHeight(): void;
42
46
  private appendWords;
43
47
  private appendChars;
48
+ private appendGraphemes;
49
+ private appendGrapheme;
44
50
  private createWordSpan;
45
51
  private createCharSpan;
46
- private createSpaceSpan;
52
+ private createWhitespaceSpan;
53
+ private createHiddenTextSpan;
54
+ private syncAccessibleLabel;
47
55
  private measureHeight;
48
- private initHeightObserver;
56
+ private syncHeightObserver;
57
+ private setManagedStyle;
58
+ private restoreManagedStyle;
59
+ private restoreManagedStyles;
60
+ private setManagedAttribute;
61
+ private restoreManagedAttribute;
49
62
  }
50
63
  //#endregion
51
64
  export { CLASSNAMES, SplitMode, TextSlicer, TextSlicerCallbacks, TextSlicerMetrics, TextSlicerOptions };
package/dist/index.js CHANGED
@@ -4,14 +4,14 @@ const DEFAULT_OPTIONS = {
4
4
  cssVariables: false,
5
5
  dataAttributes: false,
6
6
  keepWhitespaceNodes: true,
7
- containerHeightVar: false
7
+ containerHeightVar: false,
8
+ locale: void 0
8
9
  };
9
10
  const CLASSNAMES = Object.freeze({
10
11
  word: "ts-word",
11
12
  char: "ts-char",
12
13
  whitespace: "ts-whitespace"
13
14
  });
14
- const SPACE = " ";
15
15
  const CSS_VAR_WORD_TOTAL = "--word-total";
16
16
  const CSS_VAR_CHAR_TOTAL = "--char-total";
17
17
  const CSS_VAR_WORD_INDEX = "--word-index";
@@ -24,15 +24,58 @@ const resolveContainer = (container) => {
24
24
  if (!container) return null;
25
25
  return typeof container === "string" ? document.querySelector(container) : container;
26
26
  };
27
- const splitIntoGraphemes = (text) => {
27
+ const localeKey = (locale) => locale === void 0 ? "" : Array.isArray(locale) ? locale.join("\0") : locale;
28
+ const segmenterCache = /* @__PURE__ */ new Map();
29
+ const getSegmenter = (locale, granularity) => {
30
+ if (typeof Intl === "undefined") return null;
28
31
  const Seg = Intl.Segmenter;
29
- if (typeof Seg === "function") {
30
- const segmenter = new Seg("en", { granularity: "grapheme" });
31
- return Array.from(segmenter.segment(text), (s) => s.segment);
32
+ if (typeof Seg !== "function") return null;
33
+ const key = `${granularity}:${localeKey(locale)}`;
34
+ let segmenter = segmenterCache.get(key);
35
+ if (!segmenter) {
36
+ segmenter = new Seg(locale, { granularity });
37
+ segmenterCache.set(key, segmenter);
32
38
  }
39
+ return segmenter;
40
+ };
41
+ const splitIntoGraphemes = (text, locale) => {
42
+ const segmenter = getSegmenter(locale, "grapheme");
43
+ if (segmenter) return Array.from(segmenter.segment(text), (s) => s.segment);
33
44
  return Array.from(text);
34
45
  };
35
- const splitIntoWords = (text) => text.split(SPACE);
46
+ const isWhitespaceGrapheme = (ch) => /^\s+$/u.test(ch);
47
+ const countCharGraphemes = (text, locale) => splitIntoGraphemes(text, locale).filter((ch) => !isWhitespaceGrapheme(ch)).length;
48
+ const segmentWords = (text, locale) => {
49
+ const segmenter = getSegmenter(locale, "word");
50
+ if (segmenter) return Array.from(segmenter.segment(text));
51
+ const words = text.split(/\s+/u).filter(Boolean);
52
+ const segments = [];
53
+ let cursor = 0;
54
+ for (const word of words) {
55
+ const start = text.indexOf(word, cursor);
56
+ if (start > cursor) segments.push({
57
+ segment: text.slice(cursor, start),
58
+ index: cursor,
59
+ input: text,
60
+ isWordLike: false
61
+ });
62
+ segments.push({
63
+ segment: word,
64
+ index: start,
65
+ input: text,
66
+ isWordLike: true
67
+ });
68
+ cursor = start + word.length;
69
+ }
70
+ if (cursor < text.length) segments.push({
71
+ segment: text.slice(cursor),
72
+ index: cursor,
73
+ input: text,
74
+ isWordLike: false
75
+ });
76
+ return segments;
77
+ };
78
+ const countWords = (text, locale) => segmentWords(text, locale).filter((s) => s.isWordLike).length;
36
79
  const emptyElement = (el) => {
37
80
  el.replaceChildren();
38
81
  };
@@ -45,6 +88,13 @@ const omitUndefined = (obj) => {
45
88
  });
46
89
  return out;
47
90
  };
91
+ const mergeRuntimeOptions = (base, patch) => {
92
+ const { container: _container, ...runtimeOptions } = patch;
93
+ return {
94
+ ...base,
95
+ ...omitUndefined(runtimeOptions)
96
+ };
97
+ };
48
98
  var TextSlicer = class {
49
99
  el;
50
100
  original;
@@ -53,23 +103,24 @@ var TextSlicer = class {
53
103
  charIndex;
54
104
  mounted;
55
105
  resizeObserver;
106
+ managedStyles = /* @__PURE__ */ new Map();
107
+ managedAttributes = /* @__PURE__ */ new Map();
56
108
  constructor(options = {}, callbacks) {
57
- const el = resolveContainer(options.container);
109
+ const { container, ...runtimeOptions } = options;
110
+ const el = resolveContainer(container);
58
111
  this.el = el;
59
112
  this.original = isHTMLElement(el) ? el.textContent?.toString() ?? "" : "";
60
- this.opts = {
61
- ...DEFAULT_OPTIONS,
62
- ...omitUndefined(options)
63
- };
113
+ this.opts = mergeRuntimeOptions(DEFAULT_OPTIONS, runtimeOptions);
64
114
  this.callbacks = callbacks;
65
115
  this.charIndex = 0;
66
116
  this.mounted = false;
67
117
  }
68
118
  get metrics() {
69
119
  const text = this.original;
120
+ const { locale } = this.opts;
70
121
  return {
71
- wordTotal: text.length ? splitIntoWords(text).length : 0,
72
- charTotal: text.length,
122
+ wordTotal: text.length ? countWords(text, locale) : 0,
123
+ charTotal: text.length ? countCharGraphemes(text, locale) : 0,
73
124
  renderedAt: Date.now()
74
125
  };
75
126
  }
@@ -77,130 +128,197 @@ var TextSlicer = class {
77
128
  if (!this.el) return;
78
129
  this.mounted = true;
79
130
  this.split();
80
- if (this.opts.containerHeightVar) this.initHeightObserver();
131
+ this.syncHeightObserver();
81
132
  }
82
133
  reinit(newText, next) {
83
134
  if (!this.el) return;
84
135
  if (typeof newText === "string") this.original = newText;
85
- if (next) this.opts = {
86
- ...this.opts,
87
- ...omitUndefined(next)
88
- };
136
+ if (next) this.opts = mergeRuntimeOptions(this.opts, next);
137
+ this.mounted = true;
89
138
  this.split();
139
+ this.syncHeightObserver();
90
140
  }
91
141
  clear() {
92
142
  if (!this.el) return;
93
143
  emptyElement(this.el);
144
+ this.restoreManagedAttribute("aria-label");
94
145
  }
95
146
  split() {
96
147
  if (!this.el) return;
97
- this.clear();
148
+ emptyElement(this.el);
98
149
  this.charIndex = 0;
99
150
  const text = this.original;
100
151
  const fragment = document.createDocumentFragment();
101
- const words = splitIntoWords(text);
102
152
  if (this.opts.splitMode === "chars") this.appendChars(fragment, text);
103
- else this.appendWords(fragment, words);
153
+ else this.appendWords(fragment, text);
104
154
  this.el.appendChild(fragment);
155
+ this.syncAccessibleLabel(text);
156
+ const metrics = this.metrics;
105
157
  if (this.opts.cssVariables) {
106
- this.el.style.setProperty(CSS_VAR_WORD_TOTAL, String(words.length));
107
- this.el.style.setProperty(CSS_VAR_CHAR_TOTAL, String(text.length));
158
+ this.setManagedStyle(CSS_VAR_WORD_TOTAL, String(metrics.wordTotal));
159
+ this.setManagedStyle(CSS_VAR_CHAR_TOTAL, String(metrics.charTotal));
160
+ } else {
161
+ this.restoreManagedStyle(CSS_VAR_WORD_TOTAL);
162
+ this.restoreManagedStyle(CSS_VAR_CHAR_TOTAL);
108
163
  }
109
- this.callbacks?.onAfterRender?.(this.metrics);
164
+ this.callbacks?.onAfterRender?.(metrics);
110
165
  }
111
166
  destroy() {
112
167
  if (!this.el) return;
113
- this.clear();
114
- this.unlockHeight();
115
- if (this.resizeObserver) {
116
- this.resizeObserver.disconnect();
117
- this.resizeObserver = void 0;
118
- }
119
- if (this.opts.containerHeightVar) this.el.style.removeProperty(CSS_VAR_CONTAINER_HEIGHT);
168
+ this.syncHeightObserver(false);
169
+ this.restoreManagedStyles();
170
+ this.restoreManagedAttribute("aria-label");
171
+ this.el.textContent = this.original;
120
172
  this.mounted = false;
121
173
  }
122
174
  updateOptions(next) {
123
- this.opts = {
124
- ...this.opts,
125
- ...omitUndefined(next)
126
- };
127
- if (this.mounted) this.split();
175
+ this.opts = mergeRuntimeOptions(this.opts, next);
176
+ if (this.mounted) {
177
+ this.split();
178
+ this.syncHeightObserver();
179
+ }
128
180
  }
129
181
  lockHeight() {
130
182
  if (!this.el) return;
131
183
  const h = this.measureHeight();
132
- if (h > 0) this.el.style.height = `${h}px`;
184
+ if (h > 0) this.setManagedStyle("height", `${h}px`);
133
185
  }
134
186
  unlockHeight() {
135
- if (!this.el) return;
136
- this.el.style.removeProperty("height");
137
- }
138
- appendWords(fragment, words) {
139
- words.forEach((word, wordIndex) => {
140
- if (this.opts.splitMode === "both") {
141
- const wordSpan = this.createWordSpan(wordIndex, word);
142
- for (const ch of splitIntoGraphemes(word)) {
143
- const charSpan = this.createCharSpan(ch);
144
- wordSpan.append(charSpan);
145
- }
146
- fragment.append(wordSpan);
147
- } else {
148
- const wordSpan = this.createWordSpan(wordIndex);
149
- wordSpan.append(document.createTextNode(word));
150
- fragment.append(wordSpan);
151
- }
152
- if (wordIndex < words.length - 1) fragment.append(this.createSpaceSpan());
153
- });
187
+ this.restoreManagedStyle("height");
188
+ }
189
+ appendWords(fragment, text) {
190
+ const segments = segmentWords(text, this.opts.locale);
191
+ let wordIndex = 0;
192
+ for (const { segment, isWordLike } of segments) if (isWordLike) {
193
+ const wordSpan = this.createWordSpan(wordIndex, segment);
194
+ if (this.opts.splitMode === "both") this.appendGraphemes(wordSpan, segment);
195
+ else wordSpan.append(document.createTextNode(segment));
196
+ fragment.append(wordSpan);
197
+ wordIndex += 1;
198
+ } else if (isWhitespaceGrapheme(segment)) if (this.opts.splitMode === "both") this.appendGraphemes(fragment, segment);
199
+ else fragment.append(this.createWhitespaceSpan(segment));
200
+ else if (segment.length > 0) if (this.opts.splitMode === "both") this.appendGraphemes(fragment, segment);
201
+ else fragment.append(this.createHiddenTextSpan(segment));
154
202
  }
155
203
  appendChars(fragment, text) {
156
- for (const ch of splitIntoGraphemes(text)) {
157
- const span = this.createCharSpan(ch);
158
- fragment.append(span);
204
+ for (const ch of splitIntoGraphemes(text, this.opts.locale)) this.appendGrapheme(fragment, ch);
205
+ }
206
+ appendGraphemes(parent, text) {
207
+ for (const ch of splitIntoGraphemes(text, this.opts.locale)) this.appendGrapheme(parent, ch);
208
+ }
209
+ appendGrapheme(parent, ch) {
210
+ if (isWhitespaceGrapheme(ch)) {
211
+ if (this.opts.keepWhitespaceNodes) parent.append(this.createWhitespaceSpan(ch));
212
+ else parent.append(document.createTextNode(ch));
213
+ return;
159
214
  }
215
+ parent.append(this.createCharSpan(ch));
160
216
  }
161
- createWordSpan(index, word = "") {
217
+ createWordSpan(index, word) {
162
218
  const span = document.createElement("span");
163
219
  span.classList.add(CLASSNAMES.word);
164
- if (this.opts.dataAttributes && word) span.setAttribute("data-word", word);
220
+ span.setAttribute("aria-hidden", "true");
221
+ if (this.opts.dataAttributes) span.setAttribute("data-word", word);
165
222
  if (this.opts.cssVariables) span.style.setProperty(CSS_VAR_WORD_INDEX, String(index));
166
223
  return span;
167
224
  }
168
225
  createCharSpan(ch) {
169
226
  const span = document.createElement("span");
170
227
  span.textContent = ch;
228
+ span.setAttribute("aria-hidden", "true");
229
+ span.classList.add(CLASSNAMES.char);
171
230
  if (this.opts.dataAttributes) span.setAttribute("data-char", ch);
172
- if (ch === SPACE) {
173
- span.classList.add(CLASSNAMES.whitespace);
174
- if (!this.opts.keepWhitespaceNodes) span.textContent = SPACE;
175
- } else {
176
- span.classList.add(CLASSNAMES.char);
177
- if (this.opts.cssVariables) span.style.setProperty(CSS_VAR_CHAR_INDEX, String(this.charIndex));
178
- this.charIndex += 1;
179
- }
231
+ if (this.opts.cssVariables) span.style.setProperty(CSS_VAR_CHAR_INDEX, String(this.charIndex));
232
+ this.charIndex += 1;
180
233
  return span;
181
234
  }
182
- createSpaceSpan() {
235
+ createWhitespaceSpan(text) {
183
236
  const span = document.createElement("span");
184
237
  span.classList.add(CLASSNAMES.whitespace);
185
- span.textContent = SPACE;
238
+ span.textContent = text;
239
+ span.setAttribute("aria-hidden", "true");
240
+ return span;
241
+ }
242
+ createHiddenTextSpan(text) {
243
+ const span = document.createElement("span");
244
+ span.textContent = text;
245
+ span.setAttribute("aria-hidden", "true");
186
246
  return span;
187
247
  }
248
+ syncAccessibleLabel(text) {
249
+ if (!this.el) return;
250
+ if (this.managedAttributes.has("aria-label")) {
251
+ this.setManagedAttribute("aria-label", text);
252
+ return;
253
+ }
254
+ if (this.el.hasAttribute("aria-label") || this.el.hasAttribute("aria-labelledby")) return;
255
+ this.setManagedAttribute("aria-label", text);
256
+ }
188
257
  measureHeight() {
189
258
  if (!this.el) return 0;
190
259
  this.el.classList.add(MEASURING_CLASS);
191
- this.el.offsetHeight;
192
- let h = this.el.offsetHeight || this.el.clientHeight || 0;
193
- if (!h) h = Math.round(this.el.getBoundingClientRect().height);
194
- this.el.classList.remove(MEASURING_CLASS);
195
- return Math.max(0, Math.ceil(h));
260
+ try {
261
+ this.el.offsetHeight;
262
+ let h = this.el.offsetHeight || this.el.clientHeight || 0;
263
+ if (!h) h = Math.round(this.el.getBoundingClientRect().height);
264
+ return Math.max(0, Math.ceil(h));
265
+ } finally {
266
+ this.el.classList.remove(MEASURING_CLASS);
267
+ }
268
+ }
269
+ syncHeightObserver(enable = this.opts.containerHeightVar) {
270
+ if (!this.el) return;
271
+ if (enable) {
272
+ if (typeof ResizeObserver !== "function") {
273
+ this.restoreManagedStyle(CSS_VAR_CONTAINER_HEIGHT);
274
+ return;
275
+ }
276
+ if (!this.resizeObserver) {
277
+ this.resizeObserver = new ResizeObserver((entries) => {
278
+ const entry = entries[0];
279
+ if (!entry || !this.el) return;
280
+ const boxSize = entry.borderBoxSize?.[0]?.blockSize;
281
+ const h = Math.max(0, Math.ceil(boxSize ?? entry.contentRect.height));
282
+ if (h > 0) this.setManagedStyle(CSS_VAR_CONTAINER_HEIGHT, `${h}px`);
283
+ });
284
+ this.resizeObserver.observe(this.el);
285
+ }
286
+ } else if (this.resizeObserver) {
287
+ this.resizeObserver.disconnect();
288
+ this.resizeObserver = void 0;
289
+ this.restoreManagedStyle(CSS_VAR_CONTAINER_HEIGHT);
290
+ }
196
291
  }
197
- initHeightObserver() {
292
+ setManagedStyle(property, value) {
198
293
  if (!this.el) return;
199
- this.resizeObserver = new ResizeObserver(() => {
200
- const h = this.measureHeight();
201
- if (h > 0) this.el?.style.setProperty(CSS_VAR_CONTAINER_HEIGHT, `${h}px`);
294
+ if (!this.managedStyles.has(property)) this.managedStyles.set(property, {
295
+ value: this.el.style.getPropertyValue(property),
296
+ priority: this.el.style.getPropertyPriority(property)
202
297
  });
203
- this.resizeObserver.observe(this.el);
298
+ this.el.style.setProperty(property, value);
299
+ }
300
+ restoreManagedStyle(property) {
301
+ if (!this.el) return;
302
+ const previous = this.managedStyles.get(property);
303
+ if (previous === void 0) return;
304
+ if (previous.value) this.el.style.setProperty(property, previous.value, previous.priority);
305
+ else this.el.style.removeProperty(property);
306
+ this.managedStyles.delete(property);
307
+ }
308
+ restoreManagedStyles() {
309
+ for (const property of [...this.managedStyles.keys()]) this.restoreManagedStyle(property);
310
+ }
311
+ setManagedAttribute(name, value) {
312
+ if (!this.el) return;
313
+ if (!this.managedAttributes.has(name)) this.managedAttributes.set(name, this.el.getAttribute(name));
314
+ this.el.setAttribute(name, value);
315
+ }
316
+ restoreManagedAttribute(name) {
317
+ if (!this.el || !this.managedAttributes.has(name)) return;
318
+ const previous = this.managedAttributes.get(name);
319
+ if (previous === null) this.el.removeAttribute(name);
320
+ else if (previous !== void 0) this.el.setAttribute(name, previous);
321
+ this.managedAttributes.delete(name);
204
322
  }
205
323
  };
206
324