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 | |