Commit | Line | Data |
---|---|---|
919b8a4f | 1 | import itertools, io |
cc112689 | 2 | from .. import dispatch |
62551769 | 3 | from . import cons |
ff79cdbf FT |
4 | |
5 | def findnsnames(el): | |
6 | names = {} | |
7 | nid = [1] | |
8 | def proc(el): | |
9 | if isinstance(el, cons.element): | |
10 | if el.ns not in names: | |
62551769 | 11 | names[el.ns] = "n" + str(nid[0]) |
ff79cdbf FT |
12 | nid[:] = [nid[0] + 1] |
13 | for ch in el.children: | |
14 | proc(ch) | |
15 | proc(el) | |
16 | if None in names: | |
17 | names[None] = None | |
18 | else: | |
19 | names[el.ns] = None | |
20 | return names | |
21 | ||
fb5f9f27 FT |
22 | def flatiter(root, short=True): |
23 | yield ">", root | |
24 | stack = [(root, 0)] | |
25 | while len(stack) > 0: | |
26 | el, i = stack[-1] | |
27 | if i >= len(el.children): | |
28 | yield "<", el | |
29 | stack.pop() | |
30 | else: | |
31 | ch = el.children[i] | |
32 | stack[-1] = el, i + 1 | |
33 | if isinstance(ch, cons.element): | |
34 | if short and len(ch.children) == 0: | |
35 | yield "/", ch | |
36 | else: | |
37 | yield ">", ch | |
38 | stack.append((ch, 0)) | |
39 | elif isinstance(ch, cons.text): | |
40 | yield "", ch | |
41 | elif isinstance(ch, cons.raw): | |
42 | yield "!", ch | |
43 | else: | |
44 | raise Exception("Unknown object in element tree: " + el) | |
45 | ||
ff79cdbf | 46 | class formatter(object): |
fb5f9f27 FT |
47 | def __init__(self, src, nsnames=None, charset="utf-8"): |
48 | self.src = src | |
49 | self.nsnames = nsnames or {} | |
50 | self.nextns = 1 | |
51 | self.first = False | |
52 | self.buf = bytearray() | |
ff79cdbf | 53 | self.charset = charset |
ff79cdbf FT |
54 | |
55 | def write(self, text): | |
fb5f9f27 | 56 | self.buf.extend(text.encode(self.charset)) |
ff79cdbf FT |
57 | |
58 | def quotewrite(self, buf): | |
919b8a4f FT |
59 | buf = buf.replace('&', "&") |
60 | buf = buf.replace('<', "<") | |
61 | buf = buf.replace('>', ">") | |
fb5f9f27 | 62 | self.write(buf) |
ff79cdbf | 63 | |
fb5f9f27 FT |
64 | def __iter__(self): |
65 | return self | |
ff79cdbf | 66 | |
fb5f9f27 FT |
67 | def elname(self, el): |
68 | ns = self.nsnames[el.ns] | |
69 | if ns is None: | |
70 | return el.name | |
71 | else: | |
919b8a4f | 72 | return ns + ":" + el.name |
f3464a4a | 73 | |
fb5f9f27 | 74 | def attrval(self, v): |
919b8a4f | 75 | qc, qt = ("'", "'") if '"' in v else ('"', """) |
ff79cdbf | 76 | self.write(qc) |
919b8a4f FT |
77 | v = v.replace('&', "&") |
78 | v = v.replace('<', "<") | |
79 | v = v.replace('>', ">") | |
fb5f9f27 FT |
80 | v = v.replace(qc, qt) |
81 | self.write(v) | |
ff79cdbf FT |
82 | self.write(qc) |
83 | ||
84 | def attr(self, k, v): | |
85 | self.write(k) | |
919b8a4f | 86 | self.write("=") |
ff79cdbf FT |
87 | self.attrval(v) |
88 | ||
fb5f9f27 FT |
89 | def attrs(self, attrs): |
90 | for k, v in attrs: | |
919b8a4f | 91 | self.write(" ") |
ff79cdbf | 92 | self.attr(k, v) |
ff79cdbf | 93 | |
fb5f9f27 | 94 | def inittag(self, el): |
919b8a4f FT |
95 | self.write("<" + self.elname(el)) |
96 | attrs = el.attrs.items() | |
fb5f9f27 FT |
97 | if self.first: |
98 | nsnames = [] | |
919b8a4f | 99 | for ns, name in self.nsnames.items(): |
fb5f9f27 FT |
100 | if ns is None: |
101 | if name is not None: | |
102 | raise Exception("null namespace must have null name, not" + name) | |
103 | continue | |
919b8a4f | 104 | nsnames.append(("xmlns" if name is None else ("xmlns:" + name), ns)) |
fb5f9f27 FT |
105 | attrs = itertools.chain(attrs, iter(nsnames)) |
106 | self.first = False | |
107 | self.attrs(attrs) | |
108 | ||
109 | def starttag(self, el): | |
110 | self.inittag(el) | |
919b8a4f | 111 | self.write(">") |
fb5f9f27 FT |
112 | |
113 | def shorttag(self, el): | |
114 | self.inittag(el) | |
919b8a4f | 115 | self.write(" />") |
ff79cdbf FT |
116 | |
117 | def endtag(self, el): | |
919b8a4f | 118 | self.write("</" + self.elname(el) + ">") |
ff79cdbf | 119 | |
fb5f9f27 FT |
120 | def text(self, el): |
121 | self.quotewrite(el) | |
ff79cdbf | 122 | |
fb5f9f27 FT |
123 | def rawcode(self, el): |
124 | self.write(el) | |
ff79cdbf | 125 | |
fb5f9f27 | 126 | def start(self, el): |
919b8a4f | 127 | self.write('<?xml version="1.0" encoding="' + self.charset + '" ?>\n') |
fb5f9f27 | 128 | if isinstance(el, cons.doctype): |
919b8a4f | 129 | self.write('<!DOCTYPE %s PUBLIC "%s" "%s">\n' % (el.rootname, |
fb5f9f27 FT |
130 | el.pubid, |
131 | el.dtdid)) | |
132 | self.first = True | |
133 | ||
134 | def end(self, el): | |
135 | pass | |
136 | ||
7e9080af | 137 | def handle(self, ev, el): |
fb5f9f27 FT |
138 | if ev == ">": |
139 | self.starttag(el) | |
140 | elif ev == "/": | |
141 | self.shorttag(el) | |
142 | elif ev == "<": | |
143 | self.endtag(el) | |
144 | elif ev == "": | |
ff79cdbf | 145 | self.text(el) |
fb5f9f27 | 146 | elif ev == "!": |
f3464a4a | 147 | self.rawcode(el) |
fb5f9f27 FT |
148 | elif ev == "^": |
149 | self.start(el) | |
150 | elif ev == "$": | |
151 | self.end(el) | |
7e9080af | 152 | |
cb66c33f | 153 | def __next__(self): |
7e9080af FT |
154 | if self.src is None: |
155 | raise StopIteration() | |
156 | try: | |
157 | ev, el = next(self.src) | |
158 | except StopIteration: | |
159 | self.src = None | |
160 | ev, el = "$", None | |
161 | self.handle(ev, el) | |
919b8a4f FT |
162 | ret = bytes(self.buf) |
163 | self.buf[:] = b"" | |
fb5f9f27 | 164 | return ret |
ff79cdbf | 165 | |
fb5f9f27 FT |
166 | def nsname(self, el): |
167 | for t in type(self).__mro__: | |
168 | ret = getattr(t, "defns", {}).get(el.ns, None) | |
169 | if ret is not None: | |
170 | return ret | |
171 | if el.ns is None: | |
172 | return None | |
919b8a4f | 173 | ret = "n" + str(self.nextns) |
fb5f9f27 FT |
174 | self.nextns += 1 |
175 | return ret | |
176 | ||
177 | def findnsnames(self, root): | |
178 | fnames = {} | |
179 | rnames = {} | |
180 | def proc(el): | |
181 | if isinstance(el, cons.element): | |
182 | if el.ns not in fnames: | |
183 | nm = self.nsname(el) | |
184 | fnames[el.ns] = nm | |
185 | rnames[nm] = el.ns | |
186 | for ch in el.children: | |
187 | proc(ch) | |
188 | proc(root) | |
189 | if None not in rnames: | |
190 | fnames[root.ns] = None | |
191 | rnames[None] = root.ns | |
192 | self.nsnames = fnames | |
ff79cdbf FT |
193 | |
194 | @classmethod | |
fb5f9f27 FT |
195 | def output(cls, out, root, nsnames=None, doctype=None, **kw): |
196 | if isinstance(doctype, cons.doctype): | |
197 | pass | |
198 | elif doctype is not None: | |
199 | doctype = cons.doctype(root.name, doctype[0], doctype[1]) | |
200 | src = itertools.chain(iter([("^", doctype)]), flatiter(root)) | |
201 | self = cls(src=src, nsnames=nsnames, **kw) | |
202 | if nsnames is None: | |
203 | self.findnsnames(root) | |
204 | self.first = True | |
205 | for piece in self: | |
206 | out.write(piece) | |
ff79cdbf | 207 | |
3f48e448 | 208 | @classmethod |
fb5f9f27 FT |
209 | def fragment(cls, out, root, nsnames=None, **kw): |
210 | self = cls(src=flatiter(root), nsnames=nsnames, **kw) | |
211 | if nsnames is None: | |
212 | self.findnsnames(root) | |
213 | for piece in self: | |
214 | out.write(piece) | |
3f48e448 | 215 | |
b239aa23 | 216 | @classmethod |
fb5f9f27 | 217 | def format(cls, root, **kw): |
cc112689 | 218 | buf = io.BytesIO() |
fb5f9f27 | 219 | cls.output(buf, root, **kw) |
b239aa23 FT |
220 | return buf.getvalue() |
221 | ||
ff79cdbf | 222 | class indenter(formatter): |
62551769 | 223 | def __init__(self, indent=" ", *args, **kw): |
919b8a4f | 224 | super().__init__(*args, **kw) |
ff79cdbf | 225 | self.indent = indent |
fb5f9f27 | 226 | self.col = 0 |
62551769 | 227 | self.curind = "" |
fb5f9f27 FT |
228 | self.atbreak = True |
229 | self.inline = False | |
230 | self.stack = [] | |
7e9080af | 231 | self.last = None, None |
8437855e | 232 | self.lastendbr = True |
ff79cdbf | 233 | |
fb5f9f27 | 234 | def write(self, text): |
919b8a4f | 235 | lines = text.split("\n") |
fb5f9f27 FT |
236 | if len(lines) > 1: |
237 | for ln in lines[:-1]: | |
238 | self.buf.extend(ln.encode(self.charset)) | |
919b8a4f | 239 | self.buf.extend(b"\n") |
fb5f9f27 FT |
240 | self.col = 0 |
241 | self.buf.extend(lines[-1].encode(self.charset)) | |
242 | self.col += len(lines[-1]) | |
243 | self.atbreak = False | |
244 | ||
245 | def br(self): | |
246 | if not self.atbreak: | |
919b8a4f | 247 | self.buf.extend(("\n" + self.curind).encode(self.charset)) |
fb5f9f27 FT |
248 | self.col = 0 |
249 | self.atbreak = True | |
250 | ||
251 | def inlinep(self, el): | |
ff79cdbf | 252 | for ch in el.children: |
fb5f9f27 FT |
253 | if isinstance(ch, cons.text): |
254 | return True | |
255 | return False | |
256 | ||
257 | def push(self, el): | |
258 | self.stack.append((el, self.curind, self.inline)) | |
259 | ||
260 | def pop(self): | |
261 | el, self.curind, self.inline = self.stack.pop() | |
262 | return el | |
263 | ||
264 | def starttag(self, el): | |
265 | if not self.inline: | |
8437855e | 266 | if self.last[0] == "<" and self.last[1].name == el.name and self.lastendbr: |
7e9080af FT |
267 | pass |
268 | else: | |
269 | self.br() | |
fb5f9f27 FT |
270 | self.push(el) |
271 | self.inline = self.inline or self.inlinep(el) | |
272 | self.curind += self.indent | |
919b8a4f | 273 | super().starttag(el) |
fb5f9f27 FT |
274 | |
275 | def shorttag(self, el): | |
276 | if not self.inline: | |
277 | self.br() | |
919b8a4f | 278 | super().shorttag(el) |
fb5f9f27 FT |
279 | |
280 | def endtag(self, el): | |
281 | il = self.inline | |
282 | self.pop() | |
8437855e FT |
283 | if il or (self.last[0] == ">" and self.last[1] == el): |
284 | self.lastendbr = False | |
285 | else: | |
fb5f9f27 | 286 | self.br() |
8437855e | 287 | self.lastendbr = True |
919b8a4f | 288 | super().endtag(el) |
fb5f9f27 FT |
289 | |
290 | def start(self, el): | |
919b8a4f | 291 | super().start(el) |
fb5f9f27 FT |
292 | self.atbreak = True |
293 | ||
294 | def end(self, el): | |
295 | self.br() | |
296 | ||
7e9080af | 297 | def handle(self, ev, el): |
cb66c33f | 298 | super().handle(ev, el) |
7e9080af FT |
299 | self.last = ev, el |
300 | ||
fb5f9f27 FT |
301 | class textindenter(indenter): |
302 | maxcol = 70 | |
303 | ||
304 | def text(self, el): | |
919b8a4f | 305 | left = str(el) |
fb5f9f27 FT |
306 | while True: |
307 | if len(left) + self.col > self.maxcol: | |
308 | bp = max(self.maxcol - self.col, 0) | |
919b8a4f | 309 | for i in range(bp, -1, -1): |
fb5f9f27 FT |
310 | if left[i].isspace(): |
311 | while i > 0 and left[i - 1].isspace(): i -= 1 | |
312 | break | |
313 | else: | |
919b8a4f | 314 | for i in range(bp + 1, len(left)): |
fb5f9f27 FT |
315 | if left[i].isspace(): |
316 | break | |
317 | else: | |
318 | i = None | |
319 | if i is None: | |
320 | self.quotewrite(left) | |
321 | break | |
322 | else: | |
323 | self.quotewrite(left[:i]) | |
324 | self.br() | |
325 | left = left[i + 1:].lstrip() | |
326 | else: | |
327 | self.quotewrite(left) | |
328 | break | |
b239aa23 FT |
329 | |
330 | class response(dispatch.restart): | |
331 | charset = "utf-8" | |
332 | doctype = None | |
333 | formatter = indenter | |
334 | ||
335 | def __init__(self, root): | |
cc112689 | 336 | super().__init__() |
b239aa23 FT |
337 | self.root = root |
338 | ||
339 | @property | |
340 | def ctype(self): | |
341 | raise Exception("a subclass of wrw.sp.util.response must override ctype") | |
342 | ||
343 | def handle(self, req): | |
6bb05fa8 | 344 | ret = self.formatter.format(self.root, doctype=self.doctype, charset=self.charset) |
b239aa23 | 345 | req.ohead["Content-Type"] = self.ctype |
6bb05fa8 FT |
346 | req.ohead["Content-Length"] = len(ret) |
347 | return [ret] |