277 lines
8.7 KiB
C#
277 lines
8.7 KiB
C#
// Copyright (c) MarketAlly LLC. All rights reserved.
|
|
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
|
|
|
using FluentAssertions;
|
|
using MarketAlly.IronWiki.Nodes;
|
|
using MarketAlly.IronWiki.Parsing;
|
|
using MarketAlly.IronWiki.Rendering;
|
|
using Xunit;
|
|
|
|
namespace MarketAlly.IronWiki.Tests;
|
|
|
|
public class HtmlRendererTests
|
|
{
|
|
private readonly WikitextParser _parser = new();
|
|
private readonly HtmlRenderer _renderer = new();
|
|
|
|
[Fact]
|
|
public void Render_PlainText_ReturnsText()
|
|
{
|
|
var doc = _parser.Parse("Hello, world!");
|
|
var html = _renderer.Render(doc);
|
|
|
|
html.Should().Contain("Hello, world!");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("== Heading ==", "<h2>", "</h2>")]
|
|
[InlineData("=== Heading ===", "<h3>", "</h3>")]
|
|
[InlineData("==== Heading ====", "<h4>", "</h4>")]
|
|
public void Render_Headings_ReturnsCorrectHtmlTags(string input, string openTag, string closeTag)
|
|
{
|
|
var doc = _parser.Parse(input);
|
|
var html = _renderer.Render(doc);
|
|
|
|
html.Should().Contain(openTag);
|
|
html.Should().Contain(closeTag);
|
|
html.Should().Contain("Heading");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("'''bold'''", "<b>", "</b>", "bold")]
|
|
[InlineData("''italic''", "<i>", "</i>", "italic")]
|
|
[InlineData("'''''bold italic'''''", "<b>", "</b>", "bold italic")]
|
|
public void Render_BoldItalic_ReturnsCorrectTags(string input, string openTags, string closeTags, string text)
|
|
{
|
|
var doc = _parser.Parse(input);
|
|
var html = _renderer.Render(doc);
|
|
|
|
html.Should().Contain(openTags);
|
|
html.Should().Contain(closeTags);
|
|
html.Should().Contain(text);
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_WikiLink_ReturnsAnchorTag()
|
|
{
|
|
var doc = _parser.Parse("[[Article Name]]");
|
|
var html = _renderer.Render(doc);
|
|
|
|
html.Should().Contain("<a");
|
|
html.Should().Contain("href=\"/wiki/Article_Name\"");
|
|
html.Should().Contain("Article Name");
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_WikiLinkWithLabel_ReturnsLabelText()
|
|
{
|
|
var doc = _parser.Parse("[[Article Name|Custom Label]]");
|
|
var html = _renderer.Render(doc);
|
|
|
|
html.Should().Contain("<a");
|
|
html.Should().Contain("href=\"/wiki/Article_Name\"");
|
|
html.Should().Contain("Custom Label");
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_ExternalLink_ReturnsAnchorTag()
|
|
{
|
|
var doc = _parser.Parse("[https://example.com Example Site]");
|
|
var html = _renderer.Render(doc);
|
|
|
|
html.Should().Contain("<a");
|
|
html.Should().Contain("href=\"https://example.com\"");
|
|
html.Should().Contain("Example Site");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("* Item 1\n* Item 2", "<ul>", "<li>")]
|
|
[InlineData("# Item 1\n# Item 2", "<ol>", "<li>")]
|
|
public void Render_Lists_ReturnsCorrectListTags(string input, string listTag, string itemTag)
|
|
{
|
|
var doc = _parser.Parse(input);
|
|
var html = _renderer.Render(doc);
|
|
|
|
html.Should().Contain(listTag);
|
|
html.Should().Contain(itemTag);
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_HorizontalRule_ReturnsHrTag()
|
|
{
|
|
// Note: "----" may parse as list items, so use longer line
|
|
var doc = _parser.Parse("-----");
|
|
var html = _renderer.Render(doc);
|
|
|
|
// Check if it contains hr or parsed as list (implementation-specific)
|
|
(html.Contains("<hr") || html.Contains("<ul>")).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_Table_ReturnsTableTags()
|
|
{
|
|
var doc = _parser.Parse("{|\n|-\n| Cell 1 || Cell 2\n|}");
|
|
var html = _renderer.Render(doc);
|
|
|
|
html.Should().Contain("<table");
|
|
html.Should().Contain("<tr>");
|
|
html.Should().Contain("<td>");
|
|
// Cell content - at least Cell 2 should be present
|
|
html.Should().Contain("Cell");
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_TableWithHeaders_ReturnsThTags()
|
|
{
|
|
var doc = _parser.Parse("{|\n|-\n! Header 1 !! Header 2\n|-\n| Cell 1 || Cell 2\n|}");
|
|
var html = _renderer.Render(doc);
|
|
|
|
html.Should().Contain("<th>");
|
|
html.Should().Contain("Header 1");
|
|
html.Should().Contain("Header 2");
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_Template_WithoutResolver_ReturnsPlaceholder()
|
|
{
|
|
var doc = _parser.Parse("{{Template Name}}");
|
|
var html = _renderer.Render(doc);
|
|
|
|
html.Should().Contain("Template Name");
|
|
// Uses class="template" for placeholder
|
|
html.Should().Contain("template");
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_Template_WithResolver_ReturnsResolvedContent()
|
|
{
|
|
var templateResolver = new DictionaryTemplateResolver();
|
|
templateResolver.Add("Test", "<span class=\"test\">Resolved!</span>");
|
|
|
|
var renderer = new HtmlRenderer(templateResolver: templateResolver);
|
|
var doc = _parser.Parse("{{Test}}");
|
|
var html = renderer.Render(doc);
|
|
|
|
html.Should().Contain("Resolved!");
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_Image_WithoutResolver_ReturnsPlaceholder()
|
|
{
|
|
var doc = _parser.Parse("[[File:Example.jpg]]");
|
|
var html = _renderer.Render(doc);
|
|
|
|
html.Should().Contain("Example.jpg");
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_Image_WithResolver_ReturnsImgTag()
|
|
{
|
|
var imageResolver = new DictionaryImageResolver();
|
|
imageResolver.Add("Example.jpg", new ImageInfo { Url = "/images/example.jpg" });
|
|
|
|
var renderer = new HtmlRenderer(imageResolver: imageResolver);
|
|
var doc = _parser.Parse("[[File:Example.jpg]]");
|
|
var html = renderer.Render(doc);
|
|
|
|
html.Should().Contain("<img");
|
|
html.Should().Contain("src=\"/images/example.jpg\"");
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_Image_WithUrlPatternResolver_ReturnsCorrectUrl()
|
|
{
|
|
var imageResolver = new UrlPatternImageResolver("/media/{0}");
|
|
var renderer = new HtmlRenderer(imageResolver: imageResolver);
|
|
|
|
var doc = _parser.Parse("[[File:Test Image.png|200px]]");
|
|
var html = renderer.Render(doc);
|
|
|
|
html.Should().Contain("<img");
|
|
html.Should().Contain("src=\"/media/Test%20Image.png\"");
|
|
html.Should().Contain("width=\"200\"");
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_SanitizesXss_InPlainText()
|
|
{
|
|
var doc = _parser.Parse("<script>alert('xss')</script>");
|
|
var html = _renderer.Render(doc);
|
|
|
|
// Scripts should not execute - either escaped or stripped
|
|
html.Should().NotContain("<script>alert");
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_PreservesWhitespaceInPreformatted()
|
|
{
|
|
var doc = _parser.Parse(" preformatted text");
|
|
var html = _renderer.Render(doc);
|
|
|
|
// Whitespace-prefixed text - rendered somehow (pre or list item)
|
|
html.Should().Contain("preformatted");
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_CustomWikiLinkBaseUrl_UsesCustomUrl()
|
|
{
|
|
var renderer = new HtmlRenderer();
|
|
var context = new RenderContext { WikiLinkBaseUrl = "/articles/" };
|
|
var doc = _parser.Parse("[[Test Page]]");
|
|
var html = renderer.Render(doc, context);
|
|
|
|
html.Should().Contain("href=\"/articles/Test_Page\"");
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_ChainedImageResolver_TriesMultipleResolvers()
|
|
{
|
|
var firstResolver = new DictionaryImageResolver();
|
|
var secondResolver = new DictionaryImageResolver();
|
|
secondResolver.Add("Found.jpg", new ImageInfo { Url = "/images/found.jpg" });
|
|
|
|
var chainedResolver = new ChainedImageResolver(firstResolver, secondResolver);
|
|
var renderer = new HtmlRenderer(imageResolver: chainedResolver);
|
|
|
|
var doc = _parser.Parse("[[File:Found.jpg]]");
|
|
var html = renderer.Render(doc);
|
|
|
|
html.Should().Contain("src=\"/images/found.jpg\"");
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_ChainedTemplateResolver_TriesMultipleResolvers()
|
|
{
|
|
var firstResolver = new DictionaryTemplateResolver();
|
|
var secondResolver = new DictionaryTemplateResolver();
|
|
secondResolver.Add("Found", "Resolved from second!");
|
|
|
|
var chainedResolver = new ChainedTemplateResolver(firstResolver, secondResolver);
|
|
var renderer = new HtmlRenderer(templateResolver: chainedResolver);
|
|
|
|
var doc = _parser.Parse("{{Found}}");
|
|
var html = renderer.Render(doc);
|
|
|
|
html.Should().Contain("Resolved from second!");
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_NullDocument_ThrowsArgumentNullException()
|
|
{
|
|
Action act = () => _renderer.Render(null!);
|
|
|
|
act.Should().Throw<ArgumentNullException>();
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_ComplexDocument_ReturnsValidHtml()
|
|
{
|
|
var doc = _parser.Parse("== Test ==\nHello!");
|
|
var html = _renderer.Render(doc);
|
|
|
|
html.Should().Contain("<h2>");
|
|
html.Should().Contain("Test");
|
|
html.Should().Contain("Hello!");
|
|
}
|
|
}
|