1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
|
"""This module explores a different approach to building doctree elements...
The "normal" way of building a DOCUTILS tree involves, well, building a tree
structure. So one might do::
section = docutils.nodes.section()
section += docutils.nodes.title(text="Title")
para = docutils.nodes.paragraph()
section += para
para += docutils.nodes.Text("Some ")
para += docutils.nodes.strong(text="strong text.")
That's all very nice, if one is *thinking* in terms of a tree structure,
but it is not, for me, a very natural way to construct a text.
(OK - I *know* in practice one would also have imported `paragraph`,
etc., from `docutils.nodes`, but that is not my point.)
This module allows one to use a more LaTex style of construction, with
begin and end delimitors for DOCUTILS nodes. Thus the above example becomes::
build.start("section")
build.add("title","Title")
build.start("paragraph","Some ")
build.add("strong","Strong text.")
build.end("section")
(As a convenience, paragraphs are automatically ended.)
A slightly shorter, and possibly more obfuscated, way of writing this
would be::
build.start("section",build.make("title","Title")
build.start("paragraph","Some ",build.make("strong","Strong text."))
build.end("section")
Sometimes I think that sort of approach makes more sense, sometimes not.
""" # we need a " to keep [X]Emacs Python mode happy.
import string
import docutils.nodes
import docutils.utils
__docformat__ = "reST"
# ----------------------------------------------------------------------
class group(docutils.nodes.Element):
"""Group is a way of grouping together elements.
Compare it to HTML <div> and <span>, or to TeX (?check?)
\begingroup and \endgroup.
It takes the special attribute `style`, which indicates what
sort of thing it is grouping - for instance, "docstring" or
"attributes".
Although it (should be) supplied by the standard DOCUTILS tree,
reST itself does not use `group`. It is solely used by
extensions, such as ``pysource``.
In the default HTML Writer, `group` renders invisibly
(that is, it has no effect at all on the formatted output).
"""
pass
# ----------------------------------------------------------------------
class BuildTree:
def __init__(self, with_groups=1, root=None):
self.stack = []
"""A stack of tuples of the form ("classname",classinstance).
"""
self.root = root
"""A memory of the first item on the stack (notionally, the
"document") - we need this because if we `start` a document,
fill it up, and then `end` it, that final `end` will remove
the appropriate instance from the stack, leaving no record.
Thus this is that record.
"""
if root is not None:
self._stack_add(root)
self.with_groups = with_groups
def finish(self):
"""Call this to indicate we have finished.
It will grumble if anything is left unclosed, but will
return the "root" instance of the DOCUTILS tree we've been
building if all is well...
"""
if len(self.stack) > 0:
raise ValueError,"Items still outstanding on stack: %s"%\
self._stack_as_string()
else:
return self.root
def add(self,thing,*args,**keywords):
"""Add a `thing` DOCUTILS node at the current level.
For instance::
build.add("paragraph","Some simple text.")
If `thing` is "text" then it will automagically be converted
to "Text" (this makes life easier for the user, as all of the
other DOCUTILS node classes they are likely to use start with a
lowercase letter, and "Text" is the sole exception).
See `make` (which this uses) for more details of the arguments.
"""
if thing == "group" and not self.with_groups:
return
instance = self.make(thing,*args,**keywords)
self._stack_append(instance)
def addsubtree(self,subtree):
"""Add a DOCUTILS subtree to the current item.
"""
self._stack_append(subtree)
def current(self):
"""Return the "current" item.
That is, return the item to which `add()` will add DOCUTILS nodes.
"""
return self._stack_current()
def start(self,thing,*args,**keywords):
"""Add a `thing` DOCUTILS node, starting a new level.
`thing` should be either the name of a docutils.nodes class, or
else a class itself.
If `thing` is "text" then it will automagically be converted
to "Text" (this makes life easier for the user, as all of the
other DOCUTILS node classes they are likely to use start with a
lowercase letter, and "Text" is the sole exception).
For instance::
build.start("bullet_list")
As a convenience, if `thing` is a paragraph, and if the current
item is another paragraph, this method will end the old paragraph
before starting the new.
Note that if `thing` is "document", some extra magic is worked
internally. If the keywords `warninglevel` and `errorlevel` are
given, they will be passed to a docutils.utils.Reporter instance,
as well as being passed down to the `document` class's initialiser.
See `make` (which this uses) for more details of the arguments.
"""
name = self._nameof(thing)
if name == "group" and not self.with_groups:
return
if name == "paragraph" and self._stack_ends("paragraph"):
self.end("paragraph")
if name == "document":
if self.root:
return
if len(self.stack) > 0:
raise ValueError,\
"Cannot insert 'document' except at root of stack"
warninglevel = keywords.get("warninglevel",2)
errorlevel = keywords.get("errorlevel",4)
reporter = docutils.utils.Reporter('fubar', warninglevel,
errorlevel)
instance = docutils.nodes.document(reporter,"en")
else:
instance = self.make(thing,*args,**keywords)
if len(self.stack) == 0:
self.root = instance
else:
self._stack_append(instance)
self._stack_add(instance)
def end(self,thing):
"""End the level started below a `thing` DOCUTILS node.
`thing` should be either the name of a docutils.nodes class, or
else a class itself.
For instance::
build.end("bullet_list")
As a convenience, if the last item constructed was actually
a paragraph, and `thing` is the container for said paragraph,
then the paragraph will be automatically ended.
Otherwise, for the moment at least, the `thing` being ended
must be the last thing that was begun (in the future, we *might*
support automatic "unrolling" of the stack, but not at the
moment).
"""
name = self._nameof(thing)
if thing == "group" and not self.with_groups:
return
if self._stack_ends("paragraph") and name != "paragraph":
self.end("paragraph")
self._stack_remove(name)
def make(self,thing,*args,**keywords):
"""Return an instance of `docutils.nodes.thing`
Attempts to regularise the initialisation of putting initial
text into an Element and a TextElement...
`thing` should be either the name of a docutils.nodes class, or
else a class itself (so, for instance, one might call
``build.make("paragraph")`` or
``build.make(docutils.nodes.paragraph)``),
or else None.
If `thing` is "text" then it will automagically be converted
to "Text" (this makes life easier for the user, as all of the
other DOCUTILS node classes they are likely to use start with a
lowercase letter, and "Text" is the sole exception).
If `thing` is an Element subclass, then the arguments are just
passed straight through - any *args list is taken to be children
for the element (strings are coerced to Text instances), and any
**keywords are taken as attributes.
If `thing` is an TextElement subclass, then if the first
item in *args is a string, it is passed down as the `text`
parameter. Any remaining items from *args are used as child
nodes, and any **keywords as attributes.
If `thing` is a Text subclass, then a single argument is expected
within *args, which must be a string, to be used as the Text's
content.
For instance::
n1 = build.make("paragraph","Some ",
build.make("emphasis","text"),
".",align="center")
n2 = build.make(None,"Just plain text")
"""
#print "make: %s, %s, %s"%(thing,args,keywords)
# Temporary special case - since group is not (yet) in docutils.nodes...
if thing == "group":
thing = group
if thing == None:
dps_class = docutils.nodes.Text
elif type(thing) == type(""):
if thing == "text":
thing = "Text"
try:
dps_class = getattr(docutils.nodes,thing)
except AttributeError:
raise ValueError,"docutils.nodes does not define '%s'"%thing
else:
dps_class = thing
# NB: check for TextElement before checking for Element,
# since TextElement is itself a subclass of Element!
if issubclass(dps_class,docutils.nodes.TextElement):
# Force the use of the argument list as such, by insisting
# that the `rawsource` and `text` arguments are empty strings
args = self._convert_args(args)
dps_instance = dps_class("","",*args,**keywords)
elif issubclass(dps_class,docutils.nodes.Element):
# Force the use of the argument list as such, by insisting
# that the `rawsource` arguments is an empty string
args = self._convert_args(args)
dps_instance = dps_class("",*args,**keywords)
elif issubclass(dps_class,docutils.nodes.Text):
if len(args) > 1:
raise ValueError,\
"Text subclass %s may only take one argument"%\
self._nameof(thing)
elif len(args) == 1:
text = args[0]
else:
text = ""
if keywords:
raise ValueError,\
"Text subclass %s cannot use keyword arguments"%\
self._nameof(thing)
dps_instance = dps_class(text)
else:
raise ValueError,"%s is not an Element or TextElement"%\
self._nameof(thing)
#print " ",dps_instance
return dps_instance
def _convert_args(self,args):
"""Return the arguments, with strings converted to Texts.
"""
newargs = []
for arg in args:
if type(arg) == type(""):
newargs.append(docutils.nodes.Text(arg))
else:
newargs.append(arg)
return newargs
def __getattr__(self,name):
"""Return an appropriate DOCUTILS class, for instantiation.
"""
return getattr(docutils.nodes,name)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def _nameof(self,thing):
if thing is None:
return "Text"
elif type(thing) == type(""):
return thing
else:
return thing.__name__
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Stack maintenance
def _stack_ends(self,name):
"""Return true if the stack ends with the named entity.
"""
return self.stack[-1][0] == name
def _stack_add(self,instance):
"""Add a new level to the stack.
"""
self.stack.append((instance.__class__.__name__,instance))
def _stack_remove(self,name):
"""Remove the last level from the stack
(but only if it is of the right sort).
"""
if len(self.stack) == 0:
raise ValueError,"Cannot end %s - nothing outstanding to end"%\
(name)
if name != self.stack[-1][0]:
raise ValueError,"Cannot end %s - last thing begun was %s"%\
(name,self.stack[-1][0])
del self.stack[-1]
def _stack_append(self,instance):
"""Append an instance to the last item on the stack.
"""
if len(self.stack) > 0:
self.stack[-1][1].append(instance)
else:
raise ValueError,"Cannot add %s to current level" \
" - nothing current"%(instance.__class__.__name__)
def _stack_current(self):
"""Return the "current" element from the stack
That is, the element to which we would append any new instances
with `_stack_append()`
"""
return self.stack[-1][1]
def _stack_as_string(self):
names = []
for name,inst in self.stack:
names.append(name)
return string.join(names,",")
# ----------------------------------------------------------------------
if __name__ == "__main__":
build = BuildTree()
#print build.make("paragraph",text="fred")
#print build.paragraph(text="fred")
print "Building a section"
build.start("section")
build.add("title","Fred")
build.start("paragraph")
build.add("text","This is some text.")
build.add("strong","Really.")
build.start("paragraph","Another paragraph")
build.end("section")
print build.finish()
#print "Building a broken section"
#build.start("section")
#build.add("title","Fred")
#build.start("paragraph")
#print build.finish()
|