@prose-reader/enhancer-search 1.9.0 → 1.11.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,6 +1,6 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<coverage generated="
|
|
3
|
-
<project timestamp="
|
|
2
|
+
<coverage generated="1674998537655" clover="3.2.0">
|
|
3
|
+
<project timestamp="1674998537656" name="All files">
|
|
4
4
|
<metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0" elements="0" coveredelements="0" complexity="0" loc="0" ncloc="0" packages="0" files="0" classes="0"/>
|
|
5
5
|
</project>
|
|
6
6
|
</coverage>
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
87
87
|
Code coverage generated by
|
|
88
88
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
89
|
-
at 2023-01-
|
|
89
|
+
at 2023-01-29T13:22:17.652Z
|
|
90
90
|
</div>
|
|
91
91
|
<script src="prettify.js"></script>
|
|
92
92
|
<script>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prose-reader/enhancer-search",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/prose-reader-enhancer-search.umd.cjs",
|
|
6
6
|
"module": "./dist/prose-reader-enhancer-search.js",
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
"test": "vitest run --coverage"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@prose-reader/core": "^1.
|
|
22
|
+
"@prose-reader/core": "^1.11.0",
|
|
23
23
|
"@prose-reader/enhancer-scripts": "^1.8.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"rxjs": "*"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "e4f193075dec6e18b765a9cf4c1c9a4960c9083a"
|
|
29
29
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Reader } from "@prose-reader/core"
|
|
2
2
|
import { forkJoin, from, merge, Observable, of, Subject } from "rxjs"
|
|
3
3
|
import { map, share, switchMap, takeUntil } from "rxjs/operators"
|
|
4
4
|
|
|
5
|
+
const supportedContentType = [
|
|
6
|
+
`application/xhtml+xml` as const,
|
|
7
|
+
`application/xml` as const,
|
|
8
|
+
`image/svg+xml` as const,
|
|
9
|
+
`text/html` as const,
|
|
10
|
+
`text/xml` as const,
|
|
11
|
+
]
|
|
12
|
+
|
|
5
13
|
type ResultItem = {
|
|
6
14
|
spineItemIndex: number
|
|
7
15
|
startCfi: string
|
|
@@ -12,168 +20,161 @@ type ResultItem = {
|
|
|
12
20
|
endOffset: number
|
|
13
21
|
}
|
|
14
22
|
|
|
15
|
-
const supportedContentType = [
|
|
16
|
-
`application/xhtml+xml` as const,
|
|
17
|
-
`application/xml` as const,
|
|
18
|
-
`image/svg+xml` as const,
|
|
19
|
-
`text/html` as const,
|
|
20
|
-
`text/xml` as const,
|
|
21
|
-
]
|
|
22
|
-
|
|
23
23
|
export type SearchResult = ResultItem[]
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
*
|
|
27
27
|
*/
|
|
28
|
-
export const searchEnhancer
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
export const searchEnhancer =
|
|
29
|
+
<InheritOptions, InheritOutput extends Reader>(next: (options: InheritOptions) => InheritOutput) =>
|
|
30
|
+
(
|
|
31
|
+
options: InheritOptions
|
|
32
|
+
): InheritOutput & {
|
|
31
33
|
search: {
|
|
32
34
|
search: (text: string) => void
|
|
33
35
|
$: {
|
|
34
36
|
search$: Observable<{ type: `start` } | { type: `end`; data: SearchResult }>
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const reader = next(options)
|
|
39
|
+
} => {
|
|
40
|
+
const reader = next(options)
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
const searchSubject$ = new Subject<string>()
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
const searchNodeContainingText = (node: Node, text: string) => {
|
|
45
|
+
const nodeList = node.childNodes
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
if (node.nodeName === `head`) return []
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
const rangeList: {
|
|
50
|
+
startNode: Node
|
|
51
|
+
start: number
|
|
52
|
+
endNode: Node
|
|
53
|
+
end: number
|
|
54
|
+
}[] = []
|
|
55
|
+
for (let i = 0; i < nodeList.length; i++) {
|
|
56
|
+
const subNode = nodeList[i]
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
if (!subNode) {
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
if (subNode?.hasChildNodes()) {
|
|
63
|
+
rangeList.push(...searchNodeContainingText(subNode, text))
|
|
64
|
+
}
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
66
|
+
if (subNode.nodeType === 3) {
|
|
67
|
+
const content = (subNode as Text).data.toLowerCase()
|
|
68
|
+
if (content) {
|
|
69
|
+
let match
|
|
70
|
+
const regexp = RegExp(`(${text})`, `g`)
|
|
71
|
+
|
|
72
|
+
while ((match = regexp.exec(content)) !== null) {
|
|
73
|
+
if (match.index >= 0 && subNode.ownerDocument) {
|
|
74
|
+
const range = subNode.ownerDocument.createRange()
|
|
75
|
+
range.setStart(subNode, match.index)
|
|
76
|
+
range.setEnd(subNode, match.index + text.length)
|
|
77
|
+
rangeList.push({
|
|
78
|
+
startNode: subNode,
|
|
79
|
+
start: match.index,
|
|
80
|
+
endNode: subNode,
|
|
81
|
+
end: match.index + text.length,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
}
|
|
88
|
+
|
|
89
|
+
return rangeList
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
const searchForItem = (index: number, text: string) => {
|
|
93
|
+
const item = reader.getSpineItem(index)
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
if (!item) {
|
|
96
|
+
return of([])
|
|
97
|
+
}
|
|
93
98
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
return from(item.getResource()).pipe(
|
|
100
|
+
switchMap((response) => {
|
|
101
|
+
// small optimization since we already know DOMParser only accept some documents only
|
|
102
|
+
// the reader returns us a valid HTML document anyway so it is not ultimately necessary.
|
|
103
|
+
// however we can still avoid doing unnecessary HTML generation for images resources, etc.
|
|
104
|
+
if (!supportedContentType.includes(response?.headers.get(`Content-Type`) || (`` as any))) return of([])
|
|
105
|
+
|
|
106
|
+
return from(item.getHtmlFromResource(response)).pipe(
|
|
107
|
+
map((html) => {
|
|
108
|
+
const parser = new DOMParser()
|
|
109
|
+
const doc = parser.parseFromString(html, `application/xhtml+xml`)
|
|
110
|
+
|
|
111
|
+
const ranges = searchNodeContainingText(doc, text)
|
|
112
|
+
const newResults = ranges.map((range) => {
|
|
113
|
+
const { end, start } = reader.generateCfi(range, item.item)
|
|
114
|
+
const { node, offset, spineItemIndex } = reader.resolveCfi(start) || {}
|
|
115
|
+
const pageIndex =
|
|
116
|
+
node && spineItemIndex !== undefined
|
|
117
|
+
? reader.locator.getSpineItemPageIndexFromNode(node, offset, spineItemIndex)
|
|
118
|
+
: undefined
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
spineItemIndex: index,
|
|
122
|
+
startCfi: start,
|
|
123
|
+
endCfi: end,
|
|
124
|
+
pageIndex,
|
|
125
|
+
contextText: range.startNode.parentElement?.textContent || ``,
|
|
126
|
+
startOffset: range.start,
|
|
127
|
+
endOffset: range.end,
|
|
128
|
+
}
|
|
129
|
+
})
|
|
97
130
|
|
|
98
|
-
|
|
99
|
-
switchMap((response) => {
|
|
100
|
-
// small optimization since we already know DOMParser only accept some documents only
|
|
101
|
-
// the reader returns us a valid HTML document anyway so it is not ultimately necessary.
|
|
102
|
-
// however we can still avoid doing unnecessary HTML generation for images resources, etc.
|
|
103
|
-
if (!supportedContentType.includes(response?.headers.get(`Content-Type`) || (`` as any))) return of([])
|
|
104
|
-
|
|
105
|
-
return from(item.getHtmlFromResource(response)).pipe(
|
|
106
|
-
map((html) => {
|
|
107
|
-
const parser = new DOMParser()
|
|
108
|
-
const doc = parser.parseFromString(html, `application/xhtml+xml`)
|
|
109
|
-
|
|
110
|
-
const ranges = searchNodeContainingText(doc, text)
|
|
111
|
-
const newResults = ranges.map((range) => {
|
|
112
|
-
const { end, start } = reader.generateCfi(range, item.item)
|
|
113
|
-
const { node, offset, spineItemIndex } = reader.resolveCfi(start) || {}
|
|
114
|
-
const pageIndex =
|
|
115
|
-
node && spineItemIndex !== undefined
|
|
116
|
-
? reader.locator.getSpineItemPageIndexFromNode(node, offset, spineItemIndex)
|
|
117
|
-
: undefined
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
spineItemIndex: index,
|
|
121
|
-
startCfi: start,
|
|
122
|
-
endCfi: end,
|
|
123
|
-
pageIndex,
|
|
124
|
-
contextText: range.startNode.parentElement?.textContent || ``,
|
|
125
|
-
startOffset: range.start,
|
|
126
|
-
endOffset: range.end,
|
|
127
|
-
}
|
|
131
|
+
return newResults
|
|
128
132
|
})
|
|
133
|
+
)
|
|
134
|
+
})
|
|
135
|
+
)
|
|
136
|
+
}
|
|
129
137
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
})
|
|
134
|
-
)
|
|
135
|
-
}
|
|
138
|
+
const search = (text: string) => {
|
|
139
|
+
searchSubject$.next(text)
|
|
140
|
+
}
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Main search process stream
|
|
144
|
+
*/
|
|
145
|
+
const search$ = merge(
|
|
146
|
+
searchSubject$.asObservable().pipe(map(() => ({ type: `start` as const }))),
|
|
147
|
+
searchSubject$.asObservable().pipe(
|
|
148
|
+
switchMap((text) => {
|
|
149
|
+
if (text === ``) {
|
|
150
|
+
return of([])
|
|
151
|
+
}
|
|
140
152
|
|
|
141
|
-
|
|
142
|
-
* Main search process stream
|
|
143
|
-
*/
|
|
144
|
-
const search$ = merge(
|
|
145
|
-
searchSubject$.asObservable().pipe(map(() => ({ type: `start` as const }))),
|
|
146
|
-
searchSubject$.asObservable().pipe(
|
|
147
|
-
switchMap((text) => {
|
|
148
|
-
if (text === ``) {
|
|
149
|
-
return of([])
|
|
150
|
-
}
|
|
153
|
+
const searches$ = reader.context.getManifest()?.spineItems.map((_, index) => searchForItem(index, text)) || []
|
|
151
154
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
reader.destroy()
|
|
167
|
-
}
|
|
155
|
+
return forkJoin(searches$).pipe(
|
|
156
|
+
map((results) => {
|
|
157
|
+
return results.reduce((acc, value) => [...acc, ...value], [])
|
|
158
|
+
})
|
|
159
|
+
)
|
|
160
|
+
}),
|
|
161
|
+
map((data) => ({ type: `end` as const, data }))
|
|
162
|
+
)
|
|
163
|
+
).pipe(share(), takeUntil(reader.$.destroy$))
|
|
164
|
+
|
|
165
|
+
const destroy = () => {
|
|
166
|
+
searchSubject$.complete()
|
|
167
|
+
reader.destroy()
|
|
168
|
+
}
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
170
|
+
return {
|
|
171
|
+
...reader,
|
|
172
|
+
destroy,
|
|
173
|
+
search: {
|
|
174
|
+
search,
|
|
175
|
+
$: {
|
|
176
|
+
search$,
|
|
177
|
+
},
|
|
176
178
|
},
|
|
177
|
-
}
|
|
179
|
+
}
|
|
178
180
|
}
|
|
179
|
-
}
|