private static void Render(MarkdownDocument doc, string fileName) { Console.WriteLine("Creating {0}", fileName); string directoryName = Path.GetDirectoryName(fileName); if (!Directory.Exists(directoryName)) { Directory.CreateDirectory(directoryName); } var options = new NormalizeOptions(); using (var writer = new StreamWriter(fileName)) { var renderer = new NormalizeRenderer(writer, options); renderer.ObjectRenderers.Add(new NormalizeTableRenderer()); var linkReferenceDefinitionRenderer = renderer.ObjectRenderers.Find <LinkReferenceDefinitionRenderer>(); if (linkReferenceDefinitionRenderer != null) { renderer.ObjectRenderers.Remove(linkReferenceDefinitionRenderer); } renderer.Render(doc); } }
private static void AppendInline(Inline inline, StringWriter writer, NormalizeRenderer renderer) { switch (inline) { case LineBreakInline: writer.Write(' '); break; case EmphasisInline emphasis: foreach (var item in emphasis) { AppendInline(item, writer, renderer); } break; case CodeInline code: writer.Write(code.Content); break; case LinkInline link: foreach (var item in link) { AppendInline(item, writer, renderer); } writer.Write($" ({link.Url})"); break; default: renderer.Render(inline); break; } }
private static string RenderToString(MarkdownDocument document) { using var writer = new StringWriter { NewLine = "\n" }; var renderer = new NormalizeRenderer(writer); renderer.Render(document); return(writer.ToString()); }
public MarkdownSource Update() { using var writer = new StringWriter(); var renderer = new NormalizeRenderer(writer); renderer.Render(Document); return(new MarkdownSource(writer.ToString())); }
protected override void Write(NormalizeRenderer renderer, Table obj) { renderer.EnsureLine(); foreach (var row in obj.OfType <TableRow>()) { renderer.Write(PipeSeparator); foreach (var tableCell in row) { renderer.Write(MarginSeparator); renderer.Render(tableCell); renderer.Write(MarginSeparator); renderer.Write(PipeSeparator); } renderer.WriteLine(); if (row.IsHeader) { bool alignmentEnabled = obj.ColumnDefinitions.Any(c => c.Alignment != TableColumnAlign.Left); renderer.Write(PipeSeparator); foreach (var column in obj.ColumnDefinitions) { renderer.Write(MarginSeparator); if (alignmentEnabled && (column.Alignment == TableColumnAlign.Left || column.Alignment == TableColumnAlign.Center)) { renderer.Write(AlignmentChar); } renderer.Write(HeaderSeparator); if (alignmentEnabled && (column.Alignment == TableColumnAlign.Right || column.Alignment == TableColumnAlign.Center)) { renderer.Write(AlignmentChar); } renderer.Write(MarginSeparator); renderer.Write(PipeSeparator); } renderer.WriteLine(); } } renderer.FinishBlock(true); }
static string ToMarkdown(IEnumerable <Block> blocks) { var writer = new StringWriter(); var renderer = new NormalizeRenderer(writer); var pipeline = new MarkdownPipelineBuilder().Build(); pipeline.Setup(renderer); foreach (var block in blocks) { renderer.Render(block); } // We convert \n to \r because the YAML serialization will eventually // output \n\n for \n, but \r\n for \r. return(writer.ToString().TrimEnd().Replace('\n', '\r')); }
public void RenderMarkdown(string fileName, MarkdownDocument document) { if (!_renderSettings.RenderMarkdown) { return; } string renamedFileName = fileName.Substring(0, fileName.Length - 3) + ".normalized.md"; var file = new File(_renderSettings.MarkdownFolder, renamedFileName); using (var writer = new StreamWriter(file.AbsolutePath)) { var normalizeRenderer = new NormalizeRenderer(writer); normalizeRenderer.Render(document); } }
// notes may contain html and markdown, so to include: // 1. Convert html into markdown // 2. Parse markdown into ast // 3. Normalize headings // 4. Convert ast into markdown text // 5. Add markdown text to stream public string Normalize(string text) { string markdownText = _converter.Convert(text); MarkdownDocument markdown = Markdown.Parse(markdownText); using (var writer = new StringWriter()) { var pipeline = new MarkdownPipelineBuilder().Build(); pipeline.Extensions.AddIfNotAlready <SoftlineBreakAsHardlineExtension>(); var renderer = new NormalizeRenderer(writer); pipeline.Setup(renderer); renderer.Render(markdown); writer.Flush(); return(writer.ToString()); } }
/// <summary> /// Normalizes the specified markdown to a normalized markdown text. /// </summary> /// <param name="markdown">The markdown.</param> /// <param name="writer">The destination <see cref="TextWriter"/> that will receive the result of the conversion.</param> /// <param name="options">The normalize options</param> /// <param name="pipeline">The pipeline.</param> /// <param name="context">A parser context used for the parsing.</param> /// <returns>A normalized markdown text.</returns> public static MarkdownDocument Normalize(string markdown, TextWriter writer, NormalizeOptions options = null, MarkdownPipeline pipeline = null, MarkdownParserContext context = null) { pipeline = pipeline ?? new MarkdownPipelineBuilder().Build(); pipeline = CheckForSelfPipeline(pipeline, markdown); // We override the renderer with our own writer var renderer = new NormalizeRenderer(writer, options); pipeline.Setup(renderer); var document = Parse(markdown, pipeline, context); renderer.Render(document); writer.Flush(); return(document); }
/* * public static Item ToItem(string md) * { * var pipeline = new MarkdownPipelineBuilder().UsePipeTables().Build(); * var document = MarkdownParser.Parse(md, pipeline); * * var enumerator = document.GetEnumerator(); * try * { * enumerator.MoveNext(); * while (enumerator.Current != null) * { * var block = enumerator.Current; * * if (block is HtmlBlock) * { * if (block.IsNewItem()) * { * var item = ParseItem(ref enumerator); * return item; * } * } * enumerator.MoveNext(); * } * * } * finally * { * enumerator.Dispose(); * } * return null; * } * * public static Item ParseItem(ref ContainerBlock.Enumerator enumerator) * { * var currentItem = enumerator.Current.GetNewItem(); * * if (currentItem != null) * { * enumerator.MoveNext(); * while (enumerator.Current != null) * { * var block = enumerator.Current; * * if (block is HtmlBlock) * { * if (block.IsClosingItem()) * { * return currentItem; * } * else if (block.IsNewItem()) * { * var subItem = ParseItem(ref enumerator); * * var propertyName = subItem.GetType().Name; * * if (currentItem.GetType().GetProperty(propertyName) != null) * { * PropertyInfo prop = currentItem.GetType().GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); * if (null != prop && prop.CanWrite) * { * prop.SetValue(currentItem, subItem, null); * } * } * else if (currentItem is Items) * { * var items = currentItem as Items; * items.Add(subItem); * } * } * } * * else // if (block is ContainerBlock) * { * ParseItemProperties(currentItem, block); * } * * currentItem.Markdown += enumerator.Current.ToMarkdownString(); * * enumerator.MoveNext(); * } * } * * return currentItem; * } * * public static void ParseItemProperties(Item item, Block block) * { * switch(block) * { * case Markdig.Extensions.Tables.Table table: * ParseItemProperties(item, table); * break; * case ContainerBlock blocks: * ParseItemProperties(item, blocks); * break; * case LeafBlock leaf: * ParseItemProperties(item, leaf.Inline); * break; * } * } * * public static void ParseItemProperties(Item item, ContainerBlock blocks) * { * foreach(var block in blocks) * { * ParseItemProperties(item, block); * } * } * * public static void ParseItemProperties(Item item, ContainerInline inlines) * { * if(inlines == null) * { * return; * } * PropertyInfo prop = null; * foreach (var inline in inlines) * { * if(inline is HtmlInline) * { * var tag = (inline as HtmlInline).Tag; * if(tag == "<!--br-->" || tag =="<br>") * { * * } * else if (tag.StartsWith("<!--/")) * { * prop = null; * } * else if (tag.StartsWith("<!--") && !tag.StartsWith("<!--/")) * { * var propertyName = tag.Substring(4, tag.Length - 7); * prop = item.GetType().GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); * } * } * else * { * if (null != prop && prop.CanWrite) * { * prop.SetValue(item, inline.ToMarkdownString(), null); * } * } * } * } * * * * public static bool IsNewItem(this Block block) * { * var htmlBlock = block as HtmlBlock; * if (htmlBlock.Type == HtmlBlockType.Comment) * { * var tag = htmlBlock.Lines.Lines.FirstOrDefault().Slice.ToString(); * if (!string.IsNullOrEmpty(tag) && tag != "<!--br-->" && tag != "<br>") * { * if (tag.StartsWith("<!--") && !tag.StartsWith("<!--/")) * { * return true; * } * } * } * return false; * } * * public static bool IsClosingItem(this Block block) * { * var htmlBlock = block as HtmlBlock; * if (htmlBlock.Type == HtmlBlockType.Comment) * { * var tag = htmlBlock.Lines.Lines.FirstOrDefault().Slice.ToString(); * if (!string.IsNullOrEmpty(tag) && tag != "<!--br-->" && tag != "<br>") * { * if (tag.StartsWith("<!--/")) * { * return true; * } * } * } * return false; * } * * public static Item GetNewItem(this Block block) * { * var htmlBlock = block as HtmlBlock; * if (htmlBlock.Type == HtmlBlockType.Comment) * { * var tag = htmlBlock.Lines.Lines.FirstOrDefault().Slice.ToString(); * if (!string.IsNullOrEmpty(tag) && tag != "<!--br-->" && tag != "<br>") * { * if (tag.StartsWith("<!--") && !tag.StartsWith("<!--/")) * { * var name = $"AideDeJeuLib.{tag.Substring(4, tag.Length - 7)}, AideDeJeu"; * var type = Type.GetType(name); * var instance = Activator.CreateInstance(type) as Item; * return instance; * } * } * } * return null; * } */ /* * public static Item ToItem(string md) * { * var pipeline = new MarkdownPipelineBuilder().UsePipeTables().Build(); * var document = MarkdownParser.Parse(md, pipeline); * * var enumerator = document.GetEnumerator(); * try * { * enumerator.MoveNext(); * while (enumerator.Current != null) * { * var block = enumerator.Current; * * if (enumerator.Current is ParagraphBlock) * { * if(block.IsNewItem()) * { * var item = block.GetNewItem(); * item.Parse(ref enumerator); * return item; * } * } * enumerator.MoveNext(); * } * * } * finally * { * enumerator.Dispose(); * } * return null; * } * * public static bool IsNewItem(this Block block) * { * var paragraphBlock = block as ParagraphBlock; * var linkInline = paragraphBlock?.Inline?.FirstChild as LinkInline; * if (linkInline != null) * { * var label = linkInline.Label; * var title = linkInline.Title; * if (title == string.Empty && label != string.Empty) * { * return true; * } * } * return false; * } * * public static bool IsClosingItem(this Block block) * { * var paragraphBlock = block as ParagraphBlock; * var linkInline = paragraphBlock?.Inline?.FirstChild as LinkInline; * if (linkInline != null) * { * var label = linkInline.Label; * var title = linkInline.Title; * if (title == string.Empty && label == string.Empty) * { * return true; * } * } * return false; * } * * public static Item GetNewItem(this Block block) * { * var paragraphBlock = block as ParagraphBlock; * var linkInline = paragraphBlock?.Inline?.FirstChild as LinkInline; * if (linkInline != null) * { * var label = linkInline.Label; * var title = linkInline.Title; * var url = linkInline.Url; * if (title == string.Empty) * { * var name = $"AideDeJeuLib.{label}, AideDeJeu"; * var type = Type.GetType(name); * var instance = Activator.CreateInstance(type) as Item; * return instance; * } * } * return null; * } */ public static string ToMarkdownString(this Block block) { var pipeline = new MarkdownPipelineBuilder() .UsePipeTables() .Build(); using (var writer = new StringWriter()) { var renderer = new NormalizeRenderer(writer); renderer.ObjectRenderers.Remove(renderer.ObjectRenderers.FirstOrDefault(i => i is LinkInlineRenderer)); renderer.ObjectRenderers.Add(new LinkInlineRendererEx()); renderer.ObjectRenderers.Add(new TableRenderer()); pipeline.Setup(renderer); renderer.Render(block); return(writer.ToString()); } }
public string Process(string text, Func <long, string> urlGenerator) { MarkdownDocument markdown = Markdown.Parse(text); foreach (var link in markdown.Descendants().Where(e => e is LinkInline).Cast <LinkInline>()) { if (int.TryParse(link.Url, out var dataIndex)) { link.Url = urlGenerator(dataIndex); } } var writer = new StringWriter(); NormalizeRenderer renderer = new NormalizeRenderer(writer); renderer.Render(markdown); return(writer.ToString()); }
/// <summary> /// Normalizes the specified markdown to a normalized markdown text. /// </summary> /// <param name="markdown">The markdown.</param> /// <param name="writer">The destination <see cref="TextWriter"/> that will receive the result of the conversion.</param> /// <param name="options">The normalize options</param> /// <param name="pipeline">The pipeline.</param> /// <param name="context">A parser context used for the parsing.</param> /// <returns>A normalized markdown text.</returns> public static MarkdownDocument Normalize(string markdown, TextWriter writer, NormalizeOptions?options = null, MarkdownPipeline?pipeline = null, MarkdownParserContext?context = null) { if (markdown is null) { ThrowHelper.ArgumentNullException_markdown(); } pipeline = GetPipeline(pipeline, markdown); var document = MarkdownParser.Parse(markdown, pipeline, context); var renderer = new NormalizeRenderer(writer, options); pipeline.Setup(renderer); renderer.Render(document); writer.Flush(); return(document); }
private static void AssertSyntax(string expected, MarkdownObject syntax) { var writer = new StringWriter(); var normalizer = new NormalizeRenderer(writer); var document = new MarkdownDocument(); if (syntax is Block) { document.Add(syntax as Block); } else { throw new InvalidOperationException(); } normalizer.Render(document); var actual = writer.ToString(); Assert.AreEqual(expected, actual); }
public async Task <string> SolveImagesUrlAsync(string source, Func <string, Task <string> > converter) { using (var tw = new StringWriter()) { var writer = new NormalizeRenderer(tw); Pipeline.Setup(writer); var doc1 = Parse(source); await doc1.TransverseAsync <LinkInline>(async item => { if (item.IsImage) { item.Url = await converter(item.Url); } }); writer.Render(doc1); writer.Writer.Flush(); return(tw.ToString()); } }
public override void Transform(ExtensionHtmlRenderer extensionHtmlRenderer, WorkflowNotesBlock block, Diagram diagram) { var(element, elementsEnumerable) = _provider.GetBpmnElements(new EaProvider.Path(diagram.Name)); var elements = elementsEnumerable.ToList(); elements.Sort(new BpmnElement.AliasComparer()); var sb = new StringBuilder(); sb.AppendLine($@"# {element.Name}"); var converter = new Html2Markdown.Converter(); foreach (BpmnElement e in elements) { string name = string.IsNullOrEmpty(e.Name) ? e.Alias : e.Name; string notes = converter.Convert(e.Notes); MarkdownDocument notesMd = Markdown.Parse(notes); notesMd.IncreaseHeadingLevel(2); string normalizedNotes = null; using (var writer = new StringWriter()) { var pipeline = new MarkdownPipelineBuilder().Build(); pipeline.Extensions.AddIfNotAlready <SoftlineBreakAsHardlineExtension>(); var renderer = new NormalizeRenderer(writer); pipeline.Setup(renderer); renderer.Render(notesMd); writer.Flush(); normalizedNotes = writer.ToString(); } sb.AppendLine($@"## {name}"); sb.AppendLine($@"Lane: {e.Lane}"); sb.AppendLine(); sb.AppendLine($@"Description:"); sb.AppendLine(normalizedNotes); } MarkdownDocument document = Markdown.Parse(sb.ToString()); Replace(block, document); }
public static string Converter(string mixedHtmlAndMarkdown, Action <MarkdownDocument> transform, MarkdownPipeline pipeline) { var converter = new Html2Markdown.Converter(); string markdownOnly = converter.Convert(mixedHtmlAndMarkdown); pipeline = new MarkdownPipelineBuilder() .UseAdvancedExtensions() .UsePipeTables() .UseGridTables() .Build(); pipeline.Extensions.AddIfNotAlready <SoftlineBreakAsHardlineExtension>(); MarkdownDocument ast = Markdown.Parse(markdownOnly, pipeline); transform(ast); using (var writer = new StringWriter()) { var renderer = new NormalizeRenderer(writer); pipeline.Setup(renderer); renderer.Render(ast); writer.Flush(); return(writer.ToString()); } }
private static void AppendBlock(Block block, StringWriter writer, NormalizeRenderer renderer) { switch (block) { case HeadingBlock heading: writer.Write('['); foreach (var item in heading.Inline) { AppendInline(item, writer, renderer); } writer.WriteLine(']'); break; case QuoteBlock quote: foreach (var item in quote) { using var nestedWriter = new StringWriter { NewLine = "\n" }; var nestedRenderer = new NormalizeRenderer(nestedWriter); AppendBlock(item, nestedWriter, nestedRenderer); var quotedLines = nestedWriter.ToString().Split("\n"); for (var i = 0; i < quotedLines.Length; ++i) { var line = quotedLines[i]; if (i == quotedLines.Length - 1 && line == "") { continue; } writer.WriteLine($"> {line}"); } } break; case CodeBlock code: for (var index = 0; index < code.Lines.Count; ++index) { writer.WriteLine(code.Lines.Lines[index]); } break; case ParagraphBlock paragraph: foreach (var item in paragraph.Inline) { AppendInline(item, writer, renderer); } writer.WriteLine(); break; case ThematicBreakBlock: writer.WriteLine("* * *"); break; case ListBlock { BulletType: '1' } list: var number = 1; foreach (var item in list) { writer.Write($"{number++}. "); AppendBlock(item, writer, renderer); } break; case ListBlock list: foreach (var item in list) { writer.Write("- "); AppendBlock(item, writer, renderer); } break; case LinkReferenceDefinitionGroup _: // Handled automatically when processing links; no output is expected. break; case ContainerBlock container: foreach (var item in container) { AppendBlock(item, writer, renderer); } break; default: renderer.Render(block); break; } }
/// <summary> /// Constructs a DocComment instance from the documentation comments /// associated with a source code element. /// </summary> /// <param name="docComments">The doc comments from the source code</param> /// <param name="name">The name of the element</param> /// <param name="deprecated">Flag indicating whether or not the element had a Deprecated attribute</param> /// <param name="replacement">The name of the replacement element for deprecated elements, if given</param> public DocComment(IEnumerable <string> docComments, string name, bool deprecated, string replacement) { string GetHeadingText(HeadingBlock heading) { var sb = new StringBuilder(); foreach (var item in heading.Inline) { sb.Append(item.ToString()); } return(sb.ToString()); } string GetParagraphText(LeafBlock leaf) { var sb = new StringBuilder(); foreach (var item in leaf.Inline) { sb.Append(item.ToString()); } return(sb.ToString()); } string ToMarkdown(IEnumerable <Block> blocks) { var writer = new StringWriter(); var renderer = new NormalizeRenderer(writer); var pipeline = new MarkdownPipelineBuilder().Build(); pipeline.Setup(renderer); foreach (var block in blocks) { renderer.Render(block); } // We convert \n to \r because the YAML serialization will eventually // output \n\n for \n, but \r\n for \r. return(writer.ToString().TrimEnd().Replace('\n', '\r')); } List <ValueTuple <string, List <Block> > > BreakIntoSections(IEnumerable <Block> blocks, int level) { var key = ""; var accum = new List <Block>(); var result = new List <ValueTuple <string, List <Block> > >(); foreach (var block in blocks) { if (block is HeadingBlock heading) { if (heading.Level == level) { if (accum.Count > 0) { result.Add(new ValueTuple <string, List <Block> >(key, accum)); accum = new List <Block>(); } key = GetHeadingText(heading); } else { accum.Add(block); } } else { accum.Add(block); } } if (accum.Count > 0) { result.Add(new ValueTuple <string, List <Block> >(key, accum)); } return(result); } void ParseListSection(IEnumerable <Block> blocks, List <string> accum, bool lowerCase) { foreach (var block in blocks) { if (block is ListBlock list) { foreach (var sub in block.Descendants()) { if (sub is ListItemBlock item) { // Some special treatment for funky doc comments in some of the Canon if (item.Count == 1 && item.LastChild is LeafBlock leaf && leaf.Inline != null && leaf.Inline.FirstChild is LiteralInline literal) { var itemText = lowerCase ? GetParagraphText(leaf).ToLowerInvariant() : GetParagraphText(leaf); if (itemText.StartsWith("@\"") && itemText.EndsWith("\"")) { itemText = itemText.Substring(2, itemText.Length - 3); } literal.Content = new Markdig.Helpers.StringSlice(itemText.ToLowerInvariant()); } accum.Add(ToMarkdown(new Block[] { item })); } } } } } void ParseMapSection(IEnumerable <Block> blocks, Dictionary <string, string> accum) { var subsections = BreakIntoSections(blocks, 2); foreach ((var key, var subs) in subsections) { // TODO: when we add the ability to flag warnings from the doc comment builder, // we should check here for duplicate keys and generate a warning if appropriate. accum[key] = ToMarkdown(subs); } } // First element is not matching, second is matching (List <Block>, List <Block>) PartitionNestedSection(IEnumerable <Block> blocks, int level, string name) { var inMatch = false; var result = (new List <Block>(), new List <Block>()); foreach (var block in blocks) { var skip = false; if ((block is HeadingBlock heading) && (heading.Level == level)) { inMatch = GetHeadingText(heading).Equals(name); skip = true; } if (inMatch) { if (!skip) { result.Item2.Add(block); } } else { result.Item1.Add(block); } } return(result); } // Initialize to safe empty values this.Summary = ""; this.Description = ""; this.ShortSummary = ""; this.Documentation = ""; this.Input = new Dictionary <string, string>(); this.Output = ""; this.TypeParameters = new Dictionary <string, string>(); this.Example = ""; this.Remarks = ""; this.SeeAlso = new List <string>(); this.References = ""; var deprecationSummary = String.IsNullOrWhiteSpace(replacement) ? DiagnosticItem.Message(WarningCode.DeprecationWithoutRedirect, new string[] { name }) : DiagnosticItem.Message(WarningCode.DeprecationWithRedirect, new string[] { name, "@\"" + replacement + "\"" }); var deprecationDetails = ""; var text = String.Join("\n", docComments); // Only parse if there are comments to parse if (!string.IsNullOrWhiteSpace(text)) { var doc = Markdown.Parse(text); var sections = BreakIntoSections(doc, 1); List <Block> summarySection = new List <Block>(); List <Block> descriptionSection = new List <Block>(); foreach ((var tag, var section) in sections) { switch (tag) { case "Summary": this.Summary = ToMarkdown(section); summarySection.AddRange(section); // For now, the short hover information gets the first paragraph of the summary. this.ShortSummary = ToMarkdown(section.GetRange(0, 1)); break; case "Deprecated": if (String.IsNullOrWhiteSpace(name)) { deprecationSummary = ToMarkdown(section.GetRange(0, 1)); if (section.Count > 1) { deprecationDetails = ToMarkdown(section.GetRange(1, section.Count - 1)); } } else { deprecationDetails = ToMarkdown(section); } deprecated = true; break; case "Description": this.Description = ToMarkdown(section); descriptionSection = section; break; case "Input": ParseMapSection(section, this.Input); break; case "Output": this.Output = ToMarkdown(section); break; case "Type Parameters": ParseMapSection(section, this.TypeParameters); break; case "Example": this.Example = ToMarkdown(section); break; case "Remarks": (var remarks, var examples) = PartitionNestedSection(section, 2, "Example"); if ((examples.Count > 0) && (this.Example == "")) { this.Example = ToMarkdown(examples); } this.Remarks = ToMarkdown(remarks); break; case "See Also": // seeAlso is a list of UIDs, which are all lower case, // so pass true to lowercase all strings found in this section ParseListSection(section, this.SeeAlso, true); break; case "References": this.References = ToMarkdown(section); break; default: // TODO: add diagnostic warning about unknown tag break; } } this.Documentation = ToMarkdown(summarySection.Concat(descriptionSection)); } if (deprecated) { var shortDeprecationText = DeprecatedWarning + "\r" + deprecationSummary; var longDeprecationText = shortDeprecationText + (String.IsNullOrWhiteSpace(deprecationDetails) ? "" : "\r") + deprecationDetails; this.Summary += "\r" + longDeprecationText; this.ShortSummary = shortDeprecationText; this.Documentation = deprecationSummary; } }
internal static void WriteTo(this MarkdownObject md, TextWriter writer, NormalizeOptions?options = null) { var renderer = new NormalizeRenderer(writer, options); renderer.Render(md); }