|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the QtHelp generator for the builtin documentation |
|
8 generator. |
|
9 """ |
|
10 |
|
11 import sys |
|
12 import os |
|
13 import shutil |
|
14 import subprocess # secok |
|
15 |
|
16 from Utilities import ( |
|
17 joinext, html_encode, getQtBinariesPath, generateQtToolName |
|
18 ) |
|
19 |
|
20 HelpCollection = r"""<?xml version="1.0" encoding="utf-8" ?> |
|
21 <QHelpCollectionProject version="1.0"> |
|
22 <docFiles> |
|
23 <register> |
|
24 <file>{helpfile}</file> |
|
25 </register> |
|
26 </docFiles> |
|
27 </QHelpCollectionProject> |
|
28 """ |
|
29 |
|
30 HelpProject = r"""<?xml version="1.0" encoding="UTF-8"?> |
|
31 <QtHelpProject version="1.0"> |
|
32 <namespace>{namespace}</namespace> |
|
33 <virtualFolder>{folder}</virtualFolder> |
|
34 <customFilter name="{filter_name}"> |
|
35 {filter_attributes} |
|
36 </customFilter> |
|
37 <filterSection> |
|
38 {filter_attributes} |
|
39 <toc> |
|
40 {sections} |
|
41 </toc> |
|
42 <keywords> |
|
43 {keywords} |
|
44 </keywords> |
|
45 <files> |
|
46 {files} |
|
47 </files> |
|
48 </filterSection> |
|
49 </QtHelpProject> |
|
50 """ |
|
51 |
|
52 HelpProjectFile = 'source.qhp' |
|
53 HelpHelpFile = 'source.qch' |
|
54 HelpCollectionProjectFile = 'source.qhcp' |
|
55 HelpCollectionFile = 'collection.qhc' |
|
56 |
|
57 |
|
58 class QtHelpGenerator: |
|
59 """ |
|
60 Class implementing the QtHelp generator for the builtin documentation |
|
61 generator. |
|
62 """ |
|
63 def __init__(self, htmlDir, |
|
64 outputDir, namespace, virtualFolder, filterName, |
|
65 filterAttributes, title, createCollection): |
|
66 """ |
|
67 Constructor |
|
68 |
|
69 @param htmlDir directory containing the HTML files (string) |
|
70 @param outputDir output directory for the files (string) |
|
71 @param namespace namespace to be used (string) |
|
72 @param virtualFolder virtual folder to be used (string) |
|
73 @param filterName name of the custom filter (string) |
|
74 @param filterAttributes ':' separated list of filter attributes |
|
75 (string) |
|
76 @param title title to be used for the generated help (string) |
|
77 @param createCollection flag indicating the generation of the |
|
78 collection files (boolean) |
|
79 """ |
|
80 self.htmlDir = htmlDir |
|
81 self.outputDir = outputDir |
|
82 self.namespace = namespace |
|
83 self.virtualFolder = virtualFolder |
|
84 self.filterName = filterName |
|
85 self.filterAttributes = ( |
|
86 filterAttributes and filterAttributes.split(':') or [] |
|
87 ) |
|
88 self.relPath = os.path.relpath(self.htmlDir, self.outputDir) |
|
89 self.title = title |
|
90 self.createCollection = createCollection |
|
91 |
|
92 self.packages = { |
|
93 "00index": { |
|
94 "subpackages": {}, |
|
95 "modules": {} |
|
96 } |
|
97 } |
|
98 self.remembered = False |
|
99 self.keywords = [] |
|
100 |
|
101 def remember(self, file, moduleDocument, basename=""): |
|
102 """ |
|
103 Public method to remember a documentation file. |
|
104 |
|
105 @param file The filename to be remembered. (string) |
|
106 @param moduleDocument The ModuleDocument object containing the |
|
107 information for the file. |
|
108 @param basename The basename of the file hierarchy to be documented. |
|
109 The basename is stripped off the filename if it starts with |
|
110 the basename. |
|
111 """ |
|
112 self.remembered = True |
|
113 if basename: |
|
114 file = file.replace(basename, "") |
|
115 |
|
116 if "__init__" in file: |
|
117 dirName = os.path.dirname(file) |
|
118 udir = os.path.dirname(dirName) |
|
119 if udir: |
|
120 upackage = udir.replace(os.sep, ".") |
|
121 try: |
|
122 elt = self.packages[upackage] |
|
123 except KeyError: |
|
124 elt = self.packages["00index"] |
|
125 else: |
|
126 elt = self.packages["00index"] |
|
127 package = dirName.replace(os.sep, ".") |
|
128 elt["subpackages"][package] = moduleDocument.name() |
|
129 |
|
130 self.packages[package] = { |
|
131 "subpackages": {}, |
|
132 "modules": {} |
|
133 } |
|
134 |
|
135 kwEntry = ("{0} (Package)".format(package.split('.')[-1]), |
|
136 joinext("index-{0}".format(package), ".html")) |
|
137 if kwEntry not in self.keywords: |
|
138 self.keywords.append(kwEntry) |
|
139 |
|
140 if moduleDocument.isEmpty(): |
|
141 return |
|
142 |
|
143 package = os.path.dirname(file).replace(os.sep, ".") |
|
144 try: |
|
145 elt = self.packages[package] |
|
146 except KeyError: |
|
147 elt = self.packages["00index"] |
|
148 elt["modules"][moduleDocument.name()] = moduleDocument.name() |
|
149 |
|
150 if "__init__" not in file: |
|
151 kwEntry = ( |
|
152 "{0} (Module)".format(moduleDocument.name().split('.')[-1]), |
|
153 joinext(moduleDocument.name(), ".html")) |
|
154 if kwEntry not in self.keywords: |
|
155 self.keywords.append(kwEntry) |
|
156 for kw in moduleDocument.getQtHelpKeywords(): |
|
157 kwEntry = (kw[0], "{0}{1}".format( |
|
158 joinext(moduleDocument.name(), ".html"), kw[1])) |
|
159 if kwEntry not in self.keywords: |
|
160 self.keywords.append(kwEntry) |
|
161 |
|
162 def __generateSections(self, package, level): |
|
163 """ |
|
164 Private method to generate the sections part. |
|
165 |
|
166 @param package name of the package to process (string) |
|
167 @param level indentation level (integer) |
|
168 @return sections part (string) |
|
169 """ |
|
170 indent = level * ' ' |
|
171 indent1 = indent + ' ' |
|
172 s = indent + '<section title="{0}" ref="{1}">\n'.format( |
|
173 package == "00index" and self.title or package, |
|
174 package == "00index" and |
|
175 joinext("index", ".html") or |
|
176 joinext("index-{0}".format(package), ".html")) |
|
177 for subpack in sorted(self.packages[package]["subpackages"]): |
|
178 s += self.__generateSections(subpack, level + 1) + '\n' |
|
179 for mod in sorted(self.packages[package]["modules"]): |
|
180 s += indent1 + '<section title="{0}" ref="{1}" />\n'.format( |
|
181 mod, joinext(mod, ".html")) |
|
182 s += indent + '</section>' |
|
183 return s |
|
184 |
|
185 def __convertEol(self, txt, newline): |
|
186 """ |
|
187 Private method to convert the newline characters. |
|
188 |
|
189 @param txt text to be converted (string) |
|
190 @param newline newline character to be used (string) |
|
191 @return converted text (string) |
|
192 """ |
|
193 # step 1: normalize eol to '\n' |
|
194 txt = txt.replace("\r\n", "\n").replace("\r", "\n") |
|
195 |
|
196 # step 2: convert to the target eol |
|
197 if newline is None: |
|
198 return txt.replace("\n", os.linesep) |
|
199 elif newline in ["\r", "\r\n"]: |
|
200 return txt.replace("\n", newline) |
|
201 else: |
|
202 return txt |
|
203 |
|
204 def generateFiles(self, basename="", newline=None): |
|
205 """ |
|
206 Public method to generate all index files. |
|
207 |
|
208 @param basename The basename of the file hierarchy to be documented. |
|
209 The basename is stripped off the filename if it starts with |
|
210 the basename. |
|
211 @param newline newline character to be used (string) |
|
212 """ |
|
213 if not self.remembered: |
|
214 sys.stderr.write("No QtHelp to generate.\n") |
|
215 return |
|
216 |
|
217 if basename: |
|
218 basename = basename.replace(os.sep, ".") |
|
219 if not basename.endswith("."): |
|
220 basename = "{0}.".format(basename) |
|
221 |
|
222 sections = self.__generateSections("00index", level=3) |
|
223 filesList = sorted(e for e in os.listdir(self.htmlDir) |
|
224 if e.endswith('.html')) |
|
225 files = "\n".join( |
|
226 [" <file>{0}</file>".format(f) for f in filesList]) |
|
227 filterAttribs = "\n".join( |
|
228 [" <filterAttribute>{0}</filterAttribute>".format(a) |
|
229 for a in sorted(self.filterAttributes)]) |
|
230 keywords = "\n".join( |
|
231 [' <keyword name="{0}" id="{1}" ref="{2}" />'.format( |
|
232 html_encode(kw[0]), html_encode(kw[0]), html_encode(kw[1])) |
|
233 for kw in sorted(self.keywords)]) |
|
234 |
|
235 helpAttribs = { |
|
236 "namespace": self.namespace, |
|
237 "folder": self.virtualFolder, |
|
238 "filter_name": self.filterName, |
|
239 "filter_attributes": filterAttribs, |
|
240 "sections": sections, |
|
241 "keywords": keywords, |
|
242 "files": files, |
|
243 } |
|
244 |
|
245 txt = self.__convertEol(HelpProject.format(**helpAttribs), newline) |
|
246 with open(os.path.join(self.outputDir, HelpProjectFile), "w", |
|
247 encoding="utf-8", newline=newline) as f: |
|
248 f.write(txt) |
|
249 |
|
250 if ( |
|
251 self.createCollection and |
|
252 not os.path.exists(os.path.join( |
|
253 self.outputDir, HelpCollectionProjectFile)) |
|
254 ): |
|
255 collectionAttribs = { |
|
256 "helpfile": HelpHelpFile, |
|
257 } |
|
258 |
|
259 txt = self.__convertEol( |
|
260 HelpCollection.format(**collectionAttribs), newline) |
|
261 with open(os.path.join(self.outputDir, HelpCollectionProjectFile), |
|
262 "w", encoding="utf-8", newline=newline) as f: |
|
263 f.write(txt) |
|
264 |
|
265 sys.stdout.write("QtHelp files written.\n") |
|
266 sys.stdout.write("Generating QtHelp documentation...\n") |
|
267 sys.stdout.flush() |
|
268 sys.stderr.flush() |
|
269 |
|
270 cwd = os.getcwd() |
|
271 # generate the compressed files |
|
272 qhelpgeneratorExe = os.path.join( |
|
273 getQtBinariesPath(), generateQtToolName("qhelpgenerator") |
|
274 ) |
|
275 shutil.copy( |
|
276 os.path.join(self.outputDir, HelpProjectFile), self.htmlDir) |
|
277 os.chdir(self.htmlDir) |
|
278 subprocess.call([ # secok |
|
279 qhelpgeneratorExe, |
|
280 HelpProjectFile, "-o", os.path.join(self.outputDir, HelpHelpFile)]) |
|
281 os.remove(HelpProjectFile) |
|
282 |
|
283 if self.createCollection: |
|
284 sys.stdout.write("Generating QtHelp collection...\n") |
|
285 sys.stdout.flush() |
|
286 sys.stderr.flush() |
|
287 os.chdir(self.outputDir) |
|
288 subprocess.call([ # secok |
|
289 qhelpgeneratorExe, |
|
290 HelpCollectionProjectFile, "-o", HelpCollectionFile]) |
|
291 |
|
292 os.chdir(cwd) |