diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index d951191..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "externals/HtmlXaml.Core"] - path = externals/HtmlXaml.Core - url = https://github.com/whistyun/HtmlXaml.Core.git diff --git a/MdXaml.Html/Core/Parsers.MarkdigExtensions/FigureParser.cs b/MdXaml.Html/Core/Parsers.MarkdigExtensions/FigureParser.cs new file mode 100644 index 0000000..877cf31 --- /dev/null +++ b/MdXaml.Html/Core/Parsers.MarkdigExtensions/FigureParser.cs @@ -0,0 +1,44 @@ +using HtmlAgilityPack; +using MdXaml.Html.Core.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Documents; + +namespace MdXaml.Html.Core.Parsers.MarkdigExtensions +{ + public class FigureParser : IBlockTagParser + { + public IEnumerable SupportTag => new[] { "figure" }; + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = TryReplace(node, manager, out var list); + generated = list; + return rtn; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var captionPair = + node.ChildNodes + .SkipComment() + .Filter(nd => string.Equals(nd.Name, "figcaption", StringComparison.OrdinalIgnoreCase)); + + var captionList = captionPair.Item1; + var contentList = captionPair.Item2; + + + var captionBlock = captionList.SelectMany(c => manager.Grouping(manager.ParseBlockAndInline(c))); + var contentBlock = contentList.SelectMany(c => manager.Grouping(manager.ParseChildrenJagging(c))); + + var section = new Section(); + section.Tag = manager.GetTag(Tags.TagFigure); + section.Blocks.AddRange(contentBlock); + section.Blocks.AddRange(captionBlock); + + generated = new[] { section }; + return false; + } + } +} diff --git a/MdXaml.Html/Core/Parsers.MarkdigExtensions/GridTableParser.cs b/MdXaml.Html/Core/Parsers.MarkdigExtensions/GridTableParser.cs new file mode 100644 index 0000000..3b91a45 --- /dev/null +++ b/MdXaml.Html/Core/Parsers.MarkdigExtensions/GridTableParser.cs @@ -0,0 +1,211 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Windows.Documents; +using System.Windows; +using MdXaml.Html.Core.Utils; + +namespace MdXaml.Html.Core.Parsers.MarkdigExtensions +{ + public class GridTableParser : IBlockTagParser, IHasPriority + { + public int Priority => HasPriority.DefaultPriority + 1000; + + public IEnumerable SupportTag => new[] { "table" }; + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = TryReplace(node, manager, out var list); + generated = list; + return rtn; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var table = new Table(); + + ParseColumnStyle(node, table); + + int totalColCount = 0; + + var theadRows = node.SelectNodes("./thead/tr"); + if (theadRows is not null) + { + var group = CreateRowGroup(theadRows, manager, out int colCount); + group.Tag = manager.GetTag(Tags.TagTableHeader); + table.RowGroups.Add(group); + + totalColCount = Math.Max(totalColCount, colCount); + } + + var tbodyRows = new List(); + foreach (var child in node.ChildNodes) + { + if (string.Equals(child.Name, "tr", StringComparison.OrdinalIgnoreCase)) + tbodyRows.Add(child); + + if (string.Equals(child.Name, "tbody", StringComparison.OrdinalIgnoreCase)) + tbodyRows.AddRange(child.ChildNodes.CollectTag("tr")); + } + if (tbodyRows.Count > 0) + { + var group = CreateRowGroup(tbodyRows, manager, out int colCount); + group.Tag = manager.GetTag(Tags.TagTableBody); + table.RowGroups.Add(group); + + int idx = 0; + foreach (var row in group.Rows) + { + var useTag = (++idx & 1) == 0 ? Tags.TagEvenTableRow : Tags.TagOddTableRow; + row.Tag = manager.GetTag(useTag); + } + + totalColCount = Math.Max(totalColCount, colCount); + } + + var tfootRows = node.SelectNodes("./tfoot/tr"); + if (tfootRows is not null) + { + var group = CreateRowGroup(tfootRows, manager, out int colCount); + group.Tag = manager.GetTag(Tags.TagTableFooter); + table.RowGroups.Add(group); + + totalColCount = Math.Max(totalColCount, colCount); + } + + while (totalColCount >= table.Columns.Count) + { + table.Columns.Add(new TableColumn()); + } + + var captions = node.SelectNodes("./caption"); + if (captions is not null) + { + var tableSec = new Section(); + foreach (var cap in captions) + { + tableSec.Blocks.AddRange(manager.ParseChildrenAndGroup(cap)); + } + + tableSec.Blocks.Add(table); + tableSec.Tag = manager.GetTag(Tags.TagTableCaption); + + generated = new[] { tableSec }; + } + else + { + generated = new[] { table }; + } + + return true; + } + + private static void ParseColumnStyle(HtmlNode tableTag, Table table) + { + var colHolder = tableTag.ChildNodes.HasOneTag("colgroup", out var colgroup) ? colgroup! : tableTag; + + foreach (var col in colHolder.ChildNodes.CollectTag("col")) + { + var coldef = new TableColumn(); + table.Columns.Add(coldef); + + var spanAttr = col.Attributes["span"]; + if (spanAttr is not null) + { + if (int.TryParse(spanAttr.Value, out var spanCnt)) + { + foreach (var _ in Enumerable.Range(0, spanCnt - 1)) + table.Columns.Add(coldef); + } + } + + var styleAttr = col.Attributes["style"]; + if (styleAttr is null) continue; + + var mch = Regex.Match(styleAttr.Value, "width:([^;\"]+)(%|em|ex|mm|cm|in|pt|pc|)"); + if (!mch.Success) continue; + + if (!Length.TryParse(mch.Groups[1].Value + mch.Groups[2].Value, out var length)) + continue; + + coldef.Width = length.Unit switch + { + Unit.Percentage => new GridLength(length.Value, GridUnitType.Star), + _ => new GridLength(length.ToPoint()) + }; + } + } + + + private static TableRowGroup CreateRowGroup( + IEnumerable rows, + ReplaceManager manager, + out int maxColCount) + { + var group = new TableRowGroup(); + var list = new List(); + + maxColCount = 0; + + foreach (var rowTag in rows) + { + var row = new TableRow(); + + int colCount = list.Sum(e => e.ColSpan); + + foreach (var cellTag in rowTag.ChildNodes.CollectTag("td", "th")) + { + var cell = new TableCell(); + cell.Blocks.AddRange(manager.ParseChildrenAndGroup(cellTag)); + + int colspan = TryParse(cellTag.Attributes["colspan"]?.Value); + int rowspan = TryParse(cellTag.Attributes["rowspan"]?.Value); + + cell.RowSpan = rowspan; + cell.ColumnSpan = colspan; + + row.Cells.Add(cell); + + colCount += colspan; + + if (rowspan > 1) + { + list.Add(new ColspanCounter(rowspan, colspan)); + } + } + + group.Rows.Add(row); + + maxColCount = Math.Max(maxColCount, colCount); + + for (int idx = list.Count - 1; idx >= 0; --idx) + if (list[idx].Detent()) + list.RemoveAt(idx); + } + + return group; + + static int TryParse(string? txt) => int.TryParse(txt, out var v) ? v : 1; + } + + + class ColspanCounter + { + public int Remain { get; set; } + public int ColSpan { get; } + + public ColspanCounter(int rowspan, int colspan) + { + Remain = rowspan; + ColSpan = colspan; + } + + public bool Detent() + { + return --Remain == 0; + } + } + } +} diff --git a/MdXaml.Html/Core/Parsers/ButtonParser.cs b/MdXaml.Html/Core/Parsers/ButtonParser.cs new file mode 100644 index 0000000..0e04c9f --- /dev/null +++ b/MdXaml.Html/Core/Parsers/ButtonParser.cs @@ -0,0 +1,88 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; + +namespace MdXaml.Html.Core.Parsers +{ + public class ButtonParser : IInlineTagParser + { + public IEnumerable SupportTag => new[] { "button" }; + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = TryReplace(node, manager, out var list); + generated = list; + return rtn; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var doc = new FlowDocument(); + doc.Blocks.AddRange(manager.ParseChildrenAndGroup(node)); + + var box = new FlowDocumentScrollViewer() + { + Margin = new Thickness(0), + Padding = new Thickness(0), + VerticalScrollBarVisibility = ScrollBarVisibility.Disabled, + HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, + Document = doc, + }; + + box.Loaded += (s, e) => + { + var desiredWidth = box.DesiredSize.Width; + var desiredHeight = box.DesiredSize.Height; + + + for (int i = 0; i < 10; ++i) + { + desiredWidth /= 2; + var size = new Size(desiredWidth, double.PositiveInfinity); + + box.Measure(size); + + if (desiredHeight != box.DesiredSize.Height) break; + + // Give up because it will not be wrapped back. + if (i == 9) return; + } + + var preferedWidth = desiredWidth * 2; + + for (int i = 0; i < 10; ++i) + { + var width = (desiredWidth + preferedWidth) / 2; + + var size = new Size(width, double.PositiveInfinity); + box.Measure(size); + + if (desiredHeight == box.DesiredSize.Height) + { + preferedWidth = width; + } + else + { + desiredWidth = width; + } + } + + box.Width = preferedWidth; + }; + + + var btn = new Button() + { + Content = box, + IsEnabled = false, + }; + + generated = new[] { new InlineUIContainer(btn) }; + return true; + } + } +} diff --git a/MdXaml.Html/Core/Parsers/CodeBlockParser.cs b/MdXaml.Html/Core/Parsers/CodeBlockParser.cs new file mode 100644 index 0000000..35b5b06 --- /dev/null +++ b/MdXaml.Html/Core/Parsers/CodeBlockParser.cs @@ -0,0 +1,67 @@ +using HtmlAgilityPack; +using MdXaml.Html.Core.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Documents; + +namespace MdXaml.Html.Core.Parsers +{ + public class CodeBlockParser : IBlockTagParser + { + public IEnumerable SupportTag => new[] { "pre" }; + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = TryReplace(node, manager, out var list); + generated = list; + return rtn; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + generated = EnumerableExt.Empty(); + + var codeElements = node.ChildNodes.CollectTag("code"); + if (codeElements.Count != 0) + { + var rslt = new List(); + + foreach (var codeElement in codeElements) + { + // "language-**", "lang-**", "**" or "sourceCode **" + var classVal = codeElement.Attributes["class"]?.Value; + + var langCode = ParseLangCode(classVal); + rslt.Add(DocUtils.CreateCodeBlock(langCode, codeElement.InnerText, manager)); + } + + generated = rslt; + return rslt.Count > 0; + } + else if (node.ChildNodes.TryCastTextNode(out var textNodes)) + { + var buff = new StringBuilder(); + foreach (var textNode in textNodes) + buff.Append(textNode.InnerText); + + generated = new[] { DocUtils.CreateCodeBlock(null, buff.ToString(), manager) }; + return true; + } + else return false; + } + + private static string ParseLangCode(string? classVal) + { + if (classVal is null) return ""; + + // "language-**", "lang-**", "**" or "sourceCode **" + var indics = Enumerable.Range(0, classVal.Length) + .Reverse() + .Where(i => !Char.IsLetterOrDigit(classVal[i])); + + return classVal.Substring(indics.Any() ? indics.First() + 1 : 0); + } + } +} diff --git a/MdXaml.Html/Core/Parsers/CommentParser.cs b/MdXaml.Html/Core/Parsers/CommentParser.cs new file mode 100644 index 0000000..b336012 --- /dev/null +++ b/MdXaml.Html/Core/Parsers/CommentParser.cs @@ -0,0 +1,34 @@ +using HtmlAgilityPack; +using MdXaml.Html.Core.Utils; +using System; +using System.Collections.Generic; +using System.Windows.Documents; + +namespace MdXaml.Html.Core.Parsers +{ + /// + /// remove comment element + /// + public class CommentParsre : IBlockTagParser, IInlineTagParser + { + public IEnumerable SupportTag => new[] { HtmlNode.HtmlNodeTypeNameComment }; + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + generated = EnumerableExt.Empty(); + return true; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + generated = EnumerableExt.Empty(); + return true; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + generated = EnumerableExt.Empty(); + return true; + } + } +} diff --git a/MdXaml.Html/Core/Parsers/HorizontalRuleParser.cs b/MdXaml.Html/Core/Parsers/HorizontalRuleParser.cs new file mode 100644 index 0000000..c6f3161 --- /dev/null +++ b/MdXaml.Html/Core/Parsers/HorizontalRuleParser.cs @@ -0,0 +1,33 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Controls; +using System.Windows.Documents; + +namespace MdXaml.Html.Core.Parsers +{ + public class HorizontalRuleParser : IBlockTagParser + { + public IEnumerable SupportTag => new[] { "hr" }; + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = TryReplace(node, manager, out var list); + generated = list; + return rtn; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var sep = new Separator(); + var container = new BlockUIContainer(sep) + { + Tag = manager.GetTag(Tags.TagRuleSingle) + }; + + generated = new[] { container }; + return true; + } + } +} diff --git a/MdXaml.Html/Core/Parsers/ITagParser.cs b/MdXaml.Html/Core/Parsers/ITagParser.cs new file mode 100644 index 0000000..727a966 --- /dev/null +++ b/MdXaml.Html/Core/Parsers/ITagParser.cs @@ -0,0 +1,36 @@ +using HtmlAgilityPack; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Documents; + +namespace MdXaml.Html.Core.Parsers +{ + public interface ITagParser + { + IEnumerable SupportTag { get; } + bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated); + } + + public interface IInlineTagParser : ITagParser + { + bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated); + } + + public interface IBlockTagParser : ITagParser + { + bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated); + } + + public interface IHasPriority + { + int Priority { get; } + } + + public static class HasPriority + { + public const int DefaultPriority = 10000; + + public static int GetPriority(this ITagParser parser) + => parser is IHasPriority prop ? prop.Priority : HasPriority.DefaultPriority; + } +} diff --git a/MdXaml.Html/Core/Parsers/ImageParser.cs b/MdXaml.Html/Core/Parsers/ImageParser.cs new file mode 100644 index 0000000..85b4c33 --- /dev/null +++ b/MdXaml.Html/Core/Parsers/ImageParser.cs @@ -0,0 +1,201 @@ +using HtmlAgilityPack; +using MdXaml.Html.Core.Utils; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace MdXaml.Html.Core.Parsers +{ + public class ImageParser : IInlineTagParser + { + public IEnumerable SupportTag => new[] { "img", "image" }; + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = TryReplace(node, manager, out var list); + generated = list; + return rtn; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var link = node.Attributes["src"]?.Value; + var alt = node.Attributes["alt"]?.Value; + if (link is null) + { + generated = EnumerableExt.Empty(); + return false; + } + var title = node.Attributes["title"]?.Value; + + var widthTxt = node.Attributes["width"]?.Value; + var heightTxt = node.Attributes["height"]?.Value; + + var oncompletes = new List>(); + + if (Length.TryParse(heightTxt, out var heightLen)) + { + if (heightLen.Unit == Unit.Percentage) + { + oncompletes.Add((container, image, source) => + { + image.SetBinding( + Image.HeightProperty, + new Binding(nameof(Image.Width)) + { + RelativeSource = new RelativeSource(RelativeSourceMode.Self), + Converter = new MultiplyConverter(heightLen.Value / 100) + }); + }); + } + else + { + oncompletes.Add((container, image, source) => + { + image.Height = heightLen.ToPoint(); + }); + } + } + + // Bind size so document is updated when image is downloaded + if (Length.TryParse(widthTxt, out var widthLen)) + { + if (widthLen.Unit == Unit.Percentage) + { + oncompletes.Add((container, image, source) => + { + var parent = image.Parent; + + for (; ; ) + { + if (parent is FrameworkElement element) + { + parent = element; + break; + } + else if (parent is FrameworkContentElement content) + { + parent = content.Parent; + } + else break; + } + + if (parent is FlowDocumentScrollViewer) + { + var binding = CreateMultiBindingForFlowDocumentScrollViewer(); + binding.Converter = new MultiMultiplyConverter2(widthLen.Value / 100); + image.SetBinding(Image.WidthProperty, binding); + } + else + { + var binding = CreateBinding(nameof(FrameworkElement.ActualWidth), typeof(FrameworkElement)); + binding.Converter = new MultiplyConverter(widthLen.Value / 100); + image.SetBinding(Image.WidthProperty, binding); + } + }); + } + else + { + oncompletes.Add((container, image, source) => + { + image.Width = widthLen.ToPoint(); + }); + } + } + + var container = manager.Engine.LoadImage(alt, link, title, Aggregate(oncompletes)); + + generated = new[] { container }; + return true; + } + + private MultiBinding CreateMultiBindingForFlowDocumentScrollViewer() + { + var binding = new MultiBinding(); + + var totalWidth = CreateBinding(nameof(FlowDocumentScrollViewer.ActualWidth), typeof(FlowDocumentScrollViewer)); + var verticalBarVis = CreateBinding(nameof(FlowDocumentScrollViewer.VerticalScrollBarVisibility), typeof(FlowDocumentScrollViewer)); + + binding.Bindings.Add(totalWidth); + binding.Bindings.Add(verticalBarVis); + + return binding; + } + + private static Binding CreateBinding(string propName, Type ancestorType) + { + return new Binding(propName) + { + RelativeSource = new RelativeSource() + { + Mode = RelativeSourceMode.FindAncestor, + AncestorType = ancestorType, + } + }; + } + + private static Action? Aggregate(IEnumerable> actions) + { + var acts = actions.ToList(); + return acts.Count == 0 ? + null : + (a, b, c) => acts.ForEach(act => act(a, b, c)); + } + + class MultiplyConverter : IValueConverter + { + public double Value { get; } + + public MultiplyConverter(double v) + { + Value = v; + } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return Value * (Double)value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return ((Double)value) / Value; + } + } + class MultiMultiplyConverter2 : IMultiValueConverter + { + public double Value { get; } + + public MultiMultiplyConverter2(double v) + { + Value = v; + } + + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + var value = (double)values[0]; + var visibility = (ScrollBarVisibility)values[1]; + + if (visibility == ScrollBarVisibility.Visible) + { + return Value * (value - SystemParameters.VerticalScrollBarWidth); + } + else + { + return Value * (Double)value; + } + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/MdXaml.Html/Core/Parsers/InputParser.cs b/MdXaml.Html/Core/Parsers/InputParser.cs new file mode 100644 index 0000000..763f5aa --- /dev/null +++ b/MdXaml.Html/Core/Parsers/InputParser.cs @@ -0,0 +1,140 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Media; + +namespace MdXaml.Html.Core.Parsers +{ + public class InputParser : IInlineTagParser + { + public IEnumerable SupportTag => new[] { "input" }; + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = TryReplace(node, manager, out var list); + generated = list; + return rtn; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var type = node.Attributes["type"]?.Value ?? "text"; + + double? width = null; + var widAttr = node.Attributes["width"]; + var sizAttr = node.Attributes["size"]; + + if (widAttr is not null) + { + if (double.TryParse(widAttr.Value, out var v)) + width = v; + } + if (sizAttr is not null) + { + if (int.TryParse(sizAttr.Value, out var v)) + width = v * 7; + } + + InlineUIContainer inline; + switch (type) + { + default: + case "text": + var txt = new TextBox() + { + Text = node.Attributes["value"]?.Value ?? "", + IsReadOnly = true, + }; + if (width.HasValue) txt.Width = width.Value; + else if (String.IsNullOrEmpty(txt.Text)) txt.Width = 100; + + + inline = new InlineUIContainer(txt); + break; + + + case "button": + case "reset": + case "submit": + var btn = new Button() + { + Content = node.Attributes["value"]?.Value ?? "", + IsEnabled = false, + }; + if (width.HasValue) btn.Width = width.Value; + else if (String.IsNullOrEmpty(btn.Content.ToString())) btn.Width = 100; + + inline = new InlineUIContainer(btn); + break; + + + case "radio": + var radio = new RadioButton() + { + IsEnabled = false, + }; + if (node.Attributes["checked"] != null) radio.IsChecked = true; + + inline = new InlineUIContainer(radio); + break; + + + case "checkbox": + var chk = new CheckBox() + { + IsEnabled = false + }; + if (node.Attributes["checked"] != null) + chk.IsChecked = true; + + inline = new InlineUIContainer(chk); + break; + + + case "range": + var slider = new Slider() + { + IsEnabled = false, + Minimum = 0, + Value = 50, + Maximum = 100, + Width = 100, + }; + + var minAttr = node.Attributes["min"]; + if (minAttr is not null && double.TryParse(minAttr.Value, out var minVal)) + { + slider.Minimum = minVal; + } + + var maxAttr = node.Attributes["max"]; + if (maxAttr is not null && double.TryParse(maxAttr.Value, out var maxVal)) + { + slider.Maximum = maxVal; + } + + var valAttr = node.Attributes["value"]; + if (valAttr is not null && double.TryParse(valAttr.Value, out var val)) + { + slider.Value = val; + } + else + { + slider.Value = (slider.Minimum + slider.Maximum) / 2; + } + + inline = new InlineUIContainer(slider); + break; + } + + generated = new[] { inline }; + return true; + } + } +} diff --git a/MdXaml.Html/Core/Parsers/OrderListParser.cs b/MdXaml.Html/Core/Parsers/OrderListParser.cs new file mode 100644 index 0000000..75011a1 --- /dev/null +++ b/MdXaml.Html/Core/Parsers/OrderListParser.cs @@ -0,0 +1,49 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.Windows.Documents; +using MdXaml.Html.Core.Utils; +using System.Windows; +using System.Linq; + +namespace MdXaml.Html.Core.Parsers +{ + public class OrderListParser : IBlockTagParser + { + public IEnumerable SupportTag => new[] { "ol" }; + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = TryReplace(node, manager, out var list); + generated = list; + return rtn; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var list = new List() + { + MarkerStyle = TextMarkerStyle.Decimal + }; + + var startAttr = node.Attributes["start"]; + if (startAttr is not null && Int32.TryParse(startAttr.Value, out var start)) + { + list.StartIndex = start; + } + + foreach (var listItemTag in node.ChildNodes.CollectTag("li")) + { + var itemContent = manager.ParseChildrenAndGroup(listItemTag); + + var listItem = new ListItem(); + listItem.Blocks.AddRange(itemContent); + + list.ListItems.Add(listItem); + } + + generated = new[] { list }; + return true; + } + } +} diff --git a/MdXaml.Html/Core/Parsers/ProgressParser.cs b/MdXaml.Html/Core/Parsers/ProgressParser.cs new file mode 100644 index 0000000..665fe3c --- /dev/null +++ b/MdXaml.Html/Core/Parsers/ProgressParser.cs @@ -0,0 +1,45 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Media; + +namespace MdXaml.Html.Core.Parsers +{ + public class ProgressParser : IInlineTagParser + { + public IEnumerable SupportTag => new[] { "progress", "meter" }; + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = TryReplace(node, manager, out var list); + generated = list; + return rtn; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var bar = new ProgressBar() + { + Value = TryParse(node.Attributes["value"]?.Value, 1), + Minimum = TryParse(node.Attributes["min"]?.Value, 0), + Maximum = TryParse(node.Attributes["max"]?.Value, 1), + Width = 50, + Height = 12, + }; + generated = new[] { new InlineUIContainer(bar) }; + return true; + } + + private static int TryParse(string? txt, int def) + { + if (txt is null) return def; + return int.TryParse(txt, out var v) ? v : def; + } + } +} diff --git a/MdXaml.Html/Core/Parsers/TagIgnoreParser.cs b/MdXaml.Html/Core/Parsers/TagIgnoreParser.cs new file mode 100644 index 0000000..9e9fbcc --- /dev/null +++ b/MdXaml.Html/Core/Parsers/TagIgnoreParser.cs @@ -0,0 +1,31 @@ +using HtmlAgilityPack; +using MdXaml.Html.Core.Utils; +using System; +using System.Collections.Generic; +using System.Windows.Documents; + +namespace MdXaml.Html.Core.Parsers +{ + public class TagIgnoreParser : IBlockTagParser, IInlineTagParser + { + public IEnumerable SupportTag => new[] { "title", "meta", "link", "script", "style", "datalist" }; + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + generated = EnumerableExt.Empty(); + return true; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + generated = EnumerableExt.Empty(); + return true; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + generated = EnumerableExt.Empty(); + return true; + } + } +} diff --git a/MdXaml.Html/Core/Parsers/TextAreaParser.cs b/MdXaml.Html/Core/Parsers/TextAreaParser.cs new file mode 100644 index 0000000..6235ba5 --- /dev/null +++ b/MdXaml.Html/Core/Parsers/TextAreaParser.cs @@ -0,0 +1,55 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; + +namespace MdXaml.Html.Core.Parsers +{ + public class TextAreaParser : IInlineTagParser + { + public IEnumerable SupportTag => new[] { "textarea" }; + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = TryReplace(node, manager, out var list); + generated = list; + return rtn; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var area = new TextBox() + { + AcceptsReturn = true, + AcceptsTab = true, + Text = node.InnerText.Trim(), + TextWrapping = TextWrapping.Wrap, + }; + + int? rows = null; + int? cols = null; + var rowsAttr = node.Attributes["rows"]; + var colsAttr = node.Attributes["cols"]; + + if (rowsAttr is not null) + { + if (int.TryParse(rowsAttr.Value, out var v)) + rows = v * 14; + } + if (colsAttr is not null) + { + if (int.TryParse(colsAttr.Value, out var v)) + cols = v * 7; + } + + if (rows.HasValue) area.Height = rows.Value; + if (cols.HasValue) area.Width = cols.Value; + + generated = new[] { new InlineUIContainer(area) }; + return true; + } + } +} diff --git a/MdXaml.Html/Core/Parsers/TextNodeParser.cs b/MdXaml.Html/Core/Parsers/TextNodeParser.cs new file mode 100644 index 0000000..68ac007 --- /dev/null +++ b/MdXaml.Html/Core/Parsers/TextNodeParser.cs @@ -0,0 +1,38 @@ +using HtmlAgilityPack; +using MdXaml.Html.Core.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Documents; + +namespace MdXaml.Html.Core.Parsers +{ + public class TextNodeParser : IInlineTagParser + { + public IEnumerable SupportTag => new[] { HtmlNode.HtmlNodeTypeNameText }; + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = TryReplace(node, manager, out var list); + generated = list; + return rtn; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + if (node is HtmlTextNode textNode) + { + generated = Replace(textNode.Text, manager); + return true; + } + + generated = EnumerableExt.Empty(); + return false; + } + + public IEnumerable Replace(string text, ReplaceManager manager) + => text.StartsWith("\n") ? + new[] { new Run() { Text = text.Replace('\n', ' ') } } : + manager.Engine.RunSpanGamut(text.Replace('\n', ' ')); + } +} diff --git a/MdXaml.Html/Core/Parsers/TypicalBlockParser.cs b/MdXaml.Html/Core/Parsers/TypicalBlockParser.cs new file mode 100644 index 0000000..fab6d9f --- /dev/null +++ b/MdXaml.Html/Core/Parsers/TypicalBlockParser.cs @@ -0,0 +1,49 @@ +using HtmlAgilityPack; +using MdXaml.Html.Core.Utils; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Markup; + +namespace MdXaml.Html.Core.Parsers +{ + public class TypicalBlockParser : IBlockTagParser + { + private const string _resource = "MdXaml.Html.Core.Parsers.TypicalBlockParser.tsv"; + private TypicalParseInfo _parser; + + public IEnumerable SupportTag => new[] { _parser.HtmlTag }; + + public TypicalBlockParser(TypicalParseInfo parser) + { + _parser = parser; + } + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = _parser.TryReplace(node, manager, out var list); + generated = list; + return rtn; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = _parser.TryReplace(node, manager, out var list); + generated = list.Cast(); + return rtn; + } + + public static IEnumerable Load() + { + foreach (var info in TypicalParseInfo.Load(_resource)) + { + yield return new TypicalBlockParser(info); + } + } + } +} diff --git a/MdXaml.Html/Core/Parsers/TypicalBlockParser.tsv b/MdXaml.Html/Core/Parsers/TypicalBlockParser.tsv new file mode 100644 index 0000000..16a7b40 --- /dev/null +++ b/MdXaml.Html/Core/Parsers/TypicalBlockParser.tsv @@ -0,0 +1,17 @@ +#HtmlTag | FlowDocumentTag | TagName | ExtraModify +address | System.Windows.Documents.Section | TagAddress | +article | System.Windows.Documents.Section | TagArticle | +aside | System.Windows.Documents.Section | TagAside | +blockquote | System.Windows.Documents.Section | TagBlockquote | +center | System.Windows.Documents.Section | TagCenter | Center +div | #blocks | | +footer | System.Windows.Documents.Section | TagFooter | +h1 | #blocks | TagHeading1 | +h2 | #blocks | TagHeading2 | +h3 | #blocks | TagHeading3 | +h4 | #blocks | TagHeading4 | +h5 | #blocks | TagHeading5 | +h6 | #blocks | TagHeading6 | +noframes | #blocks | | +noscript | #blocks | | +p | #blocks | | \ No newline at end of file diff --git a/MdXaml.Html/Core/Parsers/TypicalInlineParser.cs b/MdXaml.Html/Core/Parsers/TypicalInlineParser.cs new file mode 100644 index 0000000..5ab5d6b --- /dev/null +++ b/MdXaml.Html/Core/Parsers/TypicalInlineParser.cs @@ -0,0 +1,49 @@ +using HtmlAgilityPack; +using MdXaml.Html.Core.Utils; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Markup; + +namespace MdXaml.Html.Core.Parsers +{ + public class TypicalInlineParser : IInlineTagParser + { + private const string _resource = "MdXaml.Html.Core.Parsers.TypicalInlineParser.tsv"; + private readonly TypicalParseInfo _parser; + + public IEnumerable SupportTag => new[] { _parser.HtmlTag }; + + public TypicalInlineParser(TypicalParseInfo parser) + { + _parser = parser; + } + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = _parser.TryReplace(node, manager, out var list); + generated = list; + return rtn; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = _parser.TryReplace(node, manager, out var list); + generated = list.Cast(); + return rtn; + } + + public static IEnumerable Load() + { + foreach (var info in TypicalParseInfo.Load(_resource)) + { + yield return new TypicalInlineParser(info); + } + } + } +} diff --git a/MdXaml.Html/Core/Parsers/TypicalInlineParser.tsv b/MdXaml.Html/Core/Parsers/TypicalInlineParser.tsv new file mode 100644 index 0000000..b39d58b --- /dev/null +++ b/MdXaml.Html/Core/Parsers/TypicalInlineParser.tsv @@ -0,0 +1,22 @@ +#HtmlTag | FlowDocumentTag | TagName | ExtraModify +a | System.Windows.Documents.Hyperlink | TagHyperlink | Hyperlink +abbr | System.Windows.Documents.Span | TagAbbr | Acronym +acronym | System.Windows.Documents.Span | TagAbbr | Acronym +b | System.Windows.Documents.Bold | TagBold | +bdi | System.Windows.Documents.Span | TagBdi | +br | System.Windows.Documents.LineBreak | | +cite | System.Windows.Documents.Span | TagCite | +del | System.Windows.Documents.Span | TagStrikethrough | Strikethrough +em | System.Windows.Documents.Italic | TagItalic | +i | System.Windows.Documents.Italic | TagItalic | +ins | System.Windows.Documents.Underline | TagUnderline | +mark | System.Windows.Documents.Span | TagMark | +s | System.Windows.Documents.Span | TagStrikethrough | Strikethrough +strike | System.Windows.Documents.Span | TagStrikethrough | Strikethrough +strong | System.Windows.Documents.Bold | TagBold | +sub | System.Windows.Documents.Span | TagSubscript | Subscript +sup | System.Windows.Documents.Span | TagSuperscript | Superscript +u | System.Windows.Documents.Underline | TagUnderline | +code | System.Windows.Documents.Span | TagCodeSpan | +kbd | System.Windows.Documents.Span | TagCodeSpan | +var | System.Windows.Documents.Span | TagCodeSpan | \ No newline at end of file diff --git a/MdXaml.Html/Core/Parsers/TypicalParseInfo.cs b/MdXaml.Html/Core/Parsers/TypicalParseInfo.cs new file mode 100644 index 0000000..6a0ff15 --- /dev/null +++ b/MdXaml.Html/Core/Parsers/TypicalParseInfo.cs @@ -0,0 +1,230 @@ +using HtmlAgilityPack; +using MdXaml.Html.Core.Utils; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Windows; +using System.Windows.Documents; + +namespace MdXaml.Html.Core.Parsers +{ + public class TypicalParseInfo + { + public string HtmlTag { get; } + public string FlowDocumentTagText { get; } + public Type? FlowDocumentTag { get; } + public string? TagNameReference { get; } + public Tags TagName { get; } + public string? ExtraModifyName { get; } + + private readonly MethodInfo? _method; + + public TypicalParseInfo(string[] line) + { + FlowDocumentTagText = line[1]; + + if (FlowDocumentTagText.StartsWith("#")) + { + FlowDocumentTag = null; + } + else + { + Type? elementType = AppDomain.CurrentDomain + .GetAssemblies() + .Select(asm => asm.GetType(FlowDocumentTagText)) + .OfType() + .FirstOrDefault(); + + if (elementType is null) + throw new ArgumentException($"Failed to load type '{line[1]}'"); + + FlowDocumentTag = elementType; + } + + + HtmlTag = line[0]; + TagNameReference = GetArrayAt(line, 2); + ExtraModifyName = GetArrayAt(line, 3); + + if (TagNameReference is not null) + { + TagName = (Tags)Enum.Parse(typeof(Tags), TagNameReference); + } + + if (ExtraModifyName is not null) + { + _method = this.GetType().GetMethod("ExtraModify" + ExtraModifyName); + + if (_method is null) + throw new InvalidOperationException("unknown method ExtraModify" + ExtraModifyName); + } + + static string? GetArrayAt(string[] array, int idx) + { + if (idx < array.Length + && !string.IsNullOrWhiteSpace(array[idx])) + { + return array[idx]; + } + return null; + } + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + // create instance + + if (FlowDocumentTag is null) + { + switch (FlowDocumentTagText) + { + case "#blocks": + generated = manager.ParseChildrenAndGroup(node).ToArray(); + break; + + case "#jagging": + generated = manager.ParseChildrenJagging(node).ToArray(); + break; + + case "#inlines": + if (manager.ParseChildrenJagging(node).TryCast(out var inlines)) + { + generated = inlines.ToArray(); + break; + } + else + { + generated = EnumerableExt.Empty(); + return false; + } + + default: + throw new InvalidOperationException(); + } + } + else + { + var tag = (TextElement)Activator.CreateInstance(FlowDocumentTag)!; + + var cntInlines = (tag as Paragraph)?.Inlines ?? (tag as Span)?.Inlines; + if (cntInlines is not null) + { + var parseResult = manager.ParseChildrenJagging(node).ToArray(); + + + if (parseResult.TryCast(out var parsed)) + { + cntInlines.AddRange(parsed); + } + else if (tag is Paragraph && parseResult.Length == 1 && parseResult[0] is Paragraph) + { + tag = parseResult[0]; + } + else if (tag is Span && manager.Grouping(parseResult).TryCast(out var paragraphs)) + { + // FIXME: MdXaml can't bubbling a block element in a inline element. + foreach (var para in paragraphs) + foreach (var inline in para.Inlines.ToArray()) + cntInlines.Add(inline); + } + else + { + generated = EnumerableExt.Empty(); + return false; + } + + } + else if (tag is Section section) + { + section.Blocks.AddRange(manager.ParseChildrenAndGroup(node)); + } + else if (tag is not LineBreak) + { + throw new InvalidOperationException(); + } + + generated = new[] { tag }; + } + + // apply tag + + if (TagNameReference is not null) + { + foreach (var tag in generated) + { + tag.Tag = manager.GetTag(TagName); + } + } + + // extra modify + if (_method is not null) + { + foreach (var tag in generated) + { + _method.Invoke(this, new object[] { tag, node, manager }); + } + } + + return true; + } + + + public void ExtraModifyHyperlink(Hyperlink link, HtmlNode node, ReplaceManager manager) + { + var href = node.Attributes["href"]?.Value; + + if (href is not null) + { + link.CommandParameter = href; + link.Command = manager.HyperlinkCommand; + } + } + + public void ExtraModifyStrikethrough(Span span, HtmlNode node, ReplaceManager manager) + { + span.TextDecorations = TextDecorations.Strikethrough; + } + + public void ExtraModifySubscript(Span span, HtmlNode node, ReplaceManager manager) + { + Typography.SetVariants(span, FontVariants.Subscript); + } + + public void ExtraModifySuperscript(Span span, HtmlNode node, ReplaceManager manager) + { + Typography.SetVariants(span, FontVariants.Superscript); + } + + public void ExtraModifyAcronym(Span span, HtmlNode node, ReplaceManager manager) + { + var title = node.Attributes["title"]?.Value; + if (title is not null) + span.ToolTip = title; + } + + public void ExtraModifyCenter(Section center, HtmlNode node, ReplaceManager manager) + { + center.TextAlignment = TextAlignment.Center; + } + + public static IEnumerable Load(string resourcePath) + { + using var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream(resourcePath); + + if (stream is null) + throw new ArgumentException($"resource not found: '{resourcePath}'"); + + using var reader = new StreamReader(stream!); + while (reader.ReadLine() is string line) + { + if (line.StartsWith("#")) continue; + + var elements = line.Split('|').Select(t => t.Trim()).ToArray(); + yield return new TypicalParseInfo(elements); + } + } + } +} diff --git a/MdXaml.Html/Core/Parsers/UnorderListParser.cs b/MdXaml.Html/Core/Parsers/UnorderListParser.cs new file mode 100644 index 0000000..8e7f18a --- /dev/null +++ b/MdXaml.Html/Core/Parsers/UnorderListParser.cs @@ -0,0 +1,39 @@ +using HtmlAgilityPack; +using System.Collections.Generic; +using System.Windows.Documents; +using MdXaml.Html.Core.Utils; +using System.Windows; + +namespace MdXaml.Html.Core.Parsers +{ + public class UnorderListParser : IBlockTagParser + { + public IEnumerable SupportTag => new[] { "ul" }; + + bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var rtn = TryReplace(node, manager, out var list); + generated = list; + return rtn; + } + + public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) + { + var list = new List(); + list.MarkerStyle = TextMarkerStyle.Disc; + + foreach (var listItemTag in node.ChildNodes.CollectTag("li")) + { + var itemContent = manager.ParseChildrenAndGroup(listItemTag); + + var listItem = new ListItem(); + listItem.Blocks.AddRange(itemContent); + + list.ListItems.Add(listItem); + } + + generated = new[] { list }; + return true; + } + } +} diff --git a/MdXaml.Html/Core/ReplaceManager.cs b/MdXaml.Html/Core/ReplaceManager.cs new file mode 100644 index 0000000..3493cef --- /dev/null +++ b/MdXaml.Html/Core/ReplaceManager.cs @@ -0,0 +1,543 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.Windows.Documents; +using Paragraph = System.Windows.Documents.Paragraph; +using System.Windows.Input; +using MdXaml.Html.Core.Parsers; +using MdXaml.Html.Core.Utils; +using MdXaml.Html.Core.Parsers.MarkdigExtensions; +using System.Windows.Markup; +using System.Windows.Media.Imaging; +using System.Linq; +using System.Text; +using MdXaml; +using MdXaml.Plugins; + +namespace MdXaml.Html.Core +{ + public class ReplaceManager + { + private readonly Dictionary> _inlineBindParsers; + private readonly Dictionary> _blockBindParsers; + private readonly Dictionary> _bindParsers; + + private TextNodeParser textParser; + + public ReplaceManager() + { + _inlineBindParsers = new(); + _blockBindParsers = new(); + _bindParsers = new(); + + UnknownTags = UnknownTagsOption.Drop; + + Register(new TagIgnoreParser()); + Register(new CommentParsre()); + Register(new ImageParser()); + Register(new CodeBlockParser()); + //Register(new CodeSpanParser()); + Register(new OrderListParser()); + Register(new UnorderListParser()); + Register(textParser = new TextNodeParser()); + Register(new HorizontalRuleParser()); + Register(new FigureParser()); + Register(new GridTableParser()); + Register(new InputParser()); + Register(new ButtonParser()); + Register(new TextAreaParser()); + Register(new ProgressParser()); + + foreach (var parser in TypicalBlockParser.Load()) + Register(parser); + + foreach (var parser in TypicalInlineParser.Load()) + Register(parser); + } + + public IEnumerable InlineTags => _inlineBindParsers.Keys.Where(tag => !tag.StartsWith("#")); + public IEnumerable BlockTags => _blockBindParsers.Keys.Where(tag => !tag.StartsWith("#")); + + public bool MaybeSupportBodyTag(string tagName) + => _blockBindParsers.ContainsKey(tagName.ToLower()); + + public bool MaybeSupportInlineTag(string tagName) + => _inlineBindParsers.ContainsKey(tagName.ToLower()); + + public UnknownTagsOption UnknownTags { get; set; } + + public IMarkdown Engine { get; set; } + + public ICommand? HyperlinkCommand => Engine.HyperlinkCommand; + + public Uri? BaseUri => Engine.BaseUri; + + public string? AssetPathRoot => Engine.AssetPathRoot; + + public void Register(ITagParser parser) + { + + if (parser is IInlineTagParser inlineParser) + { + PrivateRegister(inlineParser, _inlineBindParsers); + } + if (parser is IBlockTagParser blockParser) + { + PrivateRegister(blockParser, _blockBindParsers); + } + + PrivateRegister(parser, _bindParsers); + + static void PrivateRegister(T parser, Dictionary> bindParsers) where T : ITagParser + { + foreach (var tag in parser.SupportTag) + { + if (!bindParsers.TryGetValue(tag.ToLower(), out var list)) + { + list = new(); + bindParsers.Add(tag.ToLower(), list); + } + + int parserPriority = GetPriority(parser); + + int i = 0; + int count = list.Count; + for (; i < count; ++i) + if (parserPriority <= GetPriority(list[i])) + break; + + list.Insert(i, parser); + } + } + + static int GetPriority(object? p) + => p is IHasPriority prop ? prop.Priority : HasPriority.DefaultPriority; + } + + public string GetTag(Tags tag) + { + return tag.ToString().Substring(3); + } + + /// + /// Convert a html tag list to an element of markdown. + /// + public IEnumerable Parse(string htmldoc) + { + var doc = new HtmlDocument(); + doc.LoadHtml(htmldoc); + + return Parse(doc); + } + + /// + /// Convert a html tag list to an element of markdown. + /// + public IEnumerable Parse(HtmlDocument doc) + { + var contents = new List(); + + var head = PickBodyOrHead(doc.DocumentNode, "head"); + if (head is not null) + contents.AddRange(head.ChildNodes.SkipComment()); + + var body = PickBodyOrHead(doc.DocumentNode, "body"); + if (body is not null) + contents.AddRange(body.ChildNodes.SkipComment()); + + if (contents.Count == 0) + { + var root = doc.DocumentNode.ChildNodes.SkipComment(); + + if (root.Count == 1 && string.Equals(root[0].Name, "html", StringComparison.OrdinalIgnoreCase)) + contents.AddRange(root[0].ChildNodes.SkipComment()); + else + contents.AddRange(root); + } + + var jaggingResult = ParseJagging(contents); + + return Grouping(jaggingResult); + } + + /// + /// Convert html tag children to an element of markdown. + /// Inline elements are aggreated into paragraph. + /// + public IEnumerable ParseChildrenAndGroup(HtmlNode node) + { + var jaggingResult = ParseChildrenJagging(node); + + return Grouping(jaggingResult); + } + + /// + /// Convert html tag children to an element of markdown. + /// this result contains a block element and an inline element. + /// + public IEnumerable ParseChildrenJagging(HtmlNode node) + { + // search empty line + var empNd = node.ChildNodes + .Select((nd, idx) => new { Node = nd, Index = idx }) + .Where(tpl => tpl.Node is HtmlTextNode) + .Select(tpl => new + { + NodeIndex = tpl.Index, + TextIndex = tpl.Node.InnerText.IndexOf("\n\n") + }) + .FirstOrDefault(tpl => tpl.TextIndex != -1); + + + if (empNd is null) + { + return ParseJagging(node.ChildNodes); + } + else + { + return ParseJaggingAndRunBlockGamut(node.ChildNodes, empNd.NodeIndex, empNd.TextIndex); + } + } + + /// + /// Convert a html tag to an element of markdown. + /// this result contains a block element and an inline element. + /// + private IEnumerable ParseJagging(IEnumerable nodes) + { + bool isPrevBlock = true; + TextElement? lastElement = null; + + foreach (var node in nodes) + { + if (node.IsComment()) + continue; + + // remove blank text between the blocks. + if (isPrevBlock + && node is HtmlTextNode txt + && String.IsNullOrWhiteSpace(txt.Text)) + continue; + + foreach (var element in ParseBlockAndInline(node)) + { + lastElement = element; + yield return element; + } + + isPrevBlock = lastElement is Block; + } + } + + private IEnumerable ParseJaggingAndRunBlockGamut(IEnumerable nodes, int nodeIdx, int textIdx) + { + var parseTargets = new List(); + var textBuf = new StringBuilder(); + var mdTextBuf = new StringBuilder(); + + foreach (var tpl in nodes.Select((value, i) => new { Node = value, Index = i })) + { + if (tpl.Index < nodeIdx) + { + parseTargets.Add(tpl.Node); + } + else if (tpl.Index == nodeIdx) + { + var nodeText = tpl.Node.InnerText; + + textBuf.Append(nodeText.Substring(0, textIdx)); + mdTextBuf.Append(nodeText.Substring(textIdx + 2)); + } + else + { + mdTextBuf.Append(tpl.Node.OuterHtml); + } + } + + foreach (var elm in ParseJagging(parseTargets)) + yield return elm; + + foreach (var elm in textParser.Replace(textBuf.ToString(), this)) + yield return elm; + + foreach (var elm in Engine.RunBlockGamut(mdTextBuf.ToString(), true)) + yield return elm; + } + + /// + /// Convert a html tag to an element of markdown. + /// Only tag node and text node are accepted. + /// + /// + /// + public IEnumerable ParseBlockAndInline(HtmlNode node) + { + if (_bindParsers.TryGetValue(node.Name.ToLower(), out var binds)) + { + foreach (var bind in binds) + { + if (bind.TryReplace(node, this, out var parsed)) + { + return parsed; + } + } + } + + return UnknownTags switch + { + UnknownTagsOption.PassThrough + => HtmlUtils.IsBlockTag(node.Name) ? + new[] { new Paragraph(new Run() { Text = node.OuterHtml }) } : + new[] { new Run(node.OuterHtml) }, + + UnknownTagsOption.Drop + => EnumerableExt.Empty(), + + UnknownTagsOption.Bypass + => ParseJagging(node.ChildNodes), + + _ => throw new UnknownTagException(node) + }; + } + + public IEnumerable ParseBlock(string html) + { + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + foreach (var node in doc.DocumentNode.ChildNodes) + foreach (var block in ParseBlock(node)) + yield return block; + } + + public IEnumerable ParseInline(string html) + { + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + foreach (var node in doc.DocumentNode.ChildNodes) + foreach (var inline in ParseInline(node)) + yield return inline; + } + + public IEnumerable ParseBlock(HtmlNode node) + { + if (_blockBindParsers.TryGetValue(node.Name.ToLower(), out var binds)) + { + foreach (var bind in binds) + { + if (bind.TryReplace(node, this, out var parsed)) + { + return parsed; + } + } + } + + return UnknownTags switch + { + UnknownTagsOption.PassThrough + => new[] { + new Paragraph( + HtmlUtils.IsBlockTag(node.Name) ? + new Run() { Text = node.OuterHtml }: + new Run(node.OuterHtml) + ) + }, + + UnknownTagsOption.Drop + => EnumerableExt.Empty(), + + UnknownTagsOption.Bypass + => node.ChildNodes + .SkipComment() + .SelectMany(nd => ParseBlock(nd)), + + _ => throw new UnknownTagException(node) + }; + } + + public IEnumerable ParseInline(HtmlNode node) + { + if (_inlineBindParsers.TryGetValue(node.Name.ToLower(), out var binds)) + { + foreach (var bind in binds) + { + if (bind.TryReplace(node, this, out var parsed)) + { + return parsed; + } + } + } + + return UnknownTags switch + { + UnknownTagsOption.PassThrough + => HtmlUtils.IsBlockTag(node.Name) ? + new[] { new Run() { Text = node.OuterHtml } } : + new[] { new Run(node.OuterHtml) }, + + UnknownTagsOption.Drop + => EnumerableExt.Empty(), + + UnknownTagsOption.Bypass + => node.ChildNodes + .SkipComment() + .SelectMany(nd => ParseInline(nd)), + + _ => throw new UnknownTagException(node) + }; + } + + /// + /// Convert IMdElement to IMdBlock. + /// Inline elements are aggreated into paragraph. + /// + public IEnumerable Grouping(IEnumerable elements) + { + static Paragraph? Group(IList inlines) + { + // trim whiltepace plain + + while (inlines.Count > 0) + { + if (inlines[0] is Run run + && String.IsNullOrWhiteSpace(run.Text)) + { + inlines.RemoveAt(0); + } + else break; + } + + while (inlines.Count > 0) + { + if (inlines[inlines.Count - 1] is Run run + && String.IsNullOrWhiteSpace(run.Text)) + { + inlines.RemoveAt(inlines.Count - 1); + } + else break; + } + + using (var list = inlines.GetEnumerator()) + { + Inline? prev = null; + + if (list.MoveNext()) + { + prev = list.Current; + DocUtils.TrimStart(prev); + + while (list.MoveNext()) + { + var now = list.Current; + + if (now is LineBreak) + { + DocUtils.TrimEnd(prev); + + if (list.MoveNext()) + { + now = list.Current; + DocUtils.TrimStart(now); + } + } + + prev = now; + } + } + + if (prev is not null) + DocUtils.TrimEnd(prev); + } + + if (inlines.Count > 0) + { + var para = new Paragraph(); + para.Inlines.AddRange(inlines); + return para; + } + return null; + } + + List stored = new(); + foreach (var e in elements) + { + if (e is Inline inline) + { + stored.Add(inline); + continue; + } + + // grouping inlines + if (stored.Count != 0) + { + var para = Group(stored); + if (para is not null) yield return para; + stored.Clear(); + } + + yield return (Block)e; + } + + if (stored.Count != 0) + { + var para = Group(stored); + if (para is not null) yield return para; + stored.Clear(); + } + } + + private static HtmlNode? PickBodyOrHead(HtmlNode documentNode, string headOrBody) + { + // html? + foreach (var child in documentNode.ChildNodes) + { + if (child.Name == HtmlNode.HtmlNodeTypeNameText + || child.Name == HtmlNode.HtmlNodeTypeNameComment) + continue; + + switch (child.Name.ToLower()) + { + case "html": + // body? head? + foreach (var descendants in child.ChildNodes) + { + if (descendants.Name == HtmlNode.HtmlNodeTypeNameText + || descendants.Name == HtmlNode.HtmlNodeTypeNameComment) + continue; + switch (descendants.Name.ToLower()) + { + case "head": + if (headOrBody == "head") + return descendants; + break; + + case "body": + if (headOrBody == "body") + return descendants; + break; + + default: + return null; + } + } + break; + + case "head": + if (headOrBody == "head") + return child; + break; + + case "body": + if (headOrBody == "body") + return child; + break; + + default: + return null; + } + } + return null; + } + } +} diff --git a/MdXaml.Html/Core/Tags.cs b/MdXaml.Html/Core/Tags.cs new file mode 100644 index 0000000..a05ae1f --- /dev/null +++ b/MdXaml.Html/Core/Tags.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MdXaml.Html.Core +{ + public enum Tags + { + TagTableHeader, + TagTableBody, + TagEvenTableRow, + TagOddTableRow, + TagTableFooter, + TagTableCaption, + + TagBlockquote, + TagBold, + TagCite, + TagFooter, + TagItalic, + TagMark, + TagStrikethrough, + TagSubscript, + TagSuperscript, + TagUnderline, + TagHyperlink, + + TagFigure, + TagRuleSingle, + + TagHeading1, + TagHeading2, + TagHeading3, + TagHeading4, + TagHeading5, + TagHeading6, + + TagCodeSpan, + TagCodeBlock, + TagAddress, + TagArticle, + TagAside, + TagCenter, + TagAbbr, + TagBdi + } +} diff --git a/MdXaml.Html/Core/TextRange.cs b/MdXaml.Html/Core/TextRange.cs new file mode 100644 index 0000000..d2b78a6 --- /dev/null +++ b/MdXaml.Html/Core/TextRange.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MdXaml.Html.Core +{ + public struct TextRange + { + public int Start { get; } + public int End { get; } + public int Length => End - Start; + + public TextRange(int start, int end) + { + Start = start; + End = end; + } + } +} diff --git a/MdXaml.Html/Core/UnknownTagException.cs b/MdXaml.Html/Core/UnknownTagException.cs new file mode 100644 index 0000000..f623a31 --- /dev/null +++ b/MdXaml.Html/Core/UnknownTagException.cs @@ -0,0 +1,28 @@ +using HtmlAgilityPack; +using System; + +namespace MdXaml.Html.Core +{ + /// + /// MarkdownFromHtml can not convert a certain tag. + /// This exception is thrown when is set to . + /// + public class UnknownTagException : Exception + { + /// + /// Tag name that could not be converted + /// + public string TagName { get; } + + /// + /// Tag that could not be converted + /// + public string Content { get; } + + public UnknownTagException(HtmlNode node) : base($"unknown tag: {node.Name}") + { + TagName = node.Name; + Content = node.OuterHtml; + } + } +} diff --git a/MdXaml.Html/Core/UnknownTagsOption.cs b/MdXaml.Html/Core/UnknownTagsOption.cs new file mode 100644 index 0000000..0c11044 --- /dev/null +++ b/MdXaml.Html/Core/UnknownTagsOption.cs @@ -0,0 +1,29 @@ +namespace MdXaml.Html.Core +{ + /// + /// Behavior options about unknown tag. + /// + public enum UnknownTagsOption + { + /// + /// Unknown tag is outputed as is. + /// + PassThrough, + + /// + /// Unknown tag is removed from the result. + /// + Drop, + + /// + /// The unknown tag itself is ignored. + /// Only the content of the tag is evaluated. + /// + Bypass, + + /// + /// Throw UnknownTagException. + /// + Raise, + } +} diff --git a/MdXaml.Html/Core/Utils/DocUtils.cs b/MdXaml.Html/Core/Utils/DocUtils.cs new file mode 100644 index 0000000..839fc0e --- /dev/null +++ b/MdXaml.Html/Core/Utils/DocUtils.cs @@ -0,0 +1,96 @@ +using ICSharpCode.AvalonEdit; +using ICSharpCode.AvalonEdit.Highlighting; +using System; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Input; + +namespace MdXaml.Html.Core.Utils +{ + static class DocUtils + { + public static Block CreateCodeBlock(string? lang, string code, ReplaceManager manager) + { + var txtEdit = new TextEditor(); + + if (!String.IsNullOrEmpty(lang)) + { + var highlight = HighlightingManager.Instance.GetDefinitionByExtension("." + lang); + txtEdit.SetCurrentValue(TextEditor.SyntaxHighlightingProperty, highlight); + txtEdit.Tag = lang; + } + + txtEdit.Text = code; + txtEdit.HorizontalAlignment = HorizontalAlignment.Stretch; + txtEdit.IsReadOnly = true; + txtEdit.PreviewMouseWheel += (s, e) => + { + if (e.Handled) return; + + e.Handled = true; + + var isShiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); + if (isShiftDown) + { + // horizontal scroll + var offset = txtEdit.HorizontalOffset; + offset -= e.Delta; + txtEdit.ScrollToHorizontalOffset(offset); + } + else + { + // event bubbles + var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta); + eventArg.RoutedEvent = UIElement.MouseWheelEvent; + eventArg.Source = s; + + var parentObj = ((Control)s).Parent; + if (parentObj is UIElement uielm) + { + uielm.RaiseEvent(eventArg); + } + else if (parentObj is ContentElement celem) + { + celem.RaiseEvent(eventArg); + } + } + }; + + + var result = new BlockUIContainer(txtEdit); + result.Tag = manager.GetTag(Tags.TagCodeBlock); + + return result; + } + + public static void TrimStart(Inline? inline) + { + if (inline is null) return; + + if (inline is Span span) + { + TrimStart(span.Inlines.FirstOrDefault()); + } + else if (inline is Run run) + { + run.Text = run.Text.TrimStart(); + } + } + + public static void TrimEnd(Inline? inline) + { + if (inline is null) return; + + if (inline is Span span) + { + TrimEnd(span.Inlines.LastOrDefault()); + } + else if (inline is Run run) + { + run.Text = run.Text.TrimEnd(); + } + } + } +} diff --git a/MdXaml.Html/Core/Utils/EnumerableExt.cs b/MdXaml.Html/Core/Utils/EnumerableExt.cs new file mode 100644 index 0000000..c718f49 --- /dev/null +++ b/MdXaml.Html/Core/Utils/EnumerableExt.cs @@ -0,0 +1,31 @@ +using System.Collections; +using System.Collections.Generic; + +namespace MdXaml.Html.Core.Utils +{ + internal static class EnumerableExt + { + public static bool TryCast(this IEnumerable list, out List casts) + { + casts = new List(); + + foreach (var e in list) + { + if (e is T t) casts.Add(t); + else return false; + } + + return true; + } + + public static T[] Empty() => EmptyArray.Value; + } + + internal class EmptyArray + { + // net45 dosen't have Array.Empty() +#pragma warning disable CA1825 + public static T[] Value = new T[0]; +#pragma warning restore CA1825 + } +} diff --git a/MdXaml.Html/Core/Utils/HtmlUtils.cs b/MdXaml.Html/Core/Utils/HtmlUtils.cs new file mode 100644 index 0000000..a05cc7d --- /dev/null +++ b/MdXaml.Html/Core/Utils/HtmlUtils.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MdXaml.Html.Core.Utils +{ + class HtmlUtils + { + private static readonly HashSet s_blockTags = new() + { + "address", + "article", + "aside", + "base", + "basefont", + "blockquote", + "caption", + "center", + "col", + "colgroup", + "dd", + "details", + "dialog", + "dir", + "div", + "dl", + "dt", + "fieldset", + "figcaption", + "figure", + "footer", + "form", + "frame", + "frameset", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "head", + "header", + "hr", + "html", + "iframe", + "legend", + "li", + "link", + "main", + "menu", + "menuitem", + "nav", + "noframes", + "ol", + "optgroup", + "option", + "p", + "param", + "pre", + "script", + "section", + "source", + "style", + "summary", + "table", + "textarea", + "tbody", + "td", + "tfoot", + "th", + "thead", + "title", + "tr", + "track", + "ul", + }; + + public static bool IsBlockTag(string tagName) => s_blockTags.Contains(tagName.ToLower()); + } +} diff --git a/MdXaml.Html/Core/Utils/Length.cs b/MdXaml.Html/Core/Utils/Length.cs new file mode 100644 index 0000000..7a904aa --- /dev/null +++ b/MdXaml.Html/Core/Utils/Length.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.RegularExpressions; + +namespace MdXaml.Html.Core.Utils +{ + internal class Length + { + public double Value { get; } + public Unit Unit { get; } + + public Length(double value, Unit unit) + { + Value = value; + Unit = unit; + } + + public double ToPoint() + { + return Unit switch + { + Unit.Percentage => throw new InvalidOperationException("Percentage canot convert point"), + Unit.em => Value * 11, + Unit.ex => Value * 11 / 2, + Unit.QuarterMillimeters => Value * 3.77952755905512 / 4, + Unit.Millimeters => Value * 3.77952755905512, + Unit.Centimeters => Value * 37.7952755905512, + Unit.Inches => Value * 96.0, + Unit.Points => Value * 1.33333333333333, + Unit.Picas => Value * 16, + Unit.Pixels => Value, + + _ => throw new NotSupportedException("") + }; + } + + public static bool TryParse(string? text, +#if NETFRAMEWORK + out Length rslt) +#else + [MaybeNullWhen(false)] + out Length rslt) +#endif + { + if (String.IsNullOrEmpty(text)) + goto failParse; + + var mch = Regex.Match(text, @"^([0-9\.\+\-eE]+)(%|em|ex|mm|Q|cm|in|pt|pc|px)$"); + + if (!mch.Success) + goto failParse; + + var numTxt = mch.Groups[1].Value.Trim(); + var unitTxt = mch.Groups[2].Value; + + if (!double.TryParse(numTxt, out var numVal)) + goto failParse; + + var unitEnm = unitTxt switch + { + "%" => Unit.Percentage, + "em" => Unit.em, + "ex" => Unit.ex, + "mm" => Unit.Millimeters, + "Q" => Unit.QuarterMillimeters, + "cm" => Unit.Centimeters, + "in" => Unit.Inches, + "pt" => Unit.Points, + "pc" => Unit.Picas, + "px" => Unit.Pixels, + _ => Unit.Pixels, + }; + + rslt = new Length(numVal, unitEnm); + return true; + + failParse: + rslt = null; + return false; + } + } + + internal enum Unit + { + Percentage, + em, + ex, + QuarterMillimeters, + Millimeters, + Centimeters, + Inches, + // pt: 1/72 in + Points, + // pc: 1/6 in + Picas, + // px; 1/96 in + Pixels + } +} diff --git a/MdXaml.Html/Core/Utils/NodeCollectionExt.cs b/MdXaml.Html/Core/Utils/NodeCollectionExt.cs new file mode 100644 index 0000000..6cf7777 --- /dev/null +++ b/MdXaml.Html/Core/Utils/NodeCollectionExt.cs @@ -0,0 +1,157 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace MdXaml.Html.Core.Utils +{ + internal static class NodeCollectionExt + { + public static List SkipComment(this HtmlNodeCollection list) + { + var count = list.Count; + + var store = new List(count); + + for (var i = 0; i < count; ++i) + { + var e = list[i]; + if (e.IsComment()) continue; + + store.Add(e); + } + + return store; + } + + public static bool IsComment(this HtmlNode node) => node is HtmlCommentNode; + + public static List CollectTag(this HtmlNodeCollection list) + { + var count = list.Count; + + var store = new List(count); + + for (var i = 0; i < count; ++i) + { + var e = list[i]; + if (e.NodeType != HtmlNodeType.Element) continue; + + store.Add(e); + } + + return store; + } + + public static List CollectTag(this HtmlNodeCollection list, string tagName) + { + var count = list.Count; + + var store = new List(count); + + for (var i = 0; i < count; ++i) + { + var e = list[i]; + if (e.NodeType != HtmlNodeType.Element) continue; + if (!string.Equals(e.Name, tagName, StringComparison.OrdinalIgnoreCase)) continue; + + store.Add(e); + } + + return store; + } + + public static List CollectTag(this HtmlNodeCollection list, params string[] tagNames) + { + var count = list.Count; + + var store = new List(count); + + for (var i = 0; i < count; ++i) + { + var e = list[i]; + if (e.NodeType != HtmlNodeType.Element) continue; + + if (tagNames.Any(tagNm => Eq(e.Name, tagNm))) + store.Add(e); + } + + return store; + + static bool Eq(string a, string b) + => string.Equals(a, b, StringComparison.OrdinalIgnoreCase); + } + + public static bool HasOneTag( + this HtmlNodeCollection list, + string tagName, +#if NETFRAMEWORK + out HtmlNode child) +#else + [MaybeNullWhen(false)] + out HtmlNode child) +#endif + { + var children = CollectTag(list, tagName); + + if (children.Count == 1) + { + child = children[1]; + return true; + } + else + { + child = null!; + return false; + } + } + + public static bool TryCastTextNode(this HtmlNodeCollection list, out List texts) + { + var count = list.Count; + + texts = new List(count); + + for (var i = 0; i < count; ++i) + { + var e = list[i]; + + if (e is HtmlTextNode txtNd) + { + texts.Add(txtNd); + continue; + } + + if (e.IsComment()) + { + continue; + } + + return false; + } + + return true; + } + + public static Tuple, List> Filter(this IEnumerable list, Func filterFunc) + { + var filterIn = new List(); + var filterOut = new List(); + + foreach (var e in list) + { + if (filterFunc(e)) + { + filterIn.Add(e); + } + else + { + filterOut.Add(e); + } + } + + return Tuple.Create(filterIn, filterOut); + } + } +} diff --git a/MdXaml.Html/Core/Utils/StringExt.cs b/MdXaml.Html/Core/Utils/StringExt.cs new file mode 100644 index 0000000..1499978 --- /dev/null +++ b/MdXaml.Html/Core/Utils/StringExt.cs @@ -0,0 +1,39 @@ +using System; +using System.Net; + +namespace MdXaml.Html.Core.Utils +{ + internal static class StringExt + { + public static string[] SplitLine(this string text) => text.Split('\n'); + + internal static bool TryDecode(this string text, ref int start, out string decoded) + { + // max length of entity is 33 (∳) + var hit = text.IndexOf(';', start, Math.Min(text.Length - start, 40)); + + if (hit == -1) + { + decoded = string.Empty; + return false; + } + + var entity = text.Substring(start, hit - start + 1); + decoded = WebUtility.HtmlDecode(entity); + start = hit; + + if (decoded == "<" && start + 1 < text.Length) + { + var c = text[start + 1]; + if ('a' <= c && c <= 'z' && 'A' <= c && c <= 'Z') + { + // '<[a-zA-Z]' may be treated as tag + decoded = entity; + } + } + + + return true; + } + } +} diff --git a/MdXaml.Html/Core/Utils/TextElementUtils.cs b/MdXaml.Html/Core/Utils/TextElementUtils.cs new file mode 100644 index 0000000..4e7b780 --- /dev/null +++ b/MdXaml.Html/Core/Utils/TextElementUtils.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Markup; + +namespace MdXaml.Html.Core.Utils +{ + internal class TextElementUtils + { + public static TResult Create(IEnumerable content) + where TResult : IAddChild, new() + { + var result = new TResult(); + foreach (var c in content) + { + result.AddChild(c); + } + + return result; + } + } +} diff --git a/MdXaml.Html/HtmlBlockParser.cs b/MdXaml.Html/HtmlBlockParser.cs index 76560ed..2cd8e1d 100644 --- a/MdXaml.Html/HtmlBlockParser.cs +++ b/MdXaml.Html/HtmlBlockParser.cs @@ -1,33 +1,41 @@ -using HtmlXaml.Core; +using MdXaml.Html.Core; using MdXaml.Plugins; using System.Collections.Generic; using System.Text.RegularExpressions; using System.Windows.Documents; +using System.Linq; +using TextRange = MdXaml.Html.Core.TextRange; namespace MdXaml.Html { public class HtmlBlockParser : IBlockParser { + private static readonly Regex s_emptyLine = new Regex("\n{2,}", RegexOptions.Compiled); + private static readonly Regex s_headTagPattern = new(@"^<[\t ]*(?'tagname'[a-z][a-z0-9]*)(?'attributes'[ \t][^>]*|/)?>", + RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex s_tagPattern = new(@"<(?'close'/?)[\t ]*(?'tagname'[a-z][a-z0-9]*)(?'attributes'[ \t][^>]*|/)?>", + RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled | RegexOptions.IgnoreCase); + private ReplaceManager _replacer; public HtmlBlockParser() { _replacer = new ReplaceManager(); - FirstMatchPattern = HtmlUtils.CreateTagstartPattern(_replacer.BlockTags); + FirstMatchPattern = s_headTagPattern; } public Regex FirstMatchPattern { get; } - public IEnumerable Parse(string text, Match firstMatch, bool supportTextAlignment, Markdown engine, out int parseTextBegin, out int parseTextEnd) + public IEnumerable Parse(string text, Match firstMatch, bool supportTextAlignment, IMarkdown engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; - parseTextEnd = HtmlUtils.SearchTagRange(text, firstMatch); + parseTextEnd = SimpleHtmlUtils.SearchTagRangeContinuous(text, firstMatch); + + _replacer.Engine = engine; - _replacer.AssetPathRoot = engine.AssetPathRoot; - _replacer.BaseUri = engine.BaseUri; - _replacer.HyperlinkCommand = engine.HyperlinkCommand; + var textchip = text.Substring(parseTextBegin, parseTextEnd - parseTextBegin); - return _replacer.ParseBlock(text.Substring(parseTextBegin, parseTextEnd - parseTextBegin)); + return _replacer.Parse(textchip); } } } diff --git a/MdXaml.Html/HtmlInlineParser.cs b/MdXaml.Html/HtmlInlineParser.cs index 3e8ad66..41fe066 100644 --- a/MdXaml.Html/HtmlInlineParser.cs +++ b/MdXaml.Html/HtmlInlineParser.cs @@ -1,4 +1,4 @@ -using HtmlXaml.Core; +using MdXaml.Html.Core; using MdXaml.Plugins; using System.Collections.Generic; using System.Text.RegularExpressions; @@ -13,19 +13,17 @@ public class HtmlInlineParser : IInlineParser public HtmlInlineParser() { _replacer = new ReplaceManager(); - FirstMatchPattern = HtmlUtils.CreateTagstartPattern(_replacer.InlineTags); + FirstMatchPattern = SimpleHtmlUtils.CreateTagstartPattern(_replacer.InlineTags); } public Regex FirstMatchPattern { get; } - public IEnumerable Parse(string text, Match firstMatch, Markdown engine, out int parseTextBegin, out int parseTextEnd) + public IEnumerable Parse(string text, Match firstMatch, IMarkdown engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; - parseTextEnd = HtmlUtils.SearchTagRange(text, firstMatch); + parseTextEnd = SimpleHtmlUtils.SearchTagRange(text, firstMatch); - _replacer.AssetPathRoot = engine.AssetPathRoot; - _replacer.BaseUri = engine.BaseUri; - _replacer.HyperlinkCommand = engine.HyperlinkCommand; + _replacer.Engine = engine; return _replacer.ParseInline(text.Substring(parseTextBegin, parseTextEnd - parseTextBegin)); } diff --git a/MdXaml.Html/MdXaml.Html.csproj b/MdXaml.Html/MdXaml.Html.csproj index f5d1b96..5663531 100644 --- a/MdXaml.Html/MdXaml.Html.csproj +++ b/MdXaml.Html/MdXaml.Html.csproj @@ -1,12 +1,12 @@ - + netcoreapp3.0;net45;net5.0-windows MdXaml.Html - 1.16.0-alpha1 + 1.16.0 whistyun Markdown XAML processor - Copyright (c) 2021 whistyun + © Simon Baynes 2013; whistyun 2022 https://github.com/whistyun/MdXaml MIT Markdown WPF Xaml FlowDocument @@ -16,13 +16,20 @@ 9 enable - + + + + + + - - + + + + diff --git a/MdXaml.Html/HtmlUtils.cs b/MdXaml.Html/SimpleHtmlUtils.cs similarity index 72% rename from MdXaml.Html/HtmlUtils.cs rename to MdXaml.Html/SimpleHtmlUtils.cs index dbdc92b..494e82e 100644 --- a/MdXaml.Html/HtmlUtils.cs +++ b/MdXaml.Html/SimpleHtmlUtils.cs @@ -5,15 +5,17 @@ namespace MdXaml.Html { - internal static class HtmlUtils + internal static class SimpleHtmlUtils { private static readonly HashSet s_emptyList = new(new[] { "area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", }); - private static readonly Regex TagPattern = new(@"<(?'close'/?)[\t ]*(?'tagname'[a-z]+)(?'attributes'[ \t][^>]*|/)?>", + private static readonly Regex s_tagPattern = new(@"<(?'close'/?)[\t ]*(?'tagname'[a-z][a-z0-9]*)(?'attributes'[ \t][^>]*|/)?>", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex s_emptylinePattern = new(@"\n{2}", RegexOptions.Compiled); + public static Regex CreateTagstartPattern(IEnumerable tags) { var taglist = string.Join("|", tags); @@ -37,6 +39,27 @@ public static int SearchTagRange(string text, Match tagStartPatternMatch) } } + public static int SearchTagRangeContinuous(string text, Match tagStartPatternMatch) + { + int idx = SearchTagRange(text, tagStartPatternMatch); + + for (; ; ) + { + if (text.Length - 1 <= idx) return idx; + + var emp = s_emptylinePattern.Match(text, idx); + if (!emp.Success) return text.Length - 1; + + var tag = s_tagPattern.Match(text, idx); + if (tag.Success && tag.Index < emp.Index) + { + idx = SearchTagRange(text, tag); + } + else return emp.Index; + } + } + + public static int SearchTagEnd(string text, int start, string startTagName) { var tags = new Stack(); @@ -46,11 +69,13 @@ public static int SearchTagEnd(string text, int start, string startTagName) { var isEmptyTag = s_emptyList.Contains(tags.Peek()); - var mch = TagPattern.Match(text, start); + var mch = s_tagPattern.Match(text, start); if (isEmptyTag && (!mch.Success || mch.Index != start)) { - return start; + if (tags.Count == 1) return start; + + tags.Pop(); } if (!mch.Success) return -1; diff --git a/MdXaml/Plugins/IBlockParser.cs b/MdXaml.Plugins/IBlockParser.cs similarity index 85% rename from MdXaml/Plugins/IBlockParser.cs rename to MdXaml.Plugins/IBlockParser.cs index 36863ff..fbb7695 100644 --- a/MdXaml/Plugins/IBlockParser.cs +++ b/MdXaml.Plugins/IBlockParser.cs @@ -3,21 +3,9 @@ using System.Windows.Documents; using System.Text.RegularExpressions; -#if MIG_FREE -using Engine = Markdown.Xaml.Markdown; -#else -using Engine = MdXaml.Markdown; -#endif - - namespace MdXaml.Plugins { -#if MIG_FREE - internal -#else - public -#endif - interface IBlockParser + public interface IBlockParser { /// /// The head pattern of parsing. It is good for performance that the pattern is match persable syntax. @@ -34,7 +22,7 @@ interface IBlockParser /// Parsed result, or null if unable to parsed. IEnumerable Parse( string text, Match firstMatch, bool supportTextAlignment, - Engine engine, + IMarkdown engine, out int parseTextBegin, out int parseTextEnd); } } diff --git a/MdXaml/Plugins/IImageLoader.cs b/MdXaml.Plugins/IImageLoader.cs similarity index 73% rename from MdXaml/Plugins/IImageLoader.cs rename to MdXaml.Plugins/IImageLoader.cs index 33d872e..0f7c808 100644 --- a/MdXaml/Plugins/IImageLoader.cs +++ b/MdXaml.Plugins/IImageLoader.cs @@ -6,12 +6,7 @@ namespace MdXaml.Plugins { -#if MIG_FREE - internal -#else - public -#endif - interface IImageLoader + public interface IImageLoader { public BitmapImage? Load(Stream stream); } diff --git a/MdXaml/Plugins/IInlineParser.cs b/MdXaml.Plugins/IInlineParser.cs similarity index 85% rename from MdXaml/Plugins/IInlineParser.cs rename to MdXaml.Plugins/IInlineParser.cs index c4e1182..f6a1831 100644 --- a/MdXaml/Plugins/IInlineParser.cs +++ b/MdXaml.Plugins/IInlineParser.cs @@ -3,20 +3,9 @@ using System.Windows.Documents; using System.Text.RegularExpressions; -#if MIG_FREE -using Engine = Markdown.Xaml.Markdown; -#else -using Engine = MdXaml.Markdown; -#endif - namespace MdXaml.Plugins { -#if MIG_FREE - internal -#else - public -#endif - interface IInlineParser + public interface IInlineParser { /// /// The head pattern of parsing. It is good for performance that the pattern is match persable syntax. @@ -33,7 +22,7 @@ interface IInlineParser /// Parsed result, or null if unable to parsed. IEnumerable Parse( string text, Match firstMatch, - Engine engine, + IMarkdown engine, out int parseTextBegin, out int parseTextEnd); } } diff --git a/MdXaml.Plugins/IMarkdown.cs b/MdXaml.Plugins/IMarkdown.cs new file mode 100644 index 0000000..947ad16 --- /dev/null +++ b/MdXaml.Plugins/IMarkdown.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; + +namespace MdXaml.Plugins +{ + public interface IMarkdown + { + public ICommand? HyperlinkCommand { get; } + + public Uri? BaseUri { get; } + + public string? AssetPathRoot { get; } + + public InlineUIContainer LoadImage( + string? tag, string urlTxt, string? tooltipTxt, + Action? onSuccess = null); + + FlowDocument Transform(string text); + + IEnumerable RunBlockGamut(string text, bool supportTextAlignment); + + IEnumerable RunSpanGamut(string text); + } +} diff --git a/MdXaml/Plugins/IPluginSetup.cs b/MdXaml.Plugins/IPluginSetup.cs similarity index 52% rename from MdXaml/Plugins/IPluginSetup.cs rename to MdXaml.Plugins/IPluginSetup.cs index 67a6576..b00c697 100644 --- a/MdXaml/Plugins/IPluginSetup.cs +++ b/MdXaml.Plugins/IPluginSetup.cs @@ -1,12 +1,6 @@ namespace MdXaml.Plugins { -#if MIG_FREE - internal -#else - public -#endif - - interface IPluginSetup + public interface IPluginSetup { void Setup(MdXamlPlugins plugins); } diff --git a/MdXaml.Plugins/MdXaml.Plugins.csproj b/MdXaml.Plugins/MdXaml.Plugins.csproj new file mode 100644 index 0000000..4ebd223 --- /dev/null +++ b/MdXaml.Plugins/MdXaml.Plugins.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.0;net45;net5.0-windows + MdXaml.Plugins + 1.16.0 + whistyun + + Markdown XAML processor + Copyright (c) 2022 whistyun + https://github.com/whistyun/MdXaml + MIT + Markdown WPF Xaml FlowDocument + Debug;Release + + true + 9 + enable + + diff --git a/MdXaml/Plugins/MdXamlPlugins.cs b/MdXaml.Plugins/MdXamlPlugins.cs similarity index 94% rename from MdXaml/Plugins/MdXamlPlugins.cs rename to MdXaml.Plugins/MdXamlPlugins.cs index 151d9bf..ae42622 100644 --- a/MdXaml/Plugins/MdXamlPlugins.cs +++ b/MdXaml.Plugins/MdXamlPlugins.cs @@ -9,12 +9,7 @@ namespace MdXaml.Plugins { [ContentProperty(nameof(Setups))] -#if MIG_FREE - internal -#else - public -#endif - class MdXamlPlugins + public class MdXamlPlugins { public static readonly MdXamlPlugins Default = new(); diff --git a/MdXaml/Plugins/SyntaxManager.cs b/MdXaml.Plugins/SyntaxManager.cs similarity index 82% rename from MdXaml/Plugins/SyntaxManager.cs rename to MdXaml.Plugins/SyntaxManager.cs index 11660e4..257c1f0 100644 --- a/MdXaml/Plugins/SyntaxManager.cs +++ b/MdXaml.Plugins/SyntaxManager.cs @@ -5,12 +5,7 @@ namespace MdXaml.Plugins { -#if MIG_FREE - internal -#else - public -#endif - class SyntaxManager + public class SyntaxManager { public bool EnableNoteBlock { set; get; } = true; public bool EnableTableBlock { set; get; } = true; diff --git a/MdXaml.Svg/MdXaml.Svg.csproj b/MdXaml.Svg/MdXaml.Svg.csproj index e2ad6b5..15e8ec7 100644 --- a/MdXaml.Svg/MdXaml.Svg.csproj +++ b/MdXaml.Svg/MdXaml.Svg.csproj @@ -3,11 +3,11 @@ netcoreapp3.0;net45;net5.0-windows MdXaml.Svg - 1.16.0-alpha1 + 1.16.0 whistyun Markdown XAML processor - Copyright (c) 2021 whistyun + Copyright (c) 2022 whistyun https://github.com/whistyun/MdXaml MIT Markdown WPF Xaml FlowDocument @@ -23,6 +23,7 @@ + diff --git a/MdXaml.Svg/SvgImageLoader.cs b/MdXaml.Svg/SvgImageLoader.cs index 55b2b90..9f7672b 100644 --- a/MdXaml.Svg/SvgImageLoader.cs +++ b/MdXaml.Svg/SvgImageLoader.cs @@ -1,4 +1,4 @@ -using MdXaml.Plugins; +using MdXaml.Plugins; using Svg; using System; using System.Drawing; diff --git a/MdXaml.Svg/SvgPluginSetup.cs b/MdXaml.Svg/SvgPluginSetup.cs index 391374f..995623d 100644 --- a/MdXaml.Svg/SvgPluginSetup.cs +++ b/MdXaml.Svg/SvgPluginSetup.cs @@ -1,4 +1,4 @@ -using MdXaml.Plugins; +using MdXaml.Plugins; using System; using System.Collections.Generic; using System.Text; diff --git a/MdXaml.sln b/MdXaml.sln index 723821c..d4cfc30 100644 --- a/MdXaml.sln +++ b/MdXaml.sln @@ -30,15 +30,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MdXamlMigfree", "MdXaml\MdX EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WithFluentWPF", "samples\WithFluentWPF\WithFluentWPF.csproj", "{C539C7B4-BBF2-4AC2-A732-0C47E125CCD2}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "externals", "externals", "{B37FB22C-258D-49E6-9671-6455A9C92C7C}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MdXaml.Html", "MdXaml.Html\MdXaml.Html.csproj", "{67BB50BF-B50A-4EBE-A232-BE8C407B38E0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HtmlXaml.Core", "externals\HtmlXaml.Core\HtmlXaml.Core\HtmlXaml.Core.csproj", "{69156EF7-C120-4C0D-AD26-BD1A9421513F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mixing.Test", "tests\Mixing.Test\Mixing.Test.csproj", "{C45A1400-B4E9-4638-BF1F-4BB38CEC5E93}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MdXaml.Svg", "MdXaml.Svg\MdXaml.Svg.csproj", "{83D2F5A4-6489-4078-BDBC-F4F941B9D27B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MdXaml.Svg", "MdXaml.Svg\MdXaml.Svg.csproj", "{83D2F5A4-6489-4078-BDBC-F4F941B9D27B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MdXaml.Html.Test", "tests\MdXaml.Html.Test\MdXaml.Html.Test.csproj", "{CEF2B49F-90AF-4B65-AA0C-A12AB1C0A18F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MdXaml.Plugins", "MdXaml.Plugins\MdXaml.Plugins.csproj", "{823DB5D5-17C0-4418-895C-A28DD7D04CE9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -86,10 +86,6 @@ Global {67BB50BF-B50A-4EBE-A232-BE8C407B38E0}.Debug|Any CPU.Build.0 = Debug|Any CPU {67BB50BF-B50A-4EBE-A232-BE8C407B38E0}.Release|Any CPU.ActiveCfg = Release|Any CPU {67BB50BF-B50A-4EBE-A232-BE8C407B38E0}.Release|Any CPU.Build.0 = Release|Any CPU - {69156EF7-C120-4C0D-AD26-BD1A9421513F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {69156EF7-C120-4C0D-AD26-BD1A9421513F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {69156EF7-C120-4C0D-AD26-BD1A9421513F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {69156EF7-C120-4C0D-AD26-BD1A9421513F}.Release|Any CPU.Build.0 = Release|Any CPU {C45A1400-B4E9-4638-BF1F-4BB38CEC5E93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C45A1400-B4E9-4638-BF1F-4BB38CEC5E93}.Debug|Any CPU.Build.0 = Debug|Any CPU {C45A1400-B4E9-4638-BF1F-4BB38CEC5E93}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -98,6 +94,14 @@ Global {83D2F5A4-6489-4078-BDBC-F4F941B9D27B}.Debug|Any CPU.Build.0 = Debug|Any CPU {83D2F5A4-6489-4078-BDBC-F4F941B9D27B}.Release|Any CPU.ActiveCfg = Release|Any CPU {83D2F5A4-6489-4078-BDBC-F4F941B9D27B}.Release|Any CPU.Build.0 = Release|Any CPU + {CEF2B49F-90AF-4B65-AA0C-A12AB1C0A18F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CEF2B49F-90AF-4B65-AA0C-A12AB1C0A18F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CEF2B49F-90AF-4B65-AA0C-A12AB1C0A18F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CEF2B49F-90AF-4B65-AA0C-A12AB1C0A18F}.Release|Any CPU.Build.0 = Release|Any CPU + {823DB5D5-17C0-4418-895C-A28DD7D04CE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {823DB5D5-17C0-4418-895C-A28DD7D04CE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {823DB5D5-17C0-4418-895C-A28DD7D04CE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {823DB5D5-17C0-4418-895C-A28DD7D04CE9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -110,8 +114,8 @@ Global {3FA0CDA5-C86F-4CF9-AB9E-1A8C02FDD8F4} = {435867FA-EE25-4708-9BBA-8509ABC7E389} {855272D5-AB9E-43FC-801F-F0E7CAEB28E0} = {435867FA-EE25-4708-9BBA-8509ABC7E389} {C539C7B4-BBF2-4AC2-A732-0C47E125CCD2} = {435867FA-EE25-4708-9BBA-8509ABC7E389} - {69156EF7-C120-4C0D-AD26-BD1A9421513F} = {B37FB22C-258D-49E6-9671-6455A9C92C7C} {C45A1400-B4E9-4638-BF1F-4BB38CEC5E93} = {09BEAB2A-F47E-4D2B-AE81-8DC1BBB52638} + {CEF2B49F-90AF-4B65-AA0C-A12AB1C0A18F} = {09BEAB2A-F47E-4D2B-AE81-8DC1BBB52638} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {23DF7019-3B25-4B82-8955-25F1DDD72D84} diff --git a/MdXaml/Plugins/ImageLoaderManager.cs b/MdXaml/ImageLoaderManager.cs similarity index 99% rename from MdXaml/Plugins/ImageLoaderManager.cs rename to MdXaml/ImageLoaderManager.cs index cba5f24..57bb92e 100644 --- a/MdXaml/Plugins/ImageLoaderManager.cs +++ b/MdXaml/ImageLoaderManager.cs @@ -1,4 +1,5 @@ -using System; +using MdXaml.Plugins; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -11,7 +12,7 @@ using System.Windows.Media.Imaging; using System.Windows.Threading; -namespace MdXaml.Plugins +namespace MdXaml { internal class ImageLoaderManager { diff --git a/MdXaml/Markdown.Style.xaml b/MdXaml/Markdown.Style.xaml index 6324651..4dd0832 100644 --- a/MdXaml/Markdown.Style.xaml +++ b/MdXaml/Markdown.Style.xaml @@ -86,6 +86,15 @@ + + + + + +