Coverage for lobster/html/htmldoc.py: 11%

172 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-26 14:55 +0000

1#!/usr/bin/env python3 

2# 

3# lobster_html_report - Visualise LOBSTER report in HTML 

4# Copyright (C) 2022-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) 

5# 

6# This program is free software: you can redistribute it and/or modify 

7# it under the terms of the GNU Affero General Public License as 

8# published by the Free Software Foundation, either version 3 of the 

9# License, or (at your option) any later version. 

10# 

11# This program is distributed in the hope that it will be useful, but 

12# WITHOUT ANY WARRANTY; without even the implied warranty of 

13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 

14# Affero General Public License for more details. 

15# 

16# You should have received a copy of the GNU Affero General Public 

17# License along with this program. If not, see 

18# <https://www.gnu.org/licenses/>. 

19 

20import html 

21 

22from lobster.html import assets 

23 

24NAVBAR_STICKY_SCRIPT = """ 

25window.onscroll = function() {stickyNavbar()}; 

26 

27var navbar = document.getElementById("navbar"); 

28var sticky = navbar.offsetTop; 

29 

30function stickyNavbar() { 

31 if (window.pageYOffset >= sticky) { 

32 navbar.classList.add("sticky") 

33 } else { 

34 navbar.classList.remove("sticky"); 

35 } 

36} 

37""" 

38 

39 

40class Menu_Item: 

41 def __init__(self, name): 

42 assert isinstance(name, str) 

43 self.name = name 

44 

45 def generate(self, doc): 

46 assert isinstance(doc, Document) 

47 assert False 

48 

49 

50class Menu_Link(Menu_Item): 

51 def __init__(self, name, target): 

52 assert isinstance(target, str) 

53 super().__init__(name) 

54 

55 self.target = target 

56 

57 def generate(self, doc): 

58 assert isinstance(doc, Document) 

59 rv = '<a href="%s">' % self.target 

60 if self.target.startswith("http"): 

61 rv += '<svg class="icon"><use href="#svg-external-link"></use></svg>' + " " 

62 rv += html.escape(self.name) 

63 rv += "</a>" 

64 return [rv] 

65 

66 

67class Dropdown_Menu(Menu_Item): 

68 def __init__(self, name): 

69 super().__init__(name) 

70 self.items = [] 

71 

72 def add_link(self, name, target): 

73 self.items.append(Menu_Link(name, target)) 

74 

75 def generate(self, doc): 

76 assert isinstance(doc, Document) 

77 

78 doc.style["#navbar .dropdown"] = { 

79 "float" : "left", 

80 "overflow" : "hidden", 

81 } 

82 doc.style[".navbar-right .dropdown"] = { 

83 "float" : "right", 

84 "overflow" : "hidden", 

85 } 

86 doc.style[".dropdown .dropbtn"] = { 

87 "font-size" : "inherit", 

88 "border" : "none", 

89 "outline" : "none", 

90 "padding" : "14px 16px", 

91 "background-color" : "inherit", 

92 "color" : "white", 

93 "font-family" : "inherit", 

94 "margin" : "0", 

95 } 

96 doc.style[".dropdown:hover .dropbtn"] = { 

97 "background-color" : "white", 

98 "color" : doc.primary_color, 

99 } 

100 doc.style[".dropdown-content"] = { 

101 "display" : "none", 

102 "position" : "absolute", 

103 "background-color" : doc.primary_color, 

104 "box-shadow" : "0px 8px 16px 0px rgba(0,0,0,0.2)", 

105 "z-index" : "1", 

106 } 

107 doc.style[".navbar-right .dropdown-content"] = { 

108 "right" : "0", 

109 } 

110 doc.style[".dropdown-content a"] = { 

111 "float" : "none", 

112 "color" : "white", 

113 "padding" : "12px 16px", 

114 "text-decoration" : "none", 

115 "display" : "block", 

116 "text-align" : "left", 

117 } 

118 doc.style[".dropdown-content a:hover"] = { 

119 "color" : doc.primary_color, 

120 "background-color" : "white", 

121 } 

122 doc.style[".dropdown:hover .dropdown-content"] = { 

123 "display" : "flex", 

124 "flex-direction" : "column", 

125 } 

126 doc.style[".sticky .dropdown-content"] = { 

127 "position" : "fixed", 

128 } 

129 

130 rv = ['<div class="dropdown">'] 

131 rv.append('<button class="dropbtn">%s%s</button>' % 

132 (html.escape(self.name), 

133 '<svg class="icon"><use href="#svg-chevron-down"></use></svg>')) 

134 rv.append('<div class="dropdown-content">') 

135 for item in self.items: 

136 rv += item.generate(doc) 

137 rv.append("</div>") 

138 rv.append("</div>") 

139 return rv 

140 

141 

142class Navigation_Bar: 

143 def __init__(self): 

144 self.left_items = [] 

145 self.right_items = [] 

146 

147 def add_link(self, name, target, alignment="left"): 

148 assert alignment in ("left", "right") 

149 

150 item = Menu_Link(name, target) 

151 if alignment == "left": 

152 self.left_items.append(item) 

153 else: 

154 self.right_items.append(item) 

155 

156 def add_dropdown(self, name, alignment="left"): 

157 assert alignment in ("left", "right") 

158 

159 menu = Dropdown_Menu(name) 

160 if alignment == "left": 

161 self.left_items.append(menu) 

162 else: 

163 self.right_items.append(menu) 

164 

165 return menu 

166 

167 def generate(self, doc): 

168 assert isinstance(doc, Document) 

169 

170 doc.style["#navbar"] = { 

171 "overflow" : "hidden", 

172 "background-color" : doc.primary_color, 

173 } 

174 if self.right_items: 

175 doc.style[".navbar-right"] = { 

176 "float" : "right", 

177 } 

178 doc.style["#navbar a"] = { 

179 "float" : "left", 

180 "display" : "block", 

181 "color" : "white", 

182 "padding" : "14px", 

183 "text-decoration" : "none", 

184 } 

185 doc.style["#navbar a:hover"] = { 

186 "background-color" : "white", 

187 "color" : doc.primary_color, 

188 } 

189 doc.style[".sticky"] = { 

190 "position" : "fixed", 

191 "top" : "0", 

192 "width" : "100%", 

193 } 

194 doc.style[".sticky + .htmlbody"] = { 

195 "padding-top" : "60px", 

196 } 

197 

198 doc.scripts.append(NAVBAR_STICKY_SCRIPT) 

199 

200 rv = [] 

201 

202 rv.append('<div id="navbar">') 

203 for item in self.left_items: 

204 rv += item.generate(doc) 

205 if self.right_items: 

206 rv.append('<div class="navbar-right">') 

207 for item in self.right_items: 

208 rv += item.generate(doc) 

209 rv.append('</div>') 

210 rv.append('</div>') 

211 

212 return rv 

213 

214 

215class Document: 

216 def __init__(self, title, subtitle): 

217 assert isinstance(title, str) 

218 assert isinstance(subtitle, str) 

219 self.title = title 

220 self.subtitle = subtitle 

221 

222 self.primary_color = "#009ada" 

223 

224 self.navbar = Navigation_Bar() 

225 self.style = { 

226 "html" : {"scroll-padding-top": "5em"}, 

227 "body" : {"margin": "0"}, 

228 ".title" : { 

229 "background-color" : self.primary_color, 

230 "color" : "white", 

231 "padding" : "0.5em", 

232 "margin" : "0" 

233 }, 

234 "h1" : { 

235 "padding" : "0", 

236 "margin" : "0" 

237 }, 

238 "h2" : { 

239 "padding" : "0.5em", 

240 "margin" : "0", 

241 "border-bottom" : "0.25em solid %s" % self.primary_color, 

242 "text-align" : "right", 

243 }, 

244 ".content" : { 

245 "padding" : "0.5em", 

246 }, 

247 ".icon" : { 

248 "width": "24px", 

249 "height": "24px", 

250 "vertical-align": "middle", 

251 } 

252 } 

253 self.scripts = [] 

254 self.body = [] 

255 self.css = [] 

256 

257 def add_line(self, line): 

258 assert isinstance(line, str) 

259 if len(self.body) == 0: 

260 self.body.append('<div class="content">') 

261 self.body.append(line) 

262 

263 def add_heading(self, level, text, anchor=None): 

264 assert isinstance(level, int) 

265 assert isinstance(text, str) 

266 assert 2 <= level <= 7 

267 assert anchor is None or isinstance(anchor, str) 

268 

269 if level == 2 and self.body: 

270 self.body.append("</div>") 

271 

272 if anchor is None: 

273 self.body.append("<h%u>%s</h%u>" % (level, 

274 text, 

275 level)) 

276 else: 

277 self.body.append('<h%u id="sec-%s">%s</h%u>' % 

278 (level, 

279 anchor, 

280 text, 

281 level)) 

282 

283 if level == 2: 

284 self.body.append('<div class="content">') 

285 

286 def render(self): 

287 navbar_content = self.navbar.generate(self) 

288 

289 rv = [ 

290 "<!DOCTYPE html>", 

291 "<html>", 

292 "<head>", 

293 "<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>", 

294 "<title>%s</title>" % html.escape(self.title), 

295 "<style>" 

296 ] 

297 for elem, style in self.style.items(): 

298 rv.append("%s {" % elem) 

299 for attr, value in style.items(): 

300 rv.append(" %s: %s;" % (attr, value)) 

301 rv.append("}") 

302 

303 # add css files that are appended to self.files 

304 for css_file in self.css: 

305 rv.append(css_file) 

306 rv.append("</style>") 

307 rv.append("</head>") 

308 rv.append("<body>") 

309 

310 rv.append('<div class="title">') 

311 rv.append("<h1>%s</h1>" % html.escape(self.title)) 

312 rv.append('<div class="subtitle">%s</div>' % 

313 html.escape(self.subtitle)) 

314 rv.append('</div>') 

315 

316 rv += navbar_content 

317 

318 rv.append('<div class="htmlbody">') 

319 rv.append('<svg style="display: none;">') 

320 rv.append('<defs>') 

321 rv.append('<symbol id="svg-check-square" viewBox="0 0 24 24">') 

322 rv.append(assets.SVG_CHECK_SQUARE) 

323 rv.append('</symbol>') 

324 rv.append('</defs>') 

325 rv.append('</svg>') 

326 

327 rv.append('<svg style="display: none;">') 

328 rv.append('<defs>') 

329 rv.append('<symbol id="svg-alert-triangle" viewBox="0 0 24 24">') 

330 rv.append(assets.SVG_ALERT_TRIANGLE) 

331 rv.append('</symbol>') 

332 rv.append('</defs>') 

333 rv.append('</svg>') 

334 

335 rv.append('<svg style="display: none;">') 

336 rv.append('<defs>') 

337 rv.append('<symbol id="svg-external-link" viewBox="0 0 24 24">') 

338 rv.append(assets.SVG_EXTERNAL_LINK) 

339 rv.append('</symbol>') 

340 rv.append('</defs>') 

341 rv.append('</svg>') 

342 

343 rv.append('<svg style="display: none;">') 

344 rv.append('<defs>') 

345 rv.append('<symbol id="svg-chevron-down" viewBox="0 0 24 24">') 

346 rv.append(assets.SVG_CHEVRON_DOWN) 

347 rv.append('</symbol>') 

348 rv.append('</defs>') 

349 rv.append('</svg>') 

350 

351 rv += self.body 

352 rv.append('</div>') 

353 rv.append('</div>') 

354 

355 for script in self.scripts: 

356 rv.append("<script>") 

357 rv += script.splitlines() 

358 rv.append("</script>") 

359 

360 rv.append("</body>") 

361 rv.append("</html>") 

362 

363 return "\n".join(rv)