Skip to content

Commit 4730570

Browse files
committed
Initial release
1 parent 7c18f38 commit 4730570

File tree

3 files changed

+1317
-0
lines changed

3 files changed

+1317
-0
lines changed

chm.py

+339
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
from io import StringIO
2+
from contextlib import contextmanager
3+
from os.path import splitext
4+
5+
class Buffer:
6+
def __init__(self, indent=0):
7+
self.buf = StringIO()
8+
self._indent = indent
9+
10+
def line(self, text=None):
11+
if text is not None:
12+
self.buf.write(('\t' * self._indent) + text + '\n')
13+
else:
14+
self.buf.write('\n')
15+
16+
@contextmanager
17+
def indent(self, ind=None):
18+
if ind is None:
19+
ind = self._indent + 1
20+
old_indent = self._indent
21+
self._indent = ind
22+
yield
23+
self._indent = old_indent
24+
25+
def __str__(self):
26+
return self.buf.getvalue()
27+
28+
class Sitemap:
29+
# map from underscore key name to key name in the output file
30+
property_map = {}
31+
32+
def __init__(self, **kwargs):
33+
self.properties = kwargs
34+
self.children = []
35+
36+
def __setitem__(self, key, value):
37+
self.properties[key] = value
38+
39+
def __getitem__(self, key):
40+
return self.properties[key]
41+
42+
def serialize(self):
43+
b = Buffer()
44+
b.line('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">')
45+
b.line('<HTML>')
46+
b.line('<HEAD>')
47+
b.line('<meta name="GENERATOR" content="Microsoft&reg; HTML Help Workshop 4.1">')
48+
b.line('<!-- Sitemap 1.0 -->')
49+
b.line('</HEAD><BODY>')
50+
if len(self.properties):
51+
b.line('<OBJECT type="text/site properties">')
52+
with b.indent():
53+
for k, v in self.properties.items():
54+
if v is not None:
55+
b.line('<param name="{}" value="{}">'.format(self.property_map.get(k, k), v))
56+
b.line('</OBJECT>')
57+
b.line('<UL>')
58+
with b.indent():
59+
for item in self.children:
60+
item.serialize(b)
61+
b.line('</UL>')
62+
b.line('</BODY></HTML>')
63+
print('Writing', self.filename)
64+
with open(self.filename, 'w') as f:
65+
f.write(str(b))
66+
67+
class Toc(Sitemap):
68+
property_map = {
69+
'image_type': 'ImageType',
70+
'window_styles': 'Window Styles',
71+
'font': 'Font'
72+
}
73+
74+
def __init__(self, filename, **kwargs):
75+
kwargs.setdefault('image_type', 'Folder')
76+
super().__init__(**kwargs)
77+
self.filename = filename
78+
79+
def append(self, name, local=None):
80+
item = TocItem(name, local)
81+
self.children.append(item)
82+
return item
83+
84+
class TocItem:
85+
def __init__(self, name, local=None):
86+
if name is None:
87+
raise Exception('Name should not be None')
88+
name = name.strip()
89+
if len(name) == 0:
90+
raise Exception('Name should not be an empty string')
91+
self.name = name
92+
self.local = local
93+
self.children = []
94+
95+
def append(self, name, local=None):
96+
child = TocItem(name, local)
97+
self.children.append(child)
98+
return child
99+
100+
def serialize(self, b):
101+
b.line('<LI> <OBJECT type="text/sitemap">')
102+
with b.indent():
103+
b.line('<param name="Name" value="{}">'.format(self.name))
104+
b.line('<param name="Local" value="{}">'.format(self.local))
105+
b.line('</OBJECT>')
106+
if len(self.children):
107+
b.line('<UL>')
108+
with b.indent():
109+
for child in self.children:
110+
child.serialize(b)
111+
b.line('</UL>')
112+
113+
class Index(Sitemap):
114+
def __init__(self, filename):
115+
super().__init__()
116+
self.filename = filename
117+
self.names = {}
118+
119+
def append(self, name, local, title=None):
120+
if name in self.names:
121+
item = self.names[name]
122+
item.add_local(local, title)
123+
else:
124+
item = IndexItem(name, local, title)
125+
self.children.append(item)
126+
self.names[name] = item
127+
return item
128+
129+
def serialize(self):
130+
"""Make the keywords sorted in list"""
131+
self.children.sort(key=lambda item: item.name.lower())
132+
return super().serialize()
133+
134+
class IndexItem:
135+
def __init__(self, name, local, title=None):
136+
if name is None:
137+
raise Exception('Name should not be None')
138+
name = name.strip()
139+
if len(name) == 0:
140+
raise Exception('Name should not be an empty string')
141+
if local is None:
142+
raise Exception('Invalid local for {}'.format(name))
143+
if isinstance(local, str):
144+
local = [(local, title)]
145+
self.name = name
146+
self.local = local
147+
self.children = []
148+
self.children_names = {}
149+
150+
def append(self, name, local, title=None):
151+
if name in self.children_names:
152+
child = self.children_names[name]
153+
child.add_local(local, title)
154+
else:
155+
child = IndexItem(name, local, title)
156+
self.children.append(child)
157+
self.children_names[name] = child
158+
return child
159+
160+
def add_local(self, local, title=None):
161+
self.local.append((local, title))
162+
163+
def serialize(self, b):
164+
b.line('<LI> <OBJECT type="text/sitemap">')
165+
with b.indent():
166+
b.line('<param name="Name" value="{}">'.format(self.name))
167+
for filename, title in self.local:
168+
if title:
169+
b.line('<param name="Name" value="{}">'.format(title))
170+
b.line('<param name="Local" value="{}">'.format(filename))
171+
b.line('</OBJECT>')
172+
if len(self.children):
173+
b.line('<UL>')
174+
with b.indent():
175+
for child in self.children:
176+
child.serialize(b)
177+
b.line('</UL>')
178+
179+
class Window:
180+
arguments = (
181+
'title',
182+
'contents_file',
183+
'index_file',
184+
'default_topic',
185+
'home',
186+
'jump1',
187+
'jump1_text',
188+
'jump2',
189+
'jump2_text',
190+
'navigation_pane_styles',
191+
'navigation_pane_width',
192+
'buttons',
193+
'initial_position',
194+
'style_flags',
195+
'extended_style_flags',
196+
'window_show_state',
197+
'navigation_pane_closed',
198+
'default_navigation_pane',
199+
'navigation_pane_position',
200+
'id'
201+
)
202+
203+
def __init__(self, project, name, **kwargs):
204+
self.project = project
205+
self.name = name
206+
self.options = kwargs
207+
self.options.setdefault('id', 0)
208+
self.options.setdefault('navigation_pane_styles', '0x2120')
209+
self.options.setdefault('buttons', '0x3006')
210+
211+
def _copy_project_options(self):
212+
keys = {
213+
'contents_file': 'contents_file',
214+
'index_file': 'index_file',
215+
'default_topic': 'default_topic',
216+
'default_topic': 'home'
217+
}
218+
for project_key, window_key in keys.items():
219+
if project_key in self.project:
220+
self.options.setdefault(window_key, self.project[project_key])
221+
222+
def __setitem__(self, key, value):
223+
self.options[key] = value
224+
225+
def __getitem__(self, key):
226+
return self.options[key]
227+
228+
def __str__(self):
229+
self._copy_project_options()
230+
arguments = [self.options.get(arg, '') for arg in self.arguments]
231+
print(arguments)
232+
return '{}={}'.format(
233+
self.name,
234+
','.join(self._quote(arg) for arg in arguments)
235+
)
236+
237+
def _quote(self, val):
238+
if val is None or val == '':
239+
return ''
240+
if isinstance(val, int):
241+
return str(val)
242+
if val.isdigit() or val.startswith('0x'):
243+
return val
244+
return '"{}"'.format(val)
245+
246+
class Project:
247+
def __init__(self, filename, **kwargs):
248+
self.filename = filename
249+
self.files = []
250+
self.options = {
251+
'compatibility': '1.1 or later',
252+
'compiled_file': splitext(filename)[0] + '.chm',
253+
'display_compile_progress': 'No',
254+
'language': '0x409 English (United States)',
255+
'default_window': 'main',
256+
'binary_index': 'No', # with binary index, multi topic keyword will not be displayed
257+
}
258+
self.window = Window(self, 'main')
259+
self.options.update(kwargs)
260+
261+
def __setitem__(self, key, value):
262+
self.options[key] = value
263+
264+
def __getitem__(self, key):
265+
return self.options[key]
266+
267+
def __contains__(self, key):
268+
return key in self.options
269+
270+
def append(self, filename):
271+
self.files.append(filename)
272+
273+
def serialize(self):
274+
b = Buffer()
275+
b.line('[OPTIONS]')
276+
for k, v in sorted(self.options.items()):
277+
if v is not None:
278+
b.line('{}={}'.format(k.replace('_', ' ').capitalize(), v))
279+
b.line()
280+
b.line('[WINDOWS]')
281+
b.line(str(self.window))
282+
b.line()
283+
b.line()
284+
if len(self.files):
285+
b.line('[FILES]')
286+
for file in self.files:
287+
b.line(file)
288+
b.line()
289+
b.line('[INFOTYPES]')
290+
b.line()
291+
print('Writing', self.filename)
292+
with open(self.filename, 'w') as f:
293+
f.write(str(b))
294+
295+
class Chm:
296+
def __init__(self, name, compiled_file=None, default_topic='index.html', title=None):
297+
self.name = name
298+
self.project = Project(name + '.hhp', compiled_file=compiled_file)
299+
self.project['default_topic'] = default_topic
300+
if title:
301+
self.project.window['title'] = title
302+
self._toc = None
303+
self._index = None
304+
305+
@property
306+
def toc(self):
307+
if self._toc is None:
308+
self._toc = Toc(self.name + '.hhc')
309+
self.project['contents_file'] = self.toc.filename
310+
return self._toc
311+
312+
@property
313+
def index(self):
314+
if self._index is None:
315+
self._index = Index(self.name + '.hhk')
316+
self.project['index_file'] = self.index.filename
317+
return self._index
318+
319+
def append(self, filename):
320+
self.project.append(filename)
321+
322+
def save(self):
323+
self.project.serialize()
324+
if self._toc:
325+
self._toc.serialize()
326+
if self._index:
327+
self._index.serialize()
328+
329+
class DocChm(Chm):
330+
"""Chm tailored for documentation with default settings"""
331+
def __init__(self, *args, **kwargs):
332+
super().__init__(*args, **kwargs)
333+
334+
self.project.window['navigation_pane_styles'] = '0x12120' # show menu
335+
self.project.window['buttons'] = '0x10184e' # without toolbar buttons, the font size menu doesn't work
336+
337+
self.toc['image_type'] = None
338+
self.toc['window_styles'] = '0x801627'
339+
self.toc['font'] = 'Tahoma,8,0'

0 commit comments

Comments
 (0)