website-highlighter 1.0.0 → 1.2.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 +13 -7
- package/dist/website-highlighter.js +134 -63
- package/dist/website-highlighter.umd.cjs +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -10,21 +10,24 @@ npm install website-highlighter
|
|
|
10
10
|
|
|
11
11
|
## Basic Usage
|
|
12
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 `
|
|
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 `website-highlighter` CSS highlight.
|
|
14
14
|
|
|
15
15
|
```js
|
|
16
16
|
import WebsiteHighlighter from 'website-highlighter'
|
|
17
17
|
|
|
18
18
|
const article = document.querySelector('article')
|
|
19
|
-
const range = WebsiteHighlighter('custom highlight api',
|
|
19
|
+
const { range, value } = WebsiteHighlighter('custom highlight api', {
|
|
20
|
+
root: article
|
|
21
|
+
})
|
|
20
22
|
|
|
21
23
|
console.log(range.toString())
|
|
24
|
+
console.log(value)
|
|
22
25
|
```
|
|
23
26
|
|
|
24
27
|
Add styles for the highlight name:
|
|
25
28
|
|
|
26
29
|
```css
|
|
27
|
-
::highlight(
|
|
30
|
+
::highlight(website-highlighter) {
|
|
28
31
|
background: #f6d85f;
|
|
29
32
|
color: #111;
|
|
30
33
|
}
|
|
@@ -50,7 +53,7 @@ iframe.addEventListener('load', () => {
|
|
|
50
53
|
})
|
|
51
54
|
```
|
|
52
55
|
|
|
53
|
-
The child iframe must load this module too. Importing it registers the message listener that handles `
|
|
56
|
+
The child iframe must load this module too. Importing it registers the message listener that handles `website-highlighter` messages.
|
|
54
57
|
|
|
55
58
|
```html
|
|
56
59
|
<script type="module">
|
|
@@ -66,12 +69,15 @@ highlightInIframe(iframe, 'text inside the iframe', 'https://example.com')
|
|
|
66
69
|
|
|
67
70
|
## API
|
|
68
71
|
|
|
69
|
-
### `WebsiteHighlighter(text,
|
|
72
|
+
### `WebsiteHighlighter(text, options)`
|
|
70
73
|
|
|
71
|
-
Finds the best fuzzy match for `text
|
|
74
|
+
Finds the best fuzzy match for `text`, applies the `website-highlighter` custom highlight, and returns `{ range, value }` or resolves to it when threshold retries are enabled.
|
|
72
75
|
|
|
73
76
|
- `text`: string to search for.
|
|
74
|
-
- `root`: optional DOM node to search. Defaults to `document.body`.
|
|
77
|
+
- `options.root`: optional DOM node to search. Defaults to `document.body`.
|
|
78
|
+
- `options.threshold`: optional minimum match score from `0` to `1`. When greater than `0`, the function returns a Promise and retries until the threshold is met or retries are exhausted.
|
|
79
|
+
- `options.retries`: optional retry count. Defaults to `6`.
|
|
80
|
+
- `options.retryInterval`: optional retry delay in milliseconds. Defaults to `500`.
|
|
75
81
|
|
|
76
82
|
### `highlightInIframe(iframe, text, targetOrigin)`
|
|
77
83
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
class
|
|
1
|
+
class M {
|
|
2
2
|
/**
|
|
3
3
|
* Calculates the Levenshtein distance for all substrings and returns their
|
|
4
4
|
* distance and insertion-deletion offset.
|
|
@@ -8,15 +8,15 @@ class w {
|
|
|
8
8
|
*
|
|
9
9
|
* @return {array} Array of all substring matches in pairs [distance, offset]
|
|
10
10
|
*/
|
|
11
|
-
getEditDistances(
|
|
12
|
-
var n = new Array(
|
|
13
|
-
for (let
|
|
14
|
-
let
|
|
15
|
-
for (let
|
|
16
|
-
let
|
|
17
|
-
|
|
11
|
+
getEditDistances(e, t) {
|
|
12
|
+
var n = new Array(t.length + 1).fill([0, 0]);
|
|
13
|
+
for (let r = 0; r < e.length; r++) {
|
|
14
|
+
let a = [[r + 1, 0]];
|
|
15
|
+
for (let o = 0; o < t.length; o++) {
|
|
16
|
+
let s = e[r] != t[o], l = n[o + 1][0] + 1, c = a[o][0] + 1, d = n[o][0] + s, u = Math.min(l, Math.min(c, d)), h = [u, n[o][1]];
|
|
17
|
+
l === u ? h[1] = n[o + 1][1] - 1 : c === u && (h[1] = a[o][1] + 1), a.push(h);
|
|
18
18
|
}
|
|
19
|
-
n =
|
|
19
|
+
n = a;
|
|
20
20
|
}
|
|
21
21
|
return n;
|
|
22
22
|
}
|
|
@@ -29,79 +29,150 @@ class w {
|
|
|
29
29
|
*
|
|
30
30
|
* @return {array} Array of best substring matches.
|
|
31
31
|
*/
|
|
32
|
-
getMatches(
|
|
33
|
-
let n = this.getEditDistances(
|
|
34
|
-
for (let
|
|
35
|
-
let
|
|
36
|
-
|
|
32
|
+
getMatches(e, t) {
|
|
33
|
+
let n = this.getEditDistances(e, t), r = [0], a = n[0][0];
|
|
34
|
+
for (let s = 1; s < n.length; s++) {
|
|
35
|
+
let l = n[s][0];
|
|
36
|
+
l < a ? (r = [s], a = l) : l == a && r.push(s);
|
|
37
37
|
}
|
|
38
|
-
let
|
|
39
|
-
for (let
|
|
40
|
-
let
|
|
41
|
-
distance:
|
|
42
|
-
start:
|
|
38
|
+
let o = [];
|
|
39
|
+
for (let s of r) {
|
|
40
|
+
let l = n[s], c = {
|
|
41
|
+
distance: l[0],
|
|
42
|
+
start: s - e.length - l[1],
|
|
43
43
|
//simplification of startPos = endPos − (needleLength + insertions − deletions)
|
|
44
|
-
end:
|
|
44
|
+
end: s
|
|
45
45
|
};
|
|
46
|
-
|
|
46
|
+
o.push(c);
|
|
47
47
|
}
|
|
48
|
-
return
|
|
48
|
+
return o;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
function
|
|
52
|
-
return new
|
|
51
|
+
function T(i, e) {
|
|
52
|
+
return new M().getMatches(i, e);
|
|
53
53
|
}
|
|
54
|
-
function
|
|
55
|
-
const
|
|
54
|
+
function b(i, e) {
|
|
55
|
+
const t = T(i, e);
|
|
56
56
|
let n;
|
|
57
|
-
for (const
|
|
58
|
-
(n === void 0 ||
|
|
59
|
-
const
|
|
60
|
-
return { value:
|
|
57
|
+
for (const s of t)
|
|
58
|
+
(n === void 0 || s.distance < n.distance) && (n = s);
|
|
59
|
+
const r = Math.max(n.start, 0), a = Math.min(Math.max(n.end, r), e.length);
|
|
60
|
+
return { value: e.slice(r, a), start: r, end: a, distance: n.distance };
|
|
61
61
|
}
|
|
62
|
-
function
|
|
63
|
-
const n =
|
|
64
|
-
if (!Number.isInteger(
|
|
62
|
+
function I(i, e, t) {
|
|
63
|
+
const n = i.textContent ?? "";
|
|
64
|
+
if (!Number.isInteger(e) || !Number.isInteger(t) || e < 0 || e >= t || t > n.length)
|
|
65
65
|
throw new RangeError(
|
|
66
|
-
`Invalid range [${
|
|
66
|
+
`Invalid range [${e}, ${t}) for text length ${n.length}`
|
|
67
67
|
);
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
const r = i.ownerDocument, a = r.createTreeWalker(
|
|
69
|
+
i,
|
|
70
|
+
r.defaultView.NodeFilter.SHOW_TEXT
|
|
71
71
|
);
|
|
72
|
-
let
|
|
73
|
-
for (let
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
72
|
+
let o = 0, s = null, l = 0, c = null, d = 0;
|
|
73
|
+
for (let h = a.nextNode(); h; h = a.nextNode()) {
|
|
74
|
+
const g = o + h.data.length;
|
|
75
|
+
if (s === null && e >= o && e < g && (s = h, l = e - o), c === null && t > o && t <= g && (c = h, d = t - o), s && c)
|
|
76
76
|
break;
|
|
77
|
-
|
|
77
|
+
o = g;
|
|
78
78
|
}
|
|
79
|
-
if (!
|
|
79
|
+
if (!s || !c)
|
|
80
80
|
throw new Error(
|
|
81
81
|
"Could not map offsets to the DOM. The DOM may have changed."
|
|
82
82
|
);
|
|
83
|
-
const
|
|
84
|
-
return
|
|
83
|
+
const u = r.createRange();
|
|
84
|
+
return u.setStart(s, l), u.setEnd(c, d), u;
|
|
85
85
|
}
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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;
|
|
86
|
+
const f = "website-highlighter", w = `${f}:response`, m = 500;
|
|
87
|
+
let y = 0;
|
|
88
|
+
function H(i) {
|
|
89
|
+
return new Promise((e) => globalThis.setTimeout(e, i));
|
|
92
90
|
}
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
function S(i, e) {
|
|
92
|
+
const t = i.ownerDocument?.defaultView ?? window;
|
|
93
|
+
return t.MutationObserver ? new Promise((n) => {
|
|
94
|
+
let r;
|
|
95
|
+
const a = new t.MutationObserver(() => {
|
|
96
|
+
t.clearTimeout(r), a.disconnect(), n(!0);
|
|
97
|
+
});
|
|
98
|
+
r = t.setTimeout(() => {
|
|
99
|
+
a.disconnect(), n(!1);
|
|
100
|
+
}, e), a.observe(i, {
|
|
101
|
+
childList: !0,
|
|
102
|
+
characterData: !0,
|
|
103
|
+
subtree: !0
|
|
104
|
+
});
|
|
105
|
+
}) : H(e).then(() => !1);
|
|
106
|
+
}
|
|
107
|
+
function x(i, e, t) {
|
|
108
|
+
const n = Math.max(i.length, e.length);
|
|
109
|
+
return n === 0 ? 1 : (n - t) / n;
|
|
110
|
+
}
|
|
111
|
+
function p(i, e) {
|
|
112
|
+
const t = e.textContent ?? "", { start: n, end: r, value: a, distance: o } = b(i, t), s = e.ownerDocument?.defaultView ?? window;
|
|
113
|
+
if (!s.CSS?.highlights || !s.Highlight) throw new Error("This browser does not support the CSS Custom Highlight API.");
|
|
114
|
+
return {
|
|
115
|
+
haystack: t,
|
|
116
|
+
range: n < r ? I(e, n, r) : null,
|
|
117
|
+
value: a,
|
|
118
|
+
view: s,
|
|
119
|
+
score: x(i, a, o)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function E({ range: i, value: e, view: t }) {
|
|
123
|
+
if (!i) throw new Error("Could not find text to highlight.");
|
|
124
|
+
return t.CSS.highlights.set(f, new t.Highlight(i)), { range: i, value: e };
|
|
125
|
+
}
|
|
126
|
+
async function v(i, e, t, n, r) {
|
|
127
|
+
let a = 0;
|
|
128
|
+
for (; ; ) {
|
|
129
|
+
const o = p(i, e);
|
|
130
|
+
if (o.score >= t) return E(o);
|
|
131
|
+
for (; a < n; ) {
|
|
132
|
+
const s = await S(e, r);
|
|
133
|
+
if (a += 1, s) break;
|
|
134
|
+
}
|
|
135
|
+
if (a >= n) throw new Error(`Could not find "${i}" with threshold ${t}. Best match was "${o.value}".`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function C(i, {
|
|
139
|
+
root: e = document.body,
|
|
140
|
+
threshold: t = 0,
|
|
141
|
+
retries: n = 6,
|
|
142
|
+
retryInterval: r = m
|
|
143
|
+
} = {}) {
|
|
144
|
+
return t > 0 ? v(i, e, t, n, r) : E(p(i, e));
|
|
145
|
+
}
|
|
146
|
+
function N(i, e, t = "*") {
|
|
147
|
+
if (!i?.contentWindow) throw new TypeError("Expected an iframe with a contentWindow");
|
|
148
|
+
const n = `${Date.now()}-${y++}`, r = i.contentWindow;
|
|
149
|
+
let a;
|
|
150
|
+
function o() {
|
|
151
|
+
r.postMessage({
|
|
152
|
+
type: f,
|
|
153
|
+
id: n,
|
|
154
|
+
text: e
|
|
155
|
+
}, t);
|
|
156
|
+
}
|
|
157
|
+
function s(l) {
|
|
158
|
+
l.source === r && l.data?.type === w && l.data.id === n && (window.clearInterval(a), window.removeEventListener("message", s));
|
|
159
|
+
}
|
|
160
|
+
window.addEventListener("message", s), o(), a = window.setInterval(o, m);
|
|
161
|
+
}
|
|
162
|
+
if (typeof window < "u") {
|
|
163
|
+
let i = function(t) {
|
|
164
|
+
return t.origin === "null" ? "*" : t.origin;
|
|
165
|
+
}, e = function(t) {
|
|
166
|
+
t.source?.postMessage({
|
|
167
|
+
type: w,
|
|
168
|
+
id: t.data.id
|
|
169
|
+
}, i(t));
|
|
170
|
+
};
|
|
171
|
+
window.addEventListener("message", (t) => {
|
|
172
|
+
t.data?.type === f && (e(t), C(t.data.text, { threshold: 0.9 }).catch((n) => console.error(n)));
|
|
173
|
+
});
|
|
100
174
|
}
|
|
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
175
|
export {
|
|
105
|
-
|
|
106
|
-
|
|
176
|
+
C as default,
|
|
177
|
+
N as highlightInIframe
|
|
107
178
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(c,f){typeof exports=="object"&&typeof module<"u"?f(exports):typeof define=="function"&&define.amd?define(["exports"],f):(c=typeof globalThis<"u"?globalThis:c||self,f(c.WebsiteHighlighter={}))})(this,(function(c){"use strict";class f{getEditDistances(t,e){var n=new Array(e.length+1).fill([0,0]);for(let o=0;o<t.length;o++){let l=[[o+1,0]];for(let r=0;r<e.length;r++){let s=t[o]!=e[r],a=n[r+1][0]+1,u=l[r][0]+1,w=n[r][0]+s,d=Math.min(a,Math.min(u,w)),h=[d,n[r][1]];a===d?h[1]=n[r+1][1]-1:u===d&&(h[1]=l[r][1]+1),l.push(h)}n=l}return n}getMatches(t,e){let n=this.getEditDistances(t,e),o=[0],l=n[0][0];for(let s=1;s<n.length;s++){let a=n[s][0];a<l?(o=[s],l=a):a==l&&o.push(s)}let r=[];for(let s of o){let a=n[s],u={distance:a[0],start:s-t.length-a[1],end:s};r.push(u)}return r}}function y(i,t){return new f().getMatches(i,t)}function I(i,t){const e=y(i,t);let n;for(const s of e)(n===void 0||s.distance<n.distance)&&(n=s);const o=Math.max(n.start,0),l=Math.min(Math.max(n.end,o),t.length);return{value:t.slice(o,l),start:o,end:l,distance:n.distance}}function S(i,t,e){const n=i.textContent??"";if(!Number.isInteger(t)||!Number.isInteger(e)||t<0||t>=e||e>n.length)throw new RangeError(`Invalid range [${t}, ${e}) for text length ${n.length}`);const o=i.ownerDocument,l=o.createTreeWalker(i,o.defaultView.NodeFilter.SHOW_TEXT);let r=0,s=null,a=0,u=null,w=0;for(let h=l.nextNode();h;h=l.nextNode()){const m=r+h.data.length;if(s===null&&t>=r&&t<m&&(s=h,a=t-r),u===null&&e>r&&e<=m&&(u=h,w=e-r),s&&u)break;r=m}if(!s||!u)throw new Error("Could not map offsets to the DOM. The DOM may have changed.");const d=o.createRange();return d.setStart(s,a),d.setEnd(u,w),d}const g="website-highlighter",p=`${g}:response`,b=500;let H=0;function v(i){return new Promise(t=>globalThis.setTimeout(t,i))}function x(i,t){const e=i.ownerDocument?.defaultView??window;return e.MutationObserver?new Promise(n=>{let o;const l=new e.MutationObserver(()=>{e.clearTimeout(o),l.disconnect(),n(!0)});o=e.setTimeout(()=>{l.disconnect(),n(!1)},t),l.observe(i,{childList:!0,characterData:!0,subtree:!0})}):v(t).then(()=>!1)}function C(i,t,e){const n=Math.max(i.length,t.length);return n===0?1:(n-e)/n}function T(i,t){const e=t.textContent??"",{start:n,end:o,value:l,distance:r}=I(i,e),s=t.ownerDocument?.defaultView??window;if(!s.CSS?.highlights||!s.Highlight)throw new Error("This browser does not support the CSS Custom Highlight API.");return{haystack:e,range:n<o?S(t,n,o):null,value:l,view:s,score:C(i,l,r)}}function M({range:i,value:t,view:e}){if(!i)throw new Error("Could not find text to highlight.");return e.CSS.highlights.set(g,new e.Highlight(i)),{range:i,value:t}}async function O(i,t,e,n,o){let l=0;for(;;){const r=T(i,t);if(r.score>=e)return M(r);for(;l<n;){const s=await x(t,o);if(l+=1,s)break}if(l>=n)throw new Error(`Could not find "${i}" with threshold ${e}. Best match was "${r.value}".`)}}function E(i,{root:t=document.body,threshold:e=0,retries:n=6,retryInterval:o=b}={}){return e>0?O(i,t,e,n,o):M(T(i,t))}function N(i,t,e="*"){if(!i?.contentWindow)throw new TypeError("Expected an iframe with a contentWindow");const n=`${Date.now()}-${H++}`,o=i.contentWindow;let l;function r(){o.postMessage({type:g,id:n,text:t},e)}function s(a){a.source===o&&a.data?.type===p&&a.data.id===n&&(window.clearInterval(l),window.removeEventListener("message",s))}window.addEventListener("message",s),r(),l=window.setInterval(r,b)}if(typeof window<"u"){let i=function(e){return e.origin==="null"?"*":e.origin},t=function(e){e.source?.postMessage({type:p,id:e.data.id},i(e))};window.addEventListener("message",e=>{e.data?.type===g&&(t(e),E(e.data.text,{threshold:.9}).catch(n=>console.error(n)))})}c.default=E,c.highlightInIframe=N,Object.defineProperties(c,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "website-highlighter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Fuzzy text matching and highlighting for DOM content.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/website-highlighter.umd.cjs",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|
|
27
|
-
"url": "git+ssh://git@github.com/
|
|
27
|
+
"url": "git+ssh://git@github.com/jasonford/website-highlighter.git"
|
|
28
28
|
},
|
|
29
29
|
"author": "Jason Ford",
|
|
30
30
|
"license": "ISC",
|
|
31
31
|
"bugs": {
|
|
32
|
-
"url": "https://github.com/
|
|
32
|
+
"url": "https://github.com/jasonford/website-highlighter/issues"
|
|
33
33
|
}
|
|
34
34
|
}
|