dom-text-highlight 1.0.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.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # dom-text-highlight
2
+
3
+ Fuzzy text matching and highlighting for DOM content using the CSS Custom Highlight API.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install dom-text-highlight
9
+ ```
10
+
11
+ ## Basic Usage
12
+
13
+ Import the default function and pass the text you want to find. The library finds the closest fuzzy match inside the target root and applies a `dom-highlight` CSS highlight.
14
+
15
+ ```js
16
+ import DOMTextHighlight from 'dom-text-highlight'
17
+
18
+ const article = document.querySelector('article')
19
+ const range = DOMTextHighlight('custom highlight api', article)
20
+
21
+ console.log(range.toString())
22
+ ```
23
+
24
+ Add styles for the highlight name:
25
+
26
+ ```css
27
+ ::highlight(dom-highlight) {
28
+ background: #f6d85f;
29
+ color: #111;
30
+ }
31
+ ```
32
+
33
+ If no root is passed, the library searches `document.body`.
34
+
35
+ ```js
36
+ DOMTextHighlight('some text on the page')
37
+ ```
38
+
39
+ ## Iframe Usage
40
+
41
+ The package also exports `highlightInIframe`, which sends a highlight request from a parent page to a child iframe.
42
+
43
+ ```js
44
+ import { highlightInIframe } from 'dom-text-highlight'
45
+
46
+ const iframe = document.querySelector('iframe')
47
+
48
+ iframe.addEventListener('load', () => {
49
+ highlightInIframe(iframe, 'text inside the iframe')
50
+ })
51
+ ```
52
+
53
+ The child iframe must load this module too. Importing it registers the message listener that handles `dom-highlight` messages.
54
+
55
+ ```html
56
+ <script type="module">
57
+ import 'dom-text-highlight'
58
+ </script>
59
+ ```
60
+
61
+ You can pass a stricter `targetOrigin` as the third argument:
62
+
63
+ ```js
64
+ highlightInIframe(iframe, 'text inside the iframe', 'https://example.com')
65
+ ```
66
+
67
+ ## API
68
+
69
+ ### `DOMTextHighlight(text, root)`
70
+
71
+ Finds the best fuzzy match for `text` inside `root`, applies the `dom-highlight` custom highlight, and returns the created `Range`.
72
+
73
+ - `text`: string to search for.
74
+ - `root`: optional DOM node to search. Defaults to `document.body`.
75
+
76
+ ### `highlightInIframe(iframe, text, targetOrigin)`
77
+
78
+ Posts a highlight request to an iframe.
79
+
80
+ - `iframe`: target iframe element.
81
+ - `text`: string to search for inside the iframe document.
82
+ - `targetOrigin`: optional `postMessage` target origin. Defaults to `'*'`.
83
+
84
+ ## Browser Support
85
+
86
+ This module requires the CSS Custom Highlight API, including `CSS.highlights` and `Highlight`. It throws an error when those APIs are unavailable.
87
+
88
+ ## Development
89
+
90
+ Install dependencies:
91
+
92
+ ```sh
93
+ npm install
94
+ ```
95
+
96
+ Start the Vite test page:
97
+
98
+ ```sh
99
+ npm run dev
100
+ ```
101
+
102
+ Build the library:
103
+
104
+ ```sh
105
+ npm run build
106
+ ```
@@ -0,0 +1,107 @@
1
+ class w {
2
+ /**
3
+ * Calculates the Levenshtein distance for all substrings and returns their
4
+ * distance and insertion-deletion offset.
5
+ *
6
+ * @param {string} needle The search string.
7
+ * @param {string} haystack The string to search.
8
+ *
9
+ * @return {array} Array of all substring matches in pairs [distance, offset]
10
+ */
11
+ getEditDistances(t, o) {
12
+ var n = new Array(o.length + 1).fill([0, 0]);
13
+ for (let s = 0; s < t.length; s++) {
14
+ let r = [[s + 1, 0]];
15
+ for (let e = 0; e < o.length; e++) {
16
+ let i = t[s] != o[e], a = n[e + 1][0] + 1, h = r[e][0] + 1, g = n[e][0] + i, f = Math.min(a, Math.min(h, g)), c = [f, n[e][1]];
17
+ a === f ? c[1] = n[e + 1][1] - 1 : h === f && (c[1] = r[e][1] + 1), r.push(c);
18
+ }
19
+ n = r;
20
+ }
21
+ return n;
22
+ }
23
+ /**
24
+ * Search haystack for all instances of needle and returns an array of
25
+ * objects containing string position and levenshtein distance.
26
+ *
27
+ * @param {string} needle The search string.
28
+ * @param {string} haystack The string to search.
29
+ *
30
+ * @return {array} Array of best substring matches.
31
+ */
32
+ getMatches(t, o) {
33
+ let n = this.getEditDistances(t, o), s = [0], r = n[0][0];
34
+ for (let i = 1; i < n.length; i++) {
35
+ let a = n[i][0];
36
+ a < r ? (s = [i], r = a) : a == r && s.push(i);
37
+ }
38
+ let e = [];
39
+ for (let i of s) {
40
+ let a = n[i], h = {
41
+ distance: a[0],
42
+ start: i - t.length - a[1],
43
+ //simplification of startPos = endPos − (needleLength + insertions − deletions)
44
+ end: i
45
+ };
46
+ e.push(h);
47
+ }
48
+ return e;
49
+ }
50
+ }
51
+ function m(l, t) {
52
+ return new w().getMatches(l, t);
53
+ }
54
+ function p(l, t) {
55
+ const o = m(l, t);
56
+ let n;
57
+ for (const i of o)
58
+ (n === void 0 || i.distance < n.distance) && (n = i);
59
+ const s = Math.max(n.start, 0), r = Math.min(Math.max(n.end, s), t.length);
60
+ return { value: t.slice(s, r), start: s, end: r };
61
+ }
62
+ function x(l, t, o) {
63
+ const n = l.textContent ?? "";
64
+ if (!Number.isInteger(t) || !Number.isInteger(o) || t < 0 || t >= o || o > n.length)
65
+ throw new RangeError(
66
+ `Invalid range [${t}, ${o}) for text length ${n.length}`
67
+ );
68
+ const s = l.ownerDocument, r = s.createTreeWalker(
69
+ l,
70
+ s.defaultView.NodeFilter.SHOW_TEXT
71
+ );
72
+ let e = 0, i = null, a = 0, h = null, g = 0;
73
+ for (let c = r.nextNode(); c; c = r.nextNode()) {
74
+ const d = e + c.data.length;
75
+ if (i === null && t >= e && t < d && (i = c, a = t - e), h === null && o > e && o <= d && (h = c, g = o - e), i && h)
76
+ break;
77
+ e = d;
78
+ }
79
+ if (!i || !h)
80
+ throw new Error(
81
+ "Could not map offsets to the DOM. The DOM may have changed."
82
+ );
83
+ const f = s.createRange();
84
+ return f.setStart(i, a), f.setEnd(h, g), f;
85
+ }
86
+ const u = "dom-highlight";
87
+ function M(l, t = document.body) {
88
+ const o = t.textContent ?? "", { start: n, end: s } = p(l, o), r = x(t, n, s), e = t.ownerDocument?.defaultView ?? window;
89
+ if (!e.CSS?.highlights || !e.Highlight)
90
+ throw new Error("This browser does not support the CSS Custom Highlight API.");
91
+ return e.CSS.highlights.set(u, new e.Highlight(r)), r;
92
+ }
93
+ function y(l, t, o = "*") {
94
+ if (!l?.contentWindow)
95
+ throw new TypeError("Expected an iframe with a contentWindow");
96
+ l.contentWindow.postMessage({
97
+ type: u,
98
+ text: t
99
+ }, o);
100
+ }
101
+ typeof window < "u" && window.addEventListener("message", (l) => {
102
+ l.data?.type !== u || typeof l.data.text != "string" || M(l.data.text, document.body);
103
+ });
104
+ export {
105
+ M as default,
106
+ y as highlightInIframe
107
+ };
@@ -0,0 +1 @@
1
+ (function(d,c){typeof exports=="object"&&typeof module<"u"?c(exports):typeof define=="function"&&define.amd?define(["exports"],c):(d=typeof globalThis<"u"?globalThis:d||self,c(d.DOMTextHighlight={}))})(this,(function(d){"use strict";class c{getEditDistances(t,o){var n=new Array(o.length+1).fill([0,0]);for(let s=0;s<t.length;s++){let r=[[s+1,0]];for(let e=0;e<o.length;e++){let i=t[s]!=o[e],h=n[e+1][0]+1,a=r[e][0]+1,g=n[e][0]+i,u=Math.min(h,Math.min(a,g)),f=[u,n[e][1]];h===u?f[1]=n[e+1][1]-1:a===u&&(f[1]=r[e][1]+1),r.push(f)}n=r}return n}getMatches(t,o){let n=this.getEditDistances(t,o),s=[0],r=n[0][0];for(let i=1;i<n.length;i++){let h=n[i][0];h<r?(s=[i],r=h):h==r&&s.push(i)}let e=[];for(let i of s){let h=n[i],a={distance:h[0],start:i-t.length-h[1],end:i};e.push(a)}return e}}function M(l,t){return new c().getMatches(l,t)}function x(l,t){const o=M(l,t);let n;for(const i of o)(n===void 0||i.distance<n.distance)&&(n=i);const s=Math.max(n.start,0),r=Math.min(Math.max(n.end,s),t.length);return{value:t.slice(s,r),start:s,end:r}}function y(l,t,o){const n=l.textContent??"";if(!Number.isInteger(t)||!Number.isInteger(o)||t<0||t>=o||o>n.length)throw new RangeError(`Invalid range [${t}, ${o}) for text length ${n.length}`);const s=l.ownerDocument,r=s.createTreeWalker(l,s.defaultView.NodeFilter.SHOW_TEXT);let e=0,i=null,h=0,a=null,g=0;for(let f=r.nextNode();f;f=r.nextNode()){const m=e+f.data.length;if(i===null&&t>=e&&t<m&&(i=f,h=t-e),a===null&&o>e&&o<=m&&(a=f,g=o-e),i&&a)break;e=m}if(!i||!a)throw new Error("Could not map offsets to the DOM. The DOM may have changed.");const u=s.createRange();return u.setStart(i,h),u.setEnd(a,g),u}const w="dom-highlight";function p(l,t=document.body){const o=t.textContent??"",{start:n,end:s}=x(l,o),r=y(t,n,s),e=t.ownerDocument?.defaultView??window;if(!e.CSS?.highlights||!e.Highlight)throw new Error("This browser does not support the CSS Custom Highlight API.");return e.CSS.highlights.set(w,new e.Highlight(r)),r}function b(l,t,o="*"){if(!l?.contentWindow)throw new TypeError("Expected an iframe with a contentWindow");l.contentWindow.postMessage({type:w,text:t},o)}typeof window<"u"&&window.addEventListener("message",l=>{l.data?.type!==w||typeof l.data.text!="string"||p(l.data.text,document.body)}),d.default=p,d.highlightInIframe=b,Object.defineProperties(d,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "dom-text-highlight",
3
+ "version": "1.0.0",
4
+ "description": "Fuzzy text matching and highlighting for DOM content.",
5
+ "type": "module",
6
+ "main": "./dist/dom-text-highlight.umd.cjs",
7
+ "module": "./dist/dom-text-highlight.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/dom-text-highlight.js",
11
+ "require": "./dist/dom-text-highlight.umd.cjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "dev": "vite --host 0.0.0.0",
19
+ "build": "vite build",
20
+ "preview": "vite preview --host 0.0.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "vite": "^7.0.0"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+ssh://git@github.com/ol-c/dom-text-highlight.git"
28
+ },
29
+ "author": "Jason Ford",
30
+ "license": "ISC",
31
+ "bugs": {
32
+ "url": "https://github.com/ol-c/dom-text-highlight/issues"
33
+ }
34
+ }