1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2000-2007  Donald N. Allingham
5# Copyright (C) 2010       Peter G. Landgren
6# Copyright (C) 2010       Craig J. Anderson
7# Copyright (C) 2014       Paul Franklin
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22#
23
24"""
25Provide the SubstKeywords class that will replace keywords in a passed
26string with information about the person/marriage/spouse. For example:
27
28foo = SubstKeywords(database, person_handle)
29print foo.replace_and_clean(['$n was born on $b.'])
30
31Will return a value such as:
32
33Mary Smith was born on 3/28/1923.
34"""
35
36#------------------------------------------------------------------------
37#
38# Gramps modules
39#
40#------------------------------------------------------------------------
41from gramps.gen.lib import EventType, PlaceType, Location
42from gramps.gen.utils.db import get_birth_or_fallback, get_death_or_fallback
43from gramps.gen.utils.location import get_main_location
44from gramps.gen.display.place import displayer as _pd
45from gramps.gen.const import GRAMPS_LOCALE as glocale
46
47
48#------------------------------------------------------------------------
49#
50# Local constants
51#
52#------------------------------------------------------------------------
53class TextTypes:
54    """Four enumerations that are used to for the four main parts of a string.
55
56    and used for states.  Separator is not used in states.
57    text   -> remove or display
58    remove -> display
59    """
60    separator, text, remove, display = list(range(4))
61TXT = TextTypes()
62
63
64#------------------------------------------------------------------------
65#
66# Formatting classes
67#
68#------------------------------------------------------------------------
69class GenericFormat:
70    """A Generic parsing class.  Will be subclassed by specific format strings
71    """
72
73    def __init__(self, string_in, qlocale=glocale):
74        self.string_in = string_in
75        self._locale = qlocale
76
77    def _default_format(self, item):
78        """ The default format if there is no format string """
79        pass
80
81    def is_blank(self, item):
82        """ if the information is not known (item is None), remove the format
83        string information from the input string if any.
84        """
85        if item is None:
86            self.string_in.remove_start_end("(", ")")
87            return True
88        return False
89
90    def generic_format(self, item, code, uppr, function):
91        """the main parsing engine.
92
93        Needed are the following:  the input string
94        code - List of one character (string) codes (all lowercase)
95        uppr - list of one character (string) codes that can be uppercased
96            each needs to have a lowercase equivalent in code
97        function - list of functions.
98        there is a one to one relationship with character codes and functions.
99        """
100        if self.string_in.this != "(":
101            return self._default_format(item)
102        self.string_in.step()
103
104        main = VarString()
105        separator = SeparatorParse(self.string_in)
106        #code given in args
107        #function given in args
108
109        while self.string_in.this and self.string_in.this != ")":
110            #Check to see if _in.this is in code
111            to_upper = False
112            if uppr.find(self.string_in.this) != -1:
113                #and the result should be uppercased.
114                to_upper = True
115                where = code.find(self.string_in.this.lower())
116            else:
117                where = code.find(self.string_in.this)
118            if where != -1:
119                self.string_in.step()
120                tmp = function[where]()
121                if to_upper:
122                    tmp = tmp.upper()
123                if tmp == "" or tmp is None:
124                    main.add_remove()
125                elif isinstance(tmp, VarString):  # events cause this
126                    main.extend(tmp)
127                else:
128                    main.add_variable(tmp)
129            elif separator.is_a():
130                main.add_separator(separator.parse_format())
131            else:
132                main.add_text(self.string_in.parse_format())
133
134        if self.string_in.this == ")":
135            self.string_in.step()
136
137        return main
138
139
140#------------------------------------------------------------------------
141# Name Format strings
142#------------------------------------------------------------------------
143class NameFormat(GenericFormat):
144    """ The name format class.
145    If no format string, the name is displayed as per preference options
146    otherwise, parse through a format string and put the name parts in
147    """
148
149    def __init__(self, _in, locale, name_displayer):
150        GenericFormat.__init__(self, _in, locale)
151        self._nd = name_displayer
152
153    def get_name(self, person, aka):
154        """ A helper method for retrieving the person's name """
155        name = None
156        if person:
157            if aka is None:
158                name = person.get_primary_name()
159            else:
160                for names in person.get_alternate_names():
161                    if names.get_type() == aka:
162                        name = names
163                        break
164        return name
165
166    def _default_format(self, name):
167        """ display the name as set in preferences """
168        return self._nd.sorted_name(name)
169
170    def parse_format(self, name):
171        """ Parse the name """
172        if self.is_blank(name):
173            return
174
175        def common():
176            """ return the common name of the person """
177            return (name.get_call_name() or
178                    name.get_first_name().split(' ')[0])
179
180        code = "tfcnxslg"
181        upper = code.upper()
182        function = [name.get_title,            # t
183                    name.get_first_name,       # f
184                    name.get_call_name,        # c
185                    name.get_nick_name,        # n
186                    common,                    # x
187                    name.get_suffix,           # s
188                    name.get_surname,          # l
189                    name.get_family_nick_name  # g
190                   ]
191
192        return self.generic_format(name, code, upper, function)
193
194
195#------------------------------------------------------------------------
196# Date Format strings
197#------------------------------------------------------------------------
198class DateFormat(GenericFormat):
199    """ The date format class.
200    If no format string, the date is displayed as per preference options
201    otherwise, parse through a format string and put the date parts in
202    """
203
204    def get_date(self, event):
205        """ A helper method for retrieving a date from an event """
206        if event:
207            return event.get_date_object()
208        return None
209
210    def _default_format(self, date):
211        return self._locale.date_displayer.display(date)
212
213    def __count_chars(self, char, max_amount):
214        """ count the year/month/day codes """
215        count = 1  # already have seen/passed one
216        while count < max_amount and self.string_in.this == char:
217            self.string_in.step()
218            count = count +1
219        return count
220
221    def parse_format(self, date):
222        """ Parse the name """
223
224        if self.is_blank(date):
225            return
226
227        def year(year, count):
228            """  The year part only """
229            year = str(year)
230            if year == "0":
231                return
232
233            if count == 1:  # found 'y'
234                if len(year) == 1:
235                    return year
236                elif year[-2] == "0":
237                    return year[-1]
238                else:
239                    return year[-2:]
240            elif count == 2:  # found 'yy'
241                tmp = "0" + year
242                return tmp[-2:]
243            elif count == 3:  # found 'yyy'
244                if len(year) > 2:
245                    return year
246                else:
247                    tmp = "00" + year
248                    return tmp[-3:]
249            else:  # count == 4  # found 'yyyy'
250                tmp = "000" + year
251                return tmp[-4:]
252
253        def month(month, count):
254            """  The month part only """
255            month = str(month)
256            if month == "0":
257                return
258
259            if count == 1:
260                return month
261            elif count == 2:  # found 'mm'
262                tmp = "0" + month
263                return tmp[-2:]
264            elif count == 3:   # found 'mmm'
265                return self._locale.date_displayer.short_months[int(month)]
266            else:  # found 'mmmm'
267                return self._locale.date_displayer.long_months[int(month)]
268
269        def day(day, count):
270            """  The day part only """
271            day = str(day)
272            if day == "0":  # 0 means not defined!
273                return
274
275            if count == 1:  # found 'd'
276                return day
277            else:  # found 'dd'
278                tmp = "0" + day
279                return tmp[-2:]
280
281        def text():
282            return date.get_text()
283
284        def s_year():
285            return year(date.get_year(), self.__count_chars("y", 4))
286
287        def s_month():
288            return month(date.get_month(), self.__count_chars("m", 4))
289
290        def su_month():
291            return month(date.get_month(), self.__count_chars("M", 4)).upper()
292
293        def s_day():
294            return day(date.get_day(), self.__count_chars("d", 2))
295
296        def e_year():
297            return year(date.get_stop_year(), self.__count_chars("z", 4))
298
299        def e_month():
300            return month(date.get_stop_month(), self.__count_chars("n", 4))
301
302        def eu_month():
303            return month(date.get_stop_month(),
304                         self.__count_chars("N", 4)).upper()
305
306        def e_day():
307            return day(date.get_stop_day(), self.__count_chars("e", 2))
308
309        def modifier():
310            #ui_mods taken from date.py def lookup_modifier(self, modifier):
311            # trans_text is a defined keyword (in po/update_po.py, po/genpot.sh)
312            trans_text = self._locale.translation.gettext
313            ui_mods = ["", trans_text("before"), trans_text("after"),
314                       trans_text("about"), "", "", ""]
315            return ui_mods[date.get_modifier()]
316
317
318        code = "ymMd" + "znNe" + "ot"
319        upper = "OT"
320        function = [s_year, s_month, su_month, s_day,
321                    e_year, e_month, eu_month, e_day,
322                    modifier, text]
323
324        return self.generic_format(date, code, upper, function)
325
326
327#------------------------------------------------------------------------
328# Place Format strings
329#------------------------------------------------------------------------
330class PlaceFormat(GenericFormat):
331    """ The place format class.
332    If no format string, the place is displayed as per preference options
333    otherwise, parse through a format string and put the place parts in
334    """
335
336    def __init__(self, database, _in):
337        self.database = database
338        GenericFormat.__init__(self, _in)
339
340    def get_place(self, database, event):
341        """ A helper method for retrieving a place from an event """
342        if event:
343            bplace_handle = event.get_place_handle()
344            if bplace_handle:
345                return database.get_place_from_handle(bplace_handle)
346        return None
347
348    def _default_format(self, place):
349        return _pd.display(self.database, place)
350
351    def parse_format(self, database, place):
352        """ Parse the place """
353
354        if self.is_blank(place):
355            return
356
357        code = "elcuspn" + "oitxy"
358        upper = code.upper()
359
360        main_loc = get_main_location(database, place)
361        location = Location()
362        location.set_street(main_loc.get(PlaceType.STREET, ''))
363        location.set_locality(main_loc.get(PlaceType.LOCALITY, ''))
364        location.set_parish(main_loc.get(PlaceType.PARISH, ''))
365        location.set_city(main_loc.get(PlaceType.CITY, ''))
366        location.set_county(main_loc.get(PlaceType.COUNTY, ''))
367        location.set_state(main_loc.get(PlaceType.STATE, ''))
368        location.set_postal_code(main_loc.get(PlaceType.STREET, ''))
369        location.set_country(main_loc.get(PlaceType.COUNTRY, ''))
370
371        function = [location.get_street,
372                    location.get_locality,
373                    location.get_city,
374                    location.get_county,
375                    location.get_state,
376                    place.get_code,
377                    location.get_country,
378
379                    location.get_phone,
380                    location.get_parish,
381                    place.get_title,
382                    place.get_longitude,
383                    place.get_latitude
384                   ]
385
386        return self.generic_format(place, code, upper, function)
387
388
389#------------------------------------------------------------------------
390# Event Format strings
391#------------------------------------------------------------------------
392class EventFormat(GenericFormat):
393    """ The event format class.
394    If no format string, the event description is displayed
395    otherwise, parse through the format string and put in the parts
396        dates and places can have their own format strings
397    """
398
399    def __init__(self, database, _in, locale):
400        self.database = database
401        GenericFormat.__init__(self, _in, locale)
402
403    def _default_format(self, event):
404        if event is None:
405            return
406        else:
407            return event.get_description()
408
409    def __empty_format(self):
410        """ clear out a sub format string """
411        self.string_in.remove_start_end("(", ")")
412        return
413
414    def __empty_attrib(self):
415        """ clear out an attribute name """
416        self.string_in.remove_start_end("[", "]")
417        return
418
419    def parse_format(self, event):
420        """ Parse the event format string.
421        let the date or place classes handle any sub-format strings """
422
423        if self.is_blank(event):
424            return
425
426        def format_date():
427            """ start formatting a date in this event """
428            date_format = DateFormat(self.string_in, self._locale)
429            return date_format.parse_format(date_format.get_date(event))
430
431        def format_place():
432            """ start formatting a place in this event """
433            place_format = PlaceFormat(self.database, self.string_in)
434            place = place_format.get_place(self.database, event)
435            return place_format.parse_format(self.database, place)
436
437        def format_attrib():
438            """ Get the name and then get the attributes value """
439            #Event's Atribute
440            attrib_parse = AttributeParse(self.string_in)
441            #self.string_in.step()
442            name = attrib_parse.get_name()
443            if name:
444                return attrib_parse.get_attribute(event.get_attribute_list(),
445                                                  name)
446            else:
447                return
448
449        code = "ndDia"
450        upper = ""
451        function = [event.get_description,
452                    format_date,
453                    format_place,
454                    event.get_gramps_id,
455                    format_attrib
456                   ]
457
458        return self.generic_format(event, code, upper, function)
459
460    def parse_empty(self):
461        """ remove the format string """
462
463        code = "dDa"
464        function = [self.__empty_format, self.__empty_format,
465                    self.__empty_attrib]
466
467        return self.generic_format(None, code, "", function)
468
469
470#------------------------------------------------------------------------
471# gramps info Format strings
472#------------------------------------------------------------------------
473class GrampsFormat:
474    """ The Gramps Info Format class.
475        This only polls information from system information.
476    """
477
478    def __init__(self, _in, _db):
479        self.string_in = _in
480        self.db = _db
481
482    def parse_format(self):
483        """ Parse the Gramps format string.
484        let the date or place classes handle any sub-format strings """
485        from gramps.version import VERSION
486
487        from gramps.gen.utils.config import get_researcher
488        owner = get_researcher()
489
490        code = "vtd" + "elcspn" + "om"
491        info = [VERSION,
492                owner.get_name(),
493                self.db.get_dbname(),
494
495                owner.get_address(),
496                owner.get_locality(),
497                owner.get_city(),
498                owner.get_state(),
499                owner.get_postal_code(),
500                owner.get_country(),
501
502                owner.get_phone(),
503                owner.get_email()
504               ]
505
506        where = code.find(self.string_in.this)
507        if where != -1:
508            self.string_in.step()
509            return info[where]
510        return "$G"
511
512
513#------------------------------------------------------------------------
514# Gallery Format strings
515#------------------------------------------------------------------------
516class GalleryFormat(GenericFormat):
517    """ The gallery format class.
518    If no format string, the photo description is displayed
519    otherwise, parse through the format string and put in the parts
520        dates (no places) can have their own format strings
521    """
522
523    def __init__(self, database, _in, locale):
524        self.database = database
525        GenericFormat.__init__(self, _in, locale)
526
527    def _default_format(self, photo):
528        if photo is None:
529            return
530        else:
531            return photo.get_description()
532
533    def __empty_format(self):
534        """ clear out a sub format string """
535        self.string_in.remove_start_end("(", ")")
536        return
537
538    def __empty_attrib(self):
539        """ clear out an attribute name """
540        self.string_in.remove_start_end("[", "]")
541        return
542
543    def parse_format(self, photo):
544        """ Parse the photo format string.
545        let the date or place classes handle any sub-format strings """
546
547        if self.is_blank(photo):
548            return
549
550        def format_date():
551            """ start formatting a date in this photo """
552            date_format = DateFormat(self.string_in, self._locale)
553            return date_format.parse_format(date_format.get_date(photo))
554
555        def format_attrib():
556            """ Get the name and then get the attributes value """
557            #photo's Atribute
558            attrib_parse = AttributeParse(self.string_in)
559            name = attrib_parse.get_name()
560            if name:
561                return attrib_parse.get_attribute(photo.get_attribute_list(),
562                                                  name)
563            else:
564                return
565
566        code = "ndia"
567        upper = ""
568        function = [photo.get_description,
569                    format_date,
570                    photo.get_gramps_id,
571                    format_attrib
572                   ]
573
574        return self.generic_format(photo, code, upper, function)
575
576    def parse_empty(self):
577        """ remove the format string """
578
579        code = "da"
580        function = [self.__empty_format, self.__empty_attrib]
581
582        return self.generic_format(None, code, "", function)
583
584
585#------------------------------------------------------------------------
586#
587# ConsumableString - The Input string class
588#
589#------------------------------------------------------------------------
590class ConsumableString:
591    """
592    A simple string implementation with extras to help with parsing.
593
594    This will contain the string to be parsed.  or string in.
595    There will only be one of these for each processed line.
596    """
597    def __init__(self, string):
598        self.__this_string = string
599        self.__setup()
600
601    def __setup(self):
602        """ update class attributes this and next """
603        if len(self.__this_string) > 0:
604            self.this = self.__this_string[0]
605        else:
606            self.this = None
607        if len(self.__this_string) > 1:
608            self.next = self.__this_string[1]
609        else:
610            self.next = None
611
612    def step(self):
613        """ remove the first char from the string """
614        self.__this_string = self.__this_string[1:]
615        self.__setup()
616        return self.this
617
618    def step2(self):
619        """ remove the first two chars from the string """
620        self.__this_string = self.__this_string[2:]
621        self.__setup()
622        return self.this
623
624    def remove_start_end(self, start, end):
625        """ Removes a start, end block from the string if there """
626        if self.this == start:
627            self.text_to_next(end)
628
629    def __get_a_char_of_text(self):
630        """ Removes one char of TEXT from the string and returns it. """
631        if self.this == "\\":
632            if self.next is None:
633                rtrn = "\\"
634            else:
635                rtrn = self.next
636            self.step2()
637        else:
638            rtrn = self.this
639            self.step()
640        return rtrn
641
642    def text_to_next(self, char):
643        """ return/remove a format strings from here """
644        new_str = ""
645        while self.this is not None and self.this != char:
646            new_str += self.__get_a_char_of_text()
647        if self.this == char:
648            self.step()
649        return new_str
650
651    def is_a(self):
652        return True
653
654    def parse_format(self):
655        rtrn = self.__get_a_char_of_text()
656
657        if rtrn:
658            return rtrn
659        return ''
660
661
662#------------------------------------------------------------------------
663#
664# VarString class  - The Output string class
665#
666#------------------------------------------------------------------------
667class VarString:
668    """
669    The current state of the entire string (integer from TextTypes)
670    A list to hold tuple object (integer from TextTypes, string)
671
672    This will contain the string that will be displayed.  or string out.
673    it is used for groups and format strings.
674    """
675    def __init__(self, start_state=TXT.remove):
676        self.state = start_state  # overall state of the string.
677        self._text = []  # list of tuples (TXT.?, string)
678
679    def __update_state(self, new_status):
680        if new_status > self.state:
681            self.state = new_status
682
683    def add_text(self, text):
684        self._text.append((TXT.text, text))
685
686    def add_variable(self, text):
687        self.state = TXT.display
688        self._text.append((TXT.text, text))
689
690    def add_remove(self):
691        self.__update_state(TXT.remove)
692        self._text.append((TXT.remove, ""))
693
694    def add_separator(self, text):
695        self._text.append((TXT.separator, text))
696
697    def get_final(self):
698        #if self.state == TXT.remove:
699        #    return (TXT.remove, "")
700
701        curr_string = ""
702        index = 0
703
704        while index < len(self._text):
705
706            if self._text[index][0] == TXT.text:
707                curr_string += self._text[index][1]
708                index = index + 1
709                continue  # while self._text:
710            if index +1 == len(self._text):
711                if self._text[index][0] == TXT.separator and curr_string != '':
712                    curr_string += self._text[index][1]
713                index = index + 1
714                break  # while self._text:
715
716            type_0_1 = (self._text[index][0], self._text[index+1][0])
717
718            #if   type_0_1 == (TXT.remove, TXT.remove):
719            #    pass
720            if type_0_1 == (TXT.remove, TXT.separator):
721                index = index + 1
722            #elif type_0_1 == (TXT.remove, TXT.text):
723            #    pass
724            elif type_0_1 == (TXT.separator, TXT.remove):
725                index = index + 1
726            #elif type_0_1 == (TXT.separator, TXT.separator):
727            #    pass
728            elif type_0_1 == (TXT.separator, TXT.text):
729                curr_string += self._text[index][1]
730            #else:
731            #    print "#oops  Should never get here."
732            index = index + 1
733
734        #return what we have
735        return (self.state, curr_string)
736        #print("===" + str(self.state) + " '" + str(curr_string) + "'")
737
738    def extend(self, acquisition):
739        """
740            acquisition is a VarString object
741            Merge the content of acquisition into this place.
742        """
743        self.__update_state(acquisition.state)
744
745        if acquisition.state != TXT.display:
746            #The sub {} was TXT.remove.  We don't want to simply ignore it.
747            self.add_remove() # add a remove que here to note it.
748            return
749
750        self._text.extend(acquisition._text)
751
752
753#------------------------------------------------------------------------
754#
755# Parsers
756#
757#------------------------------------------------------------------------
758#------------------------------------------------------------------------
759# SeparatorParse
760#------------------------------------------------------------------------
761class SeparatorParse:
762    """ parse out a separator """
763    def __init__(self, consumer_in):
764        self._in = consumer_in
765
766    def is_a(self):
767        return self._in.this == "<"
768
769    def parse_format(self):
770        if not self.is_a():
771            return
772        """ get the text and return it """
773        self._in.step()
774        return self._in.text_to_next(">")
775
776#------------------------------------------------------------------------
777# AttributeParse
778#------------------------------------------------------------------------
779class AttributeParse:
780    """  Parse attributes """
781
782    def __init__(self, consumer_in):
783        self._in = consumer_in
784
785    def get_name(self):
786        """ Gets a name inside a [] block """
787        if self._in.this != "[":
788            return
789        self._in.step()
790        return self._in.text_to_next("]")
791
792    def get_attribute(self, attrib_list, attrib_name):
793        """ Get an attribute by name """
794        if attrib_name == "":
795            return
796        for attr in attrib_list:
797            if str(attr.get_type()) == attrib_name:
798                return str(attr.get_value())
799        return
800
801    def is_a(self):
802        """ check """
803        return self._in.this == "a"
804
805    def parse_format(self, attrib_list):
806        """ Get the attribute and add it to the string out """
807        name = self.get_name()
808        return self.get_attribute(attrib_list, name)
809
810#------------------------------------------------------------------------
811# VariableParse
812#------------------------------------------------------------------------
813class VariableParse:
814    """ Parse the individual variables """
815
816    def __init__(self, friend, database, consumer_in, locale, name_displayer):
817        self.friend = friend
818        self.database = database
819        self._in = consumer_in
820        self._locale = locale
821        self._nd = name_displayer
822
823    def is_a(self):
824        """ check """
825        return self._in.this == "$" and self._in.next is not None and \
826                              "nsijbBdDmMvVauetTpPG".find(self._in.next) != -1
827
828    def get_event_by_type(self, marriage, e_type):
829        """ get an event from a type """
830        if marriage is None:
831            return None
832        for e_ref in marriage.get_event_ref_list():
833            if not e_ref:
834                continue
835            event = self.friend.database.get_event_from_handle(e_ref.ref)
836            if event.get_type() == e_type:
837                return event
838        return None
839
840    def get_event_by_name(self, person, event_name):
841        """ get an event from a name. """
842        if not person:
843            return None
844        for e_ref in person.get_event_ref_list():
845            if not e_ref:
846                continue
847            event = self.friend.database.get_event_from_handle(e_ref.ref)
848            if str(event.get_type()) == event_name:
849                return event
850        return None
851
852    def empty_item(self, item):
853        """ return false if there is a valid item(date or place).
854        Otherwise
855            add a TXT.remove marker in the output string
856            remove any format strings from the input string
857        """
858        if item is not None:
859            return False
860
861        self._in.remove_start_end("(", ")")
862        return True
863
864    def empty_attribute(self, person):
865        """ return false if there is a valid person.
866        Otherwise
867            add a TXT.remove marker in the output string
868            remove any attribute name from the input string
869        """
870        if person:
871            return False
872
873        self._in.remove_start_end("[", "]")
874        return True
875
876    def __parse_date(self, event):
877        """ sub to process a date
878        Given an event, get the date object, process the format,
879        return the result """
880        date_f = DateFormat(self._in, self._locale)
881        date = date_f.get_date(event)
882        if self.empty_item(date):
883            return
884        return date_f.parse_format(date)
885
886    def __parse_place(self, event):
887        """ sub to process a date
888        Given an event, get the place object, process the format,
889        return the result """
890        place_f = PlaceFormat(self.database, self._in)
891        place = place_f.get_place(self.database, event)
892        if self.empty_item(place):
893            return
894        return place_f.parse_format(self.database, place)
895
896    def __parse_name(self, person, attrib_parse):
897        name_format = NameFormat(self._in, self._locale, self._nd)
898        name = name_format.get_name(person, attrib_parse.get_name())
899        return name_format.parse_format(name)
900
901    def __parse_id(self, first_class_object):
902        if first_class_object is not None:
903            return first_class_object.get_gramps_id()
904        else:
905            return
906
907    def __parse_event(self, person, attrib_parse):
908        event = self.get_event_by_name(person, attrib_parse.get_name())
909        event_f = EventFormat(self.database, self._in, self._locale)
910        if event:
911            return event_f.parse_format(event)
912        else:
913            event_f.parse_empty()
914            return
915
916    def __get_photo(self, person_or_marriage):
917        """ returns the first photo in the media list or None """
918        media_list = person_or_marriage.get_media_list()
919        for media_ref in media_list:
920            media_handle = media_ref.get_reference_handle()
921            media = self.database.get_media_from_handle(media_handle)
922            mime_type = media.get_mime_type()
923            if mime_type and mime_type.startswith("image"):
924                return media
925        return None
926
927    def __parse_photo(self, person_or_marriage):
928        photo_f = GalleryFormat(self.database, self._in, self._locale)
929        if person_or_marriage is None:
930            return photo_f.parse_empty()
931        photo = self.__get_photo(person_or_marriage)
932        if photo:
933            return photo_f.parse_format(photo)
934        else:
935            return photo_f.parse_empty()
936
937    def parse_format(self):
938        """Parse the $ variables. """
939        if not self.is_a():
940            return
941
942        attrib_parse = AttributeParse(self._in)
943        next_char = self._in.next
944        self._in.step2()
945
946        if next_char == "n":
947            #Person's name
948            return self.__parse_name(self.friend.person, attrib_parse)
949        elif next_char == "s":
950            #Souses name
951            return self.__parse_name(self.friend.spouse, attrib_parse)
952
953        elif next_char == "i":
954            #Person's Id
955            return self.__parse_id(self.friend.person)
956        elif next_char == "j":
957            #Marriage Id
958            return self.__parse_id(self.friend.family)
959
960        elif next_char == "b":
961            #Person's Birth date
962            if self.empty_item(self.friend.person):
963                return
964            return self.__parse_date(
965                get_birth_or_fallback(self.friend.database, self.friend.person))
966        elif next_char == "d":
967            #Person's Death date
968            if self.empty_item(self.friend.person):
969                return
970            return self.__parse_date(
971                get_death_or_fallback(self.friend.database, self.friend.person))
972        elif next_char == "m":
973            #Marriage date
974            if self.empty_item(self.friend.family):
975                return
976            return self.__parse_date(
977                self.get_event_by_type(self.friend.family,
978                                       EventType.MARRIAGE))
979        elif next_char == "v":
980            #Divorce date
981            if self.empty_item(self.friend.family):
982                return
983            return self.__parse_date(
984                self.get_event_by_type(self.friend.family,
985                                       EventType.DIVORCE))
986        elif next_char == "T":
987            #Todays date
988            date_f = DateFormat(self._in)
989            from gramps.gen.lib.date import Today
990            date = Today()
991            if self.empty_item(date):
992                return
993            return date_f.parse_format(date)
994
995        elif next_char == "B":
996            #Person's birth place
997            if self.empty_item(self.friend.person):
998                return
999            return self.__parse_place(
1000                get_birth_or_fallback(self.friend.database, self.friend.person))
1001        elif next_char == "D":
1002            #Person's death place
1003            if self.empty_item(self.friend.person):
1004                return
1005            return self.__parse_place(
1006                get_death_or_fallback(self.friend.database, self.friend.person))
1007        elif next_char == "M":
1008            #Marriage place
1009            if self.empty_item(self.friend.family):
1010                return
1011            return self.__parse_place(
1012                self.get_event_by_type(self.friend.family,
1013                                       EventType.MARRIAGE))
1014        elif next_char == "V":
1015            #Divorce place
1016            if self.empty_item(self.friend.family):
1017                return
1018            return self.__parse_place(
1019                self.get_event_by_type(self.friend.family,
1020                                       EventType.DIVORCE))
1021
1022        elif next_char == "a":
1023            #Person's Atribute
1024            if self.empty_attribute(self.friend.person):
1025                return
1026            return attrib_parse.parse_format(
1027                self.friend.person.get_attribute_list())
1028        elif next_char == "u":
1029            #Marriage Atribute
1030            if self.empty_attribute(self.friend.family):
1031                return
1032            return attrib_parse.parse_format(
1033                self.friend.family.get_attribute_list())
1034
1035        elif next_char == "e":
1036            #person event
1037            return self.__parse_event(self.friend.person, attrib_parse)
1038        elif next_char == "t":
1039            #family event
1040            return self.__parse_event(self.friend.family, attrib_parse)
1041
1042        elif next_char == 'p':
1043            #photo for the person
1044            return self.__parse_photo(self.friend.person)
1045        elif next_char == 'P':
1046            #photo for the marriage
1047            return self.__parse_photo(self.friend.family)
1048
1049        elif next_char == "G":
1050            gramps_format = GrampsFormat(self._in, self.database)
1051            return gramps_format.parse_format()
1052
1053
1054#------------------------------------------------------------------------
1055#
1056# SubstKeywords
1057#
1058#------------------------------------------------------------------------
1059class SubstKeywords:
1060    """Accepts a person/family with format lines and returns a new set of lines
1061    using variable substitution to make it.
1062
1063    The individual variables are defined with the classes that look for them.
1064
1065    Needed:
1066        Database object
1067        person_handle
1068            This will be the center person for the display
1069        family_handle
1070            this will specify the specific family/spouse to work with.
1071            If none given, then the first/preferred family/spouse is used
1072    """
1073    def __init__(self, database, locale, name_displayer,
1074                 person_handle, family_handle=None):
1075        """get the person and find the family/spouse to use for this display"""
1076
1077        self.database = database
1078        self.family = None
1079        self.spouse = None
1080        self.line = None   # Consumable_string - set below
1081        self._locale = locale
1082        self._nd = name_displayer
1083
1084        self.person = None
1085        if person_handle is not None:
1086            self.person = database.get_person_from_handle(person_handle)
1087        if self.person is None:
1088            return
1089
1090        fam_hand_list = self.person.get_family_handle_list()
1091        if fam_hand_list:
1092            if family_handle in fam_hand_list:
1093                self.family = database.get_family_from_handle(family_handle)
1094            else:
1095                #Error.  fam_hand_list[0] below may give wrong marriage info.
1096                #only here because of OLD specifications.  Specs read:
1097                # * $S/%S
1098                #   Displays the name of the person's preferred ...
1099                # 'preferred' means FIRST.
1100                #The first might not be the correct marriage to display.
1101                #else: clause SHOULD be removed.
1102                self.family = database.get_family_from_handle(fam_hand_list[0])
1103
1104            father_handle = self.family.get_father_handle()
1105            mother_handle = self.family.get_mother_handle()
1106            self.spouse = None
1107            if father_handle == person_handle:
1108                if mother_handle:
1109                    self.spouse = database.get_person_from_handle(mother_handle)
1110            else:
1111                if father_handle:
1112                    self.spouse = database.get_person_from_handle(father_handle)
1113
1114    def __parse_line(self):
1115        """parse each line of text and return the new displayable line
1116
1117        There are four things we can find here
1118            A {} group which will make/end as needed.
1119            A <> separator
1120            A $  variable - Handled separately
1121            or text
1122        """
1123        stack_var = []
1124        curr_var = VarString(TXT.text)
1125
1126        #First we are going take care of all variables/groups
1127        #break down all {} (groups) and $ (vars) into either
1128        #(TXT.text, resulting_string) or (TXT.remove, '')
1129        variable = VariableParse(self, self.database, self.line,
1130                                 self._locale, self._nd)
1131
1132        while self.line.this:
1133            if self.line.this == "{":
1134                #Start of a group
1135                #push what we have onto the stack
1136                stack_var.append(curr_var)
1137                #Setup
1138                curr_var = VarString()
1139                #step
1140                self.line.step()
1141
1142            elif self.line.this == "}" and len(stack_var) > 0: #End of a group
1143                #add curr to what is on the (top) stack and pop into current
1144                #or pop the stack into current and add TXT.remove
1145                direction = curr_var.state
1146                if direction == TXT.display:
1147                    #add curr onto the top slot of the stack
1148                    stack_var[-1].extend(curr_var)
1149
1150                #pop what we have on the stack
1151                curr_var = stack_var.pop()
1152
1153                if direction == TXT.remove:
1154                    #add remove que
1155                    curr_var.add_remove()
1156                #step
1157                self.line.step()
1158
1159            elif variable.is_a():  # $  (variables)
1160                rtrn = variable.parse_format()
1161                if rtrn is None:
1162                    curr_var.add_remove()
1163                elif isinstance(rtrn, VarString):
1164                    curr_var.extend(rtrn)
1165                else:
1166                    curr_var.add_variable(rtrn)
1167
1168            elif self.line.this == "<":  # separator
1169                self.line.step()
1170                curr_var.add_separator(self.line.text_to_next(">"))
1171
1172            else:  #regular text
1173                curr_var.add_text(self.line.parse_format())
1174
1175        #the stack is for groups/subgroup and may contain items
1176        #if the user does not close his/her {}
1177        #squash down the stack
1178        while stack_var:
1179            direction = curr_var.state
1180            if direction == TXT.display:
1181                #add curr onto the top slot of the stack
1182                stack_var[-1].extend(curr_var)
1183
1184            #pop what we have on the stack
1185            curr_var = stack_var.pop()
1186
1187            if direction == TXT.remove:
1188                #add remove que
1189                curr_var.add_remove()
1190            #step
1191            self.line.step()
1192
1193        #return what we have
1194        return curr_var.get_final()
1195
1196
1197    def __main_level(self):
1198        #Check only if the user wants to not display the line if TXT.remove
1199        remove_line_tag = False
1200        if self.line.this == "-":
1201            remove_line_tag = True
1202            self.line.step()
1203
1204        state, line = self.__parse_line()
1205
1206        if state is TXT.remove and remove_line_tag:
1207            return None
1208        return line
1209
1210    def replace_and_clean(self, lines):
1211        """
1212        return a new array of lines with all of the substitutions done
1213        """
1214        new = []
1215        for this_line in lines:
1216            if this_line == "":
1217                new.append(this_line)
1218                continue
1219            #print "- ", this_line
1220            self.line = ConsumableString(this_line)
1221            new_line = self.__main_level()
1222            #print "+ ", new_line
1223            if new_line is not None:
1224                new.append(new_line)
1225
1226        if new == []:
1227            new = [""]
1228        return new
1229
1230
1231#Acts 20:35 (New International Version)
1232#In everything I did, I showed you that by this kind of hard work
1233#we must help the weak, remembering the words the Lord Jesus himself
1234#said: 'It is more blessed to give than to receive.'
1235
1236
1237if __name__ == '__main__':
1238#-------------------------------------------------------------------------
1239#
1240# For Testing everything except VariableParse, SubstKeywords and EventFormat
1241# apply it as a script:
1242#
1243#     ==> in command line do "PYTHONPATH=??? python libsubstkeyword.py"
1244#
1245# You will need to put in your own path to the src directory
1246#
1247#-------------------------------------------------------------------------
1248    # pylint: disable=C0103
1249
1250    def combinations(c, r):
1251        # combinations('ABCD', 2) --> AB AC AD BC BD CD
1252        # combinations(range(4), 3) --> 012 013 023 123
1253        pool = tuple(range(c))
1254        n = len(pool)
1255        if r > n:
1256            return
1257        indices = list(range(r))
1258        yield tuple(pool[i] for i in indices)
1259        while True:
1260            for i in reversed(list(range(r))):
1261                if indices[i] != i + n - r:
1262                    break
1263            else:
1264                return
1265            indices[i] += 1
1266            for j in range(i+1, r):
1267                indices[j] = indices[j-1] + 1
1268            yield tuple(pool[i] for i in indices)
1269
1270    def main_level_test(_in, testing_class, testing_what):
1271        """This is a mini def __main_level(self):
1272        """
1273        main = _in
1274        sepa = SeparatorParse(_in)
1275        test = testing_class(_in)
1276
1277        while _in.this:
1278            if main.is_a():
1279                main.parse_format(_in)
1280            elif sepa.is_a():
1281                sepa.parse_format(main)
1282            elif _in.this == "$":
1283                _in.step()
1284                main.add_variable(
1285                    test.parse_format(testing_what))
1286            else:
1287                _in.parse_format(main)
1288
1289        main.combine_all()
1290
1291        state, line = main.get_string()
1292        if state is TXT.remove:
1293            return None
1294        else:
1295            return line
1296
1297
1298    from gramps.gen.lib.date import Date
1299    y_or_n = ()
1300    date_to_test = Date()
1301
1302    def date_set():
1303        date_to_test.set_yr_mon_day(
1304            1970 if 0 in y_or_n else 0,
1305            9 if 1 in y_or_n else 0,
1306            3 if 2 in y_or_n else 0
1307            )
1308        #print date_to_test
1309
1310    line_in = "<Z>$(yyy) <a>$(<Z>Mm)<b>$(mm){<c>$(d)}{<d>$(yyyy)<e>}<f>$(yy)"
1311    consume_str = ConsumableString(line_in)
1312
1313    print(line_in)
1314    print("#None are known")
1315    tmp = main_level_test(consume_str, DateFormat, date_to_test)
1316    print(tmp)
1317    print("Good" if tmp == " " else "!! bad !!")
1318
1319
1320    print()
1321    print()
1322    print("#One is known")
1323    answer = []
1324    for y_or_n in combinations(3, 1):
1325        date_set()
1326        consume_str = ConsumableString(line_in)
1327        tmp = main_level_test(consume_str, DateFormat, date_to_test)
1328        print(tmp)
1329        answer.append(tmp)
1330    print("Good" if answer == [
1331        "1970 d1970f70",
1332        " a99b09",
1333        " c3"
1334        ] else "!! bad !!")
1335
1336
1337    print()
1338    print()
1339    print("#Two are known")
1340    answer = []
1341    for y_or_n in combinations(3, 2):
1342        date_set()
1343        consume_str = ConsumableString(line_in)
1344        tmp = main_level_test(consume_str, DateFormat, date_to_test)
1345        print(tmp)
1346        answer.append(tmp)
1347    print("Good" if answer == [
1348        "1970 a99b09d1970f70",
1349        "1970 c3d1970f70",
1350        " a99b09c3"
1351        ] else "!! bad !!")
1352
1353
1354    print()
1355    print()
1356    print("#All are known")
1357    answer = []
1358    y_or_n = (0, 1, 2)
1359    date_set()
1360    consume_str = ConsumableString(line_in)
1361    tmp = main_level_test(consume_str, DateFormat, date_to_test)
1362    print(tmp)
1363    answer.append(tmp)
1364    print("Good" if answer == [
1365        "1970 a99b09c3d1970f70"
1366        ] else "!! bad !!")
1367
1368    import sys
1369    sys.exit()
1370    print()
1371    print()
1372    print("=============")
1373    print("=============")
1374
1375    from gramps.gen.lib.name import Name
1376    y_or_n = ()
1377    name_to_test = Name()
1378
1379    def name_set():
1380        #code = "tfcnxslg"
1381        name_to_test.set_call_name("Bob" if 0 in y_or_n else "")
1382        name_to_test.set_title("Dr." if 1 in y_or_n else "")
1383        name_to_test.set_first_name("Billy" if 2 in y_or_n else "")
1384        name_to_test.set_nick_name("Buck" if 3 in y_or_n else "")
1385        name_to_test.set_suffix("IV" if 4 in y_or_n else "")
1386        #now can we put something in for the last name?
1387        name_to_test.set_family_nick_name("The Clubs" if 5 in y_or_n else "")
1388
1389    line_in = "{$(c)$(t)<1>{<2>$(f)}{<3>$(n){<0> "
1390    line_in = line_in + "<0>}<4>$(x)}$(s)<5>$(l)<6>$(g)<0>"
1391    consume_str = ConsumableString(line_in)
1392
1393    print()
1394    print()
1395    print(line_in)
1396    print("#None are known")
1397    tmp = main_level_test(consume_str, NameFormat, name_to_test)
1398    print(tmp)
1399    print("Good" if tmp is None else "!! bad !!")
1400
1401
1402    print()
1403    print()
1404    print("#Two are known")
1405    answer = []
1406    for y_or_n in combinations(6, 2):
1407        name_set()
1408        consume_str = ConsumableString(line_in)
1409        tmp = main_level_test(consume_str, NameFormat, name_to_test)
1410        print(tmp)
1411        answer.append(tmp)
1412    print("Good" if answer == [
1413        "BobDr.4Bob",
1414        "Bob2Billy4Bob",
1415        "Bob3Buck4Bob",
1416        "Bob4BobIV",
1417        "Bob4BobThe Clubs",
1418        "Dr.2Billy4Billy",
1419        "Dr.3Buck",
1420        "Dr.1IV",
1421        "Dr.6The Clubs",
1422        "Billy3Buck4Billy",
1423        "Billy4BillyIV",
1424        "Billy4BillyThe Clubs",
1425        "BuckIV",
1426        "BuckThe Clubs",
1427        "IV6The Clubs"
1428        ] else "!! bad !!")
1429
1430
1431    print()
1432    print()
1433    print("#All are known")
1434    y_or_n = (0, 1, 2, 3, 4, 5)
1435    name_set()
1436    consume_str = ConsumableString(line_in)
1437    answer = main_level_test(consume_str, NameFormat, name_to_test)
1438    print(answer)
1439    print("Good" if answer == "BobDr.2Billy3Buck4BobIV6The Clubs"
1440          else "!! bad !!")
1441
1442
1443    print()
1444    print()
1445    print("=============")
1446    print("=============")
1447
1448    from gramps.gen.lib.place import Place
1449    y_or_n = ()
1450    place_to_test = Place()
1451
1452    def place_set():
1453        #code = "elcuspnitxy"
1454        main_loc = place_to_test.get_main_location()
1455        main_loc.set_street(
1456            "Lost River Ave." if 0 in y_or_n else ""
1457        )
1458        main_loc.set_locality(
1459            "Second district" if 1 in y_or_n else ""
1460        )
1461        main_loc.set_city(
1462            "Arco" if 2 in y_or_n else ""
1463        )
1464        main_loc.set_county(
1465            "Butte" if 3 in y_or_n else ""
1466        )
1467        main_loc.set_state(
1468            "Idaho" if 4 in y_or_n else ""
1469        )
1470        main_loc.set_postal_code(
1471            "83213" if 5 in y_or_n else ""
1472        )
1473        main_loc.set_country(
1474            "USA" if 6 in y_or_n else ""
1475        )
1476        main_loc.set_parish(
1477            "St Anns" if 7 in y_or_n else ""
1478        )
1479        place_to_test.set_title(
1480            "Atomic City" if 8 in y_or_n else ""
1481        )
1482        place_to_test.set_longitude(
1483            "N43H38'5\"N" if 9 in y_or_n else ""
1484        )
1485        place_to_test.set_latitude(
1486            "W113H18'5\"W" if 10 in y_or_n else ""
1487        )
1488
1489    #code = "txy"
1490    line_in = "$(e)<1>{<2>$(l) <3> $(c)<4><0><5>{$(s)<6>$(p)<7>" + \
1491              "{<1>$(n)<2>}<3>$(i<0>)<4>}<5>$(t)<6>$(x)<7>}<8>$(y)"
1492    consume_str = ConsumableString(line_in)
1493
1494    print()
1495    print()
1496    print(line_in)
1497    print("#None are known")
1498    tmp = main_level_test(consume_str, PlaceFormat, place_to_test)
1499    print(tmp)
1500    print("Good" if tmp == "" else "!! bad !!")
1501
1502
1503    print()
1504    print()
1505    print("#Three are known (string lengths only)")
1506    answer = []
1507    for y_or_n in combinations(11, 4):
1508        place_set()
1509        consume_str = ConsumableString(line_in)
1510        tmp = main_level_test(consume_str, PlaceFormat, place_to_test)
1511        #print tmp
1512        answer.append(len(tmp))
1513    print(answer)
1514    print("Good" if answer == [
1515        38, 44, 44, 42, 46, 50, 49, 50, 40, 40, 38, 42,
1516        46, 45, 46, 46, 44, 48, 52, 51, 52, 44, 48, 52, 51, 52, 46, 50, 49, 50,
1517        54, 53, 54, 57, 58, 57, 28, 28, 26, 30, 34, 33, 34, 34, 32, 36, 40, 39,
1518        40, 32, 36, 40, 39, 40, 34, 38, 37, 38, 42, 41, 42, 45, 46, 45, 30, 28,
1519        32, 36, 35, 36, 28, 32, 36, 35, 36, 30, 34, 33, 34, 38, 37, 38, 41, 42,
1520        41, 34, 38, 42, 41, 42, 36, 40, 39, 40, 44, 43, 44, 47, 48, 47, 36, 40,
1521        39, 40, 44, 43, 44, 47, 48, 47, 42, 41, 42, 45, 46, 45, 49, 50, 49, 53,
1522        28, 28, 26, 30, 34, 33, 34, 34, 32, 36, 40, 39, 40, 32, 36, 40, 39, 40,
1523        34, 38, 37, 38, 42, 41, 42, 45, 46, 45, 30, 28, 32, 36, 35, 36, 28, 32,
1524        36, 35, 36, 30, 34, 33, 34, 38, 37, 38, 41, 42, 41, 34, 38, 42, 41, 42,
1525        36, 40, 39, 40, 44, 43, 44, 47, 48, 47, 36, 40, 39, 40, 44, 43, 44, 47,
1526        48, 47, 42, 41, 42, 45, 46, 45, 49, 50, 49, 53, 19, 17, 21, 25, 24, 25,
1527        17, 21, 25, 24, 25, 19, 23, 22, 23, 27, 26, 27, 30, 31, 30, 23, 27, 31,
1528        30, 31, 25, 29, 28, 29, 33, 32, 33, 36, 37, 36, 25, 29, 28, 29, 33, 32,
1529        33, 36, 37, 36, 31, 30, 31, 34, 35, 34, 38, 39, 38, 42, 19, 23, 27, 26,
1530        27, 21, 25, 24, 25, 29, 28, 29, 32, 33, 32, 21, 25, 24, 25, 29, 28, 29,
1531        32, 33, 32, 27, 26, 27, 30, 31, 30, 34, 35, 34, 38, 27, 31, 30, 31, 35,
1532        34, 35, 38, 39, 38, 33, 32, 33, 36, 37, 36, 40, 41, 40, 44, 33, 32, 33,
1533        36, 37, 36, 40, 41, 40, 44, 38, 39, 38, 42, 46] else "!! bad !!")
1534
1535