Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial commit. #1

Merged
merged 1 commit into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.coverage
.project
.pydevproject
.Python
.vscode
*.egg-info
*.pyc
*.swo
*.swp
build/
18 changes: 18 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: check-ast
- id: check-case-conflict
- id: check-merge-conflict
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- id: mixed-line-ending
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.1
hooks:
- id: ruff
args: [ --fix ]
- id: ruff-format
7 changes: 7 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- id: check-chameleon
name: Check Chameleon templates
description: Check for syntax and accessibility misuse in Chameleon templates.
entry: check-chameleon
language: python
types: [text]
files: \.cpt$
7 changes: 7 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Change log
==========

1.0 (unreleased)
----------------

- Initial release.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# pre-commit-check-chameleon
Check chameleon templates for missing HTML attributes via pre-commit
Check for syntax and accessibility misuse in Chameleon templates.
24 changes: 24 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from setuptools import find_packages
from setuptools import setup

version = "1.0.dev0"

setup(
name="pre-commit-check-chameleon",
version=version,
package_dir={"": "src"},
packages=find_packages("src"),
include_package_data=True,
zip_safe=False,
install_requires=[
"lxml",
],
extras_require={
"test": [
"testfixtures",
],
},
entry_points={
"console_scripts": ["check-chameleon = check_chameleon.check_chameleon:main"]
},
)
Empty file added src/check_chameleon/__init__.py
Empty file.
225 changes: 225 additions & 0 deletions src/check_chameleon/check_chameleon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import argparse
import io
import re
import typing

import lxml.etree

NSMAP = {
"xhtml": "http://www.w3.org/1999/xhtml",
"tal": "http://xml.zope.org/namespaces/tal",
}

DOCTYPE_WRAPPER = """<!DOCTYPE html [<!ENTITY nbsp 'no-break space'>
<!ENTITY times 'multiplication sign'>]>
{0}"""

TAL_ATTRIBUTES = "{{{0}}}attributes".format(NSMAP["tal"])
TAL_CONTENT_XPATH = (
"./@tal:content|.//*/@tal:content|" ".//*/@tal:replace|.//tal:block/@replace"
)


def attribute(node, name):
found = node.attrib.get(name)
if found is not None:
return found
tal_attributes = node.attrib.get(TAL_ATTRIBUTES)
if tal_attributes is not None and name in tal_attributes:
for attr in tal_attributes.split(";"):
attr = attr.strip()
key, value = [el.strip() for el in attr.split(None, 1)]
if name == key:
return value
x_ng_attr_attribute = node.attrib.get("x-ng-attr-{0}".format(name))
if x_ng_attr_attribute is not None:
return x_ng_attr_attribute
x_ng_attribute = node.attrib.get("x-ng-{0}".format(name))
if x_ng_attribute is not None:
return x_ng_attribute
return None


class Context:
errors: typing.List[str]
checks = None

def __init__(self, filename: str, a11y_lint_exclude=None):
with open(filename, "r") as stream:
content = stream.read()
self.errors = []
self.node = None
if "<!DOCTYPE" not in content:
content = DOCTYPE_WRAPPER.format(content)
self.lineno_offset = len(DOCTYPE_WRAPPER.splitlines()) - 1
else:
self.lineno_offset = 0
self.filename = filename
self.content = content
self.a11y_lint_exclude = a11y_lint_exclude

@classmethod
def add_check(cls, func):
if cls.checks is None:
cls.checks = []
cls.checks.append(func)
return func

def report(self, node, msg):
self.errors.append(
"{0}:{1} {2}".format(
self.filename, node.sourceline - self.lineno_offset, msg
)
)

def run(self):
try:
self.node = lxml.etree.parse(io.StringIO(self.content)).getroot()
except lxml.etree.XMLSyntaxError as e:
# Line number offset correction.
msg = e.msg
for line_number in re.findall("line ([0-9]+)", msg):
msg = msg.replace(
"line {0}".format(line_number),
"line {0}".format(int(line_number) - self.lineno_offset),
)
self.errors.append("{0}: {1}".format(self.filename, msg))
else:
for check in self.checks:
if self.a11y_lint_exclude is not None:
if self.filename.startswith(self.a11y_lint_exclude):
continue
check(self)
return self.errors


@Context.add_check
def missing_href(context):
for link in context.node.xpath("//xhtml:a|//a", namespaces=NSMAP):
href = attribute(link, "href")
if href is None:
context.report(
link,
"The <a> element is missing the href attribute."
" Without the href attribute an anchor represents"
" a placeholder for where a link might otherwise have"
" been placed and is invisible for screen readers."
" If the <a> is used to create interactive clickable"
" elements, consider using the <button type=“button”>"
" element instead for this.",
)
elif href.strip() == "#":
if attribute(link, "role") == "button":
continue
if attribute(link, "preventDefault"):
continue
context.report(
link,
'The <a> element href attribute should not be a single "#",'
" it can cause the page to scroll back to the top and it adds"
" an entry to the browser history, so it takes an additiona "
" click of the back button to go to the previous page."
" Consider using a <button type=“button”> element to create"
" interactive clickable elements.",
)


@Context.add_check
def missing_alt(context):
for image in context.node.xpath("//xhtml:img|//img", namespaces=NSMAP):
alt = attribute(image, "alt")
if alt is None:
context.report(
image,
"The <img> element requires an alt attribute. The alt"
" attribute provides descriptive information for an image if a"
" user for some reason cannot view it (because of slow"
" connection, an error, or if the user uses a screen reader)."
" If the image is considered decorative, the alt attribute"
" should be left empty, but not removed, so screen readers"
" will ignore the image.",
)


@Context.add_check
def missing_link_content(context):
for link in context.node.xpath("//xhtml:a|//a", namespaces=NSMAP):
if link.xpath(".//text()"):
continue
if link.xpath(".//xhtml:img|.//img", namespaces=NSMAP):
continue
if link.xpath(TAL_CONTENT_XPATH, namespaces=NSMAP):
continue
if attribute(link, "aria-label"):
continue
context.report(
link,
"The <a> element requires descriptive content that help users"
" better understand what they can expect if they click the link."
" An <a> element without descriptive text will only announce the"
" href path to screen reader users. Keep in mind that users of"
" screen readers have trouble distinguishing icons and need"
" descriptive text to understand the context of the <button>."
" Consider adding descriptive content in the form of text, an"
" aria-label attribute or an image.",
)


@Context.add_check
def missing_button_content(context):
for button in context.node.xpath("//xhtml:button|//button", namespaces=NSMAP):
if button.xpath(".//text()"):
continue
if button.xpath(TAL_CONTENT_XPATH, namespaces=NSMAP):
continue
if attribute(button, "aria-label"):
continue
context.report(
button,
"The <button> element requires descriptive text that helps users"
" understand what they can expect when they click it. Keep in mind"
" that users of screen readers have trouble distinguishing icons"
" and need descriptive text to understand the context of the"
" <button>. Consider adding descriptive text in the form of text"
" or an aria-label attribute.",
)


@Context.add_check
def missing_for(context):
for label in context.node.xpath("//xhtml:label|//label", namespaces=NSMAP):
if label.xpath(".//xhtml:input|.//input", namespaces=NSMAP):
continue
if label.xpath(".//xhtml:select|.//select", namespaces=NSMAP):
continue
if label.xpath(".//xhtml:textarea|.//textarea", namespaces=NSMAP):
continue
label_for = attribute(label, "for")
if label_for is None:
context.report(
label,
"The <label> element needs to be explicitly associated with a"
" form control through the use of nesting or the for"
" attribute, whose value needs to correspond to the value of"
" the id attribute of the associated form control element"
" (<input>, <textarea> and <select>).",
)


def main(argv: typing.Optional[typing.Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--a11y-lint-exclude")
parser.add_argument("filenames", nargs="*")
args = parser.parse_args(argv)

errors = []
for filename in args.filenames:
errors += Context(filename, a11y_lint_exclude=args.a11y_lint_exclude).run()
if len(errors):
print("\n".join(errors))
return 1
return 0


if __name__ == "__main__": # pragma: no cover
exit(main())
Empty file.
Loading
Loading