2
from sphinx.util.compat import Directive
3
from docutils.statemachine import StringList
4
from docutils import nodes, utils
10
def _comma_list(text):
11
return re.split(r"\s*,\s*", text.strip())
13
def _parse_content(content):
19
m = re.match(r' *\:(.+?)\:(?: +(.+))?', line)
21
attrname, value = m.group(1, 2)
22
d[attrname] = value or ''
25
d["text"] = content[idx:]
29
class EnvDirective(object):
32
return self.state.document.settings.env
34
class ChangeLogDirective(EnvDirective, Directive):
39
default_section = 'misc'
41
def _organize_by_section(self, changes):
42
compound_sections = [(s, s.split(" ")) for s in
43
self.sections if " " in s]
45
bysection = collections.defaultdict(list)
48
inner_tag = rec['tags'].intersection(self.inner_tag_sort)
50
inner_tag = inner_tag.pop()
54
for compound, comp_words in compound_sections:
55
if rec['tags'].issuperset(comp_words):
56
bysection[(compound, inner_tag)].append(rec)
57
all_sections.add(compound)
60
intersect = rec['tags'].intersection(self.sections)
62
for sec in rec['sorted_tags']:
64
bysection[(sec, inner_tag)].append(rec)
68
bysection[(self.default_section, inner_tag)].append(rec)
69
return bysection, all_sections
72
def changes(cls, env):
73
return env.temp_data['ChangeLogDirective_%s_changes' % cls.type_]
76
self.sections = self.env.config.changelog_sections
77
self.inner_tag_sort = self.env.config.changelog_inner_tag_sort + [""]
78
self.env.temp_data['ChangeLogDirective_%s_changes' % self.type_] = []
79
self._parsed_content = _parse_content(self.content)
81
p = nodes.paragraph('', '',)
82
self.state.nested_parse(self.content[1:], 0, p)
86
changes = self.changes(self.env)
89
self.version = version = self._parsed_content.get('version', '')
90
id_prefix = "%s-%s" % (self.type_, version)
91
topsection = self._run_top(id_prefix)
92
output.append(topsection)
94
bysection, all_sections = self._organize_by_section(changes)
96
counter = itertools.count()
98
sections_to_render = [s for s in self.sections if s in all_sections]
99
if not sections_to_render:
100
for cat in self.inner_tag_sort:
101
append_sec = self._append_node()
103
for rec in bysection[(self.default_section, cat)]:
104
rec["id"] = "%s-%s" % (id_prefix, next(counter))
106
self._render_rec(rec, None, cat, append_sec)
108
if append_sec.children:
109
topsection.append(append_sec)
111
for section in sections_to_render + [self.default_section]:
112
sec = nodes.section('',
113
nodes.title(section, section),
114
ids=["%s-%s" % (id_prefix, section.replace(" ", "-"))]
117
append_sec = self._append_node()
118
sec.append(append_sec)
120
for cat in self.inner_tag_sort:
121
for rec in bysection[(section, cat)]:
122
rec["id"] = "%s-%s" % (id_prefix, next(counter))
123
self._render_rec(rec, section, cat, append_sec)
125
if append_sec.children:
126
topsection.append(sec)
130
def _append_node(self):
131
return nodes.bullet_list()
133
def _run_top(self, id_prefix):
134
version = self._parsed_content.get('version', '')
135
topsection = nodes.section('',
136
nodes.title(version, version),
140
if self._parsed_content.get("released"):
141
topsection.append(nodes.Text("Released: %s" %
142
self._parsed_content['released']))
144
topsection.append(nodes.Text("no release date"))
146
intro_para = nodes.paragraph('', '')
147
for len_, text in enumerate(self._parsed_content['text']):
148
if ".. change::" in text:
151
self.state.nested_parse(self._parsed_content['text'][0:len_], 0,
153
topsection.append(intro_para)
158
def _render_rec(self, rec, section, cat, append_sec):
159
para = rec['node'].deepcopy()
161
text = _text_rawsource_from_node(para)
163
to_hash = "%s %s" % (self.version, text[0:100])
164
targetid = "%s-%s" % (self.type_,
165
md5.md5(to_hash.encode('ascii', 'ignore')
167
targetnode = nodes.target('', '', ids=[targetid])
168
para.insert(0, targetnode)
169
permalink = nodes.reference('', '',
170
nodes.Text("(link)", "(link)"),
172
classes=['changeset-link']
174
para.append(permalink)
176
insert_ticket = nodes.paragraph('')
177
para.append(insert_ticket)
180
for collection, render, prefix in (
181
(rec['tickets'], self.env.config.changelog_render_ticket, "#%s"),
182
(rec['pullreq'], self.env.config.changelog_render_pullreq,
184
(rec['changeset'], self.env.config.changelog_render_changeset, "r%s"),
186
for refname in collection:
188
insert_ticket.append(nodes.Text(", ", ", "))
190
insert_ticket.append(nodes.Text(" ", " "))
192
if render is not None:
193
refuri = render % refname
194
node = nodes.reference('', '',
195
nodes.Text(prefix % refname, prefix % refname),
199
node = nodes.Text(prefix % refname, prefix % refname)
200
insert_ticket.append(node)
203
tag_node = nodes.strong('',
204
" ".join("[%s]" % t for t
206
[t1 for t1 in [section, cat]
207
if t1 in rec['tags']] +
209
list(rec['tags'].difference([section, cat]))
212
para.children[0].insert(0, tag_node)
216
nodes.target('', '', ids=[rec['id']]),
222
class ChangeDirective(EnvDirective, Directive):
226
parent_cls = ChangeLogDirective
229
content = _parse_content(self.content)
230
p = nodes.paragraph('', '',)
231
sorted_tags = _comma_list(content.get('tags', ''))
233
'tags': set(sorted_tags).difference(['']),
234
'tickets': set(_comma_list(content.get('tickets', ''))).difference(['']),
235
'pullreq': set(_comma_list(content.get('pullreq', ''))).difference(['']),
236
'changeset': set(_comma_list(content.get('changeset', ''))).difference(['']),
239
"title": content.get("title", None),
240
'sorted_tags': sorted_tags
243
if "declarative" in rec['tags']:
244
rec['tags'].add("orm")
246
self.state.nested_parse(content['text'], 0, p)
247
self.parent_cls.changes(self.env).append(rec)
251
def _text_rawsource_from_node(node):
256
if isinstance(n, nodes.Text):
257
src.append(n.rawsource)
258
stack.extend(n.children)
261
def _rst2sphinx(text):
263
[line.strip() for line in textwrap.dedent(text).split("\n")]
267
def make_ticket_link(name, rawtext, text, lineno, inliner,
268
options={}, content=[]):
269
env = inliner.document.settings.env
270
render_ticket = env.config.changelog_render_ticket or "%s"
273
ref = render_ticket % text
274
node = nodes.reference(rawtext, prefix % text, refuri=ref, **options)
276
node = nodes.Text(prefix % text, prefix % text)
280
app.add_directive('changelog', ChangeLogDirective)
281
app.add_directive('change', ChangeDirective)
282
app.add_config_value("changelog_sections", [], 'env')
283
app.add_config_value("changelog_inner_tag_sort", [], 'env')
284
app.add_config_value("changelog_render_ticket",
288
app.add_config_value("changelog_render_pullreq",
292
app.add_config_value("changelog_render_changeset",
296
app.add_role('ticket', make_ticket_link)