Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions src/DocsTool/BuildUi.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Tanka.DocsTool.UI;
using Tanka.DocsTool.Pipelines;
using Tanka.DocsTool.UI;

internal class BuildUi : IMiddleware
{
Expand All @@ -13,20 +14,28 @@ public BuildUi(IAnsiConsole console)

public async Task Invoke(PipelineStep next, BuildContext context)
{
await _console.Progress()
.Columns(
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new ItemCountColumn(),
new ElapsedTimeColumn(),
new RemainingTimeColumn(),
new SpinnerColumn()
)
.StartAsync(async progress =>
{
var ui = new UiBuilder(context.PageCache, context.OutputFs, _console);
await ui.BuildSite(context.Site ?? throw new InvalidOperationException(), progress);
});
try
{
await _console.Progress()
.Columns(
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new ItemCountColumn(),
new ElapsedTimeColumn(),
new RemainingTimeColumn(),
new SpinnerColumn()
)
.StartAsync(async progress =>
{
var ui = new UiBuilder(context.PageCache, context.OutputFs, _console);
await ui.BuildSite(context.Site ?? throw new InvalidOperationException(), progress, context);
});
}
catch (Exception ex)
{
context.Add(new Error($"UI build failed: {ex.Message}"));
_console.MarkupLine($"[red]UI build error:[/] {ex.Message}");
}

await next(context);
}
Expand Down
136 changes: 89 additions & 47 deletions src/DocsTool/UI/SectionComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,23 @@ public SectionComposer(Site site, IFileSystem cache, IFileSystem output, IUiBund
_logger = Infra.LoggerFactory.CreateLogger<SectionComposer>();
}

public async Task ComposeSection(Section section)
public async Task ComposeSection(Section section, BuildContext buildContext)
{
var preprocessorPipe = BuildPreProcessors(section);
var router = new DocsSiteRouter(_site, section);
var renderer = await BuildMarkdownService(section, router);

var menu = await ComposeMenu(section);

await ComposeAssets(section, router);
await ComposePages(section, menu, router, renderer, preprocessorPipe);
try
{
var preprocessorPipe = BuildPreProcessors(section);
var router = new DocsSiteRouter(_site, section);
var renderer = await BuildMarkdownService(section, router);

var menu = await ComposeMenu(section, buildContext);

await ComposeAssets(section, router, buildContext);
await ComposePages(section, menu, router, renderer, preprocessorPipe, buildContext);
}
catch (Exception ex)
{
buildContext.Add(new Error($"Failed to compose section '{section}': {ex.Message}"));
}
}

private Func<FileSystemPath, PipeReader, Task<PipeReader>> BuildPreProcessors(Section section)
Expand All @@ -64,25 +71,36 @@ private Task<DocsMarkdownService> BuildMarkdownService(Section section, DocsSite
return Task.FromResult(new DocsMarkdownService(builder));
}

private async Task ComposeAssets(Section section, DocsSiteRouter router)
private async Task ComposeAssets(Section section, DocsSiteRouter router, BuildContext buildContext)
{
// compose assets from sections
foreach (var (relativePath, assetItem) in section.ContentItems.Where(ci => IsAsset(ci.Key, ci.Value)))
{
// open file streams
await using var inputStream = await assetItem.File.OpenRead();
try
{
// open file streams
await using var inputStream = await assetItem.File.OpenRead();

// create output dir for page
FileSystemPath outputPath = router.GenerateRoute(new Xref(assetItem.Version, section.Id, relativePath))
?? throw new InvalidOperationException($"Could not generate output path for '{outputPath}'.");
// create output dir for page
var outputPath = router.GenerateRoute(new Xref(assetItem.Version, section.Id, relativePath));
if (outputPath == null)
{
buildContext.Add(new Error($"Could not generate output path for asset '{relativePath}'.", assetItem));
continue;
}

await _output.GetOrCreateDirectory(outputPath.GetDirectoryPath());
await _output.GetOrCreateDirectory(Path.GetDirectoryName(outputPath));

// create output file
var outputFile = await _output.GetOrCreateFile(outputPath);
await using var outputStream = await outputFile.OpenWrite();
// create output file
var outputFile = await _output.GetOrCreateFile(outputPath);
await using var outputStream = await outputFile.OpenWrite();

await inputStream.CopyToAsync(outputStream);
await inputStream.CopyToAsync(outputStream);
}
catch (Exception ex)
{
buildContext.Add(new Error($"Failed to compose asset '{relativePath}': {ex.Message}", assetItem));
}
}
}

Expand Down Expand Up @@ -112,7 +130,8 @@ private async Task ComposePages(
IReadOnlyCollection<NavigationItem> menu,
DocsSiteRouter router,
DocsMarkdownService renderer,
Func<FileSystemPath, PipeReader, Task<PipeReader>> preprocessorPipe)
Func<FileSystemPath, PipeReader, Task<PipeReader>> preprocessorPipe,
BuildContext buildContext)
{
var pageComposer = new PageComposer(_site, section, _cache, _output, _uiBundle, renderer);

Expand All @@ -127,7 +146,10 @@ private async Task ComposePages(
}
catch (Exception e)
{
throw new InvalidOperationException($"Failed to compose page '{pageItem.Key}'.", e);
lock (buildContext)
{
buildContext.Add(new Error($"Failed to compose page '{pageItem.Key}': {e.Message}", pageItem.Value));
}
}
}));
}
Expand All @@ -149,11 +171,15 @@ private async Task ComposePages(
}
catch (Exception e)
{
throw new InvalidOperationException($"Failed to compose redirect page 'index.html'.", e);
lock (buildContext)
{
buildContext.Add(new Error($"Failed to compose redirect page 'index.html': {e.Message}"));
}
}
}));
}

// Wait for all tasks to complete, regardless of whether some fail
await Task.WhenAll(tasks);
}

Expand All @@ -162,42 +188,58 @@ private bool IsPage(FileSystemPath relativePath, ContentItem contentItem)
return relativePath.GetExtension() == ".md" && relativePath.GetFileName() != "nav.md";
}

private async Task<IReadOnlyCollection<NavigationItem>> ComposeMenu(Section section)
private async Task<IReadOnlyCollection<NavigationItem>> ComposeMenu(Section section, BuildContext buildContext)
{
var items = new List<NavigationItem>();

foreach (var naviFileLink in section.Definition.Nav)
{
if (naviFileLink.Xref == null)
throw new NotSupportedException("External navigation file links are not supported");

var xref = naviFileLink.Xref.Value;

var targetSection = _site.GetSectionByXref(xref, section);
try
{
if (naviFileLink.Xref == null)
{
buildContext.Add(new Error("External navigation file links are not supported"));
continue;
}

if (targetSection == null)
throw new InvalidOperationException($"Invalid navigation file link {naviFileLink}. Section not found.");
var xref = naviFileLink.Xref.Value;

var navigationFileItem = targetSection.GetContentItem(xref.Path);
var targetSection = _site.GetSectionByXref(xref, section);

if (navigationFileItem == null)
throw new InvalidOperationException($"Invalid navigation file link {naviFileLink}. Path not found.");
if (targetSection == null)
{
buildContext.Add(new Error($"Invalid navigation file link {naviFileLink}. Section not found."));
continue;
}

await using var fileStream = await navigationFileItem.File.OpenRead();
using var reader = new StreamReader(fileStream);
var text = await reader.ReadToEndAsync();
var navigationFileItem = targetSection.GetContentItem(xref.Path);

// override context so each navigation file is rendered in the context of the owning section
var router = new DocsSiteRouter(_site, targetSection);
var renderer = new DocsMarkdownService(new DocsMarkdownRenderingContext(_site, targetSection, router));
var builder = new NavigationBuilder(renderer, router);
var fileItems = builder.Add(new string[]
if (navigationFileItem == null)
{
text
})
.Build();
buildContext.Add(new Error($"Invalid navigation file link {naviFileLink}. Path not found.", navigationFileItem));
continue;
}

items.AddRange(fileItems);
await using var fileStream = await navigationFileItem.File.OpenRead();
using var reader = new StreamReader(fileStream);
var text = await reader.ReadToEndAsync();

// override context so each navigation file is rendered in the context of the owning section
var router = new DocsSiteRouter(_site, targetSection);
var renderer = new DocsMarkdownService(new DocsMarkdownRenderingContext(_site, targetSection, router));
var builder = new NavigationBuilder(renderer, router);
var fileItems = builder.Add(new string[]
{
text
})
.Build();

items.AddRange(fileItems);
}
catch (Exception ex)
{
buildContext.Add(new Error($"Failed to compose navigation for '{naviFileLink}': {ex.Message}"));
}
}

return items;
Expand Down
Loading
Loading