diff options
-rwxr-xr-x | bin/gla11y | 216 | ||||
-rw-r--r-- | config_host.mk.in | 1 | ||||
-rw-r--r-- | configure.ac | 10 | ||||
-rw-r--r-- | solenv/gbuild/TargetLocations.mk | 2 | ||||
-rw-r--r-- | solenv/gbuild/UIConfig.mk | 26 |
5 files changed, 254 insertions, 1 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 diff --git a/config_host.mk.in b/config_host.mk.in index 8ce71275ca5c..f4d7d67a6601 100644 --- a/config_host.mk.in +++ b/config_host.mk.in @@ -460,6 +460,7 @@ export PTHREAD_LIBS=@PTHREAD_LIBS@ export PYTHON_CFLAGS=$(gb_SPACE)@PYTHON_CFLAGS@ export PYTHON_FOR_BUILD=@PYTHON_FOR_BUILD@ export PYTHON_LIBS=$(gb_SPACE)@PYTHON_LIBS@ +export PYTHON_LXML=@PYTHON_LXML@ export PYTHON_VERSION=@PYTHON_VERSION@ export PYTHON_VERSION_MAJOR=@PYTHON_VERSION_MAJOR@ export PYTHON_VERSION_MINOR=@PYTHON_VERSION_MINOR@ diff --git a/configure.ac b/configure.ac index e20e91e7fa42..479968be94b9 100644 --- a/configure.ac +++ b/configure.ac @@ -8148,10 +8148,19 @@ if test $enable_python = system; then fi dnl By now enable_python should be "system", "internal" or "no" +PYTHON_LXML= case $enable_python in system) SYSTEM_PYTHON=TRUE + AC_MSG_CHECKING([for python lxml]) + if $PYTHON -c "import lxml.etree as ET" ; then + PYTHON_LXML=TRUE + AC_MSG_RESULT([yes]) + else + AC_MSG_RESULT([no, will not be able to check UI accessibility]) + fi + dnl Check if the headers really work save_CPPFLAGS="$CPPFLAGS" CPPFLAGS="$CPPFLAGS $PYTHON_CFLAGS" @@ -8213,6 +8222,7 @@ AC_SUBST(DISABLE_PYTHON) AC_SUBST(SYSTEM_PYTHON) AC_SUBST(PYTHON_CFLAGS) AC_SUBST(PYTHON_LIBS) +AC_SUBST(PYTHON_LXML) AC_SUBST(PYTHON_VERSION) AC_SUBST(PYTHON_VERSION_MAJOR) AC_SUBST(PYTHON_VERSION_MINOR) diff --git a/solenv/gbuild/TargetLocations.mk b/solenv/gbuild/TargetLocations.mk index a8310655039a..06ec8bea0f43 100644 --- a/solenv/gbuild/TargetLocations.mk +++ b/solenv/gbuild/TargetLocations.mk @@ -156,6 +156,7 @@ gb_ScpTemplateTarget_get_target = $(abspath $(WORKDIR)/ScpTemplateTarget/$(dir $ gb_SdiTarget_get_target = $(WORKDIR)/SdiTarget/$(1) gb_ThesaurusIndexTarget_get_target = $(WORKDIR)/ThesaurusIndexTarget/$(basename $(1)).idx gb_UIConfig_get_imagelist_target = $(WORKDIR)/UIConfig/$(1).ilst +gb_UIConfig_get_a11yerrors_target = $(WORKDIR)/UIConfig/$(1).a11yerrors gb_UIConfig_get_target = $(WORKDIR)/UIConfig/$(1).done gb_UIImageListTarget_get_target = $(WORKDIR)/UIImageListTarget/$(1).ilst gb_UIMenubarTarget_get_target = $(WORKDIR)/UIMenubarTarget/$(1).xml @@ -280,6 +281,7 @@ $(eval $(call gb_Helper_make_clean_targets,\ CppunitTestFakeExecutable \ CustomTarget \ ExternalProject \ + UIA11YErrorsTarget \ UIConfig \ UIImageListTarget \ UIMenubarTarget \ diff --git a/solenv/gbuild/UIConfig.mk b/solenv/gbuild/UIConfig.mk index 01bfdbaea5b6..fb8762e8a09c 100644 --- a/solenv/gbuild/UIConfig.mk +++ b/solenv/gbuild/UIConfig.mk @@ -94,6 +94,7 @@ endef # * UIConfig/<name> containing all nontranslatable files gb_UIConfig_INSTDIR := $(LIBO_SHARE_FOLDER)/config/soffice.cfg +gb_UIConfig_a11yerrors_COMMAND = $(SRCDIR)/bin/gla11y $(dir $(call gb_UIConfig_get_target,%)).dir : $(if $(wildcard $(dir $@)),,mkdir -p $(dir $@)) @@ -101,7 +102,7 @@ $(dir $(call gb_UIConfig_get_target,%)).dir : $(dir $(call gb_UIConfig_get_target,%))%/.dir : $(if $(wildcard $(dir $@)),,mkdir -p $(dir $@)) -$(call gb_UIConfig_get_target,%) : $(call gb_UIConfig_get_imagelist_target,%) +$(call gb_UIConfig_get_target,%) : $(call gb_UIConfig_get_imagelist_target,%) $(call gb_UIConfig_get_a11yerrors_target,%) $(call gb_Output_announce,$*,$(true),UIC,2) $(call gb_Helper_abbreviate_dirs,\ touch $@ \ @@ -117,6 +118,25 @@ $(call gb_UIConfig_get_clean_target,%) : rm -f $(call gb_UIConfig_get_target,$*) \ ) +define gb_UIConfig_a11yerrors__command +$(call gb_Output_announce,$(2),$(true),UIA,1) +$(call gb_Helper_abbreviate_dirs,\ + $(gb_UIConfig_a11yerrors_COMMAND) -W none $(UIFILE) > $@ +) +endef + +$(call gb_UIConfig_get_a11yerrors_target,%) : $(gb_UIConfig_a11yerrors_COMMAND) +ifeq ($(PYTHON_LXML),TRUE) + $(call gb_UIConfig_a11yerrors__command,$@,$*) +else + touch $@ +endif + +.PHONY : $(call gb_UIA11YErrorsTarget_get_clean_target,%) +$(call gb_UIA11YErrorsTarget_get_clean_target,%) : + $(call gb_Output_announce,$*,$(false),UIA,2) + rm -f $(call gb_UIConfig_get_a11yerrors_target,$*) + gb_UIConfig_get_packagename = UIConfig/$(1) gb_UIConfig_get_packagesetname = UIConfig/$(1) @@ -138,6 +158,7 @@ $(call gb_PackageSet_add_package,$(call gb_UIConfig_get_packagesetname,$(1)),$(c $(call gb_UIConfig_get_target,$(1)) :| $(dir $(call gb_UIConfig_get_target,$(1))).dir $(call gb_UIConfig_get_imagelist_target,$(1)) :| $(dir $(call gb_UIConfig_get_imagelist_target,$(1))).dir +$(call gb_UIConfig_get_a11yerrors_target,$(1)) :| $(dir $(call gb_UIConfig_get_a11yerrors_target,$(1))).dir $(call gb_UIConfig_get_target,$(1)) : $(call gb_PackageSet_get_target,$(call gb_UIConfig_get_packagesetname,$(1))) $(call gb_UIConfig_get_clean_target,$(1)) : $(call gb_PackageSet_get_clean_target,$(call gb_UIConfig_get_packagesetname,$(1))) @@ -168,6 +189,9 @@ $(call gb_UIConfig_get_imagelist_target,$(1)) : UI_IMAGELISTS += $(call gb_UIIma $(call gb_UIConfig_get_imagelist_target,$(1)) : $(call gb_UIImageListTarget_get_target,$(2)) $(call gb_UIConfig_get_clean_target,$(1)) : $(call gb_UIImageListTarget_get_clean_target,$(2)) +$(call gb_UIConfig_get_a11yerrors_target,$(1)) : UIFILE := $(SRCDIR)/$(2).ui +$(call gb_UIConfig_get_clean_target,$(1)) : $(call gb_UIA11YErrorsTarget_get_clean_target,$(2)) + endef gb_UIConfig_ALLFILES:= |