File indexing completed on 2024-12-29 04:51:06

0001 /*
0002    SPDX-FileCopyrightText: 2017-2022 Volker Krause <vkrause@kde.org>
0003    SPDX-License-Identifier: LGPL-2.0-or-later
0004 */
0005 
0006 // see https://community.kde.org/KDE_PIM/KItinerary/SNCF_Barcodes#Tariff_Codes
0007 const tariffs = {
0008     'CF00': 'Ayant Droit Avec Fichet',
0009     'CF90': 'Ayant Droit Sans Fichet',
0010     'CJ11': 'Carte Jeune',
0011     'CW00': 'Carte Advantage Adulte',
0012     'CW11': 'Carte Advantage Adulte',
0013     'CW12': 'Carte Advantage Adulte',
0014     'CW25': 'Carte Advantage Adulte',
0015     'EF11': 'Carte Enfant+',
0016     'EF99': 'Carte Enfant+',
0017     'IR00': 'Interrail',
0018     'IR01': 'Interrail',
0019     'JE00': 'Carte Jeune',
0020     'SE11': 'Carte Advantage Senior',
0021     'SR50': 'Carte Senior'
0022 };
0023 
0024 function parseSncfPdfText(text) {
0025     var reservations = new Array();
0026     const bookingRef = text.match(/(?:DOSSIER VOYAGE|BOOKING FILE REFERENCE|REFERENCE NUMBER|REISEREFERENZ) ?: +([A-Z0-9]{6})/);
0027     const price = text.match(/(\d+,\d\d EUR)/);
0028 
0029     var pos = 0;
0030     while (true) {
0031         var header = text.substr(pos).match(/ +(?:Départ \/ Arrivée|Departure \/ Arrival|Abfahrt \/ Ankunft).*\n/);
0032         if (!header)
0033             break;
0034         var index = header.index + header[0].length;
0035 
0036         var res = JsonLd.newTrainReservation();
0037         res.reservationNumber = bookingRef[1];
0038 
0039         var depLine = text.substr(pos + index).match(/\n {2,3}([\w -]+?)  +(\d{2}[\/\.]\d{2}) (?:à|at|um) (\d{2}[h:]\d{2})/);
0040         if (!depLine)
0041             break;
0042         index += depLine.index + depLine[0].length;
0043         res.reservationFor.departureStation.name = depLine[1];
0044         res.reservationFor.departureTime = JsonLd.toDateTime(depLine[2] + " " + depLine[3], ["dd/MM hh'h'mm", "dd/MM hh:mm", "dd.MM hh:mm"], "fr");
0045 
0046         var arrLine = text.substr(pos + index).match(/\n {2,3}([\w -]+?)  +(\d{2}[\/\.]\d{2}) (?:à|at|um) (\d{2}[h:]\d{2})/);
0047         if (!arrLine)
0048             break;
0049         index += arrLine.index + arrLine[0].length;
0050         res.reservationFor.arrivalStation.name = arrLine[1];
0051         res.reservationFor.arrivalTime = JsonLd.toDateTime(arrLine[2] + " " + arrLine[3], ["dd/MM hh'h'mm", "dd/MM hh:mm", "dd.MM hh:mm"], "fr");
0052 
0053         // parse seat, train number, etc from the text for one leg
0054         // since the stations are vertically centered, the stuff we are looking for might be at different
0055         // positions relative to them
0056         var legText = text.substring(pos + header.index + header[0].length, pos + index);
0057         var trainNumber = legText.match(/(?:TRAIN N°|TRAIN NUMBER|ZUGNUMMER) ?(\d{3,5})/);
0058         if (trainNumber)
0059             res.reservationFor.trainNumber = trainNumber[1];
0060         var seatRes = legText.match(/(?:VOITURE|COACH|WAGEN) (\d+) - (?:PLACE|SEAT|PLATZ) (\d+)/);
0061         if (seatRes) {
0062             res.reservedTicket.ticketedSeat.seatSection = seatRes[1];
0063             res.reservedTicket.ticketedSeat.seatNumber = seatRes[2];
0064         }
0065 
0066         if (price)
0067             ExtractorEngine.extractPrice(price[1], res);
0068 
0069         reservations.push(res);
0070         if (index == 0)
0071             break;
0072         pos += index;
0073     }
0074 
0075     return reservations;
0076 }
0077 
0078 function parseInouiPdfText(page)
0079 {
0080     var reservations = new Array();
0081     const price = page.text.match(/(\d+,\d\d EUR)/);
0082     var text = page.textInRect(0.0, 0.0, 0.5, 1.0);
0083 
0084     var date = text.match(/(\d+\.? [^ ]+ \d{4})\n/)
0085     if (!date)
0086         return reservations;
0087     var pos = date.index + date[0].length;
0088     while (true) {
0089         var dep = text.substr(pos).match(/(\d{2}[h:]\d{2}) +(.*)\n/);
0090         if (!dep)
0091             break;
0092         pos += dep.index + dep[0].length;
0093 
0094         var res = JsonLd.newTrainReservation();
0095         res.reservationFor.departureTime = JsonLd.toDateTime(date[1] + dep[1], ["d MMMM yyyyhh'h'mm", "dd MMMM yyyyhh:mm", "dd. MMMM yyyyhh:mm"], ["fr", "en", "de"]);
0096         res.reservationFor.departureStation.name = dep[2];
0097 
0098         var arr = text.substr(pos).match(/(\d{2}[h:]\d{2}) +(.*)\n/);
0099         if (!arr)
0100             break;
0101         var endPos = arr.index + arr[0].length;
0102         res.reservationFor.arrivalTime = JsonLd.toDateTime(date[1] + arr[1], ["d MMMM yyyyhh'h'mm", "dd MMMM yyyyhh:mm", "dd. MMMM yyyyhh:mm"], ["fr", "en", "de"]);
0103         res.reservationFor.arrivalStation.name = arr[2];
0104 
0105         var detailsText = text.substr(pos, endPos - arr[0].length);
0106         var train = detailsText.match(/^ *(.*?) *-/);
0107         res.reservationFor.trainNumber = train[1];
0108         var seat = detailsText.match(/(?:Voiture|Coach|Wagen) *(\d+) *(?:Place|Seat|Platz) *(\d+)/);
0109         if (seat) {
0110             res.reservedTicket.ticketedSeat.seatSection = seat[1];
0111             res.reservedTicket.ticketedSeat.seatNumber = seat[2];
0112         }
0113 
0114         if (price)
0115             ExtractorEngine.extractPrice(price[1], res);
0116 
0117         reservations.push(res);
0118         if (endPos == 0)
0119             break;
0120         pos += endPos;
0121     }
0122 
0123     return reservations;
0124 }
0125 
0126 // see https://community.kde.org/KDE_PIM/KItinerary/SNCF_Barcodes
0127 function parseSncfBarcode(barcode)
0128 {
0129     var reservations = new Array();
0130 
0131     var res1 = JsonLd.newTrainReservation();
0132     res1.reservationNumber = barcode.substr(4, 6);
0133     res1.underName.familyName = barcode.substr(72, 19);
0134     res1.underName.givenName = barcode.substr(91, 19);
0135     res1.reservationFor.departureStation.name = barcode.substr(33, 5);
0136     res1.reservationFor.departureStation.identifier = "sncf:" + barcode.substr(33, 5);
0137     res1.reservationFor.arrivalStation.name = barcode.substr(38, 5);
0138     res1.reservationFor.arrivalStation.identifier = "sncf:" + barcode.substr(38, 5);
0139     res1.reservationFor.departureDay = JsonLd.toDateTime(barcode.substr(48, 5), "dd/MM", "fr");
0140     res1.reservationFor.trainNumber = barcode.substr(43, 5);
0141     res1.reservedTicket.ticketToken = "aztecCode:" + barcode;
0142     res1.reservedTicket.ticketNumber = barcode.substr(10, 9);
0143     res1.reservedTicket.ticketedSeat.seatingType = barcode.substr(110, 1);
0144     res1.programMembershipUsed.programName = tariffs[barcode.substr(111, 4)];
0145     reservations.push(res1);
0146 
0147     if (barcode.substr(115, 1) != '0') {
0148         var res2 = JsonLd.clone(res1);
0149         res2.reservationFor.departureStation.name = barcode.substr(116, 5);
0150         res2.reservationFor.departureStation.identifier = "sncf:" + barcode.substr(116, 5);
0151         res2.reservationFor.arrivalStation.name = barcode.substr(121, 5);
0152         res2.reservationFor.arrivalStation.identifier = "sncf:" + barcode.substr(121, 5);
0153         res2.reservationFor.trainNumber = barcode.substr(126, 5);
0154         res2.reservedTicket.ticketedSeat.seatingType = barcode.substr(115, 1);
0155         reservations.push(res2);
0156     }
0157 
0158     return reservations;
0159 }
0160 
0161 function parsePdf(pdf) {
0162     var reservations = new Array();
0163 
0164     var barcode = null;
0165     for (var i = 0; i < pdf.pageCount; ++i) {
0166         var page = pdf.pages[i];
0167         var nextBarcode = null;
0168         var images = page.images;
0169         for (var j = 0; j < images.length && !nextBarcode; ++j) {
0170             nextBarcode = Barcode.decodeAztec(images[j]);
0171             if (nextBarcode.substr(0, 4) !== "i0CV")
0172                 nextBarcode = null;
0173         }
0174         // Guard against tickets with 3 or more legs, with the second page for the 3rd and subsequent
0175         // leg repeating the barcode of the first two legs. One would expect the barcode for the following
0176         // legs there, but that doesn't even seem to exists in the sample documents I have for this...
0177         barcode = (nextBarcode && nextBarcode != barcode) ? nextBarcode : null;
0178         if (barcode) {
0179             var barcodeRes = barcode ? parseSncfBarcode(barcode) : null;
0180         }
0181 
0182         var legs = parseSncfPdfText(page.text);
0183         if (legs.length == 0) {
0184             legs = parseInouiPdfText(page);
0185         }
0186         if (legs.length > 0) {
0187             for (var j = 0; j < legs.length; ++j) {
0188                 if (barcode && j < barcodeRes.length) {
0189                     legs[j] = JsonLd.apply(barcodeRes[j], legs[j]);
0190                 }
0191                 reservations.push(legs[j]);
0192             }
0193         } else {
0194             reservations = reservations.concat(barcodeRes);
0195         }
0196     }
0197 
0198     return reservations;
0199 }
0200 
0201 function parseSecutixPdfItineraryV1(text, res)
0202 {
0203     var reservations = new Array();
0204     var pos = 0;
0205     while (true) {
0206         var dep = text.substr(pos).match(/Départ [^ ]+ (\d+\.\d+\.\d+) à (\d+:\d+) [^ ]+ (.*)\n/);
0207         if (!dep)
0208             break;
0209         pos += dep.index + dep[0].length;
0210         var arr = text.substr(pos).match(/Arrivée [^ ]+ (\d+\.\d+\.\d+) à (\d+:\d+) [^ ]+ (.*)\n\s+(.*)\n/);
0211         if (!arr)
0212             break;
0213         pos += arr.index + arr[0].length;
0214 
0215         var leg = JsonLd.newTrainReservation();
0216         leg.reservationFor.departureStation.name = dep[3];
0217         leg.reservationFor.departureTime = JsonLd.toDateTime(dep[1] + dep[2], "dd.MM.yyyyhh:mm", "fr");
0218         leg.reservationFor.arrivalStation.name = arr[3];
0219         leg.reservationFor.arrivalTime = JsonLd.toDateTime(arr[1] + arr[2], "dd.MM.yyyyhh:mm", "fr");
0220         leg.reservationFor.trainNumber = arr[4];
0221         leg.underName = res.underName;
0222         leg.reservationNumber = res.reservationNumber;
0223         leg.reservedTicket = res.reservedTicket;
0224         leg.programMembershipUsed = res.programMembershipUsed;
0225 
0226         reservations.push(leg);
0227     }
0228     return reservations;
0229 }
0230 
0231 function parseSecutixPdfItineraryV2(text, res)
0232 {
0233     var reservations = new Array();
0234     var pos = 0;
0235     while (true) {
0236         var data = text.substr(pos).match(/ *(\d+h\d+)\n *(.*)\n *(.*)\n(?: *Voiture (\d+) - Place (\d+)\n.*\n.*\n)? *(\d+h\d+)\n(.*)\n/);
0237         if (!data)
0238             break;
0239         pos += data.index + data[0].length;
0240 
0241         var leg = JsonLd.newTrainReservation();
0242         leg.reservationFor.departureStation.name = data[2];
0243         leg.reservationFor.departureDay = res.reservationFor.departureDay;
0244         leg.reservationFor.departureTime = JsonLd.toDateTime(data[1], "hh'h'mm", "fr");
0245         leg.reservationFor.arrivalStation.name = data[7];
0246         leg.reservationFor.arrivalTime = JsonLd.toDateTime(data[6], "hh'h'mm", "fr");
0247         leg.reservationFor.trainNumber = data[3];
0248         leg.underName = res.underName;
0249         leg.reservationNumber = res.reservationNumber;
0250         leg.reservedTicket = res.reservedTicket;
0251         leg.reservedTicket.ticketedSeat.seatSection = data[4];
0252         leg.reservedTicket.ticketedSeat.seatNumber = data[5];
0253         leg.programMembershipUsed = res.programMembershipUsed;
0254 
0255         reservations.push(leg);
0256     }
0257     return reservations;
0258 }
0259 
0260 function parseSecutix(barcode)
0261 {
0262     // see https://community.kde.org/KDE_PIM/KItinerary/SNCF_Barcodes#SNCF_Secutix_Tickets
0263     let res = JsonLd.newTrainReservation();
0264     const code = ByteArray.decodeLatin1(barcode.slice(260));
0265     res.reservationFor.provider.identifier = 'uic:' + code.substr(4, 4);
0266     res.reservationNumber = code.substr(8, 9);
0267     res.reservationFor.departureStation.name = code.substr(17, 5);
0268     res.reservationFor.departureStation.identifier = "sncf:" + code.substr(17, 5);
0269     res.reservationFor.arrivalStation.name = code.substr(22, 5);
0270     res.reservationFor.arrivalStation.identifier = "sncf:" + code.substr(22, 5);
0271     res.reservationFor.departureDay = JsonLd.toDateTime(code.substr(83, 8), "ddMMyyyy", "fr");
0272     res.reservedTicket.ticketedSeat.seatingType = code.substr(91, 1);
0273     res.reservedTicket.ticketNumber = code.substr(8, 9);
0274     res.reservedTicket.ticketToken = "aztecbin:" + ByteArray.toBase64(barcode);
0275     res.underName.familyName = code.substr(116, 19);
0276     res.underName.givenName = code.substr(135, 19);
0277     res.programMembershipUsed.programName = tariffs[code.substr(92, 4)];
0278     res.reservedTicket.totalPrice = code.substr(226, 10) / 100;
0279     res.reservedTicket.priceCurrency = 'EUR';
0280     return res;
0281 }
0282 
0283 function parseSecutixPdf(pdf, node, triggerNode)
0284 {
0285     let res = triggerNode.result[0];
0286     const text = pdf.pages[triggerNode.location].text;
0287     let pnr = text.match(res.reservationNumber + '[^\n]* ([A-Z0-9]{6})\n');
0288     let layoutVersion = 1;
0289     if (!pnr) {
0290         pnr = text.match(/(?:PAO|REF)\s*:\s*([A-Z0-9]{6,8})\n/);
0291         layoutVersion = 2;
0292     }
0293     res.reservationNumber = pnr[1];
0294 
0295     var itineraryText = pdf.pages[triggerNode.location].textInRect(0.0, 0.0, 0.5, 1.0);
0296     var reservations = layoutVersion == 1 ? parseSecutixPdfItineraryV1(itineraryText, res) : parseSecutixPdfItineraryV2(itineraryText, res);
0297     if (reservations.length == 0)
0298         return res;
0299 
0300     reservations[0].reservationFor.departureStation.identifier = res.reservationFor.departureStation.identifier;
0301     reservations[reservations.length - 1].reservationFor.arrivalStation.identifier = res.reservationFor.arrivalStation.identifier;
0302     for (r of reservations) {
0303         r.reservationFor.provider = res.reservationFor.provider;
0304     }
0305 
0306     return reservations;
0307 }
0308 
0309 function parseOuiEmail(html)
0310 {
0311     if (html.eval('//*[@data-select="travel-summary-reference"]').length > 0) {
0312         return parseOuiSummary(html);
0313     } else {
0314         return parseOuiConfirmation(html);
0315     }
0316 }
0317 
0318 function parseOuiSummaryTime(htmlElem)
0319 {
0320     var timeStr = htmlElem[0].recursiveContent;
0321     var time = timeStr.match(/(\d+ [^ ]+ \d+) +[^ ]+ (\d+:\d+)/);
0322     if (time) {
0323         return JsonLd.toDateTime(time[1] + time[2], "d MMMM yyyyhh:mm", "fr");
0324     }
0325     time = timeStr.match(/(\d+\.? [^ ]+(?: \d{4})?) +[^ ]+ +(\d+[:h]\d+)/);
0326     return JsonLd.toDateTime(time[1] + ' ' + time[2].replace('h', ':'), ["d MMMM hh:mm", "d. MMMM yyyy hh:mm"], ["fr", "en", "de"]);
0327 }
0328 
0329 function parseOuiSummary(html)
0330 {
0331     // TODO extract passenger names
0332     var res = JsonLd.newTrainReservation();
0333     const origins = html.eval('//*[@data-select="travel-summary-origin"]');
0334     res.reservationFor.departureStation.name = origins[0].content;
0335     const destinations = html.eval('//*[@data-select="travel-summary-destination"]');
0336     res.reservationFor.arrivalStation.name = destinations[0].content;
0337     res.reservationNumber = html.eval('//*[@data-select="travel-summary-reference"]')[0].content;
0338 
0339     res.reservationFor.departureTime = parseOuiSummaryTime(html.eval('//*[@data-select="travel-departureDate"]'));
0340 
0341     var trainNum = html.eval('//*[@data-select="passenger-detail-outwardFares"]//*[@class="passenger-detail__equipment"]');
0342     if (trainNum.length == 2 || trainNum[1].content == trainNum[3].content) {
0343         // can occur multiple times for multi-leg journeys or multiple passengers
0344         // we don't have information about connections on multi-leg journeys, so omit the train number in that case
0345         res.reservationFor.trainNumber = trainNum[0].content + " " + trainNum[1].content;
0346     }
0347 
0348     const price = html.eval('//*[@class="transaction__total-amount-value"]');
0349     if (price)
0350         ExtractorEngine.extractPrice(price[0].recursiveContent, res);
0351 
0352     // check if this is a return ticket
0353     var retourTime = html.eval('//*[@data-select="travel-returnDate"]');
0354     if (retourTime.length == 0) {
0355         return res;
0356     }
0357     var retour = JsonLd.newTrainReservation();
0358     retour.reservationFor.departureStation.name = origins[1] ? origins[1].content : res.reservationFor.arrivalStation.name;
0359     retour.reservationFor.arrivalStation.name = destinations[1] ? destinations[1].content : res.reservationFor.departureStation.name;
0360     retour.reservationFor.departureTime = parseOuiSummaryTime(retourTime);
0361     trainNum = html.eval('//*[@data-select="passenger-detail-inwardFares"]//*[@class="passenger-detail__equipment"]');
0362     if (trainNum.length == 2 || trainNum[1].content == trainNum[3].content) {
0363         retour.reservationFor.trainNumber = trainNum[0].content + " " + trainNum[1].content;
0364     }
0365     if (price)
0366         ExtractorEngine.extractPrice(price[0].recursiveContent, retour);
0367 
0368     return [res, retour];
0369 }
0370 
0371 function parseOuiConfirmation(html)
0372 {
0373     var reservations = new Array();
0374 
0375     var pnr = html.eval('//*[@class="pnr-ref"]/*[@class="pnr-info"]');
0376     var pnrOuigo = html.eval('//*[@class="pnr-info-digital pnr-info-digital-ouigo"]');
0377     var passengerName = html.eval('//*[@class="passenger"]/*[@class="name"]');
0378 
0379     var productDts = html.eval('//*[@class="product-travel-date"]');
0380     var productDetails = html.eval('//table[@class="product-details"]');
0381     var passengerDetails = html.eval('//table[@class="passengers"]');
0382     for (productDetailIdx in productDetails) {
0383         // date is in the table before us
0384         var dt = productDts[productDetailIdx].content.replace(/\S+ (.*)/, "$1");
0385 
0386         var segmentDetail = productDetails[productDetailIdx].eval(".//td")[0];
0387         var placement = passengerDetails[productDetailIdx].eval('.//td[@class="placement "]'); // yes, there is a space behind placement there...
0388         var seat = placement[0].content.match(/Voiture (.*?) - Place (.*?) /);
0389         var res = null;
0390         while (segmentDetail && !segmentDetail.isNull) {
0391             var cls = segmentDetail.attribute("class");
0392             if (cls.includes("segment-departure")) {
0393                 res = JsonLd.newTrainReservation();
0394                 res.reservationFor.departureTime = JsonLd.toDateTime(dt + segmentDetail.content, "d MMMMhh'h'mm", "fr");
0395                 segmentDetail = segmentDetail.nextSibling;
0396                 res.reservationFor.departureStation.name = segmentDetail.content;
0397                 if (seat) {
0398                     res.reservedTicket.ticketedSeat.seatSection = seat[1];
0399                     res.reservedTicket.ticketedSeat.seatNumber = seat[2];
0400                 }
0401             }
0402             else if (cls.includes("segment-arrival")) {
0403                 res.reservationFor.arrivalTime = JsonLd.toDateTime(dt + segmentDetail.content, "d MMMMhh'h'mm", "fr");
0404                 segmentDetail = segmentDetail.nextSibling;
0405                 res.reservationFor.arrivalStation.name = segmentDetail.content;
0406 
0407                 if (res.reservationFor.trainName == "OUIGO" && pnrOuigo.length) {
0408                     res.reservationNumber = pnrOuigo[0].content;
0409                 } else if (pnr.length) {
0410                     res.reservationNumber = pnr[0].content;
0411                 }
0412                 if (passengerName.length) {
0413                     res.underName.name = passengerName[0].content;
0414                 }
0415 
0416                 // HACK drop invalid elements so the structured fallback kicks in correctly
0417                 // this should be done automatically in the engine
0418                 if (res.reservationFor.departureTime > 0)
0419                     reservations.push(res);
0420             }
0421             else if (cls === "segment") {
0422                 res.reservationFor.trainName = segmentDetail.content;
0423             }
0424             else if (cls === "segment-ref-train") {
0425                 res.reservationFor.trainNumber = segmentDetail.content;
0426             }
0427 
0428             segmentDetail = segmentDetail.nextSibling.isNull ? segmentDetail.parent.nextSibling.firstChild : segmentDetail.nextSibling;
0429         }
0430     }
0431 
0432     return reservations;
0433 }
0434 
0435 function parseOuigoTicket(pdf, node) {
0436     var text = pdf.pages[0].textInRect(0, 0, 0.5, 1);
0437 
0438     var res = JsonLd.newTrainReservation();
0439     res.reservationNumber = text.match(/numéro de réservation est\s*:\s*([\w]{6})\n/)[1];
0440     var trip = text.match(/(\d{2} .+ \d{4})\n\s*(\d{2}h\d{2})\s*(.*?)\n\s*(\d{2}h\d{2})\s*(.*?)\n/);
0441     res.reservationFor.departureStation.name = trip[3];
0442     res.reservationFor.departureTime = JsonLd.toDateTime(trip[1] + trip[2], "dd MMMM yyyyhh'h'mm", "fr");
0443     res.reservationFor.arrivalStation.name = trip[5];
0444     res.reservationFor.arrivalTime = JsonLd.toDateTime(trip[1] + trip[4], "dd MMMM yyyyhh'h'mm", "fr");
0445 
0446     res.reservationFor.trainNumber = text.match(/N°\s*(\S+)/)[1];
0447 
0448     var seat = text.match(/Voiture\s*(\S+)\s*Place\s*(\S+)/);
0449     res.reservedTicket.ticketedSeat.seatSection = seat[1];
0450     res.reservedTicket.ticketedSeat.seatNumber = seat[2];
0451 
0452     var barcodes = node.findChildNodes({ scope: "Descendants", mimeType: "text/plain", match: ".*" });
0453     for (barcode of barcodes) {
0454         if (barcode.location != undefined) {
0455             res.reservedTicket.ticketToken = "azteccode:" + barcodes[0].content;
0456             break;
0457         }
0458     }
0459 
0460     const price = text.match(/ (\d+\.\d\d)€/);
0461     if (price) {
0462         res.totalPrice = price[1];
0463         res.priceCurrency = 'EUR';
0464     }
0465     return res;
0466 }
0467 
0468 function parseTerConfirmation(html) {
0469     var reservations = new Array();
0470     const refNum = html.eval('//td[@id="referenceContainer"]')[0].content;
0471     const name = html.eval('//td[@id="nomReferenceContainer"]')[0].content;
0472     const journeys = html.eval('//table[@id ="emailTrajet" or @id="emailTrajetRetour"]');
0473     for (const journey of journeys) {
0474         var res = JsonLd.newTrainReservation();
0475         const dt = journey.eval('.//h2')[0].content.match(/ (\d.*)$/)[1];
0476         res.reservationFor.departureDay = JsonLd.toDateTime(dt, "dd MMMM yyyy", "fr");
0477         const ps = journey.eval('.//p');
0478         res.reservationFor.departureStation.name = ps[0].content;
0479         res.reservationFor.departureTime = JsonLd.toDateTime(ps[1].content.match(/ (\d.*)/)[1], "hh'h'mm", "fr");
0480         res.reservationFor.arrivalStation.name = ps[2].content;
0481         res.reservationFor.arrivalTime = JsonLd.toDateTime(ps[3].content.match(/ (\d.*)/)[1], "hh'h'mm", "fr");
0482         res.reservationNumber = refNum;
0483         res.underName.name = name;
0484         reservations.push(res);
0485     }
0486     return reservations;
0487 }
0488 
0489 function parseOuigoConfirmation(html) {
0490     var reservations = new Array();
0491     const refNum = html.eval('//strong')[1].content;
0492     const tabs = html.eval('//table//table//table[@class = "rsz_320"]');
0493     for (const tab of tabs) {
0494         const text = tab.recursiveContent;
0495         if (!text.match(/TRAJET/)) {
0496             continue;
0497         }
0498 
0499         var idx = 0;
0500         while (true) {
0501             const date = text.substr(idx).match(/\w+ (\d{1,2} \w+ \d{4})/);
0502             if (!date) {
0503                 break;
0504             }
0505             const leg = text.substr(idx).match(/(\d{2}h\d{2})\s+(.*?)\n\s*(\d{2}h\d{2})\s*(.*?)\n\s*TRAIN N° *(.*)\n/);
0506             var res = JsonLd.newTrainReservation();
0507             res.reservationNumber = refNum;
0508             res.reservationFor.departureTime = JsonLd.toDateTime(date[1] + leg[1], "d MMMM yyyyhh'h'mm", "fr");
0509             res.reservationFor.departureStation.name = leg[2];
0510             res.reservationFor.arrivalStation.name = leg[4];
0511             res.reservationFor.arrivalTime = JsonLd.toDateTime(date[1] + leg[3],  "d MMMM yyyyhh'h'mm", "fr");
0512             res.reservationFor.trainNumber = leg[5];
0513             reservations.push(res);
0514 
0515             idx += leg[0].length + leg.index;
0516         }
0517     }
0518     return reservations;
0519 }
0520 
0521 // see https://community.kde.org/KDE_PIM/KItinerary/SNCF_Barcodes#Carte_Advantage
0522 function parseSncfCarte(code) {
0523     var carte = JsonLd.newObject("ProgramMembership");
0524     carte.programName = tariffs[code.substr(111, 4)];
0525     carte.membershipNumber = code.substr(53, 17);
0526     carte.token = 'aztec:' + code;
0527     return carte.programName != undefined ? carte : undefined;
0528 }
0529 
0530 function parseSncfCartePdf(pdf, node, barcode) {
0531     const text = pdf.pages[barcode.location].text;
0532     var carte = node.result[0];
0533     carte.member = JsonLd.newObject("Person");
0534     carte.member.familyName = text.match(/(?:Nom|Name)\s*:\s*(.*)/)[1];
0535     carte.member.givenName = text.match(/(?:Prénom|Vorname)\s*:\s*(.*)/)[1];
0536     const validity = text.match(/(?:Du|Vom)\s+(\d{2}[\/\.]\d{2}[\/\.]\d{4})\s+(?:au|bis zum)\s+(\d{2}[\/\.]\d{2}[\/\.]\d{4})/);
0537     carte.validFrom = JsonLd.toDateTime(validity[1], ['dd/MM/yyyy', 'dd.MM.yyyy'], 'fr');
0538     carte.validUntil = JsonLd.toDateTime(validity[2] + ' 23:59:59', ['dd/MM/yyyy hh:mm:ss', 'dd.MM.yyyy hh:mm:ss'], 'fr');
0539     return carte;
0540 }
0541 
0542 // see https://community.kde.org/KDE_PIM/KItinerary/SNCF_Barcodes#SNCF_Normandie_Tickets
0543 // PDF layout matches that of the "secutix" v2
0544 function parseSncfNormandie(pdf, node, triggerNode) {
0545     let res = JsonLd.newTrainReservation();
0546     res.reservedTicket.ticketToken = "aztecbin:" + ByteArray.toBase64(triggerNode.content);
0547 
0548     const page = pdf.pages[triggerNode.location];
0549     const textRight = page.textInRect(0.5, 0.0, 1.0, 1.0);
0550     const pnr = textRight.match(/(.*)\n(.*)\n\d{2}\/\d{2}\/\d{4} +(?:PAO|REF)\s*:\s*([A-Z0-9]{6,8})\n/);
0551     res.reservationNumber = pnr[3];
0552     res.underName.givenName = pnr[2];
0553     res.underName.familyName = pnr[1];
0554     res.reservedTicket.ticketedSeat.seatingType = textRight.match(/Classe (.*)\n/)[1];
0555 
0556     const textLeft = pdf.pages[triggerNode.location].textInRect(0.0, 0.0, 0.5, 1.0);
0557     const date = textLeft.match(/(\d{1,2} \S+ \d{4})/)[1];
0558     res.reservationFor.departureDay = JsonLd.toDateTime(date, 'd MMMM yyyy', 'fr');
0559     let reservations = parseSecutixPdfItineraryV2(textLeft, res);
0560     return reservations;
0561 }
0562 
0563 function parseEvent(ev)
0564 {
0565     let res = JsonLd.newTrainReservation();
0566     const names = ev.description.match(/ +(.*) -> (.*)\n/);
0567     res.reservationFor.departureStation.name = names[1];
0568     res.reservationFor.departureTime = JsonLd.readQDateTime(ev, 'dtStart');
0569     res.reservationFor.arrivalStation.name = names[2];
0570     res.reservationFor.arrivalTime = JsonLd.readQDateTime(ev, 'dtEnd');
0571     res.reservationFor.trainNumber = ev.description.match(/ +([A-Z ]+ \d+)\n/i)[1];
0572     const seat = ev.description.match(/ +(?:VOITURE|COACH|WAGEN) (\d+) - (?:PLACE|SEAT|PLATZ) (\d+)/i);
0573     if (seat) {
0574         res.reservedTicket.ticketedSeat.seatSection = seat[1];
0575         res.reservedTicket.ticketedSeat.seatNumber = seat[2];
0576     }
0577     res.reservationNumber = ev.uid.substr(0, 6);
0578     res.url = ev.url;
0579     return res;
0580 }