summaryrefslogtreecommitdiff
path: root/bin/gla11y
diff options
context:
space:
mode:
authorSamuel Thibault <sthibault@hypra.fr>2018-02-16 13:22:10 +0100
committerThorsten Behrens <Thorsten.Behrens@CIB.de>2018-02-20 22:21:48 +0100
commit226697ae27ef451cad404256e83eef88262f16d1 (patch)
tree30b3361c0bb8338139f3331473688a94318373ef /bin/gla11y
parent88560550021908b7877b7c02b4601b92f97ea7d4 (diff)
Integrate initial version of gla11y tool in the build system
This is part of integrating an accessibility non-regression tool. This adds checks in configure.ac for the presence of python lxml which we will need, and adds support for calling the tool at build time, to check for definite UI errors. For now, that only emits errors for missing or duplicate accessibility relation targets, and senseless relations: a label being mnemonic for several widgets. Change-Id: Idda91b15b9a9e0322d16db33dfac8e03f2aa518c Reviewed-on: https://gerrit.libreoffice.org/49856 Tested-by: Jenkins <ci@libreoffice.org> Reviewed-by: Thorsten Behrens <Thorsten.Behrens@CIB.de>
Diffstat (limited to 'bin/gla11y')
-rwxr-xr-xbin/gla11y216
1 files changed, 216 insertions, 0 deletions
diff --git a/bin/gla11y b/bin/gla11y
new file mode 100755
index 000000000000..d0619133ad0f
--- /dev/null
+++ b/bin/gla11y
@@ -0,0 +1,216 @@
+#!/usr/bin/env python
+# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-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/.
+#
+# This file incorporates work covered by the following license notice:
+#
+# Copyright (c) 2018 Martin Pieuchot
+# Copyright (c) 2018 Samuel Thibault <sthibault@hypra.fr>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+# vim: set shiftwidth=4 softtabstop=4 expandtab:
+
+# Take LibreOffice (glade) .ui files and check for non accessible widgets
+
+from __future__ import print_function
+
+import os
+import sys
+import getopt
+import lxml.etree as ET
+
+progname = os.path.basename(sys.argv[0])
+Werror = False
+Wnone = False
+errors = 0
+warnings = 0
+
+
+def errstr(elm):
+ """
+ Print the line number of the element
+ """
+
+ return str(elm.sourceline)
+
+def err(filename, elm, msg):
+ global errors
+
+ if elm == None:
+ prefix = "%s:" % filename
+ else:
+ prefix = "%s:%s" % (filename, errstr(elm))
+
+ errors += 1
+ msg = "%s ERROR: %s" % (prefix, msg)
+ print(msg.encode('ascii', 'ignore'))
+
+
+def warn(filename, elm, msg):
+ global Werror, Wnone, errors, warnings
+
+ if Wnone:
+ return
+
+ prefix = "%s:%s" % (filename, errstr(elm))
+
+ if Werror:
+ errors += 1
+ else:
+ warnings += 1
+
+ msg = "%s WARNING: %s" % (prefix, msg)
+ print(msg.encode('ascii', 'ignore'))
+
+
+def check_objects(filename, elm, objects, target):
+ """
+ Check that objects contains exactly one object
+ """
+ length = len(list(objects))
+ if length == 0:
+ err(filename, elm, "use of undeclared target '%s'" % target)
+ elif length > 1:
+ err(filename, elm, "sevral targets are named '%s'" % target)
+
+def check_props(filename, root, props):
+ """
+ Check the given list of relation properties
+ """
+ for prop in props:
+ objects = root.iterfind(".//object[@id='%s']" % prop.text)
+ check_objects(filename, prop, objects, prop.text)
+
+def check_rels(filename, root, rels):
+ """
+ Check the given list of relations
+ """
+ for rel in rels:
+ target = rel.attrib['target']
+ targets = root.iterfind(".//object[@id='%s']" % target)
+ check_objects(filename, rel, targets, target)
+
+def check_a11y_relation(filename, root):
+ """
+ Emit an error message if any of the 'object' elements of the XML
+ document represented by `root' doesn't comply with Accessibility
+ rules.
+ """
+
+ for obj in root.iter('object'):
+
+ label_for = obj.findall("accessibility/relation[@type='label-for']")
+ check_rels(filename, root, label_for)
+
+ labelled_by = obj.findall("accessibility/relation[@type='labelled-by']")
+ check_rels(filename, root, labelled_by)
+
+ member_of = obj.findall("accessibility/relation[@type='member-of']")
+ check_rels(filename, root, member_of)
+
+ if obj.attrib['class'] == 'GtkLabel':
+ # Case 0: A 'GtkLabel' must contain one or more "label-for"
+ # pointing to existing elements or...
+ if len(label_for) > 0:
+ continue
+
+ # ...a single "mnemonic_widget"
+ properties = obj.findall("property[@name='mnemonic_widget']")
+ check_props(filename, root, properties)
+ if len(properties) > 1:
+ # It does not make sense for a label to be a mnemonic for
+ # several actions.
+ lines = ', '.join([str(p.sourceline) for p in properties])
+ err(filename, obj, "too many sub-elements"
+ ", expected single <property name='mnemonic_widgets'>"
+ ": lines %s" % lines)
+ continue
+ if len(properties) == 1:
+ continue
+ # TODO: warn that it is a label for nothing
+ continue
+
+ # Case 1: has a <child internal-child="accessible"> sub-element
+ children = obj.findall("child[@internal-child='accessible']")
+ if children:
+ if len(children) > 1:
+ lines = ', '.join([str(c.sourceline) for c in children])
+ err(filename, obj, "too many sub-elements"
+ ", expected single <child internal-child='accessible'>"
+ ": lines %s" % lines)
+ continue
+
+ # Case 2: has an <accessibility> sub-element with a "labelled-by"
+ # <relation> pointing to an existing element.
+ if len(labelled_by) > 0:
+ continue
+
+ # TODO: after a few more checks and false-positives filtering, warn
+ # that this does not have a label
+
+
+def usage():
+ print("%s [-W error|none] [file ...]" % progname,
+ file=sys.stderr)
+ sys.exit(2)
+
+
+def main():
+ global Werror, Wnone, errors
+
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], "pW:")
+ except getopt.GetoptError:
+ usage()
+
+ for o, a in opts:
+ if o == "-W":
+ if a == "error":
+ Werror = True
+ elif a == "none":
+ Wnone = True
+
+ if not args:
+ sys.exit("%s: no input files" % progname)
+
+ for filename in args:
+ try:
+ tree = ET.parse(filename)
+ except ET.ParseError:
+ err(filename, None, "malformatted xml file")
+ except IOError:
+ err(filename, None, "unable to read file")
+
+ try:
+ check_a11y_relation(filename, tree.getroot())
+ except Exception as error:
+ import traceback
+ traceback.print_exc()
+ err(filename, None, "error parsing file")
+
+
+ if errors > 0:
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except KeyboardInterrupt:
+ pass