Skip to content

Commit 04bb4f3

Browse files
committed
Switch approach to fetch headers at first and create a separate markdown content.
1 parent 9ae8294 commit 04bb4f3

File tree

5 files changed

+173
-38
lines changed

5 files changed

+173
-38
lines changed

AzureDevOps.WikiPDFExport.Test/AzureDevOps.WikiPDFExport.Test.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@
1212
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
1313
</ItemGroup>
1414

15+
<ItemGroup>
16+
<ProjectReference Include="..\AzureDevOps.WikiPDFExport\azuredevops-export-wiki.csproj" />
17+
</ItemGroup>
18+
1519
</Project>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using azuredevops_export_wiki;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Xunit;
5+
6+
namespace AzureDevOps.WikiPDFExport
7+
{
8+
public class TableOfContent_uTest
9+
{
10+
[Fact]
11+
public void CreateGlobalTableOfContent_ShouldReturnTOCandSingleHeaderLine()
12+
{
13+
// Arrange
14+
var wikiPDFExporter = new WikiPDFExporter(new Options());
15+
var mdContent1 = "\n# SomeHeader\n"
16+
+ "SomeText";
17+
18+
// Act
19+
var result = wikiPDFExporter.CreateGlobalTableOfContent(new List<string> { mdContent1 });
20+
21+
Assert.Equal("[TOC]", result[0]);
22+
Assert.Equal("# SomeHeader", result[1]);
23+
}
24+
25+
[Fact]
26+
public void CreateGlobalTableOfContent_ShouldNotReturnTOC_WhenNoHeaderFound()
27+
{
28+
// Arrange
29+
var wikiPDFExporter = new WikiPDFExporter(new Options());
30+
var mdContent1 = "\nOnly boring text\n"
31+
+ "No header here";
32+
33+
// Act
34+
var result = wikiPDFExporter.CreateGlobalTableOfContent(new List<string> { mdContent1 });
35+
36+
Assert.False(result.Any());
37+
}
38+
39+
[Fact]
40+
public void CreateGlobalTableOfContent_ShouldReturnTOCandMultipleHeaderLines()
41+
{
42+
// Arrange
43+
var wikiPDFExporter = new WikiPDFExporter(new Options());
44+
var mdContent1 = "\n# SomeHeader\n"
45+
+ "SomeText";
46+
var mdContent2 = " ## SomeOtherHeader \n"
47+
+ " []() #Some very interesting text in wrong header format #";
48+
49+
// Act
50+
var result = wikiPDFExporter.CreateGlobalTableOfContent(new List<string> { mdContent1, mdContent2 });
51+
52+
Assert.Equal("[TOC]", result[0]);
53+
Assert.Equal("# SomeHeader", result[1]);
54+
Assert.Equal("## SomeOtherHeader", result[2]);
55+
}
56+
57+
[Fact]
58+
public void RemoveDuplicatedHeadersFromGlobalTOC()
59+
{
60+
// Arrange
61+
var wikiPDFExporter = new WikiPDFExporter(new Options());
62+
var htmlContent = "<h1>SomeHeader</h1>\n"
63+
+ "<h2>SomeOtherHeader</h2>\n";
64+
65+
// Act
66+
var result = wikiPDFExporter.RemoveDuplicatedHeadersFromGlobalTOC(htmlContent);
67+
68+
Assert.Equal("", result);
69+
}
70+
71+
[Fact]
72+
public void RemoveDuplicatedHeadersFromGlobalTOC_WhenIdsDefined()
73+
{
74+
// Arrange
75+
var wikiPDFExporter = new WikiPDFExporter(new Options());
76+
var htmlContent = "<h1 id='interestingID'>SomeHeader</h1>\n"
77+
+ "<h2>SomeOtherHeader</h2>\n";
78+
79+
// Act
80+
var result = wikiPDFExporter.RemoveDuplicatedHeadersFromGlobalTOC(htmlContent);
81+
82+
Assert.Equal("", result);
83+
}
84+
85+
[Fact]
86+
public void RemoveDuplicatedHeadersFromGlobalTOC_ExceptNavTag()
87+
{
88+
// Arrange
89+
var wikiPDFExporter = new WikiPDFExporter(new Options());
90+
var nav = "<nav>Some cool nav content</nav>\n";
91+
var htmlContent = nav
92+
+ "<h1>SomeHeader</h1>\n"
93+
+ "<h2>SomeOtherHeader</h2>\n";
94+
95+
// Act
96+
var result = wikiPDFExporter.RemoveDuplicatedHeadersFromGlobalTOC(htmlContent);
97+
98+
Assert.Equal(nav.Trim('\n'), result);
99+
}
100+
}
101+
}

AzureDevOps.WikiPDFExport/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public class Options
101101
[Option("organization", Required = false, HelpText = "Azure Devops organization URL used to convert work item references to work item links. Ex: https://dev.azure.com/MyOrganizationName/")]
102102
public string AzureDevopsOrganization { get; set; }
103103

104-
[Option("singletoc", Required = false, HelpText = "Creates a single table of content for all markdown files (handles all markdown files as a single one)")]
105-
public bool SingleTOC { get; set; }
104+
[Option("globaltoc", Required = false, HelpText = "Title for a global table of content for all markdown files. When not specified each markdown creates its own toc if defined")]
105+
public string GlobalTOC { get; set; }
106106
}
107107
}

AzureDevOps.WikiPDFExport/WikiPDFExporter.cs

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030

3131
using Microsoft.VisualStudio.Services.WebApi;
3232
using Process = System.Diagnostics.Process;
33+
using System.Runtime.CompilerServices;
34+
35+
[assembly:InternalsVisibleTo("AzureDevOps.WikiPDFExport.Test")]
3336

3437
namespace azuredevops_export_wiki
3538
{
@@ -100,7 +103,6 @@ public WikiPDFExporter(Options options)
100103
{"icon_pull_request", "bowtie-tfvc-pull-request"},
101104
{"icon_github_issue", "bowtie-status-error-outline"},
102105
};
103-
104106
}
105107

106108
public async Task Export()
@@ -416,31 +418,6 @@ private string ConvertMarkdownToHTML(List<MarkdownFile> files)
416418
pipelineBuilder = pipelineBuilder.UseMermaidContainers();
417419
}
418420

419-
var firstMDFileInfo = new FileInfo(files[0].AbsolutePath);
420-
var singleMDFilePath = firstMDFileInfo.DirectoryName;
421-
if (_options.SingleTOC)
422-
{
423-
var completeMarkdown = "[TOC]\n";
424-
for (var i = 0; i < files.Count; i++)
425-
{
426-
var file = new FileInfo(files[i].AbsolutePath);
427-
var md = File.ReadAllText(file.FullName);
428-
completeMarkdown += md;
429-
}
430-
431-
var directoryName = firstMDFileInfo.Directory.Name;
432-
var relativePath = "/" + directoryName + ".md";
433-
singleMDFilePath = new FileInfo(files[0].AbsolutePath).DirectoryName + relativePath;
434-
if (File.Exists(singleMDFilePath))
435-
{
436-
Log($"File {singleMDFilePath} can't be used as a single md!", LogLevel.Error, 1);
437-
return null;
438-
}
439-
File.WriteAllText(singleMDFilePath, completeMarkdown);
440-
files = new List<MarkdownFile> { new MarkdownFile { AbsolutePath = singleMDFilePath, Level = 0, RelativePath = relativePath } };
441-
}
442-
443-
444421
for (var i = 0; i < files.Count; i++)
445422
{
446423
var mf = files[i];
@@ -455,14 +432,37 @@ private string ConvertMarkdownToHTML(List<MarkdownFile> files)
455432
continue;
456433
}
457434

458-
var md = File.ReadAllText(file.FullName);
435+
var markdownContent = File.ReadAllText(file.FullName);
436+
files[i].Content = markdownContent;
437+
}
459438

460-
//rename TOC tags to fit to MarkdigToc
461-
if (_options.SingleTOC)
462-
md = md.Replace("[[_TOC_]]", "");
463-
else
464-
md = RenameTableOfContent(md);
439+
if (!string.IsNullOrEmpty(_options.GlobalTOC))
440+
{
441+
var firstMDFileInfo = new FileInfo(files[0].AbsolutePath);
442+
var directoryName = firstMDFileInfo.Directory.Name;
443+
var tocName = _options.GlobalTOC == "" ? directoryName : _options.GlobalTOC;
444+
var relativePath = "/" + tocName + ".md";
445+
var tocMDFilePath = new FileInfo(files[0].AbsolutePath).DirectoryName + relativePath;
446+
447+
var contents = files.Select(x => x.Content).ToList();
448+
var tocContent = CreateGlobalTableOfContent(contents);
449+
var tocString = string.Join("\n", tocContent);
450+
var tocMarkdownFile = new MarkdownFile { AbsolutePath = tocMDFilePath, Level = 0, RelativePath = relativePath, Content = tocString };
451+
files.Insert(0, tocMarkdownFile);
452+
}
465453

454+
for (var i = 0; i < files.Count; i++)
455+
{
456+
var mf = files[i];
457+
var file = new FileInfo(files[i].AbsolutePath);
458+
459+
Log($"{file.Name}", LogLevel.Information, 1);
460+
461+
var md = mf.Content;
462+
463+
//rename TOC tags to fit to MarkdigToc or delete them from each markdown document
464+
var newTOCString = _options.GlobalTOC != null ? "" : "[TOC]";
465+
md = md.Replace("[[_TOC_]]", newTOCString);
466466

467467
// remove scalings from image links, width & height: file.png =600x500
468468
var regexImageScalings = @"\((.[^\)]*?[png|jpg|jpeg]) =(\d+)x(\d+)\)";
@@ -485,7 +485,7 @@ private string ConvertMarkdownToHTML(List<MarkdownFile> files)
485485
var pipeline = pipelineBuilder.Build();
486486

487487
//parse the markdown document so we can alter it later
488-
var document = (MarkdownDocument)Markdown.Parse(md, pipeline);
488+
var document = Markdown.Parse(md, pipeline);
489489

490490
if (_options.NoFrontmatter)
491491
{
@@ -520,6 +520,12 @@ private string ConvertMarkdownToHTML(List<MarkdownFile> files)
520520
}
521521
html = builder.ToString();
522522

523+
if (!string.IsNullOrEmpty(_options.GlobalTOC) && i == 0)
524+
{
525+
html = RemoveDuplicatedHeadersFromGlobalTOC(html);
526+
Log($"Removed duplicated headers from toc html", LogLevel.Information, 1);
527+
}
528+
523529
//add html anchor
524530
var anchorPath = file.FullName.Substring(_path.Length);
525531
anchorPath = anchorPath.Replace("\\", "");
@@ -588,14 +594,35 @@ private string ConvertMarkdownToHTML(List<MarkdownFile> files)
588594
sb.Append(html);
589595
}
590596

591-
if (_options.SingleTOC && File.Exists(singleMDFilePath))
592-
File.Delete(singleMDFilePath);
593-
594597
var result = sb.ToString();
595598

596599
return result;
597600
}
598601

602+
internal string RemoveDuplicatedHeadersFromGlobalTOC(string html)
603+
{
604+
var result = Regex.Replace(html, @"^ *<h[123456].*>.*<\/h[123456]> *\n?$", "", RegexOptions.Multiline);
605+
result = result.Trim('\n');
606+
return result;
607+
}
608+
609+
internal List<string> CreateGlobalTableOfContent(List<string> contents)
610+
{
611+
var headers = new List<string>();
612+
foreach (var content in contents)
613+
{
614+
var headerMatches = Regex.Matches(content, "^ *#+ ?.*$", RegexOptions.Multiline);
615+
headers.AddRange(headerMatches.Select(x => x.Value.Trim()));
616+
}
617+
618+
if (!headers.Any())
619+
return new List<string>(); // no header -> no toc
620+
621+
var tocContent = new List<string> { "[TOC]" }; // MarkdigToc style
622+
tocContent.AddRange(headers);
623+
return tocContent;
624+
}
625+
599626
private MarkdownDocument RemoveFrontmatter(MarkdownDocument document)
600627
{
601628
var frontmatter = document.Descendants<YamlFrontMatterBlock>().FirstOrDefault();
@@ -889,6 +916,7 @@ public class MarkdownFile
889916
public string AbsolutePath;
890917
public string RelativePath;
891918
public int Level;
919+
public string Content;
892920

893921
public override string ToString()
894922
{

azuredevops-export-wiki.sln

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.30907.101
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "azuredevops-export-wiki", "AzureDevOps.WikiPDFExport\azuredevops-export-wiki.csproj", "{9E8EFB6E-03E6-4C3A-B971-D31DFE483B59}"
77
EndProject
8+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureDevOps.WikiPDFExport.Test", "AzureDevOps.WikiPDFExport.Test\AzureDevOps.WikiPDFExport.Test.csproj", "{6DDA14AF-1CEA-4038-88C9-17BA6E8F3AC2}"
9+
EndProject
810
Global
911
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1012
Debug|Any CPU = Debug|Any CPU

0 commit comments

Comments
 (0)