From 284197a06f68a93ba2173ccd2e02111da9738673 Mon Sep 17 00:00:00 2001 From: Lukasz Nosko Date: Wed, 4 Dec 2019 20:29:49 +0100 Subject: [PATCH 1/9] adds parsing zendesk article md components --- sdiff/__init__.py | 10 +++-- sdiff/model.py | 38 ++++++++++++++++++ sdiff/parser.py | 48 +++++++++++++++++++++-- tests/test_parser.py | 92 ++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 174 insertions(+), 14 deletions(-) diff --git a/sdiff/__init__.py b/sdiff/__init__.py index 3835f9c..267566d 100644 --- a/sdiff/__init__.py +++ b/sdiff/__init__.py @@ -1,11 +1,13 @@ -from .parser import parse +from typing import Type + +from .parser import parse, BlockLexer, KsBlockLexer from .renderer import TextRenderer from .compare import diff_struct, diff_links # noqa -def diff(md1, md2, renderer=TextRenderer()): - tree1 = parse(md1) - tree2 = parse(md2) +def diff(md1, md2, renderer=TextRenderer(), parser_cls: Type[BlockLexer] = BlockLexer): + tree1 = parse(md1, parser_cls) + tree2 = parse(md2, parser_cls) tree1, tree2, struct_errors = diff_struct(tree1, tree2) # tree1, tree2, links_errors = diff_links(tree1, tree2) diff --git a/sdiff/model.py b/sdiff/model.py index 03060f5..7b0cb81 100644 --- a/sdiff/model.py +++ b/sdiff/model.py @@ -1,5 +1,7 @@ from enum import Enum +import typing + class Symbols(Enum): null = '' @@ -14,6 +16,12 @@ class Symbols(Enum): new_line = 'n' +class KsSymbols(Enum): + steps = 'S' + tabs = 'T' + callout = 'C' + + class Node(object): symbol = Symbols.null.value name = '' @@ -148,6 +156,36 @@ def original(self, renderer): return renderer.render_node(self, self.text) +class KsSteps(Node): + symbol = KsSymbols.steps.value + name = 'steps' + + +class KsTabs(Node): + symbol = KsSymbols.tabs.value + name = 'tabs' + + +class KsCallout(Node): + symbol = KsSymbols.callout.value + name = 'callout' + + def __init__(self, style: str = None, nodes: typing.List[Node] = None): + super().__init__(nodes) + self.style = style + + def __repr__(self): + return repr({'type': self.name, 'meta': self.meta, 'nodes': self.nodes, 'style': self.style}) + + def __hash__(self): + return hash((self.name, self.style)) + + def __eq__(self, other): + if not isinstance(other, KsCallout): + return False + return self.style == other.style + + class Text(Node): symbol = Symbols.text.value name = 'text' diff --git a/sdiff/parser.py b/sdiff/parser.py index 6a1e1c9..a50480b 100644 --- a/sdiff/parser.py +++ b/sdiff/parser.py @@ -1,3 +1,5 @@ +from typing import Match, Type + import mistune import re @@ -67,6 +69,10 @@ class BlockLexer(mistune.BlockLexer): 'hrule', 'list_block', 'text', ) + @classmethod + def get_lexer(cls): + return cls() + def __init__(self): super().__init__() self.grammar_class.block_html = re.compile( @@ -156,13 +162,49 @@ def _process_list_item(self, cap, bull): loose = _next node = ListItem() - block_lexer = BlockLexer() + block_lexer = self.get_lexer() nodes = block_lexer.parse(item, self.list_rules) node.add_nodes(nodes) result.append(node) return result +class KsBlockLexer(BlockLexer): + TAG_CONTENT_GROUP = 'tag_content' + TAG_PATTERN = r'^\s*(<{tag_name}{attr_re}>(?P<%s>[\s\S]+?))\s*$' % TAG_CONTENT_GROUP + CALLOUT_STYLE_GROUP = 'style' + CALLOUT_ATTR_PATTERN = r'( (?P<%s>green|red|yellow))*' % CALLOUT_STYLE_GROUP + + def __init__(self): + super().__init__() + self.grammar_class.callout = re.compile(self.TAG_PATTERN.format(tag_name='callout', + attr_re=self.CALLOUT_ATTR_PATTERN)) + self.default_rules.insert(0, 'callout') + + self.grammar_class.steps = re.compile(self.TAG_PATTERN.format(tag_name='steps', attr_re='')) + self.default_rules.insert(0, 'steps') + + self.grammar_class.tabs = re.compile(self.TAG_PATTERN.format(tag_name='tabs', attr_re='')) + self.default_rules.insert(0, 'tabs') + + def parse_callout(self, m: Match[str]) -> None: + style = m.group(self.CALLOUT_STYLE_GROUP) + self._parse_nested(KsCallout(style), m) + + def parse_steps(self, m: Match[str]) -> None: + self._parse_nested(KsSteps(), m) + + def parse_tabs(self, m: Match[str]) -> None: + self._parse_nested(KsTabs(), m) + + def _parse_nested(self, node: Node, m: Match[str]) -> None: + nested_content = m.group(self.TAG_CONTENT_GROUP) + nested_nodes = self.get_lexer().parse(nested_content) + node.add_nodes(nested_nodes) + self.tokens.append(node) + + + def _remove_spaces_from_empty_lines(text): return '\n'.join([re.sub(r'^( {1,}|\t{1,})$', '\n', line) for line in text.splitlines()]) @@ -171,9 +213,9 @@ def _remove_ltr_rtl_marks(text): return re.sub(r'(\u200e|\u200f)', '', text) -def parse(text): +def parse(text, lexer_cls: Type[BlockLexer] = BlockLexer): # HACK dirty hack to be consistent with Markdown list_block text = _remove_spaces_from_empty_lines(text) text = _remove_ltr_rtl_marks(text) - block_lexer = BlockLexer() + block_lexer = lexer_cls() return Root(block_lexer.parse(text)) diff --git a/tests/test_parser.py b/tests/test_parser.py index 982e16c..14d0d88 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,13 +1,21 @@ from unittest import TestCase -from sdiff import parser +from sdiff import parser, BlockLexer, KsBlockLexer -class TestParser(TestCase): +class ParserTestCase(TestCase): + def setUp(self) -> None: + super().setUp() + self.parser_cls = BlockLexer - def _run_and_assert(self, data, expected): - actual = parser.parse(data).print_all() + def _run_and_assert(self, data: str, expected: str): + actual = parser.parse(data, lexer_cls=self.parser_cls).print_all() self.assertEqual(expected, actual) + def _parse(self, data: str): + return parser.parse(data, lexer_cls=self.parser_cls) + + +class TestParser(ParserTestCase): def test_empty(self): self._run_and_assert('', '') @@ -36,7 +44,7 @@ def test_exclamation_mark(self): self._run_and_assert('Danke!', 'pt') def test_escape_html(self): - actual = parser.parse('text') + actual = self._parse('text') self.assertEqual('<sub>text</sub>', actual.nodes[0].nodes[0].text) def test_ignore_single_space(self): @@ -49,17 +57,87 @@ def test_space_new_line_saparated_as_single_text(self): self._run_and_assert(' \n', 'xpt') def test_lheading_text(self): - actual = parser.parse('heading\n=============') + actual = self._parse('heading\n=============') self.assertEqual('heading', actual.nodes[0].nodes[0].text) def test_heading_text(self): - actual = parser.parse('###heading') + actual = self._parse('### heading') self.assertEqual('heading', actual.nodes[0].nodes[0].text) def test_link_wrapped_in_text(self): self._run_and_assert('some text [link](url) new text', 'ptat') +class TestZendeskParser(ParserTestCase): + def setUp(self) -> None: + super().setUp() + self.parser_cls = KsBlockLexer + + def test_callout(self): + fixture = """ + + # title + content + + """ + self._run_and_assert(fixture, 'C1tpt') + + def test_callout_style(self): + fixture = """ + + # title + content + + """ + actual = self._parse(fixture) + self.assertEqual(actual.nodes[0].style, 'green') + import logging + logging.debug('%s', actual.nodes[0].nodes) + assert False + + def test_callout_invalid_style(self): + fixture = """ + + # title + content + + """ + actual = self._parse(fixture) + self.assertNotEqual(actual.nodes[0].name, 'callout') + + def test_tabs(self): + fixture = """ + + # title 1 + content 1 + # title 2 + content 2 + + """ + self._run_and_assert(fixture, 'T1tpt1tpt') + + def test_steps(self): + steps_fixture = """ + + 1. one + 2. two + 3. tri + + """ + with self.subTest('happy path'): + self._run_and_assert(steps_fixture, 'Slmtmtmt') + with self.subTest('nested in tabs'): + fixture = """ + + # title 1 + content 1 + # title 2 + %s + + """ % steps_fixture + self._run_and_assert(fixture, 'T1tpt1tSlmtmtmt') + + class TestReplaceLines(TestCase): def test_single_empty_line(self): From 536440ea28974f36febd04baf44943fd8675b44a Mon Sep 17 00:00:00 2001 From: Lukasz Nosko Date: Wed, 4 Dec 2019 20:34:33 +0100 Subject: [PATCH 2/9] rename refactor --- sdiff/__init__.py | 2 +- sdiff/model.py | 62 ++++++++++++++++++++++---------------------- sdiff/parser.py | 9 +++---- tests/test_parser.py | 7 ++--- 4 files changed, 38 insertions(+), 42 deletions(-) diff --git a/sdiff/__init__.py b/sdiff/__init__.py index 267566d..bdd845a 100644 --- a/sdiff/__init__.py +++ b/sdiff/__init__.py @@ -1,6 +1,6 @@ from typing import Type -from .parser import parse, BlockLexer, KsBlockLexer +from .parser import parse, BlockLexer, ZendeskArtBlockLexer from .renderer import TextRenderer from .compare import diff_struct, diff_links # noqa diff --git a/sdiff/model.py b/sdiff/model.py index 7b0cb81..a667019 100644 --- a/sdiff/model.py +++ b/sdiff/model.py @@ -16,7 +16,7 @@ class Symbols(Enum): new_line = 'n' -class KsSymbols(Enum): +class ZendeskArtSymbols(Enum): steps = 'S' tabs = 'T' callout = 'C' @@ -156,36 +156,6 @@ def original(self, renderer): return renderer.render_node(self, self.text) -class KsSteps(Node): - symbol = KsSymbols.steps.value - name = 'steps' - - -class KsTabs(Node): - symbol = KsSymbols.tabs.value - name = 'tabs' - - -class KsCallout(Node): - symbol = KsSymbols.callout.value - name = 'callout' - - def __init__(self, style: str = None, nodes: typing.List[Node] = None): - super().__init__(nodes) - self.style = style - - def __repr__(self): - return repr({'type': self.name, 'meta': self.meta, 'nodes': self.nodes, 'style': self.style}) - - def __hash__(self): - return hash((self.name, self.style)) - - def __eq__(self, other): - if not isinstance(other, KsCallout): - return False - return self.style == other.style - - class Text(Node): symbol = Symbols.text.value name = 'text' @@ -240,3 +210,33 @@ def __repr__(self): def original(self, renderer): return renderer.render_node(self, u' \u00B6\n') + + +class ZendeskArtSteps(Node): + symbol = ZendeskArtSymbols.steps.value + name = 'steps' + + +class ZendeskArtTabs(Node): + symbol = ZendeskArtSymbols.tabs.value + name = 'tabs' + + +class ZendeskArtCallout(Node): + symbol = ZendeskArtSymbols.callout.value + name = 'callout' + + def __init__(self, style: str = None, nodes: typing.List[Node] = None): + super().__init__(nodes) + self.style = style + + def __repr__(self): + return repr({'type': self.name, 'meta': self.meta, 'nodes': self.nodes, 'style': self.style}) + + def __hash__(self): + return hash((self.name, self.style)) + + def __eq__(self, other): + if not isinstance(other, ZendeskArtCallout): + return False + return self.style == other.style diff --git a/sdiff/parser.py b/sdiff/parser.py index a50480b..fd6014b 100644 --- a/sdiff/parser.py +++ b/sdiff/parser.py @@ -169,7 +169,7 @@ def _process_list_item(self, cap, bull): return result -class KsBlockLexer(BlockLexer): +class ZendeskArtBlockLexer(BlockLexer): TAG_CONTENT_GROUP = 'tag_content' TAG_PATTERN = r'^\s*(<{tag_name}{attr_re}>(?P<%s>[\s\S]+?))\s*$' % TAG_CONTENT_GROUP CALLOUT_STYLE_GROUP = 'style' @@ -189,13 +189,13 @@ def __init__(self): def parse_callout(self, m: Match[str]) -> None: style = m.group(self.CALLOUT_STYLE_GROUP) - self._parse_nested(KsCallout(style), m) + self._parse_nested(ZendeskArtCallout(style), m) def parse_steps(self, m: Match[str]) -> None: - self._parse_nested(KsSteps(), m) + self._parse_nested(ZendeskArtSteps(), m) def parse_tabs(self, m: Match[str]) -> None: - self._parse_nested(KsTabs(), m) + self._parse_nested(ZendeskArtTabs(), m) def _parse_nested(self, node: Node, m: Match[str]) -> None: nested_content = m.group(self.TAG_CONTENT_GROUP) @@ -204,7 +204,6 @@ def _parse_nested(self, node: Node, m: Match[str]) -> None: self.tokens.append(node) - def _remove_spaces_from_empty_lines(text): return '\n'.join([re.sub(r'^( {1,}|\t{1,})$', '\n', line) for line in text.splitlines()]) diff --git a/tests/test_parser.py b/tests/test_parser.py index 14d0d88..b5d2d3d 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,5 +1,5 @@ from unittest import TestCase -from sdiff import parser, BlockLexer, KsBlockLexer +from sdiff import parser, BlockLexer, ZendeskArtBlockLexer class ParserTestCase(TestCase): @@ -71,7 +71,7 @@ def test_link_wrapped_in_text(self): class TestZendeskParser(ParserTestCase): def setUp(self) -> None: super().setUp() - self.parser_cls = KsBlockLexer + self.parser_cls = ZendeskArtBlockLexer def test_callout(self): fixture = """ @@ -91,9 +91,6 @@ def test_callout_style(self): """ actual = self._parse(fixture) self.assertEqual(actual.nodes[0].style, 'green') - import logging - logging.debug('%s', actual.nodes[0].nodes) - assert False def test_callout_invalid_style(self): fixture = """ From 66f95b26ca24b5bf50482d5cd19e33967961d437 Mon Sep 17 00:00:00 2001 From: Lukasz Nosko Date: Thu, 5 Dec 2019 17:04:28 +0100 Subject: [PATCH 3/9] adds zendesk components rendering --- sdiff/model.py | 36 +++++++++++++++++++++++++++++++++--- sdiff/renderer.py | 9 ++++++--- tests/fixtures/trees.py | 41 +++++++++++++++++++++++++++++++++++++++++ tests/test_renderer.py | 16 ++++++++++++++++ 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/sdiff/model.py b/sdiff/model.py index a667019..d79c4d2 100644 --- a/sdiff/model.py +++ b/sdiff/model.py @@ -1,6 +1,11 @@ +from abc import ABC from enum import Enum import typing +from typing import Union + +if typing.TYPE_CHECKING: + from sdiff.renderer import HtmlRenderer, TextRenderer class Symbols(Enum): @@ -212,17 +217,35 @@ def original(self, renderer): return renderer.render_node(self, u' \u00B6\n') -class ZendeskArtSteps(Node): +class ZendeskArtNode(Node, ABC): + def wrap(self, content: str) -> str: + return f'<{self.name}>\n\n{content}\n' + + def original(self, renderer: Union['HtmlRenderer', 'TextRenderer']) -> str: + nested_content = ''.join(node.original(renderer) for node in self.nodes) + result = self.wrap(nested_content) + return renderer.render_node(self, result) + + +class ZendeskArtSteps(ZendeskArtNode): symbol = ZendeskArtSymbols.steps.value name = 'steps' + def wrap(self, content: str) -> str: + return f'<{self.name}>\n\n{content}\n' -class ZendeskArtTabs(Node): + def original(self, renderer: Union['HtmlRenderer', 'TextRenderer']) -> str: + nested_content = ''.join(node.original(renderer) for node in self.nodes) + result = self.wrap(nested_content) + return renderer.render_node(self, result) + + +class ZendeskArtTabs(ZendeskArtNode): symbol = ZendeskArtSymbols.tabs.value name = 'tabs' -class ZendeskArtCallout(Node): +class ZendeskArtCallout(ZendeskArtNode): symbol = ZendeskArtSymbols.callout.value name = 'callout' @@ -240,3 +263,10 @@ def __eq__(self, other): if not isinstance(other, ZendeskArtCallout): return False return self.style == other.style + + def wrap(self, content: str) -> str: + if self.style: + attr = f' {self.style}' + else: + attr = '' + return f'<{self.name}{attr}>\n\n{content}\n' diff --git a/sdiff/renderer.py b/sdiff/renderer.py index b27e260..4adba59 100644 --- a/sdiff/renderer.py +++ b/sdiff/renderer.py @@ -1,6 +1,9 @@ +from sdiff.model import Root, Node + + class HtmlRenderer(object): - def render(self, tree): + def render(self, tree: Root): result = tree.original(self) return '
\n%s\n
' % result.strip() @@ -14,9 +17,9 @@ def render_node(self, node, text): class TextRenderer(object): - def render(self, tree): + def render(self, tree: Root): result = tree.original(self) return result.strip() - def render_node(self, node, text): + def render_node(self, node: Node, text): return text diff --git a/tests/fixtures/trees.py b/tests/fixtures/trees.py index 0188a62..0efe333 100644 --- a/tests/fixtures/trees.py +++ b/tests/fixtures/trees.py @@ -93,3 +93,44 @@ def pta2t(): Text('heading') ]) ]) + + +def Slmtmt(): # noqa + return Root([ + ZendeskArtSteps([ + List(True, [ + ListItem([ + Text('one') + ]), + ListItem([ + Text('two') + ]) + ]) + ]) + ]) + + +def T1tpt(): # noqa + return Root([ + ZendeskArtTabs([ + Header(1, [ + Text('tab title') + ]), + Paragraph([ + Text('tab content') + ]) + ]) + ]) + + +def C1tpt(style=None): # noqa + return Root([ + ZendeskArtCallout(style, [ + Header(1, [ + Text('callout title') + ]), + Paragraph([ + Text('callout content') + ]) + ]) + ]) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 33e0d3d..4668b0a 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -22,6 +22,22 @@ def test_several_elements(self): actual = self.renderer.render(trees.pta2t()) self.assertEqual('test link\n\n##heading', actual) + def test_zendesk_steps(self): + actual = self.renderer.render(trees.Slmtmt()) + self.assertEqual('\n\n0. one\n1. two\n\n\n', actual) + + def test_zendesk_tabs(self): + actual = self.renderer.render(trees.T1tpt()) + self.assertEqual('\n\n#tab title\n\ntab content\n\n', actual) + + def test_zendesk_callout(self): + actual = self.renderer.render(trees.C1tpt()) + self.assertEqual('\n\n#callout title\n\ncallout content\n\n', actual) + + def test_zendesk_callout_styled(self): + actual = self.renderer.render(trees.C1tpt(style='awesome')) + self.assertEqual('\n\n#callout title\n\ncallout content\n\n', actual) + class TestHtmlRenderer(TestCase): From 7a8797f6884c44829f8493ab71d6cf21cc96a922 Mon Sep 17 00:00:00 2001 From: Lukasz Nosko Date: Fri, 6 Dec 2019 15:40:08 +0100 Subject: [PATCH 4/9] fix list comparison sdiff doesn't point the difference between ordered list vs. unordered list --- sdiff/model.py | 10 +++++++++- tests/fixtures/trees.py | 4 ++-- tests/test_compare.py | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/sdiff/model.py b/sdiff/model.py index d79c4d2..b31a0af 100644 --- a/sdiff/model.py +++ b/sdiff/model.py @@ -32,7 +32,7 @@ class Node(object): name = '' def __init__(self, nodes=None): - self.nodes = nodes or [] + self.nodes: typing.List[Node] = nodes or [] self.meta = {} def __str__(self): @@ -123,6 +123,14 @@ def __init__(self, ordered, nodes=None): super().__init__(nodes) self.ordered = ordered + def __hash__(self): + return hash((self.symbol, self.ordered)) + + def __eq__(self, other): + if not isinstance(other, List): + return False + return self.ordered == other.ordered + def __repr__(self): return repr({'type': self.name, 'meta': self.meta, 'nodes': self.nodes, 'ordered': self.ordered}) diff --git a/tests/fixtures/trees.py b/tests/fixtures/trees.py index 0efe333..3e525cd 100644 --- a/tests/fixtures/trees.py +++ b/tests/fixtures/trees.py @@ -29,9 +29,9 @@ def r4t(): ]) -def lm2tm2t(): +def lm2tm2t(ordered=False): return Root([ - List([ + List(ordered, [ ListItem([ Header(2, [ Text('dummy text') diff --git a/tests/test_compare.py b/tests/test_compare.py index 9486637..ef7e5f2 100644 --- a/tests/test_compare.py +++ b/tests/test_compare.py @@ -1,6 +1,7 @@ from unittest import TestCase from sdiff.compare import diff_links, diff_struct +from sdiff.model import List from .fixtures import trees @@ -61,3 +62,18 @@ def test_missing_new_line(self): _, _, errors = diff_struct(trees.pt(), trees.ptnt()) self.assertEqual('n', errors[0].node.symbol) self.assertEqual('text', errors[1].node.text) + + def test_different_lists(self): + unordered = trees.lm2tm2t(False) + ordered = trees.lm2tm2t(True) + _, _, errors = diff_struct(unordered, ordered) + + with self.subTest('missing unordered list'): + actual = errors[0].node + self.assertEqual(actual, List(ordered=False)) + self.assertEqual(actual.meta.get('style'), 'del') + with self.subTest('additional ordered list'): + actual = errors[1].node + self.assertEqual(actual, List(ordered=True)) + self.assertEqual(actual.meta.get('style'), 'ins') + From 3a940b0fdb70126ccaa34a2a4fb17805e6946a08 Mon Sep 17 00:00:00 2001 From: Lukasz Nosko Date: Fri, 6 Dec 2019 16:13:57 +0100 Subject: [PATCH 5/9] comparision tests --- tests/test_parser.py | 16 ++++++++++++++++ tests/test_sdiff.py | 12 ++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index b5d2d3d..969b442 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,5 +1,6 @@ from unittest import TestCase from sdiff import parser, BlockLexer, ZendeskArtBlockLexer +from sdiff.model import ZendeskArtSteps class ParserTestCase(TestCase): @@ -134,6 +135,21 @@ def test_steps(self): """ % steps_fixture self._run_and_assert(fixture, 'T1tpt1tSlmtmtmt') + def test_invalid_closing_tag(self): + fixture = """ + + 1. one + + """ + actual = self._parse(fixture) + self.assertNotEqual(actual.nodes[0], ZendeskArtSteps()) + + def test_parses_with_invalid_formatting(self): + fixture = '1. one' + actual = self._parse(fixture) + self.assertEqual(actual.nodes[0], ZendeskArtSteps()) + + class TestReplaceLines(TestCase): diff --git a/tests/test_sdiff.py b/tests/test_sdiff.py index 27e56d9..b39d633 100644 --- a/tests/test_sdiff.py +++ b/tests/test_sdiff.py @@ -4,6 +4,8 @@ import sdiff from pathlib import Path +from sdiff import ZendeskArtBlockLexer + def _load_fixture(*path): return open(os.path.join('tests/fixtures', *path)).read() @@ -22,13 +24,15 @@ def test_same(self): for case in cases: with self.subTest(case=case): path1, path2 = case - _, _, errors = sdiff.diff(_load_fixture('same', path1), _load_fixture('same', path2)) - self.assertEqual([], errors) + _, _, errors = sdiff.diff(_load_fixture('same', path1), _load_fixture('same', path2), + parser_cls=ZendeskArtBlockLexer) + self.assertEqual([], errors, msg=case) def test_different(self): cases = _read_test_files('different') for case in cases: with self.subTest(case=case): path1, path2 = case - _, _, errors = sdiff.diff(_load_fixture('different', path1), _load_fixture('different', path2)) - self.assertNotEqual([], errors) + _, _, errors = sdiff.diff(_load_fixture('different', path1), _load_fixture('different', path2), + parser_cls=ZendeskArtBlockLexer) + self.assertNotEqual([], errors, msg=case) From ec849e8f083eec7dcefee34dfde355dc45422ece Mon Sep 17 00:00:00 2001 From: Lukasz Nosko Date: Fri, 6 Dec 2019 16:17:23 +0100 Subject: [PATCH 6/9] tests fixtures --- .../different/zendesk_callout_in_tabs.de.md | 12 ++++++++++++ .../different/zendesk_callout_in_tabs.en.md | 12 ++++++++++++ tests/fixtures/different/zendesk_steps.de.md | 6 ++++++ tests/fixtures/different/zendesk_steps.en.md | 7 +++++++ tests/fixtures/same/zendesk_steps_in_tabs.de.md | 16 ++++++++++++++++ tests/fixtures/same/zendesk_steps_in_tabs.en.md | 16 ++++++++++++++++ 6 files changed, 69 insertions(+) create mode 100644 tests/fixtures/different/zendesk_callout_in_tabs.de.md create mode 100644 tests/fixtures/different/zendesk_callout_in_tabs.en.md create mode 100644 tests/fixtures/different/zendesk_steps.de.md create mode 100644 tests/fixtures/different/zendesk_steps.en.md create mode 100644 tests/fixtures/same/zendesk_steps_in_tabs.de.md create mode 100644 tests/fixtures/same/zendesk_steps_in_tabs.en.md diff --git a/tests/fixtures/different/zendesk_callout_in_tabs.de.md b/tests/fixtures/different/zendesk_callout_in_tabs.de.md new file mode 100644 index 0000000..d3519f4 --- /dev/null +++ b/tests/fixtures/different/zendesk_callout_in_tabs.de.md @@ -0,0 +1,12 @@ + + +# Hallo + + + +1. ein +2. zwei + + + + \ No newline at end of file diff --git a/tests/fixtures/different/zendesk_callout_in_tabs.en.md b/tests/fixtures/different/zendesk_callout_in_tabs.en.md new file mode 100644 index 0000000..9a15f31 --- /dev/null +++ b/tests/fixtures/different/zendesk_callout_in_tabs.en.md @@ -0,0 +1,12 @@ + + +# Hello + + + +1. one +2. two + + + + \ No newline at end of file diff --git a/tests/fixtures/different/zendesk_steps.de.md b/tests/fixtures/different/zendesk_steps.de.md new file mode 100644 index 0000000..7771550 --- /dev/null +++ b/tests/fixtures/different/zendesk_steps.de.md @@ -0,0 +1,6 @@ + + +* ein +* zwei + + diff --git a/tests/fixtures/different/zendesk_steps.en.md b/tests/fixtures/different/zendesk_steps.en.md new file mode 100644 index 0000000..c9a64d6 --- /dev/null +++ b/tests/fixtures/different/zendesk_steps.en.md @@ -0,0 +1,7 @@ + + +1. one +2. two + + + diff --git a/tests/fixtures/same/zendesk_steps_in_tabs.de.md b/tests/fixtures/same/zendesk_steps_in_tabs.de.md new file mode 100644 index 0000000..14425f2 --- /dev/null +++ b/tests/fixtures/same/zendesk_steps_in_tabs.de.md @@ -0,0 +1,16 @@ + + +# Hallo + + + +1. ein +2. zwei + + + +# Zwei tab + +content + + \ No newline at end of file diff --git a/tests/fixtures/same/zendesk_steps_in_tabs.en.md b/tests/fixtures/same/zendesk_steps_in_tabs.en.md new file mode 100644 index 0000000..66f8b2b --- /dev/null +++ b/tests/fixtures/same/zendesk_steps_in_tabs.en.md @@ -0,0 +1,16 @@ + + +# Steps tab + + + +1. one +2. two + + + +# Second tab + +content + + \ No newline at end of file From a9f35038c40a5f26b7da0f62b397732472f9305c Mon Sep 17 00:00:00 2001 From: Lukasz Nosko Date: Fri, 6 Dec 2019 17:33:43 +0100 Subject: [PATCH 7/9] refactor: more accurate naming --- sdiff/__init__.py | 4 ++-- sdiff/model.py | 10 +++++----- sdiff/parser.py | 14 +++++++------- tests/fixtures/trees.py | 6 +++--- tests/test_parser.py | 16 ++++++++-------- tests/test_sdiff.py | 6 +++--- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/sdiff/__init__.py b/sdiff/__init__.py index bdd845a..85f6849 100644 --- a/sdiff/__init__.py +++ b/sdiff/__init__.py @@ -1,11 +1,11 @@ from typing import Type -from .parser import parse, BlockLexer, ZendeskArtBlockLexer +from .parser import parse, MdParser, ZendeskHelpMdParser from .renderer import TextRenderer from .compare import diff_struct, diff_links # noqa -def diff(md1, md2, renderer=TextRenderer(), parser_cls: Type[BlockLexer] = BlockLexer): +def diff(md1, md2, renderer=TextRenderer(), parser_cls: Type[MdParser] = MdParser): tree1 = parse(md1, parser_cls) tree2 = parse(md2, parser_cls) diff --git a/sdiff/model.py b/sdiff/model.py index b31a0af..040ee12 100644 --- a/sdiff/model.py +++ b/sdiff/model.py @@ -225,7 +225,7 @@ def original(self, renderer): return renderer.render_node(self, u' \u00B6\n') -class ZendeskArtNode(Node, ABC): +class ZendeskHelpNode(Node, ABC): def wrap(self, content: str) -> str: return f'<{self.name}>\n\n{content}\n' @@ -235,7 +235,7 @@ def original(self, renderer: Union['HtmlRenderer', 'TextRenderer']) -> str: return renderer.render_node(self, result) -class ZendeskArtSteps(ZendeskArtNode): +class ZendeskHelpSteps(ZendeskHelpNode): symbol = ZendeskArtSymbols.steps.value name = 'steps' @@ -248,12 +248,12 @@ def original(self, renderer: Union['HtmlRenderer', 'TextRenderer']) -> str: return renderer.render_node(self, result) -class ZendeskArtTabs(ZendeskArtNode): +class ZendeskHelpTabs(ZendeskHelpNode): symbol = ZendeskArtSymbols.tabs.value name = 'tabs' -class ZendeskArtCallout(ZendeskArtNode): +class ZendeskHelpCallout(ZendeskHelpNode): symbol = ZendeskArtSymbols.callout.value name = 'callout' @@ -268,7 +268,7 @@ def __hash__(self): return hash((self.name, self.style)) def __eq__(self, other): - if not isinstance(other, ZendeskArtCallout): + if not isinstance(other, ZendeskHelpCallout): return False return self.style == other.style diff --git a/sdiff/parser.py b/sdiff/parser.py index fd6014b..634a08c 100644 --- a/sdiff/parser.py +++ b/sdiff/parser.py @@ -57,7 +57,7 @@ def parse_text(self, m): self.tokens.append(node) -class BlockLexer(mistune.BlockLexer): +class MdParser(mistune.BlockLexer): default_rules = [ 'newline', 'list_block', 'block_html', 'heading', 'lheading', @@ -169,7 +169,7 @@ def _process_list_item(self, cap, bull): return result -class ZendeskArtBlockLexer(BlockLexer): +class ZendeskHelpMdParser(MdParser): TAG_CONTENT_GROUP = 'tag_content' TAG_PATTERN = r'^\s*(<{tag_name}{attr_re}>(?P<%s>[\s\S]+?))\s*$' % TAG_CONTENT_GROUP CALLOUT_STYLE_GROUP = 'style' @@ -189,13 +189,13 @@ def __init__(self): def parse_callout(self, m: Match[str]) -> None: style = m.group(self.CALLOUT_STYLE_GROUP) - self._parse_nested(ZendeskArtCallout(style), m) + self._parse_nested(ZendeskHelpCallout(style), m) def parse_steps(self, m: Match[str]) -> None: - self._parse_nested(ZendeskArtSteps(), m) + self._parse_nested(ZendeskHelpSteps(), m) def parse_tabs(self, m: Match[str]) -> None: - self._parse_nested(ZendeskArtTabs(), m) + self._parse_nested(ZendeskHelpTabs(), m) def _parse_nested(self, node: Node, m: Match[str]) -> None: nested_content = m.group(self.TAG_CONTENT_GROUP) @@ -212,9 +212,9 @@ def _remove_ltr_rtl_marks(text): return re.sub(r'(\u200e|\u200f)', '', text) -def parse(text, lexer_cls: Type[BlockLexer] = BlockLexer): +def parse(text, parser_cls: Type[MdParser] = MdParser): # HACK dirty hack to be consistent with Markdown list_block text = _remove_spaces_from_empty_lines(text) text = _remove_ltr_rtl_marks(text) - block_lexer = lexer_cls() + block_lexer = parser_cls() return Root(block_lexer.parse(text)) diff --git a/tests/fixtures/trees.py b/tests/fixtures/trees.py index 3e525cd..9fdea43 100644 --- a/tests/fixtures/trees.py +++ b/tests/fixtures/trees.py @@ -97,7 +97,7 @@ def pta2t(): def Slmtmt(): # noqa return Root([ - ZendeskArtSteps([ + ZendeskHelpSteps([ List(True, [ ListItem([ Text('one') @@ -112,7 +112,7 @@ def Slmtmt(): # noqa def T1tpt(): # noqa return Root([ - ZendeskArtTabs([ + ZendeskHelpTabs([ Header(1, [ Text('tab title') ]), @@ -125,7 +125,7 @@ def T1tpt(): # noqa def C1tpt(style=None): # noqa return Root([ - ZendeskArtCallout(style, [ + ZendeskHelpCallout(style, [ Header(1, [ Text('callout title') ]), diff --git a/tests/test_parser.py b/tests/test_parser.py index 969b442..c0fa3ef 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,19 +1,19 @@ from unittest import TestCase -from sdiff import parser, BlockLexer, ZendeskArtBlockLexer -from sdiff.model import ZendeskArtSteps +from sdiff import parser, MdParser, ZendeskHelpMdParser +from sdiff.model import ZendeskHelpSteps class ParserTestCase(TestCase): def setUp(self) -> None: super().setUp() - self.parser_cls = BlockLexer + self.parser_cls = MdParser def _run_and_assert(self, data: str, expected: str): - actual = parser.parse(data, lexer_cls=self.parser_cls).print_all() + actual = parser.parse(data, parser_cls=self.parser_cls).print_all() self.assertEqual(expected, actual) def _parse(self, data: str): - return parser.parse(data, lexer_cls=self.parser_cls) + return parser.parse(data, parser_cls=self.parser_cls) class TestParser(ParserTestCase): @@ -72,7 +72,7 @@ def test_link_wrapped_in_text(self): class TestZendeskParser(ParserTestCase): def setUp(self) -> None: super().setUp() - self.parser_cls = ZendeskArtBlockLexer + self.parser_cls = ZendeskHelpMdParser def test_callout(self): fixture = """ @@ -142,12 +142,12 @@ def test_invalid_closing_tag(self): """ actual = self._parse(fixture) - self.assertNotEqual(actual.nodes[0], ZendeskArtSteps()) + self.assertNotEqual(actual.nodes[0], ZendeskHelpSteps()) def test_parses_with_invalid_formatting(self): fixture = '1. one' actual = self._parse(fixture) - self.assertEqual(actual.nodes[0], ZendeskArtSteps()) + self.assertEqual(actual.nodes[0], ZendeskHelpSteps()) diff --git a/tests/test_sdiff.py b/tests/test_sdiff.py index b39d633..8179659 100644 --- a/tests/test_sdiff.py +++ b/tests/test_sdiff.py @@ -4,7 +4,7 @@ import sdiff from pathlib import Path -from sdiff import ZendeskArtBlockLexer +from sdiff import ZendeskHelpMdParser def _load_fixture(*path): @@ -25,7 +25,7 @@ def test_same(self): with self.subTest(case=case): path1, path2 = case _, _, errors = sdiff.diff(_load_fixture('same', path1), _load_fixture('same', path2), - parser_cls=ZendeskArtBlockLexer) + parser_cls=ZendeskHelpMdParser) self.assertEqual([], errors, msg=case) def test_different(self): @@ -34,5 +34,5 @@ def test_different(self): with self.subTest(case=case): path1, path2 = case _, _, errors = sdiff.diff(_load_fixture('different', path1), _load_fixture('different', path2), - parser_cls=ZendeskArtBlockLexer) + parser_cls=ZendeskHelpMdParser) self.assertNotEqual([], errors, msg=case) From d6bd86bdf1d05eae3e59ad06ed0312c30a633ada Mon Sep 17 00:00:00 2001 From: Lukasz Nosko Date: Fri, 6 Dec 2019 17:41:54 +0100 Subject: [PATCH 8/9] style: fix linting --- sdiff/__init__.py | 2 +- sdiff/model.py | 2 +- tests/test_compare.py | 1 - tests/test_parser.py | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/sdiff/__init__.py b/sdiff/__init__.py index 85f6849..fed9ad1 100644 --- a/sdiff/__init__.py +++ b/sdiff/__init__.py @@ -1,6 +1,6 @@ from typing import Type -from .parser import parse, MdParser, ZendeskHelpMdParser +from .parser import parse, MdParser, ZendeskHelpMdParser # noqa from .renderer import TextRenderer from .compare import diff_struct, diff_links # noqa diff --git a/sdiff/model.py b/sdiff/model.py index 040ee12..492c84b 100644 --- a/sdiff/model.py +++ b/sdiff/model.py @@ -5,7 +5,7 @@ from typing import Union if typing.TYPE_CHECKING: - from sdiff.renderer import HtmlRenderer, TextRenderer + from sdiff.renderer import HtmlRenderer, TextRenderer # noqa class Symbols(Enum): diff --git a/tests/test_compare.py b/tests/test_compare.py index ef7e5f2..f083a2f 100644 --- a/tests/test_compare.py +++ b/tests/test_compare.py @@ -76,4 +76,3 @@ def test_different_lists(self): actual = errors[1].node self.assertEqual(actual, List(ordered=True)) self.assertEqual(actual.meta.get('style'), 'ins') - diff --git a/tests/test_parser.py b/tests/test_parser.py index c0fa3ef..498c070 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -150,7 +150,6 @@ def test_parses_with_invalid_formatting(self): self.assertEqual(actual.nodes[0], ZendeskHelpSteps()) - class TestReplaceLines(TestCase): def test_single_empty_line(self): From b9cd6ed4d98033272b1ccf6a9a7c48dcf779c82c Mon Sep 17 00:00:00 2001 From: Lukasz Nosko Date: Fri, 6 Dec 2019 18:20:14 +0100 Subject: [PATCH 9/9] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 341372d..cc60c1c 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -version = '0.3.0' +version = '0.4.0' def read(f):