Skip to content
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
45 changes: 45 additions & 0 deletions tools/yaml2x/a11y_guidelines/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Models package for a11y-guidelines.

This package contains models for:
- Base functionality (BaseModel, RelationshipManager)
- Content (Category, Guideline)
- Checks (Check, CheckTool, etc.)
- FAQs (Faq, FaqTag)
- References (WcagSc, InfoRef)
- axe-core integration (AxeRule)
"""
from .base import BaseModel, RelationshipManager
from .content import Category, Guideline
from .check import Check, CheckTool, Condition, Procedure, Implementation, Method
from .faq import Faq, FaqTag
from .reference import WcagSc, InfoRef
from .axe import AxeRule

__all__ = [
# Base
'BaseModel',
'RelationshipManager',

# Content
'Category',
'Guideline',

# Checks
'Check',
'CheckTool',
'Condition',
'Procedure',
'Implementation',
'Method',

# FAQs
'Faq',
'FaqTag',

# References
'WcagSc',
'InfoRef',

# axe-core
'AxeRule',
]
150 changes: 150 additions & 0 deletions tools/yaml2x/a11y_guidelines/models/axe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""Models for axe-core accessibility testing tool."""
import re
from typing import Dict, List, Optional, Any, ClassVar
from dataclasses import dataclass
from .base import BaseModel, RelationshipManager

@dataclass
class AxeMessage:
"""Container for axe-core message translations."""
help: Dict[str, str]
description: Dict[str, str]

class AxeRule(BaseModel):
"""axe-core rule model."""

object_type = "axe_rule"
_instances: Dict[str, 'AxeRule'] = {}

# Class-level metadata
timestamp: Optional[str] = None
version: Optional[str] = None
major_version: Optional[str] = None
deque_url: Optional[str] = None

def __init__(self, rule: Dict[str, Any], messages_ja: Dict[str, Any]):
"""Initialize axe rule.

Args:
rule: Dictionary containing rule data
messages_ja: Dictionary containing Japanese translations
"""
super().__init__(rule['id'])

if self.id in self._instances:
raise ValueError(f'Duplicate rule ID: {self.id}')

# Set message data
if self.id not in messages_ja['rules']:
msg_ja = {
'help': rule['metadata']['help'],
'description': rule['metadata']['description']
}
self.translated = False
else:
msg_ja = messages_ja['rules'][self.id]
self.translated = True

self.message = AxeMessage(
help={
'en': rule['metadata']['help'],
'ja': msg_ja['help']
},
description={
'en': rule['metadata']['description'],
'ja': msg_ja['description']
}
)

# Set relationships
self.has_wcag_sc = False
self.has_guideline = False
rel = RelationshipManager()

# Find and associate WCAG success criteria
wcag_scs = [
tag2sc(tag)
for tag in rule['tags']
if re.match(r'wcag\d{3,}', tag)
]

for sc in wcag_scs:
from .reference import WcagSc
if sc not in WcagSc._instances:
continue
rel.associate_objects(self, WcagSc.get_by_id(sc))
self.has_wcag_sc = True
for guideline in rel.get_related_objects(WcagSc.get_by_id(sc), 'guideline'):
rel.associate_objects(self, guideline)
self.has_guideline = True

AxeRule._instances[self.id] = self

def template_data(self, lang: str) -> Dict[str, Any]:
"""Get template data for axe rule.

Args:
lang: Language code

Returns:
Dictionary with template data
"""
rel = RelationshipManager()
data = {
'id': self.id,
'help': self.message.help,
'description': self.message.description
}

if self.translated:
data['translated'] = True

if self.has_wcag_sc:
from .reference import WcagSc
scs = sorted(
rel.get_related_objects(self, 'wcag_sc'),
key=lambda x: x.sort_key
)
data['scs'] = [sc.template_data() for sc in scs]

if self.has_guideline:
guidelines = sorted(
rel.get_related_objects(self, 'guideline'),
key=lambda x: x.sort_key
)
data['guidelines'] = [
gl.get_category_and_id(lang)
for gl in guidelines
]

return data

@classmethod
def list_all(cls) -> List['AxeRule']:
"""Get all axe rules sorted by relevance and ID."""
sorted_all_rules = sorted(cls._instances, key=lambda x: cls._instances[x].id)
with_guidelines = []
with_sc = []
without_sc = []

for rule_id in sorted_all_rules:
rule = cls._instances[rule_id]
if rule.has_guideline:
with_guidelines.append(rule)
elif rule.has_wcag_sc:
with_sc.append(rule)
else:
without_sc.append(rule)

return with_guidelines + with_sc + without_sc

def tag2sc(tag: str) -> str:
"""Convert axe-core tag to WCAG SC identifier.

Args:
tag: axe-core tag (e.g., 'wcag111')

Returns:
WCAG SC identifier (e.g., '1.1.1')
"""
return re.sub(r'wcag(\d)(\d)(\d+)', r'\1.\2.\3', tag)
141 changes: 141 additions & 0 deletions tools/yaml2x/a11y_guidelines/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""Base model classes and relationship management."""
from typing import Any, Dict, List, Optional, Type, TypeVar

T = TypeVar('T', bound='BaseModel')

class BaseModel:
"""Base class for all models."""

object_type: str = ""
_instances: Dict[str, Any] = {}

def __init__(self, id: str):
"""Initialize base model.

Args:
id: Unique identifier for the model instance
"""
self.id = id
if not hasattr(self.__class__, '_instances'):
self.__class__._instances = {}

@classmethod
def get_by_id(cls: Type[T], id: str) -> Optional[T]:
"""Get model instance by ID.

Args:
id: Instance identifier

Returns:
Model instance if found, None otherwise
"""
return cls._instances.get(id)

class RelationshipManager:
"""Manages relationships between different model objects."""

_instance = None
_initialized = False

def __new__(cls):
if cls._instance is None:
cls._instance = super(RelationshipManager, cls).__new__(cls)
return cls._instance

def __init__(self):
"""Initialize the relationship manager.

Uses singleton pattern to ensure only one instance exists.
"""
if self._initialized:
return
self._data = {}
self._unresolved_faqs = {}
self._initialized = True

def associate_objects(self, obj1: BaseModel, obj2: BaseModel) -> None:
"""Associate two objects bidirectionally.

Args:
obj1: First object to associate
obj2: Second object to associate
"""
obj1_type = obj1.object_type
obj2_type = obj2.object_type
obj1_id = obj1.id
obj2_id = obj2.id

# Create nested dictionaries if they don't exist
for obj_type, obj_id in [(obj1_type, obj1_id), (obj2_type, obj2_id)]:
if obj_type not in self._data:
self._data[obj_type] = {}
if obj_id not in self._data[obj_type]:
self._data[obj_type][obj_id] = {}

# Add bidirectional relationship
for (src_type, src_id, src_obj, dest_type, dest_obj) in [
(obj1_type, obj1_id, obj1, obj2_type, obj2),
(obj2_type, obj2_id, obj2, obj1_type, obj1)
]:
if dest_type not in self._data[src_type][src_id]:
self._data[src_type][src_id][dest_type] = []
if dest_obj not in self._data[src_type][src_id][dest_type]:
self._data[src_type][src_id][dest_type].append(dest_obj)

def add_unresolved_faqs(self, faq1_id: str, faq2_id: str) -> None:
"""Add unresolved FAQ relationship to be resolved later.

Args:
faq1_id: ID of first FAQ
faq2_id: ID of second FAQ
"""
for id1, id2 in [(faq1_id, faq2_id), (faq2_id, faq1_id)]:
if id1 not in self._unresolved_faqs:
self._unresolved_faqs[id1] = []
if id2 not in self._unresolved_faqs[id1]:
self._unresolved_faqs[id1].append(id2)

def resolve_faqs(self) -> None:
"""Resolve all unresolved FAQ relationships."""
from .faq import Faq # Import here to avoid circular imports

for faq_id in self._unresolved_faqs:
for faq2_id in self._unresolved_faqs[faq_id]:
faq1 = Faq.get_by_id(faq_id)
faq2 = Faq.get_by_id(faq2_id)
if faq1 and faq2:
self.associate_objects(faq1, faq2)

def get_related_objects(self, obj: BaseModel, related_type: str) -> List[Any]:
"""Get all related objects of a specific type for an object.

Args:
obj: Source object
related_type: Type of related objects to retrieve

Returns:
List of related objects
"""
try:
return self._data[obj.object_type][obj.id][related_type]
except KeyError:
return []

def get_sorted_related_objects(
self,
obj: BaseModel,
related_type: str,
key: str = 'sort_key'
) -> List[Any]:
"""Get sorted related objects of a specific type.

Args:
obj: Source object
related_type: Type of related objects to get
key: Key to sort by (defaults to 'sort_key')

Returns:
Sorted list of related objects
"""
objects = self.get_related_objects(obj, related_type)
return sorted(objects, key=lambda x: getattr(x, key))
Loading