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.
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env python
2
+ # Compatibility API for rnc2rng 1.0
3
+ from . import parser, serializer
4
+
5
+ class Tree(object):
6
+
7
+ def __init__(self, root):
8
+ self.root = root
9
+
10
+ def toxml(self):
11
+ return serializer.XMLSerializer().toxml(self.root)
12
+
13
+ def token_list(src):
14
+ return parser.lex(src)
15
+
16
+ def make_nodetree(tokens):
17
+ return Tree(parser.parser.parse(tokens, parser.State()))
@@ -0,0 +1,255 @@
1
+ # Convert an RELAX NG compact syntax schema to a Node tree
2
+ # This file released to the Public Domain by David Mertz
3
+ from . import parser
4
+ from usfmtc.rnc2rng.parser import (
5
+ ANNO_ATTR, ANNOTATION, ANY, ASSIGN, ATTR, CHOICE, DATATAG, DATATYPES,
6
+ DEFAULT_NS, DEFINE, DIV, DOCUMENTATION, ELEM, EMPTY, EXCEPT, GRAMMAR,
7
+ GROUP, INTERLEAVE, LIST, LITERAL, MAYBE, MIXED, NAME, NOT_ALLOWED, NS,
8
+ PARAM, PARENT, REF, ROOT, SEQ, SOME, TEXT,
9
+ )
10
+
11
+ import html
12
+
13
+ QUANTS = {SOME: 'oneOrMore', MAYBE: 'optional', ANY: 'zeroOrMore'}
14
+ TYPELIB_NS = 'http://www.w3.org/2001/XMLSchema-datatypes'
15
+ NAMESPACES = {
16
+ 'a': 'http://relaxng.org/ns/compatibility/annotations/1.0',
17
+ 'xml': 'http://www.w3.org/XML/1998/namespace',
18
+ }
19
+
20
+ class XMLSerializer(object):
21
+
22
+ def __init__(self, indent=None):
23
+ self.indent = indent or ' '
24
+ self.reset()
25
+
26
+ def reset(self):
27
+ self.buf = []
28
+ self.needs = {}
29
+ self.types = None
30
+ self.ns = {}
31
+ self.default = ''
32
+ self.level = 0
33
+
34
+ def write(self, s):
35
+ self.buf.append(self.indent * self.level + s)
36
+
37
+ def namespace(self, ns):
38
+ assert ns in self.ns or ns in NAMESPACES, ns
39
+ if ns not in self.ns:
40
+ self.ns[ns] = NAMESPACES[ns]
41
+ return self.ns[ns]
42
+
43
+ def toxml(self, node):
44
+
45
+ self.reset()
46
+ types = None
47
+ for n in node.value:
48
+ if n.type == DATATYPES:
49
+ types = n.value[0]
50
+ self.types = types
51
+ elif n.type == DEFAULT_NS:
52
+ self.default = n.value[0]
53
+ if n.name is not None:
54
+ self.ns[n.name] = n.value[0]
55
+ elif n.type == NS:
56
+ self.ns[n.name] = n.value[0]
57
+
58
+ prelude = ['<?xml version="1.0" encoding="UTF-8"?>']
59
+ prelude.append('<grammar xmlns="http://relaxng.org/ns/structure/1.0"')
60
+ if self.default:
61
+ prelude.append(' ns="%s"' % self.default)
62
+
63
+ self.visit(node.value)
64
+ for ns, url in sorted(self.ns.items()):
65
+ prelude.append(' xmlns:%s="%s"' % (ns, url))
66
+ if types is not None or self.needs.get('types'):
67
+ url = types if types is not None else TYPELIB_NS
68
+ prelude.append(' datatypeLibrary="%s"' % url)
69
+
70
+ prelude[-1] = prelude[-1] + '>'
71
+ self.write('</grammar>')
72
+ return '\n'.join(prelude + self.buf)
73
+
74
+ def anno_attrs(self, nodes):
75
+ select = lambda n: isinstance(n, parser.Node) and n.type == ANNO_ATTR
76
+ pairs = [(n.name, html.escape(n.value[0])) for n in nodes if select(n)]
77
+ if not pairs:
78
+ return ''
79
+ return ' ' + ' '.join('%s="%s"' % attr for attr in pairs)
80
+
81
+ def visit(self, nodes, ctx=None, indent=True):
82
+ '''Visiting a list of nodes, writes out the XML content to the internal
83
+ line-based buffer. By default, adds one level of indentation to the
84
+ output compared to the caller's level; passing False as the second
85
+ argument will prevent this from happening.'''
86
+ if indent:
87
+ self.level += 1
88
+ for x in nodes:
89
+
90
+ if not isinstance(x, parser.Node):
91
+ raise TypeError("Not a Node: " + repr(x))
92
+ elif x.type in set([ANNO_ATTR, DATATYPES, DEFAULT_NS, NS]):
93
+ continue
94
+
95
+ attribs = self.anno_attrs(x.value)
96
+ if x.type == DEFINE:
97
+
98
+ op, attrib = x.value[0].name, ''
99
+ if op in set(['|=', '&=']):
100
+ modes = {'|': 'choice', '&': 'interleave'}
101
+ attrib = ' combine="%s"' % modes[op[0]]
102
+
103
+ if x.name == 'start':
104
+ self.write('<start%s%s>' % (attrib, attribs))
105
+ else:
106
+ bits = x.name, attrib, attribs
107
+ self.write('<define name="%s"%s%s>' % bits)
108
+
109
+ self.visit(x.value)
110
+ if x.name == 'start':
111
+ self.write('</start>')
112
+ else:
113
+ self.write('</define>')
114
+
115
+ elif x.type == ASSIGN:
116
+ self.visit(x.value, indent=False)
117
+ elif x.type == GRAMMAR:
118
+ self.write('<grammar>')
119
+ self.visit(x.value)
120
+ self.write('</grammar>')
121
+ elif x.type in set([MAYBE, SOME, ANY]):
122
+ self.write('<%s>' % QUANTS[x.type])
123
+ self.visit(x.value)
124
+ self.write('</%s>' % QUANTS[x.type])
125
+ elif x.type in set([INTERLEAVE, CHOICE, MIXED, LIST, DIV]):
126
+ self.write('<%s>' % x.type.lower())
127
+ self.visit(x.value)
128
+ self.write('</%s>' % x.type.lower())
129
+ elif x.type == EXCEPT:
130
+ self.write('<except>')
131
+ self.visit(x.value, ctx=ctx)
132
+ self.write('</except>')
133
+ elif x.type == NAME:
134
+ if not x.value and '*' in x.name:
135
+ if x.name == '*':
136
+ self.write('<anyName/>')
137
+ else:
138
+ uri = self.ns[x.name.split(':', 1)[0]]
139
+ self.write('<nsName ns="%s"/>' % uri)
140
+ elif x.value:
141
+ if x.name == '*':
142
+ self.write('<anyName>')
143
+ else:
144
+ uri = self.ns[x.name.split(':', 1)[0]]
145
+ self.write('<nsName ns="%s">' % uri)
146
+ self.visit(x.value, ctx=ctx)
147
+ if x.name == '*':
148
+ self.write('</anyName>')
149
+ else:
150
+ self.write('</nsName>')
151
+ else:
152
+ ns = '' if ctx == 'ATTR' else self.default
153
+ name = x.name
154
+ if ':' in name:
155
+ parts = x.name.split(':', 1)
156
+ ns = self.namespace(parts[0])
157
+ name = parts[1]
158
+ self.write('<name ns="%s">%s</name>' % (ns, name))
159
+ elif x.type in set([REF, PARENT]):
160
+ bits = x.type.lower(), x.name, attribs
161
+ self.write('<%s name="%s"%s/>' % bits)
162
+ elif x.type == LITERAL:
163
+ bits = attribs, html.escape(x.name)
164
+ self.write('<value%s>%s</value>' % bits)
165
+ self.visit(x.value, indent=False)
166
+ elif x.type == ANNOTATION:
167
+
168
+ literals, rest = [], []
169
+ for n in x.value:
170
+ if n.type == LITERAL:
171
+ literals.append(n.name)
172
+ elif n.type != ANNO_ATTR:
173
+ rest.append(n)
174
+
175
+ end = '/' if not (literals or rest) else ''
176
+ tail = ''
177
+ if literals and not rest:
178
+ tail = html.escape(''.join(literals)) + '</%s>' % x.name
179
+
180
+ bits = x.name, attribs, end, tail
181
+ self.write('<%s%s%s>%s' % bits)
182
+ if not rest:
183
+ continue
184
+
185
+ for n in x.value:
186
+ if n.type == ANNO_ATTR:
187
+ continue
188
+ elif n.type == LITERAL:
189
+ self.level += 1
190
+ self.write(html.escape(n.name))
191
+ self.level -= 1
192
+ else:
193
+ self.visit([n])
194
+
195
+ self.write('</%s>' % x.name)
196
+
197
+ elif x.type == DOCUMENTATION:
198
+ self.namespace('a')
199
+ fmt = '<a:documentation>%s</a:documentation>'
200
+ self.write(fmt % html.escape('\n'.join(x.value)))
201
+ elif x.type == GROUP:
202
+ if len(x.value) == 1 and x.value[0].type != SEQ:
203
+ self.visit(x.value, indent=False)
204
+ else:
205
+ self.write('<%s%s>' % (x.type.lower(), attribs))
206
+ self.visit(x.value)
207
+ self.write('</%s>' % x.type.lower())
208
+ elif x.type == NOT_ALLOWED:
209
+ self.write('<notAllowed/>')
210
+ elif x.type in set([TEXT, EMPTY]):
211
+ if not len(x.value):
212
+ self.write('<%s%s/>' % (x.type.lower(), attribs))
213
+ else:
214
+ self.write('<%s%s>' % (x.type.lower(), attribs))
215
+ self.visit(x.value)
216
+ self.write('</%s>' % x.type.lower())
217
+ elif x.type == SEQ:
218
+ self.visit(x.value, indent=False)
219
+ elif x.type == DATATAG:
220
+ self.needs['types'] = True
221
+ if not x.value: # no parameters
222
+ self.write('<data type="%s"/>' % x.name)
223
+ else:
224
+ name = x.name
225
+ if name not in ('string', 'token'):
226
+ name = x.name.split(':', 1)[1]
227
+ self.write('<data type="%s">' % name)
228
+ self.visit(x.value)
229
+ self.write('</data>')
230
+ elif x.type == PARAM:
231
+ bits = x.name, html.escape(x.value[0])
232
+ self.write('<param name="%s">%s</param>' % bits)
233
+ elif x.type == ELEM:
234
+ self.write('<element%s>' % attribs)
235
+ self.visit(x.value)
236
+ self.write('</element>')
237
+ elif x.type == ATTR:
238
+ self.write('<attribute%s>' % attribs)
239
+ self.visit(x.value, ctx=x.type)
240
+ self.write('</attribute>')
241
+ elif x.type == ROOT:
242
+ # Verify the included document has the same metadata
243
+ for n in x.value:
244
+ if n.type == DATATYPES:
245
+ assert self.types == n.value[0]
246
+ elif n.type == DEFAULT_NS:
247
+ assert self.default == n.value[0]
248
+ elif n.type == NS:
249
+ assert n.name in self.ns
250
+ assert self.ns[n.name] == n.value[0]
251
+ self.visit(x.value, indent=False)
252
+ else:
253
+ assert False, x
254
+ if indent:
255
+ self.level -= 1
usfmtc/sfmparser.py ADDED
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/python3
2
+
3
+ import re, logging
4
+ from dataclasses import dataclass
5
+ import xml.etree.ElementTree as et
6
+ import regex
7
+ import usfmtc.parser as usfmp
8
+ from usfmtc.xmlutils import ParentElement
9
+
10
+ logger = logging.getLogger("sfmparser")
11
+
12
+ class GlobalState(usfmp.GlobalState):
13
+ ''' text based global state '''
14
+ def __init__(self, txt, timeout=1e7):
15
+ super().__init__(timeout=timeout)
16
+ self.str = txt
17
+ self.captures = []
18
+ self.refs = {}
19
+
20
+ def __getitem__(self, key):
21
+ return self.str[key]
22
+
23
+ def __call__(self):
24
+ return self.str
25
+
26
+ def _ensure(self, index):
27
+ if index >= len(self.captures):
28
+ #self.captures.extend([""] * (index - len(self.captures) + 1))
29
+ self.captures += [[[]] for i in range(index - len(self.captures) + 1)]
30
+
31
+ def capture(self, index, txt):
32
+ self._ensure(index)
33
+ if isinstance(txt, (list, tuple)):
34
+ txt = "".join(txt)
35
+ logger.debug(f"Capture[{index}][{len(self.captures[index])-1}] = '{txt}'")
36
+ self.captures[index][-1] = txt
37
+
38
+ def init(self, index, txt):
39
+ self._ensure(index)
40
+ logger.debug(f"Init capture[{index}][{len(self.captures[index])}]")
41
+ self.captures[index].append("")
42
+
43
+ def release(self, index):
44
+ self.captures[index].pop()
45
+ logger.debug(f"Pop capture[{index}][{len(self.captures[index])}]")
46
+
47
+ def getcapture(self, index):
48
+ logger.debug(f"Get capture[{index}][{len(self.captures[index])}] = '{self.captures[index][-1]}'")
49
+ return self.captures[index][-1]
50
+
51
+ def pop(self):
52
+ return self.captures.pop()
53
+
54
+ def getref(self, name):
55
+ return self.refs.get(name)
56
+
57
+
58
+ class Group(usfmp.Group):
59
+
60
+ def _returnRes(self, res, s):
61
+ res = super()._returnRes(res, s)
62
+ if res is None:
63
+ return None
64
+ while res is not None and len(res) == 1:
65
+ if isinstance(res, (str, Attribute, Element)):
66
+ break
67
+ res = res[0]
68
+ final = self.result(res, self) if self.result is not None else res
69
+ if self.capture and final is not None:
70
+ s.gs.capture(self.capture, final)
71
+ return final
72
+
73
+
74
+ class String(usfmp.Parser):
75
+ def __init__(self, reg, dump=False, **kw):
76
+ self.keep = not dump
77
+ def text(s):
78
+ m = regex.match(self.re, s.gs(), pos=s.pos)
79
+ if not m:
80
+ raise usfmp.NoParseError(f'String ({self.re}) not found', s)
81
+ return (m.group(1) if self.keep else None, s.extend(m.end()-s.pos))
82
+ super().__init__(text, **kw)
83
+ reg = re.sub(r"\\u([0-9a-fA-F]{4})", lambda m:chr(int(m.group(1), 16)), reg)
84
+ self.re = reg if dump else "(" + reg + ")"
85
+ self.name = "/" + reg + "/"
86
+
87
+ def __repr__(self):
88
+ return self.asstr()
89
+
90
+ def asstr(self, context=None):
91
+ return '/{}/{}'.format(self.re, "!" if self.keep else "")
92
+
93
+ def merge(self, other, mode=None):
94
+ if not isinstance(other, String):
95
+ return super().merge(other, mode=mode)
96
+ if mode and "|" in mode:
97
+ ore = other.re[1:-1] if other.re.startswith("(") and other.re.endswith(")") else other.re
98
+ if self.re.startswith("(") and self.re.endswith(")"):
99
+ self.re = self.re[:-1] + "|" + ore + ")"
100
+ else:
101
+ self.re = "(" + self.re + "|" + ore + ")"
102
+ elif self.keep and other.keep:
103
+ return super().merge(other, mode=mode)
104
+ else:
105
+ self.re += other.re
106
+ self.name = "/" + self.re + "/"
107
+ self.keep |= other.keep
108
+ return (self,)
109
+
110
+ def empty(self):
111
+ return len(self.name) == 0
112
+
113
+
114
+ class Reference(usfmp.Parser):
115
+ def __init__(self, index, **kw):
116
+ self.backref = index
117
+ def test(s):
118
+ v = s.gs.getcapture(self.backref)
119
+ if s.gs().startswith(v, s.pos):
120
+ return(v if not kw.get('dump', False) else None, s.extend(len(v)))
121
+ else:
122
+ raise usfmp.NoParseError(f'String from backref (v) not found', s)
123
+ super().__init__(test, **kw)
124
+
125
+ def __repr__(self):
126
+ return "\u21D0{}".format(self.backref)
127
+
128
+ def asstr(self, context=None):
129
+ return "\u21D0{}".format(self.backref)
130
+
131
+
132
+ @dataclass
133
+ class Attribute:
134
+ name: str
135
+ value: str
136
+ override: str
137
+
138
+ def __init__(self, name, value, override, **kw):
139
+ self.override = override
140
+ if isinstance(value, list):
141
+ if len(value) > 1:
142
+ self.name, self.value = value
143
+ return
144
+ else:
145
+ value = value[0]
146
+ self.name = name
147
+ self.value = value
148
+
149
+ def __len__(self):
150
+ return 1
151
+
152
+
153
+ class Element(list):
154
+ def __init__(self, *a, name=None, propmap=None, **kw):
155
+ self.name = name
156
+ self.attributes = {}
157
+ self.propmap = propmap
158
+ self.kw = kw
159
+ for e in a:
160
+ self._append(e)
161
+ if usfmp.Parser.debug:
162
+ logger.debug(f"Create Element({self})")
163
+
164
+ def _append(self, e):
165
+ if isinstance(e, Attribute):
166
+ if self.propmap is not None and e.override is not None and e.name == "_default":
167
+ self.attributes[self.propmap.get(self.attributes[e.override], e.name)] = e.value
168
+ else:
169
+ self.attributes[e.name] = e.value
170
+ elif isinstance(e, list) and not isinstance(e, Element):
171
+ for c in e:
172
+ self._append(c)
173
+ else:
174
+ self.append(e)
175
+
176
+ def asEt(self, parent=None, elfactory=None):
177
+ if elfactory is None:
178
+ elfactory = ParentElement
179
+ res = elfactory(self.name, attrib=self.attributes, parent=parent)
180
+ if parent is not None:
181
+ parent.append(res)
182
+ def asetchild(c, res):
183
+ if isinstance(c, str):
184
+ if len(res):
185
+ res[-1].tail = c if res[-1].tail is None else res[-1].tail + c
186
+ else:
187
+ res.text = c if res.text is None else res.text + c
188
+ elif isinstance(c, Element):
189
+ c.asEt(res, elfactory=elfactory)
190
+ elif isinstance(c, list):
191
+ for e in c:
192
+ asetchild(e, res)
193
+ j = self[:]
194
+ self.clear()
195
+ for c in j:
196
+ self._append(c)
197
+ for c in self:
198
+ asetchild(c, res)
199
+ return res
200
+
201
+ def __str__(self):
202
+ return "{}<{}>{}".format(self.name, self.attributes, [str(x) for x in self])
203
+
204
+ def __repr__(self):
205
+ return str(self)
206
+
207
+
208
+ def parseusfm(infilename, parser, timeout=1e7, isdata=True):
209
+ if isdata:
210
+ dat = infilename
211
+ elif hasattr(infilename, 'read'):
212
+ dat = infilename.read().decode("utf-8")
213
+ else:
214
+ with open(infilename, encoding="utf-8") as inf:
215
+ dat = inf.read()
216
+ gs = GlobalState(dat, timeout=timeout)
217
+ return parser.parse(usfmp.State(gs))
218
+
219
+ escapes = {
220
+ '\\' : '\\',
221
+ 'n': '\n'
222
+ }
223
+
224
+ def expandescape(s):
225
+ return re.sub(r'\\(.)', lambda m: escapes.get(m.group(1), "\\"+m.group(1)), s)
226
+
227
+ class UsfmParserBackend:
228
+ _terminals = {
229
+ 'text': lambda **kw : Text(**kw),
230
+ 'ws': lambda **kw: String(r"\s", **kw),
231
+ 'word': lambda **kw: Text(**kw)
232
+ }
233
+ _grouptypes = {
234
+ 'sequence': "&",
235
+ 'choice': "|",
236
+ 'optional': "?",
237
+ 'zeroOrMore': "*",
238
+ 'oneOrMore': "+"
239
+ }
240
+
241
+ def __init__(self):
242
+ self.references = {}
243
+ self.elements = []
244
+ self.nodes = []
245
+ self.defines = {}
246
+ self.defstack = []
247
+ pass
248
+
249
+ def _trimTo(self, curr):
250
+ i = self.nodes.index(curr)
251
+ self.nodes[:] = self.nodes[:i]
252
+
253
+ def get_nodename(self):
254
+ res = {}
255
+ if len(self.nodes):
256
+ res['to'] = self.nodes[-1].name
257
+ return res
258
+
259
+ # --- Parser backend API
260
+
261
+ def attrib_start(self, parser, e, context, name, **kw):
262
+ if name == "*":
263
+ name = "_default"
264
+ group = Group(name=name, parent=context, mode="&",
265
+ result=(lambda r,s: Attribute(name, r, kw.get('fallback-from', None))),
266
+ **self.get_nodename())
267
+ self.nodes.append(group)
268
+ return group
269
+
270
+ def attrib_end(self, parser, e, context, **kw):
271
+ self._trimTo(context)
272
+ return context
273
+
274
+ def elem_start(self, parser, e, context, name, **kw):
275
+ group = Group(name=name, parent=context, mode="&",
276
+ result=(lambda r,s: Element(r, name=name, propmap=s.propmap.copy())),
277
+ **self.get_nodename())
278
+ self.nodes.append(group)
279
+ return group
280
+
281
+ def elem_end(self, parser, e, context, **kw):
282
+ self._trimTo(context)
283
+ return context
284
+
285
+ def append_seq(self, context=None, to=None, **kw):
286
+ return Group(parent=context, mode="&", **self.get_nodename(), **kw)
287
+
288
+ def append_opt(self, context=None, to=None, **kw):
289
+ return Group(parent=context, mode="?", **self.get_nodename(), **kw)
290
+
291
+ def append_or(self, context=None, to=None, **kw):
292
+ return Group(parent=context, mode="|", **self.get_nodename(), **kw)
293
+
294
+ def append_plus(self, context=None, to=None, **kw):
295
+ bet = context.properties.get("between", None)
296
+ # handle "between"
297
+ return Group(parent=context, mode="+", **self.get_nodename(), **kw)
298
+
299
+ def append_star(self, context=None, to=None, **kw):
300
+ return Group(parent=context, mode="*", **self.get_nodename(), **kw)
301
+
302
+ def append_group(self, index, context=None, to=None, **kw):
303
+ group = Group(parent=context, mode="&", **self.get_nodename(), **kw)
304
+ return group
305
+
306
+ def append_interleave(self, context=None, to=None, **kw):
307
+ return Group(parent=context, mode="|+", **self.get_nodename(), **kw)
308
+
309
+ def match(self, name, context, dump=False, to=None, **kw):
310
+ if not len(name):
311
+ return context
312
+ if name[0] in "\"'":
313
+ res = String(regex.escape(expandescape(name[1:-1])), parent=context, dump=dump, **self.get_nodename(), **kw)
314
+ elif name.startswith("/"):
315
+ res = String(name[1:-1], parent=context, dump=dump, **self.get_nodename(), **kw)
316
+ else:
317
+ res = self._terminals.get(name.lower(), lambda **kw: usfmp.Skip(**kw))(parent=context, dump=dump, **self.get_nodename(), **kw)
318
+ context.append(res)
319
+ return context
320
+
321
+ def terminal(self, name, context, to=None, **kw):
322
+ return self.match(name, context, dump=True, **self.get_nodename(), **kw)
323
+
324
+ def backref(self, index, context=None, to=None, **kw):
325
+ res = Reference(index, parent=context, **self.get_nodename(), **kw)
326
+ context.append(res)
327
+ return context
328
+
329
+ def add_terminal(self, name, value, context, **kw):
330
+ if value[0] in "\"'":
331
+ res = lambda **kw: String(regex.escape(value[1:-1]), **kw)
332
+ elif value[0].startswith("/"):
333
+ res = lambda **kw: String(value[1:-1], **kw)
334
+ self._terminals[name.lower()] = res
335
+
336
+ def add_define(self, name, e, context=None, **kw):
337
+ res = self.append_seq(context, inref=name)
338
+ self.defines[name] = res
339
+ return res
340
+
341
+ def ref(self, name, context, **kw):
342
+ res = self.defines[name]
343
+ context.append(res)
344
+ return res
345
+