From 43570f5752b31e11a7f21d5e694737e9fcb3bf19 Mon Sep 17 00:00:00 2001 From: László Németh Date: Fri, 2 Dec 2022 10:19:49 +0100 Subject: LibreLogo: LABEL/TEXT: add more character formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit – strike out: or – superscript and subscript: and (using character formatting) – text color: – localized names, e.g. (in English documents), (in French documents) – hexa or decimal codes: , (localized "FONTCOLOR" is allowed) – verbose form of the localized names: – highlight color: , , (localized "FILLCOLOR" is allowed) – font name: e.g. (localized "FONTFAMILY" is allowed) – font size: e.g. (localized "FONTSIZE" is allowed) – OpenType and Linux Libertine G/Biolinum G font features: : small capitals, : proportional numbers, : true subscript (scientific inferior) etc., – with arguments: – verbose form: Alternative names for bold and italic character formatting – bold text: HTML: or ; localized LibreLogo: e.g. , (in Finnish documents) – italic: HTML: or ; localized LibreLogo: e.g. , (in Czech documents) Add unit tests for the previous and new tags. Follow-up to commit 89c34706331984d12af8ce99444d53f19b40b496 "LibreLogo: add basic HTML formatting support to LABEL". Change-Id: Ie14024cae62ec09de714af5db46132375b6101d0 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/143639 Tested-by: László Németh Reviewed-by: László Németh --- librelogo/source/LibreLogo/LibreLogo.py | 138 ++++++++++++++++++-- sw/qa/uitest/librelogo/run.py | 216 ++++++++++++++++++++++++++++++++ 2 files changed, 341 insertions(+), 13 deletions(-) diff --git a/librelogo/source/LibreLogo/LibreLogo.py b/librelogo/source/LibreLogo/LibreLogo.py index bb776ca18c81..562095a15383 100644 --- a/librelogo/source/LibreLogo/LibreLogo.py +++ b/librelogo/source/LibreLogo/LibreLogo.py @@ -134,6 +134,26 @@ __TURTLE__ = "turtle" __ACTUAL__ = "actual" __BASEFONTFAMILY__ = "Linux Biolinum G" __LineStyle_DOTTED__ = 2 +# LABEL supports font features with the simplified syntax text, e.g. +# LABEL "Small caps: text" +# prints "Small caps: TEXT", where TEXT is small capital, if that feature is supported by the font +# See https://en.wikipedia.org/wiki/List_of_typographic_features +__match_fontfeatures__ = re.compile( r"()", re.IGNORECASE ) +# LABEL localized color tags, e.g. text in red +__match_localized_colors__ = {} +# LABEL not localized tags (localized translated to these): +__match_tags__ = [re.compile(i, re.IGNORECASE) for i in [r'<(b|strong)>', r'', r'<(i|em)>', r'', '', '', r'<(s|del)>', r'', '', '', '', '', r'<(fontcolor) ([^<>]*)>', r'', r'<(fillcolor) ([^<>]*)>', r'', r'<(fontfamily) ([^<>]*)>', r'', r'<(fontfeature) ([^<>]*)>', r']*)>', r'<(fontheight) ([^<>]*)>', r'']] class __Doc__: def __init__(self, doc): @@ -204,6 +224,7 @@ from com.sun.star.drawing.CircleKind import ARC as __ARC__ from com.sun.star.awt.FontSlant import NONE as __Slant_NONE__ from com.sun.star.awt.FontSlant import ITALIC as __Slant_ITALIC__ from com.sun.star.awt.FontUnderline import SINGLE as __Underline_SINGLE__ +from com.sun.star.awt.FontStrikeout import SINGLE as __Strikeout_SINGLE__ from com.sun.star.awt import Size as __Size__ from com.sun.star.awt import WindowDescriptor as __WinDesc__ from com.sun.star.awt.WindowClass import MODALTOP as __MODALTOP__ @@ -1275,51 +1296,113 @@ def __get_HTML_format__(orig_st): "Process HTML-like tags, and return with text and formatting vector" st = orig_st.replace('<', '\uE000') if not ('<' in st and '>' in st): - return st.replace('\uE000', '<'), None + return st.replace('\uE000', '<'), None, None + + # convert localized bold, and italic values to and tags + for i in ('BOLD', 'ITALIC'): + st = re.sub(r'(', r'\1%s>' % i[0], st, flags=re.I) + + for i in ('FONTCOLOR', 'FILLCOLOR', 'FONTFAMILY', 'FONTHEIGHT'): + st = re.sub(r'<(' + __l12n__(_.lng)[i] + r')( *[^<> ][^<>]*)>', r'<%s\2>' % i.lower(), st, flags=re.I) + st = re.sub(r'', r'' % i.lower(), st, flags=re.I) + + # expand localized color names + if _.lng not in __match_localized_colors__: + __match_localized_colors__[_.lng] = re.compile(r'<(/?)(' + '|'.join(__colors__[_.lng].keys()) + ')>', re.IGNORECASE) + # replacement lambda function: if it's an opening tag, return with the argument, too + get_fontcolor_tag = lambda m: "" % m.group(2) if len(m.group(1)) == 0 else "" + st = re.sub(__match_localized_colors__[_.lng], get_fontcolor_tag, st) + + # expand abbreviated forms of font features + # small caps -> small caps + st = re.sub(__match_fontfeatures__, r'\1fontfeature \2\3', st) + tex = "" # characters without HTML tags pat = [] # bit vectors of the previous characters - # 1st bit: bold - # 2nd bit: italic - # 3rd bit: underline + extra_pat = [] # extra data of the previous characters + # 0th bit: bold + # 1st bit: italic + # 2nd bit: underline + # 3rd bit: strikethrough + # 4th bit: superscript + # 5th bit: subscript + # 6th bit: color + # 7th bit: background color + # 8th bit: font family + # 9th bit: font feature (Graphite or OpenType) + # 10th bit: font size f = 0 - tags = ['', '', '', '', '', ''] # store embedding level of the same element to disable it # only at the most outer closing tag, e.g. a double italic here, too - bit_level = {0: 0, 1: 0, 2: 0} + # bit_level = {0: 0, ..., 10: 0} + bit_level = { i: 0 for i in range(11) } + + extra_data = {} i = 0 while i < len(st): is_tag = False - for j in range(len(tags)): - if st[i:i + 4].lower().startswith(tags[j]): + + if st[i] == '<': + for j in range(len(__match_tags__)): + m = __match_tags__[j].match(st[i:]) + if m: + tag = "" bit = j // 2 + if bit > 5: + tag = m.group(1).lower() # opening tag if j % 2 == 0: f |= (1 << bit) bit_level[bit] += 1 + # extra data (color bit and over) + if bit > 5: + if tag in extra_data: + extra_data[tag] = extra_data[tag] + [m.group(2)] + else: + extra_data[tag] = [m.group(2)] else: if bit_level[bit] > 0: bit_level[bit] -= 1 if bit_level[bit] == 0: f &= ~(1 << bit) - i += len(tags[j]) - 1 + # extra data for font feature + # fontfeature has a special closing tag, remove that from the extra_data + # (allowing to use overlapping elements) + if bit > 5 and (tag in extra_data): + if bit == 9 and len(m.group(2)) > 0: + # create a new list to keep the extra data of the previous characters, + # and remove the last occurance of the feature + z = list(extra_data[tag]) + for j in reversed(range(len(z))): + if z[j].startswith(m.group(2)): + z.pop(j) + extra_data[tag] = z + break + # extra data + else: + extra_data[tag] = extra_data[tag][:-1] + + i += len(m.group(0)) - 1 is_tag = True break if not is_tag: tex = tex + st[i] pat.append(f) + extra_pat.append(dict(extra_data)) i += 1 # no tags if len(st) == len(tex): pat = None + extra_pat = None - return tex.replace('\uE000', '<'), pat + return tex.replace('\uE000', '<'), pat, extra_pat def text(shape, orig_st): if shape: # analyse HTML - st, formatting = __get_HTML_format__(orig_st) + st, formatting, extra_data = __get_HTML_format__(orig_st) shape.setString(__string__(st, _.decimal)) c = shape.createTextCursor() c.gotoStart(False) @@ -1329,14 +1412,16 @@ def text(shape, orig_st): c.CharWeight = __fontweight__(_.fontweight) c.CharPosture = __fontstyle__(_.fontstyle) c.CharFontName = _.fontfamily + # has HTML-like formatting if formatting != None: prev_format = 0 + prev_extra_data = extra_data[0] c.collapseToStart() n = 0 # length of the previous text span formatting.append(0) # add terminating 0 to process last span for i in formatting: - if i != prev_format: + if i != prev_format or (len(extra_data) > 0 and extra_data[0] != prev_extra_data): do_formatting = prev_format != 0 c.goRight(n, do_formatting) # move cursor with optional selection if do_formatting: @@ -1346,10 +1431,35 @@ def text(shape, orig_st): c.CharPosture = __Slant_ITALIC__ if prev_format & (1 << 2): c.CharUnderline = __Underline_SINGLE__ + if prev_format & (1 << 3): + c.CharStrikeout = __Strikeout_SINGLE__ + if prev_format & (1 << 4): + c.CharEscapement = 14000 # magic number for default superscript, see DFLT_ESC_AUTO_SUPER + c.CharEscapementHeight = 58 + if prev_format & (1 << 5): + c.CharEscapement = -14000 # magic number for default subscript, see DFLT_ESC_AUTO_SUB + c.CharEscapementHeight = 58 + if prev_format & (1 << 6): + c.CharColor, c.CharTransparence = __splitcolor__(__color__(prev_extra_data['fontcolor'][-1])) + if prev_format & (1 << 7): + c.CharBackColor = __color__(prev_extra_data['fillcolor'][-1]) + if prev_format & (1 << 8): + c.CharFontName = prev_extra_data['fontfamily'][-1] + if prev_format & (1 << 9): + # font features uses the following syntax: font_name:feat1&feat2&feat3=value&etc. + if ":" in c.CharFontName: + c.CharFontName = c.CharFontName + "&" + "&".join(prev_extra_data['fontfeature']) + else: + c.CharFontName = c.CharFontName + ":" + "&".join(prev_extra_data['fontfeature']) + if prev_format & (1 << 10): + c.CharHeight = prev_extra_data['fontheight'][-1] + c.collapseToEnd() n = 0 n += 1 prev_format = i + if len(extra_data) > 0: + prev_extra_data = extra_data.pop(0) def sleep(t): _.time = _.time + t @@ -1401,11 +1511,13 @@ def __color__(c): for i in range(0, 3): newcol[i] = 255 * (rgray + (newcol[i]/255.0 - rgray) * rdark) return __color__(newcol) - if c[0:1] == '~': + elif c[0:1] == '~': c = __componentcolor__(__colors__[_.lng][c[1:].lower()]) for i in range(3): c[i] = max(min(c[i] + int(random.random() * 64) - 32, 255), 0) return __color__(c) + elif c[0].isdigit(): + return int(c, 0) # recognize hex and decimal numbers as strings return __colors__[_.lng][c.lower()] if type(c) == list: if len(c) == 1: # color index diff --git a/sw/qa/uitest/librelogo/run.py b/sw/qa/uitest/librelogo/run.py index 1f01fbf0d16c..6bcca4ade8a8 100644 --- a/sw/qa/uitest/librelogo/run.py +++ b/sw/qa/uitest/librelogo/run.py @@ -9,6 +9,12 @@ from uitest.framework import UITestCase from uitest.uihelper.common import type_text +from com.sun.star.awt.FontSlant import NONE as __Slant_NONE__ +from com.sun.star.awt.FontSlant import ITALIC as __Slant_ITALIC__ +from com.sun.star.awt.FontUnderline import NONE as __Underline_NONE__ +from com.sun.star.awt.FontUnderline import SINGLE as __Underline_SINGLE__ +from com.sun.star.awt.FontStrikeout import NONE as __Strikeout_NONE__ +from com.sun.star.awt.FontStrikeout import SINGLE as __Strikeout_SINGLE__ class LibreLogoTest(UITestCase): LIBRELOGO_PATH = "vnd.sun.star.script:LibreLogo|LibreLogo.py$%s?language=Python&location=share" @@ -94,5 +100,215 @@ x 3 ; draw only a few levels # new shape + previous two ones = 3 self.assertEqual(document.DrawPage.getCount(), 3) + def test_LABEL(self): + with self.ui_test.create_doc_in_start_center("writer") as document: + xWriterDoc = self.xUITest.getTopFocusWindow() + xWriterEdit = xWriterDoc.getChild("writer_edit") + # to check the state of LibreLogo program execution + xIsAlive = self.getScript("__is_alive__") + + #1 run a program with basic LABEL command + + type_text(xWriterEdit, "LABEL 'Hello, World!'") + self.logo("run") + # wait for LibreLogo program termination + while xIsAlive.invoke((), (), ())[0]: + pass + + # turtle and text shape + self.assertEqual(document.DrawPage.getCount(), 2) + textShape = document.DrawPage.getByIndex(1) + # text in the text shape + self.assertEqual(textShape.getString(), "Hello, World!") + + #2 check italic, bold, underline + red and blue formatting + + document.Text.String = "CLEARSCREEN LABEL 'Hello, World!'" + self.logo("run") + # wait for LibreLogo program termination + while xIsAlive.invoke((), (), ())[0]: + pass + + # turtle and text shape + self.assertEqual(document.DrawPage.getCount(), 2) + textShape = document.DrawPage.getByIndex(1) + # text in the text shape + self.assertEqual(textShape.getString(), "Hello, World!") + # check portion formatting + c = textShape.createTextCursor() + c.gotoStart(False) + # before character "H" + self.assertEqual(c.CharPosture, __Slant_ITALIC__) # cursive + self.assertEqual(c.CharUnderline, __Underline_NONE__) # no underline + self.assertEqual(c.CharWeight, 100) # normal weight + self.assertEqual(c.CharColor, 0xFF0000) # red color + # after character " " + c.goRight(6, False) + self.assertEqual(c.CharPosture, __Slant_ITALIC__) # cursive + self.assertEqual(c.CharUnderline, __Underline_NONE__) # no underline + self.assertEqual(c.CharWeight, 100) # normal weight + self.assertEqual(c.CharColor, 0x000000) # black color + # after character "W" + c.goRight(2, False) + self.assertEqual(c.CharPosture, __Slant_ITALIC__) # cursive + self.assertEqual(c.CharUnderline, __Underline_NONE__) # no underline + self.assertEqual(c.CharWeight, 150) # bold + self.assertEqual(c.CharColor, 0x0000FF) # blue color + # 9th: after character "o" + c.goRight(1, False) + self.assertEqual(c.CharPosture, __Slant_ITALIC__) # cursive + self.assertEqual(c.CharUnderline, __Underline_SINGLE__) # underline + self.assertEqual(c.CharWeight, 150) # bold + self.assertEqual(c.CharColor, 0x0000FF) # blue color + # last: after character "!" + c.gotoEnd(False) + self.assertEqual(c.CharPosture, __Slant_ITALIC__) # cursive + self.assertEqual(c.CharUnderline, __Underline_SINGLE__) # underline + self.assertEqual(c.CharWeight, 100) # normal weight + self.assertEqual(c.CharColor, 0x000000) # black color + + #2 check strike out, sub, sup, font name and font size formatting + + document.Text.String = ( + "CLEARSCREEN FONTFAMILY 'Linux Biolinum G' FONTSIZE 12 " + + "LABEL 'x, x, x, " + + "x, " + + "x...'" ) + + self.logo("run") + # wait for LibreLogo program termination + while xIsAlive.invoke((), (), ())[0]: + pass + + # turtle and text shape + self.assertEqual(document.DrawPage.getCount(), 2) + textShape = document.DrawPage.getByIndex(1) + # text in the text shape + self.assertEqual(textShape.getString(), "x, x, x, x, x...") + # check portion formatting + c = textShape.createTextCursor() + c.gotoStart(False) + # check portion formatting + c = textShape.createTextCursor() + c.gotoStart(False) + + # strike out + self.assertEqual(c.CharStrikeout, __Strikeout_SINGLE__) # strike out + c.goRight(4, False) + + # subscript + self.assertEqual(c.CharStrikeout, __Strikeout_NONE__) # no strike out + self.assertEqual(c.CharEscapement, -14000) # magic number for default subscript, see DFLT_ESC_AUTO_SUB + self.assertEqual(c.CharEscapementHeight, 58) # size in percent + c.goRight(3, False) + + # superscript + self.assertEqual(c.CharEscapement, 14000) # magic number for default superscript, see DFLT_ESC_AUTO_SUPER + self.assertEqual(c.CharEscapementHeight, 58) # size in percent + c.goRight(3, False) + + # font family + self.assertEqual(c.CharEscapement, 0) # no superscript + self.assertEqual(c.CharEscapementHeight, 100) # no superscript + self.assertEqual(c.CharFontName, "Liberation Sans") # new font family + c.goRight(3, False) + + # font size + self.assertEqual(c.CharFontName, "Linux Biolinum G") # default font family + self.assertEqual(c.CharHeight, 20) # new font size + c.goRight(3, False) + + # default font size + self.assertEqual(c.CharHeight, 12) + + #3 check colors + + document.Text.String = ( + "CLEARSCREEN " + + "LABEL 'x, x, " + # check ignoring case + "x, " + # check with command + "x, " + # check with hexa code + "x, " + # blue text with orange highlighting + "x" + # blue text with purple highlighting + "...'" ) + + self.logo("run") + # wait for LibreLogo program termination + while xIsAlive.invoke((), (), ())[0]: + pass + + # turtle and text shape + self.assertEqual(document.DrawPage.getCount(), 2) + textShape = document.DrawPage.getByIndex(1) + # text in the text shape + self.assertEqual(textShape.getString(), "x, x, x, x, x, x...") + # check portion formatting + c = textShape.createTextCursor() + c.gotoStart(False) + # check portion formatting + c = textShape.createTextCursor() + c.gotoStart(False) + + self.assertEqual(c.CharColor, 0xFF0000) # red + self.assertEqual(c.CharBackColor, -1) # transparent highlight + c.goRight(4, False) + + self.assertEqual(c.CharColor, 0x0000FF) # blue + self.assertEqual(c.CharBackColor, -1) # transparent highlight + c.goRight(3, False) + + self.assertEqual(c.CharColor, 0x008000) # green + self.assertEqual(c.CharBackColor, -1) # transparent highlight + c.goRight(3, False) + + self.assertEqual(c.CharColor, 0x0000FF) # blue + self.assertEqual(c.CharBackColor, -1) # transparent highlight + c.goRight(3, False) + + self.assertEqual(c.CharColor, 0x0000FF) # blue + self.assertEqual(c.CharBackColor, 0xFFA500) # orange highlight + c.goRight(3, False) + + self.assertEqual(c.CharColor, 0x0000FF) # blue + self.assertEqual(c.CharBackColor, 0xFF00FF) # purple highlight + c.goRight(3, False) + + self.assertEqual(c.CharColor, 0x0000FF) # blue + self.assertEqual(c.CharBackColor, -1) # transparent highlight + + #4 check font features + + document.Text.String = ( + "CLEARSCREEN FONTFAMILY 'Linux Biolinum G' " + + "LABEL 'a smcp 11 11...'" ) + + self.logo("run") + # wait for LibreLogo program termination + while xIsAlive.invoke((), (), ())[0]: + pass + + # turtle and text shape + self.assertEqual(document.DrawPage.getCount(), 2) + textShape = document.DrawPage.getByIndex(1) + # text in the text shape + self.assertEqual(textShape.getString(), "a smcp 11 11...") + # check portion formatting + c = textShape.createTextCursor() + c.gotoStart(False) + # check portion formatting + c = textShape.createTextCursor() + c.gotoStart(False) + + self.assertEqual(c.CharFontName, "Linux Biolinum G") + c.goRight(3, False) + self.assertEqual(c.CharFontName, "Linux Biolinum G:smcp") + c.goRight(5, False) + self.assertEqual(c.CharFontName, "Linux Biolinum G:smcp&pnum") + c.goRight(1, False) + self.assertEqual(c.CharFontName, "Linux Biolinum G:smcp&pnum&onum") + c.goRight(2, False) + self.assertEqual(c.CharFontName, "Linux Biolinum G:smcp&onum") + c.goRight(1, False) + self.assertEqual(c.CharFontName, "Linux Biolinum G:smcp") # vim: set shiftwidth=4 softtabstop=4 expandtab: -- cgit v1.2.3