A game framework written with osu! in mind.
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.Collections.Generic;
6using System.Linq;
7using Markdig.Syntax.Inlines;
8using osu.Framework.Allocation;
9using osu.Framework.Extensions.IEnumerableExtensions;
10using osu.Framework.Graphics.Sprites;
11using osuTK.Graphics;
12
13namespace osu.Framework.Graphics.Containers.Markdown
14{
15 /// <summary>
16 /// Markdown text flow container.
17 /// </summary>
18 public class MarkdownTextFlowContainer : CustomizableTextContainer, IMarkdownTextComponent
19 {
20 public float TotalTextWidth => Padding.TotalHorizontal + FlowingChildren.Sum(x => x.BoundingBox.Size.X);
21
22 [Resolved]
23 private IMarkdownTextComponent parentTextComponent { get; set; }
24
25 public MarkdownTextFlowContainer()
26 {
27 RelativeSizeAxes = Axes.X;
28 AutoSizeAxes = Axes.Y;
29 }
30
31 protected void AddDrawable(Drawable drawable)
32 => base.AddText("[" + AddPlaceholder(drawable) + "]");
33
34 public new void AddText(string text, Action<SpriteText> creationParameters = null)
35 => base.AddText(Escape(text), creationParameters);
36
37 public new IEnumerable<Drawable> AddParagraph(string text, Action<SpriteText> creationParameters = null)
38 => base.AddParagraph(Escape(text), creationParameters);
39
40 public void AddInlineText(ContainerInline container)
41 {
42 foreach (var single in container)
43 {
44 switch (single)
45 {
46 case LiteralInline literal:
47 var text = literal.Content.ToString();
48
49 if (container.GetPrevious(literal) is HtmlInline && container.GetNext(literal) is HtmlInline)
50 AddHtmlInLineText(text, literal);
51 else if (container.GetNext(literal) is HtmlEntityInline entityInLine)
52 AddHtmlEntityInlineText(text, entityInLine);
53 else
54 {
55 switch (literal.Parent)
56 {
57 case EmphasisInline _:
58 var parent = literal.Parent;
59
60 var emphases = new List<string>();
61
62 while (parent is EmphasisInline e)
63 {
64 emphases.Add(e.DelimiterCount == 2 ? new string(e.DelimiterChar, 2) : e.DelimiterChar.ToString());
65 parent = parent.Parent;
66 }
67
68 addEmphasis(text, emphases);
69
70 break;
71
72 case LinkInline linkInline:
73 {
74 if (!linkInline.IsImage)
75 AddLinkText(text, linkInline);
76 break;
77 }
78
79 default:
80 AddText(text);
81 break;
82 }
83 }
84
85 break;
86
87 case CodeInline codeInline:
88 AddCodeInLine(codeInline);
89 break;
90
91 case LinkInline linkInline when linkInline.IsImage:
92 AddImage(linkInline);
93 break;
94
95 case HtmlInline _:
96 case HtmlEntityInline _:
97 // Handled by the next literal
98 break;
99
100 case LineBreakInline lineBreak:
101 if (lineBreak.IsHard)
102 NewParagraph();
103 else
104 NewLine();
105 break;
106
107 case ContainerInline innerContainer:
108 AddInlineText(innerContainer);
109 break;
110
111 case AutolinkInline autoLink:
112 AddAutoLink(autoLink);
113 break;
114
115 default:
116 AddNotImplementedInlineText(single);
117 break;
118 }
119 }
120 }
121
122 protected virtual void AddHtmlInLineText(string text, LiteralInline literalInline)
123 => AddText(text, t => t.Colour = Color4.MediumPurple);
124
125 protected virtual void AddHtmlEntityInlineText(string text, HtmlEntityInline entityInLine)
126 => AddText(text, t => t.Colour = Color4.GreenYellow);
127
128 protected virtual void AddLinkText(string text, LinkInline linkInline)
129 => AddDrawable(new MarkdownLinkText(text, linkInline));
130
131 protected virtual void AddAutoLink(AutolinkInline autolinkInline)
132 => AddDrawable(new MarkdownLinkText(autolinkInline));
133
134 protected virtual void AddCodeInLine(CodeInline codeInline)
135 => AddText(codeInline.Content, t => { t.Colour = Color4.Orange; });
136
137 protected virtual void AddImage(LinkInline linkInline)
138 => AddDrawable(new MarkdownImage(linkInline.Url));
139
140 protected virtual void AddNotImplementedInlineText(Inline inline)
141 => AddText(inline.GetType() + " not implemented.", t => t.Colour = Color4.Red);
142
143 private void addEmphasis(string text, List<string> emphases)
144 {
145 bool hasItalic = false;
146 bool hasBold = false;
147
148 foreach (var e in emphases)
149 {
150 switch (e)
151 {
152 case "*":
153 case "_":
154 hasItalic = true;
155 break;
156
157 case "**":
158 case "__":
159 hasBold = true;
160 break;
161 }
162 }
163
164 var textDrawable = CreateEmphasisedSpriteText(hasBold, hasItalic);
165 textDrawable.Text = text;
166
167 AddDrawable(textDrawable);
168 }
169
170 protected override SpriteText CreateSpriteText() => parentTextComponent.CreateSpriteText();
171
172 /// <summary>
173 /// Creates an emphasised <see cref="SpriteText"/>.
174 /// </summary>
175 /// <param name="bold">Whether the text should be emboldened.</param>
176 /// <param name="italic">Whether the text should be italicised.</param>
177 /// <returns>The <see cref="SpriteText"/> with emphases applied.</returns>
178 protected virtual SpriteText CreateEmphasisedSpriteText(bool bold, bool italic)
179 {
180 var textDrawable = CreateSpriteText();
181
182 textDrawable.Font = textDrawable.Font.With(weight: bold ? "Bold" : null, italics: italic);
183
184 return textDrawable;
185 }
186
187 SpriteText IMarkdownTextComponent.CreateSpriteText() => CreateSpriteText();
188 }
189}