usfmtc 0.2.2__tar.gz → 0.2.4__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 (32) hide show
  1. {usfmtc-0.2.2/python/lib/usfmtc.egg-info → usfmtc-0.2.4}/PKG-INFO +2 -2
  2. {usfmtc-0.2.2 → usfmtc-0.2.4}/pyproject.toml +1 -1
  3. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/__init__.py +30 -22
  4. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/extension.py +2 -2
  5. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/usfmparser.py +144 -71
  6. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/xmlutils.py +13 -3
  7. {usfmtc-0.2.2 → usfmtc-0.2.4/python/lib/usfmtc.egg-info}/PKG-INFO +2 -2
  8. {usfmtc-0.2.2 → usfmtc-0.2.4}/LICENSE.md +0 -0
  9. {usfmtc-0.2.2 → usfmtc-0.2.4}/README.md +0 -0
  10. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/README.md +0 -0
  11. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/diagrams.py +0 -0
  12. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/grammar.py +0 -0
  13. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/parser.py +0 -0
  14. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/railroad.py +0 -0
  15. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/reference.py +0 -0
  16. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/rnc2rng/__init__.py +0 -0
  17. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/rnc2rng/parser.py +0 -0
  18. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/rnc2rng/rnctree.py +0 -0
  19. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/rnc2rng/serializer.py +0 -0
  20. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/sfmparser.py +0 -0
  21. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/usfmgenerate.py +0 -0
  22. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/usjproc.py +0 -0
  23. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/usx.rng +0 -0
  24. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/usxgrammar.py +0 -0
  25. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/usxmodel.py +0 -0
  26. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc/usxparser.py +0 -0
  27. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc.egg-info/SOURCES.txt +0 -0
  28. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc.egg-info/dependency_links.txt +0 -0
  29. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc.egg-info/entry_points.txt +0 -0
  30. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc.egg-info/requires.txt +0 -0
  31. {usfmtc-0.2.2 → usfmtc-0.2.4}/python/lib/usfmtc.egg-info/top_level.txt +0 -0
  32. {usfmtc-0.2.2 → usfmtc-0.2.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: usfmtc
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: USFM reference parser
5
5
  Author-email: USFM Technical Committee <fonts@sil.org>
6
6
  License: Copyright © 2022-2023 by the members of the USFM/USX technical committee, listed below:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "usfmtc"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  authors = [{name = "USFM Technical Committee", email = "fonts@sil.org"}]
9
9
  readme = "python/README.md"
10
10
  license = {file = "LICENSE.md"}
@@ -58,15 +58,15 @@ def _usfmGrammar(rdoc, backend=None, start=None):
58
58
 
59
59
  def usfmGrammar(gsrc, extensions=[], altparser=True, backend=None, start=None):
60
60
  """ Create UsfmGrammarParser from gsrc as used by USX.fromUsfm """
61
- if len(extensions) or not altparser:
62
- rdoc = _grammarDoc(gsrc, extensions)
63
61
  if altparser:
62
+ rdoc = _grammarDoc(gsrc, extensions)
63
+ return _usfmGrammar(rdoc, backend, start)
64
+ else:
64
65
  res = Grammar()
65
66
  if len(extensions):
66
67
  for e in extensions:
67
68
  res.readmrkrs(e)
68
69
  return res
69
- return _usfmGrammar(rdoc, backend, start)
70
70
 
71
71
 
72
72
  _filetypes = {".xml": "usx", ".usx": "usx", ".usfm": "usfm", ".sfm": "usfm3.0", ".json": "usj"}
@@ -129,10 +129,12 @@ class USX:
129
129
  @classmethod
130
130
  def fromUsfm(cls, src, grammar, altparser=True, elfactory=None, timeout=1e7):
131
131
  """ Parses USFM using UsfmGrammarParser grammar and creates USX object.
132
- Raise usfmtc.parser.NoParseError on error. """
132
+ Raise usfmtc.parser.NoParseError on error.
133
+ elfactory must take parent and pos named parameters not as attributes
134
+ """
133
135
  data = _readsrc(src)
134
136
 
135
- if altparser:
137
+ if not altparser:
136
138
  p = USFMParser(data, factory=elfactory or ParentElement, grammar=grammar)
137
139
  xml = p.parse()
138
140
  else:
@@ -171,7 +173,7 @@ class USX:
171
173
  fn(file, dat)
172
174
  return True
173
175
 
174
- def outUsx(self, file=None):
176
+ def outUsx(self, file=None, **kw):
175
177
  """ Output pretty XML USX. If file is None returns string """
176
178
  if self.xml is None:
177
179
  return None
@@ -181,7 +183,7 @@ class USX:
181
183
  def outUsfm(self, grammar, file=None, altparser=True, **kw):
182
184
  """ Output USFM from USX object. grammar is et doc. If file is None returns string """
183
185
  el = messup(self.xml)
184
- if altparser:
186
+ if not altparser:
185
187
  return self._outwrite(file, el, fn=usx2usfm)
186
188
  parser = USXConverter(grammar.getroot(), **kw)
187
189
  res = parser.parse(el)
@@ -190,16 +192,17 @@ class USX:
190
192
  return self._outwrite(file, dat)
191
193
  return False
192
194
 
193
- def outUsj(self, file=None):
195
+ def outUsj(self, file=None, ensure_ascii=False, **kw):
194
196
  """ Output USJ from USX object. If file is None returns dict """
195
197
  res = usxtousj(self.xml)
196
198
  if file is None:
197
199
  return res
198
200
  else:
199
- dat = json.dumps(res, indent=2)
201
+ dat = json.dumps(res, indent=2, ensure_ascii=ensure_ascii)
200
202
  self._outwrite(file, dat)
201
203
 
202
- def saveAs(self, outfpath, outformat=None, addesids=False, grammar=None, gramfile=None, version=None, altparser=True):
204
+ def saveAs(self, outfpath, outformat=None, addesids=False, grammar=None,
205
+ gramfile=None, version=None, altparser=True, **kw):
203
206
  """ Saves the document to a file in the appropriate format, either given
204
207
  or inferred from the filename extension. """
205
208
  if outformat is None:
@@ -213,11 +216,11 @@ class USX:
213
216
  if outtype == "usx":
214
217
  if addesids:
215
218
  usxdoc.addesids()
216
- self.outUsx(outfpath)
219
+ self.outUsx(outfpath, **kw)
217
220
  elif outtype == "usj":
218
- self.outUsj(outfpath)
221
+ self.outUsj(outfpath, **kw)
219
222
  elif outtype == "usfm":
220
- if grammar is None and not altparser:
223
+ if grammar is None and altparser:
221
224
  if gramfile is None:
222
225
  for a in ([], ['..', '..', '..', 'grammar']):
223
226
  gramfile = os.path.join(os.path.dirname(__file__), *a, "usx.rng")
@@ -228,7 +231,7 @@ class USX:
228
231
  outtype = "usfm"
229
232
  if version is None:
230
233
  version = "3.0"
231
- self.outUsfm(grammar, outfpath, outversion=version, altparser=altparser)
234
+ self.outUsfm(grammar, outfpath, outversion=version, altparser=altparser, **kw)
232
235
 
233
236
  def canonicalise(self):
234
237
  canonicalise(self.getroot())
@@ -260,11 +263,14 @@ def main(hookcli=None, hookusx=None):
260
263
  parser.add_argument("-F","--outformat",help="Output format [usfm, usx, usj, usfm3.0]")
261
264
  parser.add_argument("-I","--informat",help="Input format [usfm, usx, usj]")
262
265
  parser.add_argument("-g","--grammar",help="Grammar file to use, if needed")
263
- parser.add_argument("-e","--esids",action="store_true",help="Add esids, vids, sids, etc. to USX output")
266
+ parser.add_argument("-e","--esids",action="store_true",
267
+ help="Add esids, vids, sids, etc. to USX output")
264
268
  parser.add_argument("-v","--version",default=None,help="Set USFM version [3.1]")
265
- parser.add_argument("-x","--extfiles",action="append",default=[],help="markers.ext files to include")
266
- parser.add_argument("--qusfm",action="store_true",help="Use quick nonvalidating USFM parser")
269
+ parser.add_argument("-x","--extfiles",action="append",default=[],
270
+ help="markers.ext files to include")
271
+ parser.add_argument("-V","--validate",action="store_true",help="Use validating parsers for USFM")
267
272
  parser.add_argument("-C","--canonical",action="store_true",help="Do not canonicalise")
273
+ parser.add_argument("-A","--ascii",action="store_true",help="Output as ASCII only in json")
268
274
  parser.add_argument("-l","--logging",help="Set logging level to usfmxtest.log")
269
275
  parser.add_argument("-q","--quiet",action="store_true",help="Don't say much")
270
276
  parser.add_argument("--nooutput",action="store_true",help="Don't output any data")
@@ -326,16 +332,16 @@ def main(hookcli=None, hookusx=None):
326
332
  if args.informat.startswith("usfm"):
327
333
  args.extfiles.append(os.path.join(os.path.dirname(infiles[0]), "markers.ext"))
328
334
  exts = [x for x in args.extfiles if os.path.exists(x)]
329
- ingrammar = usfmGrammar(args.grammar, altparser=args.qusfm, extensions=exts)
335
+ ingrammar = usfmGrammar(args.grammar, altparser=args.validate, extensions=exts)
330
336
  if args.outformat and args.outformat.startswith("usfm"):
331
337
  outgrammar = _grammarDoc(args.grammar)
332
338
 
333
339
  for infile in infiles:
334
340
  outfile = None
335
341
  if args.outfile is None:
336
- outfile = _makeoutfile(infile, args.format)
342
+ outfile = _makeoutfile(infile, args.outformat)
337
343
  elif os.path.isdir(args.outfile):
338
- outf = _makeoutfile(infile, args.format)
344
+ outf = _makeoutfile(infile, args.outformat)
339
345
  if outf is None:
340
346
  doerror(f"invalid output format {args.format} in {args.outfile}")
341
347
  outfile = os.path.join(args.outfile, os.path.basename(outf))
@@ -351,7 +357,8 @@ def main(hookcli=None, hookusx=None):
351
357
  if not args.quiet:
352
358
  print(f"{infile} -> {outfile}" if outfile else f"{infile}")
353
359
  try:
354
- usxdoc = readFile(infile, informat=args.informat, grammar=ingrammar, altparser=args.qusfm)
360
+ usxdoc = readFile(infile, informat=args.informat, grammar=ingrammar,
361
+ altparser=args.validate)
355
362
  except NoParseError as e:
356
363
  doerror(f"Failed to parse {infile}: {e}", False)
357
364
 
@@ -373,7 +380,8 @@ def main(hookcli=None, hookusx=None):
373
380
  if not args.canonical:
374
381
  usxdoc.canonicalise()
375
382
 
376
- usxdoc.saveAs(outfile, outformat=args.outformat, addesids=args.esids, grammar=outgrammar, altparser=args.qusfm)
383
+ usxdoc.saveAs(outfile, outformat=args.outformat, addesids=args.esids,
384
+ grammar=outgrammar, altparser=args.validate, ensure_ascii=args.ascii)
377
385
 
378
386
  if __name__ == "__main__":
379
387
  main()
@@ -58,11 +58,11 @@ class Extensions(SFMFile):
58
58
 
59
59
  def applyto(self, rdoc, factory=et):
60
60
  res = False # nothing added
61
- allcats = set([x.get('category', None) for x in self.markers.values()])
61
+ allcats = set([x.get('category', "").split()[0] for x in self.markers.values()])
62
62
  for a in allcats:
63
63
  if a not in categories:
64
64
  continue
65
- ms = [k for k, v in self.markers.items() if v.get('category', None) == a]
65
+ ms = [k for k, v in self.markers.items() if a in v.get('category', "").split()]
66
66
  enum = categories[a]+".style.enum"
67
67
  e = rdoc.find('./{0}define[@name="{1}"]/{0}choice'.format(relaxns, enum))
68
68
  allmks = set([v.text for v in e.findall(f'./{relaxns}value')])
@@ -5,8 +5,15 @@ import xml.etree.ElementTree as et
5
5
  from usfmtc.extension import SFMFile
6
6
  from collections import UserDict, UserString
7
7
 
8
+
9
+ class Pos:
10
+ def __init__(self, l, c, **kw):
11
+ self.l = l
12
+ self.c = c
13
+ self.kw = kw
14
+
8
15
  class Tag(str):
9
- def __new__(cls, s):
16
+ def __new__(cls, s, l=0, c=0, **kw):
10
17
  isend = False
11
18
  isplus = False
12
19
  if s.startswith("+"):
@@ -16,6 +23,7 @@ class Tag(str):
16
23
  isend = True
17
24
  s = s[:-1]
18
25
  res = super().__new__(cls, s)
26
+ res.kw = kw
19
27
  res.isplus = isplus
20
28
  res.isend = isend
21
29
  return res
@@ -23,7 +31,7 @@ class Tag(str):
23
31
  def __init__(self, s, l=0, c=0):
24
32
  self.isplus = getattr(self, 'isplus', False)
25
33
  self.isend = getattr(self, 'isend', False)
26
- self.pos = (l, c)
34
+ self.pos = Pos(l, c)
27
35
 
28
36
  def __repr__(self):
29
37
  return "Tag("+str(self)+")"
@@ -41,14 +49,16 @@ class Tag(str):
41
49
  self.pos = pos
42
50
 
43
51
  class AttribText(UserString):
44
- def __init__(self, s, l=0, c=0):
52
+ def __init__(self, s, l=0, c=0, **kw):
45
53
  super().__init__(s)
46
- self.pos = (l, c)
54
+ self.pos = Pos(l, c)
55
+ self.kw = kw
47
56
  pass
48
57
 
49
58
  class OptBreak:
50
- def __init__(self, l=0, c=0):
51
- self.pos = (l, c)
59
+ def __init__(self, l=0, c=0, **kw):
60
+ self.pos = Pos(l, c)
61
+ self.kw = kw
52
62
 
53
63
  def __repr__(self):
54
64
  return "OptBreak(" + str(self) + ")"
@@ -56,9 +66,10 @@ class OptBreak:
56
66
  return "//"
57
67
 
58
68
  class String(UserString):
59
- def __init__(self, s, l=0, c=0):
69
+ def __init__(self, s, l=0, c=0, **kw):
60
70
  super().__init__(s)
61
- self.pos = (l, c)
71
+ self.pos = Pos(l, c)
72
+ self.kw = kw
62
73
 
63
74
  def __add__(self, s):
64
75
  return String(str(self) + s)
@@ -66,25 +77,28 @@ class String(UserString):
66
77
  pass
67
78
 
68
79
  class Attribs(UserDict):
69
- def __init__(self, l=0, c=0):
80
+ def __init__(self, l=0, c=0, **kw):
70
81
  super().__init__()
71
- self.pos = (l, c)
82
+ self.pos = Pos(l, c)
83
+ self.kw = kw
72
84
 
73
85
  class Lexer:
74
86
 
75
87
  tokenre = regex.compile(r'((?:[^\\|/]|\\[\\|\n]|/[^/])+)|([\\|\n]|//)')
76
- tagre = regex.compile(r'(?:\+?[a-zA-Z_][a-zA-Z_0-9-]*)?\*?')
88
+ tagre = regex.compile(r'(?:\+?[a-zA-Z_][a-zA-Z_0-9^-]*)?\*?')
77
89
  attribsre = regex.compile(r'\s*([a-zA-Z_][a-zA-Z0-9_-]*)\s*=\s*"((?:\\"|[^"])*)"')
78
90
  textrunre = regex.compile(r'(?:[^\\]|\\[^a-zA-Z_*+])*')
79
91
 
80
- def __init__(self, txt):
92
+ def __init__(self, txt, expanded=False):
81
93
  self.txt = txt
94
+ self.expanded = expanded
82
95
 
83
96
  def __iter__(self):
84
97
  self.nexts = []
85
98
  self.cindex = 0
86
99
  self.lindex = 0
87
100
  self.lpos = 0
101
+ self.currxpand = None
88
102
  return self
89
103
 
90
104
  def __next__(self):
@@ -113,7 +127,9 @@ class Lexer:
113
127
  curri = m.end() + 1
114
128
  continue
115
129
  else:
116
- res = Tag(t.group(0))
130
+ tagname = self.processtag(t.group(0))
131
+ extras = {"xp": self.currxpand} if self.expanded else {}
132
+ res = Tag(tagname, l=self.lindex, c=curri-self.lpos, **extras)
117
133
  curri += t.end()
118
134
  break
119
135
  elif n == '|':
@@ -155,6 +171,13 @@ class Lexer:
155
171
  self.cindex = m.end()
156
172
  self.nexts.append(String(m.group(1)))
157
173
 
174
+ def processtag(self, t):
175
+ i = t.find("^")
176
+ if self.expanded and i >= 0:
177
+ self.currxpand = t[i+1:]
178
+ return t[:i]
179
+ return t
180
+
158
181
  class Grammar:
159
182
  category_markers = {
160
183
  "attrib": "cp vp usfm ca va cat",
@@ -178,8 +201,14 @@ class Grammar:
178
201
  "title": "mt1 mt2 mt3 mt4 mt",
179
202
  "versepara": "cls nb pc pi1 pi2 pi3 pi po pr pmo pmc pmr pm ph1 ph2 ph3 ph p q1 q2 q3 q4 qc qr qm1 qm2 qm3 qm qd q b d mi1 mi2 mi3 mi4 mi m",
180
203
  }
204
+ category_tags = {
205
+ "char": ("char", "crossreferencechar", "footnotechar", "introchar", "listchar"),
206
+ "para": ("header", "introduction", "list", "otherpara", "sectionpara", "title", "versepara"),
207
+ "ms": ("milestone", )
208
+ }
181
209
 
182
- marker_categories = {t:k for k, v in category_markers.items() for t in v.split(' ')}
210
+ marker_categories = {t:k for k, v in category_markers.items() for t in v.split()}
211
+ marker_tags = {t:k for k, v in category_tags.items() for t in v}
183
212
 
184
213
  attribmap = { 'jmp' : 'href', 'k' : 'key', 'qt-s': 'who', 'qt1-s': 'who', 'qt2-s': 'who',
185
214
  'qt3-s': 'who', 'qt4-s': 'who', 'qt5-s': 'who', 'rb': 'gloss', 't-s': 'sid', 'ts-s': 'sid',
@@ -211,13 +240,15 @@ def isfirstText(e):
211
240
  return True
212
241
 
213
242
  class Node:
214
- def __init__(self, parser, usxtag, tag, ispara=False, notag=False):
243
+ def __init__(self, parser, usxtag, tag, ispara=False, notag=False, pos=None, **kw):
215
244
  self.parser = parser
216
245
  self.tag = tag
217
246
  self.ispara = ispara
218
247
  parent = parser.stack[-1] if len(parser.stack) else None
219
- self.element = parser.factory(usxtag, {} if notag or tag is None else {"style": tag},
220
- parent=getattr(parent, 'element', None))
248
+ attribs = kw
249
+ if not notag and tag is not None:
250
+ attribs['style'] = tag
251
+ self.element = parser.factory(usxtag, attribs, parent=getattr(parent, 'element', None), pos=pos)
221
252
  if parent:
222
253
  parent.addNodeElement(self.element)
223
254
  self.attribnodes = []
@@ -227,7 +258,7 @@ class Node:
227
258
 
228
259
  def appendElement(self, child):
229
260
  if isinstance(child, OptBreak):
230
- c = self.parser.factory("optbreak", {}, parent=self.element)
261
+ c = self.parser.factory("optbreak", {}, parent=self.element, pos=child.pos)
231
262
  self.element.append(c)
232
263
  self.clearAttribNodes()
233
264
 
@@ -297,11 +328,12 @@ class USXNode(Node):
297
328
  self.element.set('version', str(t).strip())
298
329
 
299
330
  class AttribNode(Node):
300
- def __init__(self, parser, parent, tag):
331
+ def __init__(self, parser, parent, tag, pos=None, **kw):
301
332
  self.parser = parser
302
333
  self.parent = parent
303
334
  self.parent.addAttribNode(self)
304
335
  self.tag = tag
336
+ self.pos = pos
305
337
  self.attribnodes = []
306
338
 
307
339
  def addNodeElement(self, e):
@@ -313,8 +345,8 @@ class AttribNode(Node):
313
345
  self.parent.element.set(attrib, str(t).strip())
314
346
 
315
347
  class NumberNode(Node):
316
- def __init__(self, parser, usxtag, tag, ispara=False):
317
- super().__init__(parser, usxtag, tag, ispara=ispara)
348
+ def __init__(self, parser, usxtag, tag, ispara=False, pos=None, **kw):
349
+ super().__init__(parser, usxtag, tag, ispara=ispara, pos=pos, **kw)
318
350
  self.hasarg = False
319
351
 
320
352
  def appendText(self, t):
@@ -327,7 +359,7 @@ class NumberNode(Node):
327
359
  if len(b) > 1 and b[1].strip():
328
360
  self.parser.removeTag(self.tag)
329
361
  if self.ispara:
330
- self.parser.addNode(Node(self.parser, 'para', 'p'))
362
+ self.parser.addNode(Node(self.parser, 'para', 'p', pos=self.element.pos))
331
363
  self.parser.stack[-1].appendText(b[1])
332
364
 
333
365
  def addNodeElement(self, e):
@@ -335,8 +367,8 @@ class NumberNode(Node):
335
367
  self.parser.stack[-1].addNodeElement(e)
336
368
 
337
369
  class NoteNode(Node):
338
- def __init__(self, parser, usxtag, tag):
339
- super().__init__(parser, usxtag, tag)
370
+ def __init__(self, parser, usxtag, tag, pos=None, **kw):
371
+ super().__init__(parser, usxtag, tag, pos=pos, **kw)
340
372
  self.hascaller = False
341
373
 
342
374
  def appendText(self, t):
@@ -358,38 +390,51 @@ class PeriphNode(Node):
358
390
  if t:
359
391
  self.element.set('alt', str(t).strip())
360
392
 
393
+ class UnknownNode(Node):
394
+ pass
395
+
361
396
 
362
397
  paratypes = ('header', 'introduction', 'list', 'otherpara', 'sectionpara', 'versepara', 'title', 'chapter', 'ident')
363
398
  paratags = ('rem', ' table')
364
- def setupParser(cls):
365
- # tag closed types
366
- for a in (('introchar', 'char'), ('listchar', 'char'), ('char', 'char'), ('_fig', 'figure')):
367
- def maketype(c, t):
368
- def dotype(self, tag):
369
- if tag.isend:
370
- return self.removeTag(str(tag))
371
- return self.addNode(Node(self, t, tag.basestr()))
372
- return dotype
373
- setattr(cls, a[0], maketype(*a))
374
- # implicit closed paras
375
- for a in paratypes:
376
- def dotype(self, tag):
377
- self.removeType(paratypes, paratags)
378
- return self.addNode(Node(self, 'para', str(tag), ispara=True))
379
- if not hasattr(cls, a):
380
- setattr(cls, a, dotype)
381
-
382
399
 
383
400
  class USFMParser:
384
401
 
385
- def __init__(self, txt, factory=None, grammar=None):
402
+ def __init__(self, txt, factory=None, grammar=None, expanded=False, strict=False):
386
403
  if factory is None:
387
- factory = et.Element
404
+ def makeel(tag, attrib, **extras):
405
+ attrib.update({" "+k:v for k, v in extras.items()})
406
+ return et.Element(tag, attrib)
407
+ factory = makeel
388
408
  if grammar is None:
389
409
  grammar = Grammar()
390
410
  self.factory = factory
391
411
  self.grammar = grammar
392
- self.lexer = Lexer(txt)
412
+ self._setup(expanded=expanded)
413
+ self.lexer = Lexer(txt, expanded=expanded)
414
+
415
+ def _setup(s, expanded=False):
416
+ clsself = s.__class__
417
+ # tag closed types
418
+ for a in (('introchar', 'char'), ('listchar', 'char'), ('char', 'char'), ('_fig', 'figure')):
419
+ def maketype(c, t):
420
+ def dotype(self, tag):
421
+ if tag.isend:
422
+ return self.removeTag(str(tag))
423
+ return self.addNode(Node(self, t, tag.basestr()))
424
+ return dotype
425
+ setattr(clsself, a[-1], maketype(*a))
426
+ # implicit closed paras
427
+ for a in paratypes:
428
+ if expanded:
429
+ def dotype(self, tag):
430
+ self.removeType(paratypes, paratags)
431
+ return self.addNode(Node(self, 'para', str(tag), ispara=True, xpand=tag.xp))
432
+ else:
433
+ def dotype(self, tag):
434
+ self.removeType(paratypes, paratags)
435
+ return self.addNode(Node(self, 'para', str(tag), ispara=True))
436
+ if not hasattr(clsself, a):
437
+ setattr(clsself, a, dotype)
393
438
 
394
439
  def parse(self):
395
440
  self.result = []
@@ -439,6 +484,8 @@ class USFMParser:
439
484
  while len(self.stack):
440
485
  curr = self.stack.pop()
441
486
  if curr.tag == tag:
487
+ if curr.element.tag == "unk":
488
+ curr.element.tag = "char"
442
489
  break
443
490
  else:
444
491
  self.stack = oldstack
@@ -450,12 +497,31 @@ class USFMParser:
450
497
  oldstack = self.stack[:]
451
498
  while len(self.stack):
452
499
  curr = self.stack.pop()
500
+ e = curr.element
501
+ if e.tag == "usx":
502
+ self.stack.append(curr)
503
+ break
453
504
  curr.close()
454
- if curr is not None and (curr.tag in tags or self.grammar.marker_categories.get(curr.tag, None) in t):
505
+ cat = self.grammar.marker_categories.get(e.tag, None)
506
+ if e.tag == "unk":
507
+ e.tag = self.grammar.marker_tags.get(t[0], "para")
508
+ if e.tag == 'para' and e.parent is not None:
509
+ parent = e.parent
510
+ while parent is not None and parent.tag not in ("usx", "para"):
511
+ parent = parent.parent
512
+ if parent.tag != "usx":
513
+ i, p = parent._getindex()
514
+ if p is not None:
515
+ e.parent.remove(e)
516
+ p.insert(i+1, e)
517
+ e.parent = p
518
+ self.removeType(t, tags=tags)
519
+ break
520
+ if curr.tag in tags or cat in t:
455
521
  break
456
522
  else:
457
523
  self.stack = oldstack
458
- return self.stack[-1]
524
+ return self.stack[-1] if len(self.stack) else None
459
525
 
460
526
  #### Event methods
461
527
 
@@ -467,31 +533,31 @@ class USFMParser:
467
533
  if tag.isend:
468
534
  return self.removeTag(str(tag))
469
535
  elif self.stack[-1].tag == "c":
470
- parent = AttribNode(self, self.stack[-1], str(tag))
536
+ parent = AttribNode(self, self.stack[-1], str(tag), pos=tag.pos)
471
537
  else:
472
- parent = Node(self, 'para', str(tag), ispara=True)
538
+ parent = Node(self, 'para', str(tag), ispara=True, pos=tag.pos)
473
539
  self.stack.append(parent)
474
540
  return parent
475
541
 
476
542
  def _esb(self, tag):
477
543
  self.removeType(paratypes, paratags)
478
- return self.addNode(Node(self, "sidebar", str(tag)))
544
+ return self.addNode(Node(self, "sidebar", str(tag), pos=tag.pos))
479
545
 
480
546
  def _esbe(self, tag):
481
547
  return self.removeTag("esb")
482
548
 
483
549
  def _periph(self, tag):
484
550
  self.removeTag(str(tag))
485
- return self.addNode(PeriphNode(self, "periph", str(tag), notag=True))
551
+ return self.addNode(PeriphNode(self, "periph", str(tag), notag=True, pos=tag.pos))
486
552
 
487
553
  def _ref(self, tag):
488
554
  if tag.isend:
489
555
  return self.removeTag(str(tag))
490
- return self.addNode(Node(self, 'ref', tag.basestr(), notag=True))
556
+ return self.addNode(Node(self, 'ref', tag.basestr(), notag=True, pos=tag.pos))
491
557
 
492
558
  def _rem(self, tag):
493
559
  self.removeType(paratypes, paratags)
494
- res = self.addNode(Node(self, 'para', str(tag), ispara=True))
560
+ res = self.addNode(Node(self, 'para', str(tag), ispara=True, pos=tag.pos))
495
561
  self.lexer.readLine()
496
562
  return res
497
563
 
@@ -499,23 +565,23 @@ class USFMParser:
499
565
  self.removeTag('tr')
500
566
  if not len(self.stack) or self.stack[-1].element.tag != "table":
501
567
  self.removeType(paratypes,)
502
- self.addNode(Node(self, 'table', ' table', notag=True))
503
- return self.addNode(Node(self, 'row', 'tr'))
568
+ self.addNode(Node(self, 'table', ' table', notag=True, pos=tag.pos))
569
+ return self.addNode(Node(self, 'row', 'tr', pos=tag.pos))
504
570
 
505
571
  def _v(self, tag):
506
572
  self.removeTag('v')
507
573
  if len(self.stack) and self.stack[-1].tag == "c":
508
574
  self.removeTag('c')
509
- self.addNode(Node(self, 'para', 'p'))
510
- return self.addNode(NumberNode(self, "verse", str(tag)))
575
+ self.addNode(Node(self, 'para', 'p', pos=tag.pos))
576
+ return self.addNode(NumberNode(self, "verse", str(tag), pos=tag.pos))
511
577
 
512
578
  def _vp(self, tag):
513
579
  if tag.isend:
514
580
  return self.removeTag(str(tag))
515
581
  elif self.stack[-1].tag == "v":
516
- parent = AttribNode(self, self.stack[-1], str(tag))
582
+ parent = AttribNode(self, self.stack[-1], str(tag), pos=tag.pos)
517
583
  else:
518
- parent = Node(self, 'para', tag.basestr())
584
+ parent = Node(self, 'para', tag.basestr(), pos=tag.pos)
519
585
  self.stack.append(parent)
520
586
  return parent
521
587
 
@@ -523,43 +589,43 @@ class USFMParser:
523
589
  if self.grammar.marker_categories.get(self.stack[-1].tag, "") == "crossreferencechar":
524
590
  res = self.removeType('crossreferencechar')
525
591
  if not tag.isend:
526
- res = self.addNode(Node(self, 'char', tag.basestr()))
592
+ res = self.addNode(Node(self, 'char', tag.basestr(), pos=tag.pos))
527
593
  return res
528
594
 
529
595
  def attrib(self, tag):
530
596
  if tag.isend:
531
597
  return self.removeTag(str(tag))
532
598
  else:
533
- parent = AttribNode(self, self.stack[-1], str(tag))
599
+ parent = AttribNode(self, self.stack[-1], str(tag), pos=tag.pos)
534
600
  self.stack.append(parent)
535
601
  return parent
536
602
 
537
603
  def cell(self, tag):
538
604
  self.removeType('cell')
539
605
  if not tag.isend:
540
- return self.addNode(Node(self, 'cell', str(tag)))
606
+ return self.addNode(Node(self, 'cell', str(tag), pos=tag.pos))
541
607
  return self.parent
542
608
 
543
609
  def crossreference(self, tag):
544
610
  if tag.isend:
545
611
  return self.removeTag(str(tag))
546
- return self.addNode(NoteNode(self, 'note', str(tag)))
612
+ return self.addNode(NoteNode(self, 'note', str(tag), pos=tag.pos))
547
613
 
548
614
  def crossreferencechar(self, tag):
549
615
  res = self.removeType('crossreferencechar')
550
616
  if not tag.isend:
551
- res = self.addNode(Node(self, 'char', tag.basestr()))
617
+ res = self.addNode(Node(self, 'char', tag.basestr(), pos=tag.pos))
552
618
  return res
553
619
 
554
620
  def footnote(self, tag):
555
621
  if tag.isend:
556
622
  return self.removeTag(str(tag))
557
- return self.addNode(NoteNode(self, 'note', str(tag)))
623
+ return self.addNode(NoteNode(self, 'note', str(tag), pos=tag.pos))
558
624
 
559
625
  def footnotechar(self, tag):
560
626
  res = self.removeType('footnotechar')
561
627
  if not tag.isend:
562
- res = self.addNode(Node(self, 'char', tag.basestr()))
628
+ res = self.addNode(Node(self, 'char', tag.basestr(), pos=tag.pos))
563
629
  return res
564
630
 
565
631
  def ident(self, tag):
@@ -570,15 +636,22 @@ class USFMParser:
570
636
  def milestone(self, tag):
571
637
  if tag.isend:
572
638
  return self.removeType('milestone')
573
- return self.addNode(Node(self, 'ms', tag.basestr()))
639
+ return self.addNode(Node(self, 'ms', tag.basestr(), pos=tag.pos))
574
640
 
575
641
  def unknown(self, tag):
576
- self.addNode(Node(self, 'ms', str(tag)))
577
- res = self.removeTag(tag)
642
+ if strict:
643
+ if len(self.stack):
644
+ raise SyntaxError(f"Unknown tag {tag} in {self.stack[-1]}")
645
+ else:
646
+ raise SyntaxError(f"Unknown tag {tag}")
647
+ return None
648
+ if not tag.isend:
649
+ res = self.addNode(UnknownNode(self, 'unk', str(tag), pos=tag.pos))
650
+ else:
651
+ res = self.removeTag(str(tag))
652
+ res.tag = "char"
578
653
  return res
579
654
 
580
- setupParser(USFMParser)
581
-
582
655
  def main():
583
656
  import sys
584
657
  p = USFMParser(sys.argv[1])
@@ -3,12 +3,13 @@ import re
3
3
  import xml.etree.ElementTree as et
4
4
 
5
5
  class ParentElement(et.Element):
6
- def __init__(self, tag, attrib=None, parent=None):
6
+ def __init__(self, tag, attrib=None, parent=None, pos=None):
7
7
  et.Element.__init__(self, tag, attrib)
8
8
  self.parent = parent
9
+ self.pos = pos
9
10
 
10
- def makeelement(self, tag, attrib):
11
- return self.__class__(tag, attrib, parent=self)
11
+ def makeelement(self, tag, attrib, pos=None):
12
+ return self.__class__(tag, attrib, parent=self, pos=pos or self.pos)
12
13
 
13
14
  def __str__(self):
14
15
  return "{}[{}]".format(self.tag, " ".join('@{}="{}"'.format(k, v) for k, v in self.attrib.items()))
@@ -18,19 +19,24 @@ class ParentElement(et.Element):
18
19
  return "{}/{}".format(p, str(self))
19
20
 
20
21
  def _getindex(self):
22
+ ''' Finds my index in my parent, returning both '''
21
23
  if self.parent is None:
22
24
  return -1, None
23
25
  return list(self.parent).index(self), self.parent
24
26
 
25
27
  def getprevious(self):
28
+ ''' Returns the previous element with the same parent, if any '''
26
29
  i, parent = self._getindex()
27
30
  return parent[i-1] if parent is not None and i > 0 else None
28
31
 
29
32
  def getnext(self):
33
+ ''' Returns the next element with the same parent, if any '''
30
34
  i, parent = self._getindex()
31
35
  return parent[i+1] if parent is not None and i < len(parent) - 1 else None
32
36
 
33
37
  def getnext_sibling(self):
38
+ ''' Returns the next element, while going up and down the hierarchy.
39
+ Notice that any text at the start of an element stops any hierarchy descent '''
34
40
  i, parent = self._getindex()
35
41
  if i < len(parent) - 1:
36
42
  return parent[i+1]
@@ -41,6 +47,8 @@ class ParentElement(et.Element):
41
47
  return parent
42
48
 
43
49
  def getprevious_sibling(self):
50
+ ''' Returns the previous element, while going up and down the hierarchy
51
+ Notice that any text after an element stops any hierarchy descent '''
44
52
  i, parent = self._getindex()
45
53
  if i > 0:
46
54
  return parent[i-1]
@@ -51,11 +59,13 @@ class ParentElement(et.Element):
51
59
  return parent
52
60
 
53
61
  def addprevious(self, el):
62
+ ''' Inserts el before the current element in the same parent '''
54
63
  i, parent = self._getindex()
55
64
  if parent is not None:
56
65
  parent.insert(i, el)
57
66
 
58
67
  def getparent(self):
68
+ ''' Returns the parent '''
59
69
  return self.parent
60
70
 
61
71
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: usfmtc
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: USFM reference parser
5
5
  Author-email: USFM Technical Committee <fonts@sil.org>
6
6
  License: Copyright © 2022-2023 by the members of the USFM/USX technical committee, listed below:
File without changes
File without changes
File without changes
File without changes
File without changes