diff --git a/osu.Framework.Tests/Visual/TestCaseUserInterface/TestCaseMarkdown.cs b/osu.Framework.Tests/Visual/TestCaseUserInterface/TestCaseMarkdown.cs index be425fd17f..b0e6d3691b 100644 --- a/osu.Framework.Tests/Visual/TestCaseUserInterface/TestCaseMarkdown.cs +++ b/osu.Framework.Tests/Visual/TestCaseUserInterface/TestCaseMarkdown.cs @@ -70,6 +70,28 @@ public TestCaseMarkdown() ```"; }); + AddStep("Markdown Fenced Code (CSharp)", () => + { + markdownContainer.Text = @"```csharp +// A Hello World! program in C#. +using System; +namespace HelloWorld +{ + class Hello + { + static void Main() + { + Console.WriteLine(""Hello World!""); + + // Keep the console window open in debug mode. + Console.WriteLine(""Press any key to exit.""); + Console.ReadKey(); + } + } +} +```"; + }); + AddStep("Markdown Table", () => { markdownContainer.Text = @@ -112,7 +134,9 @@ public TestCaseMarkdown() AddStep("MarkdownFromInternet", () => { - var req = new WebRequest("https://raw.githubusercontent.com/ppy/osu-wiki/master/wiki/Skinning/skin.ini/en.md"); + const string url = "https://raw.githubusercontent.com/ppy/osu-wiki/master/wiki/Skinning/skin.ini/en.md"; + markdownContainer.RootUrl = url; + var req = new WebRequest(url); req.Finished += () => markdownContainer.Text = req.ResponseString; Task.Run(() => req.PerformAsync()); diff --git a/osu.Framework/Graphics/Containers/Markdown/IMarkdownCodeFlowComponent.cs b/osu.Framework/Graphics/Containers/Markdown/IMarkdownCodeFlowComponent.cs new file mode 100644 index 0000000000..06c1e77f3f --- /dev/null +++ b/osu.Framework/Graphics/Containers/Markdown/IMarkdownCodeFlowComponent.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Framework.Graphics.Containers.Markdown +{ + public interface IMarkdownCodeFlowComponent + { + /// + /// Creates a to display text within this . + /// + /// + /// The defined by the is used by default, + /// but may be overridden via this method to provide additional styling local to this . + /// + /// The . + MarkdownCodeFlowContainer CreateCodeFlow(); + } +} diff --git a/osu.Framework/Graphics/Containers/Markdown/MarkdownCodeFlowContainer.cs b/osu.Framework/Graphics/Containers/Markdown/MarkdownCodeFlowContainer.cs new file mode 100644 index 0000000000..32935ee74d --- /dev/null +++ b/osu.Framework/Graphics/Containers/Markdown/MarkdownCodeFlowContainer.cs @@ -0,0 +1,119 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using ColorCode; +using ColorCode.Parsing; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Framework.Graphics.Containers.Markdown +{ + public class MarkdownCodeFlowContainer : CustomizableTextContainer, IMarkdownTextComponent + { + [Resolved] + private IMarkdownTextComponent parentTextComponent { get; set; } + + public MarkdownCodeFlowContainer() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + public new void AddText(string text, Action creationParameters = null) + => base.AddText(text.Replace("[", "[[").Replace("]", "]]"), creationParameters); + + public void AddCodeText(string codeText,string languageCode) + { + if(string.IsNullOrEmpty(codeText)) + AddParagraph(""); + + var formatter = new ClassFormatter(Styles); + + var language = Languages.FindById(languageCode); + if (language == null) + AddParagraph(codeText); + else + { + //Change new line + AddParagraph(""); + + var codeSyntaxes = formatter.GetCodeSyntaxes(codeText, language); + + foreach (var codeSyntax in codeSyntaxes) + AddText(codeSyntax.ParsedSourceCode, + x => + { + var style = codeSyntax.CodeStyle; + if(style == null) + return; + + ApplyCodeText(x, style); + }); + } + } + + SpriteText IMarkdownTextComponent.CreateSpriteText() => CreateSpriteText(); + + protected virtual MarkdownCodeStyle Styles => new MarkdownCodeStyle().CreateDefaultStyle(); + + protected virtual void ApplyCodeText(SpriteText text, MarkdownCodeStyle.Style codeStyle) + { + var textDrawable = CreateSpriteText(); + + text.Colour = codeStyle.Colour; + text.ShadowColour = codeStyle.BackgroundColour ?? text.ShadowColour; + text.Font = textDrawable.Font.With(weight: codeStyle.Bold ? "Bold" : null, italics: codeStyle.Italic); + } + + public class ClassFormatter : CodeColorizerBase + { + private readonly List codeSyntaxes = new List(); + + private readonly MarkdownCodeStyle markdownCodeStyle; + + public ClassFormatter(MarkdownCodeStyle style, ILanguageParser languageParser = null) : base(null, languageParser) + { + markdownCodeStyle = style; + } + + public List GetCodeSyntaxes(string sourceCode, ILanguage language) + { + codeSyntaxes.Clear(); + languageParser.Parse(sourceCode, language, Write); + return codeSyntaxes; + } + + protected override void Write(string parsedSourceCode, IList scopes) + { + var scopeName = scopes.FirstOrDefault()?.Name; + var style = new MarkdownCodeStyle.Style("unknown"); + + if (!string.IsNullOrEmpty(scopeName) && markdownCodeStyle.Contains(scopeName)) + style = markdownCodeStyle[scopeName]; + + codeSyntaxes.Add(new CodeSyntax + { + ParsedSourceCode = parsedSourceCode, + CodeStyle = style + }); + } + } + + public class CodeSyntax + { + /// + /// Gets or sets the parsed source code + /// + /// The background color. + public string ParsedSourceCode { get; set; } + + /// + /// Gets or sets the parsed code style + /// + public MarkdownCodeStyle.Style CodeStyle { get; set; } + } + } +} diff --git a/osu.Framework/Graphics/Containers/Markdown/MarkdownCodeStyle.cs b/osu.Framework/Graphics/Containers/Markdown/MarkdownCodeStyle.cs new file mode 100644 index 0000000000..ea4cf81c75 --- /dev/null +++ b/osu.Framework/Graphics/Containers/Markdown/MarkdownCodeStyle.cs @@ -0,0 +1,310 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osuTK.Graphics; +using System.Collections.ObjectModel; +using ColorCode.Common; + +namespace osu.Framework.Graphics.Containers.Markdown +{ + public class MarkdownCodeStyle : KeyedCollection + { + protected override string GetKeyForItem(Style item) + { + return item.ScopeName; + } + + protected virtual Color4 Blue => new Color4(0,0,255,255); + protected virtual Color4 White => new Color4(255,255,255,255); + protected virtual Color4 Black => new Color4(0,0,0,255); + protected virtual Color4 DullRed => new Color4(163, 21, 21, 255); + protected virtual Color4 Yellow => new Color4(255, 255, 0,255); + protected virtual Color4 Green => new Color4(0, 128, 0,255); + protected virtual Color4 PowderBlue => new Color4(176, 224, 230,255); + protected virtual Color4 Teal => new Color4(0, 128, 12,255); + protected virtual Color4 Gray => new Color4(128, 128, 128,255); + protected virtual Color4 Navy => new Color4(0, 0, 128,255); + protected virtual Color4 OrangeRed => new Color4(255, 69, 0,255); + protected virtual Color4 Purple => new Color4(128, 0, 128,255); + protected virtual Color4 Red => new Color4(255, 0, 0,255); + protected virtual Color4 MediumTurqoise => new Color4(72, 209, 204,255); + protected virtual Color4 Magenta => new Color4(255, 0, 255,255); + protected virtual Color4 OliveDrab =>new Color4(107, 142, 35,255); + protected virtual Color4 DarkOliveGreen => new Color4(85, 107, 47,255); + protected virtual Color4 DarkCyan => new Color4(0, 139, 139,255); + + protected virtual Color4 DarkBackground => new Color4(30, 30, 30,255); + protected virtual Color4 DarkPlainText => new Color4(218, 218, 218,255); + + protected virtual Color4 DarkXmlDelimeter => new Color4(128, 128, 128,255); + protected virtual Color4 DarkXmlName => new Color4(230, 230, 230,255); + protected virtual Color4 DarkXmlAttribute => new Color4(146, 202, 244,255); + protected virtual Color4 DarkXamlCData => new Color4(192, 208, 136,255); + protected virtual Color4 DarkXmlComment => new Color4(96, 139, 78,255); + + protected virtual Color4 DarkComment => new Color4(87, 166, 74,255); + protected virtual Color4 DarkKeyword =>new Color4(86, 156, 214,255); + protected virtual Color4 DarkGray => new Color4(155, 155, 155,255); + protected virtual Color4 DarkNumber => new Color4(181, 206, 168,255); + protected virtual Color4 DarkClass =>new Color4(78, 201, 176,255); + protected virtual Color4 DarkString =>new Color4(214, 157, 133,255); + + public virtual MarkdownCodeStyle CreateDefaultStyle() + { + return new MarkdownCodeStyle + { + new Style(ScopeName.PlainText) + { + Colour = DarkPlainText, + BackgroundColour = DarkBackground, + }, + new Style(ScopeName.HtmlServerSideScript) + { + BackgroundColour = Yellow, + }, + new Style(ScopeName.HtmlComment) + { + Colour = DarkComment, + }, + new Style(ScopeName.HtmlTagDelimiter) + { + Colour = DarkKeyword, + }, + new Style(ScopeName.HtmlElementName) + { + Colour = DullRed, + }, + new Style(ScopeName.HtmlAttributeName) + { + Colour = Red, + }, + new Style(ScopeName.HtmlAttributeValue) + { + Colour = DarkKeyword, + }, + new Style(ScopeName.HtmlOperator) + { + Colour = DarkKeyword, + }, + new Style(ScopeName.Comment) + { + Colour = DarkComment, + }, + new Style(ScopeName.XmlDocTag) + { + Colour = DarkXmlComment, + }, + new Style(ScopeName.XmlDocComment) + { + Colour = DarkXmlComment, + }, + new Style(ScopeName.String) + { + Colour = DarkString, + }, + new Style(ScopeName.StringCSharpVerbatim) + { + Colour = DarkString, + }, + new Style(ScopeName.Keyword) + { + Colour = DarkKeyword, + }, + new Style(ScopeName.PreprocessorKeyword) + { + Colour = DarkKeyword, + }, + new Style(ScopeName.HtmlEntity) + { + Colour = Red, + }, + new Style(ScopeName.XmlAttribute) + { + Colour = DarkXmlAttribute, + }, + new Style(ScopeName.XmlAttributeQuotes) + { + Colour = DarkKeyword, + }, + new Style(ScopeName.XmlAttributeValue) + { + Colour = DarkKeyword, + }, + new Style(ScopeName.XmlCDataSection) + { + Colour = DarkXamlCData, + }, + new Style(ScopeName.XmlComment) + { + Colour = DarkComment, + }, + new Style(ScopeName.XmlDelimiter) + { + Colour = DarkXmlDelimeter, + }, + new Style(ScopeName.XmlName) + { + Colour = DarkXmlName, + }, + new Style(ScopeName.ClassName) + { + Colour = DarkClass, + }, + new Style(ScopeName.CssSelector) + { + Colour = DullRed, + }, + new Style(ScopeName.CssPropertyName) + { + Colour = Red, + }, + new Style(ScopeName.CssPropertyValue) + { + Colour = DarkKeyword, + }, + new Style(ScopeName.SqlSystemFunction) + { + Colour = Magenta, + }, + new Style(ScopeName.PowerShellAttribute) + { + Colour = PowderBlue, + }, + new Style(ScopeName.PowerShellOperator) + { + Colour = DarkGray, + }, + new Style(ScopeName.PowerShellType) + { + Colour = Teal, + }, + new Style(ScopeName.PowerShellVariable) + { + Colour = OrangeRed, + }, + + new Style(ScopeName.Type) + { + Colour = Teal, + }, + new Style(ScopeName.TypeVariable) + { + Colour = Teal, + Italic = true, + }, + new Style(ScopeName.NameSpace) + { + Colour = Navy, + }, + new Style(ScopeName.Constructor) + { + Colour = Purple, + }, + new Style(ScopeName.Predefined) + { + Colour = Navy, + }, + new Style(ScopeName.PseudoKeyword) + { + Colour = Navy, + }, + new Style(ScopeName.StringEscape) + { + Colour = DarkGray, + }, + new Style(ScopeName.ControlKeyword) + { + Colour = DarkKeyword, + }, + new Style(ScopeName.Number) + { + Colour = DarkNumber + }, + new Style(ScopeName.Operator), + new Style(ScopeName.Delimiter), + + new Style(ScopeName.MarkdownHeader) + { + Colour = DarkKeyword, + Bold = true, + }, + new Style(ScopeName.MarkdownCode) + { + Colour = DarkString, + }, + new Style(ScopeName.MarkdownListItem) + { + Bold = true, + }, + new Style(ScopeName.MarkdownEmph) + { + Italic = true, + }, + new Style(ScopeName.MarkdownBold) + { + Bold = true, + }, + + new Style(ScopeName.BuiltinFunction) + { + Colour = OliveDrab, + Bold = true, + }, + new Style(ScopeName.BuiltinValue) + { + Colour = DarkOliveGreen, + Bold = true, + }, + new Style(ScopeName.Attribute) + { + Colour = DarkCyan, + Italic = true, + }, + new Style(ScopeName.SpecialCharacter), + }; + } + + public class Style + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the scope the style defines. + public Style(string scopeName) + { + ScopeName = scopeName; + Colour = Color4.White; + } + + /// + /// Gets or sets the background color. + /// + /// The background color. + public Color4? BackgroundColour{ get; set; } + + /// + /// Gets or sets the foreground color. + /// + /// The foreground color. + public Color4 Colour { get; set; } + + /// + /// Gets or sets the name of the scope the style defines. + /// + /// The name of the scope the style defines. + public string ScopeName { get; set; } + + /// + /// Gets or sets italic font style. + /// + /// True if italic. + public bool Italic { get; set; } + + /// + /// Gets or sets bold font style. + /// + /// True if bold. + public bool Bold { get; set; } + } + } +} diff --git a/osu.Framework/Graphics/Containers/Markdown/MarkdownContainer.cs b/osu.Framework/Graphics/Containers/Markdown/MarkdownContainer.cs index 21f38d7fab..3377eb6cb8 100644 --- a/osu.Framework/Graphics/Containers/Markdown/MarkdownContainer.cs +++ b/osu.Framework/Graphics/Containers/Markdown/MarkdownContainer.cs @@ -1,10 +1,14 @@ // 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 System.Text.RegularExpressions; 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.Graphics.Sprites; @@ -17,12 +21,15 @@ namespace osu.Framework.Graphics.Containers.Markdown /// [Cached(Type = typeof(IMarkdownTextComponent))] [Cached(Type = typeof(IMarkdownTextFlowComponent))] - public class MarkdownContainer : CompositeDrawable, IMarkdownTextComponent, IMarkdownTextFlowComponent + [Cached(Type = typeof(IMarkdownCodeFlowComponent))] + public class MarkdownContainer : CompositeDrawable, IMarkdownTextComponent, IMarkdownTextFlowComponent , IMarkdownCodeFlowComponent { private const int root_level = 0; private string text = string.Empty; + private string rootUrl = string.Empty; + /// /// The text to visualise. /// @@ -39,6 +46,22 @@ public string Text } } + /// + /// Root url for relative link in document. + /// + public string RootUrl + { + get => rootUrl; + set + { + if (rootUrl == value) + return; + rootUrl = value; + + contentCache.Invalidate(); + } + } + /// /// The vertical spacing between lines. /// @@ -103,10 +126,31 @@ private void validateContent() if (!contentCache.IsValid) { var markdownText = Text; + + document.Clear(); + + if (string.IsNullOrEmpty(Text)) + return; + var pipeline = CreateBuilder(); var parsed = Markdig.Markdown.Parse(markdownText, pipeline); - document.Clear(); + //convert relative path to absolute path + if (!string.IsNullOrEmpty(RootUrl)) + { + var linkInlines = parsed.Descendants().OfType(); + foreach (var linkInline in linkInlines) + { + var url = linkInline.Url; + if (!string.IsNullOrEmpty(url) && !Regex.IsMatch(url, @"(http|https)://(([www\.])?|([\da-z-\.]+))\.([a-z\.]{2,3})$")) + { + var baseUri = new Uri(RootUrl); + var relativeUri = new Uri(baseUri, url); + linkInline.Url = relativeUri.AbsoluteUri; + } + } + } + foreach (var component in parsed) AddMarkdownComponent(component, document, root_level); @@ -123,6 +167,8 @@ protected override void Update() public virtual MarkdownTextFlowContainer CreateTextFlow() => new MarkdownTextFlowContainer(); + public virtual MarkdownCodeFlowContainer CreateCodeFlow() => new MarkdownCodeFlowContainer(); + public virtual SpriteText CreateSpriteText() => new SpriteText(); /// diff --git a/osu.Framework/Graphics/Containers/Markdown/MarkdownFencedCodeBlock.cs b/osu.Framework/Graphics/Containers/Markdown/MarkdownFencedCodeBlock.cs index d35fbed48e..d8ddecec83 100644 --- a/osu.Framework/Graphics/Containers/Markdown/MarkdownFencedCodeBlock.cs +++ b/osu.Framework/Graphics/Containers/Markdown/MarkdownFencedCodeBlock.cs @@ -16,12 +16,12 @@ namespace osu.Framework.Graphics.Containers.Markdown /// code /// ``` /// - public class MarkdownFencedCodeBlock : CompositeDrawable, IMarkdownTextFlowComponent + public class MarkdownFencedCodeBlock : CompositeDrawable, IMarkdownCodeFlowComponent { private readonly FencedCodeBlock fencedCodeBlock; [Resolved] - private IMarkdownTextFlowComponent parentFlowComponent { get; set; } + private IMarkdownCodeFlowComponent parentFlowComponent { get; set; } public MarkdownFencedCodeBlock(FencedCodeBlock fencedCodeBlock) { @@ -34,15 +34,18 @@ public MarkdownFencedCodeBlock(FencedCodeBlock fencedCodeBlock) [BackgroundDependencyLoader] private void load() { - TextFlowContainer textFlowContainer; + MarkdownCodeFlowContainer textFlowContainer; InternalChildren = new [] { CreateBackground(), - textFlowContainer = CreateTextFlow(), + textFlowContainer = CreateCodeFlow(), }; - foreach (var line in fencedCodeBlock.Lines.Lines) - textFlowContainer.AddParagraph(line.ToString()); + var lines = fencedCodeBlock.Lines; + for (int i = 0; i < lines.Count; i++) + { + textFlowContainer.AddCodeText(lines.Lines[i].ToString(), fencedCodeBlock.Info); + } } protected virtual Drawable CreateBackground() => new Box @@ -52,9 +55,9 @@ private void load() Alpha = 0.5f }; - public virtual MarkdownTextFlowContainer CreateTextFlow() + public virtual MarkdownCodeFlowContainer CreateCodeFlow() { - var textFlow = parentFlowComponent.CreateTextFlow(); + var textFlow = parentFlowComponent.CreateCodeFlow(); textFlow.Margin = new MarginPadding { Left = 10, Right = 10, Top = 10, Bottom = 10 }; return textFlow; } diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index 82ac5cfad0..e8eb06245b 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -31,6 +31,7 @@ $(DefineConstants);JETBRAINS_ANNOTATIONS +