summaryrefslogtreecommitdiffstats
path: root/sw/source/core/access/AccessibilityCheck.cxx
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:06:44 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:06:44 +0000
commited5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch)
tree7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /sw/source/core/access/AccessibilityCheck.cxx
parentInitial commit. (diff)
downloadlibreoffice-upstream.tar.xz
libreoffice-upstream.zip
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sw/source/core/access/AccessibilityCheck.cxx')
-rw-r--r--sw/source/core/access/AccessibilityCheck.cxx969
1 files changed, 969 insertions, 0 deletions
diff --git a/sw/source/core/access/AccessibilityCheck.cxx b/sw/source/core/access/AccessibilityCheck.cxx
new file mode 100644
index 000000000..50a37dc00
--- /dev/null
+++ b/sw/source/core/access/AccessibilityCheck.cxx
@@ -0,0 +1,969 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ */
+
+#include <AccessibilityCheck.hxx>
+#include <AccessibilityIssue.hxx>
+#include <AccessibilityCheckStrings.hrc>
+#include <ndnotxt.hxx>
+#include <ndtxt.hxx>
+#include <docsh.hxx>
+#include <IDocumentDrawModelAccess.hxx>
+#include <drawdoc.hxx>
+#include <svx/svdpage.hxx>
+#include <swtable.hxx>
+#include <com/sun/star/frame/XModel.hpp>
+#include <com/sun/star/text/XTextContent.hpp>
+#include <com/sun/star/document/XDocumentPropertiesSupplier.hpp>
+#include <unoparagraph.hxx>
+#include <tools/urlobj.hxx>
+#include <editeng/langitem.hxx>
+#include <charatr.hxx>
+#include <svx/xfillit0.hxx>
+#include <svx/xflclit.hxx>
+#include <ftnidx.hxx>
+#include <txtftn.hxx>
+#include <svl/itemiter.hxx>
+#include <o3tl/vector_utils.hxx>
+#include <svx/swframetypes.hxx>
+#include <fmtanchr.hxx>
+#include <dcontact.hxx>
+#include <svx/svdoashp.hxx>
+#include <svx/sdasitm.hxx>
+
+namespace sw
+{
+namespace
+{
+std::shared_ptr<sw::AccessibilityIssue>
+lclAddIssue(sfx::AccessibilityIssueCollection& rIssueCollection, OUString const& rText,
+ sfx::AccessibilityIssueID eIssue = sfx::AccessibilityIssueID::UNSPECIFIED)
+{
+ auto pIssue = std::make_shared<sw::AccessibilityIssue>(eIssue);
+ pIssue->m_aIssueText = rText;
+ rIssueCollection.getIssues().push_back(pIssue);
+ return pIssue;
+}
+
+class BaseCheck
+{
+protected:
+ sfx::AccessibilityIssueCollection& m_rIssueCollection;
+
+public:
+ BaseCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : m_rIssueCollection(rIssueCollection)
+ {
+ }
+ virtual ~BaseCheck() {}
+};
+
+class NodeCheck : public BaseCheck
+{
+public:
+ NodeCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : BaseCheck(rIssueCollection)
+ {
+ }
+
+ virtual void check(SwNode* pCurrent) = 0;
+};
+
+// Check NoTextNodes: Graphic, OLE for alt (title) text
+class NoTextNodeAltTextCheck : public NodeCheck
+{
+ void checkNoTextNode(SwNoTextNode* pNoTextNode)
+ {
+ if (!pNoTextNode)
+ return;
+
+ OUString sAlternative = pNoTextNode->GetTitle();
+ if (!sAlternative.isEmpty())
+ return;
+
+ OUString sName = pNoTextNode->GetFlyFormat()->GetName();
+
+ OUString sIssueText = SwResId(STR_NO_ALT).replaceAll("%OBJECT_NAME%", sName);
+
+ if (pNoTextNode->IsOLENode())
+ {
+ auto pIssue = lclAddIssue(m_rIssueCollection, sIssueText,
+ sfx::AccessibilityIssueID::NO_ALT_OLE);
+ pIssue->setDoc(pNoTextNode->GetDoc());
+ pIssue->setIssueObject(IssueObject::OLE);
+ pIssue->setObjectID(pNoTextNode->GetFlyFormat()->GetName());
+ }
+ else if (pNoTextNode->IsGrfNode())
+ {
+ auto pIssue = lclAddIssue(m_rIssueCollection, sIssueText,
+ sfx::AccessibilityIssueID::NO_ALT_GRAPHIC);
+ pIssue->setDoc(pNoTextNode->GetDoc());
+ pIssue->setIssueObject(IssueObject::GRAPHIC);
+ pIssue->setObjectID(pNoTextNode->GetFlyFormat()->GetName());
+ }
+ }
+
+public:
+ NoTextNodeAltTextCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : NodeCheck(rIssueCollection)
+ {
+ }
+
+ void check(SwNode* pCurrent) override
+ {
+ if (pCurrent->GetNodeType() & SwNodeType::NoTextMask)
+ {
+ SwNoTextNode* pNoTextNode = pCurrent->GetNoTextNode();
+ if (pNoTextNode)
+ checkNoTextNode(pNoTextNode);
+ }
+ }
+};
+
+// Check Table node if the table is merged and split.
+class TableNodeMergeSplitCheck : public NodeCheck
+{
+private:
+ void addTableIssue(SwTable const& rTable, SwDoc& rDoc)
+ {
+ const SwTableFormat* pFormat = rTable.GetFrameFormat();
+ OUString sName = pFormat->GetName();
+ OUString sIssueText = SwResId(STR_TABLE_MERGE_SPLIT).replaceAll("%OBJECT_NAME%", sName);
+ auto pIssue = lclAddIssue(m_rIssueCollection, sIssueText,
+ sfx::AccessibilityIssueID::TABLE_MERGE_SPLIT);
+ pIssue->setDoc(rDoc);
+ pIssue->setIssueObject(IssueObject::TABLE);
+ pIssue->setObjectID(sName);
+ }
+
+ void checkTableNode(SwTableNode* pTableNode)
+ {
+ if (!pTableNode)
+ return;
+
+ SwTable const& rTable = pTableNode->GetTable();
+ SwDoc& rDoc = pTableNode->GetDoc();
+ if (rTable.IsTableComplex())
+ {
+ addTableIssue(rTable, rDoc);
+ }
+ else
+ {
+ if (rTable.GetTabLines().size() > 1)
+ {
+ int i = 0;
+ size_t nFirstLineSize = 0;
+ bool bAllColumnsSameSize = true;
+ bool bCellSpansOverMoreRows = false;
+
+ for (SwTableLine const* pTableLine : rTable.GetTabLines())
+ {
+ if (i == 0)
+ {
+ nFirstLineSize = pTableLine->GetTabBoxes().size();
+ }
+ else
+ {
+ size_t nLineSize = pTableLine->GetTabBoxes().size();
+ if (nFirstLineSize != nLineSize)
+ {
+ bAllColumnsSameSize = false;
+ }
+ }
+ i++;
+
+ // Check for row span in each table box (cell)
+ for (SwTableBox const* pBox : pTableLine->GetTabBoxes())
+ {
+ if (pBox->getRowSpan() > 1)
+ bCellSpansOverMoreRows = true;
+ }
+ }
+ if (!bAllColumnsSameSize || bCellSpansOverMoreRows)
+ {
+ addTableIssue(rTable, rDoc);
+ }
+ }
+ }
+ }
+
+public:
+ TableNodeMergeSplitCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : NodeCheck(rIssueCollection)
+ {
+ }
+
+ void check(SwNode* pCurrent) override
+ {
+ if (pCurrent->GetNodeType() & SwNodeType::Table)
+ {
+ SwTableNode* pTableNode = pCurrent->GetTableNode();
+ if (pTableNode)
+ checkTableNode(pTableNode);
+ }
+ }
+};
+
+class NumberingCheck : public NodeCheck
+{
+private:
+ SwTextNode* m_pPreviousTextNode;
+
+ const std::vector<std::pair<OUString, OUString>> m_aNumberingCombinations{
+ { "1.", "2." }, { "(1)", "(2)" }, { "1)", "2)" }, { "a.", "b." }, { "(a)", "(b)" },
+ { "a)", "b)" }, { "A.", "B." }, { "(A)", "(B)" }, { "A)", "B)" }
+ };
+
+public:
+ NumberingCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : NodeCheck(rIssueCollection)
+ , m_pPreviousTextNode(nullptr)
+ {
+ }
+
+ void check(SwNode* pCurrent) override
+ {
+ if (!pCurrent->IsTextNode())
+ return;
+
+ if (m_pPreviousTextNode)
+ {
+ for (auto& rPair : m_aNumberingCombinations)
+ {
+ if (pCurrent->GetTextNode()->GetText().startsWith(rPair.second)
+ && m_pPreviousTextNode->GetText().startsWith(rPair.first))
+ {
+ OUString sNumbering = rPair.first + " " + rPair.second + "...";
+ OUString sIssueText
+ = SwResId(STR_FAKE_NUMBERING).replaceAll("%NUMBERING%", sNumbering);
+ lclAddIssue(m_rIssueCollection, sIssueText);
+ }
+ }
+ }
+ m_pPreviousTextNode = pCurrent->GetTextNode();
+ }
+};
+
+class HyperlinkCheck : public NodeCheck
+{
+private:
+ void checkTextRange(uno::Reference<text::XTextRange> const& xTextRange)
+ {
+ uno::Reference<beans::XPropertySet> xProperties(xTextRange, uno::UNO_QUERY);
+ if (!xProperties->getPropertySetInfo()->hasPropertyByName("HyperLinkURL"))
+ return;
+
+ OUString sHyperlink;
+ xProperties->getPropertyValue("HyperLinkURL") >>= sHyperlink;
+ if (!sHyperlink.isEmpty())
+ {
+ OUString sText = xTextRange->getString();
+ if (INetURLObject(sText) == INetURLObject(sHyperlink))
+ {
+ OUString sIssueText
+ = SwResId(STR_HYPERLINK_TEXT_IS_LINK).replaceFirst("%LINK%", sHyperlink);
+ lclAddIssue(m_rIssueCollection, sIssueText);
+ }
+ }
+ }
+
+public:
+ HyperlinkCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : NodeCheck(rIssueCollection)
+ {
+ }
+
+ void check(SwNode* pCurrent) override
+ {
+ if (!pCurrent->IsTextNode())
+ return;
+
+ SwTextNode* pTextNode = pCurrent->GetTextNode();
+ uno::Reference<text::XTextContent> xParagraph
+ = SwXParagraph::CreateXParagraph(pTextNode->GetDoc(), pTextNode);
+ if (!xParagraph.is())
+ return;
+
+ uno::Reference<container::XEnumerationAccess> xRunEnumAccess(xParagraph, uno::UNO_QUERY);
+ uno::Reference<container::XEnumeration> xRunEnum = xRunEnumAccess->createEnumeration();
+ while (xRunEnum->hasMoreElements())
+ {
+ uno::Reference<text::XTextRange> xRun(xRunEnum->nextElement(), uno::UNO_QUERY);
+ if (xRun.is())
+ {
+ checkTextRange(xRun);
+ }
+ }
+ }
+};
+
+// Based on https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
+double calculateRelativeLuminance(Color const& rColor)
+{
+ // Convert to BColor which has R, G, B colors components
+ // represented by a floating point number from [0.0, 1.0]
+ const basegfx::BColor aBColor = rColor.getBColor();
+
+ double r = aBColor.getRed();
+ double g = aBColor.getGreen();
+ double b = aBColor.getBlue();
+
+ // Calculate the values according to the described algorithm
+ r = (r <= 0.03928) ? r / 12.92 : std::pow((r + 0.055) / 1.055, 2.4);
+ g = (g <= 0.03928) ? g / 12.92 : std::pow((g + 0.055) / 1.055, 2.4);
+ b = (b <= 0.03928) ? b / 12.92 : std::pow((b + 0.055) / 1.055, 2.4);
+
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
+}
+
+// TODO move to common color tools (BColorTools maybe)
+// Based on https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio
+double calculateContrastRatio(Color const& rColor1, Color const& rColor2)
+{
+ const double fLuminance1 = calculateRelativeLuminance(rColor1);
+ const double fLuminance2 = calculateRelativeLuminance(rColor2);
+ const std::pair<const double, const double> aMinMax = std::minmax(fLuminance1, fLuminance2);
+
+ // (L1 + 0.05) / (L2 + 0.05)
+ // L1 is the lighter color (greater luminance value)
+ // L2 is the darker color (smaller luminance value)
+ return (aMinMax.second + 0.05) / (aMinMax.first + 0.05);
+}
+
+class TextContrastCheck : public NodeCheck
+{
+private:
+ void checkTextRange(uno::Reference<text::XTextRange> const& xTextRange,
+ uno::Reference<text::XTextContent> const& xParagraph,
+ const SwTextNode* pTextNode)
+ {
+ Color nParaBackColor(COL_AUTO);
+ uno::Reference<beans::XPropertySet> xParagraphProperties(xParagraph, uno::UNO_QUERY);
+ if (!(xParagraphProperties->getPropertyValue("ParaBackColor") >>= nParaBackColor))
+ {
+ SAL_WARN("sw.a11y", "ParaBackColor void");
+ return;
+ }
+
+ uno::Reference<beans::XPropertySet> xProperties(xTextRange, uno::UNO_QUERY);
+ if (!xProperties.is())
+ return;
+
+ // Foreground color
+ sal_Int32 nCharColor = {}; // spurious -Werror=maybe-uninitialized
+ if (!(xProperties->getPropertyValue("CharColor") >>= nCharColor))
+ { // not sure this is impossible, can the default be void?
+ SAL_WARN("sw.a11y", "CharColor void");
+ return;
+ }
+ Color aForegroundColor(ColorTransparency, nCharColor);
+ if (aForegroundColor == COL_AUTO)
+ return;
+
+ const SwPageDesc* pPageDescription = pTextNode->FindPageDesc();
+ const SwFrameFormat& rPageFormat = pPageDescription->GetMaster();
+ const SwAttrSet& rPageSet = rPageFormat.GetAttrSet();
+
+ const XFillStyleItem* pXFillStyleItem(
+ rPageSet.GetItem<XFillStyleItem>(XATTR_FILLSTYLE, false));
+ Color aPageBackground(COL_AUTO);
+
+ if (pXFillStyleItem && pXFillStyleItem->GetValue() == css::drawing::FillStyle_SOLID)
+ {
+ const XFillColorItem* rXFillColorItem
+ = rPageSet.GetItem<XFillColorItem>(XATTR_FILLCOLOR, false);
+ aPageBackground = rXFillColorItem->GetColorValue();
+ }
+
+ Color nCharBackColor(COL_AUTO);
+
+ if (!(xProperties->getPropertyValue("CharBackColor") >>= nCharBackColor))
+ {
+ SAL_WARN("sw.a11y", "CharBackColor void");
+ return;
+ }
+ // Determine the background color
+ // Try Character background (highlight)
+ Color aBackgroundColor(nCharBackColor);
+
+ // If not character background color, try paragraph background color
+ if (aBackgroundColor == COL_AUTO)
+ aBackgroundColor = nParaBackColor;
+
+ // If not paragraph background color, try page color
+ if (aBackgroundColor == COL_AUTO)
+ aBackgroundColor = aPageBackground;
+
+ // If not page color, assume white background color
+ if (aBackgroundColor == COL_AUTO)
+ aBackgroundColor = COL_WHITE;
+
+ double fContrastRatio = calculateContrastRatio(aForegroundColor, aBackgroundColor);
+ if (fContrastRatio < 4.5)
+ {
+ lclAddIssue(m_rIssueCollection, SwResId(STR_TEXT_CONTRAST));
+ }
+ }
+
+public:
+ TextContrastCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : NodeCheck(rIssueCollection)
+ {
+ }
+
+ void check(SwNode* pCurrent) override
+ {
+ if (!pCurrent->IsTextNode())
+ return;
+
+ SwTextNode* pTextNode = pCurrent->GetTextNode();
+ uno::Reference<text::XTextContent> xParagraph;
+ xParagraph = SwXParagraph::CreateXParagraph(pTextNode->GetDoc(), pTextNode);
+ if (!xParagraph.is())
+ return;
+
+ uno::Reference<container::XEnumerationAccess> xRunEnumAccess(xParagraph, uno::UNO_QUERY);
+ uno::Reference<container::XEnumeration> xRunEnum = xRunEnumAccess->createEnumeration();
+ while (xRunEnum->hasMoreElements())
+ {
+ uno::Reference<text::XTextRange> xRun(xRunEnum->nextElement(), uno::UNO_QUERY);
+ if (xRun.is())
+ checkTextRange(xRun, xParagraph, pTextNode);
+ }
+ }
+};
+
+class TextFormattingCheck : public NodeCheck
+{
+private:
+public:
+ TextFormattingCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : NodeCheck(rIssueCollection)
+ {
+ }
+
+ void checkAutoFormat(SwTextNode* pTextNode, const SwTextAttr* pTextAttr)
+ {
+ const SwFormatAutoFormat& rAutoFormat = pTextAttr->GetAutoFormat();
+ SfxItemIter aItemIter(*rAutoFormat.GetStyleHandle());
+ const SfxPoolItem* pItem = aItemIter.GetCurItem();
+ std::vector<OUString> aFormattings;
+ while (pItem)
+ {
+ OUString sFormattingType;
+ switch (pItem->Which())
+ {
+ case RES_CHRATR_WEIGHT:
+ case RES_CHRATR_CJK_WEIGHT:
+ case RES_CHRATR_CTL_WEIGHT:
+ sFormattingType = "Weight";
+ break;
+ case RES_CHRATR_POSTURE:
+ case RES_CHRATR_CJK_POSTURE:
+ case RES_CHRATR_CTL_POSTURE:
+ sFormattingType = "Posture";
+ break;
+
+ case RES_CHRATR_SHADOWED:
+ sFormattingType = "Shadowed";
+ break;
+
+ case RES_CHRATR_COLOR:
+ sFormattingType = "Font Color";
+ break;
+
+ case RES_CHRATR_FONTSIZE:
+ case RES_CHRATR_CJK_FONTSIZE:
+ case RES_CHRATR_CTL_FONTSIZE:
+ sFormattingType = "Font Size";
+ break;
+
+ case RES_CHRATR_FONT:
+ case RES_CHRATR_CJK_FONT:
+ case RES_CHRATR_CTL_FONT:
+ sFormattingType = "Font";
+ break;
+
+ case RES_CHRATR_EMPHASIS_MARK:
+ sFormattingType = "Emphasis Mark";
+ break;
+
+ case RES_CHRATR_UNDERLINE:
+ sFormattingType = "Underline";
+ break;
+
+ case RES_CHRATR_OVERLINE:
+ sFormattingType = "Overline";
+ break;
+
+ case RES_CHRATR_CROSSEDOUT:
+ sFormattingType = "Strikethrough";
+ break;
+
+ case RES_CHRATR_RELIEF:
+ sFormattingType = "Relief";
+ break;
+
+ case RES_CHRATR_CONTOUR:
+ sFormattingType = "Outline";
+ break;
+ default:
+ break;
+ }
+ if (!sFormattingType.isEmpty())
+ aFormattings.push_back(sFormattingType);
+ pItem = aItemIter.NextItem();
+ }
+ if (aFormattings.empty())
+ return;
+
+ o3tl::remove_duplicates(aFormattings);
+ auto pIssue = lclAddIssue(m_rIssueCollection, SwResId(STR_TEXT_FORMATTING_CONVEYS_MEANING),
+ sfx::AccessibilityIssueID::TEXT_FORMATTING);
+ pIssue->setIssueObject(IssueObject::TEXT);
+ pIssue->setNode(pTextNode);
+ SwDoc& rDocument = pTextNode->GetDoc();
+ pIssue->setDoc(rDocument);
+ pIssue->setStart(pTextAttr->GetStart());
+ pIssue->setEnd(pTextAttr->GetAnyEnd());
+ }
+ void check(SwNode* pCurrent) override
+ {
+ if (!pCurrent->IsTextNode())
+ return;
+
+ SwTextNode* pTextNode = pCurrent->GetTextNode();
+ if (pTextNode->HasHints())
+ {
+ SwpHints& rHints = pTextNode->GetSwpHints();
+ for (size_t i = 0; i < rHints.Count(); ++i)
+ {
+ const SwTextAttr* pTextAttr = rHints.Get(i);
+ if (pTextAttr->Which() == RES_TXTATR_AUTOFMT)
+ {
+ checkAutoFormat(pTextNode, pTextAttr);
+ }
+ }
+ }
+ }
+};
+
+class BlinkingTextCheck : public NodeCheck
+{
+private:
+ void checkTextRange(uno::Reference<text::XTextRange> const& xTextRange)
+ {
+ uno::Reference<beans::XPropertySet> xProperties(xTextRange, uno::UNO_QUERY);
+ if (xProperties.is() && xProperties->getPropertySetInfo()->hasPropertyByName("CharFlash"))
+ {
+ bool bBlinking = false;
+ xProperties->getPropertyValue("CharFlash") >>= bBlinking;
+
+ if (bBlinking)
+ {
+ lclAddIssue(m_rIssueCollection, SwResId(STR_TEXT_BLINKING));
+ }
+ }
+ }
+
+public:
+ BlinkingTextCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : NodeCheck(rIssueCollection)
+ {
+ }
+
+ void check(SwNode* pCurrent) override
+ {
+ if (!pCurrent->IsTextNode())
+ return;
+
+ SwTextNode* pTextNode = pCurrent->GetTextNode();
+ uno::Reference<text::XTextContent> xParagraph;
+ xParagraph = SwXParagraph::CreateXParagraph(pTextNode->GetDoc(), pTextNode);
+ if (!xParagraph.is())
+ return;
+
+ uno::Reference<container::XEnumerationAccess> xRunEnumAccess(xParagraph, uno::UNO_QUERY);
+ uno::Reference<container::XEnumeration> xRunEnum = xRunEnumAccess->createEnumeration();
+ while (xRunEnum->hasMoreElements())
+ {
+ uno::Reference<text::XTextRange> xRun(xRunEnum->nextElement(), uno::UNO_QUERY);
+ if (xRun.is())
+ checkTextRange(xRun);
+ }
+ }
+};
+
+class HeaderCheck : public NodeCheck
+{
+private:
+ int m_nPreviousLevel;
+
+public:
+ HeaderCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : NodeCheck(rIssueCollection)
+ , m_nPreviousLevel(0)
+ {
+ }
+
+ void check(SwNode* pCurrent) override
+ {
+ if (!pCurrent->IsTextNode())
+ return;
+
+ SwTextNode* pTextNode = pCurrent->GetTextNode();
+ SwTextFormatColl* pCollection = pTextNode->GetTextColl();
+ if (!pCollection->IsAssignedToListLevelOfOutlineStyle())
+ return;
+
+ int nLevel = pCollection->GetAssignedOutlineStyleLevel();
+ assert(nLevel >= 0);
+ if (nLevel > m_nPreviousLevel && std::abs(nLevel - m_nPreviousLevel) > 1)
+ {
+ lclAddIssue(m_rIssueCollection, SwResId(STR_HEADINGS_NOT_IN_ORDER));
+ }
+ m_nPreviousLevel = nLevel;
+ }
+};
+
+// ISO 142891-1 : 7.14
+class NonInteractiveFormCheck : public NodeCheck
+{
+public:
+ NonInteractiveFormCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : NodeCheck(rIssueCollection)
+ {
+ }
+
+ void check(SwNode* pCurrent) override
+ {
+ if (!pCurrent->IsTextNode())
+ return;
+
+ const auto& text = pCurrent->GetTextNode()->GetText();
+
+ // Series of tests to detect if there are fake forms in the text.
+
+ bool bCheck = text.indexOf("___") == -1; // Repeated underscores.
+
+ if (bCheck)
+ bCheck = text.indexOf("....") == -1; // Repeated dots.
+
+ if (bCheck)
+ bCheck = text.indexOf(u"……") == -1; // Repeated ellipsis.
+
+ if (bCheck)
+ bCheck = text.indexOf(u"….") == -1; // A dot after an ellipsis.
+
+ if (bCheck)
+ bCheck = text.indexOf(u".…") == -1; // An ellipsis after a dot.
+
+ // Checking if all the tests are passed successfully. If not, adding a warning.
+ if (!bCheck)
+ lclAddIssue(m_rIssueCollection, SwResId(STR_NON_INTERACTIVE_FORMS));
+ }
+};
+
+/// Check for floating text frames, as it causes problems with reading order.
+class FloatingTextCheck : public NodeCheck
+{
+public:
+ FloatingTextCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : NodeCheck(rIssueCollection)
+ {
+ }
+
+ void check(SwNode* pCurrent) override
+ {
+ // if node is a text-node and if it has text, we proceed. Otherwise - return.
+ const SwTextNode* textNode = pCurrent->GetTextNode();
+ if (!textNode || textNode->GetText().isEmpty())
+ return;
+
+ // If a node is in fly and if it is not anchored as char, throw warning.
+ const SwNode* startFly = pCurrent->FindFlyStartNode();
+ if (startFly
+ && startFly->GetFlyFormat()->GetAnchor().GetAnchorId() != RndStdIds::FLY_AS_CHAR)
+ lclAddIssue(m_rIssueCollection, SwResId(STR_FLOATING_TEXT));
+ }
+};
+
+/// Heading paragraphs (with outline levels > 0) are not allowed in tables
+class TableHeadingCheck : public NodeCheck
+{
+private:
+ // Boolean indicating if heading-in-table warning is already triggered.
+ bool m_bPrevPassed;
+
+public:
+ TableHeadingCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : NodeCheck(rIssueCollection)
+ , m_bPrevPassed(true)
+ {
+ }
+
+ void check(SwNode* pCurrent) override
+ {
+ if (!m_bPrevPassed)
+ return;
+
+ const SwTextNode* textNode = pCurrent->GetTextNode();
+
+ if (textNode && textNode->GetAttrOutlineLevel() != 0)
+ {
+ const SwTableNode* parentTable = pCurrent->FindTableNode();
+
+ if (parentTable)
+ {
+ m_bPrevPassed = false;
+ lclAddIssue(m_rIssueCollection, SwResId(STR_HEADING_IN_TABLE));
+ }
+ }
+ }
+};
+
+/// Checking if headings are ordered correctly.
+class HeadingOrderCheck : public NodeCheck
+{
+public:
+ HeadingOrderCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : NodeCheck(rIssueCollection)
+ {
+ }
+
+ void check(SwNode* pCurrent) override
+ {
+ const SwTextNode* pTextNode = pCurrent->GetTextNode();
+ if (!pTextNode)
+ return;
+
+ // If outline level stands for heading level...
+ const int currentLevel = pTextNode->GetAttrOutlineLevel();
+ if (!currentLevel)
+ return;
+
+ // ... and if is bigger than previous by more than 1, warn.
+ if (currentLevel - m_prevLevel > 1)
+ {
+ // Preparing and posting a warning.
+ OUString resultString = SwResId(STR_HEADING_ORDER);
+ resultString
+ = resultString.replaceAll("%LEVEL_CURRENT%", OUString::number(currentLevel));
+ resultString = resultString.replaceAll("%LEVEL_PREV%", OUString::number(m_prevLevel));
+
+ lclAddIssue(m_rIssueCollection, resultString);
+ }
+
+ // Updating previous level.
+ m_prevLevel = currentLevel;
+ }
+
+private:
+ // Previous heading level to compare with.
+ int m_prevLevel = 0;
+};
+
+class DocumentCheck : public BaseCheck
+{
+public:
+ DocumentCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : BaseCheck(rIssueCollection)
+ {
+ }
+
+ virtual void check(SwDoc* pDoc) = 0;
+};
+
+// Check default language
+class DocumentDefaultLanguageCheck : public DocumentCheck
+{
+public:
+ DocumentDefaultLanguageCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : DocumentCheck(rIssueCollection)
+ {
+ }
+
+ void check(SwDoc* pDoc) override
+ {
+ // TODO maybe - also check RES_CHRATR_CJK_LANGUAGE, RES_CHRATR_CTL_LANGUAGE if CJK or CTL are enabled
+ const SvxLanguageItem& rLang = pDoc->GetDefault(RES_CHRATR_LANGUAGE);
+ LanguageType eLanguage = rLang.GetLanguage();
+ if (eLanguage == LANGUAGE_NONE)
+ {
+ lclAddIssue(m_rIssueCollection, SwResId(STR_DOCUMENT_DEFAULT_LANGUAGE),
+ sfx::AccessibilityIssueID::DOCUMENT_LANGUAGE);
+ }
+ else
+ {
+ for (SwTextFormatColl* pTextFormatCollection : *pDoc->GetTextFormatColls())
+ {
+ const SwAttrSet& rAttrSet = pTextFormatCollection->GetAttrSet();
+ if (rAttrSet.GetLanguage(false).GetLanguage() == LANGUAGE_NONE)
+ {
+ OUString sName = pTextFormatCollection->GetName();
+ OUString sIssueText
+ = SwResId(STR_STYLE_NO_LANGUAGE).replaceAll("%STYLE_NAME%", sName);
+ lclAddIssue(m_rIssueCollection, sIssueText,
+ sfx::AccessibilityIssueID::STYLE_LANGUAGE);
+ }
+ }
+ }
+ }
+};
+
+class DocumentTitleCheck : public DocumentCheck
+{
+public:
+ DocumentTitleCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : DocumentCheck(rIssueCollection)
+ {
+ }
+
+ void check(SwDoc* pDoc) override
+ {
+ SwDocShell* pShell = pDoc->GetDocShell();
+ if (!pShell)
+ return;
+
+ const uno::Reference<document::XDocumentPropertiesSupplier> xDPS(pShell->GetModel(),
+ uno::UNO_QUERY_THROW);
+ const uno::Reference<document::XDocumentProperties> xDocumentProperties(
+ xDPS->getDocumentProperties());
+ OUString sTitle = xDocumentProperties->getTitle();
+ if (sTitle.trim().isEmpty())
+ {
+ lclAddIssue(m_rIssueCollection, SwResId(STR_DOCUMENT_TITLE),
+ sfx::AccessibilityIssueID::DOCUMENT_TITLE);
+ }
+ }
+};
+
+class FootnoteEndnoteCheck : public DocumentCheck
+{
+public:
+ FootnoteEndnoteCheck(sfx::AccessibilityIssueCollection& rIssueCollection)
+ : DocumentCheck(rIssueCollection)
+ {
+ }
+
+ void check(SwDoc* pDoc) override
+ {
+ for (SwTextFootnote const* pTextFootnote : pDoc->GetFootnoteIdxs())
+ {
+ SwFormatFootnote const& rFootnote = pTextFootnote->GetFootnote();
+ if (rFootnote.IsEndNote())
+ {
+ lclAddIssue(m_rIssueCollection, SwResId(STR_AVOID_ENDNOTES));
+ }
+ else
+ {
+ lclAddIssue(m_rIssueCollection, SwResId(STR_AVOID_FOOTNOTES));
+ }
+ }
+ }
+};
+
+} // end anonymous namespace
+
+// Check Shapes, TextBox
+void AccessibilityCheck::checkObject(SdrObject* pObject)
+{
+ if (!pObject)
+ return;
+
+ // Check for fontworks.
+ if (SdrObjCustomShape* pCustomShape = dynamic_cast<SdrObjCustomShape*>(pObject))
+ {
+ const SdrCustomShapeGeometryItem& rGeometryItem
+ = pCustomShape->GetMergedItem(SDRATTR_CUSTOMSHAPE_GEOMETRY);
+
+ if (const uno::Any* pAny = rGeometryItem.GetPropertyValueByName("Type"))
+ if (pAny->get<OUString>().startsWith("fontwork-"))
+ lclAddIssue(m_aIssueCollection, SwResId(STR_FONTWORKS));
+ }
+
+ // Checking if there is floating Writer text draw object and if so, throwing a warning.
+ // (Floating objects with text create problems with reading order)
+ if (pObject->HasText()
+ && FindFrameFormat(pObject)->GetAnchor().GetAnchorId() != RndStdIds::FLY_AS_CHAR)
+ lclAddIssue(m_aIssueCollection, SwResId(STR_FLOATING_TEXT));
+
+ if (pObject->GetObjIdentifier() == SdrObjKind::CustomShape
+ || pObject->GetObjIdentifier() == SdrObjKind::Text)
+ {
+ OUString sAlternative = pObject->GetTitle();
+ if (sAlternative.isEmpty())
+ {
+ OUString sName = pObject->GetName();
+ OUString sIssueText = SwResId(STR_NO_ALT).replaceAll("%OBJECT_NAME%", sName);
+ lclAddIssue(m_aIssueCollection, sIssueText, sfx::AccessibilityIssueID::NO_ALT_SHAPE);
+ }
+ }
+}
+
+void AccessibilityCheck::check()
+{
+ if (m_pDoc == nullptr)
+ return;
+
+ std::vector<std::unique_ptr<DocumentCheck>> aDocumentChecks;
+ aDocumentChecks.push_back(std::make_unique<DocumentDefaultLanguageCheck>(m_aIssueCollection));
+ aDocumentChecks.push_back(std::make_unique<DocumentTitleCheck>(m_aIssueCollection));
+ aDocumentChecks.push_back(std::make_unique<FootnoteEndnoteCheck>(m_aIssueCollection));
+
+ for (std::unique_ptr<DocumentCheck>& rpDocumentCheck : aDocumentChecks)
+ {
+ rpDocumentCheck->check(m_pDoc);
+ }
+
+ std::vector<std::unique_ptr<NodeCheck>> aNodeChecks;
+ aNodeChecks.push_back(std::make_unique<NoTextNodeAltTextCheck>(m_aIssueCollection));
+ aNodeChecks.push_back(std::make_unique<TableNodeMergeSplitCheck>(m_aIssueCollection));
+ aNodeChecks.push_back(std::make_unique<NumberingCheck>(m_aIssueCollection));
+ aNodeChecks.push_back(std::make_unique<HyperlinkCheck>(m_aIssueCollection));
+ aNodeChecks.push_back(std::make_unique<TextContrastCheck>(m_aIssueCollection));
+ aNodeChecks.push_back(std::make_unique<BlinkingTextCheck>(m_aIssueCollection));
+ aNodeChecks.push_back(std::make_unique<HeaderCheck>(m_aIssueCollection));
+ aNodeChecks.push_back(std::make_unique<TextFormattingCheck>(m_aIssueCollection));
+ aNodeChecks.push_back(std::make_unique<NonInteractiveFormCheck>(m_aIssueCollection));
+ aNodeChecks.push_back(std::make_unique<FloatingTextCheck>(m_aIssueCollection));
+ aNodeChecks.push_back(std::make_unique<TableHeadingCheck>(m_aIssueCollection));
+ aNodeChecks.push_back(std::make_unique<HeadingOrderCheck>(m_aIssueCollection));
+
+ auto const& pNodes = m_pDoc->GetNodes();
+ SwNode* pNode = nullptr;
+ for (SwNodeOffset n(0); n < pNodes.Count(); ++n)
+ {
+ pNode = pNodes[n];
+ if (pNode)
+ {
+ for (std::unique_ptr<NodeCheck>& rpNodeCheck : aNodeChecks)
+ {
+ rpNodeCheck->check(pNode);
+ }
+ }
+ }
+
+ IDocumentDrawModelAccess& rDrawModelAccess = m_pDoc->getIDocumentDrawModelAccess();
+ auto* pModel = rDrawModelAccess.GetDrawModel();
+ for (sal_uInt16 nPage = 0; nPage < pModel->GetPageCount(); ++nPage)
+ {
+ SdrPage* pPage = pModel->GetPage(nPage);
+ for (size_t nObject = 0; nObject < pPage->GetObjCount(); ++nObject)
+ {
+ SdrObject* pObject = pPage->GetObj(nObject);
+ if (pObject)
+ checkObject(pObject);
+ }
+ }
+}
+
+} // end sw namespace
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */