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

62 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 16:35 +0000

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

2 

3from typing import Any 

4 

5from ..core.constants import RTFConstants 

6 

7 

8class RTFSyntaxGenerator: 

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

10 

11 @staticmethod 

12 def generate_document_start() -> str: 

13 """Generate RTF document start code.""" 

14 return "{\\rtf1\\ansi\\deff0" 

15 

16 @staticmethod 

17 def generate_document_end() -> str: 

18 """Generate RTF document end code.""" 

19 return "}" 

20 

21 @staticmethod 

22 def generate_font_table() -> str: 

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

24 

25 Returns: 

26 RTF font table string 

27 """ 

28 from ..row import Utils 

29 

30 font_types = Utils._font_type() 

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

32 font_style = font_types["style"] 

33 font_name = font_types["name"] 

34 font_charset = font_types["charset"] 

35 

36 font_table = RTFConstants.Control.FONT_TABLE_START 

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

38 font_rtf, font_style, font_name, font_charset 

39 ): 

40 font_table = ( 

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

42 ) 

43 font_table += "}" 

44 return font_table 

45 

46 @staticmethod 

47 def generate_color_table(used_colors: list[str] | None = None) -> str: 

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

49 

50 Args: 

51 used_colors: List of color names used in document. If None, includes all 657 colors. 

52 

53 Returns: 

54 RTF color table string 

55 """ 

56 from ..services.color_service import color_service 

57 

58 return color_service.generate_rtf_color_table(used_colors) 

59 

60 @staticmethod 

61 def generate_page_settings( 

62 width: float, height: float, margins: list[float], orientation: str = "portrait" 

63 ) -> str: 

64 """Generate RTF page settings. 

65 

66 Args: 

67 width: Page width in inches 

68 height: Page height in inches 

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

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

71 

72 Returns: 

73 RTF page settings string 

74 """ 

75 from ..row import Utils 

76 

77 # Convert to twips 

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

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

80 

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

82 

83 # Add landscape command if needed 

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

85 

86 return ( 

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

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

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

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

91 ) 

92 

93 @staticmethod 

94 def generate_page_break() -> str: 

95 """Generate RTF page break.""" 

96 return "\\page" 

97 

98 @staticmethod 

99 def generate_paragraph_break() -> str: 

100 """Generate RTF paragraph break.""" 

101 return "\\par" 

102 

103 @staticmethod 

104 def generate_line_break() -> str: 

105 """Generate RTF line break.""" 

106 return "\\line" 

107 

108 

109class RTFDocumentAssembler: 

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

111 

112 def __init__(self): 

113 self.syntax = RTFSyntaxGenerator() 

114 

115 def assemble_document(self, components: dict[str, Any]) -> str: 

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

117 

118 Args: 

119 components: Dictionary containing document components 

120 

121 Returns: 

122 Complete RTF document string 

123 """ 

124 parts = [] 

125 

126 # Document start 

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

128 

129 # Font table 

130 if "fonts" in components: 

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

132 

133 # Page settings 

134 if "page_settings" in components: 

135 settings = components["page_settings"] 

136 parts.append( 

137 self.syntax.generate_page_settings( 

138 settings["width"], 

139 settings["height"], 

140 settings["margins"], 

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

142 ) 

143 ) 

144 

145 # Content sections 

146 content_sections = [ 

147 "page_header", 

148 "page_footer", 

149 "title", 

150 "subline", 

151 "column_headers", 

152 "body", 

153 "footnotes", 

154 "sources", 

155 ] 

156 

157 for section in content_sections: 

158 if section in components and components[section]: 

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

160 parts.extend(components[section]) 

161 else: 

162 parts.append(components[section]) 

163 

164 # Document end 

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

166 

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

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