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
17 changes: 12 additions & 5 deletions docs/source/templates/index.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,26 @@ Disponibles para todas las plantillas están las siguientes funciones:
- Este es el contenido HTML principal que se está renderizando en esta plantilla. Debes generar esto a través del método `Raw` ya que probablemente contendrá HTML.
- `@Model.GetMeta("title")`
- con esto puedes acceder a cualquier valor en el front matter (por ejemplo, `title`).
- `@await PartialAsync("NombrePlantilla", "valor del modelo")`
- `@await PartialAsync("NombrePlantilla", Model)`
- Esto te permite renderizar sub plantillas.
- el parámetro del nombre de plantilla coincidirá con el nombre del archivo razor... así que en el ejemplo anterior, estaría buscando un archivo llamado `Templates/NombrePlantilla.cshtml`
- el segundo parámetro puede ser cualquier tipo de objeto realmente, lo que sea que la plantilla en cuestión esté esperando.
- el segundo parámetro es el modelo a pasar a la plantilla (típicamente `Model` para pasar el modelo actual).
- **Nota:** Los nombres de plantilla son relativos a `templates_folder`, así que los referencias sin el prefijo `templates/`. Por ejemplo, usa `"TranslationWarning"` no `"templates/TranslationWarning"` (aunque esto último funcionará como respaldo).
- `@await PartialAsync("ruta")`
- **Enrutamiento inteligente basado en extensión de archivo:**
- Extensión `.cshtml` → renderiza como plantilla Razor con el `Model` actual
- Extensión `.md` → renderiza como archivo markdown (puede definir su propia plantilla, por defecto `Default`, sin `SiteFrame.cshtml`)
- Sin extensión → verifica si existe una plantilla, de lo contrario trata como ruta de archivo markdown
- Ejemplos:
- `@await PartialAsync("topNav")` → renderiza plantilla si existe `topNav.cshtml`
- `@await PartialAsync("source/menu.md")` → renderiza archivo markdown
- `@await PartialAsync("template/nav.cshtml")` → renderiza explícitamente como plantilla
- Colección `@Model.Headers`
- Cada `h1`-`h6` será parseado y agregado a esta colección de headers.
- Cada objeto `Header` tiene las siguientes propiedades:
- Level: el valor numérico del header
- Value: el texto
- Slug: una versión slug del `Value`, que corresponde a un ancla agregada al HTML antes del header.
- `@await PartialAsync("Docs/area/menu.md")`
- Esta sobrecarga del método `PartialAsync` asume que estás renderizando un archivo markdown.
- El archivo markdown puede definir su propia plantilla (por defecto será `Default`), y solo renderizará los contenidos de esa plantilla específica (es decir, no `SiteFrame.cshtml`).

## Métodos de URL y Recursos

Expand Down
19 changes: 13 additions & 6 deletions docs/source/templates/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,26 @@ Available to all templates are the following functions:
- This is the main HTML content being rendered in this template. You should output this through the `Raw` method since it will likely contain HTML.
- `@Model.GetMeta("title")`
- with this you can access any value in the front matter (for example, `title`).
- `@await PartialAsync("TemplateName", "model value")`
- `@await PartialAsync("TemplateName", Model)`
- This lets you render sub templates.
- the template name parameter will match the razor fil.e name ... so in the example above, it would be looking for a file named `Templates/TemplateName.cshtml`
- the second parameter can be any kind of object really, whatever the template in question is expecting.
- the template name parameter will match the razor file name ... so in the example above, it would be looking for a file named `Templates/TemplateName.cshtml`
- the second parameter is the model to pass to the template (typically `Model` to pass the current model).
- **Note:** Template names are relative to the `templates_folder`, so you reference them without the `templates/` prefix. For example, use `"TranslationWarning"` not `"templates/TranslationWarning"` (though the latter will work as a fallback).
- `@await PartialAsync("path")`
- **Smart routing based on file extension:**
- `.cshtml` extension → renders as a Razor template with current `Model`
- `.md` extension → renders as markdown file (can define its own template, defaults to `Default`, no `SiteFrame.cshtml`)
- No extension → checks if a template exists, otherwise treats as markdown file path
- Examples:
- `@await PartialAsync("topNav")` → renders template if `topNav.cshtml` exists
- `@await PartialAsync("source/menu.md")` → renders markdown file
- `@await PartialAsync("template/nav.cshtml")` → explicitly renders template
- `@Model.Headers` collection
- Each `h1`-`h6` will be parsed and added to this headers collection.
- Every `Header` object has the following properties:
- Level: the numeric value of the header
- Value: the text
- Slug: a slug version of the `Value`, which corresponds to an anchor added to the HTML before the header.
- `@await PartialAsync("Docs/area/menu.md")`
- This overload of the `PartialAsync` method assumes you're rendering a markdown file.
- The markdown file can define its own template (will default to `Default`), and will only render the contents of that specific template (ie. no `SiteFrame.cshtml`).

## URL and Asset Methods

Expand Down
19 changes: 18 additions & 1 deletion toolsrc/Chloroplast.Core/Rendering/ChloroplastTemplateBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,24 @@ protected Task<RawString> PartialAsync<K>(string templateName, K model)
return RazorRenderer.Instance.RenderTemplateContent (templateName, model);
}

protected async Task<RawString> PartialAsync(string menuPath)
protected async Task<RawString> PartialAsync(string path)
{
// Route based on file extension to determine intent
var extension = System.IO.Path.GetExtension(path).ToLowerInvariant();

// If .cshtml or no extension and a template exists, render as template
if (extension == ".cshtml" ||
(string.IsNullOrEmpty(extension) && RazorRenderer.Instance.TemplateExists(path)))
{
// Render as Razor template with current Model
return await PartialAsync(path, Model);
}

// Otherwise, render as markdown file (includes .md or paths to content files)
return await RenderMarkdownPartialAsync(path);
}

private async Task<RawString> RenderMarkdownPartialAsync(string menuPath)
{
string fullMenuPath;

Expand Down
92 changes: 82 additions & 10 deletions toolsrc/Chloroplast.Core/Rendering/RazorRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,34 @@ public class RazorRenderer
public static RazorRenderer Instance;

Dictionary<string, TemplateDescriptor> templates = new Dictionary<string, TemplateDescriptor> ();
string templatesFolderPath;
//TemplateEngine engine = new TemplateEngine ();

public async Task AddTemplateAsync (string templatePath)
public async Task AddTemplateAsync (string templatePath, string templatesFolderPath)
{
string fileName = Path.GetFileNameWithoutExtension (templatePath);
if (!templates.ContainsKey (fileName))

// Calculate the relative path from templates folder
string relativePath = Path.GetRelativePath(templatesFolderPath, templatePath);
// Normalize to forward slashes and remove .cshtml extension
string relativeKey = relativePath.Replace('\\', '/').Replace(".cshtml", "");

// Store with both the relative path and the filename for backward compatibility
var compiledTemplate = Razor.Compile (await File.ReadAllTextAsync (templatePath));

// Store by relative path (e.g., "template/topNav")
if (!templates.ContainsKey (relativeKey))
{
Chloroplast.Core.Loaders.EcmaXml.Namespace ns = new Chloroplast.Core.Loaders.EcmaXml.Namespace ();
Console.WriteLine (ns.ToString ());
templates[fileName] = Razor.Compile (await File.ReadAllTextAsync (templatePath));
templates[relativeKey] = compiledTemplate;
}

// Also store by filename only for backward compatibility (e.g., "topNav")
// But only if there's no conflict
if (!templates.ContainsKey (fileName))
{
templates[fileName] = compiledTemplate;
}
}

Expand All @@ -35,13 +53,13 @@ public async Task InitializeAsync (IConfigurationRoot config)
templateFolderSetting = "templates";

// Rely on CombinePath/NormalizePath to handle absolute/relative + separator normalization
string fullTemplatePath = rootPath
templatesFolderPath = rootPath
.CombinePath(templateFolderSetting)
.NormalizePath();

foreach (var razorPath in Directory.EnumerateFiles (fullTemplatePath, "*.cshtml", SearchOption.AllDirectories))
foreach (var razorPath in Directory.EnumerateFiles (templatesFolderPath, "*.cshtml", SearchOption.AllDirectories))
{
await this.AddTemplateAsync (razorPath);
await this.AddTemplateAsync (razorPath, templatesFolderPath);
}

// danger will robinson ...
Expand Down Expand Up @@ -70,7 +88,8 @@ public async Task<string> RenderContentAsync (FrameRenderedContent parsed)

public async Task<RawString> RenderTemplateContent<T> (string templateName, T model)
{
if (!templates.TryGetValue(templateName, out var template))
var template = FindTemplate(templateName);
if (template == null)
{
// Template not found - log warning and return empty string instead of throwing
Console.ForegroundColor = ConsoleColor.Yellow;
Expand All @@ -81,6 +100,59 @@ public async Task<RawString> RenderTemplateContent<T> (string templateName, T mo
return new RawString (await template.RenderAsync (model));
}

public bool TemplateExists(string templateName)
{
return FindTemplate(templateName) != null;
}

private TemplateDescriptor FindTemplate(string templateName)
{
// Try multiple lookup strategies to find the template

// 1. Try exact match (could be relative path like "template/topNav")
if (templates.TryGetValue(templateName, out var template))
{
return template;
}

// 2. Try with .cshtml extension if not already present
if (!templateName.EndsWith(".cshtml"))
{
string withExtension = templateName + ".cshtml";
if (templates.TryGetValue(withExtension.Replace('\\', '/').Replace(".cshtml", ""), out template))
{
return template;
}
}

// 3. Try removing .cshtml if present
if (templateName.EndsWith(".cshtml"))
{
string withoutExtension = templateName.Substring(0, templateName.Length - 7);
if (templates.TryGetValue(withoutExtension.Replace('\\', '/'), out template))
{
return template;
}
}

// 4. Normalize path separators and try again
string normalizedName = templateName.Replace('\\', '/');
if (templates.TryGetValue(normalizedName, out template))
{
return template;
}

// 5. Last resort: if path starts with "templates/", strip it and try all lookups again
// This helps users who mistakenly include the templates folder in their path
if (normalizedName.StartsWith("templates/", StringComparison.OrdinalIgnoreCase))
{
string withoutTemplatesPrefix = normalizedName.Substring("templates/".Length);
return FindTemplate(withoutTemplatesPrefix); // Recursive call with stripped path
}

return null;
}

public async Task<string> RenderContentAsync (RenderedContent parsed)
{
try
Expand All @@ -94,10 +166,10 @@ public async Task<string> RenderContentAsync (RenderedContent parsed)
if (parsed.Metadata.ContainsKey ("layout"))
templateName = parsed.Metadata["layout"];

TemplateDescriptor template;
TemplateDescriptor template = FindTemplate(templateName);

if (!templates.TryGetValue (templateName, out template))
template = templates[defaultTemplateName];
if (template == null)
template = FindTemplate(defaultTemplateName);

// Render template
var result = await template.RenderAsync (parsed);
Expand Down
Loading
Loading