예제 #1
0
        /// <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>&lt;...&gt;</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>&lt;...&gt;</c> tag marks a parameter to an immediately following tag. For example, if the
        ///             input EggsML contains <c>&lt;X&gt;{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);
        }
예제 #2
0
            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);
                }
            }
예제 #3
0
 /// <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);
 }
예제 #4
0
            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));
                }
            }