diff --git a/AUTHORS b/AUTHORS index a7928ea88b..4ec64ba1ef 100644 --- a/AUTHORS +++ b/AUTHORS @@ -116,6 +116,8 @@ Other contributors, listed alphabetically, are: MSDOS session, BC, WDiff * Brian R. Jackson -- Tea lexer * Christian Jann -- ShellSession lexer +* Jonas Camillus Jeppesen -- Line numbers and line highlighting for + RTF-formatter * Dennis Kaarsemaker -- sources.list lexer * Dmitri Kabak -- Inferno Limbo lexer * Igor Kalnitsky -- vhdl lexer diff --git a/pygments/formatters/rtf.py b/pygments/formatters/rtf.py index 9905ca0045..ee0e581553 100644 --- a/pygments/formatters/rtf.py +++ b/pygments/formatters/rtf.py @@ -8,8 +8,10 @@ :license: BSD, see LICENSE for details. """ +from collections import OrderedDict from pygments.formatter import Formatter -from pygments.util import get_int_opt, surrogatepair +from pygments.style import _ansimap +from pygments.util import get_bool_opt, get_int_opt, get_list_opt, surrogatepair __all__ = ['RtfFormatter'] @@ -42,6 +44,59 @@ class RtfFormatter(Formatter): default is 24 half-points, giving a size 12 font. .. versionadded:: 2.0 + + `linenos` + Turn on line numbering (default: ``False``). + + .. versionadded:: 2.18 + + `lineno_fontsize` + Font size for line numbers. Size is specified in half points + (default: `fontsize`). + + .. versionadded:: 2.18 + + `lineno_padding` + Number of spaces between the (inline) line numbers and the + source code (default: ``2``). + + .. versionadded:: 2.18 + + `linenostart` + The line number for the first line (default: ``1``). + + .. versionadded:: 2.18 + + `linenostep` + If set to a number n > 1, only every nth line number is printed. + + .. versionadded:: 2.18 + + `lineno_color` + Color for line numbers specified as a hex triplet, e.g. ``'5e5e5e'``. + Defaults to the style's line number color if it is a hex triplet, + otherwise ansi bright black. + + .. versionadded:: 2.18 + + `hl_lines` + Specify a list of lines to be highlighted, as line numbers separated by + spaces, e.g. ``'3 7 8'``. The line numbers are relative to the input + (i.e. the first line is line 1) unless `hl_linenostart` is set. + + .. versionadded:: 2.18 + + `hl_color` + Color for highlighting the lines specified in `hl_lines`, specified as + a hex triplet (default: style's `highlight_color`). + + .. versionadded:: 2.18 + + `hl_linenostart` + If set to ``True`` line numbers in `hl_lines` are specified + relative to `linenostart` (default ``False``). + + .. versionadded:: 2.18 """ name = 'RTF' aliases = ['rtf'] @@ -62,6 +117,40 @@ def __init__(self, **options): Formatter.__init__(self, **options) self.fontface = options.get('fontface') or '' self.fontsize = get_int_opt(options, 'fontsize', 0) + self.linenos = get_bool_opt(options, 'linenos', False) + self.lineno_fontsize = get_int_opt(options, 'lineno_fontsize', + self.fontsize) + self.lineno_padding = get_int_opt(options, 'lineno_padding', 2) + self.linenostart = abs(get_int_opt(options, 'linenostart', 1)) + self.linenostep = abs(get_int_opt(options, 'linenostep', 1)) + self.hl_linenostart = get_bool_opt(options, 'hl_linenostart', False) + + self.hl_color = options.get('hl_color', '') + if not self.hl_color: + self.hl_color = self.style.highlight_color + + self.hl_lines = [] + for lineno in get_list_opt(options, 'hl_lines', []): + try: + lineno = int(lineno) + if self.hl_linenostart: + lineno = lineno - self.linenostart + 1 + self.hl_lines.append(lineno) + except ValueError: + pass + + self.lineno_color = options.get('lineno_color', '') + if not self.lineno_color: + if self.style.line_number_color == 'inherit': + # style color is the css value 'inherit' + # default to ansi bright-black + self.lineno_color = _ansimap['ansibrightblack'] + else: + # style color is assumed to be a hex triplet as other + # colors in pygments/style.py + self.lineno_color = self.style.line_number_color + + self.color_mapping = self._create_color_mapping() def _escape(self, text): return text.replace('\\', '\\\\') \ @@ -90,43 +179,147 @@ def _escape_text(self, text): # Force surrogate pairs buf.append('{\\u%d}{\\u%d}' % surrogatepair(cn)) - return ''.join(buf).replace('\n', '\\par\n') + return ''.join(buf).replace('\n', '\\par') - def format_unencoded(self, tokensource, outfile): - # rtf 1.8 header - outfile.write('{\\rtf1\\ansi\\uc0\\deff0' - '{\\fonttbl{\\f0\\fmodern\\fprq1\\fcharset0%s;}}' - '{\\colortbl;' % (self.fontface and - ' ' + self._escape(self.fontface) or - '')) - - # convert colors and save them in a mapping to access them later. - color_mapping = {} + @staticmethod + def hex_to_rtf_color(hex_color): + if hex_color[0] == "#": + hex_color = hex_color[1:] + + return '\\red%d\\green%d\\blue%d;' % ( + int(hex_color[0:2], 16), + int(hex_color[2:4], 16), + int(hex_color[4:6], 16) + ) + + def _split_tokens_on_newlines(self, tokensource): + """ + Split tokens containing newline characters into multiple token + each representing a line of the input file. Needed for numbering + lines of e.g. multiline comments. + """ + for ttype, value in tokensource: + if value == '\n': + yield (ttype, value) + elif "\n" in value: + lines = value.split("\n") + for line in lines[:-1]: + yield (ttype, line+"\n") + if lines[-1]: + yield (ttype, lines[-1]) + else: + yield (ttype, value) + + def _create_color_mapping(self): + """ + Create a mapping of style hex colors to index/offset in + the RTF color table. + """ + color_mapping = OrderedDict() offset = 1 + + if self.linenos: + color_mapping[self.lineno_color] = offset + offset += 1 + + if self.hl_lines: + color_mapping[self.hl_color] = offset + offset += 1 + for _, style in self.style: for color in style['color'], style['bgcolor'], style['border']: if color and color not in color_mapping: color_mapping[color] = offset - outfile.write('\\red%d\\green%d\\blue%d;' % ( - int(color[0:2], 16), - int(color[2:4], 16), - int(color[4:6], 16) - )) offset += 1 - outfile.write('}\\f0 ') + + return color_mapping + + @property + def _lineno_template(self): + if self.lineno_fontsize != self.fontsize: + return '{\\fs%s \\cf%s %%s%s}' \ + % (self.lineno_fontsize, + self.color_mapping[self.lineno_color], + " " * self.lineno_padding) + + return '{\\cf%s %%s%s}' \ + % (self.color_mapping[self.lineno_color], + " " * self.lineno_padding) + + @property + def _hl_open_str(self): + return r'{\highlight%s ' % self.color_mapping[self.hl_color] + + @property + def _rtf_header(self): + lines = [] + # rtf 1.8 header + lines.append('{\\rtf1\\ansi\\uc0\\deff0' + '{\\fonttbl{\\f0\\fmodern\\fprq1\\fcharset0%s;}}' + % (self.fontface and ' ' + + self._escape(self.fontface) or '')) + + # color table + lines.append('{\\colortbl;') + for color, _ in self.color_mapping.items(): + lines.append(self.hex_to_rtf_color(color)) + lines.append('}') + + # font and fontsize + lines.append('\\f0') if self.fontsize: - outfile.write('\\fs%d' % self.fontsize) + lines.append('\\fs%d' % self.fontsize) + + # ensure Libre Office Writer imports and renders consecutive + # space characters the same width, needed for line numbering. + # https://bugs.documentfoundation.org/show_bug.cgi?id=144050 + lines.append('\\dntblnsbdb') + + return lines + + def format_unencoded(self, tokensource, outfile): + for line in self._rtf_header: + outfile.write(line + "\n") + + tokensource = self._split_tokens_on_newlines(tokensource) + + # first pass of tokens to count lines, needed for line numbering + if self.linenos: + line_count = 0 + tokens = [] # for copying the token source generator + for ttype, value in tokensource: + tokens.append((ttype, value)) + if value.endswith("\n"): + line_count += 1 + + # width of line number strings (for padding with spaces) + linenos_width = len(str(line_count+self.linenostart-1)) + + tokensource = tokens # highlight stream + lineno = 1 + start_new_line = True for ttype, value in tokensource: + if start_new_line and lineno in self.hl_lines: + outfile.write(self._hl_open_str) + + if start_new_line and self.linenos: + if (lineno-self.linenostart+1)%self.linenostep == 0: + current_lineno = lineno + self.linenostart - 1 + lineno_str = str(current_lineno).rjust(linenos_width) + else: + lineno_str = "".rjust(linenos_width) + outfile.write(self._lineno_template % lineno_str) + while not self.style.styles_token(ttype) and ttype.parent: ttype = ttype.parent style = self.style.style_for_token(ttype) buf = [] if style['bgcolor']: - buf.append('\\cb%d' % color_mapping[style['bgcolor']]) + buf.append('\\cb%d' % self.color_mapping[style['bgcolor']]) if style['color']: - buf.append('\\cf%d' % color_mapping[style['color']]) + buf.append('\\cf%d' % self.color_mapping[style['color']]) if style['bold']: buf.append('\\b') if style['italic']: @@ -135,12 +328,24 @@ def format_unencoded(self, tokensource, outfile): buf.append('\\ul') if style['border']: buf.append('\\chbrdr\\chcfpat%d' % - color_mapping[style['border']]) + self.color_mapping[style['border']]) start = ''.join(buf) if start: outfile.write('{%s ' % start) outfile.write(self._escape_text(value)) if start: outfile.write('}') + start_new_line = False + + # complete line of input + if value.endswith("\n"): + # close line highlighting + if lineno in self.hl_lines: + outfile.write('}') + # newline in RTF file after closing } + outfile.write("\n") + + start_new_line = True + lineno += 1 - outfile.write('}') + outfile.write('}\n') diff --git a/tests/test_rtf_formatter.py b/tests/test_rtf_formatter.py index a21939f043..6379e37d16 100644 --- a/tests/test_rtf_formatter.py +++ b/tests/test_rtf_formatter.py @@ -7,12 +7,17 @@ """ from io import StringIO +import itertools +import re +import pytest from pygments.formatters import RtfFormatter +from pygments.lexers import CppLexer, PythonLexer from pygments.lexers.special import TextLexer +from pygments.style import _ansimap, Style +from pygments.token import Name, String, Token - -foot = (r'\par' '\n' r'}') +foot = r'\par' '\n' r'}' + '\n' def _escape(string): @@ -26,9 +31,9 @@ def _build_message(*args, **kwargs): result = _escape(kwargs.get('result', '')) if string is None: - string = ("The expected output of '{t}'\n" - "\t\tShould be '{expected}'\n" - "\t\tActually outputs '{result}'\n" + string = ("The expected output of '{t}'\n\n" + "\t\tShould be '{expected}'\n\n" + "\t\tActually outputs '{result}'\n\n" "\t(WARNING: Partial Output of Result!)") end = -len(_escape(foot)) @@ -39,9 +44,11 @@ def _build_message(*args, **kwargs): expected = expected) -def format_rtf(t): - tokensource = list(TextLexer().get_tokens(t)) - fmt = RtfFormatter() +def format_rtf(t, options=None, lexer=TextLexer): + if options is None: + options = {} + tokensource = lexer().get_tokens(t) + fmt = RtfFormatter(**options) buf = StringIO() fmt.format(tokensource, buf) result = buf.getvalue() @@ -49,6 +56,17 @@ def format_rtf(t): return result +def extract_color_table(rtf): + r""" + Return af list of \redR\greenG\blueB; color definitions + extracted from the input (the color table). + """ + return re.findall((r"\\red[0-9]{1,3}" + r"\\green[0-9]{1,3}" + r"\\blue[0-9]{1,3};"), + rtf) + + def test_rtf_header(): t = '' result = format_rtf(t) @@ -72,7 +90,7 @@ def test_rtf_footer(): def test_ascii_characters(): t = 'a b c d ~' result = format_rtf(t) - expected = (r'a b c d ~') + expected = r'a b c d ~' msg = _build_message(t=t, result=result, expected=expected) assert result.endswith(expected+foot), msg @@ -101,3 +119,434 @@ def test_double_characters(): r'{\u8597}{\u65038} {\u55422}{\u56859}') msg = _build_message(t=t, result=result, expected=expected) assert result.endswith(expected+foot), msg + + +def test_linenos_all_defaults(): + t = 'line1\nline2\n' + options = {} + result = format_rtf(t, options) + expected = (r'line1\par' + '\n' + r'line2\par' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + assert result.endswith(expected), msg + + +def test_linenos_text(): + t = 'line1\nline2\n' + options = dict(linenos=True, lineno_padding=2) + result = format_rtf(t, options) + expected = (r'{\cf1 1 }line1\par' + '\n' + r'{\cf1 2 }line2\par' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + assert result.endswith(expected), msg + + +def test_linenos_newline_characters(): + t = r'line1\nline2' + '\n' + options = dict(linenos=True, lineno_padding=2) + result = format_rtf(t, options) + expected = (r'{\cf1 1 }line1\\nline2\par' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + assert result.endswith(expected), msg + + +def test_linenos_python(): + class TestStyle(Style): + name = 'rtf_formatter_test' + line_number_color = "#ff0000" + styles = {Token: '', String: '#00ff00', Name: '#0000ff'} + + t = r's = "line1\nline2"' + '\n' + options = dict(linenos=True, lineno_padding=2, style=TestStyle) + result = format_rtf(t, options, PythonLexer) + expected = (r'{\rtf1\ansi\uc0\deff0{\fonttbl{\f0\fmodern\fprq1\fcharset0;}}' + '\n' + r'{\colortbl;' + '\n' + r'\red255\green0\blue0;' + '\n' + r'\red0\green255\blue0;' + '\n' + r'\red0\green0\blue255;' + '\n' + r'}' + '\n' + r'\f0' + '\n' + r'\dntblnsbdb' + '\n' + r'{\cf1 1 }{\cf3 s} = {\cf2 "}{\cf2 line1}{\cf2 \\n}{\cf2 line2}{\cf2 "}\par' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + assert result.endswith(expected), msg + + +def test_linenos_left_padding(): + t = '0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n' + options = dict(linenos=True, lineno_padding=2) + result = format_rtf(t, options) + expected = (r'{\cf1 9 }8\par' + '\n' + r'{\cf1 10 }9\par' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + assert result.endswith(expected), msg + + +def test_lineno_padding(): + t = 'line1\nline2\n' + options = dict(linenos=True, lineno_padding=3) + result = format_rtf(t, options) + expected = (r'{\cf1 1 }line1\par' + '\n' + r'{\cf1 2 }line2\par' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + assert result.endswith(expected), msg + + +def test_linenostep(): + t = 'line1\nline2\nline3\nline4\n' + options = dict(linenos=True, + linenostep=2, + lineno_padding=2) + result = format_rtf(t, options) + expected = (r'{\cf1 }line1\par' + '\n' + r'{\cf1 2 }line2\par' + '\n' + r'{\cf1 }line3\par' + '\n' + r'{\cf1 4 }line4\par' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + assert result.endswith(expected), msg + + +def test_linenostart(): + t = 'line1\nline2\nline3\nline4\n' + options = dict(linenos=True, + linenostart=3, + lineno_padding=2) + result = format_rtf(t, options) + expected = (r'{\cf1 3 }line1\par' + '\n' + r'{\cf1 4 }line2\par' + '\n' + r'{\cf1 5 }line3\par' + '\n' + r'{\cf1 6 }line4\par' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + assert result.endswith(expected), msg + + +def test_linenostart_left_padding(): + t = 'line1\nline2\nline3\n' + options = dict(linenos=True, + linenostart=98, + lineno_padding=2) + result = format_rtf(t, options) + expected = (r'{\cf1 98 }line1\par' + '\n' + r'{\cf1 99 }line2\par' + '\n' + r'{\cf1 100 }line3\par' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + assert result.endswith(expected), msg + + +def test_linenos_hl_lines(): + t = 'line1\nline2\nline3\n' + options = dict(linenos=True, + hl_lines="2 3", + lineno_padding=2) + result = format_rtf(t, options) + expected = (r'{\cf1 1 }line1\par' + '\n' + r'{\highlight2 {\cf1 2 }line2\par}' + '\n' + r'{\highlight2 {\cf1 3 }line3\par}' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + assert result.endswith(expected), msg + + +def test_linenos_off_hl_lines(): + t = 'line1\nline2\nline3\n' + options = dict(linenos=False, + hl_lines="2 3") + result = format_rtf(t, options) + expected = (r'line1\par' + '\n' + r'{\highlight1 line2\par}' + '\n' + r'{\highlight1 line3\par}' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + assert result.endswith(expected), msg + + +def test_hl_linenostart_no_lines_highlighted(): + t = 'line11\nline12\nline13\n' + options = dict(linenos=False, + hl_lines="2 3", + hl_linenostart=True, + linenostart=11) + result = format_rtf(t, options) + expected = (r'line11\par' + '\n' + r'line12\par' + '\n' + r'line13\par' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + assert result.endswith(expected), msg + + +def test_hl_linenostart_lines_highlighted(): + t = 'line11\nline12\nline13\n' + options = dict(linenos=False, + hl_lines="12 13", + hl_linenostart=True, + linenostart=11) + result = format_rtf(t, options) + expected = (r'line11\par' + '\n' + r'{\highlight1 line12\par}' + '\n' + r'{\highlight1 line13\par}' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + assert result.endswith(expected), msg + + +def test_lineno_color_style_specify_hex(): + class TestStyle(Style): + name = 'rtf_formatter_test' + line_number_color = "#123456" + + t = 'line1\nline2\n' + options = dict(linenos=True, + style=TestStyle) + result = format_rtf(t, options) + rtf_color_str = RtfFormatter.hex_to_rtf_color(TestStyle.line_number_color) + color_tbl = extract_color_table(result) + msg = (f"Color table {color_tbl} " + f"should have '{rtf_color_str}' " + "as first entry") + + # With linenos=True the color table should contain: + # 1st entry: line number color (hence \cf1) + assert color_tbl[0] == rtf_color_str, msg + + +def test_lineno_color_style_specify_inherit(): + class TestStyle(Style): + name = 'rtf_formatter_test' + line_number_color = "inherit" # Default from pygments/style.py + + t = 'line1\nline2\n' + options = dict(linenos=True, + style=TestStyle) + result = format_rtf(t, options) + rtf_color_str = RtfFormatter.hex_to_rtf_color(_ansimap['ansibrightblack']) + color_tbl = extract_color_table(result) + msg = (f"Color table {color_tbl} " + f"should have '{rtf_color_str}' " + "as first entry") + + # With linenos=True the color table should contain: + # 1st entry: line number color (hence \cf1) + assert color_tbl[0] == rtf_color_str, msg + + +def test_lineno_color_from_cli_option(): + class TestStyle(Style): + name = 'rtf_formatter_test' + line_number_color = "#123456" # Default from pygments/style.py + + option_color = "112233" + t = 'line1\nline2\n' + options = dict(linenos=True, + style=TestStyle, + lineno_color=option_color) + result = format_rtf(t, options) + rtf_color_str = RtfFormatter.hex_to_rtf_color(option_color) + color_tbl = extract_color_table(result) + msg = (f"Color table {color_tbl} " + f"should have '{rtf_color_str}' " + "as first entry") + + # With linenos=True the color table should contain: + # 1st entry: line number color (hence \cf1) + assert color_tbl[0] == rtf_color_str, msg + + +def test_hl_color_style(): + class TestStyle(Style): + name = 'rtf_formatter_test' + line_number_color = "#123456" + highlight_color = "#abcdef" + + t = 'line1\nline2\n' + options = dict(linenos=True, + lineno_padding=2, + style=TestStyle, + hl_lines="1 2") + result = format_rtf(t, options) + + rtf_color = RtfFormatter.hex_to_rtf_color(TestStyle.highlight_color) + + color_tbl = extract_color_table(result) + msg = (f"Color table {color_tbl} " + f"should have '{rtf_color}' " + "as second entry") + + # With linenos=True and hl_lines="1 2" the color table should contain: + # 1st entry: line number color (hence \cf1) + # 2nd entry: highlight color (hence \highlight2) + assert color_tbl[1] == rtf_color, msg + + expected = (r'{\highlight2 {\cf1 1 }line1\par}' + '\n' + r'{\highlight2 {\cf1 2 }line2\par}' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + + assert result.endswith(expected), msg + + +def test_hl_color_style_no_linenos(): + class TestStyle(Style): + name = 'rtf_formatter_test' + line_number_color = "#123456" + highlight_color = "#abcdef" + + t = 'line1\nline2\n' + options = dict(linenos=False, + style=TestStyle, + hl_lines="1 2") + result = format_rtf(t, options) + + rtf_color = RtfFormatter.hex_to_rtf_color(TestStyle.highlight_color) + + color_tbl = extract_color_table(result) + msg = (f"Color table {color_tbl} " + f"should have '{rtf_color}' " + "as second entry") + + # With linenos=False and hl_lines="1 2" the color table should contain: + # 1st entry: highlight color (hence \highlight1) + assert rtf_color in color_tbl and color_tbl[0] == rtf_color, msg + + expected = (r'{\highlight1 line1\par}' + '\n' + r'{\highlight1 line2\par}' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + + assert result.endswith(expected), msg + + +def test_hl_color_option(): + class TestStyle(Style): + name = 'rtf_formatter_test' + line_number_color = "#123456" + highlight_color = "#abcdef" + + t = 'line1\nline2\n' + hl_color = "aabbcc" + options = dict(linenos=False, + style=TestStyle, + hl_lines="1 2", + hl_color=hl_color) + result = format_rtf(t, options) + + rtf_color = RtfFormatter.hex_to_rtf_color(hl_color) + + color_tbl = extract_color_table(result) + msg = (f"Color table {color_tbl} " + f"should have '{rtf_color}' " + "as second entry") + + # With linenos=False and hl_lines="1 2" the color table should contain: + # 1st entry: highlight color (hence \highlight1) + assert rtf_color in color_tbl and color_tbl[0] == rtf_color, msg + + expected = (r'{\highlight1 line1\par}' + '\n' + r'{\highlight1 line2\par}' + '\n' + r'}' + '\n') + msg = _build_message(t=t, result=result, expected=expected) + + assert result.endswith(expected), msg + + +def test_all_options(): + # Test if all combinations of options (given values and defaults) + # produce output: + # + # - No uncaught exceptions + # - Output contains one \par control word per input line + + def get_option_combinations(options): + for _, values in options.items(): + values.append('default') + # https://stackoverflow.com/a/40623158 + combinations = (dict(zip(options.keys(), x)) + for x in itertools.product(*options.values())) + for c in combinations: + yield {opt:val for opt,val in c.items() if val!='default'} + + options = {'linenos': [True], + 'lineno_fontsize': [36], + 'fontsize': [36], + 'lineno_padding': [4], + 'linenostart': [10], + 'linenostep': [3], + 'lineno_color': ['ff0000'], + 'hl_lines': ['2'], + 'hl_linenostart': [True], + 'hl_color': ['00ff00'] + } + + t_cpp = [r'#include ', + r'int main(int argc, char** argv) {', + r' /* Multi-line comment', + r' with \n escape sequence */' + r' for (int i = 0; i < argc; i++){', + r' std::cout << i << ": " << argv[i] << "\n";', + r' }', + r' return 0;', + r'}' + ] + + t_python = [r'# Description of program', + r'def add(a, b):', + r' """ Add numbers a and b.', + r' Newline \n in docstring."""', + r' return a+b', + r'if __name__ == "__main__":', + r'result = add(2,2)', + r'print(f"Result:\n{result}")' + ] + + t_text = [r'Header1;"Long', + r'Header2";Header3', + r'1,2;Single Line;20/02/2024', + r'1,3;"Multiple', + r'Lines";21/02/2024'] + + + for opts in get_option_combinations(options): + + opt_strs = '\n'.join([f"{k}: {v}" for k,v in opts.items()]) + opt_str_for_copying = "-O " + ",".join([f"{k}={v}" for k,v in opts.items()]) + + for t, lexer in [(t_cpp, CppLexer), + (t_python, PythonLexer), + (t_text, TextLexer)]: + + input_text = '\n'.join(t) + '\n' # Last line should end in \n + + try: + result = format_rtf(input_text, opts,lexer) + except Exception as e: + msg = (f"RTF-formatting caused an exception with options\n\n" + f"{opt_strs}\n\n" + f"{opt_str_for_copying}\n\n" + f"Lexer: {lexer.__name__}\n\n" + f"Input:\n" + f"{input_text}\n" + f"{type(e)}: {e}\n") + + pytest.fail(msg) + + num_input_lines = len(t) + num_of_pars = result.count(r'\par') + + msg = (f"Different of number of input lines and formatted lines:\n" + f"{opt_strs}\n\n" + f"{opt_str_for_copying}\n\n" + f"\\par control words: {num_of_pars}\n" + f"Input lines: {num_input_lines}\n\n" + f"Input:\n" + f"{input_text}\n") + + assert num_of_pars == num_input_lines, msg