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