diff --git a/sl_util/sl_util/secure_regex.py b/sl_util/sl_util/secure_regex.py index 77b0b229..bac7aa4d 100644 --- a/sl_util/sl_util/secure_regex.py +++ b/sl_util/sl_util/secure_regex.py @@ -23,3 +23,7 @@ def split(pattern, text, maxsplit=0, options=None): def compile(pattern, options=None): return re2.compile(pattern, options) + + +def search(pattern, string, options=None): + return re2.search(pattern, string, options) diff --git a/sl_util/sl_util/str_utils.py b/sl_util/sl_util/str_utils.py index 0d4cc284..67c8feda 100644 --- a/sl_util/sl_util/str_utils.py +++ b/sl_util/sl_util/str_utils.py @@ -1,7 +1,11 @@ +import html import random import uuid + from word2number import w2n +from sl_util.sl_util import secure_regex as re + def deterministic_uuid(source): if source: @@ -22,5 +26,19 @@ def to_number(input, default_value: int = 0) -> int: except ValueError: return default_value + def truncate(s: str, max_length: int) -> str: - return s[:max_length] if s else s \ No newline at end of file + return s[:max_length] if s else s + + +def remove_html_tags_and_entities(s: str) -> str: + if s is None: + return '' + + pattern_tags = re.compile(r'<\s*/?\s*[a-zA-Z]+.*?>') + no_html = re.sub(pattern_tags, ' ', s).strip() if s else s + + pattern_spaces = re.compile(r'\s+') + no_spaces = re.sub(pattern_spaces, ' ', no_html) if no_html else no_html + + return html.unescape(no_spaces).replace('\xa0', ' ').strip() diff --git a/sl_util/tests/unit/test_secure_regex_wrapper.py b/sl_util/tests/unit/test_secure_regex_wrapper.py index fad975f8..ef1ef85f 100644 --- a/sl_util/tests/unit/test_secure_regex_wrapper.py +++ b/sl_util/tests/unit/test_secure_regex_wrapper.py @@ -54,3 +54,8 @@ def test_find_all(self, expression, value, expected): ]) def test_split(self, expression, value, expected): assert sre.findall(expression, value) == expected + + + def test_search(self): + assert sre.search(r"match\d+.*match\d{1}", "match1 and match2") is not None + assert sre.search(r"match\d+.*match\d{1}", "matchA not found") is None diff --git a/sl_util/tests/unit/test_str_utils.py b/sl_util/tests/unit/test_str_utils.py index 9430afad..d8bdb4b2 100644 --- a/sl_util/tests/unit/test_str_utils.py +++ b/sl_util/tests/unit/test_str_utils.py @@ -1,7 +1,9 @@ -from pytest import mark, param import random from unittest.mock import patch -from sl_util.sl_util.str_utils import deterministic_uuid, to_number + +from pytest import mark, param + +from sl_util.sl_util.str_utils import deterministic_uuid, to_number, remove_html_tags_and_entities class TestStrUtils: @@ -76,3 +78,26 @@ def test_number_conversions_to_alphanumeric(self, source): number2 = to_number(source) # Then we obtain default value 0 assert number1 == number2 == 0 + + @mark.parametrize('source, expected', [ + param('Link', 'Link', id='only link tag'), + param('

This is an AWS component.

', 'This is an AWS component.', id='with nested tags'), + param('

DDBB

Postgres SQL

', 'DDBB Postgres SQL', id='with multiple nested tags'), + param('< p>This is an AWS component.< /p > Link', + 'This is an AWS component. Link', id='with tags and link'), + param('

Void tag', 'Void tag', id='void tag'), + param('IN < http & https', 'IN < http & https', id='with lt and ampersand'), + param('OUT > socket & https', 'OUT > socket & https', id='with gt and ampersand'), + param(' 2 < 3 socket > <&udp> <=tcp>', '2 < 3 socket > <&udp> <=tcp>', id='with non html gt and lt'), + param('No HTML tags here.', 'No HTML tags here.', id='without html tags'), + param('HTML entities <>&£€©', 'HTML entities <>&£€©', id='with html entities'), + param('', '', id='empty string'), + param(None, '', id='null value') + ]) + def test_remove_html_tags_and_entities(self, source, expected): + # GIVEN a string with html tags + # WHEN removing html tags + result = remove_html_tags_and_entities(source) + + # THEN we obtain the expected string + assert result == expected diff --git a/slp_drawio/slp_drawio/drawio_processor.py b/slp_drawio/slp_drawio/drawio_processor.py index 13215c71..c7b7fcbe 100644 --- a/slp_drawio/slp_drawio/drawio_processor.py +++ b/slp_drawio/slp_drawio/drawio_processor.py @@ -14,7 +14,7 @@ class DrawioProcessor(OTMProcessor): Drawio implementation of OTMProcessor """ - def __init__(self, project_id: str, project_name: str, source, mappings: [bytes], diag_type=None): + def __init__(self, project_id: str, project_name: str, source, mappings: list[bytes], diag_type=None): self.project_id = project_id self.project_name = project_name self.source: bytes = \ diff --git a/slp_drawio/slp_drawio/load/diagram_component_loader.py b/slp_drawio/slp_drawio/load/diagram_component_loader.py index 31aa4fee..f1945cbb 100644 --- a/slp_drawio/slp_drawio/load/diagram_component_loader.py +++ b/slp_drawio/slp_drawio/load/diagram_component_loader.py @@ -1,23 +1,11 @@ -from typing import Optional - from otm.otm.entity.representation import RepresentationElement + from slp_drawio.slp_drawio.load.drawio_dict_utils import get_position, get_size, get_mx_cell_components +from slp_drawio.slp_drawio.load.drawio_mxcell_utils import get_cell_name, get_cell_parent_id, get_cell_style from slp_drawio.slp_drawio.load.stencil_extractors import extract_stencil_type from slp_drawio.slp_drawio.objects.diagram_objects import DiagramComponent -def _get_shape_parent_id(mx_cell: dict, mx_cell_components: list[dict]): - return mx_cell.get('parent') \ - if any(item.get('id') == mx_cell.get('parent') for item in mx_cell_components) else None - - -def _get_shape_name(mx_cell: dict) -> Optional[str]: - cell_value = mx_cell.get('value') or mx_cell.get('label') - if cell_value: - return cell_value if len(cell_value) > 1 else f'_{cell_value}' - return None - - class DiagramComponentLoader: def __init__(self, project_id: str, source: dict): @@ -31,9 +19,9 @@ def load(self) -> list[DiagramComponent]: for mx_cell in mx_cell_components: result.append(DiagramComponent( id=mx_cell.get('id'), - name=_get_shape_name(mx_cell), + name=get_cell_name(mx_cell), shape_type=extract_stencil_type(mx_cell), - shape_parent_id=_get_shape_parent_id(mx_cell, mx_cell_components), + shape_parent_id=get_cell_parent_id(mx_cell, mx_cell_components), representations=[self._get_representation_element(mx_cell)] )) @@ -46,5 +34,5 @@ def _get_representation_element(self, mx_cell: dict) -> RepresentationElement: representation=f"{self._project_id}-diagram", position=get_position(mx_cell), size=get_size(mx_cell), - attributes={'style': mx_cell.get('style')} + attributes={'style': get_cell_style(mx_cell)} ) diff --git a/slp_drawio/slp_drawio/load/drawio_mxcell_utils.py b/slp_drawio/slp_drawio/load/drawio_mxcell_utils.py new file mode 100644 index 00000000..e541cba9 --- /dev/null +++ b/slp_drawio/slp_drawio/load/drawio_mxcell_utils.py @@ -0,0 +1,30 @@ +from typing import Optional + +from sl_util.sl_util.str_utils import remove_html_tags_and_entities +from slp_drawio.slp_drawio.parse.drawio_styles_from_html_tags_parser import DrawioStylesFromHtmlTagsParser + + +def get_cell_style(mx_cell: dict) -> str: + cell_value = mx_cell.get('value') or mx_cell.get('label') + return str(mx_cell.get('style')) + _extract_css_from_cell_value(cell_value) + + +def get_cell_parent_id(mx_cell: dict, mx_cell_components: list[dict]): + return mx_cell.get('parent') \ + if any(item.get('id') == mx_cell.get('parent') for item in mx_cell_components) else None + + +def get_cell_name(mx_cell: dict) -> Optional[str]: + cell_value = mx_cell.get('value') or mx_cell.get('label') + if cell_value: + cell_value = remove_html_tags_and_entities(cell_value).strip() + return cell_value if len(cell_value) > 1 else f'_{cell_value}' + return None + + +def _extract_css_from_cell_value(html: Optional[str]) -> str: + if not html: + return "" + parser = DrawioStylesFromHtmlTagsParser() + css_str = ";".join(parser.parse(html)) + return f"{css_str};" if css_str else "" diff --git a/slp_drawio/slp_drawio/parse/drawio_styles_from_html_tags_parser.py b/slp_drawio/slp_drawio/parse/drawio_styles_from_html_tags_parser.py new file mode 100644 index 00000000..0299ee9e --- /dev/null +++ b/slp_drawio/slp_drawio/parse/drawio_styles_from_html_tags_parser.py @@ -0,0 +1,73 @@ +from html.parser import HTMLParser + +DRAWIO_FONT_STYLE_KEY = 'fontStyle' +DRAWIO_FONT_COLOR_KEY = 'fontColor' +DRAWIO_FONT_FAMILY_KEY = 'fontFamily' +DRAWIO_FONT_SIZE_KEY = 'fontSize' + + + +def _sum_drawio_font_styles(styles): + font_style_sum = 0 + result = [] + + for item in styles: + key, value = item.split('=', 1) + key = key.strip() + value = value.strip() + + if key == DRAWIO_FONT_STYLE_KEY: + font_style_sum += int(value) + else: + result.append(f'{key}={value}') + + if font_style_sum: + result.insert(0, f'{DRAWIO_FONT_STYLE_KEY}={font_style_sum}') + + return result + + + +class DrawioStylesFromHtmlTagsParser(HTMLParser): + + + def __init__(self): + super().__init__() + self.styles = [] + + def parse(self, html: str) -> list[str]: + """ + Parses the given HTML string and extracts Drawio-compatible styles. + :param html: The HTML string to parse. + :return: A list of Drawio-compatible style strings. + """ + self.styles = [] + self.feed(html) + return _sum_drawio_font_styles(self.styles) + + def handle_starttag(self, tag, attrs): + """ + Handles an HTML tag and extracts styles. + + Drawio uses specific CSS styles for formatting: + Style fontStyle + Bold 1 + Italic 2 + Underline 4 + Strikethrough 8 + All of them combined: sum of values (e.g., Bold + Italic + Underline + Strikethrough = 15) + """ + if tag == "b": + self.styles.append(f"{DRAWIO_FONT_STYLE_KEY}= 1") + elif tag == "i": + self.styles.append(f"{DRAWIO_FONT_STYLE_KEY}= 2") + elif tag == "u": + self.styles.append(f"{DRAWIO_FONT_STYLE_KEY}= 4") + elif tag == "strike" or tag == "s": + self.styles.append(f"{DRAWIO_FONT_STYLE_KEY}= 8") + elif tag == "font": + attr_dict = dict(attrs) + if "color" in attr_dict: + self.styles.append(f"{DRAWIO_FONT_COLOR_KEY}= {attr_dict['color']}") + if "face" in attr_dict: + self.styles.append(f"{DRAWIO_FONT_FAMILY_KEY}= {attr_dict['face']}") diff --git a/slp_drawio/tests/integration/test_drawio_processor.py b/slp_drawio/tests/integration/test_drawio_processor.py index d5b4744e..d8697944 100644 --- a/slp_drawio/tests/integration/test_drawio_processor.py +++ b/slp_drawio/tests/integration/test_drawio_processor.py @@ -1,6 +1,7 @@ import pytest from pytest import mark, param +from sl_util.sl_util import secure_regex as re from sl_util.sl_util.file_utils import get_byte_data from sl_util.tests.util.file_utils import generate_temporary_file from slp_base import MappingFileNotValidError @@ -8,12 +9,14 @@ from slp_base.slp_base.mapping import MAX_SIZE as MAPPING_MAX_SIZE, MIN_SIZE as MAPPING_MIN_SIZE from slp_drawio import DrawioProcessor from slp_drawio.tests.resources import test_resource_paths +from slp_drawio.tests.resources.test_resource_paths import shape_names_with_html, default_drawio_mapping SAMPLE_ID = 'id' SAMPLE_NAME = 'name' SAMPLE_VALID_DRAWIO_PATH = test_resource_paths.aws_minimal_xml DEFAULT_MAPPING_FILE = get_byte_data(test_resource_paths.default_drawio_mapping) + class TestDrawioProcessor: @mark.parametrize('mappings', [ param([generate_temporary_file(MAPPING_MIN_SIZE - 1), DEFAULT_MAPPING_FILE], id='mapping file too small'), @@ -37,3 +40,57 @@ def test_invalid_mapping_size(self, mappings: list[bytes]): assert 'Mapping files are not valid' == error.value.title assert 'Mapping files are not valid. Invalid size' == error.value.detail assert 'Mapping files are not valid. Invalid size' == error.value.message + + @pytest.mark.parametrize('filepath', [ + pytest.param(shape_names_with_html, id='aws_with_html'), + ]) + def test_handle_html_shape_names(self, filepath: str): + # GIVEN the valid file + file = open(filepath, 'rb') + # AND the default mapping + default_drawio_mapping_file = get_byte_data(default_drawio_mapping) + + # AND the processor + processor = DrawioProcessor('html_names', 'HTML Names', file, [default_drawio_mapping_file]) + + # WHEN we process the file + result = processor.process() + + # THEN the component names are correctly parsed + components = result.components + components.sort(key=lambda c: c.name) + assert len(components) == 10 + assert components[0].name == 'Bold EC2' + assert components[1].name == 'Combined EC2' + assert components[2].name == 'Courier EC2' + assert components[3].name == 'Drawio example with Cell names with HTML' + assert components[4].name == 'Font size 16 EC2' + assert components[5].name == 'Italic EC2' + assert components[6].name == 'Non HTML EC2' + assert components[7].name == 'Red EC2' + assert components[8].name == 'Strikethrough EC2' + assert components[9].name == 'Underline EC2' + + # AND the representation attributes has the style from the html original name + assert 'fontStyle=1;' in components[0].representations[0].attributes['style'] + c1 = components[1].representations[0].attributes['style'] + assert _validate_font_styles(c1, '0', '15') + assert 'fontFamily=Courier New;' in c1 + assert 'fontColor=#ff0000;' in c1 + assert 'fontFamily=Courier New;' in components[2].representations[0].attributes['style'] + assert 'fontSize=16;' in components[4].representations[0].attributes['style'] + c5 = components[5].representations[0].attributes['style'] + assert _validate_font_styles(c5, '0', '2') + assert 'fontStyle=0;' in components[6].representations[0].attributes['style'] + assert 'fontColor=#ff0000;' in components[7].representations[0].attributes['style'] + assert _validate_font_styles(components[8].representations[0].attributes['style'], '0', '8') + assert _validate_font_styles(components[9].representations[0].attributes['style'], '0', '4') + + +def _validate_font_styles(style: str, value1: str, value2: str) -> bool: + """ + Returns true if in the given style string there are exactly two fontStyle + definitions (value1 then value2), with none before, between, or after. + """ + m = re.search(fr"(.*)fontStyle\s*=\s*{value1}(.*)?fontStyle\s*=\s*{value2}(.*)", style) + return m and "fontStyle" not in m.group(1) and "fontStyle" not in m.group(2) and "fontStyle" not in m.group(3) diff --git a/slp_drawio/tests/resources/drawio/drawio_shape_names_with_html.drawio b/slp_drawio/tests/resources/drawio/drawio_shape_names_with_html.drawio new file mode 100644 index 00000000..3a266bf5 --- /dev/null +++ b/slp_drawio/tests/resources/drawio/drawio_shape_names_with_html.drawio @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/slp_drawio/tests/resources/test_resource_paths.py b/slp_drawio/tests/resources/test_resource_paths.py index bd9a4e5c..2d549aad 100644 --- a/slp_drawio/tests/resources/test_resource_paths.py +++ b/slp_drawio/tests/resources/test_resource_paths.py @@ -16,4 +16,5 @@ wrong_root_drawio = f'{drawio}/wrong_root.drawio' not_xml = f'{drawio}/not_xml.drawio' lean_ix_drawio = f'{drawio}/lean_ix.drawio.xml' +shape_names_with_html = f'{drawio}/drawio_shape_names_with_html.drawio' default_drawio_mapping = f'{drawio}/default_drawio_mapping.yaml' diff --git a/slp_drawio/tests/unit/load/test_diagram_component_loader.py b/slp_drawio/tests/unit/load/test_diagram_component_loader.py index 61f91efc..6b1c3c0b 100644 --- a/slp_drawio/tests/unit/load/test_diagram_component_loader.py +++ b/slp_drawio/tests/unit/load/test_diagram_component_loader.py @@ -1,30 +1,12 @@ import json -from typing import Dict, List from unittest.mock import patch -import pytest - from sl_util.sl_util.file_utils import get_byte_data -from slp_drawio.slp_drawio.load import diagram_component_loader from slp_drawio.slp_drawio.load.diagram_component_loader import DiagramComponentLoader from slp_drawio.slp_drawio.load.drawio_dict_utils import get_size, get_position from slp_drawio.tests.resources import test_resource_paths -@pytest.mark.parametrize('mx_cell, components, expected', [ - pytest.param({}, [], None, id="with mxCell without parent None"), - pytest.param({'parent': 1}, [{'id': 1}], 1, id="parent exists in components"), - pytest.param({'parent': 1}, [{'id': 2}], None, id="parent not exists in components"), -]) -def test_get_shape_parent_id(mx_cell: Dict, components: List, expected): - # GIVEN a mx_cell - # WHEN diagram_component_loader::_get_shape_parent_id - parent_id = diagram_component_loader._get_shape_parent_id(mx_cell, components) - - # THEN the parent is as expected - assert parent_id == expected - - class TestDiagramComponentLoader: PROJECT_ID = 'drawio-project' @@ -66,7 +48,7 @@ def test_get_representation_element(self, get_size_wrapper, get_position_wrapper # GIVEN the mx_cell with the following attributes mx_cell = { 'id': 'mx-cell-identifier', - 'style': "spacingLeft=30;fontColor=#232F3E;dashed=0", + 'style': "spacingLeft=30;fontColor=#232F3E;dashed=0;", 'mxGeometry': {'x': '100', 'y': '200', 'height': '10', 'width': '20'} } @@ -82,4 +64,4 @@ def test_get_representation_element(self, get_size_wrapper, get_position_wrapper assert representation_element.representation == f"{self.PROJECT_ID}-diagram" assert representation_element.position == {'x': 100, 'y': 200} assert representation_element.size == {'height': 10, 'width': 20} - assert representation_element.attributes['style'] == "spacingLeft=30;fontColor=#232F3E;dashed=0" + assert representation_element.attributes['style'] == "spacingLeft=30;fontColor=#232F3E;dashed=0;" diff --git a/slp_drawio/tests/unit/load/test_drawio_mxcell_utils.py b/slp_drawio/tests/unit/load/test_drawio_mxcell_utils.py new file mode 100644 index 00000000..fb8ebedf --- /dev/null +++ b/slp_drawio/tests/unit/load/test_drawio_mxcell_utils.py @@ -0,0 +1,76 @@ +from typing import Dict, List + +import pytest + +from slp_drawio.slp_drawio.load.drawio_mxcell_utils import get_cell_parent_id, get_cell_name +from slp_drawio.slp_drawio.load.drawio_mxcell_utils import get_cell_style + + +@pytest.mark.parametrize('mx_cell, components, expected', [ + pytest.param({}, [], None, id="with mxCell without parent None"), + pytest.param({'parent': 1}, [{'id': 1}], 1, id="parent exists in components"), + pytest.param({'parent': 1}, [{'id': 2}], None, id="parent not exists in components"), +]) +def test_get_cell_parent_id(mx_cell: Dict, components: List, expected): + # GIVEN a mx_cell + # WHEN we get the parent id + parent_id = get_cell_parent_id(mx_cell, components) + + # THEN the parent is as expected + assert parent_id == expected + + +@pytest.mark.parametrize('mx_cell, expected', [ + pytest.param({}, None, id="with mxCell without value or label None"), + pytest.param({'value': ''}, None, id="empty value None"), + pytest.param({'label': ''}, None, id="empty label None"), + pytest.param({'value': 'A'}, '_A', id="single character value"), + pytest.param({'label': 'B'}, '_B', id="single character label"), + pytest.param({'value': ' Test Value '}, 'Test Value', id="trailing spaces in value"), + pytest.param({'label': ' Test Value '}, 'Test Value', id="trailing spaces in label"), + pytest.param({'label': '
Bold Label
'}, 'Bold Label', id="HTML label"), + pytest.param({'value': '
Bold Label
'}, 'Bold Label', id="HTML value"), +]) +def test_get_cell_name(mx_cell: Dict, expected): + # GIVEN a mx_cell + # WHEN we get the cell name + cell_name = get_cell_name(mx_cell) + + # THEN the cell name is as expected + assert cell_name == expected + + +@pytest.mark.parametrize('cell_name, expected', [ + pytest.param('Bold Text', 'sketch=0;image;image=data:image/png,iVBORw0KGgoA;fontStyle=0;fontStyle=1;', + id="bold"), + pytest.param('Italic Text', 'sketch=0;image;image=data:image/png,iVBORw0KGgoA;fontStyle=0;fontStyle=2;', + id="italic"), + pytest.param('Underlined Text', 'sketch=0;image;image=data:image/png,iVBORw0KGgoA;fontStyle=0;fontStyle=4;', + id="underline"), + pytest.param('Custom Font', + 'sketch=0;image;image=data:image/png,iVBORw0KGgoA;fontStyle=0;fontStyle=7;fontColor=#fa09bc;fontFamily=Arial;', + id="all combined"), + pytest.param('Plain Text', 'sketch=0;image;image=data:image/png,iVBORw0KGgoA;fontStyle=0;', + id="plain text with no HTML"), + pytest.param('', 'sketch=0;image;image=data:image/png,iVBORw0KGgoA;fontStyle=0;', + id="empty string"), + pytest.param(None, 'sketch=0;image;image=data:image/png,iVBORw0KGgoA;fontStyle=0;', + id="None value") +]) +def test_get_cell_style(cell_name, expected): + # GIVEN a default styles that will be overridden + default_styles = 'sketch=0;image;image=data:image/png,iVBORw0KGgoA;fontStyle=0;' + + # AND a mx_cell with value with HTML + value_mx_cell = {'value': cell_name, 'style': default_styles} + + # AND a mx_cell with label with HTML + label_mx_cell = {'label': cell_name, 'style': default_styles} + + # WHEN we get the font styles + value_font_styles = get_cell_style(value_mx_cell) + label_font_styles = get_cell_style(label_mx_cell) + + # THEN the font styles are as expected + assert value_font_styles == expected + assert label_font_styles == expected diff --git a/slp_drawio/tests/unit/parse/test_drawio_styles_from_html_tags_parser.py b/slp_drawio/tests/unit/parse/test_drawio_styles_from_html_tags_parser.py new file mode 100644 index 00000000..2f50e6b0 --- /dev/null +++ b/slp_drawio/tests/unit/parse/test_drawio_styles_from_html_tags_parser.py @@ -0,0 +1,38 @@ +import pytest + +from slp_drawio.slp_drawio.parse.drawio_styles_from_html_tags_parser import DrawioStylesFromHtmlTagsParser + +TEST_DRAWIO_FONT_SIZE_KEY = 'fontSize' +TEST_DRAWIO_FONT_STYLE_KEY = 'fontStyle' +TEST_DRAWIO_FONT_COLOR_KEY = 'fontColor' +TEST_DRAWIO_FONT_FAMILY_KEY = 'fontFamily' + + +@pytest.mark.parametrize('html,expected', [ + pytest.param('Bold text', [f'{TEST_DRAWIO_FONT_STYLE_KEY}=1'], id="bold"), + pytest.param('Italic Text', [f'{TEST_DRAWIO_FONT_STYLE_KEY}=2'], id="italic"), + pytest.param('Underlined Text', [f'{TEST_DRAWIO_FONT_STYLE_KEY}=4'], id="underline"), + pytest.param('Strikethrough Text', [f'{TEST_DRAWIO_FONT_STYLE_KEY}=8'], id="Strikethrough"), + pytest.param('Combined Text', [f'{TEST_DRAWIO_FONT_STYLE_KEY}=3'], id="bold + italic"), + pytest.param('Combined Text', [f'{TEST_DRAWIO_FONT_STYLE_KEY}=7'], + id="bold + italic + underline"), + pytest.param('Combined Text', [f'{TEST_DRAWIO_FONT_STYLE_KEY}=15'], + id="bold + italic + underline + strikethrough"), + pytest.param('Custom Font', + [f'{TEST_DRAWIO_FONT_COLOR_KEY}=#fb08cb', f'{TEST_DRAWIO_FONT_FAMILY_KEY}=Courier'], + id="font color, face and size"), + pytest.param('' + 'EC2 with HTML', + [f'{TEST_DRAWIO_FONT_STYLE_KEY}=7', f'{TEST_DRAWIO_FONT_COLOR_KEY}=#ffdd00', + f'{TEST_DRAWIO_FONT_FAMILY_KEY}=Courier'], id="all styles combined"), + + pytest.param('Plain Text', [], id="plain text with no HTML"), + pytest.param('', [], id="empty string") +]) +def test_parse_style(html, expected): + # GIVEN the parser + parser = DrawioStylesFromHtmlTagsParser() + # WHEN OldHTMLStyleParser::parse is called + result = parser.parse(html) + # THEN the style is correctly parsed + assert result == expected