usfmtc 0.4__tar.gz → 0.4.2__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 (46) hide show
  1. {usfmtc-0.4/src/usfmtc.egg-info → usfmtc-0.4.2}/PKG-INFO +1 -1
  2. {usfmtc-0.4 → usfmtc-0.4.2}/pyproject.toml +1 -1
  3. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/__init__.py +23 -6
  4. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/reference.py +2 -2
  5. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/usfmgenerate.py +4 -1
  6. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/usfmparser.py +52 -33
  7. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/usxcursor.py +9 -8
  8. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/usxmodel.py +46 -15
  9. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/utils.py +13 -3
  10. {usfmtc-0.4 → usfmtc-0.4.2/src/usfmtc.egg-info}/PKG-INFO +1 -1
  11. {usfmtc-0.4 → usfmtc-0.4.2}/tests/test_parser.py +40 -1
  12. {usfmtc-0.4 → usfmtc-0.4.2}/tests/test_refs.py +33 -1
  13. {usfmtc-0.4 → usfmtc-0.4.2}/.gitignore +0 -0
  14. {usfmtc-0.4 → usfmtc-0.4.2}/LICENSE +0 -0
  15. {usfmtc-0.4 → usfmtc-0.4.2}/MANIFEST.in +0 -0
  16. {usfmtc-0.4 → usfmtc-0.4.2}/README.md +0 -0
  17. {usfmtc-0.4 → usfmtc-0.4.2}/scripts/usfmconv +0 -0
  18. {usfmtc-0.4 → usfmtc-0.4.2}/scripts/usfmreversify +0 -0
  19. {usfmtc-0.4 → usfmtc-0.4.2}/setup.cfg +0 -0
  20. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/diagrams/diagrams.py +0 -0
  21. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/diagrams/railroad.py +0 -0
  22. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/diagrams/usxgrammar.py +0 -0
  23. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/eng.vrs +0 -0
  24. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/extension.py +0 -0
  25. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/lxmlutils.py +0 -0
  26. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/org.vrs +0 -0
  27. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/rnc2rng/__init__.py +0 -0
  28. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/rnc2rng/parser.py +0 -0
  29. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/rnc2rng/rnctree.py +0 -0
  30. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/rnc2rng/serializer.py +0 -0
  31. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/usjproc.py +0 -0
  32. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/usx.rng +0 -0
  33. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/validating/rngparser.py +0 -0
  34. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/validating/usfmgrammar.py +0 -0
  35. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/validating/usfmparser.py +0 -0
  36. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/validating/usxparser.py +0 -0
  37. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/versification.py +0 -0
  38. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc/xmlutils.py +0 -0
  39. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc.egg-info/SOURCES.txt +0 -0
  40. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc.egg-info/dependency_links.txt +0 -0
  41. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc.egg-info/entry_points.txt +0 -0
  42. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc.egg-info/requires.txt +0 -0
  43. {usfmtc-0.4 → usfmtc-0.4.2}/src/usfmtc.egg-info/top_level.txt +0 -0
  44. {usfmtc-0.4 → usfmtc-0.4.2}/tests/32JONBSB.usfm +0 -0
  45. {usfmtc-0.4 → usfmtc-0.4.2}/tests/test_linkages.py +0 -0
  46. {usfmtc-0.4 → usfmtc-0.4.2}/tests/test_versification.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usfmtc
3
- Version: 0.4
3
+ Version: 0.4.2
4
4
  Summary: USFM reference parser
5
5
  Author-email: USFM Technical Committee <fonts@sil.org>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "usfmtc"
7
- version = "0.4"
7
+ version = "0.4.2"
8
8
  authors = [{name = "USFM Technical Committee", email = "fonts@sil.org"}]
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -9,7 +9,7 @@
9
9
  # nuitka-project: --output-filename=usfmconv.bin
10
10
 
11
11
  import os, json, io
12
- from usfmtc.utils import readsrc
12
+ from usfmtc.utils import readsrc, getSrcName
13
13
  from usfmtc.validating.usfmparser import parseusfm, UsfmParserBackend
14
14
  from usfmtc.validating.rngparser import NoParseError
15
15
  from usfmtc.extension import Extensions
@@ -25,7 +25,7 @@ from usfmtc.usfmgenerate import usx2usfm
25
25
  from usfmtc.reference import RefList
26
26
  import xml.etree.ElementTree as et
27
27
 
28
- version = "0.4"
28
+ version = "0.4.2"
29
29
 
30
30
  def _grammarDoc(gsrc, extensions=[], factory=et):
31
31
  data = readsrc(gsrc)
@@ -126,7 +126,12 @@ class USX:
126
126
  Raise usfmtc.parser.NoParseError on error.
127
127
  elfactory must take parent and pos named parameters not as attributes
128
128
  """
129
- data = readsrc(src)
129
+ readerr = None
130
+ try:
131
+ data = readsrc(src, errors='strict')
132
+ except UnicodeError as e:
133
+ readerr = f"In file {getSrcName(src)}: {e}"
134
+ data = readsrc(src)
130
135
 
131
136
  if not altparser:
132
137
  p = USFMParser(data, factory=elfactory or ParentElement, grammar=grammar, strict=strict, **kw)
@@ -137,6 +142,8 @@ class USX:
137
142
  result = parseusfm(data, grammar, timeout=timeout, isdata=True, **kw)
138
143
  xml = result.asEt(elfactory=elfactory)
139
144
 
145
+ if p and readerr is not None:
146
+ p.errors.insert(0, (readerr,))
140
147
  cleanup(xml) # normalize space, de-escape chars, cell aligns, etc.
141
148
  res = cls(xml, grammar, errors=p.errors if p else None)
142
149
  if keepparser:
@@ -145,7 +152,7 @@ class USX:
145
152
 
146
153
  @classmethod
147
154
  def fromUsj(cls, src, elfactory=None, grammar=None, **kw):
148
- data = readsrc(src)
155
+ data = readsrc(src, errors=kw.get("errors", "replace"))
149
156
  djson = json.loads(data)
150
157
  xml = usjtousx(djson, elfactory=elfactory)
151
158
  return cls(xml, grammar)
@@ -291,7 +298,7 @@ class USX:
291
298
  if outtype == "usfm3.0":
292
299
  outtype = "usfm"
293
300
  if version is None:
294
- version = "0.4"
301
+ version = "0.4.2"
295
302
  self.outUsfm(grammar=grammar, file=outfpath, outversion=version, altparser=altparser, **kw)
296
303
 
297
304
  def canonicalise(self, version=None):
@@ -337,7 +344,7 @@ class USX:
337
344
  @version.setter
338
345
  def version(self, version):
339
346
  if isinstance(version, (list, tuple)):
340
- version = "0.4".join([str(x) for x in version])
347
+ version = "0.4.2".join([str(x) for x in version])
341
348
  if version is not None:
342
349
  self.getroot().set('version', str(version))
343
350
 
@@ -348,6 +355,12 @@ class USX:
348
355
  return bke.get("code", None)
349
356
  return None
350
357
 
358
+ @book.setter
359
+ def book(self, code):
360
+ bke = self.getroot().find(".//book")
361
+ if bke is not None:
362
+ bke.set('code', code)
363
+
351
364
  def main(hookcli=None, hookusx=None):
352
365
 
353
366
  import argparse, logging, sys
@@ -470,6 +483,10 @@ def main(hookcli=None, hookusx=None):
470
483
  if len(infiles) == 1 and usxdoc is None:
471
484
  doerror(f"Unable to read in {args.infile}")
472
485
 
486
+ if usxdoc.errors is not None and len(usxdoc.errors):
487
+ for m, p, r in usxdoc.errors:
488
+ doerror(f"{r} ({p}): {m}", False)
489
+
473
490
  if reflist is not None:
474
491
  book = usxdoc.book
475
492
  bkrefs = [r for r in reflist if r.first.book == book]
@@ -696,7 +696,7 @@ class RefRange:
696
696
  return first
697
697
  return super().__new__(cls)
698
698
 
699
- def __init__(self, first: Optional[Ref]=None, last: Optional[Ref]=None, strict: bool=False, **kw):
699
+ def __init__(self, first: Optional[Ref]=None, last: Optional[Ref]=None, strict: bool=False, test: bool=True, **kw):
700
700
  self.strict = strict
701
701
  self.first = first.first
702
702
  self.last = last.last
@@ -705,7 +705,7 @@ class RefRange:
705
705
  self.sep = kw['sep']
706
706
  if getattr(self.last, 'book', None) is None:
707
707
  self.last.book = self.first.book
708
- if self.last < self.first:
708
+ if test and self.last < self.first:
709
709
  raise ValueError(f"{first=} is after {last=}")
710
710
  if isinstance(self.first, RefRange):
711
711
  raise ValueError(f"Nested RefRange({self.first})")
@@ -10,13 +10,14 @@ def usvout(m):
10
10
  else:
11
11
  return "\\u{:04X}".format(u)
12
12
 
13
- _reesc = re.compile(r'([\\|]|//)')
13
+ _reesc = re.compile(r'([\\|~]|//)')
14
14
  _reatt = re.compile(r'([\\|"])')
15
15
  def attribescaped(s, escapes):
16
16
  return escaped(s, escapes, reg=_reatt)
17
17
 
18
18
  def escaped(s, escapes, reg=None):
19
19
  res = (reg or _reesc).sub(r'\\\1', s)
20
+ res = res.replace("\u00A0", "~")
20
21
  if escapes:
21
22
  res = re.sub(r'([{}])'.format(escapes), usvout, res)
22
23
  return res
@@ -94,6 +95,8 @@ def iterels(el, events):
94
95
  def _isnextref(cref, ref, e):
95
96
  if ref.chapter == cref.chapter:
96
97
  return ref.verse == cref.verse or ref.verse == cref.verse + 1
98
+ elif cref.chapter is None:
99
+ return False
97
100
  else:
98
101
  return ref.chapter == cref.chapter + 1 and ref.verse == 1
99
102
 
@@ -4,26 +4,32 @@ import regex
4
4
  import xml.etree.ElementTree as et
5
5
  from usfmtc.extension import SFMFile
6
6
  from collections import UserDict, UserString
7
+ from typing import Optional, Dict, Any, List, Type, Union
7
8
 
8
9
  WS = "\t\n\r " # \v\f\u001C\u001D\u001E\u001F " full python ASCII WS
9
10
 
10
11
  class Pos:
11
- def __init__(self, l, c, **kw):
12
+ def __init__(self, l: int, c: int, **kw) -> None:
12
13
  self.l = l
13
14
  self.c = c
14
15
  self.kw = kw
15
16
 
16
- def __str__(self):
17
+ def __str__(self) ->str:
17
18
  return f"{self.l}:{self.c}"
18
19
 
19
20
  def __repr__(self):
20
21
  return f"Pos({self.l}:{self.c})"
21
22
 
22
- def copy(self):
23
+ def copy(self) -> 'Pos':
23
24
  return self.__class__(self.l, self.c, **self.kw)
24
25
 
25
26
  class Tag(str):
26
- def __new__(cls, s, l=0, c=0, **kw):
27
+ pos: Pos
28
+ isplus: bool
29
+ isend: bool
30
+ attribs: Optional[Dict[str, str]]
31
+
32
+ def __new__(cls, s: str, l: int = 0, c: int = 0, **kw: Any) -> "Tag":
27
33
  isend = False
28
34
  isplus = False
29
35
  if s.startswith("+"):
@@ -55,7 +61,7 @@ class Tag(str):
55
61
  res += "*" if self.isend else ""
56
62
  return res
57
63
 
58
- def basestr(self):
64
+ def basestr(self) -> str:
59
65
  """ Returns marker with no final *, etc. """
60
66
  return super().__str__()
61
67
 
@@ -80,7 +86,10 @@ class OptBreak:
80
86
  return "//"
81
87
 
82
88
  class String(UserString):
83
- def __init__(self, s, l=0, c=0, **kw):
89
+ pos: Pos
90
+ kw: Dict[str, Any]
91
+
92
+ def __init__(self, s: str, l: int=0, c: int=0, **kw: Any) -> None:
84
93
  super().__init__(s)
85
94
  self.pos = Pos(l, c)
86
95
  self.kw = kw
@@ -91,7 +100,7 @@ class String(UserString):
91
100
  def be(self, s):
92
101
  return String(str(s), l=self.pos.l, c=self.pos.c, **self.kw)
93
102
 
94
- def addToNode(self, node, position, lstrip=False):
103
+ def addToNode(self, node: et.Element, position: str, lstrip: bool=False) -> None:
95
104
  cp = self.pos.c
96
105
  s = str(self)
97
106
  if lstrip:
@@ -114,14 +123,18 @@ class Attribs(UserDict):
114
123
  self.kw = kw
115
124
 
116
125
  class Lexer:
126
+ txt: str
127
+ cindex: int
128
+ lindex: int
129
+ nexts: List[Union[Tag, String, 'Attribs', 'OptBreak']]
117
130
 
118
131
  tokenre = regex.compile(r''' # the next token in text
119
132
  ((?: [^\\|/\r\n] # string component consists of: normal characters
120
- | \\ (?:[\\|] | \r?\n) # escaped specials
133
+ | \\ (?:[\\|~] | \r?\n) # escaped specials
121
134
  | / (?!/) # / sequence, not //
122
135
  )+ # one or more of these
123
136
  )
124
- | ( [\\|] | \r?\n | // ) # then specials: tag start or attributes start, newline, //
137
+ | ( [\\|~] | \r?\n | // ) # then specials: tag start or attributes start, newline, //
125
138
  ''', regex.X)
126
139
  tagre = regex.compile(r'''
127
140
  (?: \+? [a-zA-Z_][a-zA-Z_0-9^-]* )? # optional + then the usfm tag spec
@@ -139,14 +152,15 @@ class Lexer:
139
152
  afterattribs = regex.compile(r'\s*\\')
140
153
  usvre = regex.compile(r'(?:\\u([0-9a-fA-F]{4})|\\U([0-9a-fA-F]{8}))')
141
154
 
142
- def __init__(self, txt, parser, expanded=False, strict=False, tagger=Tag):
155
+ def __init__(self, txt: str, parser: "USFMParser", expanded: bool=False,
156
+ strict: bool=False, tagger: Type[Tag]=Tag):
143
157
  self.txt = txt
144
158
  self.expanded = expanded
145
159
  self.strict = strict
146
160
  self.parser = parser
147
161
  self.tagger = tagger
148
162
 
149
- def __iter__(self):
163
+ def __iter__(self) -> "Lexer":
150
164
  self.nexts = []
151
165
  self.cindex = 0
152
166
  self.lindex = 0
@@ -155,7 +169,7 @@ class Lexer:
155
169
  self.currxpand = None
156
170
  return self
157
171
 
158
- def __next__(self):
172
+ def __next__(self) -> Union[Tag, String, "Attribs", "AttribText", "OptBreak"]:
159
173
  if len(self.nexts):
160
174
  return self.nexts.pop(0)
161
175
  curri = self.cindex
@@ -181,7 +195,7 @@ class Lexer:
181
195
  u = self.usvre.match(self.txt[m.end():])
182
196
  if u:
183
197
  c = chr(int((m.group(1) or m.group(2)), 16))
184
- s += c
198
+ res += c
185
199
  curri += u.end()
186
200
  continue
187
201
  t = self.tagre.match(self.txt[m.end():])
@@ -207,9 +221,9 @@ class Lexer:
207
221
  elif n == '|':
208
222
  t, curri = self.readAttrib(curri)
209
223
  if not len(t):
210
- res = String("", l=self.lindex, c=curri-self.lpos)
211
- continue
212
- res = t
224
+ res = AttribText("", l=self.lindex, c=curri-self.lpos)
225
+ else:
226
+ res = t
213
227
  break
214
228
  elif n == '//':
215
229
  res = OptBreak(l=self.lindex, c=curri-self.lpos)
@@ -299,6 +313,9 @@ class Grammar:
299
313
  "note": ("footnote", "crossreference"),
300
314
  "intro": ("introduction", "introchar")
301
315
  }
316
+ node_depths = {"usx": 1, "book": 2, "periph": 3, "sidebar": 4, "chapter": 5, "para": 5, "table": 5,
317
+ "row": 6, "cell": 7, "char": 8, "optbreak": 8, "figure": 8, "ref": 8, "link": 8, "verse": 8,
318
+ "note": 9, "ms": 10}
302
319
 
303
320
  marker_categories = {t:k for k, v in category_markers.items() for t in v.split()}
304
321
  marker_tags = {t:k for k, v in category_tags.items() for t in v}
@@ -362,7 +379,7 @@ class Grammar:
362
379
  def copy(self):
363
380
  res = self.__class__(quick=True)
364
381
  for a in ('marker_categories', 'attribmap', 'attributes'):
365
- setattr(res, a, getattr(self, a))
382
+ setattr(res, a, getattr(self, a).copy())
366
383
  return res
367
384
 
368
385
  def readmrkrs(self, fname):
@@ -393,7 +410,7 @@ def isfirstText(e):
393
410
  return True
394
411
 
395
412
  class Node:
396
- def __init__(self, parser, usxtag, tag, ispara=False, notag=False, pos=None, **kw):
413
+ def __init__(self, parser, usxtag, tag, *, ispara=False, notag=False, pos=None, **kw):
397
414
  self.parser = parser
398
415
  self.tag = tag
399
416
  self.ispara = ispara
@@ -495,7 +512,7 @@ class USXNode(Node):
495
512
  self.element.set('version', str(t).strip(WS))
496
513
 
497
514
  class AttribNode(Node):
498
- def __init__(self, parser, parent, tag, pos=None, only=[], **kw):
515
+ def __init__(self, parser, parent, tag, *, pos=None, only=[], **kw):
499
516
  self.parser = parser
500
517
  self.parent = parent
501
518
  self.tag = tag
@@ -550,7 +567,7 @@ class FwdAttribNode(Node):
550
567
 
551
568
 
552
569
  class NumberNode(Node):
553
- def __init__(self, parser, usxtag, tag, ispara=False, pos=None, **kw):
570
+ def __init__(self, parser, usxtag, tag, *, ispara=False, pos=None, **kw):
554
571
  super().__init__(parser, usxtag, tag, ispara=ispara, pos=pos, **kw)
555
572
  self.hasarg = False
556
573
 
@@ -631,9 +648,10 @@ class FallBackError(Exception):
631
648
  paratypes = ('header', 'introduction', 'list', 'otherpara', 'sectionpara', 'versepara', 'title', 'chapter', 'ident')
632
649
  paratags = ('rem', ' table', 'sidebar')
633
650
 
651
+
634
652
  class USFMParser:
635
653
 
636
- def __init__(self, txt, factory=None, grammar=None, expanded=False, strict=False, version=[99], index=True, **kw):
654
+ def __init__(self, txt, *, factory=None, grammar=None, expanded=False, strict=False, version=[99], index=True, **kw):
637
655
  if factory is None:
638
656
  def makeel(tag, attrib, **extras):
639
657
  attrib.update({" "+k:v for k, v in extras.items()})
@@ -753,7 +771,7 @@ class USFMParser:
753
771
  self.error(SyntaxError, f"Closing '{tag}' with no corresponding opening", self.lexer.currpos())
754
772
  return self.stack[-1]
755
773
 
756
- def removeType(self, t, tags=[]):
774
+ def removeType(self, t, tags=[], node=None):
757
775
  if isinstance(t, str):
758
776
  t = [t]
759
777
  oldstack = self.stack[:]
@@ -761,7 +779,8 @@ class USFMParser:
761
779
  curr = self.stack.pop()
762
780
  if hasattr(curr, 'element'):
763
781
  e = curr.element
764
- if e.tag in ("usx", "periph") or e.tag in tags:
782
+ if self.grammar.node_depths.get(e.tag, 10) < self.grammar.node_depths.get(node, 4) \
783
+ or e.tag in tags:
765
784
  self.stack.append(curr)
766
785
  break
767
786
  curr.close()
@@ -786,7 +805,7 @@ class USFMParser:
786
805
  elif e.tag in tags or cat in t:
787
806
  break
788
807
  else:
789
- self.stack = oldstack
808
+ self.stack = oldstack # what about everything we closed on getting here?
790
809
  return self.stack[-1] if len(self.stack) else None
791
810
 
792
811
  def cvref(self):
@@ -795,7 +814,7 @@ class USFMParser:
795
814
 
796
815
  #### Event methods
797
816
  def _c(self, tag):
798
- self.removeType(paratypes, paratags)
817
+ self.removeType(paratypes, paratags, node="chapter")
799
818
  return self.addNode(NumberNode(self, "chapter", tag, ispara=True, pos=tag.pos))
800
819
 
801
820
  def _cp(self, tag):
@@ -809,7 +828,7 @@ class USFMParser:
809
828
  return parent
810
829
 
811
830
  def _esb(self, tag):
812
- self.removeType(paratypes, paratags)
831
+ self.removeType(paratypes, paratags, node="sidebar")
813
832
  return self.addNode(Node(self, "sidebar", tag, pos=tag.pos))
814
833
 
815
834
  def _esbe(self, tag):
@@ -831,7 +850,7 @@ class USFMParser:
831
850
  return self.otherpara(tag)
832
851
  if tag.isend:
833
852
  return self.removeTag(str(tag))
834
- self.removeType(paratypes, paratags)
853
+ self.removeType(paratypes, paratags, node="para")
835
854
  res = self.addNode(Node(self, 'para', tag, ispara=True, pos=tag.pos))
836
855
  self.lexer.readLine()
837
856
  self.lexer.appendtag("rem*")
@@ -840,7 +859,7 @@ class USFMParser:
840
859
  def _tr(self, tag):
841
860
  self.removeTag('tr', absentok=True)
842
861
  if not len(self.stack) or self.stack[-1].element.tag != "table":
843
- self.removeType(paratypes,)
862
+ self.removeType(paratypes, node="table")
844
863
  self.addNode(Node(self, 'table', ' table', notag=True, pos=tag.pos))
845
864
  return self.addNode(Node(self, 'row', 'tr', pos=tag.pos))
846
865
 
@@ -865,7 +884,7 @@ class USFMParser:
865
884
  def _xt(self, tag):
866
885
  removed = False
867
886
  if self.grammar.marker_categories.get(self.stack[-1].tag, "") == "crossreferencechar":
868
- res = self.removeType('crossreferencechar')
887
+ res = self.removeType('crossreferencechar', node="char")
869
888
  removed = True
870
889
  if not tag.isend:
871
890
  res = self.addNode(Node(self, 'char', tag, pos=tag.pos))
@@ -894,7 +913,7 @@ class USFMParser:
894
913
  return self.addNode(FwdAttribNode(self, 'ms', tag, pos=tag.pos))
895
914
 
896
915
  def cell(self, tag):
897
- self.removeType('cell', tags=["row"])
916
+ self.removeType('cell', tags=["row"], node="cell")
898
917
  if not tag.isend:
899
918
  return self.addNode(Node(self, 'cell', tag, pos=tag.pos))
900
919
  return self.parent
@@ -916,7 +935,7 @@ class USFMParser:
916
935
  return self.addNode(NoteNode(self, 'note', tag, pos=tag.pos))
917
936
 
918
937
  def crossreferencechar(self, tag):
919
- res = self.removeType('crossreferencechar', tags=["note"])
938
+ res = self.removeType('crossreferencechar', tags=["note"], node="char")
920
939
  if not tag.isend:
921
940
  res = self.addNode(Node(self, 'char', tag, pos=tag.pos))
922
941
  return res
@@ -927,7 +946,7 @@ class USFMParser:
927
946
  return self.addNode(NoteNode(self, 'note', tag, pos=tag.pos))
928
947
 
929
948
  def footnotechar(self, tag):
930
- res = self.removeType('footnotechar', tags=["note"])
949
+ res = self.removeType('footnotechar', tags=["note"], node="char")
931
950
  if not tag.isend:
932
951
  res = self.addNode(Node(self, 'char', tag, pos=tag.pos))
933
952
  return res
@@ -963,7 +982,7 @@ class USFMParser:
963
982
 
964
983
  def main():
965
984
  import sys
966
- p = USFMParser(sys.argv[1])
985
+ p = USFMParser(open(sys.argv[1], encoding="utf-8").read())
967
986
  e = p.parse()
968
987
  et.indent(e)
969
988
  et.dump(e)
@@ -117,9 +117,8 @@ def _findcvel(ref, usx, atend=False, parindex=0):
117
117
  the end of the elment, the next element. parindex speeds up the hunt for the
118
118
  chapter.'''
119
119
  resm = 0
120
- if ref.book:
121
- if ref.book is not None and usx.book != ref.book:
122
- raise ValueError("Reference book {} != text book {}".format(ref, usx.book))
120
+ if ref.book is not None and usx.book != ref.book:
121
+ raise ValueError("Reference book {} != text book {}".format(ref, usx.book))
123
122
  root = usx.getroot()
124
123
  c = ref.chapter
125
124
  foundend = False
@@ -331,11 +330,12 @@ class USXCursor(ETCursor):
331
330
  p = p.parent
332
331
  else:
333
332
  p = a.el
334
- i = list(root).index(p)
333
+ #i = list(root).index(p)
335
334
  allnew = {}
336
335
  res = factory(root.tag, attrib=root.attrib)
337
336
  currp = res
338
337
  curr = root
338
+ start = 0
339
339
  if addintro or titles:
340
340
  categories = ["versepara", "sectionpara"] + ["title", "introduction"] if not addintro else []
341
341
  def isendintro(e):
@@ -344,21 +344,22 @@ class USXCursor(ETCursor):
344
344
  s = e.get("style", "")
345
345
  return grammar.marker_categories.get(s, "") in categories
346
346
  # copy the tree up to the first chapter or verse text
347
- for eloc in root:
347
+ for i, eloc in enumerate(root):
348
348
  if isendintro(eloc):
349
+ start = i
349
350
  break
350
351
  newp = eloc.copy(deep=True, parent=currp, factory=factory)
351
352
  currp.append(newp)
352
353
  curr = root
353
354
  currp = res
354
355
 
355
- for eloc, isin in iterusx(root, parindex=i, start=a.el, until=b.el, untilafter=bool(b.attrib)):
356
+ for eloc, isin in iterusx(root, parindex=start, start=a.el, until=b.el, untilafter=bool(b.attrib)):
356
357
  if isin and eloc == b.el:
357
358
  break
358
359
  # at the start
359
- elif isin and eloc == a.el and eloc.tag not in ("para", "book", "sidebar"):
360
+ elif isin and eloc == a.el and eloc.tag not in ("para", "book", "sidebar", "chapter"):
360
361
  # if we are at the start of the parent (para, since a verse is always only in a para) check for subheadings
361
- if headers and grammar and isempty(eloc.parent.text) and eloc.parent.index(eloc) == 0: # are we first?
362
+ if headers and grammar and isempty(eloc.parent.text) and eloc.parent.index(eloc) == start: # are we first?
362
363
  r = eloc.parent.parent # should be root
363
364
  i = r.index(eloc.parent) - 1
364
365
  # scan back over section heads
@@ -5,7 +5,7 @@ from usfmtc.xmlutils import isempty, ParentElement
5
5
  from usfmtc.usfmparser import Grammar, WS
6
6
  from usfmtc.reference import Ref, RefRange, MarkerRef
7
7
  import xml.etree.ElementTree as et
8
- from typing import Optional
8
+ from typing import Optional, Dict, List, Any, Tuple, Type, Union
9
9
 
10
10
  # This should be read from usx.rng
11
11
  allpartypes = {
@@ -226,16 +226,18 @@ def addindexes(root):
226
226
  return chapters, ids
227
227
 
228
228
  escapes = {
229
- '\\' : '\\',
230
- 'n': '\n'
229
+ r'\n': '\n',
230
+ r'\~': '~',
231
+ r'\|': '|',
232
+ r'\\' : '\\',
233
+ '~': '\u00A0'
231
234
  }
232
235
 
236
+ escapere = re.compile(r"(\\[n~\\|]|~)")
233
237
  def add_specials(t, node, parent, istext=False):
234
238
  if t is None:
235
239
  return t
236
- t = re.sub(r'\\(.)', lambda m: escapes.get(m.group(1), "\\"+m.group(1)), t)
237
- if "~" in t:
238
- t = t.replace("~", "\u00A0")
240
+ t = escapere.sub(lambda m: escapes.get(m.group(1), m.group(1)[1:]), t)
239
241
  return t
240
242
 
241
243
  alignments = {
@@ -249,6 +251,7 @@ def _stripws(s, atend=False):
249
251
  return None
250
252
  return _wsere.sub("", s) if atend else _wssre.sub("", s)
251
253
 
254
+ backre = re.compile(r"\\([\\~|])")
252
255
  def cleanup(node, parent=None):
253
256
  if node.tag == 'para':
254
257
  # cleanup spaces at start and end of para
@@ -305,7 +308,11 @@ def cleanup(node, parent=None):
305
308
  node = refnode
306
309
  # strip character escapes
307
310
  for k, v in node.attrib.items():
308
- node.attrib[k] = re.sub(r"\\(.)", r"\1", v)
311
+ node.attrib[k] = backre.sub(r"\1", v)
312
+ #if node.text is not None:
313
+ # node.text = backre.sub(r"\1", node.text)
314
+ #if node.tail is not None:
315
+ # node.tail = backre.sub(r"\1", node.tail)
309
316
  for c in node:
310
317
  cleanup(c, parent=node)
311
318
 
@@ -382,18 +389,42 @@ def canonicalise(node, endofpara=False, factory=et, version=None):
382
389
  inserted += 1
383
390
  ft.text = c.tail.lstrip()
384
391
  c.tail = c.tail[:len(c.tail)-len(ft.text)]
385
- for i, c in enumerate(list(node)):
392
+ nodes = list(node)
393
+ i = 0
394
+ while i < len(nodes):
395
+ c = nodes[i]
386
396
  if c.get('style', '') not in notechars[0 if replace == "ft" else 1]:
387
- if len(node) == 1:
397
+ if i > 0 and c.getprevious().get('style', '') == replace:
398
+ prev = c.getprevious()
399
+ prev.append(c)
400
+ c.parent = prev
401
+ if len(prev) > 1:
402
+ prev[-2].tail = (prev[-2].tail or "") + (prev.tail or "")
403
+ elif prev.tail is not None:
404
+ prev.text += prev.tail
405
+ prev.tail = c.tail
406
+ c.tail = None
407
+ if i < len(nodes) - 1 and nodes[i+1].get('style', '') == replace:
408
+ # merge the nodes
409
+ c.tail = prev.tail + nodes[i+1].text
410
+ for n in nodes[i+1]:
411
+ prev.append(n)
412
+ n.parent = prev
413
+ prev.tail = nodes[i+1].tail
414
+ node.remove(nodes[i+1])
415
+ i += 1
416
+ elif i < len(nodes) - 1 and nodes[i+1].get('style', '') == replace:
417
+ nodes[i+1].insert(0, c)
418
+ c.tail = nodes[i+1].text
419
+ nodes[i+1].text = None
420
+ c.parent = nodes[i+1]
421
+ else:
388
422
  ft = node.makeelement("char", {"style": replace})
389
423
  ft.append(c)
390
424
  c.parent = ft
391
- elif i == 0:
392
- node[1].insert(0, c)
393
- else:
394
- node[i-1].append(c)
425
+ node.insert(node.find(c), ft)
395
426
  node.remove(c)
396
- break
427
+ i += 1
397
428
  if [int(x) for x in version.split(".")] >= [3, 1] and node.tag == "char" and node.get("style", "") == "xt" \
398
429
  and len(node) == 1 and isempty(node.text) and isempty(node[0].tail) and node[0].tag == "ref":
399
430
  i = node.parent.index(node)
@@ -856,7 +887,7 @@ def _getref(e, book, lastchap=0):
856
887
  ref = Ref(refstr)
857
888
  return ref
858
889
 
859
- def _list_insert(arr, k, v, default=0):
890
+ def _list_insert(arr: List[int], k: int, v: int, default: int=0):
860
891
  if k >= len(arr):
861
892
  arr += [default] * (k - len(arr) + 1)
862
893
  arr[k] = v
@@ -5,13 +5,13 @@ import traceback
5
5
 
6
6
  logger = logging.getLogger(__name__)
7
7
 
8
- def readsrc(src):
8
+ def readsrc(src, errors="replace"):
9
9
  if hasattr(src, "read"):
10
10
  return src.read()
11
11
  elif not isinstance(src, str): # we're a parsed xml doc
12
12
  return src
13
- elif len(src) < 128 and os.path.exists(src):
14
- with open(src, encoding="utf-8") as inf:
13
+ elif os.path.exists(src):
14
+ with open(src, encoding="utf-8", errors=errors) as inf:
15
15
  data = inf.read()
16
16
  return data
17
17
  elif "\n" in src or len(src) > 127:
@@ -19,5 +19,15 @@ def readsrc(src):
19
19
  else:
20
20
  raise FileNotFoundError
21
21
 
22
+ def getSrcName(src):
23
+ if hasattr(src, "name"):
24
+ return src.name
25
+ elif not isinstance(src, str):
26
+ return ""
27
+ elif os.path.isfile(src):
28
+ return src
29
+ else:
30
+ return ""
31
+
22
32
  def get_trace():
23
33
  return traceback.format_stack()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usfmtc
3
- Version: 0.4
3
+ Version: 0.4.2
4
4
  Summary: USFM reference parser
5
5
  Author-email: USFM Technical Committee <fonts@sil.org>
6
6
  License: MIT
@@ -36,7 +36,7 @@ def test_hardspaces():
36
36
  t = e.text
37
37
  if not t.endswith("\u00A0"):
38
38
  fail("No hard space after fr in usx")
39
- if not re.search("\u00A0\\\\ft", f):
39
+ if not re.search("~\\\\ft", f):
40
40
  fail("No hard space after fr in usfm")
41
41
 
42
42
  def test_glossary():
@@ -246,3 +246,42 @@ def test_regularise():
246
246
  if not doc.getroot()[2][0].tail.endswith(" "):
247
247
  fail(f"Missing inserted space before v2 in {f}")
248
248
 
249
+ def test_canonicalxt():
250
+ usfm = r"""\id JHN SLU
251
+ \c 6
252
+ \p
253
+ \v 53 Lemadendye Yesuske tyanuk ti sir ma byohe, \wj “Nam kutanuk ti e ne mlay: kolnye ei neke lema mi Irkye Ananke tenanke,\wj*\f + \fr 6:53 \ft Ti ayat ne Yesuske byohe, \fq Irkye Ananke\fq*, neke Yesuske yal ma lyosu I a myesan bo. Kola ne ti ayat \xt 6:27\xt*ke ode \xt 6:62\xt*ke dakun. (Matos ti \xt Dan. 7:13-14\xt*.)\f* \wj ode lema myenw a larake, desikeo ana mormyorif manal nini namke lema kimin a e.\wj*
254
+ \v 54 \wj Klala esei desike nya tenakkwe ode yenw a larakkwe, desikeo i desike mormyorif manal nini namke kimin a i ti kiba knaru ne, ode ana kaorif a i ti sew a nuske ktem ne ana kilodur ne.\wj*
255
+ """
256
+ doc, f = _dousfm(usfm)
257
+ if len(doc.getroot()[2][2]) != 4 or len(doc.getroot()[2][2][3]) != 3:
258
+ fail(f"Bad footnote canonicalising in {f}")
259
+
260
+ def test_specialchars():
261
+ usfm = r"""\id JHN test
262
+ \c 4
263
+ \p
264
+ \v 1 This tests \| pipe, \~ tilde and \\ backslash. Notice the hard~space
265
+ """
266
+ print(usfm)
267
+ doc, f = _dousfm(usfm)
268
+ if '\\\\\\' in f or '~' not in f:
269
+ fail(f"Bad character escaping in {f}")
270
+
271
+ def test_emptyattrib():
272
+ usfm = r"""\id JHN empty attributes
273
+ \p
274
+ \v 1 \rb normal|good\rb* \rb test|\rb* \rb |empty\rb*
275
+ """
276
+ doc, f = _dousfm(usfm)
277
+ if 'test' not in f or 'empty' not in f:
278
+ fail("{f} is missing data")
279
+
280
+ def test_tilde():
281
+ usfm = r"""\id JHN with tildes
282
+ \p
283
+ \v 1 This is a~test
284
+ """
285
+ doc, f = _dousfm(usfm)
286
+ if "~" not in f or "\u00A0" not in doc.getroot()[1][0].tail:
287
+ fail("{f} has bad hard space")
@@ -434,7 +434,6 @@ def test_isvalid():
434
434
 
435
435
  def test_subdoc1():
436
436
  res = jon_usfm.getrefs(*RefList("JON 1:4-8; 4:9-11"), headers=True, titles=False)
437
- et.dump(res.xml)
438
437
  f = res.outUsfm(None, forcevid=True)
439
438
  print(f)
440
439
  if "Acts 27:13-26" not in f and "vid|JON 4:9" not in f:
@@ -442,6 +441,14 @@ def test_subdoc1():
442
441
  if f.count('vid') != 2:
443
442
  fail(f"{f} wrong number of \\vid in the text")
444
443
 
444
+ def test_subdoc2():
445
+ res = jon_usfm.getrefs(*RefList("JON 1:1-3"))
446
+ et.dump(res.getroot())
447
+ f = res.outUsfm(None, forcevid=True)
448
+ print(f)
449
+ if len(res.getroot()) != 7:
450
+ fail(f"{f} has {len(res.getroot())} children and not 7")
451
+
445
452
  def test_finalv():
446
453
  res = jon_usfm.getrefs(Ref("JON 1:17"), titles=False)
447
454
  et.dump(res.xml)
@@ -471,3 +478,28 @@ def test_rangeseq():
471
478
  print(r.str())
472
479
  if "19-2" in str(r):
473
480
  fail(f"{r} has {len(r)} refs: {r[0]}, {r[1]}")
481
+
482
+ def test_subsetemptyfinal():
483
+ usfm = r"""\id ISA test
484
+ \c 4
485
+ \q1 \v 4 Ka wakava some other text
486
+ \q2 This is a secondary line
487
+ \q1 Now another line of poetry:
488
+ \q2 with its secondary.
489
+ \q1 More lines of poetry:
490
+ \q2 with its secondary again.
491
+ \b
492
+ \q1
493
+ \v 5 The start of another verse
494
+ \q2 And the secondary line for this verse
495
+ \s A subheading leading forward
496
+ \q1
497
+ \v 6 Another verse
498
+ \q2 with its secondary
499
+ """
500
+ doc = readFile(usfm, informat="usfm")
501
+ doc.canonicalise()
502
+ subdoc = doc.getrefs(Ref("ISA 4:4-5"))
503
+ f = subdoc.outUsfm(None)
504
+ if '\\s' in f:
505
+ fail(f"{f} has a final section")
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes