|
| 1 | +using XafLogicExplainer.Core.Generators; |
| 2 | +using XafLogicExplainer.Core.Models; |
| 3 | + |
| 4 | +namespace XafLogicExplainer.Tests; |
| 5 | + |
| 6 | +/// <summary> |
| 7 | +/// That the Markdown we generate is Markdown, and not Markdown plus HTML that only a browser opens. |
| 8 | +/// </summary> |
| 9 | +/// <remarks> |
| 10 | +/// The seed section wrapped a method's source in a <c><details></c> fold. On GitHub that |
| 11 | +/// collapses; anywhere else the wrapper is literal text and the fold's label — "Source code of |
| 12 | +/// PopulateStatuses" — stops being a label and becomes a line of markup. "Anywhere else" is most |
| 13 | +/// places these files go: a Word or PDF export, a plain Markdown viewer, and a language model, for |
| 14 | +/// which the fold is tokens spent on something it cannot open. |
| 15 | +/// <para> |
| 16 | +/// Found by walking our output against the Markdig-based converter in |
| 17 | +/// <see href="https://github.com/MBrekhof/mcpOffice">mcpOffice</see>, whose documented behaviour for |
| 18 | +/// an HTML block is to emit it as a plain text paragraph. Every other construct we write — headings, |
| 19 | +/// pipe tables, fenced code, lists, bold, inline code — maps to a real Word equivalent, so this one |
| 20 | +/// call site was the whole difference between an extraction and a document someone can hand over. |
| 21 | +/// </para> |
| 22 | +/// </remarks> |
| 23 | +public class PortableMarkdownTests |
| 24 | +{ |
| 25 | + private static readonly (string Name, ExtractedProject Project)[] Samples = |
| 26 | + [ |
| 27 | + ("Xpo", SampleProjects.Xpo), |
| 28 | + ("EfCore", SampleProjects.EfCore), |
| 29 | + ("LegacyEf", SampleProjects.LegacyEf), |
| 30 | + ("PocoEf", SampleProjects.PocoEf), |
| 31 | + ("NoOrm", SampleProjects.NoOrm), |
| 32 | + ("DeepXpo", SampleProjects.DeepXpo), |
| 33 | + ("AuditedXpo", SampleProjects.AuditedXpo), |
| 34 | + ("Demo", SampleProjects.Demo), |
| 35 | + ]; |
| 36 | + |
| 37 | + private static string Markdown(ExtractedProject project, string language) => |
| 38 | + string.Join("\n", new MarkdownDocumentationGenerator(language) |
| 39 | + .GenerateSections(project) |
| 40 | + .Select(section => section.Content)) |
| 41 | + .Replace("\r", ""); |
| 42 | + |
| 43 | + /// <summary> |
| 44 | + /// Lines that begin a CommonMark HTML block: outside a fence, a line whose first character is |
| 45 | + /// <c><</c>. Inside a fence the same line is source code and is left alone, which is why this |
| 46 | + /// tracks the fence rather than matching the whole document at once. |
| 47 | + /// </summary> |
| 48 | + private static List<string> RawHtmlLines(string markdown) |
| 49 | + { |
| 50 | + var offenders = new List<string>(); |
| 51 | + var insideFence = false; |
| 52 | + |
| 53 | + foreach (var line in markdown.Split('\n')) |
| 54 | + { |
| 55 | + if (line.TrimStart().StartsWith("```", StringComparison.Ordinal)) |
| 56 | + { |
| 57 | + insideFence = !insideFence; |
| 58 | + continue; |
| 59 | + } |
| 60 | + |
| 61 | + if (!insideFence && line.TrimStart().StartsWith('<')) |
| 62 | + offenders.Add(line.Trim()); |
| 63 | + } |
| 64 | + |
| 65 | + return offenders; |
| 66 | + } |
| 67 | + |
| 68 | + [Theory] |
| 69 | + [InlineData("en")] |
| 70 | + [InlineData("es")] |
| 71 | + public void NoGeneratedPageOpensALineWithRawHtml(string language) |
| 72 | + { |
| 73 | + foreach (var (name, project) in Samples) |
| 74 | + { |
| 75 | + var offenders = RawHtmlLines(Markdown(project, language)); |
| 76 | + |
| 77 | + Assert.True(offenders.Count == 0, |
| 78 | + $"{name} ({language}) emits raw HTML outside a code fence, which renders as literal " |
| 79 | + + $"text everywhere but a browser: {string.Join(" | ", offenders)}"); |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + [Fact] |
| 84 | + public void TheSeedSourceIsIntroducedByAHeadingRatherThanAFold() |
| 85 | + { |
| 86 | + // A heading survives the trip and keeps its place in the document outline; the fold did |
| 87 | + // neither. The fixture that carries this is the XPO sample, whose updater has a body. |
| 88 | + var english = Markdown(SampleProjects.Xpo, "en"); |
| 89 | + |
| 90 | + Assert.Contains("#### Source code of ", english, StringComparison.Ordinal); |
| 91 | + Assert.Contains("#### Codigo fuente de ", Markdown(SampleProjects.Xpo, "es"), StringComparison.Ordinal); |
| 92 | + |
| 93 | + // The label is what was lost: inside <summary> it was markup, and the reader met angle |
| 94 | + // brackets where a title belonged. |
| 95 | + Assert.DoesNotContain("<summary>", english, StringComparison.Ordinal); |
| 96 | + } |
| 97 | +} |
0 commit comments