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.Threading.Tasks;
7using Markdig.Syntax.Inlines;
8using NUnit.Framework;
9using osu.Framework.Graphics;
10using osu.Framework.Graphics.Containers;
11using osu.Framework.Graphics.Containers.Markdown;
12using osu.Framework.Graphics.Sprites;
13using osu.Framework.IO.Network;
14
15namespace osu.Framework.Tests.Visual.UserInterface
16{
17 public class TestSceneMarkdownContainer : FrameworkTestScene
18 {
19 private TestMarkdownContainer markdownContainer;
20
21 [SetUp]
22 public void Setup() => Schedule(() =>
23 {
24 Child = new BasicScrollContainer
25 {
26 RelativeSizeAxes = Axes.Both,
27 Child = markdownContainer = new TestMarkdownContainer
28 {
29 RelativeSizeAxes = Axes.X,
30 AutoSizeAxes = Axes.Y
31 }
32 };
33 });
34
35 [Test]
36 public void TestHeading()
37 {
38 AddStep("Markdown Heading", () =>
39 {
40 markdownContainer.Text = @"# Header 1
41## Header 2
42### Header 3
43#### Header 4
44##### Header 5";
45 });
46 }
47
48 [Test]
49 public void TestSeparator()
50 {
51 AddStep("Markdown Seperator", () =>
52 {
53 markdownContainer.Text = @"Line above
54
55---
56
57Line below";
58 });
59 }
60
61 [Test]
62 public void TestUnorderedList()
63 {
64 AddStep("Markdown Unordered List", () =>
65 {
66 markdownContainer.Text = @"- [1. Blocks](#1-blocks)
67 - [1.1 Code block](#11-code-block)
68 - [1.2 Text block](#12-text-block)
69 - [1.3 Escape block](#13-escape-block)
70 - [1.4 Whitespace control](#14-whitespace-control)
71- [2 Comments](#2-comments)
72- [3 Literals](#3-literals)
73 - [3.1 Strings](#31-strings)
74 - [3.2 Numbers](#32-numbers)
75 - [3.3 Boolean](#33-boolean)
76 - [3.4 null](#34-null)";
77 });
78 }
79
80 [Test]
81 public void TestQuote()
82 {
83 AddStep("Markdown Quote", () => { markdownContainer.Text = @"> **input**"; });
84 }
85
86 [Test]
87 public void TestFencedCode()
88 {
89 AddStep("Markdown Fenced Code", () =>
90 {
91 markdownContainer.Text = @"```scriban-html
92
93[Escape me]
94[[Escape me]]
95
96{{
97 x = ""5"" # This assignment will not output anything
98 x # This expression will print 5
99 x + 1 # This expression will print 6
100}}
101```";
102 });
103 }
104
105 [Test]
106 public void TestTable()
107 {
108 AddStep("Markdown Table", () =>
109 {
110 markdownContainer.Text =
111 @"|Operator | Description
112|--------------------|------------
113| `'left' + <right>` | concatenates left to right string: `""ab"" + ""c"" -> ""abc""`
114| `'left' * <right>` | concatenates the left string `right` times: `'a' * 5 -> aaaaa`. left and right and be swapped as long as there is one string and one number.";
115 });
116 }
117
118 [Test]
119 public void TestTableAlignment()
120 {
121 AddStep("Markdown Table (Aligned)", () =>
122 {
123 markdownContainer.Text =
124 @"| Left-Aligned | Center Aligned | Right Aligned |
125| :------------ |:---------------:| -----:|
126| col 3 is | some wordy text | $1600 |
127| col 2 is | centered | $12 |
128| zebra stripes | are neat | $1 |";
129 });
130 }
131
132 [Test]
133 public void TestParagraph()
134 {
135 AddStep("Markdown Paragraph", () =>
136 {
137 markdownContainer.Text = @"A text enclosed by `{{` and `}}` is a scriban **code block** that will be evaluated by the scriban templating engine.
138
139The greedy mode using the character - (e.g {{- or -}}), removes any whitespace, including newlines Examples with the variable name = ""foo"":";
140 });
141 }
142
143 [Test]
144 public void TestLink()
145 {
146 AddStep("MarkdownLink", () => { markdownContainer.Text = @"[click the circles to the beat](https://osu.ppy.sh)"; });
147 }
148
149 [Test]
150 public void TestImage()
151 {
152 AddStep("MarkdownImage", () => { markdownContainer.Text = @""; });
153 }
154
155 [Test]
156 public void TestMarkdownFromInternet()
157 {
158 WebRequest req = null;
159
160 AddStep("MarkdownFromInternet", () =>
161 {
162 req = new WebRequest("https://raw.githubusercontent.com/ppy/osu-wiki/master/wiki/Skinning/skin.ini/en.md");
163 req.Finished += () => Schedule(() => markdownContainer.Text = req.GetResponseString());
164
165 Task.Run(() => req.PerformAsync());
166 });
167
168 AddUntilStep("wait for request completed", () => req.Completed);
169
170 AddAssert("ensure content", () => !string.IsNullOrEmpty(markdownContainer.Text));
171 }
172
173 [Test]
174 public void TestEmphases()
175 {
176 AddStep("Emphases", () =>
177 {
178 markdownContainer.Text = @"_italic with underscore_
179*italic with asterisk*
180__bold with underscore__
181**bold with asterisk**
182*__italic with asterisk, bold with underscore__*
183_**italic with underscore, bold with asterisk**_";
184 });
185 }
186
187 [Test]
188 public void TestLineBreaks()
189 {
190 AddStep("new lines", () =>
191 {
192 markdownContainer.Text = @"line 1
193soft break\
194soft break with '\'";
195 });
196 }
197
198 [Test]
199 public void TestRootRelativeLink()
200 {
201 AddStep("set content", () =>
202 {
203 markdownContainer.DocumentUrl = "https://some.test.url/some/path/2";
204 markdownContainer.Text = "[link](/file)";
205 });
206
207 AddAssert("has correct link", () => markdownContainer.Links[0].Url == "https://some.test.url/file");
208 }
209
210 [Test]
211 public void TestDocumentRelativeLink()
212 {
213 AddStep("set content", () => markdownContainer.DocumentUrl = "https://some.test.url/some/path/2");
214
215 AddStep("set 'file'", () => markdownContainer.Text = "[link](file)");
216 AddAssert("has correct link", () => markdownContainer.Links[0].Url == "https://some.test.url/some/path/file");
217
218 AddStep("set './file'", () => markdownContainer.Text = "[link](./file)");
219 AddAssert("has correct link", () => markdownContainer.Links[1].Url == "https://some.test.url/some/path/file");
220
221 AddStep("set '../folder/file'", () => markdownContainer.Text = "[link](../folder/file)");
222 AddAssert("has correct link", () => markdownContainer.Links[2].Url == "https://some.test.url/some/folder/file");
223 }
224
225 [Test]
226 public void TestDocumentRelativeLinkWithNoUri()
227 {
228 AddStep("set content", () => { markdownContainer.Text = "[link](file)"; });
229
230 AddAssert("has correct link", () => markdownContainer.Links[0].Url == "file");
231 }
232
233 [Test]
234 public void TestRootRelativeLinkWithNoUri()
235 {
236 AddStep("set content", () => { markdownContainer.Text = "[link](/file)"; });
237
238 AddAssert("has correct link", () => markdownContainer.Links[0].Url == "/file");
239 }
240
241 [Test]
242 public void TestDocumentRelativeLinkWithRootOverride()
243 {
244 AddStep("set content", () =>
245 {
246 markdownContainer.DocumentUrl = "https://some.test.url/some/path/2";
247 markdownContainer.RootUrl = "https://some.test.url/some/";
248 markdownContainer.Text = "[link](file)";
249 });
250
251 AddAssert("has correct link", () => markdownContainer.Links[0].Url == "https://some.test.url/some/path/file");
252 }
253
254 [Test]
255 public void TestRootRelativeLinkWithRootOverride()
256 {
257 AddStep("set content", () =>
258 {
259 markdownContainer.DocumentUrl = "https://some.test.url/some/path/2";
260 markdownContainer.RootUrl = "https://some.test.url/some/";
261 markdownContainer.Text = "[link](/file)";
262 });
263
264 AddAssert("has correct link", () => markdownContainer.Links[0].Url == "https://some.test.url/some/file");
265 }
266
267 [Test]
268 public void TestRootRelativeLinkWithRootOverrideCantEscape()
269 {
270 AddStep("set content", () =>
271 {
272 markdownContainer.DocumentUrl = "https://some.test.url/some/path/2";
273 markdownContainer.RootUrl = "https://some.test.url/some/";
274 markdownContainer.Text = "[link](/../../../file)";
275 });
276
277 AddAssert("has correct link", () => markdownContainer.Links[0].Url == "https://some.test.url/file");
278 }
279
280 [Test]
281 public void TestAbsoluteLinkWithDifferentScheme()
282 {
283 AddStep("set content", () =>
284 {
285 markdownContainer.DocumentUrl = "https://some.test.url/some/path/2";
286 markdownContainer.RootUrl = "https://some.test.url/some/";
287 markdownContainer.Text = "[link](mailto:contact@ppy.sh)";
288 });
289
290 AddAssert("has correct link", () => markdownContainer.Links[0].Url == "mailto:contact@ppy.sh");
291 }
292
293 [Test]
294 public void TestAutoLinkInline()
295 {
296 AddStep("set content", () =>
297 {
298 markdownContainer.Text = "<https://discord.gg/ppy>";
299 });
300
301 AddAssert("has correct autolink", () => markdownContainer.AutoLinks[0].Url == "https://discord.gg/ppy");
302 }
303
304 private class TestMarkdownContainer : MarkdownContainer
305 {
306 public new string DocumentUrl
307 {
308 get => base.DocumentUrl;
309 set => base.DocumentUrl = value;
310 }
311
312 public new string RootUrl
313 {
314 get => base.RootUrl;
315 set => base.RootUrl = value;
316 }
317
318 public readonly List<LinkInline> Links = new List<LinkInline>();
319
320 public readonly List<AutolinkInline> AutoLinks = new List<AutolinkInline>();
321
322 public override MarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer
323 {
324 UrlAdded = url => Links.Add(url),
325 AutoLinkAdded = autolink => AutoLinks.Add(autolink),
326 };
327
328 public override SpriteText CreateSpriteText() => base.CreateSpriteText().With(t => t.Font = t.Font.With("Roboto", weight: "Regular"));
329
330 private class TestMarkdownTextFlowContainer : MarkdownTextFlowContainer
331 {
332 public Action<LinkInline> UrlAdded;
333
334 public Action<AutolinkInline> AutoLinkAdded;
335
336 protected override void AddLinkText(string text, LinkInline linkInline)
337 {
338 base.AddLinkText(text, linkInline);
339
340 UrlAdded?.Invoke(linkInline);
341 }
342
343 protected override void AddAutoLink(AutolinkInline autolinkInline)
344 {
345 base.AddAutoLink(autolinkInline);
346
347 AutoLinkAdded?.Invoke(autolinkInline);
348 }
349 }
350 }
351 }
352}