 /*
    This file is part of Msc-generator.
    Copyright (C) 2008-2024 Zoltan Turanyi
    Distributed under GNU Affero General Public License.

    Msc-generator is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Msc-generator is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Msc-generator.  If not, see <http://www.gnu.org/licenses/>.
*/
/** @file stringparse.cpp Implementation of StringFormat and Label for text functions.
 * @ingroup libcgencommon_files */

#define _CRT_SECURE_NO_DEPRECATE //For visual studio to allow sscanf

#include <assert.h>
#include <iostream>
#include <cstdint>
#include "stringparse.h"
#include "utf8utils.h" //for utf8
#include "chartbase.h"
#include "numbering.h"


//////////////////////////////////////////////////////////////////////////////////

/** Values for text.ident attribute*/
template<> const char EnumEncapsulator<EIdentType>::names[][ENUM_STRING_LEN] =
    {"invalid", "left", "center", "right", ""};

/** Values for vident attributes*/
template<> const char EnumEncapsulator<EVIdentType>::names[][ENUM_STRING_LEN] =
{"invalid", "top", "center", "bottom", "justified", ""};

/** Values for text.type attribute*/
template<> const char EnumEncapsulator<EFontType>::names[][ENUM_STRING_LEN] =
    {"invalid", "normal", "small", "superscript", "subscript", ""};


/** Merge two ETriState values.*/
void AddTristate(OptAttr<ETriState> &a, const OptAttr<ETriState> b)
{
    if (!b) return;
    if (!a || b != invert) {
        a = b;
        return;
    }
    //here b is set to is invert
    switch (*a) {
    case no: a = yes; return;
    case yes: a = no; return;
    case invert:
        a.reset();
        return;
    }
}

//////////////////////////////////////////////////////////////////////////////////

void StringFormat::Empty()
{
    color.reset();
    bgcolor.reset();
    fontType.reset();
    spacingBelow.reset();
    bold.reset();
    italics.reset();
    underline.reset();
    face.reset();
    lang.reset();
    textHGapPre.reset();
    textHGapPost.reset();
    textVGapAbove.reset();
    textVGapBelow.reset();
    textVGapLineSpacing.reset();
    ident.reset();
    normalFontSize.reset();
    smallFontSize.reset();
    word_wrap.reset();
    link_format.reset();
}

bool StringFormat::IsComplete() const noexcept
{
    return
        color &&
        bgcolor &&
        fontType &&
        spacingBelow &&
        bold && bold!=invert &&
        italics && italics!=invert &&
        underline && underline!=invert &&
        face &&
        lang &&
        textHGapPre &&
        textHGapPost &&
        textVGapAbove &&
        textVGapBelow &&
        textVGapLineSpacing &&
        ident &&
        normalFontSize &&
        smallFontSize &&
        word_wrap &&
        link_format;
}


bool StringFormat::IsEmpty() const noexcept
{
    return
        !color &&
        !bgcolor &&
        !fontType &&
        !spacingBelow &&
        !bold &&
        !italics &&
        !underline &&
        !face &&
        !lang &&
        !textHGapPre &&
        !textHGapPost &&
        !textVGapAbove &&
        !textVGapBelow &&
        !textVGapLineSpacing &&
        !ident &&
        !normalFontSize &&
        !smallFontSize &&
        !word_wrap &&
        !link_format;
}

/** Sets the fully specified default font.
 * Font sizes are 16 and 10 point, all margins at 2,
 * no line spacing, centered, black, not bold, not italic,
 * not underlined, face name is empty. */
void StringFormat::Default()
{
    normalFontSize = 16;
    smallFontSize = 10;
    textHGapPre = 2;
    textHGapPost = 2;
    textVGapAbove = 2;
    textVGapBelow = 2;
    textVGapLineSpacing = 0;
    ident = MSC_IDENT_CENTER;
    color = ColorType(0,0,0);
    bgcolor= ColorType(0,0,0,0); //fully transparent
    fontType = MSC_FONT_NORMAL;
    spacingBelow =  0;
    face = "";
    lang= "";
    bold = no;
    italics = no;
    underline = no;
    word_wrap = false;
    link_format = "\\c(0,0,1)\\U";
}

StringFormat &StringFormat::operator =(const StringFormat &f)
{
    if (&f==this) return *this;
    color = f.color;
    bgcolor = f.bgcolor;
    fontType = f.fontType;
    spacingBelow = f.spacingBelow;
    bold = f.bold;
    italics = f.italics;
    underline = f.underline;
    face = f.face;
    lang = f.lang;

    textHGapPre = f.textHGapPre;
    textHGapPost = f.textHGapPost;
    textVGapAbove = f.textVGapAbove;
    textVGapBelow = f.textVGapBelow;
    textVGapLineSpacing = f.textVGapLineSpacing;
    ident = f.ident;

    normalFontSize = f.normalFontSize;
    smallFontSize = f.smallFontSize;

    smallFontExtents = f.smallFontExtents;
    normalFontExtents = f.normalFontExtents;

    word_wrap = f.word_wrap;

    link_format = f.link_format;

    return *this;
}

/** Parse a \\S escape sequence.
 * Check for errors and send them to 'csh' and/or 'chart'.*/
bool StringFormat::ShapeEscape::Parse(std::string_view text, CharByteIndex startloc_csh, FileLineCol startloc_parse,
                                      const ShapeCollection *shapes, const ColorSet *colors,
                                      MscError *error, Csh *csh)
{
    //invalidate all
    loc_name = CshPos::invalid();
    loc_height = CshPos::invalid();
    loc_fillcolor = CshPos::invalid();
    shape = -1;
    height = -1;
    fillcolor.type = ColorType::COMPLETE;
    fillcolor.a = 0;
    _ASSERT(text.length()>3);
    _ASSERT(text[0]=='\\');
    _ASSERT(text[1]=='S');
    _ASSERT(text[2]=='(');
    text.remove_prefix(3);
    //find commas and/or closing parenthesis or string end
    //dont react to commas in fillcolor...
    //*pos variables are in bytes
    //*loc variables are in characters (considering UTF8)
    startloc_csh = startloc_csh.ShiftASCII(3);
    startloc_parse.col += 3;
    int section = 0;  //0-name, 1-height, 2-colorname
    bool ret = true;
    while (section<=2) {
        CharByteIndex loc_csh = startloc_csh;
        FileLineCol loc_parse = startloc_parse;
        CharByteIndex len = UTF8FindAnyOf(text, '|', ')');
        _ASSERT(len.byte_index <= (int)text.size()); //valid UTF8
        if ((int)text.size() < len.byte_index) len.byte_index = text.size();
        loc_csh += len;
        loc_parse.col += len.char_index;
        CshPos modloc_csh = {startloc_csh, loc_csh};
        std::string param(text.substr(0, len.byte_index));
        text.remove_prefix(len.byte_index);
        while (param.length() && param.front()==' ') {
            param.erase(param.begin());
            modloc_csh.first_pos = modloc_csh.first_pos.ShiftASCII(1);
        }
        while (param.length() && param.back()==' ') {
            param.erase(--param.end());
            modloc_csh.end_pos = modloc_csh.end_pos.ShiftASCII(-1);
        }
        switch (section) {
        case 0: //name
            loc_name = modloc_csh;
            if (param.length()==0) {
                if (error) error->Error(startloc_parse, "Missing shape name. Ignoring \\S() escape sequence.");
                ret = false;
            } else {
                shape_name = std::move(param);
                if (shapes) {
                    shape = shapes->GetShapeNo(shape_name);
                    if (shape==-1) {
                        auto msg = shapes->ErrorOnShapeName(shape_name);
                        if (error) error->Error(startloc_parse, msg.first + " Ignoring \\S() escape sequence.", msg.second);
                        if (csh) {
                            if (csh->shape_names.end() == std::find_if(csh->shape_names.begin(), csh->shape_names.end(),
                                                                       [this](auto &s) {return s.first == shape_name; }))
                            {
                                ret = false;
                                shape_name.clear();
                                csh->AddCSH_Error(loc_name, std::string(msg.first));
                            } else {
                                csh->AddCSH(loc_name, COLOR_ATTRVALUE);
                            }
                        } else {
                            ret = false;
                            shape_name.clear();
                        }
                    } else
                        if (csh) csh->AddCSH(loc_name, COLOR_ATTRVALUE);
                } else {
                    shape = -1;
                    if (csh) csh->AddCSH(loc_name, COLOR_ATTRVALUE);
                }
            }
            break;
        case 1: //height
            loc_height = modloc_csh;
            if (param.length()==0) break;
            if (1!=sscanf(param.c_str(), "%lf", &height)) {
                if (error) error->Error(startloc_parse, "Bad numeric value for shape height. Ignoring \\S() escape sequence.");
                if (csh) csh->AddCSH_Error(loc_height, "Bad numeric value for shape height.");
                height = -1;
                ret = false;
            } else
                if (csh) csh->AddCSH(loc_height, COLOR_ATTRVALUE);
            break;
        case 2: //color
            loc_fillcolor = modloc_csh;
            if (param.length()==0) break;
            if (colors)
                fillcolor = colors->GetColor(param);
            else
                fillcolor = ColorType(param);
            if (fillcolor.type==ColorType::INVALID) {
                fillcolor.type = ColorType::COMPLETE;
                fillcolor.a = 0;
                if (error) error->Error(startloc_parse, "Unrecognized color. Ignoring \\S() escape sequence.");
                //Don't error on csh. We may not have all the colors checked.
            }
            if (csh) csh->AddCSH(loc_fillcolor, COLOR_ATTRVALUE);
            break;
        }
        if (text.starts_with('|')) {
            startloc_csh = loc_csh.ShiftASCII(1);
            startloc_parse.col = loc_parse.col+1;
            text.remove_prefix(1);
            section++;
            if (csh) csh->AddCSH(CshPos(loc_csh, loc_csh), COLOR_LABEL_ESCAPE);
        } else {//')' or null ==> we will exit the loop
            startloc_csh = loc_csh;
            startloc_parse.col = loc_parse.col;
            break;
        }
    }
    if (section>2 || text.empty() || text.front() == '\0') {
        if (error) error->Error(startloc_parse, "Expecting closing parenthesis. Ignoring \\S() escape sequence.");
        if (csh) csh->AddCSH_Error(CshPos(startloc_csh, startloc_csh), "Expecting closing parenthesis.");
        ret = false;
    }
    return ret;
}

/** Create a canonic representation of a shape escape with resolved color and well formed numbers.*/
std::string StringFormat::ShapeEscape::Print(const ShapeCollection &shapes) const
{
    if (shape_name.length()==0) return std::string();
    std::string msg = "\\S(" + (shape>=0 ? shapes[shape].name : shape_name);
    if (height>0)
        msg += "|"+std::to_string(height);
    if (fillcolor.a>0 && fillcolor.type!=ColorType::INVALID) {
        if (height<=0)
            msg += "|";
        msg += "|" + fillcolor.Print().substr(1); //peel off the parenthesis returned by color
    } else
        msg += ")"; //closing parenthesis if we do not add a color
    return msg;
}

/** Recognize and process one escape sequence.
 * The escape is sought for at at the beginning of `input`. The string itself is not
 * changed, but a suggested replacement/resolution is returned in `replaceto`.
 * References to styles and colors are resolved from the top of `chart->Contexts`,
 * references to the default style, color, etc. (via "\s()" or "\c()") are taken
 * from `basic` if supplied.
 * In general, `linenum` carries the location of the first char of input.
 * `When` we return, it contains the position
 * of the character in `input`+`length`. (In the original input file this can e.g.,
 * be in a different line.)
 * If `linenum`!=nullptr then any problem generates an Error/Warning.
 * If `sayIgnore` is true the we say we ignore the error,
 * otherwise we say we keep the problematic escape verbatim.
 *
 * This function recognize the following escape sequences:
 * - "\-" - switch to small font (also "\s")
 * - "\+" - switch to normal font
 * - "\^" - switch to superscript
 * - "\_" - switch to subscript
 * - "\b\B\u\U\i\I" - to set bold, underline, italics
 * - "\mX(num)" - set text margins (X=_u_p, _d_own, _l_eft, _r_ight)
 * -             and font size (X=_s_mall, _n_ormal)
 * -             E.g. "\mu(7)" sets upper margin to 7.
 * - "\c(color)" - set color, E.g., "\c(0,0,0)" is black
 * - "\C(color)" - set bgcolor, E.g., "\C(0,0,0,0)" is transparent
 * - "\s(style)" - set style
 * - "\S(shape)" - insert a shape
 * - "\pX"- set paragraph ident to _c_enter, _l_eft, _r_ight
 * - "\0".."\9" - keep this much of line space after the line
 * - "\\" - an escaped "\"
 * - \#[]{}"; - an escaped chars
 * - \$xxxx - an unicode character (specified in exactly four hex characters)
 * - "\|" - a zero length non-formatting escape. Can be used to separate number from initial escapes.
 * - "\n" - a line break
 * - "\0x3" - a soft line break, which is replaced to a space when doing word wrapping
 * - "\ N" - insert line number here - should not appear in a numbering format
 * - "\ r(ref)" - insert the number line of another entity here - "\ r()" equals "\ N"
 * - "\ L()" links
 * - "\ Q($param) quote the value of a parameter
 * - "\0x21" "\0x2a" "\0x2A" "\0x2i" "\0x2I" - used to define numbering formats, should not appear in a label
 * - "\0x1(file,line,col)" - notes the position of the next char in the original file
 * - "~" The tilde escape (used to insert a fixed string)
 *
 * @param [in] input The string to recognize.
 * @param [out] length Returns the length of the escape found (in bytes!, not characters)
 *                     (or for NON_ESCAPE the offset to the next '\').
 * @param [in] resolve If true we resolve color/style references (stored in MscChart::Contexts).
 *   (If chart==nullptr, or we do not find the style/color we return INVALID_ESCAPE)
 *   If apply==true, resolve parameter is ignored, we assume resolve==true.
 *   This parameter has impact only on "\s" and "\c" escapes and only if chart!=nullptr.
 * @param [in] apply If true then besides parsing, we apply the string formatting escape
 *                   to "this". (Do nothing for invalid escapes.)
 * @param [out] replaceto If not nullptr, return a suggested resolution for the escape.
 * - for FORMATTING_OK we return what to replace the escape to:
 *    + color names are resolved to actual RGBA numbers in a syntax suitable for MscColor::MscColor();
 *    + style names are resolved to actual formatting instructions (a lot of "\b\i" and similar escapes);
 *    + empty parenthesis escapes "\c()", "\s()", "\f()", "\mX()" are replaced with the respective
 *      value in `basic` or if that is nullptr, then are left in place.
 * - for LINK_ESCAPE, and LINK2_ESCAPE we return the parameter, the URL itself
 * - for NON_FORMATTING we return the character represented by the escape (can be zero length for "\|")
 * - for SHAPE_ESCAPE we return the escape itself, if valid - else we return an INVALID_ESCAPE code
 * - for NON_ESCAPE we return the text verbatim
 * - for LINE_BREAK and INVALID_ESCAPE we return empty string
 * - for REFERENCE we attempt to resolve it to an element number if `references` is
 *   true. If so and we do not find this reference, we generate an error. If
 *   `references` is false, we return the escape verbatim.
 * - for NUMBERING and NUMBERING_FORMAT we return the actual escape verbatim
 * - for SOLO_ESCAPE we return a single backslash
 * - for QUOTATIONs ("\Q" escapes) we return the parameter - you would parse it, before moving on
 * - for HIDDEN escapes (ESCAPE_STRING_HIDDEN), we return empty replacement.
 * @param [in] basic This is a string format that is used for resolving empty "\c()", "\s()", "\f()", "\mX()"
 *                   escapes. If nullptr than those are returned verbatim in `replaceto`.
 * @param chart This object is used as a source of context for style, color names and references.
 *            We also use it to send errors to. If nullptr, we do not generate errors nor do we
 *            resolve style, color names and references.
 * @param [in] references If true, indicates that chart->References is complete and therefore we attempt
 *                        to replace "\r" escapes with actual numbers. (Empty "\r()" is always treated
 *                        as equal to "\N" and we return NUMBERING.)
 *                        If true, we emit error/warning messages only in relation to "\r" escapes,
 *                        others are silently ignored. This allows calling this function twice, once
 *                        with references=false and once with references=true.
 *                        If false, we prepend the "\r" escape with a location escape - for the case when the string
 *                        is quoted as a parameter.
 * @param linenum When called, this contains the position of the first character of `input`.
 *                At return it is updated (if not nullptr) to point to the first character after
 *                the escape.
 * @param [in] sayIgnore If true we use language in error messages that say we ignore
 *                       a bad escape. If false, we say that we keep it verbatim.
 * @returns The code for what is found at the beginning of `input`:
 * - a valid format-changing escape sequence (FORMATTING_OK)
 * - an URL escape (LINK_ESCAPE) - here replaceto contains the parameter (the URL itself)
 *    (when a LINK2_ESCAPE is returned, we do not need to modify formatting - that is already done)
 *    The returned replaceto may contain ESCAPE_CHAR_CLOSING_PARA, which need to be substituted to ')'.
 * - an escape resulting in no formatting, but also no characters (FORMATTING_OK)
 * - an invalid escape sequence (INVALID_ESCAPE)
 * - a non-format-changing escape sequence (e.g., \\) (NON_FORMATTING)
 * - an escape representing a shape (SHAPE_ESCAPE)
 * - just literal text up until the next escape (NON_ESCAPE)
 * - a "\n" escape (LINE_BREAK)
 * - a soft line break (SOFT_LINE_BREAK)
 * - a "\r(xxx)" escape (REFERENCE) - no lookups or replacement is offered if `references` is false
 * - a "\N" or "\r()" escape (NUMBERING) - the location of the number in this label
 * - a "\0x2{1aAiI}" escape (NUMBERING_FORMAT)
 * - a lone '\' at the end of the string (SOLO_ESCAPE)
 * - a "\Q" escape (QUOTATION)
 * - a hidden escape (HIDDEN)
 */
StringFormat::EEscapeType StringFormat::ProcessEscape(
    std::string_view input, size_t &length,
	bool resolve, bool apply, string *replaceto, const StringFormat *basic,
	Chart *chart, bool references, FileLineCol *linenum, bool sayIgnore)
{
    /** The message saying that the use of "\s" escape for small text is deprecated.*/
    static const char PER_S_DEPRECATED_MSG[] =
        "The use of '\\s' control escape to indicate small text is deprecated. Use '\\-' instead.";
    /** Maximum margin for text margins */
    static const double MAX_ESCAPE_M_VALUE = 500;
    /** The explanatory message for out-of bound margins */
    static const char TOO_LARGE_M_VALUE_MSG[] = "Use an integer between [0..500].";

    ColorType c;
    const ProcParamResolved* dollar = nullptr; //potential procedure parameter inside the parenthesis
    std::string maybe_s_msg;
    const Style *pStyle;
    const std::string_view errorAction = sayIgnore ? " Ignoring it." : " Keeping it as verbatim text.";
    size_t loc_len = 0; //length in characters, not bytes

    //If this is not an escape, search for the next escape
    if (input.length()==0 || input.front()!='\\') {
        length = 0;
        while (length<input.length() && input[length] && input[length]!='\\') {
            //If there is an unescaped ']' or '}' in the verbatim string give a warning
            if (linenum && chart && !references)
                if (input[length] == '}' || input[length] == ']')
                    chart->Error.Warning(FileLineCol(linenum->file, linenum->line, linenum->col+length),
                    string("'") + input[length] + "' character found in label without escape. Is this what you want?",
                    "Insert a '\\' in front of it to remove this warning.");
            length++;
        }
        loc_len = UTF8len(input.substr(0, length)); //length is in bytes, loc_len in characters
        if (replaceto) replaceto->assign(input.substr(0, length));
        if (linenum) linenum->col += loc_len;
        return NON_ESCAPE;
    }
    if (input.length()<2 || input[1]==0) {
        length = 1;
        if (replaceto) *replaceto = "\\";
        if (linenum) linenum->col += length;
        return SOLO_ESCAPE;
    }
    //First check for two-or three character escapes not taking an argument
    switch (input[1]) {
	case 0:      //End of string (string ends with single '\'), replace to quoted version
        _ASSERT(0); //handled above
        break;
    case '*':    //tilde escape
        length = 2;
        if (replaceto) replaceto->clear();
        if (linenum) linenum->col += length;
        return ASTERISK;

    case '-':    // small font
    case '+':    // normal font
    case '^':    // superscript
    case '_':    // subscript
        if (apply)
            fontType = (EFontType)string("+-^_").find(input[1]);
    ok2:
        loc_len = length = 2;
        //fallthrough
    ok:
        if (replaceto) replaceto->assign(input.substr(0, length));
        if (linenum) linenum->col += loc_len;
        return FORMATTING_OK;

    case 'b':    // bold font
    case 'B':
        if (apply)
            AddTristate(bold, OptAttr<ETriState>(input[1]=='B'?yes:invert));
        goto ok2; //valid formatting character of length 2

    case 'i':    // bold font
    case 'I':
        if (apply)
            AddTristate(italics, OptAttr<ETriState>(input[1]=='I'?yes:invert));
        goto ok2; //valid formatting character of length 2

    case 'u':    // bold font
    case 'U':
        if (apply)
            AddTristate(underline, OptAttr<ETriState>(input[1]=='U'?yes:invert));
        goto ok2; //valid formatting character of length 2

    case '0': //Line spacing mark
    case '1': //Line spacing mark
    case '2': //Line spacing mark
    case '3': //Line spacing mark
    case '4': //Line spacing mark
    case '5': //Line spacing mark
    case '6': //Line spacing mark
    case '7': //Line spacing mark
    case '8': //Line spacing mark
    case '9': //Line spacing mark
        if (apply)
            spacingBelow = input[1] - '0';
        goto ok2; //valid formatting character of length 2

    case 'p':
        EIdentType id;
        switch (input.size()>2 ? input[2] : 0) {
        case 'c': id = MSC_IDENT_CENTER; break;
        case 'l': id = MSC_IDENT_LEFT; break;
        case 'r': id = MSC_IDENT_RIGHT; break;
        default: id = MSC_IDENT_INVALID; break;
        }
        if (id == MSC_IDENT_INVALID) {
            if (chart && linenum && !references) {
                FileLineCol l = *linenum;
                l.col += 2;
                chart->Error.Warning(l, StrCat("Escape '\\p' shall be followed by one of 'lrc'.", errorAction));
            }
            loc_len = length = 2;
        nok:
            if (replaceto) replaceto->clear();
            if (linenum) linenum->col += loc_len;
            return INVALID_ESCAPE;
        }
        if (apply)
            ident = id;
        loc_len = length = 3;
        goto ok;

    case ESCAPE_CHAR_SOFT_NEWLINE: //soft line breaks: should disappear after splitting a parsedline into fragments
        length = 2;
        if (replaceto) replaceto->clear();
        if (linenum) linenum->col += 2;
        return SOFT_LINE_BREAK;

    case 'n':           // enter: should disappear after splitting a parsedline into fragments
        length = 2;
        if (replaceto) replaceto->clear();
        if (linenum) linenum->col += 2;
        return LINE_BREAK;

    case 'N': // location of label numbering
        length = 2;
        if (replaceto)
            *replaceto = "\\N";
        if (linenum) linenum->col += length;
        return NUMBERING;

    case ESCAPE_CHAR_NUMBERFORMAT:
        length = std::min(input.length(), size_t(3));
        if (input.length()<3 || !strchr("1aAiI", input[2])) { //not one of 1, a, A, i, I
            if (replaceto) replaceto->clear();
            if (chart && !references) chart->Error.Error(*linenum, "Internal error: bad number format escape.");
            return FORMATTING_OK;
        }
        if (replaceto) replaceto->assign(input.substr(0, length));
        //No change to linenum, these should always be followed by position escapes
        return NUMBERING_FORMAT;

    case ESCAPE_CHAR_WORD_WRAP:
        loc_len = length = std::min(input.length(), size_t(3));
        _ASSERT(input.length()>=3 && (input[2] == '+' || input[2] == '-'));
        if (input.length()>=3 && apply)
            word_wrap = (input[2] == '+');
        goto ok;

    case '\\':          // escaped "\"
    case '#':           // escaped "#"
    case '{':           // escaped "{"
    case '}':           // escaped "}"
    case '[':           // escaped "["
    case ']':           // escaped "]"
    case ';':           // escaped ";"
    case '\"':          // escaped quotation mark
        length = 2;
        if (replaceto) replaceto->assign(input.substr(1, 1));
        if (linenum) linenum->col += 2;
        return NON_FORMATTING;

    case '$':           // unicode
        if (input.length()>=6 &&
            strchr("0123456789ABCDEFabcdef", input[2]) &&
            strchr("0123456789ABCDEFabcdef", input[3]) &&
            strchr("0123456789ABCDEFabcdef", input[4]) &&
            strchr("0123456789ABCDEFabcdef", input[5])) {
            loc_len = length = 6;
            //copy the unicode char to an UTF-16 string
            uint16_t utf16=0;
            for (unsigned u = 2; u<6; u++)
                if ('0'<=input[u] && input[u]<='9') utf16 = utf16*16 + input[u]-'0';
                else if ('a'<=input[u] && input[u]<='f') utf16 = utf16*16 + input[u]-'a'+10;
                else if ('A'<=input[u] && input[u]<='F') utf16 = utf16*16 + input[u]-'A'+10;
            //Check if it is a control character
            if (utf16<0x20 || (0x7f<=utf16 && utf16<0xa0)) {
                if (chart && linenum && !references)
                    chart->Error.Warning(*linenum, "This is a control character - ignored.");
                if (replaceto) replaceto->clear();
                if (linenum) linenum->col += 6;
                return NON_FORMATTING;
            }
            //Convert to UTF-8
            std::string utf8 = ConvertFromUTF16_to_UTF8({(wchar_t*)&utf16,1}); //no BOM, assumes native
            if (replaceto) replaceto->assign(std::move(utf8));
            if (linenum) linenum->col += 6; //even if this collapses to one unicode char, in the input file it was represented by 6 ascii chars
            return NON_FORMATTING;
        }
        loc_len = length = 2;
        if (chart && linenum && !references)
            chart->Error.Error(*linenum, StrCat("Need exactly four hexacecimal digits after '\\$'.",errorAction));
        goto nok;

    case ESCAPE_CHAR_SPACE: //escaped space
        length = 2;
        if (replaceto) replaceto->assign(" ");
        if (linenum) linenum->col += 2;
        return NON_FORMATTING;

    case '|':          // zero-length non-formatting escape
        length = 2;
        if (replaceto) replaceto->clear();
        if (linenum) linenum->col += 2;
        return NON_FORMATTING;
    }

    if (!strchr(ESCAPE_STRING_LOCATION "cCsSflmrLQ" ESCAPE_STRING_NON_FORMATTING_LINK ESCAPE_STRING_HIDDEN, input[1])) {
        //Unrecognized escape comes here
        loc_len = length = 2;
        if (chart && linenum && !references)
            chart->Error.Error(*linenum, StrCat("Unrecognized escape: '", input.substr(0, 2), "'.", errorAction));
        goto nok;
    }

    //All the following escapes take a parameter in parenthesis
    bool was_m = (input[1] == 'm');

    if (input.length()<size_t(3+was_m) || input[was_m+2]!='(') {
        //report length of 3 if we have a \m escape followed by one of 'udlrins' (valid)
        loc_len = length = 2 + (was_m && input.length()>=3 && strchr("udlrins", input[2]));
        if (input[1] == 's') {
            maybe_s_msg = "Missing style name after \\s control escape. Assuming small text switch.";
        maybe_s: //we get here when it is \s, but not a valid style or ()s after
            if (replaceto) replaceto->assign("\\-");
            length = 2;
            if (apply)
                fontType = MSC_FONT_SMALL;
            if (chart && linenum && !references)
                chart->Error.Warning(*linenum, maybe_s_msg, PER_S_DEPRECATED_MSG);
            if (linenum) linenum->col += 2; //length==2 here
            return FORMATTING_OK;
        }
        //skip silently if a location escape: we have inserted a bad one???
        if (input[1] == ESCAPE_CHAR_LOCATION) {
            if (replaceto) replaceto->clear();
            if (chart && !references) chart->Error.Error(*linenum, "Internal error: no '(' after position escape.");
            return FORMATTING_OK;
        }

        if (chart && linenum && !references) {
            FileLineCol l = *linenum;
            l.col += 2+was_m;
            chart->Error.Error(l, StrCat("Missing parameter after ", input.substr(0, length),
                                         " control escape.", errorAction),
                             "This escape requires a parameter in parenthesis right after it.");
        }
        goto nok;
    }
    size_t end = input.find_first_of(')', was_m);
    if (end==std::string_view::npos) {
        if (input[1] == 's') {
            maybe_s_msg = "Missing closing parenthesis after \\s control escape. Assuming small text switch.";
            goto maybe_s;
        }
        //skip silently if a location escape: we have inserted a bad one???
        if (input[1] == ESCAPE_CHAR_LOCATION) {
            if (replaceto) replaceto->clear();
            if (chart && !references) chart->Error.Error(*linenum, "Internal error: no matching ')' for position escape.");
            return FORMATTING_OK;
        }
        if (chart && linenum && !references)
            chart->Error.Error(*linenum, StrCat("Missing closing parenthesis after ", input.substr(0, length),
                                                " control escape.", errorAction));
        length = input.length();
        loc_len = UTF8len(input); //length is characters
        goto nok;
    }
    length = end+1; //since we have both ( and ) found, we have at least () after the escape
    loc_len = UTF8len(input.substr(0, length)); //'length' is bytes, 'loc_len' is characters
    std::string parameter(input.substr(was_m+3, length-was_m-4)); //stuff inside parenthesis
    if (parameter.length() && parameter.front()=='$' && chart && !chart->SkipContent()) {
        dollar = chart->GetParameter(parameter);
        if (dollar==nullptr) {
            if (linenum) {
                bool alpha_only = true;
                for (const char *c = &parameter[1]; c<&parameter.back() && alpha_only; c++)
                    if (isalpha(*c)) continue;
                    else if (UTF8TrailingBytes(*c)) continue;
                    else
                        alpha_only = false;
                chart->Error.Error(*linenum, StrCat("Bad procedure parameter name '", parameter, "'. Ignoring escape sequence."),
                    alpha_only ? "" : "You cannot use string composition in the parameters of text formatting escapes. Use either a parameter name or a string.");
            }
            goto nok;
        } else
            parameter = dollar->value;
    }

    //start with escapes taking a string value as parameter
    switch (input[1]) {
    case ESCAPE_CHAR_LOCATION:
        if (replaceto) replaceto->clear();
        if (linenum) {
            FileLineCol l = *linenum;
            std::string_view s = input.substr(0, length);
            if (!l.Read(s) || s.length()) {
                if (chart && !references) chart->Error.Error(*linenum, "Internal error: could not parse position escape.");
                return FORMATTING_OK;
            }
            //'l' is updated with the new location
            *linenum = l;
        }
        return FORMATTING_OK;
    case 'L':
        if (replaceto) replaceto->assign(parameter);
        if (linenum) linenum->col += loc_len;
        return LINK_ESCAPE;
    case ESCAPE_CHAR_NON_FORMATTING_LINK:
        if (replaceto) replaceto->assign(parameter);
        if (linenum) linenum->col += loc_len;
        return LINK2_ESCAPE;
    case ESCAPE_CHAR_HIDDEN:
        if (replaceto) replaceto->clear();
        if (linenum) linenum->col += loc_len;
        return HIDDEN;
    case 'S':
        //parse and give errors
        if (chart && !chart->SkipContent()) {
            StringFormat::ShapeEscape shape;
            if (!shape.Parse(input, CharByteIndex{}, linenum ? *linenum : FileLineCol(), chart ? &chart->Shapes : nullptr,
                             chart ? &chart->GetCurrentContext()->colors : nullptr, chart ? &chart->Error : nullptr, nullptr))
                goto nok;
            if (replaceto) replaceto->assign(shape.Print(chart->Shapes));
        } else if (replaceto)
            replaceto->assign(input.substr(0, length));
        if (linenum) linenum->col += loc_len;
        return SHAPE_ESCAPE;

    case 'c':
    case 'C':
        if (chart && chart->SkipContent())
            goto ok; //when parsing a procedure, silently accept any style & color name
        if (length==4) { // this is a "\c()" or a \C()
            if (basic == nullptr) {
                if (replaceto) {
                    if (input[1]=='c')
                        *replaceto = "\\c()";
                    else
                        *replaceto = "\\C()";
                }
                if (linenum) linenum->col += loc_len;
                return FORMATTING_OK;
            }
            //substitute parameter to the value from basic

            parameter = (input[1]=='c' ? basic->color : basic->bgcolor)->Print().substr(1);
            parameter.pop_back();
        }
        if (chart)
            c = chart->GetCurrentContext()->colors.GetColor(parameter); //consider color names and defs
        else if (apply || resolve) //try to resolve this if we are applying
            c = ColorType(parameter);  //just consider defs
        else	   //if we are just parsing (probably for csh) keep as is.
            goto ok;
        if (c.type!=ColorType::INVALID) {
            if (apply) {
                if (input[1]=='c') color = c;
                else bgcolor = c;
            }
            if (replaceto) replaceto->assign(StrCat(input.substr(0,2), c.Print()));
            if (linenum) linenum->col += loc_len;
            return FORMATTING_OK;
        }
        if (chart && linenum && !references) {
            FileLineCol l = *linenum;
            l.col += 3;
            chart->Error.Error(l, StrCat("Unrecognized color name or definition: '", parameter, "'.", errorAction));
        }
        goto nok;

    case 's':
        if (chart && chart->SkipContent())
            goto ok; //when parsing a procedure, silently accept any style & color name
        if (length==4) { // this is a "\s()"
            if (basic == nullptr) {
                if (replaceto) *replaceto="\\s()";
                if (linenum) linenum->col += loc_len;
                return FORMATTING_OK;
            }
            if (replaceto) replaceto->assign((*basic-*this).Print());
            if (linenum) linenum->col += loc_len;
            if (apply)
                *this += *basic;
            return FORMATTING_OK;
        }
        if (!chart) {
            if (apply || resolve) //drop silently if there is a style in a place where it should not be
                goto nok;
            else	   //if we are just parsing (probably for csh) keep as is.
                goto ok;
        }
        pStyle = chart->GetCurrentContext()->GetStyle4Read(parameter);
        if (pStyle==nullptr) {
            maybe_s_msg = StrCat("Unrecognized style '", parameter,
                                 "'. Treating style name as small text in parenthesis.");
            goto maybe_s;
        } else {
            const SimpleStyle *pSimpleStyle = dynamic_cast<const SimpleStyle *>(pStyle);
            if (pSimpleStyle && pSimpleStyle->f_text) {
                if (replaceto) replaceto->assign((pSimpleStyle->text-*this).Print());
                if (apply)
                    *this += pSimpleStyle->text;
            } else
                if (replaceto)
                    replaceto->clear();
            if (linenum) linenum->col += loc_len;
            return FORMATTING_OK;
        }

    case 'Q':
        //If we do not have a chart, we keep the \\Q verbatim.
        if (!chart) goto ok;
        if (!chart->SkipContent() && !chart->Reparsing()) {
            if (linenum)
                chart->Error.Error(*linenum, "You can only include parameter values inside a procedure. Ignoring escape sequence.");
            goto nok;
        }
        if (length==4) { // this is a "\Q()"
            if (linenum)
                chart->Error.Error(*linenum, "Missing procedure parameter name. Ignoring escape sequence.");
            goto nok;
        }
        if (!chart || chart->SkipContent()) {
            if (replaceto) replaceto->clear();
            if (linenum) linenum->col += loc_len;
            return QUOTATION;
        }
        //check if we have previously detected a $ in the parameter
        if (dollar==nullptr) {
            if (linenum)
                chart->Error.Error(*linenum, "Missing a procedure parameter name beginning with a '$' sign. Ignoring escape sequence.");
            goto nok;
        }
        //if we return replacement and have linenum,
        //replace linenum to the location of the parameter value and
        //return the parameter value followed by the original position coming after
        if (replaceto) {
            replaceto->assign(dollar->value);
            if (linenum) {
                replaceto->append(linenum->Print());
                *linenum = dollar->linenum_value;
            }
        } else if (linenum)
            linenum->col += loc_len;
        return QUOTATION;
    case 'r':
        if (length==4) { // this is a "\r()"
            if (replaceto)
                *replaceto = "\\N";
            if (linenum) linenum->col += loc_len;
            return NUMBERING;
        }
        //suggest the number
        if (replaceto && chart && references) {
            auto pRef = chart->GetReference(parameter);
            if (pRef) {
                *replaceto = pRef->number_text;
            } else if (linenum) {
                //here we did not find the reference
                chart->Error.Error(*linenum, "Unrecognized reference '" + parameter +
                                        "'. Ignoring it.", "References are case-sensitive.");
            }
        } else if (replaceto) {
            //If we could not replace the reference, we add a linenum here from LabelledArc::AddAttributeList()
            //(during the first call of this function), so that when we get back here later in
            //from LabelledArc::FinalizeLabels() we can have a valid line number, so any error above will be OK.
            if (linenum) {
                *replaceto = linenum->Print();
                replaceto->append(input.substr(0, length));
            } else {
                replaceto->assign(input.substr(0, length));
            }
        }
        if (linenum) linenum->col += loc_len;
        return REFERENCE;
    case 'f':
        if (length==4) { // this is a "\f()"
            if (basic == nullptr) {
                if (replaceto) *replaceto="\\f()";
                if (linenum) linenum->col += loc_len;
                return FORMATTING_OK;
            }
            //substitute parameter to the value from basic
            parameter = *basic->face;
        } else if (chart && linenum && !references) {
            FileLineCol l = *linenum;
            l.col += 3;
            string font_name = parameter;
            if (!Canvas::HasFontFace(font_name)) {
                if (font_name.length()) {
                    chart->Error.Warning(l, "Font '" + parameter + "' not found. "
                        "Using '"+font_name+"' instead.");
                    parameter = font_name;
                } else {
                    chart->Error.Error(l, "Font '" + parameter + "' not found "
                        "and no substitute available. Ignoring option.");
                    goto nok;
                }
            }
        }
        if (apply) {
            string font_name = parameter;
            Canvas::HasFontFace(font_name);
            if (font_name.length())
                face = font_name;
        }
        if (replaceto) replaceto->assign("\\f(" + parameter + ")");
        if (linenum) linenum->col += loc_len;
        return FORMATTING_OK;

    case 'l':
        if (length==4) { // this is a "\l()"
            if (basic == nullptr) {
                if (replaceto) *replaceto = "\\l()";
                if (linenum) linenum->col += loc_len;
                return FORMATTING_OK;
            }
            //substitute parameter to the value from basic
            parameter = *basic->lang;
        }
        if (apply) lang = parameter;
        if (replaceto) replaceto->assign("\\l(" + parameter + ")");
        if (linenum) linenum->col += loc_len;
        return FORMATTING_OK;

    case 'm':
        using BoolDouble = OptAttr<double>;
        BoolDouble StringFormat::* p = nullptr;
        int modifer = 0;
        switch (input[2]) {
        case 'u': p = &StringFormat::textVGapAbove; modifer = +2; break;
        case 'd': p = &StringFormat::textVGapBelow; modifer = +2; break;
        case 'l': p = &StringFormat::textHGapPre;   modifer = +4; break;
        case 'r': p = &StringFormat::textHGapPost;  modifer = +2; break;
        case 'i': p = &StringFormat::textVGapLineSpacing; break;
        case 'n': p = &StringFormat::normalFontSize; break;
        case 's': p = &StringFormat::smallFontSize; break;
        }
        if (!p) {
            if (chart && linenum && !references) {
                FileLineCol l = *linenum;
                l.col += 2;
                chart->Error.Warning(l, StrCat("Escape '\\m' shall be followed by one of 'udlrins'.",errorAction));
            }
            length = 2;
            goto nok;
        }
        if (parameter.length()==0) { //this is \mX()
            if (basic) {
                if (basic->*p) {
                    if (apply)
                        this->*p = basic->*p;
                    if (replaceto) {
                        replaceto->assign("\\m");
                        (*replaceto) << input[2] << "(" << *(basic->*p) << ")";
                    }
                    if (linenum) linenum->col += loc_len;
                    return FORMATTING_OK;
                }
	            if (replaceto) replaceto->clear();
            } else {
                if (replaceto) {
                    *replaceto = "\\mX()";
                    (*replaceto)[2] = input[2];
                }
            }
            if (linenum) linenum->col += loc_len;
            return FORMATTING_OK;
        }
        //OK, now we know we have a valid escape with a non-empty parameter, digest number
        size_t local_pos = 0;
        double val = 0;
        while (parameter.length()>local_pos && parameter[local_pos]>='0' && parameter[local_pos]<='9') {
            val = val*10 + parameter[local_pos]-'0';
            local_pos++;
        }
        if (parameter.length()>local_pos) {
            string msg = "Invalid value to the '\\m";
            msg += input[2];
            msg.append("' control escape: '").append(parameter).append("'.");
            if (val>MAX_ESCAPE_M_VALUE) {
                msg.append(" I could deduct '").append(parameter.substr(0, local_pos));
                msg.append("', but that seems too large.");
                if (sayIgnore)
                    msg.append(" Ignoring control escape.");
                else
                    msg.append(" Keeping escape as verbatim text.");
                if (chart && linenum && !references) {
                    FileLineCol l = *linenum;
                    l.col += 4;
                    chart->Error.Error(l, msg, TOO_LARGE_M_VALUE_MSG);
                }
                goto nok;
            }
            msg.append(" Using value '").append(parameter.substr(0, local_pos));
            msg.append("' instead.");
            if (chart && linenum && !references) {
                FileLineCol l = *linenum;
                l.col += 4;
                chart->Error.Error(l, msg, TOO_LARGE_M_VALUE_MSG);
            }
        } else if (val>MAX_ESCAPE_M_VALUE) { //Ok here we successfully parsed the number
            string msg = "Too large value after the '\\m";
            msg += input[2];
            msg.append("' control escape: '").append(parameter).append("'.");
            if (sayIgnore)
                msg.append(" Ignoring control escape.");
            else
                msg.append(" Keeping escape as verbatim text.");
            if (chart && linenum && !references) {
                FileLineCol l = *linenum;
                l.col += 4;
                chart->Error.Error(*linenum, msg, TOO_LARGE_M_VALUE_MSG);
            }
            goto nok;
        }
        //OK, here good value we have
        if (apply) this->*p = val + modifer;
        //It is this complicated because it may be that we use just (the valid) part of parameter
        if (replaceto) replaceto->assign(StrCat(input.substr(0, 3), '(', parameter.substr(0, local_pos), ')'));
        if (linenum) linenum->col += loc_len;
        return FORMATTING_OK;
    } /* big switch */

    //fallthrough, but we should not be here
    _ASSERT(0);
    goto nok;
}

/** Tells if the string has any escape character or not*/
bool StringFormat::HasEscapes(std::string_view text)
{
    StringFormat sf;
    while (text.length() && text[0]) {
        size_t length;
        const EEscapeType t = sf.ProcessEscape(text.data(), length);
        if (t!=NON_ESCAPE && t!=SOLO_ESCAPE) return true;
        text.remove_prefix(length);
    }
return false;
}

/** Tells if the string has link escape character ('\\L') or not*/
bool StringFormat::HasLinkEscapes(std::string_view text)
{
    StringFormat sf;
    while (text.length() && text[0]) {
        size_t length;
        const EEscapeType t = sf.ProcessEscape(text.data(), length);
        if (t==LINK_ESCAPE || t==LINK2_ESCAPE) return true;
        text.remove_prefix(length);
    }
    return false;
}


/** Adds CSH entries to csh for each formatting escape and the verbatim text.
 * Malformed "\c" and "\s" arguments are assumed OK
 * @param [in] startloc The location of the first byte of `text` in the input file.
 * @param [in] text The text to process. This can contain UTF-8 characters.
 *                  If it has a zero before its end, we terminate processing there.
 * @param csh The object collecting the entries.
 * @returns what kind of hint shall we provide based on the cursor position in 'csh'.
 *               If we shall provide escape hints also set hintedStringPos to the escape (all of it),
 *               if we shall provide parameter hint, we set hintedStringPos to the parameter itself.
 * Remember, locations count characters, not bytes (one unicode characer may be multiple bytes)
 * the xxx_pos variables in this function count bytes. Also, location ranges contain the first
 * and last character of the range, so [first, last] and not like STL iterators, which are
 * [begin, end), with 'end' pointing beyond the last element.*/
EEscapeHintType StringFormat::ExtractCSH(CharByteIndex startloc, std::string_view text, Csh &csh)
{
    _ASSERT(text.length()<32000000U); //safety against negative numbers
    EEscapeHintType ret = HINTE_NONE;
    StringFormat sf;
    StringFormat::ShapeEscape shape;
    while (text.length() && text[0]) {
        size_t length;
        const EEscapeType escape = sf.ProcessEscape(text, length);
        const std::string_view escape_text = text.substr(0, length);
        const CharByteIndex escape_len = { .char_index = (int)UTF8len(escape_text), .byte_index = (int)length };
        const CshPos escape_loc{ startloc, startloc + escape_len };
        switch (escape) {
        case SOLO_ESCAPE:
            if (csh.cursor_pos==escape_loc.end_pos) {
                ret = HINTE_ESCAPE;
                csh.hintedStringPos = escape_loc;
            }
            FALLTHROUGH;
        case NON_ESCAPE:
            if (length==0)
                return ret; //len>actual string length - we are done
            FALLTHROUGH;
        default:
            csh.AddCSH(escape_loc, COLOR_LABEL_TEXT);
            break;
        case SHAPE_ESCAPE:
            shape.Parse(escape_text, startloc, FileLineCol(), csh.pShapes, nullptr, nullptr, &csh);
            FALLTHROUGH; //note that above we have added CSH to the param
        case NUMBERING_FORMAT:
        case NUMBERING:
        case LINE_BREAK:
        case SOFT_LINE_BREAK:
        case FORMATTING_OK:
        case ASTERISK:
        case LINK_ESCAPE:
        case LINK2_ESCAPE:
        case NON_FORMATTING:
        case REFERENCE:
        case QUOTATION:
        case INVALID_ESCAPE:
            //check if we have a parameter
            const CharByteIndex p = UTF8FindAnyOf(escape_text, '(');
            _ASSERT(p.byte_index <= (int)escape_text.length());
            //p is now the byte index of the first opening parenthesis in the parsed escape seq
            //or equals length (first char after the escape seq) if no opening parenthesis
            const bool ends_in_par = escape_text.ends_with(')');
            const int end = length - 1 - ends_in_par; //last byte index of the escape seq's parameter
            const CshPos param_pos = p.byte_index <= end
                ? CshPos{ escape_loc.first_pos + p.ShiftASCII(1), escape_loc.end_pos.ShiftASCII(ends_in_par ? -1 : 0)}
                : CshPos::invalid();
            if (escape == INVALID_ESCAPE)
                csh.AddCSH_Error(escape_loc, "Invalid escape sequence.");
            else if (param_pos.IsValid() && param_pos.char_length()) { //we have a parameter (at least an opening parenthesis
                csh.AddCSH(CshPos(escape_loc.first_pos, param_pos.first_pos), COLOR_LABEL_ESCAPE);
                //now color the parameter
                //for shape elements, we have already added CSH
                if (escape != SHAPE_ESCAPE) {
                    EColorSyntaxType t;
                    switch (escape_text[1]) {
                    case 's': t = COLOR_STYLENAME; break;
                    case 'Q': t = COLOR_PARAMNAME; break;
                    case 'S': _ASSERT(0); FALLTHROUGH;
                    default:  t = COLOR_ATTRVALUE; break;
                    }
                    csh.AddCSH(param_pos, t);
                }
                if (ends_in_par)
                    csh.AddCSH(CshPos(param_pos.end_pos, escape_loc.end_pos), COLOR_LABEL_ESCAPE);
            } else //Escape has no param - color all of it as escape
                csh.AddCSH(escape_loc, COLOR_LABEL_ESCAPE); //add label color for all escape

            //now calculate hints
            const ECursorRelPosType cursor_loc = csh.CursorIn(escape_loc);
            //we hint if we are inside an escape, or at the end (unless there is a closing para) (but not at the beginning, which is a '\'
            if (cursor_loc==CURSOR_IN || (!ends_in_par && cursor_loc==CURSOR_AT_END) ) {
                if (param_pos.IsValid() && csh.CursorIn(param_pos) >= CURSOR_AT_BEGINNING) {
                    //Cursor is in the param or at its end/beginning. Hint the param
                    csh.hintedStringPos = param_pos;
                    if (escape == SHAPE_ESCAPE) {
                        if (csh.CursorIn(shape.loc_name) >= CURSOR_AT_BEGINNING) {
                            ret = HINTE_PARAM_SHAPE; csh.hintedStringPos = shape.loc_name;
                        } else if (csh.CursorIn(shape.loc_height) >= CURSOR_AT_BEGINNING) {
                            ret = HINTE_PARAM_NUMBER; csh.hintedStringPos = shape.loc_height;
                        } else if (csh.CursorIn(shape.loc_fillcolor) >= CURSOR_AT_BEGINNING) {
                            ret = HINTE_PARAM_COLOR; csh.hintedStringPos = shape.loc_fillcolor;
                        } else
                            ret = HINTE_NONE;
                    } else switch (text[1]) {
                        case 'C':
                        case 'c': ret = HINTE_PARAM_COLOR; break;
                        case 's': ret = HINTE_PARAM_STYLE; break;
                        case 'S': ret = HINTE_NONE; break;
                        case 'Q': ret = HINTE_PARAM_PARAM; break;
                        case 'f': ret = HINTE_PARAM_FONT; break;
                        case 'r': ret = HINTE_PARAM_REF; break;
                        case ESCAPE_CHAR_NON_FORMATTING_LINK:
                        case 'L': ret = HINTE_PARAM_LINK; break;
                        default: _ASSERT(0); FALLTHROUGH;
                        case 'm': ret = HINTE_PARAM_NUMBER; break;
                    }
                } else if (CshPos esc_char_loc{ escape_loc.first_pos, escape_loc.first_pos + p } //char_p/byte_p are the position of an opening para or -its absence- the char after the escape
                           ; csh.CursorIn(esc_char_loc)>=CURSOR_IN) {
                    //cursor does not stand in a parameter
                    //->we hint the whole escape (up to any opening parenthesis)
                    ret = HINTE_ESCAPE;
                    csh.hintedStringPos = escape_loc;
                }
            }
        }
        text.remove_prefix(length);
        startloc = escape_loc.end_pos;
    }
    return ret;
}

/** Replaces style, color names and URL marks to actual definitions.
 * Definitions are pulled from in chart->Contexts.back()
 * and in chart->ReferenceNames.
 * Also performs syntax error checking and generates errors/warnings into chart->Error.
 * Replaces any "\L(<url>)" escapes to "\ ESCAPE_STRING_NON_FORMATTING_LINK (<url>)" after
 * applying the string formatting for URLs. ESCAPE_STRING_NON_FORMATTING_LINK escapes then
 * return LINK2_ESCAPE instead of LINK_ESCAPE from ProcessEscape(), thus we know we do not
 * need to apply the formatting again. (URL formatting is stored in basic->link_format)
 * @param text The text to process.
 * @param chart The chart to add errors to and a source for style, color and reference names.
 * @param [in] linenum The location of the first byte of `text` in the input file.
 * @param [in] basic The formatting to use for empty "\s()" "\c()" "\mX()" and "\f()" escapes.
 *                   Can be nullptr if not available, in this case the above escapes are left as is.
 *                   If not null, it must point to a fully specified StringFormat object.
 * @param [in] references True if the chart->References are complete. If false, "\r" escapes left intact.
 *                        If true, we emit error/warning messages only for "\r" substitution.
 * @param [in] ignore If true then in error messages we say we ignore the erroneous escape and also
 *                    remove it from the string, if not then we say we keep verbatim and we do so.
 * @param [in] textType The type of text we process. This is to generate the right errors.
 *                      NUMBER_FORMAT cannot contain "\N", whereas LABEL and TEXT_FORMAT cannot contain
 *                      numbering format token escapes ("\0x2{1aAiI}").
 * @param [in] remove_hidden If true, we remove HIDDEN escapes, else we keep them.
 * @param [in] asterisk_replacement Specifies what to replace the asterisk escape to.*/
void StringFormat::ExpandReferences(string &text, Chart* chart, FileLineCol linenum,
                                    const StringFormat *basic, bool references,
                                    bool ignore,  ETextType textType, bool remove_hidden,
                                    std::string_view asterisk_replacement)
{
    //We have three cases regarding linenum
    //1. it is a colon-label, in which case msc_process_colon_string() inserted
    //   a position escape to the beginning of the string, so the linenum we got
    //   here will be overridden
    //2. it is an unquoted string, which cannot contain escapes, hence neither
    //   errors, so the linenum we got as parameter here will not be used
    //3. it is a quoted string. In this case we do not start by a pos escape
    //   and we may need the linenum parameter to report an error. In this case
    //   we know that the linenum parameter points to the opening quotation mark
    //   so we increment it at the beginning - for cases #1 and #2 it does not matter
    linenum.col++;
    string::size_type pos=0;
    string replaceto;
    StringFormat sf, sf_with_links;
    if (basic)
        sf = *basic;
    FileLineCol beginning_of_URL;
    beginning_of_URL.MakeInvalid();
    string ignoreText = ignore?" Ignoring it.":"";
    while(text.length()>pos && text[pos]) {
        size_t length;
        const FileLineCol beginning_of_escape = linenum;
        switch (sf.ProcessEscape(text.substr(pos), length, true, true, &replaceto,
                                 basic, chart, references, &linenum, ignore)) {
        case FORMATTING_OK: //do nothing, just reflect
        case REFERENCE:     //a valid \r(xxx) escape. If we "references" is true we have the number
            break;          //if not, we keep the escape as is.
        case ASTERISK:
            replaceto = asterisk_replacement;
            break;
        case LINK_ESCAPE:
            //resolve formatting, if we have a style given to us.
            if (beginning_of_URL.IsInvalid()) {
                if (replaceto.length()) {
                    //replace \\L to ESCAPE_STRING_NON_FORMATTING_LINK
                    beginning_of_URL = beginning_of_escape;
                    if (basic) {
                        replaceto = "\\" ESCAPE_STRING_NON_FORMATTING_LINK "(" + replaceto + ")";
                        //insert the formatting needed for a link, plus a location escape
                        if (basic->link_format) {
                            sf_with_links = sf;
                            sf_with_links.Apply(basic->link_format->c_str());
                            replaceto.append(*basic->link_format + linenum.Print());
                        }
                        //sf will not contain the format added by link_format here, so
                        //we can use sf to restore formatting to at the end of the link.
                        //but sf_with_links will, so we can take their difference
                    } else
                        replaceto = "\\L(" + replaceto + ")";
                } else {
                    //problem: an empty first \\L
                    chart->Error.Error(beginning_of_escape, "Empty link ('\\L') escape - you need to specify a link. Ignoring escape.",
                        "You can specify a link using an \\L() escape with the link target, followed by the link text, terminated by an empty '\\L()' escape.");
                    replaceto.clear();
                }
            } else {
                if (replaceto.length()) {
                    //Error - two non-empty \\L escapes
                    chart->Error.Error(beginning_of_escape, "Non-empty link ('\\L') escape, use an empty one to terminate the link text. Assuming empty one.",
                        "You can specify a link using an \\L() escape with the link target, followed by the link text, terminated by an empty '\\L()' escape.");
                    chart->Error.Error(beginning_of_escape, beginning_of_URL, "Here is the first non-empty '\\L()' escape.");
                    replaceto.clear();
                    //fallthrough
                }
                //We are terminating an URL
                //restore formatting, if we have changed it prior
                if (!basic)
                    replaceto = "\\L(" + replaceto + ")";
                else if (basic->link_format)
                    replaceto = "\\" ESCAPE_STRING_NON_FORMATTING_LINK "()" +
                        (sf-sf_with_links).Print() + linenum.Print();
                else
                    replaceto = "\\" ESCAPE_STRING_NON_FORMATTING_LINK "()" + linenum.Print();
                beginning_of_URL.MakeInvalid();
            }
            break;
        case LINK2_ESCAPE:
            //here replaceto contains just the parameter
            //Add the escape back, but otherwise keep the escape
            replaceto = "\\" ESCAPE_STRING_NON_FORMATTING_LINK "("+replaceto+")";
            break;
        case INVALID_ESCAPE:
            if (ignore) break;  //replaceto is empty here, we will remove the bad escape
            FALLTHROUGH; //if we do not ignore: this will set replaceto to the bad escape
        case LINE_BREAK:        //keep \n as is
        case SOFT_LINE_BREAK:
        case NON_FORMATTING:
            replaceto.assign(text.c_str()+pos, length);
            break; //do not (yet) resolve: keep what is in input
        case SHAPE_ESCAPE:
            break; //use returned replaceto - we need the color expanded. Also if error in shape we skip this.
        case HIDDEN:
            if (remove_hidden) break; //remove it: use returned empty replaceto
            //just step over
            pos += replaceto.length();
            continue;
        case SOLO_ESCAPE:
            replaceto = "\\\\"; //replace to an escaped version: allows trouble-free string concatenation
            break;
        case NUMBERING_FORMAT:  //keep \x02{1aAiI} as is, if
            if (textType != NUMBER_FORMAT) {
                chart->Error.Error(beginning_of_escape, "Internal error: Number format escape in a label or text format.");
                replaceto.clear();
            }
            break;
        case NUMBERING:         //keep \N as is
            if (textType == NUMBER_FORMAT) {
                chart->Error.Error(beginning_of_escape, "The '\\N' escape can not be used for numbering formatting options. Ignoring it.");
                replaceto.clear();
            }
            break;
        case NON_ESCAPE:       //verbatim text. If numberformat search it
            if (textType != NUMBER_FORMAT) break;
            if (NumberingStyleFragment::FindReplaceNumberFormatToken(text, beginning_of_escape, pos, pos+length))
                //OK, a number descriptor found and has been replaced to
                //a numberformat escape. Re-parse verbatim text again
                continue;
            break;
        case QUOTATION: // valid \Q attribute. We need to re-parse replaceto again
            text.replace(pos, length, replaceto);
            //DO NOT update 'pos'
            continue;
        }
        text.replace(pos, length, replaceto);
        pos += replaceto.length();
    }
}

/** Returns the byte offset of first numbering format escape, -1 if none*/
int StringFormat::FindNumberingFormatEscape(std::string_view text)
{
    StringFormat sf;
    size_t pos = 0;
    while (text.length() && text[0]) {
        size_t length;
		if (NUMBERING_FORMAT == sf.ProcessEscape(text, length))
            return (int)pos;
        pos += length;
        text.remove_prefix(length);
    }
	return -1;
}

/** Pushes all location escapes ("\0x1(xxx)") with 'loc'.
 * Also prepends 'loc'.
 * Used when a string is quoted somewhere else. */
std::string StringFormat::PushPosEscapes(std::string_view text, const FileLineCol &loc, EInclusionReason r)
{
    StringFormat sf;
    std::string ret;
    while (text.length() && text[0]) {
        size_t length;
        if (FORMATTING_OK == sf.ProcessEscape(text, length))
            if (text[0]=='\\' && text[1]==ESCAPE_CHAR_LOCATION) {
                FileLineCol l;
                bool v = l.Read(text); //progresses text.begin()
                _ASSERT(v);
                if (!v)
                    text.remove_prefix(length); //to avoid infinite loops
                l.Push(loc, r);
                ret.append(l.Print());
                continue;

            }
        ret.append(text.substr(0, length));
        text.remove_prefix(length);
    }
    return ret;
}

/** Removes any potential leftover position escape ("\0x1(xxx)")
 * If 'l' is non-null, it is assumed to be the starting position of 'text'
 * and the location of the last character of 'text' is returned in
 * an UTF8 compliant way, honouring any changes in the location
 * represented by a location escape potentially in the middle of 'text'.
 * Invalid position escapes are simply removed, _ASSERTed in debug mode
 * Returns true if we have made a change to 'text'.*/
bool StringFormat::RemovePosEscapes(string &text, FileLineCol *l)
{
    StringFormat sf;
    size_t pos = 0;
    size_t last_col = 0;
    bool ret = false;
    while (pos < text.length() && text[pos]) {
        size_t length;
        switch (sf.ProcessEscape(text.substr(pos), length)) {
        case LINE_BREAK:      ///<A line break "\n"
        case SOFT_LINE_BREAK: ///<A soft line break (a newline in input file of a colon label)
            if (l) {
                last_col=l->col;
                l->line++;
                l->col = 1;
            }
            break;
        case FORMATTING_OK:
            if (text[pos]=='\\' && text[pos+1]==ESCAPE_CHAR_LOCATION) {
                if (l) {
                    std::string_view loc{text.data()+pos,length};
                    if (!l->Read(loc)) {
                        //on failure we treat these chars as UTF8
                        l->col += UTF8len(loc);
                        _ASSERT(0);
                    } //else we have just moved 'l'
                }
                text.erase(pos, length);
                ret = true;
                continue;
            }
            FALLTHROUGH;
        default:
            if (l) {
                //OK, this fragment is not a location escape and not a newline
                l->col += UTF8len(text.substr(pos,length));
            }
        }
        pos += length;
    }
    //Now substract one to match the last char of 'text' and not the one beyond.
    if (l) {
        if (l->col>1) l->col--;
        else l->col = last_col; //this may happen on empty text or if we end with a newline
    }
    return ret;
}

/** Removes a version of the string without any potential leftover position escape ("\0x1(xxx)")*/
std::string StringFormat::RemovePosEscapesCopy(std::string_view text, FileLineCol *l)
{
    std::string ret;
    size_t last_col = 0;
    ret.reserve(text.length());
    StringFormat sf;
    while (text.length() && text[0]) {
        size_t length;
        switch (sf.ProcessEscape(text, length)) {
        case SOFT_LINE_BREAK: ///<A soft line break (a newline in input file of a colon label)
            if (l) {
                last_col = l->col;
                l->line++;
                l->col = 1;
            }
            break;
        case FORMATTING_OK:
            if (text[0]=='\\' && text[1]==ESCAPE_CHAR_LOCATION) {
                if (l) {
                    if (!l->Read(text)) {
                        //on failure we treat these chars as UTF8
                        l->col += UTF8len(text.substr(0, length));
                        _ASSERT(0);
                    } //else we have just moved 'l'
                } else
                    text.remove_prefix(length);
                continue;
            }
            FALLTHROUGH;
        default:
            if (l) {
                //OK, this fragment is not a location escape and not a newline
                l->col += UTF8len(text.substr(0, length));
            }
        }
        ret.append(text.substr(0, length));
        text.remove_prefix(length);
    }
    //Now substract one to match the last char of 'text' and not the one beyond.
    if (l) {
        if (l->col>1) l->col--;
        else l->col = last_col; //this may happen on empty text or if we end with a newline
    }
    ret.reserve(ret.length());
    return ret;
}

/** Removes all escapes, except line breaks, which are replaced with "\n"*/
void StringFormat::ConvertToPlainText(string &text, std::string_view asterisk_replacement)
{
    StringFormat sf;
    size_t pos = 0;
    while (pos < text.length() && text[pos]) {
        size_t length;
        switch (sf.ProcessEscape(text.substr(pos), length)) {
        case NON_FORMATTING:
            if (length) {
                text.erase(pos, 1);
                pos += length-1;
            }
            break;
        case NON_ESCAPE:
            pos += length;
            break;
        case ASTERISK:
            text.replace(pos, length, asterisk_replacement);
            pos += asterisk_replacement.length();
            break;
        case LINE_BREAK:
        case SOFT_LINE_BREAK:
            text.replace(pos, length, "\n");
            pos ++;  //since the replaced text is of length 1
            break;
        default:
            text.erase(pos, length);  //all other escapes
            break;
        }
    }
}

/** Returns a version without all escapes, except line breaks, which are replaced with "\n"*/
std::string StringFormat::ConvertToPlainTextCopy(std::string_view text, std::string_view asterisk_replacement)
{
    std::string ret;
    ret.reserve(text.length());
    StringFormat sf;
    while (text.length() && text[0]) {
        size_t length;
        switch (sf.ProcessEscape(text, length)) {
        case NON_FORMATTING:
            if (length)
                ret.append(text.substr(1, length-1));
            break;
        case NON_ESCAPE:
            ret.append(text.substr(0, length));
            break;
        case ASTERISK:
            ret.append(asterisk_replacement);
            break;
        case LINE_BREAK:
        case SOFT_LINE_BREAK:
            ret.append("\n");
            break;
        default: //all other escapes
            break;
        }
        text.remove_prefix(length);
    }
    return ret;
}

/** Replace hidden escapes (ESCAPE_CHAR_HIDDEN) to either their parameter or to a fixed string.
 * Returns true if a change has been made.*/
bool StringFormat::ReplaceHiddenEscapes(string & text, bool to_its_param, std::string_view to)
{
    bool ret = false;
    StringFormat sf;
    size_t pos = 0;
    while (pos < text.length() && text[pos]) {
        size_t length;
        if (HIDDEN == sf.ProcessEscape(text.substr(pos), length)) {
            ret = true;
            if (to_its_param) {
                text.erase(pos+length-1, 1);
                text.erase(pos, 2);
                pos += length-3;
            } else {
                text.replace(pos, length, to);
                pos += to.length();
            }
        } else
            pos += length;
    }
    return ret;
}

bool StringFormat::ReplaceAsteriskEscapes(string & text, std::string_view to)
{
    bool ret = false;
    StringFormat sf;
    size_t pos = 0;
    size_t length;
    while (pos < text.length() && text[pos])
        if (ASTERISK == sf.ProcessEscape(text.substr(pos), length)) {
            ret = true;
            text.replace(pos, length, to);
            pos += to.length();
        } else
            pos += length;
    return ret;
}

/** Find a substring (in the plain string) ignoring escapes.
 @param [in] decorated A string potentially containing escape sequences.
 @param [in] find The plain string to search for.
 @param [in] asterisk_replacement This is the string \* escapes shall be exchanged for
 @returns the byte position of the first occurrence of 'first' in 'decorated'.
          Don't forget that in 'decorated' the characters at the returned pos
          may be different from 'find', e.g., they may contain escapes, etc.
          But after removing the escapes, you get 'find'.*/
 size_t StringFormat::FindVerbatim(std::string_view decorated, std::string_view find,
                                   std::string_view asterisk_replacement)
 {
     StringFormat sf;
     size_t pos = 0;
     size_t segment_start = 0;  //The currently found beginning is in the segment started here
     size_t segment_offset = 0; //with this offset within the segment
     size_t matched_so_far = 0;
     std::string replaceto;
     while (pos<decorated.length() && decorated[pos]) {
         size_t length;
         switch (sf.ProcessEscape(decorated.substr(pos), length, false, false, &replaceto)) {
         case ASTERISK:
             replaceto = asterisk_replacement;
             break;
         case LINE_BREAK:
         case SOFT_LINE_BREAK:
             replaceto = "\n";
             break;
         default:
             break;
         }
         if (pos == segment_start && segment_offset >= replaceto.length()) {
             segment_start = pos += length;
             segment_offset = 0;
             continue;
         }
         const size_t off = pos == segment_start ? segment_offset : 0;
         auto match = std::mismatch(find.begin()+matched_so_far, find.end(), replaceto.begin()+off, replaceto.end());
         //if we have consumed all the string to find, we are done.
         if (match.first == find.end())
             return segment_start+segment_offset;
         if (match.second==replaceto.end()) {
             //we have consumed the whole of the replacement
             matched_so_far += replaceto.length()-off;
             pos += length;
         } else {
             //this part of the string do not match.
             //try from +1 char
             segment_offset++;
             matched_so_far = 0;
             pos = segment_start;
         }
     }
     return std::string::npos;
 }


/** Find the decorated string position of a character in the plain string.
 @param [in] decorated A string potentially containing escape sequences.
 @param [in] plain_pos The index of a character in the plain string
 @returns the byte position of the character in 'decorated' if 'plain_pos'
          denotes a character of the plain string (i.e. < plain.size()); or
          decorated.size() if plain_pos==plain.size(); or std::string::npos
          if plain_pos>plain.size().*/
 size_t StringFormat::FindPosInDecorated(std::string_view decorated, size_t plain_pos) {
     StringFormat sf;
     size_t dpos = 0, ppos = 0;
     std::string replaceto;
     while (dpos<decorated.length() && decorated[dpos]) {
         size_t length;
         const StringFormat::EEscapeType res =
             sf.ProcessEscape(decorated.substr(dpos), length, false, false, &replaceto);
         //move pos to after the piece analyzed
         dpos += length;
         _ASSERT(StringFormat::LINK_ESCAPE != res);
         if (StringFormat::FORMATTING_OK == res || StringFormat::SHAPE_ESCAPE == res ||
             StringFormat::LINK_ESCAPE == res || StringFormat::LINK2_ESCAPE == res) continue;
         //ignore link and shapes escapes - but nothing else
         ppos += replaceto.length();
         if (ppos>plain_pos)
             return dpos - (ppos-plain_pos);
     }
     if (ppos==plain_pos) return decorated.size();
     return std::string::npos;
 }

 /** Splits a string to sub-strings delineated by any of a certain set of characters.
 * In addition to splitting, it tracks the location inside the input file associated
 * with each chunk. The input string may contain location escapes, marking the
 * location of the following character - we follow these in an UTF8 compliant way.
 * Any other escape simply kept verbatim, and we don't find escaped characters, such
 * as "\{" or "\n". You also cannot search for "\n" or "\0".
 * If the text starts or ends with one of the separator characters, we start or end
 * the returned list with an empty string (or a string containing the separator character
 * depending on 'action').
 * Currently separator characters must be ASCII.
 * @param [in] text The input text. We stop at any zero character even if the view is longer.
 * @param [in] start The location of the first character of the input text. May be omitted or
 *                   invalid if the input text starts with a location escape right away.
 * @param [in] separator The list of characters acting as a separator.
 * @param [in] action A string containing one character for each character of 'separator'
 *                    telling what to do when splitting based on that char. '<' means
 *                    "append it to the end of the preceding string"; '>' means
 *                    "prepend it to the start of the following string". Any other char
 *                    or a shorter 'action' string means we ignore and drop the separator
 *                    character. The default is an empty action string meaning dropping
 *                    all and any separator character.*/
 StringWithPosList StringFormat::Split(std::string_view text, FileLineCol start,
                                       std::string_view separator, std::string_view action)
 {
     StringWithPosList blocks;
     char prev_sep = 0; //any previous separator to prepend
     StringFormat sf;

     size_t startpos = 0;
     while (text.length()>startpos && text[startpos]) {
         size_t length;
         switch (sf.ProcessEscape(text.substr(startpos), length)) {
         default:
             startpos += length;
             continue;
         case NON_ESCAPE:
             const auto pos = text.substr(0, startpos+length).find_first_of(separator, startpos); //limit search to verbatim text
             //if we did not find, just move on keeping startpos - maybe we find in a next verbatim fragment
             if (pos == std::string_view::npos) {
                 startpos += length;
                 continue;
             }
             //next_sep.first contains a char to append to the newly found block,
             //next_sep.second will contain a char to prepend to the next block after
             const auto next_sep = [&]()->std::pair<char, char> {
                 const auto sep_pos = separator.find(text[pos]);
                 if (action.length()<=sep_pos) return {0,0};
                 if (action[sep_pos]=='<') return {text[pos],0};
                 if (action[sep_pos]=='>') return {0,text[pos]};
                 return {0,0};
             }();
             std::string_view bname = text.substr(0, pos);
             auto &b = blocks.emplace_back(std::string(), FileLineColRange(start, start));
             b.name = StringFormat::RemovePosEscapesCopy(bname, &b.file_pos.end);
             if (prev_sep) {
                 b.name.insert(0, 1, prev_sep);
                 if (b.file_pos.start.col>0)
                     b.file_pos.start.col--;
             }
             if (next_sep.first) {
                 b.name.push_back(next_sep.first);
                 b.file_pos.end.col++;
             }
             prev_sep = next_sep.second;
             text.remove_prefix(pos+1);
             startpos = 0;
         }
     }
     if (startpos) {
         //leftover at the end
         auto &b = blocks.emplace_back(std::string(), FileLineColRange(start, start));
         b.name = StringFormat::RemovePosEscapesCopy(text, &b.file_pos.end);
         if (prev_sep) {
             b.name.insert(0, 1, prev_sep);
             if (b.file_pos.start.col>0)
                 b.file_pos.start.col--;
         }
     }
     return blocks;
 }



size_t StringFormat::Apply(std::string_view text)
{
    size_t length, pos=0;
	StringFormat basic(*this);
    while (text.length() && text[0]) {
        const EEscapeType t =
            ProcessEscape(text, length, true, true, nullptr, &basic);
        if (t != FORMATTING_OK && t != LINK_ESCAPE && t != LINK2_ESCAPE)
            break;
        text.remove_prefix(length);
        pos += length;
    }
    return pos;
}

void StringFormat::Apply(string &text)
{
    size_t start = 0;
    StringFormat basic(*this);
    while (start < text.length()) {
        EEscapeType t = NON_FORMATTING;
        size_t pos = 0;
        size_t length;
        while (start+pos<text.length()) {
            t = ProcessEscape(text.substr(start+pos), length, true, true, nullptr, &basic);
            if (t!=FORMATTING_OK) break;
            pos += length;
        }
        text.erase(start, pos);
        if (t != LINK_ESCAPE && t != LINK2_ESCAPE)
            break;
        else
            start += length;
    }
}


void StringFormat::SetColor(ColorType c)
{
    color = c;
}

StringFormat &StringFormat::operator +=(const StringFormat& toadd)
{
    if (toadd.fontType) fontType = toadd.fontType;
    if (toadd.color) {
        if (color)
            *color += *toadd.color;
        else
            color = toadd.color;
    }

    if (toadd.bgcolor) {
        if (bgcolor)
            *bgcolor += *toadd.bgcolor;
        else
            bgcolor = toadd.bgcolor;
    }

    if (toadd.face) face = toadd.face;
    if (toadd.lang) lang = toadd.lang;
    if (toadd.spacingBelow) {
        if (spacingBelow)
            *spacingBelow = std::max(*toadd.spacingBelow, *spacingBelow);
        else
            spacingBelow = toadd.spacingBelow;
    }
    AddTristate(bold, toadd.bold);
    AddTristate(italics, toadd.italics);
    AddTristate(underline, toadd.underline);

    if (toadd.textHGapPre) textHGapPre = toadd.textHGapPre;
    if (toadd.textHGapPost) textHGapPost = toadd.textHGapPost;
    if (toadd.textVGapAbove) textVGapAbove = toadd.textVGapAbove;
    if (toadd.textVGapBelow) textVGapBelow = toadd.textVGapBelow;
    if (toadd.textVGapLineSpacing) textVGapLineSpacing = toadd.textVGapLineSpacing;
    if (toadd.ident) ident = toadd.ident;
    if (toadd.normalFontSize) normalFontSize = toadd.normalFontSize;
    if (toadd.smallFontSize) smallFontSize = toadd.smallFontSize;
    if (toadd.word_wrap) word_wrap = toadd.word_wrap;
    if (toadd.link_format) link_format = toadd.link_format;

    return *this;
}

StringFormat &StringFormat::operator -=(const StringFormat& base)
{
    if (base.fontType == fontType) fontType.reset();
    if (base.color == color) color.reset();
    if (base.bgcolor == bgcolor) bgcolor.reset();
    if (base.face == face) face.reset();
    if (base.lang == lang) lang.reset();
    if (base.spacingBelow == spacingBelow) spacingBelow.reset();
    if (base.bold == bold) bold.reset();
    if (base.italics == italics) italics.reset();
    if (base.underline == underline) underline.reset();
    if (base.textHGapPre == textHGapPre) textHGapPre.reset();
    if (base.textHGapPost == textHGapPost) textHGapPost.reset();
    if (base.textVGapAbove == textVGapAbove) textVGapAbove.reset();
    if (base.textVGapBelow == textVGapBelow) textVGapBelow.reset();
    if (base.textVGapLineSpacing == textVGapLineSpacing) textVGapLineSpacing.reset();
    if (base.ident == ident) ident.reset();
    if (base.normalFontSize == normalFontSize) normalFontSize.reset();
    if (base.smallFontSize == smallFontSize) smallFontSize.reset();
    if (base.word_wrap == word_wrap) word_wrap.reset();
    if (base.link_format == link_format) link_format.reset();
    return *this;
}

string StringFormat::Print() const
{
    string ret;
    char fonttypes[] = "+-^_";
    if (fontType) ret << string("\\") + fonttypes[(unsigned)*fontType];
    if (color) ret << "\\c" + color->Print();
    if (bgcolor) ret << "\\C" + bgcolor->Print();
    if (face) ret << "\\f(" + *face + ")";
    if (lang) ret << "\\l(" + *lang + ")";
    if (spacingBelow) ret << string("\\") << std::clamp(int(*spacingBelow), 0, 9);

    if (bold) switch (*bold) {
    case no:     ret << "\\B\\b"; break;
    case yes:    ret << "\\B"; break;
    case invert: ret << "\\b"; break;
    }
    if (italics) switch  (*italics) {
    case no:     ret << "\\I\\i"; break;
    case yes:    ret << "\\I"; break;
    case invert: ret << "\\i"; break;
    }
    if (underline) switch (*underline) {
    case no:     ret << "\\U\\u"; break;
    case yes:    ret << "\\U"; break;
    case invert: ret << "\\u"; break;
    }

    if (ident) switch (*ident) {
    default:
    case MSC_IDENT_LEFT:   ret << "\\pl"; break;
    case MSC_IDENT_CENTER: ret << "\\pc"; break;
    case MSC_IDENT_RIGHT:  ret << "\\pr"; break;
    }

    if (textHGapPre) ret << "\\ml(" << *textHGapPre << ")";
    if (textHGapPost) ret << "\\mr(" << *textHGapPost << ")";
    if (textVGapAbove) ret << "\\mu(" << *textVGapAbove << ")";
    if (textVGapBelow) ret << "\\md(" << *textVGapBelow << ")";
    if (textVGapLineSpacing) ret << "\\mi(" << *textVGapLineSpacing << ")";
    if (normalFontSize) ret << "\\mn(" << *normalFontSize << ")";
    if (smallFontSize) ret << "\\ms(" << *smallFontSize << ")";
    if (word_wrap) ret << "\\" ESCAPE_STRING_WORD_WRAP << (*word_wrap ? "+" : "-");
    //link format is not written out

    return ret;
}

/** Take an attribute and apply it to us.
 *
 * We consider attributes ending with 'color', 'ident', 'format', 'font.face',
 * 'font.type', 'bold', 'italics', 'underline', 'gap.*', 'size.normal' and
 * 'size.small' or any style at the current context in `chart`.
 * We also accept the clearing of
 * an attribute if `t` is EStyleType::STYLE, that is for style definitions only.
 * At a problem, we generate an error into chart->Error.
 * @param [in] a The attribute to apply.
 * @param chart The chart we build.
 * @param [in] t The situation we set the attribute.
 * @returns True, if the attribute was recognized as ours (may have been a bad value though).*/
bool StringFormat::AddAttribute(const Attribute &a, Chart* chart, EStyleType t)
{
    if (a.type == EAttrType::STYLE)
        return false;
    if (a.EndsWith("color") || a.EndsWith("bgcolor")) {
        OptAttr<ColorType> &ref = a.EndsWith("bgcolor") ? bgcolor : color;
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                ref.reset();
            return true;
        }
        if (!chart->CheckColor(a)) return true;
        if (ref) *ref += chart->GetCurrentContext()->colors.GetColor(a.value);
        else ref = chart->GetCurrentContext()->colors.GetColor(a.value);
        return true;
    }
    if (a.EndsWith("ident")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                ident.reset();
            return true;
        }
        if (EIdentType i; a.type == EAttrType::STRING && Convert(a.value, i)) {
            ident = i;
            return true;
        } else
            a.InvalidValueError(CandidatesFor(i), chart->Error);
        return true;
    }
    if (a.EndsWith("format")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                Empty(); //remove all formatting
            return true;
        }
        string tmp = a.value;
        if (tmp.length()==0) return true;

        StringFormat::ExpandReferences(tmp, chart, a.linenum_value.start, this, false, true, TEXT_FORMAT, true);

        StringFormat sf(tmp);
        RemovePosEscapes(tmp);
        if (tmp.length()) {
            string s("Found some literal text: '");
            s.append(tmp).append("' in attribute '").append(a.name).append("'.");
            s.append(" Ignoring them.");
            chart->Error.Warning(a, true, s, "Attriute 'text.format' can only contain formatting escapes.");
            return true;
        }
        operator += (sf);
        return true;
    }
    if (a.EndsWith("font.face")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                face.reset();
            return true;
        }
        if (a.CheckType(EAttrType::STRING, chart->Error)) {
            string font_name = a.value;
            if (Canvas::HasFontFace(font_name))
                face = a.value;
            else if (font_name.length()) {
                face = font_name;
                chart->Error.Warning(a, true, "Font '" + StringFormat::RemovePosEscapesCopy(a.value.c_str()) +
                    "' not found. Using '"+font_name+"' instead.");
            } else {
                chart->Error.Error(a, true, "Font '" + StringFormat::RemovePosEscapesCopy(a.value.c_str()) +
                    "' not found and no substitute available. Ignoring option.");
            }
        }
        return true;
    }
    if (a.EndsWith("font.lang")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                lang.reset();
        } else if (a.CheckType(EAttrType::STRING, chart->Error))
            lang = a.value;
        return true;
    }
    if (a.EndsWith("font.type")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                fontType.reset();
            return true;
        }
        if (EFontType t;  a.type == EAttrType::STRING && Convert(a.value, t)) {
            fontType = t;
            return true;
        } else
            a.InvalidValueError(CandidatesFor(t), chart->Error);
        return true;
    }

    if (a.EndsWith("wrap")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                word_wrap.reset();
            return true;
        }
        if (a.CheckType(EAttrType::BOOL, chart->Error)) {
            word_wrap = a.yes;
            return true;
        }
        return true;
    }

    if (a.EndsWith("link_format")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                link_format.reset();
            return true;
        }
        string tmp = a.value;
        if (tmp.length()==0) return true;

        //completely remove all ambigous escapes, such as empty '\\c()' - use the
        //chart default as a fallback.
        StringFormat::ExpandReferences(tmp, chart, a.linenum_value.start,
            &chart->GetCurrentContext()->text, false, true, TEXT_FORMAT, true);
        string tmp_2(tmp);

        StringFormat sf(tmp);
        RemovePosEscapes(tmp);
        if (tmp.length()) {
            string s("Found some literal text: '");
            s.append(tmp).append("' in attribute '").append(a.name).append("'.");
            s.append(" Ignoring them.");
            chart->Error.Warning(a, true, s, "Attriute 'text.link_format' can only contain formatting escapes.");
            return true;
        }
        link_format = tmp_2;
        return true;
    }

    OptAttr<ETriState> *tri = nullptr;
    if (a.EndsWith("bold")) tri = &bold;
    else if (a.EndsWith("italic")) tri = &italics;
    else if (a.EndsWith("underline")) tri = &underline;
    //handle bold, italics and underline
    if (tri) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                tri->reset();
            return true;
        }
        if (a.CheckType(EAttrType::BOOL, chart->Error))
            *tri = a.yes ? yes : no;
        return true;
    }

    OptAttr<double> *dou = nullptr;
	bool all_gap = false;
	if (a.EndsWith("gap")) all_gap = true;
	else if (a.EndsWith("gap.up")) dou = &textVGapAbove;
	else if (a.EndsWith("gap.down")) dou = &textVGapBelow;
    else if (a.EndsWith("gap.left")) dou = &textHGapPre;
    else if (a.EndsWith("gap.right")) dou = &textHGapPost;
    else if (a.EndsWith("gap.spacing")) dou = &textVGapLineSpacing;
    else if (a.EndsWith("size.normal")) dou = &normalFontSize;
    else if (a.EndsWith("size.small")) dou = &smallFontSize;

    if (dou || all_gap) {
        if (a.type == EAttrType::CLEAR) {
			if (a.EnsureNotClear(chart->Error, t)) {
                if (all_gap) {
                    textVGapAbove.reset();
                    textVGapBelow.reset();
                    textHGapPost.reset();
                    textHGapPre.reset();
                } else
					dou->reset();
			}
            return true;
        }
        if (a.CheckType(EAttrType::NUMBER, chart->Error)) {
			if (all_gap)
				textVGapAbove = textVGapBelow =
				textHGapPost = textHGapPre = a.number;
			else
				*dou = a.number;
        }
        return true;
    }
    return false;

}

/** Add the attribute names we take to `csh`.*/
void StringFormat::AttributeNames(Csh &csh, std::string_view prefix)
{
    static const char * const names_descriptions[] =
    {"", nullptr,
    "color", "Set the color of the font",
    "bgcolor", "Set the color behind the text.",
    "ident", "Select left, right indented or centered text.",
    "format", "Use this attribute to set text format via formatting escapes, like '\\b'.",
    "font.face", "Select font face, such as 'Arial'.",
    "font.lang", "Select font language using the RFC-3066 language tags with optional sub-tags, such as 'hu', 'ja' or 'en-US'. This is needed to select the font file that has coverage for this language. Ignored on Windows.",
    "font.type", "Select between normal or small text, subscript or superscript.",
    "font.*", "Select font face, language and type.",
    "bold", "Select bold face.",
    "italic", "Select italics.",
    "underline", "Make the font underlined.",
    "gap.up", "Set the top margin of the label.",
    "gap.down", "Set the bottom margin of the label.",
    "gap.left", "Set the left margin of the label.",
    "gap.right", "Set the right margin of the label.",
    "gap", "Set all margins of the label to the same value. Leaves 'gap.spacing' unchanged.",
    "gap.spacing", "Set the extra space between the lines of a multiline label.",
    "gap.*", "Set margins and spacing.",
    "size.normal", "Set the height of the normal text (i.e., not superscript or small font) in pixels.",
    "size.small", "Set the height of small text (including superscript and subscript) in pixels.",
    "wrap", "Turning this on will make the text word wrapped. This also means that horizontal auto-scaling is off, so caution with 'hscape=auto;'",
    "link_format", "Specify here what formatting shall be applied to a link (\\L escape).",
    ""};
    csh.AddToHints(names_descriptions, csh.HintPrefix(COLOR_ATTRNAME).append(prefix), EHintType::ATTR_NAME);
}

/** Callback for drawing a symbol before text ident types in the hints popup list box.
* @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForTextIdent(Canvas *canvas, CshHintGraphicParam p, CshHintStore &)
{
    if (!canvas) return false;
    const EIdentType t = (EIdentType)(int)p;
    const static double sizePercentage[] = {50, 30, 60};
    const LineAttr line(ELineType::SOLID, ColorType(0, 0, 0), 1, ECornerType::NONE, 0);
    double y = floor(HINT_GRAPHIC_SIZE_Y*0.2)+0.5;
    double y_inc = ceil(HINT_GRAPHIC_SIZE_Y*0.3/(sizeof(sizePercentage)/sizeof(double)-1));
    for (size_t i = 0; i<sizeof(sizePercentage)/sizeof(double); i++) {
        double x1 = floor(HINT_GRAPHIC_SIZE_X*sizePercentage[i]/100.+0.5);
        double x2 = floor(HINT_GRAPHIC_SIZE_X*0.2);
        switch (t) {
        default:
        case MSC_IDENT_LEFT: x1 = x2+x1; break;
        case MSC_IDENT_CENTER: x2 = floor(HINT_GRAPHIC_SIZE_X*0.5)-x1/2; x1 = x1+x2; break;
        case MSC_IDENT_RIGHT: x2 = floor(HINT_GRAPHIC_SIZE_X*0.8); x1 = x2-x1; break;
        }
        canvas->Line(XY(x1, y+y_inc), XY(x2, y+y_inc), line);
        y += y_inc;
    }
    return true;
}

/** Callback for drawing a symbol before text ident types in the hints popup list box.
* @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForVIdent(Canvas *canvas, CshHintGraphicParam p, CshHintStore&)
{
    if (!canvas) return false;
    const EVIdentType t = (EVIdentType)(int)p;
    const LineAttr line(ELineType::SOLID, ColorType(0, 0, 0), 1, ECornerType::NONE, 0);
    double y = floor(HINT_GRAPHIC_SIZE_Y*0.2)+0.5;
    double y_inc = ceil(HINT_GRAPHIC_SIZE_Y*0.1);
    double y_span = HINT_GRAPHIC_SIZE_Y*0.6;
    switch (t) {
    default:
    case MSC_VIDENT_TOP: break;
    case MSC_VIDENT_CENTER: y += y_span/2 - y_inc;  break;
    case MSC_VIDENT_BOTTOM: y = HINT_GRAPHIC_SIZE_Y - y; y_inc *= -1; break;
    case MSC_VIDENT_JUSTIFIED: y_inc = y_span/2; break;
    }
    double x1 = floor(HINT_GRAPHIC_SIZE_X*0.2);
    double x2 = floor(HINT_GRAPHIC_SIZE_X*0.8);
    for (size_t i = 0; i<3; i++, y += y_inc)
        canvas->Line(XY(x1, y), XY(x2, y), line);
    return true;
}


/** Draws a symbol for text formatting escapes in the hints popup list box.
 * Draw the two texts concatenated, but such as text2 begins at 'percent' percent of the X range.
 * Helper to CshHintGraphicCallbackForEscapes() .*/
bool DrawTextForHint(Canvas *canvas, const std::string &text1, const std::string &text2, double percent=40)
{
    StringFormat sf;
    sf.Default();
    string ss("\\pl\\mn(");
    ss << (int)(HINT_GRAPHIC_SIZE_Y*0.8) << ")";
    sf.Apply(ss.c_str());
    canvas->Clip(0, HINT_GRAPHIC_SIZE_X, 0, HINT_GRAPHIC_SIZE_Y);
    ShapeCollection dummy;
    Label label1(text1, *canvas, dummy, sf);
    Label label2(text1+text2, *canvas, dummy, sf);
    const double w1 = label1.getTextWidthHeight().x;
    const double h2 = label2.getTextWidthHeight().y;
    label2.Draw(*canvas, dummy, HINT_GRAPHIC_SIZE_X*percent/100.-w1, HINT_GRAPHIC_SIZE_X,
        HINT_GRAPHIC_SIZE_Y/2-h2/2); //center vertically. Start text2 at 40% of X range
    canvas->UnClip();
    return true;
}

/** Callback for drawing a symbol before font types in the hints popup list box.
 * @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForFontType(Canvas *canvas, CshHintGraphicParam p, CshHintStore&)
{
    if (!canvas) return false;
    const EFontType t = (EFontType)((int)p-1);
    switch (t) {
        case MSC_FONT_NORMAL:      DrawTextForHint(canvas, {}, "M\\c(1,0,0)\\+Text", -30); break;
        case MSC_FONT_SMALL:       DrawTextForHint(canvas, {}, "M\\c(1,0,0)\\-Text", -30); break;
        case MSC_FONT_SUPERSCRIPT: DrawTextForHint(canvas, {}, "M\\c(1,0,0)\\^Text", -30); break;
        case MSC_FONT_SUBSCRIPT:   DrawTextForHint(canvas, {}, "M\\c(1,0,0)\\_Text", -30); break;
        default:
            return false;
    }
    return true;
}


/** Draws a symbol before some of the text formatting escapes in the hints popup list box.
* Helper to CshHintGraphicCallbackForEscapes() .*/
bool DrawFormattingEscape(Canvas *canvas, const char *format)
{
    return DrawTextForHint(canvas, "ABD", string(format)+"FGH");
}

/** Draws a symbol before literal escapes in the hints popup list box.
* Helper to CshHintGraphicCallbackForEscapes() .*/
bool DrawLiteral(Canvas *canvas, const char *literal)
{
    return DrawTextForHint(canvas, "", literal, 0);
}

/** Callback for drawing a symbol before text formatting escapes in the hints popup list box.
* @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForEscapes(Canvas *canvas, CshHintGraphicParam p, CshHintStore &csh)
{
    if (!canvas) return false;

    LineAttr line; //black, single line

    const double X = HINT_GRAPHIC_SIZE_X/10.;
    const double Y = HINT_GRAPHIC_SIZE_Y/10.;

    switch (unsigned(p)) {
    case 01: // \n
        canvas->Line(XY(9*X,   0), XY(9*X, 7*Y), line);
        canvas->Line(XY(9*X, 7*Y), XY(4*X, 7*Y), line);
        canvas->Line(XY(2*X, 7*Y), XY(6*X, 4*Y), line);
        canvas->Line(XY(2*X, 7*Y), XY(6*X, 10*Y), line);
        return true;
    case 02: // \b
    case 34: // \B
        return DrawFormattingEscape(canvas, "\\b");
    case 03: // \i
    case 35: // \I
        return DrawFormattingEscape(canvas, "\\i");
    case 04: // \u
    case 36: // \U
        return DrawFormattingEscape(canvas, "\\u");
    case 05: // \+
        return DrawFormattingEscape(canvas, "\\+");
    case 06: // \-
        return DrawFormattingEscape(canvas, "\\-");
    case 07: // \_
        return DrawFormattingEscape(canvas, "\\_");
    case  8: // \^
        return DrawFormattingEscape(canvas, "\\^");
    case 12: // \p*
    case  9: // \pc
        return CshHintGraphicCallbackForTextIdent(canvas, (CshHintGraphicParam)(int)MSC_IDENT_CENTER, csh);
    case 10: // \pl
        return CshHintGraphicCallbackForTextIdent(canvas, (CshHintGraphicParam)(int)MSC_IDENT_LEFT, csh);
    case 11: // \pr
        return CshHintGraphicCallbackForTextIdent(canvas, (CshHintGraphicParam)(int)MSC_IDENT_RIGHT, csh);
    case 13: // \c()
        return DrawFormattingEscape(canvas, "\\c(1,0,0)");
    case 14: // \C()
        return DrawFormattingEscape(canvas, "\\C(1,0,0)");
    case 15: // \s()
        return CshHintGraphicCallbackForStyles(canvas, 0, csh);
    case 16: // '\\'
        return DrawLiteral(canvas, "\\");
    case 17: // \#
        return DrawLiteral(canvas, "#");
    case 18: // \{
        return DrawLiteral(canvas, "{");
    case 19: // \}
        return DrawLiteral(canvas, "}");
    case 20: // \[
        return DrawLiteral(canvas, "[");
    case 21: // \]
        return DrawLiteral(canvas, "]");
    case 22: // \;
        return DrawLiteral(canvas, ";");
    case 23: // \\"
        return DrawLiteral(canvas, "\"");
    case 24: // \r()
        return DrawLiteral(canvas, "Ref");
    case 25: // \f()
        return DrawFormattingEscape(canvas, "\\f(Courier New)");
    case 32: // \m*()
    case 26: // \mu()
    case 27: // \md()
    case 28: // \ml()
    case 29: // \mr()
    case 30: // \mn()
    case 31: // \ms()
        return true;
    case 33: // \N
        return DrawTextForHint(canvas, "(", "1)");
    case 37: // \0
    case 38: // \1
    case 39: // \2
    case 40: // \3
    case 41: // \4
    case 42: // \5
    case 43: // \6
    case 44: // \7
    case 45: // \8
    case 46: // \9
        canvas->Line(XY(5*X, 3*Y), XY(5*X, 7*Y), line);
        canvas->Line(XY(5*X, 3*Y), XY(4*X, 4*Y), line);
        canvas->Line(XY(5*X, 3*Y), XY(6*X, 4*Y), line);
        canvas->Line(XY(5*X, 7*Y), XY(4*X, 6*Y), line);
        canvas->Line(XY(5*X, 7*Y), XY(6*X, 6*Y), line);
        //line.width.value = 1.5;
        canvas->Line(XY(2*X, 3*Y), XY(8*X, 3*Y), line);
        canvas->Line(XY(2*X, 7*Y), XY(8*X, 7*Y), line);
        return true;
    case 47: // \|
        return DrawTextForHint(canvas, "a", "|b");
    case 48: // \L()
        return DrawLiteral(canvas, "\\c(0,0,1)\\uhttp");
    case 49: // \S()
        return DrawLiteral(canvas, "Shape");
    case 50: {// \$
        const wchar_t phi[4] = {0x3a8, 0};
        const std::string buff = ConvertFromUTF16_to_UTF8(phi);
        DrawLiteral(canvas, buff.c_str());
        return true;
    }
    case 51: // \Q()
        return DrawLiteral(canvas, "\\mn(40)\"");
    };
    return false;
}

/** Adds the applicable escapes to hints.
 * @param csh The Csh to add to.
 * @param prefix Use this as HintPrefix.*/
void StringFormat::EscapeHints(Csh &csh, std::string_view prefix)
{
    //We escape the initial escape so that a decorated->plain conversion keeps a literal '\' at the font
    static const char * const names_descriptions[] ={ "", nullptr,
/*01*/  "\\\\n", "Insert a manual line break. This line break will be honoured even with word wrapping turned on.",
/*02*/  "\\\\b", "Toggle bold font.",
/*03*/  "\\\\i", "Toggle italics font.",
/*04*/  "\\\\u", "Toggle font underline.",
/*05*/  "\\\\+", "Change to normal font (back from superscript, subscript or small font).",
/*06*/  "\\\\-", "Change to small font. (Revert to normal font via '\\+'.)",
/*07*/  "\\\\_", "Change to subscript. (Revert to normal font via '\\+'.)",
/*08*/  "\\\\^", "Change to superscript. (Revert to normal font via '\\+'.)",
/*09*/  "\\\\pc", "Center the text.",
/*10*/  "\\\\pl", "Ident the text left.",
/*11*/  "\\\\pr", "Ident the text right.",
/*12*/  "\\\\p*", "Set paragraph ident.",
/*13*/  "\\\\c()", "Set font color. Omitting the color name will restore the default color of the label (in effect at its beginning).",
/*14*/  "\\\\C()", "Set background color. Omitting the color name will restore the default background color of the label (in effect at its beginning).",
/*15*/  "\\\\s()", "Apply a style. Omitting the style name will restore the default formatting of the label (in effect at its beginning).",
/*16*/  "\\\\\\", "Insert a literal '\\'.",
/*17*/  "\\\\#", "Insert a literal '#'.",
/*18*/  "\\\\{", "Insert a literal '{'.",
/*19*/  "\\\\}", "Insert a literal '}'.",
/*20*/  "\\\\[", "Insert a literal '['.",
/*21*/  "\\\\]", "Insert a literal ']'.",
/*22*/  "\\\\;", "Insert a literal semicolon (';').",
/*23*/  "\\\\\"", "Insert a literal quotation mark ('\"').",
/*24*/  "\\\\r()", "Paste the number of another chart element name by its 'refname' attribute.",
/*25*/  "\\\\f()", "Set the font family of the text. Omitting the font name will restore the default font of the label (in effect at its beginning).",
/*26*/  "\\\\mu()", "Set the top margin of the label in pixels.",
/*27*/  "\\\\md()", "Set the bottom margin of the label in pixels.",
/*28*/  "\\\\ml()", "Set the left margin of the label in pixels.",
/*29*/  "\\\\mr()", "Set the right margin of the label in pixels.",
/*30*/  "\\\\mn()", "Set the font height of normal font in pixels.",
/*31*/  "\\\\ms()", "Set the font height of small font, superscript and subscript in pixels.",
/*32*/  "\\\\m*()", "Set margins and font height.",
/*33*/  "\\\\N", "Use this escape to specify the location of the automatic numbering within a label. Useful if you want it somewhere else than the front of the label.",
/*34*/  "\\\\B", "Make the font bold (no change if already bold).",
/*35*/  "\\\\I", "Make the font italic (no change if already italic).",
/*36*/  "\\\\U", "Make the font underlined (no change if already so).",
/*37*/  "\\\\0", "Remove line spacing from below this line.",
/*38*/  "\\\\1", "Create one pixel of line spacing below this line.",
/*39*/  "\\\\2", "Create two pixels of line spacing below this line.",
/*40*/  "\\\\3", "Create three pixels of line spacing below this line.",
/*41*/  "\\\\4", "Create four pixels of line spacing below this line.",
/*42*/  "\\\\5", "Create five pixels of line spacing below this line.",
/*43*/  "\\\\6", "Create six pixels of line spacing below this line.",
/*44*/  "\\\\7", "Create seven pixels of line spacing below this line.",
/*45*/  "\\\\8", "Create eight pixels of line spacing below this line.",
/*46*/  "\\\\9", "Create nine pixels of line spacing below this line.",
/*47*/  "\\\\|", "Use this around the beginning of a label to separate initial formatting escapes into two groups. "
               "The escapes before this one will determine the default format of the label, used by empty '\\s()', '\\c()', etc. escapes to restore default style, color and so on. "
               "The escapes after this one will take effect from the first character, but will not be included in the default format.",
/*48*/  "\\\\L()", "Use this escape to mark the beginning and end of a link. At the beginning, specify the link target (e.g., URL) in between the parentheses. "
                   "At the end, specify an empty escape, e.g., '\\L(http://cnn.com)CNN website\\L()'.",
/*49*/  "\\\\S()", "Insert a shape in-line to the text. Specify its name, optionally its height in pixels and optionally its fill color (separated by commas).",
/*50*/  "\\\\$", "Specify a unicode character by exactly four hex digits, e.g., '\\$03a9'.",
/*51*/  "\\\\Q()", "Literally include (quote) the value of a parameter.",
""};
    csh.AddToHints(names_descriptions, csh.HintPrefix(COLOR_LABEL_ESCAPE).append(prefix), EHintType::ESCAPE,
        CshHintGraphicCallbackForEscapes, true);
}

/** Add a list of possible attribute value names to `csh` for attribute `attr`.*/
bool StringFormat::AttributeValues(std::string_view attr, Csh &csh)
{
    if (CaseInsensitiveEndsWith(attr, "color") ||
        CaseInsensitiveEndsWith(attr, "bgcolor")) {
        csh.AddColorValuesToHints(false);
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "ident")) {
        csh.AddToHints(EnumEncapsulator<EIdentType>::names, nullptr, csh.HintPrefix(COLOR_ATTRVALUE),
                       EHintType::ATTR_VALUE, CshHintGraphicCallbackForTextIdent);
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "format")||
        CaseInsensitiveEndsWith(attr, "link_format")) {
        if (!csh.hadEscapeHint)
            csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"\"format string\"",
                "Specify a format string using text escapes, like '\\b'. Do not use plain text (non-formatting characters or escapes).",
                EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "font.type")) {
        csh.AddToHints(EnumEncapsulator<EFontType>::names, nullptr, csh.HintPrefix(COLOR_ATTRVALUE),
                       EHintType::ATTR_VALUE, CshHintGraphicCallbackForFontType);
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "font.face")) {
        for (const auto &str : csh.params->fontnames)
            if (str.length() && str[0]!='@') {
                //Add font names containing space using quotation marks
                if (str.find_first_of(' ')==string::npos)
                    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRVALUE)+str, nullptr,
                        EHintType::ATTR_VALUE));
                else
                    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRVALUE)+"\""+str+"\"", nullptr,
                    EHintType::ATTR_VALUE));
            }
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "font.lang")) {
        static std::array<std::pair<const char*, const char*>, 11> languages = {{
            {"zh", "Chineese"},
            {"ka", "Georgian"},
            {"el", "Greek (modern)"},
            {"he", "Hebrew"},
            {"hi", "Hindi"},
            {"hu", "Hungarian"},
            {"ja", "Japanese"},
            {"ks", "Kashmiri"},
            {"ko", "Korean"},
            {"ku", "Kurdish"},
            {"ru", "Russian"},
        }};
        for (auto [code, lang] : languages)
            csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRVALUE)+code, lang, EHintType::ATTR_VALUE));
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"RFC 3066 style language codes",
                               "Use a 2 letter ISO language code optionally with a country, like 'en-US' to select a language.",
                               EHintType::ATTR_VALUE, false));

        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "bold") ||
        CaseInsensitiveEndsWith(attr, "italic") ||
        CaseInsensitiveEndsWith(attr, "wrap") ||
        CaseInsensitiveEndsWith(attr, "underline")) {
        csh.AddYesNoToHints();
        return true;
    }
	if (CaseInsensitiveEndsWith(attr, "gap")) {
		csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<number in pixels>",
							   "Set all margins of the label to the same pixel value.",
							   EHintType::ATTR_VALUE, false));
		return true;
	}
	if (CaseInsensitiveEndsWith(attr, "gap.up")) {
		csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<number in pixels>",
							   "Set the top margin of the label in pixels.",
							   EHintType::ATTR_VALUE, false));
		return true;
	}
	if (CaseInsensitiveEndsWith(attr, "gap.down")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<number in pixels>",
            "Set the bottom margin of the label in pixels.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "gap.left")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<number in pixels>",
            "Set the left margin of the label in pixels.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "gap.right")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<number in pixels>",
            "Set the right margin of the label in pixels.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "gap.spacing")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<number in pixels>",
            "Set the line spacing (the gap between lines) of the label in pixels.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "size.normal")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<number in pixels>",
            "Set the height of the normal text in pixels.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "size.small")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<number in pixels>",
            "Set the height of small text (inclusing subscript and superscript) in pixels.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    return false;
}


/** Adds numbering to a label.
 * By default to the beginning of it, unless "\N" escape directs it elsewhere.
 * If we find a "\N" escape in the label we replace that to `num` (for multiple "\N"s, if needed).
 * If we find no such escape, we add `pre_num_post` to the beginning,
 * but after the initial formatting strings.
 * @param label The label to add numbers to.
 * @param [in] num The number to replace "\N" with.
 * @param [in] pre_num_post The number to add to the beginning.*/
//This shall be called only after StringFormat::ExpandReferences on a label and both num strings
void StringFormat::AddNumbering(string &label, std::string_view num, std::string_view pre_num_post)
{
    if (label.length()==0) return;
	StringFormat sf;
	size_t pos= 0;
	size_t length;
    //skip over initial formatting & links
	while (pos < label.length() && label[pos]) {
        const EEscapeType t = sf.ProcessEscape(label.substr(pos), length);
        if (FORMATTING_OK != t && LINK2_ESCAPE != t && LINK_ESCAPE != t) break;
        pos += length;
    }
    bool number_added = false;
    size_t beginning_pos = pos;
    while (pos < label.length() && label[pos]) {
        if (NUMBERING == sf.ProcessEscape(label.substr(pos), length)) {
            label.replace(pos, length, num);
            length = num.length();
            number_added = true;
        }
        pos += length;
    }
    //Add number at the beginning, if not yet added somewhere
    if (!number_added)
        label.insert(beginning_pos, pre_num_post);
}

/** Apply the formatting in us to `canvas`.
 * After this putting text to the canvas will use
 * our formatting*/
void StringFormat::ApplyFontTo(Canvas &canvas) const
{
    _ASSERT(IsComplete());
    if (!canvas.GetContext()) return;
    static const std::string empty;
    canvas.SetFontFace(face.value_or(empty), lang.value_or(empty),
                       italics==ETriState::yes, bold==ETriState::yes);

    canvas.SetFontSize(*smallFontSize);
    cairo_font_extents(canvas.GetContext(), &smallFontExtents);
    canvas.SetFontSize(*normalFontSize);
    cairo_font_extents(canvas.GetContext(), &normalFontExtents);

    if (fontType == MSC_FONT_NORMAL) /*Normal font*/
        canvas.SetFontSize(*normalFontSize) ;
    else /* Small, subscript, superscript */
        canvas.SetFontSize(*smallFontSize);
    canvas.SetColor(*color);
}

/** Returns the width of spaces at the beginning or at the end of a string.
 * This is because cairo removes these spaces and sometimes we need
 * them.
 * @param [in] text The text in which we look for heading/trailing spaces.
 * @param canvas A canvas used to query font sizes
 * @param [in] front If true we return the width of heading spaces, else trailing.
 * @returns The width of the spaces in pixels. */
double StringFormat::spaceWidth(std::string_view text, Canvas &canvas, bool front) const
{
    if (!canvas.GetContext()) return 0;
    if (text.length()==0 || !canvas.fake_spaces) return 0;
    size_t s;
    if (front) {
        s = text.find_first_not_of(' ');
        if (s==text.npos) s=text.length();
    } else {
        s = text.find_last_not_of(' ');
        if (s==text.npos) return 0; //if it is spaces only, we only report front
        s = text.length() - s - 1;
    }
    if (s==0) return 0;
    ApplyFontTo(canvas);
    cairo_text_extents_t te;
	thread_safe_cairo_text_extents (canvas.GetContext(), "M N", &te); //this counts one more space then needed
    double  w = te.x_advance;
    thread_safe_cairo_text_extents (canvas.GetContext(), "MN", &te);
    w -= te.x_advance;
	return w*s;
}

double StringFormat::getCharHeight(Canvas &canvas) const
{
    ApplyFontTo(canvas);
    return normalFontExtents.ascent + normalFontExtents.descent;
}

double StringFormat::getFragmentWidth(const std::string &s, Canvas &canvas) const
{
    if (s.length()==0) return 0;
    ApplyFontTo(canvas);
    cairo_text_extents_t te;
    if (canvas.individual_chars_path) {
        double advance = 0;
        char tmp_stirng[10];
        for (unsigned i = 0; i<s.length(); /*nope*/) {
            unsigned from = i;
            unsigned till = i + UTF8TrailingBytes(s[i]) + 1;
            if (till>s.length())
                break;
            for (/*nope*/; i<till; i++)
                tmp_stirng[i-from] = s[i];
            tmp_stirng[till-from] = 0;
            thread_safe_cairo_text_extents(canvas.GetContext(), tmp_stirng, &te);
            advance += te.x_advance;
        }
        te.x_advance = advance;
    } else {
        thread_safe_cairo_text_extents (canvas.GetContext(), s.c_str(), &te);
    }
    return spaceWidth(s, canvas, true) + te.x_advance + spaceWidth(s, canvas, false);
}

double StringFormat::getFragmentWidth(const Shape &s, Canvas &canvas, double height) const
{
    if (height<0) {
        ApplyFontTo(canvas);
        height = fontType == MSC_FONT_NORMAL ? normalFontExtents.ascent : smallFontExtents.ascent;
    }
    XY sp = s.GetMax().Spans();
    return sp.x * (height/sp.y);
}

double StringFormat::getFragmentHeightAboveBaseLine(std::string_view s, Canvas &canvas) const
{
    if (s.length()==0) return 0;
    ApplyFontTo(canvas);
    switch(fontType.value_or(MSC_FONT_NORMAL)) {
    case MSC_FONT_NORMAL:  //normal font
        return normalFontExtents.ascent;
    case MSC_FONT_SMALL:  //Small font
        return smallFontExtents.ascent;
    case MSC_FONT_SUPERSCRIPT: //superscript - typeset 20% higher than normal font
        return normalFontExtents.ascent*1.2;
    case MSC_FONT_SUBSCRIPT: //subscript - typeset 20% lower than normal font
        return std::max(0., smallFontExtents.ascent-normalFontExtents.ascent*0.2);
    }
    return 0;
}

double StringFormat::getFragmentHeightAboveBaseLine(const Shape&, double height, Canvas & canvas) const
{
    if (height<0) {
        ApplyFontTo(canvas);
        height = fontType == MSC_FONT_NORMAL ? normalFontExtents.ascent : smallFontExtents.ascent;
    }
    switch (fontType.value_or(MSC_FONT_NORMAL)) {
    case MSC_FONT_NORMAL:  //normal font
        return height;
    case MSC_FONT_SMALL:  //Small font
        return height;
    case MSC_FONT_SUPERSCRIPT: //superscript - typeset 20% higher than normal font
        return normalFontExtents.ascent*0.2 + height;
    case MSC_FONT_SUBSCRIPT: //subscript - typeset 20% lower than normal font
        return std::max(0., height-normalFontExtents.ascent*0.2);
    }
    return 0;
}

double StringFormat::getFragmentHeightBelowBaseLine(std::string_view s, Canvas &canvas) const
{
    if (s.length()==0) return 0;
    ApplyFontTo(canvas);
    switch(fontType.value_or(MSC_FONT_NORMAL)) {
    case MSC_FONT_NORMAL:  //normal font
        return normalFontExtents.descent;
    case MSC_FONT_SMALL:  //Small font
        return smallFontExtents.descent;
    case MSC_FONT_SUPERSCRIPT: //superscript - typeset 20% higher than normal font
        return std::max(0., smallFontExtents.descent - normalFontExtents.ascent*0.2);
    case MSC_FONT_SUBSCRIPT: //subscript - typeset 20% lower than normal font
        return smallFontExtents.descent + normalFontExtents.ascent*0.2;
    }
    return 0;
}

double StringFormat::getFragmentHeightBelowBaseLine(const Shape&, double height, Canvas & canvas) const
{
    if (height<0) {
        ApplyFontTo(canvas);
        height = fontType == MSC_FONT_NORMAL ? normalFontExtents.ascent : smallFontExtents.ascent;
    }
    switch (fontType.value_or(MSC_FONT_NORMAL)) {
    case MSC_FONT_NORMAL:  //normal font
    case MSC_FONT_SMALL:  //Small font
    case MSC_FONT_SUPERSCRIPT: //superscript - typeset 20% higher than normal font
        return 0;
    case MSC_FONT_SUBSCRIPT: //subscript - typeset 20% lower than normal font
        return normalFontExtents.ascent*0.2;
    }
    return 0;
}

/** Draw a piece of text with our (uniform) formatting.
 * All drawing is strictly in english. Left to right and ASCII only.
 * @param [in] s The text to output.
 * @param canvas The canvas to draw to.
 * @param [in] xy The starting point. `y` specifies the baseline of the text, not the
 *                upper left corner. (Baseline is not meant in cairo sense.)
 * @param [in] Y The y extent to draw for background. (Shall be scaled by the caller, taken verbatim.)
 * @param [in] isRotated If true, and the canvas cannot display rotated text,
 *                       the canvas will use cairo_text_path() as fallback.
 * @param [in] scale Outputs characters scaled like this.
 * @returns The y advancement (taking scale into account).*/
double StringFormat::drawFragment(const string &s, Canvas &canvas,
                                  XY xy, Range Y, bool isRotated,
                                  const XY &scale) const
{
    if (s.length()==0) return 0;
    const double advance = getFragmentWidth(s, canvas)*scale.x;

    xy.x += spaceWidth(s, canvas, true)*scale.x;

    if (bgcolor && !bgcolor->IsFullyTransparent()) {
        canvas.Fill(Block(Range(xy.x, xy.x+advance), Y), FillAttr::Solid(*bgcolor));
        //reset color to that of the font
        canvas.SetColor(*color);
    }

    switch (fontType.value_or(MSC_FONT_NORMAL)) {
    case MSC_FONT_NORMAL:  //normal font
    case MSC_FONT_SMALL:  //Small font
        break;
    case MSC_FONT_SUPERSCRIPT: //superscript - typeset 20% higher than normal font
        xy.y -= normalFontExtents.ascent*0.2*scale.y;
        break;
    case MSC_FONT_SUBSCRIPT: //subscript - typeset 20% lower than normal font
        xy.y += normalFontExtents.ascent*0.2*scale.y;
        break;
    }

	canvas.Text(xy, s, isRotated, scale);

    if (underline && *underline) {
        xy.y += scale.y; //'1' pixel if unscaled
        XY xy2(xy.x+advance, xy.y);
        canvas.Line(xy, xy2, LineAttr(ELineType::SOLID, *color, scale.y, ECornerType::NONE, 0)); //width of '1' if unscaled
    }
    return advance;
}

/** Draws a fragment that is a shape (with underline and all).
 * @param [in] s The shape to draw.
 * @param [in] height The height of the shape. If <0, the current text height is used.
 * @param [in] fillcolor The fill color to apply to the shape. Line color will be that of the text.
 * @param canvas The canvas to draw to.
 * @param [in] xy The starting point. `y` specifies the baseline of the text, not the
 *                upper left corner. (Baseline is not meant in cairo sense.)
 * @param [in] Y The y extent to draw for background. (Shall be scaled by the caller, taken verbatim.)
 * @param [in] isRotated If true, and the canvas cannot display rotated text,
 *                       the canvas will use cairo_text_path() as fallback.
 * @param [in] scale Outputs characters scaled like this.
 * @returns The y advancement (taking scale into account).*/
double StringFormat::drawFragment(const Shape &s, double height, ColorType fillcolor, Canvas &canvas,
                                  XY xy, Range Y, bool isRotated [[maybe_unused]], const XY &scale) const
{
    const double advance = getFragmentWidth(s, canvas, height)*scale.x;

    if (bgcolor && !bgcolor->IsFullyTransparent()) {
        canvas.Fill(Block(Range(xy.x, xy.x+advance), Y), FillAttr::Solid(*bgcolor));
        //reset color to that of the font
        canvas.SetColor(*color);
    }

    switch (fontType.value_or(MSC_FONT_NORMAL)) {
    case MSC_FONT_NORMAL:  //normal font
    case MSC_FONT_SMALL:  //Small font
        break;
    case MSC_FONT_SUPERSCRIPT: //superscript - typeset 20% higher than normal font
        xy.y -= normalFontExtents.ascent*0.2*scale.y;
        break;
    case MSC_FONT_SUBSCRIPT: //subscript - typeset 20% lower than normal font
        xy.y += normalFontExtents.ascent*0.2*scale.y;
        break;
    }

    Block b = s.GetMax();
    b.Scale(advance/b.x.Spans()); //scale.x is incorporated into 'advance'. b.x.Span() will be == advance after this.
    b.y.Scale(scale.y/scale.x);   //scale differently in y dimension.
    b.Shift(xy - b.LowerLeft());
    const LineAttr line(ELineType::SOLID, *color, (scale.x+scale.y)/2, ECornerType::NONE, 0);
    const FillAttr fill = FillAttr::Solid(fillcolor);
    s.Draw(canvas, b, line, fill);

    if (underline && *underline) {
        xy.y += scale.y;
        XY xy2(xy.x+advance, xy.y);
        canvas.Line(xy, xy2, LineAttr(ELineType::SOLID, *color, scale.y, ECornerType::NONE, 0));
    }
    return advance;
}

//////////////////////////////////////////////////////////////

/** Creates one element of an NCSA formatted ISMAP file */
string ISMapElement::Print() const
{
    string ret("rect ");
    ret << target << " ";
    ret << round(rect.x.from) << "," << round(rect.y.from) << " ";
    ret << round(rect.x.till) << "," << round(rect.y.till);
    return ret;
}


ParsedLine::ParsedLine(std::string_view in, Canvas &canvas, const ShapeCollection &shapes, StringFormat &format, bool h) :
    line(in), width(0), heightAboveBaseLine(0), heightBelowBaseLine(0), hard_new_line(h)
{
    //Remove heading and trailing whitespace.
    while (line.size() && line.front() == ' ') line.erase(0,1);
    while (line.size() && line.back() == ' ') line.erase(line.size()-1,1);
    format.Apply(line); //eats away initial formatting, so that we do not process them every time
    while (line.size() && line.front() == ' ') line.erase(0, 1);
    //but we keep link escapes among the initial formatting escapes.
    //'line' may start with escapes, like a link escape or non-formatting ones.
    startFormat = format;
    size_t pos = 0;
    size_t length;
    string replaceto;
    string fragment;
    while (line.length()>pos) {
        //collect characters up until we hit a valid formatting escape (or string end)
        while (line.length()>pos && line[pos]) {
            const StringFormat::EEscapeType res =
                format.ProcessEscape(line.substr(pos), length, true, false, &replaceto, &startFormat);
            if (res==StringFormat::FORMATTING_OK || StringFormat::SHAPE_ESCAPE == res) break;
            pos += length;
            _ASSERT(StringFormat::LINK_ESCAPE != res);
            if (StringFormat::LINK_ESCAPE == res || StringFormat::LINK2_ESCAPE == res) continue;
            fragment.append(replaceto);
        }
        //fragment can be empty due to labels beginning with link escapes & repeated formatting escapes
        if (fragment.length()) {
            //store fragment data
            width += format.getFragmentWidth(fragment, canvas);
            heightAboveBaseLine =
                std::max(heightAboveBaseLine, format.getFragmentHeightAboveBaseLine(fragment, canvas));
            heightBelowBaseLine =
                std::max(heightBelowBaseLine, format.getFragmentHeightBelowBaseLine(fragment, canvas));
            fragment.clear();
        }
        if (line.length()>pos && line[pos]) {
            const StringFormat::EEscapeType res =
                format.ProcessEscape(line.substr(pos), length, true, true, &replaceto, &startFormat);
            if (StringFormat::SHAPE_ESCAPE == res) {
                StringFormat::ShapeEscape shape;
                bool b = shape.Parse(line.c_str()+pos, CharByteIndex::invalid(), FileLineCol(), &shapes, nullptr, nullptr, nullptr);
                _ASSERT(b); //at this point we cannot have errors any longer
                if (b) {
                    width += format.getFragmentWidth(shapes[shape.shape], canvas, shape.height);
                    heightAboveBaseLine =
                        std::max(heightAboveBaseLine, format.getFragmentHeightAboveBaseLine(shapes[shape.shape], shape.height, canvas));
                    heightBelowBaseLine =
                        std::max(heightBelowBaseLine, format.getFragmentHeightBelowBaseLine(shapes[shape.shape], shape.height, canvas));
                }
            } else {
                _ASSERT(StringFormat::FORMATTING_OK == res);
            }
            pos += length;
        }
    }
    //If an empty line, add standard height
    if (line.length()==0) {
        fragment = "M"; //the functions below ignore the text, just return pre-stored values for non-empty stings
        heightAboveBaseLine =
            std::max(heightAboveBaseLine, format.getFragmentHeightAboveBaseLine(fragment, canvas));
        heightBelowBaseLine =
            std::max(heightBelowBaseLine, format.getFragmentHeightBelowBaseLine(fragment, canvas));
    }
    //Add spacing below. If there is spacingbelow, span a full height line
    if (heightAboveBaseLine == 0 && format.getSpacingBelow())
        heightAboveBaseLine = format.getFragmentHeightAboveBaseLine("M", canvas);
}

/** Set scaling for this line.*/
void ParsedLine::SetScale(double x, double y)
{
    width *= x/scale.x;
    heightAboveBaseLine *= y/scale.y;
    heightBelowBaseLine *= y/scale.y;
    scale = {x,y};
}

ParsedLine::operator std::string() const
{
    StringFormat format(startFormat);
    size_t pos = 0;
    size_t length;
    string replaceto;
    string ret;

    while (line.length()>pos && line[pos]) {
        const StringFormat::EEscapeType res =
            format.ProcessEscape(line.substr(pos), length, true, false, &replaceto, &startFormat);
        //move pos to after the piece analyzed
        pos += length;
        _ASSERT(StringFormat::LINK_ESCAPE != res);
        if (StringFormat::FORMATTING_OK == res || StringFormat::SHAPE_ESCAPE == res ||
            StringFormat::LINK_ESCAPE == res || StringFormat::LINK2_ESCAPE == res) continue;
            //do not copy link escapes - but all else
        ret.append(replaceto);
    }
    return ret;
}

void ParsedLine::CollectIsMapElements(XY xy, Canvas &canvas, const ShapeCollection &shapes, ISMap &ismap,
                                      string &target_at_front) const
{
    StringFormat format(startFormat);
    size_t pos = 0;
    size_t length;
    string replaceto;
    string fragment;

    ISMapElement elem;
    if (target_at_front.length()) {
        elem.target = target_at_front;
        elem.rect.x.from = xy.x;
        elem.rect.y.from = xy.y;
        target_at_front.clear();
    }

    xy.y += heightAboveBaseLine;

    while (line.length()>pos && line[pos]) {
        StringFormat::EEscapeType res = StringFormat::NON_ESCAPE;
        //collect characters up until we hit a valid formatting escape (or string end)
        while (line.length()>pos) {
            res = format.ProcessEscape(line.substr(pos), length, true, true, &replaceto, &startFormat);
            //move pos to after the piece analyzed
            pos += length;
            _ASSERT(StringFormat::LINK_ESCAPE != res);
            if (StringFormat::FORMATTING_OK == res || StringFormat::SHAPE_ESCAPE == res ||
                StringFormat::LINK_ESCAPE == res || StringFormat::LINK2_ESCAPE == res)
                break; // in this case 'format' already contains the effect of this escape
            //just append whatever we got - verbatim text
            fragment.append(replaceto);
        }
        if (fragment.length()) {
            xy.x += format.getFragmentWidth(fragment, canvas)*scale.x;
            fragment.clear();
        }
        if (StringFormat::SHAPE_ESCAPE == res) {
            StringFormat::ShapeEscape shape;
            if (shape.Parse(replaceto.c_str(), CharByteIndex::invalid(), FileLineCol(), &shapes, nullptr, nullptr, nullptr))
                xy.x += format.getFragmentWidth(shapes[shape.shape], canvas, shape.height)*scale.x;
        }
        if (StringFormat::LINK_ESCAPE == res || StringFormat::LINK2_ESCAPE == res) {
            if (elem.target.length()) {
                //If a second \L is non-empty -> we ignore parameter (so here we ignore 'replaceto')
                elem.rect.x.till = xy.x;
                elem.rect.y.till = xy.y + heightBelowBaseLine;
                ismap.push_back(std::move(elem));
                elem.target.clear();
            } else {
                if (replaceto.length()) {
                    elem.target = replaceto;
                    //replace ESCAPE_CHAR_CLOSING_PARA to actual closing parenthesis
                    std::replace(elem.target.begin(), elem.target.end(), ESCAPE_CHAR_CLOSING_PARA, ')');
                    elem.rect.x.from = xy.x;
                    elem.rect.y.from = xy.y - heightAboveBaseLine;
                }
                //just do nothing if we encounter a first empty '\L'. Such escape is to ignore.
            }
        }
    }
    //flush any remaining opened links.
    if (elem.target.length()) {
        target_at_front = elem.target;
        elem.rect.x.till = xy.x;
        elem.rect.y.till = xy.y + heightBelowBaseLine;
        ismap.push_back(std::move(elem));
    }
}

void ParsedLine::Draw(XY xy, Canvas &canvas, const ShapeCollection &shapes, bool isRotated) const
{
    StringFormat format(startFormat);
    size_t pos = 0;
    size_t length;
    string replaceto;
    string fragment;
    const Range Y(xy.y, xy.y+heightAboveBaseLine+heightBelowBaseLine); //scale.y is incorporated into 'Y' here

    xy.y += heightAboveBaseLine;

    while (line.length()>pos && line[pos]) {
        //collect characters up until we hit a valid shape, formatting escape (or string end)
        while (line.length()>pos) {
            const StringFormat::EEscapeType res =
                format.ProcessEscape(line.substr(pos), length, true, false, &replaceto, &startFormat);
            if (StringFormat::FORMATTING_OK == res || StringFormat::SHAPE_ESCAPE == res) break;
            pos += length;
            _ASSERT(StringFormat::LINK_ESCAPE != res);
            if (StringFormat::LINK_ESCAPE == res || StringFormat::LINK2_ESCAPE == res) continue;
            //do not draw link escapes
            fragment.append(replaceto);
        }
        if (fragment.length()) {
            //draw Fragment
            xy.x += format.drawFragment(fragment, canvas, xy, Y, isRotated, scale);
            fragment.clear();
        }
        //apply the formatting or draw the shape
        if (line.length()>pos) {
            const StringFormat::EEscapeType res =
                format.ProcessEscape(line.substr(pos), length, true, true, &replaceto, &startFormat);
            if (StringFormat::SHAPE_ESCAPE == res) {
                StringFormat::ShapeEscape shape;
                if (shape.Parse(replaceto.c_str(), CharByteIndex::invalid(), FileLineCol(), &shapes, nullptr, nullptr, nullptr))
                    xy.x += format.drawFragment(shapes[shape.shape], shape.height, shape.fillcolor, canvas,
                                                xy, Y, isRotated, scale);
            } else {
                _ASSERT(StringFormat::FORMATTING_OK == res);
            }
            pos += length;
        }
    }
}

//////////////////////////////////////////////////


size_t Label::Set(std::string_view input, Canvas &canvas, const ShapeCollection &shapes,
                  StringFormat format, const XY &scale)
{
    clear();
    while (input.length() && input[0]) {
        bool hard_line_break = false; //=false is just there to suppress a compilation warning
        //find next new line
        size_t pos = 0;
        size_t length = 0;
        while (pos < input.length() && input[pos]) {
            const auto ret = format.ProcessEscape(input.substr(pos), length);
            hard_line_break = ret == StringFormat::LINE_BREAK;
            if (ret == StringFormat::LINE_BREAK ||
                ret == StringFormat::SOFT_LINE_BREAK)
                break;
            pos += length;
            length = 0;
        }
        //Add a new line
        push_back(ParsedLine(input.substr(0, pos), canvas, shapes, format, hard_line_break)); //format changed here
        back().SetScale(scale.x, scale.y);
        input.remove_prefix(pos+length);
    }
    return this->size();
}

void Label::SetScale(double x, double y)
{
    for (auto &pl : *this)
        pl.SetScale(x, y);
}

Label::operator std::string() const
{
    string ret;
    if (size()>0)
        for (size_t i = 0; i<size(); i++)
            ret += at(i);
    return ret;
}

void Label::ApplyStyle(const StringFormat &sf)
{
    for (auto &line : *this)
        line.startFormat += sf;
}

void Label::EnsureMargins(double left, double right)
{
    if (size()==0) return;
    double lmin = DBL_MAX, rmin = DBL_MAX;
    for (const auto &l : *this) {
        if (lmin > *l.startFormat.textHGapPre)
            lmin = *l.startFormat.textHGapPre;
        if (rmin > *l.startFormat.textHGapPost)
            rmin = *l.startFormat.textHGapPost;
    }
    lmin = std::max(0., left-lmin);  //we need to add this much so that all margins are >= 'left'
    rmin = std::max(0., right-rmin); //we need to add this much so that all margins are >= 'right'
    if (lmin || rmin)
        for (auto &l : *this) {
            *l.startFormat.textHGapPre += lmin;
            *l.startFormat.textHGapPost += rmin;
        }
}


/* Get the width of a (potentially multi-line) text or one of its lines
*  If text is empty return 0
* We add spacings around. For an individual lines, we add the spacings above,
* but not the ones below. The result takes scaling into account and returns
* a scaled value.*/
XY Label::getTextWidthHeight(int line) const
{
    XY xy(0, 0);
    if (size()==0) return xy;
    //special case: a single line with no visible characters
    if (size() == 1 && front().getWidthHeight() == XY{ 0,0 }) return xy;
    if (line>=0) {
        if (size()<=unsigned(line)) return xy;
        xy = at(line).getWidthHeight();
        if (line == 0)  //First line
            xy.y += *front().startFormat.textVGapAbove*front().scale.y;
        else
            xy.y += (*at(line-1).startFormat.textVGapLineSpacing +
                     *at(line-1).startFormat.spacingBelow) *
                      at(line-1).scale.y;
    } else {
        for (size_t i = 0; i<size(); i++) {
            double x = at(i).getWidthHeight().x +
                           (*at(i).startFormat.textHGapPre +
                            *at(i).startFormat.textHGapPost) *
                             at(i).scale.x;
            if (xy.x < x)
                xy.x = x;
            xy.y += at(i).getWidthHeight().y;
            if (i!=size()-1)
                xy.y += (*at(i).startFormat.textVGapLineSpacing +
                         *at(i).startFormat.spacingBelow) *
                          at(i).scale.y;
        }
        xy.y += *front().startFormat.textVGapAbove*front().scale.y+
                *back().startFormat.textVGapBelow*back().scale.y;
    }
    return xy;
};

/** Apologies for the WTF, this is a list of letters and numbers. */
#define LETTERS_NUMBERS "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"
/** This is a list of punctuation marks of which we accept one
 * after a word at word wrapping. */
#define PUNCTUATION_MARKS_AFTER ",;.!?-+=/:)}]*&@%'\""
 /** This is a list of punctuation marks of which we accept one
  * before a word at word wrapping. */
#define PUNCTUATION_MARKS_BEFORE "({['\""

double Label::Reflow(Canvas &c, const ShapeCollection &shapes, double x)
{
    if (size()<1) return false;
    double overflow = 0;
    const XY scale = front().scale;
    std::vector<ParsedLine> reflown; //the resulted set of lines
    size_t lnum = 0; //the number of the line we process
    size_t pos = 0;  //the position within at(lnum)
    string line;     //the line we gather in the new flow
    string keep_with; //elements that shall stick to the next line just before a break
    double width = 0;//width of the text in "line"
    StringFormat start_format = front().startFormat;  //format at the beginning of "line"
    StringFormat running_format = start_format;
    double available = x - *start_format.textHGapPre
                         - *start_format.textHGapPost;
    do {
        while (pos < at(lnum).line.length() && at(lnum).line[pos]) {
            string replaceto;
            size_t length;
            switch (running_format.ProcessEscape(at(lnum).line.substr(pos), length,
                                                 true, true, &replaceto,
                                                 &front().startFormat)) {
            case StringFormat::FORMATTING_OK:
                //running format has already applied this formatting
                line.append(at(lnum).line.c_str()+pos, length); //now we copy it to the output.
                pos += length;
                break;
            case StringFormat::LINK2_ESCAPE:
                //keep it, do not change 'width' - it does not impact reflow
                //escapes with target stick to the word after them, empty ones to the word before
                (replaceto.length() ? keep_with : line).
                    append("\\" ESCAPE_STRING_NON_FORMATTING_LINK "(").append(replaceto).append(")");
                pos += length;
                break;
            case StringFormat::INVALID_ESCAPE:
            case StringFormat::REFERENCE:
            case StringFormat::LINK_ESCAPE:
            case StringFormat::LINE_BREAK:
            case StringFormat::SOFT_LINE_BREAK:
            case StringFormat::ASTERISK:
            case StringFormat::NUMBERING:
            case StringFormat::NUMBERING_FORMAT:
            case StringFormat::QUOTATION:
            case StringFormat::HIDDEN:
                _ASSERT(0);  //These should not come here
                pos+=length; //If they come we just ignore them
                break;
            case StringFormat::SHAPE_ESCAPE:
            {
                StringFormat::ShapeEscape shape;
                if (shape.Parse(replaceto.c_str(), CharByteIndex::invalid(), FileLineCol(), &shapes, nullptr, nullptr, nullptr)) {
                    const double shape_width = running_format.getFragmentWidth(shapes[shape.shape], c, shape.height)*scale.x;
                    if (shape_width + width <= available || line.length() == 0) {
                        //shape fits or we are at the beginning of the line - append to current line
                        line.append(replaceto);
                        width += shape_width;
                        overflow = std::max(overflow, width - available);
                    } else {
                        //shape has to move to a new line
                        reflown.push_back(ParsedLine(line, c, shapes, start_format, false));
                        reflown.back().SetScale(scale.x, scale.y);
                        start_format = running_format;
                        line = std::move(replaceto);
                        width = shape_width;
                        available = x - shape_width -
                                    (*start_format.textHGapPre + *start_format.textHGapPost) * scale.x;
                        overflow = std::max(overflow, width - available);
                    }
                }
                break;
            }
            case StringFormat::NON_FORMATTING:
            case StringFormat::NON_ESCAPE:
            case StringFormat::SOLO_ESCAPE:
                //Literal text. Calculate width using current format. Go word-by-word.
                size_t replaceto_pos = 0; //position within `replace_to`
                while (replaceto_pos < replaceto.length()) {
                    size_t replaceto_end = replaceto_pos;
                    while (replaceto_end < replaceto.length()) {
                        const size_t beginning_of_word = replaceto_end;
                        const bool had_punct_before =
                            std::string_view(PUNCTUATION_MARKS_BEFORE).
                            find(replaceto[beginning_of_word])!=std::string_view::npos;
                        replaceto_end = replaceto.find_first_not_of(LETTERS_NUMBERS,
                                                                    beginning_of_word+had_punct_before);

                        if (replaceto_end >= replaceto.length()) break; //can be true after find_first_not_of
                        //if we have found letters or numbers check for punctuation after
                        if (beginning_of_word+had_punct_before != replaceto_end &&
                            std::string_view(PUNCTUATION_MARKS_AFTER).
                            find(replaceto[replaceto_end])!=std::string_view::npos)
                            replaceto_end++; //step over
                        if (replaceto_end >= replaceto.length() || //can be true after find_first_not_of
                            (unsigned char)replaceto[replaceto_end] < 0x80) break; //non-letter ASCII char - end of word
                        //step beyond unicode char
                        replaceto_end += UTF8TrailingBytes(replaceto[replaceto_end])+1;
                    }
                    //here replaceto_end can be npos or equal to length (or larger if malformed UTF8) ) if a full word remains
                    if (replaceto_end == string::npos) //a single word remaining
                        replaceto_end = replaceto.length();
                    else if (replaceto_end == replaceto_pos) //non-letter first char: process a single char
                        replaceto_end++;
                    _ASSERT(replaceto_end <= replaceto.length()); //malformed UTF8, last char extends beyond 'length'
                    const string word = replaceto.substr(replaceto_pos, replaceto_end-replaceto_pos);
                    const double word_width = running_format.getFragmentWidth(word, c)*scale.x;
                    if (word_width + width <= available || line.length() == 0) {
                        //either the word fits or we are at the beginning of the
                        //line - add this word to the current line anyway
                        if (replaceto[replaceto_pos] != ' ' || line.length() != 0) {
                            //..but do not insert a space to the beginning of the line
                            if (keep_with.length()) {
                                line.append(keep_with);
                                keep_with.clear();
                            }
                            line.append(word);
                            width += word_width;
                            overflow = std::max(overflow, width - available);
                        }
                        replaceto_pos = replaceto_end;
                    } else {
                        // word has to move to a new line
                        reflown.push_back(ParsedLine(line, c, shapes, start_format, false));
                        reflown.back().SetScale(scale.x, scale.y);
                        start_format = running_format;
                        line.clear();
                        width = 0;
                        available = x - (*start_format.textHGapPre + *start_format.textHGapPost)*scale.x;
                    }
                } //while (processing `replaceto`)
                pos += length;
            } //switch
        } // while (processing at(lnum)
        //if line ended in hard line break, we break line here
        if (at(lnum).hard_new_line) {
            if (line.length() == 0)
                reflown.back().hard_new_line = true;
            else {
                reflown.push_back(ParsedLine(line, c, shapes, start_format, false));
                reflown.back().SetScale(scale.x, scale.y);
                start_format = running_format;
                line.clear();
                width = 0;
                available = x - (*start_format.textHGapPre + *start_format.textHGapPost)*scale.x;
            }
        }
        //if not at the beginning of a new line, add a space
        if (line.length() != 0) {
            line.append(" ");
            width += running_format.getFragmentWidth(" ", c);
        }
        lnum++;
        pos = 0;
        if (lnum < size())
            running_format = at(lnum).startFormat;
    } while (lnum < size());
    //Flush any remaining text to a last line
    if (line.length()) {
        reflown.push_back(ParsedLine(line, c, shapes, start_format, false));
        reflown.back().SetScale(scale.x, scale.y);
    }
    //swap reflown in
    std::vector<ParsedLine>::swap(reflown);
    return overflow;
}

double Label::getSpaceRequired(double def, int line) const
{
    if (size()==0) return 0;
    if (front().startFormat.word_wrap.value_or(false))
        return def;
    return getTextWidthHeight(line).x;
}


/** Create the cover of the label, draw it and/or collect the ISMap elements,
 * A convenience function, since all three are so similar.
 * The dimensions we enter here take scaling into account, so that
 * if you set scale = 0.5, you can also reduce, e.g., dx-sx.*/
void Label::CoverOrDrawOrISMap(Canvas *canvas, const ShapeCollection &shapes,
                               double sx, double dx, double y, double cx,
                               bool isRotated, Contour *area, ISMap *ismap) const
{
    if (empty()) return;
    if (canvas && !canvas->does_graphics()) {
        canvas->Add(GSText(*this, shapes, sx, dx, y, cx));
        canvas = nullptr; //do not draw below
    }

    XY xy;
    string target_at_front;
    xy.y = y + *front().startFormat.textVGapAbove*front().scale.y;
    for (size_t i = 0; i<size(); i++) {
        const XY wh = at(i).getWidthHeight(); //already scaled
        switch (*at(i).startFormat.ident) {
        case MSC_IDENT_LEFT:
        default:
            xy.x = sx + *at(i).startFormat.textHGapPre*at(i).scale.x;
            break;
        case MSC_IDENT_CENTER: {
            double w = wh.x + at(i).scale.x * (*at(i).startFormat.textHGapPre + *at(i).startFormat.textHGapPost);
            //if center, attempt to center around cx, but minimize extension beyond sx and dx
            if (cx<sx || cx>dx)  {                //if bad params, ignore cx
                xy.x = (sx + dx - w) / 2;
            } else if (w <= dx-sx) {              //if text fits between, try around cx, but do not let outside sx or dx
                xy.x = std::max(cx - w/2, sx);
                xy.x = std::min(xy.x, dx - w);
            } else {                              //if text does not fit anyway, do it around cx
                xy.x = cx - w/2;
            }
            xy.x += *at(i).startFormat.textHGapPre*at(i).scale.x;
            break;
            }
        case MSC_IDENT_RIGHT:
            xy.x = dx - wh.x - *at(i).startFormat.textHGapPost*at(i).scale.x; break;
        }
        //Draw line of text
        if (area)
            *area += Block(xy, xy+wh); //TODO: Make this finer if there are smaller text or italics...
        else if (ismap && canvas)
            at(i).CollectIsMapElements(xy, *canvas, shapes, *ismap, target_at_front);
        else if (canvas)
            at(i).Draw(xy, *canvas, shapes, isRotated);
        xy.y += wh.y +
            at(i).scale.y * (*at(i).startFormat.spacingBelow + *at(i).startFormat.textVGapLineSpacing);
        if (i==0)
            xy.y += first_line_extra_spacing*front().scale.y;
    }
}

void Label::CollectIsMapElements(ISMap &ismap, Canvas &canvas, const ShapeCollection &shapes,
                                 double sx, double dx, double y, double cx,
                                 const XY &c, double angle) const
{
    ISMap tmp_ismap;
    CollectIsMapElements(tmp_ismap, canvas, shapes, sx, dx, y, cx);
    for (auto &e: tmp_ismap) {
        e.rect = Contour(e.rect).RotateAround(c, angle).GetBoundingBox();
        ismap.push_back(std::move(e));
    }
}


void Label::CollectIsMapElements(ISMap &ismap, Canvas &canvas, const ShapeCollection &shapes,
                                 double s, double d, double t, ESide side, double c) const
{
    if (side==ESide::LEFT)
        t -= getTextWidthHeight().y;
    ISMap tmp_ismap;
    CollectIsMapElements(tmp_ismap, canvas, shapes, s, d, t, c);
    for (auto &e: tmp_ismap) {
        switch (side) {
        case ESide::LEFT:
            e.rect.Rotate90CW();
            e.rect.Shift(XY(2*t+getTextWidthHeight().y, 0));
            break;
        case ESide::RIGHT:
            e.rect.Rotate90CCW();
            e.rect.Shift(XY(0, s+d));
        break;
        default:
            break;
        }
        ismap.push_back(std::move(e));
    }
}

Contour Label::Cover(const ShapeCollection &shapes, double s, double d, double t, ESide side, double c) const
{
    if (side==ESide::LEFT)
        t -= getTextWidthHeight().y;
    Contour ret = Cover(shapes, s, d, t, c);
    switch (side) {
    case ESide::LEFT:
        ret.Rotate(90);
        ret.Shift(XY(2*t+getTextWidthHeight().y, 0));
        break;
    case ESide::RIGHT:
        ret.Rotate(-90);
        ret.Shift(XY(0, s+d));
        break;
    default:
        break;
    }
    return ret;
}

void Label::Draw(Canvas &canvas, const ShapeCollection &shapes, double s, double d, double t, ESide side, double c) const
{
    if (empty()) return;
    switch (side) {
    case ESide::LEFT:
        t -= getTextWidthHeight().y;
        canvas.Transform_Rotate90(2*t, getTextWidthHeight().y, false);
        break;
    case ESide::RIGHT:
        canvas.Transform_Rotate90(s, d, true);
        break;
    default:
        break;
    }
    Draw(canvas, shapes, std::min(s, d), std::max(s, d), t, c, side != ESide::END);
    if (side != ESide::END)
        canvas.UnTransform();
}
