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

63 statements  

« prev     ^ index     » next       coverage.py v7.10.5, created at 2025-08-25 22:35 +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 

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 document. If None, includes all 657 colors. 

53 

54 Returns: 

55 RTF color table string 

56 """ 

57 from ..services.color_service import color_service 

58 

59 return color_service.generate_rtf_color_table(used_colors) 

60 

61 @staticmethod 

62 def generate_page_settings( 

63 width: float, 

64 height: float, 

65 margins: Sequence[float], 

66 orientation: str = "portrait", 

67 ) -> str: 

68 """Generate RTF page settings. 

69 

70 Args: 

71 width: Page width in inches 

72 height: Page height in inches 

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

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

75 

76 Returns: 

77 RTF page settings string 

78 """ 

79 from ..row import Utils 

80 

81 # Convert to twips 

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

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

84 

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

86 

87 # Add landscape command if needed 

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

89 

90 return ( 

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

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

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

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

95 ) 

96 

97 @staticmethod 

98 def generate_page_break() -> str: 

99 """Generate RTF page break.""" 

100 return "\\page" 

101 

102 @staticmethod 

103 def generate_paragraph_break() -> str: 

104 """Generate RTF paragraph break.""" 

105 return "\\par" 

106 

107 @staticmethod 

108 def generate_line_break() -> str: 

109 """Generate RTF line break.""" 

110 return "\\line" 

111 

112 

113class RTFDocumentAssembler: 

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

115 

116 def __init__(self): 

117 self.syntax = RTFSyntaxGenerator() 

118 

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

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

121 

122 Args: 

123 components: Dictionary containing document components 

124 

125 Returns: 

126 Complete RTF document string 

127 """ 

128 parts = [] 

129 

130 # Document start 

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

132 

133 # Font table 

134 if "fonts" in components: 

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

136 

137 # Page settings 

138 if "page_settings" in components: 

139 settings = components["page_settings"] 

140 parts.append( 

141 self.syntax.generate_page_settings( 

142 settings["width"], 

143 settings["height"], 

144 settings["margins"], 

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

146 ) 

147 ) 

148 

149 # Content sections 

150 content_sections = [ 

151 "page_header", 

152 "page_footer", 

153 "title", 

154 "subline", 

155 "column_headers", 

156 "body", 

157 "footnotes", 

158 "sources", 

159 ] 

160 

161 for section in content_sections: 

162 if section in components and components[section]: 

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

164 parts.extend(components[section]) 

165 else: 

166 parts.append(components[section]) 

167 

168 # Document end 

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

170 

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

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