searchsocket 0.4.0 → 0.5.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 +49 -31
- package/dist/cli.js +634 -1326
- package/dist/client.cjs +41 -117
- package/dist/client.d.cts +3 -17
- package/dist/client.d.ts +3 -17
- package/dist/client.js +41 -117
- package/dist/index.cjs +608 -1398
- package/dist/index.d.cts +73 -35
- package/dist/index.d.ts +73 -35
- package/dist/index.js +605 -1392
- package/dist/plugin-B_npJSux.d.cts +36 -0
- package/dist/plugin-M-aW0ev6.d.ts +36 -0
- package/dist/scroll.cjs +185 -0
- package/dist/scroll.d.cts +42 -0
- package/dist/scroll.d.ts +42 -0
- package/dist/scroll.js +183 -0
- package/dist/sveltekit.cjs +781 -1278
- package/dist/sveltekit.d.cts +3 -43
- package/dist/sveltekit.d.ts +3 -43
- package/dist/sveltekit.js +779 -1276
- package/dist/{types-z2dw3H6E.d.cts → types-Dk43uz25.d.cts} +46 -141
- package/dist/{types-z2dw3H6E.d.ts → types-Dk43uz25.d.ts} +46 -141
- package/package.json +10 -3
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { R as ResolvedSearchSocketConfig, c as SearchSocketConfig } from './types-Dk43uz25.cjs';
|
|
2
|
+
|
|
3
|
+
interface SearchSocketHandleOptions {
|
|
4
|
+
configPath?: string;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
maxBodyBytes?: number;
|
|
8
|
+
config?: ResolvedSearchSocketConfig;
|
|
9
|
+
rawConfig?: SearchSocketConfig;
|
|
10
|
+
}
|
|
11
|
+
declare function searchsocketHandle(options?: SearchSocketHandleOptions): ({ event, resolve }: {
|
|
12
|
+
event: any;
|
|
13
|
+
resolve: (event: any) => Promise<Response>;
|
|
14
|
+
}) => Promise<Response>;
|
|
15
|
+
|
|
16
|
+
interface MinimalVitePlugin {
|
|
17
|
+
name: string;
|
|
18
|
+
apply?: "build" | "serve";
|
|
19
|
+
config?: () => Record<string, unknown>;
|
|
20
|
+
closeBundle?: () => Promise<void> | void;
|
|
21
|
+
}
|
|
22
|
+
interface SearchSocketAutoIndexOptions {
|
|
23
|
+
cwd?: string;
|
|
24
|
+
configPath?: string;
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
triggerEnvVar?: string;
|
|
27
|
+
disableEnvVar?: string;
|
|
28
|
+
changedOnly?: boolean;
|
|
29
|
+
force?: boolean;
|
|
30
|
+
dryRun?: boolean;
|
|
31
|
+
scope?: string;
|
|
32
|
+
verbose?: boolean;
|
|
33
|
+
}
|
|
34
|
+
declare function searchsocketVitePlugin(options?: SearchSocketAutoIndexOptions): MinimalVitePlugin;
|
|
35
|
+
|
|
36
|
+
export { type SearchSocketAutoIndexOptions as S, type SearchSocketHandleOptions as a, searchsocketVitePlugin as b, searchsocketHandle as s };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { R as ResolvedSearchSocketConfig, c as SearchSocketConfig } from './types-Dk43uz25.js';
|
|
2
|
+
|
|
3
|
+
interface SearchSocketHandleOptions {
|
|
4
|
+
configPath?: string;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
maxBodyBytes?: number;
|
|
8
|
+
config?: ResolvedSearchSocketConfig;
|
|
9
|
+
rawConfig?: SearchSocketConfig;
|
|
10
|
+
}
|
|
11
|
+
declare function searchsocketHandle(options?: SearchSocketHandleOptions): ({ event, resolve }: {
|
|
12
|
+
event: any;
|
|
13
|
+
resolve: (event: any) => Promise<Response>;
|
|
14
|
+
}) => Promise<Response>;
|
|
15
|
+
|
|
16
|
+
interface MinimalVitePlugin {
|
|
17
|
+
name: string;
|
|
18
|
+
apply?: "build" | "serve";
|
|
19
|
+
config?: () => Record<string, unknown>;
|
|
20
|
+
closeBundle?: () => Promise<void> | void;
|
|
21
|
+
}
|
|
22
|
+
interface SearchSocketAutoIndexOptions {
|
|
23
|
+
cwd?: string;
|
|
24
|
+
configPath?: string;
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
triggerEnvVar?: string;
|
|
27
|
+
disableEnvVar?: string;
|
|
28
|
+
changedOnly?: boolean;
|
|
29
|
+
force?: boolean;
|
|
30
|
+
dryRun?: boolean;
|
|
31
|
+
scope?: string;
|
|
32
|
+
verbose?: boolean;
|
|
33
|
+
}
|
|
34
|
+
declare function searchsocketVitePlugin(options?: SearchSocketAutoIndexOptions): MinimalVitePlugin;
|
|
35
|
+
|
|
36
|
+
export { type SearchSocketAutoIndexOptions as S, type SearchSocketHandleOptions as a, searchsocketVitePlugin as b, searchsocketHandle as s };
|
package/dist/scroll.cjs
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/sveltekit/scroll-to-text.ts
|
|
4
|
+
var HIGHLIGHT_CLASS = "ssk-highlight";
|
|
5
|
+
var HIGHLIGHT_DURATION = 2e3;
|
|
6
|
+
var HIGHLIGHT_MARKER_ATTR = "data-ssk-highlight-marker";
|
|
7
|
+
var HIGHLIGHT_NAME = "ssk-search-match";
|
|
8
|
+
var styleInjected = false;
|
|
9
|
+
function ensureHighlightStyle() {
|
|
10
|
+
if (styleInjected || typeof document === "undefined") return;
|
|
11
|
+
styleInjected = true;
|
|
12
|
+
const style = document.createElement("style");
|
|
13
|
+
style.textContent = `
|
|
14
|
+
@keyframes ssk-highlight-fade {
|
|
15
|
+
0% { background-color: rgba(16, 185, 129, 0.18); }
|
|
16
|
+
100% { background-color: transparent; }
|
|
17
|
+
}
|
|
18
|
+
.${HIGHLIGHT_CLASS} {
|
|
19
|
+
animation: ssk-highlight-fade ${HIGHLIGHT_DURATION}ms ease-out forwards;
|
|
20
|
+
border-radius: 4px;
|
|
21
|
+
}
|
|
22
|
+
::highlight(${HIGHLIGHT_NAME}) {
|
|
23
|
+
background-color: rgba(16, 185, 129, 0.18);
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
document.head.appendChild(style);
|
|
27
|
+
}
|
|
28
|
+
var IGNORED_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"]);
|
|
29
|
+
function buildTextMap(root) {
|
|
30
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
31
|
+
acceptNode(node) {
|
|
32
|
+
const parent = node.parentElement;
|
|
33
|
+
if (!parent || IGNORED_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
|
34
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
const chunks = [];
|
|
38
|
+
let text = "";
|
|
39
|
+
let current;
|
|
40
|
+
while (current = walker.nextNode()) {
|
|
41
|
+
const value = current.nodeValue ?? "";
|
|
42
|
+
if (!value) continue;
|
|
43
|
+
chunks.push({ node: current, start: text.length, end: text.length + value.length });
|
|
44
|
+
text += value;
|
|
45
|
+
}
|
|
46
|
+
return { text, chunks };
|
|
47
|
+
}
|
|
48
|
+
function normalize(text) {
|
|
49
|
+
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
50
|
+
}
|
|
51
|
+
function escapeRegExp(value) {
|
|
52
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
53
|
+
}
|
|
54
|
+
function buildNeedleRegex(needle) {
|
|
55
|
+
const tokenParts = needle.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
56
|
+
if (tokenParts.length > 1) {
|
|
57
|
+
const pattern = tokenParts.map(escapeRegExp).join("[^\\p{L}\\p{N}]+");
|
|
58
|
+
return new RegExp(pattern, "iu");
|
|
59
|
+
}
|
|
60
|
+
if (tokenParts.length === 1) {
|
|
61
|
+
return new RegExp(escapeRegExp(tokenParts[0]), "iu");
|
|
62
|
+
}
|
|
63
|
+
if (!needle) return null;
|
|
64
|
+
return new RegExp(escapeRegExp(needle).replace(/\s+/g, "\\s+"), "i");
|
|
65
|
+
}
|
|
66
|
+
function buildLenientRegex(needle) {
|
|
67
|
+
const tokenParts = needle.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
68
|
+
if (tokenParts.length <= 1) return null;
|
|
69
|
+
const pattern = tokenParts.map(escapeRegExp).join("[^\\p{L}\\p{N}]*");
|
|
70
|
+
return new RegExp(pattern, "iu");
|
|
71
|
+
}
|
|
72
|
+
function findMatch(fullText, needle) {
|
|
73
|
+
const regex = buildNeedleRegex(needle);
|
|
74
|
+
if (regex) {
|
|
75
|
+
const m = regex.exec(fullText);
|
|
76
|
+
if (m && typeof m.index === "number") {
|
|
77
|
+
return { start: m.index, end: m.index + m[0].length };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const lenient = buildLenientRegex(needle);
|
|
81
|
+
if (lenient) {
|
|
82
|
+
const m = lenient.exec(fullText);
|
|
83
|
+
if (m && typeof m.index === "number") {
|
|
84
|
+
return { start: m.index, end: m.index + m[0].length };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
function resolveRange(map, offsets) {
|
|
90
|
+
let startChunk;
|
|
91
|
+
let endChunk;
|
|
92
|
+
for (const chunk of map.chunks) {
|
|
93
|
+
if (!startChunk && offsets.start >= chunk.start && offsets.start < chunk.end) {
|
|
94
|
+
startChunk = chunk;
|
|
95
|
+
}
|
|
96
|
+
if (offsets.end > chunk.start && offsets.end <= chunk.end) {
|
|
97
|
+
endChunk = chunk;
|
|
98
|
+
}
|
|
99
|
+
if (startChunk && endChunk) break;
|
|
100
|
+
}
|
|
101
|
+
if (!startChunk || !endChunk) return null;
|
|
102
|
+
const range = document.createRange();
|
|
103
|
+
range.setStart(startChunk.node, offsets.start - startChunk.start);
|
|
104
|
+
range.setEnd(endChunk.node, offsets.end - endChunk.start);
|
|
105
|
+
return range;
|
|
106
|
+
}
|
|
107
|
+
function hasCustomHighlightAPI() {
|
|
108
|
+
return typeof CSS !== "undefined" && typeof CSS.highlights !== "undefined";
|
|
109
|
+
}
|
|
110
|
+
var highlightTimer = null;
|
|
111
|
+
function highlightWithCSS(range) {
|
|
112
|
+
ensureHighlightStyle();
|
|
113
|
+
const hl = new globalThis.Highlight(range);
|
|
114
|
+
CSS.highlights.set(HIGHLIGHT_NAME, hl);
|
|
115
|
+
if (highlightTimer) clearTimeout(highlightTimer);
|
|
116
|
+
highlightTimer = setTimeout(() => {
|
|
117
|
+
CSS.highlights.delete(HIGHLIGHT_NAME);
|
|
118
|
+
highlightTimer = null;
|
|
119
|
+
}, HIGHLIGHT_DURATION);
|
|
120
|
+
}
|
|
121
|
+
function unwrapMarker(marker) {
|
|
122
|
+
if (!marker.isConnected) return;
|
|
123
|
+
const parent = marker.parentNode;
|
|
124
|
+
if (!parent) return;
|
|
125
|
+
while (marker.firstChild) parent.insertBefore(marker.firstChild, marker);
|
|
126
|
+
parent.removeChild(marker);
|
|
127
|
+
if (parent instanceof Element) parent.normalize();
|
|
128
|
+
}
|
|
129
|
+
function highlightWithDOM(range) {
|
|
130
|
+
ensureHighlightStyle();
|
|
131
|
+
try {
|
|
132
|
+
const marker = document.createElement("span");
|
|
133
|
+
marker.classList.add(HIGHLIGHT_CLASS);
|
|
134
|
+
marker.setAttribute(HIGHLIGHT_MARKER_ATTR, "true");
|
|
135
|
+
range.surroundContents(marker);
|
|
136
|
+
setTimeout(() => unwrapMarker(marker), HIGHLIGHT_DURATION);
|
|
137
|
+
return marker;
|
|
138
|
+
} catch {
|
|
139
|
+
const ancestor = range.commonAncestorContainer;
|
|
140
|
+
const el = ancestor instanceof Element ? ancestor : ancestor.parentElement;
|
|
141
|
+
if (el) {
|
|
142
|
+
el.classList.add(HIGHLIGHT_CLASS);
|
|
143
|
+
setTimeout(() => el.classList.remove(HIGHLIGHT_CLASS), HIGHLIGHT_DURATION);
|
|
144
|
+
return el;
|
|
145
|
+
}
|
|
146
|
+
return document.body;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function scrollToRange(range) {
|
|
150
|
+
const rect = range.getBoundingClientRect();
|
|
151
|
+
window.scrollTo({
|
|
152
|
+
top: window.scrollY + rect.top - window.innerHeight / 3,
|
|
153
|
+
behavior: "smooth"
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function scrollIntoViewIfPossible(el) {
|
|
157
|
+
if (typeof el.scrollIntoView === "function") {
|
|
158
|
+
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function searchsocketScrollToText(navigation) {
|
|
162
|
+
if (typeof document === "undefined") return;
|
|
163
|
+
const params = navigation.to?.url.searchParams;
|
|
164
|
+
const raw = params?.get("_sskt") ?? params?.get("_ssk");
|
|
165
|
+
if (!raw) return;
|
|
166
|
+
const needle = normalize(raw);
|
|
167
|
+
if (!needle) return;
|
|
168
|
+
const map = buildTextMap(document.body);
|
|
169
|
+
const offsets = findMatch(map.text, needle);
|
|
170
|
+
if (!offsets) return;
|
|
171
|
+
const range = resolveRange(map, offsets);
|
|
172
|
+
if (!range) return;
|
|
173
|
+
if (hasCustomHighlightAPI()) {
|
|
174
|
+
highlightWithCSS(range);
|
|
175
|
+
scrollToRange(range);
|
|
176
|
+
} else {
|
|
177
|
+
const marker = highlightWithDOM(range);
|
|
178
|
+
const target = typeof marker.scrollIntoView === "function" ? marker : marker.parentElement;
|
|
179
|
+
if (target) scrollIntoViewIfPossible(target);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
exports.searchsocketScrollToText = searchsocketScrollToText;
|
|
184
|
+
//# sourceMappingURL=scroll.cjs.map
|
|
185
|
+
//# sourceMappingURL=scroll.cjs.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvelteKit scroll-to-text helper for search result navigation.
|
|
3
|
+
*
|
|
4
|
+
* When a visitor arrives on a page via a search result link that contains
|
|
5
|
+
* a `_sskt` (text target) or `_ssk` (section title) query parameter, this
|
|
6
|
+
* hook finds matching text in the document, scrolls it into view, and briefly
|
|
7
|
+
* highlights it.
|
|
8
|
+
*
|
|
9
|
+
* Uses a TreeWalker-based text map to match across split DOM nodes (e.g.
|
|
10
|
+
* `<em>Install</em>ation`), and the CSS Custom Highlight API when available
|
|
11
|
+
* for non-destructive highlighting that avoids DOM mutation.
|
|
12
|
+
*
|
|
13
|
+
* Usage in a SvelteKit layout:
|
|
14
|
+
* ```svelte
|
|
15
|
+
* <script>
|
|
16
|
+
* import { afterNavigate } from '$app/navigation';
|
|
17
|
+
* import { searchsocketScrollToText } from 'searchsocket/sveltekit';
|
|
18
|
+
* afterNavigate(searchsocketScrollToText);
|
|
19
|
+
* </script>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
/** Minimal representation of SvelteKit's `AfterNavigate` parameter. */
|
|
23
|
+
interface AfterNavigateParam {
|
|
24
|
+
to: {
|
|
25
|
+
url: URL;
|
|
26
|
+
} | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A function compatible with SvelteKit's `afterNavigate` callback.
|
|
30
|
+
*
|
|
31
|
+
* Reads `_sskt` (preferred) or `_ssk` from the destination URL, walks text
|
|
32
|
+
* nodes in document order via a TreeWalker text map, and scrolls/highlights
|
|
33
|
+
* the first match. Matches can span multiple DOM text nodes.
|
|
34
|
+
*
|
|
35
|
+
* Uses the CSS Custom Highlight API when available for non-destructive
|
|
36
|
+
* highlighting, with a DOM mutation fallback for older browsers.
|
|
37
|
+
*
|
|
38
|
+
* Silent no-op when no target parameter is present or no match is found.
|
|
39
|
+
*/
|
|
40
|
+
declare function searchsocketScrollToText(navigation: AfterNavigateParam): void;
|
|
41
|
+
|
|
42
|
+
export { type AfterNavigateParam, searchsocketScrollToText };
|
package/dist/scroll.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvelteKit scroll-to-text helper for search result navigation.
|
|
3
|
+
*
|
|
4
|
+
* When a visitor arrives on a page via a search result link that contains
|
|
5
|
+
* a `_sskt` (text target) or `_ssk` (section title) query parameter, this
|
|
6
|
+
* hook finds matching text in the document, scrolls it into view, and briefly
|
|
7
|
+
* highlights it.
|
|
8
|
+
*
|
|
9
|
+
* Uses a TreeWalker-based text map to match across split DOM nodes (e.g.
|
|
10
|
+
* `<em>Install</em>ation`), and the CSS Custom Highlight API when available
|
|
11
|
+
* for non-destructive highlighting that avoids DOM mutation.
|
|
12
|
+
*
|
|
13
|
+
* Usage in a SvelteKit layout:
|
|
14
|
+
* ```svelte
|
|
15
|
+
* <script>
|
|
16
|
+
* import { afterNavigate } from '$app/navigation';
|
|
17
|
+
* import { searchsocketScrollToText } from 'searchsocket/sveltekit';
|
|
18
|
+
* afterNavigate(searchsocketScrollToText);
|
|
19
|
+
* </script>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
/** Minimal representation of SvelteKit's `AfterNavigate` parameter. */
|
|
23
|
+
interface AfterNavigateParam {
|
|
24
|
+
to: {
|
|
25
|
+
url: URL;
|
|
26
|
+
} | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A function compatible with SvelteKit's `afterNavigate` callback.
|
|
30
|
+
*
|
|
31
|
+
* Reads `_sskt` (preferred) or `_ssk` from the destination URL, walks text
|
|
32
|
+
* nodes in document order via a TreeWalker text map, and scrolls/highlights
|
|
33
|
+
* the first match. Matches can span multiple DOM text nodes.
|
|
34
|
+
*
|
|
35
|
+
* Uses the CSS Custom Highlight API when available for non-destructive
|
|
36
|
+
* highlighting, with a DOM mutation fallback for older browsers.
|
|
37
|
+
*
|
|
38
|
+
* Silent no-op when no target parameter is present or no match is found.
|
|
39
|
+
*/
|
|
40
|
+
declare function searchsocketScrollToText(navigation: AfterNavigateParam): void;
|
|
41
|
+
|
|
42
|
+
export { type AfterNavigateParam, searchsocketScrollToText };
|
package/dist/scroll.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// src/sveltekit/scroll-to-text.ts
|
|
2
|
+
var HIGHLIGHT_CLASS = "ssk-highlight";
|
|
3
|
+
var HIGHLIGHT_DURATION = 2e3;
|
|
4
|
+
var HIGHLIGHT_MARKER_ATTR = "data-ssk-highlight-marker";
|
|
5
|
+
var HIGHLIGHT_NAME = "ssk-search-match";
|
|
6
|
+
var styleInjected = false;
|
|
7
|
+
function ensureHighlightStyle() {
|
|
8
|
+
if (styleInjected || typeof document === "undefined") return;
|
|
9
|
+
styleInjected = true;
|
|
10
|
+
const style = document.createElement("style");
|
|
11
|
+
style.textContent = `
|
|
12
|
+
@keyframes ssk-highlight-fade {
|
|
13
|
+
0% { background-color: rgba(16, 185, 129, 0.18); }
|
|
14
|
+
100% { background-color: transparent; }
|
|
15
|
+
}
|
|
16
|
+
.${HIGHLIGHT_CLASS} {
|
|
17
|
+
animation: ssk-highlight-fade ${HIGHLIGHT_DURATION}ms ease-out forwards;
|
|
18
|
+
border-radius: 4px;
|
|
19
|
+
}
|
|
20
|
+
::highlight(${HIGHLIGHT_NAME}) {
|
|
21
|
+
background-color: rgba(16, 185, 129, 0.18);
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
document.head.appendChild(style);
|
|
25
|
+
}
|
|
26
|
+
var IGNORED_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"]);
|
|
27
|
+
function buildTextMap(root) {
|
|
28
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
29
|
+
acceptNode(node) {
|
|
30
|
+
const parent = node.parentElement;
|
|
31
|
+
if (!parent || IGNORED_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
|
32
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
const chunks = [];
|
|
36
|
+
let text = "";
|
|
37
|
+
let current;
|
|
38
|
+
while (current = walker.nextNode()) {
|
|
39
|
+
const value = current.nodeValue ?? "";
|
|
40
|
+
if (!value) continue;
|
|
41
|
+
chunks.push({ node: current, start: text.length, end: text.length + value.length });
|
|
42
|
+
text += value;
|
|
43
|
+
}
|
|
44
|
+
return { text, chunks };
|
|
45
|
+
}
|
|
46
|
+
function normalize(text) {
|
|
47
|
+
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
48
|
+
}
|
|
49
|
+
function escapeRegExp(value) {
|
|
50
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
51
|
+
}
|
|
52
|
+
function buildNeedleRegex(needle) {
|
|
53
|
+
const tokenParts = needle.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
54
|
+
if (tokenParts.length > 1) {
|
|
55
|
+
const pattern = tokenParts.map(escapeRegExp).join("[^\\p{L}\\p{N}]+");
|
|
56
|
+
return new RegExp(pattern, "iu");
|
|
57
|
+
}
|
|
58
|
+
if (tokenParts.length === 1) {
|
|
59
|
+
return new RegExp(escapeRegExp(tokenParts[0]), "iu");
|
|
60
|
+
}
|
|
61
|
+
if (!needle) return null;
|
|
62
|
+
return new RegExp(escapeRegExp(needle).replace(/\s+/g, "\\s+"), "i");
|
|
63
|
+
}
|
|
64
|
+
function buildLenientRegex(needle) {
|
|
65
|
+
const tokenParts = needle.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
66
|
+
if (tokenParts.length <= 1) return null;
|
|
67
|
+
const pattern = tokenParts.map(escapeRegExp).join("[^\\p{L}\\p{N}]*");
|
|
68
|
+
return new RegExp(pattern, "iu");
|
|
69
|
+
}
|
|
70
|
+
function findMatch(fullText, needle) {
|
|
71
|
+
const regex = buildNeedleRegex(needle);
|
|
72
|
+
if (regex) {
|
|
73
|
+
const m = regex.exec(fullText);
|
|
74
|
+
if (m && typeof m.index === "number") {
|
|
75
|
+
return { start: m.index, end: m.index + m[0].length };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const lenient = buildLenientRegex(needle);
|
|
79
|
+
if (lenient) {
|
|
80
|
+
const m = lenient.exec(fullText);
|
|
81
|
+
if (m && typeof m.index === "number") {
|
|
82
|
+
return { start: m.index, end: m.index + m[0].length };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function resolveRange(map, offsets) {
|
|
88
|
+
let startChunk;
|
|
89
|
+
let endChunk;
|
|
90
|
+
for (const chunk of map.chunks) {
|
|
91
|
+
if (!startChunk && offsets.start >= chunk.start && offsets.start < chunk.end) {
|
|
92
|
+
startChunk = chunk;
|
|
93
|
+
}
|
|
94
|
+
if (offsets.end > chunk.start && offsets.end <= chunk.end) {
|
|
95
|
+
endChunk = chunk;
|
|
96
|
+
}
|
|
97
|
+
if (startChunk && endChunk) break;
|
|
98
|
+
}
|
|
99
|
+
if (!startChunk || !endChunk) return null;
|
|
100
|
+
const range = document.createRange();
|
|
101
|
+
range.setStart(startChunk.node, offsets.start - startChunk.start);
|
|
102
|
+
range.setEnd(endChunk.node, offsets.end - endChunk.start);
|
|
103
|
+
return range;
|
|
104
|
+
}
|
|
105
|
+
function hasCustomHighlightAPI() {
|
|
106
|
+
return typeof CSS !== "undefined" && typeof CSS.highlights !== "undefined";
|
|
107
|
+
}
|
|
108
|
+
var highlightTimer = null;
|
|
109
|
+
function highlightWithCSS(range) {
|
|
110
|
+
ensureHighlightStyle();
|
|
111
|
+
const hl = new globalThis.Highlight(range);
|
|
112
|
+
CSS.highlights.set(HIGHLIGHT_NAME, hl);
|
|
113
|
+
if (highlightTimer) clearTimeout(highlightTimer);
|
|
114
|
+
highlightTimer = setTimeout(() => {
|
|
115
|
+
CSS.highlights.delete(HIGHLIGHT_NAME);
|
|
116
|
+
highlightTimer = null;
|
|
117
|
+
}, HIGHLIGHT_DURATION);
|
|
118
|
+
}
|
|
119
|
+
function unwrapMarker(marker) {
|
|
120
|
+
if (!marker.isConnected) return;
|
|
121
|
+
const parent = marker.parentNode;
|
|
122
|
+
if (!parent) return;
|
|
123
|
+
while (marker.firstChild) parent.insertBefore(marker.firstChild, marker);
|
|
124
|
+
parent.removeChild(marker);
|
|
125
|
+
if (parent instanceof Element) parent.normalize();
|
|
126
|
+
}
|
|
127
|
+
function highlightWithDOM(range) {
|
|
128
|
+
ensureHighlightStyle();
|
|
129
|
+
try {
|
|
130
|
+
const marker = document.createElement("span");
|
|
131
|
+
marker.classList.add(HIGHLIGHT_CLASS);
|
|
132
|
+
marker.setAttribute(HIGHLIGHT_MARKER_ATTR, "true");
|
|
133
|
+
range.surroundContents(marker);
|
|
134
|
+
setTimeout(() => unwrapMarker(marker), HIGHLIGHT_DURATION);
|
|
135
|
+
return marker;
|
|
136
|
+
} catch {
|
|
137
|
+
const ancestor = range.commonAncestorContainer;
|
|
138
|
+
const el = ancestor instanceof Element ? ancestor : ancestor.parentElement;
|
|
139
|
+
if (el) {
|
|
140
|
+
el.classList.add(HIGHLIGHT_CLASS);
|
|
141
|
+
setTimeout(() => el.classList.remove(HIGHLIGHT_CLASS), HIGHLIGHT_DURATION);
|
|
142
|
+
return el;
|
|
143
|
+
}
|
|
144
|
+
return document.body;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function scrollToRange(range) {
|
|
148
|
+
const rect = range.getBoundingClientRect();
|
|
149
|
+
window.scrollTo({
|
|
150
|
+
top: window.scrollY + rect.top - window.innerHeight / 3,
|
|
151
|
+
behavior: "smooth"
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function scrollIntoViewIfPossible(el) {
|
|
155
|
+
if (typeof el.scrollIntoView === "function") {
|
|
156
|
+
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function searchsocketScrollToText(navigation) {
|
|
160
|
+
if (typeof document === "undefined") return;
|
|
161
|
+
const params = navigation.to?.url.searchParams;
|
|
162
|
+
const raw = params?.get("_sskt") ?? params?.get("_ssk");
|
|
163
|
+
if (!raw) return;
|
|
164
|
+
const needle = normalize(raw);
|
|
165
|
+
if (!needle) return;
|
|
166
|
+
const map = buildTextMap(document.body);
|
|
167
|
+
const offsets = findMatch(map.text, needle);
|
|
168
|
+
if (!offsets) return;
|
|
169
|
+
const range = resolveRange(map, offsets);
|
|
170
|
+
if (!range) return;
|
|
171
|
+
if (hasCustomHighlightAPI()) {
|
|
172
|
+
highlightWithCSS(range);
|
|
173
|
+
scrollToRange(range);
|
|
174
|
+
} else {
|
|
175
|
+
const marker = highlightWithDOM(range);
|
|
176
|
+
const target = typeof marker.scrollIntoView === "function" ? marker : marker.parentElement;
|
|
177
|
+
if (target) scrollIntoViewIfPossible(target);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export { searchsocketScrollToText };
|
|
182
|
+
//# sourceMappingURL=scroll.js.map
|
|
183
|
+
//# sourceMappingURL=scroll.js.map
|