File: Text\AttributedTextExtensions.cs
Web Access
Project: src\src\Graphics\src\Graphics\Graphics.csproj (Microsoft.Maui.Graphics)
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Maui.Graphics.Text;
 
namespace Microsoft.Maui.Graphics.Text
{
	public static class AttributedTextExtensions
	{
		public static IAttributedText Optimize(this IAttributedText attributedText)
		{
			if (attributedText?.Text == null)
				return null;
 
			if (attributedText is AbstractAttributedText abstractAttributedText && abstractAttributedText.Optimal)
				return attributedText;
 
			var start = 0;
			var attributeIndex = 0;
			var text = attributedText.Text;
			var length = text.Length;
			var runs = new List<IAttributedTextRun>();
			attributedText.CreateParagraphRun(start, length, runs, attributeIndex);
			return new AttributedText(text, runs, true);
		}
 
		internal static List<IAttributedTextRun> OptimizeRuns(this IAttributedText attributedText)
		{
			if (attributedText?.Text == null)
				return null;
 
			if (attributedText is AbstractAttributedText abstractAttributedText && abstractAttributedText.Optimal)
			{
				if (attributedText.Runs == null)
					return null;
 
				if (attributedText.Runs is List<IAttributedTextRun> list)
					return list;
 
				return attributedText.Runs.ToList();
			}
 
			var start = 0;
			var attributeIndex = 0;
			var text = attributedText.Text;
			var length = text.Length;
			var runs = new List<IAttributedTextRun>();
			attributedText.CreateParagraphRun(start, length, runs, attributeIndex);
			return runs;
		}
 
		public static IReadOnlyList<IAttributedText> CreateParagraphs(this IAttributedText attributedText)
		{
			if (attributedText?.Text == null)
				return null;
 
			List<IAttributedText> paragraphs = new List<IAttributedText>();
 
			int start = 0;
			int attributeIndex = 0;
 
			using (var sr = new StringReader(attributedText.Text))
			{
				string line;
				while ((line = sr.ReadLine()) != null)
				{
					var length = line.Length;
 
					var runs = new List<IAttributedTextRun>();
					attributeIndex = attributedText.CreateParagraphRun(start, length, runs, attributeIndex);
 
					var paragraph = new AttributedText(line, runs);
					paragraphs.Add(paragraph);
 
					start += length + 1;
				}
			}
 
			return paragraphs;
		}
 
		public static int CreateParagraphRun(
			this IAttributedText text,
			int start,
			int length,
			IList<IAttributedTextRun> runs,
			int startIndexForSearch = 0)
		{
			// If the text doesn't have any runs, then we can simply return
			if (text.Runs == null || text.Runs.Count == 0)
				return 0;
 
			// If we've already reached the end of the runs, we can simply return
			if (!(startIndexForSearch < text.Runs.Count))
				return startIndexForSearch;
 
			var end = start + length;
			var index = startIndexForSearch;
 
			do
			{
				var run = text.Runs[index];
 
				// If the run is after the end index, then we can go ahead and return
				if (end < run.Start)
					return index;
 
				if (run.Intersects(start, length))
				{
					if (start == run.Start)
					{
						var paragraphStart = run.Start - start;
						var paragraphLength = Math.Min(run.Length, length);
						runs.Add(new AttributedTextRun(paragraphStart, paragraphLength, run.Attributes));
 
						// If the length of the run is the same as the paragraph, then we know
						// that the next run (if any) will apply to to the next paragraph.
						if (run.Length == length)
							return index + 1;
 
						// If the run is longer than the line, then we know that the attributes from this run
						// will also apply to the next paragraph.
						if (run.Length > length)
							return index;
 
						// If the run length is less than the length of the line, then the next run may apply
						// to this line, so continue
					}
					else if (end == run.GetEnd())
					{
						var paragraphStart = Math.Max(run.Start - start, 0);
						var paragraphLength = Math.Min(run.Length, end - paragraphStart);
						runs.Add(new AttributedTextRun(paragraphStart, paragraphLength, run.Attributes));
 
						// Since the run and the line have the same end, we know that the next run (if any) will
						// apply to to the next paragraph.
						return index + 1;
					}
					else
					{
						var paragraphStart = Math.Max(run.Start - start, 0);
						var paragraphLength = Math.Min(run.Length, length - paragraphStart);
						runs.Add(new AttributedTextRun(paragraphStart, paragraphLength, run.Attributes));
					}
				}
 
				index++;
			} while (index < text.Runs.Count);
 
			return index;
		}
 
		public static IList<AttributedTextBlock> CreateBlocks(this IAttributedText text)
		{
			if (text?.Text == null)
				return null;
 
			var blocks = new List<AttributedTextBlock>();
 
			var start = 0;
			var end = text.Text.Length;
 
			if (text.Runs?.Count > 0)
			{
				foreach (var run in text.Runs)
				{
					if (start < run.Start)
					{
						var noAttrLength = run.Start - start;
						var noAttrValue = text.Text.Substring(start, noAttrLength);
						blocks.Add(new AttributedTextBlock(noAttrValue, null));
						start = run.Start;
					}
 
					var length = run.Length;
					if (length > 0)
					{
						var value = text.Text.Substring(start, length);
						blocks.Add(new AttributedTextBlock(value, run.Attributes));
						start = run.GetEnd();
					}
#if DEBUG
					else
						System.Diagnostics.Debug.WriteLine("Length should not be less then 0");
#endif
				}
			}
 
			if (start < end)
			{
				var value = text.Text.Substring(start);
				blocks.Add(new AttributedTextBlock(value, null));
			}
 
			return blocks;
		}
	}
}