From 38b45cb2fb341b1dd4d90dc6befb7ff6e111c3f6 Mon Sep 17 00:00:00 2001 From: Alexandre Mutel Date: Mon, 28 Mar 2016 07:43:46 +0900 Subject: [PATCH] Add better helpers for HtmlAttributes. Use this for MediaExtension --- .../Specs/MediaSpecs.md | 4 +- src/Textamina.Markdig.Tests/Specs/Specs.cs | 6 +- .../Extensions/Medias/MediaExtension.cs | 157 ++++++------------ .../Extensions/Medias/MediaOptions.cs | 90 ++++++++++ .../Renderers/Html/HtmlAttributes.cs | 60 ++++++- .../Renderers/HtmlRenderer.cs | 61 ++++--- 6 files changed, 238 insertions(+), 140 deletions(-) create mode 100644 src/Textamina.Markdig/Extensions/Medias/MediaOptions.cs diff --git a/src/Textamina.Markdig.Tests/Specs/MediaSpecs.md b/src/Textamina.Markdig.Tests/Specs/MediaSpecs.md index e4930019..6b8537bb 100644 --- a/src/Textamina.Markdig.Tests/Specs/MediaSpecs.md +++ b/src/Textamina.Markdig.Tests/Specs/MediaSpecs.md @@ -11,6 +11,6 @@ Allows to embed audio/video links to popular website: ![Video2](https://vimeo.com/8607834) . -

-

+

+

```````````````````````````````` \ No newline at end of file diff --git a/src/Textamina.Markdig.Tests/Specs/Specs.cs b/src/Textamina.Markdig.Tests/Specs/Specs.cs index a0b90cd9..ab609c31 100644 --- a/src/Textamina.Markdig.Tests/Specs/Specs.cs +++ b/src/Textamina.Markdig.Tests/Specs/Specs.cs @@ -18428,11 +18428,11 @@ namespace Textamina.Markdig.Tests // ![Video2](https://vimeo.com/8607834) // // Should be rendered as: - //

- //

+ //

+ //

Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 1, "Extensions Media links"); - TestParser.TestSpec("![Video1](https://www.youtube.com/watch?v=mswPy5bt3TQ)\n\n![Video2](https://vimeo.com/8607834)", "

\n

", "medias"); + TestParser.TestSpec("![Video1](https://www.youtube.com/watch?v=mswPy5bt3TQ)\n\n![Video2](https://vimeo.com/8607834)", "

\n

", "medias"); } } } diff --git a/src/Textamina.Markdig/Extensions/Medias/MediaExtension.cs b/src/Textamina.Markdig/Extensions/Medias/MediaExtension.cs index 625454e5..5be42dcc 100644 --- a/src/Textamina.Markdig/Extensions/Medias/MediaExtension.cs +++ b/src/Textamina.Markdig/Extensions/Medias/MediaExtension.cs @@ -1,105 +1,27 @@ // Copyright (c) Alexandre Mutel. All rights reserved. // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. - using System; -using System.Collections.Generic; using Textamina.Markdig.Renderers; +using Textamina.Markdig.Renderers.Html; using Textamina.Markdig.Renderers.Html.Inlines; using Textamina.Markdig.Syntax.Inlines; namespace Textamina.Markdig.Extensions.Medias { - public class MediaOptions - { - public MediaOptions() - { - Width = 500; - Height = 281; - ExtensionToMimeType = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - {".3gp", "video/3gpp"}, - {".3g2", "video/3gpp2"}, - {".avi", "video/x-msvideo"}, - {".uvh", "video/vnd.dece.hd"}, - {".uvm", "video/vnd.dece.mobile"}, - {".uvu", "video/vnd.uvvu.mp4"}, - {".uvp", "video/vnd.dece.pd"}, - {".uvs", "video/vnd.dece.sd"}, - {".uvv", "video/vnd.dece.video"}, - {".fvt", "video/vnd.fvt"}, - {".f4v", "video/x-f4v"}, - {".flv", "video/x-flv"}, - {".fli", "video/x-fli"}, - {".h261", "video/h261"}, - {".h263", "video/h263"}, - {".h264", "video/h264"}, - {".jpm", "video/jpm"}, - {".jpgv", "video/jpeg"}, - {".m4v", "video/x-m4v"}, - {".asf", "video/x-ms-asf"}, - {".pyv", "video/vnd.ms-playready.media.pyv"}, - {".wm", "video/x-ms-wm"}, - {".wmx", "video/x-ms-wmx"}, - {".wmv", "video/x-ms-wmv"}, - {".wvx", "video/x-ms-wvx"}, - {".mj2", "video/mj2"}, - {".mxu", "video/vnd.mpegurl"}, - {".mpeg", "video/mpeg"}, - {".mp4", "video/mp4"}, - {".ogv", "video/ogg"}, - {".webm", "video/webm"}, - {".qt", "video/quicktime"}, - {".movie", "video/x-sgi-movie"}, - {".viv", "video/vnd.vivo"}, - - {".adp", "audio/adpcm"}, - {".aac", "audio/x-aac"}, - {".aif", "audio/x-aiff"}, - {".uva", "audio/vnd.dece.audio"}, - {".eol", "audio/vnd.digital-winds"}, - {".dra", "audio/vnd.dra"}, - {".dts", "audio/vnd.dts"}, - {".dtshd", "audio/vnd.dts.hd"}, - {".rip", "audio/vnd.rip"}, - {".lvp", "audio/vnd.lucent.voice"}, - {".m3u", "audio/x-mpegurl"}, - {".pya", "audio/vnd.ms-playready.media.pya"}, - {".wma", "audio/x-ms-wma"}, - {".wax", "audio/x-ms-wax"}, - {".mid", "audio/midi"}, - {".mpga", "audio/mpeg"}, - {".mp4a", "audio/mp4"}, - {".ecelp4800", "audio/vnd.nuera.ecelp4800"}, - {".ecelp7470", "audio/vnd.nuera.ecelp7470"}, - {".ecelp9600", "audio/vnd.nuera.ecelp9600"}, - {".oga", "audio/ogg"}, - {".weba", "audio/webm"}, - {".ram", "audio/x-pn-realaudio"}, - {".rmp", "audio/x-pn-realaudio-plugin"}, - {".au", "audio/basic"}, - {".wav", "audio/x-wav"}, - }; - } - - public int Width { get; set; } - - public int Height { get; set; } - - public Dictionary ExtensionToMimeType { get; } - } - - - /// /// Extension for extending image Markdown links in case a video or an audio file is linked and output proper link. /// /// public class MediaExtension : IMarkdownExtension { - public MediaExtension(MediaOptions options = null) + public MediaExtension() : this(new MediaOptions()) { - Options = options; + } + + public MediaExtension(MediaOptions options) + { + Options = options ?? new MediaOptions(); } public MediaOptions Options { get; } @@ -125,22 +47,25 @@ namespace Textamina.Markdig.Extensions.Medias Uri uri; if (Uri.TryCreate(linkInline.Url, UriKind.RelativeOrAbsolute, out uri)) { - // TODO: change code for extensions with Options + var htmlAttributes = new HtmlAttributes(); + var fromAttributes = linkInline.TryGetAttributes(); + if (fromAttributes != null) + { + fromAttributes.CopyTo(htmlAttributes, false, false); + } + + // TODO: this code is not pluggable, so for now, we handle only the following web providers: + // - youtube + // - vimeo var path = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped); - var isMp3 = path.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase); - var isOgg = path.EndsWith(".ogg", StringComparison.OrdinalIgnoreCase); - var isMp4 = path.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase); - var isWebM = path.EndsWith(".webm", StringComparison.OrdinalIgnoreCase); - + string iFrameUrl = null; if (uri.Host.StartsWith("www.youtube.com", StringComparison.OrdinalIgnoreCase)) { var query = SplitQuery(uri); if (query.Length > 0 && query[0].StartsWith("v=")) { - renderer.Write( - $""); - return true; + iFrameUrl = $"https://www.youtube.com/embed/{query[0].Substring(2)}"; } } else if (uri.Host.StartsWith("vimeo.com", StringComparison.OrdinalIgnoreCase)) @@ -148,22 +73,46 @@ namespace Textamina.Markdig.Extensions.Medias var items = path.Split('/'); if (items.Length > 0) { - renderer.Write( - $""); - return true; + iFrameUrl = $"https://player.vimeo.com/video/{items[items.Length - 1]}"; } } - else if (isMp3 || isOgg) + + if (iFrameUrl != null) { - var audioType = isMp3 ? "mp3" : "ogg"; - renderer.Write($""); + renderer.Write($""); return true; } - else if (isMp4 || isWebM) + else { - var videoType = isMp4 ? "mp4" : "webm"; - renderer.Write($""); - return true; + // Otherwise try to detect if we have an audio/video from the file extension + string mimeType; + var lastDot = path.LastIndexOf('.'); + if (lastDot >= 0 && + Options.ExtensionToMimeType.TryGetValue(path.Substring(lastDot), out mimeType)) + { + var isAudio = mimeType.StartsWith("audio"); + var tagType = isAudio ? "audio" : "video"; + + renderer.Write($"<{tagType}"); + htmlAttributes.AddPropertyIfNotExist("width", Options.Width); + if (!isAudio) + { + htmlAttributes.AddPropertyIfNotExist("height", Options.Height); + } + htmlAttributes.AddPropertyIfNotExist("controls", null); + renderer.WriteAttributes(htmlAttributes); + + renderer.Write($">"); + renderer.Write(""); + + return true; + } } } } diff --git a/src/Textamina.Markdig/Extensions/Medias/MediaOptions.cs b/src/Textamina.Markdig/Extensions/Medias/MediaOptions.cs new file mode 100644 index 00000000..dce9b0be --- /dev/null +++ b/src/Textamina.Markdig/Extensions/Medias/MediaOptions.cs @@ -0,0 +1,90 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. +using System; +using System.Collections.Generic; + +namespace Textamina.Markdig.Extensions.Medias +{ + /// + /// Options for the . + /// + public class MediaOptions + { + public MediaOptions() + { + Width = "500"; + Height = "281"; + ExtensionToMimeType = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + {".3gp", "video/3gpp"}, + {".3g2", "video/3gpp2"}, + {".avi", "video/x-msvideo"}, + {".uvh", "video/vnd.dece.hd"}, + {".uvm", "video/vnd.dece.mobile"}, + {".uvu", "video/vnd.uvvu.mp4"}, + {".uvp", "video/vnd.dece.pd"}, + {".uvs", "video/vnd.dece.sd"}, + {".uvv", "video/vnd.dece.video"}, + {".fvt", "video/vnd.fvt"}, + {".f4v", "video/x-f4v"}, + {".flv", "video/x-flv"}, + {".fli", "video/x-fli"}, + {".h261", "video/h261"}, + {".h263", "video/h263"}, + {".h264", "video/h264"}, + {".jpm", "video/jpm"}, + {".jpgv", "video/jpeg"}, + {".m4v", "video/x-m4v"}, + {".asf", "video/x-ms-asf"}, + {".pyv", "video/vnd.ms-playready.media.pyv"}, + {".wm", "video/x-ms-wm"}, + {".wmx", "video/x-ms-wmx"}, + {".wmv", "video/x-ms-wmv"}, + {".wvx", "video/x-ms-wvx"}, + {".mj2", "video/mj2"}, + {".mxu", "video/vnd.mpegurl"}, + {".mpeg", "video/mpeg"}, + {".mp4", "video/mp4"}, + {".ogv", "video/ogg"}, + {".webm", "video/webm"}, + {".qt", "video/quicktime"}, + {".movie", "video/x-sgi-movie"}, + {".viv", "video/vnd.vivo"}, + + {".adp", "audio/adpcm"}, + {".aac", "audio/x-aac"}, + {".aif", "audio/x-aiff"}, + {".uva", "audio/vnd.dece.audio"}, + {".eol", "audio/vnd.digital-winds"}, + {".dra", "audio/vnd.dra"}, + {".dts", "audio/vnd.dts"}, + {".dtshd", "audio/vnd.dts.hd"}, + {".rip", "audio/vnd.rip"}, + {".lvp", "audio/vnd.lucent.voice"}, + {".m3u", "audio/x-mpegurl"}, + {".pya", "audio/vnd.ms-playready.media.pya"}, + {".wma", "audio/x-ms-wma"}, + {".wax", "audio/x-ms-wax"}, + {".mid", "audio/midi"}, + {".mpga", "audio/mpeg"}, + {".mp4a", "audio/mp4"}, + {".ecelp4800", "audio/vnd.nuera.ecelp4800"}, + {".ecelp7470", "audio/vnd.nuera.ecelp7470"}, + {".ecelp9600", "audio/vnd.nuera.ecelp9600"}, + {".oga", "audio/ogg"}, + {".weba", "audio/webm"}, + {".ram", "audio/x-pn-realaudio"}, + {".rmp", "audio/x-pn-realaudio-plugin"}, + {".au", "audio/basic"}, + {".wav", "audio/x-wav"}, + }; + } + + public string Width { get; set; } + + public string Height { get; set; } + + public Dictionary ExtensionToMimeType { get; } + } +} \ No newline at end of file diff --git a/src/Textamina.Markdig/Renderers/Html/HtmlAttributes.cs b/src/Textamina.Markdig/Renderers/Html/HtmlAttributes.cs index eaf4777f..fd9f7bf7 100644 --- a/src/Textamina.Markdig/Renderers/Html/HtmlAttributes.cs +++ b/src/Textamina.Markdig/Renderers/Html/HtmlAttributes.cs @@ -3,6 +3,7 @@ // See the license.txt file in the project root for more information. using System; using System.Collections.Generic; +using System.Globalization; using Textamina.Markdig.Syntax; namespace Textamina.Markdig.Renderers.Html @@ -64,18 +65,55 @@ namespace Textamina.Markdig.Renderers.Html } /// - /// Copies the values from this instance to the specified instance. + /// Adds the specified property only if it does not already exist. + /// + /// The name. + /// The value. + public void AddPropertyIfNotExist(string name, object value) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (Properties == null) + { + Properties = new List> {new KeyValuePair(name, value == null ? null : Convert.ToString(value, CultureInfo.InvariantCulture))}; + } + else + { + // Check that there is not already a property with the same key + bool copyProp = true; + for (int i = 0; i < Properties.Count; i++) + { + var againstProp = Properties[i]; + if (againstProp.Key == name) + { + copyProp = false; + break; + } + } + if (copyProp) + { + Properties.Add(new KeyValuePair(name, value == null ? null : Convert.ToString(value, CultureInfo.InvariantCulture))); + } + } + } + + /// + /// Copies/merge the values from this instance to the specified instance. /// /// The HTML attributes. + /// If set to true it will merge properties to the target htmlAttributes. Default is false + /// If set to true it will try to share Classes and Properties if destination don't have them, otherwise it will make a copy. Default is true /// - public void CopyTo(HtmlAttributes htmlAttributes) + public void CopyTo(HtmlAttributes htmlAttributes, bool mergeIdAndProperties = false, bool shared = true) { if (htmlAttributes == null) throw new ArgumentNullException(nameof(htmlAttributes)); // Add html htmlAttributes to the object - htmlAttributes.Id = Id; + if (!mergeIdAndProperties || Id != null) + { + htmlAttributes.Id = Id; + } if (htmlAttributes.Classes == null) { - htmlAttributes.Classes = Classes; + htmlAttributes.Classes = shared ? Classes : new List(Classes); } else if (Classes != null) { @@ -84,11 +122,21 @@ namespace Textamina.Markdig.Renderers.Html if (htmlAttributes.Properties == null) { - htmlAttributes.Properties = Properties; + htmlAttributes.Properties = shared ? Properties : new List>(Properties); } else if (Properties != null) { - htmlAttributes.Properties.AddRange(Properties); + if (mergeIdAndProperties) + { + foreach (var prop in Properties) + { + htmlAttributes.AddPropertyIfNotExist(prop.Key, prop.Value); + } + } + else + { + htmlAttributes.Properties.AddRange(Properties); + } } } } diff --git a/src/Textamina.Markdig/Renderers/HtmlRenderer.cs b/src/Textamina.Markdig/Renderers/HtmlRenderer.cs index e25aac42..ab0fdf49 100644 --- a/src/Textamina.Markdig/Renderers/HtmlRenderer.cs +++ b/src/Textamina.Markdig/Renderers/HtmlRenderer.cs @@ -200,40 +200,51 @@ namespace Textamina.Markdig.Renderers public HtmlRenderer WriteAttributes(MarkdownObject obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); - var attributes = obj.TryGetAttributes(); - if (attributes != null) + return WriteAttributes(obj.TryGetAttributes()); + } + + /// + /// Writes the specified . + /// + /// The attributes to render. + /// This instance + public HtmlRenderer WriteAttributes(HtmlAttributes attributes) + { + if (attributes == null) { - if (attributes.Id != null) - { - Write(" id=\"").WriteEscape(attributes.Id).Write("\""); - } + return this; + } - if (attributes.Classes != null && attributes.Classes.Count > 0) + if (attributes.Id != null) + { + Write(" id=\"").WriteEscape(attributes.Id).Write("\""); + } + + if (attributes.Classes != null && attributes.Classes.Count > 0) + { + Write(" class=\""); + for (int i = 0; i < attributes.Classes.Count; i++) { - Write(" class=\""); - for (int i = 0; i < attributes.Classes.Count; i++) + var cssClass = attributes.Classes[i]; + if (i > 0) { - var cssClass = attributes.Classes[i]; - if (i > 0) - { - Write(" "); - } - WriteEscape(cssClass); + Write(" "); } - Write("\""); + WriteEscape(cssClass); } + Write("\""); + } - if (attributes.Properties != null && attributes.Properties.Count > 0) + if (attributes.Properties != null && attributes.Properties.Count > 0) + { + foreach (var property in attributes.Properties) { - foreach (var property in attributes.Properties) + Write(" ").Write(property.Key); + if (property.Value != null) { - Write(" ").Write(property.Key); - if (property.Value != null) - { - Write("=").Write("\""); - WriteEscape(property.Value); - Write("\""); - } + Write("=").Write("\""); + WriteEscape(property.Value); + Write("\""); } } }