diff --git a/capture_gedcom_warnings.py b/capture_gedcom_warnings.py new file mode 100644 index 00000000000..7620b83b752 --- /dev/null +++ b/capture_gedcom_warnings.py @@ -0,0 +1,103 @@ +#! /usr/bin/env python3 +"""Capture the GEDCOM import warning text without the GUI. + +Replicates the import path used by Gramps (importData -> GedcomParser -> +parse_gedcom_file -> user.info(..., "".join(errors), ...)) but with a +User subclass that records the info text instead of showing a dialog. + +Usage: + python capture_gedcom_warnings.py [input_file] [output_file] + +Examples: + python capture_gedcom_warnings.py + python capture_gedcom_warnings.py imp_notetest_dfs.ged + python capture_gedcom_warnings.py imp_notetest_dfs.ged report.txt +""" + +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2026 Gramps Development Team +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, see . +# + +# ------------------------------------------------------------------------- +# +# Standard Python modules +# +# ------------------------------------------------------------------------- +import os +import sys + +# ------------------------------------------------------------------------- +# +# Gramps modules +# +# ------------------------------------------------------------------------- +from gramps.gen.const import TEST_DIR +from gramps.gen.db.utils import make_database +from gramps.gen.user import User +from gramps.gen.utils.config import config +from gramps.plugins.importer.importgedcom import importData + + +class CaptureUser(User): + """A User that captures the info() text instead of displaying it.""" + + def __init__(self): + super().__init__() + self.report_title = "" + self.infotext = "" + + def info(self, msg1, infotext, parent=None, monospaced=False): + self.report_title = msg1 + self.infotext = infotext + + +def main(): + """Import the GEDCOM and print the captured warning text.""" + input_file = ( + sys.argv[1] if len(sys.argv) > 1 else "imp_notetest_lds_in-out-in_dfs.ged" + ) + output_file = sys.argv[2] if len(sys.argv) > 2 else None + fn1 = os.path.join(TEST_DIR, input_file) + + # Same "_dfs" preferences as imports_test.py make_tst_function() + config.set("preferences.default-source", True) + config.set("preferences.tag-on-import-format", "Imported") + config.set("preferences.tag-on-import", True) + + # Set up a fresh in-memory database like the test does. + db = make_database("sqlite") + db.load(":memory:") + db.set_feature("skip-import-additions", False) + + user = CaptureUser() + importData(db, fn1, user) + + print("REPORT TITLE:", user.report_title) + print("REPORT TEXT:") + print(user.infotext) + + if output_file: + with open(output_file, "w", encoding="utf-8") as out: + out.write(user.report_title + "\n") + out.write(user.infotext) + print(f"Wrote {output_file}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/data/tests/imp_FTM_16dec2015a-mod1.gramps b/data/tests/imp_FTM_16dec2015a-mod1.gramps index 93f391ed7c1..5a128f7eee2 100644 --- a/data/tests/imp_FTM_16dec2015a-mod1.gramps +++ b/data/tests/imp_FTM_16dec2015a-mod1.gramps @@ -1,9 +1,9 @@ - - + +
- +
@@ -147,19 +147,19 @@ Records not imported into OBJE (multi-media object) Gramps ID M159: -Could not import 1850 United States Federal Census(11)-1.jpg Line 70: 1 FILE 1850 United States Federal Census(11)-1.jpg +Could not import 1850 United States Federal Census(11)-1.jpg Line 70: 1 FILE 1850 United States Federal Census(11)-1.jpg Records not imported into OBJE (multi-media object) Gramps ID M158: -Could not import D:\Users\PRC\Downloads\1850 United States Federa Line 75: 1 FILE D:\Users\PRC\Downloads\1850 United States Federal Census(11)-1.jpg +Could not import D:\Users\PRC\Downloads\1850 United States Federal Census(11)-1.jpg Line 75: 1 FILE D:\Users\PRC\Downloads\1850 United States Federal Census(11)-1.jpg diff --git a/data/tests/imp_FTM_PHOTO.gramps b/data/tests/imp_FTM_PHOTO.gramps index ec144dc5ae6..2d82b90b3c4 100644 --- a/data/tests/imp_FTM_PHOTO.gramps +++ b/data/tests/imp_FTM_PHOTO.gramps @@ -1,14 +1,14 @@ - - + +
- +
- + U The @@ -21,7 +21,7 @@ - + U She @@ -35,7 +35,7 @@ - + U Edwin @@ -49,102 +49,109 @@ - + - + - + - + - + - + - + - + - + The primary photo should be a male Sourpuss in a Hat 03.jpg or Gid:M3 - + The primary photo should be a girl eating 04.jpg or Gid:M4 - + Records not imported into INDI (individual) Gramps ID I0002: -Line ignored as not understood Line 35: 3 WHAT ??? +Line ignored as not understood Line 35: 3 WHAT ??? + - + Records not imported into OBJE (multi-media object) Gramps ID M1: -Could not import O1.jpg Line 38: 1 FILE O1.jpg +Could not import O1.jpg Line 38: 1 FILE O1.jpg + - + Records not imported into OBJE (multi-media object) Gramps ID M2: -Could not import O2.jpg Line 41: 1 FILE O2.jpg +Could not import O2.jpg Line 41: 1 FILE O2.jpg + - + Records not imported into OBJE (multi-media object) Gramps ID M3: -Could not import O3.jpg Line 44: 1 FILE O3.jpg +Could not import O3.jpg Line 44: 1 FILE O3.jpg + - + Records not imported into OBJE (multi-media object) Gramps ID M4: -Could not import O4.jpg Line 47: 1 FILE O4.jpg +Could not import O4.jpg Line 47: 1 FILE O4.jpg + - + Records not imported into OBJE (multi-media object) Gramps ID M5: -Could not import O5.jpg Line 50: 1 FILE O5.jpg +Could not import O5.jpg Line 50: 1 FILE O5.jpg + - + The primary photo should be a male of pair 05.jpg - + Records not imported into INDI (individual) Gramps ID I0003: -Could not import O3.jpg Line 55: 1 OBJE -Could not import O4.jpg Line 59: 1 OBJE -Could not import O5.jpg Line 63: 1 OBJE +Could not import O3.jpg Line 55: 1 OBJE +Could not import O4.jpg Line 59: 1 OBJE +Could not import O5.jpg Line 63: 1 OBJE + diff --git a/data/tests/imp_MediaTest.gramps b/data/tests/imp_MediaTest.gramps index 86decb3568c..1b26571deb6 100644 --- a/data/tests/imp_MediaTest.gramps +++ b/data/tests/imp_MediaTest.gramps @@ -1,9 +1,9 @@ - - + +
- +
@@ -68,7 +68,7 @@
- + @@ -77,12 +77,12 @@ - + - + @@ -106,7 +106,7 @@ - + @@ -161,12 +161,12 @@ n OBJE Records not imported into INDI (individual) Gramps ID I0001: -Could not import test_emb_55.jpg Line 18: 1 OBJE -Could not import test_emb_551.jpg Line 26: 1 OBJE -Could not import test_emb_55.jpg Line 34: 1 OBJE +Could not import test_emb_55.jpg Line 18: 1 OBJE +Could not import test_emb_551.jpg Line 26: 1 OBJE +Could not import test_emb_55.jpg Line 34: 1 OBJE @@ -189,30 +189,32 @@ n @XREF:OBJE@ OBJE {1:1} Records not imported into OBJE (multi-media object) Gramps ID M1: -Tag recognized but not supported Line 58: 1 BLOB - .HM.......k.1..F.jwA.Dzzzzw............A....1.........0U.66..E.8 - .......A..k.a6.A.......A..k.........../6....G.......0../..U..... - .w1/m........HC0..../...zzzzzzzz..5zzk..AnA..U..W6U....2rRrRrRrR - .Dw...............k.1.......1..A...5ykE/zzzx/.g//.Hxzk6/.Tzy/.k1 - /Dw/.Tvz.E5zzUE9/kHz.Tw2/DzzzEEA.kE2zk5yzk2/zzs21.U2/Dw/.Tw/.Tzy - /.fy/.HzzkHzzzo21Ds00.E2.UE2.U62/.k./Ds0.UE0/Do0..E8/UE2.U62.U9w - /.Tx/.20.jg2/jo2..9u/.0U.6A.zk -Line ignored as not understood Line 66: 1 OBJE @M2@ -Filename omitted Line 55: 0 M1 OBJE +Tag recognized but not supported: ' +.HM.......k.1..F.jwA.Dzzzzw............A....1.........0U.66..E. Line 58: 1 BLOB + .HM.......k.1..F.jwA.Dzzzzw............A....1.........0U.66..E.8 + .......A..k.a6.A.......A..k.........../6....G.......0../..U..... + .w1/m........HC0..../...zzzzzzzz..5zzk..AnA..U..W6U....2rRrRrRrR + .Dw...............k.1.......1..A...5ykE/zzzx/.g//.Hxzk6/.Tzy/.k1 + /Dw/.Tvz.E5zzUE9/kHz.Tw2/DzzzEEA.kE2zk5yzk2/zzs21.U2/Dw/.Tw/.Tzy + /.fy/.HzzkHzzzo21Ds00.E2.UE2.U62/.k./Ds0.UE0/Do0..E8/UE2.U62.U9w + /.Tx/.20.jg2/jo2..9u/.0U.6A.zk +Line ignored as not understood Line 66: 1 OBJE @M2@ +Filename omitted Line 55: 0 M1 OBJE Records not imported into OBJE (multi-media object) Gramps ID M2: -Tag recognized but not supported Line 77: 1 BLOB - 67890gramps doesn't do this anyway, so don't bother doing it right. -Filename omitted Line 74: 0 M2 OBJE +Tag recognized but not supported: ' +67890gramps doesn't do this anyway, so don't bother doing it ri Line 77: 1 BLOB + 67890gramps doesn't do this anyway, so don't bother doing it right. +Filename omitted Line 74: 0 M2 OBJE @@ -226,10 +228,10 @@ n @XREF:OBJE@ OBJE {1:1} Records not imported into OBJE (multi-media object) Gramps ID M3: -Could not import test.jpg Line 82: 1 FILE test.jpg +Could not import test.jpg Line 82: 1 FILE test.jpg @@ -255,10 +257,10 @@ n @XREF:OBJE@ OBJE {1:1} Records not imported into OBJE (multi-media object) Gramps ID M4: -Could not import test.jpg Line 88: 1 FILE test.jpg +Could not import test.jpg Line 88: 1 FILE test.jpg @@ -280,10 +282,10 @@ n @XREF:OBJE@ OBJE {1:1} Records not imported into OBJE (multi-media object) Gramps ID M5: -Could not import test.jpg Line 103: 1 FILE test.jpg +Could not import test.jpg Line 103: 1 FILE test.jpg @@ -298,23 +300,23 @@ Could not import test.jpg Line 103: Records not imported into OBJE (multi-media object) Gramps ID M6: -Could not import test.jpg Line 111: 1 FILE test.jpg -Multiple FILE in a single OBJE ignored Line 115: 1 FILE test1.jpg -Skipped subordinate line Line 116: 2 FORM jpeg -Skipped subordinate line Line 117: 3 TYPE photo -Skipped subordinate line Line 118: 2 TITL Multimedia link to linked form v5.5.1 with two files(2) +Could not import test.jpg Line 111: 1 FILE test.jpg +Multiple FILE in a single OBJE ignored Line 115: 1 FILE test1.jpg +Skipped subordinate line Line 116: 2 FORM jpeg +Skipped subordinate line Line 117: 3 TYPE photo +Skipped subordinate line Line 118: 2 TITL Multimedia link to linked form v5.5.1 with two files(2) Records not imported into OBJE (multi-media object) Gramps ID M8: -Could not import No_path_No_Title_NoForm.jpg Line 138: 1 FILE No_path_No_Title_NoForm.jpg +Could not import No_path_No_Title_NoForm.jpg Line 138: 1 FILE No_path_No_Title_NoForm.jpg diff --git a/data/tests/imp_PhonFax_dfs.gramps b/data/tests/imp_PhonFax_dfs.gramps index a8d551f04e5..ea51462dc41 100644 --- a/data/tests/imp_PhonFax_dfs.gramps +++ b/data/tests/imp_PhonFax_dfs.gramps @@ -1,9 +1,9 @@ - - + +
- + The Subm /Tester/ 123 Main St. @@ -204,21 +204,21 @@ Records not imported into HEAD (header): -Only one phone number supported Line 9: 3 PHON (800) 705-7000 -Line ignored as not understood Line 15: 2 DATE 1111-01-01 -GEDCOM FORM should be in uppercase Line 26: 2 FORM Lineage-Linked +Only one phone number supported Line 9: 3 PHON (800) 705-7000 +Line ignored as not understood Line 15: 2 DATE 1111-01-01 +GEDCOM FORM should be in uppercase Line 26: 2 FORM Lineage-Linked Records not imported into SUBM (Submitter): (@SUBM@) The Subm /Tester/: -Only one phone number supported Line 35: 1 PHON 800-871-3401 +Only one phone number supported Line 35: 1 PHON 800-871-3401 @@ -236,10 +236,10 @@ Only one phone number supported Line 35: Records not imported into REPO (repository) Gramps ID R0002: -Only one phone number supported Line 93: 1 PHON 800-765-4321 +Only one phone number supported Line 93: 1 PHON 800-765-4321 diff --git a/data/tests/imp_bug_8322_test.gramps b/data/tests/imp_bug_8322_test.gramps index 3a0edbd355f..808a1728497 100644 --- a/data/tests/imp_bug_8322_test.gramps +++ b/data/tests/imp_bug_8322_test.gramps @@ -1,171 +1,171 @@ - - + +
- +
- + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + Residence - + M Living @@ -206,76 +206,76 @@ - + the place - + the address - + the address - + another address - + another address - + the place - + the address - + the address - + second address - + the place 2 - + second address - + a third address ignored again - + place test @@ -284,209 +284,210 @@ - + address place test - + different place test - + address place test - + a new place - + address place test - + plus an address just for good measure also ignored - + address with no place - + Woerden, Zuid-Holland, Netherlands - + Kromwijkerkade 63 - + Hasselt, Overijssel, Netherlands - + Prinsenstraat 69 - + Enschede, Overijssel, Netherlands - + Calslaan 26-52 - + Calslaan 26-44 - + Enschede, Overijssel, Netherlands - + Calslaan 26-61 - + Amsterdam, Noord-Holland, Netherlands - + Papendrechtstraat 37 - + Olympiaplein 46-2 - + Papendrechtstraat 37 - + Amsterdam, Noord-Holland, Netherlands - + the place created not previously used so changed to add the address; __event_addr(len==0, place is None) - + check that this note is retained when the place is deleted. it should be merged into place - + the place created and then deleted and old data reused; __event_addr(len==0, place is not None) - + setup the place - + the place already exists; but now set doesn't match ; __event_addr(len!=0, place is None) - + the place already exists but now set matches; __event_addr(len!=0, place is not None) - + the address created, then destroyed as we find a matching set; __event_addr(no place_handle, create place) - + address reused, then destroyed as we find a matching set; __event_addr(no place_handle, place found) - + second address ignored - + second address ignored again - + ADDR created; __event_place finds it but place does not match; __event_place(len==0, place is None) - + this note is stored with the old address and then merged into the matching place - + ADDR created; __event_place finds it and now place does match; __event_place(len==0, place is not None) - + setup address place test - + address place test found; place exists and can be reused; __event_place(len!=0, place is not None) - + address place test found; but matching addr/plac not found; __event_place(len!=0, place is not None) - + PLAC occurs first; matching entry found; __event_place(no place handle, place is not None) - + PLAC occurs first; matching entry not found; __event_place(no place handle, place is None) - + note is stashed with a Place, and then merged into the address - + Place note - + ADDR note - + PLAC previously encountered, new ADDR, so new Place - + ADDR before PLAC (check ADDR is removed) - + ADDR before PLAC (address matches previous one, then needs to be reassigned) - + PLAC and no ADDR - + PLAC matches previous one, then when ADDR is read need to create a new Place - + PLAC and ADDR match, use existing one - + Records not imported into INDI (individual) Gramps ID I0310: -A second PLAC ignored Line 109: 2 PLAC a second PLACe ignored -A second PLAC ignored Line 110: 2 PLAC and a third one also ignored +A second PLAC ignored Line 109: 2 PLAC a second PLACe ignored +A second PLAC ignored Line 110: 2 PLAC and a third one also ignored + diff --git a/data/tests/imp_notetest_dfs.gramps b/data/tests/imp_notetest_dfs.gramps index 40d1d966e4d..70d2b405f96 100644 --- a/data/tests/imp_notetest_dfs.gramps +++ b/data/tests/imp_notetest_dfs.gramps @@ -1,9 +1,9 @@ - - + +
- + John A. Tester @@ -48,7 +48,7 @@ - + M Tom @@ -225,7 +225,7 @@ - + @@ -266,44 +266,44 @@ Records not imported into HEAD (header): -Line ignored as not understood Line 18: 2 TEST Header Note -Empty note ignored Line 19: 1 NOTE -Skipped subordinate line Line 20: 2 TEST Empty Note +Line ignored as not understood Line 18: 2 TEST Header Note +Empty note ignored Line 19: 1 NOTE +Skipped subordinate line Line 20: 2 TEST Empty Note Submission Note - + Submission xref note Records not imported into Top Level: -Line ignored as not understood Line 26: 2 TEST Submission Note -Empty note ignored Line 27: 1 NOTE -Skipped subordinate line Line 28: 2 TEST Empty Note -Line ignored as not understood Line 29: 1 TEST submission +Line ignored as not understood Line 26: 2 TEST Submission Note +Empty note ignored Line 27: 1 NOTE +Skipped subordinate line Line 28: 2 TEST Empty Note +Line ignored as not understood Line 29: 1 TEST submission Records not imported into NOTE Gramps ID N0003: -Tag recognized but not supported Line 32: 1 RIN Submission Note RIN -Tag recognized but not supported Line 33: 1 REFN Submission Note REFN -Skipped subordinate line Line 34: 2 TYPE Submission Note REFN TYPE -Tag recognized but not supported Line 35: 1 SOUR Submission note source -Line ignored as not understood Line 39: 1 TEST on XREF Note +Tag recognized but not supported: 'Submission Note RIN' on line Line 32: 1 RIN Submission Note RIN +Tag recognized but not supported: 'Submission Note REFN' on line Line 33: 1 REFN Submission Note REFN +Skipped subordinate line Line 34: 2 TYPE Submission Note REFN TYPE +Tag recognized but not supported: 'Submission note source' on line Line 35: 1 SOUR Submission note source +Line ignored as not understood Line 39: 1 TEST on XREF Note @@ -316,13 +316,13 @@ Line ignored as not understood Line 39: Records not imported into SUBM (Submitter): (@SUBMITTER@) John A. Tester: -Line ignored as not understood Line 43: 2 TEST Submitter Note -Empty note ignored Line 44: 1 NOTE -Skipped subordinate line Line 45: 2 TEST Empty Note -Line ignored as not understood Line 46: 1 TEST Submitter +Line ignored as not understood Line 43: 2 TEST Submitter Note +Empty note ignored Line 44: 1 NOTE +Skipped subordinate line Line 45: 2 TEST Empty Note +Line ignored as not understood Line 46: 1 TEST Submitter @@ -359,7 +359,7 @@ Line ignored as not understood Line 46: Media Note - + Media xref note @@ -368,45 +368,45 @@ Line ignored as not understood Line 46: Records not imported into INDI (individual) Gramps ID I0001: -Empty note ignored Line 54: 2 NOTE -Skipped subordinate line Line 55: 3 NOTE Empty note subordinate (Should be skipped) -Skipped subordinate line Line 56: 3 TEST Empty Note -Line ignored as not understood Line 58: 3 TEST 123456 Event Note -Line ignored as not understood Line 62: 4 TEST Location Note -Line ignored as not understood Line 63: 3 TEST Location -Empty note ignored Line 66: 2 NOTE -Skipped subordinate line Line 67: 3 TEST Empty Note -Empty note ignored Line 68: 2 NOTE -Skipped subordinate line Line 69: 3 TEST Empty Note -Line ignored as not understood Line 73: 3 TEST FAMS Note -Skipped subordinate line Line 74: 4 TEST skip on FAMS Note -Line ignored as not understood Line 75: 2 TEST FAMS -Line ignored as not understood Line 79: 2 TEST Person Note -Line ignored as not understood Line 82: 2 TEST media -Empty note ignored Line 85: 2 NOTE -Skipped subordinate line Line 86: 3 TEST Empty Note -Line ignored as not understood Line 88: 3 TEST 123456 Note -Empty note ignored Line 90: 2 NOTE -Skipped subordinate line Line 91: 3 TEST Empty Note -Line ignored as not understood Line 94: 2 TEST REFN +Empty note ignored Line 54: 2 NOTE +Skipped subordinate line Line 55: 3 NOTE Empty note subordinate (Should be skipped) +Skipped subordinate line Line 56: 3 TEST Empty Note +Line ignored as not understood Line 58: 3 TEST 123456 Event Note +Line ignored as not understood Line 62: 4 TEST Location Note +Line ignored as not understood Line 63: 3 TEST Location +Empty note ignored Line 66: 2 NOTE +Skipped subordinate line Line 67: 3 TEST Empty Note +Empty note ignored Line 68: 2 NOTE +Skipped subordinate line Line 69: 3 TEST Empty Note +Line ignored as not understood Line 73: 3 TEST FAMS Note +Skipped subordinate line Line 74: 4 TEST skip on FAMS Note +Line ignored as not understood Line 75: 2 TEST FAMS +Line ignored as not understood Line 79: 2 TEST Person Note +Line ignored as not understood Line 82: 2 TEST media +Empty note ignored Line 85: 2 NOTE +Skipped subordinate line Line 86: 3 TEST Empty Note +Line ignored as not understood Line 88: 3 TEST 123456 Note +Empty note ignored Line 90: 2 NOTE +Skipped subordinate line Line 91: 3 TEST Empty Note +Line ignored as not understood Line 94: 2 TEST REFN Records not imported into NOTE Gramps ID N0018: -Tag recognized but not supported Line 103: 1 RIN 123456 -Tag recognized but not supported Line 104: 1 REFN 98765 -Skipped subordinate line Line 105: 2 TYPE Who knows REFN TYPE -Tag recognized but not supported Line 109: 2 NOTE Note on a change on a note!!! -Skipped subordinate line Line 110: 3 CHAN -Skipped subordinate line Line 111: 4 DATE 2001-01-11 -Skipped subordinate line Line 112: 5 TIME 16:00:06 +Tag recognized but not supported: '123456' on line Line 103: 1 RIN 123456 +Tag recognized but not supported: '98765' on line Line 104: 1 REFN 98765 +Skipped subordinate line Line 105: 2 TYPE Who knows REFN TYPE +Tag recognized but not supported: 'Note on a change on a note!!!' on line Line 109: 2 NOTE Note on a change on a note!!! +Skipped subordinate line Line 110: 3 CHAN +Skipped subordinate line Line 111: 4 DATE 2001-01-11 +Skipped subordinate line Line 112: 5 TIME 16:00:06 @@ -415,27 +415,27 @@ Skipped subordinate line Line 112: Records not imported into OBJE (multi-media object) Gramps ID M1: -Could not import photo.jpg Line 114: 1 FILE photo.jpg +Could not import photo.jpg Line 114: 1 FILE photo.jpg - + Family Spouse reference Note Records not imported into INDI (individual) Gramps ID I0002: -Empty note ignored Line 132: 2 NOTE -Skipped subordinate line Line 133: 3 TEST Empty Note +Empty note ignored Line 132: 2 NOTE +Skipped subordinate line Line 133: 3 TEST Empty Note - + Name note @@ -446,31 +446,31 @@ Skipped subordinate line Line 133: LDS xref note - + Association link note Records not imported into INDI (individual) Gramps ID I0003: -Empty note ignored Line 141: 2 NOTE -Skipped subordinate line Line 142: 3 TEST Empty Note -Line ignored as not understood Line 163: 3 TEST Accociation note -Empty note ignored Line 167: 2 NOTE -Skipped subordinate line Line 168: 3 TEST Empty Note +Empty note ignored Line 141: 2 NOTE +Skipped subordinate line Line 142: 3 TEST Empty Note +Line ignored as not understood Line 163: 3 TEST Accociation note +Empty note ignored Line 167: 2 NOTE +Skipped subordinate line Line 168: 3 TEST Empty Note - + Just for association Family xref note - + Family note @@ -481,43 +481,43 @@ Skipped subordinate line Line 168: A sample text from a source of this family - + A note this citation is on the FAMILY record. Records not imported into FAM (family) Gramps ID F0001: -Line ignored as not understood Line 183: 2 TEST Family Note -Empty note ignored Line 187: 1 NOTE -Skipped subordinate line Line 188: 2 TEST Empty Note -Line ignored as not understood Line 190: 2 TEST citation -Line ignored as not understood Line 194: 3 TEST Citation Data -Line ignored as not understood Line 196: 4 TEST Citation Data Note +Line ignored as not understood Line 183: 2 TEST Family Note +Empty note ignored Line 187: 1 NOTE +Skipped subordinate line Line 188: 2 TEST Empty Note +Line ignored as not understood Line 190: 2 TEST citation +Line ignored as not understood Line 194: 3 TEST Citation Data +Line ignored as not understood Line 196: 4 TEST Citation Data Note - + A short note about the repository link. - + note embedded in the SOURCE Record Records not imported into SOUR (source) Gramps ID S0001: -Line ignored as not understood Line 206: 1 TEST source -Empty note ignored Line 208: 2 NOTE -Skipped subordinate line Line 209: 3 TEST Empty Note -Empty note ignored Line 214: 1 NOTE -Skipped subordinate line Line 215: 2 TEST Empty Note +Line ignored as not understood Line 206: 1 TEST source +Empty note ignored Line 208: 2 NOTE +Skipped subordinate line Line 209: 3 TEST Empty Note +Empty note ignored Line 214: 1 NOTE +Skipped subordinate line Line 215: 2 TEST Empty Note @@ -527,12 +527,12 @@ Skipped subordinate line Line 215: Records not imported into REPO (repository) Gramps ID R0002: -Line ignored as not understood Line 223: 1 TEST Repo -Empty note ignored Line 224: 1 NOTE -Skipped subordinate line Line 225: 2 TEST Empty Note +Line ignored as not understood Line 223: 1 TEST Repo +Empty note ignored Line 224: 1 NOTE +Skipped subordinate line Line 225: 2 TEST Empty Note diff --git a/data/tests/imp_notetest_lds_dfs.ged b/data/tests/imp_notetest_lds_dfs.ged new file mode 100644 index 00000000000..7b5ff24d5f4 --- /dev/null +++ b/data/tests/imp_notetest_lds_dfs.ged @@ -0,0 +1,384 @@ +0 HEAD +1 SOUR GEDitCOM +2 NAME GEDitCOM +2 VERS 2.9.4 +2 CORP RSAC Software +1 SUBM @SUBMITTER@ +1 SUBN @SUBMISSION@ +1 DEST ANSTFILE +1 DATE 1 JAN 1998 +2 TIME 13:57:24.80 +1 FILE imp_notetest.ged +1 COPR Tom Tester 2016 +1 GEDC +2 VERS 5.5 +2 FORM LINEAGE-LINKED +1 LANG English +1 NOTE Header note +2 TEST Header Note +1 NOTE +2 TEST Empty Note +1 CHAR UTF8 +0 @SUBMISSION@ SUBN +1 SUBM @SUBMITTER@ +1 FAMF NameOfFamilyFile +1 NOTE Submission Note +2 TEST Submission Note +1 NOTE +2 TEST Empty Note +1 TEST submission +1 NOTE @N2@ +0 @N2@ NOTE Submission xref note +1 RIN Submission Note RIN +1 REFN Submission Note REFN +2 TYPE Submission Note REFN TYPE +1 SOUR Submission note source +1 CHAN +2 DATE 11 Jan 2001 +3 TIME 16:00:06 +1 TEST on XREF Note +0 @SUBMITTER@ SUBM +1 NAME John A. Tester +1 NOTE Submitter Note +2 TEST Submitter Note +1 NOTE +2 TEST Empty Note +1 TEST Submitter +1 NOTE @N3@ +0 @N3@ NOTE Submitter xref note +0 @I1@ INDI +1 NAME Tom /Tester/ +1 TEST person +1 SEX M +1 BIRT +2 NOTE +3 NOTE Empty note subordinate (Should be skipped) +3 TEST Empty Note +2 NOTE Birth Event note +3 TEST 123456 Event Note +2 DATE 15 JUN 1900 +2 ADDR 123 main, Norwalk, Ohio, USA +3 NOTE Location Note +4 TEST Location Note +3 TEST Location +3 NOTE Location Note 2 +1 DEAT +2 NOTE +3 TEST Empty Note +2 NOTE +3 TEST Empty Note +2 NOTE @N5@ +1 FAMS @F1@ +2 NOTE FAMS Note +3 TEST FAMS Note +4 TEST skip on FAMS Note +2 TEST FAMS +2 NOTE FAMS Note 2 +1 NOTE @N4@ +1 NOTE Tom Tester Note +2 TEST Person Note +1 OBJE +2 FORM URL +2 TEST media +2 TITL GEDCOM 5.5 documentation web site +2 FILE http://homepages.rootsweb.com/~pmcbride/gedcom/55gctoc.htm +2 NOTE +3 TEST Empty Note +2 NOTE Media Note +3 TEST 123456 Note +2 NOTE @N6@ +2 NOTE +3 TEST Empty Note +1 RIN 123456 Person +1 REFN 98765 for PERSON +2 TEST REFN +2 TYPE Who knows OBJE REFN TYPE +1 CHAN +2 DATE 11 Jan 2001 +3 TIME 16:00:06 +1 OBJE @M1@ +1 _PPEXCLUDE +1 _UID EAE358D5CAEC4F4A8146EFCAFB572480BF11 +1 _FSFTID LBYN-GFG +1 _FSLINK https://familysearch.org +0 @N4@ NOTE Tom Tester xref note +0 @N5@ NOTE Death Event xref note +0 @N6@ NOTE Media xref note +1 RIN 123456 +1 REFN 98765 +2 TYPE Who knows REFN TYPE +1 CHAN +2 DATE 11 Jan 2001 +3 TIME 16:00:06 +2 NOTE Note on a change on a note!!! +3 CHAN +4 DATE 11 Jan 2001 +5 TIME 16:00:06 +0 @M1@ OBJE +1 FILE photo.jpg +2 FORM jpeg +3 TYPE Film +2 TITL Tom Tester's photo +1 RIN 123456 +1 REFN 98765 +2 TYPE Who knows REFN TYPE +1 CHAN +2 DATE 11 Jan 2001 +3 TIME 16:00:06 +0 @I2@ INDI +1 NAME Mrs /Tester/ +1 SEX F +1 BIRT +2 DATE 15 JUN 1901 +1 DEAT +2 DATE 5 JUL 1975 +1 FAMS @F1@ +2 NOTE +3 TEST Empty Note +2 NOTE Family Spouse reference Note +3 CHAN +4 DATE 11 Jan 2001 +5 TIME 16:00:06 +0 @I3@ INDI +1 NAME Ed /Tester/ +2 NICK Eddie +2 NOTE +3 TEST Empty Note +2 NOTE Name note +3 CHAN +4 DATE 11 Jan 2001 +5 TIME 16:00:06 +1 SEX M +1 BIRT +2 DATE 15 JUN 1922 +1 DEAT +2 DATE 5 JUL 1994 +1 FAMC @F1@ +1 BAPL +2 DATE 5 MAY 0005 B.C. +2 PLAC Salt Lake City +3 NOTE Place note +2 STAT Cleared +2 TEMP SLAKE +2 SOUR @SOURCE1@ +2 NOTE @N8@ +1 ASSO @I4@ +2 NOTE Association link note +3 TEST Accociation note +3 CHAN +4 DATE 11 Jan 2001 +5 TIME 16:00:06 +2 NOTE +3 TEST Empty Note +0 @I4@ INDI +1 NAME George /Testee/ +2 NOTE Just for association +3 CHAN +4 DATE 11 Jan 2001 +5 TIME 16:00:06 +0 @F1@ FAM +1 HUSB @I1@ +2 _PREF Y +1 TEST family +1 WIFE @I2@ +2 _PREF Y +1 CHIL @I3@ +2 _PREF Y +1 SOUR @S1@ +1 NOTE @N7@ +1 NOTE Family note +2 TEST Family Note +2 CHAN +3 DATE 11 Jan 2001 +4 TIME 16:00:06 +1 NOTE +2 TEST Empty Note +1 SOUR @S1@ +2 TEST citation +2 PAGE 42 +2 DATA +3 DATE 31 DEC 1900 +3 TEST Citation Data +3 NOTE Citation Data Note +4 TEST Citation Data Note +3 TEXT A sample text from a source of this family +2 QUAY 0 +2 NOTE A note this citation is on the FAMILY record. +3 CHAN +4 DATE 11 Jan 2001 +5 TIME 16:00:06 +0 @N7@ NOTE Family xref note +0 @S1@ SOUR +1 TITL Note Test file Source: Tester +1 TEST source +1 REPO @R1@ +2 NOTE +3 TEST Empty Note +2 NOTE A short note about the repository link. +3 CHAN +4 DATE 11 Jan 2001 +5 TIME 16:00:06 +1 NOTE +2 TEST Empty Note +1 NOTE note embedded in the SOURCE Record +2 CHAN +3 DATE 11 Jan 2001 +4 TIME 16:00:06 +0 @N8@ NOTE LDS xref note +1 NOTE note embedded in the SOURCE Record +2 CHAN +3 DATE 11 Jan 2001 +4 TIME 16:00:06 +0 @S55@ SOUR +1 ABBR 1860 United States Federal Census +1 TITL 1860 United States Federal Census +1 _PAREN Y +0 @S33@ SOUR +1 ABBR 1870 United States Federal Census +1 TITL 1870 United States Federal Census +1 _PAREN Y +0 @S28@ SOUR +1 ABBR 1880 United States Federal Census +1 TITL 1880 United States Federal Census +0 @S71@ SOUR +1 ABBR FamilySearch Family Tree (https://www.familysearch.org) +1 TITL "FamilySearch Family Tree," database, FamilySearch +1 AUTH The Church of Jesus Christ of Latter-day Saints +1 PUBL (https://www.familysearch.org) +1 _ITALIC Y +1 _PAREN Y +0 @N8@ NOTE LDS xref note +0 @R2@ REPO +1 NAME The National Archives and Records Administration (NARA) +1 ADDR The National Archives and Records Administration (NARA) +2 _NAME The National Archives and Records Administration (NARA) +2 ADR1 The National Archives and Records Administration (NARA) +0 _PLAC_DEFN +1 PLAC New Rochelle, New York +2 ABBR New Rochelle, New York +2 _RESOLVED -1 +0 _PLAC_DEFN +1 PLAC Austin, Texas +2 ABBR Austin, Texas +2 _RESOLVED -1 +0 _PLAC_DEFN +1 PLAC Cedar Rapids, Linn, Iowa, United States +2 ABBR Cedar Rapids, Linn, Iowa, United States +2 _RESOLVED -1 +0 _PLAC_DEFN +1 PLAC Philadelphia, Philadelphia, Penn. +2 ABBR Philadelphia, Philadelphia, Penn. +2 _RESOLVED -1 +0 _PLAC_DEFN +1 PLAC Oakland Cemetery, Philadelphia, Pennsylvania, United States +2 ABBR Oakland Cemetery, Philadelphia, Pennsylvania, United States +2 _RESOLVED -1 +0 _PLAC_DEFN +1 PLAC Greenmount Cemetery, Philadelphia, Pennsylvania, United States +2 ABBR Greenmount Cemetery, Philadelphia, Pennsylvania, United States +2 _RESOLVED -1 +0 _EVENT_DEFN Adoption +1 TYPE I +1 TITL Adoption +1 _SEN1 [HeShe] was adopted [onDate] [inPlace] by [Desc]. +1 _SEN2 [HeShe] was adopted [onDate]. +1 _SEN3 [HeShe] was adopted [inPlace]. +1 _SEN4 [HeShe] was adopted by [Desc]. +1 _SEN5 [HeShe] was adopted [onDate] by [Desc]. +1 _SEN6 [HeShe] was adopted [inPlace] by [Desc]. +1 _SEN7 [HeShe] was adopted [onDate] [inPlace]. +1 _SEN8 [HeShe] was adopted. +1 ROLE Witness +2 _SEN1 [FirstHeShe] witnessed the adoption of [MainPerson] [onDate] [inPlace]: [Desc]. +2 _SEN2 [FirstHeShe] witnessed the adoption of [MainPerson] [onDate]. +2 _SEN3 [FirstHeShe] witnessed the adoption of [MainPerson] [inPlace]. +2 _SEN4 [FirstHeShe] witnessed the adoption of [MainPerson]: [Desc]. +2 _SEN5 [FirstHeShe] witnessed the adoption of [MainPerson] [onDate]: [Desc]. +2 _SEN6 [FirstHeShe] witnessed the adoption of [MainPerson] [inPlace]: [Desc]. +2 _SEN7 [FirstHeShe] witnessed the adoption of [MainPerson] [onDate] [inPlace]. +2 _SEN8 [FirstHeShe] witnessed the adoption of [MainPerson]. +2 _DEF Y +1 _DATE_TYPE 1 +1 _PLACE_TYPE 1 +1 _DESC_FLAG 1 +1 _INC_NOTES +0 _EVENT_DEFN Alt. Birth +1 TYPE I +1 TITL Alt. Birth +1 _SEN1 [HeShe] has conflicting birth information of [Desc], [Date] and [Place]. +1 _SEN2 [HeShe] has conflicting birth information of [Date]. +1 _SEN3 [HeShe] has conflicting birth information of [Place]. +1 _SEN4 [HeShe] has conflicting birth information of [Desc]. +1 _SEN5 [HeShe] has conflicting birth information of [Desc] and [Date]. +1 _SEN6 [HeShe] has conflicting birth information of [Desc] and [Place]. +1 _SEN7 [HeShe] has conflicting birth information of [Date] and [Place]. +1 _SEN8 [HeShe] has conflicting birth information. +1 ROLE Doctor +2 _SEN1 [FirstHeShe] delivered [MainPerson] [onDate] [inPlace]: [Desc]. +2 _SEN2 [FirstHeShe] delivered [MainPerson] [onDate]. +2 _SEN3 [FirstHeShe] delivered [MainPerson] [inPlace]. +2 _SEN4 [FirstHeShe] delivered [MainPerson]: [Desc]. +2 _SEN5 [FirstHeShe] delivered [MainPerson] [onDate]: [Desc]. +2 _SEN6 [FirstHeShe] delivered [MainPerson] [inPlace]: [Desc]. +2 _SEN7 [FirstHeShe] delivered [MainPerson] [onDate] [inPlace]. +2 _SEN8 [FirstHeShe] delivered [MainPerson]. +1 ROLE Father +2 _SEN1 [FirstHeShe] is the father of [MainPerson] born [onDate] [inPlace]: [Desc]. +2 _SEN2 [FirstHeShe] is the father of [MainPerson] born [onDate]. +2 _SEN3 [FirstHeShe] is the father of [MainPerson] born [inPlace]. +2 _SEN4 [FirstHeShe] is the father of [MainPerson] born: [Desc]. +2 _SEN5 [FirstHeShe] is the father of [MainPerson] born [onDate]: [Desc]. +2 _SEN6 [FirstHeShe] is the father of [MainPerson] born [inPlace]: [Desc]. +2 _SEN7 [FirstHeShe] is the father of [MainPerson] born [onDate] [inPlace]. +2 _SEN8 [FirstHeShe] is the father of [MainPerson] born. +1 ROLE Godfather +2 _SEN1 [FirstHeShe] is the godfather of [MainPerson] [onDate] [inPlace]: [Desc]. +2 _SEN2 [FirstHeShe] is the godfather of [MainPerson] [onDate]. +2 _SEN3 [FirstHeShe] is the godfather of [MainPerson] [inPlace]. +2 _SEN4 [FirstHeShe] is the godfather of [MainPerson]: [Desc]. +2 _SEN5 [FirstHeShe] is the godfather of [MainPerson] [onDate]: [Desc]. +2 _SEN6 [FirstHeShe] is the godfather of [MainPerson] [inPlace]: [Desc]. +2 _SEN7 [FirstHeShe] is the godfather of [MainPerson] [onDate] [inPlace]. +2 _SEN8 [FirstHeShe] is the godfather of [MainPerson]. +1 ROLE Godmother +2 _SEN1 [FirstHeShe] is the godmother of [MainPerson] [onDate] [inPlace]: [Desc]. +2 _SEN2 [FirstHeShe] is the godmother of [MainPerson] [onDate]. +2 _SEN3 [FirstHeShe] is the godmother of [MainPerson] [inPlace]. +2 _SEN4 [FirstHeShe] is the godmother of [MainPerson]: [Desc]. +2 _SEN5 [FirstHeShe] is the godmother of [MainPerson] [onDate]: [Desc]. +2 _SEN6 [FirstHeShe] is the godmother of [MainPerson] [inPlace]: [Desc]. +2 _SEN7 [FirstHeShe] is the godmother of [MainPerson] [onDate] [inPlace]. +2 _SEN8 [FirstHeShe] is the godmother of [MainPerson]. +1 ROLE Midwife +2 _SEN1 [FirstHeShe] delivered [MainPerson] [onDate] [inPlace]: [Desc]. +2 _SEN2 [FirstHeShe] delivered [MainPerson] [onDate]. +2 _SEN3 [FirstHeShe] delivered [MainPerson] [inPlace]. +2 _SEN4 [FirstHeShe] delivered [MainPerson]: [Desc]. +2 _SEN5 [FirstHeShe] delivered [MainPerson] [onDate]: [Desc]. +2 _SEN6 [FirstHeShe] delivered [MainPerson] [inPlace]: [Desc]. +2 _SEN7 [FirstHeShe] delivered [MainPerson] [onDate] [inPlace]. +2 _SEN8 [FirstHeShe] delivered [MainPerson]. +1 ROLE Mother +2 _SEN1 [FirstHeShe] is the mother of [MainPerson] born [onDate] [inPlace]: [Desc]. +2 _SEN2 [FirstHeShe] is the mother of [MainPerson] born [onDate]. +2 _SEN3 [FirstHeShe] is the mother of [MainPerson] born [inPlace]. +2 _SEN4 [FirstHeShe] is the mother of [MainPerson] born: [Desc]. +2 _SEN5 [FirstHeShe] is the mother of [MainPerson] born [onDate]: [Desc]. +2 _SEN6 [FirstHeShe] is the mother of [MainPerson] born [inPlace]: [Desc]. +2 _SEN7 [FirstHeShe] is the mother of [MainPerson] born [onDate] [inPlace]. +2 _SEN8 [FirstHeShe] is the mother of [MainPerson] born. +1 ROLE Witness +2 _SEN1 [FirstHeShe] witnessed the birth of [MainPerson] [onDate] [inPlace]: [Desc]. +2 _SEN2 [FirstHeShe] witnessed the birth of [MainPerson] [onDate]. +2 _SEN3 [FirstHeShe] witnessed the birth of [MainPerson] [inPlace]. +2 _SEN4 [FirstHeShe] witnessed the birth of [MainPerson]: [Desc]. +2 _SEN5 [FirstHeShe] witnessed the birth of [MainPerson] [onDate]: [Desc]. +2 _SEN6 [FirstHeShe] witnessed the birth of [MainPerson] [inPlace]: [Desc]. +2 _SEN7 [FirstHeShe] witnessed the birth of [MainPerson] [onDate] [inPlace]. +2 _SEN8 [FirstHeShe] witnessed the birth of [MainPerson]. +2 _DEF Y +1 _DATE_TYPE 1 +1 _PLACE_TYPE 1 +1 _DESC_FLAG 1 +1 _INC_NOTES +0 TRLR \ No newline at end of file diff --git a/data/tests/imp_notetest_lds_dfs.gramps b/data/tests/imp_notetest_lds_dfs.gramps new file mode 100644 index 00000000000..60fa726c40d --- /dev/null +++ b/data/tests/imp_notetest_lds_dfs.gramps @@ -0,0 +1,816 @@ + + + +
+ + + John A. Tester + +
+ + + + + + TEST + person + + + Birth + + + + + + Death + + + + _PPEXCLUDE + + + _FSLINK + https://familysearch.org + + + Birth + + + + Death + + + + Birth + + + + Death + + + + _PREF + + + TEST + family + + + _PREF + + + + + M + + Tom + Tester + + + + + + + + + + + + + + + + + + + + + + + + + F + + Mrs + Tester + + + + + + + + + + + M + + Ed + Tester + Eddie + + + + + + + + + + + + + + + + + + + + + + U + + George + Testee + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + 2 + + + + 2 + + + + 2 + + + + 2 + + + + 2 + + + + 2 + + + + + 42 + 0 + + + + + + + + + Import from imp_notetest.ged + Tom Tester 2016 + + + + + + + + + + + + + + + + + + + + @SOURCE1@ + + + Note Test file Source: Tester + + + + + + + + 1860 United States Federal Census + 1860 United States Federal Census + + + + 1870 United States Federal Census + 1870 United States Federal Census + + + + 1880 United States Federal Census + 1880 United States Federal Census + + + "FamilySearch Family Tree," database, <i>FamilySearch</i> + The Church of Jesus Christ of Latter-day Saints + (https://www.familysearch.org) + FamilySearch Family Tree (https://www.familysearch.org) + + + + + + 123 main, Norwalk, Ohio, USA + + + + + + + Salt Lake City + + + + + + + + + + + + + + + + + + + Business that produced the product: RSAC Software + GEDCOM data + + + SUBM (Submitter): (@SUBMITTER@) John A. Tester + GEDCOM data +
+
+ + + +
+ + Unknown + Unknown + + + + The National Archives and Records Administration (NARA) + Library +
+ The National Archives and Records Administration (NARA) +
+ +
+
+ + + Header note + + + + Records not imported into HEAD (header): + +Line ignored as not understood Line 18: 2 TEST Header Note +Empty note ignored Line 19: 1 NOTE +Skipped subordinate line Line 20: 2 TEST Empty Note + + + + + Submission Note + + + + Submission xref note + + + Records not imported into Top Level: + +Line ignored as not understood Line 26: 2 TEST Submission Note +Empty note ignored Line 27: 1 NOTE +Skipped subordinate line Line 28: 2 TEST Empty Note +Line ignored as not understood Line 29: 1 TEST submission + + + + + Records not imported into NOTE Gramps ID N0003: + +Tag recognized but not supported: 'Submission Note RIN' on line Line 32: 1 RIN Submission Note RIN +Tag recognized but not supported: 'Submission Note REFN' on line Line 33: 1 REFN Submission Note REFN +Skipped subordinate line Line 34: 2 TYPE Submission Note REFN TYPE +Tag recognized but not supported: 'Submission note source' on line Line 35: 1 SOUR Submission note source +Line ignored as not understood Line 39: 1 TEST on XREF Note + + + + + Submitter Note + + + + Submitter xref note + + + Records not imported into SUBM (Submitter): (@SUBMITTER@) John A. Tester: + +Line ignored as not understood Line 43: 2 TEST Submitter Note +Empty note ignored Line 44: 1 NOTE +Skipped subordinate line Line 45: 2 TEST Empty Note +Line ignored as not understood Line 46: 1 TEST Submitter + + + + + Birth Event note + + + + Location Note + + + + Location Note 2 + + + + Death Event xref note + + + FAMS Note + + + + FAMS Note 2 + + + + Tom Tester xref note + + + Tom Tester Note + + + + Media Note + + + + Media xref note + + + Who knows OBJE REFN TYPE + + + Records not imported into INDI (individual) Gramps ID I0001: + +Empty note ignored Line 54: 2 NOTE +Skipped subordinate line Line 55: 3 NOTE Empty note subordinate (Should be skipped) +Skipped subordinate line Line 56: 3 TEST Empty Note +Line ignored as not understood Line 58: 3 TEST 123456 Event Note +Line ignored as not understood Line 62: 4 TEST Location Note +Line ignored as not understood Line 63: 3 TEST Location +Empty note ignored Line 66: 2 NOTE +Skipped subordinate line Line 67: 3 TEST Empty Note +Empty note ignored Line 68: 2 NOTE +Skipped subordinate line Line 69: 3 TEST Empty Note +Line ignored as not understood Line 73: 3 TEST FAMS Note +Skipped subordinate line Line 74: 4 TEST skip on FAMS Note +Line ignored as not understood Line 75: 2 TEST FAMS +Line ignored as not understood Line 79: 2 TEST Person Note +Line ignored as not understood Line 82: 2 TEST media +Empty note ignored Line 85: 2 NOTE +Skipped subordinate line Line 86: 3 TEST Empty Note +Line ignored as not understood Line 88: 3 TEST 123456 Note +Empty note ignored Line 90: 2 NOTE +Skipped subordinate line Line 91: 3 TEST Empty Note +Line ignored as not understood Line 94: 2 TEST REFN + + + + + Records not imported into NOTE Gramps ID N0018: + +Tag recognized but not supported: '123456' on line Line 107: 1 RIN 123456 +Tag recognized but not supported: '98765' on line Line 108: 1 REFN 98765 +Skipped subordinate line Line 109: 2 TYPE Who knows REFN TYPE +Tag recognized but not supported: 'Note on a change on a note!!!' on line Line 113: 2 NOTE Note on a change on a note!!! +Skipped subordinate line Line 114: 3 CHAN +Skipped subordinate line Line 115: 4 DATE 2001-01-11 +Skipped subordinate line Line 116: 5 TIME 16:00:06 + + + + + Who knows REFN TYPE + + + Records not imported into OBJE (multi-media object) Gramps ID M1: + +Could not import photo.jpg Line 118: 1 FILE photo.jpg + + + + + Family Spouse reference Note + + + + Records not imported into INDI (individual) Gramps ID I0002: + +Empty note ignored Line 136: 2 NOTE +Skipped subordinate line Line 137: 3 TEST Empty Note + + + + + Name note + + + + Place note + + + + LDS xref note + + + Association link note + + + + Records not imported into INDI (individual) Gramps ID I0003: + +Empty note ignored Line 145: 2 NOTE +Skipped subordinate line Line 146: 3 TEST Empty Note +Line ignored as not understood Line 167: 3 TEST Accociation note +Empty note ignored Line 171: 2 NOTE +Skipped subordinate line Line 172: 3 TEST Empty Note + + + + + Just for association + + + + Family xref note + + + Family note + + + + Citation Data Note + + + + A sample text from a source of this family + + + A note this citation is on the FAMILY record. + + + + Records not imported into FAM (family) Gramps ID F0001: + +Tag recognized but not supported: 'Y' on line Line 186: 2 _PREF Y +Line ignored as not understood Line 190: 2 TEST Family Note +Empty note ignored Line 194: 1 NOTE +Skipped subordinate line Line 195: 2 TEST Empty Note +Line ignored as not understood Line 197: 2 TEST citation +Line ignored as not understood Line 201: 3 TEST Citation Data +Line ignored as not understood Line 203: 4 TEST Citation Data Note + + + + + A short note about the repository link. + + + + note embedded in the SOURCE Record + + + + Records not imported into SOUR (source) Gramps ID S0001: + +Line ignored as not understood Line 213: 1 TEST source +Empty note ignored Line 215: 2 NOTE +Skipped subordinate line Line 216: 3 TEST Empty Note +Empty note ignored Line 221: 1 NOTE +Skipped subordinate line Line 222: 2 TEST Empty Note + + + + + Records not imported into NOTE Gramps ID N0028: + +Line ignored as not understood Line 228: 1 NOTE note embedded in the SOURCE Record +Skipped subordinate line Line 229: 2 CHAN +Skipped subordinate line Line 230: 3 DATE 2001-01-11 +Skipped subordinate line Line 231: 4 TIME 16:00:06 + + + + + Records not imported into SOUR (source) Gramps ID S0055: + +Tag recognized but not supported: 'Y' on line Line 235: 1 _PAREN Y + + + + + Records not imported into SOUR (source) Gramps ID S0033: + +Tag recognized but not supported: 'Y' on line Line 239: 1 _PAREN Y + + + + + Records not imported into SOUR (source) Gramps ID S0071: + +Tag recognized but not supported: 'Y' on line Line 248: 1 _ITALIC Y +Tag recognized but not supported: 'Y' on line Line 249: 1 _PAREN Y + + + + + Records not imported into REPO (repository) Gramps ID R0003: + +Tag recognized but not supported: 'The National Archives and Records Administration (NARA)' on line Line 254: 2 _NAME The National Archives and Records Administration (NARA) + + + + + Records not imported into Top Level: + +Unknown tag Line 256: 0 _PLAC_DEFN +Skipped subordinate line Line 257: 1 PLAC New Rochelle, New York +Skipped subordinate line Line 258: 2 ABBR New Rochelle, New York +Skipped subordinate line Line 259: 2 _RESOLVED -1 + + + + + Records not imported into Top Level: + +Unknown tag Line 260: 0 _PLAC_DEFN +Skipped subordinate line Line 261: 1 PLAC Austin, Texas +Skipped subordinate line Line 262: 2 ABBR Austin, Texas +Skipped subordinate line Line 263: 2 _RESOLVED -1 + + + + + Records not imported into Top Level: + +Unknown tag Line 264: 0 _PLAC_DEFN +Skipped subordinate line Line 265: 1 PLAC Cedar Rapids, Linn, Iowa, United States +Skipped subordinate line Line 266: 2 ABBR Cedar Rapids, Linn, Iowa, United States +Skipped subordinate line Line 267: 2 _RESOLVED -1 + + + + + Records not imported into Top Level: + +Unknown tag Line 268: 0 _PLAC_DEFN +Skipped subordinate line Line 269: 1 PLAC Philadelphia, Philadelphia, Penn. +Skipped subordinate line Line 270: 2 ABBR Philadelphia, Philadelphia, Penn. +Skipped subordinate line Line 271: 2 _RESOLVED -1 + + + + + Records not imported into Top Level: + +Unknown tag Line 272: 0 _PLAC_DEFN +Skipped subordinate line Line 273: 1 PLAC Oakland Cemetery, Philadelphia, Pennsylvania, United States +Skipped subordinate line Line 274: 2 ABBR Oakland Cemetery, Philadelphia, Pennsylvania, United States +Skipped subordinate line Line 275: 2 _RESOLVED -1 + + + + + Records not imported into Top Level: + +Unknown tag Line 276: 0 _PLAC_DEFN +Skipped subordinate line Line 277: 1 PLAC Greenmount Cemetery, Philadelphia, Pennsylvania, United States +Skipped subordinate line Line 278: 2 ABBR Greenmount Cemetery, Philadelphia, Pennsylvania, United States +Skipped subordinate line Line 279: 2 _RESOLVED -1 + + + + + Records not imported into Top Level: + +Unknown tag Line 280: 0 _EVENT_DEFN Adoption +Skipped subordinate line Line 281: 1 TYPE I +Skipped subordinate line Line 282: 1 TITL Adoption +Skipped subordinate line Line 283: 1 _SEN1 [HeShe] was adopted [onDate] [inPlace] by [Desc]. +Skipped subordinate line Line 284: 1 _SEN2 [HeShe] was adopted [onDate]. +Skipped subordinate line Line 285: 1 _SEN3 [HeShe] was adopted [inPlace]. +Skipped subordinate line Line 286: 1 _SEN4 [HeShe] was adopted by [Desc]. +Skipped subordinate line Line 287: 1 _SEN5 [HeShe] was adopted [onDate] by [Desc]. +Skipped subordinate line Line 288: 1 _SEN6 [HeShe] was adopted [inPlace] by [Desc]. +Skipped subordinate line Line 289: 1 _SEN7 [HeShe] was adopted [onDate] [inPlace]. +Skipped subordinate line Line 290: 1 _SEN8 [HeShe] was adopted. +Skipped subordinate line Line 291: 1 ROLE Witness +Skipped subordinate line Line 292: 2 _SEN1 [FirstHeShe] witnessed the adoption of [MainPerson] [onDate] [inPlace]: [Desc]. +Skipped subordinate line Line 293: 2 _SEN2 [FirstHeShe] witnessed the adoption of [MainPerson] [onDate]. +Skipped subordinate line Line 294: 2 _SEN3 [FirstHeShe] witnessed the adoption of [MainPerson] [inPlace]. +Skipped subordinate line Line 295: 2 _SEN4 [FirstHeShe] witnessed the adoption of [MainPerson]: [Desc]. +Skipped subordinate line Line 296: 2 _SEN5 [FirstHeShe] witnessed the adoption of [MainPerson] [onDate]: [Desc]. +Skipped subordinate line Line 297: 2 _SEN6 [FirstHeShe] witnessed the adoption of [MainPerson] [inPlace]: [Desc]. +Skipped subordinate line Line 298: 2 _SEN7 [FirstHeShe] witnessed the adoption of [MainPerson] [onDate] [inPlace]. +Skipped subordinate line Line 299: 2 _SEN8 [FirstHeShe] witnessed the adoption of [MainPerson]. +Skipped subordinate line Line 300: 2 _DEF Y +Skipped subordinate line Line 301: 1 _DATE_TYPE 1 +Skipped subordinate line Line 302: 1 _PLACE_TYPE 1 +Skipped subordinate line Line 303: 1 _DESC_FLAG 1 +Skipped subordinate line Line 304: 1 _INC_NOTES + + + + + Records not imported into Top Level: + +Unknown tag Line 305: 0 _EVENT_DEFN Alt. Birth +Skipped subordinate line Line 306: 1 TYPE I +Skipped subordinate line Line 307: 1 TITL Alt. Birth +Skipped subordinate line Line 308: 1 _SEN1 [HeShe] has conflicting birth information of [Desc], [Date] and [Place]. +Skipped subordinate line Line 309: 1 _SEN2 [HeShe] has conflicting birth information of [Date]. +Skipped subordinate line Line 310: 1 _SEN3 [HeShe] has conflicting birth information of [Place]. +Skipped subordinate line Line 311: 1 _SEN4 [HeShe] has conflicting birth information of [Desc]. +Skipped subordinate line Line 312: 1 _SEN5 [HeShe] has conflicting birth information of [Desc] and [Date]. +Skipped subordinate line Line 313: 1 _SEN6 [HeShe] has conflicting birth information of [Desc] and [Place]. +Skipped subordinate line Line 314: 1 _SEN7 [HeShe] has conflicting birth information of [Date] and [Place]. +Skipped subordinate line Line 315: 1 _SEN8 [HeShe] has conflicting birth information. +Skipped subordinate line Line 316: 1 ROLE Doctor +Skipped subordinate line Line 317: 2 _SEN1 [FirstHeShe] delivered [MainPerson] [onDate] [inPlace]: [Desc]. +Skipped subordinate line Line 318: 2 _SEN2 [FirstHeShe] delivered [MainPerson] [onDate]. +Skipped subordinate line Line 319: 2 _SEN3 [FirstHeShe] delivered [MainPerson] [inPlace]. +Skipped subordinate line Line 320: 2 _SEN4 [FirstHeShe] delivered [MainPerson]: [Desc]. +Skipped subordinate line Line 321: 2 _SEN5 [FirstHeShe] delivered [MainPerson] [onDate]: [Desc]. +Skipped subordinate line Line 322: 2 _SEN6 [FirstHeShe] delivered [MainPerson] [inPlace]: [Desc]. +Skipped subordinate line Line 323: 2 _SEN7 [FirstHeShe] delivered [MainPerson] [onDate] [inPlace]. +Skipped subordinate line Line 324: 2 _SEN8 [FirstHeShe] delivered [MainPerson]. +Skipped subordinate line Line 325: 1 ROLE Father +Skipped subordinate line Line 326: 2 _SEN1 [FirstHeShe] is the father of [MainPerson] born [onDate] [inPlace]: [Desc]. +Skipped subordinate line Line 327: 2 _SEN2 [FirstHeShe] is the father of [MainPerson] born [onDate]. +Skipped subordinate line Line 328: 2 _SEN3 [FirstHeShe] is the father of [MainPerson] born [inPlace]. +Skipped subordinate line Line 329: 2 _SEN4 [FirstHeShe] is the father of [MainPerson] born: [Desc]. +Skipped subordinate line Line 330: 2 _SEN5 [FirstHeShe] is the father of [MainPerson] born [onDate]: [Desc]. +Skipped subordinate line Line 331: 2 _SEN6 [FirstHeShe] is the father of [MainPerson] born [inPlace]: [Desc]. +Skipped subordinate line Line 332: 2 _SEN7 [FirstHeShe] is the father of [MainPerson] born [onDate] [inPlace]. +Skipped subordinate line Line 333: 2 _SEN8 [FirstHeShe] is the father of [MainPerson] born. +Skipped subordinate line Line 334: 1 ROLE Godfather +Skipped subordinate line Line 335: 2 _SEN1 [FirstHeShe] is the godfather of [MainPerson] [onDate] [inPlace]: [Desc]. +Skipped subordinate line Line 336: 2 _SEN2 [FirstHeShe] is the godfather of [MainPerson] [onDate]. +Skipped subordinate line Line 337: 2 _SEN3 [FirstHeShe] is the godfather of [MainPerson] [inPlace]. +Skipped subordinate line Line 338: 2 _SEN4 [FirstHeShe] is the godfather of [MainPerson]: [Desc]. +Skipped subordinate line Line 339: 2 _SEN5 [FirstHeShe] is the godfather of [MainPerson] [onDate]: [Desc]. +Skipped subordinate line Line 340: 2 _SEN6 [FirstHeShe] is the godfather of [MainPerson] [inPlace]: [Desc]. +Skipped subordinate line Line 341: 2 _SEN7 [FirstHeShe] is the godfather of [MainPerson] [onDate] [inPlace]. +Skipped subordinate line Line 342: 2 _SEN8 [FirstHeShe] is the godfather of [MainPerson]. +Skipped subordinate line Line 343: 1 ROLE Godmother +Skipped subordinate line Line 344: 2 _SEN1 [FirstHeShe] is the godmother of [MainPerson] [onDate] [inPlace]: [Desc]. +Skipped subordinate line Line 345: 2 _SEN2 [FirstHeShe] is the godmother of [MainPerson] [onDate]. +Skipped subordinate line Line 346: 2 _SEN3 [FirstHeShe] is the godmother of [MainPerson] [inPlace]. +Skipped subordinate line Line 347: 2 _SEN4 [FirstHeShe] is the godmother of [MainPerson]: [Desc]. +Skipped subordinate line Line 348: 2 _SEN5 [FirstHeShe] is the godmother of [MainPerson] [onDate]: [Desc]. +Skipped subordinate line Line 349: 2 _SEN6 [FirstHeShe] is the godmother of [MainPerson] [inPlace]: [Desc]. +Skipped subordinate line Line 350: 2 _SEN7 [FirstHeShe] is the godmother of [MainPerson] [onDate] [inPlace]. +Skipped subordinate line Line 351: 2 _SEN8 [FirstHeShe] is the godmother of [MainPerson]. +Skipped subordinate line Line 352: 1 ROLE Midwife +Skipped subordinate line Line 353: 2 _SEN1 [FirstHeShe] delivered [MainPerson] [onDate] [inPlace]: [Desc]. +Skipped subordinate line Line 354: 2 _SEN2 [FirstHeShe] delivered [MainPerson] [onDate]. +Skipped subordinate line Line 355: 2 _SEN3 [FirstHeShe] delivered [MainPerson] [inPlace]. +Skipped subordinate line Line 356: 2 _SEN4 [FirstHeShe] delivered [MainPerson]: [Desc]. +Skipped subordinate line Line 357: 2 _SEN5 [FirstHeShe] delivered [MainPerson] [onDate]: [Desc]. +Skipped subordinate line Line 358: 2 _SEN6 [FirstHeShe] delivered [MainPerson] [inPlace]: [Desc]. +Skipped subordinate line Line 359: 2 _SEN7 [FirstHeShe] delivered [MainPerson] [onDate] [inPlace]. +Skipped subordinate line Line 360: 2 _SEN8 [FirstHeShe] delivered [MainPerson]. +Skipped subordinate line Line 361: 1 ROLE Mother +Skipped subordinate line Line 362: 2 _SEN1 [FirstHeShe] is the mother of [MainPerson] born [onDate] [inPlace]: [Desc]. +Skipped subordinate line Line 363: 2 _SEN2 [FirstHeShe] is the mother of [MainPerson] born [onDate]. +Skipped subordinate line Line 364: 2 _SEN3 [FirstHeShe] is the mother of [MainPerson] born [inPlace]. +Skipped subordinate line Line 365: 2 _SEN4 [FirstHeShe] is the mother of [MainPerson] born: [Desc]. +Skipped subordinate line Line 366: 2 _SEN5 [FirstHeShe] is the mother of [MainPerson] born [onDate]: [Desc]. +Skipped subordinate line Line 367: 2 _SEN6 [FirstHeShe] is the mother of [MainPerson] born [inPlace]: [Desc]. +Skipped subordinate line Line 368: 2 _SEN7 [FirstHeShe] is the mother of [MainPerson] born [onDate] [inPlace]. +Skipped subordinate line Line 369: 2 _SEN8 [FirstHeShe] is the mother of [MainPerson] born. +Skipped subordinate line Line 370: 1 ROLE Witness +Skipped subordinate line Line 371: 2 _SEN1 [FirstHeShe] witnessed the birth of [MainPerson] [onDate] [inPlace]: [Desc]. +Skipped subordinate line Line 372: 2 _SEN2 [FirstHeShe] witnessed the birth of [MainPerson] [onDate]. +Skipped subordinate line Line 373: 2 _SEN3 [FirstHeShe] witnessed the birth of [MainPerson] [inPlace]. +Skipped subordinate line Line 374: 2 _SEN4 [FirstHeShe] witnessed the birth of [MainPerson]: [Desc]. +Skipped subordinate line Line 375: 2 _SEN5 [FirstHeShe] witnessed the birth of [MainPerson] [onDate]: [Desc]. +Skipped subordinate line Line 376: 2 _SEN6 [FirstHeShe] witnessed the birth of [MainPerson] [inPlace]: [Desc]. +Skipped subordinate line Line 377: 2 _SEN7 [FirstHeShe] witnessed the birth of [MainPerson] [onDate] [inPlace]. +Skipped subordinate line Line 378: 2 _SEN8 [FirstHeShe] witnessed the birth of [MainPerson]. +Skipped subordinate line Line 379: 2 _DEF Y +Skipped subordinate line Line 380: 1 _DATE_TYPE 1 +Skipped subordinate line Line 381: 1 _PLACE_TYPE 1 +Skipped subordinate line Line 382: 1 _DESC_FLAG 1 +Skipped subordinate line Line 383: 1 _INC_NOTES + + + + + Objects referenced by this note were missing in a file imported on 12/25/1999 12:00:00 AM. + + +
diff --git a/data/tests/imp_notetest_lds_in-out-in_dfs.ged b/data/tests/imp_notetest_lds_in-out-in_dfs.ged new file mode 100644 index 00000000000..710d58e624a --- /dev/null +++ b/data/tests/imp_notetest_lds_in-out-in_dfs.ged @@ -0,0 +1,198 @@ +0 HEAD +1 SOUR Legacy +2 VERS 10.0 +2 NAME Legacy (R) +2 CORP MyHeritage USA Inc. +3 ADDR 3098 W Executive Pkwy Ste 275 +4 CONT Lehi, UT 84043 USA +1 DEST Legacy +1 DATE 3 Aug 2026 +1 SUBM @S0@ +1 FILE C:\gramps_dev\gramps_legacy_import\data\tests\imp_notetest_dfs_lds.ged +1 GEDC +2 VERS 5.5.1 +2 FORM LINEAGE-LINKED +1 CHAR UTF-8 +0 @S0@ SUBM +1 NAME Not Given +0 @I1@ INDI +1 NAME Tom /Tester/ +2 GIVN Tom +2 SURN Tester +1 SEX M +1 BIRT +2 DATE 15 Jun 1900 +2 ADDR 123 main, Norwalk, Ohio, USA +3 ADR1 123 main, Norwalk, Ohio, USA +3 NOTE Location Note 2 +2 NOTE @BI1@ +1 DEAT +2 NOTE @DI1@ +1 _PPEXCLUDE +1 REFN 98765 for PERSON +1 EVEN 98765 for PERSON +2 TYPE Who knows OBJE REFN TYPE +1 _UID 8D70ED25CB654853AC16E58EEB1FD4AB9847 +1 CHAN +2 DATE 11 Jan 2001 +3 TIME 16:00 +1 OBJE +2 FILE http://homepages.rootsweb.com/~pmcbride/gedcom/55gctoc.htm +3 FORM URL +4 TYPE URL +3 TITL GEDCOM 5.5 documentation web site +2 _PRIM Y +1 OBJE +2 FILE photo.jpg +3 FORM jpg +4 TYPE PHOTO +3 TITL Tom Tester's photo +2 REFN 98765 +1 FAMS @F1@ +1 NOTE @NI1@ +1 NOTE @HI1@ +0 @NI1@ NOTE +1 CONC Tom Tester xref note +1 CONT +1 CONT Tom Tester Note +0 @HI1@ NOTE +1 CONC (Research):from yearTEST: TEST person +1 CONT NOTE: NOTE Empty note subordinate (Should be skipped) +1 CONT NOTE: TEST Empty Note +1 CONT NOTE: TEST 123456 Event Note +1 CONT NOTE: TEST Location Note +1 CONT NOTE: TEST Person Note +1 CONT OBJE: TEST media +1 CONT NOTE: TEST 123456 Note +1 CONT OBJE: RIN 123456 +0 @BI1@ NOTE +1 CONC Birth Event note +0 @DI1@ NOTE +1 CONC Death Event xref note +0 @I2@ INDI +1 NAME Mrs /Tester/ +2 GIVN Mrs +2 SURN Tester +1 SEX F +1 BIRT +2 DATE 15 Jun 1901 +1 DEAT +2 DATE 5 Jul 1975 +1 _UID C58628B95D0B41EEB3305DFE5F824E16463C +1 CHAN +2 DATE 3 Aug 2026 +3 TIME 18:39 +1 FAMS @F1@ +0 @I3@ INDI +1 NAME Ed /Tester/ +2 GIVN Ed +2 SURN Tester +1 NAME Eddie /Tester/ +2 GIVN Eddie +2 SURN Tester +1 SEX M +1 BIRT +2 DATE 15 Jun 1922 +1 DEAT +2 DATE 5 Jul 1994 +1 _PPEXCLUDE +1 EVEN +2 TYPE Association +2 NOTE Association link note +1 _UID DA9DFB151BEE490C983CBB0B6D9DCD4096C4 +1 BAPL +2 DATE 5 May 0005 B.C. +2 TEMP SLAKE - Salt Lake Temple +1 CHAN +2 DATE 3 Aug 2026 +3 TIME 18:38 +1 FAMC @F1@ +1 NOTE @NI3@ +1 NOTE @HI3@ +0 @NI3@ NOTE +1 CONC Name note +1 CONT +1 CONT >LDS Baptism note: LDS xref note +0 @HI3@ NOTE +1 CONC (Research):from yearNOTE: TEST Empty Note +1 CONT BAPL: SOUR @SOURCE1@ +1 CONT NOTE: TEST Accociation note +0 @I4@ INDI +1 NAME George /Testee/ +2 GIVN George +2 SURN Testee +1 SEX U +1 _PPEXCLUDE +1 _UID 77DA2714AB9D464DBAE1E44EB7A328974D11 +1 CHAN +2 DATE 3 Aug 2026 +3 TIME 18:38 +1 NOTE @NI4@ +0 @NI4@ NOTE +1 CONC Just for association +0 @F1@ FAM +1 HUSB @I1@ +2 _PREF Y +1 WIFE @I2@ +2 _PREF Y +1 MARR +2 SOUR @S2@ +2 SOUR @S2@ +3 PAGE 42 +3 QUAY 0 +3 DATE 31 Dec 1900 +2 NOTE @NF1@ +1 CHIL @I3@ +2 _PREF Y +1 CHAN +2 DATE 3 Aug 2026 +3 TIME 18:38 +0 @NF1@ NOTE +1 CONC Family xref note +1 CONT Family note +1 CONT +1 CONT from year +1 CONT TEST: TEST family +0 _EVENT_DEFN Who knows OBJE REFN TYPE +1 TYPE I +1 TITL Who knows OBJE REFN TYPE +1 _SEN1 [EventName]: [Desc], [onDate], [inPlace]. +1 _SEN2 [EventName]: [Date]. +1 _SEN3 [EventName]: [inPlace]. +1 _SEN4 [EventName]: [Desc]. +1 _SEN5 [EventName]: [Desc], [onDate]. +1 _SEN6 [EventName]: [Desc], [inPlace]. +1 _SEN7 [EventName]: [Date], [inPlace]. +1 _SEN8 [EventName]. +1 _DATE_TYPE 1 +1 _PLACE_TYPE 1 +1 _DESC_FLAG 1 +1 _INC_NOTES +0 _EVENT_DEFN Association +1 TYPE I +1 TITL Association +1 _SEN1 [EventName]: [Desc], [onDate], [inPlace]. +1 _SEN2 [EventName]: [Date]. +1 _SEN3 [EventName]: [inPlace]. +1 _SEN4 [EventName]: [Desc]. +1 _SEN5 [EventName]: [Desc], [onDate]. +1 _SEN6 [EventName]: [Desc], [inPlace]. +1 _SEN7 [EventName]: [Date], [inPlace]. +1 _SEN8 [EventName]. +1 _DATE_TYPE 1 +1 _PLACE_TYPE 1 +1 _DESC_FLAG 1 +1 _INC_NOTES +0 @S2@ SOUR +1 ABBR Note Test file Source: Tester +1 TITL Note Test file Source: Tester +1 NOTE note embedded in the SOURCE Record +1 REPO @R1@ +1 _PAREN Y +0 @R1@ REPO +1 NAME The Testers repository +1 ADDR The Testers repository +2 _NAME The Testers repository +2 ADR1 The Testers repository +2 NOTE Repository Note +0 TRLR diff --git a/data/tests/imp_notetest_lds_in-out-in_dfs.gramps b/data/tests/imp_notetest_lds_in-out-in_dfs.gramps new file mode 100644 index 00000000000..108737bde31 --- /dev/null +++ b/data/tests/imp_notetest_lds_in-out-in_dfs.gramps @@ -0,0 +1,397 @@ + + + +
+ + + +
+ + + + + + Birth + + + + + + Death + + + + _PPEXCLUDE + + + Who knows OBJE REFN TYPE + 98765 for PERSON + + + Birth + + + + Death + + + + Birth + + + + Death + + + + _PPEXCLUDE + + + Association + + + + _PPEXCLUDE + + + _PREF + + + _PREF + + + Marriage + + + + + + + + M + + Tom + Tester + + + + + + + + + + + + + + + + + + F + + Mrs + Tester + + + + + + + + + + M + + Ed + Tester + + + Eddie + Tester + + + + + + + + + + + + + + + + + + U + + George + Testee + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + 2 + + + + 2 + + + + 2 + + + + 2 + + + + + 42 + 0 + + + + 2 + + + + + + Import from imp_notetest_dfs_lds.ged + + + + + + + + + + + + + Note Test file Source: Tester + Note Test file Source: Tester + + + + + + + + 123 main, Norwalk, Ohio, USA + + + + + + + + + + + + + Business that produced the product: MyHeritage USA Inc. + GEDCOM data +
+ 3098 W Executive Pkwy Ste 275, Lehi, UT 84043 USA +
+
+ + SUBM (Submitter): (@S0@) Not Given + GEDCOM data +
+
+
+ + The Testers repository + Library +
+ The Testers repository + +
+ +
+
+ + + Location Note 2 + + + + Birth Event note + + + Death Event xref note + + + Tom Tester xref note + +Tom Tester Note + + + (Research):from yearTEST: TEST person +NOTE: NOTE Empty note subordinate (Should be skipped) +NOTE: TEST Empty Note +NOTE: TEST 123456 Event Note +NOTE: TEST Location Note +NOTE: TEST Person Note +OBJE: TEST media +NOTE: TEST 123456 Note +OBJE: RIN 123456 + + + Records not imported into INDI (individual) Gramps ID I0001: + +ADDR element ignored '123 main' Line 25: 2 ADDR 123 main, Norwalk, Ohio, USA +ADDR element ignored ' Norwalk' Line 25: 2 ADDR 123 main, Norwalk, Ohio, USA +ADDR element ignored ' Ohio' Line 25: 2 ADDR 123 main, Norwalk, Ohio, USA +ADDR element ignored ' USA' Line 25: 2 ADDR 123 main, Norwalk, Ohio, USA +Tag recognized but not supported: 'URL' on line Line 42: 4 TYPE URL +Tag recognized but not supported: 'PHOTO' on line Line 48: 4 TYPE PHOTO +Tag recognized but not supported: '98765' on line Line 50: 2 REFN 98765 +Could not import photo.jpg Line 45: 1 OBJE + + + + + Association link note + + + + Name note + +>LDS Baptism note: LDS xref note + + + (Research):from yearNOTE: TEST Empty Note +BAPL: SOUR @SOURCE1@ +NOTE: TEST Accociation note + + + Just for association + + + Family xref note +Family note + +from year +TEST: TEST family + + + Records not imported into FAM (family) Gramps ID F0001: + +Tag recognized but not supported: 'Y' on line Line 146: 2 _PREF Y + + + + + Records not imported into Top Level: + +Unknown tag Line 156: 0 _EVENT_DEFN Who knows OBJE REFN TYPE +Skipped subordinate line Line 157: 1 TYPE I +Skipped subordinate line Line 158: 1 TITL Who knows OBJE REFN TYPE +Skipped subordinate line Line 159: 1 _SEN1 [EventName]: [Desc], [onDate], [inPlace]. +Skipped subordinate line Line 160: 1 _SEN2 [EventName]: [Date]. +Skipped subordinate line Line 161: 1 _SEN3 [EventName]: [inPlace]. +Skipped subordinate line Line 162: 1 _SEN4 [EventName]: [Desc]. +Skipped subordinate line Line 163: 1 _SEN5 [EventName]: [Desc], [onDate]. +Skipped subordinate line Line 164: 1 _SEN6 [EventName]: [Desc], [inPlace]. +Skipped subordinate line Line 165: 1 _SEN7 [EventName]: [Date], [inPlace]. +Skipped subordinate line Line 166: 1 _SEN8 [EventName]. +Skipped subordinate line Line 167: 1 _DATE_TYPE 1 +Skipped subordinate line Line 168: 1 _PLACE_TYPE 1 +Skipped subordinate line Line 169: 1 _DESC_FLAG 1 +Skipped subordinate line Line 170: 1 _INC_NOTES + + + + + Records not imported into Top Level: + +Unknown tag Line 171: 0 _EVENT_DEFN Association +Skipped subordinate line Line 172: 1 TYPE I +Skipped subordinate line Line 173: 1 TITL Association +Skipped subordinate line Line 174: 1 _SEN1 [EventName]: [Desc], [onDate], [inPlace]. +Skipped subordinate line Line 175: 1 _SEN2 [EventName]: [Date]. +Skipped subordinate line Line 176: 1 _SEN3 [EventName]: [inPlace]. +Skipped subordinate line Line 177: 1 _SEN4 [EventName]: [Desc]. +Skipped subordinate line Line 178: 1 _SEN5 [EventName]: [Desc], [onDate]. +Skipped subordinate line Line 179: 1 _SEN6 [EventName]: [Desc], [inPlace]. +Skipped subordinate line Line 180: 1 _SEN7 [EventName]: [Date], [inPlace]. +Skipped subordinate line Line 181: 1 _SEN8 [EventName]. +Skipped subordinate line Line 182: 1 _DATE_TYPE 1 +Skipped subordinate line Line 183: 1 _PLACE_TYPE 1 +Skipped subordinate line Line 184: 1 _DESC_FLAG 1 +Skipped subordinate line Line 185: 1 _INC_NOTES + + + + + note embedded in the SOURCE Record + + + + Records not imported into SOUR (source) Gramps ID S0002: + +Tag recognized but not supported: 'Y' on line Line 191: 1 _PAREN Y + + + + + Repository Note + + + + Records not imported into REPO (repository) Gramps ID R0002: + +Tag recognized but not supported: 'The Testers repository' on line Line 195: 2 _NAME The Testers repository + + + + +
diff --git a/data/tests/imp_sample.difs b/data/tests/imp_sample.difs new file mode 100644 index 00000000000..2452bbb55cf --- /dev/null +++ b/data/tests/imp_sample.difs @@ -0,0 +1,42 @@ +Mismatch on file: imp_sample.ged +Note: N0015 handle=0000004800000048 + Diff on: Note, text, string + 164: Records not imported into INDI (individual) Gramps ID I0018: + +Tag recognized but not supported: 'first generaton' on line Line 247: 2 TYPE first generaton + + 164: Records not imported into INDI (individual) Gramps ID I0018: + +Tag recognized but not supported Line 247: 2 TYPE first generaton + +Note: N0022 handle=000000d0000000d0 + Diff on: Note, text, string + 144: Records not imported into FAM (family) Gramps ID F0010: + +Tag recognized but not supported: '' on line Line 865: 2 _STAT + + 144: Records not imported into FAM (family) Gramps ID F0010: + +Tag recognized but not supported Line 865: 2 _STAT + +Note: N0025 handle=000000e5000000e5 + Diff on: Note, text, string + 182: Records not imported into FAM (family) Gramps ID F0008: + +Tag recognized but not supported: '123 Main st, Grantville, Virgi Line 1007: 1 ADDR 123 Main st, Grantville, Virginia, USA + + 182: Records not imported into FAM (family) Gramps ID F0008: + +Tag recognized but not supported Line 1007: 1 ADDR 123 Main st, Grantville, Virginia, USA + +Note: N0031 handle=000000ef000000ef + Diff on: Note, text, string + 251: Records not imported into SOUR (source) Gramps ID S0003: + +Tag recognized but not supported: '' on line Line 1047: 1 DATA +Skipped subordinate line Line 1048: 2 AGNC NYC Public Library + + 251: Records not imported into SOUR (source) Gramps ID S0003: + +Tag recognized but not supported Line 1047: 1 DATA +Skipped subordinate line Line 1048: 2 AGNC NYC Public Library \ No newline at end of file diff --git a/data/tests/imp_sample.gramps b/data/tests/imp_sample.gramps index 73734e9c7c7..e8ffb431614 100644 --- a/data/tests/imp_sample.gramps +++ b/data/tests/imp_sample.gramps @@ -1,9 +1,9 @@ - - + +
- + Alex Roitman,,, Not Provided @@ -594,7 +594,7 @@ - + M Gustaf @@ -632,7 +632,7 @@ - + F Kirsti Marie @@ -643,7 +643,7 @@ - + F Astrid Shermanna @@ -664,7 +664,7 @@ - + M Hjalmar @@ -685,7 +685,7 @@ - + M Hjalmar @@ -699,7 +699,7 @@ - + M Gus @@ -710,7 +710,7 @@ - + M Carl Emil @@ -720,7 +720,7 @@ - + M Hans Peter @@ -733,7 +733,7 @@ - + M Keith Lloyd @@ -744,7 +744,7 @@ - + F Hanna @@ -754,7 +754,7 @@ - + M Herman Julius @@ -765,7 +765,7 @@ - + F Evelyn @@ -775,7 +775,7 @@ - + F Marjorie Lee @@ -787,7 +787,7 @@ - + F Jennifer @@ -799,7 +799,7 @@ - + F Lillie Harriet @@ -825,7 +825,7 @@ - + M Eric Lloyd @@ -836,7 +836,7 @@ - + F Amber Marie @@ -861,7 +861,7 @@ - + F Marta @@ -889,7 +889,7 @@ - + M Ingeman @@ -898,7 +898,7 @@ - + F Anna @@ -908,7 +908,7 @@ - + M Craig Peter @@ -918,7 +918,7 @@ - + X Magnes @@ -930,7 +930,7 @@ - + F Janice Ann @@ -941,7 +941,7 @@ - + F Marjorie @@ -951,7 +951,7 @@ - + F Darcy @@ -960,7 +960,7 @@ - + M Lloyd @@ -970,7 +970,7 @@ - + F Alice Paula @@ -979,7 +979,7 @@ - + M Lars Peter @@ -996,7 +996,7 @@ - + F Elna @@ -1007,7 +1007,7 @@ - + M Edwin Michael @@ -1023,7 +1023,7 @@ - + F Kerstina @@ -1033,7 +1033,7 @@ - + M Martin @@ -1044,7 +1044,7 @@ - + M Ingeman @@ -1053,7 +1053,7 @@ - + F Marjorie Alice @@ -1062,7 +1062,7 @@ - + F Janis Elaine @@ -1071,7 +1071,7 @@ - + M Mason Michael @@ -1081,7 +1081,7 @@ - + M Edwin @@ -1090,7 +1090,7 @@ - + F Ingar @@ -1099,7 +1099,7 @@ - + M Emil @@ -1128,7 +1128,7 @@ - + @@ -1138,20 +1138,20 @@ - + - + - + @@ -1161,19 +1161,19 @@ - + - + - + @@ -1181,7 +1181,7 @@ - + @@ -1195,7 +1195,7 @@ - + @@ -1204,7 +1204,7 @@ - + @@ -1213,7 +1213,7 @@ - + @@ -1222,7 +1222,7 @@ - + @@ -1232,13 +1232,13 @@ - + - + @@ -1295,7 +1295,7 @@ @S999@ - + Birth Records @@ -1306,14 +1306,14 @@ - + Birth Certificate - + Birth, Death and Marriage Records goodstuff @@ -1468,7 +1468,7 @@
- + New York Public Library Library
@@ -1508,42 +1508,42 @@ Records not imported into HEAD (header): -GEDCOM FORM not supported Line 14: 2 FORM NOT LINEAGE-LINKED +GEDCOM FORM not supported Line 14: 2 FORM NOT LINEAGE-LINKED Records not imported into SUBM (Submitter): (@SUBM@) Alex Roitman,,,: -Line ignored as not understood Line 23: 2 NOTE No address provided (note not supported) +Line ignored as not understood Line 23: 2 NOTE No address provided (note not supported) Records not imported into Top Level: -Line ignored as not understood Line 24: 0 SUBN SUBN -Skipped subordinate line Line 25: 1 TEMP Mormon Temple -Skipped subordinate line Line 26: 1 ANCE 4 -Skipped subordinate line Line 27: 1 DESC 4 -Skipped subordinate line Line 28: 1 ORDI Yes +Line ignored as not understood Line 24: 0 SUBN SUBN +Skipped subordinate line Line 25: 1 TEMP Mormon Temple +Skipped subordinate line Line 26: 1 ANCE 4 +Skipped subordinate line Line 27: 1 DESC 4 +Skipped subordinate line Line 28: 1 ORDI Yes Records not imported into FAM (family) Gramps ID F0003: -Line ignored as not understood Line 46: 2 SOUR Not really allowed here -Filename omitted Line 48: 1 OBJE +Line ignored as not understood Line 46: 2 SOUR Not really allowed here +Filename omitted Line 48: 1 OBJE @@ -1579,11 +1579,11 @@ Filename omitted Line 48: Records not imported into INDI (individual) Gramps ID I0016: -Warn: ADDR overwritten Line 206: 3 ADR1 456 Main St again -ADDR element ignored '459 Main St.' Line 204: 2 ADDR 459 Main St., The Village, San Francisco, CA, USA +Warn: ADDR overwritten Line 206: 3 ADR1 456 Main St again +ADDR element ignored '459 Main St.' Line 204: 2 ADDR 459 Main St., The Village, San Francisco, CA, USA @@ -1592,10 +1592,10 @@ ADDR element ignored '459 Main St.' Line 204: Records not imported into INDI (individual) Gramps ID I0018: -Tag recognized but not supported Line 247: 2 TYPE first generaton +Tag recognized but not supported: 'first generaton' on line Line 247: 2 TYPE first generaton @@ -1624,38 +1624,38 @@ Company. He enlisted in the army at Sparks 7 December 1917 and served as a Corpo Records not imported into FAM (family) Gramps ID F0010: -Tag recognized but not supported Line 865: 2 _STAT +Tag recognized but not supported: '' on line Line 865: 2 _STAT Records not imported into FAM (family) Gramps ID F0011: -Could not import Magnes&Anna_smiths_marr_cert.jpg Line 880: 3 OBJE -Could not import Magnes&Anna_smiths_marr_cert.jpg Line 883: 2 OBJE +Could not import Magnes&Anna_smiths_marr_cert.jpg Line 880: 3 OBJE +Could not import Magnes&Anna_smiths_marr_cert.jpg Line 883: 2 OBJE Records not imported into FAM (family) Gramps ID F0012: -Could not import John&Alice_smiths_marr_cert.jpg Line 907: 1 OBJE +Could not import John&Alice_smiths_marr_cert.jpg Line 907: 1 OBJE Records not imported into FAM (family) Gramps ID F0008: -Tag recognized but not supported Line 1007: 1 ADDR 123 Main st, Grantville, Virginia, USA +Tag recognized but not supported: '123 Main st, Grantville, Virginia, USA' on line Line 1007: 1 ADDR 123 Main st, Grantville, Virginia, USA @@ -1676,11 +1676,11 @@ Tag recognized but not supported Line 1007: Records not imported into SOUR (source) Gramps ID S0003: -Tag recognized but not supported Line 1047: 1 DATA -Skipped subordinate line Line 1048: 2 AGNC NYC Public Library +Tag recognized but not supported: '' on line Line 1047: 1 DATA +Skipped subordinate line Line 1048: 2 AGNC NYC Public Library @@ -1692,33 +1692,33 @@ Skipped subordinate line Line 1048: Records not imported into REPO (repository) Gramps ID R0003: -REFN ignored Line 1077: 3 REFN blah blah -Skipped subordinate line Line 1078: 4 TYPE who knows -Could not import Attic_photo.jpg Line 1081: 3 OBJE +REFN ignored Line 1077: 3 REFN blah blah +Skipped subordinate line Line 1078: 4 TYPE who knows +Could not import Attic_photo.jpg Line 1081: 3 OBJE Records not imported into Top Level: -Unknown tag Line 1108: 0 XXX an unknown token at level 0 +Unknown tag Line 1108: 0 XXX an unknown token at level 0 Records not imported into Top Level: -Unknown tag Line 1111: 1 @X1@ XXX and unknown token xref definition +Unknown tag Line 1111: 1 @X1@ XXX and unknown token xref definition - + Objects referenced by this note were missing in a file imported on 12/25/1999 12:00:00 AM. diff --git a/gramps/plugins/lib/libgedcom.py b/gramps/plugins/lib/libgedcom.py index 0b00f44d78a..44ee605abe3 100644 --- a/gramps/plugins/lib/libgedcom.py +++ b/gramps/plugins/lib/libgedcom.py @@ -1,8840 +1,8848 @@ -# -# Gramps - a GTK+/GNOME based genealogy program -# -# Copyright (C) 2000-2007 Donald N. Allingham -# Copyright (C) 2009-2010 Gary Burton -# Copyright (C) 2010 Nick Hall -# Copyright (C) 2011 Tim G L Lyons -# Copyright (C) 2016 Paul R. Culley -# Copyright (C) 2024 Doug Blank -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, see . -# - -""" -Import from GEDCOM - -The GEDCOM file format is defined by the GEDCOM 5.5 Specfication, found -at http://www.familysearch.org/GEDCOM/GEDCOM55.EXE - -The basic structure is a line with three attributes: - - - -Because of this structure, it does not lend itself to more traditional -parsing techniques, such as LALR. The LEVEL token implies too much to be -useful in this context. While this makes parsing more difficult, it -does provide one very beneficial feature for GEDCOM: Error recoverability. -GEDCOM is a poorly implemented standard, primarily because it is a poor -standard to begin with. - -Most commercial applications that implement GEDCOM output add custom -extensions, and feel free to violate the existing structure. If one were -cynical, one might believe that the commercial programs were trying to -make it difficult to transfer your data to another application. - -This parser takes a different approach to parsing a GEDCOM file. The first -state, Lexer, reads lines from the file, and does some basic lexical -analysis on each line (actually several lines, since it automatically -combines CONT and CONC tagged lines). Each logical line returned to this -parser contains: - - Level, Token, Token text, Data, and line number. - -The Data field is typically text, but in some cases, it may be a integer -value representing an enumerated type or a Gramps object (in the case of -dates). - -The parser works on the current level. Each context and level has a an -associated table (dictionary) of functions indexed by the corresponding -TOKEN. When a token is found, we index into the table to find the function -associated with the token. If no token is found, a function that skips the -line and all subordinate (lines with a higher number). If a function is -found, then we call that function, which in turn processes the line, and -all tokens at the lower level. - -For example: - -1 BIRT - 2 DATE 1 JAN 2000 - 2 UKNOWN TAG - 3 NOTE DATA - -The function parsing the individual at level 1, would encounter the BIRT tag. -It would look up the BIRT token in the table to see if a function as defined -for this TOKEN, and pass control to this function. This function would then -start parsing level 2. It would encounter the DATE tag, look up the -corresponding function in the level 2 table, and pass control to its -associated function. This function would terminate, and return control back to -the level 2 parser, which would then encounter the "UKNOWN" tag. Since this is -not a valid token, it would not be in the table, and a function that would skip -all lines until the next level 2 token is found (in this case, skipping the -"3 NOTE DATA" line. -""" - -# ------------------------------------------------------------------------- -# -# standard python modules -# -# ------------------------------------------------------------------------- -import os -import re -import time - -# from xml.parsers.expat import ParserCreate -from collections import defaultdict, OrderedDict -import string -import mimetypes -from io import StringIO, TextIOWrapper -from urllib.parse import urlparse - -# ------------------------------------------------------------------------ -# -# Set up logging -# -# ------------------------------------------------------------------------ -import logging - -LOG = logging.getLogger(".libgedcom") - -# ------------------------------------------------------------------------- -# -# Gramps modules -# -# ------------------------------------------------------------------------- -from gramps.gen.const import GRAMPS_LOCALE as glocale - -_ = glocale.translation.gettext -from gramps.gen.errors import GedcomError -from gramps.gen.lib import ( - Address, - Attribute, - AttributeType, - ChildRef, - ChildRefType, - Citation, - Date, - Event, - EventRef, - EventRoleType, - EventType, - Family, - FamilyRelType, - LdsOrd, - Location, - Media, - MediaRef, - Name, - NameType, - Note, - NoteType, - Person, - PersonRef, - Place, - RepoRef, - Repository, - RepositoryType, - Researcher, - Source, - SourceMediaType, - SrcAttribute, - Surname, - Tag, - Url, - UrlType, - PlaceType, - PlaceRef, - PlaceName, -) -from gramps.gen.lib.json_utils import data_to_object, object_to_dict -from gramps.gen.db import DbTxn -from gramps.gen.updatecallback import UpdateCallback -from gramps.gen.utils.file import media_path -from gramps.gen.utils.id import create_id -from gramps.gen.utils.lds import TEMPLES -from gramps.gen.utils.unknown import make_unknown, create_explanation_note -from gramps.gen.datehandler._dateparser import DateParser -from gramps.gen.db.dbconst import EVENT_KEY -from gramps.gen.lib.const import IDENTICAL -from gramps.gen.lib import StyledText, StyledTextTag, StyledTextTagType -from gramps.gen.lib.urlbase import UrlBase -from gramps.plugins.lib.libplaceimport import PlaceImport -from gramps.gen.display.place import displayer as _pd -from gramps.gen.utils.grampslocale import GrampsLocale - -# ------------------------------------------------------------------------- -# -# constants -# -# ------------------------------------------------------------------------- -TOKEN_UNKNOWN = 0 -TOKEN_ABBR = 1 -TOKEN_ADDR = 2 -TOKEN_ADOP = 3 -TOKEN_ADR1 = 4 -TOKEN_ADR2 = 5 -TOKEN_AFN = 6 -TOKEN_IGNORE = 7 -TOKEN_REFN = 8 -TOKEN__AKA = 9 -TOKEN_ALIA = 11 -TOKEN_ANCI = 12 -TOKEN_ASSO = 13 -TOKEN_AUTH = 14 -TOKEN_BAPL = 15 -TOKEN_BIRT = 16 -TOKEN__CAT = 17 -TOKEN_CAUS = 18 -TOKEN_CHAN = 19 -TOKEN_CHAR = 20 -TOKEN_CHIL = 21 -TOKEN_CITY = 22 -TOKEN__COMM = 23 -TOKEN_CONC = 24 -TOKEN_CONT = 25 -TOKEN_COPR = 26 -TOKEN_CORP = 27 -TOKEN_CTRY = 28 -TOKEN_DATA = 29 -TOKEN_DATE = 30 -TOKEN_DEAT = 32 -TOKEN_DESI = 33 -TOKEN_DEST = 34 -TOKEN_ENDL = 35 -TOKEN_EVEN = 36 -TOKEN_FAM = 38 -TOKEN_FAMC = 39 -TOKEN_FAMS = 40 -TOKEN_FILE = 41 -TOKEN_FORM = 42 -TOKEN__FREL = 43 -TOKEN_GEDC = 44 -TOKEN_GIVN = 45 -TOKEN__GODP = 46 -TOKEN_HUSB = 47 -TOKEN_INDI = 48 -TOKEN_LABL = 49 -TOKEN_LANG = 50 -TOKEN__LOC = 51 -TOKEN__MARNM = 52 -TOKEN__MREL = 53 -TOKEN__NAME = 54 -TOKEN_NAME = 55 -TOKEN_NCHI = 56 -TOKEN_NICK = 57 -TOKEN_NOTE = 58 -TOKEN_NPFX = 59 -TOKEN_NSFX = 60 -TOKEN_OBJE = 61 -TOKEN_OFFI = 62 -TOKEN_PAGE = 63 -TOKEN_PEDI = 64 -TOKEN_PERI = 65 -TOKEN_PHON = 66 -TOKEN_PLAC = 67 -TOKEN_POST = 68 -TOKEN__PRIMARY = 69 -TOKEN__PRIV = 70 -TOKEN_PUBL = 71 -TOKEN_QUAY = 72 -TOKEN_RELI = 74 -TOKEN_REPO = 75 -TOKEN_RESI = 76 -TOKEN_RFN = 77 -TOKEN_RIN = 78 -TOKEN__SCHEMA = 79 -TOKEN_SEX = 80 -TOKEN_SLGC = 81 -TOKEN_SLGS = 82 -TOKEN_SOUR = 83 -TOKEN_SPFX = 84 -TOKEN_STAE = 85 -TOKEN__STAT = 86 -TOKEN_STAT = 87 -TOKEN_SUBM = 88 -TOKEN_SUBN = 89 -TOKEN_SURN = 90 -TOKEN_TAXT = 91 -TOKEN_TEMP = 92 -TOKEN_TEXT = 93 -TOKEN_TIME = 94 -TOKEN_TITL = 95 -TOKEN__TODO = 96 -TOKEN_TRLR = 97 -TOKEN_TYPE = 98 -TOKEN__UID = 99 -TOKEN_VERS = 100 -TOKEN_WIFE = 101 -TOKEN__WITN = 102 -TOKEN__WTN = 103 -TOKEN_AGNC = 104 -TOKEN_HEAD = 105 -TOKEN_CALN = 106 -TOKEN_MEDI = 107 -TOKEN_RELA = 108 -TOKEN__LKD = 109 -TOKEN_BLOB = 110 -TOKEN_CONL = 111 -TOKEN_AGE = 112 -TOKEN_RESN = 114 -TOKEN_ID = 115 -TOKEN_GEVENT = 116 -TOKEN_RNOTE = 117 -TOKEN_GATTR = 118 -TOKEN_ATTR = 119 -TOKEN_MAP = 120 -TOKEN_LATI = 121 -TOKEN_LONG = 122 -TOKEN_FACT = 123 -TOKEN_EMAIL = 124 -TOKEN_WWW = 125 -TOKEN_FAX = 126 -TOKEN_ROLE = 127 -TOKEN__MAR = 128 -TOKEN__MARN = 129 -TOKEN__ADPN = 130 -TOKEN__FSFTID = 131 -TOKEN__PHOTO = 132 -TOKEN__LINK = 133 -TOKEN__PRIM = 134 -TOKEN__JUST = 135 -TOKEN__TEXT = 136 -TOKEN__DATE = 137 -TOKEN__APID = 138 -TOKEN__CALLNAME = 139 -TOKEN_INIL = 140 - -TOKENS = { - "_ADPN": TOKEN__ADPN, - "_AKA": TOKEN__AKA, - "_AKAN": TOKEN__AKA, - "_ALIA": TOKEN_ALIA, - "_ANCES_ORDRE": TOKEN_IGNORE, - "_APID": TOKEN__APID, # Ancestry.com database and page id - "_CAT": TOKEN_IGNORE, - "_CHUR": TOKEN_IGNORE, - "_COMM": TOKEN__COMM, - "_DATE": TOKEN__DATE, - "_DATE2": TOKEN_IGNORE, - "_DETAIL": TOKEN_IGNORE, - "_EMAIL": TOKEN_EMAIL, - "_E-MAIL": TOKEN_EMAIL, - "_FREL": TOKEN__FREL, - "_FSFTID": TOKEN__FSFTID, - "_GODP": TOKEN__GODP, - "_ITALIC": TOKEN_IGNORE, - "_JUST": TOKEN__JUST, # FTM Citation Quality Justification - "_LEVEL": TOKEN_IGNORE, - "_LINK": TOKEN__LINK, - "_LKD": TOKEN__LKD, - "_LOC": TOKEN__LOC, - "_MAR": TOKEN__MAR, - "_MARN": TOKEN__MARN, - "_MARNM": TOKEN__MARNM, - "_MASTER": TOKEN_IGNORE, - "_MEDI": TOKEN_MEDI, - "_MREL": TOKEN__MREL, - "_NAME": TOKEN__NAME, - "_PAREN": TOKEN_IGNORE, - "_PHOTO": TOKEN__PHOTO, - "_PLACE": TOKEN_IGNORE, - "_PREF": TOKEN__PRIMARY, - "_PRIM": TOKEN__PRIM, - "_PRIMARY": TOKEN__PRIMARY, - "_PRIV": TOKEN__PRIV, - "_PUBLISHER": TOKEN_IGNORE, - "_RUFNAME": TOKEN__CALLNAME, - "_SCBK": TOKEN_IGNORE, - "_SCHEMA": TOKEN__SCHEMA, - "_SSHOW": TOKEN_IGNORE, - "_STAT": TOKEN__STAT, - "_TEXT": TOKEN__TEXT, - "_TODO": TOKEN__TODO, - "_TYPE": TOKEN_TYPE, - "_UID": TOKEN__UID, - "_URL": TOKEN_WWW, - "_WITN": TOKEN__WITN, - "_WTN": TOKEN__WTN, - "ABBR": TOKEN_ABBR, - "ABBREVIATION": TOKEN_ABBR, - "ADDR": TOKEN_ADDR, - "ADDRESS": TOKEN_ADDR, - "ADDRESS1": TOKEN_ADR1, - "ADDRESS2": TOKEN_ADR2, - "ADOP": TOKEN_ADOP, - "ADOPT": TOKEN_ADOP, - "ADR1": TOKEN_ADR1, - "ADR2": TOKEN_ADR2, - "AFN": TOKEN_AFN, - "AGE": TOKEN_AGE, - "AGENCY": TOKEN_IGNORE, - "AGNC": TOKEN_AGNC, - "AKA": TOKEN__AKA, - "ALIA": TOKEN_ALIA, - "ALIAS": TOKEN_ALIA, - "ANCI": TOKEN_ANCI, - "ASSO": TOKEN_ASSO, - "ASSOCIATES": TOKEN_ASSO, - "AUTH": TOKEN_AUTH, - "AUTHOR": TOKEN_AUTH, - "BAPL": TOKEN_BAPL, - "BAPTISM-LDS": TOKEN_BAPL, - "BIRT": TOKEN_BIRT, - "BIRTH": TOKEN_BIRT, - "BLOB": TOKEN_BLOB, - "CALL_NUMBER": TOKEN_CALN, - "CALN": TOKEN_CALN, - "CAUS": TOKEN_CAUS, - "CAUSE": TOKEN_CAUS, - "CHAN": TOKEN_CHAN, - "CHANGE": TOKEN_CHAN, - "CHAR": TOKEN_CHAR, - "CHARACTER": TOKEN_CHAR, - "CHIL": TOKEN_CHIL, - "CHILD": TOKEN_CHIL, - "CHILDREN_COUNT": TOKEN_NCHI, - "CITY": TOKEN_CITY, - "CONC": TOKEN_CONC, - "CONCATENATION": TOKEN_CONC, - "CONCATENTATE": TOKEN_CONC, - "CONL": TOKEN_CONL, - "CONT": TOKEN_CONT, - "CONTINUATION": TOKEN_CONT, - "CONTINUED": TOKEN_CONT, - "COPR": TOKEN_COPR, - "COPYRIGHT": TOKEN_COPR, - "CORP": TOKEN_CORP, - "CORPORATION": TOKEN_CORP, - "COUNTRY": TOKEN_CTRY, - "CTRY": TOKEN_CTRY, - "DATA": TOKEN_DATA, - "DATE": TOKEN_DATE, - "DEAT": TOKEN_DEAT, - "DEATH": TOKEN_DEAT, - "DESI": TOKEN_DESI, - "DEST": TOKEN_DEST, - "DESTINATION": TOKEN_DEST, - "EMAI": TOKEN_EMAIL, - "EMAIL": TOKEN_EMAIL, - "ENDL": TOKEN_ENDL, - "ENDOWMENT": TOKEN_ENDL, - "EVEN": TOKEN_EVEN, - "EVENT": TOKEN_EVEN, - "FACT": TOKEN_FACT, - "FAM": TOKEN_FAM, - "FAMC": TOKEN_FAMC, - "FAMILY": TOKEN_FAM, - "FAMILY_CHILD": TOKEN_FAMC, - "FAMILY_SPOUSE": TOKEN_FAMS, - "FAMS": TOKEN_FAMS, - "FAX": TOKEN_FAX, - "FILE": TOKEN_FILE, - "FORM": TOKEN_FORM, - "GEDC": TOKEN_GEDC, - "GEDCOM": TOKEN_GEDC, - "GIVEN_NAME": TOKEN_GIVN, - "GIVN": TOKEN_GIVN, - "HEAD": TOKEN_HEAD, - "HEADER": TOKEN_HEAD, - "HUSB": TOKEN_HUSB, - "HUSBAND": TOKEN_HUSB, - "INDI": TOKEN_INDI, - "INDIVIDUAL": TOKEN_INDI, - "INIL": TOKEN_INIL, - "LABEL": TOKEN_LABL, - "LABL": TOKEN_LABL, - "LANG": TOKEN_LANG, - "LATI": TOKEN_LATI, - "LONG": TOKEN_LONG, - "MAP": TOKEN_MAP, - "MEDI": TOKEN_MEDI, - "MEDIA": TOKEN_MEDI, - "NAME": TOKEN_NAME, - "NAME_PREFIX": TOKEN_NPFX, - "NAME_SUFFIX": TOKEN_NSFX, - "NCHI": TOKEN_NCHI, - "NICK": TOKEN_NICK, - "NICKNAME": TOKEN_NICK, - "NOTE": TOKEN_NOTE, - "NPFX": TOKEN_NPFX, - "NSFX": TOKEN_NSFX, - "OBJE": TOKEN_OBJE, - "OBJECT": TOKEN_OBJE, - "OFFI": TOKEN_OFFI, - "PAGE": TOKEN_PAGE, - "PEDI": TOKEN_PEDI, - "PEDIGREE": TOKEN_PEDI, - "PERI": TOKEN_PERI, - "PHON": TOKEN_PHON, - "PHONE": TOKEN_PHON, - "PHONE_NUMBER": TOKEN_PHON, - "PLAC": TOKEN_PLAC, - "PLACE": TOKEN_PLAC, - "POST": TOKEN_POST, - "POSTAL_CODE": TOKEN_POST, - "PUBL": TOKEN_PUBL, - "PUBLICATION": TOKEN_PUBL, - "QUALITY_OF_DATA": TOKEN_QUAY, - "QUAY": TOKEN_QUAY, - "REFERENCE": TOKEN_REFN, - "REFN": TOKEN_REFN, - "RELA": TOKEN_RELA, - "RELI": TOKEN_RELI, - "RELIGION": TOKEN_RELI, - "REPO": TOKEN_REPO, - "REPOSITORY": TOKEN_REPO, - "RESN": TOKEN_RESN, - "RFN": TOKEN_RFN, - "RIN": TOKEN_RIN, - "ROLE": TOKEN_ROLE, - "SCHEMA": TOKEN__SCHEMA, - "SEX": TOKEN_SEX, - "SLGC": TOKEN_SLGC, - "SLGS": TOKEN_SLGS, - "SOUR": TOKEN_SOUR, - "SOURCE": TOKEN_SOUR, - "SPFX": TOKEN_SPFX, - "STAE": TOKEN_STAE, - "STAT": TOKEN_STAT, - "STATE": TOKEN_STAE, - "STATUS": TOKEN_STAT, - "SUBM": TOKEN_SUBM, - "SUBMISSION": TOKEN_SUBN, - "SUBMITTER": TOKEN_SUBM, - "SUBN": TOKEN_SUBN, - "SURN": TOKEN_SURN, - "SURN_PREFIX": TOKEN_SPFX, - "SURNAME": TOKEN_SURN, - "TAXT": TOKEN_TAXT, - "TEMP": TOKEN_TEMP, - "TEMPLE": TOKEN_TEMP, - "TEXT": TOKEN_TEXT, - "TIME": TOKEN_TIME, - "TITL": TOKEN_TITL, - "TITLE": TOKEN_TITL, - "TRAILER": TOKEN_TRLR, - "TRLR": TOKEN_TRLR, - "TYPE": TOKEN_TYPE, - "URL": TOKEN_WWW, - "VERS": TOKEN_VERS, - "VERSION": TOKEN_VERS, - "WIFE": TOKEN_WIFE, - "WWW": TOKEN_WWW, -} - -ADOPT_NONE = 0 -ADOPT_EVENT = 1 -ADOPT_FTW = 2 -ADOPT_LEGACY = 3 -ADOPT_PEDI = 4 -ADOPT_STD = 5 -CONC_OK = 0 -CONC_BROKEN = 1 -ALT_NAME_NONE = 0 -ALT_NAME_STD = 1 -ALT_NAME_ALIAS = 2 -ALT_NAME_AKA = 3 -ALT_NAME_EVENT_AKA = 4 -ALT_NAME_UALIAS = 5 -CALENDAR_NO = 0 -CALENDAR_YES = 1 -OBJE_NO = 0 -OBJE_YES = 1 -PREFIX_NO = 0 -PREFIX_YES = 1 -RESIDENCE_ADDR = 0 -RESIDENCE_PLAC = 1 -SOURCE_REFS_NO = 0 -SOURCE_REFS_YES = 1 - -TYPE_BIRTH = ChildRefType() -TYPE_ADOPT = ChildRefType(ChildRefType.ADOPTED) -TYPE_FOSTER = ChildRefType(ChildRefType.FOSTER) - -RELATION_TYPES = (ChildRefType.BIRTH, ChildRefType.UNKNOWN, ChildRefType.NONE) - -PEDIGREE_TYPES = { - "birth": ChildRefType(), - "natural": ChildRefType(), - "step": ChildRefType(ChildRefType.STEPCHILD), - "adopted": TYPE_ADOPT, - "foster": TYPE_FOSTER, -} - -FTW_BAD_PLACE = [ - EventType.OCCUPATION, - EventType.RELIGION, - EventType.DEGREE, -] - -MEDIA_MAP = { - "audio": SourceMediaType.AUDIO, - "book": SourceMediaType.BOOK, - "card": SourceMediaType.CARD, - "electronic": SourceMediaType.ELECTRONIC, - "fiche": SourceMediaType.FICHE, - "microfiche": SourceMediaType.FICHE, - "microfilm": SourceMediaType.FICHE, - "film": SourceMediaType.FILM, - "magazine": SourceMediaType.MAGAZINE, - "manuscript": SourceMediaType.MANUSCRIPT, - "map": SourceMediaType.MAP, - "newspaper": SourceMediaType.NEWSPAPER, - "photo": SourceMediaType.PHOTO, - "tombstone": SourceMediaType.TOMBSTONE, - "grave": SourceMediaType.TOMBSTONE, - "video": SourceMediaType.VIDEO, -} - -OBJ_NOTETYPE = { - "Attribute": NoteType.ATTRIBUTE, - "Address": NoteType.ADDRESS, - "Citation": NoteType.CITATION, - "Event": NoteType.EVENT, - "Family": NoteType.FAMILY, - "LdsOrd": NoteType.LDS, - "Media": NoteType.MEDIA, - "Name": NoteType.GENERAL, - "Place": NoteType.PLACE, - "Person": NoteType.PERSON, - "Repository": NoteType.REPO, - "RepoRef": NoteType.REPOREF, - "Source": NoteType.SOURCE, - "PersonRef": NoteType.ASSOCIATION, -} - -# ------------------------------------------------------------------------- -# -# Integer to GEDCOM tag mappings for constants -# -# ------------------------------------------------------------------------- -CALENDAR_MAP_GEDCOM2XML = { - "FRENCH R": Date.CAL_FRENCH, - "JULIAN": Date.CAL_JULIAN, - "HEBREW": Date.CAL_HEBREW, -} - -QUALITY_MAP = { - "CAL": Date.QUAL_CALCULATED, - "INT": Date.QUAL_CALCULATED, - "EST": Date.QUAL_ESTIMATED, -} - -SEX_MAP = { - "F": Person.FEMALE, - "M": Person.MALE, - "X": Person.OTHER, -} - -FAMILYCONSTANTEVENTS = { - EventType.ANNULMENT: "ANUL", - EventType.DIV_FILING: "DIVF", - EventType.DIVORCE: "DIV", - EventType.CENSUS: "CENS", - EventType.ENGAGEMENT: "ENGA", - EventType.MARR_BANNS: "MARB", - EventType.MARR_CONTR: "MARC", - EventType.MARR_LIC: "MARL", - EventType.MARR_SETTL: "MARS", - EventType.MARRIAGE: "MARR", -} - -PERSONALCONSTANTEVENTS = { - EventType.ADOPT: "ADOP", - EventType.ADULT_CHRISTEN: "CHRA", - EventType.BIRTH: "BIRT", - EventType.DEATH: "DEAT", - EventType.BAPTISM: "BAPM", - EventType.BAR_MITZVAH: "BARM", - EventType.BAS_MITZVAH: "BASM", - EventType.BLESS: "BLES", - EventType.BURIAL: "BURI", - # EventType.CAUSE_DEATH : "CAUS", Not legal Gedcom since v5.0 - EventType.ORDINATION: "ORDN", - EventType.CENSUS: "CENS", - EventType.CHRISTEN: "CHR", - EventType.CONFIRMATION: "CONF", - EventType.CREMATION: "CREM", - EventType.DEGREE: "_DEG", - EventType.DIV_FILING: "DIVF", - EventType.EDUCATION: "EDUC", - EventType.ELECTED: "_ELEC", # FTM custom tag - EventType.EMIGRATION: "EMIG", - EventType.FIRST_COMMUN: "FCOM", - EventType.GRADUATION: "GRAD", - EventType.MED_INFO: "_MDCL", - EventType.MILITARY_SERV: "_MILT", - EventType.NATURALIZATION: "NATU", - EventType.NOB_TITLE: "TITL", - EventType.NUM_MARRIAGES: "NMR", - EventType.IMMIGRATION: "IMMI", - EventType.OCCUPATION: "OCCU", - EventType.PROBATE: "PROB", - EventType.PROPERTY: "PROP", - EventType.RELIGION: "RELI", - EventType.RESIDENCE: "RESI", - EventType.RETIREMENT: "RETI", - EventType.WILL: "WILL", -} - -FAMILYCONSTANTATTRIBUTES = { - AttributeType.NUM_CHILD: "NCHI", -} - -PERSONALCONSTANTATTRIBUTES = { - AttributeType.CASTE: "CAST", - AttributeType.DESCRIPTION: "DSCR", - AttributeType.ID: "IDNO", - AttributeType.NATIONAL: "NATI", - AttributeType.NUM_CHILD: "NCHI", - AttributeType.SSN: "SSN", -} - -# ------------------------------------------------------------------------- -# -# Gedcom to int constants -# -# ------------------------------------------------------------------------- -LDS_STATUS = { - "BIC": LdsOrd.STATUS_BIC, - "CANCELED": LdsOrd.STATUS_CANCELED, - "CHILD": LdsOrd.STATUS_CHILD, - "CLEARED": LdsOrd.STATUS_CLEARED, - "COMPLETED": LdsOrd.STATUS_COMPLETED, - "DNS": LdsOrd.STATUS_DNS, - "INFANT": LdsOrd.STATUS_INFANT, - "PRE-1970": LdsOrd.STATUS_PRE_1970, - "QUALIFIED": LdsOrd.STATUS_QUALIFIED, - "DNS/CAN": LdsOrd.STATUS_DNS_CAN, - "STILLBORN": LdsOrd.STATUS_STILLBORN, - "SUBMITTED": LdsOrd.STATUS_SUBMITTED, - "UNCLEARED": LdsOrd.STATUS_UNCLEARED, -} -# ------------------------------------------------------------------------- -# -# Custom event friendly names. These are non-standard GEDCOM "NEW_TAG" -# tags that start with an '_' i.e. "_DNA". FTM has several of these, other -# programs may have more. If a tag with this format is encountered it is -# checked in this table for a "friendly" name translation and thereafter is -# displayed and exported as such. If the tag is NOT in this table and not -# otherwise handled by the code, the tag itself is used for display and -# export. For example "_XYZ" is not in the table and will be displayed as -# "_XYZ" and exported as an EVEN.TYPE=_XYZ -# As Custom entries, they do not appear in Gramps Events add choice unless -# already imported via GEDCOM. -# -# ------------------------------------------------------------------------- -CUSTOMEVENTTAGS = { - "_CIRC": _("Circumcision"), - "_COML": _("Common Law Marriage"), - "_DEST": _("Destination"), - "_DNA": _("DNA"), - "_DCAUSE": _("Cause of Death"), - "_EMPLOY": _("Employment"), - "_EXCM": _("Excommunication"), - "_EYC": _("Eye Color"), - "_FUN": _("Funeral"), - "_HEIG": _("Height"), - "_INIT": _("Initiatory (LDS)"), - "_MILTID": _("Military ID"), - "_MISN": _("Mission (LDS)"), - "_NAMS": _("Namesake"), - "_ORDI": _("Ordinance"), - "_ORIG": _("Origin"), - "_SEPR": _("Separation"), # Applies to Families - "_WEIG": _("Weight"), -} -# table for skipping illegal control chars in GEDCOM import -# Only 09, 0A, 0D are allowed. -STRIP_DICT = dict.fromkeys(list(range(9)) + list(range(11, 13)) + list(range(14, 32))) -# The C1 Control characters are not treated in Latin-1 (ISO-8859-1) as -# undefined, but if they have been used, the file is probably supposed to be -# cp1252 -DEL_AND_C1 = dict.fromkeys(list(range(0x7F, 0x9F))) - -# ------------------------------------------------------------------------- -# -# GEDCOM events to Gramps events conversion -# -# ------------------------------------------------------------------------- -GED_TO_GRAMPS_EVENT = {} -for __val, __key in PERSONALCONSTANTEVENTS.items(): - if __key != "": - GED_TO_GRAMPS_EVENT[__key] = __val -GED_TO_GRAMPS_EVENT["Stillbirth"] = EventType.STILLBIRTH - -for __val, __key in FAMILYCONSTANTEVENTS.items(): - if __key != "": - GED_TO_GRAMPS_EVENT[__key] = __val - -GED_TO_GRAMPS_ATTR = {} -for __val, __key in PERSONALCONSTANTATTRIBUTES.items(): - if __key != "": - GED_TO_GRAMPS_ATTR[__key] = __val - -# ------------------------------------------------------------------------- -# -# GEDCOM Date Constants -# -# ------------------------------------------------------------------------- -HMONTH = [ - "", - "TSH", - "CSH", - "KSL", - "TVT", - "SHV", - "ADR", - "ADS", - "NSN", - "IYR", - "SVN", - "TMZ", - "AAV", - "ELL", -] - -FMONTH = [ - "", - "VEND", - "BRUM", - "FRIM", - "NIVO", - "PLUV", - "VENT", - "GERM", - "FLOR", - "PRAI", - "MESS", - "THER", - "FRUC", - "COMP", -] - -MONTH = [ - "", - "JAN", - "FEB", - "MAR", - "APR", - "MAY", - "JUN", - "JUL", - "AUG", - "SEP", - "OCT", - "NOV", - "DEC", -] - -CALENDAR_MAP = { - Date.CAL_HEBREW: (HMONTH, "@#DHEBREW@"), - Date.CAL_FRENCH: (FMONTH, "@#DFRENCH R@"), - Date.CAL_JULIAN: (MONTH, "@#DJULIAN@"), - Date.CAL_SWEDISH: (MONTH, "@#DUNKNOWN@"), -} - -CALENDAR_MAP_PARSESTRING = { - Date.CAL_HEBREW: " (h)", - Date.CAL_FRENCH: " (f)", - Date.CAL_JULIAN: " (j)", - Date.CAL_SWEDISH: " (s)", -} - -# how wrong calendar use is shown -CALENDAR_MAP_WRONGSTRING = { - Date.CAL_HEBREW: " ", - Date.CAL_FRENCH: " ", - Date.CAL_JULIAN: " ", - Date.CAL_SWEDISH: " ", -} - -DATE_MODIFIER = { - Date.MOD_ABOUT: "ABT", - Date.MOD_BEFORE: "BEF", - Date.MOD_AFTER: "AFT", - Date.MOD_FROM: "FROM", - Date.MOD_TO: "TO", - # Date.MOD_INTERPRETED : "INT", -} - -DATE_QUALITY = { - Date.QUAL_CALCULATED: "CAL", - Date.QUAL_ESTIMATED: "EST", -} - -# ------------------------------------------------------------------------- -# -# regular expressions -# -# ------------------------------------------------------------------------- -NOTE_RE = re.compile(r"\s*\d+\s+\@(\S+)\@\s+NOTE(.*)$") -CONT_RE = re.compile(r"\s*\d+\s+CONT\s?(.*)$") -CONC_RE = re.compile(r"\s*\d+\s+CONC\s?(.*)$") -PERSON_RE = re.compile(r"\s*\d+\s+\@(\S+)\@\s+INDI(.*)$") -MOD = re.compile(r"\s*(INT|EST|CAL)\s+(.*)$") -CAL = re.compile(r"\s*(ABT|BEF|AFT|FROM|TO)?\s*@#D?([^@]+)@\s*(.*)$") -RANGE = re.compile(r"\s*BET\s+@#D?([^@]+)@\s*(.*)\s+AND\s+@#D?([^@]+)@\s*(.*)$") -RANGE1 = re.compile(r"\s*BET\s+\s*(.*)\s+AND\s+@#D?([^@]+)@\s*(.*)$") -RANGE2 = re.compile(r"\s*BET\s+@#D?([^@]+)@\s*(.*)\s+AND\s+\s*(.*)$") -SPAN = re.compile(r"\s*FROM\s+@#D?([^@]+)@\s*(.*)\s+TO\s+@#D?([^@]+)@\s*(.*)$") -SPAN1 = re.compile(r"\s*FROM\s+\s*(.*)\s+TO\s+@#D?([^@]+)@\s*(.*)$") -SPAN2 = re.compile(r"\s*FROM\s+@#D?([^@]+)@\s*(.*)\s+TO\s+\s*(.*)$") -NAME_RE = re.compile(r"/?([^/]*)(/([^/]*)(/([^/]*))?)?") -SURNAME_RE = re.compile(r"/([^/]*)/([^/]*)") -# Bare numeric date in DATE values (e.g. "7/11/1959"). GEDCOM requires -# 3-letter month names; numeric dates are out of spec and unavoidably -# ambiguous between MM/DD/YYYY and DD/MM/YYYY. The parser interprets -# them as MM/DD/YYYY (US format, see GedcomDateParser.dhformat_changed), -# which silently swaps day and month for exports from tools that emit -# DD/MM/YYYY (notably ancestry.com — see Mantis 9298). Used to flag -# such dates with a warning so the user can audit them after import. -BARE_NUMERIC_DATE = re.compile(r"^\s*\d{1,2}/\d{1,2}/\d{2,4}\s*$") - - -# ----------------------------------------------------------------------- -# -# GedcomDateParser -# -# ----------------------------------------------------------------------- -class GedcomDateParser(DateParser): - """Parse the dates""" - - month_to_int = { - "jan": 1, - "feb": 2, - "mar": 3, - "apr": 4, - "may": 5, - "jun": 6, - "jul": 7, - "aug": 8, - "sep": 9, - "oct": 10, - "nov": 11, - "dec": 12, - } - - _locale = GrampsLocale(lang="en_US") # no register_datehandler here - - def dhformat_changed(self): - """Allow overriding so a subclass can modify it""" - self.dhformat = "%m/%d/%y" - - -# ------------------------------------------------------------------------- -# -# Lexer - serves as the lexical analysis engine -# -# ------------------------------------------------------------------------- -class Lexer: - """low level line reading and early parsing""" - - def __init__(self, ifile, __add_msg): - self.ifile = ifile - self.current_list = [] - self.eof = False - self.cnv = None - self.cnt = 0 - self.index = 0 - self.func_map = { - TOKEN_CONT: self.__fix_token_cont, - TOKEN_CONC: self.__fix_token_conc, - } - self.__add_msg = __add_msg - - def readline(self): - """read a line from file with possibility of putting it back""" - if len(self.current_list) <= 1 and not self.eof: - self.__readahead() - try: - return GedLine(self.current_list.pop()) - except: - LOG.debug("Error in reading Gedcom line", exc_info=True) - return None - - def __fix_token_cont(self, data): - line = self.current_list[0] - new_value = line[2] + "\n" + data[2] - self.current_list[0] = (line[0], line[1], new_value, line[3], line[4]) - - def __fix_token_conc(self, data): - line = self.current_list[0] - if len(line[2]) == 4: - # This deals with lines of the form - # 0 @@ NOTE - # 1 CONC - # The previous line contains only a tag and no data so concat a - # space to separate the new line from the tag. This prevents the - # first letter of the new line being lost later - # in _GedcomParse.__parse_record - new_value = line[2] + " " + data[2] - else: - new_value = line[2] + data[2] - self.current_list[0] = (line[0], line[1], new_value, line[3], line[4]) - - def __readahead(self): - while len(self.current_list) < 5: - line = self.ifile.readline() - self.index += 1 - if not line: - self.eof = True - return - - original_line = line - try: - # According to the GEDCOM 5.5 standard, - # Chapter 1 subsection Grammar "leading whitespace preceeding - # a GEDCOM line should be ignored" - # We will also strip the terminator which is any combination - # of carriage_return and line_feed - line = line.lstrip(" ").rstrip("\n\r") - # split into level+delim+rest - line = line.partition(" ") - level = int(line[0]) - # there should only be one space after the level, - # but we can ignore more, - line = line[2].lstrip(" ") - # then split into tag+delim+line_value - # or xfef_id+delim+rest - # the xref_id can have spaces in it - if line.startswith("@"): - line = line.split("@", 2) - # line is now [None, alphanum+pointer_string, rest] - tag = "@" + line[1] + "@" - line_value = line[2].lstrip() - # Ignore meaningless @IDENT@ on CONT or CONC line - # as noted at https://www.tamurajones.net/IdentCONT.xhtml - if line_value.startswith(("CONT ", "CONC ")): - line = line_value.partition(" ") - tag = line[0] - line_value = line[2] - else: - line = line.partition(" ") - tag = line[0] - line_value = line[2] - except: - problem = _("Line ignored ") - text = original_line.rstrip("\n\r") - prob_width = 66 - problem = problem.ljust(prob_width)[0 : (prob_width - 1)] - text = text.replace("\n", "\n".ljust(prob_width + 22)) - message = "%s %s" % (problem, text) - self.__add_msg(message) - continue - - # Need to un-double '@' See Gedcom 5.5 spec 'any_char' - line_value = line_value.replace("@@", "@") - token = TOKENS.get(tag, TOKEN_UNKNOWN) - data = (level, token, line_value, tag, self.index) - - func = self.func_map.get(data[1]) - if func: - func(data) - else: - # There will normally only be one space between tag and - # line_value, but in case there is more then one, remove extra - # spaces after CONC/CONT processing - # Also, Gedcom spec says there should be no spaces at end of - # line, however some programs put them there (FTM), so let's - # leave them in place. - data = data[:2] + (data[2].lstrip(),) + data[3:] - self.current_list.insert(0, data) - - def clean_up(self): - """ - Break circular references to parsing methods stored in dictionaries - to aid garbage collection - """ - for key in list(self.func_map.keys()): - del self.func_map[key] - del self.func_map - - -# ----------------------------------------------------------------------- -# -# GedLine - represents a tokenized version of a GEDCOM line -# -# ----------------------------------------------------------------------- -class GedLine: - """ - GedLine is a class the represents a GEDCOM line. The form of a GEDCOM line - is: - - - - This gets parsed into - - Line Number, Level, Token Value, Token Text, and Data - - Data is dependent on the context the Token Value. For most of tokens, - this is just a text string. However, for certain tokens where we know - the context, we can provide some value. The current parsed tokens are: - - TOKEN_DATE - Date - TOKEN_SEX - Person gender item - TOEKN_UKNOWN - Check to see if this is a known event - """ - - __DATE_CNV = GedcomDateParser() - - @staticmethod - def __extract_date(text): - """ - Converts the specified text to a Date object. - """ - dateobj = Date() - # Horrible hack for importing illegal GEDCOM from Apple Macintosh - # Classic 'Gene' program - text = text.replace("BET ABT", "EST BET") - - # extract out the MOD line - match = MOD.match(text) - mod = "" - if match: - mod, text = match.groups() - qual = QUALITY_MAP.get(mod, Date.QUAL_NONE) - mod += " " - else: - qual = Date.QUAL_NONE - - # parse the range if we match, if so, return - match = RANGE.match(text) - match1 = RANGE1.match(text) - match2 = RANGE2.match(text) - if match or match1 or match2: - if match: - cal1, data1, cal2, data2 = match.groups() - elif match1: - cal1 = Date.CAL_GREGORIAN - data1, cal2, data2 = match1.groups() - elif match2: - cal2 = Date.CAL_GREGORIAN - cal1, data1, data2 = match2.groups() - cal1 = CALENDAR_MAP_GEDCOM2XML.get(cal1, Date.CAL_GREGORIAN) - cal2 = CALENDAR_MAP_GEDCOM2XML.get(cal2, Date.CAL_GREGORIAN) - if cal1 != cal2: - # not supported by GRAMPS, import as text, we construct a string - # that the parser will not parse as a correct date - return GedLine.__DATE_CNV.parse( - "%sbetween %s%s and %s%s" - % ( - mod, - data1, - CALENDAR_MAP_WRONGSTRING.get(cal1, ""), - CALENDAR_MAP_WRONGSTRING.get(cal2, ""), - data2, - ) - ) - - # add hebrew, ... calendar so that months are recognized - data1 += CALENDAR_MAP_PARSESTRING.get(cal1, "") - data2 += CALENDAR_MAP_PARSESTRING.get(cal2, "") - start = GedLine.__DATE_CNV.parse(data1) - stop = GedLine.__DATE_CNV.parse(data2) - dateobj.set( - Date.QUAL_NONE, - Date.MOD_RANGE, - cal1, - start.get_start_date() + stop.get_start_date(), - ) - dateobj.set_quality(qual) - return dateobj - - # parse a span if we match - match = SPAN.match(text) - match1 = SPAN1.match(text) - match2 = SPAN2.match(text) - if match or match1 or match2: - if match: - cal1, data1, cal2, data2 = match.groups() - elif match1: - cal1 = Date.CAL_GREGORIAN - data1, cal2, data2 = match1.groups() - elif match2: - cal2 = Date.CAL_GREGORIAN - cal1, data1, data2 = match2.groups() - cal1 = CALENDAR_MAP_GEDCOM2XML.get(cal1, Date.CAL_GREGORIAN) - cal2 = CALENDAR_MAP_GEDCOM2XML.get(cal2, Date.CAL_GREGORIAN) - if cal1 != cal2: - # not supported by GRAMPS, import as text, we construct a string - # that the parser will not parse as a correct date - return GedLine.__DATE_CNV.parse( - "%sfrom %s%s to %s%s" - % ( - mod, - data1, - CALENDAR_MAP_WRONGSTRING.get(cal1, ""), - CALENDAR_MAP_WRONGSTRING.get(cal2, ""), - data2, - ) - ) - # add hebrew, ... calendar so that months are recognized - data1 += CALENDAR_MAP_PARSESTRING.get(cal1, "") - data2 += CALENDAR_MAP_PARSESTRING.get(cal2, "") - start = GedLine.__DATE_CNV.parse(data1) - stop = GedLine.__DATE_CNV.parse(data2) - dateobj.set( - Date.QUAL_NONE, - Date.MOD_SPAN, - cal1, - start.get_start_date() + stop.get_start_date(), - ) - dateobj.set_quality(qual) - return dateobj - - match = CAL.match(text) - if match: - abt, call, data = match.groups() - call = CALENDAR_MAP_GEDCOM2XML.get(call, Date.CAL_GREGORIAN) - data += CALENDAR_MAP_PARSESTRING.get(call, "") - if abt: - dateobj = GedLine.__DATE_CNV.parse("%s %s" % (abt, data)) - else: - dateobj = GedLine.__DATE_CNV.parse(data) - dateobj.set_quality(qual) - return dateobj - dateobj = GedLine.__DATE_CNV.parse(text) - dateobj.set_quality(qual) - return dateobj - - def __init__(self, data): - """ - If the level is 0, then this is a top level instance. In this case, - we may find items in the form of: - - @ID@ - - If this is not the top level, we check the MAP_DATA array to see if - there is a conversion function for the data. - """ - self.line = data[4] - self.level = data[0] - self.token = data[1] - self.token_text = data[3].strip() - self.data = str(data[2]) - - if self.level == 0: - if ( - self.token_text - and self.token_text[0] == "@" - and self.token_text[-1] == "@" - ): - self.token = TOKEN_ID - self.token_text = self.token_text[1:-1] - self.data = self.data.strip() - else: - func = _MAP_DATA.get(self.token) - if func: - func(self) - - def calc_sex(self): - """ - Converts the data field to a gen.lib token indicating the gender - """ - try: - self.data = SEX_MAP.get(self.data.strip()[0], Person.UNKNOWN) - except: - self.data = Person.UNKNOWN - - def calc_date(self): - """ - Converts the data field to a Date object - """ - self.data = self.__extract_date(self.data) - self.token = TOKEN_DATE - - def calc_unknown(self): - """ - Checks to see if the token maps a known GEDCOM event. If so, we - change the type from UNKNOWN to TOKEN_GEVENT (gedcom event), and - the data is assigned to the associated Gramps EventType - """ - token = GED_TO_GRAMPS_EVENT.get(self.token_text) - if token: - event = Event() - event.set_description(self.data) - event.set_type(token) - self.token = TOKEN_GEVENT - self.data = event - else: - token = GED_TO_GRAMPS_ATTR.get(self.token_text) - if token: - attr = Attribute() - attr.set_value(self.data) - attr.set_type(token) - self.token = TOKEN_ATTR - self.data = attr - - def calc_note(self): - """look for a note xref @N0001@""" - gid = self.data.strip() - if len(gid) > 2 and gid[0] == "@" and gid[-1] == "@": - self.token = TOKEN_RNOTE - self.data = gid[1:-1] - - def calc_nchi(self): - """set attribute for number of children""" - attr = Attribute() - attr.set_value(self.data) - attr.set_type(AttributeType.NUM_CHILD) - self.data = attr - self.token = TOKEN_ATTR - - def calc_attr(self): - """set attribure for general attributes""" - attr = Attribute() - attr.set_value(self.data) - attr.set_type((AttributeType.CUSTOM, self.token_text)) - self.data = attr - self.token = TOKEN_ATTR - - def __repr__(self): - return "%d: %d (%d:%s) %s" % ( - self.line, - self.level, - self.token, - self.token_text, - self.data, - ) - - -_MAP_DATA = { - TOKEN_UNKNOWN: GedLine.calc_unknown, - TOKEN_DATE: GedLine.calc_date, - TOKEN__DATE: GedLine.calc_date, - TOKEN_SEX: GedLine.calc_sex, - TOKEN_NOTE: GedLine.calc_note, - TOKEN_NCHI: GedLine.calc_nchi, - TOKEN__STAT: GedLine.calc_attr, - TOKEN__UID: GedLine.calc_attr, - TOKEN_AFN: GedLine.calc_attr, - TOKEN__FSFTID: GedLine.calc_attr, -} - - -# ------------------------------------------------------------------------- -# -# File Readers -# -# ------------------------------------------------------------------------- -class BaseReader: - """base char level reader""" - - def __init__(self, ifile, encoding, __add_msg): - self.ifile = ifile - self.enc = encoding - self.__add_msg = __add_msg - - def reset(self): - """return to beginning""" - self.ifile.seek(0) - - def readline(self): - """Read a single line""" - raise NotImplementedError() - - def report_error(self, problem, line): - """Create an error message""" - line = line.rstrip("\n\r") - prob_width = 66 - problem = problem.ljust(prob_width)[0 : (prob_width - 1)] - text = line.replace("\n", "\n".ljust(prob_width + 22)) - message = "%s %s" % (problem, text) - self.__add_msg(message) - - -class UTF8Reader(BaseReader): - """The main UTF-8 reader, uses Python for char handling""" - - def __init__(self, ifile, __add_msg, enc): - BaseReader.__init__(self, ifile, enc, __add_msg) - self.reset() - if enc == "UTF_8_SIG": - self.ifile = TextIOWrapper( - ifile, encoding="utf_8_sig", errors="replace", newline=None - ) - else: - self.ifile = TextIOWrapper( - ifile, encoding="utf_8", errors="replace", newline=None - ) - - def readline(self): - line = self.ifile.readline() - return line.translate(STRIP_DICT) - - -class UTF16Reader(BaseReader): - """The main UTF-16 reader, uses Python for char handling""" - - def __init__(self, ifile, __add_msg): - BaseReader.__init__(self, ifile, "UTF16", __add_msg) - self.ifile = TextIOWrapper( - ifile, encoding="utf_16", errors="replace", newline=None - ) - self.reset() - - def readline(self): - line = self.ifile.readline() - return line.translate(STRIP_DICT) - - -class AnsiReader(BaseReader): - """The main ANSI (latin1) reader, uses Python for char handling""" - - def __init__(self, ifile, __add_msg): - BaseReader.__init__(self, ifile, "latin1", __add_msg) - self.ifile = TextIOWrapper( - ifile, encoding="latin1", errors="replace", newline=None - ) - - def readline(self): - line = self.ifile.readline() - if line.translate(DEL_AND_C1) != line: - self.report_error( - "DEL or C1 control chars in line did you mean " "CHAR cp1252??", line - ) - return line.translate(STRIP_DICT) - - -class CP1252Reader(BaseReader): - """The extra credit CP1252 reader, uses Python for char handling""" - - def __init__(self, ifile, __add_msg): - BaseReader.__init__(self, ifile, "cp1252", __add_msg) - self.ifile = TextIOWrapper( - ifile, encoding="cp1252", errors="replace", newline=None - ) - - def readline(self): - line = self.ifile.readline() - return line.translate(STRIP_DICT) - - -class AnselReader(BaseReader): - """ - ANSEL to Unicode Conversion - - ANSEL references: - http://lcweb2.loc.gov/diglib/codetables/45.html - http://www.gymel.com/charsets/ANSEL.html - - list of ANSEL codes that replicate ASCII - note that DEL (127=0x7F) is a control char - Note: spec allows control-chars that Gramps probably doesn't use - but 10=0x0A _is_ needed (!) - --- - Also: there are two additional control chars 0x98,0x9c (unicode same) - which we also ignore for now (start/emd of string (or sort sequence) - --- - ?: should we allow TAB, as a Gramps extension? - """ - - __printable_ascii = list(map(chr, list(range(32, 127)))) # up thru 126 - # LF CR Esc GS RS US - __use_ASCII = list(map(chr, [10, 13, 27, 29, 30, 31])) + __printable_ascii - - # mappings of single byte ANSEL codes to unicode - __onebyte = { - b"\xa1": "\u0141", - b"\xa2": "\u00d8", - b"\xa3": "\u0110", - b"\xa4": "\u00de", - b"\xa5": "\u00c6", - b"\xa6": "\u0152", - b"\xa7": "\u02b9", - b"\xa8": "\u00b7", - b"\xa9": "\u266d", - b"\xaa": "\u00ae", - b"\xab": "\u00b1", - b"\xac": "\u01a0", - b"\xad": "\u01af", - b"\xae": "\u02bc", - b"\xb0": "\u02bb", - b"\xb1": "\u0142", - b"\xb2": "\u00f8", - b"\xb3": "\u0111", - b"\xb4": "\u00fe", - b"\xb5": "\u00e6", - b"\xb6": "\u0153", - b"\xb7": "\u02ba", - b"\xb8": "\u0131", - b"\xb9": "\u00a3", - b"\xba": "\u00f0", - b"\xbc": "\u01a1", - b"\xbd": "\u01b0", - b"\xbe": "\u25a1", - b"\xbf": "\u25a0", - b"\xc0": "\u00b0", - b"\xc1": "\u2113", - b"\xc2": "\u2117", - b"\xc3": "\u00a9", - b"\xc4": "\u266f", - b"\xc5": "\u00bf", - b"\xc6": "\u00a1", - b"\xc7": "\u00df", - b"\xc8": "\u20ac", - b"\xcd": "\u0065", - b"\xce": "\u006f", - b"\xcf": "\u00df", - } - - # combining forms (in ANSEL, they precede the modified ASCII character - # whereas the unicode combining term follows the character modified - # Note: unicode allows multiple modifiers, but ANSEL may not (TDB?), - # so we ignore multiple combining forms in this module - # 8d & 8e are zero-width joiner (ZWJ), and zero-width non-joiner ZWNJ - # (strange things) probably not commonly found in our needs, unless one - # starts writing persian (or???) poetry in ANSEL - __acombiners = { - b"\x8d": "\u200d", - b"\x8e": "\u200c", - b"\xe0": "\u0309", - b"\xe1": "\u0300", - b"\xe2": "\u0301", - b"\xe3": "\u0302", - b"\xe4": "\u0303", - b"\xe5": "\u0304", - b"\xe6": "\u0306", - b"\xe7": "\u0307", - b"\xe8": "\u0308", - b"\xe9": "\u030c", - b"\xea": "\u030a", - b"\xeb": "\ufe20", - b"\xec": "\ufe21", - b"\xed": "\u0315", - b"\xee": "\u030b", - b"\xef": "\u0310", - b"\xf0": "\u0327", - b"\xf1": "\u0328", - b"\xf2": "\u0323", - b"\xf3": "\u0324", - b"\xf4": "\u0325", - b"\xf5": "\u0333", - b"\xf6": "\u0332", - b"\xf7": "\u0326", - b"\xf8": "\u031c", - b"\xf9": "\u032e", - b"\xfa": "\ufe22", - b"\xfb": "\ufe23", - b"\xfc": "\u0338", - b"\xfe": "\u0313", - } - - # mappings of two byte (precomposed forms) ANSEL codes to unicode - __twobyte = { - b"\xe0\x41": "\u1ea2", - b"\xe0\x45": "\u1eba", - b"\xe0\x49": "\u1ec8", - b"\xe0\x4f": "\u1ece", - b"\xe0\x55": "\u1ee6", - b"\xe0\x59": "\u1ef6", - b"\xe0\x61": "\u1ea3", - b"\xe0\x65": "\u1ebb", - b"\xe0\x69": "\u1ec9", - b"\xe0\x6f": "\u1ecf", - b"\xe0\x75": "\u1ee7", - b"\xe0\x79": "\u1ef7", - b"\xe1\x41": "\u00c0", - b"\xe1\x45": "\u00c8", - b"\xe1\x49": "\u00cc", - b"\xe1\x4f": "\u00d2", - b"\xe1\x55": "\u00d9", - b"\xe1\x57": "\u1e80", - b"\xe1\x59": "\u1ef2", - b"\xe1\x61": "\u00e0", - b"\xe1\x65": "\u00e8", - b"\xe1\x69": "\u00ec", - b"\xe1\x6f": "\u00f2", - b"\xe1\x75": "\u00f9", - b"\xe1\x77": "\u1e81", - b"\xe1\x79": "\u1ef3", - b"\xe2\x41": "\u00c1", - b"\xe2\x43": "\u0106", - b"\xe2\x45": "\u00c9", - b"\xe2\x47": "\u01f4", - b"\xe2\x49": "\u00cd", - b"\xe2\x4b": "\u1e30", - b"\xe2\x4c": "\u0139", - b"\xe2\x4d": "\u1e3e", - b"\xe2\x4e": "\u0143", - b"\xe2\x4f": "\u00d3", - b"\xe2\x50": "\u1e54", - b"\xe2\x52": "\u0154", - b"\xe2\x53": "\u015a", - b"\xe2\x55": "\u00da", - b"\xe2\x57": "\u1e82", - b"\xe2\x59": "\u00dd", - b"\xe2\x5a": "\u0179", - b"\xe2\x61": "\u00e1", - b"\xe2\x63": "\u0107", - b"\xe2\x65": "\u00e9", - b"\xe2\x67": "\u01f5", - b"\xe2\x69": "\u00ed", - b"\xe2\x6b": "\u1e31", - b"\xe2\x6c": "\u013a", - b"\xe2\x6d": "\u1e3f", - b"\xe2\x6e": "\u0144", - b"\xe2\x6f": "\u00f3", - b"\xe2\x70": "\u1e55", - b"\xe2\x72": "\u0155", - b"\xe2\x73": "\u015b", - b"\xe2\x75": "\u00fa", - b"\xe2\x77": "\u1e83", - b"\xe2\x79": "\u00fd", - b"\xe2\x7a": "\u017a", - b"\xe2\xa5": "\u01fc", - b"\xe2\xb5": "\u01fd", - b"\xe3\x41": "\u00c2", - b"\xe3\x43": "\u0108", - b"\xe3\x45": "\u00ca", - b"\xe3\x47": "\u011c", - b"\xe3\x48": "\u0124", - b"\xe3\x49": "\u00ce", - b"\xe3\x4a": "\u0134", - b"\xe3\x4f": "\u00d4", - b"\xe3\x53": "\u015c", - b"\xe3\x55": "\u00db", - b"\xe3\x57": "\u0174", - b"\xe3\x59": "\u0176", - b"\xe3\x5a": "\u1e90", - b"\xe3\x61": "\u00e2", - b"\xe3\x63": "\u0109", - b"\xe3\x65": "\u00ea", - b"\xe3\x67": "\u011d", - b"\xe3\x68": "\u0125", - b"\xe3\x69": "\u00ee", - b"\xe3\x6a": "\u0135", - b"\xe3\x6f": "\u00f4", - b"\xe3\x73": "\u015d", - b"\xe3\x75": "\u00fb", - b"\xe3\x77": "\u0175", - b"\xe3\x79": "\u0177", - b"\xe3\x7a": "\u1e91", - b"\xe4\x41": "\u00c3", - b"\xe4\x45": "\u1ebc", - b"\xe4\x49": "\u0128", - b"\xe4\x4e": "\u00d1", - b"\xe4\x4f": "\u00d5", - b"\xe4\x55": "\u0168", - b"\xe4\x56": "\u1e7c", - b"\xe4\x59": "\u1ef8", - b"\xe4\x61": "\u00e3", - b"\xe4\x65": "\u1ebd", - b"\xe4\x69": "\u0129", - b"\xe4\x6e": "\u00f1", - b"\xe4\x6f": "\u00f5", - b"\xe4\x75": "\u0169", - b"\xe4\x76": "\u1e7d", - b"\xe4\x79": "\u1ef9", - b"\xe5\x41": "\u0100", - b"\xe5\x45": "\u0112", - b"\xe5\x47": "\u1e20", - b"\xe5\x49": "\u012a", - b"\xe5\x4f": "\u014c", - b"\xe5\x55": "\u016a", - b"\xe5\x61": "\u0101", - b"\xe5\x65": "\u0113", - b"\xe5\x67": "\u1e21", - b"\xe5\x69": "\u012b", - b"\xe5\x6f": "\u014d", - b"\xe5\x75": "\u016b", - b"\xe5\xa5": "\u01e2", - b"\xe5\xb5": "\u01e3", - b"\xe6\x41": "\u0102", - b"\xe6\x45": "\u0114", - b"\xe6\x47": "\u011e", - b"\xe6\x49": "\u012c", - b"\xe6\x4f": "\u014e", - b"\xe6\x55": "\u016c", - b"\xe6\x61": "\u0103", - b"\xe6\x65": "\u0115", - b"\xe6\x67": "\u011f", - b"\xe6\x69": "\u012d", - b"\xe6\x6f": "\u014f", - b"\xe6\x75": "\u016d", - b"\xe7\x42": "\u1e02", - b"\xe7\x43": "\u010a", - b"\xe7\x44": "\u1e0a", - b"\xe7\x45": "\u0116", - b"\xe7\x46": "\u1e1e", - b"\xe7\x47": "\u0120", - b"\xe7\x48": "\u1e22", - b"\xe7\x49": "\u0130", - b"\xe7\x4d": "\u1e40", - b"\xe7\x4e": "\u1e44", - b"\xe7\x50": "\u1e56", - b"\xe7\x52": "\u1e58", - b"\xe7\x53": "\u1e60", - b"\xe7\x54": "\u1e6a", - b"\xe7\x57": "\u1e86", - b"\xe7\x58": "\u1e8a", - b"\xe7\x59": "\u1e8e", - b"\xe7\x5a": "\u017b", - b"\xe7\x62": "\u1e03", - b"\xe7\x63": "\u010b", - b"\xe7\x64": "\u1e0b", - b"\xe7\x65": "\u0117", - b"\xe7\x66": "\u1e1f", - b"\xe7\x67": "\u0121", - b"\xe7\x68": "\u1e23", - b"\xe7\x6d": "\u1e41", - b"\xe7\x6e": "\u1e45", - b"\xe7\x70": "\u1e57", - b"\xe7\x72": "\u1e59", - b"\xe7\x73": "\u1e61", - b"\xe7\x74": "\u1e6b", - b"\xe7\x77": "\u1e87", - b"\xe7\x78": "\u1e8b", - b"\xe7\x79": "\u1e8f", - b"\xe7\x7a": "\u017c", - b"\xe8\x41": "\u00c4", - b"\xe8\x45": "\u00cb", - b"\xe8\x48": "\u1e26", - b"\xe8\x49": "\u00cf", - b"\xe8\x4f": "\u00d6", - b"\xe8\x55": "\u00dc", - b"\xe8\x57": "\u1e84", - b"\xe8\x58": "\u1e8c", - b"\xe8\x59": "\u0178", - b"\xe8\x61": "\u00e4", - b"\xe8\x65": "\u00eb", - b"\xe8\x68": "\u1e27", - b"\xe8\x69": "\u00ef", - b"\xe8\x6f": "\u00f6", - b"\xe8\x74": "\u1e97", - b"\xe8\x75": "\u00fc", - b"\xe8\x77": "\u1e85", - b"\xe8\x78": "\u1e8d", - b"\xe8\x79": "\u00ff", - b"\xe9\x41": "\u01cd", - b"\xe9\x43": "\u010c", - b"\xe9\x44": "\u010e", - b"\xe9\x45": "\u011a", - b"\xe9\x47": "\u01e6", - b"\xe9\x49": "\u01cf", - b"\xe9\x4b": "\u01e8", - b"\xe9\x4c": "\u013d", - b"\xe9\x4e": "\u0147", - b"\xe9\x4f": "\u01d1", - b"\xe9\x52": "\u0158", - b"\xe9\x53": "\u0160", - b"\xe9\x54": "\u0164", - b"\xe9\x55": "\u01d3", - b"\xe9\x5a": "\u017d", - b"\xe9\x61": "\u01ce", - b"\xe9\x63": "\u010d", - b"\xe9\x64": "\u010f", - b"\xe9\x65": "\u011b", - b"\xe9\x67": "\u01e7", - b"\xe9\x69": "\u01d0", - b"\xe9\x6a": "\u01f0", - b"\xe9\x6b": "\u01e9", - b"\xe9\x6c": "\u013e", - b"\xe9\x6e": "\u0148", - b"\xe9\x6f": "\u01d2", - b"\xe9\x72": "\u0159", - b"\xe9\x73": "\u0161", - b"\xe9\x74": "\u0165", - b"\xe9\x75": "\u01d4", - b"\xe9\x7a": "\u017e", - b"\xea\x41": "\u00c5", - b"\xea\x61": "\u00e5", - b"\xea\x75": "\u016f", - b"\xea\x77": "\u1e98", - b"\xea\x79": "\u1e99", - b"\xea\xad": "\u016e", - b"\xee\x4f": "\u0150", - b"\xee\x55": "\u0170", - b"\xee\x6f": "\u0151", - b"\xee\x75": "\u0171", - b"\xf0\x20": "\u00b8", - b"\xf0\x43": "\u00c7", - b"\xf0\x44": "\u1e10", - b"\xf0\x47": "\u0122", - b"\xf0\x48": "\u1e28", - b"\xf0\x4b": "\u0136", - b"\xf0\x4c": "\u013b", - b"\xf0\x4e": "\u0145", - b"\xf0\x52": "\u0156", - b"\xf0\x53": "\u015e", - b"\xf0\x54": "\u0162", - b"\xf0\x63": "\u00e7", - b"\xf0\x64": "\u1e11", - b"\xf0\x67": "\u0123", - b"\xf0\x68": "\u1e29", - b"\xf0\x6b": "\u0137", - b"\xf0\x6c": "\u013c", - b"\xf0\x6e": "\u0146", - b"\xf0\x72": "\u0157", - b"\xf0\x73": "\u015f", - b"\xf0\x74": "\u0163", - b"\xf1\x41": "\u0104", - b"\xf1\x45": "\u0118", - b"\xf1\x49": "\u012e", - b"\xf1\x4f": "\u01ea", - b"\xf1\x55": "\u0172", - b"\xf1\x61": "\u0105", - b"\xf1\x65": "\u0119", - b"\xf1\x69": "\u012f", - b"\xf1\x6f": "\u01eb", - b"\xf1\x75": "\u0173", - b"\xf2\x41": "\u1ea0", - b"\xf2\x42": "\u1e04", - b"\xf2\x44": "\u1e0c", - b"\xf2\x45": "\u1eb8", - b"\xf2\x48": "\u1e24", - b"\xf2\x49": "\u1eca", - b"\xf2\x4b": "\u1e32", - b"\xf2\x4c": "\u1e36", - b"\xf2\x4d": "\u1e42", - b"\xf2\x4e": "\u1e46", - b"\xf2\x4f": "\u1ecc", - b"\xf2\x52": "\u1e5a", - b"\xf2\x53": "\u1e62", - b"\xf2\x54": "\u1e6c", - b"\xf2\x55": "\u1ee4", - b"\xf2\x56": "\u1e7e", - b"\xf2\x57": "\u1e88", - b"\xf2\x59": "\u1ef4", - b"\xf2\x5a": "\u1e92", - b"\xf2\x61": "\u1ea1", - b"\xf2\x62": "\u1e05", - b"\xf2\x64": "\u1e0d", - b"\xf2\x65": "\u1eb9", - b"\xf2\x68": "\u1e25", - b"\xf2\x69": "\u1ecb", - b"\xf2\x6b": "\u1e33", - b"\xf2\x6c": "\u1e37", - b"\xf2\x6d": "\u1e43", - b"\xf2\x6e": "\u1e47", - b"\xf2\x6f": "\u1ecd", - b"\xf2\x72": "\u1e5b", - b"\xf2\x73": "\u1e63", - b"\xf2\x74": "\u1e6d", - b"\xf2\x75": "\u1ee5", - b"\xf2\x76": "\u1e7f", - b"\xf2\x77": "\u1e89", - b"\xf2\x79": "\u1ef5", - b"\xf2\x7a": "\u1e93", - b"\xf3\x55": "\u1e72", - b"\xf3\x75": "\u1e73", - b"\xf4\x41": "\u1e00", - b"\xf4\x61": "\u1e01", - b"\xf9\x48": "\u1e2a", - b"\xf9\x68": "\u1e2b", - } - - def __ansel_to_unicode(self, text): - """Convert an ANSEL encoded text to unicode""" - - buff = StringIO() - error = "" - while text: - if text[0] < 128: - if chr(text[0]) in AnselReader.__use_ASCII: - head = chr(text[0]) - else: - # substitute space for disallowed (control) chars - error += " (%#X)" % text[0] - head = " " - text = text[1:] - else: - if text[0:2] in AnselReader.__twobyte: - head = AnselReader.__twobyte[text[0:2]] - text = text[2:] - elif bytes([text[0]]) in AnselReader.__onebyte: - head = AnselReader.__onebyte[bytes([text[0]])] - text = text[1:] - elif bytes([text[0]]) in AnselReader.__acombiners: - cmb = AnselReader.__acombiners[bytes([text[0]])] - # always consume the combiner - text = text[1:] - next_byte = text[0] - if ( - next_byte < 128 - and chr(next_byte) in AnselReader.__printable_ascii - ): - # consume next as well - text = text[1:] - # unicode: combiner follows base-char - head = chr(next_byte) + cmb - else: - # just drop the unexpected combiner - error += " (%#X)" % text[0] - continue - else: - error += " (%#X)" % text[0] - head = "\ufffd" # "Replacement Char" - text = text[1:] - buff.write(head) - ans = buff.getvalue() - - if error: - # e.g. Illegal character (oxAB) (0xCB)... 1 NOTE xyz?pqr?lmn - self.report_error(_("Illegal character%s") % error, ans) - buff.close() - return ans - - def __init__(self, ifile, __add_msg): - BaseReader.__init__(self, ifile, "ANSEL", __add_msg) - # In theory, we should have been able to skip the encode/decode from - # ascii. But this way allows us to use pythons universal newline - self.ifile = TextIOWrapper( - ifile, encoding="ascii", errors="surrogateescape", newline=None - ) - - def readline(self): - line = self.ifile.readline() - linebytes = line.encode(encoding="ascii", errors="surrogateescape") - return self.__ansel_to_unicode(linebytes) - - -# ------------------------------------------------------------------------- -# -# CurrentState -# -# ------------------------------------------------------------------------- -class CurrentState: - """ - Keep track of the current state variables. - """ - - def __init__(self, person=None, level=0, event=None, event_ref=None): - """ - Initialize the object. - """ - self.name_cnt = 0 - self.person = person - self.family = None - self.level = level - self.event = event - self.event_ref = event_ref - self.source_ref = None - self.citation = None - self.note = None - self.lds_ord = None - self.msg = "" - self.primary = False # _PRIMARY tag on an INDI.FAMC tag - self.filename = "" - self.title = "" - self.addr = None - self.res = None - self.source = None - self.ftype = None - self.pf = None # method for parsing places - self.location = None - self.place_fields = None # method for parsing places - self.ref = None # PersonRef - self.handle = None # - self.form = "" # Multimedia format - self.frel = None # Child relation to father - self.mrel = None - self.repo = None - self.attr = None - self.obj = None - self.name = "" - self.ignore = False - self.repo_ref = None - self.place = None - self.media = None - self.photo = "" # Person primary photo - self.prim = None # Photo is primary - - def __getattr__(self, name): - """ - Return the value associated with the specified attribute. - """ - return self.__dict__.get(name) - - def __setattr__(self, name, value): - """ - Set the value associated with the specified attribute. - """ - self.__dict__[name] = value - - -# ------------------------------------------------------------------------- -# -# PlaceParser -# -# ------------------------------------------------------------------------- -class PlaceParser: - """ - Provide the ability to parse GEDCOM FORM statements for places, and - the parse the line of text, mapping the text components to Location - values based of the FORM statement. - """ - - __field_map = { - "addr": Location.set_street, - "subdivision": Location.set_street, - "addr1": Location.set_street, - "adr1": Location.set_street, - "street": Location.set_street, - "addr2": Location.set_locality, - "adr2": Location.set_locality, - "locality": Location.set_locality, - "neighborhood": Location.set_locality, - "city": Location.set_city, - "town": Location.set_city, - "village": Location.set_city, - "county": Location.set_county, - "country": Location.set_country, - "state": Location.set_state, - "state/province": Location.set_state, - "region": Location.set_state, - "province": Location.set_state, - "area code": Location.set_postal_code, - "post code": Location.set_postal_code, - "zip code": Location.set_postal_code, - } - - def __init__(self, line=None): - self.parse_function = [] - - if line: - self.parse_form(line) - - def parse_form(self, line): - """ - Parses the GEDCOM PLAC.FORM into a list of function - pointers (if possible). It does this my mapping the text strings - (separated by commas) to the corresponding Location - method via the __field_map variable - """ - for item in line.data.split(","): - item = item.lower().strip() - fcn = self.__field_map.get(item, lambda x, y: None) - self.parse_function.append(fcn) - - def load_place(self, place_import, place, text): - """ - Takes the text string representing a place, splits it into - its subcomponents (comma separated), and calls the approriate - function based of its position, depending on the parsed value - from the FORM statement. - """ - items = [item.strip() for item in text.split(",")] - if len(items) != len(self.parse_function): - return - index = 0 - loc = Location() - for item in items: - self.parse_function[index](loc, item) - index += 1 - - location = ( - loc.get_street(), - loc.get_locality(), - loc.get_parish(), - loc.get_city(), - loc.get_county(), - loc.get_state(), - loc.get_country(), - ) - - for level, name in enumerate(location): - if name: - break - - if name: - type_num = 7 - level - else: - name = place.title - type_num = PlaceType.UNKNOWN - place.name.set_value(name) - place.set_type(PlaceType(type_num)) - code = loc.get_postal_code() - place.set_code(code) - if place.handle: # if handle is available, store immediately - place_import.store_location(location, place.handle) - else: # return for storage later - return location - - -# ------------------------------------------------------------------------- -# -# IdFinder -# -# ------------------------------------------------------------------------- -class IdFinder: - """ - Provide method of finding the next available ID. - """ - - def __init__(self, keys, prefix): - """ - Initialize the object. - """ - self.ids = set(keys) - self.index = 0 - self.prefix = prefix - - def find_next(self): - """ - Return the next available GRAMPS' ID for a Event object based - off the person ID prefix. - - @return: Returns the next available index - @rtype: str - """ - index = self.prefix % self.index - while index in self.ids: - self.index += 1 - index = self.prefix % self.index - self.ids.add(index) - self.index += 1 - return index - - -# ------------------------------------------------------------------------- -# -# IdMapper -# -# ------------------------------------------------------------------------- -class IdMapper: - """This class provide methods to keep track of the correspoindence between - Gedcom xrefs (@P1023@) and Gramps IDs.""" - - def __init__(self, has_gid, find_next, id2user_format): - self.has_gid = has_gid - self.find_next = find_next - self.id2user_format = id2user_format - self.swap = {} - - def __getitem__(self, gid): - if gid == "": - # We need to find the next gramps ID provided it is not already - # the target of a swap - new_val = self.find_next() - while new_val in list(self.swap.values()): - new_val = self.find_next() - else: - # remove any @ signs - gid = self.clean(gid) - if gid in self.swap: - return self.swap[gid] - else: - # now standardise the format - formatted_gid = self.id2user_format(gid) - # I1 and I0001 will both format as I0001. If we had already - # encountered I1, it would be in self.swap, so we would already - # have found it. If we had already encountered I0001 and we are - # now looking for I1, it wouldn't be in self.swap, and we now - # find that I0001 is in use, so we have to create a new id. - if self.has_gid(formatted_gid) or ( - formatted_gid in list(self.swap.values()) - ): - new_val = self.find_next() - while new_val in list(self.swap.values()): - new_val = self.find_next() - else: - new_val = formatted_gid - # we need to distinguish between I1 and I0001, so we record the map - # from the original format - self.swap[gid] = new_val - return new_val - - def clean(self, gid): - """remove '@' from start and end of xref""" - temp = gid.strip() - if len(temp) > 1 and temp[0] == "@" and temp[-1] == "@": - temp = temp[1:-1] - return temp - - def map(self): - """return the xref to GID translation map""" - return self.swap - - -# ------------------------------------------------------------------------- -# -# GedcomParser -# -# ------------------------------------------------------------------------- -class GedcomParser(UpdateCallback): - """ - Performs the second pass of the GEDCOM parser, which does all the heavy - lifting. - """ - - __TRUNC_MSG = _( - "Your GEDCOM file is corrupted. " "It appears to have been truncated." - ) - _EMPTY_LOC = object_to_dict(Location()) - - SyntaxError = "Syntax Error" - BadFile = "Not a GEDCOM file" - - @staticmethod - def __find_from_handle(gramps_id, table): - """ - Find a handle corresponding to the specified Gramps ID. - - The passed table contains the mapping. If the value is found, we return - it, otherwise we create a new handle, store it, and return it. - - """ - intid = table.get(gramps_id) - if not intid: - intid = create_id() - table[gramps_id] = intid - return intid - - @staticmethod - def __parse_name_personal(text): - """ - Parses a GEDCOM NAME value into an Name structure - """ - name = Name() - - match = SURNAME_RE.match(text) - if match: - # /surname/ extra, we assume extra is given name - names = match.groups() - name.set_first_name(names[1].strip()) - surn = Surname() - surn.set_surname(names[0].strip()) - surn.set_primary() - name.set_surname_list([surn]) - else: - try: - names = NAME_RE.match(text).groups() - # given /surname/ extra, we assume extra is suffix - name.set_first_name(names[0].strip()) - surn = Surname() - surn.set_surname(names[2].strip()) - surn.set_primary() - name.set_surname_list([surn]) - name.set_suffix(names[4].strip()) - except: - # something strange, set as first name - name.set_first_name(text.strip()) - return name - - def __init__( - self, - dbase, - ifile, - filename, - user, - stage_one, - default_source, - default_tag_format=None, - ): - UpdateCallback.__init__(self, user.callback) - self.user = user - self.set_total(stage_one.get_line_count()) - self.repo2id = {} - self.trans = None - self.errors = [] - self.number_of_errors = 0 - self.maxpeople = stage_one.get_person_count() - self.dbase = dbase - self.import_researcher = self.dbase.get_total() == 0 - event_ids = [] - for event in dbase.iter_events(): - event_ids.append(event.gramps_id) - self.emapper = IdFinder(event_ids, dbase.event_prefix) - self.famc_map = stage_one.get_famc_map() - self.fams_map = stage_one.get_fams_map() - - self.place_parser = PlaceParser() - self.inline_srcs = OrderedDict() - self.media_map = {} - self.note_type_map = {} - self.genby = "" - self.genvers = "" - self.subm = "" - self.use_def_src = default_source - self.func_list = [] - if self.use_def_src: - self.def_src = Source() - fname = os.path.basename(filename).split("\\")[-1] - self.def_src.set_title(_("Import from GEDCOM (%s)") % fname) - if default_tag_format: - name = time.strftime(default_tag_format) - tag = self.dbase.get_tag_from_name(name) - if tag: - self.default_tag = tag - else: - self.default_tag = Tag() - self.default_tag.set_name(name) - else: - self.default_tag = None - self.dir_path = os.path.dirname(filename) - self.is_ftw = False - self.addr_is_detail = False - self.groups = None - self.want_parse_warnings = True - - self.pid_map = IdMapper( - self.dbase.has_person_gramps_id, - self.dbase.find_next_person_gramps_id, - self.dbase.id2user_format, - ) - self.fid_map = IdMapper( - self.dbase.has_family_gramps_id, - self.dbase.find_next_family_gramps_id, - self.dbase.fid2user_format, - ) - self.sid_map = IdMapper( - self.dbase.has_source_gramps_id, - self.dbase.find_next_source_gramps_id, - self.dbase.sid2user_format, - ) - self.oid_map = IdMapper( - self.dbase.has_media_gramps_id, - self.dbase.find_next_media_gramps_id, - self.dbase.oid2user_format, - ) - self.rid_map = IdMapper( - self.dbase.has_repository_gramps_id, - self.dbase.find_next_repository_gramps_id, - self.dbase.rid2user_format, - ) - self.nid_map = IdMapper( - self.dbase.has_note_gramps_id, - self.dbase.find_next_note_gramps_id, - self.dbase.nid2user_format, - ) - - self.gid2id = {} - self.oid2id = {} - self.sid2id = {} - self.lid2id = {} - self.fid2id = {} - self.rid2id = {} - self.nid2id = {} - - self.place_import = PlaceImport(self.dbase) - - # - # Parse table for <> below the level 0 SUBM tag - # - # n @@ SUBM {1:1} - # +1 NAME {1:1} - # +1 <> {0:1} - # +1 <> {0:M} - # +1 LANG {0:3} - # +1 <> {0:M} - # +1 RFN {0:1} - # +1 RIN {0:1} - # +1 <> {0:1} - - # (N.B. GEDCOM allows multiple SUBMitter records) - self.subm_parse_tbl = { - # +1 NAME - TOKEN_NAME: self.__subm_name, - # +1 <> - TOKEN_ADDR: self.__subm_addr, - TOKEN_PHON: self.__subm_phon, - TOKEN_EMAIL: self.__subm_email, - TOKEN_WWW: self.__repo_www, - TOKEN_FAX: self.__repo_fax, - # +1 <> - # +1 LANG - # +1 <> - TOKEN_NOTE: self.__repo_note, - TOKEN_RNOTE: self.__repo_note, - # +1 RFN - # +1 RIN - # +1 <> - TOKEN_CHAN: self.__repo_chan, - } - self.func_list.append(self.subm_parse_tbl) - - # - # Parse table for <> below the level 0 INDI tag - # - # n @@ INDI {1:1} - # +1 RESN {0:1} - # +1 <> {0:M} - # +1 SEX {0:1} - # +1 <> {0:M} - # +1 <> {0:M} - # +1 <> {0:M} - # +1 <> {0:M} - # +1 <> {0:M} - # +1 SUBM @@ {0:M} - # +1 <> {0:M} - # +1 ALIA @@ {0:M} - # +1 ANCI @@ {0:M} - # +1 DESI @@ {0:M} - # +1 <> {0:M} - # +1 <> {0:M} - # +1 <> {0:M} - # +1 RFN {0:1} - # +1 AFN {0:1} - # +1 REFN {0:M} - # +2 TYPE {0:1} - # +1 RIN {0:1} - # +1 <> {0:1} - - self.indi_parse_tbl = { - # +1 RESN {0:1} - TOKEN_RESN: self.__person_resn, - # +1 <> {0:M} - TOKEN_NAME: self.__person_name, - # +1 SEX {0:1} - TOKEN_SEX: self.__person_sex, - # +1 <> {0:M} - TOKEN_EVEN: self.__person_even, - TOKEN_GEVENT: self.__person_std_event, - TOKEN_BIRT: self.__person_birt, - TOKEN_RELI: self.__person_reli, - TOKEN_ADOP: self.__person_adop, - TOKEN_DEAT: self.__person_deat, - # +1 <> {0:M} - # +1 AFN {0:1} - TOKEN_ATTR: self.__person_std_attr, - TOKEN_FACT: self.__person_fact, - # +1 <> {0:M} - TOKEN_BAPL: self.__person_bapl, - TOKEN_CONL: self.__person_conl, - TOKEN_INIL: self.__person_inil, - TOKEN_ENDL: self.__person_endl, - TOKEN_SLGC: self.__person_slgc, - # +1 <> {0:M} - TOKEN_FAMC: self.__person_famc, - # +1 <> {0:M} - TOKEN_FAMS: self.__person_fams, - # +1 SUBM @@ {0:M} - TOKEN_SUBM: self.__skip_record, - # +1 <> {0:M} - TOKEN_ASSO: self.__person_asso, - # +1 ALIA @@ {0:M} - TOKEN_ALIA: self.__person_alt_name, - # +1 ANCI @@ {0:M} - TOKEN_ANCI: self.__skip_record, - # +1 DESI @@ {0:M} - TOKEN_DESI: self.__skip_record, - # +1 <> {0:M} - TOKEN_SOUR: self.__person_sour, - # +1 <> {0:M} - TOKEN_OBJE: self.__person_object, - # +1 <> {0:M} - TOKEN_NOTE: self.__person_note, - TOKEN_RNOTE: self.__person_note, - TOKEN__COMM: self.__person_note, - # +1 RFN {0:1} - TOKEN_RFN: self.__person_attr, - # +1 REFN {0:M} - # +2 TYPE {0:1} - TOKEN_REFN: self.__person_refn, - # TYPE should be below REFN, but will work here anyway - TOKEN_TYPE: self.__person_attr, - # +1 RIN {0:1} - TOKEN_RIN: self.__person_attr, - # +1 <> {0:1} - TOKEN_CHAN: self.__person_chan, - # The following tags are not part of Gedcom spec but are commonly - # found here anyway - TOKEN_ADDR: self.__person_addr, - TOKEN_PHON: self.__person_phon, - TOKEN_FAX: self.__person_fax, - TOKEN_EMAIL: self.__person_email, - TOKEN_WWW: self.__person_www, - TOKEN__TODO: self.__skip_record, - TOKEN_TITL: self.__person_titl, - TOKEN__PHOTO: self.__person_photo, - } - self.func_list.append(self.indi_parse_tbl) - - self.name_parse_tbl = { - # +1 NPFX {0:1} - TOKEN_NPFX: self.__name_npfx, - # +1 GIVN {0:1} - TOKEN_GIVN: self.__name_givn, - # NICK {0:1} - TOKEN_NICK: self.__name_nick, - # _RUFNAME {0:1} - TOKEN__CALLNAME: self.__name_call, - # +1 SPFX {0:1} - TOKEN_SURN: self.__name_surn, - # +1 NSFX {0:1} - TOKEN_NSFX: self.__name_nsfx, - # +1 <> {0:M} - TOKEN_SOUR: self.__name_sour, - # +1 <> {0:M} - TOKEN_NOTE: self.__name_note, - TOKEN_RNOTE: self.__name_note, - # Extensions - TOKEN_ALIA: self.__name_alia, - TOKEN__MARNM: self.__name_marnm, - TOKEN__MAR: self.__name_marnm, # Generated by geni.com - TOKEN__MARN: self.__name_marnm, # Gen'd by BROSKEEP 6.1.31 WIN - TOKEN__AKA: self.__name_aka, # PAF and AncestQuest - TOKEN_TYPE: self.__name_type, # This is legal GEDCOM 5.5.1 - TOKEN_BIRT: self.__ignore, - TOKEN_DATE: self.__name_date, - # This handles date as a subsidiary of "1 ALIA" which might be used - # by Family Tree Maker and Reunion, and by cheating (handling a - # lower level from the current parse table) handles date as - # subsidiary to "2 _MARN", "2 _AKAN" and "2 _ADPN" which has been - # found in Brother's keeper. - TOKEN__ADPN: self.__name_adpn, - } - self.func_list.append(self.name_parse_tbl) - - # - # Parse table for <> below the level 0 REPO tag - # - # n @@ REPO {1:1} - # +1 NAME {0:1} - # +1 <> {0:1} - # +1 <> {0:M} - # +1 REFN {0:M} - # +2 TYPE {0:1} - # +1 RIN {0:1} - # +1 <> {0:1} - - self.repo_parse_tbl = { - TOKEN_NAME: self.__repo_name, - TOKEN_ADDR: self.__repo_addr, - TOKEN_RIN: self.__ignore, - TOKEN_NOTE: self.__repo_note, - TOKEN_RNOTE: self.__repo_note, - TOKEN_CHAN: self.__repo_chan, - TOKEN_PHON: self.__repo_phon, - TOKEN_EMAIL: self.__repo_email, - TOKEN_WWW: self.__repo_www, - TOKEN_FAX: self.__repo_fax, - } - self.func_list.append(self.repo_parse_tbl) - - self.event_parse_tbl = { - # n TYPE {0:1} - TOKEN_TYPE: self.__event_type, - # n DATE {0:1} p.*/* - TOKEN_DATE: self.__event_date, - # n <> {0:1} p.* - TOKEN_PLAC: self.__event_place, - # n <> {0:1} p.* - TOKEN_ADDR: self.__event_addr, - # n AGE {0:1} p.* - TOKEN_AGE: self.__event_age, - # n AGNC {0:1} p.* - TOKEN_AGNC: self.__event_agnc, - # n CAUS {0:1} p.* - TOKEN_CAUS: self.__event_cause, - # n <> {0:M} p.* - TOKEN_SOUR: self.__event_source, - # n <> {0:M} p.*, * - TOKEN_OBJE: self.__event_object, - # n <> {0:M} p. - TOKEN_NOTE: self.__event_inline_note, - TOKEN_RNOTE: self.__event_note, - # Other - TOKEN__PRIV: self.__event_privacy, - TOKEN_OFFI: self.__event_note, - TOKEN_PHON: self.__event_phon, - TOKEN__GODP: self.__event_witness, - TOKEN__WITN: self.__event_witness, - TOKEN__WTN: self.__event_witness, - TOKEN_RELI: self.__ignore, - # Not legal, but inserted by PhpGedView - TOKEN_TIME: self.__event_time, - TOKEN_ASSO: self.__ignore, - TOKEN_IGNORE: self.__ignore, - TOKEN_STAT: self.__ignore, - TOKEN_TEMP: self.__ignore, - TOKEN_HUSB: self.__event_husb, - TOKEN_WIFE: self.__event_wife, - TOKEN_FAMC: self.__person_birth_famc, - # Not legal, but inserted by Ultimate Family Tree - TOKEN_CHAN: self.__ignore, - TOKEN_QUAY: self.__ignore, - # Not legal, but inserted by FamilyTreeBuilder - TOKEN_RIN: self.__event_rin, - TOKEN_ATTR: self.__event_attr, # FTB for _UID - TOKEN_EMAIL: self.__event_email, # FTB for RESI events - TOKEN_WWW: self.__event_www, # FTB for RESI events - TOKEN_FAX: self.__event_fax, # legal... - } - self.func_list.append(self.event_parse_tbl) - - self.adopt_parse_tbl = { - TOKEN_TYPE: self.__event_type, - TOKEN__PRIV: self.__event_privacy, - TOKEN_DATE: self.__event_date, - TOKEN_SOUR: self.__event_source, - TOKEN_PLAC: self.__event_place, - TOKEN_ADDR: self.__event_addr, - TOKEN_PHON: self.__event_phon, - TOKEN_CAUS: self.__event_cause, - TOKEN_AGNC: self.__event_agnc, - TOKEN_AGE: self.__event_age, - TOKEN_NOTE: self.__event_note, - TOKEN_RNOTE: self.__event_note, - TOKEN_OFFI: self.__event_note, - TOKEN__GODP: self.__event_witness, - TOKEN__WITN: self.__event_witness, - TOKEN__WTN: self.__event_witness, - TOKEN_RELI: self.__ignore, - TOKEN_TIME: self.__ignore, - TOKEN_ASSO: self.__ignore, - TOKEN_IGNORE: self.__ignore, - TOKEN_STAT: self.__ignore, - TOKEN_TEMP: self.__ignore, - TOKEN_OBJE: self.__event_object, - TOKEN_FAMC: self.__person_adopt_famc, - # Not legal, but inserted by Ultimate Family Tree - TOKEN_CHAN: self.__ignore, - TOKEN_QUAY: self.__ignore, - } - self.func_list.append(self.adopt_parse_tbl) - - self.famc_parse_tbl = { - # n FAMC @@ {1:1} - # +1 PEDI {0:1} p.* - TOKEN_PEDI: self.__person_famc_pedi, - # +1 _FREL {0:1} non-standard - TOKEN__FREL: self.__person_famc_frel, - # +1 _MREL {0:1} non-standard - TOKEN__MREL: self.__person_famc_mrel, - # +1 <> {0:M} p.* - TOKEN_NOTE: self.__person_famc_note, - TOKEN_RNOTE: self.__person_famc_note, - # Extras - TOKEN__PRIMARY: self.__person_famc_primary, - TOKEN_SOUR: self.__person_famc_sour, - # GEDit - TOKEN_STAT: self.__ignore, - } - self.func_list.append(self.famc_parse_tbl) - - self.person_fact_parse_tbl = { - TOKEN_TYPE: self.__person_fact_type, - TOKEN_SOUR: self.__person_attr_source, - TOKEN_NOTE: self.__person_attr_note, - TOKEN_RNOTE: self.__person_attr_note, - } - self.func_list.append(self.person_fact_parse_tbl) - - self.person_attr_parse_tbl = { - TOKEN_TYPE: self.__person_attr_type, - TOKEN_CAUS: self.__ignore, - TOKEN_DATE: self.__ignore, - TOKEN_TIME: self.__ignore, - TOKEN_ADDR: self.__ignore, - TOKEN_IGNORE: self.__ignore, - TOKEN_STAT: self.__ignore, - TOKEN_TEMP: self.__ignore, - TOKEN_OBJE: self.__ignore, - TOKEN_SOUR: self.__person_attr_source, - TOKEN_PLAC: self.__person_attr_place, - TOKEN_NOTE: self.__person_attr_note, - TOKEN_RNOTE: self.__person_attr_note, - } - self.func_list.append(self.person_attr_parse_tbl) - - self.lds_parse_tbl = { - TOKEN_TEMP: self.__lds_temple, - TOKEN_DATE: self.__lds_date, - TOKEN_FAMC: self.__lds_famc, - TOKEN_FORM: self.__lds_form, - TOKEN_PLAC: self.__lds_plac, - TOKEN_SOUR: self.__lds_sour, - TOKEN_NOTE: self.__lds_note, - TOKEN_RNOTE: self.__lds_note, - TOKEN_STAT: self.__lds_stat, - } - self.func_list.append(self.lds_parse_tbl) - - self.asso_parse_tbl = { - TOKEN_RELA: self.__person_asso_rela, - TOKEN_SOUR: self.__person_asso_sour, - TOKEN_NOTE: self.__person_asso_note, - TOKEN_RNOTE: self.__person_asso_note, - } - self.func_list.append(self.asso_parse_tbl) - - self.citation_parse_tbl = { - TOKEN_PAGE: self.__citation_page, - TOKEN_DATE: self.__citation_date, - TOKEN_DATA: self.__citation_data, - TOKEN_OBJE: self.__citation_obje, - TOKEN_REFN: self.__citation_refn, - TOKEN_EVEN: self.__citation_even, - TOKEN_IGNORE: self.__ignore, - TOKEN__LKD: self.__ignore, - TOKEN_QUAY: self.__citation_quay, - TOKEN_NOTE: self.__citation_note, - TOKEN_RNOTE: self.__citation_note, - TOKEN_TEXT: self.__citation_data_text, - TOKEN__LINK: self.__citation_link, - TOKEN__JUST: self.__citation__just, - TOKEN__APID: self.__citation__apid, - } - self.func_list.append(self.citation_parse_tbl) - - self.media_parse_tbl = { - TOKEN_FORM: self.__media_ref_form, - TOKEN_MEDI: self.__media_ref_medi, # v5.5.1 - TOKEN_TITL: self.__media_ref_titl, - TOKEN_FILE: self.__media_ref_file, - TOKEN_NOTE: self.__obje_note, # illegal, but often there - TOKEN_RNOTE: self.__obje_note, # illegal, but often there - TOKEN__PRIM: self.__media_ref_prim, # LFT etc. - TOKEN_IGNORE: self.__ignore, - } - self.func_list.append(self.media_parse_tbl) - - self.parse_loc_tbl = { - TOKEN_ADR1: self.__location_adr1, - TOKEN_ADR2: self.__location_adr2, - TOKEN_CITY: self.__location_city, - TOKEN_STAE: self.__location_stae, - TOKEN_POST: self.__location_post, - TOKEN_CTRY: self.__location_ctry, - # Not legal GEDCOM - not clear why these are included at this level - TOKEN_ADDR: self.__ignore, - TOKEN_DATE: self.__ignore, # there is nowhere to put a date - TOKEN_NOTE: self.__location_note, - TOKEN_RNOTE: self.__location_note, - TOKEN__LOC: self.__ignore, - TOKEN__NAME: self.__ignore, - TOKEN_PHON: self.__location_phone, - TOKEN_IGNORE: self.__ignore, - } - self.func_list.append(self.parse_loc_tbl) - - # - # Parse table for <> below the level 0 FAM tag - # - # n @@ FAM {1:1} - # +1 <> {0:M} - # +1 HUSB @@ {0:1} - # +1 WIFE @@ {0:1} - # +1 CHIL @@ {0:M} - # +1 NCHI {0:1} - # +1 SUBM @@ {0:M} - # +1 <> {0:M} - # +1 <> {0:M} - # +1 <> {0:M} - # +1 <> {0:M} - # +1 REFN {0:M} - # +1 RIN {0:1} - # +1 <> {0:1} - - self.family_func = { - # +1 <> {0:M} - TOKEN_GEVENT: self.__family_std_event, - TOKEN_EVEN: self.__fam_even, - # +1 HUSB @@ {0:1} - TOKEN_HUSB: self.__family_husb, - # +1 WIFE @@ {0:1} - TOKEN_WIFE: self.__family_wife, - # +1 CHIL @@ {0:M} - TOKEN_CHIL: self.__family_chil, - # +1 NCHI {0:1} - # +1 SUBM @@ {0:M} - # +1 <> {0:M} - TOKEN_SLGS: self.__family_slgs, - # +1 <> {0:M} - TOKEN_SOUR: self.__family_source, - # +1 <> {0:M} - TOKEN_OBJE: self.__family_object, - # +1 <> {0:M} - TOKEN__COMM: self.__family_comm, - TOKEN_NOTE: self.__family_note, - TOKEN_RNOTE: self.__family_note, - # +1 REFN {0:M} - TOKEN_REFN: self.__family_refn, - # TYPE should be below REFN, but will work here anyway - TOKEN_TYPE: self.__family_cust_attr, - # +1 RIN {0:1} - # +1 <> {0:1} - TOKEN_CHAN: self.__family_chan, - TOKEN_ENDL: self.__ignore, - TOKEN_ADDR: self.__ignore, - TOKEN_RIN: self.__family_cust_attr, - TOKEN_SUBM: self.__ignore, - TOKEN_ATTR: self.__family_attr, - } - self.func_list.append(self.family_func) - - self.family_rel_tbl = { - TOKEN__FREL: self.__family_frel, - TOKEN__MREL: self.__family_mrel, - TOKEN_ADOP: self.__family_adopt, - TOKEN__STAT: self.__family_stat, - } - self.func_list.append(self.family_rel_tbl) - - # - # Parse table for <> below the level 0 SOUR tag - # - # n @@ SOUR {1:1} - # +1 DATA {0:1} - # +2 EVEN {0:M} - # +3 DATE {0:1} - # +3 PLAC {0:1} - # +2 AGNC {0:1} - # +2 <> {0:M} - # +1 AUTH {0:1} - # +2 [CONT|CONC] {0:M} - # +1 TITL {0:1} - # +2 [CONT|CONC] {0:M} - # +1 ABBR {0:1} - # +1 PUBL {0:1} - # +2 [CONT|CONC] {0:M} - # +1 TEXT {0:1} - # +2 [CONT|CONC] {0:M} - # +1 <> {0:1} - # +1 <> {0:M} - # +1 <> {0:M} - # +1 REFN {0:M} - # +2 TYPE {0:1} - # +1 RIN {0:1} - # +1 <> {0:1} - - self.source_func = { - TOKEN_TITL: self.__source_title, - TOKEN_TAXT: self.__source_taxt_peri, - TOKEN_PERI: self.__source_taxt_peri, - TOKEN_AUTH: self.__source_auth, - TOKEN_PUBL: self.__source_publ, - TOKEN_NOTE: self.__source_note, - TOKEN_RNOTE: self.__source_note, - TOKEN_TEXT: self.__source_text, - TOKEN_ABBR: self.__source_abbr, - TOKEN_REFN: self.__source_attr, - TOKEN_RIN: self.__source_attr, - TOKEN_REPO: self.__source_repo, - TOKEN_OBJE: self.__source_object, - TOKEN_CHAN: self.__source_chan, - TOKEN_MEDI: self.__source_attr, - TOKEN__NAME: self.__source_attr, - TOKEN_DATA: self.__ignore, - # TYPE should be below REFN, but will work here anyway - TOKEN_TYPE: self.__source_attr, - TOKEN_CALN: self.__ignore, - # not legal, but Ultimate Family Tree does this - TOKEN_DATE: self.__ignore, - TOKEN_IGNORE: self.__ignore, - TOKEN__APID: self.__source_attr, - } - self.func_list.append(self.source_func) - - # - # Parse table for <> below the level 0 OBJE tag - # - # n @XREF:OBJE@ OBJE {1:1} # v5.5 layout - # +1 FILE {1:1} # de-facto extension - # +1 FORM {1:1} - # +1 TITL {0:1} - # +1 <> {0:M} - # +1 BLOB {1:1} # Deprecated, no support - # +2 CONT {1:M} - # +1 OBJE @@ /* chain */ {0:1} # Deprecated, no support - # +1 REFN {0:M} - # +2 TYPE {0:1} - # +1 RIN {0:1} - # +1 <> {0:1} - # - # n @XREF:OBJE@ OBJE {1:1} # v5.5.1 layout - # +1 FILE {1:M} # multi files, no support - # +2 FORM {1:1} - # +3 TYPE {0:1} - # +2 TITL {0:1} - # +2 DATE {0:1} # FTM extension - # +2 TEXT {0:1} # FTM extension - # +1 REFN {0:M} - # +2 TYPE {0:1} - # +1 RIN {0:1} - # +1 <> {0:M} - # +1 <> {0:M} - # +1 <> {0:1} - - self.obje_func = { - TOKEN_FORM: self.__obje_form, - TOKEN_TYPE: self.__obje_type, # v5.5.1 - TOKEN_TITL: self.__obje_title, - TOKEN_FILE: self.__obje_file, # de-facto extension - TOKEN_TEXT: self.__obje_text, # FTM extension - TOKEN__TEXT: self.__obje_text, # FTM 2017 extension - TOKEN_DATE: self.__obje_date, # FTM extension - TOKEN__DATE: self.__obje_date, # FTM 2017 extension - TOKEN_NOTE: self.__obje_note, - TOKEN_RNOTE: self.__obje_note, - TOKEN_SOUR: self.__obje_sour, - TOKEN_BLOB: self.__ignore, # v5.5.1 deprecated - TOKEN_REFN: self.__obje_refn, - TOKEN_RIN: self.__obje_rin, - TOKEN_CHAN: self.__obje_chan, - } - self.func_list.append(self.obje_func) - - self.parse_addr_tbl = { - TOKEN_DATE: self.__address_date, - TOKEN_ADR1: self.__address_adr1, - TOKEN_ADR2: self.__address_adr2, - TOKEN_CITY: self.__address_city, - TOKEN_STAE: self.__address_state, - TOKEN_POST: self.__address_post, - TOKEN_CTRY: self.__address_country, - TOKEN_PHON: self.__ignore, - TOKEN_SOUR: self.__address_sour, - TOKEN_NOTE: self.__address_note, - TOKEN_RNOTE: self.__address_note, - TOKEN__LOC: self.__ignore, - TOKEN__NAME: self.__ignore, - TOKEN_IGNORE: self.__ignore, - TOKEN_TYPE: self.__ignore, - TOKEN_CAUS: self.__ignore, - } - self.func_list.append(self.parse_addr_tbl) - - self.event_cause_tbl = { - TOKEN_SOUR: self.__event_cause_source, - } - self.func_list.append(self.event_cause_tbl) - - self.event_place_map = { - TOKEN_NOTE: self.__event_place_note, - TOKEN_RNOTE: self.__event_place_note, - TOKEN_FORM: self.__event_place_form, - # Not legal. - TOKEN_OBJE: self.__event_place_object, - TOKEN_SOUR: self.__event_place_sour, - TOKEN__LOC: self.__ignore, - TOKEN_MAP: self.__place_map, - # Not legal, but generated by Ultimate Family Tree - TOKEN_QUAY: self.__ignore, - } - self.func_list.append(self.event_place_map) - - self.place_map_tbl = { - TOKEN_LATI: self.__place_lati, - TOKEN_LONG: self.__place_long, - } - self.func_list.append(self.place_map_tbl) - - self.repo_ref_tbl = { - TOKEN_CALN: self.__repo_ref_call, - TOKEN_NOTE: self.__repo_ref_note, - TOKEN_RNOTE: self.__repo_ref_note, - TOKEN_MEDI: self.__repo_ref_medi, - TOKEN_IGNORE: self.__ignore, - } - self.func_list.append(self.repo_ref_tbl) - - self.parse_person_adopt = { - TOKEN_ADOP: self.__person_adopt_famc_adopt, - } - self.func_list.append(self.parse_person_adopt) - - self.opt_note_tbl = { - TOKEN_RNOTE: self.__optional_note, - TOKEN_NOTE: self.__optional_note, - } - self.func_list.append(self.opt_note_tbl) - - self.citation_data_tbl = { - TOKEN_DATE: self.__citation_data_date, - TOKEN_TEXT: self.__citation_data_text, - TOKEN_RNOTE: self.__citation_data_note, - TOKEN_NOTE: self.__citation_data_note, - } - self.func_list.append(self.citation_data_tbl) - - self.citation_even_tbl = { - TOKEN_ROLE: self.__citation_even_role, - } - self.func_list.append(self.citation_even_tbl) - - # - # Parse table for <
> record below the level 0 HEAD tag - # - # n HEAD {1:1} - # +1 SOUR {1:1} - # +2 VERS {0:1} - # +2 NAME {0:1} - # +2 CORP {0:1} - # +3 <> {0:1} - # +2 DATA {0:1} - # +3 DATE {0:1} - # +3 COPR {0:1} - # +1 DEST {0:1*} - # +1 DATE {0:1} - # +2 TIME {0:1} - # +1 SUBM @@ {1:1} - # +1 SUBN @@ {0:1} - # +1 FILE {0:1} - # +1 COPR {0:1} - # +1 GEDC {1:1} - # +2 VERS {1:1} - # +2 FORM {1:1} - # +1 CHAR {1:1} - # +2 VERS {0:1} - # +1 LANG {0:1} - # +1 PLAC {0:1} - # +2 FORM {1:1} - # +1 NOTE {0:1} - # +2 [CONT|CONC] {0:M} - - # * NOTE: Submissions to the Family History Department for Ancestral - # File submission or for clearing temple ordinances must use a - # DESTination of ANSTFILE or TempleReady. - - self.head_parse_tbl = { - TOKEN_SOUR: self.__header_sour, - TOKEN_NAME: self.__header_sour_name, # This should be below SOUR - TOKEN_VERS: self.__header_sour_vers, # This should be below SOUR - TOKEN_FILE: self.__header_file, - TOKEN_COPR: self.__header_copr, - TOKEN_SUBM: self.__header_subm, - TOKEN_CORP: self.__ignore, # This should be below SOUR - TOKEN_DATA: self.__ignore, # This should be below SOUR - TOKEN_SUBN: self.__header_subn, - TOKEN_LANG: self.__header_lang, - TOKEN_TIME: self.__ignore, # This should be below DATE - TOKEN_DEST: self.__header_dest, - TOKEN_CHAR: self.__header_char, - TOKEN_GEDC: self.__header_gedc, - TOKEN_PLAC: self.__header_plac, - TOKEN_DATE: self.__header_date, - TOKEN_NOTE: self.__header_note, - TOKEN__SCHEMA: self.__ignore, - } - self.func_list.append(self.head_parse_tbl) - - self.header_sour_parse_tbl = { - TOKEN_VERS: self.__header_sour_vers, - TOKEN_NAME: self.__header_sour_name, - TOKEN_CORP: self.__header_sour_corp, - TOKEN_DATA: self.__header_sour_data, - } - self.func_list.append(self.header_sour_parse_tbl) - - self.header_sour_data = { - TOKEN_DATE: self.__header_sour_date, - TOKEN_COPR: self.__header_sour_copr, - } - self.func_list.append(self.header_sour_data) - - self.header_corp_addr = { - TOKEN_ADDR: self.__repo_addr, - TOKEN_PHON: self.__repo_phon, - TOKEN_FAX: self.__repo_fax, - TOKEN_WWW: self.__repo_www, - TOKEN_EMAIL: self.__repo_email, - } - self.func_list.append(self.header_corp_addr) - - self.header_subm = { - TOKEN_NAME: self.__header_subm_name, - } - self.func_list.append(self.header_subm) - - self.place_form = { - TOKEN_FORM: self.__place_form, - } - self.func_list.append(self.place_form) - - # - # Parse table for <> below the level 0 NOTE tag - # - # n @@ NOTE {1:1} - # +1 [ CONC | CONT] {0:M} - # +1 <> {0:M} - # +1 REFN {0:M} - # +2 TYPE {0:1} - # +1 RIN {0:1} - # +1 <> {0:1} - - self.note_parse_tbl = { - TOKEN_SOUR: self.__ignore, - TOKEN_REFN: self.__ignore, - TOKEN_RIN: self.__ignore, - TOKEN_CHAN: self.__note_chan, - } - self.func_list.append(self.note_parse_tbl) - - # look for existing place titles, build a map - self.place_names = defaultdict(list) - cursor = dbase.get_place_cursor() - data = next(cursor) - while data: - handle, val = data - self.place_names[val.title].append(handle) - data = next(cursor) - cursor.close() - - enc = stage_one.get_encoding() - - if enc == "ANSEL": - rdr = AnselReader(ifile, self.__add_msg) - elif enc in ("UTF-8", "UTF8", "UTF_8_SIG"): - rdr = UTF8Reader(ifile, self.__add_msg, enc) - elif enc in ("UTF-16LE", "UTF-16BE", "UTF16", "UNICODE"): - rdr = UTF16Reader(ifile, self.__add_msg) - elif enc in ("CP1252", "WINDOWS-1252"): - rdr = CP1252Reader(ifile, self.__add_msg) - else: - rdr = AnsiReader(ifile, self.__add_msg) - - self.lexer = Lexer(rdr, self.__add_msg) - self.filename = filename - self.backoff = False - - fullpath = os.path.normpath(os.path.abspath(filename)) - self.geddir = os.path.dirname(fullpath) - - self.error_count = 0 - amap = PERSONALCONSTANTATTRIBUTES - - self.attrs = list(amap.values()) - self.gedattr = dict([key, val] for val, key in amap.items()) - - def parse_gedcom_file(self, use_trans=False): - """ - Parses the opened GEDCOM file. - - LINEAGE_LINKED_GEDCOM: = - 0 <
> {1:1} - 0 <> {0:1} - 0 <> {1:M} - 0 TRLR {1:1} - - """ - with DbTxn(_("GEDCOM import"), self.dbase, not use_trans) as self.trans: - self.dbase.disable_signals() - self.__parse_header_head() - self.want_parse_warnings = False - self.want_parse_warnings = True - if self.use_def_src: - self.dbase.add_source(self.def_src, self.trans) - if self.default_tag and self.default_tag.handle is None: - self.dbase.add_tag(self.default_tag, self.trans) - self.__parse_header() - self.__parse_record() - self.__parse_trailer() - for title, handle in self.inline_srcs.items(): - src = Source() - src.set_handle(handle) - src.set_title(title) - self.dbase.add_source(src, self.trans) - self.__clean_up() - - self.place_import.generate_hierarchy(self.trans) - - if not self.dbase.get_feature("skip-check-xref"): - self.__check_xref() - self.dbase.enable_signals() - self.dbase.request_rebuild() - if self.number_of_errors == 0: - message = _("GEDCOM import report: No errors detected") - else: - message = ( - _("GEDCOM import report: %s errors detected") % self.number_of_errors - ) - if hasattr(self.user.uistate, "window"): - parent_window = self.user.uistate.window - else: - parent_window = None - self.user.info( - message, "".join(self.errors), parent=parent_window, monospaced=True - ) - - def __clean_up(self): - """ - Break circular references to parsing methods stored in dictionaries - to aid garbage collection - """ - for func_map in self.func_list: - for key in list(func_map.keys()): - del func_map[key] - del func_map - del self.func_list - del self.update - self.lexer.clean_up() - - def __find_person_handle(self, gramps_id): - """ - Return the database handle associated with the person's Gramps ID - """ - return self.__find_from_handle(gramps_id, self.gid2id) - - def __find_family_handle(self, gramps_id): - """ - Return the database handle associated with the family's Gramps ID - """ - return self.__find_from_handle(gramps_id, self.fid2id) - - def __find_media_handle(self, gramps_id): - """ - Return the database handle associated with the media object's Gramps ID - """ - return self.__find_from_handle(gramps_id, self.oid2id) - - def __find_note_handle(self, gramps_id): - """ - Return the database handle associated with the media object's Gramps ID - """ - return self.__find_from_handle(gramps_id, self.nid2id) - - def __find_or_create_person(self, gramps_id): - """ - Finds or creates a person based on the Gramps ID. If the ID is - already used (is in the db), we return the item in the db. Otherwise, - we create a new person, assign the handle and Gramps ID. - """ - intid = self.gid2id.get(gramps_id) - if self.dbase.has_person_handle(intid): - person = data_to_object(self.dbase.get_raw_person_data(intid)) - else: - person = Person() - intid = self.__find_from_handle(gramps_id, self.gid2id) - person.set_handle(intid) - person.set_gramps_id(gramps_id) - return person - - def __find_or_create_family(self, gramps_id): - """ - Finds or creates a family based on the Gramps ID. If the ID is - already used (is in the db), we return the item in the db. Otherwise, - we create a new family, assign the handle and Gramps ID. - """ - intid = self.fid2id.get(gramps_id) - if self.dbase.has_family_handle(intid): - family = data_to_object(self.dbase.get_raw_family_data(intid)) - else: - family = Family() - intid = self.__find_from_handle(gramps_id, self.fid2id) - family.set_handle(intid) - family.set_gramps_id(gramps_id) - # Add a counter for reordering the children later: - family._child_ref_count = 0 - return family - - def __find_or_create_media(self, gramps_id): - """ - Finds or creates a media object based on the Gramps ID. If the ID is - already used (is in the db), we return the item in the db. Otherwise, - we create a new media object, assign the handle and Gramps ID. - """ - intid = self.oid2id.get(gramps_id) - if self.dbase.has_media_handle(intid): - obj = data_to_object(self.dbase.get_raw_media_data(intid)) - else: - obj = Media() - intid = self.__find_from_handle(gramps_id, self.oid2id) - obj.set_handle(intid) - obj.set_gramps_id(gramps_id) - return obj - - def __find_or_create_source(self, gramps_id): - """ - Find or create a source based on the Gramps ID. - - If the ID is already used (is in the db), we return the item in the - db. Otherwise, we create a new source, assign the handle and Gramps ID. - - """ - intid = self.sid2id.get(gramps_id) - if self.dbase.has_source_handle(intid): - obj = data_to_object(self.dbase.get_raw_source_data(intid)) - else: - obj = Source() - intid = self.__find_from_handle(gramps_id, self.sid2id) - obj.set_handle(intid) - obj.set_gramps_id(gramps_id) - return obj - - def __find_or_create_repository(self, gramps_id): - """ - Finds or creates a repository based on the Gramps ID. If the ID is - already used (is in the db), we return the item in the db. Otherwise, - we create a new repository, assign the handle and Gramps ID. - - Some GEDCOM "flavors" destroy the specification, and declare the - repository inline instead of in a object. - """ - intid = self.rid2id.get(gramps_id) - if self.dbase.has_repository_handle(intid): - repository = data_to_object(self.dbase.get_raw_repository_data(intid)) - else: - repository = Repository() - intid = self.__find_from_handle(gramps_id, self.rid2id) - repository.set_handle(intid) - repository.set_gramps_id(gramps_id) - return repository - - def __find_or_create_note(self, gramps_id): - """ - Finds or creates a note based on the Gramps ID. If the ID is - already used (is in the db), we return the item in the db. Otherwise, - we create a new note, assign the handle and Gramps ID. - If no Gramps ID is passed in, we not only make a Note with GID, we - commit it. - """ - if not gramps_id: - need_commit = True - gramps_id = self.dbase.find_next_note_gramps_id() - else: - need_commit = False - - intid = self.nid2id.get(gramps_id) - if self.dbase.has_note_handle(intid): - note = data_to_object(self.dbase.get_raw_note_data(intid)) - else: - note = Note() - intid = self.__find_from_handle(gramps_id, self.nid2id) - note.set_handle(intid) - note.set_gramps_id(gramps_id) - if need_commit: - self.dbase.add_note(note, self.trans) - return note - - def __loc_is_empty(self, location): - """ - Determines whether a location is empty. - - @param location: The current location - @type location: gen.lib.Location - @return True of False - """ - if location is None: - return True - elif object_to_dict(location) == self._EMPTY_LOC: - return True - elif location.is_empty(): - return True - return False - - def __find_place(self, title, location, placeref_list): - """ - Finds an existing place based on the title and primary location. - - @param title: The place title - @type title: string - @param location: The current location - @type location: gen.lib.Location - @return gen.lib.Place - """ - for place_handle in self.place_names[title]: - place = self.dbase.get_place_from_handle(place_handle) - if place.get_title() == title: - if ( - self.__loc_is_empty(location) - and self.__loc_is_empty(self.__get_first_loc(place)) - and place.get_placeref_list() == placeref_list - ): - return place - elif ( - not self.__loc_is_empty(location) - and not self.__loc_is_empty(self.__get_first_loc(place)) - and self.__get_first_loc(place).is_equivalent(location) == IDENTICAL - ) and place.get_placeref_list() == placeref_list: - return place - return None - - def __add_place(self, event, sub_state): - """ - Add a new place to an event if not already present, or update a - place. - - @param event: The event - @type event: gen.lib.Event - @param substate: The sub-state for PLAC or ADDR elements (i.e. parsed - by event_parse_tbl) - @type sub_state: CurrentState - """ - if sub_state.place: - # see whether this place already exists - place = self.__find_place( - sub_state.place.get_title(), - self.__get_first_loc(sub_state.place), - sub_state.place.get_placeref_list(), - ) - if place is None: - place = sub_state.place - place_title = _pd.display(self.dbase, place) - location = sub_state.pf.load_place( - self.place_import, place, place_title - ) - self.dbase.add_place(place, self.trans) - # if 'location was created, then store it, now that we have a - # handle. - if location: - self.place_import.store_location(location, place.handle) - self.place_names[place.get_title()].append(place.get_handle()) - event.set_place_handle(place.get_handle()) - else: - place.merge(sub_state.place) - place_title = _pd.display(self.dbase, place) - location = sub_state.pf.load_place( - self.place_import, place, place_title - ) - self.dbase.commit_place(place, self.trans) - if location: - self.place_import.store_location(location, place.handle) - event.set_place_handle(place.get_handle()) - - def __find_file(self, fullname, altpath): - # try to find the media file - fullname = fullname.replace("\\", os.path.sep) - - try: - if os.path.isfile(fullname): - return (1, fullname) - except UnicodeEncodeError: - # FIXME: problem possibly caused by umlaut/accented character - # in filename - return (0, fullname) - # strip off Windows drive letter, if present - if len(fullname) > 3 and fullname[1] == ":": - fullname = fullname[2:] - # look where we found the '.ged', using the full path in fullname - other = os.path.join(altpath, fullname) - if os.path.isfile(other): - return (1, other) - # lets try reducing to just where we found '.ged' - other = os.path.join(altpath, os.path.basename(fullname)) - if os.path.isfile(other): - return (1, other) - # lets try using the base path for relative media paths - other = os.path.join(media_path(self.dbase), fullname) - if os.path.isfile(other): - return (1, fullname) - # lets try using the base path for relative media paths with base name - other = os.path.join(media_path(self.dbase), os.path.basename(fullname)) - if os.path.isfile(other): - return (1, os.path.basename(fullname)) - return (0, fullname) - - def __get_next_line(self): - """ - Get the next line for analysis from the lexical analyzer. Return the - same value if the _backup flag is set. - """ - if not self.backoff: - self.groups = self.lexer.readline() - self.update() - - # EOF ? - if not self.groups: - self.backoff = False - # We will add the truncation warning message to the error - # messages report, even though it probably won't be reported - # because the exception below gets raised before the report is - # produced. We do this in case __add_msg is changed in the - # future to do something else - self.__add_msg(self.__TRUNC_MSG) - self.groups = None - raise GedcomError(self.__TRUNC_MSG) - - self.backoff = False - return self.groups - - def __chk_subordinate(self, level, state, token): - """ - checks for a single subordinate line with specific token. If any other - lines are present, they are not understood. - - @param level: Current level in the file - @type level: int - @param state: The current state - @type state: CurrentState - @param token: The token to search for - @type token: int - """ - skips = 0 - got_line = None - while True: - line = self.__get_next_line() - if self.__level_is_finished(line, level): - if skips: - # This improves formatting when there are long sequences of - # skipped lines - self.__add_msg("", None, None) - return got_line - if line.token == token: - got_line = line - else: - self.__add_msg(_("Line ignored as not understood"), line, state) - skips += 1 - - def __undefined(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__not_recognized(line, state) - - def __ignore(self, line, state): - """ - Prints a message when an unexpected token is found. If the token is - known, then the line is considered "not supported", otherwise the line - is "not understood". - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if line.token == TOKEN_UNKNOWN: - self.__add_msg(_("Line ignored as not understood"), line, state) - else: - self.__add_msg(_("Tag recognized but not supported"), line, state) - self.__skip_subordinate_levels(line.level + 1, state) - - def __not_recognized(self, line, state): - """ - Prints a message when an undefined token is found. All subordinate - items to the current item are ignored. - - @param level: Current level in the file - @type level: int - """ - self.__add_msg(_("Line ignored as not understood"), line, state) - self.__skip_subordinate_levels(line.level + 1, state) - - def __skip_record(self, _line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__skip_subordinate_levels(2, state) - - def __skip_subordinate_levels(self, level, state): - """ - Skip add lines of the specified level or lower. - """ - skips = 0 - while True: - line = self.__get_next_line() - if self.__level_is_finished(line, level): - if skips: - # This improves formatting when there are long sequences of - # skipped lines - self.__add_msg("", None, None) - return - self.__add_msg(_("Skipped subordinate line"), line, state) - skips += 1 - - def __level_is_finished(self, text, level): - """ - Check to see if the level has been completed, indicated by finding - a level indiciated by the passed level value. If the level is finished, - then make sure to call self._backup to reset the text pointer. - """ - done = text.level < level - if done: - self._backup() - return done - - def __add_msg(self, problem, line=None, state=None): - if problem != "": - self.number_of_errors += 1 - if line: - prob_width = 66 - problem = problem.ljust(prob_width)[0 : (prob_width - 1)] - text = str(line.data).replace("\n", "\n".ljust(prob_width + 22)) - message = "%s Line %5d: %s %s %s\n" % ( - problem, - line.line, - line.level, - line.token_text, - text, - ) - else: - message = problem + "\n" - if state: - state.msg += message - self.errors.append(message) - - def __add_warn(self, problem, line=None, state=None): - """Add an advisory message to the import report that does NOT - count as an error. Mirrors the +1/-1 pattern used at the end - of ``__finish_import`` for the unknown-references note. - Used for things like ambiguous-numeric-date warnings (Mantis 9298) - where the importer made a choice the user may want to audit - but no record was rejected or truncated.""" - self.__add_msg(problem, line, state) - self.number_of_errors -= 1 - - def __warn_if_ambiguous_numeric_date(self, line, state): - """If ``line.data`` (a :class:`Date`) was parsed from a bare - numeric GEDCOM DATE (e.g. ``7/11/1959``) and the parser - committed to a day/month reading, log a warning so the user - can verify it. Bare numerics are not GEDCOM-spec and are - unavoidably ambiguous; the importer defaults to MM/DD/YYYY, - which silently swaps day and month for sources that emit - DD/MM/YYYY (Mantis 9298).""" - date = line.data - if date.get_modifier() == Date.MOD_TEXTONLY: - # day > 12 or otherwise unparseable — already visibly - # flagged in the Events view as a textual (bold) date. - return - text = date.get_text() - if not text or not BARE_NUMERIC_DATE.match(text): - return - # ``line`` is intentionally not passed: ``__add_msg`` truncates - # the problem string to 66 columns when a line is supplied, which - # drops the actionable half of the warning. The text below - # contains the original GEDCOM date string verbatim, so the user - # can still grep their .ged for it without the line number. - self.__add_warn( - _( - "Ambiguous numeric date '%(orig)s' interpreted as " - "MM/DD/YYYY (US format) = %(year)d-%(month)02d-%(day)02d. " - "If your source uses DD/MM/YYYY (e.g. ancestry.com " - "exports), verify and correct after import." - ) - % { - "orig": text, - "year": date.get_year(), - "month": date.get_month(), - "day": date.get_day(), - }, - None, - state, - ) - - def __check_msgs(self, record_name, state, obj): - if state.msg == "": - return - message = _("Records not imported into ") + record_name + ":\n\n" + state.msg - new_note = Note() - tag = StyledTextTag( - StyledTextTagType.FONTFACE, "Monospace", [(0, len(message))] - ) - text = StyledText(message, [tag]) - new_note.set_styledtext(text) - new_note.set_handle(create_id()) - gramps_id = self.nid_map[""] - new_note.set_gramps_id(gramps_id) - note_type = NoteType() - note_type.set((NoteType.CUSTOM, _("GEDCOM import"))) - new_note.set_type(note_type) - self.dbase.add_note(new_note, self.trans) - # If possible, attach the note to the relevant object - if obj: - obj.add_note(new_note.get_handle()) - - def _backup(self): - """ - Set the _backup flag so that the current line can be accessed by the - next level up. - """ - self.backoff = True - - def __check_xref(self): - def __check(_map, has_gid_func, class_func, commit_func, gramps_id2handle, msg): - for input_id, gramps_id in _map.map().items(): - # Check whether an object exists for the mapped gramps_id - if not has_gid_func(gramps_id): - _handle = self.__find_from_handle(gramps_id, gramps_id2handle) - if msg == "FAM": - make_unknown( - gramps_id, - self.explanation.handle, - class_func, - commit_func, - self.trans, - db=self.dbase, - ) - self.missing_references += 1 - self.__add_msg( - _( - "Error: %(msg)s '%(gramps_id)s'" - " (input as @%(xref)s@) not in input" - " GEDCOM. Record synthesised" - ) - % {"msg": msg, "gramps_id": gramps_id, "xref": input_id} - ) - else: - make_unknown( - gramps_id, - self.explanation.handle, - class_func, - commit_func, - self.trans, - ) - self.missing_references += 1 - self.__add_msg( - _( - "Error: %(msg)s '%(gramps_id)s'" - " (input as @%(xref)s@) not in input" - " GEDCOM. Record with typifying" - " attribute 'Unknown' created" - ) - % {"msg": msg, "gramps_id": gramps_id, "xref": input_id} - ) - - self.explanation = create_explanation_note(self.dbase) - - self.missing_references = 0 - __check( - self.pid_map, - self.dbase.has_person_gramps_id, - self.__find_or_create_person, - self.dbase.commit_person, - self.gid2id, - "INDI", - ) - __check( - self.fid_map, - self.dbase.has_family_gramps_id, - self.__find_or_create_family, - self.dbase.commit_family, - self.fid2id, - "FAM", - ) - __check( - self.sid_map, - self.dbase.has_source_gramps_id, - self.__find_or_create_source, - self.dbase.commit_source, - self.sid2id, - "SOUR", - ) - __check( - self.oid_map, - self.dbase.has_media_gramps_id, - self.__find_or_create_media, - self.dbase.commit_media, - self.oid2id, - "OBJE", - ) - __check( - self.rid_map, - self.dbase.has_repository_gramps_id, - self.__find_or_create_repository, - self.dbase.commit_repository, - self.rid2id, - "REPO", - ) - __check( - self.nid_map, - self.dbase.has_note_gramps_id, - self.__find_or_create_note, - self.dbase.commit_note, - self.nid2id, - "NOTE", - ) - - # Check persons membership in referenced families - def __input_fid(gramps_id): - for key, val in self.fid_map.map().items(): - if val == gramps_id: - return key - - for input_id, gramps_id in self.pid_map.map().items(): - person_handle = self.__find_from_handle(gramps_id, self.gid2id) - person = self.dbase.get_person_from_handle(person_handle) - for family_handle in person.get_family_handle_list(): - family = self.dbase.get_family_from_handle(family_handle) - if ( - family - and family.get_father_handle() != person_handle - and family.get_mother_handle() != person_handle - ): - person.remove_family_handle(family_handle) - self.dbase.commit_person(person, self.trans) - self.__add_msg( - _( - "Error: family '%(family)s' (input as" - " @%(orig_family)s@) person %(person)s" - " (input as %(orig_person)s) is not a" - " member of the referenced family." - " Family reference removed from person" - ) - % { - "family": family.gramps_id, - "orig_family": __input_fid(family.gramps_id), - "person": person.gramps_id, - "orig_person": input_id, - } - ) - - def __input_pid(gramps_id): - for key, val in self.pid_map.map().items(): - if val == gramps_id: - return key - - for input_id, gramps_id in self.fid_map.map().items(): - family_handle = self.__find_from_handle(gramps_id, self.fid2id) - family = self.dbase.get_family_from_handle(family_handle) - father_handle = family.get_father_handle() - mother_handle = family.get_mother_handle() - - if father_handle: - father = self.dbase.get_person_from_handle(father_handle) - if father and family_handle not in father.get_family_handle_list(): - father.add_family_handle(family_handle) - self.dbase.commit_person(father, self.trans) - self.__add_msg( - "Error: family '%(family)s' (input as" - " @%(orig_family)s@) father '%(father)s'" - " (input as '%(orig_father)s') does not " - "refer back to the family. Reference added." - % { - "family": family.gramps_id, - "orig_family": input_id, - "father": father.gramps_id, - "orig_father": __input_pid(father.gramps_id), - } - ) - - if mother_handle: - mother = self.dbase.get_person_from_handle(mother_handle) - if mother and family_handle not in mother.get_family_handle_list(): - mother.add_family_handle(family_handle) - self.dbase.commit_person(mother, self.trans) - self.__add_msg( - "Error: family '%(family)s' (input as" - " @%(orig_family)s@) mother '%(mother)s'" - " (input as '%(orig_mother)s') does not " - "refer back to the family. Reference added." - % { - "family": family.gramps_id, - "orig_family": input_id, - "mother": mother.gramps_id, - "orig_mother": __input_pid(mother.gramps_id), - } - ) - - for child_ref in family.get_child_ref_list(): - child_handle = child_ref.ref - child = self.dbase.get_person_from_handle(child_handle) - if child: - if family_handle not in child.get_parent_family_handle_list(): - # The referenced child has no reference to the family. - # There was a link from the FAM record to the child, - # but no FAMC link from the child to the FAM. - child.add_parent_family_handle(family_handle) - self.dbase.commit_person(child, self.trans) - self.__add_msg( - "Error: family '%(family)s' (input as" - " @%(orig_family)s@) child '%(child)s'" - " (input as '%(orig_child)s') does not " - "refer back to the family. " - "Reference added." - % { - "family": family.gramps_id, - "orig_family": input_id, - "child": child.gramps_id, - "orig_child": __input_pid(child.gramps_id), - } - ) - - if self.missing_references: - self.dbase.commit_note(self.explanation, self.trans, time.time()) - txt = _( - "\nThe imported file was not self-contained.\n" - "To correct for that, %(new)d objects were created and\n" - "their typifying attribute was set to 'Unknown'.\n" - "Where possible these 'Unknown' objects are \n" - "referenced by note %(unknown)s.\n" - ) % {"new": self.missing_references, "unknown": self.explanation.gramps_id} - self.__add_msg(txt) - self.number_of_errors -= 1 - - def __merge_address(self, free_form_address, addr, line, state): - """ - Merge freeform and structured addrssses. - n ADDR {0:1} - +1 CONT {0:M} - +1 ADR1 {0:1} (Street) - +1 ADR2 {0:1} (Locality) - +1 CITY {0:1} - +1 STAE {0:1} - +1 POST {0:1} - +1 CTRY {0:1} - - This is done along the lines suggested by Tamura Jones in - https://www.tamurajones.net/GEDCOMADDR.xhtml as a result of bug 6382. - "When a GEDCOM reader encounters a double address, it should read the - structured address. ... A GEDCOM reader that does verify that the - addresses are the same should issue an error if they are not". - - This is called for SUBMitter addresses (__subm_addr), INDIvidual - addresses (__person_addr), REPO addresses and HEADer corp address - (__repo_address) and EVENt addresses (__event_adr). - - The structured address (if any) will have been accumulated into an - object of type LocationBase, which will either be a Location, or an - Address object. - - If ADDR is provided, but none of ADR1, ADR2, CITY, STAE, or POST (not - CTRY), then Street is set to the freeform address. N.B. this is a - change for Repository addresses and HEADer Corp address where - previously the free-form address was deconstrucated into different - structured components. N.B. PAF provides a free-form address and a - country, so this allows for that case. - - If both forms of address are provided, then the structured address is - used, and if the ADDR/CONT contains anything not in the structured - address, a warning is issued. - - If just ADR1, ADR2, CITY, STAE, POST or CTRY are provided (this is not - actually legal GEDCOM symtax, but may be possible by GEDCOM extensions) - then just the structrued address is used. - The routine returns a string suitable for a title. - """ - title = "" - free_form_address = free_form_address.replace("\n", ", ") - if not ( - addr.get_street() - or addr.get_locality() - or addr.get_city() - or addr.get_state() - or addr.get_postal_code() - ): - addr.set_street(free_form_address) - return free_form_address - else: - # structured address provided - addr_list = free_form_address.split(",") - str_list = [] - for func in ( - addr.get_street(), - addr.get_locality(), - addr.get_city(), - addr.get_state(), - addr.get_postal_code(), - addr.get_country(), - ): - str_list += [i.strip("," + string.whitespace) for i in func.split("\n")] - for elmn in addr_list: - if elmn.strip("," + string.whitespace) not in str_list: - # message means that the element %s was ignored, but - # expressed the wrong way round because the message is - # truncated for output - self.__add_msg(_("ADDR element ignored '%s'" % elmn), line, state) - # The free-form address ADDR is discarded - # Assemble a title out of structured address - for elmn in str_list: - if elmn: - if title != "": - # TODO for Arabic, should the next comma be translated? - title += ", " - title += elmn - return title - - def __parse_trailer(self): - """ - Looks for the expected TRLR token - """ - try: - line = self.__get_next_line() - if line and line.token != TOKEN_TRLR: - state = CurrentState() - self.__not_recognized(line, state) - self.__check_msgs(_("TRLR (trailer)"), state, None) - except TypeError: - return - - def __parse_submitter(self, line): - """ - Parses the submitter data - - n @@ SUBM - +1 NAME - +1 <> - +1 <> - +1 LANG - +1 RFN - +1 RIN - +1 <> - """ - researcher = Researcher() - state = CurrentState() - state.res = researcher - state.level = 1 - repo = Repository() - state.repo = repo - self.__parse_level(state, self.subm_parse_tbl, self.__undefined) - # If this is the submitter that we were told about in the HEADer, then - # we will need to update the researcher - if line.token_text == self.subm and self.import_researcher: - self.dbase.set_researcher(state.res) - - localized_submitter = _("(Submitter):") - if state.res.get_name() == "": - submitter_name = "SUBM %s @%s@" % (localized_submitter, line.token_text) - else: - submitter_name = "SUBM %s (@%s@) %s" % ( - localized_submitter, - line.token_text, - state.res.get_name(), - ) - if self.use_def_src: - repo.set_name(submitter_name) - repo.set_handle(create_id()) - repo.set_gramps_id(self.rid_map[""]) - - addr = Address() - addr.set_street(state.res.get_address()) - addr.set_locality(state.res.get_locality()) - addr.set_city(state.res.get_city()) - addr.set_state(state.res.get_state()) - addr.set_country(state.res.get_country()) - addr.set_postal_code(state.res.get_postal_code()) - addr.set_county(state.res.get_county()) - addr.set_phone(state.res.get_phone()) - repo.add_address(addr) - rtype = RepositoryType() - rtype.set((RepositoryType.CUSTOM, _("GEDCOM data"))) - repo.set_type(rtype) - self.__check_msgs(submitter_name, state, repo) - self.dbase.commit_repository(repo, self.trans, state.repo.change) - repo_ref = RepoRef() - repo_ref.set_reference_handle(repo.handle) - mtype = SourceMediaType() - mtype.set((SourceMediaType.UNKNOWN, "")) - repo_ref.set_media_type(mtype) - self.def_src.add_repo_reference(repo_ref) - self.dbase.commit_source(self.def_src, self.trans) - else: - self.__check_msgs(submitter_name, state, None) - - def __parse_record(self): - """ - Parse the top level (0 level) instances. - RECORD: = - [ - n <> {1:1} - | - n <> {1:1} - | - n <> {1:M} - | - n <> {1:1} - | - n <> {1:1} - | - n <> {1:1} - | - n <> {1:1} - ] - - This also deals with the SUBN (submission) record, of which there - should be exactly one. - """ - while True: - line = self.__get_next_line() - key = line.data - if not line or line.token == TOKEN_TRLR: - self._backup() - break - if line.token == TOKEN_UNKNOWN: - state = CurrentState() - self.__add_msg(_("Unknown tag"), line, state) - self.__skip_subordinate_levels(1, state) - self.__check_msgs(_("Top Level"), state, None) - elif key in ("FAM", "FAMILY"): - self.__parse_fam(line) - elif key in ("INDI", "INDIVIDUAL"): - self.__parse_indi(line) - elif key in ("OBJE", "OBJECT"): - self.__parse_obje(line) - elif key in ("REPO", "REPOSITORY"): - self.__parse_repo(line) - elif key in ("SUBM", "SUBMITTER"): - self.__parse_submitter(line) - elif key == "SUBN": - state = CurrentState(level=1) - self.__parse_submission(line, state) - self.__check_msgs(_("Top Level"), state, None) - elif line.token in (TOKEN_SUBM, TOKEN_SUBN, TOKEN_IGNORE): - state = CurrentState() - self.__skip_subordinate_levels(1, state) - self.__check_msgs(_("Top Level"), state, None) - elif key in ("SOUR", "SOURCE"): - self.__parse_source(line.token_text, 1) - elif line.data.startswith(("SOUR ", "SOURCE ")): - # A source formatted in a single line, for example: - # 0 @S62@ SOUR This is the title of the source - source = self.__find_or_create_source(self.sid_map[line.data]) - source.set_title(line.data[5:]) - self.dbase.commit_source(source, self.trans) - elif key[0:4] == "NOTE": - try: - line.data = line.data[5:] - except: - # don't think this path is ever taken, but if it is.. - # ensure a message is emitted & subordinates skipped - line.data = None - self.__parse_inline_note(line, 1) - else: - state = CurrentState() - self.__not_recognized(line, state) - self.__check_msgs(_("Top Level"), state, None) - - def __parse_level(self, state, __map, default): - """ - Loop trough the current GEDCOM level, calling the appropriate - functions associated with the TOKEN. - - If no matching function for the token is found, the default function - is called instead. - - """ - while True: - line = self.__get_next_line() - if line.level < state.level: - self.backoff = True - return - else: - func = __map.get(line.token, default) - func(line, state) - - # ---------------------------------------------------------------------- - # - # INDI parsing - # - # ---------------------------------------------------------------------- - - def __parse_indi(self, line): - """ - Handling of the GEDCOM INDI tag and all lines subordinate to the - current line. - - n @XREF:INDI@ INDI {1:1} - +1 RESN {0:1} - +1 <> {0:M} - +1 SEX {0:1} - +1 <> {0:M} - +1 <> {0:M} - +1 <> {0:M} - +1 <> {0:M} - +1 <> {0:M} - +1 SUBM @@ {0:M} - +1 <> {0:M} - +1 ALIA @@ {0:M} - +1 ANCI @@ {0:M} - +1 DESI @@ {0:M} - +1 <> {0:M} - +1 <> {0:M} - +1 <> {0:M} - +1 RFN {0:1} - +1 AFN {0:1} - +1 REFN {0:M} - +2 TYPE {0:1} - +1 RIN {0:1} - +1 <> {0:1} - """ - - # find the person - real_id = self.pid_map[line.token_text] - person = self.__find_or_create_person(real_id) - - # set up the state for the parsing - state = CurrentState(person=person, level=1) - - # do the actual parsing - self.__parse_level(state, self.indi_parse_tbl, self.__person_event) - - # Add the default reference if no source has found - self.__add_default_source(person) - - # Add a default tag if provided - self.__add_default_tag(person) - - # Set up primary photo if present - self.__do_photo(state) - - self.__check_msgs( - _("INDI (individual) Gramps ID %s") % person.get_gramps_id(), state, person - ) - # commit the person to the database - self.dbase.commit_person(person, self.trans, state.person.change) - - def __person_sour(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - citation_handle = self.handle_source(line, state.level, state) - state.person.add_citation(citation_handle) - - def __person_refn(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__do_refn(line, state, state.person) - - def __person_attr(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - attr = Attribute() - attr.set_type((AttributeType.CUSTOM, line.token_text)) - attr.set_value(line.data) - state.person.add_attribute(attr) - - def __person_event(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - # We can get here when a tag that is not valid in the indi_parse_tbl - # parse table is encountered. The tag may be of the form "_XXX". We - # try to convert to a friendly name, if fails use the tag itself as - # the TYPE in a custom event - cust_tag = CUSTOMEVENTTAGS.get(line.token_text, line.token_text) - cust_type = EventType((EventType.CUSTOM, cust_tag)) - event_ref = self.__build_event_pair( - state, cust_type, self.event_parse_tbl, str(line.data) - ) - state.person.add_event_ref(event_ref) - - def __fam_even(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - event_ref = self.__build_family_event_pair( - state, EventType.CUSTOM, self.event_parse_tbl, line.data - ) - state.family.add_event_ref(event_ref) - - def __person_chan(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_change(line, state.person, state.level + 1, state) - - def __person_resn(self, line, state): - """ - Parses the RESN tag, adding it as an attribute. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - attr = Attribute() - attr.set_type((AttributeType.CUSTOM, "RESN")) - state.person.add_attribute(attr) - - def __person_alt_name(self, line, state): - """ - This parses the standard GEDCOM structure: - - n @XREF:INDI@ INDI {1:1} - +1 ALIA @@ {0:M} - - The ALIA tag is supposed to cross reference another person. We will - store this in the Association record. - - ALIA {ALIAS}: = An indicator to link different record descriptions of a - person who may be the same person. - - Some systems use the ALIA tag as an alternate NAME tag, which is not - legal in GEDCOM, but oddly enough, is easy to support. This parses the - illegal (ALIA or ALIAS) or non-standard (_ALIA) GEDCOM. "1 ALIA" is - used by Family Tree Maker and Reunion. "1 ALIAS" and "1 _ALIA" do not - appear to be used. - - n @XREF:INDI@ INDI {1:1} - +1 {1:1} - +2 NPFX {0:1} - +2 GIVN {0:1} - +2 NICK {0:1} - +2 SPFX {0:1} - +2 SURN {0:1} - +2 NSFX {0:1} - +2 <> {0:M} - +3 <> {0:M} - +3 <> {0:M} - +2 <> {0:M} - where == ALIA | _ALIA | ALIAS - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if line.data == "": - self.__add_msg(_("Empty Alias ignored"), line, state) - self.__skip_subordinate_levels(state.level + 1, state) - elif line.data[0] == "@": - handle = self.__find_person_handle(self.pid_map[line.data]) - ref = PersonRef() - ref.ref = handle - ref.rel = "Alias" - state.person.add_person_ref(ref) - else: - self.__parse_alias_name(line, state) - - def __parse_alias_name(self, line, state): - """ - Parse a level 1 alias name and subsidiary levels when called from - __person_alt_name (when the does not start with @). - Also parses a level 2 alias name and subsidiary levels when called - from __name_alias. - - +1 {1:1} - +2 NPFX {0:1} - +2 GIVN {0:1} - +2 NICK {0:1} - +2 SPFX {0:1} - +2 SURN {0:1} - +2 NSFX {0:1} - +2 <> {0:M} - +3 <> {0:M} - +3 <> {0:M} - +2 <> {0:M} - where == ALIA | _ALIA | ALIAS - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - name = self.__parse_name_personal(line.data) - name.set_type(NameType.AKA) - state.person.add_alternate_name(name) - - # Create a new state, and parse the remainder of the NAME level - sub_state = CurrentState() - sub_state.person = state.person - sub_state.name = name - sub_state.level = state.level + 1 - - self.__parse_level(sub_state, self.name_parse_tbl, self.__undefined) - state.msg += sub_state.msg - - def __person_object(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__obje(line, state, state.person) - - def __person_photo(self, line, state): - """ - This handles the FTM _PHOTO feature, which identifies an OBJE to use - as the person's primary photo. - """ - state.photo = line.data # Just save it for now. - - def __person_name(self, line, state): - """ - Parsers the NAME token in a GEDCOM file. The text is in the format - of (according to the GEDCOM Spec): - > |//|//|//|// - We have encountered some variations that use: - > / - - The basic Name structure is: - - n NAME {1:1} - +1 NPFX {0:1} - +1 GIVN {0:1} - +1 NICK {0:1} - +1 SPFX {0:1} - +1 NSFX {0:1} - +1 <> {0:M} - +1 <> {0:M} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - - # build a Name structure from the text - - name = self.__parse_name_personal(line.data) - - # Add the name as the primary name if this is the first one that - # we have encountered for this person. Assume that if this is the - # first name, that it is a birth name. Otherwise, label it as an - # "Also Known As (AKA)". GEDCOM does not seem to have the concept - # of different name types - - if state.name_cnt == 0: - name.set_type(NameType.BIRTH) - state.person.set_primary_name(name) - else: - name.set_type(NameType.AKA) - state.person.add_alternate_name(name) - state.name_cnt += 1 - - # Create a new state, and parse the remainder of the NAME level - sub_state = CurrentState() - sub_state.person = state.person - sub_state.name = name - sub_state.level = state.level + 1 - - self.__parse_level(sub_state, self.name_parse_tbl, self.__undefined) - state.msg += sub_state.msg - - def __person_sex(self, line, state): - """ - Parses the SEX line of a GEDCOM file. It has the format of: - - +1 SEX {0:1} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.person.set_gender(line.data) - - def __person_even(self, line, state): - """ - Parses the custom EVEN tag, which has the format of: - - n <> {1:1} - +1 <> {0:1} p.* - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - event_ref = self.__build_event_pair( - state, EventType.CUSTOM, self.event_parse_tbl, line.data - ) - state.person.add_event_ref(event_ref) - - def __person_std_event(self, line, state): - """ - Parses GEDCOM event types that map to a Gramps standard type. - Additional parsing required is for the event detail: - - +1 <> {0:1} p.* - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - - event = line.data - event.set_gramps_id(self.emapper.find_next()) - event_ref = EventRef() - self.dbase.add_event(event, self.trans) - - sub_state = CurrentState() - sub_state.person = state.person - sub_state.level = state.level + 1 - sub_state.event = event - sub_state.event_ref = event_ref - sub_state.pf = self.place_parser - - self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) - state.msg += sub_state.msg - - self.__add_place(event, sub_state) - - self.dbase.commit_event(event, self.trans) - event_ref.ref = event.handle - state.person.add_event_ref(event_ref) - - def __person_reli(self, line, state): - """ - Parses the RELI tag. - - n RELI [Y|] {1:1} - +1 <> {0:1} p.* - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - event_ref = self.__build_event_pair( - state, EventType.RELIGION, self.event_parse_tbl, line.data - ) - state.person.add_event_ref(event_ref) - - def __person_birt(self, line, state): - """ - Parses GEDCOM BIRT tag into a Gramps birth event. Additional work - must be done, since additional handling must be done by Gramps to set - this up as a birth reference event. - - n BIRT [Y|] {1:1} - +1 <> {0:1} p.* - +1 FAMC @@ {0:1} p.* - - I'm not sure what value the FAMC actually offers here, since - the FAMC record should handle this. Why it is a valid sub value - is beyond me. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - event_ref = self.__build_event_pair( - state, EventType.BIRTH, self.event_parse_tbl, line.data - ) - if state.person.get_birth_ref(): - state.person.add_event_ref(event_ref) - else: - state.person.set_birth_ref(event_ref) - - def __person_adop(self, line, state): - """ - Parses GEDCOM ADOP tag, subordinate to the INDI tag. Additinal tags - are needed by the tag, so we pass a different function map. - - n ADOP [Y|] {1:1} - +1 <> {0:1} p.* - +1 FAMC @@ {0:1} p.* - +2 ADOP {0:1} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - event_ref = self.__build_event_pair( - state, EventType.ADOPT, self.adopt_parse_tbl, line.data - ) - state.person.add_event_ref(event_ref) - - def __person_deat(self, line, state): - """ - Parses GEDCOM DEAT tag into a Gramps birth event. Additional work - must be done, since additional handling must be done by Gramps to set - this up as a death reference event. - - n DEAT [Y|] {1:1} - +1 <> {0:1} p.* - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - event_ref = self.__build_event_pair( - state, EventType.DEATH, self.event_parse_tbl, line.data - ) - if state.person.get_death_ref(): - state.person.add_event_ref(event_ref) - else: - state.person.set_death_ref(event_ref) - - def __person_note(self, line, state): - """ - Parses a note associated with the person - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.person, state) - - def __person_rnote(self, line, state): - """ - Parses a note associated with the person - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.person, state) - - def __person_addr(self, line, state): - """ - Parses the INDIvidual - - n ADDR {0:1} - +1 CONT {0:M} - +1 ADR1 {0:1} (Street) - +1 ADR2 {0:1} (Locality) - +1 CITY {0:1} - +1 STAE {0:1} - +1 POST {0:1} - +1 CTRY {0:1} - n PHON {0:3} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if self.is_ftw: - self.__person_resi(line, state) - return - free_form = line.data - - sub_state = CurrentState(level=state.level + 1) - sub_state.addr = Address() - - self.__parse_level(sub_state, self.parse_addr_tbl, self.__ignore) - state.msg += sub_state.msg - - self.__merge_address(free_form, sub_state.addr, line, state) - state.person.add_address(sub_state.addr) - - def __person_resi(self, line, state): - """ - Parses GEDCOM ADDR tag, subordinate to the INDI tag, when sourced by - FTM. We treat this as a RESI event, because FTM puts standard event - details below the ADDR line. - - n ADDR {0:1} - +1 <> {0:1} p.* - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.backoff = True # reprocess the current ADDR line - line.level += 1 # as if it was next level down - event_ref = self.__build_event_pair( - state, EventType.RESIDENCE, self.event_parse_tbl, "" - ) - state.person.add_event_ref(event_ref) - - def __person_phon(self, line, state): - """ - n PHON {0:3} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - url = Url() - url.set_path(line.data) - url.set_type(UrlType(_("Phone"))) - state.person.add_url(url) - - def __person_fax(self, line, state): - """ - O INDI - 1 FAX {0:3} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - url = Url() - url.set_path(line.data) - url.set_type(UrlType(_("FAX"))) - state.person.add_url(url) - - def __person_email(self, line, state): - """ - O INDI - 1 EMAIL {0:3} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - url = Url() - url.set_path(line.data) - url.set_type(UrlType(UrlType.EMAIL)) - state.person.add_url(url) - - def __person_www(self, line, state): - """ - O INDI - 1 WWW {0:3} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - url = Url() - url.set_path(line.data) - url.set_type(UrlType(UrlType.WEB_HOME)) - state.person.add_url(url) - - def __person_titl(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - event = Event() - event_ref = EventRef() - event.set_gramps_id(self.emapper.find_next()) - event.set_type(EventType.NOB_TITLE) - event.set_description(line.data) - - sub_state = CurrentState() - sub_state.person = state.person - sub_state.level = state.level + 1 - sub_state.event = event - sub_state.event_ref = event_ref - sub_state.pf = self.place_parser - - self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) - state.msg += sub_state.msg - - self.__add_place(event, sub_state) - - self.dbase.add_event(event, self.trans) - event_ref.ref = event.handle - state.person.add_event_ref(event_ref) - - def __person_attr_plac(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if state.attr.get_value() == "": - state.attr.set_value(line.data) - - def __name_type(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if line.data.upper() in ("_OTHN", "_AKA", "AKA", "AKAN"): - state.name.set_type(NameType.AKA) - elif line.data.upper() in ("_MAR", "_MARN", "_MARNM", "MARRIED"): - state.name.set_type(NameType.MARRIED) - else: - state.name.set_type((NameType.CUSTOM, line.data)) - - def __name_date(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if state.name: - state.name.set_date_object(line.data) - - def __name_note(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.name, state) - - def __name_alia(self, line, state): - """ - This parses the illegal (ALIA or ALIAS) or non-standard (_ALIA) GEDCOM - tag as a subsidiary of the NAME tag. - - n @XREF:INDI@ INDI {1:1} - +1 NAME {1:1} - +2 NPFX {0:1} - +2 GIVN {0:1} - +2 NICK {0:1} - +2 SPFX {0:1} - +2 SURN {0:1} - +2 NSFX {0:1} - +2 {1:1} - +3 NPFX {0:1} - +3 GIVN {0:1} - +3 NICK {0:1} - +3 SPFX {0:1} - +3 SURN {0:1} - +3 NSFX {0:1} - +3 <> {0:M} - +4 <> {0:M} - +4 <> {0:M} - +3 <> {0:M} - +2 <> {0:M} - +3 <> {0:M} - +3 <> {0:M} - +2 <> {0:M} - - Note that the subsidiary name structure detail will overwrite the ALIA - name (if the same elements are provided in both), so the names should - match. - - "2 _ALIA" is used for example, by PRO-GEN v 3.0a and "2 ALIA" is used - by GTEdit and Brother's keeper 5.2 for windows. It had been supported - in previous versions of Gramps but as it was probably incorrectly coded - as it would only work if the name started with '@'. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_alias_name(line, state) - - def __name_npfx(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.name.set_title(line.data.strip()) - self.__skip_subordinate_levels(state.level + 1, state) - - def __name_givn(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.name.set_first_name(line.data.strip()) - self.__skip_subordinate_levels(state.level + 1, state) - - def __name_spfx(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - spfx = line.data.strip().split(", ")[0] - if state.name.get_surname_list(): - state.name.get_surname_list()[0].set_prefix(spfx) - else: - surn = Surname() - surn.set_prefix(spfx) - surn.set_primary() - state.name.set_surname_list([surn]) - self.__skip_subordinate_levels(state.level + 1, state) - - def __name_surn(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - names = line.data.strip().split(", ") - overwrite = bool(state.name.get_surname_list()) - for name in names: - if overwrite: - state.name.get_surname_list()[0].set_surname(name) - overwrite = False - else: - surn = Surname() - surn.set_surname(name) - surn.set_primary(primary=not state.name.get_surname_list()) - state.name.get_surname_list().append(surn) - self.__skip_subordinate_levels(state.level + 1, state) - - def __name_marnm(self, line, state): - """ - This is non-standard GEDCOM. _MARNM is reported to be used in Ancestral - Quest and Personal Ancestral File 5. This will also handle a usage - which has been found in Brother's Keeper (BROSKEEP VERS 6.1.31 WINDOWS) - as follows: - - 0 @I203@ INDI - 1 NAME John Richard/Doe/ - 2 _MARN Some Other Name - 3 DATE 27 JUN 1817 - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - text = line.data.strip() - data = text.split() - if len(data) == 1: - name = Name(state.person.primary_name) - surn = Surname() - surn.set_surname(data[0].strip()) - surn.set_primary() - name.set_surname_list([surn]) - name.set_type(NameType.MARRIED) - state.person.add_alternate_name(name) - elif len(data) > 1: - name = self.__parse_name_personal(text) - name.set_type(NameType.MARRIED) - state.person.add_alternate_name(name) - - def __name_nsfx(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if state.name.get_suffix() == "" or state.name.get_suffix() == line.data: - # suffix might be set before when parsing name string - state.name.set_suffix(line.data) - else: - # previously set suffix different, to not loose information, append - state.name.set_suffix(state.name.get_suffix() + " " + line.data) - self.__skip_subordinate_levels(state.level + 1, state) - - def __name_nick(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.name.set_nick_name(line.data.strip()) - self.__skip_subordinate_levels(state.level + 1, state) - - def __name_call(self, line, state): - """ - This parses the inofficial _RUFNAME tag that indicates which part - of a person's given name is commonly used to address them. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.name.set_call_name(line.data.strip()) - self.__skip_subordinate_levels(state.level + 1, state) - - def __name_aka(self, line, state): - """ - This parses the non-standard GEDCOM tags _AKA or _AKAN as a subsidiary - to the NAME tag, which is reported to have been found in Ancestral - Quest and Personal Ancestral File 4 and 5. Note: example AQ and PAF - files have separate 2 NICK and 2 _AKA lines for the same person. The - NICK will be stored by Gramps in the nick_name field of the name - structure, while the _AKA, if it is a single word, will be stored in - the NICKNAME attribute. If more than one word it is stored as an AKA - alternate name. - - This will also handle a usage which has been found in in Brother's - Keeper (BROSKEEP VERS 6.1.31 WINDOWS) as follows: - - 0 @I203@ INDI - 1 NAME John Richard/Doe/ - 2 _AKAN Some Other Name - 3 DATE 27 JUN 1817 - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - lname = line.data.split() - name_len = len(lname) - if name_len == 1: - attr = Attribute() - attr.set_type(AttributeType.NICKNAME) - attr.set_value(line.data) - state.person.add_attribute(attr) - elif name_len == 0: - return - else: - name = Name() - surname = Surname() - surname.set_surname(lname[-1].strip()) - surname.set_primary() - name.set_surname_list([surname]) - name.set_first_name(" ".join(lname[0 : name_len - 1])) - # name = self.__parse_name_personal(line.data) - name.set_type(NameType.AKA) - state.person.add_alternate_name(name) - - def __name_adpn(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - text = line.data.strip() - data = text.split() - if len(data) == 1: - name = Name(state.person.primary_name) - surn = Surname() - surn.set_surname(data[0].strip()) - surn.set_primary() - name.set_surname_list([surn]) - name.set_type((NameType.CUSTOM, "Adopted")) - state.person.add_alternate_name(name) - elif len(data) > 1: - name = self.__parse_name_personal(text) - name.set_type((NameType.CUSTOM, "Adopted")) - state.person.add_alternate_name(name) - - def __name_sour(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - citation_handle = self.handle_source(line, state.level, state) - state.name.add_citation(citation_handle) - - def __person_std_attr(self, line, state): - """ - Parses an TOKEN that Gramps recognizes as an Attribute - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - sub_state = CurrentState() - sub_state.person = state.person - sub_state.attr = line.data - sub_state.level = state.level + 1 - state.person.add_attribute(sub_state.attr) - self.__parse_level(sub_state, self.person_attr_parse_tbl, self.__ignore) - state.msg += sub_state.msg - - def __person_fact(self, line, state): - """ - Parses an TOKEN that Gramps recognizes as an Attribute - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - sub_state = CurrentState() - sub_state.person = state.person - sub_state.attr = Attribute() - sub_state.attr.set_value(line.data) - sub_state.level = state.level + 1 - state.person.add_attribute(sub_state.attr) - self.__parse_level(sub_state, self.person_fact_parse_tbl, self.__ignore) - state.msg += sub_state.msg - - def __person_fact_type(self, line, state): - state.attr.set_type(line.data) - - def __person_bapl(self, line, state): - """ - Parses an BAPL TOKEN, producing a Gramps LdsOrd instance - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.build_lds_ord(state, LdsOrd.BAPTISM) - - def __person_conl(self, line, state): - """ - Parses an CONL TOKEN, producing a Gramps LdsOrd instance - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.build_lds_ord(state, LdsOrd.CONFIRMATION) - - def __person_inil(self, line, state): - """ - Parses an INIL TOKEN, producing a Gramps LdsOrd instance - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.build_lds_ord(state, LdsOrd.INITIATORY) - - def __person_endl(self, line, state): - """ - Parses an ENDL TOKEN, producing a Gramps LdsOrd instance - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.build_lds_ord(state, LdsOrd.ENDOWMENT) - - def __person_slgc(self, line, state): - """ - Parses an SLGC TOKEN, producing a Gramps LdsOrd instance - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.build_lds_ord(state, LdsOrd.SEAL_TO_PARENTS) - - def build_lds_ord(self, state, lds_type): - """ - Parses an LDS ordinance, using the type passed to the routine - - @param state: The current state - @type state: CurrentState - @param lds_type: The type of the LDS ordinance - @type line: LdsOrd type - """ - sub_state = CurrentState() - sub_state.level = state.level + 1 - sub_state.lds_ord = LdsOrd() - sub_state.lds_ord.set_type(lds_type) - sub_state.place = None - sub_state.place_fields = PlaceParser() - sub_state.person = state.person - state.person.lds_ord_list.append(sub_state.lds_ord) - - self.__parse_level(sub_state, self.lds_parse_tbl, self.__ignore) - state.msg += sub_state.msg - - if sub_state.place: - place_title = _pd.display(self.dbase, sub_state.place) - sub_state.place_fields.load_place( - self.place_import, sub_state.place, place_title - ) - - def __lds_temple(self, line, state): - """ - Parses the TEMP tag, looking up the code for a match. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - value = self.__extract_temple(line) - if value: - state.lds_ord.set_temple(value) - - def __lds_date(self, line, state): - """ - Parses the DATE tag for the LdsOrd - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.lds_ord.set_date_object(line.data) - - def __lds_famc(self, line, state): - """ - Parses the FAMC tag attached to the LdsOrd - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - gid = self.fid_map[line.data] - state.lds_ord.set_family_handle(self.__find_family_handle(gid)) - - def __lds_form(self, line, state): - """ - Parses the FORM tag thate defines the place structure for a place. - This tag, if found, will override any global place structure. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.pf = PlaceParser(line) - - def __lds_plac(self, line, state): - """ - Parses the PLAC tag attached to the LdsOrd. Create a new place if - needed and set the title. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - try: - title = line.data - place = self.__find_place(title, None, None) - if place is None: - place = Place() - place.set_title(title) - place.name.set_value(title) - self.dbase.add_place(place, self.trans) - self.place_names[place.get_title()].append(place.get_handle()) - else: - pass - state.lds_ord.set_place_handle(place.handle) - except NameError: - return - - def __lds_sour(self, line, state): - """ - Parses the SOUR tag attached to the LdsOrd. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - citation_handle = self.handle_source(line, state.level, state) - state.lds_ord.add_citation(citation_handle) - - def __lds_note(self, line, state): - """ - Parses the NOTE tag attached to the LdsOrd. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.lds_ord, state) - - def __lds_stat(self, line, state): - """ - Parses the STAT (status) tag attached to the LdsOrd. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - status = LDS_STATUS.get(line.data, LdsOrd.STATUS_NONE) - state.lds_ord.set_status(status) - - def __person_famc(self, line, state): - """ - Handles the parsing of the FAMC line, which indicates which family the - person is a child of. - - n FAMC @@ {1:1} - +1 PEDI {0:1} p.* - +1 _FREL {0:1} non-standard Extension - +1 _MREL {0:1} non-standard Extension - +1 <> {0:M} p.* - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - - if not line.data: # handles empty FAMC line - self.__not_recognized(line, state) - return - sub_state = CurrentState() - sub_state.person = state.person - sub_state.level = state.level + 1 - sub_state.ftype = None - sub_state.primary = False - - gid = self.fid_map[line.data] - handle = self.__find_family_handle(gid) - - self.__parse_level(sub_state, self.famc_parse_tbl, self.__undefined) - state.msg += sub_state.msg - - # if the handle is not already in the person's parent family list, we - # need to add it to the list. - - flist = state.person.get_parent_family_handle_list() - if handle not in flist: - state.person.add_parent_family_handle(handle) - - # search childrefs - family, _new = self.dbase.find_family_from_handle(handle, self.trans) - family.set_gramps_id(gid) - - for ref in family.get_child_ref_list(): - if ref.ref == state.person.handle: - break - else: - ref = ChildRef() - ref.ref = state.person.handle - family.add_child_ref(ref) - if sub_state.ftype: - ref.set_mother_relation(sub_state.ftype) - ref.set_father_relation(sub_state.ftype) - else: - if sub_state.frel: - ref.set_father_relation(sub_state.frel) - if sub_state.mrel: - ref.set_mother_relation(sub_state.mrel) - self.dbase.commit_family(family, self.trans) - - def __person_famc_pedi(self, line, state): - """ - Parses the PEDI tag attached to a INDI.FAMC record. No values are set - at this point, because we have to do some post processing. Instead, we - assign the ftype field of the state variable. We convert the text from - the line to an index into the PEDIGREE_TYPES dictionary, which will map - to the correct ChildTypeRef. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.ftype = PEDIGREE_TYPES.get(line.data.lower(), ChildRefType.UNKNOWN) - - def __person_famc_frel(self, line, state): - """ - Parses the _FREL tag attached to a INDI.FAMC record. No values are set - at this point, because we have to do some post processing. Instead, we - assign the frel field of the state variable. We convert the text from - the line to an index into the PEDIGREE_TYPES dictionary, which will map - to the correct ChildTypeRef. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.frel = PEDIGREE_TYPES.get(line.data.lower().strip(), None) - if state.frel is None: - state.frel = ChildRefType(line.data.capitalize().strip()) - - def __person_famc_mrel(self, line, state): - """ - Parses the _MREL tag attached to a INDI.FAMC record. No values are set - at this point, because we have to do some post processing. Instead, we - assign the mrel field of the state variable. We convert the text from - the line to an index into the PEDIGREE_TYPES dictionary, which will map - to the correct ChildTypeRef. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.mrel = PEDIGREE_TYPES.get(line.data.lower().strip(), None) - if state.mrel is None: - state.mrel = ChildRefType(line.data.capitalize().strip()) - - def __person_famc_note(self, line, state): - """ - Parses the INDI.FAMC.NOTE tag . - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.person, state) - - def __person_famc_primary(self, line, state): - """ - Parses the _PRIMARY tag on an INDI.FAMC tag. This value is stored in - the state record to be used later. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.primary = True - - def __person_famc_sour(self, line, state): - """ - Parses the SOUR tag on an INDI.FAMC tag. Gramps has no corresponding - record on its family relationship, so we add the source to the Person - record. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - citation_handle = self.handle_source(line, state.level, state) - state.person.add_citation(citation_handle) - - def __person_fams(self, line, state): - """ - Parses the INDI.FAMS record, which indicates the family in which the - person is a spouse. - - n FAMS @@ {1:1} p.* - +1 <> {0:M} p.* - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - gid = self.fid_map[line.data] - handle = self.__find_family_handle(gid) - state.person.add_family_handle(handle) - - sub_state = CurrentState(level=state.level + 1) - sub_state.obj = state.person - self.__parse_level(sub_state, self.opt_note_tbl, self.__ignore) - state.msg += sub_state.msg - - def __person_asso(self, line, state): - """ - Parse the ASSO tag, add the referenced person to the person we - are currently parsing. The GEDCOM spec indicates that valid ASSO tag - is: - - n ASSO @@ {0:M} - - And the sub tags are: - - ASSOCIATION_STRUCTURE:= - +1 RELA {1:1} - +1 <> {0:M} - +1 <> {0:M} - - The Gedcom spec notes that the ASSOCIATION_STRUCTURE - can only link to an INDIVIDUAL_RECORD - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - - # find the id and person that we are referencing - handle = self.__find_person_handle(self.pid_map[line.data]) - - # create a new PersonRef, and assign the handle, add the - # PersonRef to the active person - - sub_state = CurrentState() - sub_state.person = state.person - sub_state.level = state.level + 1 - sub_state.ref = PersonRef() - sub_state.ref.ref = handle - sub_state.ignore = False - - self.__parse_level(sub_state, self.asso_parse_tbl, self.__ignore) - state.msg += sub_state.msg - if not sub_state.ignore: - state.person.add_person_ref(sub_state.ref) - - def __person_asso_rela(self, line, state): - """ - Parses the INDI.ASSO.RELA tag. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.ref.rel = line.data - - def __person_asso_sour(self, line, state): - """ - Parses the INDI.ASSO.SOUR tag. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.ref.add_citation(self.handle_source(line, state.level, state)) - - def __person_asso_note(self, line, state): - """ - Parses the INDI.ASSO.NOTE tag. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.ref, state) - - # ------------------------------------------------------------------- - # - # FAM parsing - # - # ------------------------------------------------------------------- - - def __parse_fam(self, line): - """ - n @@ FAM {1:1} - +1 <> {0:M} - +1 HUSB @@ {0:1} - +1 WIFE @@ {0:1} - +1 CHIL @@ {0:M} - +1 NCHI {0:1} - +1 SUBM @@ {0:M} - +1 <> {0:M} - +1 <> {0:M} - +1 <> {0:M} - +1 <> {0:M} - +1 REFN {0:M} - +1 RIN {0:1} - +1 <> {0:1} - """ - # create a family - - gid = self.fid_map[line.token_text] - family = self.__find_or_create_family(gid) - - # parse the family - - state = CurrentState(level=1) - state.family = family - - self.__parse_level(state, self.family_func, self.__family_even) - - # handle addresses attached to families - if state.addr is not None: - father_handle = family.get_father_handle() - father = self.dbase.get_person_from_handle(father_handle) - if father: - father.add_address(state.addr) - self.dbase.commit_person(father, self.trans) - mother_handle = family.get_mother_handle() - mother = self.dbase.get_person_from_handle(mother_handle) - if mother: - mother.add_address(state.addr) - self.dbase.commit_person(mother, self.trans) - - for child_ref in family.get_child_ref_list(): - child_handle = child_ref.ref - child = self.dbase.get_person_from_handle(child_handle) - if child: - child.add_address(state.addr) - self.dbase.commit_person(child, self.trans) - - # add default reference if no reference exists - self.__add_default_source(family) - - # Add a default tag if provided - self.__add_default_tag(family) - - self.__check_msgs( - _("FAM (family) Gramps ID %s") % family.get_gramps_id(), state, family - ) - # commit family to database - self.dbase.commit_family(family, self.trans, family.change) - - def __family_husb(self, line, state): - """ - Parses the husband line of a family - - n HUSB @@ {0:1} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - handle = self.__find_person_handle(self.pid_map[line.data]) - state.family.set_father_handle(handle) - - def __family_wife(self, line, state): - """ - Parses the wife line of a family - - n WIFE @@ {0:1} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - handle = self.__find_person_handle(self.pid_map[line.data]) - state.family.set_mother_handle(handle) - - def __family_std_event(self, line, state): - """ - Parses GEDCOM event types that map to a Gramps standard type. - Additional parsing required is for the event detail: - - +1 <> {0:1} p.* - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - event = line.data - event.set_gramps_id(self.emapper.find_next()) - event_ref = EventRef() - event_ref.set_role(EventRoleType.FAMILY) - self.dbase.add_event(event, self.trans) - - sub_state = CurrentState() - sub_state.person = state.person - sub_state.level = state.level + 1 - sub_state.event = event - sub_state.event_ref = event_ref - sub_state.pf = self.place_parser - - self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) - state.msg += sub_state.msg - - self.__add_place(event, sub_state) - - if event.type == EventType.MARRIAGE: - descr = event.get_description() - if descr == "Civil Union": - state.family.type.set(FamilyRelType.CIVIL_UNION) - event.set_description("") - elif descr == "Unmarried": - state.family.type.set(FamilyRelType.UNMARRIED) - event.set_description("") - else: - state.family.type.set(FamilyRelType.MARRIED) - if descr == "Y": - event.set_description("") - - self.dbase.commit_event(event, self.trans) - event_ref.ref = event.handle - state.family.add_event_ref(event_ref) - - def __family_even(self, line, state): - """ - Parses GEDCOM event types that map to a Gramps standard type. - Additional parsing required is for the event detail: - - +1 <> {0:1} p.* - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - # We can get here when a tag that is not valid in the family_func - # parse table is encountered. The tag may be of the form "_XXX". We - # try to convert to a friendly name, if fails use the tag itself as - # the TYPE in a custom event - cust_tag = CUSTOMEVENTTAGS.get(line.token_text, line.token_text) - cust_type = EventType((EventType.CUSTOM, cust_tag)) - event = Event() - event_ref = EventRef() - event_ref.set_role(EventRoleType.FAMILY) - event.set_gramps_id(self.emapper.find_next()) - event.set_type(cust_type) - # in case a description ever shows up - if line.data and line.data != "Y": - event.set_description(str(line.data)) - self.dbase.add_event(event, self.trans) - - sub_state = CurrentState() - sub_state.person = state.person - sub_state.level = state.level + 1 - sub_state.event = event - sub_state.event_ref = event_ref - sub_state.pf = self.place_parser - - self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) - state.msg += sub_state.msg - - self.__add_place(event, sub_state) - - self.dbase.commit_event(event, self.trans) - event_ref.ref = event.handle - state.family.add_event_ref(event_ref) - - def __family_chil(self, line, state): - """ - Parses the child line of a family - - n CHIL @@ {0:1} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - sub_state = CurrentState() - sub_state.family = state.family - sub_state.level = state.level + 1 - sub_state.mrel = None - sub_state.frel = None - - self.__parse_level(sub_state, self.family_rel_tbl, self.__ignore) - state.msg += sub_state.msg - - child = self.__find_or_create_person(self.pid_map[line.data]) - - reflist = [ - ref for ref in state.family.get_child_ref_list() if ref.ref == child.handle - ] - - if reflist: # The child has been referenced already - ref = reflist[0] - if sub_state.frel: - ref.set_father_relation(sub_state.frel) - if sub_state.mrel: - ref.set_mother_relation(sub_state.mrel) - # then we will set the order now: - self.set_child_ref_order(state.family, ref) - else: - ref = ChildRef() - ref.ref = child.handle - if sub_state.frel: - ref.set_father_relation(sub_state.frel) - if sub_state.mrel: - ref.set_mother_relation(sub_state.mrel) - state.family.add_child_ref(ref) - - def set_child_ref_order(self, family, child_ref): - """ - Sets the child_ref in family.child_ref_list to be in the position - family.child_ref_count. This reorders the children to be in the - order given in the FAM section. - """ - family.child_ref_list.remove(child_ref) - family.child_ref_list.insert(family._child_ref_count, child_ref) - family._child_ref_count += 1 - - def __family_slgs(self, line, state): - """ - n SLGS {1:1} - +1 STAT {0:1} - +1 DATE {0:1} - +1 TEMP {0:1} - +1 PLAC {0:1} - +1 <> {0:M} - +1 <> {0:M} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - sub_state = CurrentState() - sub_state.level = state.level + 1 - sub_state.lds_ord = LdsOrd() - sub_state.lds_ord.set_type(LdsOrd.SEAL_TO_SPOUSE) - sub_state.place = None - sub_state.family = state.family - sub_state.place_fields = PlaceParser() - state.family.lds_ord_list.append(sub_state.lds_ord) - - self.__parse_level(sub_state, self.lds_parse_tbl, self.__ignore) - state.msg += sub_state.msg - - if sub_state.place: - place_title = _pd.display(self.dbase, sub_state.place) - sub_state.place_fields.load_place( - self.place_import, sub_state.place, place_title - ) - - def __family_source(self, line, state): - """ - n SOUR @@ /* pointer to source record */ {1:1} p.* - +1 PAGE {0:1} p.* - +1 EVEN {0:1} p.* - +1 DATA {0:1} - +1 QUAY {0:1} p.* - +1 <> {0:M} p.*, * - +1 <> {0:M} p.* - - | /* Systems not using source records */ - n SOUR {1:1} p.* - +1 [ CONC | CONT ] {0:M} - +1 TEXT {0:M} p.* - +1 <> {0:M} p.* - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - citation_handle = self.handle_source(line, state.level, state) - state.family.add_citation(citation_handle) - - def __family_object(self, line, state): - """ - +1 <> {0:M} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__obje(line, state, state.family) - - def __family_comm(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - note = line.data - state.family.add_note(note) - self.__skip_subordinate_levels(state.level + 1, state) - - def __family_note(self, line, state): - """ - +1 <> {0:M} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.family, state) - - def __family_chan(self, line, state): - """ - +1 <> {0:1} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_change(line, state.family, state.level + 1, state) - - def __family_attr(self, line, state): - """ - Parses an TOKEN that Gramps recognizes as an Attribute - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - sub_state = CurrentState() - sub_state.person = state.person - sub_state.attr = line.data - sub_state.level = state.level + 1 - state.family.add_attribute(line.data) - self.__parse_level(sub_state, self.person_attr_parse_tbl, self.__ignore) - state.msg += sub_state.msg - - def __family_refn(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__do_refn(line, state, state.family) - - def __family_cust_attr(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - attr = Attribute() - attr.set_type(line.token_text) - attr.set_value(line.data) - state.family.add_attribute(attr) - - def __obje(self, line, state, pri_obj): - """ - Embedded form - - n OBJE @@ {1:1} - +1 _PRIM {0:1} # Indicates primary photo - - Linked form - - n OBJE {1:1} - +1 FORM {1:1} # v5.5 layout - +1 TITL {0:1} - +1 FILE {1:1} # v5.5.1 allows multiple - +2 FORM {1:1} # v5.5.1 layout - +3 MEDI {0:1} # v5.5.1 layout - +1 <> {0:M} - +1 _PRIM {0:1} # Indicates primary photo - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - @param pri_obj: The Primary object to which this is attached - @type state: Person # or Family, or Source etc. - """ - if line.data and line.data[0] == "@": - # Reference to a named multimedia object defined elsewhere - gramps_id = self.oid_map[line.data] - handle = self.__find_media_handle(gramps_id) - # check to see if this is a primary photo - line = self.__chk_subordinate(state.level + 1, state, TOKEN__PRIM) - if line and line.data == "Y": - state.photo = handle - oref = MediaRef() - oref.set_reference_handle(handle) - pri_obj.add_media_reference(oref) - return - # - # The remainder of this code is similar in concept to __parse_obje - # except that it combines references to the same media file by - # comparing path names. If they are the same, then only the first - # is kept. This does mean that if there are different notes etc. on a - # later OBJE, they will be lost. - # - sub_state = CurrentState() - sub_state.form = "" - sub_state.attr = None - sub_state.filename = "" - sub_state.title = "" - sub_state.media = Media() - sub_state.level = state.level + 1 - sub_state.prim = "" - - self.__parse_level(sub_state, self.media_parse_tbl, self.__ignore) - state.msg += sub_state.msg - if sub_state.filename == "": - self.__add_msg(_("Filename omitted"), line, state) - # The following lines are commented out because Gramps is NOT a - # Gedcom validator! - # if sub_state.form == "": - # self.__add_msg(_("Form omitted"), line, state) - - # The following code that detects URL is an older v5.5 usage; the - # modern option is to use the EMAIL tag. - if isinstance(sub_state.form, str) and sub_state.form == "url": - if isinstance(pri_obj, UrlBase): - url = Url() - url.set_path(sub_state.filename) - url.set_description(sub_state.title) - url.set_type(UrlType.WEB_HOME) - pri_obj.add_url(url) - else: # some primary objects (Event) son't have spot for URL - new_note = Note(sub_state.filename) - new_note.set_gramps_id(self.nid_map[""]) - new_note.set_handle(create_id()) - new_note.set_type( - OBJ_NOTETYPE.get(type(pri_obj).__name__, NoteType.GENERAL) - ) - self.dbase.commit_note(new_note, self.trans, new_note.change) - pri_obj.add_note(new_note.get_handle()) - - else: - # to allow import of references to URLs (especially for import from - # geni.com), do not try to find the file if it is blatently a URL - res = urlparse(sub_state.filename) - if sub_state.filename != "" and ( - res.scheme == "" or len(res.scheme) == 1 or res.scheme == "file" - ): - valid, path = self.__find_file(sub_state.filename, self.dir_path) - if not valid: - self.__add_msg( - _("Could not import %s") % sub_state.filename, line, state - ) - else: - path = sub_state.filename - # Multiple references to the same media silently drops the later - # ones, even if title, etc. are different - photo_handle = self.media_map.get(path) - if photo_handle is None: - photo = Media() - photo.set_path(path) - if sub_state.title: - photo.set_description(sub_state.title) - else: - photo.set_description(path.replace("\\", "/")) - full_path = os.path.abspath(path) - # deal with mime types - value = mimetypes.guess_type(full_path) - if value and value[0]: # found from filename - photo.set_mime_type(value[0]) - else: # get from OBJE.FILE.FORM - if "/" in sub_state.form: # already has expanded mime type - photo.set_mime_type(sub_state.form) - else: - value = mimetypes.types_map.get( - "." + sub_state.form, _("unknown") - ) - photo.set_mime_type(value) - if sub_state.attr: - photo.attribute_list.append(sub_state.attr) - self.dbase.add_media(photo, self.trans) - self.media_map[path] = photo.handle - else: - photo = self.dbase.get_media_from_handle(photo_handle) - # copy notes to our media - for note in sub_state.media.get_note_list(): - photo.add_note(note) - self.dbase.commit_media(photo, self.trans) - - if sub_state.prim == "Y": - state.photo = photo.handle - oref = MediaRef() - oref.set_reference_handle(photo.handle) - pri_obj.add_media_reference(oref) - - def __media_ref_form(self, line, state): - """ - +1 FORM {1:1} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.form = line.data.lower() - - def __media_ref_medi(self, line, state): - """ - +1 MEDI {0:1} (Photo, Audio, Book, etc.) - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.attr = Attribute() - mtype = MEDIA_MAP.get(line.data.lower(), (SourceMediaType.CUSTOM, line.data)) - state.attr.set_type(_("Media-Type")) - state.attr.set_value(str(SourceMediaType(mtype))) - - def __media_ref_titl(self, line, state): - """ - +1 TITL {0:1} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.title = line.data - - def __media_ref_file(self, line, state): - """ - +1 FILE {1:1} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if state.filename != "": - self.__add_msg(_("Multiple FILE in a single OBJE ignored"), line, state) - self.__skip_subordinate_levels(state.level + 1, state) - return - state.filename = line.data - - def __media_ref_prim(self, line, state): - """ - +1 _PRIM {0:1} - - Indicates that this OBJE is the primary photo. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.prim = line.data - - def __family_adopt(self, line, state): - """ - n ADOP - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.frel = TYPE_ADOPT - state.mrel = TYPE_ADOPT - - def __family_frel(self, line, state): - """ - The _FREL key is a FTW/FTM specific extension to indicate father/child - relationship. - - n _FREL - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.frel = PEDIGREE_TYPES.get(line.data.strip().lower()) - - def __family_mrel(self, line, state): - """ - The _MREL key is a FTW/FTM specific extension to indicate father/child - relationship. - - n _MREL - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.mrel = PEDIGREE_TYPES.get(line.data.strip().lower()) - - def __family_stat(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.mrel = TYPE_BIRTH - state.frel = TYPE_BIRTH - - def __event_object(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__obje(line, state, state.event) - - def __event_type(self, line, state): - """ - Parses the TYPE line for an event. - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if state.event.get_type().is_custom(): - if line.data in GED_TO_GRAMPS_EVENT: - name = EventType(GED_TO_GRAMPS_EVENT[line.data]) - else: - try: - name = EventType((EventType.CUSTOM, line.data)) - except AttributeError: - name = EventType(EventType.UNKNOWN) - state.event.set_type(name) - else: - try: - if line.data not in GED_TO_GRAMPS_EVENT and line.data[0] != "Y": - state.event.set_description(line.data) - except IndexError: - return - - def __event_date(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.event.set_date_object(line.data) - self.__warn_if_ambiguous_numeric_date(line, state) - - def __event_place(self, line, state): - """ - Parse the place portion of a event. A special case has to be made for - Family Tree Maker, which violates the GEDCOM spec. It uses the PLAC - field to store the description or value associated with the event. - - n PLAC {1:1} - +1 FORM {0:1} - +1 <> {0:M} - +1 <> {0:M} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - - if ( - self.is_ftw - and (state.event.type in FTW_BAD_PLACE) - and not state.event.get_description() - ): - state.event.set_description(line.data) - else: - place = state.place - if place: - # We encounter a PLAC, having previously encountered an ADDR - if state.place.place_type.string != _("Address"): - # We have previously found a PLAC - self.__add_msg(_("A second PLAC ignored"), line, state) - # ignore this second PLAC, and use the old one - else: - # This is the first PLAC - place.set_title(line.data) - place.name.set_value(line.data) - else: - # The first thing we encounter is PLAC - state.place = Place() - place = state.place - place.set_title(line.data) - place.name.set_value(line.data) - - sub_state = CurrentState() - sub_state.place = place - sub_state.level = state.level + 1 - - self.__parse_level(sub_state, self.event_place_map, self.__undefined) - state.msg += sub_state.msg - if sub_state.pf: # if we found local PLAC:FORM - state.pf = sub_state.pf # save to override global value - # merge notes etc into place - state.place.merge(sub_state.place) - - def __event_place_note(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.place, state) - - def __event_place_form(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.pf = PlaceParser(line) - - def __event_place_object(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__obje(line, state, state.place) - - def __event_place_sour(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.place.add_citation(self.handle_source(line, state.level, state)) - - def __place_map(self, line, state): - """ - - n MAP - n+1 LONG - n+1 LATI - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - sub_state = CurrentState() - sub_state.level = state.level + 1 - sub_state.place = state.place - self.__parse_level(sub_state, self.place_map_tbl, self.__undefined) - state.msg += sub_state.msg - state.place = sub_state.place - - def __place_lati(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.place.set_latitude(line.data) - - def __place_long(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.place.set_longitude(line.data) - - def __event_addr(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - free_form = line.data - - sub_state = CurrentState(level=state.level + 1) - sub_state.location = Location() - sub_state.event = state.event - sub_state.place = Place() # temp stash for notes, citations etc - - self.__parse_level(sub_state, self.parse_loc_tbl, self.__undefined) - state.msg += sub_state.msg - - title = self.__merge_address(free_form, sub_state.location, line, state) - - location = sub_state.location - - if self.addr_is_detail and state.place: - # Commit the enclosing place - place = self.__find_place( - state.place.get_title(), None, state.place.get_placeref_list() - ) - if place is None: - place = state.place - self.dbase.add_place(place, self.trans) - self.place_names[place.get_title()].append(place.get_handle()) - else: - place.merge(state.place) - self.dbase.commit_place(place, self.trans) - place_title = _pd.display(self.dbase, place) - state.pf.load_place(self.place_import, place, place_title) - - # Create the Place Details (it is committed with the event) - place_detail = Place() - place_detail.set_name(PlaceName(value=title)) - place_detail.set_title(title) - # For RootsMagic etc. Place Details e.g. address, hospital, ... - place_detail.set_type((PlaceType.CUSTOM, _("Detail"))) - placeref = PlaceRef() - placeref.ref = place.get_handle() - place_detail.set_placeref_list([placeref]) - state.place = place_detail - else: - place = state.place - if place: - # We encounter an ADDR having previously encountered a PLAC - if ( - len(place.get_alternate_locations()) != 0 - and not self.__get_first_loc(place).is_empty() - ): - # We have perviously found an ADDR, or have populated - # location from PLAC title - self.__add_msg( - _("Location already populated; ADDR " "ignored"), line, state - ) - # ignore this second ADDR, and use the old one - else: - # This is the first ADDR - place.add_alternate_locations(location) - else: - # The first thing we encounter is ADDR - state.place = Place() - place = state.place - place.add_alternate_locations(location) - place.set_name(PlaceName(value=title)) - place.set_title(title) - place.set_type((PlaceType.CUSTOM, _("Address"))) - - # merge notes etc into place - state.place.merge(sub_state.place) - - def __add_location(self, place, location): - """ - @param place: A place object we have found or created - @type place: Place - @param location: A location we want to add to this place - @type location: gen.lib.location - """ - for loc in place.get_alternate_locations(): - if loc.is_equivalent(location) == IDENTICAL: - return - place.add_alternate_locations(location) - - def __get_first_loc(self, place): - """ - @param place: A place object - @type place: Place - @return location: the first alternate location if any else None - @type location: gen.lib.location - """ - if len(place.get_alternate_locations()) == 0: - return None - else: - return place.get_alternate_locations()[0] - - def __event_privacy(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.event.set_privacy(True) - - def __event_note(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.event, state) - - def __event_inline_note(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if line.data[0:13] == "Description: ": - state.event.set_description(line.data[13:]) - else: - self.__parse_note(line, state.event, state) - - def __event_source(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.event.add_citation(self.handle_source(line, state.level, state)) - - def __event_rin(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - attr = Attribute() - attr.set_type(line.token_text) - attr.set_value(line.data) - state.event.add_attribute(attr) - - def __event_attr(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.event.add_attribute(line.data) - - def __event_phon(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - attr = Attribute() - attr.set_type(_("Phone")) - attr.set_value(line.data) - state.event.add_attribute(attr) - - def __event_fax(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - attr = Attribute() - attr.set_type(_("FAX")) - attr.set_value(line.data) - state.event.add_attribute(attr) - - def __event_email(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - attr = Attribute() - attr.set_type(_("EMAIL")) - attr.set_value(line.data) - state.event.add_attribute(attr) - - def __event_www(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - attr = Attribute() - attr.set_type(_("WWW")) - attr.set_value(line.data) - state.event.add_attribute(attr) - - def __event_cause(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - attr = Attribute() - attr.set_type(AttributeType.CAUSE) - attr.set_value(line.data) - state.event.add_attribute(attr) - - sub_state = CurrentState() - sub_state.event = state.event - sub_state.level = state.level + 1 - sub_state.attr = attr - - self.__parse_level(sub_state, self.event_cause_tbl, self.__undefined) - state.msg += sub_state.msg - - def __event_cause_source(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.attr.add_citation(self.handle_source(line, state.level, state)) - - def __event_age(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - attr = Attribute() - attr.set_type(AttributeType.AGE) - attr.set_value(line.data) - state.event_ref.add_attribute(attr) - - def __event_husb(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - while True: - line = self.__get_next_line() - if self.__level_is_finished(line, state.level + 1): - break - elif line.token == TOKEN_AGE: - attr = Attribute() - attr.set_type(AttributeType.FATHER_AGE) - attr.set_value(line.data) - state.event_ref.add_attribute(attr) - elif line.token == TOKEN_WIFE: - # wife event can be on same level, if so call it and finish - self.__event_wife(line, state) - break - - def __event_wife(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - while True: - line = self.__get_next_line() - if self.__level_is_finished(line, state.level + 1): - break - elif line.token == TOKEN_AGE: - attr = Attribute() - attr.set_type(AttributeType.MOTHER_AGE) - attr.set_value(line.data) - state.event_ref.add_attribute(attr) - elif line.token == TOKEN_HUSB: - # husband event can be on same level, if so call it and finish - self.__event_husb(line, state) - break - - def __event_agnc(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - attr = Attribute() - attr.set_type(AttributeType.AGENCY) - attr.set_value(line.data) - state.event.add_attribute(attr) - - def __event_time(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if hasattr(state, "event"): - # read in time as attribute of event - attr = Attribute() - attr.set_type(AttributeType.TIME) - attr.set_value(line.data) - state.event.add_attribute(attr) - - def __event_witness(self, line, state): - """ - Parse the witness of an event - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if line.data and line.data[0] == "@": - # n _WITN @@ - # +1 TYPE - assert state.event.handle # event handle is required to be set - wit = self.__find_or_create_person(self.pid_map[line.data]) - event_ref = EventRef() - event_ref.set_reference_handle(state.event.handle) - while True: - line = self.__get_next_line() - if self.__level_is_finished(line, state.level + 1): - break - elif line.token == TOKEN_TYPE: - if line.data == "WITNESS_OF_MARRIAGE": - role = EventRoleType(EventRoleType.WITNESS) - else: - role = EventRoleType((EventRoleType.CUSTOM, line.data)) - event_ref.set_role(role) - wit.add_event_ref(event_ref) - self.dbase.commit_person(wit, self.trans) - else: - # n _WITN - attr = Attribute() - attr.set_type(AttributeType.WITNESS) - attr.set_value(line.data) - state.event.add_attribute(attr) - - def __person_adopt_famc(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - gid = self.fid_map[line.data] - handle = self.__find_family_handle(gid) - family = self.__find_or_create_family(gid) - - sub_state = CurrentState(level=state.level + 1) - sub_state.mrel = TYPE_BIRTH - sub_state.frel = TYPE_BIRTH - - self.__parse_level(sub_state, self.parse_person_adopt, self.__undefined) - state.msg += sub_state.msg - - if ( - int(sub_state.mrel) == ChildRefType.BIRTH - and int(sub_state.frel) == ChildRefType.BIRTH - ): - sub_state.mrel = sub_state.frel = TYPE_ADOPT - - state.person.add_parent_family_handle(handle) - - reflist = [ - ref for ref in family.get_child_ref_list() if ref.ref == state.person.handle - ] - if reflist: - ref = reflist[0] - ref.set_father_relation(sub_state.frel) - ref.set_mother_relation(sub_state.mrel) - else: - ref = ChildRef() - ref.ref = state.person.handle - ref.set_father_relation(sub_state.frel) - ref.set_mother_relation(sub_state.mrel) - family.add_child_ref(ref) - self.dbase.commit_family(family, self.trans) - - def __person_adopt_famc_adopt(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if line.data.strip() == "HUSB": - state.frel = TYPE_ADOPT - elif line.data.strip() == "WIFE": - state.mrel = TYPE_ADOPT - else: - state.mrel = TYPE_ADOPT - state.frel = TYPE_ADOPT - - def __person_birth_famc(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - handle = self.__find_family_handle(self.fid_map[line.data]) - - state.person.add_parent_family_handle(handle) - - frel = mrel = ChildRefType.BIRTH - - family, _new = self.dbase.find_family_from_handle(handle, self.trans) - reflist = [ - ref for ref in family.get_child_ref_list() if ref.ref == state.person.handle - ] - if reflist: - ref = reflist[0] - ref.set_father_relation(frel) - ref.set_mother_relation(mrel) - else: - ref = ChildRef() - ref.ref = state.person.handle - ref.set_father_relation(frel) - ref.set_mother_relation(mrel) - family.add_child_ref(ref) - self.dbase.commit_family(family, self.trans) - - def __address_date(self, line, state): - """ - Parses the DATE line of an ADDR tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.addr.set_date_object(line.data) - - def __address_adr1(self, line, state): - """ - Parses the ADR1 line of an ADDR tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - # The ADDR may already have been parsed by the level above - # assert state.addr.get_street() == "" - if state.addr.get_street() != "": - self.__add_msg(_("Warn: ADDR overwritten"), line, state) - state.addr.set_street(line.data) - - def __address_adr2(self, line, state): - """ - Parses the ADR2 line of an ADDR tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.addr.set_locality(line.data) - - def __address_city(self, line, state): - """ - Parses the CITY line of an ADDR tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.addr.set_city(line.data) - - def __address_state(self, line, state): - """ - Parses the STAE line of an ADDR tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.addr.set_state(line.data) - - def __address_post(self, line, state): - """ - Parses the POST line of an ADDR tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.addr.set_postal_code(line.data) - - def __address_country(self, line, state): - """ - Parses the country line of an ADDR tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.addr.set_country(line.data) - - def __address_sour(self, line, state): - """ - Parses the SOUR line of an ADDR tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.addr.add_citation(self.handle_source(line, state.level, state)) - - def __address_note(self, line, state): - """ - Parses the NOTE line of an ADDR tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.addr, state) - - def __citation_page(self, line, state): - """ - Parses the PAGE line of an SOUR instance tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.citation.set_page(line.data) - - def __citation_date(self, line, state): - """ - Parses the DATE line of an SOUR instance tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.citation.set_date_object(line.data) - - def __citation_data(self, line, state): - """ - Parses the DATA line of an SOUR instance tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - sub_state = CurrentState(level=state.level + 1) - sub_state.citation = state.citation - - self.__parse_level(sub_state, self.citation_data_tbl, self.__undefined) - state.msg += sub_state.msg - - def __citation_data_date(self, line, state): - state.citation.set_date_object(line.data) - - def __source_text(self, line, state): - note = Note() - note.set(line.data) - gramps_id = self.nid_map[""] - note.set_gramps_id(gramps_id) - note.set_type(NoteType.SOURCE_TEXT) - self.dbase.add_note(note, self.trans) - - state.source.add_note(note.get_handle()) - - def __citation_data_text(self, line, state): - note = Note() - note.set(line.data) - gramps_id = self.nid_map[""] - note.set_gramps_id(gramps_id) - note.set_type(NoteType.SOURCE_TEXT) - self.dbase.add_note(note, self.trans) - - state.citation.add_note(note.get_handle()) - - def __citation_link(self, line, state): - """ - Not legal GEDCOM - added to support FTM, converts the _LINK tag to a - note with styled text so link can be followed in reports etc. - """ - note = Note() - tags = StyledTextTag(StyledTextTagType.LINK, line.data, [(0, len(line.data))]) - note.set_styledtext(StyledText(line.data, [tags])) - gramps_id = self.nid_map[""] - note.set_gramps_id(gramps_id) - note.set_type(NoteType.CITATION) - self.dbase.add_note(note, self.trans) - state.citation.add_note(note.get_handle()) - - def __citation__just(self, line, state): - """ - Not legal GEDCOM - added to support FTM, converts the _JUST tag to a - note. This tag represents the Justification for a source. - """ - note = Note() - note.set(line.data) - gramps_id = self.nid_map[""] - note.set_gramps_id(gramps_id) - note.set_type(_("Citation Justification")) - self.dbase.add_note(note, self.trans) - state.citation.add_note(note.get_handle()) - - def __citation__apid(self, line, state): - """ - Not legal GEDCOM - added to support Ancestry.com, converts the - _APID tag to an attribute. This tag identifies the location of - the cited page in the relevant Ancestry.com database. - """ - sattr = SrcAttribute() - sattr.set_type("_APID") - sattr.set_value(line.data) - state.citation.add_attribute(sattr) - - def __citation_data_note(self, line, state): - self.__parse_note(line, state.citation, state) - - def __citation_obje(self, line, state): - """ - Parses the OBJE line of an SOUR instance tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__obje(line, state, state.citation) - - def __citation_refn(self, line, state): - """ - Parses the REFN line of an SOUR instance tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__add_msg(_("REFN ignored"), line, state) - self.__skip_subordinate_levels(state.level + 1, state) - - def __citation_even(self, line, state): - """ - Parses the EVEN line of an SOUR instance tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - sattr = SrcAttribute() - sattr.set_type("EVEN") - sattr.set_value(line.data) - state.citation.add_attribute(sattr) - sub_state = CurrentState(level=state.level + 1) - sub_state.citation = state.citation - - self.__parse_level(sub_state, self.citation_even_tbl, self.__undefined) - state.msg += sub_state.msg - - def __citation_even_role(self, line, state): - """ - Parses the EVEN line of an SOUR instance tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - sattr = SrcAttribute() - sattr.set_type("EVEN:ROLE") - sattr.set_value(line.data) - state.citation.add_attribute(sattr) - - def __citation_quay(self, line, state): - """ - Parses the QUAY line of an SOUR instance tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - try: - val = int(line.data) - except ValueError: - return - # If value is greater than 3, cap at 3 - val = min(val, 3) - if val > 1: - state.citation.set_confidence_level(val + 1) - else: - state.citation.set_confidence_level(val) - - def __citation_note(self, line, state): - """ - Parses the NOTE line of an SOUR instance tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.citation, state) - - # ---------------------------------------------------------------------- - # - # SOUR parsing - # - # ---------------------------------------------------------------------- - - def __parse_source(self, name, level): - """ - n @@ SOUR {1:1} - +1 DATA {0:1} - +2 EVEN {0:M} - +3 DATE {0:1} - +3 PLAC {0:1} - +2 AGNC {0:1} - +2 <> {0:M} - +1 AUTH {0:1} - +1 TITL {0:1} - +1 ABBR {0:1} - +1 PUBL {0:1} - +1 TEXT {0:1} - +1 <> {0:1} - +1 <> {0:M} - +1 <> {0:M} - +1 REFN {0:M} - +2 TYPE {0:1} - +1 RIN {0:1} - +1 <> {0:1} - """ - - state = CurrentState() - state.source = self.__find_or_create_source(self.sid_map[name]) - # SOURce with the given gramps_id had no title - state.source.set_title(_("No title - ID %s") % state.source.get_gramps_id()) - state.level = level - - self.__parse_level(state, self.source_func, self.__undefined) - self.__check_msgs( - _("SOUR (source) Gramps ID %s") % state.source.get_gramps_id(), - state, - state.source, - ) - self.dbase.commit_source(state.source, self.trans, state.source.change) - - def __source_attr(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - sattr = SrcAttribute() - sattr.set_type(line.token_text) - sattr.set_value(line.data) - state.source.add_attribute(sattr) - self.__skip_subordinate_levels(state.level + 1, state) - - def __source_object(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__obje(line, state, state.source) - - def __source_chan(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_change(line, state.source, state.level + 1, state) - - def __source_repo(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if line.data and line.data[0] == "@": - # This deals with the standard GEDCOM - # SOURCE_REPOSITORY_CITATION: = - # n REPO @@ {1:1} - # +1 <> {0:M} - # +1 CALN {0:M} - # +2 MEDI {0:1} - gid = self.rid_map[line.data] - repo = self.__find_or_create_repository(gid) - elif line.data == "": - # This deals with the non-standard GEDCOM format found in Family - # Tree Maker for Windows, Broderbund Software, Banner Blue - # Division: - # SOURCE_REPOSITORY_CITATION: = - # n REPO {1:1} - # +1 <> {0:M} - # +1 CALN {0:M} - # +2 MEDI {0:1} - # - # This format has no repository name. See https://west-https://west- - # penwith.org.uk/misc/ftmged.htm which points out this is - # incorrect - gid = self.rid_map[""] - repo = self.__find_or_create_repository(gid) - self.dbase.commit_repository(repo, self.trans) - else: - # This deals with the non-standard GEDCOM - # SOURCE_REPOSITORY_CITATION: = - # n REPO {1:1} - # +1 <> {0:M} - # +1 CALN {0:M} - # +2 MEDI {0:1} - # This seems to be used by Heredis 8 PC. Heredis is notorious for - # non-standard GEDCOM. - gid = self.repo2id.get(line.data) - if gid is None: - gid = self.rid_map[""] - repo = self.__find_or_create_repository(gid) - self.repo2id[line.data] = repo.get_gramps_id() - repo.set_name(line.data) - self.dbase.commit_repository(repo, self.trans) - - repo_ref = RepoRef() - repo_ref.set_reference_handle(repo.handle) - - sub_state = CurrentState() - sub_state.repo_ref = repo_ref - sub_state.level = state.level + 1 - - self.__parse_level(sub_state, self.repo_ref_tbl, self.__undefined) - state.msg += sub_state.msg - - state.source.add_repo_reference(repo_ref) - - def __repo_ref_call(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.repo_ref.set_call_number(line.data) - # self.__skip_subordinate_levels(state.level + 1, state) - - def __repo_ref_medi(self, line, state): - name = line.data - mtype = MEDIA_MAP.get(name.lower(), (SourceMediaType.CUSTOM, name)) - state.repo_ref.set_media_type(mtype) - - def __repo_ref_note(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.repo_ref, state) - - def __repo_chan(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_change(line, state.repo, state.level + 1, state) - - def __source_abbr(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.source.set_abbreviation(line.data) - - def __source_agnc(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - attr = Attribute() - attr.set_type(AttributeType.AGENCY) - attr.set_value(line.data) - state.source.add_attribute(attr) - - def __source_note(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.source, state) - - def __source_auth(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.source.set_author(line.data) - - def __source_publ(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.source.set_publication_info(line.data) - self.__skip_subordinate_levels(state.level + 1, state) - - def __source_title(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.source.set_title(line.data.replace("\n", " ")) - - def __source_taxt_peri(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if state.source.get_title() == "": - state.source.set_title(line.data.replace("\n", " ")) - - # ---------------------------------------------------------------------- - # - # OBJE parsing - # - # ---------------------------------------------------------------------- - - def __parse_obje(self, line): - """ - n @XREF:OBJE@ OBJE {1:1} # v5.5 layout - +1 FILE {1:1} # de-facto extension - +1 FORM {1:1} - +1 TITL {0:1} - +1 <> {0:M} p.* - +1 BLOB {1:1} # Deprecated, no support - +2 CONT {1:M} - +1 OBJE @@ /* chain */ {0:1} # Deprecated, no support - +1 REFN {0:M} - +2 TYPE {0:1} - +1 RIN {0:1} - +1 <> {0:1} - - n @XREF:OBJE@ OBJE {1:1} # v5.5.1 layout - +1 FILE {1:M} # multi files, no support - +2 FORM {1:1} - +3 TYPE {0:1} - +2 TITL {0:1} - +2 DATE {0:1} # FTM extension - +2 TEXT # FTM extension - +1 REFN {0:M} - +2 TYPE {0:1} - +1 RIN {0:1} - +1 <> {0:M} - +1 <> {0:M} - +1 <> {0:1} - """ - gid = line.token_text.strip() - media = self.__find_or_create_media(self.oid_map[gid]) - - state = CurrentState() - state.media = media - state.level = 1 - - self.__parse_level(state, self.obje_func, self.__undefined) - - if state.media.get_path() == "": - self.__add_msg(_("Filename omitted"), line, state) - # deal with mime types - value = mimetypes.guess_type(state.media.get_path()) - if value and value[0]: # found from filename - state.media.set_mime_type(value[0]) - else: # get from OBJE.FILE.FORM - if "/" in state.form: # already has expanded mime type - state.media.set_mime_type(state.form) - else: - value = mimetypes.types_map.get("." + state.form, _("unknown")) - state.media.set_mime_type(value) - # Add the default reference if no source has found - self.__add_default_source(media) - - # Add a default tag if provided - self.__add_default_tag(media) - - self.__check_msgs( - _("OBJE (multi-media object) Gramps ID %s") % media.get_gramps_id(), - state, - media, - ) - # commit the person to the database - self.dbase.commit_media(media, self.trans, media.change) - - def __obje_form(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.form = line.data.lower().strip() - - def __obje_file(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - # The following checks for the odd "feature" of GEDCOM 5.5.1 that - # allows multiple files to be attached to a single OBJE; not supported - if state.media.get_path() != "": - self.__add_msg(_("Multiple FILE in a single OBJE ignored"), line, state) - self.__skip_subordinate_levels(state.level + 1, state) - return - res = urlparse(line.data) - if line.data != "" and ( - res.scheme == "" or len(res.scheme) == 1 or res.scheme == "file" - ): - file_ok, filename = self.__find_file(line.data, self.dir_path) - if state.form != "url": - # Might not work if FORM doesn't precede FILE - if not file_ok: - self.__add_msg(_("Could not import %s") % line.data, line, state) - path = filename - else: - path = line.data - - state.media.set_path(path) - if not state.media.get_description(): - state.media.set_description(path.replace("\\", "/")) - - def __obje_title(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.media.set_description(line.data) - - # FTM non-standard TEXT in OBJE, treat as note. - def __obje_text(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - new_note = Note(line.data) - new_note.set_gramps_id(self.nid_map[""]) - new_note.set_handle(create_id()) - new_note.set_type(NoteType.MEDIA) - self.dbase.commit_note(new_note, self.trans, new_note.change) - state.media.add_note(new_note.get_handle()) - - # FTM non-standard DATE in OBJE, treat as Media Date. - def __obje_date(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.media.set_date_object(line.data) - - def __obje_note(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.media, state) - - def __obje_sour(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.media.add_citation(self.handle_source(line, state.level, state)) - - def __obje_refn(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__do_refn(line, state, state.media) - - def __obje_type(self, line, state): - """ - +1 FILE {1:M} - +2 FORM {1:1} - +3 TYPE {0:1} # v5.5.1 - - Source_Media_type is one of (Photo, Audio, Book, etc.) - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - attr = Attribute() - mtype = MEDIA_MAP.get(line.data.lower(), (SourceMediaType.CUSTOM, line.data)) - attr.set_type(_("Media-Type")) - attr.set_value(str(SourceMediaType(mtype))) - state.media.attribute_list.append(attr) - - def __obje_rin(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - attr = Attribute() - attr.set_type(line.token_text) # Attribute: RIN - attr.set_value(line.data) - state.media.attribute_list.append(attr) - - def __obje_chan(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_change(line, state.media, state.level + 1, state) - - def __person_attr_type(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if state.attr.get_type() == "": - if line.data in GED_TO_GRAMPS_EVENT: - name = GED_TO_GRAMPS_EVENT[line.data] - else: - name = line.data - state.attr.set_type(name) - else: - self.__ignore(line, state) - - def __person_attr_source(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.attr.add_citation(self.handle_source(line, state.level, state)) - - def __person_attr_place(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - val = line.data - if state.attr.get_value() == "": - state.attr.set_value(val) - self.__skip_subordinate_levels(state.level + 1, state) - else: - self.__ignore(line, state) - - def __person_attr_note(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.attr, state) - - # ---------------------------------------------------------------------- - # - # REPO parsing - # - # ---------------------------------------------------------------------- - - def __parse_repo(self, line): - """ - n @@ REPO {1:1} - +1 NAME {0:1} p.* - +1 <> {0:1} p.* - +1 <> {0:M} p.* - +1 REFN {0:M} p.* - +1 RIN {0:1} p.* - +1 <> {0:1} p. - """ - repo = self.__find_or_create_repository(self.rid_map[line.token_text]) - - state = CurrentState() - state.repo = repo - state.level = 1 - self.__parse_level(state, self.repo_parse_tbl, self.__ignore) - - self.__check_msgs( - _("REPO (repository) Gramps ID %s") % repo.get_gramps_id(), state, repo - ) - self.dbase.commit_repository(repo, self.trans, repo.change) - - def __repo_name(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.repo.set_name(line.data) - - def __repo_note(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.repo, state) - - def __repo_addr(self, line, state): - """ - Parses the REPOsitory and HEADer COPR - - n ADDR {0:1} - +1 CONT {0:M} - +1 ADR1 {0:1} (Street) - +1 ADR2 {0:1} (Locality) - +1 CITY {0:1} - +1 STAE {0:1} - +1 POST {0:1} - +1 CTRY {0:1} - n PHON {0:3} - - Some repositories do not try to break up the address, - instead they put everything on a single line. Try to determine - if this happened, and try to fix it. - """ - free_form = line.data - - sub_state = CurrentState(level=state.level + 1) - sub_state.addr = Address() - - self.__parse_level(sub_state, self.parse_addr_tbl, self.__ignore) - state.msg += sub_state.msg - - self.__merge_address(free_form, sub_state.addr, line, state) - state.repo.add_address(sub_state.addr) - - def __repo_phon(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - address_list = state.repo.get_address_list() - if address_list: - if address_list[0].get_phone(): - self.__add_msg(_("Only one phone number supported"), line, state) - else: - address_list[0].set_phone(line.data) - - def __repo_fax(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - url = Url() - url.set_path(line.data) - url.set_type(UrlType(_("FAX"))) - state.repo.add_url(url) - - def __repo_www(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - url = Url() - url.set_path(line.data) - url.set_type(UrlType(UrlType.WEB_HOME)) - state.repo.add_url(url) - - def __repo_email(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - url = Url() - url.set_path(line.data) - url.set_type(UrlType(UrlType.EMAIL)) - state.repo.add_url(url) - - def __location_adr1(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if not state.location: - state.location = Location() - if state.location.get_street() != "": - self.__add_msg(_("Warn: ADDR overwritten"), line, state) - state.location.set_street(line.data) - - def __location_adr2(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if not state.location: - state.location = Location() - state.location.set_locality(line.data) - - def __location_city(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if not state.location: - state.location = Location() - state.location.set_city(line.data) - - def __location_stae(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if not state.location: - state.location = Location() - state.location.set_state(line.data) - - def __location_post(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if not state.location: - state.location = Location() - state.location.set_postal_code(line.data) - - def __location_ctry(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if not state.location: - state.location = Location() - state.location.set_country(line.data) - - def __location_phone(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if not state.location: - state.location = Location() - state.location.set_phone(line.data) - - def __location_note(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if state.event: - self.__parse_note(line, state.place, state) - else: - # This causes notes below SUBMitter to be ignored - self.__not_recognized(line, state) - - def __optional_note(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.obj, state) - - # ---------------------------------------------------------------------- - # - # HEAD parsing - # - # ---------------------------------------------------------------------- - - def __parse_header(self): - """ - Handling of the lines subordinate to the HEAD GEDCOM tag - - n HEAD {1:1} - +1 SOUR {1:1} - +2 VERS {0:1} - +2 NAME {0:1} - +2 CORP {0:1} - +3 <> {0:1} - +2 DATA {0:1} - +3 DATE {0:1} - +3 COPR {0:1} - +1 DEST {0:1*} - +1 DATE {0:1} - +2 TIME {0:1} - +1 SUBM @@ {1:1} - +1 SUBN @@ {0:1} - +1 FILE {0:1} - +1 COPR {0:1} - +1 GEDC {1:1} - +2 VERS {1:1} - +2 FORM {1:1} - +1 CHAR {1:1} - +2 VERS {0:1} - +1 LANG {0:1} - +1 PLAC {0:1} - +2 FORM {1:1} - +1 NOTE {0:1} - +2 [CONT|CONC] {0:M} - - * NOTE: Submissions to the Family History Department for Ancestral - File submission or for clearing temple ordinances must use a - DESTination of ANSTFILE or TempleReady. - - """ - state = CurrentState(level=1) - self.__parse_level(state, self.head_parse_tbl, self.__undefined) - self.__check_msgs(_("HEAD (header)"), state, None) - - def __header_sour(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if line.data.strip() in ["FTW", "FTM"]: - self.is_ftw = True - # Some software (e.g. RootsMagic (https://files.rootsmagic.com/PAF- - # Book/RootsMagic-for-PAF-Users-Printable.pdf) use the Addr fields for - # 'Place Details (address, hospital, cemetary)' - if line.data.strip().lower() in ["rootsmagic"]: - self.addr_is_detail = True - # We will use the approved system ID as the name of the generating - # software, in case we do not get the name in the proper place - self.genby = line.data - if self.use_def_src: - sattr = SrcAttribute() - sattr.set_type(_("Approved system identification")) - sattr.set_value("%s" % self.genby) - self.def_src.add_attribute(sattr) - sub_state = CurrentState(level=state.level + 1) - self.__parse_level(sub_state, self.header_sour_parse_tbl, self.__undefined) - state.msg += sub_state.msg - # We can't produce the 'Generated by' statement till the end of the - # SOUR level, because the name and version may come in any order - if self.use_def_src: - # feature request 2356: avoid genitive form - sattr = SrcAttribute() - sattr.set_type(_("Generated By")) - sattr.set_value("%s %s" % (self.genby, self.genvers)) - self.def_src.add_attribute(sattr) - - def __header_sour_name(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - # This is where the name of the product that generated the GEDCOM file - # should appear, and this will overwrite the approved system ID, if any - self.genby = line.data - if self.use_def_src: - sattr = SrcAttribute() - sattr.set_type(_("Name of software product")) - sattr.set_value(self.genby) - self.def_src.add_attribute(sattr) - - def __header_sour_vers(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.genvers = line.data - if self.use_def_src: - sattr = SrcAttribute() - sattr.set_type(_("Version number of software product")) - sattr.set_value(self.genvers) - self.def_src.add_attribute(sattr) - - def __header_sour_corp(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - repo = Repository() - sub_state = CurrentState(level=state.level + 1) - sub_state.repo = repo - self.__parse_level(sub_state, self.header_corp_addr, self.__undefined) - state.msg += sub_state.msg - - if self.use_def_src: - repo.set_name(_("Business that produced the product: %s") % line.data) - rtype = RepositoryType() - rtype.set((RepositoryType.CUSTOM, _("GEDCOM data"))) - repo.set_type(rtype) - self.dbase.add_repository(repo, self.trans) - repo_ref = RepoRef() - repo_ref.set_reference_handle(repo.handle) - mtype = SourceMediaType() - mtype.set((SourceMediaType.UNKNOWN, "")) - repo_ref.set_media_type(mtype) - self.def_src.add_repo_reference(repo_ref) - - def __header_sour_data(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if self.use_def_src: - sattr = SrcAttribute() - sattr.set_type(_("Name of source data")) - sattr.set_value(line.data) - self.def_src.add_attribute(sattr) - sub_state = CurrentState(level=state.level + 1) - self.__parse_level(sub_state, self.header_sour_data, self.__undefined) - state.msg += sub_state.msg - - def __header_sour_copr(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if self.use_def_src: - sattr = SrcAttribute() - sattr.set_type(_("Copyright of source data")) - sattr.set_value(line.data) - self.def_src.add_attribute(sattr) - - def __header_sour_date(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if self.use_def_src: - # Because there is a DATE tag, line.data is automatically converted - # to a Date object before getting to this point, so it has to be - # converted back to a string - text_date = str(line.data) - sattr = SrcAttribute() - sattr.set_type(_("Publication date of source data")) - sattr.set_value(text_date) - self.def_src.add_attribute(sattr) - - def __header_file(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if self.use_def_src: - filename = os.path.basename(line.data).split("\\")[-1] - # feature request 2356: avoid genitive form - self.def_src.set_title(_("Import from %s") % filename) - - def __header_copr(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if self.use_def_src: - self.def_src.set_publication_info(line.data) - - def __header_subm(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - - +1 SUBM @@ {1:1} - This should be simply be a cross-reference to the correct Submitter - record. Note that there can be multiple Submitter records, so it is - necessary to remember which one should be applied. - - """ - self.subm = line.data[1:-1] - sub_state = CurrentState(level=state.level + 1) - self.__parse_level(sub_state, self.header_subm, self.__ignore) - state.msg += sub_state.msg - - def __header_subn(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if self.use_def_src: - sattr = SrcAttribute() - sattr.set_type(_("Submission record identifier")) - sattr.set_value(line.token_text) - self.def_src.add_attribute(sattr) - - def __header_lang(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if self.use_def_src: - sattr = SrcAttribute() - sattr.set_type(_("Language of GEDCOM text")) - sattr.set_value(line.data) - self.def_src.add_attribute(sattr) - - def __header_dest(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - - FIXME: This processing does not depend on DEST, so there seems to be - no reason for it to be placed here. Perhaps it is supposed to be after - all the SOUR levels have been processed, but self.genby was only - assigned by the initial SOUR tag, so this could have been done there. - Perhaps, as suggested by the text of the error message, it was - supposed to test whenther the_DEST_ was LEGACY, in which case the - coding is now wrong. - """ - if self.genby.upper() == "LEGACY": - fname = os.path.basename(self.filename) - self.user.warn( - _( - "Import of GEDCOM file %(filename)s with DEST=%(by)s, " - "could cause errors in the resulting database!" - ) - % {"filename": fname, "by": self.genby}, - _("Look for nameless events."), - ) - - def __header_char(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - # +1 CHAR {1:1} - # +2 VERS {0:1} - encoding = line.data - version = "" - while True: - line = self.__get_next_line() - if self.__level_is_finished(line, state.level + 1): - break - elif line.token == TOKEN_VERS: - version = line.data - - if self.use_def_src: - if version == "": - sattr = SrcAttribute() - sattr.set_type(_("Character set")) - sattr.set_value(encoding) - self.def_src.add_attribute(sattr) - else: - sattr = SrcAttribute() - sattr.set_type(_("Character set and version")) - sattr.set_value("%s %s" % (encoding, version)) - self.def_src.add_attribute(sattr) - - def __header_gedc(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - while True: - line = self.__get_next_line() - if self.__level_is_finished(line, state.level + 1): - break - elif line.token == TOKEN_VERS: - if (not line.data) or line.data[0] != "5": - self.__add_msg(_("GEDCOM version not supported"), line, state) - if self.use_def_src: - sattr = SrcAttribute() - sattr.set_type(_("GEDCOM version")) - sattr.set_value(line.data) - self.def_src.add_attribute(sattr) - elif line.token == TOKEN_FORM: - if line.data == "LINEAGE-LINKED": - pass - elif line.data.upper() == "LINEAGE-LINKED": - # Allow Lineage-Linked etc. though it should be in - # uppercase (Note: Gramps is not a validator! prc) - self.__add_msg(_("GEDCOM FORM should be in uppercase"), line, state) - else: - self.__add_msg(_("GEDCOM FORM not supported"), line, state) - if self.use_def_src: - sattr = SrcAttribute() - sattr.set_type(_("GEDCOM form")) - sattr.set_value(line.data) - self.def_src.add_attribute(sattr) - - def __header_plac(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - sub_state = CurrentState(level=state.level + 1) - self.__parse_level(sub_state, self.place_form, self.__undefined) - state.msg += sub_state.msg - - def __place_form(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.place_parser.parse_form(line) - - def __header_date(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - - This processes the , i.e. the date when this - [GEDCOM] transmission was created (as opposed to the date when the - source data that was used to create the transmission was published or - created - - Because there is a DATE tag, line.data is automatically converted to a - Date object before getting to this point, so it has to be converted - back to a string - """ - tx_date = str(line.data) - tx_time = "" - line = self.__get_next_line() - if self.__level_is_finished(line, state.level): - pass - elif line.token == TOKEN_TIME: - tx_time = str(line.data) - - if self.use_def_src: - if tx_time == "": - sattr = SrcAttribute() - sattr.set_type(_("Creation date of GEDCOM")) - sattr.set_value(tx_date) - self.def_src.add_attribute(sattr) - else: - sattr = SrcAttribute() - sattr.set_type(_("Creation date and time of GEDCOM")) - sattr.set_value("%s %s" % (tx_date, tx_time)) - self.def_src.add_attribute(sattr) - - def __header_note(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if self.use_def_src: - self.__parse_note(line, self.def_src, state) - - def __header_subm_name(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if self.use_def_src: - self.def_src.set_author(line.data) - - def __parse_note(self, line, obj, state): - if line.token == TOKEN_RNOTE: - # reference to a named note defined elsewhere - # NOTE_STRUCTURE: = - # n NOTE @@ {1:1} - # +1 SOUR @@ {0:M} # 5.5 only, not in 5.5.1 - handle = self.__find_note_handle(self.nid_map[line.data]) - obj.add_note(handle) - self.note_type_map[handle] = OBJ_NOTETYPE.get( - type(obj).__name__, NoteType.GENERAL - ) - else: - # Embedded note - # NOTE_STRUCTURE: = - # n NOTE [ | ] {1:1} - # +1 [ CONC | CONT ] {0:M} - # +1 SOUR @@ {0:M} - if not line.data: - self.__add_msg(_("Empty note ignored"), line, state) - self.__skip_subordinate_levels(line.level + 1, state) - else: - new_note = Note(line.data) - new_note.set_gramps_id(self.nid_map[""]) - new_note.set_handle(create_id()) - - sub_state = CurrentState(level=state.level + 1) - sub_state.note = new_note - self.__parse_level(sub_state, self.note_parse_tbl, self.__undefined) - state.msg += sub_state.msg - - # Add a default tag if provided - self.__add_default_tag(new_note) - # Set the type of the note - new_note.set_type( - OBJ_NOTETYPE.get(type(obj).__name__, NoteType.GENERAL) - ) - self.dbase.commit_note(new_note, self.trans, new_note.change) - obj.add_note(new_note.get_handle()) - - # ---------------------------------------------------------------------- - # - # NOTE parsing - # - # ---------------------------------------------------------------------- - - def __parse_inline_note(self, line, level): - """ - Handling of lines subordinate to the NOTE GEDCOM tag - - n @@ NOTE {1:1} - +1 [ CONC | CONT] {0:M} - +1 <> {0:M} - +1 REFN {0:M} - +2 TYPE {0:1} - +1 RIN {0:1} - +1 <> {0:1} - """ - state = CurrentState(level=1) - if ( - not line.data - and self.nid_map.clean(line.token_text) not in self.nid_map.map() - ): - self.__add_msg(_("Empty note ignored"), line) - self.__skip_subordinate_levels(level, state) - else: - gid = self.nid_map[line.token_text] - handle = self.__find_note_handle(gid) - new_note = Note(line.data) - new_note.set_handle(handle) - new_note.set_gramps_id(gid) - if handle in self.note_type_map: - new_note.set_type(self.note_type_map[handle]) - sub_state = CurrentState(level=state.level) - sub_state.note = new_note - self.__parse_level(sub_state, self.note_parse_tbl, self.__undefined) - state.msg += sub_state.msg - - self.dbase.commit_note(new_note, self.trans, new_note.change) - self.__check_msgs( - _("NOTE Gramps ID %s") % new_note.get_gramps_id(), state, None - ) - - def __note_chan(self, line, state): - if state.note: - self.__parse_change(line, state.note, state.level + 1, state) - - def __parse_source_reference(self, citation, level, handle, state): - """ - Read the data associated with a SOUR reference. - """ - sub_state = CurrentState(level=level + 1) - sub_state.citation = citation - sub_state.handle = handle - self.__parse_level(sub_state, self.citation_parse_tbl, self.__ignore) - state.msg += sub_state.msg - - def __parse_header_head(self): - """ - Validate that this is a valid GEDCOM file. - """ - line = self.__get_next_line() - if line.token != TOKEN_HEAD: - raise GedcomError("%s is not a GEDCOM file" % self.filename) - - def __parse_submission(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - - Handling of lines subordinate to the level 0 SUMN (Submission) GEDCOM - tag - - n @@ SUBN {1:1] - +1 SUBM @@ {0:1} - +1 FAMF {0:1} - +1 TEMP {0:1} - +1 ANCE {0:1} - +1 DESC {0:1} - +1 ORDI {0:1} - +1 RIN {0:1} - +1 NOTE {0:m} - """ - if not self.use_def_src: - # no place to put data, so call it not recognized - self.__not_recognized(line, state) - return - while True: - line = self.__get_next_line() - msg = "" - if self.__level_is_finished(line, state.level): - break - elif line.token == TOKEN_SUBM: - msg = _("Submission: Submitter") - elif line.token == TOKEN_UNKNOWN and line.token_text == "FAMF": - msg = _("Submission: Family file") - elif line.token == TOKEN_TEMP: - msg = _("Submission: Temple code") - elif line.token == TOKEN_UNKNOWN and line.token_text == "ANCE": - msg = _("Submission: Generations of ancestors") - elif line.token == TOKEN_UNKNOWN and line.token_text == "DESC": - msg = _("Submission: Generations of descendants") - elif line.token == TOKEN_UNKNOWN and line.token_text == "ORDI": - msg = _("Submission: Ordinance process flag") - elif line.token == TOKEN_NOTE or line.token == TOKEN_RNOTE: - self.__parse_note(line, self.def_src, state) - self.dbase.commit_source(self.def_src, self.trans) - else: - self.__not_recognized(line, state) - continue - - if msg != "": - sattr = SrcAttribute() - sattr.set_type(msg) - sattr.set_value(line.data) - self.def_src.add_attribute(sattr) - self.dbase.commit_source(self.def_src, self.trans) - - def handle_source(self, line, level, state): - """ - Handle the specified source, building a source reference to - the object. - """ - citation = Citation() - if line.data and line.data[0] != "@": - title = line.data - handle = self.inline_srcs.get(title, create_id()) - src = Source() - src.handle = handle - src.gramps_id = self.sid_map[""] - self.inline_srcs[title] = handle - else: - src = self.__find_or_create_source(self.sid_map[line.data]) - # We need to set the title to the cross reference identifier of the - # SOURce record, just in case we never find the source record. If - # we didn't find the source record, then the source object would - # have got deleted by Chack and repair because the record is empty. - # If we find the source record, the title is overwritten in - # __source_title. - if not src.title: - src.set_title(line.data) - self.dbase.commit_source(src, self.trans) - self.__parse_source_reference(citation, level, src.handle, state) - citation.set_reference_handle(src.handle) - self.dbase.add_citation(citation, self.trans) - return citation.handle - - def __parse_change(self, line, obj, level, state): - """ - CHANGE_DATE:= - - > n CHAN {1:1} - > +1 DATE {1:1} - > +2 TIME {0:1} - > +1 <> {0:M} - - The Note structure is ignored, since we have nothing - corresponding in Gramps. - - Based on the values calculated, attempt to convert to a valid - change time using time.strptime. If this fails (and it shouldn't - unless the value is meaningless and doesn't conform to the GEDCOM - spec), the value is ignored. - """ - tstr = None - dstr = None - dobj = None - while True: - line = self.__get_next_line() - if self.__level_is_finished(line, level): - break - elif line.token == TOKEN_TIME: - tstr = line.data - elif line.token == TOKEN_DATE: - # Lexer converted already to Date object - dobj = line.data - elif line.token == TOKEN_NOTE or line.token == TOKEN_RNOTE: - self.__ignore(line, state) - else: - self.__not_recognized(line, state) - - # Attempt to convert the values to a valid change time - if dobj: - dstr = "%s %s %s" % (dobj.get_day(), dobj.get_month(), dobj.get_year()) - try: - if tstr: - try: - tstruct = time.strptime( - "%s %s" % (dstr, tstr), "%d %m %Y %H:%M:%S" - ) - except ValueError: - # seconds is optional in GEDCOM - tstruct = time.strptime( - "%s %s" % (dstr, tstr), "%d %m %Y %H:%M" - ) - else: - tstruct = time.strptime(dstr, "%d %m %Y") - val = time.mktime(tstruct) - obj.change = val - except (ValueError, OverflowError): - # parse of time structure failed, so ignore. According to the - # Python manual: "The functions in this [time] module do not - # handle dates and times before the epoch or far in the future. - # The cut-off point in the future is determined by the C - # library; for Unix, it is typically in 2038." If the time is - # too far in the future, this gives OverflowError. - pass - - def __do_refn(self, line, state, obj): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - @param obj: The object to attach the attribute - @type obj: Gramps primary object - """ - attr = Attribute() - attr.set_type(line.token_text) # Atrribute : REFN - attr.set_value(line.data) - # if there is a subsequent TYPE, we add it as a note to the attribute - line = self.__chk_subordinate(state.level + 1, state, TOKEN_TYPE) - if line: - new_note = Note(line.data) - new_note.set_gramps_id(self.nid_map[""]) - new_note.set_handle(create_id()) - new_note.set_type("REFN-TYPE") - self.dbase.commit_note(new_note, self.trans, new_note.change) - attr.add_note(new_note.get_handle()) - obj.attribute_list.append(attr) - - def __build_event_pair(self, state, event_type, event_map, description): - """ - n TYPE {0:1} p.* - n DATE {0:1} p.*/* - n <> {0:1} p.* - n <> {0:1} p.* - n AGE {0:1} p.* - n AGNC {0:1} p.* - n CAUS {0:1} p.* - n <> {0:M} p.* - n <> {0:M} p.*, * - n <> {0:M} p. - """ - event = Event() - event_ref = EventRef() - event.set_gramps_id(self.emapper.find_next()) - event.set_type(event_type) - - if description and description != "Y": - event.set_description(description) - self.dbase.add_event(event, self.trans) - - sub_state = CurrentState() - sub_state.level = state.level + 1 - sub_state.event_ref = event_ref - sub_state.event = event - sub_state.person = state.person - sub_state.pf = self.place_parser - - self.__parse_level(sub_state, event_map, self.__undefined) - if ( - description == "Y" - and event.date.is_empty() - and event.type == EventType.BIRTH - and not event.place - ): - event.set_description(_("No Date Information")) - state.msg += sub_state.msg - - self.__add_place(event, sub_state) - - self.dbase.commit_event(event, self.trans) - - event_ref.set_reference_handle(event.handle) - return event_ref - - def __build_family_event_pair(self, state, event_type, event_map, description): - event = Event() - event_ref = EventRef() - event.set_gramps_id(self.emapper.find_next()) - event.set_type(event_type) - if description and description != "Y": - event.set_description(description) - - self.dbase.add_event(event, self.trans) - - sub_state = CurrentState() - sub_state.family = state.family - sub_state.level = state.level + 1 - sub_state.event = event - sub_state.event_ref = event_ref - sub_state.pf = self.place_parser - - self.__parse_level(sub_state, event_map, self.__undefined) - state.msg += sub_state.msg - - self.__add_place(event, sub_state) - - self.dbase.commit_event(event, self.trans) - event_ref.set_reference_handle(event.handle) - return event_ref - - def __do_photo(self, state): - """ - Choose the primary photo from the list of media present for this - person. Supports FTM _PHOTO. and others _PRIM feature. - 0 INDI - +1 _PHOTO @@ {1:1} - - 0 INDI - +1 OBJE @@ - +2 _PRIM - - 0 INDI - +1 OBJE - +2 FILE primary_photo.jpg - +2 _PRIM - - For the _PHOTO varient, state.photo contains the XREF ('@M1@'). - For the _PRIM varients, state.photo contains the handle. - Since Gramps currently uses the first media in the list as the - primary, find the primary photo if already in the list, if present, - move to beginning. If not present, add at the beginning. - This is run after all of the person processing is complete but before - committing the person. - """ - if state.photo.startswith("@"): - gramps_id = self.oid_map[state.photo] - handle = self.__find_media_handle(gramps_id) - elif state.photo: - handle = state.photo - else: - return - for mref in state.person.media_list: - if handle == mref.ref: - state.person.media_list.remove(mref) - state.person.media_list.insert(0, mref) - return - mref = MediaRef() - mref.set_reference_handle(handle) - state.person.media_list.insert(0, mref) - - def __extract_temple(self, line): - """Determine the LDS Temple from the input line""" - - def get_code(code): - """get the Temple code""" - if TEMPLES.is_valid_code(code): - return code - elif TEMPLES.is_valid_name(code): - return TEMPLES.code(code) - - code = get_code(line.data) - if code: - return code - - # Not sure why we do this. Kind of ugly. - code = get_code(line.data.split()[0]) - if code: - return code - - # Okay we have no clue which temple this is. - # We should tell the user and store it anyway. - self.__add_msg(_("Invalid temple code"), line, None) - return line.data - - def __add_default_source(self, obj): - """ - Add the default source to the object. - """ - if self.use_def_src and len(obj.get_citation_list()) == 0: - citation = Citation() - citation.set_reference_handle(self.def_src.handle) - self.dbase.add_citation(citation, self.trans) - obj.add_citation(citation.handle) - - def __add_default_tag(self, obj): - """ - Add the default tag to the object. - """ - if self.default_tag: - obj.add_tag(self.default_tag.handle) - - def __subm_name(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - state.res.set_name(line.data) - - def __subm_addr(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - free_form = line.data - - sub_state = CurrentState(level=state.level + 1) - sub_state.location = state.res - - self.__parse_level(sub_state, self.parse_loc_tbl, self.__undefined) - state.msg += sub_state.msg - - self.__merge_address(free_form, state.res, line, state) - # Researcher is a sub-type of LocationBase, so get_street and - # set_street which are used in routines called from self.parse_loc_tbl - # work fine. - # Unfortunately, Researcher also has get_address and set_address, so we - # need to copy the street into that. - state.res.set_address(state.res.get_street()) - - def __subm_phon(self, line, state): - """ - n PHON {0:3} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if state.res.get_phone(): - self.__add_msg(_("Only one phone number supported"), line, state) - else: - state.res.set_phone(line.data) - - def __subm_email(self, line, state): - """ - n EMAIL {0:3} - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - # only record the first multiple emails for researcher - if not state.res.get_email(): - state.res.set_email(line.data) - self.__repo_email(line, state) - - -# ------------------------------------------------------------------------- -# -# GedcomStageOne -# -# ------------------------------------------------------------------------- -class GedcomStageOne: - """ - The GedcomStageOne parser scans the file quickly, looking for a few things. - This includes: - - 1. Character set encoding - 2. Number of people and families in the list - 3. Child to family references, since Ancestry.com creates GEDCOM files - without the FAMC references. - """ - - __BAD_UTF16 = _( - "Your GEDCOM file is corrupted. " - "The file appears to be encoded using the UTF16 " - "character set, but is missing the BOM marker." - ) - __EMPTY_GED = _("Your GEDCOM file is empty.") - - @staticmethod - def __is_xref_value(value): - """ - Return True if value is in the form of a XREF value. We assume that - if we have a leading '@' character, then we are okay. - """ - return value and value[0] == "@" - - def __init__(self, ifile): - self.ifile = ifile - self.famc = defaultdict(list) - self.fams = defaultdict(list) - self.enc = "" - self.pcnt = 0 - self.lcnt = 0 - - def __detect_file_decoder(self, input_file): - """ - Detects the file encoding of the file by looking for a BOM - (byte order marker) in the GEDCOM file. If we detect a UTF-16 or - UTF-8-BOM encoded file, we choose appropriate decoders. If no BOM - is detected, we return in UTF-8 mode it is the more modern option; - and anyway it doesn't really matter as we are only looking for GEDCOM - keywords which are only 7-bit ASCII anyway. - In any case, we Always return the file in text mode with transparent - newline (CR, LF, or CRLF). - """ - line = input_file.read(2) - if line == b"\xef\xbb": - input_file.read(1) - self.enc = "utf_8_sig" - return TextIOWrapper( - input_file, encoding="utf_8_sig", errors="replace", newline=None - ) - elif line == b"\xff\xfe" or line == b"\xfe\xff": - self.enc = "UTF16" - input_file.seek(0) - return TextIOWrapper( - input_file, encoding="utf_16", errors="replace", newline=None - ) - elif not line: - raise GedcomError(self.__EMPTY_GED) - elif line == b"\x30\x00" or line == b"\x00\x30": - raise GedcomError(self.__BAD_UTF16) - else: - input_file.seek(0) - return TextIOWrapper( - input_file, encoding="utf-8", errors="replace", newline=None - ) - - def parse(self): - """ - Parse the input file. - """ - current_family_id = "" - - reader = self.__detect_file_decoder(self.ifile) - - for line in reader: - # Scan for a few items, keep counts. Also look for actual CHAR - # Keyword to figure out actual encodeing for non-unicode file types - line = line.strip() - if not line: - continue - self.lcnt += 1 - - try: - data = line.split(None, 3) + [""] - level, key, value = data[:3] - level = int(level) - key = key.strip() - value = value.strip() - except: - continue - - if level == 0 and key[0] == "@": - if value in ("FAM", "FAMILY"): - current_family_id = key.strip()[1:-1] - elif value in ("INDI", "INDIVIDUAL"): - self.pcnt += 1 - elif key in ("HUSB", "HUSBAND", "WIFE") and self.__is_xref_value(value): - self.fams[value[1:-1]].append(current_family_id) - elif key in ("CHIL", "CHILD") and self.__is_xref_value(value): - self.famc[value[1:-1]].append(current_family_id) - elif key == "CHAR" and not self.enc: - assert isinstance(value, str) - self.enc = value - LOG.debug("parse pcnt %d", self.pcnt) - LOG.debug("parse famc %s", dict(self.famc)) - LOG.debug("parse fams %s", dict(self.fams)) - self.ifile = reader # need this to keep python from autoclosing file - - def get_famc_map(self): - """ - Return the Person to Child Family map - """ - return self.famc - - def get_fams_map(self): - """ - Return the Person to Family map (where the person is a spouse) - """ - return self.fams - - def get_encoding(self): - """ - Return the detected encoding - """ - return self.enc.upper() - - def set_encoding(self, enc): - """ - Forces the encoding - """ - assert isinstance(enc, str) - self.enc = enc - - def get_person_count(self): - """ - Return the number of INDI records found - """ - return self.pcnt - - def get_line_count(self): - """ - Return the number of lines in the file - """ - return self.lcnt - - -# ------------------------------------------------------------------------- -# -# make_gedcom_date -# -# ------------------------------------------------------------------------- -def make_gedcom_date(subdate, calendar, mode, quality): - """ - Convert a Gramps date structure into a GEDCOM compatible date. - """ - retval = "" - day, mon, year = subdate[0:3] - mmap, prefix = CALENDAR_MAP.get(calendar, (MONTH, "")) - if year < 0: - year = -year - bce = " B.C." - else: - bce = "" - try: - retval = __build_date_string(day, mon, year, bce, mmap) - except IndexError: - print("Month index error - %d" % mon) - retval = "%d%s" % (year, bce) - if calendar == Date.CAL_SWEDISH: - # If Swedish calendar use ISO for for date and append (swedish) - # to indicate calandar - if year and not mon and not day: - retval = "%i" % (year) - else: - retval = "%i-%02i-%02i" % (year, mon, day) - retval = retval + " (swedish)" - # Skip prefix @#DUNKNOWN@ as it seems - # not used in all other genealogy applications. - # Gramps can handle it on import, but not with (swedish) appended - # to explain what calendar, the unknown refer to - prefix = "" - if prefix: - retval = "%s %s" % (prefix, retval) - if mode in DATE_MODIFIER: - retval = "%s %s" % (DATE_MODIFIER[mode], retval) - if quality in DATE_QUALITY: - retval = "%s %s" % (DATE_QUALITY[quality], retval) - return retval - - -def __build_date_string(day, mon, year, bce, mmap): - """ - Build a date string from the supplied information. - """ - if day == 0: - if mon == 0: - retval = "%d%s" % (year, bce) - elif year == 0: - retval = "(%s)" % mmap[mon] - else: - retval = "%s %d%s" % (mmap[mon], year, bce) - elif mon == 0: - retval = "%d%s" % (year, bce) - elif year == 0: - retval = "(%d %s)" % (day, mmap[mon]) - else: - retval = "%d %s %d%s" % (day, mmap[mon], year, bce) - return retval +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2000-2007 Donald N. Allingham +# Copyright (C) 2009-2010 Gary Burton +# Copyright (C) 2010 Nick Hall +# Copyright (C) 2011 Tim G L Lyons +# Copyright (C) 2016 Paul R. Culley +# Copyright (C) 2024 Doug Blank +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, see . +# + +""" +Import from GEDCOM + +The GEDCOM file format is defined by the GEDCOM 5.5 Specfication, found +at http://www.familysearch.org/GEDCOM/GEDCOM55.EXE + +The basic structure is a line with three attributes: + + + +Because of this structure, it does not lend itself to more traditional +parsing techniques, such as LALR. The LEVEL token implies too much to be +useful in this context. While this makes parsing more difficult, it +does provide one very beneficial feature for GEDCOM: Error recoverability. +GEDCOM is a poorly implemented standard, primarily because it is a poor +standard to begin with. + +Most commercial applications that implement GEDCOM output add custom +extensions, and feel free to violate the existing structure. If one were +cynical, one might believe that the commercial programs were trying to +make it difficult to transfer your data to another application. + +This parser takes a different approach to parsing a GEDCOM file. The first +state, Lexer, reads lines from the file, and does some basic lexical +analysis on each line (actually several lines, since it automatically +combines CONT and CONC tagged lines). Each logical line returned to this +parser contains: + + Level, Token, Token text, Data, and line number. + +The Data field is typically text, but in some cases, it may be a integer +value representing an enumerated type or a Gramps object (in the case of +dates). + +The parser works on the current level. Each context and level has a an +associated table (dictionary) of functions indexed by the corresponding +TOKEN. When a token is found, we index into the table to find the function +associated with the token. If no token is found, a function that skips the +line and all subordinate (lines with a higher number). If a function is +found, then we call that function, which in turn processes the line, and +all tokens at the lower level. + +For example: + +1 BIRT + 2 DATE 1 JAN 2000 + 2 UKNOWN TAG + 3 NOTE DATA + +The function parsing the individual at level 1, would encounter the BIRT tag. +It would look up the BIRT token in the table to see if a function as defined +for this TOKEN, and pass control to this function. This function would then +start parsing level 2. It would encounter the DATE tag, look up the +corresponding function in the level 2 table, and pass control to its +associated function. This function would terminate, and return control back to +the level 2 parser, which would then encounter the "UKNOWN" tag. Since this is +not a valid token, it would not be in the table, and a function that would skip +all lines until the next level 2 token is found (in this case, skipping the +"3 NOTE DATA" line. +""" + +# ------------------------------------------------------------------------- +# +# standard python modules +# +# ------------------------------------------------------------------------- +import os +import re +import time + +# from xml.parsers.expat import ParserCreate +from collections import defaultdict, OrderedDict +import string +import mimetypes +from io import StringIO, TextIOWrapper +from urllib.parse import urlparse + +# ------------------------------------------------------------------------ +# +# Set up logging +# +# ------------------------------------------------------------------------ +import logging + +LOG = logging.getLogger(".libgedcom") + +# ------------------------------------------------------------------------- +# +# Gramps modules +# +# ------------------------------------------------------------------------- +from gramps.gen.const import GRAMPS_LOCALE as glocale + +_ = glocale.translation.gettext +from gramps.gen.errors import GedcomError +from gramps.gen.lib import ( + Address, + Attribute, + AttributeType, + ChildRef, + ChildRefType, + Citation, + Date, + Event, + EventRef, + EventRoleType, + EventType, + Family, + FamilyRelType, + LdsOrd, + Location, + Media, + MediaRef, + Name, + NameType, + Note, + NoteType, + Person, + PersonRef, + Place, + RepoRef, + Repository, + RepositoryType, + Researcher, + Source, + SourceMediaType, + SrcAttribute, + Surname, + Tag, + Url, + UrlType, + PlaceType, + PlaceRef, + PlaceName, +) +from gramps.gen.lib.json_utils import data_to_object, object_to_dict +from gramps.gen.db import DbTxn +from gramps.gen.updatecallback import UpdateCallback +from gramps.gen.utils.file import media_path +from gramps.gen.utils.id import create_id +from gramps.gen.utils.lds import TEMPLES +from gramps.gen.utils.unknown import make_unknown, create_explanation_note +from gramps.gen.datehandler._dateparser import DateParser +from gramps.gen.db.dbconst import EVENT_KEY +from gramps.gen.lib.const import IDENTICAL +from gramps.gen.lib import StyledText, StyledTextTag, StyledTextTagType +from gramps.gen.lib.urlbase import UrlBase +from gramps.plugins.lib.libplaceimport import PlaceImport +from gramps.gen.display.place import displayer as _pd +from gramps.gen.utils.grampslocale import GrampsLocale + +# ------------------------------------------------------------------------- +# +# constants +# +# ------------------------------------------------------------------------- +TOKEN_UNKNOWN = 0 +TOKEN_ABBR = 1 +TOKEN_ADDR = 2 +TOKEN_ADOP = 3 +TOKEN_ADR1 = 4 +TOKEN_ADR2 = 5 +TOKEN_AFN = 6 +TOKEN_IGNORE = 7 +TOKEN_REFN = 8 +TOKEN__AKA = 9 +TOKEN_ALIA = 11 +TOKEN_ANCI = 12 +TOKEN_ASSO = 13 +TOKEN_AUTH = 14 +TOKEN_BAPL = 15 +TOKEN_BIRT = 16 +TOKEN__CAT = 17 +TOKEN_CAUS = 18 +TOKEN_CHAN = 19 +TOKEN_CHAR = 20 +TOKEN_CHIL = 21 +TOKEN_CITY = 22 +TOKEN__COMM = 23 +TOKEN_CONC = 24 +TOKEN_CONT = 25 +TOKEN_COPR = 26 +TOKEN_CORP = 27 +TOKEN_CTRY = 28 +TOKEN_DATA = 29 +TOKEN_DATE = 30 +TOKEN_DEAT = 32 +TOKEN_DESI = 33 +TOKEN_DEST = 34 +TOKEN_ENDL = 35 +TOKEN_EVEN = 36 +TOKEN_FAM = 38 +TOKEN_FAMC = 39 +TOKEN_FAMS = 40 +TOKEN_FILE = 41 +TOKEN_FORM = 42 +TOKEN__FREL = 43 +TOKEN_GEDC = 44 +TOKEN_GIVN = 45 +TOKEN__GODP = 46 +TOKEN_HUSB = 47 +TOKEN_INDI = 48 +TOKEN_LABL = 49 +TOKEN_LANG = 50 +TOKEN__LOC = 51 +TOKEN__MARNM = 52 +TOKEN__MREL = 53 +TOKEN__NAME = 54 +TOKEN_NAME = 55 +TOKEN_NCHI = 56 +TOKEN_NICK = 57 +TOKEN_NOTE = 58 +TOKEN_NPFX = 59 +TOKEN_NSFX = 60 +TOKEN_OBJE = 61 +TOKEN_OFFI = 62 +TOKEN_PAGE = 63 +TOKEN_PEDI = 64 +TOKEN_PERI = 65 +TOKEN_PHON = 66 +TOKEN_PLAC = 67 +TOKEN_POST = 68 +TOKEN__PRIMARY = 69 +TOKEN__PRIV = 70 +TOKEN_PUBL = 71 +TOKEN_QUAY = 72 +TOKEN_RELI = 74 +TOKEN_REPO = 75 +TOKEN_RESI = 76 +TOKEN_RFN = 77 +TOKEN_RIN = 78 +TOKEN__SCHEMA = 79 +TOKEN_SEX = 80 +TOKEN_SLGC = 81 +TOKEN_SLGS = 82 +TOKEN_SOUR = 83 +TOKEN_SPFX = 84 +TOKEN_STAE = 85 +TOKEN__STAT = 86 +TOKEN_STAT = 87 +TOKEN_SUBM = 88 +TOKEN_SUBN = 89 +TOKEN_SURN = 90 +TOKEN_TAXT = 91 +TOKEN_TEMP = 92 +TOKEN_TEXT = 93 +TOKEN_TIME = 94 +TOKEN_TITL = 95 +TOKEN__TODO = 96 +TOKEN_TRLR = 97 +TOKEN_TYPE = 98 +TOKEN__UID = 99 +TOKEN_VERS = 100 +TOKEN_WIFE = 101 +TOKEN__WITN = 102 +TOKEN__WTN = 103 +TOKEN_AGNC = 104 +TOKEN_HEAD = 105 +TOKEN_CALN = 106 +TOKEN_MEDI = 107 +TOKEN_RELA = 108 +TOKEN__LKD = 109 +TOKEN_BLOB = 110 +TOKEN_CONL = 111 +TOKEN_AGE = 112 +TOKEN_RESN = 114 +TOKEN_ID = 115 +TOKEN_GEVENT = 116 +TOKEN_RNOTE = 117 +TOKEN_GATTR = 118 +TOKEN_ATTR = 119 +TOKEN_MAP = 120 +TOKEN_LATI = 121 +TOKEN_LONG = 122 +TOKEN_FACT = 123 +TOKEN_EMAIL = 124 +TOKEN_WWW = 125 +TOKEN_FAX = 126 +TOKEN_ROLE = 127 +TOKEN__MAR = 128 +TOKEN__MARN = 129 +TOKEN__ADPN = 130 +TOKEN__FSFTID = 131 +TOKEN__PHOTO = 132 +TOKEN__LINK = 133 +TOKEN__PRIM = 134 +TOKEN__JUST = 135 +TOKEN__TEXT = 136 +TOKEN__DATE = 137 +TOKEN__APID = 138 +TOKEN__CALLNAME = 139 +TOKEN_INIL = 140 + +TOKENS = { + "_ADPN": TOKEN__ADPN, + "_AKA": TOKEN__AKA, + "_AKAN": TOKEN__AKA, + "_ALIA": TOKEN_ALIA, + "_ANCES_ORDRE": TOKEN_IGNORE, + "_APID": TOKEN__APID, # Ancestry.com database and page id + "_CAT": TOKEN_IGNORE, + "_CHUR": TOKEN_IGNORE, + "_COMM": TOKEN__COMM, + "_DATE": TOKEN__DATE, + "_DATE2": TOKEN_IGNORE, + "_DETAIL": TOKEN_IGNORE, + "_EMAIL": TOKEN_EMAIL, + "_E-MAIL": TOKEN_EMAIL, + "_FREL": TOKEN__FREL, + "_FSFTID": TOKEN__FSFTID, + "_GODP": TOKEN__GODP, + "_ITALIC": TOKEN_IGNORE, + "_JUST": TOKEN__JUST, # FTM Citation Quality Justification + "_LEVEL": TOKEN_IGNORE, + "_LINK": TOKEN__LINK, + "_LKD": TOKEN__LKD, + "_LOC": TOKEN__LOC, + "_MAR": TOKEN__MAR, + "_MARN": TOKEN__MARN, + "_MARNM": TOKEN__MARNM, + "_MASTER": TOKEN_IGNORE, + "_MEDI": TOKEN_MEDI, + "_MREL": TOKEN__MREL, + "_NAME": TOKEN__NAME, + "_PAREN": TOKEN_IGNORE, + "_PHOTO": TOKEN__PHOTO, + "_PLACE": TOKEN_IGNORE, + "_PREF": TOKEN__PRIMARY, + "_PRIM": TOKEN__PRIM, + "_PRIMARY": TOKEN__PRIMARY, + "_PRIV": TOKEN__PRIV, + "_PUBLISHER": TOKEN_IGNORE, + "_RUFNAME": TOKEN__CALLNAME, + "_SCBK": TOKEN_IGNORE, + "_SCHEMA": TOKEN__SCHEMA, + "_SSHOW": TOKEN_IGNORE, + "_STAT": TOKEN__STAT, + "_TEXT": TOKEN__TEXT, + "_TODO": TOKEN__TODO, + "_TYPE": TOKEN_TYPE, + "_UID": TOKEN__UID, + "_URL": TOKEN_WWW, + "_WITN": TOKEN__WITN, + "_WTN": TOKEN__WTN, + "ABBR": TOKEN_ABBR, + "ABBREVIATION": TOKEN_ABBR, + "ADDR": TOKEN_ADDR, + "ADDRESS": TOKEN_ADDR, + "ADDRESS1": TOKEN_ADR1, + "ADDRESS2": TOKEN_ADR2, + "ADOP": TOKEN_ADOP, + "ADOPT": TOKEN_ADOP, + "ADR1": TOKEN_ADR1, + "ADR2": TOKEN_ADR2, + "AFN": TOKEN_AFN, + "AGE": TOKEN_AGE, + "AGENCY": TOKEN_IGNORE, + "AGNC": TOKEN_AGNC, + "AKA": TOKEN__AKA, + "ALIA": TOKEN_ALIA, + "ALIAS": TOKEN_ALIA, + "ANCI": TOKEN_ANCI, + "ASSO": TOKEN_ASSO, + "ASSOCIATES": TOKEN_ASSO, + "AUTH": TOKEN_AUTH, + "AUTHOR": TOKEN_AUTH, + "BAPL": TOKEN_BAPL, + "BAPTISM-LDS": TOKEN_BAPL, + "BIRT": TOKEN_BIRT, + "BIRTH": TOKEN_BIRT, + "BLOB": TOKEN_BLOB, + "CALL_NUMBER": TOKEN_CALN, + "CALN": TOKEN_CALN, + "CAUS": TOKEN_CAUS, + "CAUSE": TOKEN_CAUS, + "CHAN": TOKEN_CHAN, + "CHANGE": TOKEN_CHAN, + "CHAR": TOKEN_CHAR, + "CHARACTER": TOKEN_CHAR, + "CHIL": TOKEN_CHIL, + "CHILD": TOKEN_CHIL, + "CHILDREN_COUNT": TOKEN_NCHI, + "CITY": TOKEN_CITY, + "CONC": TOKEN_CONC, + "CONCATENATION": TOKEN_CONC, + "CONCATENTATE": TOKEN_CONC, + "CONL": TOKEN_CONL, + "CONT": TOKEN_CONT, + "CONTINUATION": TOKEN_CONT, + "CONTINUED": TOKEN_CONT, + "COPR": TOKEN_COPR, + "COPYRIGHT": TOKEN_COPR, + "CORP": TOKEN_CORP, + "CORPORATION": TOKEN_CORP, + "COUNTRY": TOKEN_CTRY, + "CTRY": TOKEN_CTRY, + "DATA": TOKEN_DATA, + "DATE": TOKEN_DATE, + "DEAT": TOKEN_DEAT, + "DEATH": TOKEN_DEAT, + "DESI": TOKEN_DESI, + "DEST": TOKEN_DEST, + "DESTINATION": TOKEN_DEST, + "EMAI": TOKEN_EMAIL, + "EMAIL": TOKEN_EMAIL, + "ENDL": TOKEN_ENDL, + "ENDOWMENT": TOKEN_ENDL, + "EVEN": TOKEN_EVEN, + "EVENT": TOKEN_EVEN, + "FACT": TOKEN_FACT, + "FAM": TOKEN_FAM, + "FAMC": TOKEN_FAMC, + "FAMILY": TOKEN_FAM, + "FAMILY_CHILD": TOKEN_FAMC, + "FAMILY_SPOUSE": TOKEN_FAMS, + "FAMS": TOKEN_FAMS, + "FAX": TOKEN_FAX, + "FILE": TOKEN_FILE, + "FORM": TOKEN_FORM, + "GEDC": TOKEN_GEDC, + "GEDCOM": TOKEN_GEDC, + "GIVEN_NAME": TOKEN_GIVN, + "GIVN": TOKEN_GIVN, + "HEAD": TOKEN_HEAD, + "HEADER": TOKEN_HEAD, + "HUSB": TOKEN_HUSB, + "HUSBAND": TOKEN_HUSB, + "INDI": TOKEN_INDI, + "INDIVIDUAL": TOKEN_INDI, + "INIL": TOKEN_INIL, + "LABEL": TOKEN_LABL, + "LABL": TOKEN_LABL, + "LANG": TOKEN_LANG, + "LATI": TOKEN_LATI, + "LONG": TOKEN_LONG, + "MAP": TOKEN_MAP, + "MEDI": TOKEN_MEDI, + "MEDIA": TOKEN_MEDI, + "NAME": TOKEN_NAME, + "NAME_PREFIX": TOKEN_NPFX, + "NAME_SUFFIX": TOKEN_NSFX, + "NCHI": TOKEN_NCHI, + "NICK": TOKEN_NICK, + "NICKNAME": TOKEN_NICK, + "NOTE": TOKEN_NOTE, + "NPFX": TOKEN_NPFX, + "NSFX": TOKEN_NSFX, + "OBJE": TOKEN_OBJE, + "OBJECT": TOKEN_OBJE, + "OFFI": TOKEN_OFFI, + "PAGE": TOKEN_PAGE, + "PEDI": TOKEN_PEDI, + "PEDIGREE": TOKEN_PEDI, + "PERI": TOKEN_PERI, + "PHON": TOKEN_PHON, + "PHONE": TOKEN_PHON, + "PHONE_NUMBER": TOKEN_PHON, + "PLAC": TOKEN_PLAC, + "PLACE": TOKEN_PLAC, + "POST": TOKEN_POST, + "POSTAL_CODE": TOKEN_POST, + "PUBL": TOKEN_PUBL, + "PUBLICATION": TOKEN_PUBL, + "QUALITY_OF_DATA": TOKEN_QUAY, + "QUAY": TOKEN_QUAY, + "REFERENCE": TOKEN_REFN, + "REFN": TOKEN_REFN, + "RELA": TOKEN_RELA, + "RELI": TOKEN_RELI, + "RELIGION": TOKEN_RELI, + "REPO": TOKEN_REPO, + "REPOSITORY": TOKEN_REPO, + "RESN": TOKEN_RESN, + "RFN": TOKEN_RFN, + "RIN": TOKEN_RIN, + "ROLE": TOKEN_ROLE, + "SCHEMA": TOKEN__SCHEMA, + "SEX": TOKEN_SEX, + "SLGC": TOKEN_SLGC, + "SLGS": TOKEN_SLGS, + "SOUR": TOKEN_SOUR, + "SOURCE": TOKEN_SOUR, + "SPFX": TOKEN_SPFX, + "STAE": TOKEN_STAE, + "STAT": TOKEN_STAT, + "STATE": TOKEN_STAE, + "STATUS": TOKEN_STAT, + "SUBM": TOKEN_SUBM, + "SUBMISSION": TOKEN_SUBN, + "SUBMITTER": TOKEN_SUBM, + "SUBN": TOKEN_SUBN, + "SURN": TOKEN_SURN, + "SURN_PREFIX": TOKEN_SPFX, + "SURNAME": TOKEN_SURN, + "TAXT": TOKEN_TAXT, + "TEMP": TOKEN_TEMP, + "TEMPLE": TOKEN_TEMP, + "TEXT": TOKEN_TEXT, + "TIME": TOKEN_TIME, + "TITL": TOKEN_TITL, + "TITLE": TOKEN_TITL, + "TRAILER": TOKEN_TRLR, + "TRLR": TOKEN_TRLR, + "TYPE": TOKEN_TYPE, + "URL": TOKEN_WWW, + "VERS": TOKEN_VERS, + "VERSION": TOKEN_VERS, + "WIFE": TOKEN_WIFE, + "WWW": TOKEN_WWW, +} + +ADOPT_NONE = 0 +ADOPT_EVENT = 1 +ADOPT_FTW = 2 +ADOPT_LEGACY = 3 +ADOPT_PEDI = 4 +ADOPT_STD = 5 +CONC_OK = 0 +CONC_BROKEN = 1 +ALT_NAME_NONE = 0 +ALT_NAME_STD = 1 +ALT_NAME_ALIAS = 2 +ALT_NAME_AKA = 3 +ALT_NAME_EVENT_AKA = 4 +ALT_NAME_UALIAS = 5 +CALENDAR_NO = 0 +CALENDAR_YES = 1 +OBJE_NO = 0 +OBJE_YES = 1 +PREFIX_NO = 0 +PREFIX_YES = 1 +RESIDENCE_ADDR = 0 +RESIDENCE_PLAC = 1 +SOURCE_REFS_NO = 0 +SOURCE_REFS_YES = 1 + +TYPE_BIRTH = ChildRefType() +TYPE_ADOPT = ChildRefType(ChildRefType.ADOPTED) +TYPE_FOSTER = ChildRefType(ChildRefType.FOSTER) + +RELATION_TYPES = (ChildRefType.BIRTH, ChildRefType.UNKNOWN, ChildRefType.NONE) + +PEDIGREE_TYPES = { + "birth": ChildRefType(), + "natural": ChildRefType(), + "step": ChildRefType(ChildRefType.STEPCHILD), + "adopted": TYPE_ADOPT, + "foster": TYPE_FOSTER, +} + +FTW_BAD_PLACE = [ + EventType.OCCUPATION, + EventType.RELIGION, + EventType.DEGREE, +] + +MEDIA_MAP = { + "audio": SourceMediaType.AUDIO, + "book": SourceMediaType.BOOK, + "card": SourceMediaType.CARD, + "electronic": SourceMediaType.ELECTRONIC, + "fiche": SourceMediaType.FICHE, + "microfiche": SourceMediaType.FICHE, + "microfilm": SourceMediaType.FICHE, + "film": SourceMediaType.FILM, + "magazine": SourceMediaType.MAGAZINE, + "manuscript": SourceMediaType.MANUSCRIPT, + "map": SourceMediaType.MAP, + "newspaper": SourceMediaType.NEWSPAPER, + "photo": SourceMediaType.PHOTO, + "tombstone": SourceMediaType.TOMBSTONE, + "grave": SourceMediaType.TOMBSTONE, + "video": SourceMediaType.VIDEO, +} + +OBJ_NOTETYPE = { + "Attribute": NoteType.ATTRIBUTE, + "Address": NoteType.ADDRESS, + "Citation": NoteType.CITATION, + "Event": NoteType.EVENT, + "Family": NoteType.FAMILY, + "LdsOrd": NoteType.LDS, + "Media": NoteType.MEDIA, + "Name": NoteType.GENERAL, + "Place": NoteType.PLACE, + "Person": NoteType.PERSON, + "Repository": NoteType.REPO, + "RepoRef": NoteType.REPOREF, + "Source": NoteType.SOURCE, + "PersonRef": NoteType.ASSOCIATION, +} + +# ------------------------------------------------------------------------- +# +# Integer to GEDCOM tag mappings for constants +# +# ------------------------------------------------------------------------- +CALENDAR_MAP_GEDCOM2XML = { + "FRENCH R": Date.CAL_FRENCH, + "JULIAN": Date.CAL_JULIAN, + "HEBREW": Date.CAL_HEBREW, +} + +QUALITY_MAP = { + "CAL": Date.QUAL_CALCULATED, + "INT": Date.QUAL_CALCULATED, + "EST": Date.QUAL_ESTIMATED, +} + +SEX_MAP = { + "F": Person.FEMALE, + "M": Person.MALE, + "X": Person.OTHER, +} + +FAMILYCONSTANTEVENTS = { + EventType.ANNULMENT: "ANUL", + EventType.DIV_FILING: "DIVF", + EventType.DIVORCE: "DIV", + EventType.CENSUS: "CENS", + EventType.ENGAGEMENT: "ENGA", + EventType.MARR_BANNS: "MARB", + EventType.MARR_CONTR: "MARC", + EventType.MARR_LIC: "MARL", + EventType.MARR_SETTL: "MARS", + EventType.MARRIAGE: "MARR", +} + +PERSONALCONSTANTEVENTS = { + EventType.ADOPT: "ADOP", + EventType.ADULT_CHRISTEN: "CHRA", + EventType.BIRTH: "BIRT", + EventType.DEATH: "DEAT", + EventType.BAPTISM: "BAPM", + EventType.BAR_MITZVAH: "BARM", + EventType.BAS_MITZVAH: "BASM", + EventType.BLESS: "BLES", + EventType.BURIAL: "BURI", + # EventType.CAUSE_DEATH : "CAUS", Not legal Gedcom since v5.0 + EventType.ORDINATION: "ORDN", + EventType.CENSUS: "CENS", + EventType.CHRISTEN: "CHR", + EventType.CONFIRMATION: "CONF", + EventType.CREMATION: "CREM", + EventType.DEGREE: "_DEG", + EventType.DIV_FILING: "DIVF", + EventType.EDUCATION: "EDUC", + EventType.ELECTED: "_ELEC", # FTM custom tag + EventType.EMIGRATION: "EMIG", + EventType.FIRST_COMMUN: "FCOM", + EventType.GRADUATION: "GRAD", + EventType.MED_INFO: "_MDCL", + EventType.MILITARY_SERV: "_MILT", + EventType.NATURALIZATION: "NATU", + EventType.NOB_TITLE: "TITL", + EventType.NUM_MARRIAGES: "NMR", + EventType.IMMIGRATION: "IMMI", + EventType.OCCUPATION: "OCCU", + EventType.PROBATE: "PROB", + EventType.PROPERTY: "PROP", + EventType.RELIGION: "RELI", + EventType.RESIDENCE: "RESI", + EventType.RETIREMENT: "RETI", + EventType.WILL: "WILL", +} + +FAMILYCONSTANTATTRIBUTES = { + AttributeType.NUM_CHILD: "NCHI", +} + +PERSONALCONSTANTATTRIBUTES = { + AttributeType.CASTE: "CAST", + AttributeType.DESCRIPTION: "DSCR", + AttributeType.ID: "IDNO", + AttributeType.NATIONAL: "NATI", + AttributeType.NUM_CHILD: "NCHI", + AttributeType.SSN: "SSN", +} + +# ------------------------------------------------------------------------- +# +# Gedcom to int constants +# +# ------------------------------------------------------------------------- +LDS_STATUS = { + "BIC": LdsOrd.STATUS_BIC, + "CANCELED": LdsOrd.STATUS_CANCELED, + "CHILD": LdsOrd.STATUS_CHILD, + "CLEARED": LdsOrd.STATUS_CLEARED, + "COMPLETED": LdsOrd.STATUS_COMPLETED, + "DNS": LdsOrd.STATUS_DNS, + "INFANT": LdsOrd.STATUS_INFANT, + "PRE-1970": LdsOrd.STATUS_PRE_1970, + "QUALIFIED": LdsOrd.STATUS_QUALIFIED, + "DNS/CAN": LdsOrd.STATUS_DNS_CAN, + "STILLBORN": LdsOrd.STATUS_STILLBORN, + "SUBMITTED": LdsOrd.STATUS_SUBMITTED, + "UNCLEARED": LdsOrd.STATUS_UNCLEARED, +} +# ------------------------------------------------------------------------- +# +# Custom event friendly names. These are non-standard GEDCOM "NEW_TAG" +# tags that start with an '_' i.e. "_DNA". FTM has several of these, other +# programs may have more. If a tag with this format is encountered it is +# checked in this table for a "friendly" name translation and thereafter is +# displayed and exported as such. If the tag is NOT in this table and not +# otherwise handled by the code, the tag itself is used for display and +# export. For example "_XYZ" is not in the table and will be displayed as +# "_XYZ" and exported as an EVEN.TYPE=_XYZ +# As Custom entries, they do not appear in Gramps Events add choice unless +# already imported via GEDCOM. +# +# ------------------------------------------------------------------------- +CUSTOMEVENTTAGS = { + "_CIRC": _("Circumcision"), + "_COML": _("Common Law Marriage"), + "_DEST": _("Destination"), + "_DNA": _("DNA"), + "_DCAUSE": _("Cause of Death"), + "_EMPLOY": _("Employment"), + "_EXCM": _("Excommunication"), + "_EYC": _("Eye Color"), + "_FUN": _("Funeral"), + "_HEIG": _("Height"), + "_INIT": _("Initiatory (LDS)"), + "_MILTID": _("Military ID"), + "_MISN": _("Mission (LDS)"), + "_NAMS": _("Namesake"), + "_ORDI": _("Ordinance"), + "_ORIG": _("Origin"), + "_SEPR": _("Separation"), # Applies to Families + "_WEIG": _("Weight"), +} +# table for skipping illegal control chars in GEDCOM import +# Only 09, 0A, 0D are allowed. +STRIP_DICT = dict.fromkeys(list(range(9)) + list(range(11, 13)) + list(range(14, 32))) +# The C1 Control characters are not treated in Latin-1 (ISO-8859-1) as +# undefined, but if they have been used, the file is probably supposed to be +# cp1252 +DEL_AND_C1 = dict.fromkeys(list(range(0x7F, 0x9F))) + +# ------------------------------------------------------------------------- +# +# GEDCOM events to Gramps events conversion +# +# ------------------------------------------------------------------------- +GED_TO_GRAMPS_EVENT = {} +for __val, __key in PERSONALCONSTANTEVENTS.items(): + if __key != "": + GED_TO_GRAMPS_EVENT[__key] = __val +GED_TO_GRAMPS_EVENT["Stillbirth"] = EventType.STILLBIRTH + +for __val, __key in FAMILYCONSTANTEVENTS.items(): + if __key != "": + GED_TO_GRAMPS_EVENT[__key] = __val + +GED_TO_GRAMPS_ATTR = {} +for __val, __key in PERSONALCONSTANTATTRIBUTES.items(): + if __key != "": + GED_TO_GRAMPS_ATTR[__key] = __val + +# ------------------------------------------------------------------------- +# +# GEDCOM Date Constants +# +# ------------------------------------------------------------------------- +HMONTH = [ + "", + "TSH", + "CSH", + "KSL", + "TVT", + "SHV", + "ADR", + "ADS", + "NSN", + "IYR", + "SVN", + "TMZ", + "AAV", + "ELL", +] + +FMONTH = [ + "", + "VEND", + "BRUM", + "FRIM", + "NIVO", + "PLUV", + "VENT", + "GERM", + "FLOR", + "PRAI", + "MESS", + "THER", + "FRUC", + "COMP", +] + +MONTH = [ + "", + "JAN", + "FEB", + "MAR", + "APR", + "MAY", + "JUN", + "JUL", + "AUG", + "SEP", + "OCT", + "NOV", + "DEC", +] + +CALENDAR_MAP = { + Date.CAL_HEBREW: (HMONTH, "@#DHEBREW@"), + Date.CAL_FRENCH: (FMONTH, "@#DFRENCH R@"), + Date.CAL_JULIAN: (MONTH, "@#DJULIAN@"), + Date.CAL_SWEDISH: (MONTH, "@#DUNKNOWN@"), +} + +CALENDAR_MAP_PARSESTRING = { + Date.CAL_HEBREW: " (h)", + Date.CAL_FRENCH: " (f)", + Date.CAL_JULIAN: " (j)", + Date.CAL_SWEDISH: " (s)", +} + +# how wrong calendar use is shown +CALENDAR_MAP_WRONGSTRING = { + Date.CAL_HEBREW: " ", + Date.CAL_FRENCH: " ", + Date.CAL_JULIAN: " ", + Date.CAL_SWEDISH: " ", +} + +DATE_MODIFIER = { + Date.MOD_ABOUT: "ABT", + Date.MOD_BEFORE: "BEF", + Date.MOD_AFTER: "AFT", + Date.MOD_FROM: "FROM", + Date.MOD_TO: "TO", + # Date.MOD_INTERPRETED : "INT", +} + +DATE_QUALITY = { + Date.QUAL_CALCULATED: "CAL", + Date.QUAL_ESTIMATED: "EST", +} + +# ------------------------------------------------------------------------- +# +# regular expressions +# +# ------------------------------------------------------------------------- +NOTE_RE = re.compile(r"\s*\d+\s+\@(\S+)\@\s+NOTE(.*)$") +CONT_RE = re.compile(r"\s*\d+\s+CONT\s?(.*)$") +CONC_RE = re.compile(r"\s*\d+\s+CONC\s?(.*)$") +PERSON_RE = re.compile(r"\s*\d+\s+\@(\S+)\@\s+INDI(.*)$") +MOD = re.compile(r"\s*(INT|EST|CAL)\s+(.*)$") +CAL = re.compile(r"\s*(ABT|BEF|AFT|FROM|TO)?\s*@#D?([^@]+)@\s*(.*)$") +RANGE = re.compile(r"\s*BET\s+@#D?([^@]+)@\s*(.*)\s+AND\s+@#D?([^@]+)@\s*(.*)$") +RANGE1 = re.compile(r"\s*BET\s+\s*(.*)\s+AND\s+@#D?([^@]+)@\s*(.*)$") +RANGE2 = re.compile(r"\s*BET\s+@#D?([^@]+)@\s*(.*)\s+AND\s+\s*(.*)$") +SPAN = re.compile(r"\s*FROM\s+@#D?([^@]+)@\s*(.*)\s+TO\s+@#D?([^@]+)@\s*(.*)$") +SPAN1 = re.compile(r"\s*FROM\s+\s*(.*)\s+TO\s+@#D?([^@]+)@\s*(.*)$") +SPAN2 = re.compile(r"\s*FROM\s+@#D?([^@]+)@\s*(.*)\s+TO\s+\s*(.*)$") +NAME_RE = re.compile(r"/?([^/]*)(/([^/]*)(/([^/]*))?)?") +SURNAME_RE = re.compile(r"/([^/]*)/([^/]*)") +# Bare numeric date in DATE values (e.g. "7/11/1959"). GEDCOM requires +# 3-letter month names; numeric dates are out of spec and unavoidably +# ambiguous between MM/DD/YYYY and DD/MM/YYYY. The parser interprets +# them as MM/DD/YYYY (US format, see GedcomDateParser.dhformat_changed), +# which silently swaps day and month for exports from tools that emit +# DD/MM/YYYY (notably ancestry.com — see Mantis 9298). Used to flag +# such dates with a warning so the user can audit them after import. +BARE_NUMERIC_DATE = re.compile(r"^\s*\d{1,2}/\d{1,2}/\d{2,4}\s*$") + + +# ----------------------------------------------------------------------- +# +# GedcomDateParser +# +# ----------------------------------------------------------------------- +class GedcomDateParser(DateParser): + """Parse the dates""" + + month_to_int = { + "jan": 1, + "feb": 2, + "mar": 3, + "apr": 4, + "may": 5, + "jun": 6, + "jul": 7, + "aug": 8, + "sep": 9, + "oct": 10, + "nov": 11, + "dec": 12, + } + + _locale = GrampsLocale(lang="en_US") # no register_datehandler here + + def dhformat_changed(self): + """Allow overriding so a subclass can modify it""" + self.dhformat = "%m/%d/%y" + + +# ------------------------------------------------------------------------- +# +# Lexer - serves as the lexical analysis engine +# +# ------------------------------------------------------------------------- +class Lexer: + """low level line reading and early parsing""" + + def __init__(self, ifile, __add_msg): + self.ifile = ifile + self.current_list = [] + self.eof = False + self.cnv = None + self.cnt = 0 + self.index = 0 + self.func_map = { + TOKEN_CONT: self.__fix_token_cont, + TOKEN_CONC: self.__fix_token_conc, + } + self.__add_msg = __add_msg + + def readline(self): + """read a line from file with possibility of putting it back""" + if len(self.current_list) <= 1 and not self.eof: + self.__readahead() + try: + return GedLine(self.current_list.pop()) + except: + LOG.debug("Error in reading Gedcom line", exc_info=True) + return None + + def __fix_token_cont(self, data): + line = self.current_list[0] + new_value = line[2] + "\n" + data[2] + self.current_list[0] = (line[0], line[1], new_value, line[3], line[4]) + + def __fix_token_conc(self, data): + line = self.current_list[0] + if len(line[2]) == 4: + # This deals with lines of the form + # 0 @@ NOTE + # 1 CONC + # The previous line contains only a tag and no data so concat a + # space to separate the new line from the tag. This prevents the + # first letter of the new line being lost later + # in _GedcomParse.__parse_record + new_value = line[2] + " " + data[2] + else: + new_value = line[2] + data[2] + self.current_list[0] = (line[0], line[1], new_value, line[3], line[4]) + + def __readahead(self): + while len(self.current_list) < 5: + line = self.ifile.readline() + self.index += 1 + if not line: + self.eof = True + return + + original_line = line + try: + # According to the GEDCOM 5.5 standard, + # Chapter 1 subsection Grammar "leading whitespace preceeding + # a GEDCOM line should be ignored" + # We will also strip the terminator which is any combination + # of carriage_return and line_feed + line = line.lstrip(" ").rstrip("\n\r") + # split into level+delim+rest + line = line.partition(" ") + level = int(line[0]) + # there should only be one space after the level, + # but we can ignore more, + line = line[2].lstrip(" ") + # then split into tag+delim+line_value + # or xfef_id+delim+rest + # the xref_id can have spaces in it + if line.startswith("@"): + line = line.split("@", 2) + # line is now [None, alphanum+pointer_string, rest] + tag = "@" + line[1] + "@" + line_value = line[2].lstrip() + # Ignore meaningless @IDENT@ on CONT or CONC line + # as noted at https://www.tamurajones.net/IdentCONT.xhtml + if line_value.startswith(("CONT ", "CONC ")): + line = line_value.partition(" ") + tag = line[0] + line_value = line[2] + else: + line = line.partition(" ") + tag = line[0] + line_value = line[2] + except: + problem = _("Line %d ignored ") % self.index + text = original_line.rstrip("\n\r") + prob_width = 66 + problem = problem.ljust(prob_width)[0 : (prob_width - 1)] + text = text.replace("\n", "\n".ljust(prob_width + 22)) + message = "%s %s" % (problem, text) + self.__add_msg(message) + continue + + # Need to un-double '@' See Gedcom 5.5 spec 'any_char' + line_value = line_value.replace("@@", "@") + token = TOKENS.get(tag, TOKEN_UNKNOWN) + data = (level, token, line_value, tag, self.index) + + func = self.func_map.get(data[1]) + if func: + func(data) + else: + # There will normally only be one space between tag and + # line_value, but in case there is more then one, remove extra + # spaces after CONC/CONT processing + # Also, Gedcom spec says there should be no spaces at end of + # line, however some programs put them there (FTM), so let's + # leave them in place. + data = data[:2] + (data[2].lstrip(),) + data[3:] + self.current_list.insert(0, data) + + def clean_up(self): + """ + Break circular references to parsing methods stored in dictionaries + to aid garbage collection + """ + for key in list(self.func_map.keys()): + del self.func_map[key] + del self.func_map + + +# ----------------------------------------------------------------------- +# +# GedLine - represents a tokenized version of a GEDCOM line +# +# ----------------------------------------------------------------------- +class GedLine: + """ + GedLine is a class the represents a GEDCOM line. The form of a GEDCOM line + is: + + + + This gets parsed into + + Line Number, Level, Token Value, Token Text, and Data + + Data is dependent on the context the Token Value. For most of tokens, + this is just a text string. However, for certain tokens where we know + the context, we can provide some value. The current parsed tokens are: + + TOKEN_DATE - Date + TOKEN_SEX - Person gender item + TOEKN_UKNOWN - Check to see if this is a known event + """ + + __DATE_CNV = GedcomDateParser() + + @staticmethod + def __extract_date(text): + """ + Converts the specified text to a Date object. + """ + dateobj = Date() + # Horrible hack for importing illegal GEDCOM from Apple Macintosh + # Classic 'Gene' program + text = text.replace("BET ABT", "EST BET") + + # extract out the MOD line + match = MOD.match(text) + mod = "" + if match: + mod, text = match.groups() + qual = QUALITY_MAP.get(mod, Date.QUAL_NONE) + mod += " " + else: + qual = Date.QUAL_NONE + + # parse the range if we match, if so, return + match = RANGE.match(text) + match1 = RANGE1.match(text) + match2 = RANGE2.match(text) + if match or match1 or match2: + if match: + cal1, data1, cal2, data2 = match.groups() + elif match1: + cal1 = Date.CAL_GREGORIAN + data1, cal2, data2 = match1.groups() + elif match2: + cal2 = Date.CAL_GREGORIAN + cal1, data1, data2 = match2.groups() + cal1 = CALENDAR_MAP_GEDCOM2XML.get(cal1, Date.CAL_GREGORIAN) + cal2 = CALENDAR_MAP_GEDCOM2XML.get(cal2, Date.CAL_GREGORIAN) + if cal1 != cal2: + # not supported by GRAMPS, import as text, we construct a string + # that the parser will not parse as a correct date + return GedLine.__DATE_CNV.parse( + "%sbetween %s%s and %s%s" + % ( + mod, + data1, + CALENDAR_MAP_WRONGSTRING.get(cal1, ""), + CALENDAR_MAP_WRONGSTRING.get(cal2, ""), + data2, + ) + ) + + # add hebrew, ... calendar so that months are recognized + data1 += CALENDAR_MAP_PARSESTRING.get(cal1, "") + data2 += CALENDAR_MAP_PARSESTRING.get(cal2, "") + start = GedLine.__DATE_CNV.parse(data1) + stop = GedLine.__DATE_CNV.parse(data2) + dateobj.set( + Date.QUAL_NONE, + Date.MOD_RANGE, + cal1, + start.get_start_date() + stop.get_start_date(), + ) + dateobj.set_quality(qual) + return dateobj + + # parse a span if we match + match = SPAN.match(text) + match1 = SPAN1.match(text) + match2 = SPAN2.match(text) + if match or match1 or match2: + if match: + cal1, data1, cal2, data2 = match.groups() + elif match1: + cal1 = Date.CAL_GREGORIAN + data1, cal2, data2 = match1.groups() + elif match2: + cal2 = Date.CAL_GREGORIAN + cal1, data1, data2 = match2.groups() + cal1 = CALENDAR_MAP_GEDCOM2XML.get(cal1, Date.CAL_GREGORIAN) + cal2 = CALENDAR_MAP_GEDCOM2XML.get(cal2, Date.CAL_GREGORIAN) + if cal1 != cal2: + # not supported by GRAMPS, import as text, we construct a string + # that the parser will not parse as a correct date + return GedLine.__DATE_CNV.parse( + "%sfrom %s%s to %s%s" + % ( + mod, + data1, + CALENDAR_MAP_WRONGSTRING.get(cal1, ""), + CALENDAR_MAP_WRONGSTRING.get(cal2, ""), + data2, + ) + ) + # add hebrew, ... calendar so that months are recognized + data1 += CALENDAR_MAP_PARSESTRING.get(cal1, "") + data2 += CALENDAR_MAP_PARSESTRING.get(cal2, "") + start = GedLine.__DATE_CNV.parse(data1) + stop = GedLine.__DATE_CNV.parse(data2) + dateobj.set( + Date.QUAL_NONE, + Date.MOD_SPAN, + cal1, + start.get_start_date() + stop.get_start_date(), + ) + dateobj.set_quality(qual) + return dateobj + + match = CAL.match(text) + if match: + abt, call, data = match.groups() + call = CALENDAR_MAP_GEDCOM2XML.get(call, Date.CAL_GREGORIAN) + data += CALENDAR_MAP_PARSESTRING.get(call, "") + if abt: + dateobj = GedLine.__DATE_CNV.parse("%s %s" % (abt, data)) + else: + dateobj = GedLine.__DATE_CNV.parse(data) + dateobj.set_quality(qual) + return dateobj + dateobj = GedLine.__DATE_CNV.parse(text) + dateobj.set_quality(qual) + return dateobj + + def __init__(self, data): + """ + If the level is 0, then this is a top level instance. In this case, + we may find items in the form of: + + @ID@ + + If this is not the top level, we check the MAP_DATA array to see if + there is a conversion function for the data. + """ + self.line = data[4] + self.level = data[0] + self.token = data[1] + self.token_text = data[3].strip() + self.data = str(data[2]) + + if self.level == 0: + if ( + self.token_text + and self.token_text[0] == "@" + and self.token_text[-1] == "@" + ): + self.token = TOKEN_ID + self.token_text = self.token_text[1:-1] + self.data = self.data.strip() + else: + func = _MAP_DATA.get(self.token) + if func: + func(self) + + def calc_sex(self): + """ + Converts the data field to a gen.lib token indicating the gender + """ + try: + self.data = SEX_MAP.get(self.data.strip()[0], Person.UNKNOWN) + except: + self.data = Person.UNKNOWN + + def calc_date(self): + """ + Converts the data field to a Date object + """ + self.data = self.__extract_date(self.data) + self.token = TOKEN_DATE + + def calc_unknown(self): + """ + Checks to see if the token maps a known GEDCOM event. If so, we + change the type from UNKNOWN to TOKEN_GEVENT (gedcom event), and + the data is assigned to the associated Gramps EventType + """ + token = GED_TO_GRAMPS_EVENT.get(self.token_text) + if token: + event = Event() + event.set_description(self.data) + event.set_type(token) + self.token = TOKEN_GEVENT + self.data = event + else: + token = GED_TO_GRAMPS_ATTR.get(self.token_text) + if token: + attr = Attribute() + attr.set_value(self.data) + attr.set_type(token) + self.token = TOKEN_ATTR + self.data = attr + + def calc_note(self): + """look for a note xref @N0001@""" + gid = self.data.strip() + if len(gid) > 2 and gid[0] == "@" and gid[-1] == "@": + self.token = TOKEN_RNOTE + self.data = gid[1:-1] + + def calc_nchi(self): + """set attribute for number of children""" + attr = Attribute() + attr.set_value(self.data) + attr.set_type(AttributeType.NUM_CHILD) + self.data = attr + self.token = TOKEN_ATTR + + def calc_attr(self): + """set attribure for general attributes""" + attr = Attribute() + attr.set_value(self.data) + attr.set_type((AttributeType.CUSTOM, self.token_text)) + self.data = attr + self.token = TOKEN_ATTR + + def __repr__(self): + return "%d: %d (%d:%s) %s" % ( + self.line, + self.level, + self.token, + self.token_text, + self.data, + ) + + +_MAP_DATA = { + TOKEN_UNKNOWN: GedLine.calc_unknown, + TOKEN_DATE: GedLine.calc_date, + TOKEN__DATE: GedLine.calc_date, + TOKEN_SEX: GedLine.calc_sex, + TOKEN_NOTE: GedLine.calc_note, + TOKEN_NCHI: GedLine.calc_nchi, + TOKEN__STAT: GedLine.calc_attr, + TOKEN__UID: GedLine.calc_attr, + TOKEN_AFN: GedLine.calc_attr, + TOKEN__FSFTID: GedLine.calc_attr, +} + + +# ------------------------------------------------------------------------- +# +# File Readers +# +# ------------------------------------------------------------------------- +class BaseReader: + """base char level reader""" + + def __init__(self, ifile, encoding, __add_msg): + self.ifile = ifile + self.enc = encoding + self.__add_msg = __add_msg + + def reset(self): + """return to beginning""" + self.ifile.seek(0) + + def readline(self): + """Read a single line""" + raise NotImplementedError() + + def report_error(self, problem, line): + """Create an error message""" + line = line.rstrip("\n\r") + prob_width = 66 + problem = problem.ljust(prob_width)[0 : (prob_width - 1)] + text = line.replace("\n", "\n".ljust(prob_width + 22)) + message = "%s %s" % (problem, text) + self.__add_msg(message) + + +class UTF8Reader(BaseReader): + """The main UTF-8 reader, uses Python for char handling""" + + def __init__(self, ifile, __add_msg, enc): + BaseReader.__init__(self, ifile, enc, __add_msg) + self.reset() + if enc == "UTF_8_SIG": + self.ifile = TextIOWrapper( + ifile, encoding="utf_8_sig", errors="replace", newline=None + ) + else: + self.ifile = TextIOWrapper( + ifile, encoding="utf_8", errors="replace", newline=None + ) + + def readline(self): + line = self.ifile.readline() + return line.translate(STRIP_DICT) + + +class UTF16Reader(BaseReader): + """The main UTF-16 reader, uses Python for char handling""" + + def __init__(self, ifile, __add_msg): + BaseReader.__init__(self, ifile, "UTF16", __add_msg) + self.ifile = TextIOWrapper( + ifile, encoding="utf_16", errors="replace", newline=None + ) + self.reset() + + def readline(self): + line = self.ifile.readline() + return line.translate(STRIP_DICT) + + +class AnsiReader(BaseReader): + """The main ANSI (latin1) reader, uses Python for char handling""" + + def __init__(self, ifile, __add_msg): + BaseReader.__init__(self, ifile, "latin1", __add_msg) + self.ifile = TextIOWrapper( + ifile, encoding="latin1", errors="replace", newline=None + ) + + def readline(self): + line = self.ifile.readline() + if line.translate(DEL_AND_C1) != line: + self.report_error( + "DEL or C1 control chars in line did you mean " "CHAR cp1252??", line + ) + return line.translate(STRIP_DICT) + + +class CP1252Reader(BaseReader): + """The extra credit CP1252 reader, uses Python for char handling""" + + def __init__(self, ifile, __add_msg): + BaseReader.__init__(self, ifile, "cp1252", __add_msg) + self.ifile = TextIOWrapper( + ifile, encoding="cp1252", errors="replace", newline=None + ) + + def readline(self): + line = self.ifile.readline() + return line.translate(STRIP_DICT) + + +class AnselReader(BaseReader): + """ + ANSEL to Unicode Conversion + + ANSEL references: + http://lcweb2.loc.gov/diglib/codetables/45.html + http://www.gymel.com/charsets/ANSEL.html + + list of ANSEL codes that replicate ASCII + note that DEL (127=0x7F) is a control char + Note: spec allows control-chars that Gramps probably doesn't use + but 10=0x0A _is_ needed (!) + --- + Also: there are two additional control chars 0x98,0x9c (unicode same) + which we also ignore for now (start/emd of string (or sort sequence) + --- + ?: should we allow TAB, as a Gramps extension? + """ + + __printable_ascii = list(map(chr, list(range(32, 127)))) # up thru 126 + # LF CR Esc GS RS US + __use_ASCII = list(map(chr, [10, 13, 27, 29, 30, 31])) + __printable_ascii + + # mappings of single byte ANSEL codes to unicode + __onebyte = { + b"\xa1": "\u0141", + b"\xa2": "\u00d8", + b"\xa3": "\u0110", + b"\xa4": "\u00de", + b"\xa5": "\u00c6", + b"\xa6": "\u0152", + b"\xa7": "\u02b9", + b"\xa8": "\u00b7", + b"\xa9": "\u266d", + b"\xaa": "\u00ae", + b"\xab": "\u00b1", + b"\xac": "\u01a0", + b"\xad": "\u01af", + b"\xae": "\u02bc", + b"\xb0": "\u02bb", + b"\xb1": "\u0142", + b"\xb2": "\u00f8", + b"\xb3": "\u0111", + b"\xb4": "\u00fe", + b"\xb5": "\u00e6", + b"\xb6": "\u0153", + b"\xb7": "\u02ba", + b"\xb8": "\u0131", + b"\xb9": "\u00a3", + b"\xba": "\u00f0", + b"\xbc": "\u01a1", + b"\xbd": "\u01b0", + b"\xbe": "\u25a1", + b"\xbf": "\u25a0", + b"\xc0": "\u00b0", + b"\xc1": "\u2113", + b"\xc2": "\u2117", + b"\xc3": "\u00a9", + b"\xc4": "\u266f", + b"\xc5": "\u00bf", + b"\xc6": "\u00a1", + b"\xc7": "\u00df", + b"\xc8": "\u20ac", + b"\xcd": "\u0065", + b"\xce": "\u006f", + b"\xcf": "\u00df", + } + + # combining forms (in ANSEL, they precede the modified ASCII character + # whereas the unicode combining term follows the character modified + # Note: unicode allows multiple modifiers, but ANSEL may not (TDB?), + # so we ignore multiple combining forms in this module + # 8d & 8e are zero-width joiner (ZWJ), and zero-width non-joiner ZWNJ + # (strange things) probably not commonly found in our needs, unless one + # starts writing persian (or???) poetry in ANSEL + __acombiners = { + b"\x8d": "\u200d", + b"\x8e": "\u200c", + b"\xe0": "\u0309", + b"\xe1": "\u0300", + b"\xe2": "\u0301", + b"\xe3": "\u0302", + b"\xe4": "\u0303", + b"\xe5": "\u0304", + b"\xe6": "\u0306", + b"\xe7": "\u0307", + b"\xe8": "\u0308", + b"\xe9": "\u030c", + b"\xea": "\u030a", + b"\xeb": "\ufe20", + b"\xec": "\ufe21", + b"\xed": "\u0315", + b"\xee": "\u030b", + b"\xef": "\u0310", + b"\xf0": "\u0327", + b"\xf1": "\u0328", + b"\xf2": "\u0323", + b"\xf3": "\u0324", + b"\xf4": "\u0325", + b"\xf5": "\u0333", + b"\xf6": "\u0332", + b"\xf7": "\u0326", + b"\xf8": "\u031c", + b"\xf9": "\u032e", + b"\xfa": "\ufe22", + b"\xfb": "\ufe23", + b"\xfc": "\u0338", + b"\xfe": "\u0313", + } + + # mappings of two byte (precomposed forms) ANSEL codes to unicode + __twobyte = { + b"\xe0\x41": "\u1ea2", + b"\xe0\x45": "\u1eba", + b"\xe0\x49": "\u1ec8", + b"\xe0\x4f": "\u1ece", + b"\xe0\x55": "\u1ee6", + b"\xe0\x59": "\u1ef6", + b"\xe0\x61": "\u1ea3", + b"\xe0\x65": "\u1ebb", + b"\xe0\x69": "\u1ec9", + b"\xe0\x6f": "\u1ecf", + b"\xe0\x75": "\u1ee7", + b"\xe0\x79": "\u1ef7", + b"\xe1\x41": "\u00c0", + b"\xe1\x45": "\u00c8", + b"\xe1\x49": "\u00cc", + b"\xe1\x4f": "\u00d2", + b"\xe1\x55": "\u00d9", + b"\xe1\x57": "\u1e80", + b"\xe1\x59": "\u1ef2", + b"\xe1\x61": "\u00e0", + b"\xe1\x65": "\u00e8", + b"\xe1\x69": "\u00ec", + b"\xe1\x6f": "\u00f2", + b"\xe1\x75": "\u00f9", + b"\xe1\x77": "\u1e81", + b"\xe1\x79": "\u1ef3", + b"\xe2\x41": "\u00c1", + b"\xe2\x43": "\u0106", + b"\xe2\x45": "\u00c9", + b"\xe2\x47": "\u01f4", + b"\xe2\x49": "\u00cd", + b"\xe2\x4b": "\u1e30", + b"\xe2\x4c": "\u0139", + b"\xe2\x4d": "\u1e3e", + b"\xe2\x4e": "\u0143", + b"\xe2\x4f": "\u00d3", + b"\xe2\x50": "\u1e54", + b"\xe2\x52": "\u0154", + b"\xe2\x53": "\u015a", + b"\xe2\x55": "\u00da", + b"\xe2\x57": "\u1e82", + b"\xe2\x59": "\u00dd", + b"\xe2\x5a": "\u0179", + b"\xe2\x61": "\u00e1", + b"\xe2\x63": "\u0107", + b"\xe2\x65": "\u00e9", + b"\xe2\x67": "\u01f5", + b"\xe2\x69": "\u00ed", + b"\xe2\x6b": "\u1e31", + b"\xe2\x6c": "\u013a", + b"\xe2\x6d": "\u1e3f", + b"\xe2\x6e": "\u0144", + b"\xe2\x6f": "\u00f3", + b"\xe2\x70": "\u1e55", + b"\xe2\x72": "\u0155", + b"\xe2\x73": "\u015b", + b"\xe2\x75": "\u00fa", + b"\xe2\x77": "\u1e83", + b"\xe2\x79": "\u00fd", + b"\xe2\x7a": "\u017a", + b"\xe2\xa5": "\u01fc", + b"\xe2\xb5": "\u01fd", + b"\xe3\x41": "\u00c2", + b"\xe3\x43": "\u0108", + b"\xe3\x45": "\u00ca", + b"\xe3\x47": "\u011c", + b"\xe3\x48": "\u0124", + b"\xe3\x49": "\u00ce", + b"\xe3\x4a": "\u0134", + b"\xe3\x4f": "\u00d4", + b"\xe3\x53": "\u015c", + b"\xe3\x55": "\u00db", + b"\xe3\x57": "\u0174", + b"\xe3\x59": "\u0176", + b"\xe3\x5a": "\u1e90", + b"\xe3\x61": "\u00e2", + b"\xe3\x63": "\u0109", + b"\xe3\x65": "\u00ea", + b"\xe3\x67": "\u011d", + b"\xe3\x68": "\u0125", + b"\xe3\x69": "\u00ee", + b"\xe3\x6a": "\u0135", + b"\xe3\x6f": "\u00f4", + b"\xe3\x73": "\u015d", + b"\xe3\x75": "\u00fb", + b"\xe3\x77": "\u0175", + b"\xe3\x79": "\u0177", + b"\xe3\x7a": "\u1e91", + b"\xe4\x41": "\u00c3", + b"\xe4\x45": "\u1ebc", + b"\xe4\x49": "\u0128", + b"\xe4\x4e": "\u00d1", + b"\xe4\x4f": "\u00d5", + b"\xe4\x55": "\u0168", + b"\xe4\x56": "\u1e7c", + b"\xe4\x59": "\u1ef8", + b"\xe4\x61": "\u00e3", + b"\xe4\x65": "\u1ebd", + b"\xe4\x69": "\u0129", + b"\xe4\x6e": "\u00f1", + b"\xe4\x6f": "\u00f5", + b"\xe4\x75": "\u0169", + b"\xe4\x76": "\u1e7d", + b"\xe4\x79": "\u1ef9", + b"\xe5\x41": "\u0100", + b"\xe5\x45": "\u0112", + b"\xe5\x47": "\u1e20", + b"\xe5\x49": "\u012a", + b"\xe5\x4f": "\u014c", + b"\xe5\x55": "\u016a", + b"\xe5\x61": "\u0101", + b"\xe5\x65": "\u0113", + b"\xe5\x67": "\u1e21", + b"\xe5\x69": "\u012b", + b"\xe5\x6f": "\u014d", + b"\xe5\x75": "\u016b", + b"\xe5\xa5": "\u01e2", + b"\xe5\xb5": "\u01e3", + b"\xe6\x41": "\u0102", + b"\xe6\x45": "\u0114", + b"\xe6\x47": "\u011e", + b"\xe6\x49": "\u012c", + b"\xe6\x4f": "\u014e", + b"\xe6\x55": "\u016c", + b"\xe6\x61": "\u0103", + b"\xe6\x65": "\u0115", + b"\xe6\x67": "\u011f", + b"\xe6\x69": "\u012d", + b"\xe6\x6f": "\u014f", + b"\xe6\x75": "\u016d", + b"\xe7\x42": "\u1e02", + b"\xe7\x43": "\u010a", + b"\xe7\x44": "\u1e0a", + b"\xe7\x45": "\u0116", + b"\xe7\x46": "\u1e1e", + b"\xe7\x47": "\u0120", + b"\xe7\x48": "\u1e22", + b"\xe7\x49": "\u0130", + b"\xe7\x4d": "\u1e40", + b"\xe7\x4e": "\u1e44", + b"\xe7\x50": "\u1e56", + b"\xe7\x52": "\u1e58", + b"\xe7\x53": "\u1e60", + b"\xe7\x54": "\u1e6a", + b"\xe7\x57": "\u1e86", + b"\xe7\x58": "\u1e8a", + b"\xe7\x59": "\u1e8e", + b"\xe7\x5a": "\u017b", + b"\xe7\x62": "\u1e03", + b"\xe7\x63": "\u010b", + b"\xe7\x64": "\u1e0b", + b"\xe7\x65": "\u0117", + b"\xe7\x66": "\u1e1f", + b"\xe7\x67": "\u0121", + b"\xe7\x68": "\u1e23", + b"\xe7\x6d": "\u1e41", + b"\xe7\x6e": "\u1e45", + b"\xe7\x70": "\u1e57", + b"\xe7\x72": "\u1e59", + b"\xe7\x73": "\u1e61", + b"\xe7\x74": "\u1e6b", + b"\xe7\x77": "\u1e87", + b"\xe7\x78": "\u1e8b", + b"\xe7\x79": "\u1e8f", + b"\xe7\x7a": "\u017c", + b"\xe8\x41": "\u00c4", + b"\xe8\x45": "\u00cb", + b"\xe8\x48": "\u1e26", + b"\xe8\x49": "\u00cf", + b"\xe8\x4f": "\u00d6", + b"\xe8\x55": "\u00dc", + b"\xe8\x57": "\u1e84", + b"\xe8\x58": "\u1e8c", + b"\xe8\x59": "\u0178", + b"\xe8\x61": "\u00e4", + b"\xe8\x65": "\u00eb", + b"\xe8\x68": "\u1e27", + b"\xe8\x69": "\u00ef", + b"\xe8\x6f": "\u00f6", + b"\xe8\x74": "\u1e97", + b"\xe8\x75": "\u00fc", + b"\xe8\x77": "\u1e85", + b"\xe8\x78": "\u1e8d", + b"\xe8\x79": "\u00ff", + b"\xe9\x41": "\u01cd", + b"\xe9\x43": "\u010c", + b"\xe9\x44": "\u010e", + b"\xe9\x45": "\u011a", + b"\xe9\x47": "\u01e6", + b"\xe9\x49": "\u01cf", + b"\xe9\x4b": "\u01e8", + b"\xe9\x4c": "\u013d", + b"\xe9\x4e": "\u0147", + b"\xe9\x4f": "\u01d1", + b"\xe9\x52": "\u0158", + b"\xe9\x53": "\u0160", + b"\xe9\x54": "\u0164", + b"\xe9\x55": "\u01d3", + b"\xe9\x5a": "\u017d", + b"\xe9\x61": "\u01ce", + b"\xe9\x63": "\u010d", + b"\xe9\x64": "\u010f", + b"\xe9\x65": "\u011b", + b"\xe9\x67": "\u01e7", + b"\xe9\x69": "\u01d0", + b"\xe9\x6a": "\u01f0", + b"\xe9\x6b": "\u01e9", + b"\xe9\x6c": "\u013e", + b"\xe9\x6e": "\u0148", + b"\xe9\x6f": "\u01d2", + b"\xe9\x72": "\u0159", + b"\xe9\x73": "\u0161", + b"\xe9\x74": "\u0165", + b"\xe9\x75": "\u01d4", + b"\xe9\x7a": "\u017e", + b"\xea\x41": "\u00c5", + b"\xea\x61": "\u00e5", + b"\xea\x75": "\u016f", + b"\xea\x77": "\u1e98", + b"\xea\x79": "\u1e99", + b"\xea\xad": "\u016e", + b"\xee\x4f": "\u0150", + b"\xee\x55": "\u0170", + b"\xee\x6f": "\u0151", + b"\xee\x75": "\u0171", + b"\xf0\x20": "\u00b8", + b"\xf0\x43": "\u00c7", + b"\xf0\x44": "\u1e10", + b"\xf0\x47": "\u0122", + b"\xf0\x48": "\u1e28", + b"\xf0\x4b": "\u0136", + b"\xf0\x4c": "\u013b", + b"\xf0\x4e": "\u0145", + b"\xf0\x52": "\u0156", + b"\xf0\x53": "\u015e", + b"\xf0\x54": "\u0162", + b"\xf0\x63": "\u00e7", + b"\xf0\x64": "\u1e11", + b"\xf0\x67": "\u0123", + b"\xf0\x68": "\u1e29", + b"\xf0\x6b": "\u0137", + b"\xf0\x6c": "\u013c", + b"\xf0\x6e": "\u0146", + b"\xf0\x72": "\u0157", + b"\xf0\x73": "\u015f", + b"\xf0\x74": "\u0163", + b"\xf1\x41": "\u0104", + b"\xf1\x45": "\u0118", + b"\xf1\x49": "\u012e", + b"\xf1\x4f": "\u01ea", + b"\xf1\x55": "\u0172", + b"\xf1\x61": "\u0105", + b"\xf1\x65": "\u0119", + b"\xf1\x69": "\u012f", + b"\xf1\x6f": "\u01eb", + b"\xf1\x75": "\u0173", + b"\xf2\x41": "\u1ea0", + b"\xf2\x42": "\u1e04", + b"\xf2\x44": "\u1e0c", + b"\xf2\x45": "\u1eb8", + b"\xf2\x48": "\u1e24", + b"\xf2\x49": "\u1eca", + b"\xf2\x4b": "\u1e32", + b"\xf2\x4c": "\u1e36", + b"\xf2\x4d": "\u1e42", + b"\xf2\x4e": "\u1e46", + b"\xf2\x4f": "\u1ecc", + b"\xf2\x52": "\u1e5a", + b"\xf2\x53": "\u1e62", + b"\xf2\x54": "\u1e6c", + b"\xf2\x55": "\u1ee4", + b"\xf2\x56": "\u1e7e", + b"\xf2\x57": "\u1e88", + b"\xf2\x59": "\u1ef4", + b"\xf2\x5a": "\u1e92", + b"\xf2\x61": "\u1ea1", + b"\xf2\x62": "\u1e05", + b"\xf2\x64": "\u1e0d", + b"\xf2\x65": "\u1eb9", + b"\xf2\x68": "\u1e25", + b"\xf2\x69": "\u1ecb", + b"\xf2\x6b": "\u1e33", + b"\xf2\x6c": "\u1e37", + b"\xf2\x6d": "\u1e43", + b"\xf2\x6e": "\u1e47", + b"\xf2\x6f": "\u1ecd", + b"\xf2\x72": "\u1e5b", + b"\xf2\x73": "\u1e63", + b"\xf2\x74": "\u1e6d", + b"\xf2\x75": "\u1ee5", + b"\xf2\x76": "\u1e7f", + b"\xf2\x77": "\u1e89", + b"\xf2\x79": "\u1ef5", + b"\xf2\x7a": "\u1e93", + b"\xf3\x55": "\u1e72", + b"\xf3\x75": "\u1e73", + b"\xf4\x41": "\u1e00", + b"\xf4\x61": "\u1e01", + b"\xf9\x48": "\u1e2a", + b"\xf9\x68": "\u1e2b", + } + + def __ansel_to_unicode(self, text): + """Convert an ANSEL encoded text to unicode""" + + buff = StringIO() + error = "" + while text: + if text[0] < 128: + if chr(text[0]) in AnselReader.__use_ASCII: + head = chr(text[0]) + else: + # substitute space for disallowed (control) chars + error += " (%#X)" % text[0] + head = " " + text = text[1:] + else: + if text[0:2] in AnselReader.__twobyte: + head = AnselReader.__twobyte[text[0:2]] + text = text[2:] + elif bytes([text[0]]) in AnselReader.__onebyte: + head = AnselReader.__onebyte[bytes([text[0]])] + text = text[1:] + elif bytes([text[0]]) in AnselReader.__acombiners: + cmb = AnselReader.__acombiners[bytes([text[0]])] + # always consume the combiner + text = text[1:] + next_byte = text[0] + if ( + next_byte < 128 + and chr(next_byte) in AnselReader.__printable_ascii + ): + # consume next as well + text = text[1:] + # unicode: combiner follows base-char + head = chr(next_byte) + cmb + else: + # just drop the unexpected combiner + error += " (%#X)" % text[0] + continue + else: + error += " (%#X)" % text[0] + head = "\ufffd" # "Replacement Char" + text = text[1:] + buff.write(head) + ans = buff.getvalue() + + if error: + # e.g. Illegal character (oxAB) (0xCB)... 1 NOTE xyz?pqr?lmn + self.report_error(_("Illegal character%s") % error, ans) + buff.close() + return ans + + def __init__(self, ifile, __add_msg): + BaseReader.__init__(self, ifile, "ANSEL", __add_msg) + # In theory, we should have been able to skip the encode/decode from + # ascii. But this way allows us to use pythons universal newline + self.ifile = TextIOWrapper( + ifile, encoding="ascii", errors="surrogateescape", newline=None + ) + + def readline(self): + line = self.ifile.readline() + linebytes = line.encode(encoding="ascii", errors="surrogateescape") + return self.__ansel_to_unicode(linebytes) + + +# ------------------------------------------------------------------------- +# +# CurrentState +# +# ------------------------------------------------------------------------- +class CurrentState: + """ + Keep track of the current state variables. + """ + + def __init__(self, person=None, level=0, event=None, event_ref=None): + """ + Initialize the object. + """ + self.name_cnt = 0 + self.person = person + self.family = None + self.level = level + self.event = event + self.event_ref = event_ref + self.source_ref = None + self.citation = None + self.note = None + self.lds_ord = None + self.msg = "" + self.primary = False # _PRIMARY tag on an INDI.FAMC tag + self.filename = "" + self.title = "" + self.addr = None + self.res = None + self.source = None + self.ftype = None + self.pf = None # method for parsing places + self.location = None + self.place_fields = None # method for parsing places + self.ref = None # PersonRef + self.handle = None # + self.form = "" # Multimedia format + self.frel = None # Child relation to father + self.mrel = None + self.repo = None + self.attr = None + self.obj = None + self.name = "" + self.ignore = False + self.repo_ref = None + self.place = None + self.media = None + self.photo = "" # Person primary photo + self.prim = None # Photo is primary + + def __getattr__(self, name): + """ + Return the value associated with the specified attribute. + """ + return self.__dict__.get(name) + + def __setattr__(self, name, value): + """ + Set the value associated with the specified attribute. + """ + self.__dict__[name] = value + + +# ------------------------------------------------------------------------- +# +# PlaceParser +# +# ------------------------------------------------------------------------- +class PlaceParser: + """ + Provide the ability to parse GEDCOM FORM statements for places, and + the parse the line of text, mapping the text components to Location + values based of the FORM statement. + """ + + __field_map = { + "addr": Location.set_street, + "subdivision": Location.set_street, + "addr1": Location.set_street, + "adr1": Location.set_street, + "street": Location.set_street, + "addr2": Location.set_locality, + "adr2": Location.set_locality, + "locality": Location.set_locality, + "neighborhood": Location.set_locality, + "city": Location.set_city, + "town": Location.set_city, + "village": Location.set_city, + "county": Location.set_county, + "country": Location.set_country, + "state": Location.set_state, + "state/province": Location.set_state, + "region": Location.set_state, + "province": Location.set_state, + "area code": Location.set_postal_code, + "post code": Location.set_postal_code, + "zip code": Location.set_postal_code, + } + + def __init__(self, line=None): + self.parse_function = [] + + if line: + self.parse_form(line) + + def parse_form(self, line): + """ + Parses the GEDCOM PLAC.FORM into a list of function + pointers (if possible). It does this my mapping the text strings + (separated by commas) to the corresponding Location + method via the __field_map variable + """ + for item in line.data.split(","): + item = item.lower().strip() + fcn = self.__field_map.get(item, lambda x, y: None) + self.parse_function.append(fcn) + + def load_place(self, place_import, place, text): + """ + Takes the text string representing a place, splits it into + its subcomponents (comma separated), and calls the approriate + function based of its position, depending on the parsed value + from the FORM statement. + """ + items = [item.strip() for item in text.split(",")] + if len(items) != len(self.parse_function): + return + index = 0 + loc = Location() + for item in items: + self.parse_function[index](loc, item) + index += 1 + + location = ( + loc.get_street(), + loc.get_locality(), + loc.get_parish(), + loc.get_city(), + loc.get_county(), + loc.get_state(), + loc.get_country(), + ) + + for level, name in enumerate(location): + if name: + break + + if name: + type_num = 7 - level + else: + name = place.title + type_num = PlaceType.UNKNOWN + place.name.set_value(name) + place.set_type(PlaceType(type_num)) + code = loc.get_postal_code() + place.set_code(code) + if place.handle: # if handle is available, store immediately + place_import.store_location(location, place.handle) + else: # return for storage later + return location + + +# ------------------------------------------------------------------------- +# +# IdFinder +# +# ------------------------------------------------------------------------- +class IdFinder: + """ + Provide method of finding the next available ID. + """ + + def __init__(self, keys, prefix): + """ + Initialize the object. + """ + self.ids = set(keys) + self.index = 0 + self.prefix = prefix + + def find_next(self): + """ + Return the next available GRAMPS' ID for a Event object based + off the person ID prefix. + + @return: Returns the next available index + @rtype: str + """ + index = self.prefix % self.index + while index in self.ids: + self.index += 1 + index = self.prefix % self.index + self.ids.add(index) + self.index += 1 + return index + + +# ------------------------------------------------------------------------- +# +# IdMapper +# +# ------------------------------------------------------------------------- +class IdMapper: + """This class provide methods to keep track of the correspoindence between + Gedcom xrefs (@P1023@) and Gramps IDs.""" + + def __init__(self, has_gid, find_next, id2user_format): + self.has_gid = has_gid + self.find_next = find_next + self.id2user_format = id2user_format + self.swap = {} + + def __getitem__(self, gid): + if gid == "": + # We need to find the next gramps ID provided it is not already + # the target of a swap + new_val = self.find_next() + while new_val in list(self.swap.values()): + new_val = self.find_next() + else: + # remove any @ signs + gid = self.clean(gid) + if gid in self.swap: + return self.swap[gid] + else: + # now standardise the format + formatted_gid = self.id2user_format(gid) + # I1 and I0001 will both format as I0001. If we had already + # encountered I1, it would be in self.swap, so we would already + # have found it. If we had already encountered I0001 and we are + # now looking for I1, it wouldn't be in self.swap, and we now + # find that I0001 is in use, so we have to create a new id. + if self.has_gid(formatted_gid) or ( + formatted_gid in list(self.swap.values()) + ): + new_val = self.find_next() + while new_val in list(self.swap.values()): + new_val = self.find_next() + else: + new_val = formatted_gid + # we need to distinguish between I1 and I0001, so we record the map + # from the original format + self.swap[gid] = new_val + return new_val + + def clean(self, gid): + """remove '@' from start and end of xref""" + temp = gid.strip() + if len(temp) > 1 and temp[0] == "@" and temp[-1] == "@": + temp = temp[1:-1] + return temp + + def map(self): + """return the xref to GID translation map""" + return self.swap + + +# ------------------------------------------------------------------------- +# +# GedcomParser +# +# ------------------------------------------------------------------------- +class GedcomParser(UpdateCallback): + """ + Performs the second pass of the GEDCOM parser, which does all the heavy + lifting. + """ + + __TRUNC_MSG = _( + "Your GEDCOM file is corrupted. " "It appears to have been truncated." + ) + _EMPTY_LOC = object_to_dict(Location()) + + SyntaxError = "Syntax Error" + BadFile = "Not a GEDCOM file" + + @staticmethod + def __find_from_handle(gramps_id, table): + """ + Find a handle corresponding to the specified Gramps ID. + + The passed table contains the mapping. If the value is found, we return + it, otherwise we create a new handle, store it, and return it. + + """ + intid = table.get(gramps_id) + if not intid: + intid = create_id() + table[gramps_id] = intid + return intid + + @staticmethod + def __parse_name_personal(text): + """ + Parses a GEDCOM NAME value into an Name structure + """ + name = Name() + + match = SURNAME_RE.match(text) + if match: + # /surname/ extra, we assume extra is given name + names = match.groups() + name.set_first_name(names[1].strip()) + surn = Surname() + surn.set_surname(names[0].strip()) + surn.set_primary() + name.set_surname_list([surn]) + else: + try: + names = NAME_RE.match(text).groups() + # given /surname/ extra, we assume extra is suffix + name.set_first_name(names[0].strip()) + surn = Surname() + surn.set_surname(names[2].strip()) + surn.set_primary() + name.set_surname_list([surn]) + name.set_suffix(names[4].strip()) + except: + # something strange, set as first name + name.set_first_name(text.strip()) + return name + + def __init__( + self, + dbase, + ifile, + filename, + user, + stage_one, + default_source, + default_tag_format=None, + ): + UpdateCallback.__init__(self, user.callback) + self.user = user + self.set_total(stage_one.get_line_count()) + self.repo2id = {} + self.trans = None + self.errors = [] + self.number_of_errors = 0 + self.maxpeople = stage_one.get_person_count() + self.dbase = dbase + self.import_researcher = self.dbase.get_total() == 0 + event_ids = [] + for event in dbase.iter_events(): + event_ids.append(event.gramps_id) + self.emapper = IdFinder(event_ids, dbase.event_prefix) + self.famc_map = stage_one.get_famc_map() + self.fams_map = stage_one.get_fams_map() + + self.place_parser = PlaceParser() + self.inline_srcs = OrderedDict() + self.media_map = {} + self.note_type_map = {} + self.genby = "" + self.genvers = "" + self.subm = "" + self.use_def_src = default_source + self.func_list = [] + if self.use_def_src: + self.def_src = Source() + fname = os.path.basename(filename).split("\\")[-1] + self.def_src.set_title(_("Import from GEDCOM (%s)") % fname) + if default_tag_format: + name = time.strftime(default_tag_format) + tag = self.dbase.get_tag_from_name(name) + if tag: + self.default_tag = tag + else: + self.default_tag = Tag() + self.default_tag.set_name(name) + else: + self.default_tag = None + self.dir_path = os.path.dirname(filename) + self.is_ftw = False + self.addr_is_detail = False + self.groups = None + self.want_parse_warnings = True + + self.pid_map = IdMapper( + self.dbase.has_person_gramps_id, + self.dbase.find_next_person_gramps_id, + self.dbase.id2user_format, + ) + self.fid_map = IdMapper( + self.dbase.has_family_gramps_id, + self.dbase.find_next_family_gramps_id, + self.dbase.fid2user_format, + ) + self.sid_map = IdMapper( + self.dbase.has_source_gramps_id, + self.dbase.find_next_source_gramps_id, + self.dbase.sid2user_format, + ) + self.oid_map = IdMapper( + self.dbase.has_media_gramps_id, + self.dbase.find_next_media_gramps_id, + self.dbase.oid2user_format, + ) + self.rid_map = IdMapper( + self.dbase.has_repository_gramps_id, + self.dbase.find_next_repository_gramps_id, + self.dbase.rid2user_format, + ) + self.nid_map = IdMapper( + self.dbase.has_note_gramps_id, + self.dbase.find_next_note_gramps_id, + self.dbase.nid2user_format, + ) + + self.gid2id = {} + self.oid2id = {} + self.sid2id = {} + self.lid2id = {} + self.fid2id = {} + self.rid2id = {} + self.nid2id = {} + + self.place_import = PlaceImport(self.dbase) + + # + # Parse table for <> below the level 0 SUBM tag + # + # n @@ SUBM {1:1} + # +1 NAME {1:1} + # +1 <> {0:1} + # +1 <> {0:M} + # +1 LANG {0:3} + # +1 <> {0:M} + # +1 RFN {0:1} + # +1 RIN {0:1} + # +1 <> {0:1} + + # (N.B. GEDCOM allows multiple SUBMitter records) + self.subm_parse_tbl = { + # +1 NAME + TOKEN_NAME: self.__subm_name, + # +1 <> + TOKEN_ADDR: self.__subm_addr, + TOKEN_PHON: self.__subm_phon, + TOKEN_EMAIL: self.__subm_email, + TOKEN_WWW: self.__repo_www, + TOKEN_FAX: self.__repo_fax, + # +1 <> + # +1 LANG + # +1 <> + TOKEN_NOTE: self.__repo_note, + TOKEN_RNOTE: self.__repo_note, + # +1 RFN + # +1 RIN + # +1 <> + TOKEN_CHAN: self.__repo_chan, + } + self.func_list.append(self.subm_parse_tbl) + + # + # Parse table for <> below the level 0 INDI tag + # + # n @@ INDI {1:1} + # +1 RESN {0:1} + # +1 <> {0:M} + # +1 SEX {0:1} + # +1 <> {0:M} + # +1 <> {0:M} + # +1 <> {0:M} + # +1 <> {0:M} + # +1 <> {0:M} + # +1 SUBM @@ {0:M} + # +1 <> {0:M} + # +1 ALIA @@ {0:M} + # +1 ANCI @@ {0:M} + # +1 DESI @@ {0:M} + # +1 <> {0:M} + # +1 <> {0:M} + # +1 <> {0:M} + # +1 RFN {0:1} + # +1 AFN {0:1} + # +1 REFN {0:M} + # +2 TYPE {0:1} + # +1 RIN {0:1} + # +1 <> {0:1} + + self.indi_parse_tbl = { + # +1 RESN {0:1} + TOKEN_RESN: self.__person_resn, + # +1 <> {0:M} + TOKEN_NAME: self.__person_name, + # +1 SEX {0:1} + TOKEN_SEX: self.__person_sex, + # +1 <> {0:M} + TOKEN_EVEN: self.__person_even, + TOKEN_GEVENT: self.__person_std_event, + TOKEN_BIRT: self.__person_birt, + TOKEN_RELI: self.__person_reli, + TOKEN_ADOP: self.__person_adop, + TOKEN_DEAT: self.__person_deat, + # +1 <> {0:M} + # +1 AFN {0:1} + TOKEN_ATTR: self.__person_std_attr, + TOKEN_FACT: self.__person_fact, + # +1 <> {0:M} + TOKEN_BAPL: self.__person_bapl, + TOKEN_CONL: self.__person_conl, + TOKEN_INIL: self.__person_inil, + TOKEN_ENDL: self.__person_endl, + TOKEN_SLGC: self.__person_slgc, + # +1 <> {0:M} + TOKEN_FAMC: self.__person_famc, + # +1 <> {0:M} + TOKEN_FAMS: self.__person_fams, + # +1 SUBM @@ {0:M} + TOKEN_SUBM: self.__skip_record, + # +1 <> {0:M} + TOKEN_ASSO: self.__person_asso, + # +1 ALIA @@ {0:M} + TOKEN_ALIA: self.__person_alt_name, + # +1 ANCI @@ {0:M} + TOKEN_ANCI: self.__skip_record, + # +1 DESI @@ {0:M} + TOKEN_DESI: self.__skip_record, + # +1 <> {0:M} + TOKEN_SOUR: self.__person_sour, + # +1 <> {0:M} + TOKEN_OBJE: self.__person_object, + # +1 <> {0:M} + TOKEN_NOTE: self.__person_note, + TOKEN_RNOTE: self.__person_note, + TOKEN__COMM: self.__person_note, + # +1 RFN {0:1} + TOKEN_RFN: self.__person_attr, + # +1 REFN {0:M} + # +2 TYPE {0:1} + TOKEN_REFN: self.__person_refn, + # TYPE should be below REFN, but will work here anyway + TOKEN_TYPE: self.__person_attr, + # +1 RIN {0:1} + TOKEN_RIN: self.__person_attr, + # +1 <> {0:1} + TOKEN_CHAN: self.__person_chan, + # The following tags are not part of Gedcom spec but are commonly + # found here anyway + TOKEN_ADDR: self.__person_addr, + TOKEN_PHON: self.__person_phon, + TOKEN_FAX: self.__person_fax, + TOKEN_EMAIL: self.__person_email, + TOKEN_WWW: self.__person_www, + TOKEN__TODO: self.__skip_record, + TOKEN_TITL: self.__person_titl, + TOKEN__PHOTO: self.__person_photo, + } + self.func_list.append(self.indi_parse_tbl) + + self.name_parse_tbl = { + # +1 NPFX {0:1} + TOKEN_NPFX: self.__name_npfx, + # +1 GIVN {0:1} + TOKEN_GIVN: self.__name_givn, + # NICK {0:1} + TOKEN_NICK: self.__name_nick, + # _RUFNAME {0:1} + TOKEN__CALLNAME: self.__name_call, + # +1 SPFX {0:1} + TOKEN_SURN: self.__name_surn, + # +1 NSFX {0:1} + TOKEN_NSFX: self.__name_nsfx, + # +1 <> {0:M} + TOKEN_SOUR: self.__name_sour, + # +1 <> {0:M} + TOKEN_NOTE: self.__name_note, + TOKEN_RNOTE: self.__name_note, + # Extensions + TOKEN_ALIA: self.__name_alia, + TOKEN__MARNM: self.__name_marnm, + TOKEN__MAR: self.__name_marnm, # Generated by geni.com + TOKEN__MARN: self.__name_marnm, # Gen'd by BROSKEEP 6.1.31 WIN + TOKEN__AKA: self.__name_aka, # PAF and AncestQuest + TOKEN_TYPE: self.__name_type, # This is legal GEDCOM 5.5.1 + TOKEN_BIRT: self.__ignore, + TOKEN_DATE: self.__name_date, + # This handles date as a subsidiary of "1 ALIA" which might be used + # by Family Tree Maker and Reunion, and by cheating (handling a + # lower level from the current parse table) handles date as + # subsidiary to "2 _MARN", "2 _AKAN" and "2 _ADPN" which has been + # found in Brother's keeper. + TOKEN__ADPN: self.__name_adpn, + } + self.func_list.append(self.name_parse_tbl) + + # + # Parse table for <> below the level 0 REPO tag + # + # n @@ REPO {1:1} + # +1 NAME {0:1} + # +1 <> {0:1} + # +1 <> {0:M} + # +1 REFN {0:M} + # +2 TYPE {0:1} + # +1 RIN {0:1} + # +1 <> {0:1} + + self.repo_parse_tbl = { + TOKEN_NAME: self.__repo_name, + TOKEN_ADDR: self.__repo_addr, + TOKEN_RIN: self.__ignore, + TOKEN_NOTE: self.__repo_note, + TOKEN_RNOTE: self.__repo_note, + TOKEN_CHAN: self.__repo_chan, + TOKEN_PHON: self.__repo_phon, + TOKEN_EMAIL: self.__repo_email, + TOKEN_WWW: self.__repo_www, + TOKEN_FAX: self.__repo_fax, + } + self.func_list.append(self.repo_parse_tbl) + + self.event_parse_tbl = { + # n TYPE {0:1} + TOKEN_TYPE: self.__event_type, + # n DATE {0:1} p.*/* + TOKEN_DATE: self.__event_date, + # n <> {0:1} p.* + TOKEN_PLAC: self.__event_place, + # n <> {0:1} p.* + TOKEN_ADDR: self.__event_addr, + # n AGE {0:1} p.* + TOKEN_AGE: self.__event_age, + # n AGNC {0:1} p.* + TOKEN_AGNC: self.__event_agnc, + # n CAUS {0:1} p.* + TOKEN_CAUS: self.__event_cause, + # n <> {0:M} p.* + TOKEN_SOUR: self.__event_source, + # n <> {0:M} p.*, * + TOKEN_OBJE: self.__event_object, + # n <> {0:M} p. + TOKEN_NOTE: self.__event_inline_note, + TOKEN_RNOTE: self.__event_note, + # Other + TOKEN__PRIV: self.__event_privacy, + TOKEN_OFFI: self.__event_note, + TOKEN_PHON: self.__event_phon, + TOKEN__GODP: self.__event_witness, + TOKEN__WITN: self.__event_witness, + TOKEN__WTN: self.__event_witness, + TOKEN_RELI: self.__ignore, + # Not legal, but inserted by PhpGedView + TOKEN_TIME: self.__event_time, + TOKEN_ASSO: self.__ignore, + TOKEN_IGNORE: self.__ignore, + TOKEN_STAT: self.__ignore, + TOKEN_TEMP: self.__ignore, + TOKEN_HUSB: self.__event_husb, + TOKEN_WIFE: self.__event_wife, + TOKEN_FAMC: self.__person_birth_famc, + # Not legal, but inserted by Ultimate Family Tree + TOKEN_CHAN: self.__ignore, + TOKEN_QUAY: self.__ignore, + # Not legal, but inserted by FamilyTreeBuilder + TOKEN_RIN: self.__event_rin, + TOKEN_ATTR: self.__event_attr, # FTB for _UID + TOKEN_EMAIL: self.__event_email, # FTB for RESI events + TOKEN_WWW: self.__event_www, # FTB for RESI events + TOKEN_FAX: self.__event_fax, # legal... + } + self.func_list.append(self.event_parse_tbl) + + self.adopt_parse_tbl = { + TOKEN_TYPE: self.__event_type, + TOKEN__PRIV: self.__event_privacy, + TOKEN_DATE: self.__event_date, + TOKEN_SOUR: self.__event_source, + TOKEN_PLAC: self.__event_place, + TOKEN_ADDR: self.__event_addr, + TOKEN_PHON: self.__event_phon, + TOKEN_CAUS: self.__event_cause, + TOKEN_AGNC: self.__event_agnc, + TOKEN_AGE: self.__event_age, + TOKEN_NOTE: self.__event_note, + TOKEN_RNOTE: self.__event_note, + TOKEN_OFFI: self.__event_note, + TOKEN__GODP: self.__event_witness, + TOKEN__WITN: self.__event_witness, + TOKEN__WTN: self.__event_witness, + TOKEN_RELI: self.__ignore, + TOKEN_TIME: self.__ignore, + TOKEN_ASSO: self.__ignore, + TOKEN_IGNORE: self.__ignore, + TOKEN_STAT: self.__ignore, + TOKEN_TEMP: self.__ignore, + TOKEN_OBJE: self.__event_object, + TOKEN_FAMC: self.__person_adopt_famc, + # Not legal, but inserted by Ultimate Family Tree + TOKEN_CHAN: self.__ignore, + TOKEN_QUAY: self.__ignore, + } + self.func_list.append(self.adopt_parse_tbl) + + self.famc_parse_tbl = { + # n FAMC @@ {1:1} + # +1 PEDI {0:1} p.* + TOKEN_PEDI: self.__person_famc_pedi, + # +1 _FREL {0:1} non-standard + TOKEN__FREL: self.__person_famc_frel, + # +1 _MREL {0:1} non-standard + TOKEN__MREL: self.__person_famc_mrel, + # +1 <> {0:M} p.* + TOKEN_NOTE: self.__person_famc_note, + TOKEN_RNOTE: self.__person_famc_note, + # Extras + TOKEN__PRIMARY: self.__person_famc_primary, + TOKEN_SOUR: self.__person_famc_sour, + # GEDit + TOKEN_STAT: self.__ignore, + } + self.func_list.append(self.famc_parse_tbl) + + self.person_fact_parse_tbl = { + TOKEN_TYPE: self.__person_fact_type, + TOKEN_SOUR: self.__person_attr_source, + TOKEN_NOTE: self.__person_attr_note, + TOKEN_RNOTE: self.__person_attr_note, + } + self.func_list.append(self.person_fact_parse_tbl) + + self.person_attr_parse_tbl = { + TOKEN_TYPE: self.__person_attr_type, + TOKEN_CAUS: self.__ignore, + TOKEN_DATE: self.__ignore, + TOKEN_TIME: self.__ignore, + TOKEN_ADDR: self.__ignore, + TOKEN_IGNORE: self.__ignore, + TOKEN_STAT: self.__ignore, + TOKEN_TEMP: self.__ignore, + TOKEN_OBJE: self.__ignore, + TOKEN_SOUR: self.__person_attr_source, + TOKEN_PLAC: self.__person_attr_place, + TOKEN_NOTE: self.__person_attr_note, + TOKEN_RNOTE: self.__person_attr_note, + } + self.func_list.append(self.person_attr_parse_tbl) + + self.lds_parse_tbl = { + TOKEN_TEMP: self.__lds_temple, + TOKEN_DATE: self.__lds_date, + TOKEN_FAMC: self.__lds_famc, + TOKEN_FORM: self.__lds_form, + TOKEN_PLAC: self.__lds_plac, + TOKEN_SOUR: self.__lds_sour, + TOKEN_NOTE: self.__lds_note, + TOKEN_RNOTE: self.__lds_note, + TOKEN_STAT: self.__lds_stat, + } + self.func_list.append(self.lds_parse_tbl) + + self.asso_parse_tbl = { + TOKEN_RELA: self.__person_asso_rela, + TOKEN_SOUR: self.__person_asso_sour, + TOKEN_NOTE: self.__person_asso_note, + TOKEN_RNOTE: self.__person_asso_note, + } + self.func_list.append(self.asso_parse_tbl) + + self.citation_parse_tbl = { + TOKEN_PAGE: self.__citation_page, + TOKEN_DATE: self.__citation_date, + TOKEN_DATA: self.__citation_data, + TOKEN_OBJE: self.__citation_obje, + TOKEN_REFN: self.__citation_refn, + TOKEN_EVEN: self.__citation_even, + TOKEN_IGNORE: self.__ignore, + TOKEN__LKD: self.__ignore, + TOKEN_QUAY: self.__citation_quay, + TOKEN_NOTE: self.__citation_note, + TOKEN_RNOTE: self.__citation_note, + TOKEN_TEXT: self.__citation_data_text, + TOKEN__LINK: self.__citation_link, + TOKEN__JUST: self.__citation__just, + TOKEN__APID: self.__citation__apid, + } + self.func_list.append(self.citation_parse_tbl) + + self.media_parse_tbl = { + TOKEN_FORM: self.__media_ref_form, + TOKEN_MEDI: self.__media_ref_medi, # v5.5.1 + TOKEN_TITL: self.__media_ref_titl, + TOKEN_FILE: self.__media_ref_file, + TOKEN_NOTE: self.__obje_note, # illegal, but often there + TOKEN_RNOTE: self.__obje_note, # illegal, but often there + TOKEN__PRIM: self.__media_ref_prim, # LFT etc. + TOKEN_IGNORE: self.__ignore, + } + self.func_list.append(self.media_parse_tbl) + + self.parse_loc_tbl = { + TOKEN_ADR1: self.__location_adr1, + TOKEN_ADR2: self.__location_adr2, + TOKEN_CITY: self.__location_city, + TOKEN_STAE: self.__location_stae, + TOKEN_POST: self.__location_post, + TOKEN_CTRY: self.__location_ctry, + # Not legal GEDCOM - not clear why these are included at this level + TOKEN_ADDR: self.__ignore, + TOKEN_DATE: self.__ignore, # there is nowhere to put a date + TOKEN_NOTE: self.__location_note, + TOKEN_RNOTE: self.__location_note, + TOKEN__LOC: self.__ignore, + TOKEN__NAME: self.__ignore, + TOKEN_PHON: self.__location_phone, + TOKEN_IGNORE: self.__ignore, + } + self.func_list.append(self.parse_loc_tbl) + + # + # Parse table for <> below the level 0 FAM tag + # + # n @@ FAM {1:1} + # +1 <> {0:M} + # +1 HUSB @@ {0:1} + # +1 WIFE @@ {0:1} + # +1 CHIL @@ {0:M} + # +1 NCHI {0:1} + # +1 SUBM @@ {0:M} + # +1 <> {0:M} + # +1 <> {0:M} + # +1 <> {0:M} + # +1 <> {0:M} + # +1 REFN {0:M} + # +1 RIN {0:1} + # +1 <> {0:1} + + self.family_func = { + # +1 <> {0:M} + TOKEN_GEVENT: self.__family_std_event, + TOKEN_EVEN: self.__fam_even, + # +1 HUSB @@ {0:1} + TOKEN_HUSB: self.__family_husb, + # +1 WIFE @@ {0:1} + TOKEN_WIFE: self.__family_wife, + # +1 CHIL @@ {0:M} + TOKEN_CHIL: self.__family_chil, + # +1 NCHI {0:1} + # +1 SUBM @@ {0:M} + # +1 <> {0:M} + TOKEN_SLGS: self.__family_slgs, + # +1 <> {0:M} + TOKEN_SOUR: self.__family_source, + # +1 <> {0:M} + TOKEN_OBJE: self.__family_object, + # +1 <> {0:M} + TOKEN__COMM: self.__family_comm, + TOKEN_NOTE: self.__family_note, + TOKEN_RNOTE: self.__family_note, + # +1 REFN {0:M} + TOKEN_REFN: self.__family_refn, + # TYPE should be below REFN, but will work here anyway + TOKEN_TYPE: self.__family_cust_attr, + # +1 RIN {0:1} + # +1 <> {0:1} + TOKEN_CHAN: self.__family_chan, + TOKEN_ENDL: self.__ignore, + TOKEN_ADDR: self.__ignore, + TOKEN_RIN: self.__family_cust_attr, + TOKEN_SUBM: self.__ignore, + TOKEN_ATTR: self.__family_attr, + } + self.func_list.append(self.family_func) + + self.family_rel_tbl = { + TOKEN__FREL: self.__family_frel, + TOKEN__MREL: self.__family_mrel, + TOKEN_ADOP: self.__family_adopt, + TOKEN__STAT: self.__family_stat, + } + self.func_list.append(self.family_rel_tbl) + + # + # Parse table for <> below the level 0 SOUR tag + # + # n @@ SOUR {1:1} + # +1 DATA {0:1} + # +2 EVEN {0:M} + # +3 DATE {0:1} + # +3 PLAC {0:1} + # +2 AGNC {0:1} + # +2 <> {0:M} + # +1 AUTH {0:1} + # +2 [CONT|CONC] {0:M} + # +1 TITL {0:1} + # +2 [CONT|CONC] {0:M} + # +1 ABBR {0:1} + # +1 PUBL {0:1} + # +2 [CONT|CONC] {0:M} + # +1 TEXT {0:1} + # +2 [CONT|CONC] {0:M} + # +1 <> {0:1} + # +1 <> {0:M} + # +1 <> {0:M} + # +1 REFN {0:M} + # +2 TYPE {0:1} + # +1 RIN {0:1} + # +1 <> {0:1} + + self.source_func = { + TOKEN_TITL: self.__source_title, + TOKEN_TAXT: self.__source_taxt_peri, + TOKEN_PERI: self.__source_taxt_peri, + TOKEN_AUTH: self.__source_auth, + TOKEN_PUBL: self.__source_publ, + TOKEN_NOTE: self.__source_note, + TOKEN_RNOTE: self.__source_note, + TOKEN_TEXT: self.__source_text, + TOKEN_ABBR: self.__source_abbr, + TOKEN_REFN: self.__source_attr, + TOKEN_RIN: self.__source_attr, + TOKEN_REPO: self.__source_repo, + TOKEN_OBJE: self.__source_object, + TOKEN_CHAN: self.__source_chan, + TOKEN_MEDI: self.__source_attr, + TOKEN__NAME: self.__source_attr, + TOKEN_DATA: self.__ignore, + # TYPE should be below REFN, but will work here anyway + TOKEN_TYPE: self.__source_attr, + TOKEN_CALN: self.__ignore, + # not legal, but Ultimate Family Tree does this + TOKEN_DATE: self.__ignore, + TOKEN_IGNORE: self.__ignore, + TOKEN__APID: self.__source_attr, + } + self.func_list.append(self.source_func) + + # + # Parse table for <> below the level 0 OBJE tag + # + # n @XREF:OBJE@ OBJE {1:1} # v5.5 layout + # +1 FILE {1:1} # de-facto extension + # +1 FORM {1:1} + # +1 TITL {0:1} + # +1 <> {0:M} + # +1 BLOB {1:1} # Deprecated, no support + # +2 CONT {1:M} + # +1 OBJE @@ /* chain */ {0:1} # Deprecated, no support + # +1 REFN {0:M} + # +2 TYPE {0:1} + # +1 RIN {0:1} + # +1 <> {0:1} + # + # n @XREF:OBJE@ OBJE {1:1} # v5.5.1 layout + # +1 FILE {1:M} # multi files, no support + # +2 FORM {1:1} + # +3 TYPE {0:1} + # +2 TITL {0:1} + # +2 DATE {0:1} # FTM extension + # +2 TEXT {0:1} # FTM extension + # +1 REFN {0:M} + # +2 TYPE {0:1} + # +1 RIN {0:1} + # +1 <> {0:M} + # +1 <> {0:M} + # +1 <> {0:1} + + self.obje_func = { + TOKEN_FORM: self.__obje_form, + TOKEN_TYPE: self.__obje_type, # v5.5.1 + TOKEN_TITL: self.__obje_title, + TOKEN_FILE: self.__obje_file, # de-facto extension + TOKEN_TEXT: self.__obje_text, # FTM extension + TOKEN__TEXT: self.__obje_text, # FTM 2017 extension + TOKEN_DATE: self.__obje_date, # FTM extension + TOKEN__DATE: self.__obje_date, # FTM 2017 extension + TOKEN_NOTE: self.__obje_note, + TOKEN_RNOTE: self.__obje_note, + TOKEN_SOUR: self.__obje_sour, + TOKEN_BLOB: self.__ignore, # v5.5.1 deprecated + TOKEN_REFN: self.__obje_refn, + TOKEN_RIN: self.__obje_rin, + TOKEN_CHAN: self.__obje_chan, + } + self.func_list.append(self.obje_func) + + self.parse_addr_tbl = { + TOKEN_DATE: self.__address_date, + TOKEN_ADR1: self.__address_adr1, + TOKEN_ADR2: self.__address_adr2, + TOKEN_CITY: self.__address_city, + TOKEN_STAE: self.__address_state, + TOKEN_POST: self.__address_post, + TOKEN_CTRY: self.__address_country, + TOKEN_PHON: self.__ignore, + TOKEN_SOUR: self.__address_sour, + TOKEN_NOTE: self.__address_note, + TOKEN_RNOTE: self.__address_note, + TOKEN__LOC: self.__ignore, + TOKEN__NAME: self.__ignore, + TOKEN_IGNORE: self.__ignore, + TOKEN_TYPE: self.__ignore, + TOKEN_CAUS: self.__ignore, + } + self.func_list.append(self.parse_addr_tbl) + + self.event_cause_tbl = { + TOKEN_SOUR: self.__event_cause_source, + } + self.func_list.append(self.event_cause_tbl) + + self.event_place_map = { + TOKEN_NOTE: self.__event_place_note, + TOKEN_RNOTE: self.__event_place_note, + TOKEN_FORM: self.__event_place_form, + # Not legal. + TOKEN_OBJE: self.__event_place_object, + TOKEN_SOUR: self.__event_place_sour, + TOKEN__LOC: self.__ignore, + TOKEN_MAP: self.__place_map, + # Not legal, but generated by Ultimate Family Tree + TOKEN_QUAY: self.__ignore, + } + self.func_list.append(self.event_place_map) + + self.place_map_tbl = { + TOKEN_LATI: self.__place_lati, + TOKEN_LONG: self.__place_long, + } + self.func_list.append(self.place_map_tbl) + + self.repo_ref_tbl = { + TOKEN_CALN: self.__repo_ref_call, + TOKEN_NOTE: self.__repo_ref_note, + TOKEN_RNOTE: self.__repo_ref_note, + TOKEN_MEDI: self.__repo_ref_medi, + TOKEN_IGNORE: self.__ignore, + } + self.func_list.append(self.repo_ref_tbl) + + self.parse_person_adopt = { + TOKEN_ADOP: self.__person_adopt_famc_adopt, + } + self.func_list.append(self.parse_person_adopt) + + self.opt_note_tbl = { + TOKEN_RNOTE: self.__optional_note, + TOKEN_NOTE: self.__optional_note, + } + self.func_list.append(self.opt_note_tbl) + + self.citation_data_tbl = { + TOKEN_DATE: self.__citation_data_date, + TOKEN_TEXT: self.__citation_data_text, + TOKEN_RNOTE: self.__citation_data_note, + TOKEN_NOTE: self.__citation_data_note, + } + self.func_list.append(self.citation_data_tbl) + + self.citation_even_tbl = { + TOKEN_ROLE: self.__citation_even_role, + } + self.func_list.append(self.citation_even_tbl) + + # + # Parse table for <
> record below the level 0 HEAD tag + # + # n HEAD {1:1} + # +1 SOUR {1:1} + # +2 VERS {0:1} + # +2 NAME {0:1} + # +2 CORP {0:1} + # +3 <> {0:1} + # +2 DATA {0:1} + # +3 DATE {0:1} + # +3 COPR {0:1} + # +1 DEST {0:1*} + # +1 DATE {0:1} + # +2 TIME {0:1} + # +1 SUBM @@ {1:1} + # +1 SUBN @@ {0:1} + # +1 FILE {0:1} + # +1 COPR {0:1} + # +1 GEDC {1:1} + # +2 VERS {1:1} + # +2 FORM {1:1} + # +1 CHAR {1:1} + # +2 VERS {0:1} + # +1 LANG {0:1} + # +1 PLAC {0:1} + # +2 FORM {1:1} + # +1 NOTE {0:1} + # +2 [CONT|CONC] {0:M} + + # * NOTE: Submissions to the Family History Department for Ancestral + # File submission or for clearing temple ordinances must use a + # DESTination of ANSTFILE or TempleReady. + + self.head_parse_tbl = { + TOKEN_SOUR: self.__header_sour, + TOKEN_NAME: self.__header_sour_name, # This should be below SOUR + TOKEN_VERS: self.__header_sour_vers, # This should be below SOUR + TOKEN_FILE: self.__header_file, + TOKEN_COPR: self.__header_copr, + TOKEN_SUBM: self.__header_subm, + TOKEN_CORP: self.__ignore, # This should be below SOUR + TOKEN_DATA: self.__ignore, # This should be below SOUR + TOKEN_SUBN: self.__header_subn, + TOKEN_LANG: self.__header_lang, + TOKEN_TIME: self.__ignore, # This should be below DATE + TOKEN_DEST: self.__header_dest, + TOKEN_CHAR: self.__header_char, + TOKEN_GEDC: self.__header_gedc, + TOKEN_PLAC: self.__header_plac, + TOKEN_DATE: self.__header_date, + TOKEN_NOTE: self.__header_note, + TOKEN__SCHEMA: self.__ignore, + } + self.func_list.append(self.head_parse_tbl) + + self.header_sour_parse_tbl = { + TOKEN_VERS: self.__header_sour_vers, + TOKEN_NAME: self.__header_sour_name, + TOKEN_CORP: self.__header_sour_corp, + TOKEN_DATA: self.__header_sour_data, + } + self.func_list.append(self.header_sour_parse_tbl) + + self.header_sour_data = { + TOKEN_DATE: self.__header_sour_date, + TOKEN_COPR: self.__header_sour_copr, + } + self.func_list.append(self.header_sour_data) + + self.header_corp_addr = { + TOKEN_ADDR: self.__repo_addr, + TOKEN_PHON: self.__repo_phon, + TOKEN_FAX: self.__repo_fax, + TOKEN_WWW: self.__repo_www, + TOKEN_EMAIL: self.__repo_email, + } + self.func_list.append(self.header_corp_addr) + + self.header_subm = { + TOKEN_NAME: self.__header_subm_name, + } + self.func_list.append(self.header_subm) + + self.place_form = { + TOKEN_FORM: self.__place_form, + } + self.func_list.append(self.place_form) + + # + # Parse table for <> below the level 0 NOTE tag + # + # n @@ NOTE {1:1} + # +1 [ CONC | CONT] {0:M} + # +1 <> {0:M} + # +1 REFN {0:M} + # +2 TYPE {0:1} + # +1 RIN {0:1} + # +1 <> {0:1} + + self.note_parse_tbl = { + TOKEN_SOUR: self.__ignore, + TOKEN_REFN: self.__ignore, + TOKEN_RIN: self.__ignore, + TOKEN_CHAN: self.__note_chan, + } + self.func_list.append(self.note_parse_tbl) + + # look for existing place titles, build a map + self.place_names = defaultdict(list) + cursor = dbase.get_place_cursor() + data = next(cursor) + while data: + handle, val = data + self.place_names[val.title].append(handle) + data = next(cursor) + cursor.close() + + enc = stage_one.get_encoding() + + if enc == "ANSEL": + rdr = AnselReader(ifile, self.__add_msg) + elif enc in ("UTF-8", "UTF8", "UTF_8_SIG"): + rdr = UTF8Reader(ifile, self.__add_msg, enc) + elif enc in ("UTF-16LE", "UTF-16BE", "UTF16", "UNICODE"): + rdr = UTF16Reader(ifile, self.__add_msg) + elif enc in ("CP1252", "WINDOWS-1252"): + rdr = CP1252Reader(ifile, self.__add_msg) + else: + rdr = AnsiReader(ifile, self.__add_msg) + + self.lexer = Lexer(rdr, self.__add_msg) + self.filename = filename + self.backoff = False + + fullpath = os.path.normpath(os.path.abspath(filename)) + self.geddir = os.path.dirname(fullpath) + + self.error_count = 0 + amap = PERSONALCONSTANTATTRIBUTES + + self.attrs = list(amap.values()) + self.gedattr = dict([key, val] for val, key in amap.items()) + + def parse_gedcom_file(self, use_trans=False): + """ + Parses the opened GEDCOM file. + + LINEAGE_LINKED_GEDCOM: = + 0 <
> {1:1} + 0 <> {0:1} + 0 <> {1:M} + 0 TRLR {1:1} + + """ + with DbTxn(_("GEDCOM import"), self.dbase, not use_trans) as self.trans: + self.dbase.disable_signals() + self.__parse_header_head() + self.want_parse_warnings = False + self.want_parse_warnings = True + if self.use_def_src: + self.dbase.add_source(self.def_src, self.trans) + if self.default_tag and self.default_tag.handle is None: + self.dbase.add_tag(self.default_tag, self.trans) + self.__parse_header() + self.__parse_record() + self.__parse_trailer() + for title, handle in self.inline_srcs.items(): + src = Source() + src.set_handle(handle) + src.set_title(title) + self.dbase.add_source(src, self.trans) + self.__clean_up() + + self.place_import.generate_hierarchy(self.trans) + + if not self.dbase.get_feature("skip-check-xref"): + self.__check_xref() + self.dbase.enable_signals() + self.dbase.request_rebuild() + if self.number_of_errors == 0: + message = _("GEDCOM import report: No errors detected") + else: + message = ( + _("GEDCOM import report: %s errors detected") % self.number_of_errors + ) + if hasattr(self.user.uistate, "window"): + parent_window = self.user.uistate.window + else: + parent_window = None + self.user.info( + message, "".join(self.errors), parent=parent_window, monospaced=True + ) + + def __clean_up(self): + """ + Break circular references to parsing methods stored in dictionaries + to aid garbage collection + """ + for func_map in self.func_list: + for key in list(func_map.keys()): + del func_map[key] + del func_map + del self.func_list + del self.update + self.lexer.clean_up() + + def __find_person_handle(self, gramps_id): + """ + Return the database handle associated with the person's Gramps ID + """ + return self.__find_from_handle(gramps_id, self.gid2id) + + def __find_family_handle(self, gramps_id): + """ + Return the database handle associated with the family's Gramps ID + """ + return self.__find_from_handle(gramps_id, self.fid2id) + + def __find_media_handle(self, gramps_id): + """ + Return the database handle associated with the media object's Gramps ID + """ + return self.__find_from_handle(gramps_id, self.oid2id) + + def __find_note_handle(self, gramps_id): + """ + Return the database handle associated with the media object's Gramps ID + """ + return self.__find_from_handle(gramps_id, self.nid2id) + + def __find_or_create_person(self, gramps_id): + """ + Finds or creates a person based on the Gramps ID. If the ID is + already used (is in the db), we return the item in the db. Otherwise, + we create a new person, assign the handle and Gramps ID. + """ + intid = self.gid2id.get(gramps_id) + if self.dbase.has_person_handle(intid): + person = data_to_object(self.dbase.get_raw_person_data(intid)) + else: + person = Person() + intid = self.__find_from_handle(gramps_id, self.gid2id) + person.set_handle(intid) + person.set_gramps_id(gramps_id) + return person + + def __find_or_create_family(self, gramps_id): + """ + Finds or creates a family based on the Gramps ID. If the ID is + already used (is in the db), we return the item in the db. Otherwise, + we create a new family, assign the handle and Gramps ID. + """ + intid = self.fid2id.get(gramps_id) + if self.dbase.has_family_handle(intid): + family = data_to_object(self.dbase.get_raw_family_data(intid)) + else: + family = Family() + intid = self.__find_from_handle(gramps_id, self.fid2id) + family.set_handle(intid) + family.set_gramps_id(gramps_id) + # Add a counter for reordering the children later: + family._child_ref_count = 0 + return family + + def __find_or_create_media(self, gramps_id): + """ + Finds or creates a media object based on the Gramps ID. If the ID is + already used (is in the db), we return the item in the db. Otherwise, + we create a new media object, assign the handle and Gramps ID. + """ + intid = self.oid2id.get(gramps_id) + if self.dbase.has_media_handle(intid): + obj = data_to_object(self.dbase.get_raw_media_data(intid)) + else: + obj = Media() + intid = self.__find_from_handle(gramps_id, self.oid2id) + obj.set_handle(intid) + obj.set_gramps_id(gramps_id) + return obj + + def __find_or_create_source(self, gramps_id): + """ + Find or create a source based on the Gramps ID. + + If the ID is already used (is in the db), we return the item in the + db. Otherwise, we create a new source, assign the handle and Gramps ID. + + """ + intid = self.sid2id.get(gramps_id) + if self.dbase.has_source_handle(intid): + obj = data_to_object(self.dbase.get_raw_source_data(intid)) + else: + obj = Source() + intid = self.__find_from_handle(gramps_id, self.sid2id) + obj.set_handle(intid) + obj.set_gramps_id(gramps_id) + return obj + + def __find_or_create_repository(self, gramps_id): + """ + Finds or creates a repository based on the Gramps ID. If the ID is + already used (is in the db), we return the item in the db. Otherwise, + we create a new repository, assign the handle and Gramps ID. + + Some GEDCOM "flavors" destroy the specification, and declare the + repository inline instead of in a object. + """ + intid = self.rid2id.get(gramps_id) + if self.dbase.has_repository_handle(intid): + repository = data_to_object(self.dbase.get_raw_repository_data(intid)) + else: + repository = Repository() + intid = self.__find_from_handle(gramps_id, self.rid2id) + repository.set_handle(intid) + repository.set_gramps_id(gramps_id) + return repository + + def __find_or_create_note(self, gramps_id): + """ + Finds or creates a note based on the Gramps ID. If the ID is + already used (is in the db), we return the item in the db. Otherwise, + we create a new note, assign the handle and Gramps ID. + If no Gramps ID is passed in, we not only make a Note with GID, we + commit it. + """ + if not gramps_id: + need_commit = True + gramps_id = self.dbase.find_next_note_gramps_id() + else: + need_commit = False + + intid = self.nid2id.get(gramps_id) + if self.dbase.has_note_handle(intid): + note = data_to_object(self.dbase.get_raw_note_data(intid)) + else: + note = Note() + intid = self.__find_from_handle(gramps_id, self.nid2id) + note.set_handle(intid) + note.set_gramps_id(gramps_id) + if need_commit: + self.dbase.add_note(note, self.trans) + return note + + def __loc_is_empty(self, location): + """ + Determines whether a location is empty. + + @param location: The current location + @type location: gen.lib.Location + @return True of False + """ + if location is None: + return True + elif object_to_dict(location) == self._EMPTY_LOC: + return True + elif location.is_empty(): + return True + return False + + def __find_place(self, title, location, placeref_list): + """ + Finds an existing place based on the title and primary location. + + @param title: The place title + @type title: string + @param location: The current location + @type location: gen.lib.Location + @return gen.lib.Place + """ + for place_handle in self.place_names[title]: + place = self.dbase.get_place_from_handle(place_handle) + if place.get_title() == title: + if ( + self.__loc_is_empty(location) + and self.__loc_is_empty(self.__get_first_loc(place)) + and place.get_placeref_list() == placeref_list + ): + return place + elif ( + not self.__loc_is_empty(location) + and not self.__loc_is_empty(self.__get_first_loc(place)) + and self.__get_first_loc(place).is_equivalent(location) == IDENTICAL + ) and place.get_placeref_list() == placeref_list: + return place + return None + + def __add_place(self, event, sub_state): + """ + Add a new place to an event if not already present, or update a + place. + + @param event: The event + @type event: gen.lib.Event + @param substate: The sub-state for PLAC or ADDR elements (i.e. parsed + by event_parse_tbl) + @type sub_state: CurrentState + """ + if sub_state.place: + # see whether this place already exists + place = self.__find_place( + sub_state.place.get_title(), + self.__get_first_loc(sub_state.place), + sub_state.place.get_placeref_list(), + ) + if place is None: + place = sub_state.place + place_title = _pd.display(self.dbase, place) + location = sub_state.pf.load_place( + self.place_import, place, place_title + ) + self.dbase.add_place(place, self.trans) + # if 'location was created, then store it, now that we have a + # handle. + if location: + self.place_import.store_location(location, place.handle) + self.place_names[place.get_title()].append(place.get_handle()) + event.set_place_handle(place.get_handle()) + else: + place.merge(sub_state.place) + place_title = _pd.display(self.dbase, place) + location = sub_state.pf.load_place( + self.place_import, place, place_title + ) + self.dbase.commit_place(place, self.trans) + if location: + self.place_import.store_location(location, place.handle) + event.set_place_handle(place.get_handle()) + + def __find_file(self, fullname, altpath): + # try to find the media file + fullname = fullname.replace("\\", os.path.sep) + + try: + if os.path.isfile(fullname): + return (1, fullname) + except UnicodeEncodeError: + # FIXME: problem possibly caused by umlaut/accented character + # in filename + return (0, fullname) + # strip off Windows drive letter, if present + if len(fullname) > 3 and fullname[1] == ":": + fullname = fullname[2:] + # look where we found the '.ged', using the full path in fullname + other = os.path.join(altpath, fullname) + if os.path.isfile(other): + return (1, other) + # lets try reducing to just where we found '.ged' + other = os.path.join(altpath, os.path.basename(fullname)) + if os.path.isfile(other): + return (1, other) + # lets try using the base path for relative media paths + other = os.path.join(media_path(self.dbase), fullname) + if os.path.isfile(other): + return (1, fullname) + # lets try using the base path for relative media paths with base name + other = os.path.join(media_path(self.dbase), os.path.basename(fullname)) + if os.path.isfile(other): + return (1, os.path.basename(fullname)) + return (0, fullname) + + def __get_next_line(self): + """ + Get the next line for analysis from the lexical analyzer. Return the + same value if the _backup flag is set. + """ + if not self.backoff: + self.groups = self.lexer.readline() + self.update() + + # EOF ? + if not self.groups: + self.backoff = False + # We will add the truncation warning message to the error + # messages report, even though it probably won't be reported + # because the exception below gets raised before the report is + # produced. We do this in case __add_msg is changed in the + # future to do something else + self.__add_msg(self.__TRUNC_MSG) + self.groups = None + raise GedcomError(self.__TRUNC_MSG) + + self.backoff = False + return self.groups + + def __chk_subordinate(self, level, state, token): + """ + checks for a single subordinate line with specific token. If any other + lines are present, they are not understood. + + @param level: Current level in the file + @type level: int + @param state: The current state + @type state: CurrentState + @param token: The token to search for + @type token: int + """ + skips = 0 + got_line = None + while True: + line = self.__get_next_line() + if self.__level_is_finished(line, level): + if skips: + # This improves formatting when there are long sequences of + # skipped lines + self.__add_msg("", None, None) + return got_line + if line.token == token: + got_line = line + else: + self.__add_msg(_("Line ignored as not understood"), line, state) + skips += 1 + + def __undefined(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__not_recognized(line, state) + + def __ignore(self, line, state): + """ + Prints a message when an unexpected token is found. If the token is + known, then the line is considered "not supported", otherwise the line + is "not understood". + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if line.token == TOKEN_UNKNOWN: + self.__add_msg(_("Line ignored as not understood"), line, state) + else: + self.__add_msg( + _( + "Tag recognized but not supported: '" + + str(line.data) + + "' on line " + ), + line, + state, + ) + self.__skip_subordinate_levels(line.level + 1, state) + + def __not_recognized(self, line, state): + """ + Prints a message when an undefined token is found. All subordinate + items to the current item are ignored. + + @param level: Current level in the file + @type level: int + """ + self.__add_msg(_("Line ignored as not understood"), line, state) + self.__skip_subordinate_levels(line.level + 1, state) + + def __skip_record(self, _line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__skip_subordinate_levels(2, state) + + def __skip_subordinate_levels(self, level, state): + """ + Skip add lines of the specified level or lower. + """ + skips = 0 + while True: + line = self.__get_next_line() + if self.__level_is_finished(line, level): + if skips: + # This improves formatting when there are long sequences of + # skipped lines + self.__add_msg("", None, None) + return + self.__add_msg(_("Skipped subordinate line"), line, state) + skips += 1 + + def __level_is_finished(self, text, level): + """ + Check to see if the level has been completed, indicated by finding + a level indiciated by the passed level value. If the level is finished, + then make sure to call self._backup to reset the text pointer. + """ + done = text.level < level + if done: + self._backup() + return done + + def __add_msg(self, problem, line=None, state=None): + if problem != "": + self.number_of_errors += 1 + if line: + prob_width = 100 + problem = problem.ljust(prob_width)[0 : (prob_width - 1)] + text = str(line.data).replace("\n", "\n".ljust(prob_width + 22)) + message = "%s Line %5d: %s %s %s\n" % ( + problem, + line.line, + line.level, + line.token_text, + text, + ) + else: + message = problem + "\n" + if state: + state.msg += message + self.errors.append(message) + + def __add_warn(self, problem, line=None, state=None): + """Add an advisory message to the import report that does NOT + count as an error. Mirrors the +1/-1 pattern used at the end + of ``__finish_import`` for the unknown-references note. + Used for things like ambiguous-numeric-date warnings (Mantis 9298) + where the importer made a choice the user may want to audit + but no record was rejected or truncated.""" + self.__add_msg(problem, line, state) + self.number_of_errors -= 1 + + def __warn_if_ambiguous_numeric_date(self, line, state): + """If ``line.data`` (a :class:`Date`) was parsed from a bare + numeric GEDCOM DATE (e.g. ``7/11/1959``) and the parser + committed to a day/month reading, log a warning so the user + can verify it. Bare numerics are not GEDCOM-spec and are + unavoidably ambiguous; the importer defaults to MM/DD/YYYY, + which silently swaps day and month for sources that emit + DD/MM/YYYY (Mantis 9298).""" + date = line.data + if date.get_modifier() == Date.MOD_TEXTONLY: + # day > 12 or otherwise unparseable — already visibly + # flagged in the Events view as a textual (bold) date. + return + text = date.get_text() + if not text or not BARE_NUMERIC_DATE.match(text): + return + # ``line`` is intentionally not passed: ``__add_msg`` truncates + # the problem string to 99 columns when a line is supplied, which + # drops the actionable half of the warning. The text below + # contains the original GEDCOM date string verbatim, so the user + # can still grep their .ged for it without the line number. + self.__add_warn( + _( + "Ambiguous numeric date '%(orig)s' interpreted as " + "MM/DD/YYYY (US format) = %(year)d-%(month)02d-%(day)02d. " + "If your source uses DD/MM/YYYY (e.g. ancestry.com " + "exports), verify and correct after import." + ) + % { + "orig": text, + "year": date.get_year(), + "month": date.get_month(), + "day": date.get_day(), + }, + None, + state, + ) + + def __check_msgs(self, record_name, state, obj): + if state.msg == "": + return + message = _("Records not imported into ") + record_name + ":\n\n" + state.msg + new_note = Note() + tag = StyledTextTag( + StyledTextTagType.FONTFACE, "Monospace", [(0, len(message))] + ) + text = StyledText(message, [tag]) + new_note.set_styledtext(text) + new_note.set_handle(create_id()) + gramps_id = self.nid_map[""] + new_note.set_gramps_id(gramps_id) + note_type = NoteType() + note_type.set((NoteType.CUSTOM, _("GEDCOM import"))) + new_note.set_type(note_type) + self.dbase.add_note(new_note, self.trans) + # If possible, attach the note to the relevant object + if obj: + obj.add_note(new_note.get_handle()) + + def _backup(self): + """ + Set the _backup flag so that the current line can be accessed by the + next level up. + """ + self.backoff = True + + def __check_xref(self): + def __check(_map, has_gid_func, class_func, commit_func, gramps_id2handle, msg): + for input_id, gramps_id in _map.map().items(): + # Check whether an object exists for the mapped gramps_id + if not has_gid_func(gramps_id): + _handle = self.__find_from_handle(gramps_id, gramps_id2handle) + if msg == "FAM": + make_unknown( + gramps_id, + self.explanation.handle, + class_func, + commit_func, + self.trans, + db=self.dbase, + ) + self.missing_references += 1 + self.__add_msg( + _( + "Error: %(msg)s '%(gramps_id)s'" + " (input as @%(xref)s@) not in input" + " GEDCOM. Record synthesised" + ) + % {"msg": msg, "gramps_id": gramps_id, "xref": input_id} + ) + else: + make_unknown( + gramps_id, + self.explanation.handle, + class_func, + commit_func, + self.trans, + ) + self.missing_references += 1 + self.__add_msg( + _( + "Error: %(msg)s '%(gramps_id)s'" + " (input as @%(xref)s@) not in input" + " GEDCOM. Record with typifying" + " attribute 'Unknown' created" + ) + % {"msg": msg, "gramps_id": gramps_id, "xref": input_id} + ) + + self.explanation = create_explanation_note(self.dbase) + + self.missing_references = 0 + __check( + self.pid_map, + self.dbase.has_person_gramps_id, + self.__find_or_create_person, + self.dbase.commit_person, + self.gid2id, + "INDI", + ) + __check( + self.fid_map, + self.dbase.has_family_gramps_id, + self.__find_or_create_family, + self.dbase.commit_family, + self.fid2id, + "FAM", + ) + __check( + self.sid_map, + self.dbase.has_source_gramps_id, + self.__find_or_create_source, + self.dbase.commit_source, + self.sid2id, + "SOUR", + ) + __check( + self.oid_map, + self.dbase.has_media_gramps_id, + self.__find_or_create_media, + self.dbase.commit_media, + self.oid2id, + "OBJE", + ) + __check( + self.rid_map, + self.dbase.has_repository_gramps_id, + self.__find_or_create_repository, + self.dbase.commit_repository, + self.rid2id, + "REPO", + ) + __check( + self.nid_map, + self.dbase.has_note_gramps_id, + self.__find_or_create_note, + self.dbase.commit_note, + self.nid2id, + "NOTE", + ) + + # Check persons membership in referenced families + def __input_fid(gramps_id): + for key, val in self.fid_map.map().items(): + if val == gramps_id: + return key + + for input_id, gramps_id in self.pid_map.map().items(): + person_handle = self.__find_from_handle(gramps_id, self.gid2id) + person = self.dbase.get_person_from_handle(person_handle) + for family_handle in person.get_family_handle_list(): + family = self.dbase.get_family_from_handle(family_handle) + if ( + family + and family.get_father_handle() != person_handle + and family.get_mother_handle() != person_handle + ): + person.remove_family_handle(family_handle) + self.dbase.commit_person(person, self.trans) + self.__add_msg( + _( + "Error: family '%(family)s' (input as" + " @%(orig_family)s@) person %(person)s" + " (input as %(orig_person)s) is not a" + " member of the referenced family." + " Family reference removed from person" + ) + % { + "family": family.gramps_id, + "orig_family": __input_fid(family.gramps_id), + "person": person.gramps_id, + "orig_person": input_id, + } + ) + + def __input_pid(gramps_id): + for key, val in self.pid_map.map().items(): + if val == gramps_id: + return key + + for input_id, gramps_id in self.fid_map.map().items(): + family_handle = self.__find_from_handle(gramps_id, self.fid2id) + family = self.dbase.get_family_from_handle(family_handle) + father_handle = family.get_father_handle() + mother_handle = family.get_mother_handle() + + if father_handle: + father = self.dbase.get_person_from_handle(father_handle) + if father and family_handle not in father.get_family_handle_list(): + father.add_family_handle(family_handle) + self.dbase.commit_person(father, self.trans) + self.__add_msg( + "Error: family '%(family)s' (input as" + " @%(orig_family)s@) father '%(father)s'" + " (input as '%(orig_father)s') does not " + "refer back to the family. Reference added." + % { + "family": family.gramps_id, + "orig_family": input_id, + "father": father.gramps_id, + "orig_father": __input_pid(father.gramps_id), + } + ) + + if mother_handle: + mother = self.dbase.get_person_from_handle(mother_handle) + if mother and family_handle not in mother.get_family_handle_list(): + mother.add_family_handle(family_handle) + self.dbase.commit_person(mother, self.trans) + self.__add_msg( + "Error: family '%(family)s' (input as" + " @%(orig_family)s@) mother '%(mother)s'" + " (input as '%(orig_mother)s') does not " + "refer back to the family. Reference added." + % { + "family": family.gramps_id, + "orig_family": input_id, + "mother": mother.gramps_id, + "orig_mother": __input_pid(mother.gramps_id), + } + ) + + for child_ref in family.get_child_ref_list(): + child_handle = child_ref.ref + child = self.dbase.get_person_from_handle(child_handle) + if child: + if family_handle not in child.get_parent_family_handle_list(): + # The referenced child has no reference to the family. + # There was a link from the FAM record to the child, + # but no FAMC link from the child to the FAM. + child.add_parent_family_handle(family_handle) + self.dbase.commit_person(child, self.trans) + self.__add_msg( + "Error: family '%(family)s' (input as" + " @%(orig_family)s@) child '%(child)s'" + " (input as '%(orig_child)s') does not " + "refer back to the family. " + "Reference added." + % { + "family": family.gramps_id, + "orig_family": input_id, + "child": child.gramps_id, + "orig_child": __input_pid(child.gramps_id), + } + ) + + if self.missing_references: + self.dbase.commit_note(self.explanation, self.trans, time.time()) + txt = _( + "\nThe imported file was not self-contained.\n" + "To correct for that, %(new)d objects were created and\n" + "their typifying attribute was set to 'Unknown'.\n" + "Where possible these 'Unknown' objects are \n" + "referenced by note %(unknown)s.\n" + ) % {"new": self.missing_references, "unknown": self.explanation.gramps_id} + self.__add_msg(txt) + self.number_of_errors -= 1 + + def __merge_address(self, free_form_address, addr, line, state): + """ + Merge freeform and structured addrssses. + n ADDR {0:1} + +1 CONT {0:M} + +1 ADR1 {0:1} (Street) + +1 ADR2 {0:1} (Locality) + +1 CITY {0:1} + +1 STAE {0:1} + +1 POST {0:1} + +1 CTRY {0:1} + + This is done along the lines suggested by Tamura Jones in + https://www.tamurajones.net/GEDCOMADDR.xhtml as a result of bug 6382. + "When a GEDCOM reader encounters a double address, it should read the + structured address. ... A GEDCOM reader that does verify that the + addresses are the same should issue an error if they are not". + + This is called for SUBMitter addresses (__subm_addr), INDIvidual + addresses (__person_addr), REPO addresses and HEADer corp address + (__repo_address) and EVENt addresses (__event_adr). + + The structured address (if any) will have been accumulated into an + object of type LocationBase, which will either be a Location, or an + Address object. + + If ADDR is provided, but none of ADR1, ADR2, CITY, STAE, or POST (not + CTRY), then Street is set to the freeform address. N.B. this is a + change for Repository addresses and HEADer Corp address where + previously the free-form address was deconstrucated into different + structured components. N.B. PAF provides a free-form address and a + country, so this allows for that case. + + If both forms of address are provided, then the structured address is + used, and if the ADDR/CONT contains anything not in the structured + address, a warning is issued. + + If just ADR1, ADR2, CITY, STAE, POST or CTRY are provided (this is not + actually legal GEDCOM symtax, but may be possible by GEDCOM extensions) + then just the structrued address is used. + The routine returns a string suitable for a title. + """ + title = "" + free_form_address = free_form_address.replace("\n", ", ") + if not ( + addr.get_street() + or addr.get_locality() + or addr.get_city() + or addr.get_state() + or addr.get_postal_code() + ): + addr.set_street(free_form_address) + return free_form_address + else: + # structured address provided + addr_list = free_form_address.split(",") + str_list = [] + for func in ( + addr.get_street(), + addr.get_locality(), + addr.get_city(), + addr.get_state(), + addr.get_postal_code(), + addr.get_country(), + ): + str_list += [i.strip("," + string.whitespace) for i in func.split("\n")] + for elmn in addr_list: + if elmn.strip("," + string.whitespace) not in str_list: + # message means that the element %s was ignored, but + # expressed the wrong way round because the message is + # truncated for output + self.__add_msg(_("ADDR element ignored '%s'" % elmn), line, state) + # The free-form address ADDR is discarded + # Assemble a title out of structured address + for elmn in str_list: + if elmn: + if title != "": + # TODO for Arabic, should the next comma be translated? + title += ", " + title += elmn + return title + + def __parse_trailer(self): + """ + Looks for the expected TRLR token + """ + try: + line = self.__get_next_line() + if line and line.token != TOKEN_TRLR: + state = CurrentState() + self.__not_recognized(line, state) + self.__check_msgs(_("TRLR (trailer)"), state, None) + except TypeError: + return + + def __parse_submitter(self, line): + """ + Parses the submitter data + + n @@ SUBM + +1 NAME + +1 <> + +1 <> + +1 LANG + +1 RFN + +1 RIN + +1 <> + """ + researcher = Researcher() + state = CurrentState() + state.res = researcher + state.level = 1 + repo = Repository() + state.repo = repo + self.__parse_level(state, self.subm_parse_tbl, self.__undefined) + # If this is the submitter that we were told about in the HEADer, then + # we will need to update the researcher + if line.token_text == self.subm and self.import_researcher: + self.dbase.set_researcher(state.res) + + localized_submitter = _("(Submitter):") + if state.res.get_name() == "": + submitter_name = "SUBM %s @%s@" % (localized_submitter, line.token_text) + else: + submitter_name = "SUBM %s (@%s@) %s" % ( + localized_submitter, + line.token_text, + state.res.get_name(), + ) + if self.use_def_src: + repo.set_name(submitter_name) + repo.set_handle(create_id()) + repo.set_gramps_id(self.rid_map[""]) + + addr = Address() + addr.set_street(state.res.get_address()) + addr.set_locality(state.res.get_locality()) + addr.set_city(state.res.get_city()) + addr.set_state(state.res.get_state()) + addr.set_country(state.res.get_country()) + addr.set_postal_code(state.res.get_postal_code()) + addr.set_county(state.res.get_county()) + addr.set_phone(state.res.get_phone()) + repo.add_address(addr) + rtype = RepositoryType() + rtype.set((RepositoryType.CUSTOM, _("GEDCOM data"))) + repo.set_type(rtype) + self.__check_msgs(submitter_name, state, repo) + self.dbase.commit_repository(repo, self.trans, state.repo.change) + repo_ref = RepoRef() + repo_ref.set_reference_handle(repo.handle) + mtype = SourceMediaType() + mtype.set((SourceMediaType.UNKNOWN, "")) + repo_ref.set_media_type(mtype) + self.def_src.add_repo_reference(repo_ref) + self.dbase.commit_source(self.def_src, self.trans) + else: + self.__check_msgs(submitter_name, state, None) + + def __parse_record(self): + """ + Parse the top level (0 level) instances. + RECORD: = + [ + n <> {1:1} + | + n <> {1:1} + | + n <> {1:M} + | + n <> {1:1} + | + n <> {1:1} + | + n <> {1:1} + | + n <> {1:1} + ] + + This also deals with the SUBN (submission) record, of which there + should be exactly one. + """ + while True: + line = self.__get_next_line() + key = line.data + if not line or line.token == TOKEN_TRLR: + self._backup() + break + if line.token == TOKEN_UNKNOWN: + state = CurrentState() + self.__add_msg(_("Unknown tag"), line, state) + self.__skip_subordinate_levels(1, state) + self.__check_msgs(_("Top Level"), state, None) + elif key in ("FAM", "FAMILY"): + self.__parse_fam(line) + elif key in ("INDI", "INDIVIDUAL"): + self.__parse_indi(line) + elif key in ("OBJE", "OBJECT"): + self.__parse_obje(line) + elif key in ("REPO", "REPOSITORY"): + self.__parse_repo(line) + elif key in ("SUBM", "SUBMITTER"): + self.__parse_submitter(line) + elif key == "SUBN": + state = CurrentState(level=1) + self.__parse_submission(line, state) + self.__check_msgs(_("Top Level"), state, None) + elif line.token in (TOKEN_SUBM, TOKEN_SUBN, TOKEN_IGNORE): + state = CurrentState() + self.__skip_subordinate_levels(1, state) + self.__check_msgs(_("Top Level"), state, None) + elif key in ("SOUR", "SOURCE"): + self.__parse_source(line.token_text, 1) + elif line.data.startswith(("SOUR ", "SOURCE ")): + # A source formatted in a single line, for example: + # 0 @S62@ SOUR This is the title of the source + source = self.__find_or_create_source(self.sid_map[line.data]) + source.set_title(line.data[5:]) + self.dbase.commit_source(source, self.trans) + elif key[0:4] == "NOTE": + try: + line.data = line.data[5:] + except: + # don't think this path is ever taken, but if it is.. + # ensure a message is emitted & subordinates skipped + line.data = None + self.__parse_inline_note(line, 1) + else: + state = CurrentState() + self.__not_recognized(line, state) + self.__check_msgs(_("Top Level"), state, None) + + def __parse_level(self, state, __map, default): + """ + Loop trough the current GEDCOM level, calling the appropriate + functions associated with the TOKEN. + + If no matching function for the token is found, the default function + is called instead. + + """ + while True: + line = self.__get_next_line() + if line.level < state.level: + self.backoff = True + return + else: + func = __map.get(line.token, default) + func(line, state) + + # ---------------------------------------------------------------------- + # + # INDI parsing + # + # ---------------------------------------------------------------------- + + def __parse_indi(self, line): + """ + Handling of the GEDCOM INDI tag and all lines subordinate to the + current line. + + n @XREF:INDI@ INDI {1:1} + +1 RESN {0:1} + +1 <> {0:M} + +1 SEX {0:1} + +1 <> {0:M} + +1 <> {0:M} + +1 <> {0:M} + +1 <> {0:M} + +1 <> {0:M} + +1 SUBM @@ {0:M} + +1 <> {0:M} + +1 ALIA @@ {0:M} + +1 ANCI @@ {0:M} + +1 DESI @@ {0:M} + +1 <> {0:M} + +1 <> {0:M} + +1 <> {0:M} + +1 RFN {0:1} + +1 AFN {0:1} + +1 REFN {0:M} + +2 TYPE {0:1} + +1 RIN {0:1} + +1 <> {0:1} + """ + + # find the person + real_id = self.pid_map[line.token_text] + person = self.__find_or_create_person(real_id) + + # set up the state for the parsing + state = CurrentState(person=person, level=1) + + # do the actual parsing + self.__parse_level(state, self.indi_parse_tbl, self.__person_event) + + # Add the default reference if no source has found + self.__add_default_source(person) + + # Add a default tag if provided + self.__add_default_tag(person) + + # Set up primary photo if present + self.__do_photo(state) + + self.__check_msgs( + _("INDI (individual) Gramps ID %s") % person.get_gramps_id(), state, person + ) + # commit the person to the database + self.dbase.commit_person(person, self.trans, state.person.change) + + def __person_sour(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + citation_handle = self.handle_source(line, state.level, state) + state.person.add_citation(citation_handle) + + def __person_refn(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__do_refn(line, state, state.person) + + def __person_attr(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + attr = Attribute() + attr.set_type((AttributeType.CUSTOM, line.token_text)) + attr.set_value(line.data) + state.person.add_attribute(attr) + + def __person_event(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + # We can get here when a tag that is not valid in the indi_parse_tbl + # parse table is encountered. The tag may be of the form "_XXX". We + # try to convert to a friendly name, if fails use the tag itself as + # the TYPE in a custom event + cust_tag = CUSTOMEVENTTAGS.get(line.token_text, line.token_text) + cust_type = EventType((EventType.CUSTOM, cust_tag)) + event_ref = self.__build_event_pair( + state, cust_type, self.event_parse_tbl, str(line.data) + ) + state.person.add_event_ref(event_ref) + + def __fam_even(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + event_ref = self.__build_family_event_pair( + state, EventType.CUSTOM, self.event_parse_tbl, line.data + ) + state.family.add_event_ref(event_ref) + + def __person_chan(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_change(line, state.person, state.level + 1, state) + + def __person_resn(self, line, state): + """ + Parses the RESN tag, adding it as an attribute. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + attr = Attribute() + attr.set_type((AttributeType.CUSTOM, "RESN")) + state.person.add_attribute(attr) + + def __person_alt_name(self, line, state): + """ + This parses the standard GEDCOM structure: + + n @XREF:INDI@ INDI {1:1} + +1 ALIA @@ {0:M} + + The ALIA tag is supposed to cross reference another person. We will + store this in the Association record. + + ALIA {ALIAS}: = An indicator to link different record descriptions of a + person who may be the same person. + + Some systems use the ALIA tag as an alternate NAME tag, which is not + legal in GEDCOM, but oddly enough, is easy to support. This parses the + illegal (ALIA or ALIAS) or non-standard (_ALIA) GEDCOM. "1 ALIA" is + used by Family Tree Maker and Reunion. "1 ALIAS" and "1 _ALIA" do not + appear to be used. + + n @XREF:INDI@ INDI {1:1} + +1 {1:1} + +2 NPFX {0:1} + +2 GIVN {0:1} + +2 NICK {0:1} + +2 SPFX {0:1} + +2 SURN {0:1} + +2 NSFX {0:1} + +2 <> {0:M} + +3 <> {0:M} + +3 <> {0:M} + +2 <> {0:M} + where == ALIA | _ALIA | ALIAS + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if line.data == "": + self.__add_msg(_("Empty Alias ignored"), line, state) + self.__skip_subordinate_levels(state.level + 1, state) + elif line.data[0] == "@": + handle = self.__find_person_handle(self.pid_map[line.data]) + ref = PersonRef() + ref.ref = handle + ref.rel = "Alias" + state.person.add_person_ref(ref) + else: + self.__parse_alias_name(line, state) + + def __parse_alias_name(self, line, state): + """ + Parse a level 1 alias name and subsidiary levels when called from + __person_alt_name (when the does not start with @). + Also parses a level 2 alias name and subsidiary levels when called + from __name_alias. + + +1 {1:1} + +2 NPFX {0:1} + +2 GIVN {0:1} + +2 NICK {0:1} + +2 SPFX {0:1} + +2 SURN {0:1} + +2 NSFX {0:1} + +2 <> {0:M} + +3 <> {0:M} + +3 <> {0:M} + +2 <> {0:M} + where == ALIA | _ALIA | ALIAS + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + name = self.__parse_name_personal(line.data) + name.set_type(NameType.AKA) + state.person.add_alternate_name(name) + + # Create a new state, and parse the remainder of the NAME level + sub_state = CurrentState() + sub_state.person = state.person + sub_state.name = name + sub_state.level = state.level + 1 + + self.__parse_level(sub_state, self.name_parse_tbl, self.__undefined) + state.msg += sub_state.msg + + def __person_object(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__obje(line, state, state.person) + + def __person_photo(self, line, state): + """ + This handles the FTM _PHOTO feature, which identifies an OBJE to use + as the person's primary photo. + """ + state.photo = line.data # Just save it for now. + + def __person_name(self, line, state): + """ + Parsers the NAME token in a GEDCOM file. The text is in the format + of (according to the GEDCOM Spec): + > |//|//|//|// + We have encountered some variations that use: + > / + + The basic Name structure is: + + n NAME {1:1} + +1 NPFX {0:1} + +1 GIVN {0:1} + +1 NICK {0:1} + +1 SPFX {0:1} + +1 NSFX {0:1} + +1 <> {0:M} + +1 <> {0:M} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + + # build a Name structure from the text + + name = self.__parse_name_personal(line.data) + + # Add the name as the primary name if this is the first one that + # we have encountered for this person. Assume that if this is the + # first name, that it is a birth name. Otherwise, label it as an + # "Also Known As (AKA)". GEDCOM does not seem to have the concept + # of different name types + + if state.name_cnt == 0: + name.set_type(NameType.BIRTH) + state.person.set_primary_name(name) + else: + name.set_type(NameType.AKA) + state.person.add_alternate_name(name) + state.name_cnt += 1 + + # Create a new state, and parse the remainder of the NAME level + sub_state = CurrentState() + sub_state.person = state.person + sub_state.name = name + sub_state.level = state.level + 1 + + self.__parse_level(sub_state, self.name_parse_tbl, self.__undefined) + state.msg += sub_state.msg + + def __person_sex(self, line, state): + """ + Parses the SEX line of a GEDCOM file. It has the format of: + + +1 SEX {0:1} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.person.set_gender(line.data) + + def __person_even(self, line, state): + """ + Parses the custom EVEN tag, which has the format of: + + n <> {1:1} + +1 <> {0:1} p.* + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + event_ref = self.__build_event_pair( + state, EventType.CUSTOM, self.event_parse_tbl, line.data + ) + state.person.add_event_ref(event_ref) + + def __person_std_event(self, line, state): + """ + Parses GEDCOM event types that map to a Gramps standard type. + Additional parsing required is for the event detail: + + +1 <> {0:1} p.* + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + + event = line.data + event.set_gramps_id(self.emapper.find_next()) + event_ref = EventRef() + self.dbase.add_event(event, self.trans) + + sub_state = CurrentState() + sub_state.person = state.person + sub_state.level = state.level + 1 + sub_state.event = event + sub_state.event_ref = event_ref + sub_state.pf = self.place_parser + + self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) + state.msg += sub_state.msg + + self.__add_place(event, sub_state) + + self.dbase.commit_event(event, self.trans) + event_ref.ref = event.handle + state.person.add_event_ref(event_ref) + + def __person_reli(self, line, state): + """ + Parses the RELI tag. + + n RELI [Y|] {1:1} + +1 <> {0:1} p.* + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + event_ref = self.__build_event_pair( + state, EventType.RELIGION, self.event_parse_tbl, line.data + ) + state.person.add_event_ref(event_ref) + + def __person_birt(self, line, state): + """ + Parses GEDCOM BIRT tag into a Gramps birth event. Additional work + must be done, since additional handling must be done by Gramps to set + this up as a birth reference event. + + n BIRT [Y|] {1:1} + +1 <> {0:1} p.* + +1 FAMC @@ {0:1} p.* + + I'm not sure what value the FAMC actually offers here, since + the FAMC record should handle this. Why it is a valid sub value + is beyond me. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + event_ref = self.__build_event_pair( + state, EventType.BIRTH, self.event_parse_tbl, line.data + ) + if state.person.get_birth_ref(): + state.person.add_event_ref(event_ref) + else: + state.person.set_birth_ref(event_ref) + + def __person_adop(self, line, state): + """ + Parses GEDCOM ADOP tag, subordinate to the INDI tag. Additinal tags + are needed by the tag, so we pass a different function map. + + n ADOP [Y|] {1:1} + +1 <> {0:1} p.* + +1 FAMC @@ {0:1} p.* + +2 ADOP {0:1} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + event_ref = self.__build_event_pair( + state, EventType.ADOPT, self.adopt_parse_tbl, line.data + ) + state.person.add_event_ref(event_ref) + + def __person_deat(self, line, state): + """ + Parses GEDCOM DEAT tag into a Gramps birth event. Additional work + must be done, since additional handling must be done by Gramps to set + this up as a death reference event. + + n DEAT [Y|] {1:1} + +1 <> {0:1} p.* + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + event_ref = self.__build_event_pair( + state, EventType.DEATH, self.event_parse_tbl, line.data + ) + if state.person.get_death_ref(): + state.person.add_event_ref(event_ref) + else: + state.person.set_death_ref(event_ref) + + def __person_note(self, line, state): + """ + Parses a note associated with the person + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.person, state) + + def __person_rnote(self, line, state): + """ + Parses a note associated with the person + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.person, state) + + def __person_addr(self, line, state): + """ + Parses the INDIvidual + + n ADDR {0:1} + +1 CONT {0:M} + +1 ADR1 {0:1} (Street) + +1 ADR2 {0:1} (Locality) + +1 CITY {0:1} + +1 STAE {0:1} + +1 POST {0:1} + +1 CTRY {0:1} + n PHON {0:3} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if self.is_ftw: + self.__person_resi(line, state) + return + free_form = line.data + + sub_state = CurrentState(level=state.level + 1) + sub_state.addr = Address() + + self.__parse_level(sub_state, self.parse_addr_tbl, self.__ignore) + state.msg += sub_state.msg + + self.__merge_address(free_form, sub_state.addr, line, state) + state.person.add_address(sub_state.addr) + + def __person_resi(self, line, state): + """ + Parses GEDCOM ADDR tag, subordinate to the INDI tag, when sourced by + FTM. We treat this as a RESI event, because FTM puts standard event + details below the ADDR line. + + n ADDR {0:1} + +1 <> {0:1} p.* + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.backoff = True # reprocess the current ADDR line + line.level += 1 # as if it was next level down + event_ref = self.__build_event_pair( + state, EventType.RESIDENCE, self.event_parse_tbl, "" + ) + state.person.add_event_ref(event_ref) + + def __person_phon(self, line, state): + """ + n PHON {0:3} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + url = Url() + url.set_path(line.data) + url.set_type(UrlType(_("Phone"))) + state.person.add_url(url) + + def __person_fax(self, line, state): + """ + O INDI + 1 FAX {0:3} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + url = Url() + url.set_path(line.data) + url.set_type(UrlType(_("FAX"))) + state.person.add_url(url) + + def __person_email(self, line, state): + """ + O INDI + 1 EMAIL {0:3} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + url = Url() + url.set_path(line.data) + url.set_type(UrlType(UrlType.EMAIL)) + state.person.add_url(url) + + def __person_www(self, line, state): + """ + O INDI + 1 WWW {0:3} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + url = Url() + url.set_path(line.data) + url.set_type(UrlType(UrlType.WEB_HOME)) + state.person.add_url(url) + + def __person_titl(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + event = Event() + event_ref = EventRef() + event.set_gramps_id(self.emapper.find_next()) + event.set_type(EventType.NOB_TITLE) + event.set_description(line.data) + + sub_state = CurrentState() + sub_state.person = state.person + sub_state.level = state.level + 1 + sub_state.event = event + sub_state.event_ref = event_ref + sub_state.pf = self.place_parser + + self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) + state.msg += sub_state.msg + + self.__add_place(event, sub_state) + + self.dbase.add_event(event, self.trans) + event_ref.ref = event.handle + state.person.add_event_ref(event_ref) + + def __person_attr_plac(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if state.attr.get_value() == "": + state.attr.set_value(line.data) + + def __name_type(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if line.data.upper() in ("_OTHN", "_AKA", "AKA", "AKAN"): + state.name.set_type(NameType.AKA) + elif line.data.upper() in ("_MAR", "_MARN", "_MARNM", "MARRIED"): + state.name.set_type(NameType.MARRIED) + else: + state.name.set_type((NameType.CUSTOM, line.data)) + + def __name_date(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if state.name: + state.name.set_date_object(line.data) + + def __name_note(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.name, state) + + def __name_alia(self, line, state): + """ + This parses the illegal (ALIA or ALIAS) or non-standard (_ALIA) GEDCOM + tag as a subsidiary of the NAME tag. + + n @XREF:INDI@ INDI {1:1} + +1 NAME {1:1} + +2 NPFX {0:1} + +2 GIVN {0:1} + +2 NICK {0:1} + +2 SPFX {0:1} + +2 SURN {0:1} + +2 NSFX {0:1} + +2 {1:1} + +3 NPFX {0:1} + +3 GIVN {0:1} + +3 NICK {0:1} + +3 SPFX {0:1} + +3 SURN {0:1} + +3 NSFX {0:1} + +3 <> {0:M} + +4 <> {0:M} + +4 <> {0:M} + +3 <> {0:M} + +2 <> {0:M} + +3 <> {0:M} + +3 <> {0:M} + +2 <> {0:M} + + Note that the subsidiary name structure detail will overwrite the ALIA + name (if the same elements are provided in both), so the names should + match. + + "2 _ALIA" is used for example, by PRO-GEN v 3.0a and "2 ALIA" is used + by GTEdit and Brother's keeper 5.2 for windows. It had been supported + in previous versions of Gramps but as it was probably incorrectly coded + as it would only work if the name started with '@'. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_alias_name(line, state) + + def __name_npfx(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.name.set_title(line.data.strip()) + self.__skip_subordinate_levels(state.level + 1, state) + + def __name_givn(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.name.set_first_name(line.data.strip()) + self.__skip_subordinate_levels(state.level + 1, state) + + def __name_spfx(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + spfx = line.data.strip().split(", ")[0] + if state.name.get_surname_list(): + state.name.get_surname_list()[0].set_prefix(spfx) + else: + surn = Surname() + surn.set_prefix(spfx) + surn.set_primary() + state.name.set_surname_list([surn]) + self.__skip_subordinate_levels(state.level + 1, state) + + def __name_surn(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + names = line.data.strip().split(", ") + overwrite = bool(state.name.get_surname_list()) + for name in names: + if overwrite: + state.name.get_surname_list()[0].set_surname(name) + overwrite = False + else: + surn = Surname() + surn.set_surname(name) + surn.set_primary(primary=not state.name.get_surname_list()) + state.name.get_surname_list().append(surn) + self.__skip_subordinate_levels(state.level + 1, state) + + def __name_marnm(self, line, state): + """ + This is non-standard GEDCOM. _MARNM is reported to be used in Ancestral + Quest and Personal Ancestral File 5. This will also handle a usage + which has been found in Brother's Keeper (BROSKEEP VERS 6.1.31 WINDOWS) + as follows: + + 0 @I203@ INDI + 1 NAME John Richard/Doe/ + 2 _MARN Some Other Name + 3 DATE 27 JUN 1817 + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + text = line.data.strip() + data = text.split() + if len(data) == 1: + name = Name(state.person.primary_name) + surn = Surname() + surn.set_surname(data[0].strip()) + surn.set_primary() + name.set_surname_list([surn]) + name.set_type(NameType.MARRIED) + state.person.add_alternate_name(name) + elif len(data) > 1: + name = self.__parse_name_personal(text) + name.set_type(NameType.MARRIED) + state.person.add_alternate_name(name) + + def __name_nsfx(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if state.name.get_suffix() == "" or state.name.get_suffix() == line.data: + # suffix might be set before when parsing name string + state.name.set_suffix(line.data) + else: + # previously set suffix different, to not loose information, append + state.name.set_suffix(state.name.get_suffix() + " " + line.data) + self.__skip_subordinate_levels(state.level + 1, state) + + def __name_nick(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.name.set_nick_name(line.data.strip()) + self.__skip_subordinate_levels(state.level + 1, state) + + def __name_call(self, line, state): + """ + This parses the inofficial _RUFNAME tag that indicates which part + of a person's given name is commonly used to address them. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.name.set_call_name(line.data.strip()) + self.__skip_subordinate_levels(state.level + 1, state) + + def __name_aka(self, line, state): + """ + This parses the non-standard GEDCOM tags _AKA or _AKAN as a subsidiary + to the NAME tag, which is reported to have been found in Ancestral + Quest and Personal Ancestral File 4 and 5. Note: example AQ and PAF + files have separate 2 NICK and 2 _AKA lines for the same person. The + NICK will be stored by Gramps in the nick_name field of the name + structure, while the _AKA, if it is a single word, will be stored in + the NICKNAME attribute. If more than one word it is stored as an AKA + alternate name. + + This will also handle a usage which has been found in in Brother's + Keeper (BROSKEEP VERS 6.1.31 WINDOWS) as follows: + + 0 @I203@ INDI + 1 NAME John Richard/Doe/ + 2 _AKAN Some Other Name + 3 DATE 27 JUN 1817 + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + lname = line.data.split() + name_len = len(lname) + if name_len == 1: + attr = Attribute() + attr.set_type(AttributeType.NICKNAME) + attr.set_value(line.data) + state.person.add_attribute(attr) + elif name_len == 0: + return + else: + name = Name() + surname = Surname() + surname.set_surname(lname[-1].strip()) + surname.set_primary() + name.set_surname_list([surname]) + name.set_first_name(" ".join(lname[0 : name_len - 1])) + # name = self.__parse_name_personal(line.data) + name.set_type(NameType.AKA) + state.person.add_alternate_name(name) + + def __name_adpn(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + text = line.data.strip() + data = text.split() + if len(data) == 1: + name = Name(state.person.primary_name) + surn = Surname() + surn.set_surname(data[0].strip()) + surn.set_primary() + name.set_surname_list([surn]) + name.set_type((NameType.CUSTOM, "Adopted")) + state.person.add_alternate_name(name) + elif len(data) > 1: + name = self.__parse_name_personal(text) + name.set_type((NameType.CUSTOM, "Adopted")) + state.person.add_alternate_name(name) + + def __name_sour(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + citation_handle = self.handle_source(line, state.level, state) + state.name.add_citation(citation_handle) + + def __person_std_attr(self, line, state): + """ + Parses an TOKEN that Gramps recognizes as an Attribute + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + sub_state = CurrentState() + sub_state.person = state.person + sub_state.attr = line.data + sub_state.level = state.level + 1 + state.person.add_attribute(sub_state.attr) + self.__parse_level(sub_state, self.person_attr_parse_tbl, self.__ignore) + state.msg += sub_state.msg + + def __person_fact(self, line, state): + """ + Parses an TOKEN that Gramps recognizes as an Attribute + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + sub_state = CurrentState() + sub_state.person = state.person + sub_state.attr = Attribute() + sub_state.attr.set_value(line.data) + sub_state.level = state.level + 1 + state.person.add_attribute(sub_state.attr) + self.__parse_level(sub_state, self.person_fact_parse_tbl, self.__ignore) + state.msg += sub_state.msg + + def __person_fact_type(self, line, state): + state.attr.set_type(line.data) + + def __person_bapl(self, line, state): + """ + Parses an BAPL TOKEN, producing a Gramps LdsOrd instance + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.build_lds_ord(state, LdsOrd.BAPTISM) + + def __person_conl(self, line, state): + """ + Parses an CONL TOKEN, producing a Gramps LdsOrd instance + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.build_lds_ord(state, LdsOrd.CONFIRMATION) + + def __person_inil(self, line, state): + """ + Parses an INIL TOKEN, producing a Gramps LdsOrd instance + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.build_lds_ord(state, LdsOrd.INITIATORY) + + def __person_endl(self, line, state): + """ + Parses an ENDL TOKEN, producing a Gramps LdsOrd instance + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.build_lds_ord(state, LdsOrd.ENDOWMENT) + + def __person_slgc(self, line, state): + """ + Parses an SLGC TOKEN, producing a Gramps LdsOrd instance + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.build_lds_ord(state, LdsOrd.SEAL_TO_PARENTS) + + def build_lds_ord(self, state, lds_type): + """ + Parses an LDS ordinance, using the type passed to the routine + + @param state: The current state + @type state: CurrentState + @param lds_type: The type of the LDS ordinance + @type line: LdsOrd type + """ + sub_state = CurrentState() + sub_state.level = state.level + 1 + sub_state.lds_ord = LdsOrd() + sub_state.lds_ord.set_type(lds_type) + sub_state.place = None + sub_state.place_fields = PlaceParser() + sub_state.person = state.person + state.person.lds_ord_list.append(sub_state.lds_ord) + + self.__parse_level(sub_state, self.lds_parse_tbl, self.__ignore) + state.msg += sub_state.msg + + if sub_state.place: + place_title = _pd.display(self.dbase, sub_state.place) + sub_state.place_fields.load_place( + self.place_import, sub_state.place, place_title + ) + + def __lds_temple(self, line, state): + """ + Parses the TEMP tag, looking up the code for a match. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + value = self.__extract_temple(line) + if value: + state.lds_ord.set_temple(value) + + def __lds_date(self, line, state): + """ + Parses the DATE tag for the LdsOrd + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.lds_ord.set_date_object(line.data) + + def __lds_famc(self, line, state): + """ + Parses the FAMC tag attached to the LdsOrd + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + gid = self.fid_map[line.data] + state.lds_ord.set_family_handle(self.__find_family_handle(gid)) + + def __lds_form(self, line, state): + """ + Parses the FORM tag thate defines the place structure for a place. + This tag, if found, will override any global place structure. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.pf = PlaceParser(line) + + def __lds_plac(self, line, state): + """ + Parses the PLAC tag attached to the LdsOrd. Create a new place if + needed and set the title. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + try: + title = line.data + place = self.__find_place(title, None, None) + if place is None: + place = Place() + place.set_title(title) + place.name.set_value(title) + self.dbase.add_place(place, self.trans) + self.place_names[place.get_title()].append(place.get_handle()) + else: + pass + state.lds_ord.set_place_handle(place.handle) + except NameError: + return + + def __lds_sour(self, line, state): + """ + Parses the SOUR tag attached to the LdsOrd. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + citation_handle = self.handle_source(line, state.level, state) + state.lds_ord.add_citation(citation_handle) + + def __lds_note(self, line, state): + """ + Parses the NOTE tag attached to the LdsOrd. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.lds_ord, state) + + def __lds_stat(self, line, state): + """ + Parses the STAT (status) tag attached to the LdsOrd. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + status = LDS_STATUS.get(line.data, LdsOrd.STATUS_NONE) + state.lds_ord.set_status(status) + + def __person_famc(self, line, state): + """ + Handles the parsing of the FAMC line, which indicates which family the + person is a child of. + + n FAMC @@ {1:1} + +1 PEDI {0:1} p.* + +1 _FREL {0:1} non-standard Extension + +1 _MREL {0:1} non-standard Extension + +1 <> {0:M} p.* + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + + if not line.data: # handles empty FAMC line + self.__not_recognized(line, state) + return + sub_state = CurrentState() + sub_state.person = state.person + sub_state.level = state.level + 1 + sub_state.ftype = None + sub_state.primary = False + + gid = self.fid_map[line.data] + handle = self.__find_family_handle(gid) + + self.__parse_level(sub_state, self.famc_parse_tbl, self.__undefined) + state.msg += sub_state.msg + + # if the handle is not already in the person's parent family list, we + # need to add it to the list. + + flist = state.person.get_parent_family_handle_list() + if handle not in flist: + state.person.add_parent_family_handle(handle) + + # search childrefs + family, _new = self.dbase.find_family_from_handle(handle, self.trans) + family.set_gramps_id(gid) + + for ref in family.get_child_ref_list(): + if ref.ref == state.person.handle: + break + else: + ref = ChildRef() + ref.ref = state.person.handle + family.add_child_ref(ref) + if sub_state.ftype: + ref.set_mother_relation(sub_state.ftype) + ref.set_father_relation(sub_state.ftype) + else: + if sub_state.frel: + ref.set_father_relation(sub_state.frel) + if sub_state.mrel: + ref.set_mother_relation(sub_state.mrel) + self.dbase.commit_family(family, self.trans) + + def __person_famc_pedi(self, line, state): + """ + Parses the PEDI tag attached to a INDI.FAMC record. No values are set + at this point, because we have to do some post processing. Instead, we + assign the ftype field of the state variable. We convert the text from + the line to an index into the PEDIGREE_TYPES dictionary, which will map + to the correct ChildTypeRef. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.ftype = PEDIGREE_TYPES.get(line.data.lower(), ChildRefType.UNKNOWN) + + def __person_famc_frel(self, line, state): + """ + Parses the _FREL tag attached to a INDI.FAMC record. No values are set + at this point, because we have to do some post processing. Instead, we + assign the frel field of the state variable. We convert the text from + the line to an index into the PEDIGREE_TYPES dictionary, which will map + to the correct ChildTypeRef. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.frel = PEDIGREE_TYPES.get(line.data.lower().strip(), None) + if state.frel is None: + state.frel = ChildRefType(line.data.capitalize().strip()) + + def __person_famc_mrel(self, line, state): + """ + Parses the _MREL tag attached to a INDI.FAMC record. No values are set + at this point, because we have to do some post processing. Instead, we + assign the mrel field of the state variable. We convert the text from + the line to an index into the PEDIGREE_TYPES dictionary, which will map + to the correct ChildTypeRef. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.mrel = PEDIGREE_TYPES.get(line.data.lower().strip(), None) + if state.mrel is None: + state.mrel = ChildRefType(line.data.capitalize().strip()) + + def __person_famc_note(self, line, state): + """ + Parses the INDI.FAMC.NOTE tag . + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.person, state) + + def __person_famc_primary(self, line, state): + """ + Parses the _PRIMARY tag on an INDI.FAMC tag. This value is stored in + the state record to be used later. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.primary = True + + def __person_famc_sour(self, line, state): + """ + Parses the SOUR tag on an INDI.FAMC tag. Gramps has no corresponding + record on its family relationship, so we add the source to the Person + record. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + citation_handle = self.handle_source(line, state.level, state) + state.person.add_citation(citation_handle) + + def __person_fams(self, line, state): + """ + Parses the INDI.FAMS record, which indicates the family in which the + person is a spouse. + + n FAMS @@ {1:1} p.* + +1 <> {0:M} p.* + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + gid = self.fid_map[line.data] + handle = self.__find_family_handle(gid) + state.person.add_family_handle(handle) + + sub_state = CurrentState(level=state.level + 1) + sub_state.obj = state.person + self.__parse_level(sub_state, self.opt_note_tbl, self.__ignore) + state.msg += sub_state.msg + + def __person_asso(self, line, state): + """ + Parse the ASSO tag, add the referenced person to the person we + are currently parsing. The GEDCOM spec indicates that valid ASSO tag + is: + + n ASSO @@ {0:M} + + And the sub tags are: + + ASSOCIATION_STRUCTURE:= + +1 RELA {1:1} + +1 <> {0:M} + +1 <> {0:M} + + The Gedcom spec notes that the ASSOCIATION_STRUCTURE + can only link to an INDIVIDUAL_RECORD + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + + # find the id and person that we are referencing + handle = self.__find_person_handle(self.pid_map[line.data]) + + # create a new PersonRef, and assign the handle, add the + # PersonRef to the active person + + sub_state = CurrentState() + sub_state.person = state.person + sub_state.level = state.level + 1 + sub_state.ref = PersonRef() + sub_state.ref.ref = handle + sub_state.ignore = False + + self.__parse_level(sub_state, self.asso_parse_tbl, self.__ignore) + state.msg += sub_state.msg + if not sub_state.ignore: + state.person.add_person_ref(sub_state.ref) + + def __person_asso_rela(self, line, state): + """ + Parses the INDI.ASSO.RELA tag. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.ref.rel = line.data + + def __person_asso_sour(self, line, state): + """ + Parses the INDI.ASSO.SOUR tag. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.ref.add_citation(self.handle_source(line, state.level, state)) + + def __person_asso_note(self, line, state): + """ + Parses the INDI.ASSO.NOTE tag. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.ref, state) + + # ------------------------------------------------------------------- + # + # FAM parsing + # + # ------------------------------------------------------------------- + + def __parse_fam(self, line): + """ + n @@ FAM {1:1} + +1 <> {0:M} + +1 HUSB @@ {0:1} + +1 WIFE @@ {0:1} + +1 CHIL @@ {0:M} + +1 NCHI {0:1} + +1 SUBM @@ {0:M} + +1 <> {0:M} + +1 <> {0:M} + +1 <> {0:M} + +1 <> {0:M} + +1 REFN {0:M} + +1 RIN {0:1} + +1 <> {0:1} + """ + # create a family + + gid = self.fid_map[line.token_text] + family = self.__find_or_create_family(gid) + + # parse the family + + state = CurrentState(level=1) + state.family = family + + self.__parse_level(state, self.family_func, self.__family_even) + + # handle addresses attached to families + if state.addr is not None: + father_handle = family.get_father_handle() + father = self.dbase.get_person_from_handle(father_handle) + if father: + father.add_address(state.addr) + self.dbase.commit_person(father, self.trans) + mother_handle = family.get_mother_handle() + mother = self.dbase.get_person_from_handle(mother_handle) + if mother: + mother.add_address(state.addr) + self.dbase.commit_person(mother, self.trans) + + for child_ref in family.get_child_ref_list(): + child_handle = child_ref.ref + child = self.dbase.get_person_from_handle(child_handle) + if child: + child.add_address(state.addr) + self.dbase.commit_person(child, self.trans) + + # add default reference if no reference exists + self.__add_default_source(family) + + # Add a default tag if provided + self.__add_default_tag(family) + + self.__check_msgs( + _("FAM (family) Gramps ID %s") % family.get_gramps_id(), state, family + ) + # commit family to database + self.dbase.commit_family(family, self.trans, family.change) + + def __family_husb(self, line, state): + """ + Parses the husband line of a family + + n HUSB @@ {0:1} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + handle = self.__find_person_handle(self.pid_map[line.data]) + state.family.set_father_handle(handle) + + def __family_wife(self, line, state): + """ + Parses the wife line of a family + + n WIFE @@ {0:1} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + handle = self.__find_person_handle(self.pid_map[line.data]) + state.family.set_mother_handle(handle) + + def __family_std_event(self, line, state): + """ + Parses GEDCOM event types that map to a Gramps standard type. + Additional parsing required is for the event detail: + + +1 <> {0:1} p.* + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + event = line.data + event.set_gramps_id(self.emapper.find_next()) + event_ref = EventRef() + event_ref.set_role(EventRoleType.FAMILY) + self.dbase.add_event(event, self.trans) + + sub_state = CurrentState() + sub_state.person = state.person + sub_state.level = state.level + 1 + sub_state.event = event + sub_state.event_ref = event_ref + sub_state.pf = self.place_parser + + self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) + state.msg += sub_state.msg + + self.__add_place(event, sub_state) + + if event.type == EventType.MARRIAGE: + descr = event.get_description() + if descr == "Civil Union": + state.family.type.set(FamilyRelType.CIVIL_UNION) + event.set_description("") + elif descr == "Unmarried": + state.family.type.set(FamilyRelType.UNMARRIED) + event.set_description("") + else: + state.family.type.set(FamilyRelType.MARRIED) + if descr == "Y": + event.set_description("") + + self.dbase.commit_event(event, self.trans) + event_ref.ref = event.handle + state.family.add_event_ref(event_ref) + + def __family_even(self, line, state): + """ + Parses GEDCOM event types that map to a Gramps standard type. + Additional parsing required is for the event detail: + + +1 <> {0:1} p.* + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + # We can get here when a tag that is not valid in the family_func + # parse table is encountered. The tag may be of the form "_XXX". We + # try to convert to a friendly name, if fails use the tag itself as + # the TYPE in a custom event + cust_tag = CUSTOMEVENTTAGS.get(line.token_text, line.token_text) + cust_type = EventType((EventType.CUSTOM, cust_tag)) + event = Event() + event_ref = EventRef() + event_ref.set_role(EventRoleType.FAMILY) + event.set_gramps_id(self.emapper.find_next()) + event.set_type(cust_type) + # in case a description ever shows up + if line.data and line.data != "Y": + event.set_description(str(line.data)) + self.dbase.add_event(event, self.trans) + + sub_state = CurrentState() + sub_state.person = state.person + sub_state.level = state.level + 1 + sub_state.event = event + sub_state.event_ref = event_ref + sub_state.pf = self.place_parser + + self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) + state.msg += sub_state.msg + + self.__add_place(event, sub_state) + + self.dbase.commit_event(event, self.trans) + event_ref.ref = event.handle + state.family.add_event_ref(event_ref) + + def __family_chil(self, line, state): + """ + Parses the child line of a family + + n CHIL @@ {0:1} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + sub_state = CurrentState() + sub_state.family = state.family + sub_state.level = state.level + 1 + sub_state.mrel = None + sub_state.frel = None + + self.__parse_level(sub_state, self.family_rel_tbl, self.__ignore) + state.msg += sub_state.msg + + child = self.__find_or_create_person(self.pid_map[line.data]) + + reflist = [ + ref for ref in state.family.get_child_ref_list() if ref.ref == child.handle + ] + + if reflist: # The child has been referenced already + ref = reflist[0] + if sub_state.frel: + ref.set_father_relation(sub_state.frel) + if sub_state.mrel: + ref.set_mother_relation(sub_state.mrel) + # then we will set the order now: + self.set_child_ref_order(state.family, ref) + else: + ref = ChildRef() + ref.ref = child.handle + if sub_state.frel: + ref.set_father_relation(sub_state.frel) + if sub_state.mrel: + ref.set_mother_relation(sub_state.mrel) + state.family.add_child_ref(ref) + + def set_child_ref_order(self, family, child_ref): + """ + Sets the child_ref in family.child_ref_list to be in the position + family.child_ref_count. This reorders the children to be in the + order given in the FAM section. + """ + family.child_ref_list.remove(child_ref) + family.child_ref_list.insert(family._child_ref_count, child_ref) + family._child_ref_count += 1 + + def __family_slgs(self, line, state): + """ + n SLGS {1:1} + +1 STAT {0:1} + +1 DATE {0:1} + +1 TEMP {0:1} + +1 PLAC {0:1} + +1 <> {0:M} + +1 <> {0:M} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + sub_state = CurrentState() + sub_state.level = state.level + 1 + sub_state.lds_ord = LdsOrd() + sub_state.lds_ord.set_type(LdsOrd.SEAL_TO_SPOUSE) + sub_state.place = None + sub_state.family = state.family + sub_state.place_fields = PlaceParser() + state.family.lds_ord_list.append(sub_state.lds_ord) + + self.__parse_level(sub_state, self.lds_parse_tbl, self.__ignore) + state.msg += sub_state.msg + + if sub_state.place: + place_title = _pd.display(self.dbase, sub_state.place) + sub_state.place_fields.load_place( + self.place_import, sub_state.place, place_title + ) + + def __family_source(self, line, state): + """ + n SOUR @@ /* pointer to source record */ {1:1} p.* + +1 PAGE {0:1} p.* + +1 EVEN {0:1} p.* + +1 DATA {0:1} + +1 QUAY {0:1} p.* + +1 <> {0:M} p.*, * + +1 <> {0:M} p.* + + | /* Systems not using source records */ + n SOUR {1:1} p.* + +1 [ CONC | CONT ] {0:M} + +1 TEXT {0:M} p.* + +1 <> {0:M} p.* + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + citation_handle = self.handle_source(line, state.level, state) + state.family.add_citation(citation_handle) + + def __family_object(self, line, state): + """ + +1 <> {0:M} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__obje(line, state, state.family) + + def __family_comm(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + note = line.data + state.family.add_note(note) + self.__skip_subordinate_levels(state.level + 1, state) + + def __family_note(self, line, state): + """ + +1 <> {0:M} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.family, state) + + def __family_chan(self, line, state): + """ + +1 <> {0:1} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_change(line, state.family, state.level + 1, state) + + def __family_attr(self, line, state): + """ + Parses an TOKEN that Gramps recognizes as an Attribute + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + sub_state = CurrentState() + sub_state.person = state.person + sub_state.attr = line.data + sub_state.level = state.level + 1 + state.family.add_attribute(line.data) + self.__parse_level(sub_state, self.person_attr_parse_tbl, self.__ignore) + state.msg += sub_state.msg + + def __family_refn(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__do_refn(line, state, state.family) + + def __family_cust_attr(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + attr = Attribute() + attr.set_type(line.token_text) + attr.set_value(line.data) + state.family.add_attribute(attr) + + def __obje(self, line, state, pri_obj): + """ + Embedded form + + n OBJE @@ {1:1} + +1 _PRIM {0:1} # Indicates primary photo + + Linked form + + n OBJE {1:1} + +1 FORM {1:1} # v5.5 layout + +1 TITL {0:1} + +1 FILE {1:1} # v5.5.1 allows multiple + +2 FORM {1:1} # v5.5.1 layout + +3 MEDI {0:1} # v5.5.1 layout + +1 <> {0:M} + +1 _PRIM {0:1} # Indicates primary photo + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + @param pri_obj: The Primary object to which this is attached + @type state: Person # or Family, or Source etc. + """ + if line.data and line.data[0] == "@": + # Reference to a named multimedia object defined elsewhere + gramps_id = self.oid_map[line.data] + handle = self.__find_media_handle(gramps_id) + # check to see if this is a primary photo + line = self.__chk_subordinate(state.level + 1, state, TOKEN__PRIM) + if line and line.data == "Y": + state.photo = handle + oref = MediaRef() + oref.set_reference_handle(handle) + pri_obj.add_media_reference(oref) + return + # + # The remainder of this code is similar in concept to __parse_obje + # except that it combines references to the same media file by + # comparing path names. If they are the same, then only the first + # is kept. This does mean that if there are different notes etc. on a + # later OBJE, they will be lost. + # + sub_state = CurrentState() + sub_state.form = "" + sub_state.attr = None + sub_state.filename = "" + sub_state.title = "" + sub_state.media = Media() + sub_state.level = state.level + 1 + sub_state.prim = "" + + self.__parse_level(sub_state, self.media_parse_tbl, self.__ignore) + state.msg += sub_state.msg + if sub_state.filename == "": + self.__add_msg(_("Filename omitted"), line, state) + # The following lines are commented out because Gramps is NOT a + # Gedcom validator! + # if sub_state.form == "": + # self.__add_msg(_("Form omitted"), line, state) + + # The following code that detects URL is an older v5.5 usage; the + # modern option is to use the EMAIL tag. + if isinstance(sub_state.form, str) and sub_state.form == "url": + if isinstance(pri_obj, UrlBase): + url = Url() + url.set_path(sub_state.filename) + url.set_description(sub_state.title) + url.set_type(UrlType.WEB_HOME) + pri_obj.add_url(url) + else: # some primary objects (Event) son't have spot for URL + new_note = Note(sub_state.filename) + new_note.set_gramps_id(self.nid_map[""]) + new_note.set_handle(create_id()) + new_note.set_type( + OBJ_NOTETYPE.get(type(pri_obj).__name__, NoteType.GENERAL) + ) + self.dbase.commit_note(new_note, self.trans, new_note.change) + pri_obj.add_note(new_note.get_handle()) + + else: + # to allow import of references to URLs (especially for import from + # geni.com), do not try to find the file if it is blatently a URL + res = urlparse(sub_state.filename) + if sub_state.filename != "" and ( + res.scheme == "" or len(res.scheme) == 1 or res.scheme == "file" + ): + valid, path = self.__find_file(sub_state.filename, self.dir_path) + if not valid: + self.__add_msg( + _("Could not import %s") % sub_state.filename, line, state + ) + else: + path = sub_state.filename + # Multiple references to the same media silently drops the later + # ones, even if title, etc. are different + photo_handle = self.media_map.get(path) + if photo_handle is None: + photo = Media() + photo.set_path(path) + if sub_state.title: + photo.set_description(sub_state.title) + else: + photo.set_description(path.replace("\\", "/")) + full_path = os.path.abspath(path) + # deal with mime types + value = mimetypes.guess_type(full_path) + if value and value[0]: # found from filename + photo.set_mime_type(value[0]) + else: # get from OBJE.FILE.FORM + if "/" in sub_state.form: # already has expanded mime type + photo.set_mime_type(sub_state.form) + else: + value = mimetypes.types_map.get( + "." + sub_state.form, _("unknown") + ) + photo.set_mime_type(value) + if sub_state.attr: + photo.attribute_list.append(sub_state.attr) + self.dbase.add_media(photo, self.trans) + self.media_map[path] = photo.handle + else: + photo = self.dbase.get_media_from_handle(photo_handle) + # copy notes to our media + for note in sub_state.media.get_note_list(): + photo.add_note(note) + self.dbase.commit_media(photo, self.trans) + + if sub_state.prim == "Y": + state.photo = photo.handle + oref = MediaRef() + oref.set_reference_handle(photo.handle) + pri_obj.add_media_reference(oref) + + def __media_ref_form(self, line, state): + """ + +1 FORM {1:1} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.form = line.data.lower() + + def __media_ref_medi(self, line, state): + """ + +1 MEDI {0:1} (Photo, Audio, Book, etc.) + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.attr = Attribute() + mtype = MEDIA_MAP.get(line.data.lower(), (SourceMediaType.CUSTOM, line.data)) + state.attr.set_type(_("Media-Type")) + state.attr.set_value(str(SourceMediaType(mtype))) + + def __media_ref_titl(self, line, state): + """ + +1 TITL {0:1} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.title = line.data + + def __media_ref_file(self, line, state): + """ + +1 FILE {1:1} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if state.filename != "": + self.__add_msg(_("Multiple FILE in a single OBJE ignored"), line, state) + self.__skip_subordinate_levels(state.level + 1, state) + return + state.filename = line.data + + def __media_ref_prim(self, line, state): + """ + +1 _PRIM {0:1} + + Indicates that this OBJE is the primary photo. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.prim = line.data + + def __family_adopt(self, line, state): + """ + n ADOP + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.frel = TYPE_ADOPT + state.mrel = TYPE_ADOPT + + def __family_frel(self, line, state): + """ + The _FREL key is a FTW/FTM specific extension to indicate father/child + relationship. + + n _FREL + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.frel = PEDIGREE_TYPES.get(line.data.strip().lower()) + + def __family_mrel(self, line, state): + """ + The _MREL key is a FTW/FTM specific extension to indicate father/child + relationship. + + n _MREL + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.mrel = PEDIGREE_TYPES.get(line.data.strip().lower()) + + def __family_stat(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.mrel = TYPE_BIRTH + state.frel = TYPE_BIRTH + + def __event_object(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__obje(line, state, state.event) + + def __event_type(self, line, state): + """ + Parses the TYPE line for an event. + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if state.event.get_type().is_custom(): + if line.data in GED_TO_GRAMPS_EVENT: + name = EventType(GED_TO_GRAMPS_EVENT[line.data]) + else: + try: + name = EventType((EventType.CUSTOM, line.data)) + except AttributeError: + name = EventType(EventType.UNKNOWN) + state.event.set_type(name) + else: + try: + if line.data not in GED_TO_GRAMPS_EVENT and line.data[0] != "Y": + state.event.set_description(line.data) + except IndexError: + return + + def __event_date(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.event.set_date_object(line.data) + self.__warn_if_ambiguous_numeric_date(line, state) + + def __event_place(self, line, state): + """ + Parse the place portion of a event. A special case has to be made for + Family Tree Maker, which violates the GEDCOM spec. It uses the PLAC + field to store the description or value associated with the event. + + n PLAC {1:1} + +1 FORM {0:1} + +1 <> {0:M} + +1 <> {0:M} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + + if ( + self.is_ftw + and (state.event.type in FTW_BAD_PLACE) + and not state.event.get_description() + ): + state.event.set_description(line.data) + else: + place = state.place + if place: + # We encounter a PLAC, having previously encountered an ADDR + if state.place.place_type.string != _("Address"): + # We have previously found a PLAC + self.__add_msg(_("A second PLAC ignored"), line, state) + # ignore this second PLAC, and use the old one + else: + # This is the first PLAC + place.set_title(line.data) + place.name.set_value(line.data) + else: + # The first thing we encounter is PLAC + state.place = Place() + place = state.place + place.set_title(line.data) + place.name.set_value(line.data) + + sub_state = CurrentState() + sub_state.place = place + sub_state.level = state.level + 1 + + self.__parse_level(sub_state, self.event_place_map, self.__undefined) + state.msg += sub_state.msg + if sub_state.pf: # if we found local PLAC:FORM + state.pf = sub_state.pf # save to override global value + # merge notes etc into place + state.place.merge(sub_state.place) + + def __event_place_note(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.place, state) + + def __event_place_form(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.pf = PlaceParser(line) + + def __event_place_object(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__obje(line, state, state.place) + + def __event_place_sour(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.place.add_citation(self.handle_source(line, state.level, state)) + + def __place_map(self, line, state): + """ + + n MAP + n+1 LONG + n+1 LATI + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + sub_state = CurrentState() + sub_state.level = state.level + 1 + sub_state.place = state.place + self.__parse_level(sub_state, self.place_map_tbl, self.__undefined) + state.msg += sub_state.msg + state.place = sub_state.place + + def __place_lati(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.place.set_latitude(line.data) + + def __place_long(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.place.set_longitude(line.data) + + def __event_addr(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + free_form = line.data + + sub_state = CurrentState(level=state.level + 1) + sub_state.location = Location() + sub_state.event = state.event + sub_state.place = Place() # temp stash for notes, citations etc + + self.__parse_level(sub_state, self.parse_loc_tbl, self.__undefined) + state.msg += sub_state.msg + + title = self.__merge_address(free_form, sub_state.location, line, state) + + location = sub_state.location + + if self.addr_is_detail and state.place: + # Commit the enclosing place + place = self.__find_place( + state.place.get_title(), None, state.place.get_placeref_list() + ) + if place is None: + place = state.place + self.dbase.add_place(place, self.trans) + self.place_names[place.get_title()].append(place.get_handle()) + else: + place.merge(state.place) + self.dbase.commit_place(place, self.trans) + place_title = _pd.display(self.dbase, place) + state.pf.load_place(self.place_import, place, place_title) + + # Create the Place Details (it is committed with the event) + place_detail = Place() + place_detail.set_name(PlaceName(value=title)) + place_detail.set_title(title) + # For RootsMagic etc. Place Details e.g. address, hospital, ... + place_detail.set_type((PlaceType.CUSTOM, _("Detail"))) + placeref = PlaceRef() + placeref.ref = place.get_handle() + place_detail.set_placeref_list([placeref]) + state.place = place_detail + else: + place = state.place + if place: + # We encounter an ADDR having previously encountered a PLAC + if ( + len(place.get_alternate_locations()) != 0 + and not self.__get_first_loc(place).is_empty() + ): + # We have perviously found an ADDR, or have populated + # location from PLAC title + self.__add_msg( + _("Location already populated; ADDR " "ignored"), line, state + ) + # ignore this second ADDR, and use the old one + else: + # This is the first ADDR + place.add_alternate_locations(location) + else: + # The first thing we encounter is ADDR + state.place = Place() + place = state.place + place.add_alternate_locations(location) + place.set_name(PlaceName(value=title)) + place.set_title(title) + place.set_type((PlaceType.CUSTOM, _("Address"))) + + # merge notes etc into place + state.place.merge(sub_state.place) + + def __add_location(self, place, location): + """ + @param place: A place object we have found or created + @type place: Place + @param location: A location we want to add to this place + @type location: gen.lib.location + """ + for loc in place.get_alternate_locations(): + if loc.is_equivalent(location) == IDENTICAL: + return + place.add_alternate_locations(location) + + def __get_first_loc(self, place): + """ + @param place: A place object + @type place: Place + @return location: the first alternate location if any else None + @type location: gen.lib.location + """ + if len(place.get_alternate_locations()) == 0: + return None + else: + return place.get_alternate_locations()[0] + + def __event_privacy(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.event.set_privacy(True) + + def __event_note(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.event, state) + + def __event_inline_note(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if line.data[0:13] == "Description: ": + state.event.set_description(line.data[13:]) + else: + self.__parse_note(line, state.event, state) + + def __event_source(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.event.add_citation(self.handle_source(line, state.level, state)) + + def __event_rin(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + attr = Attribute() + attr.set_type(line.token_text) + attr.set_value(line.data) + state.event.add_attribute(attr) + + def __event_attr(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.event.add_attribute(line.data) + + def __event_phon(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + attr = Attribute() + attr.set_type(_("Phone")) + attr.set_value(line.data) + state.event.add_attribute(attr) + + def __event_fax(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + attr = Attribute() + attr.set_type(_("FAX")) + attr.set_value(line.data) + state.event.add_attribute(attr) + + def __event_email(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + attr = Attribute() + attr.set_type(_("EMAIL")) + attr.set_value(line.data) + state.event.add_attribute(attr) + + def __event_www(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + attr = Attribute() + attr.set_type(_("WWW")) + attr.set_value(line.data) + state.event.add_attribute(attr) + + def __event_cause(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + attr = Attribute() + attr.set_type(AttributeType.CAUSE) + attr.set_value(line.data) + state.event.add_attribute(attr) + + sub_state = CurrentState() + sub_state.event = state.event + sub_state.level = state.level + 1 + sub_state.attr = attr + + self.__parse_level(sub_state, self.event_cause_tbl, self.__undefined) + state.msg += sub_state.msg + + def __event_cause_source(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.attr.add_citation(self.handle_source(line, state.level, state)) + + def __event_age(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + attr = Attribute() + attr.set_type(AttributeType.AGE) + attr.set_value(line.data) + state.event_ref.add_attribute(attr) + + def __event_husb(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + while True: + line = self.__get_next_line() + if self.__level_is_finished(line, state.level + 1): + break + elif line.token == TOKEN_AGE: + attr = Attribute() + attr.set_type(AttributeType.FATHER_AGE) + attr.set_value(line.data) + state.event_ref.add_attribute(attr) + elif line.token == TOKEN_WIFE: + # wife event can be on same level, if so call it and finish + self.__event_wife(line, state) + break + + def __event_wife(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + while True: + line = self.__get_next_line() + if self.__level_is_finished(line, state.level + 1): + break + elif line.token == TOKEN_AGE: + attr = Attribute() + attr.set_type(AttributeType.MOTHER_AGE) + attr.set_value(line.data) + state.event_ref.add_attribute(attr) + elif line.token == TOKEN_HUSB: + # husband event can be on same level, if so call it and finish + self.__event_husb(line, state) + break + + def __event_agnc(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + attr = Attribute() + attr.set_type(AttributeType.AGENCY) + attr.set_value(line.data) + state.event.add_attribute(attr) + + def __event_time(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if hasattr(state, "event"): + # read in time as attribute of event + attr = Attribute() + attr.set_type(AttributeType.TIME) + attr.set_value(line.data) + state.event.add_attribute(attr) + + def __event_witness(self, line, state): + """ + Parse the witness of an event + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if line.data and line.data[0] == "@": + # n _WITN @@ + # +1 TYPE + assert state.event.handle # event handle is required to be set + wit = self.__find_or_create_person(self.pid_map[line.data]) + event_ref = EventRef() + event_ref.set_reference_handle(state.event.handle) + while True: + line = self.__get_next_line() + if self.__level_is_finished(line, state.level + 1): + break + elif line.token == TOKEN_TYPE: + if line.data == "WITNESS_OF_MARRIAGE": + role = EventRoleType(EventRoleType.WITNESS) + else: + role = EventRoleType((EventRoleType.CUSTOM, line.data)) + event_ref.set_role(role) + wit.add_event_ref(event_ref) + self.dbase.commit_person(wit, self.trans) + else: + # n _WITN + attr = Attribute() + attr.set_type(AttributeType.WITNESS) + attr.set_value(line.data) + state.event.add_attribute(attr) + + def __person_adopt_famc(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + gid = self.fid_map[line.data] + handle = self.__find_family_handle(gid) + family = self.__find_or_create_family(gid) + + sub_state = CurrentState(level=state.level + 1) + sub_state.mrel = TYPE_BIRTH + sub_state.frel = TYPE_BIRTH + + self.__parse_level(sub_state, self.parse_person_adopt, self.__undefined) + state.msg += sub_state.msg + + if ( + int(sub_state.mrel) == ChildRefType.BIRTH + and int(sub_state.frel) == ChildRefType.BIRTH + ): + sub_state.mrel = sub_state.frel = TYPE_ADOPT + + state.person.add_parent_family_handle(handle) + + reflist = [ + ref for ref in family.get_child_ref_list() if ref.ref == state.person.handle + ] + if reflist: + ref = reflist[0] + ref.set_father_relation(sub_state.frel) + ref.set_mother_relation(sub_state.mrel) + else: + ref = ChildRef() + ref.ref = state.person.handle + ref.set_father_relation(sub_state.frel) + ref.set_mother_relation(sub_state.mrel) + family.add_child_ref(ref) + self.dbase.commit_family(family, self.trans) + + def __person_adopt_famc_adopt(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if line.data.strip() == "HUSB": + state.frel = TYPE_ADOPT + elif line.data.strip() == "WIFE": + state.mrel = TYPE_ADOPT + else: + state.mrel = TYPE_ADOPT + state.frel = TYPE_ADOPT + + def __person_birth_famc(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + handle = self.__find_family_handle(self.fid_map[line.data]) + + state.person.add_parent_family_handle(handle) + + frel = mrel = ChildRefType.BIRTH + + family, _new = self.dbase.find_family_from_handle(handle, self.trans) + reflist = [ + ref for ref in family.get_child_ref_list() if ref.ref == state.person.handle + ] + if reflist: + ref = reflist[0] + ref.set_father_relation(frel) + ref.set_mother_relation(mrel) + else: + ref = ChildRef() + ref.ref = state.person.handle + ref.set_father_relation(frel) + ref.set_mother_relation(mrel) + family.add_child_ref(ref) + self.dbase.commit_family(family, self.trans) + + def __address_date(self, line, state): + """ + Parses the DATE line of an ADDR tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.addr.set_date_object(line.data) + + def __address_adr1(self, line, state): + """ + Parses the ADR1 line of an ADDR tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + # The ADDR may already have been parsed by the level above + # assert state.addr.get_street() == "" + if state.addr.get_street() != "": + self.__add_msg(_("Warn: ADDR overwritten"), line, state) + state.addr.set_street(line.data) + + def __address_adr2(self, line, state): + """ + Parses the ADR2 line of an ADDR tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.addr.set_locality(line.data) + + def __address_city(self, line, state): + """ + Parses the CITY line of an ADDR tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.addr.set_city(line.data) + + def __address_state(self, line, state): + """ + Parses the STAE line of an ADDR tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.addr.set_state(line.data) + + def __address_post(self, line, state): + """ + Parses the POST line of an ADDR tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.addr.set_postal_code(line.data) + + def __address_country(self, line, state): + """ + Parses the country line of an ADDR tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.addr.set_country(line.data) + + def __address_sour(self, line, state): + """ + Parses the SOUR line of an ADDR tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.addr.add_citation(self.handle_source(line, state.level, state)) + + def __address_note(self, line, state): + """ + Parses the NOTE line of an ADDR tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.addr, state) + + def __citation_page(self, line, state): + """ + Parses the PAGE line of an SOUR instance tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.citation.set_page(line.data) + + def __citation_date(self, line, state): + """ + Parses the DATE line of an SOUR instance tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.citation.set_date_object(line.data) + + def __citation_data(self, line, state): + """ + Parses the DATA line of an SOUR instance tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + sub_state = CurrentState(level=state.level + 1) + sub_state.citation = state.citation + + self.__parse_level(sub_state, self.citation_data_tbl, self.__undefined) + state.msg += sub_state.msg + + def __citation_data_date(self, line, state): + state.citation.set_date_object(line.data) + + def __source_text(self, line, state): + note = Note() + note.set(line.data) + gramps_id = self.nid_map[""] + note.set_gramps_id(gramps_id) + note.set_type(NoteType.SOURCE_TEXT) + self.dbase.add_note(note, self.trans) + + state.source.add_note(note.get_handle()) + + def __citation_data_text(self, line, state): + note = Note() + note.set(line.data) + gramps_id = self.nid_map[""] + note.set_gramps_id(gramps_id) + note.set_type(NoteType.SOURCE_TEXT) + self.dbase.add_note(note, self.trans) + + state.citation.add_note(note.get_handle()) + + def __citation_link(self, line, state): + """ + Not legal GEDCOM - added to support FTM, converts the _LINK tag to a + note with styled text so link can be followed in reports etc. + """ + note = Note() + tags = StyledTextTag(StyledTextTagType.LINK, line.data, [(0, len(line.data))]) + note.set_styledtext(StyledText(line.data, [tags])) + gramps_id = self.nid_map[""] + note.set_gramps_id(gramps_id) + note.set_type(NoteType.CITATION) + self.dbase.add_note(note, self.trans) + state.citation.add_note(note.get_handle()) + + def __citation__just(self, line, state): + """ + Not legal GEDCOM - added to support FTM, converts the _JUST tag to a + note. This tag represents the Justification for a source. + """ + note = Note() + note.set(line.data) + gramps_id = self.nid_map[""] + note.set_gramps_id(gramps_id) + note.set_type(_("Citation Justification")) + self.dbase.add_note(note, self.trans) + state.citation.add_note(note.get_handle()) + + def __citation__apid(self, line, state): + """ + Not legal GEDCOM - added to support Ancestry.com, converts the + _APID tag to an attribute. This tag identifies the location of + the cited page in the relevant Ancestry.com database. + """ + sattr = SrcAttribute() + sattr.set_type("_APID") + sattr.set_value(line.data) + state.citation.add_attribute(sattr) + + def __citation_data_note(self, line, state): + self.__parse_note(line, state.citation, state) + + def __citation_obje(self, line, state): + """ + Parses the OBJE line of an SOUR instance tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__obje(line, state, state.citation) + + def __citation_refn(self, line, state): + """ + Parses the REFN line of an SOUR instance tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__add_msg(_("REFN ignored"), line, state) + self.__skip_subordinate_levels(state.level + 1, state) + + def __citation_even(self, line, state): + """ + Parses the EVEN line of an SOUR instance tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + sattr = SrcAttribute() + sattr.set_type("EVEN") + sattr.set_value(line.data) + state.citation.add_attribute(sattr) + sub_state = CurrentState(level=state.level + 1) + sub_state.citation = state.citation + + self.__parse_level(sub_state, self.citation_even_tbl, self.__undefined) + state.msg += sub_state.msg + + def __citation_even_role(self, line, state): + """ + Parses the EVEN line of an SOUR instance tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + sattr = SrcAttribute() + sattr.set_type("EVEN:ROLE") + sattr.set_value(line.data) + state.citation.add_attribute(sattr) + + def __citation_quay(self, line, state): + """ + Parses the QUAY line of an SOUR instance tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + try: + val = int(line.data) + except ValueError: + return + # If value is greater than 3, cap at 3 + val = min(val, 3) + if val > 1: + state.citation.set_confidence_level(val + 1) + else: + state.citation.set_confidence_level(val) + + def __citation_note(self, line, state): + """ + Parses the NOTE line of an SOUR instance tag + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.citation, state) + + # ---------------------------------------------------------------------- + # + # SOUR parsing + # + # ---------------------------------------------------------------------- + + def __parse_source(self, name, level): + """ + n @@ SOUR {1:1} + +1 DATA {0:1} + +2 EVEN {0:M} + +3 DATE {0:1} + +3 PLAC {0:1} + +2 AGNC {0:1} + +2 <> {0:M} + +1 AUTH {0:1} + +1 TITL {0:1} + +1 ABBR {0:1} + +1 PUBL {0:1} + +1 TEXT {0:1} + +1 <> {0:1} + +1 <> {0:M} + +1 <> {0:M} + +1 REFN {0:M} + +2 TYPE {0:1} + +1 RIN {0:1} + +1 <> {0:1} + """ + + state = CurrentState() + state.source = self.__find_or_create_source(self.sid_map[name]) + # SOURce with the given gramps_id had no title + state.source.set_title(_("No title - ID %s") % state.source.get_gramps_id()) + state.level = level + + self.__parse_level(state, self.source_func, self.__undefined) + self.__check_msgs( + _("SOUR (source) Gramps ID %s") % state.source.get_gramps_id(), + state, + state.source, + ) + self.dbase.commit_source(state.source, self.trans, state.source.change) + + def __source_attr(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + sattr = SrcAttribute() + sattr.set_type(line.token_text) + sattr.set_value(line.data) + state.source.add_attribute(sattr) + self.__skip_subordinate_levels(state.level + 1, state) + + def __source_object(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__obje(line, state, state.source) + + def __source_chan(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_change(line, state.source, state.level + 1, state) + + def __source_repo(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if line.data and line.data[0] == "@": + # This deals with the standard GEDCOM + # SOURCE_REPOSITORY_CITATION: = + # n REPO @@ {1:1} + # +1 <> {0:M} + # +1 CALN {0:M} + # +2 MEDI {0:1} + gid = self.rid_map[line.data] + repo = self.__find_or_create_repository(gid) + elif line.data == "": + # This deals with the non-standard GEDCOM format found in Family + # Tree Maker for Windows, Broderbund Software, Banner Blue + # Division: + # SOURCE_REPOSITORY_CITATION: = + # n REPO {1:1} + # +1 <> {0:M} + # +1 CALN {0:M} + # +2 MEDI {0:1} + # + # This format has no repository name. See https://west-https://west- + # penwith.org.uk/misc/ftmged.htm which points out this is + # incorrect + gid = self.rid_map[""] + repo = self.__find_or_create_repository(gid) + self.dbase.commit_repository(repo, self.trans) + else: + # This deals with the non-standard GEDCOM + # SOURCE_REPOSITORY_CITATION: = + # n REPO {1:1} + # +1 <> {0:M} + # +1 CALN {0:M} + # +2 MEDI {0:1} + # This seems to be used by Heredis 8 PC. Heredis is notorious for + # non-standard GEDCOM. + gid = self.repo2id.get(line.data) + if gid is None: + gid = self.rid_map[""] + repo = self.__find_or_create_repository(gid) + self.repo2id[line.data] = repo.get_gramps_id() + repo.set_name(line.data) + self.dbase.commit_repository(repo, self.trans) + + repo_ref = RepoRef() + repo_ref.set_reference_handle(repo.handle) + + sub_state = CurrentState() + sub_state.repo_ref = repo_ref + sub_state.level = state.level + 1 + + self.__parse_level(sub_state, self.repo_ref_tbl, self.__undefined) + state.msg += sub_state.msg + + state.source.add_repo_reference(repo_ref) + + def __repo_ref_call(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.repo_ref.set_call_number(line.data) + # self.__skip_subordinate_levels(state.level + 1, state) + + def __repo_ref_medi(self, line, state): + name = line.data + mtype = MEDIA_MAP.get(name.lower(), (SourceMediaType.CUSTOM, name)) + state.repo_ref.set_media_type(mtype) + + def __repo_ref_note(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.repo_ref, state) + + def __repo_chan(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_change(line, state.repo, state.level + 1, state) + + def __source_abbr(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.source.set_abbreviation(line.data) + + def __source_agnc(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + attr = Attribute() + attr.set_type(AttributeType.AGENCY) + attr.set_value(line.data) + state.source.add_attribute(attr) + + def __source_note(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.source, state) + + def __source_auth(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.source.set_author(line.data) + + def __source_publ(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.source.set_publication_info(line.data) + self.__skip_subordinate_levels(state.level + 1, state) + + def __source_title(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.source.set_title(line.data.replace("\n", " ")) + + def __source_taxt_peri(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if state.source.get_title() == "": + state.source.set_title(line.data.replace("\n", " ")) + + # ---------------------------------------------------------------------- + # + # OBJE parsing + # + # ---------------------------------------------------------------------- + + def __parse_obje(self, line): + """ + n @XREF:OBJE@ OBJE {1:1} # v5.5 layout + +1 FILE {1:1} # de-facto extension + +1 FORM {1:1} + +1 TITL {0:1} + +1 <> {0:M} p.* + +1 BLOB {1:1} # Deprecated, no support + +2 CONT {1:M} + +1 OBJE @@ /* chain */ {0:1} # Deprecated, no support + +1 REFN {0:M} + +2 TYPE {0:1} + +1 RIN {0:1} + +1 <> {0:1} + + n @XREF:OBJE@ OBJE {1:1} # v5.5.1 layout + +1 FILE {1:M} # multi files, no support + +2 FORM {1:1} + +3 TYPE {0:1} + +2 TITL {0:1} + +2 DATE {0:1} # FTM extension + +2 TEXT # FTM extension + +1 REFN {0:M} + +2 TYPE {0:1} + +1 RIN {0:1} + +1 <> {0:M} + +1 <> {0:M} + +1 <> {0:1} + """ + gid = line.token_text.strip() + media = self.__find_or_create_media(self.oid_map[gid]) + + state = CurrentState() + state.media = media + state.level = 1 + + self.__parse_level(state, self.obje_func, self.__undefined) + + if state.media.get_path() == "": + self.__add_msg(_("Filename omitted"), line, state) + # deal with mime types + value = mimetypes.guess_type(state.media.get_path()) + if value and value[0]: # found from filename + state.media.set_mime_type(value[0]) + else: # get from OBJE.FILE.FORM + if "/" in state.form: # already has expanded mime type + state.media.set_mime_type(state.form) + else: + value = mimetypes.types_map.get("." + state.form, _("unknown")) + state.media.set_mime_type(value) + # Add the default reference if no source has found + self.__add_default_source(media) + + # Add a default tag if provided + self.__add_default_tag(media) + + self.__check_msgs( + _("OBJE (multi-media object) Gramps ID %s") % media.get_gramps_id(), + state, + media, + ) + # commit the person to the database + self.dbase.commit_media(media, self.trans, media.change) + + def __obje_form(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.form = line.data.lower().strip() + + def __obje_file(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + # The following checks for the odd "feature" of GEDCOM 5.5.1 that + # allows multiple files to be attached to a single OBJE; not supported + if state.media.get_path() != "": + self.__add_msg(_("Multiple FILE in a single OBJE ignored"), line, state) + self.__skip_subordinate_levels(state.level + 1, state) + return + res = urlparse(line.data) + if line.data != "" and ( + res.scheme == "" or len(res.scheme) == 1 or res.scheme == "file" + ): + file_ok, filename = self.__find_file(line.data, self.dir_path) + if state.form != "url": + # Might not work if FORM doesn't precede FILE + if not file_ok: + self.__add_msg(_("Could not import %s") % line.data, line, state) + path = filename + else: + path = line.data + + state.media.set_path(path) + if not state.media.get_description(): + state.media.set_description(path.replace("\\", "/")) + + def __obje_title(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.media.set_description(line.data) + + # FTM non-standard TEXT in OBJE, treat as note. + def __obje_text(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + new_note = Note(line.data) + new_note.set_gramps_id(self.nid_map[""]) + new_note.set_handle(create_id()) + new_note.set_type(NoteType.MEDIA) + self.dbase.commit_note(new_note, self.trans, new_note.change) + state.media.add_note(new_note.get_handle()) + + # FTM non-standard DATE in OBJE, treat as Media Date. + def __obje_date(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.media.set_date_object(line.data) + + def __obje_note(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.media, state) + + def __obje_sour(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.media.add_citation(self.handle_source(line, state.level, state)) + + def __obje_refn(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__do_refn(line, state, state.media) + + def __obje_type(self, line, state): + """ + +1 FILE {1:M} + +2 FORM {1:1} + +3 TYPE {0:1} # v5.5.1 + + Source_Media_type is one of (Photo, Audio, Book, etc.) + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + attr = Attribute() + mtype = MEDIA_MAP.get(line.data.lower(), (SourceMediaType.CUSTOM, line.data)) + attr.set_type(_("Media-Type")) + attr.set_value(str(SourceMediaType(mtype))) + state.media.attribute_list.append(attr) + + def __obje_rin(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + attr = Attribute() + attr.set_type(line.token_text) # Attribute: RIN + attr.set_value(line.data) + state.media.attribute_list.append(attr) + + def __obje_chan(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_change(line, state.media, state.level + 1, state) + + def __person_attr_type(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if state.attr.get_type() == "": + if line.data in GED_TO_GRAMPS_EVENT: + name = GED_TO_GRAMPS_EVENT[line.data] + else: + name = line.data + state.attr.set_type(name) + else: + self.__ignore(line, state) + + def __person_attr_source(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.attr.add_citation(self.handle_source(line, state.level, state)) + + def __person_attr_place(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + val = line.data + if state.attr.get_value() == "": + state.attr.set_value(val) + self.__skip_subordinate_levels(state.level + 1, state) + else: + self.__ignore(line, state) + + def __person_attr_note(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.attr, state) + + # ---------------------------------------------------------------------- + # + # REPO parsing + # + # ---------------------------------------------------------------------- + + def __parse_repo(self, line): + """ + n @@ REPO {1:1} + +1 NAME {0:1} p.* + +1 <> {0:1} p.* + +1 <> {0:M} p.* + +1 REFN {0:M} p.* + +1 RIN {0:1} p.* + +1 <> {0:1} p. + """ + repo = self.__find_or_create_repository(self.rid_map[line.token_text]) + + state = CurrentState() + state.repo = repo + state.level = 1 + self.__parse_level(state, self.repo_parse_tbl, self.__ignore) + + self.__check_msgs( + _("REPO (repository) Gramps ID %s") % repo.get_gramps_id(), state, repo + ) + self.dbase.commit_repository(repo, self.trans, repo.change) + + def __repo_name(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.repo.set_name(line.data) + + def __repo_note(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.repo, state) + + def __repo_addr(self, line, state): + """ + Parses the REPOsitory and HEADer COPR + + n ADDR {0:1} + +1 CONT {0:M} + +1 ADR1 {0:1} (Street) + +1 ADR2 {0:1} (Locality) + +1 CITY {0:1} + +1 STAE {0:1} + +1 POST {0:1} + +1 CTRY {0:1} + n PHON {0:3} + + Some repositories do not try to break up the address, + instead they put everything on a single line. Try to determine + if this happened, and try to fix it. + """ + free_form = line.data + + sub_state = CurrentState(level=state.level + 1) + sub_state.addr = Address() + + self.__parse_level(sub_state, self.parse_addr_tbl, self.__ignore) + state.msg += sub_state.msg + + self.__merge_address(free_form, sub_state.addr, line, state) + state.repo.add_address(sub_state.addr) + + def __repo_phon(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + address_list = state.repo.get_address_list() + if address_list: + if address_list[0].get_phone(): + self.__add_msg(_("Only one phone number supported"), line, state) + else: + address_list[0].set_phone(line.data) + + def __repo_fax(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + url = Url() + url.set_path(line.data) + url.set_type(UrlType(_("FAX"))) + state.repo.add_url(url) + + def __repo_www(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + url = Url() + url.set_path(line.data) + url.set_type(UrlType(UrlType.WEB_HOME)) + state.repo.add_url(url) + + def __repo_email(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + url = Url() + url.set_path(line.data) + url.set_type(UrlType(UrlType.EMAIL)) + state.repo.add_url(url) + + def __location_adr1(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if not state.location: + state.location = Location() + if state.location.get_street() != "": + self.__add_msg(_("Warn: ADDR overwritten"), line, state) + state.location.set_street(line.data) + + def __location_adr2(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if not state.location: + state.location = Location() + state.location.set_locality(line.data) + + def __location_city(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if not state.location: + state.location = Location() + state.location.set_city(line.data) + + def __location_stae(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if not state.location: + state.location = Location() + state.location.set_state(line.data) + + def __location_post(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if not state.location: + state.location = Location() + state.location.set_postal_code(line.data) + + def __location_ctry(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if not state.location: + state.location = Location() + state.location.set_country(line.data) + + def __location_phone(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if not state.location: + state.location = Location() + state.location.set_phone(line.data) + + def __location_note(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if state.event: + self.__parse_note(line, state.place, state) + else: + # This causes notes below SUBMitter to be ignored + self.__not_recognized(line, state) + + def __optional_note(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.__parse_note(line, state.obj, state) + + # ---------------------------------------------------------------------- + # + # HEAD parsing + # + # ---------------------------------------------------------------------- + + def __parse_header(self): + """ + Handling of the lines subordinate to the HEAD GEDCOM tag + + n HEAD {1:1} + +1 SOUR {1:1} + +2 VERS {0:1} + +2 NAME {0:1} + +2 CORP {0:1} + +3 <> {0:1} + +2 DATA {0:1} + +3 DATE {0:1} + +3 COPR {0:1} + +1 DEST {0:1*} + +1 DATE {0:1} + +2 TIME {0:1} + +1 SUBM @@ {1:1} + +1 SUBN @@ {0:1} + +1 FILE {0:1} + +1 COPR {0:1} + +1 GEDC {1:1} + +2 VERS {1:1} + +2 FORM {1:1} + +1 CHAR {1:1} + +2 VERS {0:1} + +1 LANG {0:1} + +1 PLAC {0:1} + +2 FORM {1:1} + +1 NOTE {0:1} + +2 [CONT|CONC] {0:M} + + * NOTE: Submissions to the Family History Department for Ancestral + File submission or for clearing temple ordinances must use a + DESTination of ANSTFILE or TempleReady. + + """ + state = CurrentState(level=1) + self.__parse_level(state, self.head_parse_tbl, self.__undefined) + self.__check_msgs(_("HEAD (header)"), state, None) + + def __header_sour(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if line.data.strip() in ["FTW", "FTM"]: + self.is_ftw = True + # Some software (e.g. RootsMagic (https://files.rootsmagic.com/PAF- + # Book/RootsMagic-for-PAF-Users-Printable.pdf) use the Addr fields for + # 'Place Details (address, hospital, cemetary)' + if line.data.strip().lower() in ["rootsmagic"]: + self.addr_is_detail = True + # We will use the approved system ID as the name of the generating + # software, in case we do not get the name in the proper place + self.genby = line.data + if self.use_def_src: + sattr = SrcAttribute() + sattr.set_type(_("Approved system identification")) + sattr.set_value("%s" % self.genby) + self.def_src.add_attribute(sattr) + sub_state = CurrentState(level=state.level + 1) + self.__parse_level(sub_state, self.header_sour_parse_tbl, self.__undefined) + state.msg += sub_state.msg + # We can't produce the 'Generated by' statement till the end of the + # SOUR level, because the name and version may come in any order + if self.use_def_src: + # feature request 2356: avoid genitive form + sattr = SrcAttribute() + sattr.set_type(_("Generated By")) + sattr.set_value("%s %s" % (self.genby, self.genvers)) + self.def_src.add_attribute(sattr) + + def __header_sour_name(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + # This is where the name of the product that generated the GEDCOM file + # should appear, and this will overwrite the approved system ID, if any + self.genby = line.data + if self.use_def_src: + sattr = SrcAttribute() + sattr.set_type(_("Name of software product")) + sattr.set_value(self.genby) + self.def_src.add_attribute(sattr) + + def __header_sour_vers(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.genvers = line.data + if self.use_def_src: + sattr = SrcAttribute() + sattr.set_type(_("Version number of software product")) + sattr.set_value(self.genvers) + self.def_src.add_attribute(sattr) + + def __header_sour_corp(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + repo = Repository() + sub_state = CurrentState(level=state.level + 1) + sub_state.repo = repo + self.__parse_level(sub_state, self.header_corp_addr, self.__undefined) + state.msg += sub_state.msg + + if self.use_def_src: + repo.set_name(_("Business that produced the product: %s") % line.data) + rtype = RepositoryType() + rtype.set((RepositoryType.CUSTOM, _("GEDCOM data"))) + repo.set_type(rtype) + self.dbase.add_repository(repo, self.trans) + repo_ref = RepoRef() + repo_ref.set_reference_handle(repo.handle) + mtype = SourceMediaType() + mtype.set((SourceMediaType.UNKNOWN, "")) + repo_ref.set_media_type(mtype) + self.def_src.add_repo_reference(repo_ref) + + def __header_sour_data(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if self.use_def_src: + sattr = SrcAttribute() + sattr.set_type(_("Name of source data")) + sattr.set_value(line.data) + self.def_src.add_attribute(sattr) + sub_state = CurrentState(level=state.level + 1) + self.__parse_level(sub_state, self.header_sour_data, self.__undefined) + state.msg += sub_state.msg + + def __header_sour_copr(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if self.use_def_src: + sattr = SrcAttribute() + sattr.set_type(_("Copyright of source data")) + sattr.set_value(line.data) + self.def_src.add_attribute(sattr) + + def __header_sour_date(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if self.use_def_src: + # Because there is a DATE tag, line.data is automatically converted + # to a Date object before getting to this point, so it has to be + # converted back to a string + text_date = str(line.data) + sattr = SrcAttribute() + sattr.set_type(_("Publication date of source data")) + sattr.set_value(text_date) + self.def_src.add_attribute(sattr) + + def __header_file(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if self.use_def_src: + filename = os.path.basename(line.data).split("\\")[-1] + # feature request 2356: avoid genitive form + self.def_src.set_title(_("Import from %s") % filename) + + def __header_copr(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if self.use_def_src: + self.def_src.set_publication_info(line.data) + + def __header_subm(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + +1 SUBM @@ {1:1} + This should be simply be a cross-reference to the correct Submitter + record. Note that there can be multiple Submitter records, so it is + necessary to remember which one should be applied. + + """ + self.subm = line.data[1:-1] + sub_state = CurrentState(level=state.level + 1) + self.__parse_level(sub_state, self.header_subm, self.__ignore) + state.msg += sub_state.msg + + def __header_subn(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if self.use_def_src: + sattr = SrcAttribute() + sattr.set_type(_("Submission record identifier")) + sattr.set_value(line.token_text) + self.def_src.add_attribute(sattr) + + def __header_lang(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if self.use_def_src: + sattr = SrcAttribute() + sattr.set_type(_("Language of GEDCOM text")) + sattr.set_value(line.data) + self.def_src.add_attribute(sattr) + + def __header_dest(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + FIXME: This processing does not depend on DEST, so there seems to be + no reason for it to be placed here. Perhaps it is supposed to be after + all the SOUR levels have been processed, but self.genby was only + assigned by the initial SOUR tag, so this could have been done there. + Perhaps, as suggested by the text of the error message, it was + supposed to test whenther the_DEST_ was LEGACY, in which case the + coding is now wrong. + """ + if self.genby.upper() == "LEGACY": + fname = os.path.basename(self.filename) + self.user.warn( + _( + "Import of GEDCOM file %(filename)s with DEST=%(by)s, " + "could cause errors in the resulting database!" + ) + % {"filename": fname, "by": self.genby}, + _("Look for nameless events."), + ) + + def __header_char(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + # +1 CHAR {1:1} + # +2 VERS {0:1} + encoding = line.data + version = "" + while True: + line = self.__get_next_line() + if self.__level_is_finished(line, state.level + 1): + break + elif line.token == TOKEN_VERS: + version = line.data + + if self.use_def_src: + if version == "": + sattr = SrcAttribute() + sattr.set_type(_("Character set")) + sattr.set_value(encoding) + self.def_src.add_attribute(sattr) + else: + sattr = SrcAttribute() + sattr.set_type(_("Character set and version")) + sattr.set_value("%s %s" % (encoding, version)) + self.def_src.add_attribute(sattr) + + def __header_gedc(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + while True: + line = self.__get_next_line() + if self.__level_is_finished(line, state.level + 1): + break + elif line.token == TOKEN_VERS: + if (not line.data) or line.data[0] != "5": + self.__add_msg(_("GEDCOM version not supported"), line, state) + if self.use_def_src: + sattr = SrcAttribute() + sattr.set_type(_("GEDCOM version")) + sattr.set_value(line.data) + self.def_src.add_attribute(sattr) + elif line.token == TOKEN_FORM: + if line.data == "LINEAGE-LINKED": + pass + elif line.data.upper() == "LINEAGE-LINKED": + # Allow Lineage-Linked etc. though it should be in + # uppercase (Note: Gramps is not a validator! prc) + self.__add_msg(_("GEDCOM FORM should be in uppercase"), line, state) + else: + self.__add_msg(_("GEDCOM FORM not supported"), line, state) + if self.use_def_src: + sattr = SrcAttribute() + sattr.set_type(_("GEDCOM form")) + sattr.set_value(line.data) + self.def_src.add_attribute(sattr) + + def __header_plac(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + sub_state = CurrentState(level=state.level + 1) + self.__parse_level(sub_state, self.place_form, self.__undefined) + state.msg += sub_state.msg + + def __place_form(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + self.place_parser.parse_form(line) + + def __header_date(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + This processes the , i.e. the date when this + [GEDCOM] transmission was created (as opposed to the date when the + source data that was used to create the transmission was published or + created + + Because there is a DATE tag, line.data is automatically converted to a + Date object before getting to this point, so it has to be converted + back to a string + """ + tx_date = str(line.data) + tx_time = "" + line = self.__get_next_line() + if self.__level_is_finished(line, state.level): + pass + elif line.token == TOKEN_TIME: + tx_time = str(line.data) + + if self.use_def_src: + if tx_time == "": + sattr = SrcAttribute() + sattr.set_type(_("Creation date of GEDCOM")) + sattr.set_value(tx_date) + self.def_src.add_attribute(sattr) + else: + sattr = SrcAttribute() + sattr.set_type(_("Creation date and time of GEDCOM")) + sattr.set_value("%s %s" % (tx_date, tx_time)) + self.def_src.add_attribute(sattr) + + def __header_note(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if self.use_def_src: + self.__parse_note(line, self.def_src, state) + + def __header_subm_name(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if self.use_def_src: + self.def_src.set_author(line.data) + + def __parse_note(self, line, obj, state): + if line.token == TOKEN_RNOTE: + # reference to a named note defined elsewhere + # NOTE_STRUCTURE: = + # n NOTE @@ {1:1} + # +1 SOUR @@ {0:M} # 5.5 only, not in 5.5.1 + handle = self.__find_note_handle(self.nid_map[line.data]) + obj.add_note(handle) + self.note_type_map[handle] = OBJ_NOTETYPE.get( + type(obj).__name__, NoteType.GENERAL + ) + else: + # Embedded note + # NOTE_STRUCTURE: = + # n NOTE [ | ] {1:1} + # +1 [ CONC | CONT ] {0:M} + # +1 SOUR @@ {0:M} + if not line.data: + self.__add_msg(_("Empty note ignored"), line, state) + self.__skip_subordinate_levels(line.level + 1, state) + else: + new_note = Note(line.data) + new_note.set_gramps_id(self.nid_map[""]) + new_note.set_handle(create_id()) + + sub_state = CurrentState(level=state.level + 1) + sub_state.note = new_note + self.__parse_level(sub_state, self.note_parse_tbl, self.__undefined) + state.msg += sub_state.msg + + # Add a default tag if provided + self.__add_default_tag(new_note) + # Set the type of the note + new_note.set_type( + OBJ_NOTETYPE.get(type(obj).__name__, NoteType.GENERAL) + ) + self.dbase.commit_note(new_note, self.trans, new_note.change) + obj.add_note(new_note.get_handle()) + + # ---------------------------------------------------------------------- + # + # NOTE parsing + # + # ---------------------------------------------------------------------- + + def __parse_inline_note(self, line, level): + """ + Handling of lines subordinate to the NOTE GEDCOM tag + + n @@ NOTE {1:1} + +1 [ CONC | CONT] {0:M} + +1 <> {0:M} + +1 REFN {0:M} + +2 TYPE {0:1} + +1 RIN {0:1} + +1 <> {0:1} + """ + state = CurrentState(level=1) + if ( + not line.data + and self.nid_map.clean(line.token_text) not in self.nid_map.map() + ): + self.__add_msg(_("Empty note ignored"), line) + self.__skip_subordinate_levels(level, state) + else: + gid = self.nid_map[line.token_text] + handle = self.__find_note_handle(gid) + new_note = Note(line.data) + new_note.set_handle(handle) + new_note.set_gramps_id(gid) + if handle in self.note_type_map: + new_note.set_type(self.note_type_map[handle]) + sub_state = CurrentState(level=state.level) + sub_state.note = new_note + self.__parse_level(sub_state, self.note_parse_tbl, self.__undefined) + state.msg += sub_state.msg + + self.dbase.commit_note(new_note, self.trans, new_note.change) + self.__check_msgs( + _("NOTE Gramps ID %s") % new_note.get_gramps_id(), state, None + ) + + def __note_chan(self, line, state): + if state.note: + self.__parse_change(line, state.note, state.level + 1, state) + + def __parse_source_reference(self, citation, level, handle, state): + """ + Read the data associated with a SOUR reference. + """ + sub_state = CurrentState(level=level + 1) + sub_state.citation = citation + sub_state.handle = handle + self.__parse_level(sub_state, self.citation_parse_tbl, self.__ignore) + state.msg += sub_state.msg + + def __parse_header_head(self): + """ + Validate that this is a valid GEDCOM file. + """ + line = self.__get_next_line() + if line.token != TOKEN_HEAD: + raise GedcomError("%s is not a GEDCOM file" % self.filename) + + def __parse_submission(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Handling of lines subordinate to the level 0 SUMN (Submission) GEDCOM + tag + + n @@ SUBN {1:1] + +1 SUBM @@ {0:1} + +1 FAMF {0:1} + +1 TEMP {0:1} + +1 ANCE {0:1} + +1 DESC {0:1} + +1 ORDI {0:1} + +1 RIN {0:1} + +1 NOTE {0:m} + """ + if not self.use_def_src: + # no place to put data, so call it not recognized + self.__not_recognized(line, state) + return + while True: + line = self.__get_next_line() + msg = "" + if self.__level_is_finished(line, state.level): + break + elif line.token == TOKEN_SUBM: + msg = _("Submission: Submitter") + elif line.token == TOKEN_UNKNOWN and line.token_text == "FAMF": + msg = _("Submission: Family file") + elif line.token == TOKEN_TEMP: + msg = _("Submission: Temple code") + elif line.token == TOKEN_UNKNOWN and line.token_text == "ANCE": + msg = _("Submission: Generations of ancestors") + elif line.token == TOKEN_UNKNOWN and line.token_text == "DESC": + msg = _("Submission: Generations of descendants") + elif line.token == TOKEN_UNKNOWN and line.token_text == "ORDI": + msg = _("Submission: Ordinance process flag") + elif line.token == TOKEN_NOTE or line.token == TOKEN_RNOTE: + self.__parse_note(line, self.def_src, state) + self.dbase.commit_source(self.def_src, self.trans) + else: + self.__not_recognized(line, state) + continue + + if msg != "": + sattr = SrcAttribute() + sattr.set_type(msg) + sattr.set_value(line.data) + self.def_src.add_attribute(sattr) + self.dbase.commit_source(self.def_src, self.trans) + + def handle_source(self, line, level, state): + """ + Handle the specified source, building a source reference to + the object. + """ + citation = Citation() + if line.data and line.data[0] != "@": + title = line.data + handle = self.inline_srcs.get(title, create_id()) + src = Source() + src.handle = handle + src.gramps_id = self.sid_map[""] + self.inline_srcs[title] = handle + else: + src = self.__find_or_create_source(self.sid_map[line.data]) + # We need to set the title to the cross reference identifier of the + # SOURce record, just in case we never find the source record. If + # we didn't find the source record, then the source object would + # have got deleted by Chack and repair because the record is empty. + # If we find the source record, the title is overwritten in + # __source_title. + if not src.title: + src.set_title(line.data) + self.dbase.commit_source(src, self.trans) + self.__parse_source_reference(citation, level, src.handle, state) + citation.set_reference_handle(src.handle) + self.dbase.add_citation(citation, self.trans) + return citation.handle + + def __parse_change(self, line, obj, level, state): + """ + CHANGE_DATE:= + + > n CHAN {1:1} + > +1 DATE {1:1} + > +2 TIME {0:1} + > +1 <> {0:M} + + The Note structure is ignored, since we have nothing + corresponding in Gramps. + + Based on the values calculated, attempt to convert to a valid + change time using time.strptime. If this fails (and it shouldn't + unless the value is meaningless and doesn't conform to the GEDCOM + spec), the value is ignored. + """ + tstr = None + dstr = None + dobj = None + while True: + line = self.__get_next_line() + if self.__level_is_finished(line, level): + break + elif line.token == TOKEN_TIME: + tstr = line.data + elif line.token == TOKEN_DATE: + # Lexer converted already to Date object + dobj = line.data + elif line.token == TOKEN_NOTE or line.token == TOKEN_RNOTE: + self.__ignore(line, state) + else: + self.__not_recognized(line, state) + + # Attempt to convert the values to a valid change time + if dobj: + dstr = "%s %s %s" % (dobj.get_day(), dobj.get_month(), dobj.get_year()) + try: + if tstr: + try: + tstruct = time.strptime( + "%s %s" % (dstr, tstr), "%d %m %Y %H:%M:%S" + ) + except ValueError: + # seconds is optional in GEDCOM + tstruct = time.strptime( + "%s %s" % (dstr, tstr), "%d %m %Y %H:%M" + ) + else: + tstruct = time.strptime(dstr, "%d %m %Y") + val = time.mktime(tstruct) + obj.change = val + except (ValueError, OverflowError): + # parse of time structure failed, so ignore. According to the + # Python manual: "The functions in this [time] module do not + # handle dates and times before the epoch or far in the future. + # The cut-off point in the future is determined by the C + # library; for Unix, it is typically in 2038." If the time is + # too far in the future, this gives OverflowError. + pass + + def __do_refn(self, line, state, obj): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + @param obj: The object to attach the attribute + @type obj: Gramps primary object + """ + attr = Attribute() + attr.set_type(line.token_text) # Atrribute : REFN + attr.set_value(line.data) + # if there is a subsequent TYPE, we add it as a note to the attribute + line = self.__chk_subordinate(state.level + 1, state, TOKEN_TYPE) + if line: + new_note = Note(line.data) + new_note.set_gramps_id(self.nid_map[""]) + new_note.set_handle(create_id()) + new_note.set_type("REFN-TYPE") + self.dbase.commit_note(new_note, self.trans, new_note.change) + attr.add_note(new_note.get_handle()) + obj.attribute_list.append(attr) + + def __build_event_pair(self, state, event_type, event_map, description): + """ + n TYPE {0:1} p.* + n DATE {0:1} p.*/* + n <> {0:1} p.* + n <> {0:1} p.* + n AGE {0:1} p.* + n AGNC {0:1} p.* + n CAUS {0:1} p.* + n <> {0:M} p.* + n <> {0:M} p.*, * + n <> {0:M} p. + """ + event = Event() + event_ref = EventRef() + event.set_gramps_id(self.emapper.find_next()) + event.set_type(event_type) + + if description and description != "Y": + event.set_description(description) + self.dbase.add_event(event, self.trans) + + sub_state = CurrentState() + sub_state.level = state.level + 1 + sub_state.event_ref = event_ref + sub_state.event = event + sub_state.person = state.person + sub_state.pf = self.place_parser + + self.__parse_level(sub_state, event_map, self.__undefined) + if ( + description == "Y" + and event.date.is_empty() + and event.type == EventType.BIRTH + and not event.place + ): + event.set_description(_("No Date Information")) + state.msg += sub_state.msg + + self.__add_place(event, sub_state) + + self.dbase.commit_event(event, self.trans) + + event_ref.set_reference_handle(event.handle) + return event_ref + + def __build_family_event_pair(self, state, event_type, event_map, description): + event = Event() + event_ref = EventRef() + event.set_gramps_id(self.emapper.find_next()) + event.set_type(event_type) + if description and description != "Y": + event.set_description(description) + + self.dbase.add_event(event, self.trans) + + sub_state = CurrentState() + sub_state.family = state.family + sub_state.level = state.level + 1 + sub_state.event = event + sub_state.event_ref = event_ref + sub_state.pf = self.place_parser + + self.__parse_level(sub_state, event_map, self.__undefined) + state.msg += sub_state.msg + + self.__add_place(event, sub_state) + + self.dbase.commit_event(event, self.trans) + event_ref.set_reference_handle(event.handle) + return event_ref + + def __do_photo(self, state): + """ + Choose the primary photo from the list of media present for this + person. Supports FTM _PHOTO. and others _PRIM feature. + 0 INDI + +1 _PHOTO @@ {1:1} + + 0 INDI + +1 OBJE @@ + +2 _PRIM + + 0 INDI + +1 OBJE + +2 FILE primary_photo.jpg + +2 _PRIM + + For the _PHOTO varient, state.photo contains the XREF ('@M1@'). + For the _PRIM varients, state.photo contains the handle. + Since Gramps currently uses the first media in the list as the + primary, find the primary photo if already in the list, if present, + move to beginning. If not present, add at the beginning. + This is run after all of the person processing is complete but before + committing the person. + """ + if state.photo.startswith("@"): + gramps_id = self.oid_map[state.photo] + handle = self.__find_media_handle(gramps_id) + elif state.photo: + handle = state.photo + else: + return + for mref in state.person.media_list: + if handle == mref.ref: + state.person.media_list.remove(mref) + state.person.media_list.insert(0, mref) + return + mref = MediaRef() + mref.set_reference_handle(handle) + state.person.media_list.insert(0, mref) + + def __extract_temple(self, line): + """Determine the LDS Temple from the input line""" + + def get_code(code): + """get the Temple code""" + if TEMPLES.is_valid_code(code): + return code + elif TEMPLES.is_valid_name(code): + return TEMPLES.code(code) + + code = get_code(line.data) + if code: + return code + + # Not sure why we do this. Kind of ugly. + code = get_code(line.data.split()[0]) + if code: + return code + + # Okay we have no clue which temple this is. + # We should tell the user and store it anyway. + self.__add_msg(_("Invalid temple code"), line, None) + return line.data + + def __add_default_source(self, obj): + """ + Add the default source to the object. + """ + if self.use_def_src and len(obj.get_citation_list()) == 0: + citation = Citation() + citation.set_reference_handle(self.def_src.handle) + self.dbase.add_citation(citation, self.trans) + obj.add_citation(citation.handle) + + def __add_default_tag(self, obj): + """ + Add the default tag to the object. + """ + if self.default_tag: + obj.add_tag(self.default_tag.handle) + + def __subm_name(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.res.set_name(line.data) + + def __subm_addr(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + free_form = line.data + + sub_state = CurrentState(level=state.level + 1) + sub_state.location = state.res + + self.__parse_level(sub_state, self.parse_loc_tbl, self.__undefined) + state.msg += sub_state.msg + + self.__merge_address(free_form, state.res, line, state) + # Researcher is a sub-type of LocationBase, so get_street and + # set_street which are used in routines called from self.parse_loc_tbl + # work fine. + # Unfortunately, Researcher also has get_address and set_address, so we + # need to copy the street into that. + state.res.set_address(state.res.get_street()) + + def __subm_phon(self, line, state): + """ + n PHON {0:3} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + if state.res.get_phone(): + self.__add_msg(_("Only one phone number supported"), line, state) + else: + state.res.set_phone(line.data) + + def __subm_email(self, line, state): + """ + n EMAIL {0:3} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + # only record the first multiple emails for researcher + if not state.res.get_email(): + state.res.set_email(line.data) + self.__repo_email(line, state) + + +# ------------------------------------------------------------------------- +# +# GedcomStageOne +# +# ------------------------------------------------------------------------- +class GedcomStageOne: + """ + The GedcomStageOne parser scans the file quickly, looking for a few things. + This includes: + + 1. Character set encoding + 2. Number of people and families in the list + 3. Child to family references, since Ancestry.com creates GEDCOM files + without the FAMC references. + """ + + __BAD_UTF16 = _( + "Your GEDCOM file is corrupted. " + "The file appears to be encoded using the UTF16 " + "character set, but is missing the BOM marker." + ) + __EMPTY_GED = _("Your GEDCOM file is empty.") + + @staticmethod + def __is_xref_value(value): + """ + Return True if value is in the form of a XREF value. We assume that + if we have a leading '@' character, then we are okay. + """ + return value and value[0] == "@" + + def __init__(self, ifile): + self.ifile = ifile + self.famc = defaultdict(list) + self.fams = defaultdict(list) + self.enc = "" + self.pcnt = 0 + self.lcnt = 0 + + def __detect_file_decoder(self, input_file): + """ + Detects the file encoding of the file by looking for a BOM + (byte order marker) in the GEDCOM file. If we detect a UTF-16 or + UTF-8-BOM encoded file, we choose appropriate decoders. If no BOM + is detected, we return in UTF-8 mode it is the more modern option; + and anyway it doesn't really matter as we are only looking for GEDCOM + keywords which are only 7-bit ASCII anyway. + In any case, we Always return the file in text mode with transparent + newline (CR, LF, or CRLF). + """ + line = input_file.read(2) + if line == b"\xef\xbb": + input_file.read(1) + self.enc = "utf_8_sig" + return TextIOWrapper( + input_file, encoding="utf_8_sig", errors="replace", newline=None + ) + elif line == b"\xff\xfe" or line == b"\xfe\xff": + self.enc = "UTF16" + input_file.seek(0) + return TextIOWrapper( + input_file, encoding="utf_16", errors="replace", newline=None + ) + elif not line: + raise GedcomError(self.__EMPTY_GED) + elif line == b"\x30\x00" or line == b"\x00\x30": + raise GedcomError(self.__BAD_UTF16) + else: + input_file.seek(0) + return TextIOWrapper( + input_file, encoding="utf-8", errors="replace", newline=None + ) + + def parse(self): + """ + Parse the input file. + """ + current_family_id = "" + + reader = self.__detect_file_decoder(self.ifile) + + for line in reader: + # Scan for a few items, keep counts. Also look for actual CHAR + # Keyword to figure out actual encodeing for non-unicode file types + line = line.strip() + if not line: + continue + self.lcnt += 1 + + try: + data = line.split(None, 3) + [""] + level, key, value = data[:3] + level = int(level) + key = key.strip() + value = value.strip() + except: + continue + + if level == 0 and key[0] == "@": + if value in ("FAM", "FAMILY"): + current_family_id = key.strip()[1:-1] + elif value in ("INDI", "INDIVIDUAL"): + self.pcnt += 1 + elif key in ("HUSB", "HUSBAND", "WIFE") and self.__is_xref_value(value): + self.fams[value[1:-1]].append(current_family_id) + elif key in ("CHIL", "CHILD") and self.__is_xref_value(value): + self.famc[value[1:-1]].append(current_family_id) + elif key == "CHAR" and not self.enc: + assert isinstance(value, str) + self.enc = value + LOG.debug("parse pcnt %d", self.pcnt) + LOG.debug("parse famc %s", dict(self.famc)) + LOG.debug("parse fams %s", dict(self.fams)) + self.ifile = reader # need this to keep python from autoclosing file + + def get_famc_map(self): + """ + Return the Person to Child Family map + """ + return self.famc + + def get_fams_map(self): + """ + Return the Person to Family map (where the person is a spouse) + """ + return self.fams + + def get_encoding(self): + """ + Return the detected encoding + """ + return self.enc.upper() + + def set_encoding(self, enc): + """ + Forces the encoding + """ + assert isinstance(enc, str) + self.enc = enc + + def get_person_count(self): + """ + Return the number of INDI records found + """ + return self.pcnt + + def get_line_count(self): + """ + Return the number of lines in the file + """ + return self.lcnt + + +# ------------------------------------------------------------------------- +# +# make_gedcom_date +# +# ------------------------------------------------------------------------- +def make_gedcom_date(subdate, calendar, mode, quality): + """ + Convert a Gramps date structure into a GEDCOM compatible date. + """ + retval = "" + day, mon, year = subdate[0:3] + mmap, prefix = CALENDAR_MAP.get(calendar, (MONTH, "")) + if year < 0: + year = -year + bce = " B.C." + else: + bce = "" + try: + retval = __build_date_string(day, mon, year, bce, mmap) + except IndexError: + print("Month index error - %d" % mon) + retval = "%d%s" % (year, bce) + if calendar == Date.CAL_SWEDISH: + # If Swedish calendar use ISO for for date and append (swedish) + # to indicate calandar + if year and not mon and not day: + retval = "%i" % (year) + else: + retval = "%i-%02i-%02i" % (year, mon, day) + retval = retval + " (swedish)" + # Skip prefix @#DUNKNOWN@ as it seems + # not used in all other genealogy applications. + # Gramps can handle it on import, but not with (swedish) appended + # to explain what calendar, the unknown refer to + prefix = "" + if prefix: + retval = "%s %s" % (prefix, retval) + if mode in DATE_MODIFIER: + retval = "%s %s" % (DATE_MODIFIER[mode], retval) + if quality in DATE_QUALITY: + retval = "%s %s" % (DATE_QUALITY[quality], retval) + return retval + + +def __build_date_string(day, mon, year, bce, mmap): + """ + Build a date string from the supplied information. + """ + if day == 0: + if mon == 0: + retval = "%d%s" % (year, bce) + elif year == 0: + retval = "(%s)" % mmap[mon] + else: + retval = "%s %d%s" % (mmap[mon], year, bce) + elif mon == 0: + retval = "%d%s" % (year, bce) + elif year == 0: + retval = "(%d %s)" % (day, mmap[mon]) + else: + retval = "%d %s %d%s" % (day, mmap[mon], year, bce) + return retval diff --git a/gramps/plugins/test/imports_test.py b/gramps/plugins/test/imports_test.py index 800d1e2c3bd..79fe6fd1da3 100644 --- a/gramps/plugins/test/imports_test.py +++ b/gramps/plugins/test/imports_test.py @@ -63,6 +63,7 @@ DbSupportedError, DbConnectionError, ) +from gramps.plugins.importer.importgedcom import importData # logger = logging.getLogger(__name__) @@ -90,6 +91,22 @@ def mock_localtime(*args): return strptime("25 Dec 1999", "%d %b %Y") +class CaptureUser(User): + """ + A User subclass that records the GEDCOM import report text instead of + displaying it, so tests can inspect the warnings without the GUI. + """ + + def __init__(self): + super().__init__() + self.report_title = "" + self.infotext = "" + + def info(self, msg1, infotext, parent=None, monospaced=False): + self.report_title = msg1 + self.infotext = infotext + + class TestImports(unittest.TestCase): """The test class cases will be dynamically created at import time from files to be tested. The following defs are used by the test cases @@ -160,6 +177,64 @@ def _report_diff(self, path, struct1, struct2): msg += _report_details(path, struct1, struct2) return msg + def test_imp_notetest_lds_in_out_in_dfs_warnings(self): + """ + Verify that importing imp_notetest_lds_in-out-in_dfs.ged produces the + expected GEDCOM import warning report, without requiring the GUI. + """ + fn1 = os.path.join(TEST_DIR, "imp_notetest_lds_in-out-in_dfs.ged") + config.set("preferences.default-source", True) + config.set("preferences.tag-on-import-format", "Imported") + config.set("preferences.tag-on-import", True) + db = make_database("sqlite") + db.load(":memory:") + db.set_feature("skip-import-additions", False) + user = CaptureUser() + importData(db, fn1, user) + self.assertEqual(user.report_title, "GEDCOM import report: 41 errors detected") + self.assertIn("ADDR element ignored '123 main'", user.infotext) + self.assertIn("Tag recognized but not supported", user.infotext) + self.assertIn("Unknown tag", user.infotext) + self.assertIn("Skipped subordinate line", user.infotext) + self.assertIn("Could not import photo.jpg", user.infotext) + + def test_imp_notetest_dfs_warnings(self): + """ + Verify that importing imp_notetest_dfs.ged produces the expected + GEDCOM import warning report, without requiring the GUI. + """ + fn1 = os.path.join(TEST_DIR, "imp_notetest_dfs.ged") + config.set("preferences.default-source", True) + config.set("preferences.tag-on-import-format", "Imported") + config.set("preferences.tag-on-import", True) + db = make_database("sqlite") + db.load(":memory:") + db.set_feature("skip-import-additions", False) + user = CaptureUser() + importData(db, fn1, user) + self.assertEqual(user.report_title, "GEDCOM import report: 66 errors detected") + self.assertIn("Line ignored as not understood", user.infotext) + self.assertIn("Empty note ignored", user.infotext) + self.assertIn("Tag recognized but not supported", user.infotext) + self.assertIn("Skipped subordinate line", user.infotext) + self.assertIn("Could not import photo.jpg", user.infotext) + + def test_imp_notetest_lds_warnings(self): + """ + Verify that importing imp_notetest_lds_dfs.ged produces the expected + number of GEDCOM import warnings, without requiring the GUI. + """ + fn1 = os.path.join(TEST_DIR, "imp_notetest_lds_dfs.ged") + config.set("preferences.default-source", True) + config.set("preferences.tag-on-import-format", "Imported") + config.set("preferences.tag-on-import", True) + db = make_database("sqlite") + db.load(":memory:") + db.set_feature("skip-import-additions", False) + user = CaptureUser() + importData(db, fn1, user) + self.assertEqual(user.report_title, "GEDCOM import report: 202 errors detected") + def _report_details(path, diff1, diff2): """Checks if a detail is significant, needs adjusting for xml filter @@ -290,7 +365,14 @@ def make_tst_function(tstfile, file_name): (glocale.lang == "en_US") or (win() and (glocale.lang == "en")) or not ( - tstfile in ["imp_MediaTest.ged", "imp_notetest_dfs.ged", "imp_sample.ged"] + tstfile + in [ + "imp_MediaTest.ged", + "imp_notetest_dfs.ged", + "imp_notetest_lds_dfs.ged", + "imp_notetest_lds_in-out-in_dfs.ged", + "imp_sample.ged", + ] ), "skipping TestImports test case %s. en_US.utf-8 locale is required." % (tstfile), diff --git a/imp_notetest_dfs_lds.ged b/imp_notetest_dfs_lds.ged new file mode 100644 index 00000000000..710d58e624a --- /dev/null +++ b/imp_notetest_dfs_lds.ged @@ -0,0 +1,198 @@ +0 HEAD +1 SOUR Legacy +2 VERS 10.0 +2 NAME Legacy (R) +2 CORP MyHeritage USA Inc. +3 ADDR 3098 W Executive Pkwy Ste 275 +4 CONT Lehi, UT 84043 USA +1 DEST Legacy +1 DATE 3 Aug 2026 +1 SUBM @S0@ +1 FILE C:\gramps_dev\gramps_legacy_import\data\tests\imp_notetest_dfs_lds.ged +1 GEDC +2 VERS 5.5.1 +2 FORM LINEAGE-LINKED +1 CHAR UTF-8 +0 @S0@ SUBM +1 NAME Not Given +0 @I1@ INDI +1 NAME Tom /Tester/ +2 GIVN Tom +2 SURN Tester +1 SEX M +1 BIRT +2 DATE 15 Jun 1900 +2 ADDR 123 main, Norwalk, Ohio, USA +3 ADR1 123 main, Norwalk, Ohio, USA +3 NOTE Location Note 2 +2 NOTE @BI1@ +1 DEAT +2 NOTE @DI1@ +1 _PPEXCLUDE +1 REFN 98765 for PERSON +1 EVEN 98765 for PERSON +2 TYPE Who knows OBJE REFN TYPE +1 _UID 8D70ED25CB654853AC16E58EEB1FD4AB9847 +1 CHAN +2 DATE 11 Jan 2001 +3 TIME 16:00 +1 OBJE +2 FILE http://homepages.rootsweb.com/~pmcbride/gedcom/55gctoc.htm +3 FORM URL +4 TYPE URL +3 TITL GEDCOM 5.5 documentation web site +2 _PRIM Y +1 OBJE +2 FILE photo.jpg +3 FORM jpg +4 TYPE PHOTO +3 TITL Tom Tester's photo +2 REFN 98765 +1 FAMS @F1@ +1 NOTE @NI1@ +1 NOTE @HI1@ +0 @NI1@ NOTE +1 CONC Tom Tester xref note +1 CONT +1 CONT Tom Tester Note +0 @HI1@ NOTE +1 CONC (Research):from yearTEST: TEST person +1 CONT NOTE: NOTE Empty note subordinate (Should be skipped) +1 CONT NOTE: TEST Empty Note +1 CONT NOTE: TEST 123456 Event Note +1 CONT NOTE: TEST Location Note +1 CONT NOTE: TEST Person Note +1 CONT OBJE: TEST media +1 CONT NOTE: TEST 123456 Note +1 CONT OBJE: RIN 123456 +0 @BI1@ NOTE +1 CONC Birth Event note +0 @DI1@ NOTE +1 CONC Death Event xref note +0 @I2@ INDI +1 NAME Mrs /Tester/ +2 GIVN Mrs +2 SURN Tester +1 SEX F +1 BIRT +2 DATE 15 Jun 1901 +1 DEAT +2 DATE 5 Jul 1975 +1 _UID C58628B95D0B41EEB3305DFE5F824E16463C +1 CHAN +2 DATE 3 Aug 2026 +3 TIME 18:39 +1 FAMS @F1@ +0 @I3@ INDI +1 NAME Ed /Tester/ +2 GIVN Ed +2 SURN Tester +1 NAME Eddie /Tester/ +2 GIVN Eddie +2 SURN Tester +1 SEX M +1 BIRT +2 DATE 15 Jun 1922 +1 DEAT +2 DATE 5 Jul 1994 +1 _PPEXCLUDE +1 EVEN +2 TYPE Association +2 NOTE Association link note +1 _UID DA9DFB151BEE490C983CBB0B6D9DCD4096C4 +1 BAPL +2 DATE 5 May 0005 B.C. +2 TEMP SLAKE - Salt Lake Temple +1 CHAN +2 DATE 3 Aug 2026 +3 TIME 18:38 +1 FAMC @F1@ +1 NOTE @NI3@ +1 NOTE @HI3@ +0 @NI3@ NOTE +1 CONC Name note +1 CONT +1 CONT >LDS Baptism note: LDS xref note +0 @HI3@ NOTE +1 CONC (Research):from yearNOTE: TEST Empty Note +1 CONT BAPL: SOUR @SOURCE1@ +1 CONT NOTE: TEST Accociation note +0 @I4@ INDI +1 NAME George /Testee/ +2 GIVN George +2 SURN Testee +1 SEX U +1 _PPEXCLUDE +1 _UID 77DA2714AB9D464DBAE1E44EB7A328974D11 +1 CHAN +2 DATE 3 Aug 2026 +3 TIME 18:38 +1 NOTE @NI4@ +0 @NI4@ NOTE +1 CONC Just for association +0 @F1@ FAM +1 HUSB @I1@ +2 _PREF Y +1 WIFE @I2@ +2 _PREF Y +1 MARR +2 SOUR @S2@ +2 SOUR @S2@ +3 PAGE 42 +3 QUAY 0 +3 DATE 31 Dec 1900 +2 NOTE @NF1@ +1 CHIL @I3@ +2 _PREF Y +1 CHAN +2 DATE 3 Aug 2026 +3 TIME 18:38 +0 @NF1@ NOTE +1 CONC Family xref note +1 CONT Family note +1 CONT +1 CONT from year +1 CONT TEST: TEST family +0 _EVENT_DEFN Who knows OBJE REFN TYPE +1 TYPE I +1 TITL Who knows OBJE REFN TYPE +1 _SEN1 [EventName]: [Desc], [onDate], [inPlace]. +1 _SEN2 [EventName]: [Date]. +1 _SEN3 [EventName]: [inPlace]. +1 _SEN4 [EventName]: [Desc]. +1 _SEN5 [EventName]: [Desc], [onDate]. +1 _SEN6 [EventName]: [Desc], [inPlace]. +1 _SEN7 [EventName]: [Date], [inPlace]. +1 _SEN8 [EventName]. +1 _DATE_TYPE 1 +1 _PLACE_TYPE 1 +1 _DESC_FLAG 1 +1 _INC_NOTES +0 _EVENT_DEFN Association +1 TYPE I +1 TITL Association +1 _SEN1 [EventName]: [Desc], [onDate], [inPlace]. +1 _SEN2 [EventName]: [Date]. +1 _SEN3 [EventName]: [inPlace]. +1 _SEN4 [EventName]: [Desc]. +1 _SEN5 [EventName]: [Desc], [onDate]. +1 _SEN6 [EventName]: [Desc], [inPlace]. +1 _SEN7 [EventName]: [Date], [inPlace]. +1 _SEN8 [EventName]. +1 _DATE_TYPE 1 +1 _PLACE_TYPE 1 +1 _DESC_FLAG 1 +1 _INC_NOTES +0 @S2@ SOUR +1 ABBR Note Test file Source: Tester +1 TITL Note Test file Source: Tester +1 NOTE note embedded in the SOURCE Record +1 REPO @R1@ +1 _PAREN Y +0 @R1@ REPO +1 NAME The Testers repository +1 ADDR The Testers repository +2 _NAME The Testers repository +2 ADR1 The Testers repository +2 NOTE Repository Note +0 TRLR diff --git a/imp_notetest_dfs_lds.gramps b/imp_notetest_dfs_lds.gramps new file mode 100644 index 00000000000..958a567f5a2 Binary files /dev/null and b/imp_notetest_dfs_lds.gramps differ