1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
4 *
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 *
9 */
10
11 #include <AccessibilityCheck.hxx>
12 #include <AccessibilityIssue.hxx>
13 #include <AccessibilityCheckStrings.hrc>
14 #include <ndnotxt.hxx>
15 #include <ndtxt.hxx>
16 #include <docsh.hxx>
17 #include <IDocumentDrawModelAccess.hxx>
18 #include <drawdoc.hxx>
19 #include <svx/svdpage.hxx>
20 #include <swtable.hxx>
21 #include <com/sun/star/frame/XModel.hpp>
22 #include <com/sun/star/text/XTextContent.hpp>
23 #include <com/sun/star/document/XDocumentPropertiesSupplier.hpp>
24 #include <unoparagraph.hxx>
25 #include <tools/urlobj.hxx>
26 #include <editeng/langitem.hxx>
27 #include <charatr.hxx>
28 #include <svx/xfillit0.hxx>
29 #include <svx/xflclit.hxx>
30 #include <ftnidx.hxx>
31 #include <txtftn.hxx>
32 #include <svl/itemiter.hxx>
33 #include <o3tl/vector_utils.hxx>
34 #include <svx/swframetypes.hxx>
35 #include <fmtanchr.hxx>
36 #include <dcontact.hxx>
37 #include <svx/svdoashp.hxx>
38 #include <svx/sdasitm.hxx>
39
40 namespace sw
41 {
42 namespace
43 {
44 std::shared_ptr<sw::AccessibilityIssue>
lclAddIssue(sfx::AccessibilityIssueCollection & rIssueCollection,OUString const & rText,sfx::AccessibilityIssueID eIssue=sfx::AccessibilityIssueID::UNSPECIFIED)45 lclAddIssue(sfx::AccessibilityIssueCollection& rIssueCollection, OUString const& rText,
46 sfx::AccessibilityIssueID eIssue = sfx::AccessibilityIssueID::UNSPECIFIED)
47 {
48 auto pIssue = std::make_shared<sw::AccessibilityIssue>(eIssue);
49 pIssue->m_aIssueText = rText;
50 rIssueCollection.getIssues().push_back(pIssue);
51 return pIssue;
52 }
53
54 class BaseCheck
55 {
56 protected:
57 sfx::AccessibilityIssueCollection& m_rIssueCollection;
58
59 public:
BaseCheck(sfx::AccessibilityIssueCollection & rIssueCollection)60 BaseCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
61 : m_rIssueCollection(rIssueCollection)
62 {
63 }
~BaseCheck()64 virtual ~BaseCheck() {}
65 };
66
67 class NodeCheck : public BaseCheck
68 {
69 public:
NodeCheck(sfx::AccessibilityIssueCollection & rIssueCollection)70 NodeCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
71 : BaseCheck(rIssueCollection)
72 {
73 }
74
75 virtual void check(SwNode* pCurrent) = 0;
76 };
77
78 // Check NoTextNodes: Graphic, OLE for alt (title) text
79 class NoTextNodeAltTextCheck : public NodeCheck
80 {
checkNoTextNode(SwNoTextNode * pNoTextNode)81 void checkNoTextNode(SwNoTextNode* pNoTextNode)
82 {
83 if (!pNoTextNode)
84 return;
85
86 OUString sAlternative = pNoTextNode->GetTitle();
87 if (!sAlternative.isEmpty())
88 return;
89
90 OUString sName = pNoTextNode->GetFlyFormat()->GetName();
91
92 OUString sIssueText = SwResId(STR_NO_ALT).replaceAll("%OBJECT_NAME%", sName);
93
94 if (pNoTextNode->IsOLENode())
95 {
96 auto pIssue = lclAddIssue(m_rIssueCollection, sIssueText,
97 sfx::AccessibilityIssueID::NO_ALT_OLE);
98 pIssue->setDoc(pNoTextNode->GetDoc());
99 pIssue->setIssueObject(IssueObject::OLE);
100 pIssue->setObjectID(pNoTextNode->GetFlyFormat()->GetName());
101 }
102 else if (pNoTextNode->IsGrfNode())
103 {
104 auto pIssue = lclAddIssue(m_rIssueCollection, sIssueText,
105 sfx::AccessibilityIssueID::NO_ALT_GRAPHIC);
106 pIssue->setDoc(pNoTextNode->GetDoc());
107 pIssue->setIssueObject(IssueObject::GRAPHIC);
108 pIssue->setObjectID(pNoTextNode->GetFlyFormat()->GetName());
109 }
110 }
111
112 public:
NoTextNodeAltTextCheck(sfx::AccessibilityIssueCollection & rIssueCollection)113 NoTextNodeAltTextCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
114 : NodeCheck(rIssueCollection)
115 {
116 }
117
check(SwNode * pCurrent)118 void check(SwNode* pCurrent) override
119 {
120 if (pCurrent->GetNodeType() & SwNodeType::NoTextMask)
121 {
122 SwNoTextNode* pNoTextNode = pCurrent->GetNoTextNode();
123 if (pNoTextNode)
124 checkNoTextNode(pNoTextNode);
125 }
126 }
127 };
128
129 // Check Table node if the table is merged and split.
130 class TableNodeMergeSplitCheck : public NodeCheck
131 {
132 private:
addTableIssue(SwTable const & rTable,SwDoc & rDoc)133 void addTableIssue(SwTable const& rTable, SwDoc& rDoc)
134 {
135 const SwTableFormat* pFormat = rTable.GetFrameFormat();
136 OUString sName = pFormat->GetName();
137 OUString sIssueText = SwResId(STR_TABLE_MERGE_SPLIT).replaceAll("%OBJECT_NAME%", sName);
138 auto pIssue = lclAddIssue(m_rIssueCollection, sIssueText,
139 sfx::AccessibilityIssueID::TABLE_MERGE_SPLIT);
140 pIssue->setDoc(rDoc);
141 pIssue->setIssueObject(IssueObject::TABLE);
142 pIssue->setObjectID(sName);
143 }
144
checkTableNode(SwTableNode * pTableNode)145 void checkTableNode(SwTableNode* pTableNode)
146 {
147 if (!pTableNode)
148 return;
149
150 SwTable const& rTable = pTableNode->GetTable();
151 SwDoc& rDoc = pTableNode->GetDoc();
152 if (rTable.IsTableComplex())
153 {
154 addTableIssue(rTable, rDoc);
155 }
156 else
157 {
158 if (rTable.GetTabLines().size() > 1)
159 {
160 int i = 0;
161 size_t nFirstLineSize = 0;
162 bool bAllColumnsSameSize = true;
163 bool bCellSpansOverMoreRows = false;
164
165 for (SwTableLine const* pTableLine : rTable.GetTabLines())
166 {
167 if (i == 0)
168 {
169 nFirstLineSize = pTableLine->GetTabBoxes().size();
170 }
171 else
172 {
173 size_t nLineSize = pTableLine->GetTabBoxes().size();
174 if (nFirstLineSize != nLineSize)
175 {
176 bAllColumnsSameSize = false;
177 }
178 }
179 i++;
180
181 // Check for row span in each table box (cell)
182 for (SwTableBox const* pBox : pTableLine->GetTabBoxes())
183 {
184 if (pBox->getRowSpan() > 1)
185 bCellSpansOverMoreRows = true;
186 }
187 }
188 if (!bAllColumnsSameSize || bCellSpansOverMoreRows)
189 {
190 addTableIssue(rTable, rDoc);
191 }
192 }
193 }
194 }
195
196 public:
TableNodeMergeSplitCheck(sfx::AccessibilityIssueCollection & rIssueCollection)197 TableNodeMergeSplitCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
198 : NodeCheck(rIssueCollection)
199 {
200 }
201
check(SwNode * pCurrent)202 void check(SwNode* pCurrent) override
203 {
204 if (pCurrent->GetNodeType() & SwNodeType::Table)
205 {
206 SwTableNode* pTableNode = pCurrent->GetTableNode();
207 if (pTableNode)
208 checkTableNode(pTableNode);
209 }
210 }
211 };
212
213 class NumberingCheck : public NodeCheck
214 {
215 private:
216 SwTextNode* m_pPreviousTextNode;
217
218 const std::vector<std::pair<OUString, OUString>> m_aNumberingCombinations{
219 { "1.", "2." }, { "(1)", "(2)" }, { "1)", "2)" }, { "a.", "b." }, { "(a)", "(b)" },
220 { "a)", "b)" }, { "A.", "B." }, { "(A)", "(B)" }, { "A)", "B)" }
221 };
222
223 public:
NumberingCheck(sfx::AccessibilityIssueCollection & rIssueCollection)224 NumberingCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
225 : NodeCheck(rIssueCollection)
226 , m_pPreviousTextNode(nullptr)
227 {
228 }
229
check(SwNode * pCurrent)230 void check(SwNode* pCurrent) override
231 {
232 if (!pCurrent->IsTextNode())
233 return;
234
235 if (m_pPreviousTextNode)
236 {
237 for (auto& rPair : m_aNumberingCombinations)
238 {
239 if (pCurrent->GetTextNode()->GetText().startsWith(rPair.second)
240 && m_pPreviousTextNode->GetText().startsWith(rPair.first))
241 {
242 OUString sNumbering = rPair.first + " " + rPair.second + "...";
243 OUString sIssueText
244 = SwResId(STR_FAKE_NUMBERING).replaceAll("%NUMBERING%", sNumbering);
245 lclAddIssue(m_rIssueCollection, sIssueText);
246 }
247 }
248 }
249 m_pPreviousTextNode = pCurrent->GetTextNode();
250 }
251 };
252
253 class HyperlinkCheck : public NodeCheck
254 {
255 private:
checkTextRange(uno::Reference<text::XTextRange> const & xTextRange)256 void checkTextRange(uno::Reference<text::XTextRange> const& xTextRange)
257 {
258 uno::Reference<beans::XPropertySet> xProperties(xTextRange, uno::UNO_QUERY);
259 if (!xProperties->getPropertySetInfo()->hasPropertyByName("HyperLinkURL"))
260 return;
261
262 OUString sHyperlink;
263 xProperties->getPropertyValue("HyperLinkURL") >>= sHyperlink;
264 if (!sHyperlink.isEmpty())
265 {
266 OUString sText = xTextRange->getString();
267 if (INetURLObject(sText) == INetURLObject(sHyperlink))
268 {
269 OUString sIssueText
270 = SwResId(STR_HYPERLINK_TEXT_IS_LINK).replaceFirst("%LINK%", sHyperlink);
271 lclAddIssue(m_rIssueCollection, sIssueText);
272 }
273 }
274 }
275
276 public:
HyperlinkCheck(sfx::AccessibilityIssueCollection & rIssueCollection)277 HyperlinkCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
278 : NodeCheck(rIssueCollection)
279 {
280 }
281
check(SwNode * pCurrent)282 void check(SwNode* pCurrent) override
283 {
284 if (!pCurrent->IsTextNode())
285 return;
286
287 SwTextNode* pTextNode = pCurrent->GetTextNode();
288 uno::Reference<text::XTextContent> xParagraph
289 = SwXParagraph::CreateXParagraph(pTextNode->GetDoc(), pTextNode);
290 if (!xParagraph.is())
291 return;
292
293 uno::Reference<container::XEnumerationAccess> xRunEnumAccess(xParagraph, uno::UNO_QUERY);
294 uno::Reference<container::XEnumeration> xRunEnum = xRunEnumAccess->createEnumeration();
295 while (xRunEnum->hasMoreElements())
296 {
297 uno::Reference<text::XTextRange> xRun(xRunEnum->nextElement(), uno::UNO_QUERY);
298 if (xRun.is())
299 {
300 checkTextRange(xRun);
301 }
302 }
303 }
304 };
305
306 // Based on https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
calculateRelativeLuminance(Color const & rColor)307 double calculateRelativeLuminance(Color const& rColor)
308 {
309 // Convert to BColor which has R, G, B colors components
310 // represented by a floating point number from [0.0, 1.0]
311 const basegfx::BColor aBColor = rColor.getBColor();
312
313 double r = aBColor.getRed();
314 double g = aBColor.getGreen();
315 double b = aBColor.getBlue();
316
317 // Calculate the values according to the described algorithm
318 r = (r <= 0.03928) ? r / 12.92 : std::pow((r + 0.055) / 1.055, 2.4);
319 g = (g <= 0.03928) ? g / 12.92 : std::pow((g + 0.055) / 1.055, 2.4);
320 b = (b <= 0.03928) ? b / 12.92 : std::pow((b + 0.055) / 1.055, 2.4);
321
322 return 0.2126 * r + 0.7152 * g + 0.0722 * b;
323 }
324
325 // TODO move to common color tools (BColorTools maybe)
326 // Based on https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio
calculateContrastRatio(Color const & rColor1,Color const & rColor2)327 double calculateContrastRatio(Color const& rColor1, Color const& rColor2)
328 {
329 const double fLuminance1 = calculateRelativeLuminance(rColor1);
330 const double fLuminance2 = calculateRelativeLuminance(rColor2);
331 const std::pair<const double, const double> aMinMax = std::minmax(fLuminance1, fLuminance2);
332
333 // (L1 + 0.05) / (L2 + 0.05)
334 // L1 is the lighter color (greater luminance value)
335 // L2 is the darker color (smaller luminance value)
336 return (aMinMax.second + 0.05) / (aMinMax.first + 0.05);
337 }
338
339 class TextContrastCheck : public NodeCheck
340 {
341 private:
checkTextRange(uno::Reference<text::XTextRange> const & xTextRange,uno::Reference<text::XTextContent> const & xParagraph,const SwTextNode * pTextNode)342 void checkTextRange(uno::Reference<text::XTextRange> const& xTextRange,
343 uno::Reference<text::XTextContent> const& xParagraph,
344 const SwTextNode* pTextNode)
345 {
346 Color nParaBackColor(COL_AUTO);
347 uno::Reference<beans::XPropertySet> xParagraphProperties(xParagraph, uno::UNO_QUERY);
348 if (!(xParagraphProperties->getPropertyValue("ParaBackColor") >>= nParaBackColor))
349 {
350 SAL_WARN("sw.a11y", "ParaBackColor void");
351 return;
352 }
353
354 uno::Reference<beans::XPropertySet> xProperties(xTextRange, uno::UNO_QUERY);
355 if (!xProperties.is())
356 return;
357
358 // Foreground color
359 sal_Int32 nCharColor = {}; // spurious -Werror=maybe-uninitialized
360 if (!(xProperties->getPropertyValue("CharColor") >>= nCharColor))
361 { // not sure this is impossible, can the default be void?
362 SAL_WARN("sw.a11y", "CharColor void");
363 return;
364 }
365 Color aForegroundColor(ColorTransparency, nCharColor);
366 if (aForegroundColor == COL_AUTO)
367 return;
368
369 const SwPageDesc* pPageDescription = pTextNode->FindPageDesc();
370 const SwFrameFormat& rPageFormat = pPageDescription->GetMaster();
371 const SwAttrSet& rPageSet = rPageFormat.GetAttrSet();
372
373 const XFillStyleItem* pXFillStyleItem(
374 rPageSet.GetItem<XFillStyleItem>(XATTR_FILLSTYLE, false));
375 Color aPageBackground(COL_AUTO);
376
377 if (pXFillStyleItem && pXFillStyleItem->GetValue() == css::drawing::FillStyle_SOLID)
378 {
379 const XFillColorItem* rXFillColorItem
380 = rPageSet.GetItem<XFillColorItem>(XATTR_FILLCOLOR, false);
381 aPageBackground = rXFillColorItem->GetColorValue();
382 }
383
384 Color nCharBackColor(COL_AUTO);
385
386 if (!(xProperties->getPropertyValue("CharBackColor") >>= nCharBackColor))
387 {
388 SAL_WARN("sw.a11y", "CharBackColor void");
389 return;
390 }
391 // Determine the background color
392 // Try Character background (highlight)
393 Color aBackgroundColor(nCharBackColor);
394
395 // If not character background color, try paragraph background color
396 if (aBackgroundColor == COL_AUTO)
397 aBackgroundColor = nParaBackColor;
398
399 // If not paragraph background color, try page color
400 if (aBackgroundColor == COL_AUTO)
401 aBackgroundColor = aPageBackground;
402
403 // If not page color, assume white background color
404 if (aBackgroundColor == COL_AUTO)
405 aBackgroundColor = COL_WHITE;
406
407 double fContrastRatio = calculateContrastRatio(aForegroundColor, aBackgroundColor);
408 if (fContrastRatio < 4.5)
409 {
410 lclAddIssue(m_rIssueCollection, SwResId(STR_TEXT_CONTRAST));
411 }
412 }
413
414 public:
TextContrastCheck(sfx::AccessibilityIssueCollection & rIssueCollection)415 TextContrastCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
416 : NodeCheck(rIssueCollection)
417 {
418 }
419
check(SwNode * pCurrent)420 void check(SwNode* pCurrent) override
421 {
422 if (!pCurrent->IsTextNode())
423 return;
424
425 SwTextNode* pTextNode = pCurrent->GetTextNode();
426 uno::Reference<text::XTextContent> xParagraph;
427 xParagraph = SwXParagraph::CreateXParagraph(pTextNode->GetDoc(), pTextNode);
428 if (!xParagraph.is())
429 return;
430
431 uno::Reference<container::XEnumerationAccess> xRunEnumAccess(xParagraph, uno::UNO_QUERY);
432 uno::Reference<container::XEnumeration> xRunEnum = xRunEnumAccess->createEnumeration();
433 while (xRunEnum->hasMoreElements())
434 {
435 uno::Reference<text::XTextRange> xRun(xRunEnum->nextElement(), uno::UNO_QUERY);
436 if (xRun.is())
437 checkTextRange(xRun, xParagraph, pTextNode);
438 }
439 }
440 };
441
442 class TextFormattingCheck : public NodeCheck
443 {
444 private:
445 public:
TextFormattingCheck(sfx::AccessibilityIssueCollection & rIssueCollection)446 TextFormattingCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
447 : NodeCheck(rIssueCollection)
448 {
449 }
450
checkAutoFormat(SwTextNode * pTextNode,const SwTextAttr * pTextAttr)451 void checkAutoFormat(SwTextNode* pTextNode, const SwTextAttr* pTextAttr)
452 {
453 const SwFormatAutoFormat& rAutoFormat = pTextAttr->GetAutoFormat();
454 SfxItemIter aItemIter(*rAutoFormat.GetStyleHandle());
455 const SfxPoolItem* pItem = aItemIter.GetCurItem();
456 std::vector<OUString> aFormattings;
457 while (pItem)
458 {
459 OUString sFormattingType;
460 switch (pItem->Which())
461 {
462 case RES_CHRATR_WEIGHT:
463 case RES_CHRATR_CJK_WEIGHT:
464 case RES_CHRATR_CTL_WEIGHT:
465 sFormattingType = "Weight";
466 break;
467 case RES_CHRATR_POSTURE:
468 case RES_CHRATR_CJK_POSTURE:
469 case RES_CHRATR_CTL_POSTURE:
470 sFormattingType = "Posture";
471 break;
472
473 case RES_CHRATR_SHADOWED:
474 sFormattingType = "Shadowed";
475 break;
476
477 case RES_CHRATR_COLOR:
478 sFormattingType = "Font Color";
479 break;
480
481 case RES_CHRATR_FONTSIZE:
482 case RES_CHRATR_CJK_FONTSIZE:
483 case RES_CHRATR_CTL_FONTSIZE:
484 sFormattingType = "Font Size";
485 break;
486
487 case RES_CHRATR_FONT:
488 case RES_CHRATR_CJK_FONT:
489 case RES_CHRATR_CTL_FONT:
490 sFormattingType = "Font";
491 break;
492
493 case RES_CHRATR_EMPHASIS_MARK:
494 sFormattingType = "Emphasis Mark";
495 break;
496
497 case RES_CHRATR_UNDERLINE:
498 sFormattingType = "Underline";
499 break;
500
501 case RES_CHRATR_OVERLINE:
502 sFormattingType = "Overline";
503 break;
504
505 case RES_CHRATR_CROSSEDOUT:
506 sFormattingType = "Strikethrough";
507 break;
508
509 case RES_CHRATR_RELIEF:
510 sFormattingType = "Relief";
511 break;
512
513 case RES_CHRATR_CONTOUR:
514 sFormattingType = "Outline";
515 break;
516 default:
517 break;
518 }
519 if (!sFormattingType.isEmpty())
520 aFormattings.push_back(sFormattingType);
521 pItem = aItemIter.NextItem();
522 }
523 if (aFormattings.empty())
524 return;
525
526 o3tl::remove_duplicates(aFormattings);
527 auto pIssue = lclAddIssue(m_rIssueCollection, SwResId(STR_TEXT_FORMATTING_CONVEYS_MEANING),
528 sfx::AccessibilityIssueID::TEXT_FORMATTING);
529 pIssue->setIssueObject(IssueObject::TEXT);
530 pIssue->setNode(pTextNode);
531 SwDoc& rDocument = pTextNode->GetDoc();
532 pIssue->setDoc(rDocument);
533 pIssue->setStart(pTextAttr->GetStart());
534 pIssue->setEnd(pTextAttr->GetAnyEnd());
535 }
check(SwNode * pCurrent)536 void check(SwNode* pCurrent) override
537 {
538 if (!pCurrent->IsTextNode())
539 return;
540
541 SwTextNode* pTextNode = pCurrent->GetTextNode();
542 if (pTextNode->HasHints())
543 {
544 SwpHints& rHints = pTextNode->GetSwpHints();
545 for (size_t i = 0; i < rHints.Count(); ++i)
546 {
547 const SwTextAttr* pTextAttr = rHints.Get(i);
548 if (pTextAttr->Which() == RES_TXTATR_AUTOFMT)
549 {
550 checkAutoFormat(pTextNode, pTextAttr);
551 }
552 }
553 }
554 }
555 };
556
557 class BlinkingTextCheck : public NodeCheck
558 {
559 private:
checkTextRange(uno::Reference<text::XTextRange> const & xTextRange)560 void checkTextRange(uno::Reference<text::XTextRange> const& xTextRange)
561 {
562 uno::Reference<beans::XPropertySet> xProperties(xTextRange, uno::UNO_QUERY);
563 if (xProperties.is() && xProperties->getPropertySetInfo()->hasPropertyByName("CharFlash"))
564 {
565 bool bBlinking = false;
566 xProperties->getPropertyValue("CharFlash") >>= bBlinking;
567
568 if (bBlinking)
569 {
570 lclAddIssue(m_rIssueCollection, SwResId(STR_TEXT_BLINKING));
571 }
572 }
573 }
574
575 public:
BlinkingTextCheck(sfx::AccessibilityIssueCollection & rIssueCollection)576 BlinkingTextCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
577 : NodeCheck(rIssueCollection)
578 {
579 }
580
check(SwNode * pCurrent)581 void check(SwNode* pCurrent) override
582 {
583 if (!pCurrent->IsTextNode())
584 return;
585
586 SwTextNode* pTextNode = pCurrent->GetTextNode();
587 uno::Reference<text::XTextContent> xParagraph;
588 xParagraph = SwXParagraph::CreateXParagraph(pTextNode->GetDoc(), pTextNode);
589 if (!xParagraph.is())
590 return;
591
592 uno::Reference<container::XEnumerationAccess> xRunEnumAccess(xParagraph, uno::UNO_QUERY);
593 uno::Reference<container::XEnumeration> xRunEnum = xRunEnumAccess->createEnumeration();
594 while (xRunEnum->hasMoreElements())
595 {
596 uno::Reference<text::XTextRange> xRun(xRunEnum->nextElement(), uno::UNO_QUERY);
597 if (xRun.is())
598 checkTextRange(xRun);
599 }
600 }
601 };
602
603 class HeaderCheck : public NodeCheck
604 {
605 private:
606 int m_nPreviousLevel;
607
608 public:
HeaderCheck(sfx::AccessibilityIssueCollection & rIssueCollection)609 HeaderCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
610 : NodeCheck(rIssueCollection)
611 , m_nPreviousLevel(0)
612 {
613 }
614
check(SwNode * pCurrent)615 void check(SwNode* pCurrent) override
616 {
617 if (!pCurrent->IsTextNode())
618 return;
619
620 SwTextNode* pTextNode = pCurrent->GetTextNode();
621 SwTextFormatColl* pCollection = pTextNode->GetTextColl();
622 int nLevel = pCollection->GetAssignedOutlineStyleLevel();
623 if (nLevel < 0)
624 return;
625
626 if (nLevel > m_nPreviousLevel && std::abs(nLevel - m_nPreviousLevel) > 1)
627 {
628 lclAddIssue(m_rIssueCollection, SwResId(STR_HEADINGS_NOT_IN_ORDER));
629 }
630 m_nPreviousLevel = nLevel;
631 }
632 };
633
634 // ISO 142891-1 : 7.14
635 class NonInteractiveFormCheck : public NodeCheck
636 {
637 public:
NonInteractiveFormCheck(sfx::AccessibilityIssueCollection & rIssueCollection)638 NonInteractiveFormCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
639 : NodeCheck(rIssueCollection)
640 {
641 }
642
check(SwNode * pCurrent)643 void check(SwNode* pCurrent) override
644 {
645 if (!pCurrent->IsTextNode())
646 return;
647
648 const auto& text = pCurrent->GetTextNode()->GetText();
649
650 // Series of tests to detect if there are fake forms in the text.
651
652 bool bCheck = text.indexOf("___") == -1; // Repeated underscores.
653
654 if (bCheck)
655 bCheck = text.indexOf("....") == -1; // Repeated dots.
656
657 if (bCheck)
658 bCheck = text.indexOf(u"……") == -1; // Repeated ellipsis.
659
660 if (bCheck)
661 bCheck = text.indexOf(u"….") == -1; // A dot after an ellipsis.
662
663 if (bCheck)
664 bCheck = text.indexOf(u".…") == -1; // An ellipsis after a dot.
665
666 // Checking if all the tests are passed successfully. If not, adding a warning.
667 if (!bCheck)
668 lclAddIssue(m_rIssueCollection, SwResId(STR_NON_INTERACTIVE_FORMS));
669 }
670 };
671
672 /// Check for floating text frames, as it causes problems with reading order.
673 class FloatingTextCheck : public NodeCheck
674 {
675 public:
FloatingTextCheck(sfx::AccessibilityIssueCollection & rIssueCollection)676 FloatingTextCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
677 : NodeCheck(rIssueCollection)
678 {
679 }
680
check(SwNode * pCurrent)681 void check(SwNode* pCurrent) override
682 {
683 // if node is a text-node and if it has text, we proceed. Otherwise - return.
684 const SwTextNode* textNode = pCurrent->GetTextNode();
685 if (!textNode || textNode->GetText().isEmpty())
686 return;
687
688 // If a node is in fly and if it is not anchored as char, throw warning.
689 const SwNode* startFly = pCurrent->FindFlyStartNode();
690 if (startFly
691 && startFly->GetFlyFormat()->GetAnchor().GetAnchorId() != RndStdIds::FLY_AS_CHAR)
692 lclAddIssue(m_rIssueCollection, SwResId(STR_FLOATING_TEXT));
693 }
694 };
695
696 /// Heading paragraphs (with outline levels > 0) are not allowed in tables
697 class TableHeadingCheck : public NodeCheck
698 {
699 private:
700 // Boolean indicating if heading-in-table warning is already triggered.
701 bool m_bPrevPassed;
702
703 public:
TableHeadingCheck(sfx::AccessibilityIssueCollection & rIssueCollection)704 TableHeadingCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
705 : NodeCheck(rIssueCollection)
706 , m_bPrevPassed(true)
707 {
708 }
709
check(SwNode * pCurrent)710 void check(SwNode* pCurrent) override
711 {
712 if (!m_bPrevPassed)
713 return;
714
715 const SwTextNode* textNode = pCurrent->GetTextNode();
716
717 if (textNode && textNode->GetAttrOutlineLevel() != 0)
718 {
719 const SwTableNode* parentTable = pCurrent->FindTableNode();
720
721 if (parentTable)
722 {
723 m_bPrevPassed = false;
724 lclAddIssue(m_rIssueCollection, SwResId(STR_HEADING_IN_TABLE));
725 }
726 }
727 }
728 };
729
730 /// Checking if headings are ordered correctly.
731 class HeadingOrderCheck : public NodeCheck
732 {
733 public:
HeadingOrderCheck(sfx::AccessibilityIssueCollection & rIssueCollection)734 HeadingOrderCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
735 : NodeCheck(rIssueCollection)
736 {
737 }
738
check(SwNode * pCurrent)739 void check(SwNode* pCurrent) override
740 {
741 const SwTextNode* pTextNode = pCurrent->GetTextNode();
742 if (!pTextNode)
743 return;
744
745 // If outline level stands for heading level...
746 const int currentLevel = pTextNode->GetAttrOutlineLevel();
747 if (!currentLevel)
748 return;
749
750 // ... and if is bigger than previous by more than 1, warn.
751 if (currentLevel - m_prevLevel > 1)
752 {
753 // Preparing and posting a warning.
754 OUString resultString = SwResId(STR_HEADING_ORDER);
755 resultString
756 = resultString.replaceAll("%LEVEL_CURRENT%", OUString::number(currentLevel));
757 resultString = resultString.replaceAll("%LEVEL_PREV%", OUString::number(m_prevLevel));
758
759 lclAddIssue(m_rIssueCollection, resultString);
760 }
761
762 // Updating previous level.
763 m_prevLevel = currentLevel;
764 }
765
766 private:
767 // Previous heading level to compare with.
768 int m_prevLevel = 0;
769 };
770
771 class DocumentCheck : public BaseCheck
772 {
773 public:
DocumentCheck(sfx::AccessibilityIssueCollection & rIssueCollection)774 DocumentCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
775 : BaseCheck(rIssueCollection)
776 {
777 }
778
779 virtual void check(SwDoc* pDoc) = 0;
780 };
781
782 // Check default language
783 class DocumentDefaultLanguageCheck : public DocumentCheck
784 {
785 public:
DocumentDefaultLanguageCheck(sfx::AccessibilityIssueCollection & rIssueCollection)786 DocumentDefaultLanguageCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
787 : DocumentCheck(rIssueCollection)
788 {
789 }
790
check(SwDoc * pDoc)791 void check(SwDoc* pDoc) override
792 {
793 // TODO maybe - also check RES_CHRATR_CJK_LANGUAGE, RES_CHRATR_CTL_LANGUAGE if CJK or CTL are enabled
794 const SvxLanguageItem& rLang = pDoc->GetDefault(RES_CHRATR_LANGUAGE);
795 LanguageType eLanguage = rLang.GetLanguage();
796 if (eLanguage == LANGUAGE_NONE)
797 {
798 lclAddIssue(m_rIssueCollection, SwResId(STR_DOCUMENT_DEFAULT_LANGUAGE),
799 sfx::AccessibilityIssueID::DOCUMENT_LANGUAGE);
800 }
801 else
802 {
803 for (SwTextFormatColl* pTextFormatCollection : *pDoc->GetTextFormatColls())
804 {
805 const SwAttrSet& rAttrSet = pTextFormatCollection->GetAttrSet();
806 if (rAttrSet.GetLanguage(false).GetLanguage() == LANGUAGE_NONE)
807 {
808 OUString sName = pTextFormatCollection->GetName();
809 OUString sIssueText
810 = SwResId(STR_STYLE_NO_LANGUAGE).replaceAll("%STYLE_NAME%", sName);
811 lclAddIssue(m_rIssueCollection, sIssueText,
812 sfx::AccessibilityIssueID::STYLE_LANGUAGE);
813 }
814 }
815 }
816 }
817 };
818
819 class DocumentTitleCheck : public DocumentCheck
820 {
821 public:
DocumentTitleCheck(sfx::AccessibilityIssueCollection & rIssueCollection)822 DocumentTitleCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
823 : DocumentCheck(rIssueCollection)
824 {
825 }
826
check(SwDoc * pDoc)827 void check(SwDoc* pDoc) override
828 {
829 SwDocShell* pShell = pDoc->GetDocShell();
830 if (!pShell)
831 return;
832
833 const uno::Reference<document::XDocumentPropertiesSupplier> xDPS(pShell->GetModel(),
834 uno::UNO_QUERY_THROW);
835 const uno::Reference<document::XDocumentProperties> xDocumentProperties(
836 xDPS->getDocumentProperties());
837 OUString sTitle = xDocumentProperties->getTitle();
838 if (sTitle.isEmpty())
839 {
840 lclAddIssue(m_rIssueCollection, SwResId(STR_DOCUMENT_TITLE),
841 sfx::AccessibilityIssueID::DOCUMENT_TITLE);
842 }
843 }
844 };
845
846 class FootnoteEndnoteCheck : public DocumentCheck
847 {
848 public:
FootnoteEndnoteCheck(sfx::AccessibilityIssueCollection & rIssueCollection)849 FootnoteEndnoteCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
850 : DocumentCheck(rIssueCollection)
851 {
852 }
853
check(SwDoc * pDoc)854 void check(SwDoc* pDoc) override
855 {
856 for (SwTextFootnote const* pTextFootnote : pDoc->GetFootnoteIdxs())
857 {
858 SwFormatFootnote const& rFootnote = pTextFootnote->GetFootnote();
859 if (rFootnote.IsEndNote())
860 {
861 lclAddIssue(m_rIssueCollection, SwResId(STR_AVOID_ENDNOTES));
862 }
863 else
864 {
865 lclAddIssue(m_rIssueCollection, SwResId(STR_AVOID_FOOTNOTES));
866 }
867 }
868 }
869 };
870
871 } // end anonymous namespace
872
873 // Check Shapes, TextBox
checkObject(SdrObject * pObject)874 void AccessibilityCheck::checkObject(SdrObject* pObject)
875 {
876 if (!pObject)
877 return;
878
879 // Check for fontworks.
880 if (SdrObjCustomShape* pCustomShape = dynamic_cast<SdrObjCustomShape*>(pObject))
881 {
882 const SdrCustomShapeGeometryItem& rGeometryItem
883 = pCustomShape->GetMergedItem(SDRATTR_CUSTOMSHAPE_GEOMETRY);
884
885 if (const uno::Any* pAny = rGeometryItem.GetPropertyValueByName("Type"))
886 if (pAny->get<OUString>().startsWith("fontwork-"))
887 lclAddIssue(m_aIssueCollection, SwResId(STR_FONTWORKS));
888 }
889
890 // Checking if there is floating Writer text draw object and if so, throwing a warning.
891 // (Floating objects with text create problems with reading order)
892 if (pObject->HasText()
893 && FindFrameFormat(pObject)->GetAnchor().GetAnchorId() != RndStdIds::FLY_AS_CHAR)
894 lclAddIssue(m_aIssueCollection, SwResId(STR_FLOATING_TEXT));
895
896 if (pObject->GetObjIdentifier() == OBJ_CUSTOMSHAPE || pObject->GetObjIdentifier() == OBJ_TEXT)
897 {
898 OUString sAlternative = pObject->GetTitle();
899 if (sAlternative.isEmpty())
900 {
901 OUString sName = pObject->GetName();
902 OUString sIssueText = SwResId(STR_NO_ALT).replaceAll("%OBJECT_NAME%", sName);
903 lclAddIssue(m_aIssueCollection, sIssueText, sfx::AccessibilityIssueID::NO_ALT_SHAPE);
904 }
905 }
906 }
907
check()908 void AccessibilityCheck::check()
909 {
910 if (m_pDoc == nullptr)
911 return;
912
913 std::vector<std::unique_ptr<DocumentCheck>> aDocumentChecks;
914 aDocumentChecks.push_back(std::make_unique<DocumentDefaultLanguageCheck>(m_aIssueCollection));
915 aDocumentChecks.push_back(std::make_unique<DocumentTitleCheck>(m_aIssueCollection));
916 aDocumentChecks.push_back(std::make_unique<FootnoteEndnoteCheck>(m_aIssueCollection));
917
918 for (std::unique_ptr<DocumentCheck>& rpDocumentCheck : aDocumentChecks)
919 {
920 rpDocumentCheck->check(m_pDoc);
921 }
922
923 std::vector<std::unique_ptr<NodeCheck>> aNodeChecks;
924 aNodeChecks.push_back(std::make_unique<NoTextNodeAltTextCheck>(m_aIssueCollection));
925 aNodeChecks.push_back(std::make_unique<TableNodeMergeSplitCheck>(m_aIssueCollection));
926 aNodeChecks.push_back(std::make_unique<NumberingCheck>(m_aIssueCollection));
927 aNodeChecks.push_back(std::make_unique<HyperlinkCheck>(m_aIssueCollection));
928 aNodeChecks.push_back(std::make_unique<TextContrastCheck>(m_aIssueCollection));
929 aNodeChecks.push_back(std::make_unique<BlinkingTextCheck>(m_aIssueCollection));
930 aNodeChecks.push_back(std::make_unique<HeaderCheck>(m_aIssueCollection));
931 aNodeChecks.push_back(std::make_unique<TextFormattingCheck>(m_aIssueCollection));
932 aNodeChecks.push_back(std::make_unique<NonInteractiveFormCheck>(m_aIssueCollection));
933 aNodeChecks.push_back(std::make_unique<FloatingTextCheck>(m_aIssueCollection));
934 aNodeChecks.push_back(std::make_unique<TableHeadingCheck>(m_aIssueCollection));
935 aNodeChecks.push_back(std::make_unique<HeadingOrderCheck>(m_aIssueCollection));
936
937 auto const& pNodes = m_pDoc->GetNodes();
938 SwNode* pNode = nullptr;
939 for (sal_uLong n = 0; n < pNodes.Count(); ++n)
940 {
941 pNode = pNodes[n];
942 if (pNode)
943 {
944 for (std::unique_ptr<NodeCheck>& rpNodeCheck : aNodeChecks)
945 {
946 rpNodeCheck->check(pNode);
947 }
948 }
949 }
950
951 IDocumentDrawModelAccess& rDrawModelAccess = m_pDoc->getIDocumentDrawModelAccess();
952 auto* pModel = rDrawModelAccess.GetDrawModel();
953 for (sal_uInt16 nPage = 0; nPage < pModel->GetPageCount(); ++nPage)
954 {
955 SdrPage* pPage = pModel->GetPage(nPage);
956 for (size_t nObject = 0; nObject < pPage->GetObjCount(); ++nObject)
957 {
958 SdrObject* pObject = pPage->GetObj(nObject);
959 if (pObject)
960 checkObject(pObject);
961 }
962 }
963 }
964
965 } // end sw namespace
966
967 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
968