Historie 0.1.0__py3-none-any.whl
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.
- historie/__init__.py +1 -0
- historie/__main__.py +5 -0
- historie/cli.py +66 -0
- historie/directives.py +447 -0
- historie/exporters.py +32 -0
- historie/html_writer.py +61 -0
- historie/static/default_favicon.svg +5 -0
- historie/static/style.css +416 -0
- historie/tests.py +733 -0
- historie/utility.py +148 -0
- historie-0.1.0.dist-info/METADATA +292 -0
- historie-0.1.0.dist-info/RECORD +15 -0
- historie-0.1.0.dist-info/WHEEL +5 -0
- historie-0.1.0.dist-info/entry_points.txt +2 -0
- historie-0.1.0.dist-info/top_level.txt +1 -0
historie/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.1.0'
|
historie/__main__.py
ADDED
historie/cli.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import fileinput
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from . import exporters
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__package__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_parser():
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog='historie',
|
|
16
|
+
description=(
|
|
17
|
+
'Generate a résumé in a publish format from reStructuredText.'
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
'--logging-level',
|
|
22
|
+
help='Logging level. By default: %(default)s.',
|
|
23
|
+
default='INFO',
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
'-f', '--format',
|
|
27
|
+
choices=['html', 'pdf'],
|
|
28
|
+
default='html',
|
|
29
|
+
help='Output format. By default: %(default)s.',
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
'-o', '--output',
|
|
33
|
+
type=Path,
|
|
34
|
+
help='Output file: By default ".rst" is appended to input.',
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument('input', help='Input reStructuredText file')
|
|
37
|
+
|
|
38
|
+
return parser
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main():
|
|
42
|
+
logging.basicConfig(
|
|
43
|
+
level=logging.INFO,
|
|
44
|
+
format='%(asctime)s %(levelname)-7s %(name)s %(message)s',
|
|
45
|
+
datefmt='%Y-%m-%d %H:%M:%S',
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
args = build_parser().parse_args()
|
|
49
|
+
logging.root.setLevel(args.logging_level)
|
|
50
|
+
|
|
51
|
+
input_path = Path(args.input)
|
|
52
|
+
if args.input and args.input != '-' and not input_path.exists():
|
|
53
|
+
logger.error('Input file not found: %s', args.input)
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
|
|
56
|
+
if args.output:
|
|
57
|
+
output = args.output
|
|
58
|
+
elif args.input and args.input != '-':
|
|
59
|
+
output = f'{input_path.stem}.{args.format}'
|
|
60
|
+
else:
|
|
61
|
+
output = f'output.{args.format}'
|
|
62
|
+
|
|
63
|
+
exporter = exporters.registry[args.format]
|
|
64
|
+
with fileinput.input(files=args.input) as f_in:
|
|
65
|
+
with open(output, 'wb') as f_out:
|
|
66
|
+
exporter(''.join(line for line in f_in), f_out)
|
historie/directives.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from urllib.parse import quote
|
|
3
|
+
|
|
4
|
+
from docutils import nodes
|
|
5
|
+
from docutils.parsers import rst
|
|
6
|
+
from docutils.parsers.rst.directives import register_directive, unchanged
|
|
7
|
+
|
|
8
|
+
from . import utility
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = 'register_directives',
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register_directives():
|
|
15
|
+
directives = (
|
|
16
|
+
ContactDirective,
|
|
17
|
+
EducationDirective,
|
|
18
|
+
JobDirective,
|
|
19
|
+
LinkDirective,
|
|
20
|
+
ProjectDirective,
|
|
21
|
+
StackExchangeProfileDirective,
|
|
22
|
+
SummaryDirective,
|
|
23
|
+
TechnologyPreferenceDirective,
|
|
24
|
+
)
|
|
25
|
+
for d in directives:
|
|
26
|
+
register_directive(d.name, d)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def split_tags(s: str) -> list[str]:
|
|
30
|
+
return [t.strip() for t in s.split(',') if t.strip()]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def format_date_range(start: date | None, finish: date | None) -> str:
|
|
34
|
+
if start and finish:
|
|
35
|
+
return f'{start.strftime("%b %Y")} – {finish.strftime("%b %Y")}'
|
|
36
|
+
elif start:
|
|
37
|
+
return f'{start.strftime("%b %Y")} – present'
|
|
38
|
+
else:
|
|
39
|
+
return ''
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ContactDirective(rst.Directive):
|
|
43
|
+
|
|
44
|
+
name = 'his-contacts'
|
|
45
|
+
|
|
46
|
+
has_content = True
|
|
47
|
+
option_spec = {
|
|
48
|
+
'name': unchanged,
|
|
49
|
+
'title': unchanged,
|
|
50
|
+
'location': unchanged,
|
|
51
|
+
'email': unchanged,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def run(self):
|
|
55
|
+
container = nodes.container(classes=[self.name])
|
|
56
|
+
for k, v in self.options.items():
|
|
57
|
+
if k == 'email':
|
|
58
|
+
ref = nodes.reference('', v, refuri=f'mailto:{v}')
|
|
59
|
+
li = nodes.list_item('', nodes.inline('', '', ref), classes=[f'{self.name}-{k}'])
|
|
60
|
+
else:
|
|
61
|
+
li = nodes.list_item('', nodes.inline('', v), classes=[f'{self.name}-{k}'])
|
|
62
|
+
container.append(li)
|
|
63
|
+
|
|
64
|
+
if 'name' in self.options:
|
|
65
|
+
self.state.document.settings.title = self.options['name']
|
|
66
|
+
|
|
67
|
+
return [container]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class LinkDirective(rst.Directive):
|
|
71
|
+
|
|
72
|
+
name = 'his-links'
|
|
73
|
+
|
|
74
|
+
has_content = True
|
|
75
|
+
|
|
76
|
+
def run(self):
|
|
77
|
+
container = nodes.container(classes=[self.name])
|
|
78
|
+
|
|
79
|
+
if not self.content:
|
|
80
|
+
return [container]
|
|
81
|
+
|
|
82
|
+
node = nodes.Element()
|
|
83
|
+
self.state.nested_parse(self.content, self.content_offset, node)
|
|
84
|
+
|
|
85
|
+
items = []
|
|
86
|
+
for child in node:
|
|
87
|
+
if not isinstance(child, nodes.definition_list):
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
for item in child:
|
|
91
|
+
if not isinstance(item, nodes.definition_list_item):
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
term = item[0].astext() if item.children else ''
|
|
95
|
+
definition = item[1].astext().strip() if len(item) > 1 else ''
|
|
96
|
+
if term and definition:
|
|
97
|
+
ref = nodes.reference('', term, refuri=definition)
|
|
98
|
+
ref['classes'].append('his-link')
|
|
99
|
+
li = nodes.list_item('', nodes.inline('', '', ref))
|
|
100
|
+
items.append(li)
|
|
101
|
+
|
|
102
|
+
if items:
|
|
103
|
+
container.append(nodes.bullet_list('', *items))
|
|
104
|
+
|
|
105
|
+
return [container]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class StackExchangeProfileDirective(rst.Directive):
|
|
109
|
+
|
|
110
|
+
name = 'his-stack-exchange-profiles'
|
|
111
|
+
|
|
112
|
+
has_content = False
|
|
113
|
+
option_spec = {
|
|
114
|
+
'stackoverflow': unchanged,
|
|
115
|
+
'serverfault': unchanged,
|
|
116
|
+
'superuser': unchanged,
|
|
117
|
+
'askubuntu': unchanged,
|
|
118
|
+
'unix': unchanged,
|
|
119
|
+
'dba': unchanged,
|
|
120
|
+
'softwareengineering': unchanged,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_site_urls = {
|
|
124
|
+
'stackoverflow': 'https://stackoverflow.com/users/{}',
|
|
125
|
+
'serverfault': 'https://serverfault.com/users/{}',
|
|
126
|
+
'superuser': 'https://superuser.com/users/{}',
|
|
127
|
+
'askubuntu': 'https://askubuntu.com/users/{}',
|
|
128
|
+
'unix': 'https://unix.stackexchange.com/users/{}',
|
|
129
|
+
'dba': 'https://dba.stackexchange.com/users/{}',
|
|
130
|
+
'softwareengineering': 'https://softwareengineering.stackexchange.com/users/{}',
|
|
131
|
+
}
|
|
132
|
+
_site_names = {
|
|
133
|
+
'stackoverflow': 'Stack Overflow',
|
|
134
|
+
'serverfault': 'Server Fault',
|
|
135
|
+
'superuser': 'Super User',
|
|
136
|
+
'askubuntu': 'Ask Ubuntu',
|
|
137
|
+
'unix': 'Unix & Linux',
|
|
138
|
+
'dba': 'Database Administrators',
|
|
139
|
+
'softwareengineering': 'Software Engineering',
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
def _create_header(
|
|
143
|
+
self, profile: utility.StackExchangeProfile | None, site: str, user_id: int
|
|
144
|
+
) -> nodes.container:
|
|
145
|
+
header = nodes.container(classes=[f'{self.name}-header'])
|
|
146
|
+
|
|
147
|
+
site_name = self._site_names[site]
|
|
148
|
+
url = self._site_urls[site].format(user_id)
|
|
149
|
+
|
|
150
|
+
site_para = nodes.paragraph('', '', classes=[f'{self.name}-site'])
|
|
151
|
+
ref = nodes.reference('', site_name, refuri=url)
|
|
152
|
+
ref['classes'].append('his-link')
|
|
153
|
+
site_para.append(ref)
|
|
154
|
+
if profile:
|
|
155
|
+
if profile.answer_count > 0:
|
|
156
|
+
site_para.append(nodes.inline('', ' · '))
|
|
157
|
+
answers_ref = nodes.reference(
|
|
158
|
+
'', f'{profile.answer_count} answers', refuri=f'{url}?tab=answers'
|
|
159
|
+
)
|
|
160
|
+
answers_ref['classes'].append(f'{self.name}-badge')
|
|
161
|
+
site_para.append(answers_ref)
|
|
162
|
+
|
|
163
|
+
if profile.question_count > 0:
|
|
164
|
+
site_para.append(nodes.inline('', ' · '))
|
|
165
|
+
questions_ref = nodes.reference(
|
|
166
|
+
'', f'{profile.question_count} questions', refuri=f'{url}?tab=questions'
|
|
167
|
+
)
|
|
168
|
+
questions_ref['classes'].append(f'{self.name}-badge')
|
|
169
|
+
site_para.append(questions_ref)
|
|
170
|
+
header.append(site_para)
|
|
171
|
+
|
|
172
|
+
if profile:
|
|
173
|
+
date_range = format_date_range(profile.member_since.date(), None)
|
|
174
|
+
if date_range:
|
|
175
|
+
header.append(nodes.paragraph(
|
|
176
|
+
'', date_range, classes=[f'{self.name}-dates']
|
|
177
|
+
))
|
|
178
|
+
|
|
179
|
+
return header
|
|
180
|
+
|
|
181
|
+
def _create_top_tags(self, profile: utility.StackExchangeProfile) -> nodes.container:
|
|
182
|
+
tag_container = nodes.container(classes=[f'{self.name}-tags'])
|
|
183
|
+
for tag in profile.top_tags[:5]:
|
|
184
|
+
uri = 'https://stackoverflow.com/tags/{}'.format(quote(tag))
|
|
185
|
+
ref = nodes.reference('', tag, refuri=uri)
|
|
186
|
+
ref['classes'].append('his-tag')
|
|
187
|
+
tag_container.append(ref)
|
|
188
|
+
|
|
189
|
+
return tag_container
|
|
190
|
+
|
|
191
|
+
def run(self):
|
|
192
|
+
container = nodes.container(classes=[self.name])
|
|
193
|
+
|
|
194
|
+
for site, user_id in self.options.items():
|
|
195
|
+
if not user_id or site not in self._site_urls:
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
profile = utility.fetch_stack_exchange_profile(site, user_id)
|
|
199
|
+
|
|
200
|
+
item = nodes.container(classes=[f'{self.name}-item', f'{self.name}-{site}'])
|
|
201
|
+
item.append(self._create_header(profile, site, user_id))
|
|
202
|
+
if profile and profile.top_tags:
|
|
203
|
+
item.append(self._create_top_tags(profile))
|
|
204
|
+
container.append(item)
|
|
205
|
+
|
|
206
|
+
return [container]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class TechnologyPreferenceDirective(rst.Directive):
|
|
210
|
+
|
|
211
|
+
name = 'his-technology-preference'
|
|
212
|
+
|
|
213
|
+
has_content = False
|
|
214
|
+
option_spec = {
|
|
215
|
+
'desired': split_tags,
|
|
216
|
+
'undesired': split_tags,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
def _create_tag_item(self, tag: str) -> nodes.list_item:
|
|
220
|
+
uri = 'https://stackoverflow.com/tags/{}'.format(quote(tag))
|
|
221
|
+
ref = nodes.reference('', tag, refuri=uri)
|
|
222
|
+
ref['classes'].append('his-tag')
|
|
223
|
+
return nodes.list_item('', nodes.inline('', '', ref))
|
|
224
|
+
|
|
225
|
+
def run(self):
|
|
226
|
+
desired: list[str] = self.options.get('desired', [])
|
|
227
|
+
undesired: list[str] = self.options.get('undesired', [])
|
|
228
|
+
|
|
229
|
+
container = nodes.container(classes=[self.name])
|
|
230
|
+
if desired:
|
|
231
|
+
section = nodes.container(classes=[f'{self.name}-desired'])
|
|
232
|
+
section.append(nodes.paragraph('', 'I want to work with'))
|
|
233
|
+
section.append(nodes.bullet_list('', *map(self._create_tag_item, desired)))
|
|
234
|
+
container.append(section)
|
|
235
|
+
if undesired:
|
|
236
|
+
section = nodes.container(classes=[f'{self.name}-undesired'])
|
|
237
|
+
section.append(nodes.paragraph('', 'I prefer not to work with'))
|
|
238
|
+
section.append(nodes.bullet_list('', *map(self._create_tag_item, undesired)))
|
|
239
|
+
container.append(section)
|
|
240
|
+
|
|
241
|
+
return [container]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class SummaryDirective(rst.Directive):
|
|
245
|
+
|
|
246
|
+
name = 'his-summary'
|
|
247
|
+
|
|
248
|
+
has_content = True
|
|
249
|
+
|
|
250
|
+
def run(self):
|
|
251
|
+
self.assert_has_content()
|
|
252
|
+
container = nodes.container(classes=[self.name])
|
|
253
|
+
self.state.nested_parse(self.content, self.content_offset, container)
|
|
254
|
+
return [container]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class JobDirective(rst.Directive):
|
|
258
|
+
|
|
259
|
+
name = 'his-job'
|
|
260
|
+
section_title = 'Experience'
|
|
261
|
+
|
|
262
|
+
has_content = True
|
|
263
|
+
option_spec = {
|
|
264
|
+
'company-name': unchanged,
|
|
265
|
+
'company-website': unchanged,
|
|
266
|
+
'role': unchanged,
|
|
267
|
+
'start': date.fromisoformat,
|
|
268
|
+
'finish': date.fromisoformat,
|
|
269
|
+
'technology': split_tags,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
def run(self):
|
|
273
|
+
company_name: str = self.options.get('company-name', '')
|
|
274
|
+
company_website: str | None = self.options.get('company-website')
|
|
275
|
+
role: str = self.options.get('role', '')
|
|
276
|
+
start: date | None = self.options.get('start')
|
|
277
|
+
finish: date | None = self.options.get('finish')
|
|
278
|
+
technology: list[str] = self.options.get('technology', [])
|
|
279
|
+
|
|
280
|
+
result = []
|
|
281
|
+
if not getattr(self.state.document, '_his_job_seen', False):
|
|
282
|
+
self.state.document._his_job_seen = True
|
|
283
|
+
title = nodes.paragraph('', self.section_title, classes=['his-section-title'])
|
|
284
|
+
result.append(title)
|
|
285
|
+
|
|
286
|
+
container = nodes.container(classes=[self.name])
|
|
287
|
+
|
|
288
|
+
header = nodes.container(classes=[f'{self.name}-header'])
|
|
289
|
+
if company_website:
|
|
290
|
+
company_ref = nodes.reference('', company_name, refuri=company_website)
|
|
291
|
+
company_ref['classes'].append('his-link')
|
|
292
|
+
header.append(nodes.paragraph('', '', company_ref, classes=[f'{self.name}-company']))
|
|
293
|
+
else:
|
|
294
|
+
header.append(nodes.paragraph('', company_name, classes=[f'{self.name}-company']))
|
|
295
|
+
|
|
296
|
+
if role:
|
|
297
|
+
header.append(nodes.paragraph('', role, classes=[f'{self.name}-role']))
|
|
298
|
+
|
|
299
|
+
date_range = format_date_range(start, finish)
|
|
300
|
+
if date_range:
|
|
301
|
+
header.append(nodes.paragraph('', date_range, classes=[f'{self.name}-dates']))
|
|
302
|
+
|
|
303
|
+
container.append(header)
|
|
304
|
+
|
|
305
|
+
if technology:
|
|
306
|
+
tech_container = nodes.container(classes=[f'{self.name}-technology'])
|
|
307
|
+
for tag in technology:
|
|
308
|
+
uri = 'https://stackoverflow.com/tags/{}'.format(quote(tag))
|
|
309
|
+
ref = nodes.reference('', tag, refuri=uri)
|
|
310
|
+
ref['classes'].append('his-tag')
|
|
311
|
+
tech_container.append(ref)
|
|
312
|
+
container.append(tech_container)
|
|
313
|
+
|
|
314
|
+
if self.content:
|
|
315
|
+
description = nodes.container(classes=[f'{self.name}-description'])
|
|
316
|
+
self.state.nested_parse(self.content, self.content_offset, description)
|
|
317
|
+
container.append(description)
|
|
318
|
+
|
|
319
|
+
result.append(container)
|
|
320
|
+
return result
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class EducationDirective(rst.Directive):
|
|
324
|
+
|
|
325
|
+
name = 'his-education'
|
|
326
|
+
section_title = 'Education'
|
|
327
|
+
|
|
328
|
+
has_content = True
|
|
329
|
+
option_spec = {
|
|
330
|
+
'institution': unchanged,
|
|
331
|
+
'institution-website': unchanged,
|
|
332
|
+
'degree': unchanged,
|
|
333
|
+
'start': date.fromisoformat,
|
|
334
|
+
'finish': date.fromisoformat,
|
|
335
|
+
'technology': split_tags,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
def run(self):
|
|
339
|
+
institution: str = self.options.get('institution', '')
|
|
340
|
+
institution_website: str | None = self.options.get('institution-website')
|
|
341
|
+
degree: str = self.options.get('degree', '')
|
|
342
|
+
start: date | None = self.options.get('start')
|
|
343
|
+
finish: date | None = self.options.get('finish')
|
|
344
|
+
technology: list[str] = self.options.get('technology', [])
|
|
345
|
+
|
|
346
|
+
result = []
|
|
347
|
+
if not getattr(self.state.document, '_his_education_seen', False):
|
|
348
|
+
self.state.document._his_education_seen = True
|
|
349
|
+
title = nodes.paragraph('', self.section_title, classes=['his-section-title'])
|
|
350
|
+
result.append(title)
|
|
351
|
+
|
|
352
|
+
container = nodes.container(classes=[self.name])
|
|
353
|
+
|
|
354
|
+
header = nodes.container(classes=[f'{self.name}-header'])
|
|
355
|
+
if institution_website:
|
|
356
|
+
ref = nodes.reference('', institution, refuri=institution_website)
|
|
357
|
+
ref['classes'].append('his-link')
|
|
358
|
+
header.append(nodes.paragraph('', '', ref, classes=[f'{self.name}-institution']))
|
|
359
|
+
else:
|
|
360
|
+
header.append(nodes.paragraph('', institution, classes=[f'{self.name}-institution']))
|
|
361
|
+
|
|
362
|
+
if degree:
|
|
363
|
+
header.append(nodes.paragraph('', degree, classes=[f'{self.name}-degree']))
|
|
364
|
+
|
|
365
|
+
date_range = format_date_range(start, finish)
|
|
366
|
+
if date_range:
|
|
367
|
+
header.append(nodes.paragraph('', date_range, classes=[f'{self.name}-dates']))
|
|
368
|
+
|
|
369
|
+
container.append(header)
|
|
370
|
+
|
|
371
|
+
if technology:
|
|
372
|
+
tech_container = nodes.container(classes=[f'{self.name}-technology'])
|
|
373
|
+
for tag in technology:
|
|
374
|
+
uri = 'https://stackoverflow.com/tags/{}'.format(quote(tag))
|
|
375
|
+
ref = nodes.reference('', tag, refuri=uri)
|
|
376
|
+
ref['classes'].append('his-tag')
|
|
377
|
+
tech_container.append(ref)
|
|
378
|
+
container.append(tech_container)
|
|
379
|
+
|
|
380
|
+
if self.content:
|
|
381
|
+
description = nodes.container(classes=[f'{self.name}-description'])
|
|
382
|
+
self.state.nested_parse(self.content, self.content_offset, description)
|
|
383
|
+
container.append(description)
|
|
384
|
+
|
|
385
|
+
result.append(container)
|
|
386
|
+
return result
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class ProjectDirective(rst.Directive):
|
|
390
|
+
|
|
391
|
+
name = 'his-project'
|
|
392
|
+
section_title = 'Projects'
|
|
393
|
+
|
|
394
|
+
has_content = True
|
|
395
|
+
option_spec = {
|
|
396
|
+
'name': unchanged,
|
|
397
|
+
'url': unchanged,
|
|
398
|
+
'start': date.fromisoformat,
|
|
399
|
+
'finish': date.fromisoformat,
|
|
400
|
+
'technology': split_tags,
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
def run(self):
|
|
404
|
+
name: str = self.options.get('name', '')
|
|
405
|
+
url: str | None = self.options.get('url')
|
|
406
|
+
start: date | None = self.options.get('start')
|
|
407
|
+
finish: date | None = self.options.get('finish')
|
|
408
|
+
technology: list[str] = self.options.get('technology', [])
|
|
409
|
+
|
|
410
|
+
result = []
|
|
411
|
+
if not getattr(self.state.document, '_his_project_seen', False):
|
|
412
|
+
self.state.document._his_project_seen = True
|
|
413
|
+
title = nodes.paragraph('', self.section_title, classes=['his-section-title'])
|
|
414
|
+
result.append(title)
|
|
415
|
+
|
|
416
|
+
container = nodes.container(classes=[self.name])
|
|
417
|
+
|
|
418
|
+
header = nodes.container(classes=[f'{self.name}-header'])
|
|
419
|
+
if url:
|
|
420
|
+
ref = nodes.reference('', name, refuri=url)
|
|
421
|
+
ref['classes'].append('his-link')
|
|
422
|
+
header.append(nodes.paragraph('', '', ref, classes=[f'{self.name}-name']))
|
|
423
|
+
else:
|
|
424
|
+
header.append(nodes.paragraph('', name, classes=[f'{self.name}-name']))
|
|
425
|
+
|
|
426
|
+
date_range = format_date_range(start, finish)
|
|
427
|
+
if date_range:
|
|
428
|
+
header.append(nodes.paragraph('', date_range, classes=[f'{self.name}-dates']))
|
|
429
|
+
|
|
430
|
+
container.append(header)
|
|
431
|
+
|
|
432
|
+
if technology:
|
|
433
|
+
tech_container = nodes.container(classes=[f'{self.name}-technology'])
|
|
434
|
+
for tag in technology:
|
|
435
|
+
uri = 'https://stackoverflow.com/tags/{}'.format(quote(tag))
|
|
436
|
+
ref = nodes.reference('', tag, refuri=uri)
|
|
437
|
+
ref['classes'].append('his-tag')
|
|
438
|
+
tech_container.append(ref)
|
|
439
|
+
container.append(tech_container)
|
|
440
|
+
|
|
441
|
+
if self.content:
|
|
442
|
+
description = nodes.container(classes=[f'{self.name}-description'])
|
|
443
|
+
self.state.nested_parse(self.content, self.content_offset, description)
|
|
444
|
+
container.append(description)
|
|
445
|
+
|
|
446
|
+
result.append(container)
|
|
447
|
+
return result
|
historie/exporters.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from typing import BinaryIO
|
|
3
|
+
|
|
4
|
+
from docutils.core import publish_string
|
|
5
|
+
|
|
6
|
+
from .directives import register_directives
|
|
7
|
+
from .html_writer import HistorieHtmlWriter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = 'registry',
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def export_html(source_rst: str, output_file: BinaryIO):
|
|
14
|
+
register_directives()
|
|
15
|
+
writer = HistorieHtmlWriter()
|
|
16
|
+
html = publish_string(source_rst, writer=writer)
|
|
17
|
+
output_file.write(html)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def export_pdf(source_rst: str, output_file: BinaryIO):
|
|
21
|
+
import weasyprint
|
|
22
|
+
|
|
23
|
+
buffer = io.BytesIO()
|
|
24
|
+
export_html(source_rst, buffer)
|
|
25
|
+
|
|
26
|
+
weasyprint.HTML(file_obj=buffer).write_pdf(output_file)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
registry = {
|
|
30
|
+
'html': export_html,
|
|
31
|
+
'pdf': export_pdf,
|
|
32
|
+
}
|
historie/html_writer.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from docutils import nodes
|
|
4
|
+
from docutils.writers import html5_polyglot
|
|
5
|
+
|
|
6
|
+
from . import utility
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = 'HistorieHtmlWriter',
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HistorieHtmlTranslator(html5_polyglot.HTMLTranslator):
|
|
13
|
+
|
|
14
|
+
def visit_reference(self, node: nodes.reference):
|
|
15
|
+
if 'his-link' in node.get('classes', []):
|
|
16
|
+
refuri = node.get('refuri', '')
|
|
17
|
+
if refuri:
|
|
18
|
+
favicon = utility.fetch_favicon(refuri)
|
|
19
|
+
self.body.append(f'<img src="{favicon}" class="favicon" alt="" />')
|
|
20
|
+
|
|
21
|
+
super().visit_reference(node)
|
|
22
|
+
|
|
23
|
+
def visit_paragraph(self, node):
|
|
24
|
+
if 'his-section-title' in node.get('classes', []):
|
|
25
|
+
self.body.append(self.starttag(node, 'h2', ''))
|
|
26
|
+
self.context.append('</h2>\n')
|
|
27
|
+
else:
|
|
28
|
+
super().visit_paragraph(node)
|
|
29
|
+
|
|
30
|
+
def depart_paragraph(self, node):
|
|
31
|
+
if 'his-section-title' in node.get('classes', []):
|
|
32
|
+
self.body.append(self.context.pop())
|
|
33
|
+
else:
|
|
34
|
+
super().depart_paragraph(node)
|
|
35
|
+
|
|
36
|
+
def visit_list_item(self, node):
|
|
37
|
+
if 'his-contacts-name' in node.get('classes', []):
|
|
38
|
+
self.body.append(self.starttag(node, 'h1', ''))
|
|
39
|
+
self.context.append('</h1>\n')
|
|
40
|
+
else:
|
|
41
|
+
super().visit_list_item(node)
|
|
42
|
+
|
|
43
|
+
def depart_list_item(self, node):
|
|
44
|
+
if 'his-contacts-name' in node.get('classes', []):
|
|
45
|
+
self.body.append(self.context.pop())
|
|
46
|
+
else:
|
|
47
|
+
super().depart_list_item(node)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class HistorieHtmlWriter(html5_polyglot.Writer):
|
|
51
|
+
|
|
52
|
+
config_section = 'historie writer'
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
super().__init__()
|
|
56
|
+
self.translator_class = HistorieHtmlTranslator
|
|
57
|
+
|
|
58
|
+
settings_default_overrides = {
|
|
59
|
+
'stylesheet_path': [str(Path(__file__).parent / 'static' / 'style.css')],
|
|
60
|
+
'embed_stylesheet': True,
|
|
61
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#718096" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<circle cx="12" cy="12" r="10"/>
|
|
3
|
+
<line x1="2" y1="12" x2="22" y2="12"/>
|
|
4
|
+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
|
5
|
+
</svg>
|