/// <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(); } }
/// <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(); }
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); } } }
/// <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(); }
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; }
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); } } }
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);
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); } }
/// <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>&</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); }