usfmtc 0.3.2__tar.gz → 0.3.3__tar.gz

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.
Files changed (28) hide show
  1. {usfmtc-0.3.2/src/usfmtc.egg-info → usfmtc-0.3.3}/PKG-INFO +1 -1
  2. {usfmtc-0.3.2 → usfmtc-0.3.3}/pyproject.toml +1 -1
  3. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/reference.py +113 -95
  4. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/usxmodel.py +128 -20
  5. {usfmtc-0.3.2 → usfmtc-0.3.3/src/usfmtc.egg-info}/PKG-INFO +1 -1
  6. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc.egg-info/SOURCES.txt +1 -0
  7. usfmtc-0.3.3/tests/test_parser.py +40 -0
  8. usfmtc-0.3.3/tests/test_refs.py +365 -0
  9. usfmtc-0.3.2/tests/test_refs.py +0 -199
  10. {usfmtc-0.3.2 → usfmtc-0.3.3}/LICENSE +0 -0
  11. {usfmtc-0.3.2 → usfmtc-0.3.3}/setup.cfg +0 -0
  12. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/__init__.py +0 -0
  13. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/eng.vrs +0 -0
  14. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/extension.py +0 -0
  15. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/lxmlutils.py +0 -0
  16. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/usfmgenerate.py +0 -0
  17. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/usfmparser.py +0 -0
  18. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/usjproc.py +0 -0
  19. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/usx.rng +0 -0
  20. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/validating/rngparser.py +0 -0
  21. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/validating/usfmgrammar.py +0 -0
  22. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/validating/usfmparser.py +0 -0
  23. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/validating/usxparser.py +0 -0
  24. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc/xmlutils.py +0 -0
  25. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc.egg-info/dependency_links.txt +0 -0
  26. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc.egg-info/entry_points.txt +0 -0
  27. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc.egg-info/requires.txt +0 -0
  28. {usfmtc-0.3.2 → usfmtc-0.3.3}/src/usfmtc.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usfmtc
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: USFM reference parser
5
5
  Author-email: USFM Technical Committee <fonts@sil.org>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "usfmtc"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  authors = [{name = "USFM Technical Committee", email = "fonts@sil.org"}]
9
9
  readme = "python/README.md"
10
10
  license = "MIT"
@@ -97,35 +97,6 @@ class MarkerRef:
97
97
  res.append(str(self.char))
98
98
  return "".join(res)
99
99
 
100
-
101
- _regexes = {
102
- "book": r"""(?P<transid>(?:[a-z0-9_-]*[+])*)
103
- (?P<book>(?:[A-Z][A-Z0-9][A-Z0-9]|[0-9](?:[A-Z][A-Z]|[0-9][A-Z]|[A-Z][0-9])))
104
- \s*{chap}""",
105
- "id": r"(?:[a-z][a-z0-9_-]*[a-z0-9]?)",
106
- "charref": r"(?:(?:[0-9]|end)[+]?)",
107
- "wordrefanon": r"",
108
- "mrkref": r"(?:\!{id}(?:\[[0-9]\])?(?:\!(?:[0-9|end)(?:[+]{charref})?)?)",
109
- "wordrefonly": r"(?P<word>[0-9]|end)(?P<char>[+]{charref})?",
110
- "wordref1": r"""(?:(?: \!(?P<word1>[0-9]|end)(?:[+](?P<char1>{charref}))?
111
- |(?P<mrk1>{mrkref}))
112
- (?P<mrkn1>{mrkref}*))""",
113
- "wordref2": r"""(?:(?: \!(?P<word2>[0-9]|end)(?:[+](?P<char2>{charref}))?
114
- |(?P<mrk2>{mrkref}))
115
- (?P<mrkn2>{mrkref}*))""",
116
- "verse1": r"(?P<verse1>[0-9]+)(?P<subv1>[a-z]?|end)",
117
- "verse2": r"(?P<verse2>[0-9]+)(?P<subv2>[a-z]?|end)",
118
- "chapter": r"(?:(?P<chap>[0-9]+))",
119
- "chap": r"{chapter}(?:[:.]{verse1})?{wordref1}?",
120
- "context": r"""(?:{chap}
121
- | {verse2}{wordref2}? | {wordrefonly}(?P<mrkn3>{mrkref}*)
122
- | (?P<char3>{charref})(?P<mrkn4>{mrkref}*))(?=[-;,\s]|$)"""
123
- }
124
-
125
- regexes = _regexes
126
- for i in range(3):
127
- regexes = {k: v.format(**regexes) for k, v in regexes.items()}
128
-
129
100
  def intend(s: str) -> Optional[int]:
130
101
  if not s:
131
102
  return None
@@ -198,6 +169,35 @@ def readvrs(fname):
198
169
  return res
199
170
 
200
171
 
172
+ _regexes = {
173
+ "book": r"""(?P<transid>(?:[a-z0-9_-]*[+])*)
174
+ (?P<book>(?:[A-Z][A-Z0-9][A-Z0-9]|[0-9](?:[A-Z][A-Z]|[0-9][A-Z]|[A-Z][0-9])))
175
+ \s*{chap}""",
176
+ "id": r"(?:[a-z][a-z0-9_-]*[a-z0-9]?)",
177
+ "charref": r"(?:(?:[0-9]|end)[+]?)",
178
+ "wordrefanon": r"",
179
+ "mrkref": r"(?:\!{id}(?:\[[0-9]\])?(?:\!(?:[0-9]|end)(?:[+]{charref})?)?)",
180
+ "wordrefonly": r"(?P<word>[0-9]|end)(?P<char>[+]{charref})?",
181
+ "wordref1": r"""(?:(?: \!(?P<word1>[0-9]|end)(?:[+](?P<char1>{charref}))?
182
+ |(?P<mrk1>{mrkref}))
183
+ (?P<mrkn1>{mrkref}*))""",
184
+ "wordref2": r"""(?:(?: \!(?P<word2>[0-9]|end)(?:[+](?P<char2>{charref}))?
185
+ |(?P<mrk2>{mrkref}))
186
+ (?P<mrkn2>{mrkref}*))""",
187
+ "verse1": r"(?P<verse1>[0-9]+|end)(?P<subv1>[a-z]?)",
188
+ "verse2": r"(?P<verse2>[0-9]+|end)(?P<subv2>[a-z]?)",
189
+ "chapter": r"(?:(?P<chap>[0-9]+|end))",
190
+ "chap": r"{chapter}(?:[:.]{verse1})?{wordref1}?",
191
+ "context": r"""(?:{chap}
192
+ | {verse2}{wordref2}? | {wordrefonly}(?P<mrkn3>{mrkref}*)
193
+ | (?P<char3>{charref})(?P<mrkn4>{mrkref}*))(?=[-;,\s]|$)"""
194
+ }
195
+
196
+ regexes = _regexes
197
+ for i in range(3):
198
+ regexes = {k: v.format(**regexes) for k, v in regexes.items()}
199
+
200
+
201
201
  class Ref:
202
202
  product: Optional[str]
203
203
  book: Optional[str]
@@ -232,20 +232,21 @@ class Ref:
232
232
  def __init__(self, string: Optional[str] = None,
233
233
  context: Optional['Ref'] = None, start: int = 0, **kw):
234
234
  if string is not None:
235
- self.parse(string, context=context, start=start)
235
+ s = string.strip()
236
+ self.parse(s, context=(context.last if context is not None else None), start=start)
237
+ if self.strend < len(s):
238
+ raise SyntaxError(f"Extra content after reference {s[0:self.strend]} | {s[self.strend:]}")
236
239
  return
237
240
 
238
241
  if context is not None:
239
- isempty = all(not v for k, v in kw.items() if k != "word")
240
- rel = None
241
- if isempty:
242
- rel = kw["word"]
243
- kw["word"] = None
244
- idx = [k for k in self._parmlist if getattr(context.last, k, None)][-1]
242
+ hitlimit = False
245
243
  for a in self._parmlist:
246
- setattr(self, a, kw.get(a, getattr(context.last, a, None)))
247
- if rel is not None:
248
- setattr(self, idx, rel)
244
+ v = kw.get(a, None)
245
+ if v is not None:
246
+ hitlimit = True
247
+ elif not hitlimit:
248
+ v = getattr(context.last, a, None)
249
+ setattr(self, a, v)
249
250
  else:
250
251
  for a in self._parmlist:
251
252
  setattr(self, a, kw.get(a, None))
@@ -254,54 +255,31 @@ class Ref:
254
255
  if "-" in s:
255
256
  return
256
257
  p = {}
258
+ s = s.strip()
257
259
  if m := self._rebook.match(s, pos=start):
258
260
  p['product'] = m.group('transid') or None
259
261
  p['book'] = m.group('book')
260
- p['chapter'] = int(m.group('chap')) if m.group('chap') else None
261
- p['verse'] = intend(m.group('verse1'))
262
- p['subverse'] = m.group('subv1') or None
263
- p['word'] = intend(m.group('word1'))
264
- p['char'] = intend(m.group('char1'))
265
- p['mrkrs'] = asmarkers(m.group("mrk1"), m.group("mrkn1"))
266
262
  elif not (m:= self._recontext.match(s, pos=start)):
267
263
  raise SyntaxError("Cannot parse {}".format(s))
268
- elif m.group('verse1'): # we know we have a chapter and verse
269
- p['chapter'] = int(m.group('chap'))
270
- p['verse'] = intend(m.group('verse1'))
271
- p['subverse'] = m.group('subv1') or None
272
- p['word'] = intend(m.group('word1'))
273
- p['char'] = intend(m.group('char1'))
274
- p['mrkrs'] = asmarkers(m.group("mrk1"), m.group("mrkn1"))
275
- elif m.group('subv2'):
276
- p['verse'] = intend(m.group('verse2'))
277
- p['subverse'] = m.group('subv2') or None
278
- p['word'] = intend(m.group('word2'))
279
- p['char'] = intend(m.group('char2'))
280
- p['mrkrs'] = asmarkers(m.group("mrk2"), m.group("mrkn2"))
281
- elif m.group('word'):
282
- p['word'] = intend(m.group('word'))
283
- p['char'] = intend(m.group('char'))
284
- p['mrkrs'] = asmarkers("", m.group('mrkn3'))
285
- elif m.group('char3'):
286
- p['char'] = intend(m.group('char3'))
287
- p['mrkrs'] = asmarkers("", m.group('mrkn4'))
288
- elif not m.group('chap') and m.group('word1'):
289
- p['word'] = intend(m.group('word1'))
290
- p['char'] = intend(m.group('char1'))
291
- p['mrkrs'] = asmarkers(m.group('mrk1'), m.group('mrkn1'))
292
- elif context is not None and context.char:
293
- p['char'] = intend(m.group('chap'))
294
- elif context is not None and context.word:
295
- p['word'] = intend(m.group('chap'))
296
- p['char'] = intend(m.group('char1'))
297
- elif context is not None and context.verse:
298
- p['verse'] = intend(m.group('chap'))
299
- p['word'] = intend(m.group('word1'))
300
- p['char'] = intend(m.group('char1'))
301
- else:
302
- p['chapter'] = int(m.group('chap'))
303
- p['word'] = intend(m.group('word1'))
304
- p['char'] = intend(m.group('char1'))
264
+ gs = m.groupdict()
265
+ p['chapter'] = intend(gs.get('chap', None))
266
+ p['verse'] = intend(gs.get('verse1', gs.get('verse2', None)))
267
+ p['subverse'] = gs.get('subv1', gs.get('subv2', None)) or None
268
+ p['word'] = intend(gs.get('word1', gs.get('word2', gs.get('word', None))))
269
+ p['char'] = intend(gs.get('char', gs.get('char1', gs.get('char2', None))))
270
+ mnum = "2" if 'mrk2' in gs else "1"
271
+ p['mrkrs'] = asmarkers(gs.get('mrk'+mnum, None), gs.get('mrkn'+mnum, None))
272
+ if 'book' not in p and context is not None and p['chapter'] is not None and p['verse'] is None and p['word'] is None:
273
+ rep = None
274
+ if context.char is not None:
275
+ rep = 'char'
276
+ elif context.word is not None:
277
+ rep = 'word'
278
+ elif context.verse is not None:
279
+ rep = 'verse'
280
+ if rep is not None:
281
+ p[rep] = p['chapter']
282
+ p['chapter'] = None
305
283
  self.strend = m.end(0)
306
284
  if p.get('mrkrs', None) == []:
307
285
  p['mrkrs'] = None
@@ -319,7 +297,7 @@ class Ref:
319
297
  def __eq__(self, o):
320
298
  if not isinstance(o, Ref):
321
299
  return False
322
- res = all(getattr(self, a) == getattr(o, a) for a in self._parmlist)
300
+ res = all(getattr(self, a) is None or getattr(o, a) is None or getattr(self, a) == getattr(o, a) for a in self._parmlist)
323
301
  return res
324
302
 
325
303
  def __contains__(self, o):
@@ -409,7 +387,7 @@ class Ref:
409
387
  if self.mrkrs is not None and len(self.mrkrs):
410
388
  forcemkr = False
411
389
  for i, m in enumerate(self.mrkrs):
412
- if not forcemkr and context is not None and i < len(context.mrkrs):
390
+ if not forcemkr and context is not None and context.mrkrs is not None and i < len(context.mrkrs):
413
391
  if context.mrkrs[i] != m:
414
392
  res.append(m.str(context=contex.mrkrs[i], force=force or len(res)))
415
393
  forcemkr = True
@@ -429,27 +407,57 @@ class Ref:
429
407
  kw = {k: getattr(self, k) for k in self._parmlist}
430
408
  return self.__class__(**kw)
431
409
 
432
- def end(self):
410
+ def _setall(self, val):
433
411
  res = self.copy()
434
- if self.chapter is None:
435
- res.chapter = -1
436
- elif self.verse is None:
437
- res.verse = -1
438
- elif self.word is None:
439
- res.word = -1
440
- elif self.char is None:
441
- res.char = -1
412
+ for a in ('chapter', 'verse', 'word', 'char'):
413
+ if getattr(self, a, None) is None:
414
+ setattr(res, a, val)
415
+ return res
416
+
417
+ def end(self):
418
+ if self.last != self:
419
+ self.last = end(self.last)
420
+ return self.last
421
+ res = self._setall(-1)
422
+ if res.chapter == -1:
423
+ vrs = self.versification or Ref.loadversification()
424
+ book = books.get(self.book, None)
425
+ if book is not None:
426
+ res.chapter = len(vrs[book])
427
+ if res.verse == -1:
428
+ res.verse = self._getmaxvrs(self.book, self.chapter)
442
429
  return res
443
430
 
431
+ def start(self):
432
+ if self.first != self:
433
+ self.first = start(self.first)
434
+ return self.first
435
+ return self._setall(1)
436
+
437
+ def expand(self):
438
+ return RefRange(self.start(), self.end())
439
+
444
440
  def _getmaxvrs(self, bk, chap):
445
441
  vrs = self.first.versification or Ref.loadversification()
446
442
  book = books.get(bk, None)
447
443
  if book is None or len(vrs[book]) <= chap:
448
- maxvrs = 200
444
+ maxvrs = -1
449
445
  else:
450
446
  maxvrs = vrs[book][chap] - (vrs[book][chap-1] if chap > 1 else 0)
451
447
  return maxvrs
452
448
 
449
+ def isvalid(self):
450
+ vrs = self.first.versification or Ref.loadversification()
451
+ if self.book not in books:
452
+ return False
453
+ book = vrs[books.get(self.book)]
454
+ if len(book) < self.chapter:
455
+ return False
456
+ if book[self.chapter] < self.verse:
457
+ return False
458
+ # checking subverse, word and char involves having access to the document
459
+ return True
460
+
453
461
  def nextverse(self):
454
462
  r = self.first.copy()
455
463
  maxvrs = self._getmaxvrs(r.book, r.chapter)
@@ -458,7 +466,7 @@ class Ref:
458
466
  else:
459
467
  r.verse += 1
460
468
  if r.verse > maxvrs:
461
- r.chapter += 1
469
+ r.chapter = (r.chapter + 1) if r.chapter is not None else 1
462
470
  r.verse = 1
463
471
  if r.book not in books:
464
472
  r.book = "GEN"
@@ -483,6 +491,8 @@ class RefRange:
483
491
  def __init__(self, first: Optional[Ref]=None, last: Optional[Ref]=None):
484
492
  self.first = first
485
493
  self.last = last
494
+ if self.last < self.first:
495
+ raise ValueError(f"{first=} is after {last=}")
486
496
 
487
497
  def str(self, context: Optional[Ref] = None, force: bool = False):
488
498
  res = [self.first.str(context, force=force)]
@@ -523,6 +533,14 @@ class RefRange:
523
533
  def strend(self):
524
534
  return self.last.strend
525
535
 
536
+ def expand(self):
537
+ self.first = self.first.start()
538
+ self.last = self.last.end()
539
+ return self
540
+
541
+ def isvalid(self):
542
+ return self.first.isvalid() and self.last.isvalid()
543
+
526
544
  def allverses(self):
527
545
  r = self.first
528
546
  while r is not None and r <= self.last:
@@ -3,6 +3,7 @@ import re
3
3
  from dataclasses import dataclass
4
4
  from usfmtc.xmlutils import isempty
5
5
  from usfmtc.usfmparser import Grammar
6
+ from usfmtc.reference import Ref
6
7
  import xml.etree.ElementTree as et
7
8
  from typing import Optional
8
9
 
@@ -259,11 +260,11 @@ def strnormal(s, t, mode=0):
259
260
  '''
260
261
  if s is None:
261
262
  return ""
262
- res = re.sub("[\n\\s]+", " ", s) if t in ('para', 'char') else s
263
+ res = re.sub("[\n\t\r ]+", " ", s) if t in ('para', 'char') else s
263
264
  if mode & 1 == 1:
264
- res = res.lstrip()
265
+ res = res.lstrip(" \t\n\r")
265
266
  if mode & 2 == 2:
266
- res = res.rstrip()
267
+ res = res.rstrip(" \t\n\r")
267
268
  res = re.sub(r"[ \n]*\n[ ]*", "\n", res)
268
269
  for k, v in unescapes.items():
269
270
  if k in res:
@@ -442,6 +443,8 @@ def iterusx(root, parindex=0, start=None, blocks=[], filt=[], unblocks=False, gr
442
443
  yield from runiter(root, parindex=parindex, started=start is None)
443
444
 
444
445
  def copy_range(root, a, b):
446
+ ''' Returns a usx document containing paragraphs containing the content
447
+ between a and b '''
445
448
  factory = root.__class__
446
449
  if a.el not in root:
447
450
  p = a.el.parent
@@ -482,6 +485,52 @@ def copy_range(root, a, b):
482
485
  res.remove(last)
483
486
  return res
484
487
 
488
+ def copy_text(root, a, b):
489
+ ''' Returns a text string of all the main text between a and b '''
490
+ res = []
491
+ if a.el not in root:
492
+ p = a.el.parent
493
+ while p not in root:
494
+ p = p.parent
495
+ else:
496
+ p = a.el
497
+ i = list(root).index(p)
498
+ for eloc in iterusx(root, parindex=i, start=a.el, until=b.el):
499
+ if eloc.head is None:
500
+ if not isempty(eloc.parent.text):
501
+ start = 0; end = len(eloc.parent.text)
502
+ if eloc.parent == a.el:
503
+ if a.attrib == " text":
504
+ start = a.char
505
+ if eloc.parent == b.el:
506
+ if b.attrib == " text":
507
+ end = b.char
508
+ res.append(eloc.parent.text[start:end])
509
+ elif eloc.head == a.el and a.attrib == " tail":
510
+ res.append(eloc.head.tail[a.char:])
511
+ elif eloc.head == b.el and b.attrib == " text":
512
+ pass
513
+ elif not isempty(eloc.head.tail):
514
+ res.append(eloc.head.tail)
515
+ if not len(res) and a.attrib == " tail":
516
+ start = a.char
517
+ else:
518
+ start = 0
519
+ if b.attrib == " tail":
520
+ res.append(b.el.tail[start:b.char])
521
+ return "".join(res)
522
+
523
+
524
+ def vnumin(s, v, base):
525
+ b = base.copy()
526
+ b.verse = None
527
+ b.subverse = None
528
+ b.word = None
529
+ b.char = None
530
+ b.mrkrs = None
531
+ r = Ref(str(b)+":"+s)
532
+ if v >= r.first.verse and v <= r.last.verse:
533
+ return True
485
534
 
486
535
  @dataclass
487
536
  class USXLoc:
@@ -504,6 +553,8 @@ def _findel(node, tag, attrib, limits=[]):
504
553
 
505
554
  def _findtext(node, limits):
506
555
  """ Iterator returning text strings until an element in limits is encountered """
556
+ if not isempty(node.text):
557
+ yield(node.text, node, " text")
507
558
  for eloc in iterusx(node, until=lambda e: e.tag in limits):
508
559
  if eloc.head is None:
509
560
  if not isempty(eloc.parent.text):
@@ -513,7 +564,7 @@ def _findtext(node, limits):
513
564
  if not isempty(node.tail): # but what if we hit the limits?
514
565
  yield(node.tail, node, " tail")
515
566
 
516
- def _findcvel(ref, root, atend=False, parindex=0):
567
+ def _findcvel(ref, root, grammar, atend=False, parindex=0):
517
568
  if ref.book:
518
569
  bkel = root.find("./book")
519
570
  bk = bkel.get("code", "")
@@ -539,31 +590,74 @@ def _findcvel(ref, root, atend=False, parindex=0):
539
590
  v = ref.verse
540
591
  if v is not None and v > 0:
541
592
  if atend:
542
- parindex = startparindex
543
- if parindex < len(root) - 1:
544
- for n in root[parindex+1:]:
545
- el = _findel(n, "verse", {"number": str(ref.verse + (1 if atend and not ref.word else 0))}, limits=("chapter",))
546
- if el is not None:
547
- break
593
+ parindex = startparindex + 1
594
+ if not ref.mrkrs and not ref.word and not ref.char:
595
+ v += 1
596
+ else:
597
+ parindex += 1
598
+ while parindex < len(root):
599
+ n = root[parindex]
600
+ el = _findel(n, "verse", {"number": str(v)}, limits=("chapter",))
601
+ if el is not None:
602
+ break
603
+ parindex += 1
604
+ else:
605
+ raise ValueError("Reference verse {} out of range".format(ref))
606
+ if ref.mrkrs:
607
+ if not ref.word and not ref.char:
608
+ oparindex = parindex
609
+ t = ref.mrkrs[0].mrkr # look forward or backwards for a para
610
+ if grammar.marker_categories.get(t, "") in ("sectionpara",):
611
+ while parindex > 0:
612
+ p = root[parindex]
613
+ if p.get("style", "") == t:
614
+ if ref.mrkrs[0].index is None or ref.mrkrs[0].index - 1 == count:
615
+ foundme = True
616
+ break
617
+ parindex -= 1
618
+ endme = False
619
+ for e in root[parindex]:
620
+ if e.tag == "verse" and not vnumin(e.get("number", ""), v, ref.last if atend else ref.first):
621
+ endme = True
622
+ break
623
+ if endme:
624
+ parindex = oparindex
625
+ raise SyntaxError("Cannot find preceding section paragraph {t} in {ref}")
626
+ else:
627
+ el = root[parindex]
548
628
  else:
549
- raise ValueError("Reference verse {} out of range".format(ref))
629
+ for eloc in iterusx(root, parindex=parindex, start=lambda e:e.tag == "verse" and vnumin(e.get("number", ""), v, ref.last if atend else ref.first), until=lambda e:e.tag == "verse" and not vnumin(e.get("number", ""), v, ref.last if atend else ref.first)):
630
+ if eloc.head is not None:
631
+ continue
632
+ if eloc.parent.get("style", "") == t:
633
+ el = eloc.parent
634
+ break
550
635
  return el
551
636
 
552
- def _findtextref(ref, el, atend=False):
637
+ def _findtextref(ref, el, atend=False, mrkri=0):
638
+ if mrkri > 0:
639
+ r = ref.mrkrs[mrkri-1]
640
+ else:
641
+ r = ref
553
642
  a = ""
554
643
  w = -1
555
644
  c = -1
556
645
  clen = -1
557
646
  t = ""
558
- if ref.word is None or ref.word == 0:
647
+ if r.word is None or r.word == 0:
559
648
  t = el.get('number', "")
560
649
  a = "number"
561
650
  c = 0
562
651
  clen = len(t)
563
652
  else:
564
- word = ref.word - 1
653
+ word = r.word - 1
565
654
  for t, el, a in _findtext(el, limits=("chapter", "verse")):
566
655
  b = re.split("([\u0020\u00A0\u1680\u2000-\u200B\u202F\u205F\u3000]+)", t)
656
+ if not len(b[-1]):
657
+ b.pop()
658
+ if not len(b[0]):
659
+ b.pop(0)
660
+ b.pop(0)
567
661
  l = (len(b) + 1) // 2
568
662
  if not len(b[0]):
569
663
  l -= 1
@@ -572,24 +666,38 @@ def _findtextref(ref, el, atend=False):
572
666
  continue
573
667
  c = sum(len(s) for s in b[:word*2])
574
668
  clen = len(b[word*2])
669
+ if ref.mrkrs and len(ref.mrkrs) > mrkri and c + clen == len(t.rstrip()):
670
+ en = el.getnext()
671
+ while en is not None:
672
+ if en.get("style", "") == ref.mrkrs[mrkri].mrkr:
673
+ break
674
+ else:
675
+ raise SyntaxError(f"Can't find {ref.mrkrs[mrkri+1].mrkr}")
676
+ return _findtext(ref, en, atend=atend, mrkri=mrkri+1)
575
677
  break
576
678
  else:
577
679
  raise ValueError("Reference word {} out of range".format(ref))
578
- if ref.char is not None:
579
- if ref.char > clen:
680
+ if r.char is not None:
681
+ if r.char > clen:
580
682
  raise ValueError("Reference char {} out of range for word length {}".format(ref, clen))
581
- c += ref.char
683
+ c += r.char
582
684
  elif atend:
583
685
  c += clen
584
686
  return USXLoc(el, a, c)
585
687
 
586
- def findref(ref, root, atend=False, parindex=0):
688
+ def findref(ref, root, atend=False, parindex=0, grammar=None):
587
689
  """ From a reference, return a USXLoc (element, attribute and char index) """
588
- el = _findcvel(ref, root, atend=atend, parindex=parindex)
690
+ if grammar is None:
691
+ grammar = Grammar()
692
+ el = _findcvel(ref, root, grammar, atend=atend, parindex=parindex)
589
693
  if ref.word is not None or ref.char is not None:
590
694
  res = _findtextref(ref, el, atend=atend)
695
+ elif ref.mrkrs:
696
+ res = _findtextref(ref, el, atend=atend, mrkri=1)
697
+ elif atend and len(el):
698
+ res = USXLoc(el[-1], " tail", -1)
591
699
  else:
592
- res = USXLoc(el, "", -1)
700
+ res = USXLoc(el, " text", -1 if atend else 0)
593
701
  # What about markers?
594
702
  return res
595
703
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usfmtc
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: USFM reference parser
5
5
  Author-email: USFM Technical Committee <fonts@sil.org>
6
6
  License-Expression: MIT
@@ -21,4 +21,5 @@ src/usfmtc/validating/rngparser.py
21
21
  src/usfmtc/validating/usfmgrammar.py
22
22
  src/usfmtc/validating/usfmparser.py
23
23
  src/usfmtc/validating/usxparser.py
24
+ tests/test_parser.py
24
25
  tests/test_refs.py
@@ -0,0 +1,40 @@
1
+ import pytest
2
+ from pytest import fail
3
+ import usfmtc
4
+ import xml.etree.ElementTree as et
5
+ import re, io
6
+
7
+ def asusfm(root):
8
+ grammar = usfmtc.Grammar()
9
+ with io.StringIO() as fh:
10
+ usfmtc.usx2usfm(fh, root, grammar)
11
+ res = fh.getvalue()
12
+ return res
13
+
14
+ def _dousfm(s):
15
+ doc = usfmtc.readFile(s, informat="usfm")
16
+ doc.canonicalise()
17
+ r = doc.getroot()
18
+ et.dump(r)
19
+ f = asusfm(r)
20
+ print(f)
21
+ return (doc, f)
22
+
23
+ def test_hardspaces():
24
+ usfm = r"""\id TST testing hardspaces
25
+ \c 1 \p \v 1 This{0}has a hard{0}space\f + \fr 1:1{0}\ft And here\f*""".format("\u00A0")
26
+ doc, f = _dousfm(usfm)
27
+ r = doc.getroot()
28
+ e = r.find('.//char[@style="fr"]')
29
+ t = e.text
30
+ if not t.endswith("\u00A0"):
31
+ fail("No hard space after fr in usx")
32
+ if not re.search("\u00A0\\\\ft", f):
33
+ fail("No hard space after fr in usfm")
34
+
35
+ def test_glossary():
36
+ usfm = r"""\id TST glossary in text
37
+ \c 1 \p \v 1 We have \w glossary\w* words to deal with"""
38
+ doc, f = _dousfm(usfm)
39
+ if not re.search(" \\w", f):
40
+ fail("No space before glossary word")
@@ -0,0 +1,365 @@
1
+ import pytest
2
+ from pytest import fail
3
+ from usfmtc import readFile, usx2usfm
4
+ from usfmtc.usxmodel import findref, copy_range, copy_text
5
+ from usfmtc.reference import Ref, RefList, RefRange
6
+ import os, io
7
+ import xml.etree.ElementTree as et
8
+
9
+ # To see the print() output of passing tests, run pytest -s
10
+
11
+ def check_ref(ref, product=None, book=None, chapter=None, verse=None, subverse=None,
12
+ word=None, char=None, mrkrs=None):
13
+ r = Ref(product=product, book=book, chapter=chapter, verse=verse, subverse=subverse,
14
+ word=word, char=char, mrkrs=mrkrs)
15
+ return r == ref
16
+
17
+ def asusfm(root):
18
+ with io.StringIO() as fh:
19
+ usx2usfm(fh, root)
20
+ res = fh.getvalue()
21
+ return res
22
+
23
+ jon_usfm = readFile(os.path.join(os.path.dirname(__file__), "32JONBSB.usfm"))
24
+
25
+ def _get_textref(s):
26
+ r = Ref(s)
27
+ root = jon_usfm.getroot()
28
+ start = findref(r.first, root)
29
+ end = findref(r.last, root, atend=True)
30
+ res = copy_text(root, start, end)
31
+ return res
32
+
33
+ def _r(bk, chap, verse, subv=None):
34
+ return Ref(book=bk, chapter=chap, verse=verse, subverse=subv)
35
+
36
+ def _t(s, *r):
37
+ res = RefList(s)
38
+ if len(res) != len(r):
39
+ fail("Failed '{}' resulted in {} references, {}, rather than {}".format(s, len(res), res, len(r)))
40
+ for z in zip(res, r):
41
+ if z[0] != z[1]:
42
+ fail("Reference list failed '{}', {} != {}".format(s, z[0], z[1]))
43
+ if str(res) != s:
44
+ fail("{} != canonical string of {}".format(s, str(res)))
45
+ print("{} = {}".format(s, str(res)))
46
+
47
+ def _listtest(s, a):
48
+ res = RefList(s)
49
+ res.simplify()
50
+ base = RefList(a)
51
+ if res != base:
52
+ fail("{} simplified to {} instead of {}".format(s, res, base))
53
+ print("{} -> {}".format(s, res))
54
+
55
+ def _o(a, b, res):
56
+ x = RefList(a)[0]
57
+ y = RefList(b)[0]
58
+ if (isinstance(res,tuple)):
59
+ restup=res
60
+ restupd=f"x<y {restup[0]}, y<x {restup[1]}, x in y {restup[2]}"
61
+ else:
62
+ restup=(res,res,res)
63
+ restupd=res
64
+ a = (y<x)
65
+ b = (x<y)
66
+ #print(f"y<x :{a} x<y:{b}")
67
+ if restup[0] != b:
68
+ raise fail(f"{x} should be < {y} [expect: {restupd}]")
69
+ if restup[1] != a:
70
+ raise fail(f"{y} should not be < {x} [expect: {restupd}]")
71
+ if (x in y) != restup[2]:
72
+ raise fail(f"{x} in {y} should be {res}")
73
+ print("{} <-> {} ({})".format(x, y,restupd))
74
+
75
+ def _do_findreftest(first, last):
76
+ root = jon_usfm.getroot()
77
+ start = findref(first, root)
78
+ end = findref(last, root, atend=True)
79
+ res = copy_range(root, start, end)
80
+
81
+ print(f"{first=} {start}; {last=} {end}")
82
+ et.dump(res)
83
+ print(asusfm(res))
84
+ if len(res) != 7 or len(res[2]) != 1 or "vomited" not in res[-1][-1].tail:
85
+ fail(f"{len(res)=}, {res[-1][-1].tail=}")
86
+
87
+ def test_basic():
88
+ r = Ref("JHN 3:16")
89
+ rr = Ref("12", context=r)
90
+
91
+ if not check_ref(r, None, "JHN", 3, 16):
92
+ fail(f"{r} is not JHN 3:16")
93
+ if not check_ref(rr, None, "JHN", 3, 12):
94
+ fail(f"{rr} is not JHN 3:12")
95
+
96
+ def test_range():
97
+ r = RefList("JHN 3:16-18")
98
+
99
+ if not check_ref(r[0].first, None, "JHN", 3, 16):
100
+ fail(f"{r[0].first} is not JHN 3:16")
101
+ if not check_ref(r[0].last, None, "JHN", 3, 18):
102
+ fail(f"{r[0].last} is not JHN 3:18")
103
+
104
+ def test_findreftext():
105
+ r = Ref("JON 3:6!3")
106
+ loc = findref(r, jon_usfm.getroot())
107
+
108
+ if loc.el.tag != "verse" and loc.attrib != " tail" and loc.char != 18:
109
+ fail(f"{r} resulted in {loc}")
110
+ end = Ref("JON 3:6!4")
111
+ eloc = findref(end, jon_usfm.getroot(), atend=True)
112
+ if eloc.el.tag != "verse" and eloc.attrib != " tail" and eloc.char !=24:
113
+ fail(f"{end} resulted in {eloc}")
114
+ res = eloc.el.tail[loc.char:eloc.char]
115
+ if res != "reached the":
116
+ fail(f"{res} != 'reached the'")
117
+
118
+ def test_findref1():
119
+ first = Ref("JON 2:8")
120
+ last = Ref("JON 2")
121
+ _do_findreftest(first, last)
122
+
123
+ def test_findref2():
124
+ ref = Ref("JON 2:8-end")
125
+ _do_findreftest(ref.first, ref.last)
126
+
127
+ def test_getrefs():
128
+ refs = RefList("JON 1:3; 2:4-5; 3:9-end")
129
+ res = jon_usfm.getrefs(*refs)
130
+ root = res.getroot()
131
+ print(res.outUsfm())
132
+ if len(root) != 14 or root[0].get("vid", "") != "JON 1:3":
133
+ fail(f"{len(root)=}")
134
+
135
+ def test_textref1():
136
+ r = Ref("JON 2:8!2-4")
137
+ root = jon_usfm.getroot()
138
+ start = findref(r.first, root)
139
+ end = findref(r.last, root, atend=True)
140
+ print(start, end)
141
+ res = copy_text(jon_usfm.getroot(), start, end)
142
+ print(res)
143
+ if res != "who cling to":
144
+ fail(f"{res}")
145
+
146
+ def test_gen1():
147
+ _t("GEN 1:1", _r("GEN", 1, 1))
148
+
149
+ def test_jhn3():
150
+ _t("JHN 3", _r("JHN", 3, None))
151
+
152
+ def test_3jn():
153
+ _t("3JN 3", _r("3JN", 1, 3))
154
+
155
+ def test_1co():
156
+ _t("1CO 6:5a", _r("1CO", 6, 5, "a"))
157
+
158
+ def test_mat5():
159
+ _t("MAT 5:1-7", RefRange(_r("MAT", 5, 1), _r("MAT", 5, 7)))
160
+
161
+ def test_mat7():
162
+ _t("MAT 7:1,2; 8:6b-9:4", _r("MAT", 7, 1), _r("MAT", 7, 2), RefRange(_r("MAT", 8, 6, "b"), _r("MAT", 9, 4)))
163
+
164
+ def test_luk3():
165
+ _t("LUK 3:35-end", RefRange(_r("LUK", 3, 35), _r("LUK", 3, -1)))
166
+
167
+ def test_rom1():
168
+ _listtest("ROM 1; MAT 3:4-11; ROM 1:3-2:7", "MAT 3:4-11; ROM 1-2:7")
169
+
170
+ def test_gen1_1():
171
+ _t("GEN 1:1-3; 3:2-11; LUK 4:5", RefRange(_r("GEN", 1, 1), _r("GEN", 1, 3)), RefRange(_r("GEN", 3, 2), _r("GEN", 3, 11)), _r("LUK", 4, 5))
172
+
173
+ def test_jud():
174
+ _t("JUD 1,2,4", _r("JUD", 1, 1), _r("JUD", 1, 2), _r("JUD", 1, 4))
175
+
176
+ def test_gen1_1a():
177
+ _o("GEN 1:1", "EXO 2:3", (True,False,False))
178
+
179
+ def test_exo2():
180
+ _o("EXO 2:4", "EXO 2", (False,True,True))
181
+
182
+ def test_exo2a():
183
+ _o("EXO 2:4-5", "EXO 2", (False,True,True))
184
+
185
+ def test_exo2b():
186
+ _o("EXO 2:4-5", "EXO 3", (True,False,False))
187
+
188
+ def text_exo2c():
189
+ _o("EXO 2:1-2", "EXO 2:1-5", (False,False,True))
190
+
191
+ def test_exo2d():
192
+ _o("EXO 2:1-2", "EXO 2:2-7", (True,False,True))
193
+
194
+ def test_exo2e():
195
+ _o("EXO 2:1-2", "EXO 2:3-6", (True,False,False))
196
+
197
+ def test_gen2():
198
+ _o("GEN 2:1-2", "EXO 2:3-6", (True,False,False))
199
+
200
+ def test_deu2():
201
+ _o("DEU 2:1-2", "EXO 2:3-6", (False,True,False))
202
+
203
+ def test_exo2f():
204
+ _o("EXO 2:2-3", "EXO 2:1-5", (False,False,True))
205
+
206
+
207
+ # ChatGPT tests:
208
+ def test_invalid_ref():
209
+ try:
210
+ r = Ref("INVALID 1:1")
211
+ fail("Invalid reference did not raise an error")
212
+ except SyntaxError:
213
+ pass
214
+
215
+ def test_cross_book_range():
216
+ r = Ref("JON 3:10 - MIC 1:2")
217
+ if not check_ref(r.first, None, "JON", 3, 10):
218
+ fail(f"{r.first} is not JON 3:10")
219
+ if not check_ref(r.last, None, "MIC", 1, 2):
220
+ fail(f"{r.last} is not MIC 1:2")
221
+
222
+ def test_partial_chapter():
223
+ r = Ref("JON 2")
224
+ if not check_ref(r.first, None, "JON", 2, None):
225
+ fail(f"{r.first} is not JON 2")
226
+
227
+ def test_textref_boundary():
228
+ res = _get_textref("JON 3:10!1-2")
229
+ print(res)
230
+ if res != "When God":
231
+ fail(f"Unexpected result: {res}")
232
+
233
+ def test_multiple_ranges():
234
+ refs = RefList("JON 1:1-3; 2:4; 3:8-end")
235
+ res = jon_usfm.getrefs(*refs)
236
+ root = res.getroot()
237
+ et.dump(root)
238
+
239
+ if len(root) != 11 or root[0].get("vid", "") != "JON 1:1":
240
+ fail(f"Incorrect range extraction: {len(root)=}")
241
+
242
+ def test_chapter_only_range():
243
+ r = Ref("JON 3").expand()
244
+ if not check_ref(r.first, None, "JON", 3, 1):
245
+ fail(f"{r.first} is not JON 3:1")
246
+
247
+ def test_single_verse():
248
+ r = Ref("JON 1:1")
249
+ if not check_ref(r.first, None, "JON", 1, 1):
250
+ fail(f"{r.first} is not JON 1:1")
251
+
252
+ def test_multi_book_list():
253
+ refs = RefList("JON 1:1; MIC 2:3; NAH 1:7")
254
+ if len(refs) != 3:
255
+ fail("Expected three references")
256
+
257
+ def test_edge_case_verse():
258
+ r = Ref("JON 4:11")
259
+ if not check_ref(r.first, None, "JON", 4, 11):
260
+ fail(f"{r.first} is not JON 4:11")
261
+
262
+ def test_reverse_range():
263
+ try:
264
+ r = RefList("JON 3:5 - JON 2:1")
265
+ fail("Reverse range did not raise an error")
266
+ except ValueError:
267
+ pass
268
+
269
+ def test_reference_with_suffix():
270
+ r = Ref("JON 3:10a")
271
+ if not check_ref(r.first, None, "JON", 3, 10):
272
+ fail(f"{r.first} is not JON 3:10a")
273
+ if not check_ref(r.first, None, "JON", 3, 10, "a"):
274
+ fail(f"{r.first} is not JON 3:10")
275
+
276
+ def test_overlapping_ranges():
277
+ refs = RefList("JON 1:3-5; 1:4-6")
278
+ if len(refs) != 2:
279
+ fail("Expected two overlapping references")
280
+
281
+ def test_whitespace_in_ref():
282
+ try:
283
+ r = Ref(" JON 2 : 3 ")
284
+ fail(f"{r.first} has whitespace in it and should fail")
285
+ except SyntaxError:
286
+ pass
287
+
288
+ def test_missing_chapter():
289
+ try:
290
+ r = Ref("JON :5")
291
+ fail("Missing chapter did not raise an error")
292
+ except SyntaxError:
293
+ pass
294
+
295
+ def test_long_reference():
296
+ r = RefList("JON 1:1-4:11")
297
+ if not (check_ref(r[0].first, None, "JON", 1, 1) and check_ref(r[0].last, None, "JON", 4, 11)):
298
+ fail("Long reference not parsed correctly")
299
+
300
+ def test_empty_reference():
301
+ try:
302
+ r = Ref("")
303
+ fail("Empty reference did not raise an error")
304
+ except SyntaxError:
305
+ pass
306
+
307
+ def test_invalid_book_code():
308
+ r = Ref("XYZ 1:1")
309
+ if r.isvalid():
310
+ fail("Invalid book code did not raise an error")
311
+
312
+ def test_multiple_chapters():
313
+ refs = RefList("JON 2:1-3:5")
314
+ if not (check_ref(refs[0].first, None, "JON", 2, 1) and check_ref(refs[0].last, None, "JON", 3, 5)):
315
+ fail("Multiple chapters not handled correctly")
316
+
317
+ def test_partial_verse():
318
+ r = Ref("JON 3:10!1-4")
319
+ root = jon_usfm.getroot()
320
+ start = findref(r.first, root)
321
+ end = findref(r.last, root, atend=True)
322
+ res = copy_text(root, start, end)
323
+
324
+ if "God" not in res:
325
+ fail("Partial verse not extracted correctly")
326
+
327
+ def test_ref_with_extra_text():
328
+ try:
329
+ r = Ref("JON 3:10abc")
330
+ fail("Reference with extra text did not raise an error")
331
+ except SyntaxError:
332
+ pass
333
+
334
+ def test_range_with_negative_chapter():
335
+ try:
336
+ r = Ref("JON -1:5")
337
+ fail("Negative chapter did not raise an error")
338
+ except SyntaxError:
339
+ pass
340
+
341
+ def test_incomplete_reference():
342
+ try:
343
+ r = Ref("JON")
344
+ fail("Incomplete reference did not raise an error")
345
+ except SyntaxError:
346
+ pass
347
+
348
+ def test_large_chapter_number():
349
+ r = Ref("JON 999:1")
350
+ if r.isvalid():
351
+ fail("Large chapter number did not raise an error")
352
+
353
+ ### Markers
354
+
355
+ def test_parsemarker():
356
+ res = _get_textref("JON 1:4!s1!2")
357
+ if res != "Great":
358
+ fail(f"{r}: [{start}, {end}] {res}")
359
+
360
+ def test_footnote():
361
+ res = _get_textref("JON 1:13!f!5")
362
+ if res != "dug":
363
+ fail(f"{r}: [{start}, {end}] {res}")
364
+
365
+
@@ -1,199 +0,0 @@
1
- import pytest
2
- from pytest import fail
3
- from usfmtc import readFile, usx2usfm
4
- from usfmtc.usxmodel import findref, copy_range
5
- from usfmtc.reference import Ref, RefList, RefRange
6
- import os, io
7
- import xml.etree.ElementTree as et
8
-
9
- # To see the print() output of passing tests, run pytest -s
10
-
11
- def check_ref(ref, product=None, book=None, chapter=None, verse=None, subverse=None,
12
- word=None, char=None, mrkrs=None):
13
- if product != ref.product:
14
- return False
15
- if book != ref.book:
16
- return False
17
- if chapter != ref.chapter:
18
- return False
19
- if verse != ref.verse:
20
- return False
21
- if subverse != ref.subverse:
22
- return False
23
- if word != ref.word:
24
- return False
25
- if char != ref.char:
26
- return False
27
- return True
28
-
29
- def asusfm(root):
30
- with io.StringIO() as fh:
31
- usx2usfm(fh, root)
32
- res = fh.getvalue()
33
- return res
34
-
35
- jon_usfm = readFile(os.path.join(os.path.dirname(__file__), "32JONBSB.usfm"))
36
-
37
- def test_basic():
38
- r = Ref("JHN 3:16")
39
- rr = Ref("12", context=r)
40
-
41
- if not check_ref(r, None, "JHN", 3, 16):
42
- fail(f"{r} is not JHN 3:16")
43
- if not check_ref(rr, None, "JHN", 3, 12):
44
- fail(f"{r} is not JHN 3:12")
45
-
46
- def test_range():
47
- r = RefList("JHN 3:16-18")
48
-
49
- if not check_ref(r[0].first, None, "JHN", 3, 16):
50
- fail(f"{r[0].first} is not JHN 3:16")
51
- if not check_ref(r[0].last, None, "JHN", 3, 18):
52
- fail(f"{r[0].last} is not JHN 3:18")
53
-
54
- def test_findreftext():
55
- r = Ref("JON 3:6!3")
56
- loc = findref(r, jon_usfm.getroot())
57
-
58
- if loc.el.tag != "verse" and loc.attrib != " tail" and loc.char != 18:
59
- fail(f"{r} resulted in {loc}")
60
- end = Ref("JON 3:6!4")
61
- eloc = findref(end, jon_usfm.getroot(), atend=True)
62
- if eloc.el.tag != "verse" and eloc.attrib != " tail" and eloc.char !=24:
63
- fail(f"{end} resulted in {eloc}")
64
- res = eloc.el.tail[loc.char:eloc.char]
65
- if res != "reached the":
66
- fail(f"{res} != 'reached the'")
67
-
68
- def test_findref1():
69
- first = Ref("JON 2:8")
70
- last = Ref("JON 2")
71
- _do_findreftest(first, last)
72
-
73
- def test_findref2():
74
- ref = Ref("JON 2:8-end")
75
- _do_findreftest(ref.first, ref.last)
76
-
77
- def test_getrefs():
78
- refs = RefList("JON 1:3; 2:4-5; 3:9-end")
79
- res = jon_usfm.getrefs(*refs)
80
- root = res.getroot()
81
- print(res.outUsfm())
82
- if len(root) != 14 or root[0].get("vid", "") != "JON 1:3":
83
- fail(f"{len(root)=}")
84
-
85
- def _do_findreftest(first, last):
86
- root = jon_usfm.getroot()
87
- start = findref(first, root)
88
- end = findref(last, root, atend=True)
89
- res = copy_range(root, start, end)
90
-
91
- print(f"{first=} {start}; {last=} {end}")
92
- et.dump(res)
93
- print(asusfm(res))
94
- if len(res) != 7 or len(res[2]) != 1 or "vomited" not in res[-1][-1].tail:
95
- fail(f"{len(res)=}, {res[-1][-1].tail=}")
96
-
97
- def r(bk, chap, verse, subv=None):
98
- return Ref(book=bk, chapter=chap, verse=verse, subverse=subv)
99
-
100
- def t(s, *r):
101
- res = RefList(s)
102
- if len(res) != len(r):
103
- fail("Failed '{}' resulted in {} references, {}, rather than {}".format(s, len(res), res, len(r)))
104
- for z in zip(res, r):
105
- if z[0] != z[1]:
106
- fail("Reference list failed '{}', {} != {}".format(s, z[0], z[1]))
107
- if str(res) != s:
108
- fail("{} != canonical string of {}".format(s, str(res)))
109
- print("{} = {}".format(s, str(res)))
110
-
111
- def listtest(s, a):
112
- res = RefList(s)
113
- res.simplify()
114
- base = RefList(a)
115
- if res != base:
116
- fail("{} simplified to {} instead of {}".format(s, res, base))
117
- print("{} -> {}".format(s, res))
118
-
119
- def o(a, b, res):
120
- x = RefList(a)[0]
121
- y = RefList(b)[0]
122
- if (isinstance(res,tuple)):
123
- restup=res
124
- restupd=f"x<y {restup[0]}, y<x {restup[1]}, x in y {restup[2]}"
125
- else:
126
- restup=(res,res,res)
127
- restupd=res
128
- a = (y<x)
129
- b = (x<y)
130
- #print(f"y<x :{a} x<y:{b}")
131
- if restup[0] != b:
132
- raise fail(f"{x} should be < {y} [expect: {restupd}]")
133
- if restup[1] != a:
134
- raise fail(f"{y} should not be < {x} [expect: {restupd}]")
135
- if (x in y) != restup[2]:
136
- raise fail(f"{x} in {y} should be {res}")
137
- print("{} <-> {} ({})".format(x, y,restupd))
138
-
139
- def test_gen1():
140
- t("GEN 1:1", r("GEN", 1, 1))
141
-
142
- def test_jhn3():
143
- t("JHN 3", r("JHN", 3, None))
144
-
145
- def test_3jn():
146
- t("3JN 3", r("3JN", 1, 3))
147
-
148
- def test_1co():
149
- t("1CO 6:5a", r("1CO", 6, 5, "a"))
150
-
151
- def test_mat5():
152
- t("MAT 5:1-7", RefRange(r("MAT", 5, 1), r("MAT", 5, 7)))
153
-
154
- def test_mat7():
155
- t("MAT 7:1,2; 8:6b-9:4", r("MAT", 7, 1), r("MAT", 7, 2), RefRange(r("MAT", 8, 6, "b"), r("MAT", 9, 4)))
156
-
157
- def test_luk3():
158
- t("LUK 3:35-end", RefRange(r("LUK", 3, 35), r("LUK", 3, -1)))
159
-
160
- def test_rom1():
161
- listtest("ROM 1; MAT 3:4-11; ROM 1:3-2:7", "MAT 3:4-11; ROM 1-2:7")
162
-
163
- def test_gen1_1():
164
- t("GEN 1:1-3; 3:2-11; LUK 4:5", RefRange(r("GEN", 1, 1), r("GEN", 1, 3)), RefRange(r("GEN", 3, 2), r("GEN", 3, 11)), r("LUK", 4, 5))
165
-
166
- def test_jud():
167
- t("JUD 1,2,4", r("JUD", 1, 1), r("JUD", 1, 2), r("JUD", 1, 4))
168
-
169
- def test_gen1_1a():
170
- o("GEN 1:1", "EXO 2:3", (True,False,False))
171
-
172
- def test_exo2():
173
- o("EXO 2:4", "EXO 2", (False,True,True))
174
-
175
- def test_exo2a():
176
- o("EXO 2:4-5", "EXO 2", (False,True,True))
177
-
178
- def test_exo2b():
179
- o("EXO 2:4-5", "EXO 3", (True,False,False))
180
-
181
- def text_exo2c():
182
- o("EXO 2:1-2", "EXO 2:1-5", (False,False,True))
183
-
184
- def test_exo2d():
185
- o("EXO 2:1-2", "EXO 2:2-7", (True,False,True))
186
-
187
- def test_exo2e():
188
- o("EXO 2:1-2", "EXO 2:3-6", (True,False,False))
189
-
190
- def test_gen2():
191
- o("GEN 2:1-2", "EXO 2:3-6", (True,False,False))
192
-
193
- def test_deu2():
194
- o("DEU 2:1-2", "EXO 2:3-6", (False,True,False))
195
-
196
- def test_exo2f():
197
- o("EXO 2:2-3", "EXO 2:1-5", (False,False,True))
198
-
199
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes