//Finds fragment placeholders in the template and insert their contents //Fragments are placeholders that start with an asterisk("*") to indicate that instead of finding the value we should //look for a file with the same name as the current one and with the indicated suffix in their name. //If two (.md and .mdh) files exist with that name, the one with the same extension as the current file gets precedence //They allow to insert "fragments" of the same resulting file in the template positions we want. //The placeholders that they contain will be later parsed and processed as normal fields in the file private static string InjectFragments(string layoutHtml, MarkdownFile md, HttpContext ctx) { string tempContent = layoutHtml; string[] fragments = TemplatingHelper.GetAllPlaceHolderNames(tempContent, phPrefix: FILE_FRAGMENT_PREFIX); foreach (string fragmentName in fragments) { string fragmentContent = string.Empty; //Default empty value string fragmentFileName = ctx.Server.MapPath(Path.GetFileNameWithoutExtension(md.FileName) + fragmentName.Substring(FILE_FRAGMENT_PREFIX.Length)); //Removing the "*" at the beginning //Test if a file the same file extension exists if (File.Exists(fragmentFileName + md.FileExt)) { fragmentFileName += md.FileExt; } else //Try with the other file extension (.md or .mdh depending of the current file's extension) { fragmentFileName += (md.FileExt == MarkdownFile.MARKDOWN_DEF_EXT) ? MarkdownFile.HTML_EXT : MarkdownFile.MARKDOWN_DEF_EXT; } //Try to read the file with fragment try { md.AddFileDependency(fragmentFileName); MarkdownFile mdFld = new MarkdownFile(fragmentFileName); //Render the file in the context of the parent file, no its own fragmentContent = RenderLiquidTags(mdFld.RawContent, md); //Render tags in raw content (Markdown or HTML) //If it's Markdown, convert to HTML before substitution if (md.FileExt == MarkdownFile.MARKDOWN_DEF_EXT) { fragmentContent = ConvertMarkdown2Html(fragmentContent, md.UseEmoji, md.EnabledMDExtensions); } } catch { //If something is wrong (normally the file does not exist) simply return an empty string //We don't want to force this kind of files to always exist fragmentContent = string.Empty; } //Replace the placeholder with the value tempContent = TemplatingHelper.ReplacePlaceHolder(tempContent, fragmentName, fragmentContent); } return(tempContent); }
//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); }
/// <summary> /// Allows a custom tag or FM source to add a new file or folder dependency to the current MD or MDH file caching /// </summary> /// <param name="filePath">The full path to the file or folder that the current file depends on</param> public void AddFileDependency(string filePath) { _md.AddFileDependency(filePath); }