Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1"""HTML generation made easy. 

2 

3* for each HTML element there is a function to wrap attributes and content in it. 

4* additional support for more involved patches of HTML (`details`, `input`, icons) 

5* escaping of HTML elements. 

6 

7""" 

8 

9from config import Config as C, Names as N 

10from control.utils import ( 

11 pick as G, 

12 cap1, 

13 asString, 

14 E, 

15 AMP, 

16 LT, 

17 APOS, 

18 QUOT, 

19 DOLLAR, 

20 ONE, 

21 MINONE, 

22) 

23 

24CW = C.web 

25 

26EMPTY_ELEMENTS = set(CW.emptyElements) 

27ICONS = CW.icons 

28 

29CLASS = "class" 

30 

31 

32class HtmlElement: 

33 """Wrapping of attributes and content into an HTML element.""" 

34 

35 def __init__(self, name): 

36 """## Initialization 

37 

38 An HtmlElement object. 

39 

40 Parameters 

41 ---------- 

42 name: string 

43 See below. 

44 """ 

45 

46 self.name = name 

47 """*string* The element name. 

48 """ 

49 

50 @staticmethod 

51 def atNormal(k): 

52 """Substitute the `cls` attribute name with `class`. """ 

53 

54 return CLASS if k == N.cls else k 

55 

56 @staticmethod 

57 def attStr(atts, addClass=None): 

58 """Stringify attributes. 

59 

60 !!! hint 

61 Attributes with value `True` are represented as bare attributes, without 

62 value. For example: `{open=True}` translates into `open`. 

63 

64 !!! caution 

65 Use the name `cls` to get a `class` attribute inside an HTML element. 

66 The name `class` interferes too much with Python syntax to be usable 

67 as a keyowrd argument. 

68 

69 Parameters 

70 ---------- 

71 atts: dict 

72 A dictionary of attributes. 

73 addClass: string 

74 An extra `class` attribute. If there is already a class attribute 

75 `addClass` will be appended to it. 

76 Otherwise a fresh class attribute will be created. 

77 

78 Returns 

79 ------- 

80 string 

81 The serialzed attributes. 

82 """ 

83 

84 if addClass: 

85 if atts and N.cls in atts: 

86 atts[N.cls] += f" {addClass}" 

87 elif atts: 

88 atts[N.cls] = addClass 

89 else: 

90 atts = dict(cls=addClass) 

91 return E.join( 

92 f""" {HtmlElement.atNormal(k)}""" + (E if v is True else f"""='{v}'""") 

93 for (k, v) in atts.items() 

94 if v is not None 

95 ) 

96 

97 def wrap(self, material, addClass=None, **atts): 

98 """Wraps attributes and content into an element. 

99 

100 !!! caution 

101 No HTML escaping of special characters will take place. 

102 You have to use `control.html.HtmlElements.he` yourself. 

103 

104 Parameters 

105 ---------- 

106 material: string | iterable 

107 The element content. If the material is not a string but another 

108 iterable, the items will be joined by the empty string. 

109 

110 addClass: string 

111 An extra `class` attribute. If there is already a class attribute 

112 `addClass` will be appended to it. 

113 Otherwise a fresh class attribute will be created. 

114 

115 Returns 

116 ------- 

117 string 

118 The serialzed element. 

119 

120 """ 

121 

122 name = self.name 

123 content = asString(material) 

124 attributes = HtmlElement.attStr(atts, addClass=addClass) 

125 return ( 

126 f"""<{name}{attributes}>""" 

127 if name in EMPTY_ELEMENTS 

128 else f"""<{name}{attributes}>{content}</{name}>""" 

129 ) 

130 

131 

132class HtmlElements: 

133 """Wrap specific HTML elements and patterns. 

134 

135 !!! note 

136 Nearly all elements accept an arbitrary supply of attributes 

137 in the `**atts` parameter, which will not further be documented. 

138 """ 

139 

140 @staticmethod 

141 def he(val): 

142 """Escape HTML characters. 

143 

144 The following characters will be replaced by entities: 

145 ``` 

146 & < ' " 

147 ``` 

148 

149 The dollar sign will be wrapped into a `<span>`. 

150 """ 

151 

152 return ( 

153 E 

154 if val is None 

155 else ( 

156 str(val) 

157 .replace(AMP, f"""&{N.amp};""") 

158 .replace(LT, f"""&{N.lt};""") 

159 .replace(APOS, f"""&{N.apos};""") 

160 .replace(QUOT, f"""&{N.quot};""") 

161 .replace(DOLLAR, f"""<{N.span}>{DOLLAR}</{N.span}>""") 

162 ) 

163 ) 

164 

165 @staticmethod 

166 def a(material, href, **atts): 

167 """A. 

168 

169 Hyperlink. 

170 

171 Parameters 

172 ---------- 

173 material: string | iterable 

174 Text of the link. 

175 href: url 

176 Destination of the link. 

177 

178 Returns 

179 ------- 

180 string(html) 

181 """ 

182 

183 return HtmlElement(N.a).wrap(material, href=href, **atts) 

184 

185 @staticmethod 

186 def br(): 

187 """BR. 

188 

189 Line break. 

190 

191 Returns 

192 ------- 

193 string(html) 

194 """ 

195 

196 return HtmlElement(N.br).wrap(E) 

197 

198 @staticmethod 

199 def dd(material, **atts): 

200 """DD. 

201 

202 The definition part of a term. 

203 

204 Parameters 

205 ---------- 

206 material: string | iterable 

207 

208 Returns 

209 ------- 

210 string(html) 

211 """ 

212 

213 return HtmlElement(N.dd).wrap(material, **atts) 

214 

215 @staticmethod 

216 def details(summary, material, itemkey, **atts): 

217 """DETAILS. 

218 

219 Collapsible details element. 

220 

221 Parameters 

222 ---------- 

223 summary: string | iterable 

224 The summary. 

225 material: string | iterable 

226 The expansion. 

227 itemkey: string 

228 Identifier for reference from Javascript. 

229 

230 Returns 

231 ------- 

232 string(html) 

233 """ 

234 

235 content = asString(material) 

236 return HtmlElement(N.details).wrap( 

237 HtmlElement(N.summary).wrap(summary) + content, itemkey=itemkey, **atts 

238 ) 

239 

240 @staticmethod 

241 def detailx( 

242 icons, material, itemkey, openAtts={}, closeAtts={}, **atts, 

243 ): 

244 """detailx. 

245 

246 Collapsible details pseudo element. 

247 

248 Unlike the HTML `details` element, this one allows separate open and close 

249 controls. There is no summary. 

250 

251 !!! warning 

252 The `icon` names must be listed in the web.yaml config file 

253 under the key `icons`. The icon itself is a Unicode character. 

254 

255 !!! hint 

256 The `atts` go to the outermost `div` of the result. 

257 

258 Parameters 

259 ---------- 

260 icons: string | (string, string) 

261 Names of the icons that open and close the element. 

262 itemkey: string 

263 Identifier for reference from Javascript. 

264 openAtts: dict, optinal, `{}` 

265 Attributes for the open icon. 

266 closeAtts: dict, optinal, `{}` 

267 Attributes for the close icon. 

268 

269 Returns 

270 ------- 

271 string(html) 

272 """ 

273 

274 content = asString(material) 

275 (iconOpen, iconClose) = (icons, icons) if type(icons) is str else icons 

276 triggerElements = [ 

277 (HtmlElements.iconx if icon in ICONS else HtmlElements.span)( 

278 icon, itemkey=itemkey, trigger=value, **triggerAtts, 

279 ) 

280 for (icon, value, triggerAtts) in ( 

281 (iconOpen, ONE, openAtts), 

282 (iconClose, MINONE, closeAtts), 

283 ) 

284 ] 

285 return ( 

286 *triggerElements, 

287 HtmlElement(N.div).wrap(content, itemkey=itemkey, body=ONE, **atts), 

288 ) 

289 

290 @staticmethod 

291 def div(material, **atts): 

292 """DIV. 

293 

294 Parameters 

295 ---------- 

296 material: string | iterable 

297 

298 Returns 

299 ------- 

300 string(html) 

301 """ 

302 

303 return HtmlElement(N.div).wrap(material, **atts) 

304 

305 @staticmethod 

306 def dl(items, **atts): 

307 """DL. 

308 

309 Definition list. 

310 

311 Parameters 

312 ---------- 

313 items: iterable of (string, string) 

314 These are the list items, which are term-definition pairs. 

315 

316 Returns 

317 ------- 

318 string(html) 

319 """ 

320 

321 return HtmlElement(N.dl).wrap( 

322 [ 

323 HtmlElement(N.dt).wrap(item[0]) + HtmlElement(N.dd).wrap(item[1]) 

324 for item in items 

325 ], 

326 **atts, 

327 ) 

328 

329 @staticmethod 

330 def dt(material, **atts): 

331 """DT. 

332 

333 Term of a definition. 

334 

335 Parameters 

336 ---------- 

337 material: string | iterable 

338 

339 Returns 

340 ------- 

341 string(html) 

342 """ 

343 

344 return HtmlElement(N.dt).wrap(material, **atts) 

345 

346 @staticmethod 

347 def h(level, material, **atts): 

348 """H1, H2, H3, H4, H5, H6. 

349 

350 Parameters 

351 ---------- 

352 level: int 

353 The heading level. 

354 material: string | iterable 

355 The heading content. 

356 

357 Returns 

358 ------- 

359 string(html) 

360 """ 

361 

362 return HtmlElement(f"{N.h}{level}").wrap(material, **atts) 

363 

364 @staticmethod 

365 def icon(icon, asChar=False, **atts): 

366 """icon. 

367 

368 Pseudo element for an icon. 

369 

370 !!! warning 

371 The `icon` names must be listed in the web.yaml config file 

372 under the key `icons`. The icon itself is a Unicode character. 

373 

374 Parameters 

375 ---------- 

376 icon: string 

377 Name of the icon. 

378 asChar: boolean, optional, `False` 

379 If `True`, just output the icon character. 

380 Otherwise, wrap it in a `<span>` with all 

381 attributes that might have been passed. 

382 

383 Returns 

384 ------- 

385 string(html) 

386 """ 

387 

388 iconChar = G(ICONS, icon, default=ICONS[N.noicon]) 

389 if asChar: 

390 return G(ICONS, icon, default=ICONS[N.noicon]) 

391 addClass = f"{N.symbol} i-{icon} " 

392 return HtmlElement(N.span).wrap(iconChar, addClass=addClass, **atts) 

393 

394 @staticmethod 

395 def iconx(icon, href=None, **atts): 

396 """iconx. 

397 

398 Pseudo element for a clickable icon. 

399 It will be wrapped in an `<a href="...">...</a>` element or a <span...> 

400 if `href` is `None`. 

401 

402 If `href` is the empty string, the element will still be wrapped in 

403 an `<a ...>` element, but without a `href` attribute. 

404 

405 !!! warning 

406 The `icon` names must be listed in the web.yaml config file 

407 under the key `icons`. The icon itself is a Unicode character. 

408 

409 Parameters 

410 ---------- 

411 icon: string 

412 Name of the icon. 

413 href: url, optional, `None` 

414 Destination of the icon when clicked. 

415 Will be left out when equal to the empty string. 

416 

417 Returns 

418 ------- 

419 string(html) 

420 """ 

421 

422 iconChar = G(ICONS, icon, default=ICONS[N.noicon]) 

423 addClass = f"{N.icon} i-{icon} " 

424 if href: 424 ↛ 425line 424 didn't jump to line 425, because the condition on line 424 was never true

425 atts["href"] = href 

426 

427 return HtmlElement(N.span if href is None else N.a).wrap( 

428 iconChar, addClass=addClass, **atts 

429 ) 

430 

431 @staticmethod 

432 def iconr(itemKey, tag, msg=None): 

433 """iconr. 

434 

435 Refresh icon. 

436 Special case of `iconx`, but used for refreshing an element. 

437 

438 !!! warning 

439 The `icon` names must be listed in the web.yaml config file 

440 under the key `icons`. The icon itself is a Unicode character. 

441 

442 Parameters 

443 ---------- 

444 itemkey: string 

445 Identifier for reference from Javascript. 

446 tag: string 

447 Attribute `tag`, used by Javascript for scrolling to this 

448 element after the refresh. It is meant to distinhuish it from 

449 other refresh icons for the same element. 

450 msg: string, optional, `None` 

451 Message for in the tooltip. 

452 

453 Returns 

454 ------- 

455 string(html) 

456 """ 

457 

458 if msg is None: 458 ↛ 459line 458 didn't jump to line 459, because the condition on line 458 was never true

459 msg = E 

460 return HtmlElements.iconx( 

461 N.refresh, 

462 cls="small", 

463 action=N.refresh, 

464 title=f"""{cap1(N.refresh)} {msg}""", 

465 targetkey=itemKey, 

466 tag=tag, 

467 ) 

468 

469 @staticmethod 

470 def img(src, href=None, title=None, imgAtts={}, **atts): 

471 """IMG. 

472 

473 Image element. 

474 

475 !!! note 

476 The `atts` go to the outer element, which is either `<img>` if it is 

477 not further wrapped, or `<a>`. 

478 The `imgAtts` only go to the `<img>` element. 

479 

480 Parameters 

481 ---------- 

482 src: url 

483 The url of the image. 

484 href: url, optional, `None` 

485 The destination to navigate to if the image is clicked. 

486 The images is then wrapped in an `<a>` element. 

487 If missing, the image is not wrapped further. 

488 title: string, optional, `None` 

489 Tooltip. 

490 imgAtts: dict, optional `{}` 

491 Attributes that go to the `<img>` element. 

492 

493 Returns 

494 ------- 

495 string(html) 

496 """ 

497 

498 return ( 

499 HtmlElements.a( 

500 HtmlElement(N.img).wrap(E, src=src, **imgAtts), 

501 href, 

502 title=title, 

503 **atts, 

504 ) 

505 if href 

506 else HtmlElement(N.img).wrap(E, src=src, title=title, **imgAtts, **atts) 

507 ) 

508 

509 @staticmethod 

510 def input(material, **atts): 

511 """INPUT. 

512 

513 The element to receive types user input. 

514 

515 !!! caution 

516 Do not use this for checkboxes. Use 

517 `control.html.HtmlElements.checkbox` instead. 

518 

519 Parameters 

520 ---------- 

521 material: string | iterable 

522 This goes into the `value` attribute of the element, after HTML escaping. 

523 

524 Returns 

525 ------- 

526 string(html) 

527 """ 

528 

529 content = asString(material) 

530 return HtmlElement(N.input).wrap(E, value=HtmlElements.he(content), **atts) 

531 

532 @staticmethod 

533 def join(material): 

534 """fragment. 

535 

536 This is a pseudo element. 

537 The material will be joined together, without wrapping it in an element. 

538 There are no attributes. 

539 

540 Parameters 

541 ---------- 

542 material: string | iterable 

543 

544 Returns 

545 ------- 

546 string(html) 

547 """ 

548 

549 return asString(material) 

550 

551 @staticmethod 

552 def checkbox(var, **atts): 

553 """INPUT type=checkbox. 

554 

555 The element to receive user clicks. 

556 

557 Parameters 

558 ---------- 

559 var: string 

560 The name of an identifier for the element. 

561 

562 Returns 

563 ------- 

564 string(html) 

565 """ 

566 

567 return HtmlElement(N.input).wrap( 

568 E, type=N.checkbox, id=var, addClass=N.option, **atts, 

569 ) 

570 

571 @staticmethod 

572 def p(material, **atts): 

573 """P. 

574 

575 Paragraph. 

576 

577 Parameters 

578 ---------- 

579 material: string | iterable 

580 

581 Returns 

582 ------- 

583 string(html) 

584 """ 

585 

586 return HtmlElement(N.p).wrap(material, **atts) 

587 

588 @staticmethod 

589 def script(material, **atts): 

590 """SCRIPT. 

591 

592 Parameters 

593 ---------- 

594 material: string | iterable 

595 The Javascript. 

596 

597 Returns 

598 ------- 

599 string(html) 

600 """ 

601 

602 return HtmlElement(N.script).wrap(material, **atts) 

603 

604 @staticmethod 

605 def span(material, **atts): 

606 """SPAN. 

607 

608 Inline element. 

609 

610 Parameters 

611 ---------- 

612 material: string | iterable 

613 

614 Returns 

615 ------- 

616 string(html) 

617 """ 

618 

619 return HtmlElement(N.span).wrap(material, **atts) 

620 

621 @staticmethod 

622 def table(headers, rows, **atts): 

623 """TABLE. 

624 

625 The table element. 

626 

627 Parameters 

628 ---------- 

629 headers, rows: iterables of iterables 

630 An iterable of rows. 

631 Each row is a tuple: an iterable of cells, and a CSS class for the row. 

632 Each cell is a tuple: material for the cell, and a CSS class for the cell. 

633 

634 !!! note 

635 Cells in normal rows are wrapped in `<td>`, cells in header rows go 

636 into `<th>`. 

637 

638 Returns 

639 ------- 

640 string(html) 

641 """ 

642 

643 th = HtmlElement(N.th).wrap 

644 td = HtmlElement(N.td).wrap 

645 headerMaterial = HtmlElements.wrapTable(headers, th) 

646 rowMaterial = HtmlElements.wrapTable(rows, td) 

647 material = HtmlElement(N.body).wrap(headerMaterial + rowMaterial) 

648 return HtmlElement(N.table).wrap(material, **atts) 

649 

650 @staticmethod 

651 def textarea(material, **atts): 

652 """TEXTAREA. 

653 

654 Input element for larger text, typically Markdown. 

655 

656 Parameters 

657 ---------- 

658 material: string | iterable 

659 

660 Returns 

661 ------- 

662 string(html) 

663 """ 

664 

665 content = asString(material) 

666 return HtmlElement(N.textarea).wrap(content, **atts) 

667 

668 @staticmethod 

669 def wrapTable(data, td): 

670 """Rows and cells. 

671 

672 Parameters 

673 ---------- 

674 data: iterable of iterables. 

675 Rows and cells within them, both with CSS classes. 

676 td: function 

677 Funnction for wrapping the cells, typically boiling down 

678 to wrapping them in either `<th>` or `<td>` elements. 

679 

680 Returns 

681 ------- 

682 string(html) 

683 """ 

684 

685 tr = HtmlElement(N.tr).wrap 

686 material = [] 

687 for (rowData, rowAtts) in data: 

688 rowMaterial = [] 

689 for (cellData, cellAtts) in rowData: 

690 rowMaterial.append(td(cellData, **cellAtts)) 

691 material.append(tr(rowMaterial, **rowAtts)) 

692 return material