Add better helpers for HtmlAttributes. Use this for MediaExtension

This commit is contained in:
Alexandre Mutel
2016-03-28 07:43:46 +09:00
parent b6fb86b6b8
commit 38b45cb2fb
6 changed files with 238 additions and 140 deletions

View File

@@ -11,6 +11,6 @@ Allows to embed audio/video links to popular website:
![Video2](https://vimeo.com/8607834)
.
<p><iframe width="420" height="315" src="https://www.youtube.com/embed/mswPy5bt3TQ" frameborder="0" allowfullscreen></iframe></p>
<p><iframe width="500" height="281" src="https://player.vimeo.com/video/8607834" frameborder="0" allowfullscreen></iframe></p>
<p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ" width="500" height="281" frameborder="0" allowfullscreen></iframe></p>
<p><iframe src="https://player.vimeo.com/video/8607834" width="500" height="281" frameborder="0" allowfullscreen></iframe></p>
````````````````````````````````

View File

@@ -18428,11 +18428,11 @@ namespace Textamina.Markdig.Tests
// ![Video2](https://vimeo.com/8607834)
//
// Should be rendered as:
// <p><iframe width="420" height="315" src="https://www.youtube.com/embed/mswPy5bt3TQ" frameborder="0" allowfullscreen></iframe></p>
// <p><iframe width="500" height="281" src="https://player.vimeo.com/video/8607834" frameborder="0" allowfullscreen></iframe></p>
// <p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ" width="500" height="281" frameborder="0" allowfullscreen></iframe></p>
// <p><iframe src="https://player.vimeo.com/video/8607834" width="500" height="281" frameborder="0" allowfullscreen></iframe></p>
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)", "<p><iframe width=\"420\" height=\"315\" src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" frameborder=\"0\" allowfullscreen></iframe></p>\n<p><iframe width=\"500\" height=\"281\" src=\"https://player.vimeo.com/video/8607834\" frameborder=\"0\" allowfullscreen></iframe></p>", "medias");
TestParser.TestSpec("![Video1](https://www.youtube.com/watch?v=mswPy5bt3TQ)\n\n![Video2](https://vimeo.com/8607834)", "<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen></iframe></p>\n<p><iframe src=\"https://player.vimeo.com/video/8607834\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen></iframe></p>", "medias");
}
}
}

View File

@@ -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<string, string>(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<string, string> ExtensionToMimeType { get; }
}
/// <summary>
/// Extension for extending image Markdown links in case a video or an audio file is linked and output proper link.
/// </summary>
/// <seealso cref="Textamina.Markdig.IMarkdownExtension" />
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(
$"<iframe width=\"{Options.Width}\" height=\"{Options.Height}\" src=\"https://www.youtube.com/embed/{query[0].Substring(2)}\" frameborder=\"0\" allowfullscreen></iframe>");
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(
$"<iframe width=\"{Options.Width}\" height=\"{Options.Height}\" src=\"https://player.vimeo.com/video/{items[items.Length - 1]}\" frameborder=\"0\" allowfullscreen></iframe>");
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($"<audio width=\"{Options.Width}\" controls class=\"audioplayer\"><source type=\"audio/{audioType}\" src=\"{linkInline.Url}\"></source></audio>");
renderer.Write($"<iframe src=\"{iFrameUrl}\"");
htmlAttributes.AddPropertyIfNotExist("width", Options.Width);
htmlAttributes.AddPropertyIfNotExist("height", Options.Height);
htmlAttributes.AddPropertyIfNotExist("frameborder", "0");
htmlAttributes.AddPropertyIfNotExist("allowfullscreen", null);
renderer.WriteAttributes(htmlAttributes);
renderer.Write("></iframe>");
return true;
}
else if (isMp4 || isWebM)
else
{
var videoType = isMp4 ? "mp4" : "webm";
renderer.Write($"<video width=\"{Options.Width}\" height=\"{Options.Height}\" controls class=\"videoplayer\"><source type=\"video/{videoType}\" src=\"{linkInline.Url}\"></source></video>");
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($"><source type=\"{mimeType}\" src=\"{linkInline.Url}\"></source></{tagType}>");
renderer.Write("</iframe>");
return true;
}
}
}
}

View File

@@ -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
{
/// <summary>
/// Options for the <see cref="MediaExtension"/>.
/// </summary>
public class MediaOptions
{
public MediaOptions()
{
Width = "500";
Height = "281";
ExtensionToMimeType = new Dictionary<string, string>(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<string, string> ExtensionToMimeType { get; }
}
}

View File

@@ -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
}
/// <summary>
/// Copies the values from this instance to the specified <see cref="HtmlAttributes"/> instance.
/// Adds the specified property only if it does not already exist.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
public void AddPropertyIfNotExist(string name, object value)
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (Properties == null)
{
Properties = new List<KeyValuePair<string, string>> {new KeyValuePair<string, string>(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<string, string>(name, value == null ? null : Convert.ToString(value, CultureInfo.InvariantCulture)));
}
}
}
/// <summary>
/// Copies/merge the values from this instance to the specified <see cref="HtmlAttributes"/> instance.
/// </summary>
/// <param name="htmlAttributes">The HTML attributes.</param>
/// <param name="mergeIdAndProperties">If set to <c>true</c> it will merge properties to the target htmlAttributes. Default is <c>false</c></param>
/// <param name="shared">If set to <c>true</c> it will try to share Classes and Properties if destination don't have them, otherwise it will make a copy. Default is <c>true</c></param>
/// <exception cref="System.ArgumentNullException"></exception>
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<string>(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<KeyValuePair<string, string>>(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);
}
}
}
}

View File

@@ -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());
}
/// <summary>
/// Writes the specified <see cref="HtmlAttributes"/>.
/// </summary>
/// <param name="attributes">The attributes to render.</param>
/// <returns>This instance</returns>
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("\"");
}
}
}