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 +379 -0
- usfmtc/diagrams.py +247 -0
- usfmtc/extension.py +79 -0
- usfmtc/grammar.py +461 -0
- usfmtc/parser.py +300 -0
- usfmtc/railroad.py +1320 -0
- usfmtc/reference.py +302 -0
- usfmtc/rnc2rng/__init__.py +13 -0
- usfmtc/rnc2rng/parser.py +741 -0
- usfmtc/rnc2rng/rnctree.py +17 -0
- usfmtc/rnc2rng/serializer.py +255 -0
- usfmtc/sfmparser.py +345 -0
- usfmtc/usfmgenerate.py +156 -0
- usfmtc/usfmparser.py +590 -0
- usfmtc/usjproc.py +88 -0
- usfmtc/usx.rng +2182 -0
- usfmtc/usxgrammar.py +36 -0
- usfmtc/usxmodel.py +520 -0
- usfmtc/usxparser.py +568 -0
- usfmtc/xmlutils.py +139 -0
- usfmtc-0.2.dist-info/LICENSE.md +20 -0
- usfmtc-0.2.dist-info/METADATA +61 -0
- usfmtc-0.2.dist-info/RECORD +26 -0
- usfmtc-0.2.dist-info/WHEEL +5 -0
- usfmtc-0.2.dist-info/entry_points.txt +2 -0
- usfmtc-0.2.dist-info/top_level.txt +1 -0
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
|