usfmtc 0.2__py3-none-any.whl

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.
usfmtc/__init__.py ADDED
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # nuitka configuration
4
+ # nuitka-project: --onefile
5
+ # nuitka-project: --include-data-files={MAIN_DIRECTORY}/usx.rng=usx.rng
6
+ # nuitka-project-if: {OS} in ("Windows",):
7
+ # nuitka-project: --output-filename=usfmconv.exe
8
+ # nuitka-project-else:
9
+ # nuitka-project: --output-filename=usfmconv.bin
10
+
11
+ import os
12
+ from usfmtc.sfmparser import parseusfm, UsfmParserBackend
13
+ from usfmtc.parser import NoParseError
14
+ from usfmtc.extension import Extensions
15
+ from usfmtc.xmlutils import ParentElement, prettyxml, writexml
16
+ from usfmtc.usxparser import USXConverter
17
+ from usfmtc.grammar import UsfmGrammarParser
18
+ from usfmtc.diagrams import UsfmRailRoad
19
+ from usfmtc.usxmodel import addesids, cleanup, messup, canonicalise
20
+ from usfmtc.usjproc import usxtousj, usjtousx
21
+ from usfmtc.usfmparser import USFMParser, Grammar
22
+ from usfmtc.usfmgenerate import usx2usfm
23
+ import xml.etree.ElementTree as et
24
+
25
+ def _readsrc(src):
26
+ if hasattr(src, "read"):
27
+ return src.read()
28
+ elif not isinstance(src, str): # we're a parsed xml doc
29
+ return src
30
+ elif len(src) < 128 and os.path.exists(src):
31
+ with open(src, encoding="utf-8") as inf:
32
+ data = inf.read()
33
+ return data
34
+ elif "\n" in src or len(src) > 127:
35
+ return src
36
+ else:
37
+ raise FileNotFoundError(src)
38
+
39
+ def _grammarDoc(gsrc, extensions=[], factory=et):
40
+ data = _readsrc(gsrc)
41
+ if isinstance(data, (str, bytes)):
42
+ rdoc = factory.ElementTree(factory.fromstring(data))
43
+ else:
44
+ rdoc = data
45
+ for ef in extensions:
46
+ e = Extensions(ef)
47
+ dirty = e.applyto(rdoc, factory=factory)
48
+ return rdoc
49
+
50
+ def _usfmGrammar(rdoc, backend=None, start=None):
51
+ if backend is None:
52
+ backend = UsfmParserBackend()
53
+ sfmproc = UsfmGrammarParser(rdoc, backend)
54
+ if start is None:
55
+ start = "Scripture"
56
+ parser = sfmproc.parseRef(start)
57
+ return parser
58
+
59
+ def usfmGrammar(gsrc, extensions=[], altparser=True, backend=None, start=None):
60
+ """ Create UsfmGrammarParser from gsrc as used by USX.fromUsfm """
61
+ if len(extensions) or not altparser:
62
+ rdoc = _grammarDoc(gsrc, extensions)
63
+ if altparser:
64
+ res = Grammar()
65
+ if len(extensions):
66
+ for e in extensions:
67
+ res.readmrkrs(e)
68
+ return res
69
+ return _usfmGrammar(rdoc, backend, start)
70
+
71
+
72
+ _filetypes = {".xml": "usx", ".usx": "usx", ".usfm": "usfm", ".sfm": "usfm3.0", ".json": "usj"}
73
+
74
+ def readFile(infpath, informat=None, gramfile=None, grammar=None, extfiles=[], altparser=True):
75
+ """ Reads a USFM file of a given type or inferred from the filename
76
+ extension. extfiles allows for extra markers.ext files to extend the grammar"""
77
+ if informat is None:
78
+ inroot, ext = os.path.splitext(infpath)
79
+ intype = _filetypes.get(ext.lower(), informat)
80
+ else:
81
+ intype = informat
82
+ if intype is None:
83
+ return None
84
+
85
+ if intype == "usx":
86
+ usxdoc = USX.fromUsx(infpath)
87
+ elif intype == "usj":
88
+ usxdoc = USX.fromUsj(infpath)
89
+ elif intype.startswith("usfm"):
90
+ if grammar is None:
91
+ if gramfile is None:
92
+ for a in ([], ['..', '..', '..', 'grammar']):
93
+ gramfile = os.path.join(os.path.dirname(__file__), *a, "usx.rng")
94
+ if os.path.exists(gramfile):
95
+ break
96
+ fname = getattr(infpath, 'name', infpath)
97
+ extfiles.append(os.path.join(os.path.dirname(fname), "markers.ext"))
98
+ exts = [x for x in extfiles if os.path.exists(x)]
99
+ grammar = usfmGrammar(gramfile, extensions=exts, altparser=altparser)
100
+ usxdoc = USX.fromUsfm(infpath, grammar, altparser=altparser)
101
+ return usxdoc
102
+
103
+
104
+ class USX:
105
+ @classmethod
106
+ def fromUsx(cls, src, elfactory=None):
107
+ """ Loads USX and creates USX object to hold it """
108
+ if elfactory is None:
109
+ elfactory = ParentElement
110
+ tb = et.TreeBuilder(element_factory=elfactory)
111
+ parser = et.XMLParser(target=tb)
112
+ if os.path.exists(src):
113
+ inf = open(src, encoding="utf_8_sig")
114
+ else:
115
+ inf = src
116
+ if hasattr(inf, "read"):
117
+ anet = et.ElementTree()
118
+ anet.parse(inf, parser=parser)
119
+ res = anet.getroot()
120
+ if src != inf:
121
+ inf.close()
122
+ else:
123
+ try:
124
+ res = et.fromstring(src, parser=parser)
125
+ except et.ParseError:
126
+ return None
127
+ return cls(res)
128
+
129
+ @classmethod
130
+ def fromUsfm(cls, src, grammar, altparser=True, elfactory=None, timeout=1e7):
131
+ """ Parses USFM using UsfmGrammarParser grammar and creates USX object.
132
+ Raise usfmtc.parser.NoParseError on error. """
133
+ data = _readsrc(src)
134
+
135
+ if altparser:
136
+ p = USFMParser(data, factory=elfactory or ParentElement, grammar=grammar)
137
+ xml = p.parse()
138
+ else:
139
+ # This can raise usfmtc.parser.NoParseError
140
+ result = parseusfm(data, grammar, timeout=timeout, isdata=True)
141
+ xml = result.asEt(elfactory=elfactory)
142
+
143
+ cleanup(xml) # normalize space, de-escape chars, cell aligns, etc.
144
+ res = cls(xml)
145
+ return res
146
+
147
+ @classmethod
148
+ def fromUsj(cls, src, elfactory=None):
149
+ data = _readsrc(src)
150
+ djson = json.loads(data)
151
+ xml = usjtousx(djson, elfactory=elfactory)
152
+ return cls(xml)
153
+
154
+ def __init__(self, xml):
155
+ self.xml = xml # an Element, not an ElementTree
156
+
157
+ def _outwrite(self, file, dat, fn=None):
158
+ if fn is None:
159
+ fn = lambda f, d: f.write(d)
160
+ if file is None:
161
+ fh = io.StringIO()
162
+ fn(fh, dat)
163
+ res = fh.getvalue()
164
+ fh.close()
165
+ return res
166
+ if not hasattr(file, "read"):
167
+ fh = open(file, "w", encoding="utf-8")
168
+ fn(fh, dat)
169
+ fh.close()
170
+ else:
171
+ fn(file, dat)
172
+ return True
173
+
174
+ def outUsx(self, file=None):
175
+ """ Output pretty XML USX. If file is None returns string """
176
+ if self.xml is None:
177
+ return None
178
+ prettyxml(self.xml)
179
+ self._outwrite(file, self.xml, fn=writexml)
180
+
181
+ def outUsfm(self, grammar, file=None, altparser=True, **kw):
182
+ """ Output USFM from USX object. grammar is et doc. If file is None returns string """
183
+ el = messup(self.xml)
184
+ if altparser:
185
+ return self._outwrite(file, el, fn=usx2usfm)
186
+ parser = USXConverter(grammar.getroot(), **kw)
187
+ res = parser.parse(el)
188
+ if res:
189
+ dat = "".join(res.results)
190
+ return self._outwrite(file, dat)
191
+ return False
192
+
193
+ def outUsj(self, file=None):
194
+ """ Output USJ from USX object. If file is None returns dict """
195
+ res, _ = usxtousj(self.xml.getroot())
196
+ if file is None:
197
+ return res
198
+ else:
199
+ dat = json.dumps(res, indent=2)
200
+ self._outwrite(file, dat)
201
+
202
+ def saveAs(self, outfpath, outformat=None, addesids=False, grammar=None, gramfile=None, version=None, altparser=True):
203
+ """ Saves the document to a file in the appropriate format, either given
204
+ or inferred from the filename extension. """
205
+ if outformat is None:
206
+ outroot, ext = os.path.splitext(outfpath)
207
+ outtype = _filetypes.get(ext.lower(), outformat)
208
+ else:
209
+ outtype = outformat
210
+ if outtype is None:
211
+ return
212
+
213
+ if outtype == "usx":
214
+ if addesids:
215
+ usxdoc.addesids()
216
+ self.outUsx(outfpath)
217
+ elif outtype == "usj":
218
+ self.outUsj(outfpath)
219
+ elif outtype == "usfm":
220
+ if grammar is None and not altparser:
221
+ if gramfile is None:
222
+ for a in ([], ['..', '..', '..', 'grammar']):
223
+ gramfile = os.path.join(os.path.dirname(__file__), *a, "usx.rng")
224
+ if os.path.exists(gramfile):
225
+ break
226
+ grammar = _grammarDoc(gramfile)
227
+ if outtype == "usfm3.0":
228
+ outtype = "usfm"
229
+ if version is None:
230
+ version = "3.0"
231
+ self.outUsfm(grammar, outfpath, outversion=version, altparser=altparser)
232
+
233
+ def canonicalise(self):
234
+ canonicalise(self.getroot())
235
+
236
+ def getroot(self):
237
+ """ Returns root XML element """
238
+ return self.xml
239
+
240
+ def addesids(self):
241
+ """ Add esids to USX object (eid, sids, vids) """
242
+ addesids(self.xml)
243
+
244
+ @property
245
+ def version(self):
246
+ return self.getroot().get('version', None)
247
+
248
+ @version.setter
249
+ def version(self, version):
250
+ self.getroot().set('version', str(version))
251
+
252
+ def main(hookcli=None, hookusx=None):
253
+
254
+ import argparse, logging, sys
255
+ from glob import glob
256
+
257
+ parser = argparse.ArgumentParser()
258
+ parser.add_argument("infile",nargs="+",help="Input file")
259
+ parser.add_argument("-o", "--outfile",help="Output file, with inferred format")
260
+ parser.add_argument("-F","--outformat",help="Output format [usfm, usx, usj, usfm3.0]")
261
+ parser.add_argument("-I","--informat",help="Input format [usfm, usx, usj]")
262
+ 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")
264
+ 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")
267
+ parser.add_argument("-C","--canonical",action="store_true",help="Do not canonicalise")
268
+ parser.add_argument("-l","--logging",help="Set logging level to usfmxtest.log")
269
+ parser.add_argument("-q","--quiet",action="store_true",help="Don't say much")
270
+ parser.add_argument("--nooutput",action="store_true",help="Don't output any data")
271
+ if hookcli is not None:
272
+ hookcli(parser)
273
+ args = parser.parse_args()
274
+
275
+ if args.logging:
276
+ try:
277
+ loglevel = int(args.logging)
278
+ except ValueError:
279
+ loglevel = getattr(logging, args.logging.upper(), None)
280
+ if isinstance(loglevel, int):
281
+ parms = {'level': loglevel, 'datefmt': '%d/%b/%Y %H:%M:%S',
282
+ 'format': '%(asctime)s.%(msecs)03d %(levelname)s:%(module)s(%(lineno)d) %(message)s'}
283
+ logfh = open("usfmconv.log", "w", encoding="utf-8")
284
+ parms.update(stream=logfh, filemode="w") #, encoding="utf-8")
285
+ try:
286
+ logging.basicConfig(**parms)
287
+ except FileNotFoundError as e: # no write access to the log
288
+ print("Exception", e)
289
+ log = logging.getLogger('usfmconv')
290
+ else:
291
+ log = None
292
+
293
+ def doerror(msg, doexit=True):
294
+ if log:
295
+ log.error(msg)
296
+ if not args.quiet:
297
+ print(msg)
298
+ if doexit:
299
+ sys.exit(1)
300
+
301
+ fileexts = {"usx": ".xml", "usfm": ".usfm", "usj": ".json"}
302
+
303
+ def _makeoutfile(infile, oformat):
304
+ outext = fileexts.get(oformat, None)
305
+ inroot, ext = os.path.splitext(infile)
306
+ return inroot + outext if outext else None
307
+
308
+ infiles = sum((glob(x) for x in args.infile), [])
309
+ if not len(infiles):
310
+ doerror("No files found in {args.infile}")
311
+
312
+ root, ext = os.path.splitext(infiles[0])
313
+ args.informat = args.informat or _filetypes.get(ext.lower(), args.informat)
314
+ if args.outformat is None:
315
+ if args.outfile is not None and not os.path.isdir(args.outfile):
316
+ root, ext = os.path.splitext(args.outfile)
317
+ args.outformat = _filetypes.get(ext.lower(), None)
318
+ ingrammar = None
319
+ outgrammar = None
320
+ if args.informat.startswith("usfm") or args.outformat.startswith("usfm"):
321
+ if args.grammar is None:
322
+ for a in ([], ['..', '..', '..', 'grammar']):
323
+ args.grammar = os.path.join(os.path.dirname(__file__), *a, "usx.rng")
324
+ if os.path.exists(args.grammar):
325
+ break
326
+ if args.informat.startswith("usfm"):
327
+ args.extfiles.append(os.path.join(os.path.dirname(infiles[0]), "markers.ext"))
328
+ exts = [x for x in args.extfiles if os.path.exists(x)]
329
+ ingrammar = usfmGrammar(args.grammar, altparser=args.qusfm, extensions=exts)
330
+ if args.outformat and args.outformat.startswith("usfm"):
331
+ outgrammar = _grammarDoc(args.grammar)
332
+
333
+ for infile in infiles:
334
+ outfile = None
335
+ if args.outfile is None:
336
+ outfile = _makeoutfile(infile, args.format)
337
+ elif os.path.isdir(args.outfile):
338
+ outf = _makeoutfile(infile, args.format)
339
+ if outf is None:
340
+ doerror(f"invalid output format {args.format} in {args.outfile}")
341
+ outfile = os.path.join(args.outfile, os.path.basename(outf))
342
+ elif len(infiles) == 1:
343
+ outfile = args.outfile
344
+
345
+ if infile == "-":
346
+ infile = sys.stdin
347
+ if outfile == "-":
348
+ outfile = sys.stdout
349
+
350
+ usxdoc = None
351
+ if not args.quiet:
352
+ print(f"{infile} -> {outfile}" if outfile else f"{infile}")
353
+ try:
354
+ usxdoc = readFile(infile, informat=args.informat, grammar=ingrammar, altparser=args.qusfm)
355
+ except NoParseError as e:
356
+ doerror(f"Failed to parse {infile}: {e}", False)
357
+
358
+ if len(infiles) == 1 and usxdoc is None:
359
+ doerror(f"Unable to read in {args.infile}")
360
+
361
+ if hookusx is not None:
362
+ hookusx(usxdoc, args)
363
+
364
+ if args.nooutput or outfile is None or usxdoc is None:
365
+ continue
366
+
367
+ version = usxdoc.version
368
+ if version is None:
369
+ usxdoc.version = args.version or "3.1"
370
+ elif args.version is not None:
371
+ usxdoc.version = args.version
372
+
373
+ if not args.canonical:
374
+ usxdoc.canonicalise()
375
+
376
+ usxdoc.saveAs(outfile, outformat=args.outformat, addesids=args.esids, grammar=outgrammar, altparser=args.qusfm)
377
+
378
+ if __name__ == "__main__":
379
+ main()
usfmtc/diagrams.py ADDED
@@ -0,0 +1,247 @@
1
+ import usfmtc.railroad as rr
2
+
3
+ usfmns = "{http://usfm.bible/parse/2023}"
4
+
5
+ CSS_STYLE = '''
6
+ svg.railroad-diagram {{
7
+ background-color: hsl(30,20%,95%);
8
+ }}
9
+ svg.railroad-diagram path {{
10
+ stroke-width: {line-width};
11
+ stroke: black;
12
+ fill: none;
13
+ }}
14
+ svg.railroad-diagram text {{
15
+ font-size: {text-size};
16
+ text-anchor: middle;
17
+ font-family: {font};
18
+ }}
19
+ svg.railroad-diagram text.label {{
20
+ text-anchor:start;
21
+ }}
22
+ svg.railroad-diagram text.comment {{
23
+ font-family: {font};
24
+ font-size: {comment-size};
25
+ }}
26
+ svg.railroad-diagram rect{{
27
+ stroke-width: {line-width};
28
+ stroke: black;
29
+ fill: {color};
30
+ }}
31
+ svg.railroad-diagram rect.group-box {{
32
+ stroke: gray;
33
+ stroke-dasharray: 10 5;
34
+ fill: none;
35
+ }}
36
+ .terminal {{
37
+ font-family: {font};
38
+ }}
39
+ svg.railroad-diagram g.altterminal rect {{
40
+ fill: {altcolor}
41
+ }}
42
+ '''
43
+
44
+ class UsfmRailRoad:
45
+ def __init__(self):
46
+ self.terminals = {}
47
+ self.defines = {}
48
+
49
+ class WrappedRail:
50
+ def __init__(self, content, parent):
51
+ self.rtype = content
52
+ self.parent = parent
53
+ self.properties = {}
54
+ def asRail(self):
55
+ return self.rtype
56
+ def getProperty(self, k, default):
57
+ return self.properties.get(k, default)
58
+ def __str__(self):
59
+ return f"WrappedRail({self.rtype})"
60
+
61
+ class WrappedRailList(list):
62
+ def __init__(self, rtype, parent, *args, swap=False, forced=False, **kw):
63
+ self.properties = {}
64
+ self.rtype = rtype
65
+ self.parent = parent
66
+ self.args = list(args)
67
+ self.swap = swap
68
+ self.forced = forced
69
+ self.kw = kw
70
+ self.propmap = {}
71
+
72
+ def asRail(self):
73
+ content = []
74
+ for x in self:
75
+ y = x.asRail()
76
+ if y is not None:
77
+ content.append(y)
78
+ if self.rtype == rr.Choice and self.kw.get('groupby', 0) > 0:
79
+ s = []
80
+ for i in range(0, len(content), self.kw['groupby']):
81
+ s.append(self.rtype(3 if len(content) - i > 3 else 0,
82
+ *content[i:i+self.kw['groupby']], **self.kw))
83
+ return rr.HorizontalChoice(*s, **self.kw)
84
+ if self.swap:
85
+ return self.rtype(*(content + list(self.args)), **self.kw) if len(content) else None
86
+ else:
87
+ return self.rtype(*(list(self.args) + content), **self.kw) if len(content) else None
88
+ def getProperty(self, k, default):
89
+ return self.properties.get(k, default)
90
+ def __str__(self):
91
+ return f"WrappedRailList({self.rtype}{self.args})[" + ", ".join(str(x) for x in self) + "]"
92
+
93
+ def output(self, top, **kw):
94
+ defaults = {
95
+ 'font': 'DejaVu Sans',
96
+ 'text-size': '12px',
97
+ 'comment-size': '14px',
98
+ 'color': 'rgb(210, 255, 210)',
99
+ 'altcolor': 'rgb(251, 252, 207)',
100
+ 'line-width': '2'}
101
+ defaults.update(kw)
102
+ return rr.Diagram(top.asRail(), type='complex', css=CSS_STYLE.format(**defaults))
103
+
104
+ def attrib_start(self, parser, e, context, name, **kw):
105
+ res = self.append_seq(context, stacked=e.get("{}stacked".format(usfmns), "false") in ("true", "1"))
106
+ res.name = name
107
+ parser.push_element(res)
108
+ return res
109
+
110
+ def attrib_end(self, parser, e, context, **kw):
111
+ parser.pop_element()
112
+ return context
113
+
114
+ def elem_start(self, parser, e, context, name, **kw):
115
+ res = self.append_seq(context, stacked=e.get("{}stacked".format(usfmns), "false") in ("true", "1"))
116
+ res.name = name
117
+ parser.push_element(res)
118
+ return res
119
+
120
+ def elem_end(self, parser, e, context, **kw):
121
+ parser.pop_element()
122
+ return context
123
+
124
+ def append_type(self, context, tclass, *a, swap=False, forced=False, **kw):
125
+ if context is not None and not forced \
126
+ and (context.rtype == tclass or context.rtype == rr.Stack and tclass == rr.Sequence):
127
+ return context
128
+ else:
129
+ res = self.WrappedRailList(tclass, context, *a, swap=swap, forced=forced, **kw)
130
+ if context is not None:
131
+ context.append(res)
132
+ return res
133
+
134
+ def append_seq(self, context=None, stacked=False, forced=False, **kw):
135
+ return self.append_type(context, rr.Stack if stacked else rr.Sequence, forced=forced)
136
+
137
+ def append_opt(self, context=None, **kw):
138
+ return self.append_type(context, rr.Choice, 0, rr.Skip())
139
+
140
+ def append_or(self, context=None, groupby=0, **kw):
141
+ return self.append_type(context, rr.Choice, 0, groupby=groupby)
142
+
143
+ def append_plus(self, context=None, **kw):
144
+ bet = context.getProperty("between", None)
145
+ if bet is not None:
146
+ rep = rr.Terminal(bet)
147
+ res = self.append_type(context, rr.OneOrMore, rep, swap=True)
148
+ else:
149
+ res = self.append_type(context, rr.OneOrMore, swap=True)
150
+ return res
151
+
152
+ def append_star(self, context=None, **kw):
153
+ res = self.WrappedRailList(rr.ZeroOrMore, context) # returns Choice(OneOrMore)
154
+ context.append(res)
155
+ return res
156
+
157
+ def append_group(self, index, context=None, **kw):
158
+ label = chr(0x278A + index)
159
+ res = self.WrappedRailList(rr.Group, context, label, swap=True)
160
+ context.append(res)
161
+ return res
162
+
163
+ def append_interleave(self, context=None, **kw):
164
+ def inter(repeat, *items):
165
+ return rr.OneOrMore(rr.MultipleChoice(0, "any", *items), repeat)
166
+ bet = context.getProperty("interleave_between", None)
167
+ rep = rr.Terminal(bet) if bet is not None else None
168
+ res = self.WrappedRailList(inter, context, bet)
169
+ context.append(res)
170
+ return res
171
+
172
+ def match(self, txt, context, alt=None, **kw):
173
+ cls = {k: kw.get(k, None) for k in ('href',)}
174
+ if alt is None or not len(alt):
175
+ lcontext = context
176
+ else:
177
+ lcontext = self.append_or(context)
178
+ lcontext.append(self.WrappedRail(rr.Terminal(alt, **cls), lcontext))
179
+ cls["cls"] = "altterminal"
180
+ lcontext.append(self.WrappedRail(rr.Terminal(txt, **cls), lcontext))
181
+ return context
182
+
183
+ def terminal(self, name, context, **kw):
184
+ context.append(self.WrappedRail(rr.NonTerminal(name), context))
185
+ return context
186
+
187
+ def backref(self, index, context=None, **kw):
188
+ context.append(self.WrappedRail(rr.Terminal("\u21D0 {}".format(chr(0x278A + index))), context))
189
+ return context
190
+
191
+ def add_terminal(self, name, value, context, **kw):
192
+ self.terminals[name] = value
193
+
194
+ def add_define(self, name, e, context=None, box=False, **kw):
195
+ if box:
196
+ bres = self.WrappedRailList(rr.Group, context, name, swap=True)
197
+ context.append(bres)
198
+ res = self.append_seq(context=bres)
199
+ else:
200
+ res = self.append_seq(context=context)
201
+ self.defines[name] = res
202
+ return res
203
+
204
+ def ref(self, name, res, **kw):
205
+ return self.terminal(name, res)
206
+
207
+ def end_ref(self, e, name, **kw):
208
+ pass
209
+
210
+
211
+ class XmlRailRoad(UsfmRailRoad):
212
+
213
+ def elem_start(self, parser, e, context, name, **kw):
214
+ seq = self.append_type(context, rr.Sequence, forced=True)
215
+ context = self.match(f"<{name}>", seq)
216
+ res = self.append_type(seq, rr.Choice, 0, forced=True, noclose=1)
217
+ res.attribs = {}
218
+ return res
219
+
220
+ def elem_end(self, parser, e, context, **kw):
221
+ if context.args[0] == len(context):
222
+ context.append(self.WrappedRail(rr.Skip(), context))
223
+ elif context.args[0] > len(context):
224
+ print(f"{context} has index of {context.args[0]} but length of {len(context)}")
225
+ context.args[0] = len(context) - 1
226
+ return context
227
+
228
+ def attrib_start(self, parser, e, context, name, **kw):
229
+ parent = context
230
+ while parent is not None and parent.kw.get('noclose', 0) == 0:
231
+ parent = parent.parent
232
+ s = self.WrappedRailList(rr.Sequence, parent)
233
+ n = self.WrappedRail(rr.Terminal(f"@{name}"), s)
234
+ s.append(n)
235
+ if parent is None:
236
+ context.append(s)
237
+ elif getattr(parent, 'attribs', {}).get(name, None) is None:
238
+ parent.insert(0, s)
239
+ parent.args[0] += 1
240
+ parent.attribs[name] = 1
241
+ return s
242
+
243
+ def attrib_end(self, parser, e, context, **kw):
244
+ if context[-1].rtype in (rr.Choice, rr.HorizontalChoice):
245
+ context[-1].kw['noclose'] = 7
246
+ return context
247
+
usfmtc/extension.py ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import re
4
+ import xml.etree.ElementTree as et
5
+
6
+ categories = {
7
+ "char": "Char.char",
8
+ "crossreference": "CrossReference",
9
+ "crossreferencechar": "CrossReferenceChar.char",
10
+ "footnote": "Footnote",
11
+ "footnotechar": "FootnoteChar.char",
12
+ "header": "Header.para",
13
+ "internal": "Internal",
14
+ "introchar": "IntroChar.char",
15
+ "introduction": "Introduction.para",
16
+ "list": "List.para",
17
+ "listchar": "ListChar.char",
18
+ "milestone": "Milestone",
19
+ "otherpara": "OtherPara.para",
20
+ "sectionpara": "SectionPara.para",
21
+ "title": "Title.para",
22
+ "versepara": "VersePara.para",
23
+ }
24
+
25
+
26
+ class SFMFile:
27
+ def __init__(self, fname):
28
+ self.fname = fname
29
+ self.markers = {}
30
+ self.parse()
31
+
32
+ def parse(self):
33
+ with open(self.fname, encoding="utf-8") as inf:
34
+ for l in inf.readlines():
35
+ s = l.strip()
36
+ s = re.sub(r'#.*$', '', s)
37
+ if not s.startswith("\\"):
38
+ continue
39
+ b = s[1:].split(None, 1)
40
+ b = self.preproc(b)
41
+ mk = b[0].lower()
42
+ if mk == "marker":
43
+ curr = {}
44
+ self.markers[b[1]] = curr
45
+ else:
46
+ curr[mk] = b[1]
47
+
48
+ def preproc(self, b):
49
+ return b
50
+
51
+ relaxns = "{http://relaxng.org/ns/structure/1.0}"
52
+
53
+ class Extensions(SFMFile):
54
+ def preproc(self, b):
55
+ if b[0].lower() == "category":
56
+ b[1] = b[1].lower()
57
+ return b
58
+
59
+ def applyto(self, rdoc, factory=et):
60
+ res = False # nothing added
61
+ allcats = set([x.get('category', None) for x in self.markers.values()])
62
+ for a in allcats:
63
+ if a not in categories:
64
+ continue
65
+ ms = [k for k, v in self.markers.items() if v.get('category', None) == a]
66
+ enum = categories[a]+".style.enum"
67
+ e = rdoc.find('./{0}define[@name="{1}"]/{0}choice'.format(relaxns, enum))
68
+ allmks = set([v.text for v in e.findall(f'./{relaxns}value')])
69
+ added = False
70
+ for m in ms:
71
+ if m in allmks:
72
+ continue
73
+ else:
74
+ v = factory.Element(f'{relaxns}value')
75
+ v.text = m
76
+ e.insert(0, v)
77
+ added = True
78
+ res = res or added
79
+ return res