A game framework written with osu! in mind.
at master 337 lines 13 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Linq; 6using Markdig; 7using Markdig.Extensions.AutoIdentifiers; 8using Markdig.Extensions.Tables; 9using Markdig.Syntax; 10using Markdig.Syntax.Inlines; 11using osu.Framework.Allocation; 12using osu.Framework.Caching; 13using osu.Framework.Extensions.EnumExtensions; 14using osu.Framework.Graphics.Sprites; 15using osu.Framework.Utils; 16using osuTK; 17 18namespace osu.Framework.Graphics.Containers.Markdown 19{ 20 /// <summary> 21 /// Visualises a markdown text document. 22 /// </summary> 23 [Cached(Type = typeof(IMarkdownTextComponent))] 24 [Cached(Type = typeof(IMarkdownTextFlowComponent))] 25 public class MarkdownContainer : CompositeDrawable, IMarkdownTextComponent, IMarkdownTextFlowComponent 26 { 27 private const int root_level = 0; 28 29 /// <summary> 30 /// Controls which <see cref="Axes"/> are automatically sized w.r.t. <see cref="CompositeDrawable.InternalChildren"/>. 31 /// Children's <see cref="Drawable.BypassAutoSizeAxes"/> are ignored for automatic sizing. 32 /// Most notably, <see cref="Drawable.RelativePositionAxes"/> and <see cref="Drawable.RelativeSizeAxes"/> of children 33 /// do not affect automatic sizing to avoid circular size dependencies. 34 /// It is not allowed to manually set <see cref="Drawable.Size"/> (or <see cref="Drawable.Width"/> / <see cref="Drawable.Height"/>) 35 /// on any <see cref="Axes"/> which are automatically sized. 36 /// </summary> 37 public new Axes AutoSizeAxes 38 { 39 get => base.AutoSizeAxes; 40 set 41 { 42 if (value.HasFlagFast(Axes.X)) 43 throw new ArgumentException($"{nameof(MarkdownContainer)} does not support an {nameof(AutoSizeAxes)} of {value}"); 44 45 base.AutoSizeAxes = value; 46 } 47 } 48 49 private string text = string.Empty; 50 51 /// <summary> 52 /// The text to visualise. 53 /// </summary> 54 public string Text 55 { 56 get => text; 57 set 58 { 59 if (text == value) 60 return; 61 62 text = value; 63 64 contentCache.Invalidate(); 65 } 66 } 67 68 /// <summary> 69 /// The vertical spacing between lines. 70 /// </summary> 71 public virtual float LineSpacing 72 { 73 get => document.Spacing.Y; 74 set => document.Spacing = new Vector2(0, value); 75 } 76 77 /// <summary> 78 /// The margins of the contained document. 79 /// </summary> 80 public MarginPadding DocumentMargin 81 { 82 get => document.Margin; 83 set => document.Margin = value; 84 } 85 86 /// <summary> 87 /// The padding of the contained document. 88 /// </summary> 89 public MarginPadding DocumentPadding 90 { 91 get => document.Padding; 92 set => document.Padding = value; 93 } 94 95 private Uri documentUri; 96 97 /// <summary> 98 /// The URL of the loaded document. 99 /// </summary> 100 /// <exception cref="ArgumentException">If the provided URL was not a valid absolute URI.</exception> 101 protected string DocumentUrl 102 { 103 get => documentUri?.AbsoluteUri; 104 set 105 { 106 if (!Uri.TryCreate(value, UriKind.Absolute, out var uri)) 107 throw new ArgumentException($"Document URL ({value}) must be an absolute URI."); 108 109 if (documentUri == uri) 110 return; 111 112 documentUri = uri; 113 114 contentCache.Invalidate(); 115 } 116 } 117 118 private Uri rootUri; 119 120 /// <summary> 121 /// The base URL for all root-relative links. 122 /// </summary> 123 /// <exception cref="ArgumentException">If the provided URL was not a valid absolute URI.</exception> 124 protected string RootUrl 125 { 126 get => rootUri?.AbsoluteUri; 127 set 128 { 129 if (!Uri.TryCreate(value, UriKind.Absolute, out var uri)) 130 throw new ArgumentException($"Root URL ({value}) must be an absolute URI."); 131 132 if (rootUri == uri) 133 return; 134 135 rootUri = uri; 136 137 contentCache.Invalidate(); 138 } 139 } 140 141 private readonly Cached contentCache = new Cached(); 142 143 private readonly FillFlowContainer document; 144 145 public MarkdownContainer() 146 { 147 InternalChild = document = new FillFlowContainer 148 { 149 AutoSizeAxes = Axes.Y, 150 RelativeSizeAxes = Axes.X, 151 Direction = FillDirection.Vertical, 152 }; 153 154 LineSpacing = 25; 155 DocumentPadding = new MarginPadding { Left = 10, Right = 30 }; 156 DocumentMargin = new MarginPadding { Left = 10, Right = 30 }; 157 } 158 159 [BackgroundDependencyLoader] 160 private void load() 161 { 162 validateContent(); 163 } 164 165 private void validateContent() 166 { 167 if (!contentCache.IsValid) 168 { 169 var markdownText = Text; 170 var pipeline = CreateBuilder(); 171 var parsed = Markdig.Markdown.Parse(markdownText, pipeline); 172 173 // Turn all relative URIs in the document into absolute URIs 174 foreach (var link in parsed.Descendants().OfType<LinkInline>()) 175 { 176 string url = link.Url; 177 178 if (string.IsNullOrEmpty(url)) 179 continue; 180 181 if (!Validation.TryParseUri(url, out Uri linkUri)) 182 continue; 183 184 if (linkUri.IsAbsoluteUri) 185 continue; 186 187 if (documentUri != null) 188 { 189 link.Url = rootUri != null && url.StartsWith('/') 190 // Ensure the URI is document-relative by removing all trailing slashes 191 ? new Uri(rootUri, new Uri(url.TrimStart('/'), UriKind.Relative)).AbsoluteUri 192 : new Uri(documentUri, new Uri(url, UriKind.Relative)).AbsoluteUri; 193 } 194 } 195 196 document.Clear(); 197 foreach (var component in parsed) 198 AddMarkdownComponent(component, document, root_level); 199 200 contentCache.Validate(); 201 } 202 } 203 204 protected override void Update() 205 { 206 base.Update(); 207 208 validateContent(); 209 } 210 211 public virtual MarkdownTextFlowContainer CreateTextFlow() => new MarkdownTextFlowContainer(); 212 213 public virtual SpriteText CreateSpriteText() => new SpriteText(); 214 215 /// <summary> 216 /// Adds a component that visualises a <see cref="IMarkdownObject"/> to the document. 217 /// </summary> 218 /// <param name="markdownObject">The <see cref="IMarkdownObject"/> to visualise.</param> 219 /// <param name="container">The container to add the visualisation to.</param> 220 /// <param name="level">The level in the document of <paramref name="markdownObject"/>. 221 /// 0 for the root level, 1 for first-level items in a list, 2 for second-level items in a list, etc.</param> 222 protected virtual void AddMarkdownComponent(IMarkdownObject markdownObject, FillFlowContainer container, int level) 223 { 224 switch (markdownObject) 225 { 226 case ThematicBreakBlock thematicBlock: 227 container.Add(CreateSeparator(thematicBlock)); 228 break; 229 230 case HeadingBlock headingBlock: 231 container.Add(CreateHeading(headingBlock)); 232 break; 233 234 case ParagraphBlock paragraphBlock: 235 container.Add(CreateParagraph(paragraphBlock, level)); 236 break; 237 238 case QuoteBlock quoteBlock: 239 container.Add(CreateQuoteBlock(quoteBlock)); 240 break; 241 242 case FencedCodeBlock fencedCodeBlock: 243 container.Add(CreateFencedCodeBlock(fencedCodeBlock)); 244 break; 245 246 case Table table: 247 container.Add(CreateTable(table)); 248 break; 249 250 case ListBlock listBlock: 251 var childContainer = CreateList(listBlock); 252 container.Add(childContainer); 253 foreach (var single in listBlock) 254 AddMarkdownComponent(single, childContainer, level + 1); 255 break; 256 257 case ListItemBlock listItemBlock: 258 foreach (var single in listItemBlock) 259 AddMarkdownComponent(single, container, level); 260 break; 261 262 case HtmlBlock _: 263 // HTML is not supported 264 break; 265 266 case LinkReferenceDefinitionGroup _: 267 // Link reference doesn't need to be displayed. 268 break; 269 270 default: 271 container.Add(CreateNotImplemented(markdownObject)); 272 break; 273 } 274 } 275 276 /// <summary> 277 /// Creates the visualiser for a <see cref="HeadingBlock"/>. 278 /// </summary> 279 /// <param name="headingBlock">The <see cref="HeadingBlock"/> to visualise.</param> 280 /// <returns>The visualiser.</returns> 281 protected virtual MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new MarkdownHeading(headingBlock); 282 283 /// <summary> 284 /// Creates the visualiser for a <see cref="ParagraphBlock"/>. 285 /// </summary> 286 /// <param name="paragraphBlock">The <see cref="ParagraphBlock"/> to visualise.</param> 287 /// <param name="level">The level in the document of <paramref name="paragraphBlock"/>. 288 /// 0 for the root level, 1 for first-level items in a list, 2 for second-level items in a list, etc.</param> 289 /// <returns>The visualiser.</returns> 290 protected virtual MarkdownParagraph CreateParagraph(ParagraphBlock paragraphBlock, int level) => new MarkdownParagraph(paragraphBlock); 291 292 /// <summary> 293 /// Creates the visualiser for a <see cref="QuoteBlock"/>. 294 /// </summary> 295 /// <param name="quoteBlock">The <see cref="QuoteBlock"/> to visualise.</param> 296 /// <returns>The visualiser.</returns> 297 protected virtual MarkdownQuoteBlock CreateQuoteBlock(QuoteBlock quoteBlock) => new MarkdownQuoteBlock(quoteBlock); 298 299 /// <summary> 300 /// Creates the visualiser for a <see cref="FencedCodeBlock"/>. 301 /// </summary> 302 /// <param name="fencedCodeBlock">The <see cref="FencedCodeBlock"/> to visualise.</param> 303 /// <returns>The visualiser.</returns> 304 protected virtual MarkdownFencedCodeBlock CreateFencedCodeBlock(FencedCodeBlock fencedCodeBlock) => new MarkdownFencedCodeBlock(fencedCodeBlock); 305 306 /// <summary> 307 /// Creates the visualiser for a <see cref="Table"/>. 308 /// </summary> 309 /// <param name="table">The <see cref="Table"/> to visualise.</param> 310 /// <returns>The visualiser.</returns> 311 protected virtual MarkdownTable CreateTable(Table table) => new MarkdownTable(table); 312 313 /// <summary> 314 /// Creates the visualiser for a <see cref="ListBlock"/>. 315 /// </summary> 316 /// <returns>The visualiser.</returns> 317 protected virtual MarkdownList CreateList(ListBlock listBlock) => new MarkdownList(); 318 319 /// <summary> 320 /// Creates the visualiser for a horizontal separator. 321 /// </summary> 322 /// <returns>The visualiser.</returns> 323 protected virtual MarkdownSeparator CreateSeparator(ThematicBreakBlock thematicBlock) => new MarkdownSeparator(); 324 325 /// <summary> 326 /// Creates the visualiser for an element that isn't implemented. 327 /// </summary> 328 /// <param name="markdownObject">The <see cref="MarkdownObject"/> that isn't implemented.</param> 329 /// <returns>The visualiser.</returns> 330 protected virtual NotImplementedMarkdown CreateNotImplemented(IMarkdownObject markdownObject) => new NotImplementedMarkdown(markdownObject); 331 332 protected virtual MarkdownPipeline CreateBuilder() 333 => new MarkdownPipelineBuilder().UseAutoIdentifiers(AutoIdentifierOptions.GitHub) 334 .UseEmojiAndSmiley() 335 .UseAdvancedExtensions().Build(); 336 } 337}