private Task<Errorable> processDoctypeElement(RenderState st) { // <cms-doctype type="html" /> // generates // <!DOCTYPE html> var xr = st.Reader; if (!xr.IsEmptyElement) { st.Error("cms-doctype must be an empty element"); st.SkipElementAndChildren("cms-doctype"); return Task.FromResult(Errorable.NoErrors); } if (!xr.MoveToAttribute("type")) { st.Error("cms-doctype must have a type attribute"); xr.MoveToElement(); return Task.FromResult(Errorable.NoErrors); } string type = xr.Value; xr.MoveToElement(); if (type == "html") { // HTML5 doctype: st.Writer.Append("<!DOCTYPE html>\r\n\r\n"); return Task.FromResult(Errorable.NoErrors); } else { st.Error("cms-doctype has unknown type value '{0}'", type); return Task.FromResult(Errorable.NoErrors); } }
private async Task<Errorable> processScheduledElement(RenderState st) { // Specifies that content should be scheduled for the entire month of August // and the entire month of October but NOT the month of September. // 'from' is inclusive date/time. // 'to' is exclusive date/time. // <cms-scheduled> // <range from="2011-08-01 00:00 -0500" to="2011-09-01 00:00 -0500" /> // <range from="2011-10-01 00:00 -0500" to="2011-11-01 00:00 -0500" /> // <content> // content to show if scheduled (recursively including other cms- elements) // </content> // [optional:] // <else> // content to show if not scheduled (recursively including other cms- elements) // </else> // </cms-scheduled> bool displayContent = false; bool hasRanges = false; bool hasContent = false; bool hasElse = false; XmlTextReader xr = st.Reader; int knownDepth = xr.Depth; while (xr.Read() && xr.Depth > knownDepth) { if (xr.NodeType != XmlNodeType.Element) continue; if (xr.LocalName == "range") { hasRanges = true; if (!xr.IsEmptyElement) { st.Error("'range' element must be empty"); // Skip to end of cms-scheduled element and exit. st.SkipElementAndChildren("range"); continue; } // If we're already good to display, don't bother evaluating further schedule ranges: if (displayContent) // Safe to continue here because the element is empty; no more to parse. continue; string fromAttr, toAttr; // Validate the element's form: if (!xr.HasAttributes) st.Error("range element must have attributes"); if ((fromAttr = xr.GetAttribute("from")) == null) st.Error("'range' element must have 'from' attribute"); // 'to' attribute is optional: toAttr = xr.GetAttribute("to"); // Parse the dates: DateTimeOffset fromDate, toDateTmp; DateTimeOffset toDate; if (!DateTimeOffset.TryParse(fromAttr, out fromDate)) { st.Error("could not parse 'from' attribute as a date/time"); continue; } if (!String.IsNullOrWhiteSpace(toAttr)) { if (DateTimeOffset.TryParse(toAttr, out toDateTmp)) toDate = toDateTmp; else { st.Error("could not parse 'to' attribute as a date/time"); continue; } } else { toDate = st.Engine.ViewDate; } // Validate the range's dates are ordered correctly: if (toDate <= fromDate) st.Error("'to' date must be later than 'from' date or empty"); // Check the schedule range: displayContent = (st.Engine.ViewDate >= fromDate && st.Engine.ViewDate < toDate); } else if (xr.LocalName == "content") { if (hasElse) { st.Error("'content' element must come before 'else' element"); st.SkipElementAndChildren("content"); if (!xr.IsEmptyElement) xr.ReadEndElement(/* "content" */); continue; } if (hasContent) { st.Error("only one 'content' element may exist in cms-scheduled"); st.SkipElementAndChildren("content"); if (!xr.IsEmptyElement) xr.ReadEndElement(/* "content" */); continue; } hasContent = true; if (!hasRanges) { st.Error("no 'range' elements found before 'content' element in cms-scheduled"); displayContent = false; } if (displayContent) { // Stream the inner content into the StringBuilder until we get back to the end </content> element. await st.CopyElementChildren("content").ConfigureAwait(continueOnCapturedContext: false); if (!xr.IsEmptyElement) xr.ReadEndElement(/* "content" */); } else { // Skip the inner content entirely: st.SkipElementAndChildren("content"); if (!xr.IsEmptyElement) xr.ReadEndElement(/* "content" */); } } else if (xr.LocalName == "else") { if (!hasContent) { st.Error("'content' element must come before 'else' element"); st.SkipElementAndChildren("else"); if (!xr.IsEmptyElement) xr.ReadEndElement(/* "else" */); continue; } if (hasElse) { st.Error("only one 'else' element may exist in cms-scheduled"); st.SkipElementAndChildren("else"); if (!xr.IsEmptyElement) xr.ReadEndElement(/* "else" */); continue; } hasElse = true; if (!hasRanges) { st.Error("no 'range' elements found before 'else' element in cms-scheduled"); st.SkipElementAndChildren("else"); if (!xr.IsEmptyElement) xr.ReadEndElement(/* "else" */); continue; } if (!displayContent) { // Stream the inner content into the StringBuilder until we get back to the end </content> element. await st.CopyElementChildren("else").ConfigureAwait(continueOnCapturedContext: false); if (!xr.IsEmptyElement) xr.ReadEndElement(/* "else" */); } else { // Skip the inner content entirely: st.SkipElementAndChildren("else"); if (!xr.IsEmptyElement) xr.ReadEndElement(/* "else" */); } } else { st.Error("unexpected element '{0}'", xr.LocalName); } } // Report errors: if (!hasContent && !hasRanges) st.Error("no 'range' elements found"); if (!hasContent) st.Error("no 'content' element found"); // Skip Whitespace and Comments etc. until we find the end element: while (xr.NodeType != XmlNodeType.EndElement && xr.Read()) { } // Validate: if (xr.LocalName != "cms-scheduled") st.Error("expected end <cms-scheduled/> element"); // Don't read this element because the next `xr.Read()` in the main loop will: //xr.ReadEndElement(/* "cms-scheduled" */); return Errorable.NoErrors; }
private async Task<Errorable> processImportTemplateElement(RenderState st) { // Imports content directly from another blob, addressable by a relative path or an absolute path. // Relative path is always relative to the current blob's absolute path. // In the case of nested imports, relative paths are relative to the absolute path of the importee's parent blob. // <cms-import-template path="/template/main"> // <area id="head"> // <link rel="" /> // </area> // <area id="body"> // <div> // ... // </div> // </area> // </cms-import-template> // Absolute paths are canonicalized. An exception will be thrown if the path contains too many '..' references that // bring the canonicalized path above the root of the tree (which is impossible). // Recursively call RenderBlob on the imported blob and include the rendered HTMLFragment into this rendering. var xr = st.Reader; // st.Reader is pointing to "cms-import-template" Element. if (!xr.HasAttributes || !xr.MoveToAttribute("path")) { st.Error("cms-import-template requires a 'path' attribute"); st.SkipElementAndChildren("cms-import-template"); return Errorable.NoErrors; } string ncpath = xr.Value; st.Reader.MoveToElement(); TreePathStreamedBlob tmplBlob; // Canonicalize the absolute or relative path relative to the current item's path: var abspath = PathObjectModel.ParseBlobPath(ncpath); CanonicalBlobPath path = abspath.Collapse(abs => abs, rel => (st.Item.TreeBlobPath.Path.Tree + rel)).Canonicalize(); // Fetch the Blob given the absolute path constructed: TreeBlobPath tbp = new TreeBlobPath(st.Item.TreeBlobPath.RootTreeID, path); var etmplBlob = await st.Engine.TreePathStreamedBlobs.GetBlobByTreePath(tbp).ConfigureAwait(continueOnCapturedContext: false); if (etmplBlob.HasErrors) { st.SkipElementAndChildren("cms-import-template"); // Check if the error is a simple blob not found error: bool notFound = etmplBlob.Errors.Any(er => er is BlobNotFoundByPathError); if (notFound) { st.Error("cms-import-template could not find blob by path '{0}' off tree '{1}'", tbp.Path.ToString(), tbp.RootTreeID.ToString()); return Errorable.NoErrors; } // Error was more serious: foreach (var err in etmplBlob.Errors.Errors) { st.Error(err.Message); } return etmplBlob.Errors; } else tmplBlob = etmplBlob.Value; Debug.Assert(tmplBlob != null); // This lambda processes the entire imported template: Func<RenderState, Task<Errorable<bool>>> processElements = (Func<RenderState, Task<Errorable<bool>>>)(async sst => { // Make sure cms-template is the first element from the imported template blob: if (sst.Reader.LocalName != "cms-template") { sst.Error("cms-import-template expected cms-template as first element of imported template"); sst.SkipElementAndChildren(sst.Reader.LocalName); st.SkipElementAndChildren("cms-import-template"); return false; } // Don't move the st.Reader yet until we know the cms-import-template has a cms-template-area in it: string fillerAreaId = null; bool isFirstArea = !st.Reader.IsEmptyElement; // Create a new RenderState that reads from the parent blob and writes to the template's renderer: var stWriter = new RenderState(st.Engine, st.Item, st.Reader, sst.Writer); // This lambda is called recursively to handle cms-template-area elements found within parent cms-template-area elements in the template: Func<RenderState, Task<Errorable<bool>>> processTemplateAreaElements = null; processTemplateAreaElements = (Func<RenderState, Task<Errorable<bool>>>)(async tst => { // Only process cms-template-area elements: if (tst.Reader.LocalName != "cms-template-area") { // Use DefaultProcessElements to handle processing other cms- custom elements from the template: return await RenderState.DefaultProcessElements(tst); } // Read the cms-template-area's id attribute: if (!tst.Reader.MoveToAttribute("id")) { tst.Error("cms-template-area needs an 'id' attribute"); tst.Reader.MoveToElement(); tst.SkipElementAndChildren("cms-template-area"); return false; } // Assign the template's area id: string tmplAreaId = tst.Reader.Value; // Move to the first area if we have to: if (isFirstArea) { if (!st.Reader.IsEmptyElement) { fillerAreaId = moveToNextAreaElement(st); } isFirstArea = false; } // Do the ids match? if ((fillerAreaId != null) && (tmplAreaId == fillerAreaId)) { // Skip the cms-template-area in the template: tst.Reader.MoveToElement(); tst.SkipElementAndChildren("cms-template-area"); // Move the filler reader to the element node: st.Reader.MoveToElement(); // Copy the elements: await stWriter.CopyElementChildren("area"); // Move to the next area element, if available: fillerAreaId = moveToNextAreaElement(st); } else { // Insert the default content from the template: tst.Reader.MoveToElement(); // Recurse into children, allowing processing of embedded cms-template-areas: await tst.CopyElementChildren("cms-template-area", null, processTemplateAreaElements); } // We handled this: return false; }); // Now continue on stream-copying child elements until we find a cms-template-area: var err = await sst.CopyElementChildren("cms-template", null, processTemplateAreaElements) .ConfigureAwait(continueOnCapturedContext: false); if (err.HasErrors) return err.Errors; // We missed some <area />s in the cms-import-template: while (!((st.Reader.NodeType == XmlNodeType.EndElement) && (st.Reader.LocalName == "cms-import-template")) && !((st.Reader.NodeType == XmlNodeType.Element) && st.Reader.IsEmptyElement && (st.Reader.LocalName == "cms-import-template"))) { // Move to the next <area /> start element: fillerAreaId = moveToNextAreaElement(st); if (fillerAreaId != null) { st.Warning("area '{0}' unused by the template", fillerAreaId); st.SkipElementAndChildren("area"); } } return false; }); // Process the imported cms-template and inject the <area /> elements from the current <cms-import-template /> element: RenderState importedTemplate = new RenderState( st.Engine, tmplBlob, earlyExit: (Func<RenderState, bool>)(sst => { return false; }), processElements: processElements, previous: st ); // Render the template: var esbResult = await importedTemplate.Render().ConfigureAwait(continueOnCapturedContext: false); if (esbResult.HasErrors) { foreach (var err in esbResult.Errors) st.Error(err.Message); return esbResult.Errors; } StringBuilder sbResult = esbResult.Value; string blob = sbResult.ToString(); // Write the template to our current writer: st.Writer.Append(blob); return Errorable.NoErrors; }
private static string moveToNextAreaElement(RenderState st, bool suppressErrors = false) { // Now, back on the cms-import-template element from the parent blob, read up to the first child element: do { // Skip the opening element: if ((st.Reader.NodeType == System.Xml.XmlNodeType.Element) && (st.Reader.LocalName == "cms-import-template")) { if (!st.Reader.IsEmptyElement) continue; // Empty? return null; } // Early out case: if ((st.Reader.NodeType == System.Xml.XmlNodeType.EndElement) && (st.Reader.LocalName == "cms-import-template")) return null; if (st.Reader.NodeType == System.Xml.XmlNodeType.Element) { // Only <area /> elements are allowed within <cms-import-template />. if (st.Reader.LocalName != "area") { if (!suppressErrors) st.Error("cms-import-template may only contain 'area' elements"); st.SkipElementAndChildren(st.Reader.LocalName); return null; } // Need an 'id' attribute: if (!st.Reader.MoveToAttribute("id")) { if (!suppressErrors) st.Error("area element must have an 'id' attribute"); st.Reader.MoveToElement(); st.SkipElementAndChildren("area"); return null; } string id = st.Reader.Value; st.Reader.MoveToElement(); // Return the new area's id: return id; } } while (st.Reader.Read()); return null; }
private async Task<Errorable> processImportElement(RenderState st) { // Imports content directly from another blob, addressable by a relative path or an absolute path. // Relative path is always relative to the current blob's absolute path. // In the case of nested imports, relative paths are relative to the absolute path of the importee's parent blob. // <cms-import path="../templates/main" /> // <cms-import path="/templates/main" /> // Absolute paths are canonicalized. An exception will be thrown if the path contains too many '..' references that // bring the canonicalized path above the root of the tree (which is impossible). // Recursively call RenderBlob on the imported blob and include the rendered HTMLFragment into this rendering. // st.Reader is pointing to "cms-import" Element. if (!st.Reader.IsEmptyElement) st.Error("cms-import element must be empty"); if (st.Reader.HasAttributes && st.Reader.MoveToFirstAttribute()) { string ncpath = st.Reader.GetAttribute("path"); string blob; TreePathStreamedBlob tpsBlob; // Fetch the TreePathStreamedBlob for the given path: // Canonicalize the absolute or relative path relative to the current item's path: var abspath = PathObjectModel.ParseBlobPath(ncpath); CanonicalBlobPath path = abspath.Collapse(abs => abs, rel => (st.Item.TreeBlobPath.Path.Tree + rel)).Canonicalize(); // Fetch the Blob given the absolute path constructed: TreeBlobPath tbp = new TreeBlobPath(st.Item.TreeBlobPath.RootTreeID, path); var etpsBlob = await st.Engine.TreePathStreamedBlobs.GetBlobByTreePath(tbp).ConfigureAwait(continueOnCapturedContext: false); if (etpsBlob.HasErrors) { st.SkipElementAndChildren("cms-import"); // Check if the error is a simple blob not found error: bool notFound = etpsBlob.Errors.Any(er => er is BlobNotFoundByPathError); if (notFound) { st.Error("cms-import could not find blob by path '{0]' off tree '{1}'", tbp.Path, tbp.RootTreeID); return Errorable.NoErrors; } // Error was more serious: foreach (var err in etpsBlob.Errors) st.Error(err.Message); return etpsBlob.Errors; } else tpsBlob = etpsBlob.Value; Debug.Assert(tpsBlob != null); // Fetch the contents for the given TreePathStreamedBlob: // TODO: we could probably asynchronously load blobs and render their contents // then at a final sync point go in and inject their contents into the proper // places in each imported blob's parent StringBuilder. // Render the blob inline: RenderState rsInner = new RenderState(st.Engine, tpsBlob); var einnerSb = await rsInner.Render().ConfigureAwait(continueOnCapturedContext: false); if (einnerSb.HasErrors) { foreach (var err in einnerSb.Errors) st.Error(err.Message); return einnerSb.Errors; } blob = einnerSb.Value.ToString(); st.Writer.Append(blob); // Move the reader back to the element node: st.Reader.MoveToElement(); } return Errorable.NoErrors; }
private async Task<Errorable> processConditionalElement(RenderState st) { // <cms-conditional> // <if department="Sales">Hello, Sales dept!</if> // <elif department="Accounting">Hello, Accounting dept!</elif> // <elif department="Management">Hello, Management dept!</elif> // <else>Hello, unknown dept!</else> // </cms-conditional> //st.SkipElementAndChildren("cms-conditional"); Errorable err; ConditionalState c = ConditionalState.ExpectingIf; bool satisfied = false; bool condition = false; Dictionary<string, string> conditionVariables; int knownDepth = st.Reader.Depth; while (st.Reader.Read() && st.Reader.Depth > knownDepth) { // FIXME: should be non-whitespace check if (st.Reader.NodeType != XmlNodeType.Element) continue; switch (c) { case ConditionalState.ExpectingIf: if (st.Reader.LocalName != "if") { st.Error("expected 'if' element"); goto errored; } // Update state to expect 'elif' or 'else' elements: c = ConditionalState.ExpectingElseOrElIf; goto processCondition; case ConditionalState.ExpectingElseOrElIf: if (st.Reader.LocalName == "elif") { c = ConditionalState.ExpectingElseOrElIf; goto processCondition; } else if (st.Reader.LocalName == "else") { c = ConditionalState.ExpectingEnd; goto processElse; } else { st.Error("expected 'elif' or 'else' element"); goto errored; } case ConditionalState.ExpectingEnd: st.Error("expected </cms-conditional> end element"); break; processCondition: // Parse out the condition test variables: conditionVariables = new Dictionary<string, string>(StringComparer.Ordinal); if (st.Reader.HasAttributes && st.Reader.MoveToFirstAttribute()) { do { conditionVariables.Add(st.Reader.LocalName, st.Reader.Value); } while (st.Reader.MoveToNextAttribute()); st.Reader.MoveToElement(); } // Make sure we have at least one: if (conditionVariables.Count == 0) { st.Error("expected at least one attribute for '{0}' element", st.Reader.LocalName); goto errored; } // Make sure the branch has not already been satisfied: if (satisfied) { // Branch has already been satisfied, skip inner contents: st.SkipElementAndChildren(st.Reader.LocalName); break; } // Run the condition test variables through the evaluator chain: IConditionalEvaluator eval = evaluator; EitherAndOr? lastAndOr = null; while (eval != null) { Errorable<bool> etest = await eval.EvaluateConditional(conditionVariables); if (etest.HasErrors) return etest.Errors; bool test = etest.Value; if (lastAndOr.HasValue) { if (lastAndOr.Value == EitherAndOr.And) condition = condition && test; else condition = condition || test; } else { condition = test; } lastAndOr = eval.AndOr; eval = eval.Next; } // Now either render the inner content or skip it based on the `condition` evaluated: if (condition) { satisfied = true; // Copy inner contents: err = await st.CopyElementChildren(st.Reader.LocalName).ConfigureAwait(continueOnCapturedContext: false); if (err.HasErrors) return err.Errors; } else { // Skip inner contents: st.SkipElementAndChildren(st.Reader.LocalName); } break; processElse: if (st.Reader.HasAttributes) { st.Error("unexpected attributes on 'else' element"); goto errored; } if (satisfied) { // Skip inner contents: st.SkipElementAndChildren(st.Reader.LocalName); break; } // Copy inner contents: err = await st.CopyElementChildren(st.Reader.LocalName).ConfigureAwait(continueOnCapturedContext: false); if (err.HasErrors) return err.Errors; break; } } return Errorable.NoErrors; errored: // Keep reading to the end cms-conditional element: while (st.Reader.Read() && st.Reader.Depth > knownDepth) { } return Errorable.NoErrors; }
private async Task<Errorable> processLinkElement(RenderState st) { // A 'cms-link' is translated directly into an anchor tag with the 'path' attribute // translated and canonicalized into an absolute 'href' attribute, per system // configuration. All other attributes are copied to the anchor tag as-is. // e.g.: // <cms-link path="/absolute/path" ...>contents</cms-link> // becomes: // <a href="/content/absolute/path" ...>contents</a> // if the CMS requests are "mounted" to the /content/ root URL path. // Either relative or absolute paths are allowed for 'path': // <cms-link path="../../hello/world" target="_blank">Link text.</cms-link> // <cms-link path="/hello/world" target="_blank">Link text.</cms-link> // Set the current element depth so we know where to read up to on error: int knownDepth = st.Reader.Depth; bool isEmpty = st.Reader.IsEmptyElement; if (!st.Reader.HasAttributes) { st.Error("cms-link has no attributes"); goto errored; } bool foundPath = false; st.Writer.Append("<a"); st.Reader.MoveToFirstAttribute(); do { string value = st.Reader.Value; if (st.Reader.LocalName == "path") { foundPath = true; // Get the canonicalized blob path (from either absolute or relative): var abspath = PathObjectModel.ParseBlobPath(value); CanonicalBlobPath path = abspath.Collapse(abs => abs.Canonicalize(), rel => (st.Item.TreeBlobPath.Path.Tree + rel).Canonicalize()); // TODO: apply the reverse-mount prefix path from the system configuration, // or just toss the CanonicalBlobPath over to a provider implementation and // it can give us the final absolute URL path. st.Writer.AppendFormat(" href=\"{0}\"", path); continue; } // Append the normal attribute: st.Writer.AppendFormat(" {0}={2}{1}{2}", st.Reader.LocalName, value, st.Reader.QuoteChar); } while (st.Reader.MoveToNextAttribute()); // Jump back to the element node from the attributes: st.Reader.MoveToElement(); if (!foundPath) { // Issue a warning and append an "href='#'" attribute: st.WarningSuppressComment("expected 'path' attribute on 'cms-link' element was not found"); st.Writer.Append(" href=\"#\""); } // Self-close the <a /> if the <cms-link /> is empty: if (isEmpty) { st.Writer.Append(" />"); return Errorable.NoErrors; } // Copy the inner contents and close out the </a>. st.Writer.Append(">"); var err = await st.CopyElementChildren("cms-link").ConfigureAwait(continueOnCapturedContext: false); if (err.HasErrors) return err.Errors; st.Writer.Append("</a>"); return Errorable.NoErrors; errored: // Skip to the end of the cms-link element: if (!isEmpty) while (st.Reader.Read() && st.Reader.Depth > knownDepth) { } return Errorable.NoErrors; }