Exemplo n.º 1
0
        /// <summary>
        ///     Outputs the specified coloured message, marked up using EggsML, to the console window, treating newlines as
        ///     paragraph breaks. All paragraphs are word-wrapped to fit in the console buffer, or to a sensible width if
        ///     redirected to a file. Each paragraph is indented by the number of spaces at the start of the corresponding
        ///     line.</summary>
        /// <param name="message">
        ///     The message to output.</param>
        /// <param name="hangingIndent">
        ///     Specifies a number of spaces by which the message is indented in all but the first line of each paragraph.</param>
        /// <remarks>
        ///     See <see cref="EggsNode.ToConsoleColoredStringWordWrap"/> for the colour syntax.</remarks>
        public static void WriteParagraphs(EggsNode message, int hangingIndent = 0)
        {
            int width;

            try
            {
                width = WrapToWidth();
            }
            catch
            {
                // Fall back to non-word-wrapping
                WriteLine(ConsoleColoredString.FromEggsNode(message));
                return;
            }
            bool any = false;

            foreach (var line in message.ToConsoleColoredStringWordWrap(width, hangingIndent))
            {
                WriteLine(line);
                any = true;
            }

            // Special case: if the input is empty, output an empty line
            if (!any)
            {
                Console.WriteLine();
            }
        }
Exemplo n.º 2
0
        /// <summary>Override; see base.</summary>
        protected override void OnTextChanged(EventArgs e)
        {
            // If a link has focus, trigger the LinkLostFocus event.
            // OnPaint will trigger the LinkGotFocus event as appropriate
            _keyboardFocusOnLink = null;

            _cachedPreferredSizes.Clear();
            _cachedRendering = null;
            base.OnTextChanged(e);
            _mnemonic = '\0';
            TabStop   = false;
            var origText = base.Text;

            try
            {
                _parsed    = EggsML.Parse(origText);
                ParseError = null;

                // We know there are no mnemonics or links in the exception message, so only do this if there was no parse error
                extractMnemonicEtc(_parsed);
            }
            catch (EggsMLParseException epe)
            {
                ParseError = epe;
                var msg = "";
                int ind = 0;
                if (epe.FirstIndex != null)
                {
                    ind  = epe.FirstIndex.Value;
                    msg += EggsML.Escape(origText.Substring(0, ind));
                    msg += "<Red>={0}=".Fmt(EggsML.Escape(origText.Substring(ind, 1)));
                    ind++;
                }
                msg += EggsML.Escape(origText.Substring(ind, epe.Index - ind));
                ind  = epe.Index;
                if (epe.Length > 0)
                {
                    msg += "<Red>={0}=".Fmt(EggsML.Escape(origText.Substring(ind, epe.Length)));
                    ind += epe.Length;
                }
                msg    += "<Red>= ← (" + EggsML.Escape(epe.Message) + ")=";
                msg    += EggsML.Escape(origText.Substring(ind));
                _parsed = EggsML.Parse(msg);
            }
            autosize();
            Invalidate();
        }
Exemplo n.º 3
0
        private void extractMnemonicEtc(EggsNode node)
        {
            // The only legal way to use & is as a tag containing a single character. For example:
            // &F&ile       (mnemonic is 'F')
            // O&p&en    (mnemonic is 'P')

            var tag = node as EggsTag;

            if (tag == null)
            {
                return;
            }
            if (tag.Tag == '&')
            {
                if (tag.Children.Count != 1 || !(tag.Children.First() is EggsText) || ((EggsText)tag.Children.First()).Text.Length != 1)
                {
                    throw new InvalidOperationException("'&' mnemonic tag must not contain anything other than a single character.");
                }
                _mnemonic = char.ToUpperInvariant(((EggsText)tag.Children.First()).Text[0]);
            }
            else if (tag.Tag == '{')
            {
                // Deliberately skip the inside of links: don’t wanna interpret their mnemonics as the main mnemonic
                TabStop = true;
            }
            else
            {
                foreach (var child in tag.Children)
                {
                    if (TabStop && _mnemonic != '\0')
                    {
                        return;
                    }
                    extractMnemonicEtc(child);
                }
            }
        }
Exemplo n.º 4
0
        /// <summary>
        ///     Outputs the specified coloured message, marked up using EggsML, to the console window, treating newlines as
        ///     paragraph breaks. All paragraphs are word-wrapped to fit in the console buffer, or to a sensible width if
        ///     redirected to a file. Each paragraph is indented by the number of spaces at the start of the corresponding
        ///     line.</summary>
        /// <param name="message">
        ///     The message to output.</param>
        /// <param name="hangingIndent">
        ///     Specifies a number of spaces by which the message is indented in all but the first line of each paragraph.</param>
        /// <remarks>
        ///     See <see cref="EggsNode.ToConsoleColoredStringWordWrap"/> for the colour syntax.</remarks>
        public static void WriteParagraphs(EggsNode message, int hangingIndent = 0)
        {
            int width;
            try
            {
                width = WrapToWidth();
            }
            catch
            {
                // Fall back to non-word-wrapping
                WriteLine(ConsoleColoredString.FromEggsNode(message));
                return;
            }
            bool any = false;
            foreach (var line in message.ToConsoleColoredStringWordWrap(width, hangingIndent))
            {
                WriteLine(line);
                any = true;
            }

            // Special case: if the input is empty, output an empty line
            if (!any)
                Console.WriteLine();
        }
Exemplo n.º 5
0
        private Size doPaintOrMeasure(Graphics g, EggsNode node, Font initialFont, Color initialForeColor, int constrainingWidth,
            List<renderingInfo> renderings = null, List<locationInfo> locations = null)
        {
            var glyphOverhang = TextRenderer.MeasureText(g, "Wg", initialFont, _dummySize) - TextRenderer.MeasureText(g, "Wg", initialFont, _dummySize, TextFormatFlags.NoPadding);
            int x = glyphOverhang.Width / 2, y = glyphOverhang.Height / 2;
            int wrapWidth = WordWrap ? Math.Max(1, constrainingWidth - glyphOverhang.Width) : int.MaxValue;
            int hangingIndent = _hangingIndent * (_hangingIndentUnit == IndentUnit.Spaces ? measure(initialFont, " ", g).Width : 1);
            bool atBeginningOfLine = false;

            // STEP 1: Run the word-wrapping as if TextAlign were TopLeft

            int actualWidth = EggsML.WordWrap(node, new renderState(initialFont, initialForeColor), wrapWidth,
                (state, text) => measure(state.Font, text, g).Width,
                (state, text, width) =>
                {
                    if (state.Mnemonic && !string.IsNullOrWhiteSpace(text))
                        state.ActiveLocations.OfType<linkLocationInfo>().FirstOrDefault().NullOr(link => { link.Mnemonic = char.ToLowerInvariant(text.Trim()[0]); return link; });

                    if (renderings != null && !string.IsNullOrEmpty(text))
                    {
                        renderingInfo info;
                        if (!atBeginningOfLine && renderings.Count > 0 && (info = renderings[renderings.Count - 1]).State == state)
                        {
                            info.Text += text;
                            var rect = info.Rectangle;
                            rect.Width += width;
                            info.Rectangle = rect;
                        }
                        else
                        {
                            info = new renderingInfo(text, new Rectangle(x, y, width, measure(state.Font, " ", g).Height), state);
                            renderings.Add(info);
                        }
                        foreach (var location in state.ActiveLocations)
                        {
                            if (location.Rectangles.Count == 0 || location.Rectangles[location.Rectangles.Count - 1].Y != info.Rectangle.Y)
                                location.Rectangles.Add(info.Rectangle);
                            else
                            {
                                var rect = location.Rectangles[location.Rectangles.Count - 1];
                                rect.Width += width;
                                location.Rectangles[location.Rectangles.Count - 1] = rect;
                            }
                        }
                    }
                    atBeginningOfLine = false;
                    x += width;
                },
                (state, newParagraph, indent) =>
                {
                    atBeginningOfLine = true;
                    var sh = measure(state.Font, " ", g).Height;
                    y += sh;
                    if (newParagraph && _paragraphSpacing > 0)
                        y += (int) (_paragraphSpacing * sh);
                    var newIndent = state.BlockIndent + indent;
                    if (!newParagraph)
                        newIndent += hangingIndent;
                    x = newIndent + glyphOverhang.Width / 2;
                    return newIndent;
                },
                (state, tag, parameter) =>
                {
                    var font = state.Font;
                    switch (tag)
                    {
                        // ITALICS
                        case '/': return Tuple.Create(state.ChangeFont(new Font(font, font.Style | FontStyle.Italic)), 0);

                        // BOLD
                        case '*': return Tuple.Create(state.ChangeFont(new Font(font, font.Style | FontStyle.Bold)), 0);

                        // UNDERLINE
                        case '_': return Tuple.Create(state.ChangeFont(new Font(font, font.Style | FontStyle.Underline)), 0);

                        // MNEMONICS
                        case '&': return Tuple.Create(state.SetMnemonic(), 0);

                        // BULLET POINT
                        case '[':
                            var bulletSize = measure(font, _bullet, g);
                            var advance = bulletSize.Width;
                            if (renderings != null)
                                renderings.Add(new renderingInfo(_bullet, new Rectangle(x, y, advance, bulletSize.Height), new renderState(font, state.Color)));
                            x += advance;
                            return Tuple.Create(state.ChangeBlockIndent(state.BlockIndent + advance), advance);

                        // LINK (e.g. <link target>{link text}, link target may be omitted)
                        case '{':
                            if (locations == null)
                                break;
                            var linkLocation = new linkLocationInfo { LinkID = parameter };
                            locations.Add(linkLocation);
                            return Tuple.Create(state.ChangeColor(Enabled ? LinkColor : SystemColors.GrayText).AddActiveLocation(linkLocation), 0);

                        // TOOLTIP (e.g. <tooltip text>#main text#)
                        case '#':
                            if (string.IsNullOrWhiteSpace(parameter) || locations == null)
                                break;
                            var tooltipLocation = new tooltipLocationInfo { Tooltip = parameter };
                            locations.Add(tooltipLocation);
                            return Tuple.Create(state.AddActiveLocation(tooltipLocation), 0);

                        // COLOUR (e.g. <colour>=coloured text=, revert to default colour if no <colour> specified)
                        case '=':
                            var color = parameter == null ? initialForeColor : (Color) (_colorConverter ?? (_colorConverter = new ColorConverter())).ConvertFromString(parameter);
                            return Tuple.Create(state.ChangeColor(color), 0);
                    }
                    return Tuple.Create(state, 0);
                });

            var totalSize = new Size(actualWidth + glyphOverhang.Width, y + measure(initialFont, " ", g).Height + glyphOverhang.Height);

            // STEP 2: Fix everything according to TextAlign.
            if (renderings != null)
            {
                // 2a: vertical alignment
                int offsetY = 0;
                switch (_textAlign)
                {
                    case ContentAlignment.TopCenter:
                    case ContentAlignment.TopLeft:
                    case ContentAlignment.TopRight:
                        // Already top-aligned: nothing to do
                        break;

                    case ContentAlignment.MiddleCenter:
                    case ContentAlignment.MiddleLeft:
                    case ContentAlignment.MiddleRight:
                        offsetY = ClientSize.Height / 2 - totalSize.Height / 2;
                        goto default;

                    case ContentAlignment.BottomCenter:
                    case ContentAlignment.BottomLeft:
                    case ContentAlignment.BottomRight:
                        offsetY = ClientSize.Height - totalSize.Height;
                        goto default;

                    default:
                        foreach (var inf in renderings)
                        {
                            var rect = inf.Rectangle;
                            rect.Y += offsetY;
                            inf.Rectangle = rect;
                        }
                        break;
                }

                // 2b: horizontal alignment
                foreach (var group in renderings.GroupConsecutiveBy(inf => inf.Rectangle.Y))
                {
                    var width = group.Max(elem => elem.Rectangle.Right);
                    int offsetX = 0;
                    switch (_textAlign)
                    {
                        case ContentAlignment.TopLeft:
                        case ContentAlignment.MiddleLeft:
                        case ContentAlignment.BottomLeft:
                            // Already left-aligned: nothing to do
                            break;

                        case ContentAlignment.TopCenter:
                        case ContentAlignment.MiddleCenter:
                        case ContentAlignment.BottomCenter:
                            offsetX = ClientSize.Width / 2 - width / 2;
                            goto default;

                        case ContentAlignment.TopRight:
                        case ContentAlignment.MiddleRight:
                        case ContentAlignment.BottomRight:
                            offsetX = ClientSize.Width - width;
                            goto default;

                        default:
                            foreach (var inf in group)
                            {
                                var rect = inf.Rectangle;
                                rect.X += offsetX;
                                inf.Rectangle = rect;
                            }
                            break;
                    }
                }
            }

            return totalSize;
        }
Exemplo n.º 6
0
        private void extractMnemonicEtc(EggsNode node)
        {
            // The only legal way to use & is as a tag containing a single character. For example:
            // &F&ile       (mnemonic is 'F')
            // O&p&en    (mnemonic is 'P')

            var tag = node as EggsTag;
            if (tag == null)
                return;
            if (tag.Tag == '&')
            {
                if (tag.Children.Count != 1 || !(tag.Children.First() is EggsText) || ((EggsText) tag.Children.First()).Text.Length != 1)
                    throw new InvalidOperationException("'&' mnemonic tag must not contain anything other than a single character.");
                _mnemonic = char.ToUpperInvariant(((EggsText) tag.Children.First()).Text[0]);
            }
            else if (tag.Tag == '{')
            {
                // Deliberately skip the inside of links: don’t wanna interpret their mnemonics as the main mnemonic
                TabStop = true;
            }
            else
            {
                foreach (var child in tag.Children)
                {
                    if (TabStop && _mnemonic != '\0')
                        return;
                    extractMnemonicEtc(child);
                }
            }
        }
Exemplo n.º 7
0
        /// <summary>Override; see base.</summary>
        protected override void OnTextChanged(EventArgs e)
        {
            // If a link has focus, trigger the LinkLostFocus event.
            // OnPaint will trigger the LinkGotFocus event as appropriate
            _keyboardFocusOnLink = null;

            _cachedPreferredSizes.Clear();
            _cachedRendering = null;
            base.OnTextChanged(e);
            _mnemonic = '\0';
            TabStop = false;
            var origText = base.Text;
            try
            {
                _parsed = EggsML.Parse(origText);
                ParseError = null;

                // We know there are no mnemonics or links in the exception message, so only do this if there was no parse error
                extractMnemonicEtc(_parsed);
            }
            catch (EggsMLParseException epe)
            {
                ParseError = epe;
                var msg = "";
                int ind = 0;
                if (epe.FirstIndex != null)
                {
                    ind = epe.FirstIndex.Value;
                    msg += EggsML.Escape(origText.Substring(0, ind));
                    msg += "<Red>={0}=".Fmt(EggsML.Escape(origText.Substring(ind, 1)));
                    ind++;
                }
                msg += EggsML.Escape(origText.Substring(ind, epe.Index - ind));
                ind = epe.Index;
                if (epe.Length > 0)
                {
                    msg += "<Red>={0}=".Fmt(EggsML.Escape(origText.Substring(ind, epe.Length)));
                    ind += epe.Length;
                }
                msg += "<Red>= ← (" + EggsML.Escape(epe.Message) + ")=";
                msg += EggsML.Escape(origText.Substring(ind));
                _parsed = EggsML.Parse(msg);
            }
            autosize();
            Invalidate();
        }
Exemplo n.º 8
0
        private Size doPaintOrMeasure(Graphics g, EggsNode node, Font initialFont, Color initialForeColor, int constrainingWidth,
                                      List <renderingInfo> renderings = null, List <locationInfo> locations = null)
        {
            var  glyphOverhang = TextRenderer.MeasureText(g, "Wg", initialFont, _dummySize) - TextRenderer.MeasureText(g, "Wg", initialFont, _dummySize, TextFormatFlags.NoPadding);
            int  x = glyphOverhang.Width / 2, y = glyphOverhang.Height / 2;
            int  wrapWidth         = WordWrap ? Math.Max(1, constrainingWidth - glyphOverhang.Width) : int.MaxValue;
            int  hangingIndent     = _hangingIndent * (_hangingIndentUnit == IndentUnit.Spaces ? measure(initialFont, " ", g).Width : 1);
            bool atBeginningOfLine = false;

            // STEP 1: Run the word-wrapping as if TextAlign were TopLeft

            int actualWidth = EggsML.WordWrap(node, new renderState(initialFont, initialForeColor), wrapWidth,
                                              (state, text) => measure(state.Font, text, g).Width,
                                              (state, text, width) =>
            {
                if (state.Mnemonic && !string.IsNullOrWhiteSpace(text))
                {
                    state.ActiveLocations.OfType <linkLocationInfo>().FirstOrDefault().NullOr(link => { link.Mnemonic = char.ToLowerInvariant(text.Trim()[0]); return(link); });
                }

                if (renderings != null && !string.IsNullOrEmpty(text))
                {
                    renderingInfo info;
                    if (!atBeginningOfLine && renderings.Count > 0 && (info = renderings[renderings.Count - 1]).State == state)
                    {
                        info.Text     += text;
                        var rect       = info.Rectangle;
                        rect.Width    += width;
                        info.Rectangle = rect;
                    }
                    else
                    {
                        info = new renderingInfo(text, new Rectangle(x, y, width, measure(state.Font, " ", g).Height), state);
                        renderings.Add(info);
                    }
                    foreach (var location in state.ActiveLocations)
                    {
                        if (location.Rectangles.Count == 0 || location.Rectangles[location.Rectangles.Count - 1].Y != info.Rectangle.Y)
                        {
                            location.Rectangles.Add(info.Rectangle);
                        }
                        else
                        {
                            var rect    = location.Rectangles[location.Rectangles.Count - 1];
                            rect.Width += width;
                            location.Rectangles[location.Rectangles.Count - 1] = rect;
                        }
                    }
                }
                atBeginningOfLine = false;
                x += width;
            },
                                              (state, newParagraph, indent) =>
            {
                atBeginningOfLine = true;
                var sh            = measure(state.Font, " ", g).Height;
                y += sh;
                if (newParagraph && _paragraphSpacing > 0)
                {
                    y += (int)(_paragraphSpacing * sh);
                }
                var newIndent = state.BlockIndent + indent;
                if (!newParagraph)
                {
                    newIndent += hangingIndent;
                }
                x = newIndent + glyphOverhang.Width / 2;
                return(newIndent);
            },
                                              (state, tag, parameter) =>
            {
                var font = state.Font;
                switch (tag)
                {
                // ITALICS
                case '/': return(state.ChangeFont(new Font(font, font.Style | FontStyle.Italic)), 0);
Exemplo n.º 9
0
 private static void eggWalk(EggsNode node, StringBuilder text, List<ConsoleColor?> colors, List<int> colorLengths, ConsoleColor? curColor)
 {
     var tag = node as EggsTag;
     if (tag != null)
     {
         bool curLight = curColor >= ConsoleColor.DarkGray;
         switch (tag.Tag)
         {
             case '~': curColor = curLight ? ConsoleColor.DarkGray : ConsoleColor.Black; break;
             case '/': curColor = curLight ? ConsoleColor.Blue : ConsoleColor.DarkBlue; break;
             case '$': curColor = curLight ? ConsoleColor.Green : ConsoleColor.DarkGreen; break;
             case '&': curColor = curLight ? ConsoleColor.Cyan : ConsoleColor.DarkCyan; break;
             case '_': curColor = curLight ? ConsoleColor.Red : ConsoleColor.DarkRed; break;
             case '%': curColor = curLight ? ConsoleColor.Magenta : ConsoleColor.DarkMagenta; break;
             case '^': curColor = curLight ? ConsoleColor.Yellow : ConsoleColor.DarkYellow; break;
             case '=': curColor = ConsoleColor.DarkGray; curLight = true; break;
             case '*': if (!curLight) curColor = (ConsoleColor) ((int) (curColor ?? ConsoleColor.Gray) + 8); curLight = true; break;
         }
         foreach (var child in tag.Children)
             eggWalk(child, text, colors, colorLengths, curColor);
     }
     else if (node is EggsText)
     {
         var txt = (EggsText) node;
         text.Append(txt.Text);
         colors.Add(curColor);
         colorLengths.Add(txt.Text.Length);
     }
 }
Exemplo n.º 10
0
        /// <summary>
        ///     Constructs a <see cref="ConsoleColoredString"/> from an EggsML parse tree.</summary>
        /// <param name="node">
        ///     The root node of the EggsML parse tree.</param>
        /// <returns>
        ///     The <see cref="ConsoleColoredString"/> constructed from the EggsML parse tree.</returns>
        /// <remarks>
        ///     <para>
        ///         The following EggsML tags map to the following console colors:</para>
        ///     <list type="bullet">
        ///         <item><description>
        ///             <c>~</c> = black, or dark gray if inside a <c>*</c> tag</description></item>
        ///         <item><description>
        ///             <c>/</c> = dark blue, or blue if inside a <c>*</c> tag</description></item>
        ///         <item><description>
        ///             <c>$</c> = dark green, or green if inside a <c>*</c> tag</description></item>
        ///         <item><description>
        ///             <c>&amp;</c> = dark cyan, or cyan if inside a <c>*</c> tag</description></item>
        ///         <item><description>
        ///             <c>_</c> = dark red, or red if inside a <c>*</c> tag</description></item>
        ///         <item><description>
        ///             <c>%</c> = dark magenta, or magenta if inside a <c>*</c> tag</description></item>
        ///         <item><description>
        ///             <c>^</c> = dark yellow, or yellow if inside a <c>*</c> tag</description></item>
        ///         <item><description>
        ///             <c>=</c> = dark gray (independent of <c>*</c> tag)</description></item></list>
        ///     <para>
        ///         Text which is not inside any of the above color tags defaults to light gray, or white if inside a <c>*</c>
        ///         tag.</para></remarks>
        public static ConsoleColoredString FromEggsNode(EggsNode node)
        {
            StringBuilder text = new StringBuilder();
            List<ConsoleColor?> colors = new List<ConsoleColor?>();
            List<int> colorLengths = new List<int>();

            eggWalk(node, text, colors, colorLengths, null);

            var colArr = new ConsoleColor?[colorLengths.Sum()];
            var index = 0;
            for (int i = 0; i < colors.Count; i++)
            {
                var col = colors[i];
                for (int j = 0; j < colorLengths[i]; j++)
                {
                    colArr[index] = col;
                    index++;
                }
            }

            return new ConsoleColoredString(text.ToString(), colArr);
        }