/// <summary> /// Adds a <see cref="TextLayoutCommandType.ChangeSourceString"/> or <see cref="TextLayoutCommandType.ChangeSourceStringBuilder"/> command to the output /// stream if it is necessary to do so for the specified parser token. /// </summary> private Boolean EmitChangeSourceIfNecessary(TextParserTokenStream input, TextLayoutCommandStream output, ref TextParserToken token) { if (!IsSegmentForCurrentSource(token.Text)) { var isFirstSource = (sourceString == null && sourceStringBuilder == null); sourceString = token.Text.SourceString; sourceStringBuilder = token.Text.SourceStringBuilder; // NOTE: To save memory, we can elide the first change source command if it's just going to change to the input source. if (!isFirstSource || input.SourceText.SourceString != sourceString || input.SourceText.SourceStringBuilder != sourceStringBuilder) { if (sourceString != null) { var sourceIndex = output.RegisterSourceString(sourceString); output.WriteChangeSourceString(new TextLayoutSourceStringCommand(sourceIndex)); } else { var sourceIndex = output.RegisterSourceStringBuilder(sourceStringBuilder); output.WriteChangeSourceStringBuilder(new TextLayoutSourceStringBuilderCommand(sourceIndex)); } return(true); } } return(false); }
/// <summary> /// Lexes and parses the specified string. /// </summary> /// <param name="input">The <see cref="StringBuilder"/> to parse.</param> /// <param name="output">The parsed token stream.</param> /// <param name="options">A set of <see cref="TextParserOptions"/> values that specify how the text should be parsed.</param> public void Parse(StringBuilder input, TextParserTokenStream output, TextParserOptions options = TextParserOptions.None) { Contract.Require(input, "input"); Contract.Require(output, "output"); output.Clear(); Parse(new StringSource(input), output, 0, input.Length, options); }
/// <summary> /// Incrementally lexes and parses the specified string. /// </summary> /// <param name="input">The <see cref="String"/> to parse.</param> /// <param name="start">The index of the first character that was changed.</param> /// <param name="count">The number of characters that were changed.</param> /// <param name="result">The parsed token stream.</param> /// <param name="options">A set of <see cref="TextParserOptions"/> values that specify how the text should be parsed.</param> /// <returns>An <see cref="IncrementalResult"/> structure that represents the result of the operation.</returns> /// <remarks>Incremental parsing provides a performance benefit when relatively small changes are being made /// to a large source text. Only tokens which are potentially influenced by changes within the specified substring /// of the source text are re-parsed by this operation.</remarks> public IncrementalResult ParseIncremental(String input, Int32 start, Int32 count, TextParserTokenStream result, TextParserOptions options = TextParserOptions.None) { Contract.Require(input, "input"); Contract.Require(result, "output"); Contract.EnsureRange(start >= 0, "start"); return ParseIncremental(new StringSource(input), start, count, result, options); }
/// <summary> /// Lexes and parses the specified string. /// </summary> /// <param name="input">The <see cref="StringBuilder"/> to parse.</param> /// <param name="output">The parsed token stream.</param> /// <param name="options">A set of <see cref="TextParserOptions"/> values that specify how the text should be parsed.</param> public void Parse(StringBuilder input, TextParserTokenStream output, TextParserOptions options = TextParserOptions.None) { Contract.Require(input, nameof(input)); Contract.Require(output, nameof(output)); output.Clear(); Parse(new StringSource(input), output, 0, input.Length, options); }
public void TextParser_ParseIncremental_CorrectlyHandlesReplacedText() { var textParser = new TextParser(); var textParserResult = new TextParserTokenStream(); textParser.Parse(@"lorem ipsum |b|dolor|b| sit amet", textParserResult); TheResultingCollection(textParserResult.Select(x => x.Text)).ShouldBeExactly("lorem", " ", "ipsum", " ", null, "dolor", null, " ", "sit", " ", "amet"); textParser.ParseIncremental(@"lorem ipsum |b|foo bar baz qux|b| sit amet", 15, 21, textParserResult); TheResultingCollection(textParserResult.Select(x => x.Text)).ShouldBeExactly("lorem", " ", "ipsum", " ", null, "foo", " ", "bar", " ", "baz", " ", "qux", null, " ", "sit", " ", "amet"); }
public void TextParser_ParseIncremental_CorrectlyHandlesAddedText() { var textParser = new TextParser(); var textParserResult = new TextParserTokenStream(); textParser.Parse(@"lorem ipsum |b|dolor|b| sit amet", textParserResult); TheResultingCollection(textParserResult.Select(x => x.Text)).ShouldBeExactly("lorem", " ", "ipsum", " ", null, "dolor", null, " ", "sit", " ", "amet"); textParser.ParseIncremental(@"lorem ipsum hello world!! |b|dolor|b| sit amet", 12, 14, textParserResult); TheResultingCollection(textParserResult.Select(x => x.Text)).ShouldBeExactly("lorem", " ", "ipsum", " ", "hello", " ", "world!!", " ", null, "dolor", null, " ", "sit", " ", "amet"); }
/// <summary> /// Gets the next text token after the specified index, if one exists and there are no intervening /// visible tokens (excluding commands). /// </summary> private TextParserToken?GetNextTextToken(TextParserTokenStream input, Int32 index) { for (int i = index + 1; i < input.Count; i++) { var token = input[i]; if (token.TokenType == TextParserTokenType.Text) { return(token); } if (token.TokenType == TextParserTokenType.Icon) { break; } } return(null); }
/// <summary> /// Processes a parser token with type <see cref="TextParserTokenType.Text"/>. /// </summary> private Boolean ProcessTextToken(TextParserTokenStream input, TextLayoutCommandStream output, SpriteFontFace currentFontFace, ref TextParserToken token, ref LayoutState state, ref TextLayoutSettings settings, ref Int32 index) { if (token.IsNewLine) { state.AdvanceLayoutToNextLineWithBreak(output, token.SourceLength, ref settings); index++; } else { if (!AccumulateText(input, output, currentFontFace, ref index, ref state, ref settings)) { return(false); } } return(true); }
/// <summary> /// Lexes and parses the specified string. /// </summary> /// <param name="input">The <see cref="StringSource"/> to parse.</param> /// <param name="output">The parsed token stream.</param> /// <param name="index">The index at which to begin parsing the input string.</param> /// <param name="count">the number of characters to parse.</param> /// <param name="options">A set of <see cref="TextParserOptions"/> values that specify how the text should be parsed.</param> private void Parse(StringSource input, TextParserTokenStream output, Int32 index, Int32 count, TextParserOptions options = TextParserOptions.None) { var bound = index + count; while (index < bound) { if (IsStartOfNewline(input, index)) { output.Add(ConsumeNewlineToken(input, options, ref index)); continue; } if (IsStartOfNonBreakingSpace(input, index)) { output.Add(ConsumeNonBreakingSpaceToken(input, options, ref index)); continue; } if (IsStartOfBreakingSpace(input, index)) { output.Add(ConsumeBreakingSpaceToken(input, options, ref index)); continue; } if (IsEscapedPipe(input, index, options)) { output.Add(ConsumeEscapedPipeToken(input, options, ref index)); continue; } if (IsStartOfCommand(input, index)) { output.Add(ConsumeCommandToken(input, options, ref index)); continue; } if (IsStartOfWord(input, index)) { output.Add(ConsumeWordToken(input, options, ref index)); continue; } index++; } output.SourceText = input.CreateStringSegment(); output.ParserOptions = options; }
/// <summary> /// Initializes a new instance of the <see cref="ScrollingTextBlock"/> class. /// </summary> /// <param name="textRenderer">The text renderer which will be used to draw the scrolling text block's text.</param> /// <param name="font">The sprite font which will be used to draw the scrolling text block's text.</param> /// <param name="text">The string which is displayed by the scrolling text block.</param> /// <param name="width">The width of the scrolling text block's layout area in pixels, /// or <see langword="null"/> to give the layout area an unconstrained width.</param> /// <param name="height">The height of the scrolling text block's layout area in pixels, /// or <see langword="null"/> to give the layout area an unconstrained height.</param> public ScrollingTextBlock(TextRenderer textRenderer, SpriteFont font, String text, Int32? width, Int32? height) { Contract.Require(textRenderer, nameof(textRenderer)); Contract.Require(font, nameof(font)); Contract.Require(text, nameof(text)); this.TextRenderer = textRenderer; this.Font = font; this.Width = width; this.Height = height; this.textParserTokens = new TextParserTokenStream(); this.textLayoutCommands = new TextLayoutCommandStream(); this.Text = text; this.TextRenderer.Parse(text, textParserTokens); this.TextRenderer.CalculateLayout(Text, textLayoutCommands, new TextLayoutSettings(Font, width, height, TextFlags.Standard)); Reset(); }
/// <summary> /// Finds the index of the first and last tokens which are potentially affected by changes in the specified substring of the source text. /// </summary> private void FindTokensInfluencedBySubstring(TextParserTokenStream result, Int32 start, Int32 count, out Int32 ix1, out Int32 ix2) { var position = 0; var end = start + count; var ix1Found = false; var ix2Found = false; ix1 = 0; ix2 = result.Count - 1; for (int i = 0; i < result.Count; i++) { var token = result[i]; var tokenStart = token.SourceOffset; var tokenEnd = tokenStart + token.SourceLength; if (!ix1Found && (start >= tokenStart && start <= tokenEnd)) { ix1 = i; ix1Found = true; } if (!ix2Found && (end >= tokenStart && end < tokenEnd)) { ix2 = i; ix2Found = true; } if (ix1Found && ix2Found) { break; } position += token.SourceLength; } }
/// <summary> /// Accumulates sequential text tokens into a single text command. /// </summary> private Boolean AccumulateText(TextParserTokenStream input, TextLayoutCommandStream output, SpriteFontFace font, ref Int32 index, ref LayoutState state, ref TextLayoutSettings settings) { var hyphenate = (settings.Options & TextLayoutOptions.Hyphenate) == TextLayoutOptions.Hyphenate; var availableWidth = (settings.Width ?? Int32.MaxValue); var x = state.PositionX; var y = state.PositionY; var width = 0; var height = 0; var accumulatedStart = input[index].Text.Start + (state.ParserTokenOffset ?? 0); var accumulatedLength = 0; var accumulatedCount = 0; var lineOverflow = false; var lineBreakPossible = false; var tokenText = default(StringSegment); var tokenNext = default(TextParserToken?); var tokenSize = default(Size2); var tokenKerning = 0; var tokenIsBreakingSpace = false; while (index < input.Count) { var token = input[index]; if (token.TokenType != TextParserTokenType.Text || token.IsNewLine) { break; } if (!IsSegmentForCurrentSource(token.Text)) { if (accumulatedCount > 0) { break; } EmitChangeSourceIfNecessary(input, output, ref token); } tokenText = token.Text.Substring(state.ParserTokenOffset ?? 0); tokenNext = GetNextTextToken(input, index); tokenSize = MeasureToken(font, token.TokenType, tokenText, tokenNext); tokenKerning = font.Kerning.Get(tokenText[tokenText.Length - 1], ' '); // NOTE: We assume in a couple of places that tokens sizes don't exceed Int16.MaxValue, so try to // avoid accumulating tokens larger than that just in case somebody is doing something dumb if (width + tokenSize.Width > Int16.MaxValue) { break; } if (token.IsWhiteSpace && (state.LineBreakCommand == null || !token.IsNonBreakingSpace)) { lineBreakPossible = true; state.LineBreakCommand = output.Count; state.LineBreakOffset = accumulatedLength + token.Text.Length - 1; tokenIsBreakingSpace = true; } else { tokenIsBreakingSpace = false; } // For most tokens we need to bail out here if there's a line overflow, but // if it's a breaking space we need to be sure that it's part of the command stream // so that we can go back and replace it in the line break phase! var overflowsLine = state.PositionX + tokenSize.Width - tokenKerning > availableWidth; if (overflowsLine && !tokenIsBreakingSpace) { lineOverflow = true; break; } if (tokenText.Start != accumulatedStart + accumulatedLength) { break; } width = width + tokenSize.Width; height = Math.Max(height, tokenSize.Height); accumulatedLength = accumulatedLength + tokenText.Length; accumulatedCount++; state.AdvanceLineToNextCommand(tokenSize.Width, tokenSize.Height, 1, tokenText.Length); state.ParserTokenOffset = 0; state.LineLengthInCommands--; index++; // At this point, we need to bail out even for breaking spaces. if (overflowsLine && tokenIsBreakingSpace) { lineOverflow = true; break; } } if (lineBreakPossible) { var preLineBreakTextStart = accumulatedStart; var preLineBreakTextLength = state.LineBreakOffset.Value; var preLineBreakText = CreateStringSegmentFromCurrentSource(preLineBreakTextStart, preLineBreakTextLength); var preLineBreakSize = (preLineBreakText.Length == 0) ? Size2.Zero : MeasureToken(font, TextParserTokenType.Text, preLineBreakText); state.BrokenTextSizeBeforeBreak = preLineBreakSize; var postLineBreakStart = accumulatedStart + (state.LineBreakOffset.Value + 1); var postLineBreakLength = accumulatedLength - (state.LineBreakOffset.Value + 1); var postLineBreakText = CreateStringSegmentFromCurrentSource(postLineBreakStart, postLineBreakLength); var postLineBreakSize = (postLineBreakText.Length == 0) ? Size2.Zero : MeasureToken(font, TextParserTokenType.Text, postLineBreakText, GetNextTextToken(input, index - 1)); state.BrokenTextSizeAfterBreak = postLineBreakSize; } var bounds = new Rectangle(x, y, width, height); EmitTextIfNecessary(output, accumulatedStart, accumulatedLength, ref bounds, ref state); if (lineOverflow && !state.ReplaceLastBreakingSpaceWithLineBreak(output, ref settings)) { var overflowingToken = input[index]; if (overflowingToken.IsWhiteSpace && !overflowingToken.IsNonBreakingSpace) { output.WriteLineBreak(new TextLayoutLineBreakCommand(1)); state.AdvanceLineToNextCommand(0, 0, 1, 1, isLineBreak: true); state.AdvanceLayoutToNextLine(output, ref settings); if (overflowingToken.Text.Length > 1) { state.ParserTokenOffset = 1; } else { index++; } return(true); } if (!GetFittedSubstring(font, availableWidth, ref tokenText, ref tokenSize, ref state, hyphenate) && state.LineWidth == 0) { return(false); } var overflowingTokenBounds = (tokenText.Length == 0) ? Rectangle.Empty : new Rectangle(state.PositionX, state.PositionY, tokenSize.Width, tokenSize.Height); var overflowingTextEmitted = EmitTextIfNecessary(output, tokenText.Start, tokenText.Length, ref overflowingTokenBounds, ref state); if (overflowingTextEmitted) { state.AdvanceLineToNextCommand(tokenSize.Width, tokenSize.Height, 0, tokenText.Length); if (hyphenate) { output.WriteHyphen(); state.AdvanceLineToNextCommand(0, 0, 1, 0); } } state.ParserTokenOffset = (state.ParserTokenOffset ?? 0) + tokenText.Length; state.AdvanceLayoutToNextLine(output, ref settings); } return(true); }
/// <summary> /// Calculates a layout for the specified text. /// </summary> /// <param name="input">The parsed text which will be laid out according to the specified settings.</param> /// <param name="output">The layout command stream which will be populated with commands by this operation.</param> /// <param name="settings">A <see cref="TextLayoutSettings"/> structure which contains the settings for this operation.</param> public void CalculateLayout(TextParserTokenStream input, TextLayoutCommandStream output, TextLayoutSettings settings) { Contract.Require(input, nameof(input)); Contract.Require(output, nameof(output)); if (settings.Font == null) { throw new ArgumentException(UltravioletStrings.InvalidLayoutSettings); } var state = new LayoutState() { LineInfoCommandIndex = 1 }; output.Clear(); output.SourceText = input.SourceText; output.ParserOptions = input.ParserOptions; var acquiredPointers = !output.HasAcquiredPointers; if (acquiredPointers) { output.AcquirePointers(); } output.WriteBlockInfo(); output.WriteLineInfo(); var bold = (settings.Style == SpriteFontStyle.Bold || settings.Style == SpriteFontStyle.BoldItalic); var italic = (settings.Style == SpriteFontStyle.Italic || settings.Style == SpriteFontStyle.BoldItalic); if (settings.InitialLayoutStyle != null) { PrepareInitialStyle(output, ref bold, ref italic, ref settings); } var currentFont = settings.Font; var currentFontFace = settings.Font.GetFace(SpriteFontStyle.Regular); var index = 0; var processing = true; while (index < input.Count && processing) { if (state.PositionY >= (settings.Height ?? Int32.MaxValue)) { break; } var token = input[index]; currentFontFace = default(SpriteFontFace); currentFont = GetCurrentFont(ref settings, bold, italic, out currentFontFace); switch (token.TokenType) { case TextParserTokenType.Text: processing = ProcessTextToken(input, output, currentFontFace, ref token, ref state, ref settings, ref index); break; case TextParserTokenType.Icon: processing = ProcessIconToken(output, ref token, ref state, ref settings, ref index); break; case TextParserTokenType.ToggleBold: ProcessToggleBoldToken(output, ref bold, ref state, ref index); break; case TextParserTokenType.ToggleItalic: ProcessToggleItalicToken(output, ref italic, ref state, ref index); break; case TextParserTokenType.PushFont: ProcessPushFontToken(output, ref token, ref state, ref index); break; case TextParserTokenType.PushColor: ProcessPushColorToken(output, ref token, ref state, ref index); break; case TextParserTokenType.PushStyle: ProcessPushStyleToken(output, ref bold, ref italic, ref token, ref state, ref index); break; case TextParserTokenType.PushGlyphShader: ProcessPushGlyphShaderToken(output, ref token, ref state, ref index); break; case TextParserTokenType.PushLink: ProcessPushLinkToken(output, ref token, ref state, ref index); break; case TextParserTokenType.PopFont: ProcessPopFontToken(output, ref token, ref state, ref index); break; case TextParserTokenType.PopColor: ProcessPopColorToken(output, ref token, ref state, ref index); break; case TextParserTokenType.PopStyle: ProcessPopStyleToken(output, ref bold, ref italic, ref token, ref state, ref index); break; case TextParserTokenType.PopGlyphShader: ProcessPopGlyphShaderToken(output, ref token, ref state, ref index); break; case TextParserTokenType.PopLink: ProcessPopLinkToken(output, ref token, ref state, ref index); break; default: if (token.TokenType >= TextParserTokenType.Custom) { ProcessCustomCommandToken(output, ref token, ref state, ref index); break; } else { throw new InvalidOperationException(UltravioletStrings.UnrecognizedLayoutCommand.Format(token.TokenType)); } } } state.FinalizeLayout(output, ref settings); if (acquiredPointers) { output.ReleasePointers(); } ClearLayoutStacks(); }
/// <summary> /// Incrementally lexes and parses the specified string. /// </summary> /// <param name="input">The <see cref="StringSource"/> to parse.</param> /// <param name="start">The index of the first character that was changed.</param> /// <param name="count">The number of characters that were changed.</param> /// <param name="output">The parsed token stream.</param> /// <param name="options">A set of <see cref="TextParserOptions"/> values that specify how the text should be parsed.</param> /// <returns>An <see cref="IncrementalResult"/> structure that represents the result of the operation.</returns> /// <remarks>Incremental parsing provides a performance benefit when relatively small changes are being made /// to a large source text. Only tokens which are potentially influenced by changes within the specified substring /// of the source text are re-parsed by this operation.</remarks> private IncrementalResult ParseIncremental(StringSource input, Int32 start, Int32 count, TextParserTokenStream output, TextParserOptions options = TextParserOptions.None) { var inputLengthOld = output.SourceText.Length; var inputLengthNew = input.Length; var inputLengthDiff = inputLengthNew - inputLengthOld; Int32 ix1, ix2; FindTokensInfluencedBySubstring(output, start, count - inputLengthDiff, out ix1, out ix2); var token1 = output[ix1]; var token2 = output[ix2]; var invalidatedTokenCount = 1 + (ix2 - ix1); output.RemoveRange(ix1, invalidatedTokenCount); var lexStart = token1.SourceOffset; var lexCount = inputLengthDiff + (token2.SourceOffset + token2.SourceLength) - lexStart; var parserBuffer = incrementalParserBuffer.Value; Parse(input, parserBuffer, lexStart, lexCount); output.SourceText = input.CreateStringSegment(); output.InsertRange(ix1, parserBuffer); var affectedOffset = ix1; var affectedCount = parserBuffer.Count; parserBuffer.Clear(); return new IncrementalResult(affectedOffset, affectedCount); }
/// <summary> /// Gets the next text token after the specified index, if one exists and there are no intervening /// visible tokens (excluding commands). /// </summary> private TextParserToken? GetNextTextToken(TextParserTokenStream input, Int32 index) { for (int i = index + 1; i < input.Count; i++) { var token = input[i]; if (token.TokenType == TextParserTokenType.Text) return token; if (token.TokenType == TextParserTokenType.Icon) break; } return null; }
/// <summary> /// Finds the index of the first and last tokens which are potentially affected by changes in the specified substring of the source text. /// </summary> private void FindTokensInfluencedBySubstring(TextParserTokenStream result, Int32 start, Int32 count, out Int32 ix1, out Int32 ix2) { var position = 0; var end = start + count; var ix1Found = false; var ix2Found = false; ix1 = 0; ix2 = result.Count - 1; for (int i = 0; i < result.Count; i++) { var token = result[i]; var tokenStart = token.SourceOffset; var tokenEnd = tokenStart + token.SourceLength; if (!ix1Found && (start >= tokenStart && start <= tokenEnd)) { ix1 = i; ix1Found = true; } if (!ix2Found && (end >= tokenStart && end < tokenEnd)) { ix2 = i; ix2Found = true; } if (ix1Found && ix2Found) break; position += token.SourceLength; } }
/// <summary> /// Accumulates sequential text tokens into a single text command. /// </summary> private Boolean AccumulateText(TextParserTokenStream input, TextLayoutCommandStream output, SpriteFontFace font, ref Int32 index, ref LayoutState state, ref TextLayoutSettings settings) { var hyphenate = (settings.Options & TextLayoutOptions.Hyphenate) == TextLayoutOptions.Hyphenate; var availableWidth = (settings.Width ?? Int32.MaxValue); var x = state.PositionX; var y = state.PositionY; var width = 0; var height = 0; var accumulatedStart = input[index].Text.Start + (state.ParserTokenOffset ?? 0); var accumulatedLength = 0; var accumulatedCount = 0; var lineOverflow = false; var lineBreakPossible = false; var tokenText = default(StringSegment); var tokenNext = default(TextParserToken?); var tokenSize = default(Size2); while (index < input.Count) { var token = input[index]; if (token.TokenType != TextParserTokenType.Text || token.IsNewLine) break; if (!IsSegmentForCurrentSource(token.Text)) { if (accumulatedCount > 0) break; EmitChangeSourceIfNecessary(input, output, ref token); } tokenText = token.Text.Substring(state.ParserTokenOffset ?? 0); tokenNext = GetNextTextToken(input, index); tokenSize = MeasureToken(font, token.TokenType, tokenText, tokenNext); // NOTE: We assume in a couple of places that tokens sizes don't exceed Int16.MaxValue, so try to // avoid accumulating tokens larger than that just in case somebody is doing something dumb if (width + tokenSize.Width > Int16.MaxValue) break; var overflowsLine = state.PositionX + tokenSize.Width > availableWidth; if (overflowsLine) { lineOverflow = true; break; } if (tokenText.Start != accumulatedStart + accumulatedLength) break; if (token.IsWhiteSpace && (state.LineBreakCommand == null || !token.IsNonBreakingSpace)) { lineBreakPossible = true; state.LineBreakCommand = output.Count; state.LineBreakOffset = accumulatedLength + token.Text.Length - 1; } width = width + tokenSize.Width; height = Math.Max(height, tokenSize.Height); accumulatedLength = accumulatedLength + tokenText.Length; accumulatedCount++; state.AdvanceLineToNextCommand(tokenSize.Width, tokenSize.Height, 1, tokenText.Length); state.ParserTokenOffset = 0; state.LineLengthInCommands--; index++; } if (lineBreakPossible) { var preLineBreakTextStart = accumulatedStart; var preLineBreakTextLength = state.LineBreakOffset.Value; var preLineBreakText = CreateStringSegmentFromCurrentSource(preLineBreakTextStart, preLineBreakTextLength); var preLineBreakSize = (preLineBreakText.Length == 0) ? Size2.Zero : MeasureToken(font, TextParserTokenType.Text, preLineBreakText); state.BrokenTextSizeBeforeBreak = preLineBreakSize; var postLineBreakStart = accumulatedStart + (state.LineBreakOffset.Value + 1); var postLineBreakLength = accumulatedLength - (state.LineBreakOffset.Value + 1); var postLineBreakText = CreateStringSegmentFromCurrentSource(postLineBreakStart, postLineBreakLength); var postLineBreakSize = (postLineBreakText.Length == 0) ? Size2.Zero : MeasureToken(font, TextParserTokenType.Text, postLineBreakText, GetNextTextToken(input, index - 1)); state.BrokenTextSizeAfterBreak = postLineBreakSize; } var bounds = new Rectangle(x, y, width, height); EmitTextIfNecessary(output, accumulatedStart, accumulatedLength, ref bounds, ref state); if (lineOverflow && !state.ReplaceLastBreakingSpaceWithLineBreak(output, ref settings)) { var overflowingToken = input[index]; if (overflowingToken.IsWhiteSpace && !overflowingToken.IsNonBreakingSpace) { output.WriteLineBreak(new TextLayoutLineBreakCommand(1)); state.AdvanceLineToNextCommand(0, 0, 1, 1, isLineBreak: true); state.AdvanceLayoutToNextLine(output, ref settings); if (overflowingToken.Text.Length > 1) { state.ParserTokenOffset = 1; } else { index++; } return true; } if (!GetFittedSubstring(font, availableWidth, ref tokenText, ref tokenSize, ref state, hyphenate) && state.LineWidth == 0) return false; var overflowingTokenBounds = (tokenText.Length == 0) ? Rectangle.Empty : new Rectangle(state.PositionX, state.PositionY, tokenSize.Width, tokenSize.Height); var overflowingTextEmitted = EmitTextIfNecessary(output, tokenText.Start, tokenText.Length, ref overflowingTokenBounds, ref state); if (overflowingTextEmitted) { state.AdvanceLineToNextCommand(tokenSize.Width, tokenSize.Height, 0, tokenText.Length); if (hyphenate) { output.WriteHyphen(); state.AdvanceLineToNextCommand(0, 0, 1, 0); } } state.ParserTokenOffset = (state.ParserTokenOffset ?? 0) + tokenText.Length; state.AdvanceLayoutToNextLine(output, ref settings); } return true; }
/// <summary> /// Adds a <see cref="TextLayoutCommandType.ChangeSourceString"/> or <see cref="TextLayoutCommandType.ChangeSourceStringBuilder"/> command to the output /// stream if it is necessary to do so for the specified parser token. /// </summary> private Boolean EmitChangeSourceIfNecessary(TextParserTokenStream input, TextLayoutCommandStream output, ref TextParserToken token) { if (!IsSegmentForCurrentSource(token.Text)) { var isFirstSource = (sourceString == null && sourceStringBuilder == null); sourceString = token.Text.SourceString; sourceStringBuilder = token.Text.SourceStringBuilder; // NOTE: To save memory, we can elide the first change source command if it's just going to change to the input source. if (!isFirstSource || input.SourceText.SourceString != sourceString || input.SourceText.SourceStringBuilder != sourceStringBuilder) { if (sourceString != null) { var sourceIndex = output.RegisterSourceString(sourceString); output.WriteChangeSourceString(new TextLayoutSourceStringCommand(sourceIndex)); } else { var sourceIndex = output.RegisterSourceStringBuilder(sourceStringBuilder); output.WriteChangeSourceStringBuilder(new TextLayoutSourceStringBuilderCommand(sourceIndex)); } return true; } } return false; }
/// <summary> /// Processes a parser token with type <see cref="TextParserTokenType.Text"/>. /// </summary> private Boolean ProcessTextToken(TextParserTokenStream input, TextLayoutCommandStream output, SpriteFontFace currentFontFace, ref TextParserToken token, ref LayoutState state, ref TextLayoutSettings settings, ref Int32 index) { if (token.IsNewLine) { state.AdvanceLayoutToNextLineWithBreak(output, token.SourceLength, ref settings); index++; } else { if (!AccumulateText(input, output, currentFontFace, ref index, ref state, ref settings)) return false; } return true; }
/// <summary> /// Calculates a layout for the specified text. /// </summary> /// <param name="input">The parsed text which will be laid out according to the specified settings.</param> /// <param name="output">The layout command stream which will be populated with commands by this operation.</param> /// <param name="settings">A <see cref="TextLayoutSettings"/> structure which contains the settings for this operation.</param> public void CalculateLayout(TextParserTokenStream input, TextLayoutCommandStream output, TextLayoutSettings settings) { Contract.Require(input, "input"); Contract.Require(output, "output"); if (settings.Font == null) throw new ArgumentException(UltravioletStrings.InvalidLayoutSettings); var state = new LayoutState() { LineInfoCommandIndex = 1 }; output.Clear(); output.SourceText = input.SourceText; output.ParserOptions = input.ParserOptions; var acquiredPointers = !output.HasAcquiredPointers; if (acquiredPointers) output.AcquirePointers(); output.WriteBlockInfo(); output.WriteLineInfo(); var bold = (settings.Style == SpriteFontStyle.Bold || settings.Style == SpriteFontStyle.BoldItalic); var italic = (settings.Style == SpriteFontStyle.Italic || settings.Style == SpriteFontStyle.BoldItalic); if (settings.InitialLayoutStyle != null) PrepareInitialStyle(output, ref bold, ref italic, ref settings); var currentFont = settings.Font; var currentFontFace = settings.Font.GetFace(SpriteFontStyle.Regular); var index = 0; var processing = true; while (index < input.Count && processing) { if (state.PositionY >= (settings.Height ?? Int32.MaxValue)) break; var token = input[index]; currentFontFace = default(SpriteFontFace); currentFont = GetCurrentFont(ref settings, bold, italic, out currentFontFace); switch (token.TokenType) { case TextParserTokenType.Text: processing = ProcessTextToken(input, output, currentFontFace, ref token, ref state, ref settings, ref index); break; case TextParserTokenType.Icon: processing = ProcessIconToken(output, ref token, ref state, ref settings, ref index); break; case TextParserTokenType.ToggleBold: ProcessToggleBoldToken(output, ref bold, ref state, ref index); break; case TextParserTokenType.ToggleItalic: ProcessToggleItalicToken(output, ref italic, ref state, ref index); break; case TextParserTokenType.PushFont: ProcessPushFontToken(output, ref token, ref state, ref index); break; case TextParserTokenType.PushColor: ProcessPushColorToken(output, ref token, ref state, ref index); break; case TextParserTokenType.PushStyle: ProcessPushStyleToken(output, ref bold, ref italic, ref token, ref state, ref index); break; case TextParserTokenType.PushGlyphShader: ProcessPushGlyphShaderToken(output, ref token, ref state, ref index); break; case TextParserTokenType.PopFont: ProcessPopFontToken(output, ref token, ref state, ref index); break; case TextParserTokenType.PopColor: ProcessPopColorToken(output, ref token, ref state, ref index); break; case TextParserTokenType.PopStyle: ProcessPopStyleToken(output, ref bold, ref italic, ref token, ref state, ref index); break; case TextParserTokenType.PopGlyphShader: ProcessPopGlyphShaderToken(output, ref token, ref state, ref index); break; default: throw new InvalidOperationException(UltravioletStrings.UnrecognizedLayoutCommand.Format(token.TokenType)); } } state.FinalizeLayout(output, ref settings); if (acquiredPointers) output.ReleasePointers(); ClearLayoutStacks(); }
/// <summary> /// Incrementally lexes and parses the specified string. /// </summary> /// <param name="input">The <see cref="StringBuilder"/> to parse.</param> /// <param name="start">The index of the first character that was changed.</param> /// <param name="count">The number of characters that were changed.</param> /// <param name="result">The parsed token stream.</param> /// <param name="options">A set of <see cref="TextParserOptions"/> values that specify how the text should be parsed.</param> /// <returns>An <see cref="IncrementalResult"/> structure that represents the result of the operation.</returns> /// <remarks>Incremental parsing provides a performance benefit when relatively small changes are being made /// to a large source text. Only tokens which are potentially influenced by changes within the specified substring /// of the source text are re-parsed by this operation.</remarks> public IncrementalResult ParseIncremental(StringBuilder input, Int32 start, Int32 count, TextParserTokenStream result, TextParserOptions options = TextParserOptions.None) { Contract.Require(input, nameof(input)); Contract.Require(result, nameof(result)); Contract.EnsureRange(start >= 0, nameof(start)); Contract.EnsureRange(count >= 0 && start + count <= input.Length, nameof(count)); return(ParseIncremental(new StringSource(input), start, count, result, options)); }
/// <summary> /// Incrementally lexes and parses the specified string. /// </summary> /// <param name="input">The <see cref="StringSource"/> to parse.</param> /// <param name="start">The index of the first character that was changed.</param> /// <param name="count">The number of characters that were changed.</param> /// <param name="output">The parsed token stream.</param> /// <param name="options">A set of <see cref="TextParserOptions"/> values that specify how the text should be parsed.</param> /// <returns>An <see cref="IncrementalResult"/> structure that represents the result of the operation.</returns> /// <remarks>Incremental parsing provides a performance benefit when relatively small changes are being made /// to a large source text. Only tokens which are potentially influenced by changes within the specified substring /// of the source text are re-parsed by this operation.</remarks> private IncrementalResult ParseIncremental(StringSource input, Int32 start, Int32 count, TextParserTokenStream output, TextParserOptions options = TextParserOptions.None) { var inputLengthOld = output.SourceText.Length; var inputLengthNew = input.Length; var inputLengthDiff = inputLengthNew - inputLengthOld; Int32 ix1, ix2; FindTokensInfluencedBySubstring(output, start, count - inputLengthDiff, out ix1, out ix2); var token1 = output[ix1]; var token2 = output[ix2]; var invalidatedTokenCount = 1 + (ix2 - ix1); output.RemoveRange(ix1, invalidatedTokenCount); var lexStart = token1.SourceOffset; var lexCount = inputLengthDiff + (token2.SourceOffset + token2.SourceLength) - lexStart; var parserBuffer = incrementalParserBuffer.Value; Parse(input, parserBuffer, lexStart, lexCount); output.SourceText = input.CreateStringSegment(); output.InsertRange(ix1, parserBuffer); var affectedOffset = ix1; var affectedCount = parserBuffer.Count; parserBuffer.Clear(); return(new IncrementalResult(affectedOffset, affectedCount)); }
/// <summary> /// Updates the cache which contains the element's parsed text. /// </summary> private void UpdateTextParserCache() { if (textParserResult != null) textParserResult.Clear(); if (View == null) return; var content = Content; var contentElement = content as UIElement; if (contentElement == null) { if (textParserResult == null) textParserResult = new TextParserTokenStream(); var contentAsString = default(String); var contentFormat = ContentStringFormat; if (contentFormat == null) { contentAsString = (content == null) ? String.Empty : content.ToString(); } else { contentAsString = String.Format(contentFormat, content); } View.Resources.TextRenderer.Parse(contentAsString, textParserResult); } InvalidateArrange(); }
/// <summary> /// Incrementally lexes and parses the specified string. /// </summary> /// <param name="input">The <see cref="StringBuilder"/> to parse.</param> /// <param name="start">The index of the first character that was changed.</param> /// <param name="count">The number of characters that were changed.</param> /// <param name="result">The parsed token stream.</param> /// <param name="options">A set of <see cref="TextParserOptions"/> values that specify how the text should be parsed.</param> /// <returns>An <see cref="IncrementalResult"/> structure that represents the result of the operation.</returns> /// <remarks>Incremental parsing provides a performance benefit when relatively small changes are being made /// to a large source text. Only tokens which are potentially influenced by changes within the specified substring /// of the source text are re-parsed by this operation.</remarks> public IncrementalResult ParseIncremental(StringBuilder input, Int32 start, Int32 count, TextParserTokenStream result, TextParserOptions options = TextParserOptions.None) { Contract.Require(input, nameof(input)); Contract.Require(result, nameof(result)); Contract.EnsureRange(start >= 0, nameof(start)); Contract.EnsureRange(count >= 0 && start + count <= input.Length, nameof(count)); return ParseIncremental(new StringSource(input), start, count, result, options); }