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

feat: introducing the role mixin feature #70

Merged
merged 11 commits into from
May 1, 2024
114 changes: 113 additions & 1 deletion ebu_tt_live/bindings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,35 @@ class LiveStyledElementMixin(StyledElementMixin):
# NOTE: Some of the code below includes handling of SMPTE time base, which was
# removed from version 1.0 of the specification.

class RoleMixin(object):
_computed_roles = set()

def _semantic_compute_roles(self, dataset):
current_roles = set()
if self.role is not None:
AhmericUiCentric marked this conversation as resolved.
Show resolved Hide resolved
current_roles.update(self.role)

if dataset.get('role_stack') and dataset['role_stack']:
AhmericUiCentric marked this conversation as resolved.
Show resolved Hide resolved
current_roles.update(dataset['role_stack'][-1])
nigelmegitt marked this conversation as resolved.
Show resolved Hide resolved

self._computed_roles = current_roles

def _semantic_push_computed_roles(self, dataset):
if dataset.get('role_stack') is None:
dataset['role_stack'] = []
dataset['role_stack'].append(self._computed_roles)

def _semantic_pop_computed_roles(self, dataset):
if dataset.get('role_stack') and len(dataset['role_stack']) > 0:
AhmericUiCentric marked this conversation as resolved.
Show resolved Hide resolved
dataset['role_stack'].pop()
if dataset.get('metadata_roles') and len(dataset['metadata_roles']) > 0:
AhmericUiCentric marked this conversation as resolved.
Show resolved Hide resolved
self._computed_roles.update(dataset['metadata_roles'])
dataset['metadata_roles'].clear()
AhmericUiCentric marked this conversation as resolved.
Show resolved Hide resolved

@property
def computed_roles(self):
return self._computed_roles


class tt_type(SemanticDocumentMixin, raw.tt_type):

Expand Down Expand Up @@ -858,6 +887,7 @@ class p_type(
RegionedElementMixin,
LiveStyledElementMixin,
SubtitleContentContainer,
RoleMixin,
raw.p_type):

_attr_en_pre = {
Expand Down Expand Up @@ -917,6 +947,8 @@ def _semantic_before_traversal(
style_type=style_type,
parent_binding=parent_binding)
self._semantic_push_styles(dataset=dataset)
self._semantic_compute_roles(dataset=dataset)
self._semantic_push_computed_roles(dataset=dataset)

def _semantic_after_traversal(
self,
Expand All @@ -929,6 +961,7 @@ def _semantic_after_traversal(
dataset=dataset, element_content=element_content)
self._semantic_unset_region(dataset=dataset)
self._semantic_pop_styles(dataset=dataset)
self._semantic_pop_computed_roles(dataset=dataset)

def _semantic_before_copy(self, dataset, element_content=None):
self._assert_in_segment(
Expand Down Expand Up @@ -960,6 +993,7 @@ def _semantic_after_subtree_copy(
class span_type(
LiveStyledElementMixin,
SubtitleContentContainer,
RoleMixin,
raw.span_type):

_attr_en_pre = {
Expand Down Expand Up @@ -1015,6 +1049,8 @@ def _semantic_before_traversal(
style_type=style_type,
parent_binding=parent_binding)
self._semantic_push_styles(dataset=dataset)
self._semantic_compute_roles(dataset=dataset)
self._semantic_push_computed_roles(dataset=dataset)

def _semantic_after_traversal(
self,
Expand All @@ -1026,6 +1062,7 @@ def _semantic_after_traversal(
self._semantic_manage_timeline(
dataset=dataset, element_content=element_content)
self._semantic_pop_styles(dataset=dataset)
self._semantic_pop_computed_roles(dataset=dataset)

def _semantic_before_copy(self, dataset, element_content=None):
self._assert_in_segment(
Expand Down Expand Up @@ -1053,14 +1090,29 @@ def _semantic_after_subtree_copy(
raw.span_type._SetSupersedingClass(span_type)


class br_type(SemanticValidationMixin, raw.br_type):
class br_type(SemanticValidationMixin, RoleMixin, raw.br_type):

def __copy__(self):
return br_type()

def content_to_string(self, begin=None, end=None):
return '<br />'

def _semantic_before_traversal(
self,
dataset,
element_content=None,
parent_binding=None):
self._semantic_compute_roles(dataset=dataset)
self._semantic_push_computed_roles(dataset=dataset)

def _semantic_after_traversal(
self,
dataset,
element_content=None,
parent_binding=None):
self._semantic_pop_computed_roles(dataset=dataset)


raw.br_type._SetSupersedingClass(br_type)

Expand All @@ -1072,6 +1124,7 @@ class div_type(
LiveStyledElementMixin,
TimingValidationMixin,
SemanticValidationMixin,
RoleMixin,
raw.div_type):

_attr_en_pre = {
Expand Down Expand Up @@ -1132,6 +1185,8 @@ def _semantic_before_traversal(
defer_font_size=True
)
self._semantic_push_styles(dataset=dataset)
self._semantic_compute_roles(dataset=dataset)
self._semantic_push_computed_roles(dataset=dataset)

def _semantic_after_traversal(
self,
Expand All @@ -1141,6 +1196,7 @@ def _semantic_after_traversal(
self._semantic_postprocess_timing(
dataset=dataset, element_content=element_content)
self._semantic_unset_region(dataset=dataset)
self._semantic_pop_computed_roles(dataset=dataset)

def _semantic_before_copy(self, dataset, element_content=None):
self._assert_in_segment(
Expand Down Expand Up @@ -1176,6 +1232,7 @@ class body_type(
LiveStyledElementMixin,
BodyTimingValidationMixin,
SemanticValidationMixin,
RoleMixin,
raw.body_type):

_attr_en_pre = {
Expand Down Expand Up @@ -1281,6 +1338,8 @@ def _semantic_before_traversal(
defer_font_size=True
)
self._semantic_push_styles(dataset=dataset)
self._semantic_compute_roles(dataset=dataset)
self._semantic_push_computed_roles(dataset=dataset)

def _semantic_after_traversal(
self,
Expand All @@ -1290,6 +1349,7 @@ def _semantic_after_traversal(
self._semantic_postprocess_timing(
dataset=dataset, element_content=element_content)
self._semantic_pop_styles(dataset=dataset)
self._semantic_pop_computed_roles(dataset=dataset)

def _semantic_before_copy(self, dataset, element_content=None):
self._assert_in_segment(
Expand Down Expand Up @@ -1771,6 +1831,7 @@ def add(self, other):
class d_body_type(
StyledElementMixin,
SemanticValidationMixin,
RoleMixin,
raw.d_body_type):

def _semantic_before_traversal(
Expand All @@ -1785,13 +1846,16 @@ def _semantic_before_traversal(
defer_font_size=True
)
self._semantic_push_styles(dataset=dataset)
self._semantic_compute_roles(dataset=dataset)
self._semantic_push_computed_roles(dataset=dataset)

def _semantic_after_traversal(
self,
dataset,
element_content=None,
parent_binding=None):
self._semantic_pop_styles(dataset=dataset)
self._semantic_pop_computed_roles(dataset=dataset)

def _semantic_before_copy(self, dataset, element_content=None):
self._assert_in_segment(
Expand Down Expand Up @@ -1821,6 +1885,7 @@ class d_div_type(
StyledElementMixin,
SemanticValidationMixin,
RegionedElementMixin,
RoleMixin,
raw.d_div_type):

def _semantic_before_traversal(
Expand All @@ -1837,13 +1902,16 @@ def _semantic_before_traversal(
defer_font_size=True
)
self._semantic_push_styles(dataset=dataset)
self._semantic_compute_roles(dataset=dataset)
self._semantic_push_computed_roles(dataset=dataset)

def _semantic_after_traversal(
self,
dataset,
element_content=None,
parent_binding=None):
self._semantic_unset_region(dataset=dataset)
self._semantic_pop_computed_roles(dataset=dataset)

def _semantic_before_copy(self, dataset, element_content=None):
self._assert_in_segment(
Expand All @@ -1869,6 +1937,7 @@ class d_p_type(
StyledElementMixin,
SemanticValidationMixin,
RegionedElementMixin,
RoleMixin,
raw.d_p_type):

_attr_en_pre = {
Expand Down Expand Up @@ -1898,6 +1967,8 @@ def _semantic_before_traversal(
style_type=style_type,
parent_binding=parent_binding)
self._semantic_push_styles(dataset=dataset)
self._semantic_compute_roles(dataset=dataset)
self._semantic_push_computed_roles(dataset=dataset)

def _semantic_after_traversal(
self,
Expand All @@ -1912,6 +1983,7 @@ def _semantic_after_traversal(
element_content=element_content)
self._semantic_pop_styles(dataset=dataset)
self._semantic_validate_ttd_active_areas(dataset=dataset)
self._semantic_pop_computed_roles(dataset=dataset)


d_p_type._compatible_style_type = d_style_type
Expand All @@ -1924,6 +1996,7 @@ class d_span_type(
StyledElementMixin,
SemanticValidationMixin,
RegionedElementMixin,
RoleMixin,
raw.d_span_type):

_attr_en_pre = {
Expand All @@ -1942,6 +2015,8 @@ def _semantic_before_traversal(
parent_binding=None):
self._semantic_preprocess_timing(
dataset=dataset, element_content=element_content)
self._semantic_compute_roles(dataset=dataset)
self._semantic_push_computed_roles(dataset=dataset)

def _semantic_after_traversal(
self,
Expand All @@ -1950,11 +2025,48 @@ def _semantic_after_traversal(
parent_binding=None):
self._semantic_postprocess_timing(
dataset=dataset, element_content=element_content)
self._semantic_pop_computed_roles(dataset=dataset)


d_span_type._compatible_style_type = d_style_type
raw.d_span_type._SetSupersedingClass(d_span_type)


class d_metadata_type(SemanticValidationMixin, raw.d_metadata_type):

def _semantic_before_traversal(
self,
dataset,
element_content=None,
parent_binding=None):
if self.role is not None:
if 'metadata_roles' not in dataset:
dataset['metadata_roles'] = set()
dataset['metadata_roles'].add(self.role)
nigelmegitt marked this conversation as resolved.
Show resolved Hide resolved


raw.d_metadata_type._SetSupersedingClass(d_metadata_type)


class d_br_type(SemanticValidationMixin, RoleMixin, raw.d_br_type):

def _semantic_before_traversal(
self,
dataset,
element_content=None,
parent_binding=None):
self._semantic_compute_roles(dataset=dataset)
self._semantic_push_computed_roles(dataset=dataset)

def _semantic_after_traversal(
self,
dataset,
element_content=None,
parent_binding=None):
self._semantic_pop_computed_roles(dataset=dataset)

raw.d_br_type._SetSupersedingClass(d_br_type)

# EBU TT 1 classes
# ================

Expand Down
72 changes: 72 additions & 0 deletions ebu_tt_live/bindings/test/test_role_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from unittest import TestCase

from ebu_tt_live.bindings import d_div_type, d_br_type, d_body_type, d_p_type, d_span_type, d_metadata_type, div_type, \
br_type, body_type, p_type, span_type


class TestElementRoleMixin(TestCase):

def setUp(self):
self.elements = {
'd_div': d_div_type(),
'd_br': d_br_type(),
'd_body': d_body_type(),
'd_p': d_p_type(),
'd_span': d_span_type(),
'div': div_type(),
'br': br_type(),
'body': body_type(),
'p': p_type(),
'span': span_type()
}
self.dataset = {'role_stack': [], 'metadata_roles': set()}

def test_initial_computed_role(self):
"""Test that computed_role is None when no role is set and role_stack is empty for different element types."""
for element_name, element in self.elements.items():
with self.subTest(element=element_name):
self.assertEqual(element.computed_roles, set(), f"Initial computed role should be None for {element_name}.")

def test_role_propagation(self):
"""Test proper propagation of directly set role to computed_role for different element types."""
AhmericUiCentric marked this conversation as resolved.
Show resolved Hide resolved
for element_name, element in self.elements.items():
with self.subTest(element=element_name):
element.role = 'caption'
element._semantic_compute_roles({})
self.assertEqual(element.computed_roles, {'caption'}, f"Computed role should match the directly set role for {element_name}.")

def test_role_inheritance(self):
"""Test role inheritance from role_stack when no role is directly set for different element types."""
for element_name, element in self.elements.items():
with self.subTest(element=element_name):
self.dataset['role_stack'].append({'caption'})
element._semantic_compute_roles(self.dataset)
self.assertEqual(element.computed_roles, {'caption'}, f"Computed role should inherit from role_stack for {element_name}.")
self.dataset['role_stack'].pop()

def test_role_absence(self):
"""Test that computed_role is None when no role is set and role_stack is empty for different element types."""
for element_name, element in self.elements.items():
with self.subTest(element=element_name):
element._semantic_compute_roles({})
self.assertEqual(element.computed_roles, set(),f"Computed role should be None when role_stack is empty for {element_name}.")

def test_role_stack_pop(self):
"""Test proper popping from role_stack after traversal for different element types."""
for element_name, element in self.elements.items():
with self.subTest(element=element_name):
element._semantic_push_computed_roles(self.dataset)
element._semantic_pop_computed_roles(self.dataset)
self.assertEqual(len(self.dataset['role_stack']), 0, f"Role stack should be empty after pop for {element_name}.")

def test_role_stack_pop_and_merge(self):
"""Test proper popping from role_stack and metadata roles merging after traversal for different element types."""
for element_name, element in self.elements.items():
with self.subTest(element=element_name):
element._semantic_push_computed_roles(self.dataset)
self.dataset['metadata_roles'].update({'description'})
element._semantic_pop_computed_roles(self.dataset)
self.assertEqual(len(self.dataset['role_stack']), 0,
f"Role stack should be empty after pop for {element_name}.")
self.assertIn('description', element.computed_roles,
f"Metadata roles should be merged for {element_name}.")
2 changes: 1 addition & 1 deletion ebu_tt_live/xsd/ebutt_d.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
<xs:any namespace="##other" processContents="lax" minOccurs="0"
maxOccurs="unbounded"/>
</xs:sequence>

<xs:attribute ref="ttm:role"/>
</xs:complexType>
<xs:complexType name="d_styling_type">
<xs:annotation>
Expand Down
Loading
Loading