diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 51fe43f..e8b97e5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,6 +1,6 @@ name: publish env: - VERSION: '0.2.3-preview' + VERSION: '0.3.0-preview' PRERELEASE: true on: push: diff --git a/Test/SerializationTests.cs b/Test/SerializationTests.cs index a4d5e54..0b48614 100644 --- a/Test/SerializationTests.cs +++ b/Test/SerializationTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using Tavenem.DataStorage; namespace Tavenem.Wiki.Test { @@ -100,6 +101,32 @@ public void CategoryTest() Assert.AreEqual(json, System.Text.Json.JsonSerializer.Serialize(deserialized)); } + [TestMethod] + public void MarkdownItemTest() + { + var value = new MarkdownItemTestSubclass( + "Test markdown", + "Test markdown", + "Test markdown", + new ReadOnlyCollection(new[] { new WikiLink(false, false, false, false, "Test Title", "Test Namespace") })); + + var json = System.Text.Json.JsonSerializer.Serialize(value); + Console.WriteLine(); + Console.WriteLine(json); + var deserialized = System.Text.Json.JsonSerializer.Deserialize(json); + Assert.AreEqual(value, deserialized); + Assert.AreEqual(json, System.Text.Json.JsonSerializer.Serialize(deserialized)); + + value = MarkdownItemTestSubclass.New(_Options, new InMemoryDataStore(), "Test markdown"); + + json = System.Text.Json.JsonSerializer.Serialize(value); + Console.WriteLine(); + Console.WriteLine(json); + deserialized = System.Text.Json.JsonSerializer.Deserialize(json); + Assert.AreEqual(value, deserialized); + Assert.AreEqual(json, System.Text.Json.JsonSerializer.Serialize(deserialized)); + } + [TestMethod] public void MessageTest() { diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index eeecafa..c08a20a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.3.0-preview +### Changed +- Changed MarkdownItem constructor, method visibility and property attributes to facilitate + subclassing. +### Updated +- Updated Markdig dependency. + ## 0.2.3-preview ### Fixed - Fixed editor parameter signature in creation callback. diff --git a/src/Converters/ArticleConverter.cs b/src/Converters/ArticleConverter.cs index 6f144be..0d6d4f1 100644 --- a/src/Converters/ArticleConverter.cs +++ b/src/Converters/ArticleConverter.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using Tavenem.DataStorage; @@ -17,8 +16,7 @@ public class ArticleConverter : JsonConverter
/// The type to convert. /// An object that specifies serialization options to use. /// The converted value. - [return: MaybeNull] - public override Article Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override Article? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var initialReader = reader; diff --git a/src/MarkdownExtensions/TableOfContents/TableOfContentsExtension.cs b/src/MarkdownExtensions/TableOfContents/TableOfContentsExtension.cs index 6ed07f8..c35a206 100644 --- a/src/MarkdownExtensions/TableOfContents/TableOfContentsExtension.cs +++ b/src/MarkdownExtensions/TableOfContents/TableOfContentsExtension.cs @@ -2,6 +2,7 @@ using Markdig.Parsers; using Markdig.Renderers; using Markdig.Syntax; +using System.Collections.Generic; using System.Linq; namespace Tavenem.Wiki.MarkdownExtensions.TableOfContents @@ -130,19 +131,23 @@ private void PipelineOnDocumentProcessed(MarkdownDocument document) foreach (var toC in toCs.Where(x => !x.IsNoToc)) { - var levelOffset = toC.Parent.Descendants() - .Where(x => x.Line < toC.Line) - .OrderByDescending(x => x.Line) - .FirstOrDefault()?.Level ?? 0; + var levelOffset = toC.Parent is null + ? 0 + : toC.Parent.Descendants() + .Where(x => x.Line < toC.Line) + .OrderByDescending(x => x.Line) + .FirstOrDefault()?.Level ?? 0; toC.LevelOffset = levelOffset; - var headings = toC.Parent.Descendants() - .Where(x => x.Line > toC.Line) - .OrderBy(x => x.Line) - .TakeWhile(x => x.Level > levelOffset) - .Where(x => x.Level >= levelOffset + toC.StartingLevel - && x.Level < levelOffset + toC.StartingLevel + toC.Depth) - .ToList(); + var headings = toC.Parent is null + ? new List() + : toC.Parent.Descendants() + .Where(x => x.Line > toC.Line) + .OrderBy(x => x.Line) + .TakeWhile(x => x.Level > levelOffset) + .Where(x => x.Level >= levelOffset + toC.StartingLevel + && x.Level < levelOffset + toC.StartingLevel + toC.Depth) + .ToList(); if (!toC.IsDefault || headings.Count >= Options.MinimumTopLevel) { diff --git a/src/MarkdownExtensions/TableOfContents/TableOfContentsRenderer.cs b/src/MarkdownExtensions/TableOfContents/TableOfContentsRenderer.cs index 63ce1d3..eba5c64 100644 --- a/src/MarkdownExtensions/TableOfContents/TableOfContentsRenderer.cs +++ b/src/MarkdownExtensions/TableOfContents/TableOfContentsRenderer.cs @@ -101,10 +101,15 @@ protected override void Write(HtmlRenderer renderer, TableOfContentsBlock block) } string headingText; - using (var sw = new StringWriter()) + if (headings[i].Inline is null) { + headingText = string.Empty; + } + else + { + using var sw = new StringWriter(); var stripRenderer = new HtmlRenderer(sw); - stripRenderer.Render(headings[i].Inline); + stripRenderer.Render(headings[i].Inline!); headingText = stripRenderer.Writer.ToString() ?? string.Empty; } diff --git a/src/MarkdownExtensions/WikiLinks/WikiLinkInlineParser.cs b/src/MarkdownExtensions/WikiLinks/WikiLinkInlineParser.cs index 05c2601..c4c3ac3 100644 --- a/src/MarkdownExtensions/WikiLinks/WikiLinkInlineParser.cs +++ b/src/MarkdownExtensions/WikiLinks/WikiLinkInlineParser.cs @@ -347,7 +347,7 @@ and not SeparatorChar private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice text) { - var openParent = inlineState.Inline.FindParentOfType().FirstOrDefault(); + var openParent = inlineState.Inline?.FindParentOfType().FirstOrDefault(); if (openParent is null) { return false; diff --git a/src/MarkdownExtensions/WikiLinks/WikiLinkInlineRenderer.cs b/src/MarkdownExtensions/WikiLinks/WikiLinkInlineRenderer.cs index d513c3c..f730e3a 100644 --- a/src/MarkdownExtensions/WikiLinks/WikiLinkInlineRenderer.cs +++ b/src/MarkdownExtensions/WikiLinks/WikiLinkInlineRenderer.cs @@ -33,8 +33,8 @@ protected override void Write(HtmlRenderer renderer, WikiLinkInline link) } var fullTitle = !link.IsCommons && !link.IsWikipedia - && (link.Title.Length == 0 || link.Title[0] != '#') - ? Article.GetFullTitle(Options, link.Title, link.WikiNamespace, link.IsTalk) + && (string.IsNullOrEmpty(link.Title) || link.Title[0] != '#') + ? Article.GetFullTitle(Options, link.Title ?? string.Empty, link.WikiNamespace, link.IsTalk) : link.Title; if (renderer.EnableHtmlForInline) @@ -53,7 +53,7 @@ protected override void Write(HtmlRenderer renderer, WikiLinkInline link) { renderer.Write(" 0 && link.Title[0] == '#') + else if (!string.IsNullOrEmpty(link.Title) && link.Title[0] == '#') { renderer.Write(" x.Key.Equals("height", StringComparison.OrdinalIgnoreCase)) ?? -1; + var properties = link.GetAttributes()?.Properties; + var heightIndex = properties?.FindIndex(x => x.Key.Equals("height", StringComparison.OrdinalIgnoreCase)) ?? -1; if (heightIndex != -1) { - if (int.TryParse(link.GetAttributes().Properties[heightIndex].Value, out var heightInt)) + if (int.TryParse(properties![heightIndex].Value, out var heightInt)) { renderer.Write("height=\""); renderer.Write(heightInt.ToString()); renderer.Write("\""); } - else if (double.TryParse(link.GetAttributes().Properties[heightIndex].Value, out var heightFloat)) + else if (double.TryParse(properties[heightIndex].Value, out var heightFloat)) { renderer.Write("height=\""); renderer.Write(heightFloat.ToString()); @@ -127,16 +128,16 @@ protected override void Write(HtmlRenderer renderer, WikiLinkInline link) } } - var widthIndex = link.GetAttributes()?.Properties?.FindIndex(x => x.Key.Equals("width", StringComparison.OrdinalIgnoreCase)) ?? -1; + var widthIndex = properties?.FindIndex(x => x.Key.Equals("width", StringComparison.OrdinalIgnoreCase)) ?? -1; if (widthIndex != -1) { - if (int.TryParse(link.GetAttributes().Properties[widthIndex].Value, out var widthInt)) + if (int.TryParse(properties![widthIndex].Value, out var widthInt)) { renderer.Write("width=\""); renderer.Write(widthInt.ToString()); renderer.Write("\""); } - else if (double.TryParse(link.GetAttributes().Properties[widthIndex].Value, out var widthFloat)) + else if (double.TryParse(properties[widthIndex].Value, out var widthFloat)) { renderer.Write("width=\""); renderer.Write(widthFloat.ToString()); diff --git a/src/MarkdownItem.cs b/src/MarkdownItem.cs index 4e71a3b..75b1264 100644 --- a/src/MarkdownItem.cs +++ b/src/MarkdownItem.cs @@ -12,6 +12,7 @@ using System.IO; using System.Linq; using System.Runtime.Serialization; +using System.Text.Json.Serialization; using Tavenem.DataStorage; using Tavenem.DiffPatchMerge; using Tavenem.Wiki.MarkdownExtensions.TableOfContents; @@ -41,23 +42,55 @@ public abstract class MarkdownItem : IdItem, ISerializable /// /// The rendered HTML content. /// - public string Html { get; private protected set; } = null!; // Always initialized during ctor, but in one instance by the subclass. + [JsonInclude] + public string Html { get; private protected set; } /// /// The markdown content. /// + [JsonInclude] public string MarkdownContent { get; private protected set; } /// /// A preview of this item's rendered HTML. /// - public string Preview { get; private protected set; } = null!; // Always initialized during ctor, but in one instance by the subclass. + [JsonInclude] + public string Preview { get; private protected set; } /// /// The wiki links within this content. /// + [JsonInclude] public IReadOnlyCollection WikiLinks { get; private protected set; } = new List().AsReadOnly(); + /// + /// Initializes a new instance of . + /// + /// + /// Note: this constructor is most useful for deserializers. + /// + protected MarkdownItem() + { + Html = string.Empty; + MarkdownContent = string.Empty; + Preview = string.Empty; + WikiLinks = new List().AsReadOnly(); + } + + /// + /// Initializes a new instance of . + /// + /// + /// Note: this constructor is most useful for deserializers. + /// + protected MarkdownItem(string id) : base(id) + { + Html = string.Empty; + MarkdownContent = string.Empty; + Preview = string.Empty; + WikiLinks = new List().AsReadOnly(); + } + /// /// Initializes a new instance of . /// @@ -69,7 +102,7 @@ public abstract class MarkdownItem : IdItem, ISerializable /// /// Note: this constructor is most useful for deserializers. /// - private protected MarkdownItem(string id, string? markdownContent, string html, string preview, IReadOnlyCollection wikiLinks) : base(id) + protected MarkdownItem(string id, string? markdownContent, string html, string preview, IReadOnlyCollection wikiLinks) : base(id) { Html = html; MarkdownContent = markdownContent ?? string.Empty; @@ -88,7 +121,7 @@ private protected MarkdownItem(string id, string? markdownContent, string html, /// A preview of this item's rendered HTML. /// /// The included objects. - private protected MarkdownItem(string? markdown, string? html, string? preview, IReadOnlyCollection wikiLinks) + protected MarkdownItem(string? markdown, string? html, string? preview, IReadOnlyCollection wikiLinks) { MarkdownContent = markdown ?? string.Empty; Html = html ?? string.Empty; @@ -378,6 +411,53 @@ public string GetPlainText( public string GetPreview(IWikiOptions options, IDataStore dataStore) => RenderPreview(options, dataStore, PostprocessMarkdown(options, dataStore, MarkdownContent, isPreview: true)); + /// + /// Identifies the s in the given . + /// + /// An instance. + /// An instance. + /// The markdown. + /// The title of the item. + /// The namespace of the item. + /// + /// A of s. + /// + protected static List GetWikiLinks( + IWikiOptions options, + IDataStore dataStore, + string? markdown, + string? title = null, + string? wikiNamespace = null) + => string.IsNullOrEmpty(markdown) + ? new List() + : Markdown.Parse(markdown, WikiConfig.GetMarkdownPipeline(options, dataStore)) + .Descendants() + .Where(x => !x.IsWikipedia + && !x.IsCommons + && (string.IsNullOrEmpty(x.Title) + || x.Title.Length < 5 + || ((x.Title[0] != TransclusionParser.TransclusionOpenChar + || x.Title[1] != TransclusionParser.TransclusionOpenChar + || x.Title[^1] != TransclusionParser.TransclusionCloseChar + || x.Title[^2] != TransclusionParser.TransclusionCloseChar) + && (x.Title[0] != TransclusionParser.ParameterOpenChar + || x.Title[1] != TransclusionParser.ParameterOpenChar + || x.Title[^1] != TransclusionParser.ParameterCloseChar + || x.Title[^2] != TransclusionParser.ParameterCloseChar)))) + .Select(x => + { + var anchorIndex = x.Title?.LastIndexOf('#') ?? -1; + return new WikiLink( + x.Article, + x.Missing && (x.Title != title || x.WikiNamespace != wikiNamespace), + x.IsCategory, + x.IsNamespaceEscaped, + x.IsTalk, + anchorIndex == -1 ? x.Title ?? string.Empty : x.Title![..anchorIndex], + x.WikiNamespace ?? options.DefaultNamespace); + }) + .ToList(); + private static bool AnyPreviews(MarkdownObject obj) { if (obj is ContainerBlock containerBlock) @@ -420,41 +500,6 @@ private static bool AnyPreviews(MarkdownObject obj) return false; } - private protected static List GetWikiLinks( - IWikiOptions options, - IDataStore dataStore, - string? markdown, - string? title = null, - string? wikiNamespace = null) - => string.IsNullOrEmpty(markdown) - ? new List() - : Markdown.Parse(markdown, WikiConfig.GetMarkdownPipeline(options, dataStore)) - .Descendants() - .Where(x => !x.IsWikipedia - && !x.IsCommons - && (x.Title.Length < 5 - || ((x.Title[0] != TransclusionParser.TransclusionOpenChar - || x.Title[1] != TransclusionParser.TransclusionOpenChar - || x.Title[^1] != TransclusionParser.TransclusionCloseChar - || x.Title[^2] != TransclusionParser.TransclusionCloseChar) - && (x.Title[0] != TransclusionParser.ParameterOpenChar - || x.Title[1] != TransclusionParser.ParameterOpenChar - || x.Title[^1] != TransclusionParser.ParameterCloseChar - || x.Title[^2] != TransclusionParser.ParameterCloseChar)))) - .Select(x => - { - var anchorIndex = x.Title.LastIndexOf('#'); - return new WikiLink( - x.Article, - x.Missing && (x.Title != title || x.WikiNamespace != wikiNamespace), - x.IsCategory, - x.IsNamespaceEscaped, - x.IsTalk, - anchorIndex == -1 ? x.Title : x.Title[..anchorIndex], - x.WikiNamespace ?? options.DefaultNamespace); - }) - .ToList(); - private static void Trim(MarkdownObject obj, ref int minCharactersAvailable, ref int maxCharactersAvailable) { if (obj is ContainerBlock containerBlock) diff --git a/src/Tavenem.Wiki.csproj b/src/Tavenem.Wiki.csproj index 35cf324..7fb9fed 100644 --- a/src/Tavenem.Wiki.csproj +++ b/src/Tavenem.Wiki.csproj @@ -13,7 +13,7 @@ Tavenem.Wiki - 0.1.0-preview + 1.0.0 Wil Stead A .NET wiki library. Copyright © 2019-2021 Wil Stead @@ -35,9 +35,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/test/MarkdownItemTestSubclass.cs b/test/MarkdownItemTestSubclass.cs new file mode 100644 index 0000000..ba40e94 --- /dev/null +++ b/test/MarkdownItemTestSubclass.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using Tavenem.DataStorage; +using Tavenem.Wiki.MarkdownExtensions.Transclusions; + +namespace Tavenem.Wiki.Test +{ + /// + /// Test subclass of . + /// + public class MarkdownItemTestSubclass : MarkdownItem + { + /// + /// Initializes a new instance of . + /// + public MarkdownItemTestSubclass() { } + + /// + /// Initializes a new instance of . + /// + public MarkdownItemTestSubclass(string id) : base(id) { } + + /// + /// Initializes a new instance of . + /// + /// The item's . + /// The raw markdown. + /// The rendered HTML content. + /// A preview of this item's rendered HTML. + /// The included objects. + /// + /// Note: this constructor is most useful for deserializers. + /// + public MarkdownItemTestSubclass(string id, string? markdownContent, string html, string preview, IReadOnlyCollection wikiLinks) + : base(id, markdownContent, html, preview, wikiLinks) + { } + + /// + /// Initializes a new instance of . + /// + /// The raw markdown. + /// + /// The rendered HTML content. + /// + /// + /// A preview of this item's rendered HTML. + /// + /// The included objects. + public MarkdownItemTestSubclass(string? markdown, string? html, string? preview, IReadOnlyCollection wikiLinks) + : base(markdown, html, preview, wikiLinks) + { } + + public static MarkdownItemTestSubclass New(IWikiOptions options, IDataStore dataStore, string? markdown) + { + var md = string.IsNullOrEmpty(markdown) + ? null + : TransclusionParser.Transclude( + options, + dataStore, + null, + null, + markdown, + out _); + var wikiLinks = GetWikiLinks(options, dataStore, md); + return new MarkdownItemTestSubclass( + md, + RenderHtml(options, dataStore, md), + RenderPreview( + options, + dataStore, + string.IsNullOrEmpty(markdown) + ? string.Empty + : TransclusionParser.Transclude( + options, + dataStore, + null, + null, + markdown, + out _, + isPreview: true)), + wikiLinks); + } + } +} diff --git a/test/Tavenem.Wiki.Test.csproj b/test/Tavenem.Wiki.Test.csproj index a1be2d0..47f4b6a 100644 --- a/test/Tavenem.Wiki.Test.csproj +++ b/test/Tavenem.Wiki.Test.csproj @@ -8,10 +8,13 @@ - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive +