Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic static rendering #3411

Merged
merged 13 commits into from
Sep 12, 2018
21 changes: 21 additions & 0 deletions docs/specs/rendering.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
# Apply template and produce html page when output is not json
inputs:
docfx.yml: |
output:
json: false
docs/a.md:
outputs:
docs/a/index.html:
build.manifest:
---
# Use .html extension on uglify url
inputs:
docfx.yml: |
output:
json: false
uglifyUrl: true
docs/a.md:
outputs:
docs/a.html:
build.manifest:
18 changes: 18 additions & 0 deletions schemas/Conceptual.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"type": "object",
"additionalProperties": false,
"properties": {
"html": {
"type": "string"
},
"title": {
"type": "string"
},
"htmlTitle": {
"type": "string"
},
"wordCount": {
"type": "integer"
}
}
}
6 changes: 6 additions & 0 deletions schemas/docfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@
"path": {
"type": "string"
},
"json": {
"type": "boolean"
},
"uglifyUrl": {
"type": "boolean"
},
"copyResources": {
"type": "boolean"
}
Expand Down
3 changes: 2 additions & 1 deletion src/Microsoft.Docs.Template/Models/Conceptual.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ namespace Microsoft.Docs.Build
// Conceptual model is more than just an html string, it also contain other properties.
// We currently bake these other properties into PageModel to make the output flat,
// but it makes conceptual kinda special. We may consider lift them outside PageModel.
public class Conceptual
[PageSchema]
public sealed class Conceptual
{
public string Html { get; set; }

Expand Down
Empty file.
6 changes: 5 additions & 1 deletion src/docfx/build/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@ void ValidateBookmarks()
var hasErrors = context.Report(file.ToString(), errors);
if (model != null && !hasErrors)
{
context.WriteJson(model, file.OutputPath);
if (model is string str)
context.WriteText(str, file.OutputPath);
else
context.WriteJson(model, file.OutputPath);

return (false, dependencies);
}

Expand Down
10 changes: 8 additions & 2 deletions src/docfx/build/page/BuildPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Microsoft.Docs.Build
{
internal static class BuildPage
{
public static async Task<(IEnumerable<Error> errors, PageModel result, DependencyMap dependencies)> Build(
public static async Task<(IEnumerable<Error> errors, object result, DependencyMap dependencies)> Build(
Document file,
TableOfContentsMap tocMap,
ContributionInfo contribution,
Expand Down Expand Up @@ -43,7 +43,13 @@ internal static class BuildPage
if (error != null)
errors.Add(error);

return (errors, model, dependencies.Build());
var output = (object)model;
if (!file.Docset.Config.Output.Json && schema.Attribute is PageSchemaAttribute)
{
output = await Template.Render(model);
}

return (errors, output, dependencies.Build());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

output [](start = 28, length = 6)

this can be PageModel or string, but Build.cs always uses context.WriteJson to write it. Add a context.WriteString when it's string?

}

private static async Task<(List<Error> errors, Schema schema, PageModel model)>
Expand Down
6 changes: 4 additions & 2 deletions src/docfx/build/resolve/Resolve.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ public static (Error error, string href, string fragment, Document file) TryReso
{
return (error, query + fragment, fragment, null);
}
var selfUrl = HrefUtility.EscapeUrl(Document.PathToRelativeUrl(Path.GetFileName(file.SitePath), file.ContentType, file.Schema));
var selfUrl = HrefUtility.EscapeUrl(Document.PathToRelativeUrl(
Path.GetFileName(file.SitePath), file.ContentType, file.Schema, file.Docset.Config.Output.Json));
return (error, selfUrl + query + fragment, fragment, null);
}
if (string.IsNullOrEmpty(fragment))
Expand All @@ -67,7 +68,8 @@ public static (Error error, string href, string fragment, Document file) TryReso

// Make result relative to `resultRelativeTo`
var relativePath = PathUtility.GetRelativePathToFile(resultRelativeTo.SitePath, file.SitePath);
var relativeUrl = HrefUtility.EscapeUrl(Document.PathToRelativeUrl(relativePath, file.ContentType, file.Schema));
var relativeUrl = HrefUtility.EscapeUrl(Document.PathToRelativeUrl(
relativePath, file.ContentType, file.Schema, file.Docset.Config.Output.Json));

if (redirectTo != null)
{
Expand Down
1 change: 1 addition & 0 deletions src/docfx/cli/CommandLineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public JObject ToJObject()
["output"] = new JObject
{
["path"] = Output != null ? (JValue)Output : JValue.CreateNull(),
["json"] = Legacy ? (JValue)true : JValue.CreateNull(),
},
};
}
Expand Down
7 changes: 4 additions & 3 deletions src/docfx/config/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,15 @@ private static (List<Error>, Config) LoadCore(string docsetPath, string configPa

if (extend)
{
var extendErros = new List<Error>();
(extendErros, finalConfigObject) = ExtendConfigs(finalConfigObject, docsetPath, restoreMap ?? new RestoreMap(docsetPath));
errors.AddRange(extendErros);
var extendErrors = new List<Error>();
(extendErrors, finalConfigObject) = ExtendConfigs(finalConfigObject, docsetPath, restoreMap ?? new RestoreMap(docsetPath));
errors.AddRange(extendErrors);
}

var deserializeErrors = new List<Error>();
(deserializeErrors, config) = JsonUtility.ToObject<Config>(finalConfigObject);
errors.AddRange(deserializeErrors);

return (errors, config);
}

Expand Down
12 changes: 12 additions & 0 deletions src/docfx/config/OutputConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ internal sealed class OutputConfig
/// </summary>
public readonly string Path = "_site";

/// <summary>
/// Gets whether to output JSON model.
/// </summary>
public readonly bool Json = false;

/// <summary>
/// Gets whether to use ugly url or pretty url when <see cref="Json"/> is set to false.
/// - Pretty url: a.md --> a/index.html
/// - Ugly url: a.md --> a.html
/// </summary>
public readonly bool UglifyUrl = false;

/// <summary>
/// Gets whether resources are copied to output.
/// </summary>
Expand Down
15 changes: 15 additions & 0 deletions src/docfx/docset/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,21 @@ public void WriteJson(object graph, string destRelativePath)
}
}

/// <summary>
/// Writes the input text to an output file.
/// Throws if multiple threads trying to write to the same destination concurrently.
/// </summary>
public void WriteText(string contents, string destRelativePath)
{
Debug.Assert(!Path.IsPathRooted(destRelativePath));

var destinationPath = Path.Combine(_outputPath, destRelativePath);

Directory.CreateDirectory(Path.GetDirectoryName(destinationPath));

File.WriteAllText(destinationPath, contents);
}

/// <summary>
/// Copies a file from source to destination, throws if source does not exists.
/// Throws if multiple threads trying to write to the same destination concurrently.
Expand Down
41 changes: 25 additions & 16 deletions src/docfx/docset/Document.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ public static (Error error, Document doc) TryCreate(Docset docset, string path,
var isExperimental = Path.GetFileNameWithoutExtension(filePath).EndsWith(".experimental", PathUtility.PathComparison);
var routedFilePath = ApplyRoutes(filePath, docset.Config.Routes);

var sitePath = FilePathToSitePath(routedFilePath, type, schema);
var siteUrl = PathToAbsoluteUrl(sitePath, type, schema);
var sitePath = FilePathToSitePath(routedFilePath, type, schema, docset.Config.Output.Json, docset.Config.Output.UglifyUrl);
var siteUrl = PathToAbsoluteUrl(sitePath, type, schema, docset.Config.Output.Json);
var outputPath = sitePath;
var contentType = redirectionUrl != null ? ContentType.Redirection : type;

Expand Down Expand Up @@ -293,7 +293,7 @@ internal static ContentType GetContentType(string path)
return ContentType.Page;
}

internal static string FilePathToSitePath(string path, ContentType contentType, Schema schema)
internal static string FilePathToSitePath(string path, ContentType contentType, Schema schema, bool json, bool uglifyUrl)
{
switch (contentType)
{
Expand All @@ -302,8 +302,18 @@ internal static string FilePathToSitePath(string path, ContentType contentType,
{
if (Path.GetFileNameWithoutExtension(path).Equals("index", PathUtility.PathComparison))
{
return Path.Combine(Path.GetDirectoryName(path), "index.json").Replace('\\', '/');
var extension = json ? ".json" : ".html";
return Path.Combine(Path.GetDirectoryName(path), "index" + extension).Replace('\\', '/');
}
if (json)
{
return Path.ChangeExtension(path, ".json");
}
if (uglifyUrl)
{
return Path.ChangeExtension(path, ".html");
}
return Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(path), "index.html").Replace('\\', '/');
}
return Path.ChangeExtension(path, ".json");
case ContentType.TableOfContents:
Expand All @@ -313,13 +323,13 @@ internal static string FilePathToSitePath(string path, ContentType contentType,
}
}

internal static string PathToAbsoluteUrl(string path, ContentType contentType, Schema schema)
internal static string PathToAbsoluteUrl(string path, ContentType contentType, Schema schema, bool json)
{
var url = PathToRelativeUrl(path, contentType, schema);
var url = PathToRelativeUrl(path, contentType, schema, json);
return url == "." ? "/" : "/" + url;
}

internal static string PathToRelativeUrl(string path, ContentType contentType, Schema schema)
internal static string PathToRelativeUrl(string path, ContentType contentType, Schema schema, bool json)
{
var url = path.Replace('\\', '/');

Expand All @@ -328,19 +338,18 @@ internal static string PathToRelativeUrl(string path, ContentType contentType, S
case ContentType.Page:
if (schema == null || schema.Attribute is PageSchemaAttribute)
{
var extensionIndex = url.LastIndexOf('.');
if (extensionIndex >= 0)
{
url = url.Substring(0, extensionIndex);
}
if (url.Equals("index", PathUtility.PathComparison))
var fileName = Path.GetFileNameWithoutExtension(path);
if (fileName.Equals("index", PathUtility.PathComparison))
{
return ".";
var i = url.LastIndexOf('/');
return i >= 0 ? url.Substring(0, i + 1) : ".";
}
if (url.EndsWith("/index", PathUtility.PathComparison))
if (json)
{
return url.Substring(0, url.Length - 5);
var i = url.LastIndexOf('.');
return i >= 0 ? url.Substring(0, i) : url;
}
return url;
}
return url;
default:
Expand Down
10 changes: 5 additions & 5 deletions src/docfx/lib/JsonUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace Microsoft.Docs.Build
/// </summary>
internal static class JsonUtility
{
public static readonly JsonSerializer DefaultDeserializer = new JsonSerializer
public static readonly JsonSerializer DefaultSerializer = new JsonSerializer
{
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new JsonContractResolver(),
Expand Down Expand Up @@ -182,7 +182,7 @@ public static (List<Error>, object) ToObject(
var serializer = new JsonSerializer
{
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = DefaultDeserializer.ContractResolver,
ContractResolver = DefaultSerializer.ContractResolver,
};
serializer.Error += HandleError;
var value = token.ToObject(type, serializer);
Expand Down Expand Up @@ -349,7 +349,7 @@ private static void ReportUnknownFields(this JToken token, List<Error> errors, T

private static Type GetCollectionItemTypeIfArrayType(Type type)
{
var contract = DefaultDeserializer.ContractResolver.ResolveContract(type);
var contract = DefaultSerializer.ContractResolver.ResolveContract(type);
if (contract is JsonObjectContract)
{
return type;
Expand All @@ -371,7 +371,7 @@ private static Type GetCollectionItemTypeIfArrayType(Type type)

private static Type GetNestedTypeAndCheckForUnknownField(Type type, JProperty prop, List<Error> errors)
{
var contract = DefaultDeserializer.ContractResolver.ResolveContract(type);
var contract = DefaultSerializer.ContractResolver.ResolveContract(type);

if (contract is JsonObjectContract objectContract)
{
Expand All @@ -396,7 +396,7 @@ private static Type GetNestedTypeAndCheckForUnknownField(Type type, JProperty pr

private static JsonPropertyCollection GetPropertiesFromJsonArrayContract(JsonArrayContract arrayContract)
{
var itemContract = DefaultDeserializer.ContractResolver.ResolveContract(arrayContract.CollectionItemType);
var itemContract = DefaultSerializer.ContractResolver.ResolveContract(arrayContract.CollectionItemType);
if (itemContract is JsonObjectContract objectContract)
return objectContract.Properties;
else if (itemContract is JsonArrayContract contract)
Expand Down
38 changes: 22 additions & 16 deletions test/docfx.Test/build/DocumentTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,36 @@ namespace Microsoft.Docs.Build
public static class DocumentTest
{
[Theory]
[InlineData("docfx.yml", ContentType.Unknown, "docfx.yml", "/docfx.yml", "docfx.yml")]
[InlineData("docfx.json", ContentType.Unknown, "docfx.json", "/docfx.json", "docfx.json")]
[InlineData("a.md", ContentType.Page, "a.json", "/a", "a")]
[InlineData("a/b.md", ContentType.Page, "a/b.json", "/a/b", "a/b")]
[InlineData("index.md", ContentType.Page, "index.json", "/", ".")]
[InlineData("a/index.md", ContentType.Page, "a/index.json", "/a/", "a/")]
[InlineData("a.yml", ContentType.Page, "a.json", "/a", "a")]
[InlineData("a/index.yml", ContentType.Page, "a/index.json", "/a/", "a/")]
[InlineData("TOC.md", ContentType.TableOfContents, "TOC.json", "/TOC.json", "TOC.json")]
[InlineData("TOC.yml", ContentType.TableOfContents, "TOC.json", "/TOC.json", "TOC.json")]
[InlineData("TOC.json", ContentType.TableOfContents, "TOC.json", "/TOC.json", "TOC.json")]
[InlineData("image.png", ContentType.Resource, "image.png", "/image.png", "image.png")]
[InlineData("a&#/b\\.* d.png", ContentType.Resource, "a&#/b\\.* d.png", "/a&#/b/.* d.png", "a&#/b/.* d.png")]
[InlineData("docfx.yml", true, false, ContentType.Unknown, "docfx.yml", "/docfx.yml", "docfx.yml")]
[InlineData("docfx.json", true, false, ContentType.Unknown, "docfx.json", "/docfx.json", "docfx.json")]
[InlineData("a.md", true, false, ContentType.Page, "a.json", "/a", "a")]
[InlineData("a/b.md", true, false, ContentType.Page, "a/b.json", "/a/b", "a/b")]
[InlineData("index.md", true, false, ContentType.Page, "index.json", "/", ".")]
[InlineData("a/index.md", true, false, ContentType.Page, "a/index.json", "/a/", "a/")]
[InlineData("a.yml", true, false, ContentType.Page, "a.json", "/a", "a")]
[InlineData("a/index.yml", true, false, ContentType.Page, "a/index.json", "/a/", "a/")]
[InlineData("TOC.md", true, false, ContentType.TableOfContents, "TOC.json", "/TOC.json", "TOC.json")]
[InlineData("TOC.yml", true, false, ContentType.TableOfContents, "TOC.json", "/TOC.json", "TOC.json")]
[InlineData("TOC.json", true, false, ContentType.TableOfContents, "TOC.json", "/TOC.json", "TOC.json")]
[InlineData("image.png", true, false, ContentType.Resource, "image.png", "/image.png", "image.png")]
[InlineData("a&#/b\\.* d.png", true, false, ContentType.Resource, "a&#/b\\.* d.png", "/a&#/b/.* d.png", "a&#/b/.* d.png")]
[InlineData("a.md", false, false, ContentType.Page, "a/index.html", "/a/", "a/")]
[InlineData("a.md", false, true, ContentType.Page, "a.html", "/a.html", "a.html")]
[InlineData("a/index.md", false, false, ContentType.Page, "a/index.html", "/a/", "a/")]
[InlineData("a/index.md", false, true, ContentType.Page, "a/index.html", "/a/", "a/")]
internal static void FilePathToUrl(
string path,
bool json,
bool uglifyUrl,
ContentType expectedContentType,
string expectedSitePath,
string expectedSiteUrl,
string expectedRelativeSiteUrl)
{
Assert.Equal(expectedContentType, Document.GetContentType(path));
Assert.Equal(expectedSitePath, Document.FilePathToSitePath(path, expectedContentType, null));
Assert.Equal(expectedSiteUrl, Document.PathToAbsoluteUrl(expectedSitePath, expectedContentType, null));
Assert.Equal(expectedRelativeSiteUrl, Document.PathToRelativeUrl(expectedSitePath, expectedContentType, null));
Assert.Equal(expectedSitePath, Document.FilePathToSitePath(path, expectedContentType, null, json, uglifyUrl));
Assert.Equal(expectedSiteUrl, Document.PathToAbsoluteUrl(expectedSitePath, expectedContentType, null, json));
Assert.Equal(expectedRelativeSiteUrl, Document.PathToRelativeUrl(expectedSitePath, expectedContentType, null, json));
}
}
}
Loading