/// <summary> /// Gets the relative path of the template to use with the current file taking into account all the possible parameters/fields that control this setting /// </summary> /// <returns></returns> private static string GetCurrentTemplateFile(MarkdownFile md) { //Get the template name that is going to be used (Front Matter or configuration), if any. string templateName = md.TemplateName; if (string.IsNullOrEmpty(templateName) || templateName.ToLowerInvariant() == "none") { return(string.Empty); //Use the default basic HTML5 template } if (templateName.ToLowerInvariant() == "raw") { return("raw"); //Use raw contents, without any wrapping HTML tags } //The name (or sub-path) for the layout file (.html normaly) to be used string layoutName = md.Layout; if (string.IsNullOrEmpty(layoutName)) { return(string.Empty); //Use the default basic HTML5 template } //If both the template folder and the layout are established, then get the base folder for the templates //This base path for the templates parameter is only available through Web.config. NOT in the file Front Matter (we're skipping the file in the following call) string basePath = FieldValuesHelper.GetFieldValue("TemplatesBasePath", null, "~/Templates/"); return(VirtualPathUtility.AppendTrailingSlash(basePath) + VirtualPathUtility.AppendTrailingSlash(templateName) + layoutName); }
/// <summary> /// Gets any property for the file, specificaly defined (such as Title, Tags Categories...) or from the Front-Matter /// It won't process any special FM value such as File Processing Fields or Custom Front Matter Values /// </summary> /// <param name="fieldName"></param> /// <returns></returns> public override object this[object fieldName] { get { object res = base[fieldName]; if (res == null) { res = FieldValuesHelper.GetFieldObject(fieldName.ToString(), _md, null); } return(res); } }
/// <summary> /// Gets if a specified property for the file exists or not, specificaly defined (such as Title, Tags Categories...) or from the Front-Matter /// It won't process any special FM value such as File Processing Fields or Custom Front Matter Values /// </summary> /// <param name="fieldName"></param> /// <returns></returns> public override bool ContainsKey(object fieldName) { bool hasProp = base.ContainsKey(fieldName); if (!hasProp) { object propVal = FieldValuesHelper.GetFieldObject(fieldName.ToString(), _md, null); if (propVal != null) { hasProp = true; } } return(hasProp); }
//Registers all custom Front-Matter sources inside the assembly passed as a parameter private static void RegisterCustomFMSourcesInAssembly(Assembly assembly) { //Custom FM sources are obtained from classes in the FMSources namespace that implement the IFMSource interface var fmSources = from c in assembly.GetTypes() where c.IsClass && c.Namespace == CUSTOM_FMSOURCES_NAMESPACE && typeof(IFMSource).IsAssignableFrom(c) select c; //Register each FMSource globally using its factory method (GetFilterType) fmSources.ToList().ForEach(fmSourceClass => { IFMSource fms = (IFMSource)Activator.CreateInstance(fmSourceClass); //Register possible fields that will define different caches for the file if (fms is IQueryStringDependent) { MarkdownFile.AddCachingQueryStringFields( (fms as IQueryStringDependent).GetCachingQueryStringFields() ); } FieldValuesHelper.AddFrontMatterSource(fms.SourceName, fms.GetType()); }); }
private static readonly string FILE_FRAGMENT_PREFIX = "*"; //How to identify fragments placeholders in content files #endregion #region Constructor static Renderer() { //Dynamically setup and add to DotLiquid template processor all the new custom tags, filters and FM sources RegisterCustomExtensions(); //Configure DotLiquid (once) //Check if the CSharp Naming convention is to be used (RubyNamingConvention by default) //naming: csharp in the root web.config //(this is only set once for the whole application because it's an static property //of the DotLiquid template rendering engine) if (FieldValuesHelper.GetFieldValue("naming", null, "ruby") == "csharp") { Template.NamingConvention = new DotLiquid.NamingConventions.CSharpNamingConvention(); } else { Template.NamingConvention = new DotLiquid.NamingConventions.RubyNamingConvention(); } //Check which date formatting to use (Ruby/strftime or C#, C# by default) //DateFormat parameter Liquid.UseRubyDateFormat = (FieldValuesHelper.GetFieldValue("dateformat", null, "csharp") == "ruby"); }
//Retrieves the value for the specified field or returns an empty string if it doesn't exist protected override object GetValue(string name) { object res = ""; //Default value (empty string) switch (name.ToLowerInvariant()) { //Check well Known fields first case INTERNAL_REFERENCE_TO_CURRENT_FILE: //This is intended to be used internally only, from custom tags or front-matter custom sources res = _mdProxy; break; case "content": //The final HTML content, WITHOUT the template and WITH liquid tags processed res = _parentFile.RawFinalHtml; //This is needed to avoid that the Markdown conversion messes up with the liquid tags (loops, conditionals...) break; case "title": res = _parentFile.Title; break; case "excerpt": case "description": case "summary": res = _parentFile.Excerpt; break; case "filename": res = _parentFile.FileName; break; case "filenamenoext": res = _parentFile.FileNameNoExt; break; case "fileext": res = _parentFile.FileExt; break; case "dir": res = _mdProxy.Dir; break; case "date": res = _parentFile.Date; break; case "datecreated": res = _parentFile.DateCreated; break; case "datemodified": res = _parentFile.DateLastModified; break; case "isauthenticated": res = _ctx.User.Identity.IsAuthenticated; break; case "authtype": res = _ctx.User.Identity.AuthenticationType; break; case "username": res = _ctx.User.Identity.Name; break; case "domain": res = _ctx.Request.Url.Authority; break; case "baseurl": res = $"{_ctx.Request.Url.Scheme}{System.Uri.SchemeDelimiter}{_ctx.Request.Url.Authority}"; break; case "clientip": res = WebHelper.GetIPAddress(); break; case "now": case "today": res = DateTime.Now; break; case "time": res = DateTime.Now.ToString("hh:mm:ss tt"); break; case "url": res = _ctx.Request.RawUrl; break; case "urlnoext": res = IOHelper.RemoveFileExtension(_ctx.Request.RawUrl); //Files processed by MIIS always have extension on disk //res = _ctx.Request.Path.Remove(_ctx.Request.Path.LastIndexOf(".")); break; case "templatename": res = _parentFile.TemplateName; break; case "layout": res = _parentFile.Layout; break; //Custom fields default: Exception exToBeRaised = null; //Possible exceptions raised by the next code //Check if the custom field has already been retrieved before bool isCached = InternalFileFieldCache.TryGetValue(name, out res); //If it's cached the value will be saved to res if (!isCached) //If it's not cached (has not been retrieved before) then retrieve it { res = string.Empty; //Default value /* * There are 4 types of custom fields: * - Value fields: {{name}} -> Get a value from the Front-Matter or from web.config -> Simply replace them (default assumption) * - File processing fields (FPF), {{name}} whose value ends in .md or .mdh. ej: myfile.md -> if available the file is read and it's contents transformed into HTML take the place of the placeholder * Useful for menus, and other independent parts in custom templates and parts of the same page. * - Custom Dinamic Field Sources, {{name}} value that start with !! and use a custom class to populate the field with an object. Ej: !!customSource param1 param2 * - Querystring or Form fields, retrieved from the current request */ //Try to get a typed value for the field var typedValue = FieldValuesHelper.GetFieldObject(name, _parentFile, null); //If we get a string already, then process it if (typedValue is string) { //If it's a string, process it for special fields ////////////////////////////// //Simple value fields (default value if present) ////////////////////////////// string resAsString = typedValue.ToString(); ////////////////////////////// //First, Custom Dinamic Field Sources that provide values from external assemblies ////////////////////////////// if (resAsString.StartsWith(FRONT_MATTER_SOURCES_PREFIX)) { //Get the name of the source and it's params splitting the string (the first element would be the name of the source, and the rest, the parameters, if any string[] srcelements = resAsString.Substring(FRONT_MATTER_SOURCES_PREFIX.Length).Trim().Split(new char[0], StringSplitOptions.RemoveEmptyEntries); if (srcelements.Length > 0) { try { res = FieldValuesHelper.GetFieldValueFromFMSource(srcelements[0], _mdProxy, srcelements.Skip(1).ToArray()); } catch (Exception ex) { //Throw the exception to be reflected in the exToBeRaised = ex; } } } ////////////////////////////// //Second, File Processing Fields, that inject the content of .md or .mdh processing their inner fields in their own context //This is for compatbility reasons with MIIS v1.x and 2.x ////////////////////////////// else if (resAsString.ToLowerInvariant().EndsWith(MarkdownFile.MARKDOWN_DEF_EXT) || resAsString.ToLowerInvariant().EndsWith(MarkdownFile.HTML_EXT)) { //This kind of fields can only be processed in the first level of liquid tags processing. //MustProcessSubFiles is false in the second level //If this is a second level (a FPF inside another FPF) will just return an empty string (won't be processed at all) if (_parentFile.MustProcessSubFiles) { try { string fpfPath = _ctx.Server.MapPath(resAsString); //The File-Processing Field path MarkdownFile insertedFile = new MarkdownFile(fpfPath, false); //No processing of FPF beyond this layer is allowed to prevent circular references //If the parent file is a Markdown file if (_parentFile.FileExt == MarkdownFile.MARKDOWN_DEF_EXT) { //HACK: Since {{field}} placeholders are processed BEFORE transforming into HTML //I need to wrap the injected HTML into a special tag to prevent further processing of the resulting HTML when converting //the main file contents to HTML. The reason is that mixed HTML is very tricky and can lead to unexpected results //This is a sloppy way to do it, but it's the only way to mark a section of markdown as "not processable" //See: https://github.com/lunet-io/markdig/blob/master/src/Markdig.Tests/Specs/CommonMark.md#html-blocks //Later we need to remove these delimiters in a sloppy way too :-( res = HTML_NO_PROCESSING_DELIMITER_BEGIN + insertedFile.ComponentHtml + HTML_NO_PROCESSING_DELIMITER_END; } else { //If the parent file is already an HTML file, there's no need for the previous hack res = insertedFile.ComponentHtml; //Use the final HTML (WITHOUT the template (except in components) and WITH the liquid tags processed in its own context) } //Add the processed file to the dependencies of the currently processed content file, //so that the file is invalidated when the FPF changes (if caching is enabled) //The FPF is already cached too if caching is enabled _parentFile.AddFileDependency(fpfPath); } catch (System.Security.SecurityException) { res = String.Format("Can't access file for {0}", TemplatingHelper.PLACEHOLDER_PREFIX + name + TemplatingHelper.PLACEHOLDER_SUFFIX); } catch (System.IO.FileNotFoundException) { res = String.Format("File not found for {0}", TemplatingHelper.PLACEHOLDER_PREFIX + name + TemplatingHelper.PLACEHOLDER_SUFFIX); } catch (Exception ex) { //This should only happen while testing, never in production, so I send the exception's message res = String.Format("Error loading {0}: {1}", TemplatingHelper.PLACEHOLDER_PREFIX + name + TemplatingHelper.PLACEHOLDER_SUFFIX, ex.Message); } } } ////////////////////////////// //Finally, if it's not a custom source, or a FPF, then is a normal raw string value ////////////////////////////// else { res = resAsString; } } else { //If we already got a typed value, then return it by default res = typedValue; //Check if it's null. If it is null, means it hasn't been found in he fields for the file or app, //so maybe the last chance is that it's a request param if (typedValue == null) { ////////////////////////////// //Try to determine the param value from the querystring or the form values ////////////////////////////// if (!string.IsNullOrWhiteSpace(_ctx.Request.Params[name])) { var paramVal = _ctx.Request.Params[name]; //Result (checks qs, form data, cookies and sv, in that order) if (!string.IsNullOrWhiteSpace(paramVal)) { res = paramVal; //A value has been found _parentFile.CachingEnabled = false; //Disable caching if a param is to be used } } } } //Cache the retrieved value InternalFileFieldCache[name] = res; //If there's been an exception, raise it to inform the renderer about it if (exToBeRaised != null) { throw exToBeRaised; } } //Get out of the switch construction break; } //Return retrieved value return(res); }