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