1# 2# Gramps - a GTK+/GNOME based genealogy program 3# 4# Copyright (C) 2000-2006 Donald N. Allingham 5# Copyright (C) 2007-2009 Brian G. Matherly 6# Copyright (C) 2009-2010 Benny Malengier <benny.malengier@gramps-project.org> 7# Copyright (C) 2010 Peter Landgren 8# Copyright (C) 2011 Adam Stein <adam@csh.rit.edu> 9# Copyright (C) 2012,2017 Paul Franklin 10# 11# This program is free software; you can redistribute it and/or modify 12# it under the terms of the GNU General Public License as published by 13# the Free Software Foundation; either version 2 of the License, or 14# (at your option) any later version. 15# 16# This program is distributed in the hope that it will be useful, 17# but WITHOUT ANY WARRANTY; without even the implied warranty of 18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19# GNU General Public License for more details. 20# 21# You should have received a copy of the GNU General Public License 22# along with this program; if not, write to the Free Software 23# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 24# 25 26""" 27ACSII document generator. 28""" 29 30#------------------------------------------------------------------------ 31# 32# Gramps modules 33# 34#------------------------------------------------------------------------ 35from gramps.gen.const import DOCGEN_OPTIONS 36from gramps.gen.errors import ReportError 37from gramps.gen.plug.docgen import (BaseDoc, TextDoc, 38 PARA_ALIGN_RIGHT, PARA_ALIGN_CENTER) 39from gramps.gen.plug.menu import NumberOption 40from gramps.gen.plug.report import DocOptions 41from gramps.gen.const import GRAMPS_LOCALE as glocale 42_ = glocale.translation.gettext 43 44#------------------------------------------------------------------------ 45# 46# Constants 47# 48#------------------------------------------------------------------------ 49LEFT, RIGHT, CENTER = 'LEFT', 'RIGHT', 'CENTER' 50 51#------------------------------------------------------------------------ 52# 53# This routine was written by David Mertz and placed into the public 54# domain. It is sample code from his book, "Text Processing in Python" 55# 56# Modified by Alex Roitman: right-pad with spaces, if right_pad==1; 57# return empty string if no text was given 58# Another argument: "first" is the first line indent in characters 59# _relative_ to the "left" margin. It can be negative! 60# 61#------------------------------------------------------------------------ 62def reformat_para(para='', left=0, right=72, just=LEFT, right_pad=0, first=0): 63 if not para.strip(): 64 return "\n" 65 66 lines = [] 67 real_left = left+first 68 alllines = para.split('\n') 69 for realline in alllines: 70 words = realline.split() 71 line = '' 72 word = 0 73 end_words = 0 74 while not end_words: 75 if not words: 76 lines.append("\n") 77 break 78 if len(words[word]) > right-real_left: # Handle very long words 79 line = words[word] 80 word += 1 81 if word >= len(words): 82 end_words = 1 83 else: # Compose line of words 84 while len(line)+len(words[word]) <= right-real_left: 85 line += words[word] 86 word += 1 87 if word >= len(words): 88 end_words = 1 89 break 90 elif len(line) < right-real_left: 91 line += ' ' # add a space since there is still room 92 lines.append(line) 93 #first line finished, discard first 94 real_left = left 95 line = '' 96 if just == CENTER: 97 if right_pad: 98 return '\n'.join( 99 [' '*(left+first) + ln.center(right-left-first) 100 for ln in lines[0:1]] + 101 [' '*left + ln.center(right-left) for ln in lines[1:]] 102 ) 103 else: 104 return '\n'.join( 105 [' '*(left+first) + ln.center(right-left-first).rstrip() 106 for ln in lines[0:1]] + 107 [' '*left + ln.center(right-left).rstrip() 108 for ln in lines[1:]] 109 ) 110 elif just == RIGHT: 111 if right_pad: 112 return '\n'.join([line.rjust(right) for line in lines]) 113 else: 114 return '\n'.join([line.rjust(right).rstrip() for line in lines]) 115 else: # left justify 116 if right_pad: 117 return '\n'.join( 118 [' '*(left+first) + line.ljust(right-left-first) 119 for line in lines[0:1]] + 120 [' '*left + line.ljust(right-left) for line in lines[1:]] 121 ) 122 else: 123 return '\n'.join( 124 [' '*(left+first) + line for line in lines[0:1]] + 125 [' '*left + line for line in lines[1:]] 126 ) 127 128#------------------------------------------------------------------------ 129# 130# Ascii 131# 132#------------------------------------------------------------------------ 133class AsciiDoc(BaseDoc, TextDoc): 134 """ 135 ASCII document generator. 136 """ 137 def __init__(self, styles, paper_style, options=None, uistate=None): 138 BaseDoc.__init__(self, styles, paper_style, uistate=uistate) 139 self.__note_format = False 140 141 self._cpl = 72 # characters per line, in case the options are ignored 142 if options: 143 menu = options.menu 144 self._cpl = menu.get_option_by_name('linechars').get_value() 145 146 self.file = None 147 self.filename = '' 148 149 self.text = '' 150 self.para = None 151 self.leader = None 152 153 self.tbl_style = None 154 self.in_cell = None 155 self.ncols = 0 156 self.column_order = [] 157 self.cellpars = [] 158 self.cell_lines = [] 159 self.cell_widths = [] 160 self.cellnum = -1 161 self.maxlines = 0 162 163 #-------------------------------------------------------------------- 164 # 165 # Opens the file, resets the text buffer. 166 # 167 #-------------------------------------------------------------------- 168 def open(self, filename): 169 if filename[-4:] != ".txt": 170 self.filename = filename + ".txt" 171 else: 172 self.filename = filename 173 174 try: 175 self.file = open(self.filename, "w", errors='backslashreplace') 176 except Exception as msg: 177 raise ReportError(_("Could not create %s") % self.filename, msg) 178 179 self.in_cell = 0 180 self.text = "" 181 182 #-------------------------------------------------------------------- 183 # 184 # Close the file. Call the app if required. 185 # 186 #-------------------------------------------------------------------- 187 def close(self): 188 self.file.close() 189 190 def get_usable_width(self): 191 """ 192 Return the usable width of the document in characters. 193 """ 194 return self._cpl 195 196 #-------------------------------------------------------------------- 197 # 198 # Force a section page break 199 # 200 #-------------------------------------------------------------------- 201 def page_break(self): 202 self.file.write('\012') 203 204 def start_bold(self): 205 pass 206 207 def end_bold(self): 208 pass 209 210 def start_superscript(self): 211 self.text = self.text + '[' 212 213 def end_superscript(self): 214 self.text = self.text + ']' 215 216 #-------------------------------------------------------------------- 217 # 218 # Starts a paragraph. 219 # 220 #-------------------------------------------------------------------- 221 def start_paragraph(self, style_name, leader=None): 222 styles = self.get_style_sheet() 223 self.para = styles.get_paragraph_style(style_name) 224 self.leader = leader 225 226 #-------------------------------------------------------------------- 227 # 228 # End a paragraph. First format it to the desired widths. 229 # If not in table cell, write it immediately. If in the cell, 230 # add it to the list for this cell after formatting. 231 # 232 #-------------------------------------------------------------------- 233 def end_paragraph(self): 234 if self.para.get_alignment() == PARA_ALIGN_RIGHT: 235 fmt = RIGHT 236 elif self.para.get_alignment() == PARA_ALIGN_CENTER: 237 fmt = CENTER 238 else: 239 fmt = LEFT 240 241 if self.in_cell: 242 right = self.cell_widths[self.cellnum] 243 else: 244 right = self.get_usable_width() 245 246 # Compute indents in characters. Keep first_indent relative! 247 regular_indent = 0 248 first_indent = 0 249 if self.para.get_left_margin(): 250 regular_indent = int(4*self.para.get_left_margin()) 251 if self.para.get_first_indent(): 252 first_indent = int(4*self.para.get_first_indent()) 253 254 if self.in_cell and self.cellnum < self.ncols - 1: 255 right_pad = 1 256 the_pad = ' ' * right 257 else: 258 right_pad = 0 259 the_pad = '' 260 261 # Depending on the leader's presence, treat the first line differently 262 if self.leader: 263 # If we have a leader then we need to reformat the text 264 # as if there's no special treatment for the first line. 265 # Then add leader and eat up the beginning of the first line pad. 266 # Do not reformat if preformatted notes 267 if not self.__note_format: 268 self.leader += ' ' 269 start_at = regular_indent + min(len(self.leader)+first_indent, 270 0) 271 this_text = reformat_para(self.text, regular_indent, right, fmt, 272 right_pad) 273 this_text = (' ' * (regular_indent+first_indent) + 274 self.leader + 275 this_text[start_at:] 276 ) 277 else: 278 this_text = self.text 279 else: 280 # If no leader then reformat the text according to the first 281 # line indent, as specified by style. 282 # Do not reformat if preformatted notes 283 if not self.__note_format: 284 this_text = reformat_para(self.text, regular_indent, right, fmt, 285 right_pad, first_indent) 286 else: 287 this_text = ' ' * (regular_indent + first_indent) + self.text 288 289 if self.__note_format: 290 # don't add an extra LF before the_pad if preformatted notes. 291 if this_text != '\n': 292 # don't add LF if there is this_text is a LF 293 this_text += the_pad + '\n' 294 else: 295 this_text += '\n' + the_pad + '\n' 296 297 if self.in_cell: 298 self.cellpars[self.cellnum] += this_text 299 else: 300 self.file.write(this_text) 301 302 self.text = "" 303 304 #-------------------------------------------------------------------- 305 # 306 # Start a table. Grab the table style, and store it. 307 # 308 #-------------------------------------------------------------------- 309 def start_table(self, name, style_name): 310 styles = self.get_style_sheet() 311 self.tbl_style = styles.get_table_style(style_name) 312 self.ncols = self.tbl_style.get_columns() 313 self.column_order = [] 314 for cell in range(self.ncols): 315 self.column_order.append(cell) 316 if self.get_rtl_doc(): 317 self.column_order.reverse() 318 319 #-------------------------------------------------------------------- 320 # 321 # End a table. Turn off the self.in_cell flag 322 # 323 #-------------------------------------------------------------------- 324 def end_table(self): 325 self.in_cell = 0 326 327 #-------------------------------------------------------------------- 328 # 329 # Start a row. Initialize lists for cell contents, number of lines, 330 # and the widths. It is necessary to keep a list of cell contents 331 # that is to be written after all the cells are defined. 332 # 333 #-------------------------------------------------------------------- 334 def start_row(self): 335 self.cellpars = [''] * self.ncols 336 self.cell_lines = [0] * self.ncols 337 self.cell_widths = [0] * self.ncols 338 self.cellnum = -1 339 self.maxlines = 0 340 table_width = (self.get_usable_width() * 341 self.tbl_style.get_width() / 100.0) 342 for cell in self.column_order: 343 self.cell_widths[cell] = int( 344 table_width * self.tbl_style.get_column_width(cell) / 100.0) 345 346 #-------------------------------------------------------------------- 347 # 348 # End a row. Write the cell contents. Write the line of spaces 349 # if the cell has fewer lines than the maximum number. 350 # 351 #-------------------------------------------------------------------- 352 def end_row(self): 353 self.in_cell = 0 354 cell_text = [None]*self.ncols 355 for cell in self.column_order: 356 if self.cell_widths[cell]: 357 blanks = ' '*self.cell_widths[cell] + '\n' 358 if self.cell_lines[cell] < self.maxlines: 359 self.cellpars[cell] += blanks * ( 360 self.maxlines - self.cell_lines[cell] 361 ) 362 cell_text[cell] = self.cellpars[cell].split('\n') 363 for line in range(self.maxlines): 364 for cell in self.column_order: 365 if self.cell_widths[cell]: 366 self.file.write(cell_text[cell][line]) 367 self.file.write('\n') 368 369 #-------------------------------------------------------------------- 370 # 371 # Start a cell. Set the self.in_cell flag, 372 # increment the current cell number. 373 # 374 #-------------------------------------------------------------------- 375 def start_cell(self, style_name, span=1): 376 self.in_cell = 1 377 self.cellnum = self.cellnum + span 378 span -= 1 379 while span: 380 self.cell_widths[self.cellnum] += ( 381 self.cell_widths[self.cellnum-span] 382 ) 383 self.cell_widths[self.cellnum-span] = 0 384 span -= 1 385 386 387 #-------------------------------------------------------------------- 388 # 389 # End a cell. Find out the number of lines in this cell, correct 390 # the maximum number of lines if necessary. 391 # 392 #-------------------------------------------------------------------- 393 def end_cell(self): 394 self.in_cell = 0 395 self.cell_lines[self.cellnum] = self.cellpars[self.cellnum].count('\n') 396 if self.cell_lines[self.cellnum] > self.maxlines: 397 self.maxlines = self.cell_lines[self.cellnum] 398 399 def add_media(self, name, align, w_cm, h_cm, alt='', style_name=None, 400 crop=None): 401 this_text = '(photo)' 402 if self.in_cell: 403 self.cellpars[self.cellnum] += this_text 404 else: 405 self.file.write(this_text) 406 407 def write_styled_note(self, styledtext, format, style_name, 408 contains_html=False, links=False): 409 """ 410 Convenience function to write a styledtext to the ASCII doc. 411 styledtext : assumed a StyledText object to write 412 format : = 0 : Flowed, = 1 : Preformatted 413 style_name : name of the style to use for default presentation 414 contains_html: bool, the backend should not check if html is present. 415 If contains_html=True, then the textdoc is free to handle that in 416 some way. Eg, a textdoc could remove all tags, or could make sure 417 a link is clickable. AsciiDoc prints the html without handling it 418 links: bool, make the URL in the text clickable (if supported) 419 """ 420 if contains_html: 421 return 422 text = str(styledtext) 423 if format: 424 #Preformatted note, keep all white spaces, tabs, LF's 425 self.__note_format = True 426 for line in text.split('\n'): 427 self.start_paragraph(style_name) 428 self.write_text(line) 429 self.end_paragraph() 430 # Add an extra empty para all lines in each preformatted note 431 self.start_paragraph(style_name) 432 self.end_paragraph() 433 self.__note_format = False 434 else: 435 for line in text.split('\n\n'): 436 self.start_paragraph(style_name) 437 #line = line.replace('\n',' ') 438 #line = ' '.join(line.split()) 439 self.write_text(line) 440 self.end_paragraph() 441 442 #-------------------------------------------------------------------- 443 # 444 # Writes text. 445 #-------------------------------------------------------------------- 446 def write_text(self, text, mark=None, links=False): 447 self.text = self.text + text 448 449#------------------------------------------------------------------------ 450# 451# AsciiDocOptions class 452# 453#------------------------------------------------------------------------ 454class AsciiDocOptions(DocOptions): 455 """ 456 Defines options and provides handling interface. 457 """ 458 459 def __init__(self, name, dbase): 460 DocOptions.__init__(self, name) 461 462 def add_menu_options(self, menu): 463 """ 464 Add options to the document menu for the AsciiDoc docgen. 465 """ 466 467 category_name = DOCGEN_OPTIONS 468 469 linechars = NumberOption(_('Characters per line'), 72, 20, 9999) 470 linechars.set_help(_("The number of characters per line")) 471 menu.add_option(category_name, 'linechars', linechars) 472