Skip to content

feat: introducing the role mixin feature #70

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

Merged
merged 11 commits into from
May 1, 2024
43 changes: 22 additions & 21 deletions ebu_tt_live/bindings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,24 +580,29 @@ class LiveStyledElementMixin(StyledElementMixin):
# removed from version 1.0 of the specification.

class RoleMixin(object):
_computed_roles = None
_computed_roles = set()

def _semantic_compute_roles(self, dataset):
current_roles = set()
if self.role is not None:
self._computed_roles = self.role
else:
if dataset.get('metadata_roles') is not None and len(dataset['metadata_roles']) > 0:
self._computed_roles = dataset['metadata_roles'][-1]
else:
self._computed_roles = None
current_roles.update(self.role)

if dataset.get('role_stack') and dataset['role_stack']:
current_roles.update(dataset['role_stack'][-1])

self._computed_roles = current_roles

def _semantic_push_computed_roles(self, dataset):
if dataset.get('metadata_roles') is None:
dataset['metadata_roles'] = []
dataset['metadata_roles'].append(self._computed_roles)
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('metadata_roles') is not None and len(dataset['metadata_roles']) > 0:
dataset['metadata_roles'].pop()
if dataset.get('role_stack') and len(dataset['role_stack']) > 0:
dataset['role_stack'].pop()
if dataset.get('metadata_roles') and len(dataset['metadata_roles']) > 0:
self._computed_roles.update(dataset['metadata_roles'])
dataset['metadata_roles'].clear()

@property
def computed_roles(self):
Expand Down Expand Up @@ -2027,22 +2032,18 @@ def _semantic_after_traversal(
raw.d_span_type._SetSupersedingClass(d_span_type)


class d_metadata_type(SemanticValidationMixin, RoleMixin, raw.d_metadata_type):
class d_metadata_type(SemanticValidationMixin, raw.d_metadata_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)
if self.role is not None:
if 'metadata_roles' not in dataset:
dataset['metadata_roles'] = set()
dataset['metadata_roles'].add(self.role)

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

raw.d_metadata_type._SetSupersedingClass(d_metadata_type)

Expand Down
39 changes: 25 additions & 14 deletions ebu_tt_live/bindings/test/test_role_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,49 +13,60 @@ def setUp(self):
'd_body': d_body_type(),
'd_p': d_p_type(),
'd_span': d_span_type(),
'd_metadata': d_metadata_type(),
'div': div_type(),
'br': br_type(),
'body': body_type(),
'p': p_type(),
'span': span_type()
}
self.dataset = {'metadata_roles': []}
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 metadata_roles is empty for different element types."""
"""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.assertIsNone(element.computed_roles, f"Initial computed role should be None for {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."""
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}.")
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 metadata_roles when no role is directly set for different element types."""
"""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['metadata_roles'].append(['caption'])
self.dataset['role_stack'].append({'caption'})
element._semantic_compute_roles(self.dataset)
self.assertEqual(element.computed_roles, ['caption'], f"Computed role should inherit from metadata_roles for {element_name}.")
self.dataset['metadata_roles'].pop()
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 metadata_roles is empty for different element types."""
"""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.assertIsNone(element.computed_roles, f"Computed role should be None when metadata_roles is empty for {element_name}.")
self.assertEqual(element.computed_roles, set(),f"Computed role should be None when role_stack is empty for {element_name}.")

def test_metadata_roles_pop(self):
"""Test proper popping from metadata_roles after traversal for different element types."""
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['metadata_roles']), 0, f"Role stack should be empty after pop for {element_name}.")
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: 2 additions & 0 deletions ebu_tt_live/xsd/ebutt_metadata.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Please note that the EBU-TT XML Schema is a helping document and NOT normative b
xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning" vc:minVersion="1.1">
<xs:import namespace="urn:ebu:tt:datatypes" schemaLocation="ebutt_datatypes.xsd"/>
<xs:import namespace="http://www.w3.org/ns/ttml#metadata" schemaLocation="metadata.xsd"/>
<xs:attribute name="role" type="xs:NMTOKENS"/>
<xs:complexType name="binaryData_type">
<xs:annotation>
<xs:documentation>Binary data of the input formats or associated documents used to
Expand Down Expand Up @@ -561,6 +562,7 @@ Please note that the EBU-TT XML Schema is a helping document and NOT normative b
<xs:element ref="ttm:title" minOccurs="0"/>
<xs:element ref="ttm:desc" minOccurs="0"/>
</xs:sequence>
<xs:attribute ref="ttm:role"/>
</xs:complexType>

<xs:complexType name="headMetadata_type">
Expand Down
15 changes: 14 additions & 1 deletion testing/bdd/features/metadata/rolemixin_feature.feature
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,18 @@ Feature: RoleMixin role assignment functionality
| xml_file | elem_id | computed_value |
| role_inherited_content.xml | d1 | caption |
| role_inherited_content.xml | d2 | caption |
| role_inherited_content.xml | p1 | sound |
| role_inherited_content.xml | p1 | caption, sound |
| role_inherited_content.xml | d5 | caption |

Scenario Outline: If a role inherited properly with metadata
Given an xml file <xml_file>
When the document is generated
And the EBU-TT-Live document is valid
Then the role in <elem_id> is <computed_value>

Examples:
| xml_file | elem_id | computed_value |
| role_inherited_content_metadata.xml | d1 | caption, description |
| role_inherited_content_metadata.xml | d2 | caption, description |
| role_inherited_content_metadata.xml | p1 | caption, sound, description |
| role_inherited_content_metadata.xml | d5 | caption, description |
43 changes: 43 additions & 0 deletions testing/bdd/templates/role_inherited_content_metadata.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" ?>
<tt:tt
ebuttp:sequenceIdentifier="TestSequence"
ebuttp:sequenceNumber="1"
ttp:clockMode="local"
ttp:timeBase="clock"
xmlns:ebuttm="urn:ebu:tt:metadata"
xmlns:ebuttp="urn:ebu:tt:parameters"
xmlns:tt="http://www.w3.org/ns/ttml"
xmlns:ttp="http://www.w3.org/ns/ttml#parameter"
xmlns:xml="http://www.w3.org/XML/1998/namespace"
xmlns:f="urn:foreign"
xmlns:tts="http://www.w3.org/ns/ttml#styling"
xmlns:ebutts="urn:ebu:tt:style"
xmlns:ttm="http://www.w3.org/ns/ttml#metadata"
xml:lang="it" >
<tt:head>
<tt:metadata>
<ebuttm:documentMetadata/>
</tt:metadata>
<tt:styling>
<tt:style xml:id="S1" tts:fontSize="1c 2c" ebutts:linePadding="1c" tts:lineHeight="2c" tts:textAlign="center"/>
</tt:styling>
</tt:head>
<tt:body>
<tt:div ttm:role="caption" xml:id="d1" xml:lang="fr" begin="00:00:10" end="00:00:20">
<tt:metadata ttm:role="description"/>

<tt:div xml:id="d2" style="S1" begin="00:00:01" end="00:00:05" >
<tt:p ttm:role="sound" xml:id="p1">
<tt:span>Some descriptive text</tt:span>
</tt:p>
</tt:div>


<tt:div xml:id="d5" style="S1" xml:lang="en-GB" >
<tt:p xml:id="p3">
<tt:span>Another piece of text</tt:span>
</tt:p>
</tt:div>
</tt:div>
</tt:body>
</tt:tt>
3 changes: 2 additions & 1 deletion testing/bdd/test_role.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def then_computed_style_value_is(elem_id, computed_value, test_context):
if computed_value == '':
assert elem.computed_roles is None
else:
assert elem.computed_roles == [computed_value]
expected_roles = set(computed_value.split(", "))
assert elem.computed_roles == expected_roles


Loading