Skip to content

Commit

Permalink
Adds a new Title module for setting title metadata from file path, us…
Browse files Browse the repository at this point in the history
…es it for docs recipe and theme, statiqdev#342
  • Loading branch information
daveaglick committed Oct 17, 2016
1 parent 88aae8d commit 26daab5
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 177 deletions.
3 changes: 3 additions & 0 deletions src/core/Wyam.Common/Meta/Keys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,8 @@ public static class Keys
public const string Next = nameof(Next);
public const string Previous = nameof(Previous);
public const string TreePath = nameof(TreePath);

// Title
public const string Title = nameof(Title);
}
}
3 changes: 2 additions & 1 deletion src/core/Wyam.Core/Modules/Metadata/CopyMeta.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public class CopyMeta : IModule
/// The specified object in fromKey is copied to toKey. If a format is provided, the fromKey value is processed through string.Format before being copied (if the existing value is a DateTime, the format is passed as the argument to ToString).
/// </summary>
/// <param name="fromKey">The metadata key to copy from.</param>
/// <param name="metadata">The metadata key to copy to.</param>
/// <param name="toKey">The metadata key to copy to.</param>
/// <param name="format">The formatting to apply to the new value.</param>
public CopyMeta(string fromKey, string toKey, string format = null)
{
if (fromKey == null)
Expand Down
170 changes: 170 additions & 0 deletions src/core/Wyam.Core/Modules/Metadata/Title.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using Wyam.Common.Configuration;
using Wyam.Common.Documents;
using Wyam.Common.Execution;
using Wyam.Common.IO;
using Wyam.Common.Meta;
using Wyam.Common.Modules;

namespace Wyam.Core.Modules.Metadata
{
/// <summary>
/// Sets a title metadata key for documents based on their file path or source. This will
/// split the title at special characters, capitalize first letters, remove extensions, etc.
/// </summary>
/// <metadata name="Title" type="string">The title of the document.</metadata>
/// <category>Metadata</category>
public class Title : IModule
{
private readonly DocumentConfig _title = GetTitle;
private string _key = Keys.Title;
private bool _keepExisting = true;

/// <summary>
/// This will use the existing title metadata key if one exists,
/// otherwise it will set a title based on the document source
/// or the RelativeFilePath key if no source is available.
/// </summary>
public Title()
{
}

/// <summary>
/// This sets the title of all input documents to the specified string.
/// </summary>
/// <param name="title">The title to set.</param>
public Title(string title)
{
_title = (doc, ctx) => title;
}

/// <summary>
/// This sets the title of all input documents to a value from the delegate.
/// </summary>
/// <param name="title">A delegate that must return a string.</param>
public Title(ContextConfig title)
{
if (title == null)
{
throw new ArgumentNullException(nameof(title));
}

_title = (doc, ctx) => title(ctx);
}

/// <summary>
/// This sets the title of all input documents to a value from the delegate.
/// </summary>
/// <param name="title">A delegate that must return a string.</param>
public Title(DocumentConfig title)
{
if (title == null)
{
throw new ArgumentNullException(nameof(title));
}

_title = title;
}

/// <summary>
/// Specifies the key to set for the title. By default this module sets
/// a value for the key Title.
/// </summary>
/// <param name="key">The metadata key to set.</param>
public Title WithKey(string key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
if (string.IsNullOrEmpty(key))
{
throw new ArgumentException("Value cannot be null or empty.", nameof(key));
}

_key = key;
return this;
}

/// <summary>
/// Indicates that an existing value in the title key should be kept. The
/// default value is <c>true</c>. Setting to <c>false</c> will always
/// set the title metadata to the result of this module, even if the
/// result is null or empty.
/// </summary>
/// <param name="keepExisting">Whether to keep the existing title metadata value.</param>
public Title KeepExisting(bool keepExisting = true)
{
_keepExisting = keepExisting;
return this;
}

public IEnumerable<IDocument> Execute(IReadOnlyList<IDocument> inputs, IExecutionContext context)
{
return inputs
.AsParallel()
.Select(input =>
{
// Check if there's already a title set
if (_keepExisting && input.ContainsKey(_key))
{
return input;
}

// Calculate the new title
string title = _title.Invoke<string>(input, context);
return title == null
? input
: context
.GetDocument(input, new MetadataItems
{
{_key, title}
});
});
}

public static object GetTitle(IDocument doc, IExecutionContext context)
{
FilePath path = doc.Source ?? doc.FilePath(Keys.RelativeFilePath);
return path == null ? null : GetTitle(path);
}

public static string GetTitle(FilePath path)
{
// Get the filename, unless an index file, then get containing directory
string title = path.Segments.Last();
if (title.StartsWith("index.") && path.Segments.Length > 1)
{
title = path.Segments[path.Segments.Length - 2];
}

// Strip the extension(s)
int extensionIndex = title.IndexOf('.');
if (extensionIndex > 0)
{
title = title.Substring(0, extensionIndex);
}

// Decode URL escapes
title = WebUtility.UrlDecode(title);

// Replace special characters with spaces
title = title.Replace('-', ' ').Replace('_', ' ');

// Join adjacent spaces
while (title.IndexOf(" ", StringComparison.Ordinal) > 0)
{
title = title.Replace(" ", " ");
}

// Capitalize
title = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(title);

return title;
}
}
}
1 change: 1 addition & 0 deletions src/core/Wyam.Core/Wyam.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
<Compile Include="Modules\Metadata\Index.cs" />
<Compile Include="Modules\Control\OrderBy.cs" />
<Compile Include="Modules\Control\Paginate.cs" />
<Compile Include="Modules\Metadata\Title.cs" />
<Compile Include="Modules\Metadata\Tree.cs" />
<Compile Include="Modules\ModuleExtensions.cs" />
<Compile Include="Modules\Metadata\ValidateMeta.cs" />
Expand Down
11 changes: 6 additions & 5 deletions src/extensions/Wyam.Docs/Docs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ public void Apply(IEngine engine)
// Add any additional Razor pages
new ReadFiles(ctx => $"{{!{ctx.GlobalMetadata.String(DocsKeys.ApiPathPrefix)},**}}/{{!_,}}*.cshtml"),
new FrontMatter(new Yaml.Yaml())),
new Title(),
new Tree()
.WithPlaceholderFactory((path, items, context) =>
{
items.Add(Keys.RelativeFilePath, new FilePath(string.Join("/", path.Concat(new[] { "index.html" }))));
items.Add(DocsKeys.Title, path.Last().ToString());
FilePath indexPath = new FilePath(string.Join("/", path.Concat(new[] {"index.html"})));
items.Add(Keys.RelativeFilePath, indexPath);
items.Add(Keys.Title, Title.GetTitle(indexPath));
return context.GetDocument("@Html.Partial(\"_ChildPages\")", items);
})
.CollapseRoot()
Expand All @@ -57,8 +59,7 @@ public void Apply(IEngine engine)
new Meta(DocsKeys.NoSidebar, (doc, ctx) => doc.Get(DocsKeys.NoSidebar,
(doc.DocumentList(Keys.Children)?.Count ?? 0) == 0)
&& doc.Document(Keys.Parent) == null),
// Set the title to the file name if there wasn't already a title
new Meta(DocsKeys.Title, (doc, ctx) => doc.String(DocsKeys.Title, doc.Get<object[]>(Keys.TreePath).Last().ToString())),
new Title(),
new Razor.Razor()
.WithLayout("/_Layout.cshtml"),
new WriteFiles(".html")
Expand All @@ -82,7 +83,7 @@ public void Apply(IEngine engine)
new Meta(Keys.RelativeFilePath,
ctx => new DirectoryPath(ctx.GlobalMetadata.String(DocsKeys.ApiPathPrefix)).CombineFile("index.html")),
new Meta(Keys.SourceFileName, "index.html"),
new Meta(DocsKeys.Title, "API"),
new Title("API"),
new Meta(DocsKeys.NoSidebar, true),
new Razor.Razor(),
new WriteFiles()
Expand Down
1 change: 0 additions & 1 deletion src/extensions/Wyam.Docs/DocsKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ public static class DocsKeys
public const string SourceFiles = nameof(SourceFiles);
public const string IncludeGlobal = nameof(IncludeGlobal);
public const string ApiPathPrefix = nameof(ApiPathPrefix);
public const string Title = nameof(Title);
public const string Description = nameof(Description);
public const string Category = nameof(Category);
public const string Order = nameof(Order);
Expand Down
4 changes: 2 additions & 2 deletions themes/Docs/Samson/Shared/Sidebar/_ChildPagesMenu.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
// Iterate documents in this category
foreach(IDocument child in categoryGroup
.OrderBy(x => x.Get<int>(DocsKeys.Order, 1000))
.ThenBy(x => x.String(DocsKeys.Title)))
.ThenBy(x => x.String(Keys.Title)))
{
object[] currentTreePath = Document.Get<object[]>(Keys.TreePath);
object[] childTreePath = child.Get<object[]>(Keys.TreePath);
string childTitle = child.String(DocsKeys.Title, childTreePath.Last().ToString());
string childTitle = child.String(Keys.Title, childTreePath.Last().ToString());
string parentActive = currentTreePath.Take(childTreePath.Length).SequenceEqual(childTreePath) ? "active" : null;
string childActive = parentActive != null && currentTreePath.Length == childTreePath.Length ? "active" : null;
IReadOnlyList<IDocument> children = child.DocumentList(Keys.Children);
Expand Down
4 changes: 2 additions & 2 deletions themes/Docs/Samson/Shared/_ChildPages.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
<tbody>
@foreach(IDocument child in categoryGroup
.OrderBy(x => x.Get<int>(DocsKeys.Order, 1000))
.ThenBy(x => x.String(DocsKeys.Title)))
.ThenBy(x => x.String(Keys.Title)))
{
object[] childTreePath = child.Get<object[]>(Keys.TreePath);
<tr>
<td><strong><a href="@Context.GetLink(child)">@(child.String(DocsKeys.Title, childTreePath.Last().ToString()))</a></strong></td>
<td><strong><a href="@Context.GetLink(child)">@(child.String(Keys.Title, childTreePath.Last().ToString()))</a></strong></td>
<td>
@{
if(child.ContainsKey(DocsKeys.Description))
Expand Down
2 changes: 1 addition & 1 deletion themes/Docs/Samson/_ApiLayout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
containingTypeString = containingTypeString + containingType.String("DisplayName") + ".";
containingType = containingType.Get<IDocument>("ContainingType");
}
ViewData[DocsKeys.Title] = "API - " + containingTypeString + Model["DisplayName"] + " " + Model["SpecificKind"];
ViewData[Keys.Title] = "API - " + containingTypeString + Model["DisplayName"] + " " + Model["SpecificKind"];
}

@section Sidebar {
Expand Down
4 changes: 2 additions & 2 deletions themes/Docs/Samson/_Layout.cshtml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@{
Layout = "/_Master.cshtml";
ViewData[DocsKeys.Title] = @Model.Get(DocsKeys.Title, string.Empty);
ViewData[Keys.Title] = @Model.Get(Keys.Title, string.Empty);
}

@section Sidebar {
Expand All @@ -19,7 +19,7 @@
else
{
<section class="content-header">
<h1>@ViewData[DocsKeys.Title]</h1>
<h1>@ViewData[Keys.Title]</h1>
</section>
<section class="content">
@RenderBody()
Expand Down
4 changes: 2 additions & 2 deletions themes/Docs/Samson/_Master.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<title>@Context.String(DocsKeys.SiteTitle, "Docs") - @ViewData[DocsKeys.Title]</title>
<title>@Context.String(DocsKeys.SiteTitle, "Docs") - @ViewData[Keys.Title]</title>
<link href="/assets/css/bootstrap.css" rel="stylesheet" />
<link href="/assets/css/AdminLTE.css" rel="stylesheet" />
<link href="/assets/css/theme.css" rel="stylesheet" />
Expand Down Expand Up @@ -71,7 +71,7 @@
@{
List<Tuple<string, string>> pages = Context
.Documents[DocsPipelines.Pages]
.Select(x => Tuple.Create(x.String(DocsKeys.Title), Context.GetLink(x)))
.Select(x => Tuple.Create(x.String(Keys.Title), Context.GetLink(x)))
.OrderBy(x => x.Item1)
.ToList();
pages.Add(Tuple.Create("API", Context.GetLink(Context.FilePath(DocsKeys.ApiPathPrefix))));
Expand Down
Loading

0 comments on commit 26daab5

Please sign in to comment.