// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using Markdig; using Markdig.Extensions.AutoIdentifiers; using Markdig.Extensions.Tables; using Markdig.Syntax; using Markdig.Syntax.Inlines; using osu.Framework.Allocation; using osu.Framework.Caching; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; using osuTK; namespace osu.Framework.Graphics.Containers.Markdown { /// /// Visualises a markdown text document. /// [Cached(Type = typeof(IMarkdownTextComponent))] [Cached(Type = typeof(IMarkdownTextFlowComponent))] public class MarkdownContainer : CompositeDrawable, IMarkdownTextComponent, IMarkdownTextFlowComponent { private const int root_level = 0; /// /// Controls which are automatically sized w.r.t. . /// Children's are ignored for automatic sizing. /// Most notably, and of children /// do not affect automatic sizing to avoid circular size dependencies. /// It is not allowed to manually set (or / ) /// on any which are automatically sized. /// public new Axes AutoSizeAxes { get => base.AutoSizeAxes; set { if (value.HasFlagFast(Axes.X)) throw new ArgumentException($"{nameof(MarkdownContainer)} does not support an {nameof(AutoSizeAxes)} of {value}"); base.AutoSizeAxes = value; } } private string text = string.Empty; /// /// The text to visualise. /// public string Text { get => text; set { if (text == value) return; text = value; contentCache.Invalidate(); } } /// /// The vertical spacing between lines. /// public virtual float LineSpacing { get => document.Spacing.Y; set => document.Spacing = new Vector2(0, value); } /// /// The margins of the contained document. /// public MarginPadding DocumentMargin { get => document.Margin; set => document.Margin = value; } /// /// The padding of the contained document. /// public MarginPadding DocumentPadding { get => document.Padding; set => document.Padding = value; } private Uri documentUri; /// /// The URL of the loaded document. /// /// If the provided URL was not a valid absolute URI. protected string DocumentUrl { get => documentUri?.AbsoluteUri; set { if (!Uri.TryCreate(value, UriKind.Absolute, out var uri)) throw new ArgumentException($"Document URL ({value}) must be an absolute URI."); if (documentUri == uri) return; documentUri = uri; contentCache.Invalidate(); } } private Uri rootUri; /// /// The base URL for all root-relative links. /// /// If the provided URL was not a valid absolute URI. protected string RootUrl { get => rootUri?.AbsoluteUri; set { if (!Uri.TryCreate(value, UriKind.Absolute, out var uri)) throw new ArgumentException($"Root URL ({value}) must be an absolute URI."); if (rootUri == uri) return; rootUri = uri; contentCache.Invalidate(); } } private readonly Cached contentCache = new Cached(); private readonly FillFlowContainer document; public MarkdownContainer() { InternalChild = document = new FillFlowContainer { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Direction = FillDirection.Vertical, }; LineSpacing = 25; DocumentPadding = new MarginPadding { Left = 10, Right = 30 }; DocumentMargin = new MarginPadding { Left = 10, Right = 30 }; } [BackgroundDependencyLoader] private void load() { validateContent(); } private void validateContent() { if (!contentCache.IsValid) { var markdownText = Text; var pipeline = CreateBuilder(); var parsed = Markdig.Markdown.Parse(markdownText, pipeline); // Turn all relative URIs in the document into absolute URIs foreach (var link in parsed.Descendants().OfType()) { string url = link.Url; if (string.IsNullOrEmpty(url)) continue; if (!Validation.TryParseUri(url, out Uri linkUri)) continue; if (linkUri.IsAbsoluteUri) continue; if (documentUri != null) { link.Url = rootUri != null && url.StartsWith('/') // Ensure the URI is document-relative by removing all trailing slashes ? new Uri(rootUri, new Uri(url.TrimStart('/'), UriKind.Relative)).AbsoluteUri : new Uri(documentUri, new Uri(url, UriKind.Relative)).AbsoluteUri; } } document.Clear(); foreach (var component in parsed) AddMarkdownComponent(component, document, root_level); contentCache.Validate(); } } protected override void Update() { base.Update(); validateContent(); } public virtual MarkdownTextFlowContainer CreateTextFlow() => new MarkdownTextFlowContainer(); public virtual SpriteText CreateSpriteText() => new SpriteText(); /// /// Adds a component that visualises a to the document. /// /// The to visualise. /// The container to add the visualisation to. /// The level in the document of . /// 0 for the root level, 1 for first-level items in a list, 2 for second-level items in a list, etc. protected virtual void AddMarkdownComponent(IMarkdownObject markdownObject, FillFlowContainer container, int level) { switch (markdownObject) { case ThematicBreakBlock thematicBlock: container.Add(CreateSeparator(thematicBlock)); break; case HeadingBlock headingBlock: container.Add(CreateHeading(headingBlock)); break; case ParagraphBlock paragraphBlock: container.Add(CreateParagraph(paragraphBlock, level)); break; case QuoteBlock quoteBlock: container.Add(CreateQuoteBlock(quoteBlock)); break; case FencedCodeBlock fencedCodeBlock: container.Add(CreateFencedCodeBlock(fencedCodeBlock)); break; case Table table: container.Add(CreateTable(table)); break; case ListBlock listBlock: var childContainer = CreateList(listBlock); container.Add(childContainer); foreach (var single in listBlock) AddMarkdownComponent(single, childContainer, level + 1); break; case ListItemBlock listItemBlock: foreach (var single in listItemBlock) AddMarkdownComponent(single, container, level); break; case HtmlBlock _: // HTML is not supported break; case LinkReferenceDefinitionGroup _: // Link reference doesn't need to be displayed. break; default: container.Add(CreateNotImplemented(markdownObject)); break; } } /// /// Creates the visualiser for a . /// /// The to visualise. /// The visualiser. protected virtual MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new MarkdownHeading(headingBlock); /// /// Creates the visualiser for a . /// /// The to visualise. /// The level in the document of . /// 0 for the root level, 1 for first-level items in a list, 2 for second-level items in a list, etc. /// The visualiser. protected virtual MarkdownParagraph CreateParagraph(ParagraphBlock paragraphBlock, int level) => new MarkdownParagraph(paragraphBlock); /// /// Creates the visualiser for a . /// /// The to visualise. /// The visualiser. protected virtual MarkdownQuoteBlock CreateQuoteBlock(QuoteBlock quoteBlock) => new MarkdownQuoteBlock(quoteBlock); /// /// Creates the visualiser for a . /// /// The to visualise. /// The visualiser. protected virtual MarkdownFencedCodeBlock CreateFencedCodeBlock(FencedCodeBlock fencedCodeBlock) => new MarkdownFencedCodeBlock(fencedCodeBlock); /// /// Creates the visualiser for a . /// /// The to visualise. /// The visualiser. protected virtual MarkdownTable CreateTable(Table table) => new MarkdownTable(table); /// /// Creates the visualiser for a . /// /// The visualiser. protected virtual MarkdownList CreateList(ListBlock listBlock) => new MarkdownList(); /// /// Creates the visualiser for a horizontal separator. /// /// The visualiser. protected virtual MarkdownSeparator CreateSeparator(ThematicBreakBlock thematicBlock) => new MarkdownSeparator(); /// /// Creates the visualiser for an element that isn't implemented. /// /// The that isn't implemented. /// The visualiser. protected virtual NotImplementedMarkdown CreateNotImplemented(IMarkdownObject markdownObject) => new NotImplementedMarkdown(markdownObject); protected virtual MarkdownPipeline CreateBuilder() => new MarkdownPipelineBuilder().UseAutoIdentifiers(AutoIdentifierOptions.GitHub) .UseEmojiAndSmiley() .UseAdvancedExtensions().Build(); } }