@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
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "0.1.233",
7
+ "version": "0.1.234",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -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 += foldChar(char)
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(foldQuery)
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 ? 0 : -1
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.234",
3
+ "version": "0.1.235",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",