diff --git a/autotests/calendarhandlertest.cpp b/autotests/calendarhandlertest.cpp index fe795b1..b26c827 100644 --- a/autotests/calendarhandlertest.cpp +++ b/autotests/calendarhandlertest.cpp @@ -1,153 +1,153 @@ /* Copyright (c) 2017 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "calendarhandler.h" #include "extractorpostprocessor.h" #include "jsonlddocument.h" #include #include #include #include #include #include #include #include using namespace KCalCore; using namespace KItinerary; void initLocale() { qputenv("LC_ALL", "en_US.utf-8"); qputenv("TZ", "UTC"); } Q_CONSTRUCTOR_FUNCTION(initLocale) class CalendarHandlerTest : public QObject { Q_OBJECT private Q_SLOTS: void testCreateEvent_data() { QTest::addColumn("jsonFile"); QTest::addColumn("icalFile"); QDir dir(QStringLiteral(SOURCE_DIR "/calendarhandlerdata")); const auto lst = dir.entryList(QStringList(QStringLiteral("*.json")), QDir::Files | QDir::Readable | QDir::NoSymLinks); for (const auto &file : lst) { const QString refFile = dir.path() + QLatin1Char('/') + file.left(file.size() - 4) + QStringLiteral("ics"); if (!QFile::exists(refFile)) { qDebug() << "reference file" << refFile << "does not exist, skipping test file" << file; continue; } QTest::newRow(file.toLatin1().constData()) << QString(dir.path() + QLatin1Char('/') + file) << refFile; } } void testCreateEvent() { QFETCH(QString, jsonFile); QFETCH(QString, icalFile); QFile f(jsonFile); QVERIFY(f.open(QFile::ReadOnly)); const auto inArray = QJsonDocument::fromJson(f.readAll()).array(); QVERIFY(!inArray.isEmpty()); const auto preData = JsonLdDocument::fromJson(inArray); QCOMPARE(inArray.size(), preData.size()); ExtractorPostprocessor postproc; postproc.process(preData); QCOMPARE(inArray.size(), postproc.result().size()); MemoryCalendar::Ptr refCal(new MemoryCalendar(QTimeZone{})); ICalFormat format; format.load(refCal, icalFile); const auto refEvents = refCal->rawEvents(KCalCore::EventSortStartDate, KCalCore::SortDirectionAscending); QCOMPARE(refEvents.size(), inArray.size()); for (int i = 0; i < inArray.size(); ++i) { Event::Ptr newEvent(new Event); CalendarHandler::fillEvent(postproc.result(), newEvent); // sync volatile fields, we only care for differences elsewhere - const auto refEvent = refEvents.at(i); + const auto &refEvent = refEvents.at(i); newEvent->setUid(refEvent->uid()); newEvent->setLastModified(refEvent->lastModified()); newEvent->setCreated(refEvent->created()); if (*newEvent != *refEvent) { qDebug().noquote() << "Actual: " << format.toICalString(newEvent); qDebug().noquote() << "Expected: " << format.toICalString(refEvent); } QCOMPARE(newEvent->dtStart(), refEvent->dtStart()); QCOMPARE(newEvent->dtEnd(), refEvent->dtEnd()); QVERIFY(*newEvent == *refEvent); } } void testFindEvent_data() { QTest::addColumn("jsonFile"); QTest::addColumn("icalFile"); QDir dir(QStringLiteral(SOURCE_DIR "/calendarhandlerdata")); const auto lst = dir.entryList(QStringList(QStringLiteral("*.json")), QDir::Files | QDir::Readable | QDir::NoSymLinks); for (const auto &file : lst) { const QString refFile = dir.path() + QLatin1Char('/') + file.left(file.size() - 4) + QStringLiteral("ics"); if (!QFile::exists(refFile)) { qDebug() << "reference file" << refFile << "does not exist, skipping test file" << file; continue; } QTest::newRow(file.toLatin1().constData()) << QString(dir.path() + QLatin1Char('/') + file) << refFile; } } void testFindEvent() { QFETCH(QString, jsonFile); QFETCH(QString, icalFile); QFile f(jsonFile); QVERIFY(f.open(QFile::ReadOnly)); const auto inArray = QJsonDocument::fromJson(f.readAll()).array(); QVERIFY(!inArray.isEmpty()); const auto preData = JsonLdDocument::fromJson(inArray); QCOMPARE(inArray.size(), preData.size()); ExtractorPostprocessor postproc; postproc.process(preData); QCOMPARE(inArray.size(), postproc.result().size()); MemoryCalendar::Ptr refCal(new MemoryCalendar(QTimeZone{})); ICalFormat format; format.load(refCal, icalFile); const auto event = CalendarHandler::findEvent(refCal, postproc.result().at(0)); QVERIFY(event); } }; QTEST_APPLESS_MAIN(CalendarHandlerTest) #include "calendarhandlertest.moc" diff --git a/src/calendarhandler.cpp b/src/calendarhandler.cpp index 26884dc..529b48d 100644 --- a/src/calendarhandler.cpp +++ b/src/calendarhandler.cpp @@ -1,448 +1,448 @@ /* Copyright (c) 2017 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "config-kitinerary.h" #include "calendarhandler.h" #include "jsonlddocument.h" #include "logging.h" #include "mergeutil.h" #include "sortutil.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_KCAL #include #include #include #endif #include #include #include #include #include using namespace KItinerary; static QString formatAddress(const PostalAddress &addr) { KContacts::Address a; a.setStreet(addr.streetAddress()); a.setPostalCode(addr.postalCode()); a.setLocality(addr.addressLocality()); a.setCountry(KContacts::Address::ISOtoCountry(addr.addressCountry())); return a.formattedAddress(); } static QString formatAddressSingleLine(const PostalAddress &addr) { return formatAddress(addr).replace(QLatin1String("\n\n"), QLatin1String("\n")).replace(QLatin1Char('\n'), QLatin1String(", ")); } #ifdef HAVE_KCAL using namespace KCalCore; static void fillFlightReservation(const QVector &reservations, const KCalCore::Event::Ptr &event); static void fillTrainReservation(const TrainReservation &reservation, const KCalCore::Event::Ptr &event); static void fillBusReservation(const BusReservation &reservation, const KCalCore::Event::Ptr &event); static void fillLodgingReservation(const LodgingReservation &reservation, const KCalCore::Event::Ptr &event); static void fillEventReservation(const QVector &reservations, const KCalCore::Event::Ptr &event); static void fillGeoPosition(const QVariant &place, const KCalCore::Event::Ptr &event); static void fillFoodReservation(const FoodEstablishmentReservation &reservation, const KCalCore::Event::Ptr &event); static void fillRentalCarReservation(const RentalCarReservation &reservation, const KCalCore::Event::Ptr &event); static void fillTaxiReservation(const TaxiReservation &reservation, const KCalCore::Event::Ptr &event); #endif QSharedPointer CalendarHandler::findEvent(const QSharedPointer &calendar, const QVariant &reservation) { #ifdef HAVE_KCAL if (!JsonLd::canConvert(reservation)) { return {}; } const auto dt = SortUtil::startDateTime(reservation).date(); const auto events = calendar->events(dt); for (const auto &event : events) { if (!event->uid().startsWith(QLatin1String("KIT-"))) { continue; } const auto otherRes = CalendarHandler::reservationsForEvent(event); for (const auto &other : otherRes) { if (MergeUtil::isSame(other, reservation)) { return event; } } } #else Q_UNUSED(calendar); Q_UNUSED(reservation); #endif return {}; } QVector CalendarHandler::reservationsForEvent(const QSharedPointer &event) { #ifdef HAVE_KCAL const auto payload = event->customProperty("KITINERARY", "RESERVATION").toUtf8(); const auto json = QJsonDocument::fromJson(payload).array(); return JsonLdDocument::fromJson(json); #else Q_UNUSED(event); return {}; #endif } void CalendarHandler::fillEvent(const QVector &reservations, const QSharedPointer &event) { if (reservations.isEmpty()) { return; } #ifdef HAVE_KCAL // TODO pass reservationS into all functions below for multi-traveler support - const auto reservation = reservations.at(0); + const auto &reservation = reservations.at(0); const int typeId = reservation.userType(); if (typeId == qMetaTypeId()) { fillFlightReservation(reservations, event); } else if (typeId == qMetaTypeId()) { fillLodgingReservation(reservation.value(), event); } else if (typeId == qMetaTypeId()) { fillTrainReservation(reservation.value(), event); } else if (typeId == qMetaTypeId()) { fillBusReservation(reservation.value(), event); } else if (JsonLd::isA(reservation)) { fillEventReservation(reservations, event); } else if (JsonLd::isA(reservation)) { fillFoodReservation(reservation.value(), event); } else if (JsonLd::isA(reservation)) { fillRentalCarReservation(reservation.value(), event); } else if (JsonLd::isA(reservation)) { fillTaxiReservation(reservation.value(), event); } else { return; } if (!event->uid().startsWith(QLatin1String("KIT-"))) { event->setUid(QLatin1String("KIT-") + event->uid()); } const auto payload = QJsonDocument(JsonLdDocument::toJson(reservations)).toJson(QJsonDocument::Compact); event->setCustomProperty("KITINERARY", "RESERVATION", QString::fromUtf8(payload)); #else Q_UNUSED(event); #endif } #ifdef HAVE_KCAL static QString airportDisplayCode(const Airport &airport) { return airport.iataCode().isEmpty() ? airport.name() : airport.iataCode(); } static void fillFlightReservation(const QVector &reservations, const KCalCore::Event::Ptr &event) { const auto flight = reservations.at(0).value().reservationFor().value(); const auto airline = flight.airline(); const auto depPort = flight.departureAirport(); const auto arrPort = flight.arrivalAirport(); const QString flightNumber = airline.iataCode() + QLatin1Char(' ') + flight.flightNumber(); event->setSummary(i18n("Flight %1 from %2 to %3", flightNumber, airportDisplayCode(depPort), airportDisplayCode(arrPort))); event->setLocation(depPort.name()); fillGeoPosition(depPort, event); event->setDtStart(flight.departureTime()); event->setDtEnd(flight.arrivalTime()); event->setAllDay(false); const auto boardingTime = flight.boardingTime(); const auto departureGate = flight.departureGate(); if (boardingTime.isValid()) { const auto startOffset = Duration(event->dtStart(), boardingTime); const auto existinAlarms = event->alarms(); const auto it = std::find_if(existinAlarms.begin(), existinAlarms.end(), [startOffset](const Alarm::Ptr &other) { return other->startOffset() == startOffset; }); if (it == existinAlarms.end()) { Alarm::Ptr alarm(new Alarm(event.data())); alarm->setStartOffset(Duration(event->dtStart(), boardingTime)); if (departureGate.isEmpty()) { alarm->setDisplayAlarm(i18n("Boarding for flight %1", flightNumber)); } else { alarm->setDisplayAlarm(i18n("Boarding for flight %1 at gate %2", flightNumber, departureGate)); } alarm->setEnabled(true); event->addAlarm(alarm); } } QStringList desc; if (boardingTime.isValid()) { desc.push_back(i18n("Boarding time: %1", QLocale().toString(boardingTime.time(), QLocale::ShortFormat))); } if (!departureGate.isEmpty()) { desc.push_back(i18n("Departure gate: %1", departureGate)); } for (const auto &r : reservations) { const auto reservation = r.value(); const auto person = reservation.underName().value(); if (!person.name().isEmpty()) { desc.push_back(person.name()); } if (!reservation.boardingGroup().isEmpty()) { desc.push_back(i18n("Boarding group: %1", reservation.boardingGroup())); } if (!reservation.airplaneSeat().isEmpty()) { desc.push_back(i18n("Seat: %1", reservation.airplaneSeat())); } if (!reservation.reservationNumber().isEmpty()) { desc.push_back(i18n("Booking reference: %1", reservation.reservationNumber())); } } event->setDescription(desc.join(QLatin1Char('\n'))); } static void fillTrainReservation(const TrainReservation &reservation, const KCalCore::Event::Ptr &event) { const auto trip = reservation.reservationFor().value(); const auto depStation = trip.departureStation(); const auto arrStation = trip.arrivalStation(); event->setSummary(i18n("Train %1 from %2 to %3", trip.trainNumber(), depStation.name(), arrStation.name())); event->setLocation(depStation.name()); fillGeoPosition(depStation, event); event->setDtStart(trip.departureTime()); event->setDtEnd(trip.arrivalTime()); event->setAllDay(false); QStringList desc; if (!trip.departurePlatform().isEmpty()) { desc.push_back(i18n("Departure platform: %1", trip.departurePlatform())); } const auto ticket = reservation.reservedTicket().value(); const auto seat = ticket.ticketedSeat(); if (!seat.seatSection().isEmpty()) { desc.push_back(i18n("Coach: %1", seat.seatSection())); } if (!seat.seatNumber().isEmpty()) { desc.push_back(i18n("Seat: %1", seat.seatNumber())); } if (!trip.arrivalPlatform().isEmpty()) { desc.push_back(i18n("Arrival platform: %1", trip.arrivalPlatform())); } if (!reservation.reservationNumber().isEmpty()) { desc.push_back(i18n("Booking reference: %1", reservation.reservationNumber())); } event->setDescription(desc.join(QLatin1Char('\n'))); } static void fillBusReservation(const BusReservation &reservation, const KCalCore::Event::Ptr &event) { const auto trip = reservation.reservationFor().value(); const auto depStation = trip.departureBusStop(); const auto arrStation = trip.arrivalBusStop(); event->setSummary(i18n("Bus %1 from %2 to %3", trip.busNumber(), depStation.name(), arrStation.name())); event->setLocation(depStation.name()); fillGeoPosition(depStation, event); event->setDtStart(trip.departureTime()); event->setDtEnd(trip.arrivalTime()); event->setAllDay(false); QStringList desc; const auto ticket = reservation.reservedTicket().value(); const auto seat = ticket.ticketedSeat(); if (!seat.seatNumber().isEmpty()) { desc.push_back(i18n("Seat: %1", seat.seatNumber())); } if (!reservation.reservationNumber().isEmpty()) { desc.push_back(i18n("Booking reference: %1", reservation.reservationNumber())); } event->setDescription(desc.join(QLatin1Char('\n'))); } static void fillLodgingReservation(const LodgingReservation &reservation, const KCalCore::Event::Ptr &event) { const auto lodgingBusiness = reservation.reservationFor().value(); event->setSummary(i18n("Hotel reservation: %1", lodgingBusiness.name())); event->setLocation(formatAddressSingleLine(lodgingBusiness.address())); fillGeoPosition(lodgingBusiness, event); event->setDtStart(QDateTime(reservation.checkinTime().date(), QTime())); event->setDtEnd(QDateTime(reservation.checkoutTime().date(), QTime())); event->setAllDay(true); event->setDescription(i18n("Check-in: %1\nCheck-out: %2\nBooking reference: %3", QLocale().toString(reservation.checkinTime().time(), QLocale::ShortFormat), QLocale().toString(reservation.checkoutTime().time(), QLocale::ShortFormat), reservation.reservationNumber())); event->setTransparency(KCalCore::Event::Transparent); } static void fillEventReservation(const QVector &reservations, const KCalCore::Event::Ptr &event) { const auto ev = reservations.at(0).value().reservationFor().value(); Place location; if (JsonLd::canConvert(ev.location())) { location = JsonLd::convert(ev.location()); } event->setSummary(ev.name()); event->setLocation(location.name()); fillGeoPosition(location, event); event->setDtStart(ev.startDate()); event->setDtEnd(ev.endDate()); event->setAllDay(false); if (ev.doorTime().isValid()) { const auto startOffset = Duration(event->dtStart(), ev.doorTime()); const auto existinAlarms = event->alarms(); const auto it = std::find_if(existinAlarms.begin(), existinAlarms.end(), [startOffset](const Alarm::Ptr &other) { return other->startOffset() == startOffset; }); if (it == existinAlarms.end()) { Alarm::Ptr alarm(new Alarm(event.data())); alarm->setStartOffset(Duration(event->dtStart(), ev.doorTime())); alarm->setDisplayAlarm(i18n("Entrance for %1", ev.name())); alarm->setEnabled(true); event->addAlarm(alarm); } } QStringList desc; for (const auto &r : reservations) { const auto reservation = r.value(); const auto person = reservation.underName().value(); if (!person.name().isEmpty()) { desc.push_back(person.name()); } // TODO: add seat information if present if (!reservation.reservationNumber().isEmpty()) { desc.push_back(i18n("Booking reference: %1", reservation.reservationNumber())); } } event->setDescription(desc.join(QLatin1Char('\n'))); } static void fillGeoPosition(const QVariant &place, const KCalCore::Event::Ptr &event) { if (!JsonLd::canConvert(place)) { return; } const auto geo = JsonLd::convert(place).geo(); if (!geo.isValid()) { return; } event->setHasGeo(true); event->setGeoLatitude(geo.latitude()); event->setGeoLongitude(geo.longitude()); } static void fillFoodReservation(const FoodEstablishmentReservation &reservation, const KCalCore::Event::Ptr &event) { const auto foodEstablishment = reservation.reservationFor().value(); event->setSummary(i18n("Restaurant reservation: %1", foodEstablishment.name())); event->setLocation(formatAddressSingleLine(foodEstablishment.address())); fillGeoPosition(foodEstablishment, event); event->setDtStart(reservation.startTime()); auto endTime = reservation.endTime(); if (!endTime.isValid()) { endTime = QDateTime(reservation.startTime().date(), QTime(23, 59, 59)); } event->setDtEnd(endTime); event->setAllDay(false); event->setTransparency(KCalCore::Event::Transparent); event->setSummary(i18n("Restaurant reservation: %1", foodEstablishment.name())); event->setDescription(i18n("Number Of People: %1\nReservation reference: %2\nUnder name: %3", reservation.partySize(), reservation.reservationNumber(), reservation.underName().value().name())); } static void fillRentalCarReservation(const RentalCarReservation &reservation, const KCalCore::Event::Ptr &event) { const auto rentalCalPickup = reservation.pickupLocation(); const auto addressPickUp = rentalCalPickup.address(); const auto rentalCar = reservation.reservationFor().value(); event->setSummary(i18n("Rental Car reservation: %1", rentalCar.name())); event->setLocation(formatAddressSingleLine(addressPickUp)); fillGeoPosition(rentalCalPickup, event); event->setDtStart(reservation.pickupTime()); event->setDtEnd(reservation.dropoffTime()); event->setAllDay(false); event->setTransparency(KCalCore::Event::Transparent); event->setSummary(i18n("Rent car reservation: %1", rentalCar.name())); const QString pickUpAddress = formatAddress(addressPickUp); const auto rentalCalDropOff = reservation.dropoffLocation(); const auto addressDropOff = rentalCalDropOff.address(); const QString dropAddress = formatAddress(addressDropOff); const QString description = i18n("Reservation reference: %1\nUnder name: %2\n\nPickUp location: %3\n\nDropoff Location: %4", reservation.reservationNumber(), reservation.underName().value().name(), pickUpAddress, dropAddress); event->setDescription(description); } static void fillTaxiReservation(const TaxiReservation &reservation, const KCalCore::Event::Ptr &event) { const auto taxiPickup = reservation.pickupLocation(); const auto addressPickUp = taxiPickup.address(); //TODO const auto rentalCar = reservation.reservationFor().value(); //TODO event->setSummary(i18n("Rental Car reservation: %1", rentalCar.name())); event->setLocation(formatAddressSingleLine(addressPickUp)); fillGeoPosition(taxiPickup, event); event->setDtStart(reservation.pickupTime()); //TODO event->setDtEnd(reservation.dropoffTime()); event->setAllDay(false); event->setTransparency(KCalCore::Event::Transparent); //TODO event->setSummary(i18n("Rent car reservation: %1", rentalCar.name())); const QString pickUpAddress = formatAddress(addressPickUp); const QString description = i18n("Reservation reference: %1\nUnder name: %2\nPickUp location: %3", reservation.reservationNumber(), reservation.underName().value().name(), pickUpAddress); event->setDescription(description); } #endif diff --git a/src/structureddataextractor.cpp b/src/structureddataextractor.cpp index 7269efd..20c4501 100644 --- a/src/structureddataextractor.cpp +++ b/src/structureddataextractor.cpp @@ -1,163 +1,163 @@ /* Copyright (c) 2017 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "structureddataextractor.h" #include "htmldocument.h" #include "logging.h" #include #include #include #include #include using namespace KItinerary; static QByteArray fixupJson(const QByteArray &data) { auto output(data); // Eurowings doesn't put a comma between objects in top-level arrays... output.replace("}{", "},{"); return output; } static void parseJson(const QByteArray &data, QJsonArray &result) { QJsonParseError error; auto jsonDoc = QJsonDocument::fromJson(data, &error); if (jsonDoc.isNull()) { if (error.error != QJsonParseError::NoError) { // try to fix up common JSON encoding errors jsonDoc = QJsonDocument::fromJson(fixupJson(data)); } if (jsonDoc.isNull()) { qCDebug(Log).noquote() << data; qCDebug(Log) << error.errorString() << "at offset" << error.offset; return; } } if (jsonDoc.isArray()) { for (const auto &v : jsonDoc.array()) { result.push_back(v); } } else if (jsonDoc.isObject()) { result.push_back(jsonDoc.object()); } } -static QString valueForItemProperty(HtmlElement elem) +static QString valueForItemProperty(const HtmlElement &elem) { // TODO see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/itemprop#Values const auto elemName = elem.name(); QString v; if (elemName == QLatin1String("meta")) { v = elem.attribute(QLatin1String("content")); } else if (elemName == QLatin1String("time")) { v = elem.attribute(QLatin1String("datetime")); } else if (elemName == QLatin1String("link") || elemName == QLatin1String("a")) { if (elem.hasAttribute(QLatin1String("href"))) { v = elem.attribute(QLatin1String("href")); } else if (elem.hasAttribute(QLatin1String("content"))) { v = elem.attribute(QLatin1String("content")); } else { v = elem.recursiveContent(); } } else { v = elem.recursiveContent(); } return v; } -static void parseMicroData(HtmlElement elem, QJsonObject &obj) +static void parseMicroData(const HtmlElement &elem, QJsonObject &obj) { auto child = elem.firstChild(); while (!child.isNull()) { const auto prop = child.attribute(QLatin1String("itemprop")); const auto type = child.attribute(QLatin1String("itemtype")); if (type.startsWith(QLatin1String("http://schema.org/"))) { QJsonObject subObj; parseMicroData(child, subObj); const QUrl typeUrl(type); subObj.insert(QStringLiteral("@type"), typeUrl.fileName()); obj.insert(prop, subObj); } else if (!prop.isEmpty()) { obj.insert(prop, valueForItemProperty(child)); } else { // skip intermediate nodes without Microdata annotations parseMicroData(child, obj); } child = child.nextSibling(); } } -static void extractRecursive(HtmlElement elem, QJsonArray &result) +static void extractRecursive(const HtmlElement &elem, QJsonArray &result) { // JSON-LD if (elem.name() == QLatin1String("script") && elem.attribute(QLatin1String("type")) == QLatin1String("application/ld+json")) { parseJson(elem.content().toUtf8(), result); return; } // Microdata const auto itemType = elem.attribute(QLatin1String("itemtype")); if (itemType.startsWith(QLatin1String("http://schema.org/"))) { QJsonObject obj; parseMicroData(elem, obj); if (obj.isEmpty()) { return; } const QUrl typeUrl(itemType); obj.insert(QStringLiteral("@type"), typeUrl.fileName()); const auto itemProp = elem.attribute(QLatin1String("itemprop")); if (!itemProp.isEmpty() && !result.isEmpty()) { // this is likely a child of our preceding sibling, but broken XML put it here auto parent = result.last().toObject(); parent.insert(itemProp, obj); result[result.size() - 1] = parent; } else { obj.insert(QStringLiteral("@context"), QStringLiteral("http://schema.org")); result.push_back(obj); } return; } // recurse otherwise auto child = elem.firstChild(); while (!child.isNull()) { extractRecursive(child, result); child = child.nextSibling(); } } QJsonArray StructuredDataExtractor::extract(HtmlDocument *doc) { Q_ASSERT(doc); QJsonArray result; if (doc->root().isNull()) { return result; } extractRecursive(doc->root(), result); return result; }