@prose-reader/enhancer-search 1.9.0 → 1.10.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="1673253398633" clover="3.2.0">
3
- <project timestamp="1673253398633" name="All files">
2
+ <coverage generated="1674998297005" clover="3.2.0">
3
+ <project timestamp="1674998297005" 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-09T08:36:38.630Z
89
+ at 2023-01-29T13:18:17.002Z
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.9.0",
3
+ "version": "1.10.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.9.0",
22
+ "@prose-reader/core": "^1.10.0",
23
23
  "@prose-reader/enhancer-scripts": "^1.8.0"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "rxjs": "*"
27
27
  },
28
- "gitHead": "e419ffe0deab682e8c52ea27fb7fa55208f26be1"
28
+ "gitHead": "5b0993fa7f5d80980e7f4eaf972e92235fea68ba"
29
29
  }
package/src/index.ts CHANGED
@@ -1,7 +1,15 @@
1
- import { Enhancer } from "@prose-reader/core"
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: Enhancer<
29
- Record<string, never>,
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
- > = (next) => (options) => {
39
- const reader = next(options)
39
+ } => {
40
+ const reader = next(options)
40
41
 
41
- const searchSubject$ = new Subject<string>()
42
+ const searchSubject$ = new Subject<string>()
42
43
 
43
- const searchNodeContainingText = (node: Node, text: string) => {
44
- const nodeList = node.childNodes
44
+ const searchNodeContainingText = (node: Node, text: string) => {
45
+ const nodeList = node.childNodes
45
46
 
46
- if (node.nodeName === `head`) return []
47
+ if (node.nodeName === `head`) return []
47
48
 
48
- const rangeList: {
49
- startNode: Node
50
- start: number
51
- endNode: Node
52
- end: number
53
- }[] = []
54
- for (let i = 0; i < nodeList.length; i++) {
55
- const subNode = nodeList[i]
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
- if (!subNode) {
58
- continue
59
- }
58
+ if (!subNode) {
59
+ continue
60
+ }
60
61
 
61
- if (subNode?.hasChildNodes()) {
62
- rangeList.push(...searchNodeContainingText(subNode, text))
63
- }
62
+ if (subNode?.hasChildNodes()) {
63
+ rangeList.push(...searchNodeContainingText(subNode, text))
64
+ }
64
65
 
65
- if (subNode.nodeType === 3) {
66
- const content = (subNode as Text).data.toLowerCase()
67
- if (content) {
68
- let match
69
- const regexp = RegExp(`(${text})`, `g`)
70
-
71
- while ((match = regexp.exec(content)) !== null) {
72
- if (match.index >= 0 && subNode.ownerDocument) {
73
- const range = subNode.ownerDocument.createRange()
74
- range.setStart(subNode, match.index)
75
- range.setEnd(subNode, match.index + text.length)
76
- rangeList.push({
77
- startNode: subNode,
78
- start: match.index,
79
- endNode: subNode,
80
- end: match.index + text.length,
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
- return rangeList
89
- }
92
+ const searchForItem = (index: number, text: string) => {
93
+ const item = reader.getSpineItem(index)
90
94
 
91
- const searchForItem = (index: number, text: string) => {
92
- const item = reader.getSpineItem(index)
95
+ if (!item) {
96
+ return of([])
97
+ }
93
98
 
94
- if (!item) {
95
- return of([])
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
- return from(item.getResource()).pipe(
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
- return newResults
131
- })
132
- )
133
- })
134
- )
135
- }
138
+ const search = (text: string) => {
139
+ searchSubject$.next(text)
140
+ }
136
141
 
137
- const search = (text: string) => {
138
- searchSubject$.next(text)
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
- const searches$ = reader.context.getManifest()?.spineItems.map((_, index) => searchForItem(index, text)) || []
153
-
154
- return forkJoin(searches$).pipe(
155
- map((results) => {
156
- return results.reduce((acc, value) => [...acc, ...value], [])
157
- })
158
- )
159
- }),
160
- map((data) => ({ type: `end` as const, data }))
161
- )
162
- ).pipe(share(), takeUntil(reader.$.destroy$))
163
-
164
- const destroy = () => {
165
- searchSubject$.complete()
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
- return {
170
- ...reader,
171
- destroy,
172
- search: {
173
- search,
174
- $: {
175
- search$,
170
+ return {
171
+ ...reader,
172
+ destroy,
173
+ search: {
174
+ search,
175
+ $: {
176
+ search$,
177
+ },
176
178
  },
177
- },
179
+ }
178
180
  }
179
- }