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