@tekkare/romulus 0.1.234 → 0.1.235
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/dist/module.json
CHANGED
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
remonter dans une liste de resultats. Une liste cherche chaque terme
|
|
25
25
|
separement ; une chaine, et tout ce que l'utilisateur tape ensuite, cherche
|
|
26
26
|
une expression. La recherche ignore la casse et les accents, comme
|
|
27
|
-
`RmTextHighlight`.
|
|
27
|
+
`RmTextHighlight`. `anchor` vise un passage precis (l'extrait clique dans
|
|
28
|
+
une liste) : la lecture demarre sur la premiere occurrence de ce passage
|
|
29
|
+
plutot que sur la premiere du document (page HTML seulement).
|
|
28
30
|
|
|
29
31
|
Le fichier est lu par `fetch` : il doit venir de la meme origine ou d'un
|
|
30
32
|
serveur qui autorise CORS. Une app qui affiche des documents tiers les fait
|
|
@@ -37,12 +39,20 @@
|
|
|
37
39
|
-->
|
|
38
40
|
<script setup lang="ts">
|
|
39
41
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
|
42
|
+
import { rmFoldSearchChar, rmFoldSearchQuery, rmLocateAnchor } from '../utils/documentSearch'
|
|
40
43
|
|
|
41
44
|
const props = withDefaults(defineProps<{
|
|
42
45
|
/** Adresse du document, lue par `fetch`. */
|
|
43
46
|
src: string
|
|
44
47
|
/** Terme(s) cherche(s) a l'ouverture. Une liste cherche chaque terme. */
|
|
45
48
|
query?: string | readonly string[] | null
|
|
49
|
+
/**
|
|
50
|
+
* Passage a viser a l'ouverture : la premiere occurrence affichee est celle
|
|
51
|
+
* qui tombe dans ce passage (ou juste apres), pas la premiere du document.
|
|
52
|
+
* Un passage introuvable laisse la premiere. Page HTML seulement, sans
|
|
53
|
+
* effet sur un PDF.
|
|
54
|
+
*/
|
|
55
|
+
anchor?: string | null
|
|
46
56
|
/**
|
|
47
57
|
* Adresse d'origine d'une page HTML servie par un proxy : sert de `<base>`
|
|
48
58
|
* aux liens relatifs. Sans effet sur un PDF.
|
|
@@ -51,6 +61,7 @@ const props = withDefaults(defineProps<{
|
|
|
51
61
|
lang?: 'fr' | 'en'
|
|
52
62
|
}>(), {
|
|
53
63
|
query: null,
|
|
64
|
+
anchor: null,
|
|
54
65
|
base: null,
|
|
55
66
|
lang: 'fr',
|
|
56
67
|
})
|
|
@@ -98,8 +109,11 @@ const text = ref('')
|
|
|
98
109
|
const terms = shallowRef<string[] | null>(null)
|
|
99
110
|
const matches = ref({ current: 0, total: 0 })
|
|
100
111
|
const pending = ref(false)
|
|
112
|
+
/** `anchor` vaut tant que la recherche est celle de depart. */
|
|
113
|
+
let anchored = true
|
|
101
114
|
|
|
102
115
|
function seed() {
|
|
116
|
+
anchored = true
|
|
103
117
|
const q = props.query
|
|
104
118
|
if (Array.isArray(q)) {
|
|
105
119
|
const list = q.map(x => String(x).trim()).filter(Boolean)
|
|
@@ -124,6 +138,7 @@ function find(type: '' | 'again' = '', findPrevious = false) {
|
|
|
124
138
|
function onInput(event: Event) {
|
|
125
139
|
text.value = (event.target as HTMLInputElement).value
|
|
126
140
|
terms.value = null
|
|
141
|
+
anchored = false
|
|
127
142
|
find()
|
|
128
143
|
}
|
|
129
144
|
|
|
@@ -203,7 +218,7 @@ function destroy() {
|
|
|
203
218
|
}
|
|
204
219
|
|
|
205
220
|
watch(() => props.src, load)
|
|
206
|
-
watch(() => props.query, () => {
|
|
221
|
+
watch(() => [props.query, props.anchor], () => {
|
|
207
222
|
seed()
|
|
208
223
|
find()
|
|
209
224
|
})
|
|
@@ -309,22 +324,6 @@ let index: { text: string, nodes: Text[], node: number[], offset: number[] } | n
|
|
|
309
324
|
let ranges: Range[] = []
|
|
310
325
|
let currentMatch = -1
|
|
311
326
|
|
|
312
|
-
/** Un caractere, minuscule et sans accent, en un seul caractere. */
|
|
313
|
-
function foldChar(char: string): string {
|
|
314
|
-
return char.normalize('NFD')[0]!.toLowerCase()[0]!
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function foldQuery(value: string): string {
|
|
318
|
-
let out = ''
|
|
319
|
-
for (const char of value.trim()) {
|
|
320
|
-
if (/\s/.test(char)) {
|
|
321
|
-
if (!out.endsWith(' ')) out += ' '
|
|
322
|
-
}
|
|
323
|
-
else out += foldChar(char)
|
|
324
|
-
}
|
|
325
|
-
return out
|
|
326
|
-
}
|
|
327
|
-
|
|
328
327
|
function loadHtml(bytes: Uint8Array) {
|
|
329
328
|
const html = new TextDecoder(charsetOf(bytes)).decode(bytes)
|
|
330
329
|
// Couleurs du surlignage : l'iframe ne voit pas les jetons de la page, on
|
|
@@ -385,7 +384,7 @@ function indexHtml(doc: Document) {
|
|
|
385
384
|
space = true
|
|
386
385
|
}
|
|
387
386
|
else {
|
|
388
|
-
out.text +=
|
|
387
|
+
out.text += rmFoldSearchChar(char)
|
|
389
388
|
space = false
|
|
390
389
|
}
|
|
391
390
|
out.node.push(k)
|
|
@@ -402,7 +401,7 @@ function findHtml(type: '' | 'again', findPrevious: boolean) {
|
|
|
402
401
|
|
|
403
402
|
if (type === '') {
|
|
404
403
|
const queries = (Array.isArray(searched.value) ? searched.value : [searched.value])
|
|
405
|
-
.map(
|
|
404
|
+
.map(rmFoldSearchQuery)
|
|
406
405
|
.filter(Boolean)
|
|
407
406
|
const spans: [number, number][] = []
|
|
408
407
|
for (const query of queries) {
|
|
@@ -413,16 +412,18 @@ function findHtml(type: '' | 'again', findPrevious: boolean) {
|
|
|
413
412
|
// Dans l'ordre du document, sans chevauchement.
|
|
414
413
|
spans.sort((a, b) => a[0] - b[0])
|
|
415
414
|
ranges = []
|
|
415
|
+
const starts: number[] = []
|
|
416
416
|
let end = -1
|
|
417
417
|
for (const [start, stop] of spans) {
|
|
418
418
|
if (start < end) continue
|
|
419
419
|
end = stop
|
|
420
|
+
starts.push(start)
|
|
420
421
|
const range = doc.createRange()
|
|
421
422
|
range.setStart(index.nodes[index.node[start]!]!, index.offset[start]!)
|
|
422
423
|
range.setEnd(index.nodes[index.node[stop - 1]!]!, index.offset[stop - 1]! + 1)
|
|
423
424
|
ranges.push(range)
|
|
424
425
|
}
|
|
425
|
-
currentMatch = ranges.length ?
|
|
426
|
+
currentMatch = ranges.length ? firstMatch(starts) : -1
|
|
426
427
|
}
|
|
427
428
|
else if (ranges.length) {
|
|
428
429
|
currentMatch = (currentMatch + (findPrevious ? -1 : 1) + ranges.length) % ranges.length
|
|
@@ -448,6 +449,15 @@ function findHtml(type: '' | 'again', findPrevious: boolean) {
|
|
|
448
449
|
}
|
|
449
450
|
}
|
|
450
451
|
|
|
452
|
+
/** La premiere occurrence du passage vise, la premiere du document sinon. */
|
|
453
|
+
function firstMatch(starts: number[]): number {
|
|
454
|
+
if (!anchored || !props.anchor || !index) return 0
|
|
455
|
+
const at = rmLocateAnchor(index.text, props.anchor)
|
|
456
|
+
if (at === -1) return 0
|
|
457
|
+
const k = starts.findIndex(start => start >= at)
|
|
458
|
+
return k === -1 ? 0 : k
|
|
459
|
+
}
|
|
460
|
+
|
|
451
461
|
function destroyHtml() {
|
|
452
462
|
frame.value?.contentDocument?.removeEventListener('keydown', onRootKeydown)
|
|
453
463
|
index = null
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recherche dans le texte d'un document (RmDocumentViewer) : repli du texte
|
|
3
|
+
* et reperage d'un passage.
|
|
4
|
+
*
|
|
5
|
+
* Le texte est replie caractere par caractere (minuscule, sans accent, un
|
|
6
|
+
* caractere pour un) et ses blancs fusionnes : une position dans le texte
|
|
7
|
+
* replie renvoie a un caractere du document.
|
|
8
|
+
*/
|
|
9
|
+
/** Un caractere, minuscule et sans accent, en un seul caractere. */
|
|
10
|
+
export declare function rmFoldSearchChar(char: string): string;
|
|
11
|
+
/** Une saisie, repliee comme le texte du document. */
|
|
12
|
+
export declare function rmFoldSearchQuery(value: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Position du passage `anchor` dans `text` (deja replie), -1 s'il n'y est pas.
|
|
15
|
+
*
|
|
16
|
+
* Le passage vient souvent d'une autre copie du texte (un index de recherche,
|
|
17
|
+
* une extraction) : une apostrophe, un tiret insecable ou un blanc peut
|
|
18
|
+
* differer. Faute de le retrouver d'un bloc, on cherche ses morceaux de
|
|
19
|
+
* quelques mots, dans l'ordre ; le premier retrouve situe le debut du passage.
|
|
20
|
+
*/
|
|
21
|
+
export declare function rmLocateAnchor(text: string, anchor: string): number;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function rmFoldSearchChar(char) {
|
|
2
|
+
return char.normalize("NFD")[0].toLowerCase()[0];
|
|
3
|
+
}
|
|
4
|
+
export function rmFoldSearchQuery(value) {
|
|
5
|
+
let out = "";
|
|
6
|
+
for (const char of value.trim()) {
|
|
7
|
+
if (/\s/.test(char)) {
|
|
8
|
+
if (!out.endsWith(" ")) out += " ";
|
|
9
|
+
} else out += rmFoldSearchChar(char);
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
const CHUNK_WORDS = 6;
|
|
14
|
+
const CHUNK_MIN_LENGTH = 20;
|
|
15
|
+
export function rmLocateAnchor(text, anchor) {
|
|
16
|
+
const folded = rmFoldSearchQuery(anchor);
|
|
17
|
+
if (!folded) return -1;
|
|
18
|
+
const whole = text.indexOf(folded);
|
|
19
|
+
if (whole !== -1) return whole;
|
|
20
|
+
const words = folded.split(" ");
|
|
21
|
+
let offset = 0;
|
|
22
|
+
for (let i = 0; i < words.length; i += CHUNK_WORDS) {
|
|
23
|
+
const chunk = words.slice(i, i + CHUNK_WORDS).join(" ");
|
|
24
|
+
if (chunk.length >= CHUNK_MIN_LENGTH) {
|
|
25
|
+
const at = text.indexOf(chunk);
|
|
26
|
+
if (at !== -1) return Math.max(0, at - offset);
|
|
27
|
+
}
|
|
28
|
+
offset += chunk.length + 1;
|
|
29
|
+
}
|
|
30
|
+
return -1;
|
|
31
|
+
}
|