Skip to content

Commit e62b13b

Browse files
committed
Merge branch 'cpetry/main'
2 parents 73a9db9 + 88d41b7 commit e62b13b

File tree

6 files changed

+188
-12
lines changed

6 files changed

+188
-12
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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,8 @@ public class Options
100100

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; }
103+
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; }
103106
}
104107
}

AzureDevOps.WikiPDFExport/WikiPDFExporter.cs

Lines changed: 77 additions & 12 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()
@@ -145,7 +147,6 @@ public async Task Export()
145147
}
146148
};
147149
}
148-
else
149150
{
150151
files = ReadOrderFiles(_path, 0); // root level
151152
}
@@ -399,7 +400,8 @@ private string ConvertMarkdownToHTML(List<MarkdownFile> files)
399400
.UsePipeTables()
400401
.UseEmojiAndSmiley()
401402
.UseAdvancedExtensions()
402-
.UseYamlFrontMatter();
403+
.UseYamlFrontMatter()
404+
.UseTableOfContent();
403405

404406
//must be handled by us to have linking across files
405407
pipelineBuilder.Extensions.RemoveAll(x => x is Markdig.Extensions.AutoIdentifiers.AutoIdentifierExtension);
@@ -430,11 +432,38 @@ private string ConvertMarkdownToHTML(List<MarkdownFile> files)
430432
continue;
431433
}
432434

433-
var md = File.ReadAllText(file.FullName);
435+
var markdownContent = File.ReadAllText(file.FullName);
436+
files[i].Content = markdownContent;
437+
}
438+
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+
451+
var tocMarkdownFile = new MarkdownFile { AbsolutePath = tocMDFilePath, Level = 0, RelativePath = relativePath, Content = tocString };
452+
files.Insert(0, tocMarkdownFile);
453+
}
454+
455+
for (var i = 0; i < files.Count; i++)
456+
{
457+
var mf = files[i];
458+
var file = new FileInfo(files[i].AbsolutePath);
459+
460+
Log($"{file.Name}", LogLevel.Information, 1);
434461

435-
//replace Table of Content
436-
md = RemoveTableOfContent(md);
462+
var md = mf.Content;
437463

464+
//rename TOC tags to fit to MarkdigToc or delete them from each markdown document
465+
var newTOCString = _options.GlobalTOC != null ? "" : "[TOC]";
466+
md = md.Replace("[[_TOC_]]", newTOCString);
438467

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

459488
//parse the markdown document so we can alter it later
460-
var document = (MarkdownDocument)Markdown.Parse(md, pipeline);
489+
var document = Markdown.Parse(md, pipeline);
461490

462491
if (_options.NoFrontmatter)
463492
{
@@ -477,6 +506,7 @@ private string ConvertMarkdownToHTML(List<MarkdownFile> files)
477506
}
478507
}
479508

509+
480510
//adjust the links
481511
CorrectLinksAndImages(document, file, mf);
482512

@@ -491,6 +521,12 @@ private string ConvertMarkdownToHTML(List<MarkdownFile> files)
491521
}
492522
html = builder.ToString();
493523

524+
if (!string.IsNullOrEmpty(_options.GlobalTOC) && i == 0)
525+
{
526+
html = RemoveDuplicatedHeadersFromGlobalTOC(html);
527+
Log($"Removed duplicated headers from toc html", LogLevel.Information, 1);
528+
}
529+
494530
//add html anchor
495531
var anchorPath = file.FullName.Substring(_path.Length);
496532
anchorPath = anchorPath.Replace("\\", "");
@@ -513,6 +549,13 @@ private string ConvertMarkdownToHTML(List<MarkdownFile> files)
513549
html = heading + html;
514550
}
515551

552+
553+
if (!string.IsNullOrEmpty(_options.GlobalTOC) && i == 0 && !_options.Heading)
554+
{
555+
var heading = $"<h1>{_options.GlobalTOC}</h1>";
556+
html = heading + html;
557+
}
558+
516559
if (_options.Heading)
517560
{
518561
var filename = file.Name.Replace(".md", "");
@@ -564,6 +607,30 @@ private string ConvertMarkdownToHTML(List<MarkdownFile> files)
564607
return result;
565608
}
566609

610+
internal string RemoveDuplicatedHeadersFromGlobalTOC(string html)
611+
{
612+
var result = Regex.Replace(html, @"^ *<h[123456].*>.*<\/h[123456]> *\n?$", "", RegexOptions.Multiline);
613+
result = result.Trim('\n');
614+
return result;
615+
}
616+
617+
internal List<string> CreateGlobalTableOfContent(List<string> contents)
618+
{
619+
var headers = new List<string>();
620+
foreach (var content in contents)
621+
{
622+
var headerMatches = Regex.Matches(content, "^ *#+ ?.*$", RegexOptions.Multiline);
623+
headers.AddRange(headerMatches.Select(x => x.Value.Trim()));
624+
}
625+
626+
if (!headers.Any())
627+
return new List<string>(); // no header -> no toc
628+
629+
var tocContent = new List<string> { "[TOC]" }; // MarkdigToc style
630+
tocContent.AddRange(headers);
631+
return tocContent;
632+
}
633+
567634
private MarkdownDocument RemoveFrontmatter(MarkdownDocument document)
568635
{
569636
var frontmatter = document.Descendants<YamlFrontMatterBlock>().FirstOrDefault();
@@ -636,13 +703,10 @@ private bool PageMatchesFilter(MarkdownObject document)
636703
return true;
637704
}
638705

639-
private string RemoveTableOfContent(string document)
706+
private string RenameTableOfContent(string document)
640707
{
641708
if (document.Contains("TOC"))
642-
{
643-
Log("Removing Table of contents [[_TOC_]] from pdf", LogLevel.Warning, 1);
644-
document = document.Replace("[[_TOC_]]", "");
645-
}
709+
document = document.Replace("[[_TOC_]]", "[TOC]"); // MarkdigToc styled. See https://github.com/leisn/MarkdigToc
646710
return document;
647711
}
648712

@@ -860,6 +924,7 @@ public class MarkdownFile
860924
public string AbsolutePath;
861925
public string RelativePath;
862926
public int Level;
927+
public string Content;
863928

864929
public override string ToString()
865930
{

AzureDevOps.WikiPDFExport/azuredevops-export-wiki.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
<ItemGroup>
2727
<PackageReference Include="CommandLineParser" Version="2.8.0" />
28+
<PackageReference Include="Leisn.MarkdigToc" Version="0.1.3" />
2829
<PackageReference Include="Markdig" Version="0.26.0" />
2930
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.19.0" />
3031
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />

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)