Coverage for src / rtflite / rtf / syntax.py: 67%

63 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-28 05:09 +0000

1"""RTF syntax generation utilities.""" 

2 

3from collections.abc import Mapping, Sequence 

4from typing import Any 

5 

6from ..core.constants import RTFConstants 

7 

8 

9class RTFSyntaxGenerator: 

10 """Central RTF syntax generator for common RTF operations.""" 

11 

12 @staticmethod 

13 def generate_document_start() -> str: 

14 """Generate RTF document start code.""" 

15 return "{\\rtf1\\ansi\\deff0" 

16 

17 @staticmethod 

18 def generate_document_end() -> str: 

19 """Generate RTF document end code.""" 

20 return "}" 

21 

22 @staticmethod 

23 def generate_font_table() -> str: 

24 """Generate RTF font table using system fonts. 

25 

26 Returns: 

27 RTF font table string 

28 """ 

29 from ..row import Utils 

30 

31 font_types = Utils._font_type() 

32 font_rtf = [f"\\f{i}" for i in range(10)] 

33 font_style = font_types["style"] 

34 font_name = font_types["name"] 

35 font_charset = font_types["charset"] 

36 

37 font_table = RTFConstants.Control.FONT_TABLE_START 

38 for rtf, style, name, charset in zip( 

39 font_rtf, font_style, font_name, font_charset, strict=True 

40 ): 

41 font_table = ( 

42 font_table + "{" + rtf + style + charset + "\\fprq2 " + name + ";}\n" 

43 ) 

44 font_table += "}" 

45 return font_table 

46 

47 @staticmethod 

48 def generate_color_table(used_colors: Sequence[str] | None = None) -> str: 

49 """Generate RTF color table using comprehensive 657-color support. 

50 

51 Args: 

52 used_colors: List of color names used in the document. 

53 If None, includes all 657 colors. 

54 

55 Returns: 

56 RTF color table string 

57 """ 

58 from ..services.color_service import color_service 

59 

60 return color_service.generate_rtf_color_table(used_colors) 

61 

62 @staticmethod 

63 def generate_page_settings( 

64 width: float, 

65 height: float, 

66 margins: Sequence[float], 

67 orientation: str = "portrait", 

68 ) -> str: 

69 """Generate RTF page settings. 

70 

71 Args: 

72 width: Page width in inches 

73 height: Page height in inches 

74 margins: Margins [left, right, top, bottom, header, footer] in inches 

75 orientation: Page orientation ('portrait' or 'landscape') 

76 

77 Returns: 

78 RTF page settings string 

79 """ 

80 from ..row import Utils 

81 

82 # Convert to twips 

83 width_twips = int(Utils._inch_to_twip(width)) 

84 height_twips = int(Utils._inch_to_twip(height)) 

85 

86 margin_twips = [int(Utils._inch_to_twip(m)) for m in margins] 

87 

88 # Add landscape command if needed 

89 landscape_cmd = "\\landscape " if orientation == "landscape" else "" 

90 

91 return ( 

92 f"\\paperw{width_twips}\\paperh{height_twips}{landscape_cmd}\n" 

93 f"\\margl{margin_twips[0]}\\margr{margin_twips[1]}" 

94 f"\\margt{margin_twips[2]}\\margb{margin_twips[3]}" 

95 f"\\headery{margin_twips[4]}\\footery{margin_twips[5]}" 

96 ) 

97 

98 @staticmethod 

99 def generate_page_break() -> str: 

100 """Generate RTF page break.""" 

101 return "\\page" 

102 

103 @staticmethod 

104 def generate_paragraph_break() -> str: 

105 """Generate RTF paragraph break.""" 

106 return "\\par" 

107 

108 @staticmethod 

109 def generate_line_break() -> str: 

110 """Generate RTF line break.""" 

111 return "\\line" 

112 

113 

114class RTFDocumentAssembler: 

115 """Assembles complete RTF documents from components.""" 

116 

117 def __init__(self): 

118 self.syntax = RTFSyntaxGenerator() 

119 

120 def assemble_document(self, components: Mapping[str, Any]) -> str: 

121 """Assemble a complete RTF document from components. 

122 

123 Args: 

124 components: Dictionary containing document components 

125 

126 Returns: 

127 Complete RTF document string 

128 """ 

129 parts = [] 

130 

131 # Document start 

132 parts.append(self.syntax.generate_document_start()) 

133 

134 # Font table 

135 if "fonts" in components: 

136 parts.append(self.syntax.generate_font_table(components["fonts"])) 

137 

138 # Page settings 

139 if "page_settings" in components: 

140 settings = components["page_settings"] 

141 parts.append( 

142 self.syntax.generate_page_settings( 

143 settings["width"], 

144 settings["height"], 

145 settings["margins"], 

146 settings.get("orientation", "portrait"), 

147 ) 

148 ) 

149 

150 # Content sections 

151 content_sections = [ 

152 "page_header", 

153 "page_footer", 

154 "title", 

155 "subline", 

156 "column_headers", 

157 "body", 

158 "footnotes", 

159 "sources", 

160 ] 

161 

162 for section in content_sections: 

163 if section in components and components[section]: 

164 if isinstance(components[section], list): 

165 parts.extend(components[section]) 

166 else: 

167 parts.append(components[section]) 

168 

169 # Document end 

170 parts.append(self.syntax.generate_document_end()) 

171 

172 # Join with newlines, filtering out None/empty values 

173 return "\n".join(str(part) for part in parts if part)