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 | ||
919b8a4f | 137 | def __next__(self): |
fb5f9f27 FT |
138 | if self.src is None: |
139 | raise StopIteration() | |
140 | try: | |
141 | ev, el = next(self.src) | |
142 | except StopIteration: | |
143 | self.src = None | |
144 | ev, el = "$", None | |
145 | if ev == ">": | |
146 | self.starttag(el) | |
147 | elif ev == "/": | |
148 | self.shorttag(el) | |
149 | elif ev == "<": | |
150 | self.endtag(el) | |
151 | elif ev == "": | |
ff79cdbf | 152 | self.text(el) |
fb5f9f27 | 153 | elif ev == "!": |
f3464a4a | 154 | self.rawcode(el) |
fb5f9f27 FT |
155 | elif ev == "^": |
156 | self.start(el) | |
157 | elif ev == "$": | |
158 | self.end(el) | |
919b8a4f FT |
159 | ret = bytes(self.buf) |
160 | self.buf[:] = b"" | |
fb5f9f27 | 161 | return ret |
ff79cdbf | 162 | |
fb5f9f27 FT |
163 | def nsname(self, el): |
164 | for t in type(self).__mro__: | |
165 | ret = getattr(t, "defns", {}).get(el.ns, None) | |
166 | if ret is not None: | |
167 | return ret | |
168 | if el.ns is None: | |
169 | return None | |
919b8a4f | 170 | ret = "n" + str(self.nextns) |
fb5f9f27 FT |
171 | self.nextns += 1 |
172 | return ret | |
173 | ||
174 | def findnsnames(self, root): | |
175 | fnames = {} | |
176 | rnames = {} | |
177 | def proc(el): | |
178 | if isinstance(el, cons.element): | |
179 | if el.ns not in fnames: | |
180 | nm = self.nsname(el) | |
181 | fnames[el.ns] = nm | |
182 | rnames[nm] = el.ns | |
183 | for ch in el.children: | |
184 | proc(ch) | |
185 | proc(root) | |
186 | if None not in rnames: | |
187 | fnames[root.ns] = None | |
188 | rnames[None] = root.ns | |
189 | self.nsnames = fnames | |
ff79cdbf FT |
190 | |
191 | @classmethod | |
fb5f9f27 FT |
192 | def output(cls, out, root, nsnames=None, doctype=None, **kw): |
193 | if isinstance(doctype, cons.doctype): | |
194 | pass | |
195 | elif doctype is not None: | |
196 | doctype = cons.doctype(root.name, doctype[0], doctype[1]) | |
197 | src = itertools.chain(iter([("^", doctype)]), flatiter(root)) | |
198 | self = cls(src=src, nsnames=nsnames, **kw) | |
199 | if nsnames is None: | |
200 | self.findnsnames(root) | |
201 | self.first = True | |
202 | for piece in self: | |
203 | out.write(piece) | |
ff79cdbf | 204 | |
3f48e448 | 205 | @classmethod |
fb5f9f27 FT |
206 | def fragment(cls, out, root, nsnames=None, **kw): |
207 | self = cls(src=flatiter(root), nsnames=nsnames, **kw) | |
208 | if nsnames is None: | |
209 | self.findnsnames(root) | |
210 | for piece in self: | |
211 | out.write(piece) | |
3f48e448 | 212 | |
b239aa23 | 213 | @classmethod |
fb5f9f27 | 214 | def format(cls, root, **kw): |
cc112689 | 215 | buf = io.BytesIO() |
fb5f9f27 | 216 | cls.output(buf, root, **kw) |
b239aa23 FT |
217 | return buf.getvalue() |
218 | ||
ff79cdbf | 219 | class indenter(formatter): |
62551769 | 220 | def __init__(self, indent=" ", *args, **kw): |
919b8a4f | 221 | super().__init__(*args, **kw) |
ff79cdbf | 222 | self.indent = indent |
fb5f9f27 | 223 | self.col = 0 |
62551769 | 224 | self.curind = "" |
fb5f9f27 FT |
225 | self.atbreak = True |
226 | self.inline = False | |
227 | self.stack = [] | |
ff79cdbf | 228 | |
fb5f9f27 | 229 | def write(self, text): |
919b8a4f | 230 | lines = text.split("\n") |
fb5f9f27 FT |
231 | if len(lines) > 1: |
232 | for ln in lines[:-1]: | |
233 | self.buf.extend(ln.encode(self.charset)) | |
919b8a4f | 234 | self.buf.extend(b"\n") |
fb5f9f27 FT |
235 | self.col = 0 |
236 | self.buf.extend(lines[-1].encode(self.charset)) | |
237 | self.col += len(lines[-1]) | |
238 | self.atbreak = False | |
239 | ||
240 | def br(self): | |
241 | if not self.atbreak: | |
919b8a4f | 242 | self.buf.extend(("\n" + self.curind).encode(self.charset)) |
fb5f9f27 FT |
243 | self.col = 0 |
244 | self.atbreak = True | |
245 | ||
246 | def inlinep(self, el): | |
ff79cdbf | 247 | for ch in el.children: |
fb5f9f27 FT |
248 | if isinstance(ch, cons.text): |
249 | return True | |
250 | return False | |
251 | ||
252 | def push(self, el): | |
253 | self.stack.append((el, self.curind, self.inline)) | |
254 | ||
255 | def pop(self): | |
256 | el, self.curind, self.inline = self.stack.pop() | |
257 | return el | |
258 | ||
259 | def starttag(self, el): | |
260 | if not self.inline: | |
261 | self.br() | |
262 | self.push(el) | |
263 | self.inline = self.inline or self.inlinep(el) | |
264 | self.curind += self.indent | |
919b8a4f | 265 | super().starttag(el) |
fb5f9f27 FT |
266 | |
267 | def shorttag(self, el): | |
268 | if not self.inline: | |
269 | self.br() | |
919b8a4f | 270 | super().shorttag(el) |
fb5f9f27 FT |
271 | |
272 | def endtag(self, el): | |
273 | il = self.inline | |
274 | self.pop() | |
275 | if not il: | |
276 | self.br() | |
919b8a4f | 277 | super().endtag(el) |
fb5f9f27 FT |
278 | |
279 | def start(self, el): | |
919b8a4f | 280 | super().start(el) |
fb5f9f27 FT |
281 | self.atbreak = True |
282 | ||
283 | def end(self, el): | |
284 | self.br() | |
285 | ||
286 | class textindenter(indenter): | |
287 | maxcol = 70 | |
288 | ||
289 | def text(self, el): | |
919b8a4f | 290 | left = str(el) |
fb5f9f27 FT |
291 | while True: |
292 | if len(left) + self.col > self.maxcol: | |
293 | bp = max(self.maxcol - self.col, 0) | |
919b8a4f | 294 | for i in range(bp, -1, -1): |
fb5f9f27 FT |
295 | if left[i].isspace(): |
296 | while i > 0 and left[i - 1].isspace(): i -= 1 | |
297 | break | |
298 | else: | |
919b8a4f | 299 | for i in range(bp + 1, len(left)): |
fb5f9f27 FT |
300 | if left[i].isspace(): |
301 | break | |
302 | else: | |
303 | i = None | |
304 | if i is None: | |
305 | self.quotewrite(left) | |
306 | break | |
307 | else: | |
308 | self.quotewrite(left[:i]) | |
309 | self.br() | |
310 | left = left[i + 1:].lstrip() | |
311 | else: | |
312 | self.quotewrite(left) | |
313 | break | |
b239aa23 FT |
314 | |
315 | class response(dispatch.restart): | |
316 | charset = "utf-8" | |
317 | doctype = None | |
318 | formatter = indenter | |
319 | ||
320 | def __init__(self, root): | |
cc112689 | 321 | super().__init__() |
b239aa23 FT |
322 | self.root = root |
323 | ||
324 | @property | |
325 | def ctype(self): | |
326 | raise Exception("a subclass of wrw.sp.util.response must override ctype") | |
327 | ||
328 | def handle(self, req): | |
6bb05fa8 | 329 | ret = self.formatter.format(self.root, doctype=self.doctype, charset=self.charset) |
b239aa23 | 330 | req.ohead["Content-Type"] = self.ctype |
6bb05fa8 FT |
331 | req.ohead["Content-Length"] = len(ret) |
332 | return [ret] |