/// <summary> /// Word-wraps a given piece of EggsML, assuming that it is linearly flowing text. Newline (<c>\n</c>) characters /// can be used to split the text into multiple paragraphs. See remarks for the special meaning of <c>+...+</c> /// and <c><...></c>.</summary> /// <typeparam name="TState"> /// The type of the text state that an EggsML can change, e.g. font or color.</typeparam> /// <param name="node"> /// The root node of the EggsML tree to word-wrap.</param> /// <param name="initialState"> /// The initial text state.</param> /// <param name="wrapWidth"> /// The maximum width at which to word-wrap. This width can be measured in any unit, as long as <paramref /// name="measure"/> uses the same unit.</param> /// <param name="measure"> /// A delegate that measures the width of any piece of text.</param> /// <param name="render"> /// A delegate that is called whenever a piece of text is ready to be rendered.</param> /// <param name="advanceToNextLine"> /// A delegate that is called to advance to the next line.</param> /// <param name="nextState"> /// A delegate that determines how each EggsML tag character modifies the state (font, color etc.).</param> /// <returns> /// The maximum width of the text.</returns> /// <remarks> /// <list type="bullet"> /// <item><description> /// The <c>+...+</c> tag marks text that may not be broken by wrapping (effectively turning all spaces /// into non-breaking spaces).</description></item> /// <item><description> /// The <c><...></c> tag marks a parameter to an immediately following tag. For example, if the /// input EggsML contains <c><X>{Foo}</c>, the text “X” will be passed as the parameter to <paramref /// name="nextState"/> when the <c>{</c> tag is processed.</description></item></list></remarks> public static int WordWrap <TState>(EggsNode node, TState initialState, int wrapWidth, EggMeasure <TState> measure, EggRender <TState> render, EggNextLine <TState> advanceToNextLine, EggNextState <TState> nextState) { if (node == null) { throw new ArgumentNullException(nameof(node)); } if (wrapWidth <= 0) { throw new ArgumentException("Wrap width must be greater than zero.", nameof(wrapWidth)); } var data = new EggWalkData <TState> { AtStartOfLine = true, WordPieces = new List <string>(), WordPiecesState = new List <TState>(), WordPiecesWidths = new List <int>(), WordPiecesWidthsSum = 0, Measure = measure, Render = render, AdvanceToNextLine = advanceToNextLine, NextState = nextState, X = 0, WrapWidth = wrapWidth, ActualWidth = 0 }; data.EggWalkWordWrap(node, initialState); return(data.ActualWidth); }
public void EggWalkWordWrap(EggsNode node, TState initialState) { if (node == null) { throw new ArgumentNullException(nameof(node)); } eggWalkWordWrapRecursive(node, initialState, false); if (WordPieces.Count > 0) { renderPieces(initialState); } }
/// <summary> /// Adds a new child node to this tag’s children.</summary> /// <param name="child"> /// The child node to add.</param> internal void Add(EggsNode child) { child.Parent = this; _children.Add(child); }
private void eggWalkWordWrapRecursive(EggsNode node, TState state, bool curNowrap) { if (node is EggsTag tag) { var newState = state; if (tag.Tag == '+') { curNowrap = true; } else if (tag.Tag == '<') { if (CurParameter != null) { throw new InvalidOperationException("An angle-bracket tag must be immediately followed by another tag."); } CurParameter = tag.ToString(true); return; } else if (tag.Tag != null) { var tup = NextState(state, tag.Tag.Value, CurParameter); CurParameter = null; newState = tup.newState; X += tup.advance; } foreach (var child in tag.Children) { eggWalkWordWrapRecursive(child, newState, curNowrap); } if (CurParameter != null) { throw new InvalidOperationException("An angle-bracket tag must be immediately followed by another tag."); } } else if (node is EggsText text) { if (CurParameter != null) { throw new InvalidOperationException("An angle-bracket tag must be immediately followed by another tag."); } var txt = text.Text; var i = 0; while (i < txt.Length) { // Check whether we are looking at a whitespace character or not, and if not, find the end of the word. int lengthOfWord = 0; while (lengthOfWord + i < txt.Length && (curNowrap || !isWrappableAfter(txt, lengthOfWord + i)) && txt[lengthOfWord + i] != '\n') { lengthOfWord++; } if (lengthOfWord > 0) { // We are looking at a word. (It doesn’t matter whether we’re at the beginning of the word or in the middle of one.) retry1: string fragment = txt.Substring(i, lengthOfWord); var fragmentWidth = Measure(state, fragment); retry2: // If we are at the start of a line, and the word itself doesn’t fit on a line by itself, we need to break the word up. if (AtStartOfLine && X + WordPiecesWidthsSum + fragmentWidth > WrapWidth) { // We don’t know exactly where to break the word, so use binary search to discover where that is. if (lengthOfWord > 1) { lengthOfWord /= 2; goto retry1; } // If we get to here, ‘WordPieces’ contains as much of the word as fits into one line, and the next letter makes it too long. // If ‘WordPieces’ is empty, we are at the beginning of a paragraph and the first letter already doesn’t fit. if (WordPieces.Count > 0) { // Render the part of the word that fits on the line and then move to the next line. renderPieces(state); advanceToNextLine(state, false); } } else if (!AtStartOfLine && X + Measure(state, Spaces) + WordPiecesWidthsSum + fragmentWidth > WrapWidth) { // We have already rendered some text on this line, but the word we’re looking at right now doesn’t // fit into the rest of the line, so leave the rest of this line blank and advance to the next line. advanceToNextLine(state, false); // In case the word also doesn’t fit on a line all by itself, go back to top (now that ‘AtStartOfLine’ is true) // where it will check whether we need to break the word apart. goto retry2; } // If we get to here, the current fragment fits on the current line (or it is a single character that overflows // the line all by itself). WordPieces.Add(fragment); WordPiecesState.Add(state); WordPiecesWidths.Add(fragmentWidth); WordPiecesWidthsSum += fragmentWidth; i += lengthOfWord; continue; } // We encounter a whitespace character. All the word pieces fit on the current line, so render them. if (WordPieces.Count > 0) { renderPieces(state); AtStartOfLine = false; } if (txt[i] == '\n') { // If the whitespace character is actually a newline, start a new paragraph. advanceToNextLine(state, true); i++; } else { // Discover the extent of the spaces. var lengthOfSpaces = 0; while (lengthOfSpaces + i < txt.Length && isWrappableAfter(txt, lengthOfSpaces + i) && txt[lengthOfSpaces + i] != '\n') { lengthOfSpaces++; } Spaces = txt.Substring(i, lengthOfSpaces); SpaceState = state; i += lengthOfSpaces; if (AtStartOfLine) { // If we are at the beginning of the line, treat these spaces as the paragraph’s indentation. CurParagraphIndent += renderSpaces(Spaces, state); } } } } else { throw new InvalidOperationException("An EggsNode is expected to be either EggsTag or EggsText, not {0}.".Fmt(node.GetType().FullName)); } }