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

89 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-29 06:41 +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 index = str(register.index) 

133 address_readable = self._to_hex_string(register.address) 

134 else: 

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

136 

137 register_address = self._to_hex_string(4 * register_array_index) 

138 address_increment = self._to_hex_string(4 * array_index_increment) 

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

140 

141 default_value = self._to_hex_string( 

142 self.register_default_value_uint(register=register), num_nibbles=1 

143 ) 

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

145 html = f""" 

146 <tr> 

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

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

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

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

151 <td><p>{default_value}</p></td> 

152 <td> 

153{description} 

154 </td> 

155 </tr>""" 

156 

157 for field in register.fields: 

158 html += self._annotate_field(field) 

159 

160 return html 

161 

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

163 description = field.description 

164 

165 if isinstance(field, Enumeration): 

166 description += """ 

167 

168Can be set to the following values: 

169 

170""" 

171 for element in field.elements: 

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

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

174 description += f"""\ 

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

176""" 

177 

178 description = self._html_translator.translate(description) 

179 

180 if isinstance(field, Integer): 

181 description += f"""\ 

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

183""" 

184 

185 return f""" 

186 <tr> 

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

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

189 <td><p></p></td> 

190 <td><p></p></td> 

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

192 <td> 

193{description} 

194 </td> 

195 </tr>""" 

196 

197 @staticmethod 

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

199 """ 

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

201 The way it shall appear in documentation. 

202 """ 

203 if field.width == 1: 

204 return f"{field.base_index}" 

205 

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

207 

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

209 """ 

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

211 """ 

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

213 

214 @staticmethod 

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

216 """ 

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

218 """ 

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

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

221 

222 if isinstance(field, Enumeration): 

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

224 

225 if isinstance(field, Integer): 

226 return str(field.default_value) 

227 

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