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', 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 > 3 https> <&udp> <=tcp>', '2 < 3 socket > 3 https> <&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