| 1 | /* |
| 2 | This file is part of the syndication library |
| 3 | SPDX-FileCopyrightText: 2006 Frank Osterfeld <osterfeld@kde.org> |
| 4 | |
| 5 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 6 | */ |
| 7 | |
| 8 | #include "document.h" |
| 9 | #include "dublincore.h" |
| 10 | #include "image.h" |
| 11 | #include "item.h" |
| 12 | #include "model.h" |
| 13 | #include "model_p.h" |
| 14 | #include "resource.h" |
| 15 | #include "rssvocab.h" |
| 16 | #include "sequence.h" |
| 17 | #include "statement.h" |
| 18 | #include "syndicationinfo.h" |
| 19 | #include "textinput.h" |
| 20 | |
| 21 | #include <documentvisitor.h> |
| 22 | #include <tools.h> |
| 23 | |
| 24 | #include <QList> |
| 25 | #include <QString> |
| 26 | #include <QStringList> |
| 27 | #include <QVector> |
| 28 | |
| 29 | #include <algorithm> |
| 30 | |
| 31 | namespace Syndication |
| 32 | { |
| 33 | namespace RDF |
| 34 | { |
| 35 | class SYNDICATION_NO_EXPORT Document::Private |
| 36 | { |
| 37 | public: |
| 38 | Private() |
| 39 | : itemTitleContainsMarkup(false) |
| 40 | , itemTitlesGuessed(false) |
| 41 | , itemDescriptionContainsMarkup(false) |
| 42 | , itemDescGuessed(false) |
| 43 | { |
| 44 | } |
| 45 | mutable bool itemTitleContainsMarkup; |
| 46 | mutable bool itemTitlesGuessed; |
| 47 | mutable bool itemDescriptionContainsMarkup; |
| 48 | mutable bool itemDescGuessed; |
| 49 | QSharedPointer<Model::ModelPrivate> modelPrivate; |
| 50 | }; |
| 51 | |
| 52 | Document::Document() |
| 53 | : Syndication::SpecificDocument() |
| 54 | , ResourceWrapper() |
| 55 | , d(new Private) |
| 56 | { |
| 57 | d->modelPrivate = resource()->model().d; |
| 58 | } |
| 59 | |
| 60 | Document::Document(ResourcePtr resource) |
| 61 | : Syndication::SpecificDocument() |
| 62 | , ResourceWrapper(resource) |
| 63 | , d(new Private) |
| 64 | { |
| 65 | d->modelPrivate = resource->model().d; |
| 66 | } |
| 67 | |
| 68 | Document::Document(const Document &other) |
| 69 | : SpecificDocument(other) |
| 70 | , ResourceWrapper(other) |
| 71 | , d(new Private) |
| 72 | { |
| 73 | *d = *(other.d); |
| 74 | } |
| 75 | |
| 76 | Document::~Document() = default; |
| 77 | |
| 78 | bool Document::operator==(const Document &other) const |
| 79 | { |
| 80 | return ResourceWrapper::operator==(other); |
| 81 | } |
| 82 | |
| 83 | Document &Document::operator=(const Document &other) |
| 84 | { |
| 85 | ResourceWrapper::operator=(other); |
| 86 | *d = *(other.d); |
| 87 | |
| 88 | return *this; |
| 89 | } |
| 90 | |
| 91 | bool Document::accept(DocumentVisitor *visitor) |
| 92 | { |
| 93 | return visitor->visitRDFDocument(document: this); |
| 94 | } |
| 95 | |
| 96 | bool Document::isValid() const |
| 97 | { |
| 98 | return !isNull(); |
| 99 | } |
| 100 | |
| 101 | QString Document::title() const |
| 102 | { |
| 103 | const QString str = resource()->property(property: RSSVocab::self()->title())->asString(); |
| 104 | return normalize(str); |
| 105 | } |
| 106 | |
| 107 | QString Document::description() const |
| 108 | { |
| 109 | const QString str = resource()->property(property: RSSVocab::self()->description())->asString(); |
| 110 | return normalize(str); |
| 111 | } |
| 112 | |
| 113 | QString Document::link() const |
| 114 | { |
| 115 | return resource()->property(property: RSSVocab::self()->link())->asString(); |
| 116 | } |
| 117 | |
| 118 | DublinCore Document::dc() const |
| 119 | { |
| 120 | return DublinCore(resource()); |
| 121 | } |
| 122 | |
| 123 | SyndicationInfo Document::syn() const |
| 124 | { |
| 125 | return SyndicationInfo(resource()); |
| 126 | } |
| 127 | |
| 128 | struct SortItem { |
| 129 | Item item; |
| 130 | int index; |
| 131 | }; |
| 132 | |
| 133 | struct LessThanByIndex { |
| 134 | bool operator()(const SortItem &lhs, const SortItem &rhs) const |
| 135 | { |
| 136 | return lhs.index < rhs.index; |
| 137 | } |
| 138 | }; |
| 139 | |
| 140 | static QList<Item> sortListToMatchSequence(QList<Item> items, const QStringList &uriSequence) |
| 141 | { |
| 142 | QVector<SortItem> toSort; |
| 143 | toSort.reserve(asize: items.size()); |
| 144 | for (const Item &i : items) { |
| 145 | SortItem item; |
| 146 | item.item = i; |
| 147 | item.index = uriSequence.indexOf(str: i.resource()->uri()); |
| 148 | toSort.append(t: item); |
| 149 | } |
| 150 | std::sort(first: toSort.begin(), last: toSort.end(), comp: LessThanByIndex()); |
| 151 | |
| 152 | int i = 0; |
| 153 | for (const SortItem &sortItem : std::as_const(t&: toSort)) { |
| 154 | items[i] = sortItem.item; |
| 155 | i++; |
| 156 | } |
| 157 | |
| 158 | return items; |
| 159 | } |
| 160 | |
| 161 | struct UriLessThan { |
| 162 | bool operator()(const RDF::ResourcePtr &lhs, const RDF::ResourcePtr &rhs) const |
| 163 | { |
| 164 | return lhs->uri() < rhs->uri(); |
| 165 | } |
| 166 | }; |
| 167 | |
| 168 | QList<Item> Document::items() const |
| 169 | { |
| 170 | QList<ResourcePtr> items = resource()->model().resourcesWithType(type: RSSVocab::self()->item()); |
| 171 | // if there is no sequence, ensure sorting by URI to have a defined and deterministic order |
| 172 | // important for unit tests |
| 173 | std::sort(first: items.begin(), last: items.end(), comp: UriLessThan()); |
| 174 | |
| 175 | DocumentPtr doccpy(new Document(*this)); |
| 176 | |
| 177 | QList<Item> list; |
| 178 | list.reserve(asize: items.count()); |
| 179 | |
| 180 | for (const ResourcePtr &i : std::as_const(t&: items)) { |
| 181 | list.append(t: Item(i, doccpy)); |
| 182 | } |
| 183 | |
| 184 | if (resource()->hasProperty(property: RSSVocab::self()->items())) { |
| 185 | NodePtr n = resource()->property(property: RSSVocab::self()->items())->object(); |
| 186 | if (n->isSequence()) { |
| 187 | const SequencePtr seq = n.staticCast<Sequence>(); |
| 188 | |
| 189 | const QList<NodePtr> seqItems = seq->items(); |
| 190 | |
| 191 | QStringList uriSequence; |
| 192 | uriSequence.reserve(asize: seqItems.size()); |
| 193 | |
| 194 | for (const NodePtr &i : seqItems) { |
| 195 | if (i->isResource()) { |
| 196 | uriSequence.append(t: i.staticCast<Resource>()->uri()); |
| 197 | } |
| 198 | } |
| 199 | list = sortListToMatchSequence(items: list, uriSequence); |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | return list; |
| 204 | } |
| 205 | |
| 206 | Image Document::image() const |
| 207 | { |
| 208 | ResourcePtr img = resource()->property(property: RSSVocab::self()->image())->asResource(); |
| 209 | |
| 210 | return img ? Image(img) : Image(); |
| 211 | } |
| 212 | |
| 213 | TextInput Document::textInput() const |
| 214 | { |
| 215 | ResourcePtr ti = resource()->property(property: RSSVocab::self()->textinput())->asResource(); |
| 216 | |
| 217 | return ti ? TextInput(ti) : TextInput(); |
| 218 | } |
| 219 | |
| 220 | void Document::getItemTitleFormatInfo(bool *containsMarkup) const |
| 221 | { |
| 222 | if (!d->itemTitlesGuessed) { |
| 223 | QString titles; |
| 224 | QList<Item> litems = items(); |
| 225 | |
| 226 | if (litems.isEmpty()) { |
| 227 | d->itemTitlesGuessed = true; |
| 228 | return; |
| 229 | } |
| 230 | |
| 231 | const int nmax = std::min<int>(a: litems.size(), b: 10); // we check a maximum of 10 items |
| 232 | int i = 0; |
| 233 | |
| 234 | for (const auto &item : litems) { |
| 235 | if (i++ >= nmax) { |
| 236 | break; |
| 237 | } |
| 238 | titles += item.originalTitle(); |
| 239 | } |
| 240 | |
| 241 | d->itemTitleContainsMarkup = stringContainsMarkup(str: titles); |
| 242 | d->itemTitlesGuessed = true; |
| 243 | } |
| 244 | if (containsMarkup != nullptr) { |
| 245 | *containsMarkup = d->itemTitleContainsMarkup; |
| 246 | } |
| 247 | } |
| 248 | |
| 249 | void Document::getItemDescriptionFormatInfo(bool *containsMarkup) const |
| 250 | { |
| 251 | if (!d->itemDescGuessed) { |
| 252 | QString desc; |
| 253 | QList<Item> litems = items(); |
| 254 | |
| 255 | if (litems.isEmpty()) { |
| 256 | d->itemDescGuessed = true; |
| 257 | return; |
| 258 | } |
| 259 | |
| 260 | const int nmax = std::min<int>(a: litems.size(), b: 10); // we check a maximum of 10 items |
| 261 | int i = 0; |
| 262 | |
| 263 | for (const auto &item : litems) { |
| 264 | if (i++ >= nmax) { |
| 265 | break; |
| 266 | } |
| 267 | desc += item.originalDescription(); |
| 268 | } |
| 269 | |
| 270 | d->itemDescriptionContainsMarkup = stringContainsMarkup(str: desc); |
| 271 | d->itemDescGuessed = true; |
| 272 | } |
| 273 | |
| 274 | if (containsMarkup != nullptr) { |
| 275 | *containsMarkup = d->itemDescriptionContainsMarkup; |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | QString Document::debugInfo() const |
| 280 | { |
| 281 | QString info; |
| 282 | info += QLatin1String("### Document: ###################\n" ); |
| 283 | info += QLatin1String("title: #" ) + title() + QLatin1String("#\n" ); |
| 284 | info += QLatin1String("link: #" ) + link() + QLatin1String("#\n" ); |
| 285 | info += QLatin1String("description: #" ) + description() + QLatin1String("#\n" ); |
| 286 | info += dc().debugInfo(); |
| 287 | info += syn().debugInfo(); |
| 288 | Image img = image(); |
| 289 | if (!img.resource() == 0L) { |
| 290 | info += img.debugInfo(); |
| 291 | } |
| 292 | TextInput input = textInput(); |
| 293 | if (!input.isNull()) { |
| 294 | info += input.debugInfo(); |
| 295 | } |
| 296 | |
| 297 | const QList<Item> itlist = items(); |
| 298 | for (const auto &item : itlist) { |
| 299 | info += item.debugInfo(); |
| 300 | } |
| 301 | |
| 302 | info += QLatin1String("### Document end ################\n" ); |
| 303 | return info; |
| 304 | } |
| 305 | |
| 306 | } // namespace RDF |
| 307 | } // namespace Syndication |
| 308 | |