From dc520039f9c08c837a72cba71b73f3be65ec6af9 Mon Sep 17 00:00:00 2001 From: Harald Wolff-Thobaben Date: Wed, 18 Nov 2020 00:25:16 +0100 Subject: [PATCH] Initial Commit --- .gitignore | 41 ++ Expression.cs | 559 +++++++++++++++++++++ FormContext.cs | 254 ++++++++++ FormElement.cs | 35 ++ Template.cs | 134 +++++ TemplateProvider.cs | 8 + TemplateReader.cs | 151 ++++++ elements/Conditionals.cs | 99 ++++ elements/Element.cs | 97 ++++ elements/IncludeElement.cs | 45 ++ html/DocumentElement.cs | 31 ++ html/Element.cs | 100 ++++ html/ElementReader.cs | 75 +++ html/ExpressionElement.cs | 40 ++ html/FileSystemTemplateSource.cs | 63 +++ html/HtmlReader.cs | 175 +++++++ html/ITemplateSource.cs | 11 + html/RenderContext.cs | 39 ++ html/TemplateDocument.cs | 42 ++ html/TemplateElement.cs | 140 ++++++ html/TemplateReader.cs | 70 +++ html/TextElement.cs | 30 ++ ln.templates.csproj | 17 + ln.templates.test/HtmlTests.cs | 71 +++ ln.templates.test/ln.templates.test.csproj | 19 + script/IScriptContext.cs | 20 + script/NewExpression.cs | 63 +++ streams/CharStream.cs | 20 + streams/PeekableStream.cs | 50 ++ 29 files changed, 2499 insertions(+) create mode 100644 .gitignore create mode 100644 Expression.cs create mode 100644 FormContext.cs create mode 100644 FormElement.cs create mode 100644 Template.cs create mode 100644 TemplateProvider.cs create mode 100644 TemplateReader.cs create mode 100644 elements/Conditionals.cs create mode 100644 elements/Element.cs create mode 100644 elements/IncludeElement.cs create mode 100644 html/DocumentElement.cs create mode 100644 html/Element.cs create mode 100644 html/ElementReader.cs create mode 100644 html/ExpressionElement.cs create mode 100644 html/FileSystemTemplateSource.cs create mode 100644 html/HtmlReader.cs create mode 100644 html/ITemplateSource.cs create mode 100644 html/RenderContext.cs create mode 100644 html/TemplateDocument.cs create mode 100644 html/TemplateElement.cs create mode 100644 html/TemplateReader.cs create mode 100644 html/TextElement.cs create mode 100644 ln.templates.csproj create mode 100644 ln.templates.test/HtmlTests.cs create mode 100644 ln.templates.test/ln.templates.test.csproj create mode 100644 script/IScriptContext.cs create mode 100644 script/NewExpression.cs create mode 100644 streams/CharStream.cs create mode 100644 streams/PeekableStream.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd51a9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Autosave files +*~ + +# build +[Oo]bj/ +[Bb]in/ +packages/ +TestResults/ + +# globs +Makefile.in +*.DS_Store +*.sln.cache +*.suo +*.cache +*.pidb +*.userprefs +*.usertasks +config.log +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.user +*.tar.gz +tarballs/ +test-results/ +Thumbs.db +.vs/ + +# Mac bundle stuff +*.dmg +*.app + +# resharper +*_Resharper.* +*.Resharper + +# dotCover +*.dotCover diff --git a/Expression.cs b/Expression.cs new file mode 100644 index 0000000..4ad83b9 --- /dev/null +++ b/Expression.cs @@ -0,0 +1,559 @@ +using System; +using System.Reflection; +using System.IO; +using System.Text; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +namespace ln.templates +{ + delegate bool ConditionDelegate(char ch); + + public class Expression + { + public String Source { get; } + private Eval TopEval { get; set; } + + public Expression(String expr) + { + Source = expr; + + Compile(); + } + + public object Evaluate(object o) + { + Context context = new Context(o, null); + return TopEval.Evaluate(context); + } + public object Evaluate(Context context) + { + return TopEval.Evaluate(context); + } + + public bool IsTrue(Context context) + { + try + { + object v = Evaluate(context); + if ((v is bool) && ((bool)v)) + return true; + if ((v is string) && (!String.Empty.Equals(v))) + return true; + if ((v is int) && ((int)v != 0)) + return true; + return false; + } + catch (Exception) + { + return false; + } + } + + + private void Compile() + { + try + { + StringReader reader = new StringReader(Source); + List tokens = new List(); + while (reader.Peek() != -1) + ReadToken(reader, tokens); + + Queue qtokens = new Queue(tokens); + + TopEval = BuildExpression(qtokens); + } catch (Exception e) + { + throw new Exception(String.Format("Failed to compile expression: {0}", Source), e); + } + } + + + private Eval BuildName(Token nameToken,Eval parent,Queue tokens) + { + if ((tokens.Count > 0) && (tokens.Peek().TokenType == TokenType.OPENLIST)) + { + tokens.Dequeue(); + + List pl = new List(); + while (tokens.Peek().TokenType != TokenType.ENDLIST) + { + pl.Add(BuildExpression(tokens)); + + if (tokens.Peek().TokenType == TokenType.ENDLIST) + break; + + if (tokens.Peek().TokenType != TokenType.COMMA) + throw new FormatException("Expected ',' between parameters to call statement"); + + tokens.Dequeue(); + } + + tokens.Dequeue(); + + return new MethodCall(parent, nameToken.Value, pl.ToArray()); + } + else + { + return new Name(parent, nameToken.Value); + } + } + + private Eval BuildPath(Queue tokens) + { + Eval currentEval = BuildName(tokens.Dequeue(), null, tokens); + + while (tokens.Count > 0) + { + Token token = tokens.Peek(); + switch (token.TokenType) + { + case TokenType.DOT: + tokens.Dequeue(); + currentEval = BuildName(tokens.Dequeue(), currentEval, tokens); + break; + case TokenType.OPENINDEXER: + tokens.Dequeue(); + Eval index = BuildExpression(tokens); + token = tokens.Dequeue(); + if (token.TokenType != TokenType.ENDINDEXER) + throw new FormatException("Expected ']'"); + currentEval = new Indexer(currentEval, index); + break; + default: + return currentEval; + } + } + return currentEval; + } + + + private Eval BuildExpression(Queue tokens) + { + Token next = tokens.Peek(); + + switch (next.TokenType) + { + case TokenType.NAME: + return BuildPath(tokens); + case TokenType.NUMBER: + return new Number(tokens.Dequeue().Value); + case TokenType.STRING: + return new CString(tokens.Dequeue().Value); + } + + throw new FormatException(String.Format("unexpected Token: {0}", next.Value)); + } + + private void ReadToken(TextReader reader, List tokens) + { + if (reader.Peek() == -1) + return; + char ch = (char)reader.Peek(); + if (char.IsWhiteSpace(ch)) + { + reader.Read(); + } + else if (char.IsLetter(ch) || (ch == '_')) + { + ReadName(reader, tokens); + } + else if (char.IsDigit(ch)) + { + ReadNumber(reader, tokens); + } + else if (ch == '"') + { + ReadString(reader, tokens); + } + else if (ch == '.') + { + reader.Read(); + tokens.Add(new Token(TokenType.DOT, ".")); + } + else if (ch == '[') + { + reader.Read(); + tokens.Add(new Token(TokenType.OPENINDEXER, new string(new char[] { ch }))); + } + else if (ch == ']') + { + reader.Read(); + tokens.Add(new Token(TokenType.ENDINDEXER, new string(new char[] { ch }))); + } + else if (ch == '(') + { + reader.Read(); + tokens.Add(new Token(TokenType.OPENLIST, new string(new char[] { ch }))); + } + else if (ch == ')') + { + reader.Read(); + tokens.Add(new Token(TokenType.ENDLIST, new string(new char[] { ch }))); + } + else if (ch == ',') + { + reader.Read(); + tokens.Add(new Token(TokenType.COMMA, new string(new char[] { ch }))); + } + else + { + throw new FormatException(String.Format("Unexpected character: {0}", (char)reader.Peek())); + } + } + private void ReadName(TextReader reader, List tokens) + { + StringBuilder sb = new StringBuilder(); + int ch = 0; + while (char.IsLetterOrDigit((char)(ch = reader.Peek())) || (ch == '_')) + { + sb.Append((char)reader.Read()); + } + tokens.Add(new Token(TokenType.NAME, sb.ToString())); + } + private void ReadNumber(TextReader reader, List tokens) + { + StringBuilder sb = new StringBuilder(); + int ch = 0; + while (char.IsDigit((char)(ch = reader.Peek())) | (ch == '.')) + { + sb.Append((char)reader.Read()); + } + tokens.Add(new Token(TokenType.NUMBER, sb.ToString())); + } + private void ReadString(TextReader reader, List tokens) + { + StringBuilder sb = new StringBuilder(); + int ch = 0; + reader.Read(); + while ((ch = reader.Peek())!='"') + { + if (ch == '\\') + { + reader.Read(); + char sch = (char)reader.Read(); + switch (sch) + { + case 'n': + sb.Append('\n'); + break; + case 'r': + sb.Append('\r'); + break; + case 't': + sb.Append('\t'); + break; + default: + sb.Append(sch); + break; + } + } + else + { + sb.Append((char)reader.Read()); + } + } + reader.Read(); + + tokens.Add(new Token(TokenType.STRING, sb.ToString())); + } + + + enum TokenType { NAME, DOT, NUMBER, STRING, OPENINDEXER, ENDINDEXER, OPENLIST, ENDLIST, COMMA } + class Token + { + public TokenType TokenType; + public string Value; + + public Token(TokenType tokenType,String value) + { + this.TokenType = tokenType; + this.Value = value; + } + + public override string ToString() + { + return String.Format("[Token TokenType={0:8} Value={1}]", TokenType, Value); + } + } + + public class Context + { + public object This { get; } + public Dictionary MappedValues { get; } + + public Context(Context source) + : this(source.This, source.MappedValues) + { + } + + public Context(object o,IEnumerable> mappedValues) + { + this.This = o; + this.MappedValues = new Dictionary(); + + if (mappedValues != null) + foreach (KeyValuePair kvp in mappedValues) + MappedValues.Add(kvp.Key,kvp.Value); + } + + public void AddMappedValue(string name, object value) + { + MappedValues.Add(name, value); + } + public void SetMappedValue(string name, object value) + { + MappedValues[name] = value; + } + public void RemoveMappedValue(string name) + { + MappedValues.Remove(name); + } + + public object Evaluate(string name) + { + if (MappedValues.ContainsKey(name)) + return MappedValues[name]; + + if (This != null) + { + FieldInfo fieldInfo = This.GetType().GetField(name); + if (fieldInfo != null) + { + return fieldInfo.GetValue(This); + } + PropertyInfo propertyInfo = This.GetType().GetProperty(name); + if (propertyInfo != null) + { + return propertyInfo.GetValue(This); + } + } + throw new KeyNotFoundException(name); + } + + } + + abstract class Eval + { + public Eval Parent { get; } + + public Eval(Eval parent) + { + this.Parent = parent; + } + + public abstract object Evaluate(Context context); + } + + class Name : Eval + { + public string name; + + public Name(Eval parent,String name) + :base(parent) + { + this.name = name; + } + + public override object Evaluate(Context context) + { + if (Parent == null) + { + return context.Evaluate(name); + } + + object p = Parent.Evaluate(context); + if (p != null) + { + FieldInfo fieldInfo = p.GetType().GetField(name); + if (fieldInfo != null) + { + return fieldInfo.GetValue(p); + } + PropertyInfo propertyInfo = p.GetType().GetProperty(name); + if (propertyInfo != null) + { + return propertyInfo.GetValue(p); + } + throw new KeyNotFoundException(name); + } + throw new NullReferenceException(); + } + } + + class Indexer : Eval + { + Eval index; + + public Indexer(Eval parent,Eval index) + :base(parent) + { + this.index = index; + } + + public override object Evaluate(Context context) + { + object i = index.Evaluate(context); + Type itype = i.GetType(); + + object p = Parent.Evaluate(context); + + if (p is Array) + { + Array pa = p as Array; + return pa.GetValue((int)i); + } + + foreach (PropertyInfo pi in p.GetType().GetProperties()) + { + ParameterInfo[] infos = pi.GetIndexParameters(); + if (infos.Length == 1) + { + if (infos[0].ParameterType.IsAssignableFrom(itype)) + { + return pi.GetValue(p, new object[] { i }); + } + } + } + throw new KeyNotFoundException(); + } + } + + class MethodCall : Eval + { + Eval[] Parameters; + String Name; + + public MethodCall(Eval parent, String name,params Eval[] p) + : base(parent) + { + Name = name; + Parameters = p; + } + + public override object Evaluate(Context context) + { + object p = Parent.Evaluate(context); + + object[] pvalues = new object[Parameters.Length]; + Type[] ptypes = new Type[Parameters.Length]; + + for (int i=0;i char.IsLetterOrDigit(ch)); + //} + + //abstract class Token + //{ + // public abstract object Evaluate(); + //} + + //class Field : Token + //{ + // Token owner; + // String fieldName; + + // public Field(Token owner,String fieldName) + // { + // this.owner = owner; + // this.fieldName = fieldName; + // } + + // public override object Evaluate() + // { + // if (o == null) + // return null; + + // Type t = o.GetType(); + // FieldInfo fieldInfo = t.GetField(this.fieldName); + // if (fieldInfo != null) + // { + // return fieldInfo.GetValue(owner.Evaluate()); + // } + // PropertyInfo propertyInfo = t.GetProperty(this.fieldName); + // if (propertyInfo != null) + // { + // return propertyInfo.GetValue(owner.Evaluate()); + // } + // throw new MissingFieldException(t.FullName, this.fieldName); + // } + //} + + } + + + +} diff --git a/FormContext.cs b/FormContext.cs new file mode 100644 index 0000000..5cac569 --- /dev/null +++ b/FormContext.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Reflection; +using System.Linq; +using System.ComponentModel; +using System.Resources; + +namespace ln.templates +{ + //public class FormContext + //{ + //private Dictionary vars = new Dictionary(); + //protected MemoryStream contentStream, headStream; + + //public Template SharpForm { get; } + //public Request Request { get; } + + //public Stream ContentStream => contentStream; + //public TextWriter ContentWriter { get; protected set; } + + //public Stream HeadStream => headStream; + //public TextWriter HeadWriter { get; protected set; } + + //public FormContext Parent { get; } + //public bool UsesClonedVars { get; } + + //public bool IsRootContext => Parent == null; + + //public FormContext SubFrameContext { get; protected set; } + + //public FormContext(Template sharpForm,Request request,object o) + //{ + // Parent = null; + + // SharpForm = sharpForm; + // Request = request; + + // contentStream = new MemoryStream(); + // ContentWriter = new StreamWriter(contentStream); + + // headStream = new MemoryStream(); + // HeadWriter = new StreamWriter(headStream); + + // vars["o"] = o; + // vars["this"] = o; + // vars["StrMax"] = GetType().GetMethod("StrMax"); + //} + + //public FormContext(FormContext parent,bool cloneVars = false,bool independentContent = false,FormContext subFrameContext = null) + //{ + // Parent = parent; + + // SharpForm = parent.SharpForm; + // Request = parent.Request; + + // contentStream = independentContent ? new MemoryStream() : parent.contentStream; + // ContentWriter = new StreamWriter(ContentStream); + + // headStream = parent.headStream; + // HeadWriter = parent.HeadWriter; + + // UsesClonedVars = cloneVars; + + // SubFrameContext = subFrameContext == null ? parent.SubFrameContext : subFrameContext; + + // if (cloneVars){ + // foreach (KeyValuePair var in parent.vars) + // { + // this.vars.Add(var.Key, var.Value); + // } + // } + + // parent.ContentWriter.Flush(); + //} + + //public byte[] ContentBytes + //{ + // get + // { + // ContentWriter.Flush(); + // return contentStream.ToArray(); + // } + //} + //public byte[] HeadBytes + //{ + // get + // { + // HeadWriter.Flush(); + // return headStream.ToArray(); + // } + //} + //public String Content => Encoding.UTF8.GetString(ContentBytes); + //public String Head => Encoding.UTF8.GetString(HeadBytes); + + //public void Insert(FormContext context, bool allToHead = false) + //{ + // if (context.headStream != headStream) + // { + // HeadWriter.Flush(); + // byte[] headBytes = context.HeadBytes; + // headStream.Write(headBytes, 0, headBytes.Length); + // } + + // ContentWriter.Flush(); + // byte[] contentBytes = context.ContentBytes; + + // if (allToHead) + // headStream.Write(contentBytes, 0, contentBytes.Length); + // else + // contentStream.Write(contentBytes, 0, contentBytes.Length); + //} + + //public bool HasVar(String name) + //{ + // if (vars.ContainsKey(name)) + // return true; + // if (!UsesClonedVars && (Parent != null)) + // return Parent.HasVar(name); + + // return false; + //} + + //public object Get(String name){ + // if (vars.ContainsKey(name)) + // return vars[name]; + // else if (Parent != null) + // return Parent.Get(name); + // else + // return null; + //} + + //public void Set(String name,object value){ + // if (!UsesClonedVars && (Parent != null) && (Parent.HasVar(name))) + // { + // Parent.Set(name, value); + // } else { + // this.vars[name] = value; + // } + //} + + + //public object EvaluateExpression(Token[] tokens) + //{ + // if (tokens.Length == 0) + // { + // return ""; + // } + + // Stack tokenStack = new Stack(tokens); + // object currentValue = null; + // Token currentToken = null; + + // while (tokenStack.Count > 0){ + // currentToken = tokenStack.Pop(); + + // if (currentToken is PathToken) + // { + // PathToken pathToken = (PathToken)currentToken; + // Response response = Request.SubRequest(pathToken.Path); + // if (response.Reference != null) + // currentValue = response.Reference; + // else + // currentValue = Encoding.UTF8.GetString(response.ContentBytes); + // } else if (currentToken is KeywordToken) + // { + // KeywordToken keywordToken = (KeywordToken)currentToken; + // currentValue = keywordToken.Evaluate(this); + // } + + // if (tokenStack.Count > 0){ + // currentToken = tokenStack.Pop(); + // throw new NotImplementedException("Currently no expression operators are implemented, sorry"); + // } + + // } + // return currentValue; + //} + + //public object EvaluateValue(String sValue) + //{ + // Stack vpath = new Stack(sValue.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries).Reverse()); + + // if (vpath.Count > 0) + // { + // object value = null; + // string key = vpath.Pop(); + + // value = Get(key); + + // while (vpath.Count > 0) + // { + // key = vpath.Pop(); + // value = GetObjectFieldOrProperty(value, key); + // } + // return value; + // } + // return null; + //} + + //public bool EvaluateBoolean(String expression) + //{ + // bool invert = (expression[0] == '!'); + // if (invert) + // expression = expression.Substring(1); + + // object value = EvaluateValue(expression); + // if (value != null) + // { + // if ( + // ((value is bool) && (((bool)value))) || + // ((value is string) && (!String.Empty.Equals(value))) || + // ((value is int) && (((int)value) != 0)) || + // ((value is long) && (((long)value) != 0)) || + // ((value is float) && (((float)value) != 0)) || + // ((value is double) && (((double)value) != 0)) + // ) + // { + // return !invert; + // } + // } + // return invert; + //} + + //private object GetObjectFieldOrProperty(object o, string name) + //{ + // if (o == null) + // { + // return null; + // } + // FieldInfo fieldInfo = o.GetType().GetField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + // if (fieldInfo != null) + // { + // return fieldInfo.GetValue(o); + // } + // PropertyInfo propertyInfo = o.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + // if (propertyInfo != null) + // { + // return propertyInfo.GetValue(o); + // } + + // throw new KeyNotFoundException(String.Format("object of type {0} has no field/property named {1}", o.GetType().FullName, name)); + //} + + //public static string StrMax(string s,int len,string suffix = "") + //{ + // if (len < s.Length) + // return s.Substring(0, len) + suffix; + // return s; + //} + +// } +} diff --git a/FormElement.cs b/FormElement.cs new file mode 100644 index 0000000..4dfa845 --- /dev/null +++ b/FormElement.cs @@ -0,0 +1,35 @@ +using System; +using System.Xml; +using System.Collections.Generic; +namespace ln.templates +{ + //public abstract class FormElement + //{ + // /* Instance Members */ + // FormElement Container { get; } + // List children = new List(); + + // public FormElement(FormElement container) + // { + // Container = container; + // if (container != null) + // { + // container.children.Add(this); + // } + // } + + // public abstract void Run(FormContext context); + + // public FormElement[] Children => children.ToArray(); + // public virtual Template Form => Container.Form; + + // public void RunChildren(FormContext context) + // { + // foreach (FormElement child in children) + // { + // child.Run(context); + // } + // } + + //} +} diff --git a/Template.cs b/Template.cs new file mode 100644 index 0000000..683a865 --- /dev/null +++ b/Template.cs @@ -0,0 +1,134 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Text; +using ln.templates.streams; +using ln.templates.elements; + +namespace ln.templates +{ + public class Template + { + public TemplateProvider Provider { get; private set; } + public String SourceFilename { get; private set; } + public String PseudoFilename { get; private set; } + public DateTime SourceTimeStamp { get; private set; } + + public Element RootElement { get; private set; } + + public Expression FrameExpression { get; private set; } + + public Template(String sourceFilename) + { + SourceFilename = sourceFilename; + PseudoFilename = sourceFilename; + + LoadSource(null); + } + + public Template(String sourceFilename, TemplateProvider provider) + { + Provider = provider; + SourceFilename = sourceFilename; + PseudoFilename = sourceFilename; + + LoadSource(null); + } + + public Template(String source,String pseudoFilename,TemplateProvider provider) + { + Provider = provider; + PseudoFilename = pseudoFilename; + + LoadSource(source); + } + + private void LoadSource(String source) + { + if (source == null) + { + using (FileStream fileStream = new FileStream(SourceFilename, FileMode.Open)) + { + byte[] loadBuffer = new byte[fileStream.Length]; + fileStream.Read(loadBuffer, 0, loadBuffer.Length); + fileStream.Close(); + source = Encoding.UTF8.GetString(loadBuffer); + + SourceTimeStamp = File.GetLastWriteTimeUtc(SourceFilename); + } + } + + TemplateReader templateReader = new TemplateReader(source); + + RootElement = templateReader.RootElement; + FrameExpression = templateReader.FrameExpression; + } + + public String Generate() + { + return Generate(false); + } + public String Generate(bool unframed) + { + Context context = new Context(this); + return Generate(context); + } + public String Generate(Context context) + { + return Generate(context, false); + } + public String Generate(Context context,bool unframed) + { + if (SourceFilename != null) + { + DateTime ts = File.GetLastWriteTimeUtc(SourceFilename); + if (ts > SourceTimeStamp) + LoadSource(null); + } + + StringWriter writer = new StringWriter(); + RootElement.Generate(writer, context); + + if (!unframed && (FrameExpression != null)) + { + Template frame = context.Provider.FindTemplate(FrameExpression.Evaluate(context.ExpressionContext) as string); + if (frame == null) + { + throw new NullReferenceException(String.Format("FindTemplate() returned null for {0}",FrameExpression.Source)); + } + return frame.Generate(new Context(context, writer.ToString())); + } + + return writer.ToString(); + } + + + public class Context + { + public Context Parent { get; } + public Template Template { get; } + public TemplateProvider Provider => Template.Provider; + + public String FramedContent { get; } + public Expression.Context ExpressionContext { get; } + + public Context(Template template) + { + Template = template; + ExpressionContext = new Expression.Context(null, null); + ExpressionContext.AddMappedValue("__template__", template.SourceFilename); + ExpressionContext.AddMappedValue("__provider__", template.Provider); + } + public Context(Context parent,String framedContent) + { + Parent = parent; + Template = parent.Template; + FramedContent = framedContent; + ExpressionContext = new Expression.Context(parent.ExpressionContext); + if (framedContent != null) + ExpressionContext.SetMappedValue("__frame__", framedContent); + } + } + + } +} diff --git a/TemplateProvider.cs b/TemplateProvider.cs new file mode 100644 index 0000000..158191b --- /dev/null +++ b/TemplateProvider.cs @@ -0,0 +1,8 @@ +using System; +namespace ln.templates +{ + public interface TemplateProvider + { + Template FindTemplate(string templatePath); + } +} diff --git a/TemplateReader.cs b/TemplateReader.cs new file mode 100644 index 0000000..c1254bd --- /dev/null +++ b/TemplateReader.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using ln.templates.elements; +using System.IO; +using System.Text; +namespace ln.templates +{ + public class TemplateReader + { + public ContainerElement RootElement { get; private set; } = new ContainerElement(null); + public ContainerElement CurrentElement { get; private set; } + + public Expression FrameExpression { get; private set; } + + public String Source { get; private set; } + + public TemplateReader(String source) + { + Source = source; + Parse(); + } + + private void Parse() + { + CurrentElement = RootElement; + + while (Source.Length > 0) + { + String t, op; + int i, j; + + i = Source.IndexOf("<%"); + if (i == -1) + { + t = Source; + op = null; + Source = ""; + } + else + { + t = Source.Substring(0, i); + j = Source.IndexOf("%>", i + 1); + if (j == -1) + { + throw new FormatException(String.Format("missing '%>' in {0}", Source.Substring(i, 64))); + } + op = Source.Substring(i + 2, j - i - 2); + Source = Source.Substring(j + 2); + } + + new TextElement(CurrentElement, t); + if (op != null) + ParseOP(op); + } + } + + private void ParseOP(String op) + { + StringReader reader = new StringReader(op); + String opkey; + + if (reader.Peek() == '=') + { + reader.Read(); + opkey = "="; + } + else + { + opkey = reader.PopWord(); + } + + switch (opkey) + { + case "=": + new ExpressionElement(CurrentElement, new Expression(reader.ReadToEnd())); + break; + case "frame": + FrameExpression = new Expression(reader.ReadToEnd()); + break; + case "iterate": + String iterName = reader.PopWord(); + CurrentElement = new IteratorElement(CurrentElement, iterName, new Expression(reader.ReadToEnd())); + break; + case "if": + CurrentElement = new ConditionalElement(CurrentElement, new Expression(reader.ReadToEnd())); + break; + case "else": + if (CurrentElement is ConditionalElement) + { + ConditionalElement ce = CurrentElement as ConditionalElement; + CurrentElement = ce.AlternativeContainer; + } + break; + case "elseif": + if (CurrentElement is ConditionalElement) + { + ConditionalElement ce = CurrentElement as ConditionalElement; + CurrentElement = new ConditionalElement(null, new Expression(reader.ReadToEnd())); + ce.AlternativeContainer = CurrentElement; + } + break; + + case "include": + new IncludeElement(CurrentElement, new Expression(reader.ReadToEnd())); + break; + + case "set": + String setName = reader.PopWord(); + new SetElement(CurrentElement, setName, new Expression(reader.ReadToEnd())); + break; + + case "end": + CurrentElement = CurrentElement.Container; + break; + default: + throw new NotImplementedException(opkey); + } + } + } + + static class StringHelper + { + public static int IndexOf(this String s,Func predicate) + { + for (int i=0;i children { get; } = new List(); + + public ContainerElement(ContainerElement container) + :base(container) + { + } + + public void GenerateChildren(TextWriter writer, Template.Context context) + { + foreach (Element c in children) + c.Generate(writer, context); + } + + public override void Generate(TextWriter writer, Template.Context context) + { + GenerateChildren(writer, context); + } + + public Element[] Children + { + get => children.ToArray(); + } + } + + public class TextElement : Element + { + public String Text { get; } + public TextElement(ContainerElement container,String text) + :base(container) + { + Text = text; + } + + public override void Generate(TextWriter writer, Template.Context context) + { + writer.Write(Text); + } + } + + public class ExpressionElement : Element + { + public Expression Expression { get; } + + public ExpressionElement(ContainerElement container,Expression expression) + :base(container) + { + Expression = expression; + } + + public override void Generate(TextWriter writer, Template.Context context) + { + object v = Expression.Evaluate(context.ExpressionContext); + if (v != null) + writer.Write(v.ToString()); + } + } + public class SetElement : Element + { + public String SetName { get; } + public Expression Expression { get; } + + public SetElement(ContainerElement container, String setName,Expression expression) + : base(container) + { + SetName = setName; + Expression = expression; + } + + public override void Generate(TextWriter writer, Template.Context context) + { + object v = Expression.Evaluate(context.ExpressionContext); + context.ExpressionContext.SetMappedValue(SetName, v); + } + } +} diff --git a/elements/IncludeElement.cs b/elements/IncludeElement.cs new file mode 100644 index 0000000..7cd04d3 --- /dev/null +++ b/elements/IncludeElement.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using System.Collections.Generic; + +namespace ln.templates.elements +{ + public class IncludeElement : Element + { + Expression TargetExpression { get; set; } + + public IncludeElement(ContainerElement container,Expression targetExpression) + :base(container) + { + TargetExpression = targetExpression; + } + + public void Generate(TextWriter writer, Template.Context context,String templatePath) + { + Template iTemplate = context.Provider.FindTemplate(templatePath); + Template.Context iContext = new Template.Context(context, null); + String iContent = iTemplate.Generate(iContext); + + writer.Write(iContent); + } + + public override void Generate(TextWriter writer, Template.Context context) + { + object target = TargetExpression.Evaluate(context.ExpressionContext); + + if (target is IEnumerable) + { + IEnumerable targets = (IEnumerable)target; + foreach (String t in targets) + { + Generate(writer, context, t); + } + } + else + { + Generate(writer, context, target as String); + } + + } + } +} diff --git a/html/DocumentElement.cs b/html/DocumentElement.cs new file mode 100644 index 0000000..a80f486 --- /dev/null +++ b/html/DocumentElement.cs @@ -0,0 +1,31 @@ +// /** +// * File: DocumentElement.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; + +namespace ln.templates.html +{ + public class DocumentElement : Element + { + public DocumentElement() + :base("#document") + { + } + + public override void Render(TextWriter writer) + { + writer.Write(""); + + foreach (Element element in Children) + element.Render(writer); + } + + } +} diff --git a/html/Element.cs b/html/Element.cs new file mode 100644 index 0000000..5691e1b --- /dev/null +++ b/html/Element.cs @@ -0,0 +1,100 @@ +// /** +// * File: Element.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +namespace ln.templates.html +{ + public class Element + { + public string Name { get; } + public Element Parent { get; private set; } + + public bool IsVoid => IsVoidElement(Name); + + List children = new List(); + Dictionary attributes = new Dictionary(); + public IEnumerable> Attributes => attributes; + + public Element(string elementName) + { + Name = elementName; + } + public Element(string elementName,IEnumerable> attributes) + :this(elementName) + { + foreach (KeyValuePair keyValuePair in attributes) + this.attributes[keyValuePair.Key] = keyValuePair.Value; + } + + public virtual bool HasAttribute(string attributeName) => attributes.ContainsKey(attributeName); + public virtual string GetAttribute(string attributeName) => attributes[attributeName]; + public virtual void SetAttribute(string attributeName, string attributeValue) => attributes[attributeName] = attributeValue; + + public IEnumerable Children => children; + public void AppendChild(Element element) + { + if (element.Parent != null) + element.Parent.RemoveChild(element); + + if (IsVoid) + { + Parent.AppendChild(element); + } + else + { + children.Add(element); + element.Parent = this; + } + } + public void RemoveChild(Element element) + { + if (element.Parent != this) + throw new KeyNotFoundException(); + + children.Remove(element); + element.Parent = null; + } + + public virtual void Render(TextWriter writer) + { + writer.Write("<{0}", Name); + foreach (KeyValuePair attributePair in attributes) + writer.Write(" {0}=\"{1}\"", attributePair.Key, attributePair.Value); + writer.Write(">"); + + if (!IsVoid) + { + foreach (Element element in children) + element.Render(writer); + + writer.Write("",Name); + } + } + + public override string ToString() + { + StringWriter stringWriter = new StringWriter(); + Render(stringWriter); + return stringWriter.ToString(); + } + + + + static HashSet voidElementNames = new HashSet(){ + "!doctype", + "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" + }; + public static string[] VoidElementNames => voidElementNames.ToArray(); + public bool IsVoidElement(string elementName) => voidElementNames.Contains(elementName); + + } +} diff --git a/html/ElementReader.cs b/html/ElementReader.cs new file mode 100644 index 0000000..8bcaf16 --- /dev/null +++ b/html/ElementReader.cs @@ -0,0 +1,75 @@ +// /** +// * File: ElementReader.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Collections.Generic; +namespace ln.templates.html +{ + public class ElementReader : HtmlReader + { + Stack elements = new Stack(); + + public Element CurrentElement => elements.Peek(); + public virtual DocumentElement Document { get; } + + public ElementReader() + { + Document = CreateDocument(); + elements.Push(Document); + } + + public virtual DocumentElement CreateDocument() => new DocumentElement(); + + public virtual Element CreateElement(string tagName) + { + switch (tagName.ToUpper()) + { + default: + return new Element(tagName); + } + } + public virtual Element CreateTextElement(string text) => new TextElement(text); + + void Push(Element element) + { + if (CurrentElement.IsVoid) + elements.Pop(); + + CurrentElement.AppendChild(element); + elements.Push(element); + } + + void Pop() + { + if (CurrentElement.IsVoid) + elements.Pop(); + + elements.Pop(); + } + + public override void OpenTag(string tagName) + { + Push(CreateElement(tagName)); + } + public override void CloseTag(string tagName) + { + Pop(); + } + public override void Attribute(string attributeName, string attributeValue) + { + CurrentElement.SetAttribute(attributeName, attributeValue); + } + public override void Text(string text) + { + CurrentElement.AppendChild( + CreateTextElement(text) + ); + } + } +} diff --git a/html/ExpressionElement.cs b/html/ExpressionElement.cs new file mode 100644 index 0000000..435beab --- /dev/null +++ b/html/ExpressionElement.cs @@ -0,0 +1,40 @@ +// /** +// * File: ExpressionElement.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using ln.templates.script; +using System.IO; + +namespace ln.templates.html +{ + public class ExpressionElement : TemplateElement + { + public string ExpressionText { get; } + public NewExpression Expression { get; } + + public ExpressionElement(string expression) + :base("#expression") + { + ExpressionText = expression; + Expression = new NewExpression(ExpressionText); + } + + public override void Render(TextWriter writer) + { + writer.Write("{{{{{0}}}}}", ExpressionText); + } + + public override void RenderTemplate(RenderContext renderContext) + { + object o = Expression.Resolve(renderContext); + renderContext.ContentWriter.Write(o?.ToString()); + } + + + } +} diff --git a/html/FileSystemTemplateSource.cs b/html/FileSystemTemplateSource.cs new file mode 100644 index 0000000..4bf1014 --- /dev/null +++ b/html/FileSystemTemplateSource.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace ln.templates.html +{ + public class FileSystemTemplateSource : ITemplateSource + { + public String BasePath { get; } + + Dictionary templateDocuments = new Dictionary(); + + public FileSystemTemplateSource(String path) + { + if (!Directory.Exists(path)) + throw new FileNotFoundException(); + BasePath = path; + } + + public TemplateDocument GetTemplateByPath(string path) + { + string templatePath = Path.Combine(BasePath, path); + if (!File.Exists(templatePath)) + { + if (templateDocuments.ContainsKey(path)) + templateDocuments.Remove(path); + + throw new FileNotFoundException(); + } + + if (!templateDocuments.TryGetValue(path,out TemplateDocument templateDocument)) + { + templateDocument = ReadTemplateDocument(path); + templateDocuments[path] = templateDocument; + } else + { + DateTime templateDateTime = File.GetLastWriteTime(templatePath); + if (templateDateTime.Ticks != templateDocument.TemplateVersion) + { + templateDocument = ReadTemplateDocument(path); + templateDocuments[path] = templateDocument; + } + } + + return templateDocument; + } + + TemplateDocument ReadTemplateDocument(string path) + { + string templatePath = Path.Combine(BasePath, path); + + TemplateReader templateReader = new TemplateReader(); + using (StreamReader sr = new StreamReader(templatePath)) + { + templateReader.Read(sr); + } + templateReader.TemplateDocument.TemplateVersion = File.GetLastWriteTime(templatePath).Ticks; + + return templateReader.TemplateDocument; + } + } +} diff --git a/html/HtmlReader.cs b/html/HtmlReader.cs new file mode 100644 index 0000000..be21375 --- /dev/null +++ b/html/HtmlReader.cs @@ -0,0 +1,175 @@ +// /** +// * File: Parser.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; +using System.Collections.Generic; +using System.Text; +namespace ln.templates.html +{ + public class HtmlReader + { + public HtmlReader() + { + } + + public void Read(string source) => Read(new StringReader(source)); + public void Read(Stream sourceStream) + { + using (StreamReader streamReader = new StreamReader(sourceStream)) + Read(streamReader); + } + + public void Read(TextReader textReader) + { + while (textReader.Peek() != -1) + { + switch (textReader.Peek()) + { + case '<': + ReadTag(textReader); + break; + default: + ReadText(textReader); + break; + } + } + } + public void ReadTag(TextReader textReader) + { + if (textReader.Read() != '<') + throw new FormatException("Expected <"); + + bool closing = false; + + if (textReader.Peek()=='/') + { + textReader.Read(); + closing = true; + } + + if (textReader.Peek()=='!') + { + textReader.Read(); + string doctype = ReadTagName(textReader); + if (!doctype.Equals("DOCTYPE")) + throw new FormatException("Expected DOCTYPE"); + + string type = ReadAttributeName(textReader); + DOCTYPE(type); + + if (textReader.Read() != '>') + throw new FormatException("Expected >"); + + return; + } + string tagName = ReadTagName(textReader); + + if (closing) + { + CloseTag(tagName); + } + else + { + OpenTag(tagName); + + while (TestChar((char)textReader.Peek(), (ch) => !char.IsWhiteSpace(ch) && (ch != '\0') && (ch != '"') && (ch != '\'') && (ch != '>') && (ch != '/') && (ch != '='))) + { + string attributeName = ReadAttributeName(textReader); + string attributeValue = ""; + if (textReader.Peek() == '=') + { + textReader.Read(); + attributeValue = ReadAttributeValue(textReader); + } + Attribute(attributeName, attributeValue); + } + + if (textReader.Peek()=='/') + { + textReader.Read(); + CloseTag(tagName); + } + } + + if (textReader.Read() != '>') + throw new FormatException("Expected >"); + } + + bool TestChar(char ch, Func condition) => condition(ch); + public string ReadToken(TextReader textReader, Func condition, bool clearFinalChar = false) + { + StringBuilder characters = new StringBuilder(); + while (condition((char)textReader.Peek()) && textReader.Peek() != -1) + { + characters.Append((char)textReader.Read()); + } + + if ((textReader.Peek() != -1) && clearFinalChar) + textReader.Read(); + + return characters.ToString(); + } + public string ReadTokenLWS(TextReader textReader, Func condition,bool clearFinalChar = false) + { + string token = ReadToken(textReader, condition, clearFinalChar); + ReadToken(textReader, char.IsWhiteSpace); + return token; + } + void ReadLWS(TextReader textReader) => ReadToken(textReader, char.IsWhiteSpace); + + public string ReadTagName(TextReader textReader) => ReadTokenLWS(textReader, (ch) => char.IsLetterOrDigit(ch)); + public string ReadAttributeName(TextReader textReader) => ReadTokenLWS(textReader, (ch) => !char.IsWhiteSpace(ch) && (ch != '\0') && (ch != '"') && (ch != '\'') && (ch != '>') && (ch != '/') && (ch != '=')); + public string ReadAttributeValue(TextReader textReader) + { + switch (textReader.Peek()) + { + case '"': + textReader.Read(); + return ReadTokenLWS(textReader, (ch) => ch != '"', true); + case '\'': + textReader.Read(); + return ReadTokenLWS(textReader, (ch)=> ch != '\'', true); + default: + return ReadTokenLWS(textReader, (ch) => !char.IsWhiteSpace(ch) && (ch != '"') && (ch != '\'') && (ch != '<') && (ch != '>') && (ch != '`')); + } + } + + + + + public void ReadText(TextReader textReader) + { + StringBuilder stringBuilder = new StringBuilder(); + while (textReader.Peek() != '<') + stringBuilder.Append((char)textReader.Read()); + + Text(stringBuilder.ToString()); + } + + public virtual void DOCTYPE(string type) + { + } + + public virtual void OpenTag(string tagName) + { + } + public virtual void CloseTag(string tagName) + { + } + public virtual void Attribute(string attributeName,string attributeValue) + { + } + public virtual void Text(string text) + { + } + + + } +} diff --git a/html/ITemplateSource.cs b/html/ITemplateSource.cs new file mode 100644 index 0000000..1dde637 --- /dev/null +++ b/html/ITemplateSource.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ln.templates.html +{ + public interface ITemplateSource + { + TemplateDocument GetTemplateByPath(string path); + } +} diff --git a/html/RenderContext.cs b/html/RenderContext.cs new file mode 100644 index 0000000..39406fd --- /dev/null +++ b/html/RenderContext.cs @@ -0,0 +1,39 @@ +// /** +// * File: RenderContext.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; +using System.Collections.Generic; +using ln.templates.script; +namespace ln.templates.html +{ + public class RenderContext : IScriptContext + { + public TextWriter ContentWriter { get; } + public ITemplateSource TemplateSource { get; set; } + + Dictionary scriptObjects = new Dictionary(); + public IEnumerable ScriptObjectNames => scriptObjects.Keys; + + public RenderContext(TextWriter contentWriter) + { + ContentWriter = contentWriter; + } + public RenderContext(TextWriter contentWriter,ITemplateSource templateSource) + :this(contentWriter) + { + TemplateSource = templateSource; + } + + Dictionary values = new Dictionary(); + + public object GetScriptObject(string itemName) => scriptObjects[itemName]; + public void SetScriptObject(string itemName, object value) => scriptObjects[itemName] = value; + } +} diff --git a/html/TemplateDocument.cs b/html/TemplateDocument.cs new file mode 100644 index 0000000..4d45f19 --- /dev/null +++ b/html/TemplateDocument.cs @@ -0,0 +1,42 @@ +// /** +// * File: TemplateDocument.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; +namespace ln.templates.html +{ + public class TemplateDocument : DocumentElement + { + public long TemplateVersion { get; set; } + + public TemplateDocument() + { + } + + public void RenderTemplate(TextWriter contentWriter) => RenderTemplate(new RenderContext(contentWriter)); + public void RenderTemplate(RenderContext renderContext,bool withoutDocType = false) + { + if (!withoutDocType) + renderContext.ContentWriter.Write(""); + + foreach (Element element in Children) + { + if (element is TemplateElement templateElement) + { + templateElement.RenderTemplate(renderContext); + } else + { + renderContext.ContentWriter.Write(element.ToString()); + } + } + + renderContext.ContentWriter.Flush(); + } + } +} diff --git a/html/TemplateElement.cs b/html/TemplateElement.cs new file mode 100644 index 0000000..dc2794d --- /dev/null +++ b/html/TemplateElement.cs @@ -0,0 +1,140 @@ +// /** +// * File: TemplateElement.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Collections; +using System.Collections.Generic; +using ln.templates.script; + +namespace ln.templates.html +{ + public class TemplateElement : Element + { + Dictionary expressions = new Dictionary(); + Dictionary loops = new Dictionary(); + List conditions = new List(); + + NewExpression includeExpression; + + bool hideTag; + + public TemplateElement(string tagName) + :base(tagName) + { + hideTag = "template".Equals(tagName.ToLower()); + } + + public override void SetAttribute(string attributeName, string attributeValue) + { + if (attributeName.StartsWith("v-for:", StringComparison.InvariantCultureIgnoreCase)) + loops.Add(attributeName.Substring(6), new NewExpression(attributeValue)); + else if (attributeName.Equals("v-if", StringComparison.InvariantCultureIgnoreCase)) + conditions.Add(new NewExpression(attributeValue)); + else if (attributeName.Equals("v-include", StringComparison.InvariantCultureIgnoreCase)) + includeExpression = new NewExpression(attributeValue); + else if (attributeName[0] == ':') + expressions.Add(attributeName.Substring(1), new NewExpression(attributeValue)); + else + base.SetAttribute(attributeName, attributeValue); + } + + public virtual void RenderTemplate(RenderContext renderContext) + { + if (loops.Count == 0) + { + RenderElement(renderContext); + } else + { + Stack> loopStack = new Stack>(loops); + DoLoop(renderContext, loopStack); + } + } + + void DoLoop(RenderContext renderContext, Stack> loopStack) + { + if (loopStack.Count == 0) + { + RenderElement(renderContext); + } else + { + KeyValuePair loop = loopStack.Pop(); + IEnumerable enumerable = (IEnumerable)loop.Value.Resolve(renderContext); + foreach (object o in enumerable) + { + renderContext.SetScriptObject(loop.Key, o); + DoLoop(renderContext, loopStack); + } + loopStack.Push(loop); + } + } + + void RenderElement(RenderContext renderContext) + { + if (checkConditions(renderContext)) + { + if (includeExpression != null) + { + object includeValue = includeExpression.Resolve(renderContext); + if (!(includeValue is TemplateDocument includeTemplate)) + { + includeTemplate = renderContext.TemplateSource.GetTemplateByPath((string)includeValue); + } + includeTemplate.RenderTemplate(renderContext, true); + } + else + { + if (!hideTag) + { + renderContext.ContentWriter.Write("<{0}", Name); + foreach (KeyValuePair attributePair in Attributes) + renderContext.ContentWriter.Write(" {0}=\"{1}\"", attributePair.Key, attributePair.Value); + + foreach (KeyValuePair keyValuePair in expressions) + { + object value = keyValuePair.Value.Resolve(renderContext); + if (value != null) + { + renderContext.ContentWriter.Write(" {0}=\"{1}\"", keyValuePair.Key, value); + } + } + + renderContext.ContentWriter.Write(">"); + } + + if (!IsVoid) + { + foreach (Element element in Children) + { + if (element is TemplateElement templateElement) + { + templateElement.RenderTemplate(renderContext); + } + else + { + renderContext.ContentWriter.Write(element.ToString()); + } + } + + if (!hideTag) + renderContext.ContentWriter.Write("", Name); + } + } + } + } + + bool checkConditions(RenderContext renderContext) + { + foreach (NewExpression condition in conditions) + if (!condition.IsTrue(renderContext)) + return false; + return true; + } + + } +} diff --git a/html/TemplateReader.cs b/html/TemplateReader.cs new file mode 100644 index 0000000..6704231 --- /dev/null +++ b/html/TemplateReader.cs @@ -0,0 +1,70 @@ +// /** +// * File: TemplateReader.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; +namespace ln.templates.html +{ + public class TemplateReader : ElementReader + { + + public virtual TemplateDocument TemplateDocument => Document as TemplateDocument; + + public TemplateReader() + { + } + + public override DocumentElement CreateDocument() => new TemplateDocument(); + public override Element CreateElement(string tagName) => new TemplateElement(tagName); + + public override void Text(string text) + { + if (text.Contains("{{")) + { + int pOpen = 0; + int pClose = 0; + + while (pOpen < text.Length) + { + pOpen = text.IndexOf("{{", pClose); + if (pOpen == -1) + pOpen = text.Length; + + string preText = text.Substring(pClose, pOpen - pClose); + if (preText.Length > 0) + { + CurrentElement.AppendChild(CreateTextElement(preText)); + } + + if (pOpen == text.Length) + break; + + pOpen += 2; + + pClose = text.IndexOf("}}", pOpen); + if (pClose == -1) + throw new FormatException("missing }}"); + + string expr = text.Substring(pOpen, pClose - pOpen - 1); + + pClose += 2; + + CurrentElement.AppendChild(new ExpressionElement(expr)); + } + + } else + { + CurrentElement.AppendChild( + CreateTextElement(text) + ); + } + } + + } +} diff --git a/html/TextElement.cs b/html/TextElement.cs new file mode 100644 index 0000000..8541eec --- /dev/null +++ b/html/TextElement.cs @@ -0,0 +1,30 @@ +// /** +// * File: TextElement.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; + +namespace ln.templates.html +{ + public class TextElement : Element + { + public string Text { get; } + + public TextElement(string text) + :base("#text") + { + Text = text; + } + + public override void Render(TextWriter writer) + { + writer.Write(Text); + } + } +} diff --git a/ln.templates.csproj b/ln.templates.csproj new file mode 100644 index 0000000..21463e2 --- /dev/null +++ b/ln.templates.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + diff --git a/ln.templates.test/HtmlTests.cs b/ln.templates.test/HtmlTests.cs new file mode 100644 index 0000000..dabd2de --- /dev/null +++ b/ln.templates.test/HtmlTests.cs @@ -0,0 +1,71 @@ +// /** +// * File: HtmlTests.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using NUnit.Framework; +using System; +using ln.templates.html; +using System.IO; +namespace ln.templates.test +{ + [TestFixture()] + public class HtmlTests + { + [Test()] + public void TestCase() + { + html.TemplateReader templateReader = new html.TemplateReader(); + templateReader.Read("\n\n\n Hello\n\n\n

Welcome to this example.

\n

{{ 'Ich bin ein ScriptText!' }}

\n

DateTime: {{ Date }}

\n

Ein bisschen JavaScript: {{ 5 + ' mal ' + 5 + ' = ' + (5*5) }}

\n\n"); + + Console.WriteLine("Source rendered:\n{0}", templateReader.Document.ToString()); + + StringWriter stringWriter = new StringWriter(); + RenderContext renderContext = new RenderContext(stringWriter); + + renderContext.SetScriptObject("Date", DateTime.Now); + + templateReader.TemplateDocument.RenderTemplate(renderContext); + + Console.WriteLine("Template rendered:\n{0}", stringWriter.ToString()); + } + + [Test()] + public void TestFileSystemTemplateSource() + { + if (File.Exists("templates.tst/frame.html")) + File.Delete("templates.tst/frame.html"); + if (File.Exists("templates.tst/head.html")) + File.Delete("templates.tst/head.html"); + + Directory.CreateDirectory("templates.tst"); + using (FileStream fs = new FileStream("templates.tst/frame.html", FileMode.CreateNew)) + using (TextWriter tw = new StreamWriter(fs)) + { + tw.Write(""); + tw.Flush(); + } + + using (FileStream fs = new FileStream("templates.tst/head.html", FileMode.CreateNew)) + using (TextWriter tw = new StreamWriter(fs)) + { + tw.Write("{{ title }}"); + tw.Flush(); + } + + FileSystemTemplateSource fileSystemTemplateSource = new FileSystemTemplateSource("templates.tst"); + TemplateDocument templateDocument = fileSystemTemplateSource.GetTemplateByPath("frame.html"); + + StringWriter stringWriter = new StringWriter(); + RenderContext renderContext = new RenderContext(stringWriter,fileSystemTemplateSource); + + templateDocument.RenderTemplate(renderContext); + + Console.WriteLine("Document rendered to: {0}", stringWriter.ToString()); + } + } +} diff --git a/ln.templates.test/ln.templates.test.csproj b/ln.templates.test/ln.templates.test.csproj new file mode 100644 index 0000000..01b8a74 --- /dev/null +++ b/ln.templates.test/ln.templates.test.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + diff --git a/script/IScriptContext.cs b/script/IScriptContext.cs new file mode 100644 index 0000000..72d89ed --- /dev/null +++ b/script/IScriptContext.cs @@ -0,0 +1,20 @@ +// /** +// * File: IScriptContext.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Collections.Generic; +namespace ln.templates.script +{ + public interface IScriptContext + { + IEnumerable ScriptObjectNames { get; } + object GetScriptObject(string itemName); + void SetScriptObject(string itemName, object value); + } +} diff --git a/script/NewExpression.cs b/script/NewExpression.cs new file mode 100644 index 0000000..74a3353 --- /dev/null +++ b/script/NewExpression.cs @@ -0,0 +1,63 @@ +// /** +// * File: NewExpression.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using Jint; +using Jint.Native; +using Jint.Native.String; + +namespace ln.templates.script +{ + public class NewExpression + { + public string ExpressionText { get; } + + + public NewExpression(string expression) + { + ExpressionText = expression; + } + + + public virtual object Resolve(IScriptContext scriptContext) + { + Engine engine = new Engine(); + foreach (string itemName in scriptContext.ScriptObjectNames) + engine.SetValue(itemName, scriptContext.GetScriptObject(itemName)); + + JsValue jsv = engine.Eval.Call(null, new Jint.Native.JsValue[] { ExpressionText }); + return jsv.ToObject(); + } + + public virtual bool IsTrue(IScriptContext scriptContext) + { + object resolved = Resolve(scriptContext); + if (resolved == null) + return false; + if (resolved is bool b) + return b; + if (resolved is string s) + return s.Length > 0; + if (resolved is int i) + return i != 0; + if (resolved is long l) + return l != 0; + if (resolved is short sh) + return sh != 0; + if (resolved is byte by) + return by != 0; + if (resolved is float f) + return Math.Abs(f) > float.Epsilon; + if (resolved is double d) + return Math.Abs(d) > double.Epsilon; + + return true; + } + } +} diff --git a/streams/CharStream.cs b/streams/CharStream.cs new file mode 100644 index 0000000..a2572d4 --- /dev/null +++ b/streams/CharStream.cs @@ -0,0 +1,20 @@ +using System; +using System.Text; +namespace ln.templates.streams +{ + public class CharStream : PeekAbleStream + { + public CharStream(char[] buffer) + :base(buffer) + { + } + public CharStream(byte[] byteBuffer, Encoding encoding) + : this(encoding.GetChars(byteBuffer)) + { + } + public CharStream(byte[] byteBuffer) + :this(Encoding.UTF8.GetChars(byteBuffer)) + { + } + } +} diff --git a/streams/PeekableStream.cs b/streams/PeekableStream.cs new file mode 100644 index 0000000..ebc08ed --- /dev/null +++ b/streams/PeekableStream.cs @@ -0,0 +1,50 @@ +using System; +using System.Text; +namespace ln.templates.streams +{ + public class PeekAbleStream + { + private T[] buffer; + private int position; + + public int Position => position; + public bool EndOfBuffer => (position >= buffer.Length); + + public int MarkedPosition { get; set; } + + public PeekAbleStream(T[] buffer) + { + this.buffer = buffer; + } + + public T Read(){ + if (position < buffer.Length) + return buffer[position++]; + + throw new IndexOutOfRangeException("Tried to read after end of buffer"); + } + + public T Peek(int shift = 0) + { + if ((position + shift) < buffer.Length) + return buffer[position + shift]; + + throw new IndexOutOfRangeException("Tried to peek after end of buffer"); + } + + public void Mark() + { + MarkedPosition = position; + } + + public T[] GetMarkedIntervall() + { + T[] intervall = new T[position - MarkedPosition]; + Array.Copy(buffer, MarkedPosition, intervall, 0, intervall.Length); + return intervall; + } + + public int Remaining => buffer.Length - position; + + } +}