Coverage for hdl_registers/generator/html/register_table.py: 93%

88 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-12 11:11 +0000

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the hdl-registers project, an HDL register generator fast enough to run 

5# in real time. 

6# https://hdl-registers.com 

7# https://github.com/hdl-registers/hdl-registers 

8# -------------------------------------------------------------------------------------------------- 

9 

10from __future__ import annotations 

11 

12from typing import TYPE_CHECKING, Any 

13 

14from hdl_registers.field.bit import Bit 

15from hdl_registers.field.bit_vector import BitVector 

16from hdl_registers.field.enumeration import Enumeration 

17from hdl_registers.field.integer import Integer 

18from hdl_registers.register import Register 

19 

20from .html_generator_common import HtmlGeneratorCommon 

21from .html_translator import HtmlTranslator 

22 

23if TYPE_CHECKING: 

24 from pathlib import Path 

25 

26 from hdl_registers.field.register_field import RegisterField 

27 from hdl_registers.register_array import RegisterArray 

28 from hdl_registers.register_list import RegisterList 

29 

30 

31class HtmlRegisterTableGenerator(HtmlGeneratorCommon): 

32 """ 

33 Generate HTML code with register information in a table. 

34 See the :ref:`generator_html` article for usage details. 

35 """ 

36 

37 __version__ = "1.0.0" 

38 

39 SHORT_DESCRIPTION = "HTML register table" 

40 

41 @property 

42 def output_file(self) -> Path: 

43 """ 

44 Result will be placed in this file. 

45 """ 

46 return self.output_folder / f"{self.name}_register_table.html" 

47 

48 def __init__(self, register_list: RegisterList, output_folder: Path) -> None: 

49 super().__init__(register_list=register_list, output_folder=output_folder) 

50 

51 self._html_translator = HtmlTranslator() 

52 

53 def get_code( 

54 self, 

55 **kwargs: Any, # noqa: ANN401, ARG002 

56 ) -> str: 

57 if not self.register_list.register_objects: 

58 return "" 

59 

60 html = """\ 

61<table> 

62<thead> 

63 <tr> 

64 <th>Name</th> 

65 <th>Index</th> 

66 <th>Address</th> 

67 <th>Mode</th> 

68 <th>Default value</th> 

69 <th>Description</th> 

70 </tr> 

71</thead> 

72<tbody>""" 

73 

74 for register_object in self.iterate_register_objects(): 

75 if isinstance(register_object, Register): 

76 html += self._annotate_register(register_object) 

77 else: 

78 html += self._annotate_register_array(register_object) 

79 

80 html += """ 

81</tbody> 

82</table>""" 

83 

84 return html 

85 

86 @staticmethod 

87 def _to_hex_string(value: int, num_nibbles: int = 4) -> str: 

88 """ 

89 Convert an integer value to a hexadecimal string. E.g. "0x1000". 

90 """ 

91 if value < 0: 

92 return "-" 

93 

94 formatting_string = f"0x{ :0{num_nibbles}X} " 

95 return formatting_string.format(value) 

96 

97 def _annotate_register_array(self, register_object: RegisterArray) -> str: 

98 description = self._html_translator.translate(register_object.description) 

99 html = f""" 

100 <tr> 

101 <td class="array_header" colspan=5> 

102 <p> 

103 Register array <strong>{register_object.name}</strong>, \ 

104repeated {register_object.length} times. 

105 Iterator <span class="formula">i &isin; [0, {register_object.length - 1}]</span>. 

106 </p> 

107 </td> 

108 <td class="array_header"> 

109{description} 

110 </td> 

111 </tr>""" 

112 array_index_increment = len(register_object.registers) 

113 for register in register_object.registers: 

114 register_index = register_object.base_index + register.index 

115 html += self._annotate_register(register, register_index, array_index_increment) 

116 

117 html += f""" 

118 <tr> 

119 <td colspan="6" class="array_footer"> 

120 <p>End register array <strong>{register_object.name}.</strong></p> 

121 </td> 

122 </tr>""" 

123 return html 

124 

125 def _annotate_register( 

126 self, 

127 register: Register, 

128 register_array_index: int | None = None, 

129 array_index_increment: int | None = None, 

130 ) -> str: 

131 if register_array_index is None: 

132 address_readable = self._to_hex_string(register.address) 

133 index = str(register.address // 4) 

134 else: 

135 register_address = self._to_hex_string(4 * register_array_index) 

136 address_increment = self._to_hex_string(4 * array_index_increment) 

137 address_readable = f"{register_address} + i &times; {address_increment}" 

138 

139 index = f"{register_array_index} + i &times; {array_index_increment}" 

140 

141 description = self._html_translator.translate(register.description) 

142 html = f""" 

143 <tr> 

144 <td><p><strong>{register.name}</strong></p></td> 

145 <td><p>{index}</p></td> 

146 <td><p>{address_readable}</p></td> 

147 <td><p>{register.mode.name}</p></td> 

148 <td><p>{self._to_hex_string(register.default_value, num_nibbles=1)}</p></td> 

149 <td> 

150{description} 

151 </td> 

152 </tr>""" 

153 

154 for field in register.fields: 

155 html += self._annotate_field(field) 

156 

157 return html 

158 

159 def _annotate_field(self, field: RegisterField) -> str: 

160 description = field.description 

161 

162 if isinstance(field, Enumeration): 

163 description += """ 

164 

165Can be set to the following values: 

166 

167""" 

168 for element in field.elements: 

169 # Indent so multi-line description is part of the bullet point. 

170 element_description = element.description.strip().replace("\n", "\n ") 

171 description += f"""\ 

172* ``{element.name}`` ({element.value}): {element_description} 

173""" 

174 

175 description = self._html_translator.translate(description) 

176 

177 if isinstance(field, Integer): 

178 description += f"""\ 

179<p>Valid numeric range: [{field.min_value} &ndash; {field.max_value}].</p> 

180""" 

181 

182 return f""" 

183 <tr> 

184 <td><p>&nbsp;&nbsp;<em>{field.name}</em></p></td> 

185 <td><p>&nbsp;&nbsp;{self._field_range(field=field)}</p></td> 

186 <td><p></p></td> 

187 <td><p></p></td> 

188 <td>{self._get_field_default_value(field=field)}</td> 

189 <td> 

190{description} 

191 </td> 

192 </tr>""" 

193 

194 @staticmethod 

195 def _field_range(field: RegisterField) -> str: 

196 """ 

197 Return the bits that this field occupies in a readable format. 

198 The way it shall appear in documentation. 

199 """ 

200 if field.width == 1: 

201 return f"{field.base_index}" 

202 

203 return f"{field.base_index + field.width - 1}:{field.base_index}" 

204 

205 def _get_field_default_value(self, field: RegisterField) -> str: 

206 """ 

207 A human-readable string representation of the default value. 

208 """ 

209 return self._html_translator.translate(self._field_default_value(field=field)) 

210 

211 @staticmethod 

212 def _field_default_value(field: RegisterField) -> str: 

213 """ 

214 A human-readable string representation of the default value. 

215 """ 

216 if isinstance(field, (Bit, BitVector)): 

217 return f"0b{field.default_value}" 

218 

219 if isinstance(field, Enumeration): 

220 return f"``{field.default_value.name}``" 

221 

222 if isinstance(field, Integer): 

223 return str(field.default_value) 

224 

225 raise ValueError(f"Unknown field: {field}")