Skip to content

Commit

Permalink
Xref support
Browse files Browse the repository at this point in the history
  • Loading branch information
daveaglick committed Apr 20, 2020
1 parent 6907139 commit c3aac82
Show file tree
Hide file tree
Showing 11 changed files with 634 additions and 5 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<!-- Controls whether references are local projects or NuGet packages -->
<LocalReferences>false</LocalReferences>
<!-- The NuGet version of Statiq that should be referenced if LocalReferences is false -->
<StatiqFrameworkVersion>1.0.0-beta.4</StatiqFrameworkVersion>
<StatiqFrameworkVersion>1.0.0-beta.5</StatiqFrameworkVersion>
</PropertyGroup>

<PropertyGroup>
Expand Down
4 changes: 4 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# 1.0.0-alpha.7

- Added xref support for links like "xref:xyz" where "xyz" is the value of the "Xref" metadata, the document title with spaces converted to underscores if no "Xref" value is defined, or the source file name if neither of those are available.
- Added `IExecutionContext.TryGetXrefDocument()` and `IExecutionContext.GetXrefDocument()` extension methods to get a document by xref.
- Added `IExecutionContext.TryGetXrefLink()` and `IExecutionContext.GetXrefLink()` extension methods to get a document link by xref.

# 1.0.0-alpha.6

- Added support for Handlebars for files with a ".hbs" or ".handlebars" extension.
Expand Down
3 changes: 2 additions & 1 deletion src/Statiq.Web/BootstrapperFactoryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public static Bootstrapper CreateWeb(this BootstrapperFactory factory, string[]
.ConfigureServices(services => services.AddSingleton(new Templates()))
.AddSettingsIfNonExisting(new Dictionary<string, object>
{
{ WebKeys.MirrorResources, true }
{ WebKeys.MirrorResources, true },
{ WebKeys.Xref, Config.FromDocument(doc => doc.GetTitle().Replace(' ', '_')) }
});
}
}
50 changes: 50 additions & 0 deletions src/Statiq.Web/IExecutionContextXrefExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using Statiq.Common;

namespace Statiq.Web
{
public static class IExecutionContextXrefExtensions
{
public static bool TryGetXrefDocument(this IExecutionContext context, string xref, out IDocument document)
{
_ = context ?? throw new ArgumentNullException(nameof(context));

ImmutableArray<IDocument> matches = context.Outputs[nameof(Pipelines.Content)].Flatten()
.Where(x => x.GetString(WebKeys.Xref)?.Equals(xref, StringComparison.OrdinalIgnoreCase) == true)
.ToImmutableDocumentArray();
if (matches.Length > 1)
{
throw new ExecutionException($"Multiple ambiguous matching documents found for xref \"{xref}\"");
}
if (matches.Length == 1)
{
document = matches[0];
return true;
}
document = default;
return false;
}

public static IDocument GetXrefDocument(this IExecutionContext context, string xref) =>
context.TryGetXrefDocument(xref, out IDocument document) ? document : throw new ExecutionException($"Couldn't find document with xref \"{xref}\"");

public static bool TryGetXrefLink(this IExecutionContext context, string xref, out string link) =>
context.TryGetXrefLink(xref, false, out link);

public static bool TryGetXrefLink(this IExecutionContext context, string xref, bool includeHost, out string link)
{
if (context.TryGetXrefDocument(xref, out IDocument document))
{
link = document.GetLink(includeHost);
return link != null;
}
link = default;
return false;
}

public static string GetXrefLink(this IExecutionContext context, string xref, bool includeHost = false) =>
context.TryGetXrefLink(xref, includeHost, out string link) ? link : throw new ExecutionException($"Couldn't get link for document with xref \"{xref}\"");
}
}
2 changes: 1 addition & 1 deletion src/Statiq.Web/Modules/ProcessMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ public ProcessMetadata()
{
}
}
}
}
3 changes: 2 additions & 1 deletion src/Statiq.Web/Modules/RenderPostProcessTemplates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public RenderPostProcessTemplates(Templates templates)
new ExecuteIf(Config.FromSetting<bool>(WebKeys.MirrorResources))
{
new MirrorResources()
}
},
new ResolveXrefs()
})
.ToArray())
{
Expand Down
62 changes: 62 additions & 0 deletions src/Statiq.Web/Modules/ResolveXrefs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AngleSharp.Dom;
using AngleSharp.Dom.Html;
using AngleSharp.Parser.Html;
using Statiq.Common;
using Statiq.Html;

namespace Statiq.Web.Modules
{
public class ResolveXrefs : ParallelModule
{
private static readonly HtmlParser HtmlParser = new HtmlParser();

protected override async Task<IEnumerable<Common.IDocument>> ExecuteInputAsync(Common.IDocument input, IExecutionContext context)
{
IHtmlDocument htmlDocument = await input.ParseHtmlAsync(context, HtmlParser);
if (htmlDocument != null)
{
// Find and replace "xref:" in links
bool modifiedDocument = false;
foreach (IElement element in htmlDocument
.GetElementsByTagName("a")
.Where(x => x.HasAttribute("href")))
{
string href = element.GetAttribute("href");
if (href.StartsWith("xref:") && href.Length > 5)
{
string xref = href.Substring(5);
string queryAndFragment = string.Empty;
int queryAndFragmentIndex = xref.IndexOfAny(new[] { '#', '?' });
if (queryAndFragmentIndex > 0)
{
queryAndFragment = xref.Substring(queryAndFragmentIndex);
xref = xref.Substring(0, queryAndFragmentIndex);
}
element.Attributes["href"].Value = context.GetXrefLink(xref) + queryAndFragment;
modifiedDocument = true;
}
}

// Return a new document with the replacements if we performed any
if (modifiedDocument)
{
using (Stream contentStream = await context.GetContentStreamAsync())
{
using (StreamWriter writer = contentStream.GetWriter())
{
htmlDocument.ToHtml(writer, ProcessingInstructionFormatter.Instance);
writer.Flush();
return input.Clone(context.GetContentProvider(contentStream, MediaTypes.Html)).Yield();
}
}
}
}

return input.Yield();
}
}
}
40 changes: 40 additions & 0 deletions src/Statiq.Web/ProcessingInstructionFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Html;

namespace Statiq.Web
{
/// <summary>
/// This uncomments shortcode processing instructions which currently get parsed as comments
/// in AngleSharp. See https://github.com/Wyamio/Statiq/issues/784.
/// This can be removed once https://github.com/AngleSharp/AngleSharp/pull/762 is merged.
/// </summary>
internal class ProcessingInstructionFormatter : IMarkupFormatter
{
private static readonly IMarkupFormatter Formatter = HtmlMarkupFormatter.Instance;

public static readonly IMarkupFormatter Instance = new ProcessingInstructionFormatter();

public string Attribute(IAttr attribute) => Formatter.Attribute(attribute);

public string CloseTag(IElement element, bool selfClosing) => Formatter.CloseTag(element, selfClosing);

public string Doctype(IDocumentType doctype) => Formatter.Doctype(doctype);

public string OpenTag(IElement element, bool selfClosing) => Formatter.OpenTag(element, selfClosing);

public string Text(string text) => Formatter.Text(text);

public string Processing(IProcessingInstruction processing) => Formatter.Processing(processing);

public string Comment(IComment comment)
{
if (comment.Data.StartsWith("?") && comment.Data.EndsWith("?"))
{
// This was probably a shortcode, so uncomment it
return $"<{comment.Data}>";
}
return Formatter.Comment(comment);
}
}
}
9 changes: 8 additions & 1 deletion src/Statiq.Web/WebKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,20 @@ public static class WebKeys
public const string FeedItemThreadUpdated = nameof(FeedItemThreadUpdated);

/// <summary>
/// Indicates that the data file (.json, .yaml, etc.) should be output (by defaut data files are not output).
/// Indicates that the data file (.json, .yaml, etc.) should be output (by default data files are not output).
/// </summary>
public const string OutputData = nameof(OutputData);

/// <summary>
/// Indicates the layout file that should be used for this document.
/// </summary>
public const string Layout = nameof(Layout);

/// <summary>
/// Specifies the cross-reference ID of the current document. If not explicitly provided, it will default
/// to the title of the document with spaces replaced by underscores (which is derived from the source file name
/// if no <see cref="Title"/> metadata is defined for the document).
/// </summary>
public const string Xref = nameof(Xref);
}
}
Loading

0 comments on commit c3aac82

Please sign in to comment.