diff --git a/dotnet/Applications/Mocha.Compiler/Mocha.Compiler.csproj b/dotnet/Applications/Mocha.Compiler/Mocha.Compiler.csproj new file mode 100755 index 0000000..3d5b1fe --- /dev/null +++ b/dotnet/Applications/Mocha.Compiler/Mocha.Compiler.csproj @@ -0,0 +1,65 @@ + + + + Debug + AnyCPU + {DBEE1BAC-D658-420D-96D6-417523326650} + Exe + Mocha.Compiler + mcc + + + true + full + false + ..\..\Output\Debug + DEBUG; + prompt + 4 + false + + + true + ..\..\Output\Release + prompt + 4 + false + + + + + + + + + + + {2D4737E6-6D95-408A-90DB-8DFF38147E85} + UniversalEditor.Core + + + {3D0893DC-9961-4EAA-BF6C-604686068D09} + UniversalEditor.Plugins.Mocha + + + {30467E5C-05BC-4856-AADC-13906EF4CADD} + UniversalEditor.Essential + + + {00266B21-35C9-4A7F-A6BA-D54D7FDCC25C} + MBS.Framework + + + {1D5668E8-4540-426B-922A-E3A739D16713} + Mocha.MSBuild.Tasks + + + {8D3211A6-B2D6-4A26-ABE3-5B57636A4196} + Mocha.Core + + + + + + + \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Compiler/Program.cs b/dotnet/Applications/Mocha.Compiler/Program.cs new file mode 100755 index 0000000..b5bc8ee --- /dev/null +++ b/dotnet/Applications/Mocha.Compiler/Program.cs @@ -0,0 +1,203 @@ +// +// Program.cs - main entry point for the ZeQuaL compiler (zq) +// +// Author: +// Michael Becker +// +// Copyright (c) 2020 Mike Becker's Software +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +using System; +using System.Collections.Generic; +using Mocha.Core; +using UniversalEditor; +using UniversalEditor.Accessors; +using UniversalEditor.Plugins.Mocha.DataFormats.MochaBinary; +using UniversalEditor.Plugins.Mocha.DataFormats.MochaXML; +using UniversalEditor.Plugins.Mocha.ObjectModels.MochaClassLibrary; + +namespace Mocha.Compiler +{ + public class Program + { + public static void Main(string[] args) + { + List listFileNames = new List(); + + string outputFileName = "output.mcx"; + bool foundFileName = false; + for (int i = 0; i < args.Length; i++) + { + if (args[i].StartsWith("/") && !foundFileName) + { + if (args[i].StartsWith("/out:")) + { + outputFileName = args[i].Substring(5); + } + } + else + { + // is file name + foundFileName = true; + + listFileNames.Add(args[i]); + } + } + + listFileNames.Sort(); + + MochaClassLibraryObjectModel mcl = new MochaClassLibraryObjectModel(); + MochaBinaryDataFormat mcx = new MochaBinaryDataFormat(); + MochaXMLDataFormat xml = new MochaXMLDataFormat(); + + System.Text.StringBuilder sb = new System.Text.StringBuilder(); + int totalInstances = 0, totalRelationships = 0; + for (int i = 0; i < listFileNames.Count; i++) + { + Console.Error.WriteLine("reading {0}", listFileNames[i]); + + if (!System.IO.File.Exists(listFileNames[i])) + { + MBS.Framework.ConsoleExtensions.LogMSBuildMessage(MBS.Framework.MessageSeverity.Error, "File not found", "MCX0003", listFileNames[i]); + continue; + } + + MochaClassLibraryObjectModel mcl1 = new MochaClassLibraryObjectModel(); + FileAccessor fain = new FileAccessor(listFileNames[i]); + Document.Load(mcl1, xml, fain); + + for (int j = 0; j < mcl1.Libraries.Count; j++) + { + mcl.Libraries.Merge(mcl1.Libraries[j]); + totalInstances += mcl1.Libraries[j].Instances.Count; + totalRelationships += mcl1.Libraries[j].Relationships.Count; + } + for (int j = 0; j < mcl1.Tenants.Count; j++) + { + Console.Error.WriteLine("registered tenant {0}", mcl1.Tenants[j].Name); + mcl.Tenants.Merge(mcl1.Tenants[j]); + } + } + + ZqLinkRelationships(mcl); + + Console.Error.WriteLine("wrote {0} libraries with {1} instances and {2} relationships total", mcl.Libraries.Count, totalInstances, totalRelationships); + + /* + // LINKER SANITY CHECK + // we need to load all the referenced MCX libraries in a testing + // environment and check MCX relationships for existing instances + foreach (MochaLibrary library in mcl.Libraries) + { + CheckInstancesForMochaStore(mcl, library); + } + foreach (MochaTenant tenant in mcl.Tenants) + { + CheckInstancesForMochaStore(mcl, tenant); + } + */ + + FileAccessor faout = new FileAccessor(outputFileName, true, true); + Document.Save(mcl, mcx, faout); + } + + private static void ZqLinkRelationships(MochaClassLibraryObjectModel mcl) + { + for (int i = 0; i < mcl.Libraries.Count; i++) + { + ZqLinkRelationships(mcl, mcl.Libraries[i]); + } + for (int i = 0; i < mcl.Tenants.Count; i++) + { + ZqLinkRelationships(mcl, mcl.Tenants[i]); + } + } + private static void ZqLinkRelationships(MochaClassLibraryObjectModel mcl, IMochaStore store) + { + for (int j = 0; j < store.Relationships.Count; j++) + { + MochaInstance instRel = mcl.FindInstance(store.Relationships[j].RelationshipInstanceID); + if (instRel != null) + { + MochaRelationship relSiblingRel = mcl.FindRelationship(new RelationshipKey(instRel.ID, KnownRelationshipGuids.Relationship__has_sibling__Relationship)); + if (relSiblingRel != null) + { + MochaInstance instSibling = mcl.FindInstance(relSiblingRel.DestinationInstanceIDs[0]); + if (instSibling != null) + { + for (int k = 0; k < store.Relationships[j].DestinationInstanceIDs.Count; k++) + { + RelationshipKey rkSibling = new RelationshipKey(store.Relationships[j].DestinationInstanceIDs[k], instSibling.ID); + MochaRelationship relSibling = store.Relationships[rkSibling]; + if (relSibling == null) + { + Console.Error.WriteLine("zq2: linking relationship {0}", instSibling.ID.ToString("B")); + relSibling = new MochaRelationship() { SourceInstanceID = store.Relationships[j].DestinationInstanceIDs[k], RelationshipInstanceID = instSibling.ID }; + store.Relationships.Add(relSibling); + } + + if (!relSibling.DestinationInstanceIDs.Contains(store.Relationships[j].SourceInstanceID)) + { + relSibling.DestinationInstanceIDs.Add(store.Relationships[j].SourceInstanceID); + } + } + } + else + { + Console.Error.WriteLine("zq2: no sibling relationship '{0}' found", relSiblingRel.DestinationInstanceIDs[0].ToString("B")); + } + } + } + } + } + + private static void CheckInstancesForMochaStore(MochaClassLibraryObjectModel mcl, IMochaStore library) + { + foreach (MochaRelationship rel in library.Relationships) + { + if (FindInstance(mcl, library, rel.SourceInstanceID) == null) + { + MBS.Framework.ConsoleExtensions.LogMSBuildMessage(MBS.Framework.MessageSeverity.Error, String.Format("relationship references nonexistent sourceInstanceId '{0}'", rel.SourceInstanceID)); + } + if (FindInstance(mcl, library, rel.RelationshipInstanceID) == null) + { + MBS.Framework.ConsoleExtensions.LogMSBuildMessage(MBS.Framework.MessageSeverity.Error, String.Format("relationship references nonexistent relationshipInstanceId '{0}'", rel.RelationshipInstanceID)); + } + foreach (Guid id in rel.DestinationInstanceIDs) + { + if (FindInstance(mcl, library, id) == null) + { + MBS.Framework.ConsoleExtensions.LogMSBuildMessage(MBS.Framework.MessageSeverity.Error, String.Format("relationship references nonexistent target instanceReference '{0}'", id)); + } + } + } + } + + private static MochaInstance FindInstance(MochaClassLibraryObjectModel mcl, IMochaStore store, Guid id) + { + MochaInstance inst = store.FindInstance(id); + if (inst != null) + { + return inst; + } + foreach (Guid libraryId in store.LibraryReferences) + { + inst = mcl.Libraries[libraryId].FindInstance(id); + if (inst != null) + return inst; + } + return null; + } + } +} diff --git a/dotnet/Applications/Mocha.Compiler/Properties/AssemblyInfo.cs b/dotnet/Applications/Mocha.Compiler/Properties/AssemblyInfo.cs new file mode 100755 index 0000000..975c66b --- /dev/null +++ b/dotnet/Applications/Mocha.Compiler/Properties/AssemblyInfo.cs @@ -0,0 +1,46 @@ +// +// AssemblyInfo.cs +// +// Author: +// Michael Becker +// +// Copyright (c) 2020 Mike Becker's Software +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("Mocha.Compiler")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Mike Becker's Software")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("Mike Becker's Software")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/dotnet/Applications/Mocha.Compiler/obj/Debug/.NETFramework,Version=v4.0.AssemblyAttributes.cs b/dotnet/Applications/Mocha.Compiler/obj/Debug/.NETFramework,Version=v4.0.AssemblyAttributes.cs new file mode 100644 index 0000000..5d01041 --- /dev/null +++ b/dotnet/Applications/Mocha.Compiler/obj/Debug/.NETFramework,Version=v4.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.0", FrameworkDisplayName = ".NET Framework 4")] diff --git a/dotnet/Applications/Mocha.Compiler/obj/Debug/Mocha.Compiler.csproj.AssemblyReference.cache b/dotnet/Applications/Mocha.Compiler/obj/Debug/Mocha.Compiler.csproj.AssemblyReference.cache new file mode 100644 index 0000000..b79b90d Binary files /dev/null and b/dotnet/Applications/Mocha.Compiler/obj/Debug/Mocha.Compiler.csproj.AssemblyReference.cache differ diff --git a/dotnet/Applications/Mocha.Compiler/obj/Debug/Mocha.Compiler.csproj.CopyComplete b/dotnet/Applications/Mocha.Compiler/obj/Debug/Mocha.Compiler.csproj.CopyComplete new file mode 100644 index 0000000..e69de29 diff --git a/dotnet/Applications/Mocha.Compiler/obj/Debug/Mocha.Compiler.csproj.CoreCompileInputs.cache b/dotnet/Applications/Mocha.Compiler/obj/Debug/Mocha.Compiler.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..f19f9bb --- /dev/null +++ b/dotnet/Applications/Mocha.Compiler/obj/Debug/Mocha.Compiler.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +5cdeef7b70eda6eae969492610195a3ac3d59ee6 diff --git a/dotnet/Applications/Mocha.Compiler/obj/Debug/Mocha.Compiler.csproj.FileListAbsolute.txt b/dotnet/Applications/Mocha.Compiler/obj/Debug/Mocha.Compiler.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..19dc8fe --- /dev/null +++ b/dotnet/Applications/Mocha.Compiler/obj/Debug/Mocha.Compiler.csproj.FileListAbsolute.txt @@ -0,0 +1,7 @@ +/home/beckermj/Documents/Projects/Mocha/Output/Debug/mcc.exe +/home/beckermj/Documents/Projects/Mocha/Output/Debug/mcc.pdb +/home/beckermj/Documents/Projects/Mocha/Applications/Mocha.Compiler/obj/Debug/Mocha.Compiler.csproj.AssemblyReference.cache +/home/beckermj/Documents/Projects/Mocha/Applications/Mocha.Compiler/obj/Debug/Mocha.Compiler.csproj.CoreCompileInputs.cache +/home/beckermj/Documents/Projects/Mocha/Applications/Mocha.Compiler/obj/Debug/Mocha.Compiler.csproj.CopyComplete +/home/beckermj/Documents/Projects/Mocha/Applications/Mocha.Compiler/obj/Debug/mcc.exe +/home/beckermj/Documents/Projects/Mocha/Applications/Mocha.Compiler/obj/Debug/mcc.pdb diff --git a/dotnet/Applications/Mocha.Compiler/obj/Debug/mcc.exe b/dotnet/Applications/Mocha.Compiler/obj/Debug/mcc.exe new file mode 100644 index 0000000..60acd1a Binary files /dev/null and b/dotnet/Applications/Mocha.Compiler/obj/Debug/mcc.exe differ diff --git a/dotnet/Applications/Mocha.Compiler/obj/Debug/mcc.pdb b/dotnet/Applications/Mocha.Compiler/obj/Debug/mcc.pdb new file mode 100644 index 0000000..592236a Binary files /dev/null and b/dotnet/Applications/Mocha.Compiler/obj/Debug/mcc.pdb differ diff --git a/dotnet/Applications/Mocha.Compiler/test.xml b/dotnet/Applications/Mocha.Compiler/test.xml new file mode 100755 index 0000000..8dad92d --- /dev/null +++ b/dotnet/Applications/Mocha.Compiler/test.xml @@ -0,0 +1,369 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/Applications/Mocha.Debugger/Mocha.Debugger.csproj b/dotnet/Applications/Mocha.Debugger/Mocha.Debugger.csproj new file mode 100755 index 0000000..606c84c --- /dev/null +++ b/dotnet/Applications/Mocha.Debugger/Mocha.Debugger.csproj @@ -0,0 +1,69 @@ + + + + Debug + AnyCPU + {FF61C373-835F-4160-8499-30324E8B9909} + Exe + Mocha.Debugger + Mocha.Debugger + v4.7 + + + true + full + false + ..\..\Output\Debug + DEBUG; + prompt + 4 + true + mcxdebug + + + true + bin\Release + prompt + 4 + true + + + + + + + + + + + + {8588928F-2868-4248-8993-533307A05C0C} + Mocha.OMS + + + {C9A3AE1D-0658-4A70-BC48-9D8DEF9C205B} + Mocha.Storage + + + {A4B8D6D8-3365-49FC-8123-58B3749A5427} + Mocha.Storage.Local + + + {5743EE32-F3ED-4162-A0E3-B105503BF139} + MBS.Networking.Plugins.HyperTextTransfer + + + {DBD65B3F-81C8-4E44-B268-3FAB3B12AA1E} + MBS.Networking + + + {00266B21-35C9-4A7F-A6BA-D54D7FDCC25C} + MBS.Framework + + + {8D3211A6-B2D6-4A26-ABE3-5B57636A4196} + Mocha.Core + + + + \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Debugger/Program.cs b/dotnet/Applications/Mocha.Debugger/Program.cs new file mode 100755 index 0000000..923d106 --- /dev/null +++ b/dotnet/Applications/Mocha.Debugger/Program.cs @@ -0,0 +1,175 @@ +// +// Program.cs +// +// Author: +// Michael Becker +// +// Copyright (c) 2022 Mike Becker's Software +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +using System; +using System.Net.Sockets; +using System.Text; +using Mocha.Core; +using Mocha.OMS; + +namespace Mocha.Debugger +{ + class MainClass + { + private static Oms _CurrentOms = null; + + public static void Main(string[] args) + { + _CurrentOms = InitializeOms(); + _CurrentOms.TenantName = "default"; + + System.Net.Sockets.TcpListener listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Any, 63320); + listener.Start(); + + while (true) + { + System.Net.Sockets.TcpClient client = listener.AcceptTcpClient(); + + System.Threading.Thread t = new System.Threading.Thread(t_ParameterizedThreadStart); + t.Start(client); + } + } + + + public static Oms InitializeOms() + { + Oms oms = null; + bool useLocalOms = true; + if (useLocalOms) + { + oms = new LocalOms(); + (oms as LocalOms).Environment = new OmsEnvironment(new Storage.Local.LocalStorageProvider("/usr/share/mocha/system")); + oms.Environment.Initialize(); + oms.Environment.StorageProvider.DefaultTenantName = oms.TenantName; + } + else + { + /* + // attempt to connect + oms = new OMSClient(); + + string serverName = Mocha.Web.Configuration.ConfigurationSections.OMS.OMSConfigurationSection.Settings.Server.HostName; + int portNumber = Mocha.Web.Configuration.ConfigurationSections.OMS.OMSConfigurationSection.Settings.Server.PortNumber; + + System.Net.IPHostEntry entry = System.Net.Dns.GetHostEntry(serverName); + if (entry != null) + { + try + { + oms.Connect(entry.AddressList[0], portNumber); + } + catch (System.Net.Sockets.SocketException ex) + { + // OMSUnavailable.Visible = true; + } + } + */ + } + return oms; + } + + private static void t_ParameterizedThreadStart(object parm) + { + System.Net.Sockets.TcpClient client = (System.Net.Sockets.TcpClient)parm; + + System.IO.StreamReader sr = new System.IO.StreamReader(client.GetStream()); + if (sr.EndOfStream) + return; + + + string firstline = sr.ReadLine(); + if (String.IsNullOrEmpty(firstline)) + return; + + MBS.Networking.Protocols.HyperTextTransfer.Request req = MBS.Networking.Protocols.HyperTextTransfer.Request.Parse(firstline); + if (req.Method == "GET") + { + Uri uri = new Uri(String.Format("http://localhost:63320{0}", req.Path)); + System.Collections.Specialized.NameValueCollection nvc = System.Web.HttpUtility.ParseQueryString(uri.Query); + + string reff = nvc["ref"]; + if (!String.IsNullOrEmpty(reff)) + { + if (Guid.TryParse(reff, out Guid guid)) + { + + lock (_CurrentOms) + { + if (nvc["tenant"] != null) + { + _CurrentOms.TenantName = nvc["tenant"]; + } + else + { + _CurrentOms.TenantName = "default"; + } + + Instance inst = _CurrentOms.GetInstance(guid); + if (inst != null) + { + Instance instDefinition = _CurrentOms.GetRelatedInstance(inst, KnownRelationshipGuids.Instance__has__Instance_Definition); + + // FIXME: we don't know how to pass the project name into this + string rootpath = "/home/beckermj/Documents/Projects/Mocha.0/Content/Mocha.System"; + string filename = _CurrentOms.GetAttributeValue(instDefinition, KnownAttributeGuids.Text.DebugDefinitionFileName); + decimal linenum = _CurrentOms.GetAttributeValue(instDefinition, KnownAttributeGuids.Numeric.DebugDefinitionLineNumber); + decimal colnum = _CurrentOms.GetAttributeValue(instDefinition, KnownAttributeGuids.Numeric.DebugDefinitionColumnNumber); + + string args = String.Format("\"{0}/{1}\";{2};{3}", rootpath, filename, linenum, colnum); + Console.WriteLine("mcxdebug: launching monodevelop {0}", args); + System.Diagnostics.ProcessStartInfo psi = new System.Diagnostics.ProcessStartInfo("monodevelop", args); + psi.UseShellExecute = true; + System.Diagnostics.Process.Start(psi); + + RespondWith(client, _CurrentOms.GetInstanceText(inst), _CurrentOms.GetInstanceText(_CurrentOms.GetRelatedInstance(inst, KnownRelationshipGuids.Instance__for__Module))); + return; + } + } + } + } + } + + while (!sr.EndOfStream) + { + string nextline = sr.ReadLine(); + if (String.IsNullOrEmpty(nextline)) + break; + + string[] nextparts = nextline.Split(new char[] { ':' }, 2); + } + } + + private static void RespondWith(TcpClient client, string nodename, string projname) + { + StringBuilder resp = new StringBuilder(); + resp.AppendLine("HTTP/1.1 200 OK"); + resp.AppendLine(); + resp.AppendLine("Mocha HTTP Debugger Plugin"); + resp.AppendLine("Mocha HTTP Debugger Plugin"); + resp.AppendLine("

The requested node has been opened in your IDE

"); + resp.AppendLine(String.Format("

Node: {0}

Project: {1}

", nodename, projname)); + resp.AppendLine(""); + resp.AppendLine(); + byte[] respdata = Encoding.UTF8.GetBytes(resp.ToString()); + client.GetStream().Write(respdata, 0, respdata.Length); + client.Close(); + } + } +} diff --git a/dotnet/Applications/Mocha.Debugger/Properties/AssemblyInfo.cs b/dotnet/Applications/Mocha.Debugger/Properties/AssemblyInfo.cs new file mode 100755 index 0000000..f03ca78 --- /dev/null +++ b/dotnet/Applications/Mocha.Debugger/Properties/AssemblyInfo.cs @@ -0,0 +1,46 @@ +// +// AssemblyInfo.cs +// +// Author: +// Michael Becker +// +// Copyright (c) 2022 Mike Becker's Software +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("Mocha.Debugger")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Mike Becker's Software")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("Mike Becker's Software")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/dotnet/Applications/Mocha.Debugger/obj/Debug/.NETFramework,Version=v4.7.AssemblyAttributes.cs b/dotnet/Applications/Mocha.Debugger/obj/Debug/.NETFramework,Version=v4.7.AssemblyAttributes.cs new file mode 100644 index 0000000..4c82453 --- /dev/null +++ b/dotnet/Applications/Mocha.Debugger/obj/Debug/.NETFramework,Version=v4.7.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7", FrameworkDisplayName = ".NET Framework 4.7")] diff --git a/dotnet/Applications/Mocha.Debugger/obj/Debug/Mocha.Debugger.csproj.AssemblyReference.cache b/dotnet/Applications/Mocha.Debugger/obj/Debug/Mocha.Debugger.csproj.AssemblyReference.cache new file mode 100644 index 0000000..8c02396 Binary files /dev/null and b/dotnet/Applications/Mocha.Debugger/obj/Debug/Mocha.Debugger.csproj.AssemblyReference.cache differ diff --git a/dotnet/Applications/Mocha.Debugger/obj/Debug/Mocha.Debugger.csproj.CopyComplete b/dotnet/Applications/Mocha.Debugger/obj/Debug/Mocha.Debugger.csproj.CopyComplete new file mode 100644 index 0000000..e69de29 diff --git a/dotnet/Applications/Mocha.Debugger/obj/Debug/Mocha.Debugger.csproj.CoreCompileInputs.cache b/dotnet/Applications/Mocha.Debugger/obj/Debug/Mocha.Debugger.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..fcace06 --- /dev/null +++ b/dotnet/Applications/Mocha.Debugger/obj/Debug/Mocha.Debugger.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +65bc769232bbd0a8cc10bbb13b101907578febc4 diff --git a/dotnet/Applications/Mocha.Debugger/obj/Debug/Mocha.Debugger.csproj.FileListAbsolute.txt b/dotnet/Applications/Mocha.Debugger/obj/Debug/Mocha.Debugger.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..861e534 --- /dev/null +++ b/dotnet/Applications/Mocha.Debugger/obj/Debug/Mocha.Debugger.csproj.FileListAbsolute.txt @@ -0,0 +1,7 @@ +/home/beckermj/Documents/Projects/Mocha/Output/Debug/mcxdebug.exe +/home/beckermj/Documents/Projects/Mocha/Output/Debug/mcxdebug.pdb +/home/beckermj/Documents/Projects/Mocha/Applications/Mocha.Debugger/obj/Debug/Mocha.Debugger.csproj.AssemblyReference.cache +/home/beckermj/Documents/Projects/Mocha/Applications/Mocha.Debugger/obj/Debug/Mocha.Debugger.csproj.CoreCompileInputs.cache +/home/beckermj/Documents/Projects/Mocha/Applications/Mocha.Debugger/obj/Debug/Mocha.Debugger.csproj.CopyComplete +/home/beckermj/Documents/Projects/Mocha/Applications/Mocha.Debugger/obj/Debug/mcxdebug.exe +/home/beckermj/Documents/Projects/Mocha/Applications/Mocha.Debugger/obj/Debug/mcxdebug.pdb diff --git a/dotnet/Applications/Mocha.Debugger/obj/Debug/mcxdebug.exe b/dotnet/Applications/Mocha.Debugger/obj/Debug/mcxdebug.exe new file mode 100644 index 0000000..7e226c7 Binary files /dev/null and b/dotnet/Applications/Mocha.Debugger/obj/Debug/mcxdebug.exe differ diff --git a/dotnet/Applications/Mocha.Debugger/obj/Debug/mcxdebug.pdb b/dotnet/Applications/Mocha.Debugger/obj/Debug/mcxdebug.pdb new file mode 100644 index 0000000..030a5b6 Binary files /dev/null and b/dotnet/Applications/Mocha.Debugger/obj/Debug/mcxdebug.pdb differ diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Cache/ICache.cs b/dotnet/Applications/Mocha.Web.Server/Core/Cache/ICache.cs new file mode 100755 index 0000000..c0e874e --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Cache/ICache.cs @@ -0,0 +1,11 @@ +namespace dotless.Core.Cache +{ + using System.Collections.Generic; + + public interface ICache + { + void Insert(string cacheKey, IEnumerable fileDependancies, string css); + bool Exists(string cacheKey); + string Retrieve(string cacheKey); + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Cache/InMemoryCache.cs b/dotnet/Applications/Mocha.Web.Server/Core/Cache/InMemoryCache.cs new file mode 100755 index 0000000..f579aa5 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Cache/InMemoryCache.cs @@ -0,0 +1,32 @@ +namespace dotless.Core.Cache +{ + using System.Collections.Generic; + + public class InMemoryCache : ICache + { + private readonly Dictionary _cache; + + public InMemoryCache() + { + _cache = new Dictionary(); + } + + public void Insert(string fileName, IEnumerable imports, string css) + { + _cache[fileName] = css; + } + + public bool Exists(string filename) + { + return _cache.ContainsKey(filename); + } + + public string Retrieve(string filename) + { + if (_cache.ContainsKey(filename)) + return _cache[filename]; + + return ""; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Engine/CacheDecorator.cs b/dotnet/Applications/Mocha.Web.Server/Core/Engine/CacheDecorator.cs new file mode 100755 index 0000000..b1bc774 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Engine/CacheDecorator.cs @@ -0,0 +1,78 @@ +namespace dotless.Core +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + using Cache; + using Loggers; + + public class CacheDecorator : ILessEngine + { + public readonly ILessEngine Underlying; + public readonly ICache Cache; + public ILogger Logger { get; set; } + + public CacheDecorator(ILessEngine underlying, ICache cache) : this(underlying, cache, NullLogger.Instance) + {} + + public CacheDecorator(ILessEngine underlying, ICache cache, ILogger logger) + { + Underlying = underlying; + Cache = cache; + Logger = logger; + } + + public string TransformToCss(string source, string fileName) + { + //Compute Cache Key + var hash = ComputeContentHash(source); + var cacheKey = fileName + hash; + if (!Cache.Exists(cacheKey)) + { + Logger.Debug(String.Format("Inserting cache entry for {0}", cacheKey)); + + var css = Underlying.TransformToCss(source, fileName); + var dependancies = new[] { fileName }.Concat(GetImports()); + + Cache.Insert(cacheKey, dependancies, css); + + return css; + } + Logger.Debug(String.Format("Retrieving cache entry {0}", cacheKey)); + return Cache.Retrieve(cacheKey); + } + + private string ComputeContentHash(string source) + { + SHA1 sha1 = SHA1.Create(); + byte[] computeHash = sha1.ComputeHash(Encoding.Default.GetBytes(source)); + return Convert.ToBase64String(computeHash); + } + + public IEnumerable GetImports() + { + return Underlying.GetImports(); + } + + public void ResetImports() + { + Underlying.ResetImports(); + } + + public bool LastTransformationSuccessful + { + get + { + return Underlying.LastTransformationSuccessful; + } + } + + public string CurrentDirectory + { + get { return Underlying.CurrentDirectory; } + set { Underlying.CurrentDirectory = value; } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Engine/ILessEngine.cs b/dotnet/Applications/Mocha.Web.Server/Core/Engine/ILessEngine.cs new file mode 100755 index 0000000..abc7842 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Engine/ILessEngine.cs @@ -0,0 +1,15 @@ +namespace dotless.Core +{ + using System.Collections.Generic; + using System; + + public interface ILessEngine + { + string TransformToCss(string source, string fileName); + void ResetImports(); + IEnumerable GetImports(); + bool LastTransformationSuccessful { get; } + + string CurrentDirectory { get; set; } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Engine/LessEngine.cs b/dotnet/Applications/Mocha.Web.Server/Core/Engine/LessEngine.cs new file mode 100755 index 0000000..f5be716 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Engine/LessEngine.cs @@ -0,0 +1,145 @@ +using System; +using dotless.Core.Parser; +using dotless.Core.Parser.Tree; +using dotless.Core.Plugins; +using dotless.Core.Stylizers; + +namespace dotless.Core +{ + using System.Collections.Generic; + using System.Linq; + using Exceptions; + using Loggers; + using Parser.Infrastructure; + + public class LessEngine : ILessEngine + { + public Parser.Parser Parser { get; set; } + public ILogger Logger { get; set; } + public bool Compress { get; set; } + public bool Debug { get; set; } + [Obsolete("The Variable Redefines feature has been removed to align with less.js")] + public bool DisableVariableRedefines { get; set; } + [Obsolete("The Color Compression feature has been removed to align with less.js")] + public bool DisableColorCompression { get; set; } + public bool KeepFirstSpecialComment { get; set; } + public bool StrictMath { get; set; } + public Env Env { get; set; } + public IEnumerable Plugins { get; set; } + public bool LastTransformationSuccessful { get; private set; } + + public string CurrentDirectory + { + get { return Parser.CurrentDirectory; } + set { Parser.CurrentDirectory = value; } + } + + public LessEngine(Parser.Parser parser, ILogger logger, dotless.Core.configuration.DotlessConfiguration config) + : this(parser, logger, config.MinifyOutput, config.Debug, config.DisableVariableRedefines, config.DisableColorCompression, config.KeepFirstSpecialComment, config.Plugins) + { + } + + public LessEngine(Parser.Parser parser, ILogger logger, bool compress, bool debug, bool disableVariableRedefines, bool disableColorCompression, bool keepFirstSpecialComment, bool strictMath, IEnumerable plugins) + { + Parser = parser; + Logger = logger; + Compress = compress; + Debug = debug; + Plugins = plugins; + KeepFirstSpecialComment = keepFirstSpecialComment; + StrictMath = strictMath; + } + + public LessEngine(Parser.Parser parser, ILogger logger, bool compress, bool debug, bool disableVariableRedefines, bool disableColorCompression, bool keepFirstSpecialComment, IEnumerable plugins) + : this(parser, logger, compress, debug, disableVariableRedefines, disableColorCompression, keepFirstSpecialComment, false, plugins) + { + } + + public LessEngine(Parser.Parser parser, ILogger logger, bool compress, bool debug) + : this(parser, logger, compress, debug, false, false, false, null) + { + } + + public LessEngine(Parser.Parser parser, ILogger logger, bool compress, bool debug, bool disableVariableRedefines) + : this(parser, logger, compress, debug, disableVariableRedefines, false, false, null) + { + } + + public LessEngine(Parser.Parser parser) + : this(parser, new ConsoleLogger(LogLevel.Error), false, false, false, false, false, null) + { + } + + public LessEngine() + : this(new Parser.Parser()) + { + } + + public string TransformToCss(string source, string fileName) + { + try + { + Parser.StrictMath = StrictMath; + var tree = Parser.Parse(source, fileName); + + var env = Env ?? + new Env(Parser) + { + Compress = Compress, + Debug = Debug, + KeepFirstSpecialComment = KeepFirstSpecialComment, + }; + + if (Plugins != null) + { + foreach (IPluginConfigurator configurator in Plugins) + { + env.AddPlugin(configurator.CreatePlugin()); + } + } + + var css = tree.ToCSS(env); + + var stylizer = new PlainStylizer(); + + foreach (var unmatchedExtension in env.FindUnmatchedExtensions()) { + Logger.Warn("Warning: extend '{0}' has no matches {1}\n", + unmatchedExtension.BaseSelector.ToCSS(env).Trim(), + stylizer.Stylize(new Zone(unmatchedExtension.Extend.Location)).Trim()); + } + + tree.Accept(DelegateVisitor.For(m => { + foreach (var unmatchedExtension in m.FindUnmatchedExtensions()) { + Logger.Warn("Warning: extend '{0}' has no matches {1}\n", + unmatchedExtension.BaseSelector.ToCSS(env).Trim(), + stylizer.Stylize(new Zone(unmatchedExtension.Extend.Location)).Trim()); + } + })); + + LastTransformationSuccessful = true; + return css; + } + catch (ParserException e) + { + LastTransformationSuccessful = false; + LastTransformationError = e; + Logger.Error(e.Message); + } + + return ""; + } + + public ParserException LastTransformationError { get; set; } + + public IEnumerable GetImports() + { + return Parser.Importer.GetImports(); + } + + public void ResetImports() + { + Parser.Importer.ResetImports(); + } + + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Engine/ParameterDecorator.cs b/dotnet/Applications/Mocha.Web.Server/Core/Engine/ParameterDecorator.cs new file mode 100755 index 0000000..17926a7 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Engine/ParameterDecorator.cs @@ -0,0 +1,81 @@ +using System; +using dotless.Core.Exceptions; +using dotless.Core.Parser.Infrastructure; + +namespace dotless.Core +{ + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Parameters; + + public class ParameterDecorator : ILessEngine + { + public readonly ILessEngine Underlying; + private readonly IParameterSource parameterSource; + + public ParameterDecorator(ILessEngine underlying, IParameterSource parameterSource) + { + this.Underlying = underlying; + this.parameterSource = parameterSource; + } + + public string TransformToCss(string source, string fileName) + { + var sb = new StringBuilder(); + var parameters = parameterSource.GetParameters() + .Where(ValueIsNotNullOrEmpty); + + var parser = new Parser.Parser(); + sb.Append(source); + foreach (var parameter in parameters) + { + sb.AppendLine(); + var variableDeclaration = string.Format("@{0}: {1};", parameter.Key, parameter.Value); + + try + { + // Attempt to evaluate the generated variable to see if it's OK + parser.Parse(variableDeclaration, "").ToCSS(new Env()); + sb.Append(variableDeclaration); + } + catch (ParserException) + { + // Result wasn't valid LESS, output a comment instead + sb.AppendFormat("/* Omitting variable '{0}'. The expression '{1}' is not valid. */", parameter.Key, + parameter.Value); + } + } + return Underlying.TransformToCss(sb.ToString(), fileName); + } + + public IEnumerable GetImports() + { + return Underlying.GetImports(); + } + + public void ResetImports() + { + Underlying.ResetImports(); + } + + public bool LastTransformationSuccessful + { + get + { + return Underlying.LastTransformationSuccessful; + } + } + + private static bool ValueIsNotNullOrEmpty(KeyValuePair kvp) + { + return !string.IsNullOrEmpty(kvp.Value); + } + + public string CurrentDirectory + { + get { return Underlying.CurrentDirectory; } + set { Underlying.CurrentDirectory = value; } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Exceptions/ParserException.cs b/dotnet/Applications/Mocha.Web.Server/Core/Exceptions/ParserException.cs new file mode 100755 index 0000000..c374ba4 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Exceptions/ParserException.cs @@ -0,0 +1,26 @@ +using dotless.Core.Parser; + +namespace dotless.Core.Exceptions +{ + using System; + + public class ParserException : Exception + { + public ParserException(string message) + : base(message) + { + } + + public ParserException(string message, Exception innerException) + : base(message, innerException) + { + } + + public ParserException(string message, Exception innerException, Zone errorLocation) + : base(message, innerException) { + ErrorLocation = errorLocation; + } + + public Zone ErrorLocation { get; set; } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Exceptions/ParsingException.cs b/dotnet/Applications/Mocha.Web.Server/Core/Exceptions/ParsingException.cs new file mode 100755 index 0000000..61bf107 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Exceptions/ParsingException.cs @@ -0,0 +1,28 @@ +namespace dotless.Core.Exceptions +{ + using System; + using dotless.Core.Parser; + + public class ParsingException : Exception + { + public NodeLocation Location { get; set; } + public NodeLocation CallLocation { get; set; } + + public ParsingException(string message, NodeLocation location) : this(message, null, location, null) { } + + public ParsingException(string message, NodeLocation location, NodeLocation callLocation) : this(message, null, location, callLocation) { } + + public ParsingException(Exception innerException, NodeLocation location) : this(innerException, location, null) { } + + public ParsingException(Exception innerException, NodeLocation location, NodeLocation callLocation) : this(innerException.Message, innerException, location, callLocation) { } + + public ParsingException(string message, Exception innerException, NodeLocation location) : this(message, innerException, location, null) { } + + public ParsingException(string message, Exception innerException, NodeLocation location, NodeLocation callLocation) + : base(message, innerException) + { + Location = location; + CallLocation = callLocation; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Extensions/MissingTypeRegistrationException.cs b/dotnet/Applications/Mocha.Web.Server/Core/Extensions/MissingTypeRegistrationException.cs new file mode 100755 index 0000000..58c095e --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Extensions/MissingTypeRegistrationException.cs @@ -0,0 +1,45 @@ +// Copyright (c) 2015 Kristian Hellang + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace dotless.Core.Extensions +{ + public class MissingTypeRegistrationException : InvalidOperationException + { + public MissingTypeRegistrationException(Type serviceType) + : base($"Could not find any registered services for type '{GetFriendlyName(serviceType)}'.") + { + ServiceType = serviceType; + } + + public Type ServiceType { get; } + private static string GetFriendlyName(Type type) + { + if (type == typeof(int)) return "int"; + if (type == typeof(short)) return "short"; + if (type == typeof(byte)) return "byte"; + if (type == typeof(bool)) return "bool"; + if (type == typeof(char)) return "char"; + if (type == typeof(long)) return "long"; + if (type == typeof(float)) return "float"; + if (type == typeof(double)) return "double"; + if (type == typeof(decimal)) return "decimal"; + if (type == typeof(string)) return "string"; + if (type == typeof(object)) return "object"; + // var typeInfo = type.GetTypeInfo(); + if (type.IsGenericType) return GetGenericFriendlyName(type); + return type.Name; + } + + private static string GetGenericFriendlyName(Type typeInfo) + { + var argumentNames = typeInfo.GetGenericArguments().Select(GetFriendlyName).ToArray(); + var baseName = typeInfo.Name.Split('`').First(); + return $"{baseName}<{string.Join(", ", argumentNames)}>"; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Importers/IImporter.cs b/dotnet/Applications/Mocha.Web.Server/Core/Importers/IImporter.cs new file mode 100755 index 0000000..3b40b68 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Importers/IImporter.cs @@ -0,0 +1,72 @@ +namespace dotless.Core.Importers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Parser; + using Parser.Tree; + + public interface IImporter + { + /// + /// Get a list of the current paths, used to pass back in to alter url's after evaluation + /// + /// + List GetCurrentPathsClone(); + + /// + /// Imports an import and return true if successful + /// + ImportAction Import(Import import); + + /// + /// A method set by the parser implementation in order to get a new parser for use in importing + /// + Func Parser { get; set; } + + /// + /// Called for every Url and allows the importer to adjust relative url's to be relative to the + /// primary url + /// + string AlterUrl(string url, List pathList); + + string CurrentDirectory { get; set; } + + IDisposable BeginScope(Import parent); + + /// + /// Resets the imports. + /// + void ResetImports(); + + /// + /// Gets the already imported files + /// + /// + IEnumerable GetImports(); + } + + /// + /// The action to do with the @import statement + /// + public enum ImportAction + { + /// + /// Import as less (process the file and include) + /// + ImportLess, + /// + /// Import verbatim as CSS + /// + ImportCss, + /// + /// Leave a @import statement + /// + LeaveImport, + /// + /// Do nothing (e.g. when it is an import-once and has already been imported) + /// + ImportNothing, + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Importers/Importer.cs b/dotnet/Applications/Mocha.Web.Server/Core/Importers/Importer.cs new file mode 100755 index 0000000..2f5dc3a --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Importers/Importer.cs @@ -0,0 +1,488 @@ +namespace dotless.Core.Importers +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using Input; + using Parser; + using Parser.Tree; + using Utils; + using System.Text.RegularExpressions; + using System.Reflection; + + public class Importer : IImporter + { + private static readonly Regex _embeddedResourceRegex = new Regex(@"^dll://(?.+?)#(?.+)$"); + + public static Regex EmbeddedResourceRegex { get { return _embeddedResourceRegex; } } + public IFileReader FileReader { get; set; } + + /// + /// List of successful imports + /// + public List Imports { get; set; } + + public Func Parser { get; set; } + private readonly List _paths = new List(); + + /// + /// The raw imports of every @import node, for use with @import + /// + protected readonly List _rawImports = new List(); + + /// + /// Duplicates of reference imports should be ignored just like normal imports + /// but a reference import must not interfere with a regular import, hence a different list + /// + private readonly List _referenceImports = new List(); + + public virtual string CurrentDirectory { get; set; } + + /// + /// Whether or not the importer should alter urls + /// + public bool IsUrlRewritingDisabled { get; set; } + + public string RootPath { get; set; } + + /// + /// Import all files as if they are less regardless of file extension + /// + public bool ImportAllFilesAsLess { get; set; } + + /// + /// Import the css and include inline + /// + public bool InlineCssFiles { get; set; } + + + public Importer() : this(new FileReader()) + { + } + + public Importer(IFileReader fileReader) : this(fileReader, false, "", false, false) + { + } + + public Importer(IFileReader fileReader, bool disableUrlReWriting, string rootPath, bool inlineCssFiles, bool importAllFilesAsLess) + { + FileReader = fileReader; + IsUrlRewritingDisabled = disableUrlReWriting; + RootPath = rootPath; + InlineCssFiles = inlineCssFiles; + ImportAllFilesAsLess = importAllFilesAsLess; + Imports = new List(); + CurrentDirectory = ""; + } + + /// + /// Whether a url has a protocol on it + /// + private static bool IsProtocolUrl(string url) + { + return Regex.IsMatch(url, @"^([a-zA-Z]{2,}:)"); + } + + /// + /// Whether a url has a protocol on it + /// + private static bool IsNonRelativeUrl(string url) + { + return url.StartsWith(@"/") || url.StartsWith(@"~/") || Regex.IsMatch(url, @"^[a-zA-Z]:"); + } + + /// + /// Whether a url represents an embedded resource + /// + private static bool IsEmbeddedResource(string path) + { + return _embeddedResourceRegex.IsMatch(path); + } + + /// + /// Get a list of the current paths, used to pass back in to alter url's after evaluation + /// + /// + public List GetCurrentPathsClone() + { + return new List(_paths); + } + + /// + /// returns true if the import should be ignored because it is a duplicate and import-once was used + /// + /// + /// + protected bool CheckIgnoreImport(Import import) + { + return CheckIgnoreImport(import, import.Path); + } + + /// + /// returns true if the import should be ignored because it is a duplicate and import-once was used + /// + /// + /// + protected bool CheckIgnoreImport(Import import, string path) + { + if (IsOptionSet(import.ImportOptions, ImportOptions.Multiple)) + { + return false; + } + + // The import option Reference is set at parse time, + // but the IsReference bit is set at evaluation time (inherited from parent) + // so we check both. + if (import.IsReference || IsOptionSet(import.ImportOptions, ImportOptions.Reference)) + { + if (_rawImports.Contains(path, StringComparer.InvariantCultureIgnoreCase)) + { + // Already imported as a regular import, so the reference import is redundant + return true; + } + + return CheckIgnoreImport(_referenceImports, path); + } + + return CheckIgnoreImport(_rawImports, path); + } + + private bool CheckIgnoreImport(List importList, string path) { + if (importList.Contains(path, StringComparer.InvariantCultureIgnoreCase)) + { + return true; + } + importList.Add(path); + return false; + } + + /// + /// Imports the file inside the import as a dot-less file. + /// + /// + /// The action for the import node to process + public virtual ImportAction Import(Import import) + { + // if the import is protocol'd (e.g. http://www.opencss.com/css?blah) then leave the import alone + if (IsProtocolUrl(import.Path) && !IsEmbeddedResource(import.Path)) + { + if (import.Path.EndsWith(".less")) + { + throw new FileNotFoundException(string.Format(".less cannot import non local less files [{0}].", import.Path), import.Path); + } + + if (CheckIgnoreImport(import)) + { + return ImportAction.ImportNothing; + } + + return ImportAction.LeaveImport; + } + + var file = import.Path; + + if (!IsNonRelativeUrl(file)) + { + file = GetAdjustedFilePath(import.Path, _paths); + } + + if (CheckIgnoreImport(import, file)) + { + return ImportAction.ImportNothing; + } + + bool importAsless = ImportAllFilesAsLess || IsOptionSet(import.ImportOptions, ImportOptions.Less); + + if (!importAsless && import.Path.EndsWith(".css") && !import.Path.EndsWith(".less.css")) + { + if (InlineCssFiles || IsOptionSet(import.ImportOptions, ImportOptions.Inline)) + { + if (IsEmbeddedResource(import.Path) && ImportEmbeddedCssContents(file, import)) + return ImportAction.ImportCss; + if (ImportCssFileContents(file, import)) + return ImportAction.ImportCss; + } + + return ImportAction.LeaveImport; + } + + if (Parser == null) + throw new InvalidOperationException("Parser cannot be null."); + + string fullPath = String.Empty; + if (!ImportLessFile(file, import, out fullPath)) + { + if (IsOptionSet(import.ImportOptions, ImportOptions.Optional)) + { + return ImportAction.ImportNothing; + } + + if (IsOptionSet(import.ImportOptions, ImportOptions.Css)) + { + return ImportAction.LeaveImport; + } + + if (import.Path.EndsWith(".less", StringComparison.InvariantCultureIgnoreCase)) + { + throw new FileNotFoundException(string.Format("You are importing a file ending in .less that cannot be found [{0}].", fullPath), file); + } + return ImportAction.LeaveImport; + } + + return ImportAction.ImportLess; + } + + public IDisposable BeginScope(Import parentScope) { + return new ImportScope(this, Path.GetDirectoryName(parentScope.Path)); + } + + /// + /// Uses the paths to adjust the file path + /// + protected string GetAdjustedFilePath(string path, IEnumerable pathList) + { + return pathList.Concat(new[] { path }).AggregatePaths(CurrentDirectory); + } + + /// + /// Imports a less file and puts the root into the import node + /// + protected bool ImportLessFile(string lessPath, Import import, out string fullName) + { + string contents, file = null; + if (IsEmbeddedResource(lessPath)) + { + contents = ResourceLoader.GetResource(lessPath, FileReader, out file); + if (contents == null) + { + fullName = lessPath; + return false; + } + } + else + { + fullName = RootPath + '/' + lessPath; + if (Path.IsPathRooted(lessPath)) + { + fullName = lessPath; + } + else if (!string.IsNullOrEmpty(CurrentDirectory)) { + fullName = CurrentDirectory.Replace(@"\", "/").TrimEnd('/') + '/' + lessPath; + } + + bool fileExists = FileReader.DoesFileExist(fullName); + if (!fileExists && !fullName.EndsWith(".less")) + { + fullName += ".less"; + fileExists = FileReader.DoesFileExist(fullName); + } + + if (!fileExists) return false; + + contents = FileReader.GetFileContents(fullName); + + file = fullName; + } + + _paths.Add(Path.GetDirectoryName(import.Path)); + + try + { + if (!string.IsNullOrEmpty(file)) + { + Imports.Add(file); + } + import.InnerRoot = Parser().Parse(contents, lessPath); + } + catch + { + Imports.Remove(file); + throw; + } + finally + { + _paths.RemoveAt(_paths.Count - 1); + } + + fullName = file; + return true; + } + + /// + /// Imports a css file from an embedded resource and puts the contents into the import node + /// + /// + /// + /// + private bool ImportEmbeddedCssContents(string file, Import import) + { + string content = ResourceLoader.GetResource(file, FileReader, out file); + if (content == null) return false; + import.InnerContent = content; + return true; + } + + /// + /// Imports a css file and puts the contents into the import node + /// + protected bool ImportCssFileContents(string file, Import import) + { + if (!FileReader.DoesFileExist(file)) + { + return false; + } + + import.InnerContent = FileReader.GetFileContents(file); + Imports.Add(file); + + return true; + } + + /// + /// Called for every Url and allows the importer to adjust relative url's to be relative to the + /// primary url + /// + public string AlterUrl(string url, List pathList) + { + if (!IsProtocolUrl (url) && !IsNonRelativeUrl (url)) + { + if (pathList.Any() && !IsUrlRewritingDisabled) + { + url = GetAdjustedFilePath(url, pathList); + } + return RootPath + url; + } + return url; + } + + public void ResetImports() + { + Imports.Clear(); + _rawImports.Clear(); + } + + public IEnumerable GetImports() + { + return Imports.Distinct(); + } + + private class ImportScope : IDisposable { + private readonly Importer importer; + + public ImportScope(Importer importer, string path) { + this.importer = importer; + this.importer._paths.Add(path); + } + + public void Dispose() { + this.importer._paths.RemoveAt(this.importer._paths.Count - 1); + } + } + + private bool IsOptionSet(ImportOptions options, ImportOptions test) + { + return (options & test) == test; + } + } + + /// + /// Utility class used to retrieve the content of an embedded resource using a separate app domain in order to unload the assembly when done. + /// + class ResourceLoader : MarshalByRefObject + { + private byte[] _fileContents; + private string _resourceName; + private string _resourceContent; + + /// + /// Gets the text content of an embedded resource. + /// + /// The path in the form: dll://AssemblyName#ResourceName + /// The content of the resource + public static string GetResource(string file, IFileReader fileReader, out string fileDependency) + { + fileDependency = null; + + var match = Importer.EmbeddedResourceRegex.Match(file); + if (!match.Success) return null; + + var loader = new ResourceLoader + { + _resourceName = match.Groups["Resource"].Value + }; + + try + { + fileDependency = match.Groups["Assembly"].Value; + + LoadFromCurrentAppDomain(loader, fileDependency); + + if (String.IsNullOrEmpty(loader._resourceContent)) + LoadFromNewAppDomain(loader, fileReader, fileDependency); + + } + catch (Exception) + { + throw new FileNotFoundException("Unable to load resource [" + loader._resourceName + "] in assembly [" + fileDependency + "]"); + } + finally + { + loader._fileContents = null; + } + + return loader._resourceContent; + } + + private static void LoadFromCurrentAppDomain(ResourceLoader loader, String assemblyName) + { + foreach (var assembly in AppDomain.CurrentDomain + .GetAssemblies() + .Where(x => !IsDynamicAssembly(x) && x.Location.EndsWith(assemblyName, StringComparison.InvariantCultureIgnoreCase))) + { + if (assembly.GetManifestResourceNames().Contains(loader._resourceName)) + { + using (var stream = assembly.GetManifestResourceStream(loader._resourceName)) + using (var reader = new StreamReader(stream)) + { + loader._resourceContent = reader.ReadToEnd(); + + if (!String.IsNullOrEmpty(loader._resourceContent)) + return; + } + } + } + } + + private static bool IsDynamicAssembly(Assembly assembly) { + try { + string loc = assembly.Location; + return false; + } catch (NotSupportedException) { + // Location is not supported for dynamic assemblies, so it will throw a NotSupportedException + return true; + } + } + + private static void LoadFromNewAppDomain(ResourceLoader loader, IFileReader fileReader, String assemblyName) + { + if (!fileReader.DoesFileExist(assemblyName)) + { + throw new FileNotFoundException("Unable to locate assembly file [" + assemblyName + "]"); + } + + loader._fileContents = fileReader.GetBinaryFileContents(assemblyName); + + var domain = AppDomain.CreateDomain("LoaderDomain"); + var assembly = domain.Load(loader._fileContents); + + using (var stream = assembly.GetManifestResourceStream(loader._resourceName)) + using (var reader = new StreamReader(stream)) + { + loader._resourceContent = reader.ReadToEnd(); + } + + AppDomain.Unload(domain); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Input/FileReader.cs b/dotnet/Applications/Mocha.Web.Server/Core/Input/FileReader.cs new file mode 100755 index 0000000..e26eab5 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Input/FileReader.cs @@ -0,0 +1,41 @@ +namespace dotless.Core.Input +{ + using System.IO; + + public class FileReader : IFileReader + { + public IPathResolver PathResolver { get; set; } + + public FileReader() : this(new RelativePathResolver()) + { + } + + public FileReader(IPathResolver pathResolver) + { + PathResolver = pathResolver; + } + + public byte[] GetBinaryFileContents(string fileName) + { + fileName = PathResolver.GetFullPath(fileName); + + return File.ReadAllBytes(fileName); + } + + public string GetFileContents(string fileName) + { + fileName = PathResolver.GetFullPath(fileName); + + return File.ReadAllText(fileName); + } + + public bool DoesFileExist(string fileName) + { + fileName = PathResolver.GetFullPath(fileName); + + return File.Exists(fileName); + } + + public bool UseCacheDependencies { get { return true; } } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Input/IFileReader.cs b/dotnet/Applications/Mocha.Web.Server/Core/Input/IFileReader.cs new file mode 100755 index 0000000..b4ee7fd --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Input/IFileReader.cs @@ -0,0 +1,13 @@ +namespace dotless.Core.Input +{ + public interface IFileReader + { + byte[] GetBinaryFileContents(string fileName); + + string GetFileContents(string fileName); + + bool DoesFileExist(string fileName); + + bool UseCacheDependencies { get; } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Input/IPathResolver.cs b/dotnet/Applications/Mocha.Web.Server/Core/Input/IPathResolver.cs new file mode 100755 index 0000000..2b849b3 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Input/IPathResolver.cs @@ -0,0 +1,7 @@ +namespace dotless.Core.Input +{ + public interface IPathResolver + { + string GetFullPath(string path); + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Input/RelativePathResolver.cs b/dotnet/Applications/Mocha.Web.Server/Core/Input/RelativePathResolver.cs new file mode 100755 index 0000000..7fa74dd --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Input/RelativePathResolver.cs @@ -0,0 +1,10 @@ +namespace dotless.Core.Input +{ + public class RelativePathResolver : IPathResolver + { + public string GetFullPath(string path) + { + return path; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Less.cs b/dotnet/Applications/Mocha.Web.Server/Core/Less.cs new file mode 100755 index 0000000..0aa2757 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Less.cs @@ -0,0 +1,25 @@ +namespace dotless.Core +{ + using configuration; + using System; + + public static class Less + { + public static string Parse(string less) + { + return Parse(less, DotlessConfiguration.GetDefault()); + } + + public static string Parse(string less, DotlessConfiguration config) + { + if (config.Web) + { + // throw new Exception("Please use dotless.Core.LessWeb.Parse for web applications. This makes sure all web features are available."); + } + + LessEngine engine = new LessEngine(); + engine.CurrentDirectory = config.RootPath; + return engine.TransformToCss(less, null); + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Loggers/ConsoleLogger.cs b/dotnet/Applications/Mocha.Web.Server/Core/Loggers/ConsoleLogger.cs new file mode 100755 index 0000000..3dceb6a --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Loggers/ConsoleLogger.cs @@ -0,0 +1,20 @@ +namespace dotless.Core.Loggers +{ + using dotless.Core.configuration; + using System; + + public class ConsoleLogger : Logger + { + public ConsoleLogger(LogLevel level) : base(level) { } + + public ConsoleLogger(DotlessConfiguration config) : this(config.LogLevel) + { + + } + + protected override void Log(string message) + { + Console.WriteLine(message); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Loggers/DiagnosticsLogger.cs b/dotnet/Applications/Mocha.Web.Server/Core/Loggers/DiagnosticsLogger.cs new file mode 100755 index 0000000..443059f --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Loggers/DiagnosticsLogger.cs @@ -0,0 +1,14 @@ +namespace dotless.Core.Loggers +{ + public class DiagnosticsLogger : Logger + { + public DiagnosticsLogger(LogLevel level) : base(level) + { + } + + protected override void Log(string message) + { + System.Diagnostics.Debug.WriteLine(message); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Loggers/ILogger.cs b/dotnet/Applications/Mocha.Web.Server/Core/Loggers/ILogger.cs new file mode 100755 index 0000000..5211267 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Loggers/ILogger.cs @@ -0,0 +1,23 @@ +namespace dotless.Core.Loggers +{ + public interface ILogger + { + void Log(LogLevel level, string message); + void Info(string message); + void Info(string message, params object[] args); + void Debug(string message); + void Debug(string message, params object[] args); + void Warn(string message); + void Warn(string message, params object[] args); + void Error(string message); + void Error(string message, params object[] args); + } + + public enum LogLevel + { + Info = 1, + Debug, + Warn, + Error + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Loggers/Logger.cs b/dotnet/Applications/Mocha.Web.Server/Core/Loggers/Logger.cs new file mode 100755 index 0000000..1991805 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Loggers/Logger.cs @@ -0,0 +1,30 @@ +namespace dotless.Core.Loggers +{ + public abstract class Logger : ILogger + { + public LogLevel Level { get; set; } + + protected Logger(LogLevel level) + { + Level = level; + } + + public void Log(LogLevel level, string message) + { + if (Level <= level) + Log(message); + } + + protected abstract void Log(string message); + + public void Info(string message) { Log(LogLevel.Info, message); } + public void Debug(string message) { Log(LogLevel.Debug, message); } + public void Warn(string message) { Log(LogLevel.Warn, message); } + public void Error(string message) { Log(LogLevel.Error, message); } + public void Info(string message, params object[] args) { Log(LogLevel.Info, string.Format(message, args)); } + public void Debug(string message, params object[] args) { Log(LogLevel.Debug, string.Format(message, args)); } + public void Warn(string message, params object[] args) { Log(LogLevel.Warn, string.Format(message, args)); } + public void Error(string message, params object[] args) { Log(LogLevel.Error, string.Format(message, args)); } + + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Loggers/NullLogger.cs b/dotnet/Applications/Mocha.Web.Server/Core/Loggers/NullLogger.cs new file mode 100755 index 0000000..08bd731 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Loggers/NullLogger.cs @@ -0,0 +1,23 @@ +namespace dotless.Core.Loggers +{ + public class NullLogger : Logger + { + public NullLogger(LogLevel level) : base(level) + { + } + + protected override void Log(string message) + { + //Swallow + } + + private static readonly NullLogger instance = new NullLogger(LogLevel.Warn); + public static NullLogger Instance + { + get + { + return instance; + } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parameters/ConsoleArgumentParameterSource.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parameters/ConsoleArgumentParameterSource.cs new file mode 100755 index 0000000..793346a --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parameters/ConsoleArgumentParameterSource.cs @@ -0,0 +1,13 @@ +namespace dotless.Core.Parameters +{ + using System.Collections.Generic; + + public class ConsoleArgumentParameterSource : IParameterSource + { + public static IDictionary ConsoleArguments = new Dictionary(); + public IDictionary GetParameters() + { + return ConsoleArguments; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parameters/IParameterSource.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parameters/IParameterSource.cs new file mode 100755 index 0000000..c50d08d --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parameters/IParameterSource.cs @@ -0,0 +1,9 @@ +namespace dotless.Core.Parameters +{ + using System.Collections.Generic; + + public interface IParameterSource + { + IDictionary GetParameters(); + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parameters/NullParameterSource.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parameters/NullParameterSource.cs new file mode 100755 index 0000000..693b3af --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parameters/NullParameterSource.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace dotless.Core.Parameters { + public class NullParameterSource : IParameterSource + { + public IDictionary GetParameters() + { + return new Dictionary(); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/AbsFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/AbsFunction.cs new file mode 100755 index 0000000..d71fb18 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/AbsFunction.cs @@ -0,0 +1,17 @@ +namespace dotless.Core.Parser.Functions +{ + using System; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + + public class AbsFunction : NumberFunctionBase + { + protected override Node Eval(Env env, Number number, Node[] args) + { + WarnNotSupportedByLessJS("abs(number)"); + + return new Number(Math.Abs(number.Value), number.Unit); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/AddFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/AddFunction.cs new file mode 100755 index 0000000..7bfbb89 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/AddFunction.cs @@ -0,0 +1,20 @@ +namespace dotless.Core.Parser.Functions +{ + using System.Linq; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class AddFunction : Function + { + protected override Node Evaluate(Env env) + { + Guard.ExpectAllNodes(Arguments, this, Location); + + var value = Arguments.Cast().Select(d => d.Value).Aggregate(0d, (a, b) => a + b); + + return new Number(value); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/AlphaFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/AlphaFunction.cs new file mode 100755 index 0000000..96670f2 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/AlphaFunction.cs @@ -0,0 +1,51 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure.Nodes; + using Tree; + + public class FadeInFunction : ColorFunctionBase + { + protected override Node Eval(Color color) + { + return new Number(color.Alpha); + } + + protected override Node EditColor(Color color, Number number) + { + var alpha = number.Value/100d; + + return new Color(color.R, color.G, color.B, ProcessAlpha( color.Alpha, alpha)); + } + + protected virtual double ProcessAlpha(double originalAlpha, double newAlpha) + { + return originalAlpha + newAlpha; + } + } + + public class AlphaFunction : FadeInFunction + { + protected override Node EditColor(Color color, Number number) + { + WarnNotSupportedByLessJS("alpha(color, number)", "fadein(color, number) or the opposite fadeout(color, number),"); + + return base.EditColor(color, number); + } + } + + public class FadeOutFunction : AlphaFunction + { + protected override Node EditColor(Color color, Number number) + { + return base.EditColor(color, -number); + } + } + + public class FadeFunction : AlphaFunction + { + protected override double ProcessAlpha(double originalAlpha, double newAlpha) + { + return newAlpha; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ArgbFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ArgbFunction.cs new file mode 100755 index 0000000..88487af --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ArgbFunction.cs @@ -0,0 +1,18 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class ArgbFunction : Function + { + protected override Node Evaluate(Env env) + { + Guard.ExpectNumArguments(1, Arguments.Count, this, Location); + var color = Guard.ExpectNode(Arguments[0], this, Location); + + return new TextNode(color.ToArgb()); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/AverageFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/AverageFunction.cs new file mode 100755 index 0000000..019147c --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/AverageFunction.cs @@ -0,0 +1,10 @@ +namespace dotless.Core.Parser.Functions +{ + public class AverageFunction : ColorMixFunction + { + protected override double Operate(double a, double b) + { + return (a + b) / 2; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/BlueFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/BlueFunction.cs new file mode 100755 index 0000000..6b8fe65 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/BlueFunction.cs @@ -0,0 +1,25 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure.Nodes; + using Tree; + + public class BlueFunction : ColorFunctionBase + { + protected override Node Eval(Color color) + { + return new Number(color.B); + } + + protected override Node EditColor(Color color, Number number) + { + WarnNotSupportedByLessJS("blue(color, number)"); + + var value = number.Value; + + if (number.Unit == "%") + value = (value*255)/100d; + + return new Color(color.R, color.G, color.B + value); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/CeilFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/CeilFunction.cs new file mode 100755 index 0000000..6530d20 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/CeilFunction.cs @@ -0,0 +1,15 @@ +namespace dotless.Core.Parser.Functions +{ + using System; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + + public class CeilFunction : NumberFunctionBase + { + protected override Node Eval(Env env, Number number, Node[] args) + { + return new Number(Math.Ceiling(number.Value), number.Unit); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ColorFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ColorFunction.cs new file mode 100755 index 0000000..0d23ee7 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ColorFunction.cs @@ -0,0 +1,28 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + using System; + using Exceptions; + + public class ColorFunction : Function + { + protected override Node Evaluate(Env env) + { + Guard.ExpectNumArguments(1, Arguments.Count, this, Location); + var node = Guard.ExpectNode(Arguments[0], this, Location); + + try + { + return Color.From(node.Value); + } + catch (FormatException ex) + { + var message = string.Format("Invalid RGB color string '{0}'", node.Value); + throw new ParsingException(message, ex, Location, null); + } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ColorFunctionBase.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ColorFunctionBase.cs new file mode 100755 index 0000000..3aa368d --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ColorFunctionBase.cs @@ -0,0 +1,39 @@ +namespace dotless.Core.Parser.Functions +{ + using System.Linq; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public abstract class ColorFunctionBase : Function + { + protected override Node Evaluate(Env env) + { + Guard.ExpectMinArguments(1, Arguments.Count(), this, Location); + Guard.ExpectNode(Arguments[0], this, Arguments[0].Location); + + var color = Arguments[0] as Color; + + if (Arguments.Count == 2) + { + Guard.ExpectNode(Arguments[1], this, Arguments[1].Location); + + var number = Arguments[1] as Number; + var edit = EditColor(color, number); + + if (edit != null) + return edit; + } + + return Eval(color); + } + + protected abstract Node Eval(Color color); + + protected virtual Node EditColor(Color color, Number number) + { + return null; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ColorMixFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ColorMixFunction.cs new file mode 100755 index 0000000..d42fa58 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ColorMixFunction.cs @@ -0,0 +1,48 @@ +using System; + +namespace dotless.Core.Parser.Functions +{ + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public abstract class ColorMixFunction : Function + { + protected override Node Evaluate(Env env) + { + Guard.ExpectNumArguments(2, Arguments.Count, this, Location); + Guard.ExpectAllNodes(Arguments, this, Location); + + var color1 = (Color) Arguments[0]; + var color2 = (Color) Arguments[1]; + + var resultAlpha = color2.Alpha + color1.Alpha*(1 - color2.Alpha); + + return new Color( + Compose(color1, color2, resultAlpha, c => c.R), + Compose(color1, color2, resultAlpha, c => c.G), + Compose(color1, color2, resultAlpha, c => c.B), + resultAlpha); + } + + /// + /// Color composition rules from http://www.w3.org/TR/compositing-1/ + /// (translated from the less.js version + /// + private double Compose(Color backdrop, Color source, double ar, Func channel) { + var cb = channel(backdrop); + var cs = channel(source); + var ab = backdrop.Alpha; + var @as = source.Alpha; + double result = Operate(cb, cs); + if (ar > 0) + { + result = (@as * cs + ab * (cb - @as * (cb + cs - result))) / ar; + } + return result; + } + + protected abstract double Operate(double a, double b); + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ComplementFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ComplementFunction.cs new file mode 100755 index 0000000..439b287 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ComplementFunction.cs @@ -0,0 +1,22 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class ComplementFunction : HslColorFunctionBase + { + protected override Node EvalHsl(HslColor color) + { + WarnNotSupportedByLessJS("complement(color)"); + + color.Hue += 0.5; + return color.ToRgbColor(); + } + + protected override Node EditHsl(HslColor color, Number number) + { + return null; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ContrastFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ContrastFunction.cs new file mode 100755 index 0000000..556208c --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ContrastFunction.cs @@ -0,0 +1,40 @@ + +namespace dotless.Core.Parser.Functions +{ + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class ContrastFunction : Function + { + protected override Node Evaluate(Env env) + { + Guard.ExpectMinArguments(1, Arguments.Count, this, Location); + Guard.ExpectMaxArguments(4, Arguments.Count, this, Location); + Guard.ExpectNode(Arguments[0], this, Location); + + var color = (Color) Arguments[0]; + + if (Arguments.Count > 1) + Guard.ExpectNode(Arguments[1], this, Location); + if (Arguments.Count > 2) + Guard.ExpectNode(Arguments[2], this, Location); + if (Arguments.Count > 3) + Guard.ExpectNode(Arguments[3], this, Location); + + var lightColor = Arguments.Count > 1 ? (Color)Arguments[1] : new Color(255d, 255d, 255d); + var darkColor = Arguments.Count > 2 ? (Color)Arguments[2] : new Color(0d, 0d, 0d); + var threshold = Arguments.Count > 3 ? ((Number) Arguments[3]).ToNumber() : 0.43d; + + if (darkColor.Luma > lightColor.Luma) + { + var tempColor = lightColor; + lightColor = darkColor; + darkColor = tempColor; + } + + return (color.Luma < threshold) ? lightColor : darkColor; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/DataUriFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/DataUriFunction.cs new file mode 100755 index 0000000..7a3bd3d --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/DataUriFunction.cs @@ -0,0 +1,72 @@ +namespace dotless.Core.Parser.Functions +{ + using System; + using System.IO; + using Exceptions; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class DataUriFunction : Function + { + protected override Node Evaluate(Env env) + { + var filename = GetDataUriFilename(); + string base64 = ConvertFileToBase64(filename); + string mimeType = GetMimeType(filename); + + return new TextNode(string.Format("url(\"data:{0};base64,{1}\")", mimeType, base64)); + } + + private string GetDataUriFilename() + { + Guard.ExpectMinArguments(1, Arguments.Count, this, Location); + + // Thanks a lot LESS for putting the optional parameter first! + var filenameNode = Arguments[0]; + if (Arguments.Count > 1) + filenameNode = Arguments[1]; + + Guard.ExpectNode(filenameNode, this, Location); + var filename = ((Quoted)filenameNode).Value; + + Guard.Expect(!(filename.StartsWith("http://") || filename.StartsWith("https://")), + string.Format("Invalid filename passed to data-uri '{0}'. Filename must be a local file", filename), Location); + + return filename; + } + + private string ConvertFileToBase64(string filename) + { + string base64; + try + { + base64 = Convert.ToBase64String(File.ReadAllBytes(filename)); + } + catch (IOException e) + { + // this is more general than just a check to see whether the file exists + // it could fail for other reasons like security permissions + throw new ParsingException(String.Format("Data-uri function could not read file '{0}'", filename), e, Location); + } + return base64; + } + + private string GetMimeType(string filename) + { + if (Arguments.Count > 1) + { + Guard.ExpectNode(Arguments[0], this, Location); + var mimeType = ((Quoted) Arguments[0]).Value; + + if (mimeType.IndexOf(';') > -1) + mimeType = mimeType.Split(';')[0]; + + return mimeType; + } + + return new MimeTypeLookup().ByFilename(filename); + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/DefaultFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/DefaultFunction.cs new file mode 100755 index 0000000..1eb9277 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/DefaultFunction.cs @@ -0,0 +1,13 @@ +using dotless.Core.Parser.Infrastructure; +using dotless.Core.Parser.Infrastructure.Nodes; + +namespace dotless.Core.Parser.Functions +{ + public class DefaultFunction : Function + { + protected override Node Evaluate(Env env) + { + return new TextNode("default()"); + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/DifferenceFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/DifferenceFunction.cs new file mode 100755 index 0000000..6aac819 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/DifferenceFunction.cs @@ -0,0 +1,11 @@ +namespace dotless.Core.Parser.Functions +{ + using System; + public class DifferenceFunction : ColorMixFunction + { + protected override double Operate(double a, double b) + { + return Math.Abs(a - b); + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ExclusionFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ExclusionFunction.cs new file mode 100755 index 0000000..93a60dc --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ExclusionFunction.cs @@ -0,0 +1,10 @@ +namespace dotless.Core.Parser.Functions +{ + public class ExclusionFunction : ColorMixFunction + { + protected override double Operate(double a, double b) + { + return a + b * (255 - a - a) / 255; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ExtractFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ExtractFunction.cs new file mode 100755 index 0000000..4c062d3 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ExtractFunction.cs @@ -0,0 +1,21 @@ +using dotless.Core.Parser.Infrastructure; +using dotless.Core.Parser.Infrastructure.Nodes; +using dotless.Core.Parser.Tree; +using dotless.Core.Utils; + +namespace dotless.Core.Parser.Functions +{ + public class ExtractFunction : ListFunctionBase + { + protected override Node Eval(Env env, Node[] list, Node[] args) + { + Guard.ExpectNumArguments(1, args.Length, this, Location); + Guard.ExpectNode(args[0], this, args[0].Location); + + var index = (int)(args[0] as Number).Value; + + // Extract function indecies are 1-based + return list[index-1]; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/FloorFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/FloorFunction.cs new file mode 100755 index 0000000..6316a36 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/FloorFunction.cs @@ -0,0 +1,15 @@ +namespace dotless.Core.Parser.Functions +{ + using System; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + + public class FloorFunction : NumberFunctionBase + { + protected override Node Eval(Env env, Number number, Node[] args) + { + return new Number(Math.Floor(number.Value), number.Unit); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/FormatStringFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/FormatStringFunction.cs new file mode 100755 index 0000000..d2b20b3 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/FormatStringFunction.cs @@ -0,0 +1,39 @@ +namespace dotless.Core.Parser.Functions +{ + using System; + using System.Linq; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using dotless.Core.Exceptions; + + public class FormatStringFunction : Function + { + protected override Node Evaluate(Env env) + { + WarnNotSupportedByLessJS("formatstring(string, args...)", null, @" You may want to consider using string interpolation (""@{variable}"") which does the same thing and is supported."); + + if (Arguments.Count == 0) + return new Quoted("", false); + + Func unescape = n => n is Quoted ? ((Quoted) n).UnescapeContents() : n.ToCSS(env); + + var format = unescape(Arguments[0]); + + var args = Arguments.Skip(1).Select(unescape).ToArray(); + + string result; + + try + { + result = string.Format(format, args); + } + catch (FormatException e) + { + throw new ParserException(string.Format("Error in formatString :{0}", e.Message), e); + } + + return new Quoted(result, false); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/Function.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/Function.cs new file mode 100755 index 0000000..9837b54 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/Function.cs @@ -0,0 +1,70 @@ +namespace dotless.Core.Parser.Functions +{ + using System.Collections.Generic; + using System.Linq; + using Infrastructure; + using Infrastructure.Nodes; + using dotless.Core.Loggers; + + public abstract class Function + { + public string Name { get; set; } + protected List Arguments { get; set; } + public ILogger Logger { get; set; } + public NodeLocation Location { get; set; } + + public Node Call(Env env, IEnumerable arguments) + { + Arguments = arguments.ToList(); + + var node = Evaluate(env); + node.Location = Location; + return node; + } + + protected abstract Node Evaluate(Env env); + + public override string ToString() + { + return string.Format("function '{0}'", Name.ToLowerInvariant()); + } + + /// + /// Warns that a function is not supported by less.js + /// + /// unsupported pattern function call e.g. alpha(color number) + protected void WarnNotSupportedByLessJS(string functionPattern) + { + WarnNotSupportedByLessJS(functionPattern, null, null); + } + + /// + /// Warns that a function is not supported by less.js + /// + /// unsupported pattern function call e.g. alpha(color number) + /// Replacement pattern function call e.g. fadein(color, number) + protected void WarnNotSupportedByLessJS(string functionPattern, string replacementPattern) + { + WarnNotSupportedByLessJS(functionPattern, replacementPattern, null); + } + + /// + /// Warns that a function is not supported by less.js + /// + /// unsupported pattern function call e.g. alpha(color number) + /// Replacement pattern function call e.g. fadein(color, number) + /// Extra information to put on the end. + protected void WarnNotSupportedByLessJS(string functionPattern, string replacementPattern, string extraInfo) + { + if (string.IsNullOrEmpty(replacementPattern)) + { + Logger.Info("{0} is not supported by less.js, so this will work but not compile with other less implementations.{1}", functionPattern, extraInfo); + } + else + { + Logger.Warn("{0} is not supported by less.js, so this will work but not compile with other less implementations." + + " You may want to consider using {1} which does the same thing and is supported.{2}", functionPattern, replacementPattern, extraInfo); + } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/GradientImageFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/GradientImageFunction.cs new file mode 100755 index 0000000..c74704f --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/GradientImageFunction.cs @@ -0,0 +1,220 @@ +using System.Threading; + +namespace dotless.Core.Parser.Functions +{ + using System; + using System.Collections.Generic; + using System.Drawing; + using System.Drawing.Drawing2D; + using System.Drawing.Imaging; + using System.IO; + using System.Linq; + using Exceptions; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + using Color = Tree.Color; + + /// + /// Outputs node with gradient image implemented as described in RFC-2397 (The "data" URL scheme, http://tools.ietf.org/html/rfc2397) + /// + /// + /// Acepts color point definitions and outputs image data URL. Usage: + /// gradient(#color1, #color2[, position2][, #color3[, position3]]...) + /// Position is a zero-based offset of the color stop. If omitted, it is increased by 50 (px) or by the previous offset, if any. + /// + /// Currently only vertical 1px width gradients with many color stops are supported. + /// Image data URLs are not supported by IE7 and below. IE8 restricts the size of URL to 32k. + /// See details here: http://en.wikipedia.org/wiki/Data_URI_scheme. + /// + /// The following example shows how to render a gradient (.less file): + /// + /// .ui-widget-header { background: @headerBgColor gradientImage(@headerBgColor, desaturate(lighten(@headerBgColor, 30), 30)) 50% 50% repeat-x; } + /// + /// + /// + public class GradientImageFunction : Function + { + /* + * TODO: + * 1. Add URI length check for IE8: max 0x8000 - needs access to config and Context.Request.Browser + * 2. Add fallback for IE7 and lower - dump the image to disk and refer it as ordinal url(...) - needs access to config and Context.Request.Browser + disk write permissions. + * 3. Implement horisontal gradients (1px height) + * + * Open questions: + * 1. PNG 32bpp - is it ok for all cases? + */ + + #region Nested classes + private class ColorPoint + { + public ColorPoint(System.Drawing.Color color, int position) + { + Color = color; + Position = position; + } + + public static string Stringify(IEnumerable points) + { + return points.Aggregate( + "", (s, point) => string.Format("{0}{1}#{2:X2}{3:X2}{4:X2}{5:X2},{6}", s, s == "" ? "" : ",", + point.Color.A, point.Color.R, point.Color.G, point.Color.B, point.Position)); + } + + public System.Drawing.Color Color { get; private set; } + public int Position { get; private set; } + } + + private class CacheItem + { + public readonly string _def; + public readonly string _url; + + public CacheItem(string def, string url) + { + _def = def; + _url = url; + } + } + #endregion Nested classes + + #region Fields + public const int DEFAULT_COLOR_OFFSET = 50; + + private const int CACHE_LIMIT = 50; + private static readonly ReaderWriterLockSlim _cacheLock = new ReaderWriterLockSlim(); + private static readonly List _cache = new List(); + #endregion Fields + + protected override Node Evaluate(Env env) + { + ColorPoint[] points = GetColorPoints(); + + WarnNotSupportedByLessJS("gradientImage(color, color[, position])"); + + string colorDefs = ColorPoint.Stringify(points); + string imageUrl = GetFromCache(colorDefs); + if (imageUrl == null) + { + imageUrl = "data:image/png;base64," + Convert.ToBase64String(GetImageData(points)); + AddToCache(colorDefs, imageUrl); + } + + return new Url(new TextNode(imageUrl)); + } + + private byte[] GetImageData(ColorPoint[] points) + { + ColorPoint last = points.Last(); + int size = last.Position + 1; + + using (var ms = new MemoryStream()) + { + using (var bmp = new Bitmap(1, size, PixelFormat.Format32bppArgb)) + { + using (Graphics g = Graphics.FromImage(bmp)) + { + for (int i = 1; i < points.Length; i++) + { + var rect = new Rectangle(0, points[i - 1].Position, 1, points[i].Position); + var brush = new LinearGradientBrush( + rect, + points[i - 1].Color, + points[i].Color, + LinearGradientMode.Vertical); + g.FillRectangle(brush, rect); + } + + bmp.SetPixel(0, last.Position, last.Color); + bmp.Save(ms, ImageFormat.Png); + } + } + return ms.ToArray(); + } + } + + private ColorPoint[] GetColorPoints() + { + int argCount = Arguments.Count; + Guard.ExpectMinArguments(2, argCount, this, Location); + Guard.ExpectAllNodes(Arguments.Take(2), this, Location); + var first = (Color) Arguments[0]; + var points = new List + { + new ColorPoint((System.Drawing.Color) first, 0) + }; + + int prevPos = 0; + int prevOffset = DEFAULT_COLOR_OFFSET; + + for (int i = 1; i < argCount; i++) + { + Node arg = Arguments[i]; + Guard.ExpectNode(arg, this, Location); + var color = arg as Color; + int pos = prevPos + prevOffset; + if (i < argCount - 1) + { + var numberArg = Arguments[i + 1] as Number; + if (numberArg) + { + pos = Convert.ToInt32(numberArg.Value); + if (pos <= prevPos) + { + throw new ParsingException( + string.Format("Incrementing color point position expected, at least {0}, found {1}", prevPos + 1, numberArg.Value), Location); + } + prevOffset = pos - prevPos; + i++; + } + } + points.Add(new ColorPoint((System.Drawing.Color) color, pos)); + prevPos = pos; + } + + return points.ToArray(); + } + + private static string GetFromCache(string colorDefs) + { + _cacheLock.EnterReadLock(); + try + { + var cached = _cache.FirstOrDefault(item => item._def == colorDefs); + return cached != null ? cached._url : null; + } + finally + { + _cacheLock.ExitReadLock(); + } + } + + private static void AddToCache(string colorDefs, string imageUrl) + { + _cacheLock.EnterUpgradeableReadLock(); + try + { + if (_cache.All(item => item._def != colorDefs)) + { + _cacheLock.EnterWriteLock(); + try + { + if (_cache.Count >= CACHE_LIMIT) + _cache.RemoveRange(0, CACHE_LIMIT / 2); + var item = new CacheItem(colorDefs, imageUrl); + _cache.Add(item); + } + finally + { + _cacheLock.ExitWriteLock(); + } + } + } + finally + { + _cacheLock.ExitUpgradeableReadLock(); + } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/GrayscaleFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/GrayscaleFunction.cs new file mode 100755 index 0000000..709a28e --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/GrayscaleFunction.cs @@ -0,0 +1,25 @@ +namespace dotless.Core.Parser.Functions +{ + using System.Linq; + using Infrastructure.Nodes; + using Tree; + + public class GreyscaleFunction : ColorFunctionBase + { + protected override Node Eval(Color color) + { + var grey = (color.RGB.Max() + color.RGB.Min())/2; + + return new Color(grey, grey, grey); + } + } + + public class GrayscaleFunction : GreyscaleFunction + { + protected override Node Eval(Color color) + { + WarnNotSupportedByLessJS("grayscale(color)", "greyscale(color)"); + return base.Eval(color); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/GreenFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/GreenFunction.cs new file mode 100755 index 0000000..4a9f101 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/GreenFunction.cs @@ -0,0 +1,25 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure.Nodes; + using Tree; + + public class GreenFunction : ColorFunctionBase + { + protected override Node Eval(Color color) + { + return new Number(color.G); + } + + protected override Node EditColor(Color color, Number number) + { + WarnNotSupportedByLessJS("green(color, number)"); + + var value = number.Value; + + if (number.Unit == "%") + value = (value*255)/100d; + + return new Color(color.R, color.G + value, color.B); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HardlightFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HardlightFunction.cs new file mode 100755 index 0000000..e3f7803 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HardlightFunction.cs @@ -0,0 +1,10 @@ +namespace dotless.Core.Parser.Functions +{ + public class HardlightFunction : ColorMixFunction + { + protected override double Operate(double a, double b) + { + return b < 128 ? 2 * b * a / 255 : 255 - 2 * (255 - b) * (255 - a) / 255; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HexFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HexFunction.cs new file mode 100755 index 0000000..1035d9b --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HexFunction.cs @@ -0,0 +1,29 @@ +namespace dotless.Core.Parser.Functions +{ + using Exceptions; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + + public class HexFunction : NumberFunctionBase + { + protected override Node Eval(Env env, Number number, Node[] args) + { + WarnNotSupportedByLessJS("hex(number)"); + + if (!string.IsNullOrEmpty(number.Unit)) + throw new ParsingException(string.Format("Expected unitless number in function 'hex', found {0}", number.ToCSS(env)), number.Location); + + number.Value = Clamp(number.Value, 255, 0); + + return new TextNode(((int)number.Value).ToString("X2")); + } + + private static double Clamp(double value, double max, double min) + { + if (value < min) value = min; + if (value > max) value = max; + return value; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HslColorFunctionBase.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HslColorFunctionBase.cs new file mode 100755 index 0000000..f6fb1d5 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HslColorFunctionBase.cs @@ -0,0 +1,27 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure.Nodes; + using Tree; + using Utils; + + public abstract class HslColorFunctionBase : ColorFunctionBase + { + protected override Node Eval(Color color) + { + var hsl = HslColor.FromRgbColor(color); + + return EvalHsl(hsl); + } + + protected override Node EditColor(Color color, Number number) + { + var hsl = HslColor.FromRgbColor(color); + + return EditHsl(hsl, number); + } + + protected abstract Node EvalHsl(HslColor color); + + protected abstract Node EditHsl(HslColor color, Number number); + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HslFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HslFunction.cs new file mode 100755 index 0000000..4faf006 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HslFunction.cs @@ -0,0 +1,19 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class HslFunction : HslaFunction + { + protected override Node Evaluate(Env env) + { + Guard.ExpectNumArguments(3, Arguments.Count, this, Location); + + Arguments.Add(new Number(1d, "")); + + return base.Evaluate(env); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HslaFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HslaFunction.cs new file mode 100755 index 0000000..86bf17a --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HslaFunction.cs @@ -0,0 +1,21 @@ +namespace dotless.Core.Parser.Functions +{ + using System.Linq; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class HslaFunction : Function + { + protected override Node Evaluate(Env env) + { + Guard.ExpectNumArguments(4, Arguments.Count, this, Location); + Guard.ExpectAllNodes(Arguments, this, Location); + + var args = Arguments.Cast().ToArray(); + + return new HslColor(args[0], args[1], args[2], args[3]).ToRgbColor(); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HueFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HueFunction.cs new file mode 100755 index 0000000..ea3a77c --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/HueFunction.cs @@ -0,0 +1,24 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class HueFunction : HslColorFunctionBase + { + protected override Node EvalHsl(HslColor color) + { + return color.GetHueInDegrees(); + } + + protected override Node EditHsl(HslColor color, Number number) + { + WarnNotSupportedByLessJS("hue(color, number)"); + + color.Hue += number.Value/360d; + return color.ToRgbColor(); + } + } + + public class SpinFunction : HueFunction { } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/IncrementFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/IncrementFunction.cs new file mode 100755 index 0000000..f3316ae --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/IncrementFunction.cs @@ -0,0 +1,14 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + + public class IncrementFunction : NumberFunctionBase + { + protected override Node Eval(Env env, Number number, Node[] args) + { + return new Number(number.Value + 1, number.Unit); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/IsFunctions.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/IsFunctions.cs new file mode 100755 index 0000000..ea9bf6c --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/IsFunctions.cs @@ -0,0 +1,101 @@ +namespace dotless.Core.Parser.Functions +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using dotless.Core.Utils; + using dotless.Core.Parser.Infrastructure.Nodes; + using dotless.Core.Parser.Tree; + + public abstract class IsFunction : Function + { + protected override Infrastructure.Nodes.Node Evaluate(Infrastructure.Env env) + { + Guard.ExpectNumArguments(1, Arguments.Count, this, Location); + + return new Keyword(IsEvaluator(Arguments[0]) ? "true" : "false"); + } + + protected abstract bool IsEvaluator(Node node); + } + + public abstract class IsTypeFunction : IsFunction where T:Node + { + protected override bool IsEvaluator(Node node) + { + return node is T; + } + } + + public class IsColorFunction : IsTypeFunction + { + } + + public class IsNumber : IsTypeFunction + { + } + + public class IsString : IsTypeFunction + { + } + + public class IsKeyword : IsTypeFunction + { + } + + public class IsUrl : IsTypeFunction + { + } + + public abstract class IsDimensionUnitFunction : IsTypeFunction + { + protected abstract string Unit { get; } + + protected override bool IsEvaluator(Node node) + { + bool isNumber = base.IsEvaluator(node); + if (isNumber) + { + if (((Number)node).Unit == Unit) + { + return true; + } + } + return false; + } + } + + public class IsEm : IsDimensionUnitFunction + { + protected override string Unit + { + get + { + return "em"; + } + } + } + + public class IsPercentage : IsDimensionUnitFunction + { + protected override string Unit + { + get + { + return "%"; + } + } + } + + public class IsPixel : IsDimensionUnitFunction + { + protected override string Unit + { + get + { + return "px"; + } + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/LengthFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/LengthFunction.cs new file mode 100755 index 0000000..b706084 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/LengthFunction.cs @@ -0,0 +1,14 @@ +using dotless.Core.Parser.Infrastructure; +using dotless.Core.Parser.Infrastructure.Nodes; +using dotless.Core.Parser.Tree; + +namespace dotless.Core.Parser.Functions +{ + public class LengthFunction : ListFunctionBase + { + protected override Node Eval(Env env, Node[] list, Node[] args) + { + return new Number(list.Length); + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/LightnessFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/LightnessFunction.cs new file mode 100755 index 0000000..c248ca7 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/LightnessFunction.cs @@ -0,0 +1,38 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class LightenFunction : HslColorFunctionBase + { + protected override Node EvalHsl(HslColor color) + { + return color.GetLightness(); + } + + protected override Node EditHsl(HslColor color, Number number) + { + color.Lightness += number.Value/100; + return color.ToRgbColor(); + } + } + + public class DarkenFunction : LightenFunction + { + protected override Node EditHsl(HslColor color, Number number) + { + return base.EditHsl(color, -number); + } + } + + public class LightnessFunction : LightenFunction + { + protected override Node EditHsl(HslColor color, Number number) + { + WarnNotSupportedByLessJS("lightness(color, number)", "lighten(color, number) or its opposite darken(color, number),"); + + return base.EditHsl(color, number); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ListFunctionBase.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ListFunctionBase.cs new file mode 100755 index 0000000..47e085f --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ListFunctionBase.cs @@ -0,0 +1,38 @@ +namespace dotless.Core.Parser.Functions +{ + using System; + using System.Linq; + using dotless.Core.Exceptions; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public abstract class ListFunctionBase : Function + { + protected override Node Evaluate(Env env) + { + Guard.ExpectMinArguments(1, Arguments.Count, this, Location); + Guard.ExpectNodeToBeOneOf(Arguments[0], this, Arguments[0].Location); + + if(Arguments[0] is Expression) + { + var list = Arguments[0] as Expression; + var args = Arguments.Skip(1).ToArray(); + return Eval(env, list.Value.ToArray(), args); + } + + if(Arguments[0] is Value) + { + var list = Arguments[0] as Value; + var args = Arguments.Skip(1).ToArray(); + return Eval(env, list.Values.ToArray(), args); + } + + // We should never get here due to the type guard... + throw new ParsingException(string.Format("First argument to the list function was a {0}", Arguments[0].GetType().Name.ToLowerInvariant()), Location); + } + + protected abstract Node Eval(Env env, Node[] list, Node[] args); + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/MixFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/MixFunction.cs new file mode 100755 index 0000000..63ef750 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/MixFunction.cs @@ -0,0 +1,77 @@ +namespace dotless.Core.Parser.Functions +{ + using System.Linq; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class MixFunction : Function + { + protected override Node Evaluate(Env env) + { + Guard.ExpectMinArguments(2, Arguments.Count, this, Location); + Guard.ExpectMaxArguments(3, Arguments.Count, this, Location); + Guard.ExpectAllNodes(Arguments.Take(2), this, Location); + + double weight = 50; + if (Arguments.Count == 3) + { + Guard.ExpectNode(Arguments[2], this, Location); + + weight = ((Number)Arguments[2]).Value; + } + + var colors = Arguments.Take(2).Cast().ToArray(); + + return Mix(colors[0], colors[1], weight); + } + + protected Color Mix(Color color1, Color color2, double weight) + { + // Note: this algorithm taken from http://github.com/nex3/haml/blob/0e249c844f66bd0872ed68d99de22b774794e967/lib/sass/script/functions.rb + + var p = weight / 100.0; + var w = p * 2 - 1; + var a = color1.Alpha - color2.Alpha; + + var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0; + var w2 = 1 - w1; + + var rgb = color1.RGB.Select((x, i) => x * w1 + color2.RGB[i] * w2).ToArray(); + + var alpha = color1.Alpha * p + color2.Alpha * (1 - p); + + var color = new Color(rgb[0], rgb[1], rgb[2], alpha); + return color; + } + } + + public class TintFunction : MixFunction + { + protected override Node Evaluate(Env env) + { + Guard.ExpectNumArguments(2, Arguments.Count, this, Location); + Guard.ExpectNode(Arguments[0], this, Location); + Guard.ExpectNode(Arguments[1], this, Location); + + double weight = ((Number)Arguments[1]).Value; + + return Mix(new Color(255, 255, 255),(Color)Arguments[0], weight); + } + } + + public class ShadeFunction : MixFunction + { + protected override Node Evaluate(Env env) + { + Guard.ExpectNumArguments(2, Arguments.Count, this, Location); + Guard.ExpectNode(Arguments[0], this, Location); + Guard.ExpectNode(Arguments[1], this, Location); + + double weight = ((Number)Arguments[1]).Value; + + return Mix(new Color(0, 0, 0), (Color)Arguments[0], weight); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/MultiplyFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/MultiplyFunction.cs new file mode 100755 index 0000000..2f32ae4 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/MultiplyFunction.cs @@ -0,0 +1,10 @@ +namespace dotless.Core.Parser.Functions +{ + public class MultiplyFunction : ColorMixFunction + { + protected override double Operate(double a, double b) + { + return a * b / 255; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/NegationFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/NegationFunction.cs new file mode 100755 index 0000000..9bdcb4f --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/NegationFunction.cs @@ -0,0 +1,11 @@ +namespace dotless.Core.Parser.Functions +{ + using System; + public class NegationFunction : ColorMixFunction + { + protected override double Operate(double a, double b) + { + return 255 - Math.Abs(255 - b - a); + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/NumberFunctionBase.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/NumberFunctionBase.cs new file mode 100755 index 0000000..868a054 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/NumberFunctionBase.cs @@ -0,0 +1,24 @@ +namespace dotless.Core.Parser.Functions +{ + using System.Linq; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public abstract class NumberFunctionBase : Function + { + protected override Node Evaluate(Env env) + { + Guard.ExpectMinArguments(1, Arguments.Count, this, Location); + Guard.ExpectNode(Arguments[0], this, Arguments[0].Location); + + var number = Arguments[0] as Number; + var args = Arguments.Skip(1).ToArray(); + + return Eval(env, number, args); + } + + protected abstract Node Eval(Env env, Number number, Node[] args); + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/OverlayFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/OverlayFunction.cs new file mode 100755 index 0000000..d77d7c6 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/OverlayFunction.cs @@ -0,0 +1,10 @@ +namespace dotless.Core.Parser.Functions +{ + public class OverlayFunction : ColorMixFunction + { + protected override double Operate(double a, double b) + { + return a < 128 ? 2 * a * b / 255 : 255 - 2 * (255 - a) * (255 - b) / 255; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/PercentageFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/PercentageFunction.cs new file mode 100755 index 0000000..6db052b --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/PercentageFunction.cs @@ -0,0 +1,15 @@ +namespace dotless.Core.Parser.Functions +{ + using Exceptions; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + + public class PercentageFunction : NumberFunctionBase + { + protected override Node Eval(Env env, Number number, Node[] args) + { + return new Number(number.Value * 100, "%"); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/PowFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/PowFunction.cs new file mode 100755 index 0000000..ba2bc60 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/PowFunction.cs @@ -0,0 +1,27 @@ +namespace dotless.Core.Parser.Functions +{ + using System; + using System.Linq; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class PowFunction : Function + { + protected override Node Evaluate(Env env) + { + Guard.ExpectMinArguments(2, Arguments.Count, this, Location); + Guard.ExpectMaxArguments(2, Arguments.Count, this, Location); + Guard.ExpectAllNodes(Arguments, this, Location); + + WarnNotSupportedByLessJS("pow(number, number)"); + + var first = Arguments.Cast().First(); + var second = Arguments.Cast().ElementAt(1); + var value = Math.Pow(first.Value, second.Value); + + return new Number(value); + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/RedFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/RedFunction.cs new file mode 100755 index 0000000..99245c8 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/RedFunction.cs @@ -0,0 +1,25 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure.Nodes; + using Tree; + + public class RedFunction : ColorFunctionBase + { + protected override Node Eval(Color color) + { + return new Number(color.RGB[0]); + } + + protected override Node EditColor(Color color, Number number) + { + WarnNotSupportedByLessJS("red(color, number)"); + + var value = number.Value; + + if (number.Unit == "%") + value = (value*255)/100d; + + return new Color(color.R + value, color.G, color.B); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/RgbFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/RgbFunction.cs new file mode 100755 index 0000000..dfde85d --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/RgbFunction.cs @@ -0,0 +1,19 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class RgbFunction : RgbaFunction + { + protected override Node Evaluate(Env env) + { + Guard.ExpectNumArguments(3, Arguments.Count, this, Location); + + Arguments.Add(new Number(1d, "")); + + return base.Evaluate(env); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/RgbaFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/RgbaFunction.cs new file mode 100755 index 0000000..3da50a2 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/RgbaFunction.cs @@ -0,0 +1,30 @@ +namespace dotless.Core.Parser.Functions +{ + using System.Linq; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class RgbaFunction : Function + { + protected override Node Evaluate(Env env) + { + if (Arguments.Count == 2) + { + var color = Guard.ExpectNode(Arguments[0], this, Location); + var number = Guard.ExpectNode(Arguments[1], this, Location); + + return new Color(color.RGB, number.Value); + } + + Guard.ExpectNumArguments(4, Arguments.Count, this, Location); + var args = Guard.ExpectAllNodes(Arguments, this, Location); + + var rgb = args.Take(3).Select(n => n.ToNumber(255.0)).ToArray(); + var alpha = args[3].ToNumber(1.0); + + return new Color(rgb, alpha); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/RoundFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/RoundFunction.cs new file mode 100755 index 0000000..6023dcc --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/RoundFunction.cs @@ -0,0 +1,24 @@ +namespace dotless.Core.Parser.Functions +{ + using System; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using dotless.Core.Utils; + + public class RoundFunction : NumberFunctionBase + { + protected override Node Eval(Env env, Number number, Node[] args) + { + if (args.Length == 0) + { + return new Number(Math.Round(number.Value, MidpointRounding.AwayFromZero), number.Unit); + } + else + { + Guard.ExpectNode(args[0], this, args[0].Location); + return new Number(Math.Round(number.Value, (int)((Number)args[0]).Value, MidpointRounding.AwayFromZero), number.Unit); + } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/SaturationFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/SaturationFunction.cs new file mode 100755 index 0000000..90010cb --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/SaturationFunction.cs @@ -0,0 +1,39 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class SaturateFunction : HslColorFunctionBase + { + protected override Node EvalHsl(HslColor color) + { + return color.GetSaturation(); + } + + protected override Node EditHsl(HslColor color, Number number) + { + color.Saturation += number.Value/100; + return color.ToRgbColor(); + } + } + + public class DesaturateFunction : SaturateFunction + { + protected override Node EditHsl(HslColor color, Number number) + { + return base.EditHsl(color, -number); + } + } + + public class SaturationFunction : SaturateFunction + { + protected override Node EditHsl(HslColor color, Number number) + { + WarnNotSupportedByLessJS("saturation(color, number)", "saturate(color, number) or its opposite desaturate(color, number),"); + + return base.EditHsl(color, number); + } + } + +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ScreenFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ScreenFunction.cs new file mode 100755 index 0000000..5977cae --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/ScreenFunction.cs @@ -0,0 +1,10 @@ +namespace dotless.Core.Parser.Functions +{ + public class ScreenFunction : ColorMixFunction + { + protected override double Operate(double a, double b) + { + return 255 - (255 - a) * (255 - b) / 255; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/SoftlightFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/SoftlightFunction.cs new file mode 100755 index 0000000..04c03b4 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/SoftlightFunction.cs @@ -0,0 +1,11 @@ +namespace dotless.Core.Parser.Functions +{ + public class SoftlightFunction : ColorMixFunction + { + protected override double Operate(double a, double b) + { + double t = b * a / 255; + return t + a * (255 - (255 - a) * (255 - b) / 255 - t) / 255; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/StringFunctions.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/StringFunctions.cs new file mode 100755 index 0000000..b08be65 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/StringFunctions.cs @@ -0,0 +1,65 @@ +namespace dotless.Core.Parser.Functions +{ + using System; + using System.Linq; + using System.Text.RegularExpressions; + using System.Web; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class EFunction : Function + { + protected override Node Evaluate(Env env) + { + Guard.ExpectMaxArguments(1, Arguments.Count, this, Location); + + WarnNotSupportedByLessJS("e(string)", @"~"""""); + + if (Arguments.Count == 0) + return new TextNode(""); + + var str = Arguments[0]; + if (str is Quoted) + return new TextNode((str as Quoted).UnescapeContents()); + + return new TextNode(str.ToCSS(env)); + } + } + + public class CFormatString : Function + { + protected override Node Evaluate(Env env) + { + WarnNotSupportedByLessJS("%(string, args...)", @"~"""" and string interpolation"); + + if (Arguments.Count == 0) + return new Quoted("", false); + + Func stringValue = n => n is Quoted ? ((Quoted)n).Value : n.ToCSS(env); + + var str = stringValue(Arguments[0]); + + var args = Arguments.Skip(1).ToList(); + var i = 0; + + MatchEvaluator replacement = m => + { + var value = (m.Value == "%s") ? + stringValue(args[i++]) : + args[i++].ToCSS(env); + + return char.IsUpper(m.Value[1]) ? + Uri.EscapeDataString(value) : + value; + }; + + str = Regex.Replace(str, "%[sda]", replacement, RegexOptions.IgnoreCase); + + var quote = Arguments[0] is Quoted ? (Arguments[0] as Quoted).Quote : null; + + return new Quoted(str, quote); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/UnitFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/UnitFunction.cs new file mode 100755 index 0000000..7ea0423 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Functions/UnitFunction.cs @@ -0,0 +1,33 @@ +namespace dotless.Core.Parser.Functions +{ + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + using Utils; + + public class UnitFunction : Function + { + protected override Node Evaluate(Env env) + { + Guard.ExpectMaxArguments(2, Arguments.Count, this, Location); + Guard.ExpectNode(Arguments[0], this, Location); + + var number = Arguments[0] as Number; + + var unit = string.Empty; + if (Arguments.Count == 2) + { + if (Arguments[1] is Keyword) + { + unit = ((Keyword)Arguments[1]).Value; + } + else + { + unit = Arguments[1].ToCSS(env); + } + } + + return new Number(number.Value, unit); + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Closure.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Closure.cs new file mode 100755 index 0000000..5396bd4 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Closure.cs @@ -0,0 +1,11 @@ +namespace dotless.Core.Parser.Infrastructure +{ + using System.Collections.Generic; + using Tree; + + public class Closure + { + public Ruleset Ruleset { get; set; } + public List Context { get; set; } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Context.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Context.cs new file mode 100755 index 0000000..853c928 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Context.cs @@ -0,0 +1,263 @@ +namespace dotless.Core.Parser.Infrastructure +{ + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using Tree; + using Utils; + using dotless.Core.Parser.Infrastructure.Nodes; + + public class Context : IEnumerable> + { + private List> Paths { get; set; } + + public Context() + { + Paths = new List>(); + } + + public Context Clone() + { + var newPathList = new List>(); + return new Context {Paths = newPathList}; + } + + public void AppendSelectors(Context context, IEnumerable selectors) + { + foreach (var selector in selectors) + { + AppendSelector(context, selector); + } + } + + private void AppendSelector(Context context, Selector selector) + { + if (!selector.Elements.Any(e => e.Value == "&")) + { + if (context != null && context.Paths.Count > 0) + { + Paths.AddRange(context.Paths.Select(path => path.Concat(new[] { selector }).ToList())); + } + else + { + Paths.Add(new List() { selector }); + } + return; + } + + // The paths are List> + // The first list is a list of comma seperated selectors + // The inner list is a list of inheritance seperated selectors + // e.g. + // .a, .b { + // .c { + // } + // } + // == {{.a} {.c}} {{.b} {.c}} + // + + // the elements from the current selector so far + var currentElements = new NodeList(); + // the current list of new selectors to add to the path. + // We will build it up. We initiate it with one empty selector as we "multiply" the new selectors + // by the parents + var newSelectors = new List>() { new List() }; + + foreach (Element el in selector.Elements) + { + // non parent reference elements just get added + if (el.Value != "&") + { + currentElements.Add(el); + } else + { + // the new list of selectors to add + var selectorsMultiplied = new List>(); + + // merge the current list of non parent selector elements + // on to the current list of selectors to add + if (currentElements.Count > 0) + { + MergeElementsOnToSelectors(currentElements, newSelectors); + } + + // loop through our current selectors + foreach(List sel in newSelectors) + { + // if we don't have any parent paths, the & might be in a mixin so that it can be used + // whether there are parents or not + if (context.Paths.Count == 0) + { + // the combinator used on el should now be applied to the next element instead so that + // it is not lost + if (sel.Count > 0) + { + sel[0].Elements = new NodeList(sel[0].Elements); + sel[0].Elements.Add(new Element(el.Combinator, "")); + } + selectorsMultiplied.Add(sel); + } + else + { + // and the parent selectors + foreach (List parentSel in context.Paths) + { + // We need to put the current selectors + // then join the last selector's elements on to the parents selectors + + // our new selector path + List newSelectorPath = new List(); + // selectors from the parent after the join + List afterParentJoin = new List(); + Selector newJoinedSelector; + bool newJoinedSelectorEmpty = true; + + //construct the joined selector - if & is the first thing this will be empty, + // if not newJoinedSelector will be the last set of elements in the selector + if (sel.Count > 0) + { + newJoinedSelector = new Selector(new NodeList(sel[sel.Count - 1].Elements)); + newSelectorPath.AddRange(sel.Take(sel.Count - 1)); + newJoinedSelectorEmpty = false; + } + else + { + newJoinedSelector = new Selector(new NodeList()); + } + + //put together the parent selectors after the join + if (parentSel.Count > 1) + { + afterParentJoin.AddRange(parentSel.Skip(1)); + } + + if (parentSel.Count > 0) + { + newJoinedSelectorEmpty = false; + + // join the elements so far with the first part of the parent + if (parentSel[0].Elements[0].Value == null) + { + newJoinedSelector.Elements.Add(new Element(el.Combinator, parentSel[0].Elements[0].NodeValue)); + } + else + { + newJoinedSelector.Elements.Add(new Element(el.Combinator, parentSel[0].Elements[0].Value)); + } + newJoinedSelector.Elements.AddRange(parentSel[0].Elements.Skip(1)); + } + + if (!newJoinedSelectorEmpty) + { + // now add the joined selector + newSelectorPath.Add(newJoinedSelector); + } + + // and the rest of the parent + newSelectorPath.AddRange(afterParentJoin); + + // add that to our new set of selectors + selectorsMultiplied.Add(newSelectorPath); + } + } + } + + // our new selectors has been multiplied, so reset the state + newSelectors = selectorsMultiplied; + currentElements = new NodeList(); + } + } + + // if we have any elements left over (e.g. .a& .b == .b) + // add them on to all the current selectors + if (currentElements.Count > 0) + { + MergeElementsOnToSelectors(currentElements, newSelectors); + } + + Paths.AddRange(newSelectors.Select(sel => sel.Select(MergeJoinedElements).ToList())); + } + + private void MergeElementsOnToSelectors(NodeList elements, List> selectors) + { + if (selectors.Count == 0) + { + selectors.Add(new List() { new Selector(elements) }); + return; + } + + foreach (List sel in selectors) + { + // if the previous thing in sel is a parent this needs to join on to it? + if (sel.Count > 0) + { + sel[sel.Count - 1] = new Selector(sel[sel.Count - 1].Elements.Concat(elements)); + } + else + { + sel.Add(new Selector(elements)); + } + } + } + + private static readonly char[] LeaveUnmerged = {'.', '#', ':'}; + + private Selector MergeJoinedElements(Selector selector) { + var elements = selector.Elements + .Select(e => e.Clone()) + .ToList(); + + for (int i = 1; i < elements.Count; i++) { + var previousElement = elements[i - 1]; + var currentElement = elements[i]; + + if (string.IsNullOrEmpty(currentElement.Value)) { + continue; + } + + if (LeaveUnmerged.Contains(currentElement.Value[0])) { + continue; + } + + if (currentElement.Combinator.Value != "") { + continue; + } + + elements[i - 1] = new Element(previousElement.Combinator, previousElement.Value += currentElement.Value); + elements.RemoveAt(i); + i--; + } + + return new Selector(elements); + } + + public void AppendCSS(Env env) { + var selectors = Paths + .Where(p => p.Any(s => !s.IsReference)) + .Select(path => path.Select(p => p.ToCSS(env)).JoinStrings("").Trim()) + .Distinct(); + + env.Output.AppendMany(selectors, env.Compress ? "," : ",\n"); + } + + public string ToCss(Env env) + { + return string.Join(env.Compress ? "," : ",\n",Paths.Select(path => path.Select(p => p.ToCSS(env)).JoinStrings("").Trim()).ToArray()); + } + + public int Count + { + get { return Paths.Count; } + } + + public IEnumerator> GetEnumerator() + { + return Paths.Cast>().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/DefaultNodeProvider.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/DefaultNodeProvider.cs new file mode 100755 index 0000000..4933ffc --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/DefaultNodeProvider.cs @@ -0,0 +1,198 @@ +namespace dotless.Core.Parser.Infrastructure +{ + using System.Collections.Generic; + using Importers; + using Nodes; + using Tree; + + public class DefaultNodeProvider : INodeProvider + { + public Element Element(Combinator combinator, Node value, NodeLocation location) + { + return new Element(combinator, value) { Location = location }; + } + + public Combinator Combinator(string value, NodeLocation location) + { + return new Combinator(value) { Location = location }; + } + + public Selector Selector(NodeList elements, NodeLocation location) + { + return new Selector(elements) { Location = location }; + } + + public Rule Rule(string name, Node value, NodeLocation location) + { + return new Rule(name, value) { Location = location }; + } + + public Rule Rule(string name, Node value, bool variadic, NodeLocation location) + { + return new Rule(name, value, variadic) { Location = location }; + } + + public Ruleset Ruleset(NodeList selectors, NodeList rules, NodeLocation location) + { + return new Ruleset(selectors, rules) { Location = location }; + } + + public CssFunction CssFunction(string name, Node value, NodeLocation location) + { + return new CssFunction() { Name = name, Value = value, Location = location }; + } + + public Alpha Alpha(Node value, NodeLocation location) + { + return new Alpha(value) { Location = location }; + } + + public Call Call(string name, NodeList arguments, NodeLocation location) + { + return new Call(name, arguments) { Location = location }; + } + + public Color Color(string rgb, NodeLocation location) + { + var color = Tree.Color.FromHex(rgb); + color.Location = location; + return color; + } + + public Keyword Keyword(string value, NodeLocation location) + { + return new Keyword(value) { Location = location }; + } + + public Number Number(string value, string unit, NodeLocation location) + { + return new Number(value, unit) { Location = location }; + } + + public Shorthand Shorthand(Node first, Node second, NodeLocation location) + { + return new Shorthand(first, second) { Location = location }; + } + + public Variable Variable(string name, NodeLocation location) + { + return new Variable(name) { Location = location }; + } + + public Url Url(Node value, IImporter importer, NodeLocation location) + { + return new Url(value, importer) { Location = location }; + } + + public Script Script(string script, NodeLocation location) + { + return new Script(script) { Location = location }; + } + + public GuardedRuleset GuardedRuleset(NodeList selectors, NodeList rules, Condition condition, NodeLocation location) + { + return new GuardedRuleset(selectors, rules, condition) { Location = location }; + } + + public MixinCall MixinCall(NodeList elements, List arguments, bool important, NodeLocation location) + { + return new MixinCall(elements, arguments, important) { Location = location }; + } + + public MixinDefinition MixinDefinition(string name, NodeList parameters, NodeList rules, Condition condition, bool variadic, NodeLocation location) + { + return new MixinDefinition(name, parameters, rules, condition, variadic) { Location = location }; + } + + public Import Import(Url path, Value features, ImportOptions option, NodeLocation location) + { + return new Import(path, features, option) { Location = location }; + } + + public Import Import(Quoted path, Value features, ImportOptions option, NodeLocation location) + { + return new Import(path, features, option) { Location = location }; + } + + public Directive Directive(string name, string identifier, NodeList rules, NodeLocation location) + { + return new Directive(name, identifier, rules) { Location = location }; + } + + public Media Media(NodeList rules, Value features, NodeLocation location) + { + return new Media(features, rules) { Location = location }; + } + + public KeyFrame KeyFrame(NodeList identifier, NodeList rules, NodeLocation location) + { + return new KeyFrame(identifier, rules) { Location = location }; + } + + public Directive Directive(string name, Node value, NodeLocation location) + { + return new Directive(name, value) { Location = location }; + } + + public Expression Expression(NodeList expression, NodeLocation location) + { + return new Expression(expression) { Location = location }; + } + + public Value Value(IEnumerable values, string important, NodeLocation location) + { + return new Value(values, important) { Location = location }; + } + + public Operation Operation(string operation, Node left, Node right, NodeLocation location) + { + return new Operation(operation, left, right) { Location = location }; + } + + public Assignment Assignment(string key, Node value, NodeLocation location) + { + return new Assignment(key, value) {Location = location}; + } + + public Comment Comment(string value, NodeLocation location) + { + return new Comment(value) { Location = location }; + } + + public TextNode TextNode(string contents, NodeLocation location) + { + return new TextNode(contents) { Location = location }; + } + + public Quoted Quoted(string value, string contents, bool escaped, NodeLocation location) + { + return new Quoted(value, contents, escaped) { Location = location }; + } + + public Extend Extend(List exact, List partial, NodeLocation location) + { + return new Extend(exact,partial) { Location = location }; + } + + public Node Attribute(Node key, Node op, Node val, NodeLocation location) + { + return new Attribute(key, op, val) { Location = location }; + } + + public Paren Paren(Node value, NodeLocation location) + { + return new Paren(value) { Location = location }; + } + + public Condition Condition(Node left, string operation, Node right, bool negate, NodeLocation location) + { + return new Condition(left, operation, right, negate) { Location = location }; + } +#if CSS3EXPERIMENTAL + public RepeatEntity RepeatEntity(Node value, Node repeatCount, int index) + { + return new RepeatEntity(value, repeatCount) { Location = location }; + } +#endif + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Env.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Env.cs new file mode 100755 index 0000000..ac6ee36 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Env.cs @@ -0,0 +1,446 @@ +using dotless.Core.Utils; + +namespace dotless.Core.Parser.Infrastructure +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using System.Text.RegularExpressions; + using Functions; + using Nodes; + using Plugins; + using Tree; + using dotless.Core.Loggers; + + public class Env + { + private Dictionary _functionTypes; + private readonly List _plugins; + private readonly List _extensions; + + public Stack Frames { get; protected set; } + public bool Compress { get; set; } + public bool Debug { get; set; } + public Node Rule { get; set; } + public ILogger Logger { get; set; } + public Output Output { get; private set; } + public Stack MediaPath { get; private set; } + public List MediaBlocks { get; private set; } + [Obsolete("The Variable Redefines feature has been removed to align with less.js")] + public bool DisableVariableRedefines { get; set; } + [Obsolete("The Color Compression feature has been removed to align with less.js")] + public bool DisableColorCompression { get; set; } + public bool KeepFirstSpecialComment { get; set; } + public bool IsFirstSpecialCommentOutput { get; set; } + public Parser Parser { get; set; } + + public Env() : this(new Parser()) + { + } + + public Env(Parser parser) : this(parser, null, null) + { + } + + protected Env(Parser parser, Stack frames, Dictionary functions) : this(frames, functions) { + Parser = parser; + } + + protected Env(Stack frames, Dictionary functions) { + Frames = frames ?? new Stack(); + Output = new Output(this); + MediaPath = new Stack(); + MediaBlocks = new List(); + Logger = new NullLogger(LogLevel.Info); + + _plugins = new List(); + _functionTypes = functions ?? new Dictionary(); + _extensions = new List(); + ExtendMediaScope = new Stack(); + + if (_functionTypes.Count == 0) + AddCoreFunctions(); + } + + /// + /// Creates a new Env variable for the purposes of scope + /// + [Obsolete("Argument is ignored as of version 1.4.3.0. Use the parameterless overload of CreateChildEnv instead.", false)] + public virtual Env CreateChildEnv(Stack ruleset) + { + return CreateChildEnv(); + } + + /// + /// Creates a new Env variable for the purposes of scope + /// + public virtual Env CreateChildEnv() + { + return new Env(null, _functionTypes) + { + Parser = Parser, + Parent = this, + Debug = Debug, + Compress = Compress, + DisableVariableRedefines = DisableVariableRedefines + }; + } + + private Env Parent { get; set; } + + /// + /// Creates a new Env variable for the purposes of scope + /// + public virtual Env CreateVariableEvaluationEnv(string variableName) { + var env = CreateChildEnv(); + env.EvaluatingVariable = variableName; + return env; + } + + private string EvaluatingVariable { get; set; } + + public bool IsEvaluatingVariable(string variableName) { + if (string.Equals(variableName, EvaluatingVariable, StringComparison.InvariantCulture)) { + return true; + } + + if (Parent != null) { + return Parent.IsEvaluatingVariable(variableName); + } + + return false; + } + + public virtual Env CreateChildEnvWithClosure(Closure closure) { + var env = CreateChildEnv(); + env.Rule = Rule; + env.ClosureEnvironment = new Env(new Stack(closure.Context), this._functionTypes); + return env; + } + + private Env ClosureEnvironment { get; set; } + + /// + /// Adds a plugin to this Env + /// + public void AddPlugin(IPlugin plugin) + { + if (plugin == null) throw new ArgumentNullException("plugin"); + + _plugins.Add(plugin); + + IFunctionPlugin functionPlugin = plugin as IFunctionPlugin; + if (functionPlugin != null) + { + foreach(KeyValuePair function in functionPlugin.GetFunctions()) + { + string functionName = function.Key.ToLowerInvariant(); + + if (_functionTypes.ContainsKey(functionName)) + { + string message = string.Format("Function '{0}' already exists in environment but is added by plugin {1}", + functionName, plugin.GetName()); + throw new InvalidOperationException(message); + } + + AddFunction(functionName, function.Value); + } + } + } + + /// + /// All the visitor plugins to use + /// + public IEnumerable VisitorPlugins + { + get + { + return _plugins.OfType(); + } + } + + //Keep track of media scoping for extenders + public Stack ExtendMediaScope { get; set; } + + /// + /// Returns whether the comment should be silent + /// + /// + /// + public bool IsCommentSilent(bool isValidCss, bool isCssHack, bool isSpecialComment) + { + if (!isValidCss) + return true; + + if (isCssHack) + return false; + + if (Compress && KeepFirstSpecialComment && !IsFirstSpecialCommentOutput && isSpecialComment) + { + IsFirstSpecialCommentOutput = true; + return false; + } + + return Compress; + } + + /// + /// Finds the first scoped variable with this name + /// + public Rule FindVariable(string name) + { + return FindVariable(name, Rule); + } + + /// + /// Finds the first scoped variable matching the name, using Rule as the current rule to work backwards from + /// + public Rule FindVariable(string name, Node rule) + { + var previousNode = rule; + foreach (var frame in Frames) + { + var v = frame.Variable(name, null); + if (v) + return v; + previousNode = frame; + } + + Rule result = null; + if (Parent != null) { + result = Parent.FindVariable(name, rule); + } + + if (result != null) { + return result; + } + + if (ClosureEnvironment != null) { + return ClosureEnvironment.FindVariable(name, rule); + } + + return null; + } + + [Obsolete("This method will be removed in a future release.", false)] + public IEnumerable FindRulesets(Selector selector) where TRuleset : Ruleset + { + return FindRulesets(selector).Where(c => c.Ruleset is TRuleset); + } + + /// + /// Finds the first Ruleset matching the selector argument that inherits from or is of type TRuleset (pass this as Ruleset if + /// you are trying to find ANY Ruleset that matches the selector) + /// + public IEnumerable FindRulesets(Selector selector) + { + var matchingRuleSets = Frames + .Reverse() + .SelectMany(frame => frame.Find(this, selector, null)) + .Where(matchedClosure => { + if (!Frames.Any(frame => frame.IsEqualOrClonedFrom(matchedClosure.Ruleset))) + return true; + + var mixinDef = matchedClosure.Ruleset as MixinDefinition; + if (mixinDef != null) + return mixinDef.Condition != null; + + return false; + }).ToList(); + + if (matchingRuleSets.Any()) + { + return matchingRuleSets; + } + + if (Parent != null) + { + var parentRulesets = Parent.FindRulesets(selector); + if (parentRulesets != null) + { + return parentRulesets; + } + } + + if (ClosureEnvironment != null) + { + return ClosureEnvironment.FindRulesets(selector); + } + + return null; + } + + /// + /// Adds a Function to this Env object + /// + public void AddFunction(string name, Type type) + { + if (name == null) throw new ArgumentNullException("name"); + if (type == null) throw new ArgumentNullException("type"); + + _functionTypes[name] = type; + } + + /// + /// Given an assembly, adds all the dotless Functions in that assembly into this Env. + /// + public void AddFunctionsFromAssembly(Assembly assembly) + { + if (assembly == null) throw new ArgumentNullException("assembly"); + + var functions = GetFunctionsFromAssembly(assembly); + + AddFunctionsToRegistry(functions); + } + + private void AddFunctionsToRegistry(IEnumerable> functions) { + foreach (var func in functions) { + AddFunction(func.Key, func.Value); + } + } + + private static Dictionary GetFunctionsFromAssembly(Assembly assembly) { + var functionType = typeof (Function); + + return assembly + .GetTypes() + .Where(t => functionType.IsAssignableFrom(t) && t != functionType) + .Where(t => !t.IsAbstract) + .SelectMany(GetFunctionNames) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + } + + private static Dictionary GetCoreFunctions() { + var functions = GetFunctionsFromAssembly(Assembly.GetExecutingAssembly()); + functions["%"] = typeof (CFormatString); + return functions; + } + + private static readonly Dictionary CoreFunctions = GetCoreFunctions(); + + private void AddCoreFunctions() { + _functionTypes = new Dictionary(CoreFunctions); + } + + /// + /// Given a function name, returns a new Function matching that name. + /// + public virtual Function GetFunction(string name) + { + Function function = null; + name = name.ToLowerInvariant(); + + if (_functionTypes.ContainsKey(name)) + { + function = (Function)Activator.CreateInstance(_functionTypes[name]); + function.Logger = Logger; + } + + return function; + } + + private static IEnumerable> GetFunctionNames(Type t) + { + var name = t.Name; + + if (name.EndsWith("function", StringComparison.InvariantCultureIgnoreCase)) + name = name.Substring(0, name.Length - 8); + + name = Regex.Replace(name, @"\B[A-Z]", "-$0"); + + name = name.ToLowerInvariant(); + + yield return new KeyValuePair(name, t); + + if(name.Contains("-")) + yield return new KeyValuePair(name.Replace("-", ""), t); + } + + public void AddExtension(Selector selector, Extend extends, Env env) + { + foreach (var extending in extends.Exact) + { + Extender match = null; + if ((match = _extensions.OfType().FirstOrDefault(e => e.BaseSelector.ToString().Trim() == extending.ToString().Trim())) == null) + { + match = new ExactExtender(extending, extends); + _extensions.Add(match); + } + + match.AddExtension(selector,env); + } + + foreach (var extending in extends.Partial) + { + Extender match = null; + if ((match = _extensions.OfType().FirstOrDefault(e => e.BaseSelector.ToString().Trim() == extending.ToString().Trim())) == null) + { + match = new PartialExtender(extending, extends); + _extensions.Add(match); + } + + match.AddExtension(selector,env); + } + + if (Parent != null) { + Parent.AddExtension(selector, extends, env); + } + } + + public void RegisterExtensionsFrom(Env child) { + _extensions.AddRange(child._extensions); + } + + public IEnumerable FindUnmatchedExtensions() { + return _extensions.Where(e => !e.IsMatched); + } + + public ExactExtender FindExactExtension(string selection) + { + if (ExtendMediaScope.Any()) + { + var mediaScopedExtensions = ExtendMediaScope.Select(media => media.FindExactExtension(selection)).FirstOrDefault(result => result != null); + if (mediaScopedExtensions != null) { + return mediaScopedExtensions; + } + } + + return _extensions.OfType().FirstOrDefault(e => e.BaseSelector.ToString().Trim() == selection); + } + + public PartialExtender[] FindPartialExtensions(Context selection) + { + if (ExtendMediaScope.Any()) + { + var mediaScopedExtensions = ExtendMediaScope.Select(media => media.FindPartialExtensions(selection)).FirstOrDefault(result => result.Any()); + if (mediaScopedExtensions != null) { + return mediaScopedExtensions; + } + } + + return _extensions.OfType() + .WhereExtenderMatches(selection) + .ToArray(); + } + + [Obsolete("This method doesn't return the correct results. Use FindPartialExtensions(Context) instead.", false)] + public PartialExtender[] FindPartialExtensions(string selection) + { + if (ExtendMediaScope.Any()) + { + var mediaScopedExtensions = ExtendMediaScope.Select(media => media.FindPartialExtensions(selection)).FirstOrDefault(result => result.Any()); + if (mediaScopedExtensions != null) { + return mediaScopedExtensions; + } + } + + return _extensions.OfType().Where(e => selection.Contains(e.BaseSelector.ToString().Trim())).ToArray(); + } + + public override string ToString() + { + return Frames.Select(f => f.ToString()).JoinStrings(" <- "); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Extender.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Extender.cs new file mode 100755 index 0000000..b21164f --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Extender.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Design; +using System.Linq; +using System.Text; +using System.Threading; +using dotless.Core.Parser.Tree; +using dotless.Core.Utils; + +namespace dotless.Core.Parser.Infrastructure +{ + public class ExactExtender : Extender + { + [Obsolete("Use the overload that accepts the Extend node")] + public ExactExtender(Selector baseSelector) : this(baseSelector, null) + { + + } + public ExactExtender(Selector baseSelector, Extend extend) : base(baseSelector, extend) + { + + } + } + + public class PartialExtender : Extender + { + [Obsolete("Use the overload that accepts the Extend node")] + public PartialExtender(Selector baseSelector) : this(baseSelector, null) + { + + } + public PartialExtender(Selector baseSelector, Extend extend) : base(baseSelector, extend) + { + + } + + public IEnumerable Replacements(string selection) + { + foreach (var ex in ExtendedBy) + { + yield return new Selector(new []{new Element(null, selection.Replace(BaseSelector.ToString().Trim(), ex.ToString().Trim()))}); + } + } + } + internal static class ExtenderMatcherExtensions { + internal static IEnumerable WhereExtenderMatches(this IEnumerable extenders, Context selection) { + var selectionElements = selection.SelectMany(selectors => selectors.SelectMany(s => s.Elements)).ToList(); + + return extenders.Where(e => e.ElementListMatches(selectionElements)); + } + + /// + /// Tests whether or not this extender matches the selector elements in + /// by checking if the elements in are a subsequence of the ones in + /// . + /// + /// The special case in the comparison is that if the subsequence element we are comparing is the last one in its + /// sequence, we don't compare combinators. + /// + /// A practical example of why we do that is an extendee with the selector: + /// + /// .test .a + /// + /// and an extender with the selector + /// + /// .test + /// + /// The extender should match, even though the .test element in the extendee will have the descendant combinator " " + /// and the .test element in the extender won't. + /// + private static bool ElementListMatches(this PartialExtender extender, IList list) { + int count = extender.BaseSelector.Elements.Count; + + return extender.BaseSelector.Elements.IsSubsequenceOf(list, (subIndex, subElement, index, seqelement) => { + if (subIndex < count - 1) { + return Equals(subElement.Combinator, seqelement.Combinator) && + string.Equals(subElement.Value, seqelement.Value) && + Equals(subElement.NodeValue, seqelement.NodeValue); + } + + return string.Equals(subElement.Value, seqelement.Value) && + Equals(subElement.NodeValue, seqelement.NodeValue); + }); + } + } + + + public class Extender + { + public Selector BaseSelector { get; private set; } + public List ExtendedBy { get; private set; } + public bool IsReference { get; set; } + public bool IsMatched { get; set; } + public Extend Extend { get; private set; } + + [Obsolete("Use the overload that accepts the Extend node")] + public Extender(Selector baseSelector) + { + BaseSelector = baseSelector; + ExtendedBy = new List(); + IsReference = baseSelector.IsReference; + } + + public Extender(Selector baseSelector, Extend extend) : this(baseSelector) + { + Extend = extend; + } + + public static string FullPathSelector() + { + throw new NotImplementedException(); + } + + public void AddExtension(Selector selector, Env env) + { + var selectorPath = new List> {new [] {selector} }; + selectorPath.AddRange(env.Frames.Skip(1).Select(f => f.Selectors.Where(partialSelector => partialSelector != null))); + + ExtendedBy.Add(GenerateExtenderSelector(env, selectorPath)); + } + + private Selector GenerateExtenderSelector(Env env, List> selectorPath) { + var context = GenerateExtenderSelector(selectorPath); + return new Selector(new[] {new Element(null, context.ToCss(env)) }) { IsReference = IsReference }; + } + + private Context GenerateExtenderSelector(List> selectorStack) { + if (!selectorStack.Any()) { + return null; + } + + var parentContext = GenerateExtenderSelector(selectorStack.Skip(1).ToList()); + + var childContext = new Context(); + childContext.AppendSelectors(parentContext, selectorStack.First()); + return childContext; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/INodeProvider.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/INodeProvider.cs new file mode 100755 index 0000000..9fdf21b --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/INodeProvider.cs @@ -0,0 +1,64 @@ +namespace dotless.Core.Parser.Infrastructure +{ + using System.Collections.Generic; + using Importers; + using Nodes; + using Tree; + + public interface INodeProvider + { + Element Element(Combinator combinator, Node Value, NodeLocation location); + Combinator Combinator(string value, NodeLocation location); + Selector Selector(NodeList elements, NodeLocation location); + Rule Rule(string name, Node value, NodeLocation location); + Rule Rule(string name, Node value, bool variadic, NodeLocation location); + Ruleset Ruleset(NodeList selectors, NodeList rules, NodeLocation location); + + CssFunction CssFunction(string name, Node value, NodeLocation location); + + //entities + Alpha Alpha(Node value, NodeLocation location); + Call Call(string name, NodeList arguments, NodeLocation location); + Color Color(string rgb, NodeLocation location); + Keyword Keyword(string value, NodeLocation location); + Number Number(string value, string unit, NodeLocation location); + Shorthand Shorthand(Node first, Node second, NodeLocation location); + Variable Variable(string name, NodeLocation location); + Url Url(Node value, IImporter importer, NodeLocation location); + Script Script(string script, NodeLocation location); + Paren Paren(Node node, NodeLocation location); + + GuardedRuleset GuardedRuleset(NodeList selectors, NodeList rules, Condition condition, NodeLocation location); + + //mixins + MixinCall MixinCall(NodeList elements, List arguments, bool important, NodeLocation location); + MixinDefinition MixinDefinition(string name, NodeList parameters, NodeList rules, Condition condition, bool variadic, NodeLocation location); + Condition Condition(Node left, string operation, Node right, bool negate, NodeLocation location); + + //directives + Import Import(Url path, Value features, ImportOptions option, NodeLocation location); + Import Import(Quoted path, Value features, ImportOptions option, NodeLocation location); + Directive Directive(string name, string identifier, NodeList rules, NodeLocation location); + Directive Directive(string name, Node value, NodeLocation location); + Media Media(NodeList rules, Value features, NodeLocation location); + KeyFrame KeyFrame(NodeList identifier, NodeList rules, NodeLocation location); + + //expressions + Expression Expression(NodeList expression, NodeLocation location); + #if CSS3EXPERIMENTAL + RepeatEntity RepeatEntity(Node value, Node repeatCount, int index); +#endif + Value Value(IEnumerable values, string important, NodeLocation location); + Operation Operation(string operation, Node left, Node right, NodeLocation location); + Assignment Assignment(string key, Node value, NodeLocation location); + + //text + Comment Comment(string value, NodeLocation location); + TextNode TextNode(string contents, NodeLocation location); + Quoted Quoted(string value, string contents, bool escaped, NodeLocation location); + + //extenders + Extend Extend(List exact, List partial, NodeLocation location); + Node Attribute(Node key, Node op, Node val, NodeLocation location); + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/IOperable.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/IOperable.cs new file mode 100755 index 0000000..843c55e --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/IOperable.cs @@ -0,0 +1,11 @@ +namespace dotless.Core.Parser.Infrastructure +{ + using Nodes; + using Tree; + + public interface IOperable + { + Node Operate(Operation op, Node other); + Color ToColor(); + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/MimeTypeLookup.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/MimeTypeLookup.cs new file mode 100755 index 0000000..5cd0f7c --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/MimeTypeLookup.cs @@ -0,0 +1,1302 @@ +using System; +using System.Collections.Generic; + +namespace dotless.Core.Parser.Infrastructure +{ + // Source: https://gist.github.com/ChristianWeyer/eea2cb567932e345cdfa + // If this project is moved to .NET 4.5, this can be replaced with System.Web.MimeMapping + // http://msdn.microsoft.com/en-us/library/system.web.mimemapping.getmimemapping.aspx + internal class MimeTypeLookup + { + public string ByFilename(string filename) + { + var extension = filename.Substring(filename.LastIndexOf('.')); + + string mimeType; + if (!String.IsNullOrEmpty(extension) && _mappings.TryGetValue(extension, out mimeType)) + return mimeType; + + return null; + } + + #region MimeTypes + private static readonly Dictionary _mappings = new Dictionary(2000, StringComparer.InvariantCultureIgnoreCase) + { + {".ez", "application/andrew-inset"}, + {".aw", "application/applixware"}, + {".atom", "application/atom+xml"}, + {".atomcat", "application/atomcat+xml"}, + {".atomsvc", "application/atomsvc+xml"}, + {".ccxml", "application/ccxml+xml"}, + {".cdmia", "application/cdmi-capability"}, + {".cdmic", "application/cdmi-container"}, + {".cdmid", "application/cdmi-domain"}, + {".cdmio", "application/cdmi-object"}, + {".cdmiq", "application/cdmi-queue"}, + {".cu", "application/cu-seeme"}, + {".davmount", "application/davmount+xml"}, + {".dbk", "application/docbook+xml"}, + {".dssc", "application/dssc+der"}, + {".xdssc", "application/dssc+xml"}, + {".ecma", "application/ecmascript"}, + {".emma", "application/emma+xml"}, + {".epub", "application/epub+zip"}, + {".exi", "application/exi"}, + {".pfr", "application/font-tdpfr"}, + {".gml", "application/gml+xml"}, + {".gpx", "application/gpx+xml"}, + {".gxf", "application/gxf"}, + {".stk", "application/hyperstudio"}, + {".ink", "application/inkml+xml"}, + {".inkml", "application/inkml+xml"}, + {".ipfix", "application/ipfix"}, + {".jar", "application/java-archive"}, + {".ser", "application/java-serialized-object"}, + {".class", "application/java-vm"}, + {".js", "application/javascript"}, + {".json", "application/json"}, + {".jsonml", "application/jsonml+json"}, + {".lostxml", "application/lost+xml"}, + {".hqx", "application/mac-binhex40"}, + {".cpt", "application/mac-compactpro"}, + {".mads", "application/mads+xml"}, + {".mrc", "application/marc"}, + {".mrcx", "application/marcxml+xml"}, + {".ma", "application/mathematica"}, + {".nb", "application/mathematica"}, + {".mb", "application/mathematica"}, + {".mathml", "application/mathml+xml"}, + {".mbox", "application/mbox"}, + {".mscml", "application/mediaservercontrol+xml"}, + {".metalink", "application/metalink+xml"}, + {".meta4", "application/metalink4+xml"}, + {".mets", "application/mets+xml"}, + {".mods", "application/mods+xml"}, + {".m21", "application/mp21"}, + {".mp21", "application/mp21"}, + {".mp4s", "application/mp4"}, + {".doc", "application/msword"}, + {".dot", "application/msword"}, + {".mxf", "application/mxf"}, + {".bin", "application/octet-stream"}, + {".dms", "application/octet-stream"}, + {".lrf", "application/octet-stream"}, + {".mar", "application/octet-stream"}, + {".so", "application/octet-stream"}, + {".dist", "application/octet-stream"}, + {".distz", "application/octet-stream"}, + {".pkg", "application/octet-stream"}, + {".bpk", "application/octet-stream"}, + {".dump", "application/octet-stream"}, + {".elc", "application/octet-stream"}, + {".deploy", "application/octet-stream"}, + {".oda", "application/oda"}, + {".opf", "application/oebps-package+xml"}, + {".ogx", "application/ogg"}, + {".omdoc", "application/omdoc+xml"}, + {".onetoc", "application/onenote"}, + {".onetoc2", "application/onenote"}, + {".onetmp", "application/onenote"}, + {".onepkg", "application/onenote"}, + {".oxps", "application/oxps"}, + {".xer", "application/patch-ops-error+xml"}, + {".pdf", "application/pdf"}, + {".pgp", "application/pgp-encrypted"}, + {".asc", "application/pgp-signature"}, + {".sig", "application/pgp-signature"}, + {".prf", "application/pics-rules"}, + {".p10", "application/pkcs10"}, + {".p7m", "application/pkcs7-mime"}, + {".p7c", "application/pkcs7-mime"}, + {".p7s", "application/pkcs7-signature"}, + {".p8", "application/pkcs8"}, + {".ac", "application/pkix-attr-cert"}, + {".cer", "application/pkix-cert"}, + {".crl", "application/pkix-crl"}, + {".pkipath", "application/pkix-pkipath"}, + {".pki", "application/pkixcmp"}, + {".pls", "application/pls+xml"}, + {".ai", "application/postscript"}, + {".eps", "application/postscript"}, + {".ps", "application/postscript"}, + {".cww", "application/prs.cww"}, + {".pskcxml", "application/pskc+xml"}, + {".rdf", "application/rdf+xml"}, + {".rif", "application/reginfo+xml"}, + {".rnc", "application/relax-ng-compact-syntax"}, + {".rl", "application/resource-lists+xml"}, + {".rld", "application/resource-lists-diff+xml"}, + {".rs", "application/rls-services+xml"}, + {".gbr", "application/rpki-ghostbusters"}, + {".mft", "application/rpki-manifest"}, + {".roa", "application/rpki-roa"}, + {".rsd", "application/rsd+xml"}, + {".rss", "application/rss+xml"}, + {".rtf", "application/rtf"}, + {".sbml", "application/sbml+xml"}, + {".scq", "application/scvp-cv-request"}, + {".scs", "application/scvp-cv-response"}, + {".spq", "application/scvp-vp-request"}, + {".spp", "application/scvp-vp-response"}, + {".sdp", "application/sdp"}, + {".setpay", "application/set-payment-initiation"}, + {".setreg", "application/set-registration-initiation"}, + {".shf", "application/shf+xml"}, + {".smi", "application/smil+xml"}, + {".smil", "application/smil+xml"}, + {".rq", "application/sparql-query"}, + {".srx", "application/sparql-results+xml"}, + {".gram", "application/srgs"}, + {".grxml", "application/srgs+xml"}, + {".sru", "application/sru+xml"}, + {".ssdl", "application/ssdl+xml"}, + {".ssml", "application/ssml+xml"}, + {".tei", "application/tei+xml"}, + {".teicorpus", "application/tei+xml"}, + {".tfi", "application/thraud+xml"}, + {".tsd", "application/timestamped-data"}, + {".plb", "application/vnd.3gpp.pic-bw-large"}, + {".psb", "application/vnd.3gpp.pic-bw-small"}, + {".pvb", "application/vnd.3gpp.pic-bw-var"}, + {".tcap", "application/vnd.3gpp2.tcap"}, + {".pwn", "application/vnd.3m.post-it-notes"}, + {".aso", "application/vnd.accpac.simply.aso"}, + {".imp", "application/vnd.accpac.simply.imp"}, + {".acu", "application/vnd.acucobol"}, + {".atc", "application/vnd.acucorp"}, + {".acutc", "application/vnd.acucorp"}, + {".air", "application/vnd.adobe.air-application-installer-package+zip"}, + {".fcdt", "application/vnd.adobe.formscentral.fcdt"}, + {".fxp", "application/vnd.adobe.fxp"}, + {".fxpl", "application/vnd.adobe.fxp"}, + {".xdp", "application/vnd.adobe.xdp+xml"}, + {".xfdf", "application/vnd.adobe.xfdf"}, + {".ahead", "application/vnd.ahead.space"}, + {".azf", "application/vnd.airzip.filesecure.azf"}, + {".azs", "application/vnd.airzip.filesecure.azs"}, + {".azw", "application/vnd.amazon.ebook"}, + {".acc", "application/vnd.americandynamics.acc"}, + {".ami", "application/vnd.amiga.ami"}, + {".apk", "application/vnd.android.package-archive"}, + {".cii", "application/vnd.anser-web-certificate-issue-initiation"}, + {".fti", "application/vnd.anser-web-funds-transfer-initiation"}, + {".atx", "application/vnd.antix.game-component"}, + {".mpkg", "application/vnd.apple.installer+xml"}, + {".m3u8", "application/vnd.apple.mpegurl"}, + {".swi", "application/vnd.aristanetworks.swi"}, + {".iota", "application/vnd.astraea-software.iota"}, + {".aep", "application/vnd.audiograph"}, + {".mpm", "application/vnd.blueice.multipass"}, + {".bmi", "application/vnd.bmi"}, + {".rep", "application/vnd.businessobjects"}, + {".cdxml", "application/vnd.chemdraw+xml"}, + {".mmd", "application/vnd.chipnuts.karaoke-mmd"}, + {".cdy", "application/vnd.cinderella"}, + {".cla", "application/vnd.claymore"}, + {".rp9", "application/vnd.cloanto.rp9"}, + {".c4g", "application/vnd.clonk.c4group"}, + {".c4d", "application/vnd.clonk.c4group"}, + {".c4f", "application/vnd.clonk.c4group"}, + {".c4p", "application/vnd.clonk.c4group"}, + {".c4u", "application/vnd.clonk.c4group"}, + {".c11amc", "application/vnd.cluetrust.cartomobile-config"}, + {".c11amz", "application/vnd.cluetrust.cartomobile-config-pkg"}, + {".csp", "application/vnd.commonspace"}, + {".cdbcmsg", "application/vnd.contact.cmsg"}, + {".cmc", "application/vnd.cosmocaller"}, + {".clkx", "application/vnd.crick.clicker"}, + {".clkk", "application/vnd.crick.clicker.keyboard"}, + {".clkp", "application/vnd.crick.clicker.palette"}, + {".clkt", "application/vnd.crick.clicker.template"}, + {".clkw", "application/vnd.crick.clicker.wordbank"}, + {".wbs", "application/vnd.criticaltools.wbs+xml"}, + {".pml", "application/vnd.ctc-posml"}, + {".ppd", "application/vnd.cups-ppd"}, + {".car", "application/vnd.curl.car"}, + {".pcurl", "application/vnd.curl.pcurl"}, + {".dart", "application/vnd.dart"}, + {".rdz", "application/vnd.data-vision.rdz"}, + {".uvf", "application/vnd.dece.data"}, + {".uvvf", "application/vnd.dece.data"}, + {".uvd", "application/vnd.dece.data"}, + {".uvvd", "application/vnd.dece.data"}, + {".uvt", "application/vnd.dece.ttml+xml"}, + {".uvvt", "application/vnd.dece.ttml+xml"}, + {".uvx", "application/vnd.dece.unspecified"}, + {".uvvx", "application/vnd.dece.unspecified"}, + {".uvz", "application/vnd.dece.zip"}, + {".uvvz", "application/vnd.dece.zip"}, + {".fe_launch", "application/vnd.denovo.fcselayout-link"}, + {".dna", "application/vnd.dna"}, + {".mlp", "application/vnd.dolby.mlp"}, + {".dpg", "application/vnd.dpgraph"}, + {".dfac", "application/vnd.dreamfactory"}, + {".kpxx", "application/vnd.ds-keypoint"}, + {".ait", "application/vnd.dvb.ait"}, + {".svc", "application/vnd.dvb.service"}, + {".geo", "application/vnd.dynageo"}, + {".mag", "application/vnd.ecowin.chart"}, + {".nml", "application/vnd.enliven"}, + {".esf", "application/vnd.epson.esf"}, + {".msf", "application/vnd.epson.msf"}, + {".qam", "application/vnd.epson.quickanime"}, + {".slt", "application/vnd.epson.salt"}, + {".ssf", "application/vnd.epson.ssf"}, + {".es3", "application/vnd.eszigno3+xml"}, + {".et3", "application/vnd.eszigno3+xml"}, + {".ez2", "application/vnd.ezpix-album"}, + {".ez3", "application/vnd.ezpix-package"}, + {".fdf", "application/vnd.fdf"}, + {".mseed", "application/vnd.fdsn.mseed"}, + {".seed", "application/vnd.fdsn.seed"}, + {".dataless", "application/vnd.fdsn.seed"}, + {".gph", "application/vnd.flographit"}, + {".ftc", "application/vnd.fluxtime.clip"}, + {".fm", "application/vnd.framemaker"}, + {".frame", "application/vnd.framemaker"}, + {".maker", "application/vnd.framemaker"}, + {".book", "application/vnd.framemaker"}, + {".fnc", "application/vnd.frogans.fnc"}, + {".ltf", "application/vnd.frogans.ltf"}, + {".fsc", "application/vnd.fsc.weblaunch"}, + {".oas", "application/vnd.fujitsu.oasys"}, + {".oa2", "application/vnd.fujitsu.oasys2"}, + {".oa3", "application/vnd.fujitsu.oasys3"}, + {".fg5", "application/vnd.fujitsu.oasysgp"}, + {".bh2", "application/vnd.fujitsu.oasysprs"}, + {".ddd", "application/vnd.fujixerox.ddd"}, + {".xdw", "application/vnd.fujixerox.docuworks"}, + {".xbd", "application/vnd.fujixerox.docuworks.binder"}, + {".fzs", "application/vnd.fuzzysheet"}, + {".txd", "application/vnd.genomatix.tuxedo"}, + {".ggb", "application/vnd.geogebra.file"}, + {".ggt", "application/vnd.geogebra.tool"}, + {".gex", "application/vnd.geometry-explorer"}, + {".gre", "application/vnd.geometry-explorer"}, + {".gxt", "application/vnd.geonext"}, + {".g2w", "application/vnd.geoplan"}, + {".g3w", "application/vnd.geospace"}, + {".gmx", "application/vnd.gmx"}, + {".kml", "application/vnd.google-earth.kml+xml"}, + {".kmz", "application/vnd.google-earth.kmz"}, + {".gqf", "application/vnd.grafeq"}, + {".gqs", "application/vnd.grafeq"}, + {".gac", "application/vnd.groove-account"}, + {".ghf", "application/vnd.groove-help"}, + {".gim", "application/vnd.groove-identity-message"}, + {".grv", "application/vnd.groove-injector"}, + {".gtm", "application/vnd.groove-tool-message"}, + {".tpl", "application/vnd.groove-tool-template"}, + {".vcg", "application/vnd.groove-vcard"}, + {".hal", "application/vnd.hal+xml"}, + {".zmm", "application/vnd.handheld-entertainment+xml"}, + {".hbci", "application/vnd.hbci"}, + {".les", "application/vnd.hhe.lesson-player"}, + {".hpgl", "application/vnd.hp-hpgl"}, + {".hpid", "application/vnd.hp-hpid"}, + {".hps", "application/vnd.hp-hps"}, + {".jlt", "application/vnd.hp-jlyt"}, + {".pcl", "application/vnd.hp-pcl"}, + {".pclxl", "application/vnd.hp-pclxl"}, + {".sfd-hdstx", "application/vnd.hydrostatix.sof-data"}, + {".mpy", "application/vnd.ibm.minipay"}, + {".afp", "application/vnd.ibm.modcap"}, + {".listafp", "application/vnd.ibm.modcap"}, + {".list3820", "application/vnd.ibm.modcap"}, + {".irm", "application/vnd.ibm.rights-management"}, + {".sc", "application/vnd.ibm.secure-container"}, + {".icc", "application/vnd.iccprofile"}, + {".icm", "application/vnd.iccprofile"}, + {".igl", "application/vnd.igloader"}, + {".ivp", "application/vnd.immervision-ivp"}, + {".ivu", "application/vnd.immervision-ivu"}, + {".igm", "application/vnd.insors.igm"}, + {".xpw", "application/vnd.intercon.formnet"}, + {".xpx", "application/vnd.intercon.formnet"}, + {".i2g", "application/vnd.intergeo"}, + {".qbo", "application/vnd.intu.qbo"}, + {".qfx", "application/vnd.intu.qfx"}, + {".rcprofile", "application/vnd.ipunplugged.rcprofile"}, + {".irp", "application/vnd.irepository.package+xml"}, + {".xpr", "application/vnd.is-xpr"}, + {".fcs", "application/vnd.isac.fcs"}, + {".jam", "application/vnd.jam"}, + {".rms", "application/vnd.jcp.javame.midlet-rms"}, + {".jisp", "application/vnd.jisp"}, + {".joda", "application/vnd.joost.joda-archive"}, + {".ktz", "application/vnd.kahootz"}, + {".ktr", "application/vnd.kahootz"}, + {".karbon", "application/vnd.kde.karbon"}, + {".chrt", "application/vnd.kde.kchart"}, + {".kfo", "application/vnd.kde.kformula"}, + {".flw", "application/vnd.kde.kivio"}, + {".kon", "application/vnd.kde.kontour"}, + {".kpr", "application/vnd.kde.kpresenter"}, + {".kpt", "application/vnd.kde.kpresenter"}, + {".ksp", "application/vnd.kde.kspread"}, + {".kwd", "application/vnd.kde.kword"}, + {".kwt", "application/vnd.kde.kword"}, + {".htke", "application/vnd.kenameaapp"}, + {".kia", "application/vnd.kidspiration"}, + {".kne", "application/vnd.kinar"}, + {".knp", "application/vnd.kinar"}, + {".skp", "application/vnd.koan"}, + {".skd", "application/vnd.koan"}, + {".skt", "application/vnd.koan"}, + {".skm", "application/vnd.koan"}, + {".sse", "application/vnd.kodak-descriptor"}, + {".lasxml", "application/vnd.las.las+xml"}, + {".lbd", "application/vnd.llamagraphics.life-balance.desktop"}, + {".lbe", "application/vnd.llamagraphics.life-balance.exchange+xml"}, + {".123", "application/vnd.lotus-1-2-3"}, + {".apr", "application/vnd.lotus-approach"}, + {".pre", "application/vnd.lotus-freelance"}, + {".nsf", "application/vnd.lotus-notes"}, + {".org", "application/vnd.lotus-organizer"}, + {".scm", "application/vnd.lotus-screencam"}, + {".lwp", "application/vnd.lotus-wordpro"}, + {".portpkg", "application/vnd.macports.portpkg"}, + {".mcd", "application/vnd.mcd"}, + {".mc1", "application/vnd.medcalcdata"}, + {".cdkey", "application/vnd.mediastation.cdkey"}, + {".mwf", "application/vnd.mfer"}, + {".mfm", "application/vnd.mfmp"}, + {".flo", "application/vnd.micrografx.flo"}, + {".igx", "application/vnd.micrografx.igx"}, + {".mif", "application/vnd.mif"}, + {".daf", "application/vnd.mobius.daf"}, + {".dis", "application/vnd.mobius.dis"}, + {".mbk", "application/vnd.mobius.mbk"}, + {".mqy", "application/vnd.mobius.mqy"}, + {".msl", "application/vnd.mobius.msl"}, + {".plc", "application/vnd.mobius.plc"}, + {".txf", "application/vnd.mobius.txf"}, + {".mpn", "application/vnd.mophun.application"}, + {".mpc", "application/vnd.mophun.certificate"}, + {".xul", "application/vnd.mozilla.xul+xml"}, + {".cil", "application/vnd.ms-artgalry"}, + {".cab", "application/vnd.ms-cab-compressed"}, + {".xls", "application/vnd.ms-excel"}, + {".xlm", "application/vnd.ms-excel"}, + {".xla", "application/vnd.ms-excel"}, + {".xlc", "application/vnd.ms-excel"}, + {".xlt", "application/vnd.ms-excel"}, + {".xlw", "application/vnd.ms-excel"}, + {".xlam", "application/vnd.ms-excel.addin.macroenabled.12"}, + {".xlsb", "application/vnd.ms-excel.sheet.binary.macroenabled.12"}, + {".xlsm", "application/vnd.ms-excel.sheet.macroenabled.12"}, + {".xltm", "application/vnd.ms-excel.template.macroenabled.12"}, + {".eot", "application/vnd.ms-fontobject"}, + {".chm", "application/vnd.ms-htmlhelp"}, + {".ims", "application/vnd.ms-ims"}, + {".lrm", "application/vnd.ms-lrm"}, + {".thmx", "application/vnd.ms-officetheme"}, + {".cat", "application/vnd.ms-pki.seccat"}, + {".stl", "application/vnd.ms-pki.stl"}, + {".ppt", "application/vnd.ms-powerpoint"}, + {".pps", "application/vnd.ms-powerpoint"}, + {".pot", "application/vnd.ms-powerpoint"}, + {".ppam", "application/vnd.ms-powerpoint.addin.macroenabled.12"}, + {".pptm", "application/vnd.ms-powerpoint.presentation.macroenabled.12"}, + {".sldm", "application/vnd.ms-powerpoint.slide.macroenabled.12"}, + {".ppsm", "application/vnd.ms-powerpoint.slideshow.macroenabled.12"}, + {".potm", "application/vnd.ms-powerpoint.template.macroenabled.12"}, + {".mpp", "application/vnd.ms-project"}, + {".mpt", "application/vnd.ms-project"}, + {".docm", "application/vnd.ms-word.document.macroenabled.12"}, + {".dotm", "application/vnd.ms-word.template.macroenabled.12"}, + {".wps", "application/vnd.ms-works"}, + {".wks", "application/vnd.ms-works"}, + {".wcm", "application/vnd.ms-works"}, + {".wdb", "application/vnd.ms-works"}, + {".wpl", "application/vnd.ms-wpl"}, + {".xps", "application/vnd.ms-xpsdocument"}, + {".mseq", "application/vnd.mseq"}, + {".mus", "application/vnd.musician"}, + {".msty", "application/vnd.muvee.style"}, + {".taglet", "application/vnd.mynfc"}, + {".nlu", "application/vnd.neurolanguage.nlu"}, + {".ntf", "application/vnd.nitf"}, + {".nitf", "application/vnd.nitf"}, + {".nnd", "application/vnd.noblenet-directory"}, + {".nns", "application/vnd.noblenet-sealer"}, + {".nnw", "application/vnd.noblenet-web"}, + {".ngdat", "application/vnd.nokia.n-gage.data"}, + {".n-gage", "application/vnd.nokia.n-gage.symbian.install"}, + {".rpst", "application/vnd.nokia.radio-preset"}, + {".rpss", "application/vnd.nokia.radio-presets"}, + {".edm", "application/vnd.novadigm.edm"}, + {".edx", "application/vnd.novadigm.edx"}, + {".ext", "application/vnd.novadigm.ext"}, + {".odc", "application/vnd.oasis.opendocument.chart"}, + {".otc", "application/vnd.oasis.opendocument.chart-template"}, + {".odb", "application/vnd.oasis.opendocument.database"}, + {".odf", "application/vnd.oasis.opendocument.formula"}, + {".odft", "application/vnd.oasis.opendocument.formula-template"}, + {".odg", "application/vnd.oasis.opendocument.graphics"}, + {".otg", "application/vnd.oasis.opendocument.graphics-template"}, + {".odi", "application/vnd.oasis.opendocument.image"}, + {".oti", "application/vnd.oasis.opendocument.image-template"}, + {".odp", "application/vnd.oasis.opendocument.presentation"}, + {".otp", "application/vnd.oasis.opendocument.presentation-template"}, + {".ods", "application/vnd.oasis.opendocument.spreadsheet"}, + {".ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, + {".odt", "application/vnd.oasis.opendocument.text"}, + {".odm", "application/vnd.oasis.opendocument.text-master"}, + {".ott", "application/vnd.oasis.opendocument.text-template"}, + {".oth", "application/vnd.oasis.opendocument.text-web"}, + {".xo", "application/vnd.olpc-sugar"}, + {".dd2", "application/vnd.oma.dd2+xml"}, + {".oxt", "application/vnd.openofficeorg.extension"}, + {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + {".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"}, + {".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"}, + {".potx", "application/vnd.openxmlformats-officedocument.presentationml.template"}, + {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, + {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"}, + {".mgp", "application/vnd.osgeo.mapguide.package"}, + {".dp", "application/vnd.osgi.dp"}, + {".esa", "application/vnd.osgi.subsystem"}, + {".pdb", "application/vnd.palm"}, + {".pqa", "application/vnd.palm"}, + {".oprc", "application/vnd.palm"}, + {".paw", "application/vnd.pawaafile"}, + {".str", "application/vnd.pg.format"}, + {".ei6", "application/vnd.pg.osasli"}, + {".efif", "application/vnd.picsel"}, + {".wg", "application/vnd.pmi.widget"}, + {".plf", "application/vnd.pocketlearn"}, + {".pbd", "application/vnd.powerbuilder6"}, + {".box", "application/vnd.previewsystems.box"}, + {".mgz", "application/vnd.proteus.magazine"}, + {".qps", "application/vnd.publishare-delta-tree"}, + {".ptid", "application/vnd.pvi.ptid1"}, + {".qxd", "application/vnd.quark.quarkxpress"}, + {".qxt", "application/vnd.quark.quarkxpress"}, + {".qwd", "application/vnd.quark.quarkxpress"}, + {".qwt", "application/vnd.quark.quarkxpress"}, + {".qxl", "application/vnd.quark.quarkxpress"}, + {".qxb", "application/vnd.quark.quarkxpress"}, + {".bed", "application/vnd.realvnc.bed"}, + {".mxl", "application/vnd.recordare.musicxml"}, + {".musicxml", "application/vnd.recordare.musicxml+xml"}, + {".cryptonote", "application/vnd.rig.cryptonote"}, + {".cod", "application/vnd.rim.cod"}, + {".rm", "application/vnd.rn-realmedia"}, + {".rmvb", "application/vnd.rn-realmedia-vbr"}, + {".link66", "application/vnd.route66.link66+xml"}, + {".st", "application/vnd.sailingtracker.track"}, + {".see", "application/vnd.seemail"}, + {".sema", "application/vnd.sema"}, + {".semd", "application/vnd.semd"}, + {".semf", "application/vnd.semf"}, + {".ifm", "application/vnd.shana.informed.formdata"}, + {".itp", "application/vnd.shana.informed.formtemplate"}, + {".iif", "application/vnd.shana.informed.interchange"}, + {".ipk", "application/vnd.shana.informed.package"}, + {".twd", "application/vnd.simtech-mindmapper"}, + {".twds", "application/vnd.simtech-mindmapper"}, + {".mmf", "application/vnd.smaf"}, + {".teacher", "application/vnd.smart.teacher"}, + {".sdkm", "application/vnd.solent.sdkm+xml"}, + {".sdkd", "application/vnd.solent.sdkm+xml"}, + {".dxp", "application/vnd.spotfire.dxp"}, + {".sfs", "application/vnd.spotfire.sfs"}, + {".sdc", "application/vnd.stardivision.calc"}, + {".sda", "application/vnd.stardivision.draw"}, + {".sdd", "application/vnd.stardivision.impress"}, + {".smf", "application/vnd.stardivision.math"}, + {".sdw", "application/vnd.stardivision.writer"}, + {".vor", "application/vnd.stardivision.writer"}, + {".sgl", "application/vnd.stardivision.writer-global"}, + {".smzip", "application/vnd.stepmania.package"}, + {".sm", "application/vnd.stepmania.stepchart"}, + {".sxc", "application/vnd.sun.xml.calc"}, + {".stc", "application/vnd.sun.xml.calc.template"}, + {".sxd", "application/vnd.sun.xml.draw"}, + {".std", "application/vnd.sun.xml.draw.template"}, + {".sxi", "application/vnd.sun.xml.impress"}, + {".sti", "application/vnd.sun.xml.impress.template"}, + {".sxm", "application/vnd.sun.xml.math"}, + {".sxw", "application/vnd.sun.xml.writer"}, + {".sxg", "application/vnd.sun.xml.writer.global"}, + {".stw", "application/vnd.sun.xml.writer.template"}, + {".sus", "application/vnd.sus-calendar"}, + {".susp", "application/vnd.sus-calendar"}, + {".svd", "application/vnd.svd"}, + {".sis", "application/vnd.symbian.install"}, + {".sisx", "application/vnd.symbian.install"}, + {".xsm", "application/vnd.syncml+xml"}, + {".bdm", "application/vnd.syncml.dm+wbxml"}, + {".xdm", "application/vnd.syncml.dm+xml"}, + {".tao", "application/vnd.tao.intent-module-archive"}, + {".pcap", "application/vnd.tcpdump.pcap"}, + {".cap", "application/vnd.tcpdump.pcap"}, + {".dmp", "application/vnd.tcpdump.pcap"}, + {".tmo", "application/vnd.tmobile-livetv"}, + {".tpt", "application/vnd.trid.tpt"}, + {".mxs", "application/vnd.triscape.mxs"}, + {".tra", "application/vnd.trueapp"}, + {".ufd", "application/vnd.ufdl"}, + {".ufdl", "application/vnd.ufdl"}, + {".utz", "application/vnd.uiq.theme"}, + {".umj", "application/vnd.umajin"}, + {".unityweb", "application/vnd.unity"}, + {".uoml", "application/vnd.uoml+xml"}, + {".vcx", "application/vnd.vcx"}, + {".vsd", "application/vnd.visio"}, + {".vst", "application/vnd.visio"}, + {".vss", "application/vnd.visio"}, + {".vsw", "application/vnd.visio"}, + {".vis", "application/vnd.visionary"}, + {".vsf", "application/vnd.vsf"}, + {".wbxml", "application/vnd.wap.wbxml"}, + {".wmlc", "application/vnd.wap.wmlc"}, + {".wmlsc", "application/vnd.wap.wmlscriptc"}, + {".wtb", "application/vnd.webturbo"}, + {".nbp", "application/vnd.wolfram.player"}, + {".wpd", "application/vnd.wordperfect"}, + {".wqd", "application/vnd.wqd"}, + {".stf", "application/vnd.wt.stf"}, + {".xar", "application/vnd.xara"}, + {".xfdl", "application/vnd.xfdl"}, + {".hvd", "application/vnd.yamaha.hv-dic"}, + {".hvs", "application/vnd.yamaha.hv-script"}, + {".hvp", "application/vnd.yamaha.hv-voice"}, + {".osf", "application/vnd.yamaha.openscoreformat"}, + {".osfpvg", "application/vnd.yamaha.openscoreformat.osfpvg+xml"}, + {".saf", "application/vnd.yamaha.smaf-audio"}, + {".spf", "application/vnd.yamaha.smaf-phrase"}, + {".cmp", "application/vnd.yellowriver-custom-menu"}, + {".zir", "application/vnd.zul"}, + {".zirz", "application/vnd.zul"}, + {".zaz", "application/vnd.zzazz.deck+xml"}, + {".vxml", "application/voicexml+xml"}, + {".wgt", "application/widget"}, + {".hlp", "application/winhlp"}, + {".wsdl", "application/wsdl+xml"}, + {".wspolicy", "application/wspolicy+xml"}, + {".7z", "application/x-7z-compressed"}, + {".abw", "application/x-abiword"}, + {".ace", "application/x-ace-compressed"}, + {".dmg", "application/x-apple-diskimage"}, + {".aab", "application/x-authorware-bin"}, + {".x32", "application/x-authorware-bin"}, + {".u32", "application/x-authorware-bin"}, + {".vox", "application/x-authorware-bin"}, + {".aam", "application/x-authorware-map"}, + {".aas", "application/x-authorware-seg"}, + {".bcpio", "application/x-bcpio"}, + {".torrent", "application/x-bittorrent"}, + {".blb", "application/x-blorb"}, + {".blorb", "application/x-blorb"}, + {".bz", "application/x-bzip"}, + {".bz2", "application/x-bzip2"}, + {".boz", "application/x-bzip2"}, + {".cbr", "application/x-cbr"}, + {".cba", "application/x-cbr"}, + {".cbt", "application/x-cbr"}, + {".cbz", "application/x-cbr"}, + {".cb7", "application/x-cbr"}, + {".vcd", "application/x-cdlink"}, + {".cfs", "application/x-cfs-compressed"}, + {".chat", "application/x-chat"}, + {".pgn", "application/x-chess-pgn"}, + {".nsc", "application/x-conference"}, + {".cpio", "application/x-cpio"}, + {".csh", "application/x-csh"}, + {".deb", "application/x-debian-package"}, + {".udeb", "application/x-debian-package"}, + {".dgc", "application/x-dgc-compressed"}, + {".dir", "application/x-director"}, + {".dcr", "application/x-director"}, + {".dxr", "application/x-director"}, + {".cst", "application/x-director"}, + {".cct", "application/x-director"}, + {".cxt", "application/x-director"}, + {".w3d", "application/x-director"}, + {".fgd", "application/x-director"}, + {".swa", "application/x-director"}, + {".wad", "application/x-doom"}, + {".ncx", "application/x-dtbncx+xml"}, + {".dtb", "application/x-dtbook+xml"}, + {".res", "application/x-dtbresource+xml"}, + {".dvi", "application/x-dvi"}, + {".evy", "application/x-envoy"}, + {".eva", "application/x-eva"}, + {".bdf", "application/x-font-bdf"}, + {".gsf", "application/x-font-ghostscript"}, + {".psf", "application/x-font-linux-psf"}, + {".otf", "application/x-font-otf"}, + {".pcf", "application/x-font-pcf"}, + {".snf", "application/x-font-snf"}, + {".ttf", "application/x-font-ttf"}, + {".ttc", "application/x-font-ttf"}, + {".pfa", "application/x-font-type1"}, + {".pfb", "application/x-font-type1"}, + {".pfm", "application/x-font-type1"}, + {".afm", "application/x-font-type1"}, + {".woff", "application/font-woff"}, + {".arc", "application/x-freearc"}, + {".spl", "application/x-futuresplash"}, + {".gca", "application/x-gca-compressed"}, + {".ulx", "application/x-glulx"}, + {".gnumeric", "application/x-gnumeric"}, + {".gramps", "application/x-gramps-xml"}, + {".gtar", "application/x-gtar"}, + {".hdf", "application/x-hdf"}, + {".install", "application/x-install-instructions"}, + {".iso", "application/x-iso9660-image"}, + {".jnlp", "application/x-java-jnlp-file"}, + {".latex", "application/x-latex"}, + {".lzh", "application/x-lzh-compressed"}, + {".lha", "application/x-lzh-compressed"}, + {".mie", "application/x-mie"}, + {".prc", "application/x-mobipocket-ebook"}, + {".mobi", "application/x-mobipocket-ebook"}, + {".application", "application/x-ms-application"}, + {".lnk", "application/x-ms-shortcut"}, + {".wmd", "application/x-ms-wmd"}, + {".wmz", "application/x-ms-wmz"}, + {".xbap", "application/x-ms-xbap"}, + {".mdb", "application/x-msaccess"}, + {".obd", "application/x-msbinder"}, + {".crd", "application/x-mscardfile"}, + {".clp", "application/x-msclip"}, + {".exe", "application/x-msdownload"}, + {".dll", "application/x-msdownload"}, + {".com", "application/x-msdownload"}, + {".bat", "application/x-msdownload"}, + {".msi", "application/x-msdownload"}, + {".mvb", "application/x-msmediaview"}, + {".m13", "application/x-msmediaview"}, + {".m14", "application/x-msmediaview"}, + {".wmf", "application/x-msmetafile"}, + {".emf", "application/x-msmetafile"}, + {".emz", "application/x-msmetafile"}, + {".mny", "application/x-msmoney"}, + {".pub", "application/x-mspublisher"}, + {".scd", "application/x-msschedule"}, + {".trm", "application/x-msterminal"}, + {".wri", "application/x-mswrite"}, + {".nc", "application/x-netcdf"}, + {".cdf", "application/x-netcdf"}, + {".nzb", "application/x-nzb"}, + {".p12", "application/x-pkcs12"}, + {".pfx", "application/x-pkcs12"}, + {".p7b", "application/x-pkcs7-certificates"}, + {".spc", "application/x-pkcs7-certificates"}, + {".p7r", "application/x-pkcs7-certreqresp"}, + {".rar", "application/x-rar-compressed"}, + {".ris", "application/x-research-info-systems"}, + {".sh", "application/x-sh"}, + {".shar", "application/x-shar"}, + {".swf", "application/x-shockwave-flash"}, + {".xap", "application/x-silverlight-app"}, + {".sql", "application/x-sql"}, + {".sit", "application/x-stuffit"}, + {".sitx", "application/x-stuffitx"}, + {".srt", "application/x-subrip"}, + {".sv4cpio", "application/x-sv4cpio"}, + {".sv4crc", "application/x-sv4crc"}, + {".t3", "application/x-t3vm-image"}, + {".gam", "application/x-tads"}, + {".tar", "application/x-tar"}, + {".tcl", "application/x-tcl"}, + {".tex", "application/x-tex"}, + {".tfm", "application/x-tex-tfm"}, + {".texinfo", "application/x-texinfo"}, + {".texi", "application/x-texinfo"}, + {".obj", "application/x-tgif"}, + {".ustar", "application/x-ustar"}, + {".src", "application/x-wais-source"}, + {".der", "application/x-x509-ca-cert"}, + {".crt", "application/x-x509-ca-cert"}, + {".fig", "application/x-xfig"}, + {".xlf", "application/x-xliff+xml"}, + {".xpi", "application/x-xpinstall"}, + {".xz", "application/x-xz"}, + {".z1", "application/x-zmachine"}, + {".z2", "application/x-zmachine"}, + {".z3", "application/x-zmachine"}, + {".z4", "application/x-zmachine"}, + {".z5", "application/x-zmachine"}, + {".z6", "application/x-zmachine"}, + {".z7", "application/x-zmachine"}, + {".z8", "application/x-zmachine"}, + {".xaml", "application/xaml+xml"}, + {".xdf", "application/xcap-diff+xml"}, + {".xenc", "application/xenc+xml"}, + {".xhtml", "application/xhtml+xml"}, + {".xht", "application/xhtml+xml"}, + {".xml", "application/xml"}, + {".xsl", "application/xml"}, + {".dtd", "application/xml-dtd"}, + {".xop", "application/xop+xml"}, + {".xpl", "application/xproc+xml"}, + {".xslt", "application/xslt+xml"}, + {".xspf", "application/xspf+xml"}, + {".mxml", "application/xv+xml"}, + {".xhvml", "application/xv+xml"}, + {".xvml", "application/xv+xml"}, + {".xvm", "application/xv+xml"}, + {".yang", "application/yang"}, + {".yin", "application/yin+xml"}, + {".zip", "application/zip"}, + {".adp", "audio/adpcm"}, + {".au", "audio/basic"}, + {".snd", "audio/basic"}, + {".mid", "audio/midi"}, + {".midi", "audio/midi"}, + {".kar", "audio/midi"}, + {".rmi", "audio/midi"}, + {".mp4a", "audio/mp4"}, + {".mpga", "audio/mpeg"}, + {".mp2", "audio/mpeg"}, + {".mp2a", "audio/mpeg"}, + {".mp3", "audio/mpeg"}, + {".m2a", "audio/mpeg"}, + {".m3a", "audio/mpeg"}, + {".oga", "audio/ogg"}, + {".ogg", "audio/ogg"}, + {".spx", "audio/ogg"}, + {".s3m", "audio/s3m"}, + {".sil", "audio/silk"}, + {".uva", "audio/vnd.dece.audio"}, + {".uvva", "audio/vnd.dece.audio"}, + {".eol", "audio/vnd.digital-winds"}, + {".dra", "audio/vnd.dra"}, + {".dts", "audio/vnd.dts"}, + {".dtshd", "audio/vnd.dts.hd"}, + {".lvp", "audio/vnd.lucent.voice"}, + {".pya", "audio/vnd.ms-playready.media.pya"}, + {".ecelp4800", "audio/vnd.nuera.ecelp4800"}, + {".ecelp7470", "audio/vnd.nuera.ecelp7470"}, + {".ecelp9600", "audio/vnd.nuera.ecelp9600"}, + {".rip", "audio/vnd.rip"}, + {".weba", "audio/webm"}, + {".aac", "audio/x-aac"}, + {".aif", "audio/x-aiff"}, + {".aiff", "audio/x-aiff"}, + {".aifc", "audio/x-aiff"}, + {".caf", "audio/x-caf"}, + {".flac", "audio/x-flac"}, + {".mka", "audio/x-matroska"}, + {".m3u", "audio/x-mpegurl"}, + {".wax", "audio/x-ms-wax"}, + {".wma", "audio/x-ms-wma"}, + {".ram", "audio/x-pn-realaudio"}, + {".ra", "audio/x-pn-realaudio"}, + {".rmp", "audio/x-pn-realaudio-plugin"}, + {".wav", "audio/x-wav"}, + {".xm", "audio/xm"}, + {".cdx", "chemical/x-cdx"}, + {".cif", "chemical/x-cif"}, + {".cmdf", "chemical/x-cmdf"}, + {".cml", "chemical/x-cml"}, + {".csml", "chemical/x-csml"}, + {".xyz", "chemical/x-xyz"}, + {".bmp", "image/bmp"}, + {".cgm", "image/cgm"}, + {".g3", "image/g3fax"}, + {".gif", "image/gif"}, + {".ief", "image/ief"}, + {".jpeg", "image/jpeg"}, + {".jpg", "image/jpeg"}, + {".jpe", "image/jpeg"}, + {".ktx", "image/ktx"}, + {".png", "image/png"}, + {".btif", "image/prs.btif"}, + {".sgi", "image/sgi"}, + {".svg", "image/svg+xml"}, + {".svgz", "image/svg+xml"}, + {".tiff", "image/tiff"}, + {".tif", "image/tiff"}, + {".psd", "image/vnd.adobe.photoshop"}, + {".uvi", "image/vnd.dece.graphic"}, + {".uvvi", "image/vnd.dece.graphic"}, + {".uvg", "image/vnd.dece.graphic"}, + {".uvvg", "image/vnd.dece.graphic"}, + {".sub", "text/vnd.dvb.subtitle"}, + {".djvu", "image/vnd.djvu"}, + {".djv", "image/vnd.djvu"}, + {".dwg", "image/vnd.dwg"}, + {".dxf", "image/vnd.dxf"}, + {".fbs", "image/vnd.fastbidsheet"}, + {".fpx", "image/vnd.fpx"}, + {".fst", "image/vnd.fst"}, + {".mmr", "image/vnd.fujixerox.edmics-mmr"}, + {".rlc", "image/vnd.fujixerox.edmics-rlc"}, + {".mdi", "image/vnd.ms-modi"}, + {".wdp", "image/vnd.ms-photo"}, + {".npx", "image/vnd.net-fpx"}, + {".wbmp", "image/vnd.wap.wbmp"}, + {".xif", "image/vnd.xiff"}, + {".webp", "image/webp"}, + {".3ds", "image/x-3ds"}, + {".ras", "image/x-cmu-raster"}, + {".cmx", "image/x-cmx"}, + {".fh", "image/x-freehand"}, + {".fhc", "image/x-freehand"}, + {".fh4", "image/x-freehand"}, + {".fh5", "image/x-freehand"}, + {".fh7", "image/x-freehand"}, + {".ico", "image/x-icon"}, + {".sid", "image/x-mrsid-image"}, + {".pcx", "image/x-pcx"}, + {".pic", "image/x-pict"}, + {".pct", "image/x-pict"}, + {".pnm", "image/x-portable-anymap"}, + {".pbm", "image/x-portable-bitmap"}, + {".pgm", "image/x-portable-graymap"}, + {".ppm", "image/x-portable-pixmap"}, + {".rgb", "image/x-rgb"}, + {".tga", "image/x-tga"}, + {".xbm", "image/x-xbitmap"}, + {".xpm", "image/x-xpixmap"}, + {".xwd", "image/x-xwindowdump"}, + {".eml", "message/rfc822"}, + {".mime", "message/rfc822"}, + {".igs", "model/iges"}, + {".iges", "model/iges"}, + {".msh", "model/mesh"}, + {".mesh", "model/mesh"}, + {".silo", "model/mesh"}, + {".dae", "model/vnd.collada+xml"}, + {".dwf", "model/vnd.dwf"}, + {".gdl", "model/vnd.gdl"}, + {".gtw", "model/vnd.gtw"}, + {".mts", "model/vnd.mts"}, + {".vtu", "model/vnd.vtu"}, + {".wrl", "model/vrml"}, + {".vrml", "model/vrml"}, + {".x3db", "model/x3d+binary"}, + {".x3dbz", "model/x3d+binary"}, + {".x3dv", "model/x3d+vrml"}, + {".x3dvz", "model/x3d+vrml"}, + {".x3d", "model/x3d+xml"}, + {".x3dz", "model/x3d+xml"}, + {".appcache", "text/cache-manifest"}, + {".ics", "text/calendar"}, + {".ifb", "text/calendar"}, + {".css", "text/css"}, + {".csv", "text/csv"}, + {".html", "text/html"}, + {".htm", "text/html"}, + {".n3", "text/n3"}, + {".txt", "text/plain"}, + {".text", "text/plain"}, + {".conf", "text/plain"}, + {".def", "text/plain"}, + {".list", "text/plain"}, + {".log", "text/plain"}, + {".in", "text/plain"}, + {".dsc", "text/prs.lines.tag"}, + {".rtx", "text/richtext"}, + {".sgml", "text/sgml"}, + {".sgm", "text/sgml"}, + {".tsv", "text/tab-separated-values"}, + {".t", "text/troff"}, + {".tr", "text/troff"}, + {".roff", "text/troff"}, + {".man", "text/troff"}, + {".me", "text/troff"}, + {".ms", "text/troff"}, + {".ttl", "text/turtle"}, + {".uri", "text/uri-list"}, + {".uris", "text/uri-list"}, + {".urls", "text/uri-list"}, + {".vcard", "text/vcard"}, + {".curl", "text/vnd.curl"}, + {".dcurl", "text/vnd.curl.dcurl"}, + {".scurl", "text/vnd.curl.scurl"}, + {".mcurl", "text/vnd.curl.mcurl"}, + {".fly", "text/vnd.fly"}, + {".flx", "text/vnd.fmi.flexstor"}, + {".gv", "text/vnd.graphviz"}, + {".3dml", "text/vnd.in3d.3dml"}, + {".spot", "text/vnd.in3d.spot"}, + {".jad", "text/vnd.sun.j2me.app-descriptor"}, + {".wml", "text/vnd.wap.wml"}, + {".wmls", "text/vnd.wap.wmlscript"}, + {".s", "text/x-asm"}, + {".asm", "text/x-asm"}, + {".c", "text/x-c"}, + {".cc", "text/x-c"}, + {".cxx", "text/x-c"}, + {".cpp", "text/x-c"}, + {".h", "text/x-c"}, + {".hh", "text/x-c"}, + {".dic", "text/x-c"}, + {".f", "text/x-fortran"}, + {".for", "text/x-fortran"}, + {".f77", "text/x-fortran"}, + {".f90", "text/x-fortran"}, + {".java", "text/x-java-source"}, + {".opml", "text/x-opml"}, + {".p", "text/x-pascal"}, + {".pas", "text/x-pascal"}, + {".nfo", "text/x-nfo"}, + {".etx", "text/x-setext"}, + {".sfv", "text/x-sfv"}, + {".uu", "text/x-uuencode"}, + {".vcs", "text/x-vcalendar"}, + {".vcf", "text/x-vcard"}, + {".3gp", "video/3gpp"}, + {".3g2", "video/3gpp2"}, + {".h261", "video/h261"}, + {".h263", "video/h263"}, + {".h264", "video/h264"}, + {".jpgv", "video/jpeg"}, + {".jpm", "video/jpm"}, + {".jpgm", "video/jpm"}, + {".mj2", "video/mj2"}, + {".mjp2", "video/mj2"}, + {".mp4", "video/mp4"}, + {".mp4v", "video/mp4"}, + {".mpg4", "video/mp4"}, + {".mpeg", "video/mpeg"}, + {".mpg", "video/mpeg"}, + {".mpe", "video/mpeg"}, + {".m1v", "video/mpeg"}, + {".m2v", "video/mpeg"}, + {".ogv", "video/ogg"}, + {".qt", "video/quicktime"}, + {".mov", "video/quicktime"}, + {".uvh", "video/vnd.dece.hd"}, + {".uvvh", "video/vnd.dece.hd"}, + {".uvm", "video/vnd.dece.mobile"}, + {".uvvm", "video/vnd.dece.mobile"}, + {".uvp", "video/vnd.dece.pd"}, + {".uvvp", "video/vnd.dece.pd"}, + {".uvs", "video/vnd.dece.sd"}, + {".uvvs", "video/vnd.dece.sd"}, + {".uvv", "video/vnd.dece.video"}, + {".uvvv", "video/vnd.dece.video"}, + {".dvb", "video/vnd.dvb.file"}, + {".fvt", "video/vnd.fvt"}, + {".mxu", "video/vnd.mpegurl"}, + {".m4u", "video/vnd.mpegurl"}, + {".pyv", "video/vnd.ms-playready.media.pyv"}, + {".uvu", "video/vnd.uvvu.mp4"}, + {".uvvu", "video/vnd.uvvu.mp4"}, + {".viv", "video/vnd.vivo"}, + {".webm", "video/webm"}, + {".f4v", "video/x-f4v"}, + {".fli", "video/x-fli"}, + {".flv", "video/x-flv"}, + {".m4v", "video/x-m4v"}, + {".mkv", "video/x-matroska"}, + {".mk3d", "video/x-matroska"}, + {".mks", "video/x-matroska"}, + {".mng", "video/x-mng"}, + {".asf", "video/x-ms-asf"}, + {".asx", "video/x-ms-asf"}, + {".vob", "video/x-ms-vob"}, + {".wm", "video/x-ms-wm"}, + {".wmv", "video/x-ms-wmv"}, + {".wmx", "video/x-ms-wmx"}, + {".wvx", "video/x-ms-wvx"}, + {".avi", "video/x-msvideo"}, + {".movie", "video/x-sgi-movie"}, + {".smv", "video/x-smv"}, + {".ice", "x-conference/x-cooltalk"}, + {".323", "text/h323"}, + {".3gp2", "video/3gpp2"}, + {".3gpp", "video/3gpp"}, + {".aa", "audio/audible"}, + {".aaf", "application/octet-stream"}, + {".aax", "audio/vnd.audible.aax"}, + {".ac3", "audio/ac3"}, + {".aca", "application/octet-stream"}, + {".accda", "application/msaccess.addin"}, + {".accdb", "application/msaccess"}, + {".accdc", "application/msaccess.cab"}, + {".accde", "application/msaccess"}, + {".accdr", "application/msaccess.runtime"}, + {".accdt", "application/msaccess"}, + {".accdw", "application/msaccess.webapplication"}, + {".accft", "application/msaccess.ftemplate"}, + {".acx", "application/internet-property-stream"}, + {".AddIn", "text/xml"}, + {".ade", "application/msaccess"}, + {".adobebridge", "application/x-bridge-url"}, + {".ADT", "audio/vnd.dlna.adts"}, + {".ADTS", "audio/aac"}, + {".amc", "application/x-mpeg"}, + {".art", "image/x-jg"}, + {".asa", "application/xml"}, + {".asax", "application/xml"}, + {".ascx", "application/xml"}, + {".asd", "application/octet-stream"}, + {".ashx", "application/xml"}, + {".asi", "application/octet-stream"}, + {".asmx", "application/xml"}, + {".aspx", "application/xml"}, + {".asr", "video/x-ms-asf"}, + {".axs", "application/olescript"}, + {".bas", "text/plain"}, + {".calx", "application/vnd.ms-office.calx"}, + {".cd", "text/plain"}, + {".cdda", "audio/aiff"}, + {".cnf", "text/plain"}, + {".config", "application/xml"}, + {".contact", "text/x-ms-contact"}, + {".coverage", "application/xml"}, + {".cs", "text/plain"}, + {".csdproj", "text/plain"}, + {".csproj", "text/plain"}, + {".cur", "application/octet-stream"}, + {".dat", "application/octet-stream"}, + {".datasource", "application/xml"}, + {".dbproj", "text/plain"}, + {".dgml", "application/xml"}, + {".dib", "image/bmp"}, + {".dif", "video/x-dv"}, + {".disco", "text/xml"}, + {".dll.config", "text/xml"}, + {".dlm", "text/dlm"}, + {".dsp", "application/octet-stream"}, + {".dsw", "text/plain"}, + {".dtsConfig", "text/xml"}, + {".dv", "video/x-dv"}, + {".dwp", "application/octet-stream"}, + {".etl", "application/etl"}, + {".exe.config", "text/xml"}, + {".fif", "application/fractals"}, + {".filters", "Application/xml"}, + {".fla", "application/octet-stream"}, + {".flr", "x-world/x-vrml"}, + {".fsscript", "application/fsharp-script"}, + {".fsx", "application/fsharp-script"}, + {".generictest", "application/xml"}, + {".group", "text/x-ms-group"}, + {".gsm", "audio/x-gsm"}, + {".gz", "application/x-gzip"}, + {".hdml", "text/x-hdml"}, + {".hhc", "application/x-oleobject"}, + {".hhk", "application/octet-stream"}, + {".hhp", "application/octet-stream"}, + {".hpp", "text/plain"}, + {".hta", "application/hta"}, + {".htc", "text/x-component"}, + {".htt", "text/webviewhtml"}, + {".hxa", "application/xml"}, + {".hxc", "application/xml"}, + {".hxd", "application/octet-stream"}, + {".hxe", "application/xml"}, + {".hxf", "application/xml"}, + {".hxh", "application/octet-stream"}, + {".hxi", "application/octet-stream"}, + {".hxk", "application/xml"}, + {".hxq", "application/octet-stream"}, + {".hxr", "application/octet-stream"}, + {".hxs", "application/octet-stream"}, + {".hxt", "text/html"}, + {".hxv", "application/xml"}, + {".hxw", "application/octet-stream"}, + {".hxx", "text/plain"}, + {".i", "text/plain"}, + {".idl", "text/plain"}, + {".iii", "application/x-iphone"}, + {".inc", "text/plain"}, + {".inf", "application/octet-stream"}, + {".inl", "text/plain"}, + {".ins", "application/x-internet-signup"}, + {".ipa", "application/x-itunes-ipa"}, + {".ipg", "application/x-itunes-ipg"}, + {".ipproj", "text/plain"}, + {".ipsw", "application/x-itunes-ipsw"}, + {".iqy", "text/x-ms-iqy"}, + {".isp", "application/x-internet-signup"}, + {".ite", "application/x-itunes-ite"}, + {".itlp", "application/x-itunes-itlp"}, + {".itms", "application/x-itunes-itms"}, + {".itpc", "application/x-itunes-itpc"}, + {".IVF", "video/x-ivf"}, + {".jck", "application/liquidmotion"}, + {".jcz", "application/liquidmotion"}, + {".jfif", "image/pjpeg"}, + {".jpb", "application/octet-stream"}, + {".jsx", "text/jscript"}, + {".jsxbin", "text/plain"}, + {".library-ms", "application/windows-library+xml"}, + {".lit", "application/x-ms-reader"}, + {".loadtest", "application/xml"}, + {".lpk", "application/octet-stream"}, + {".lsf", "video/x-la-asf"}, + {".lst", "text/plain"}, + {".lsx", "video/x-la-asf"}, + {".m2t", "video/vnd.dlna.mpeg-tts"}, + {".m2ts", "video/vnd.dlna.mpeg-tts"}, + {".m4a", "audio/m4a"}, + {".m4b", "audio/m4b"}, + {".m4p", "audio/m4p"}, + {".m4r", "audio/x-m4r"}, + {".mac", "image/x-macpaint"}, + {".mak", "text/plain"}, + {".manifest", "application/x-ms-manifest"}, + {".map", "text/plain"}, + {".master", "application/xml"}, + {".mda", "application/msaccess"}, + {".mde", "application/msaccess"}, + {".mdp", "application/octet-stream"}, + {".mfp", "application/x-shockwave-flash"}, + {".mht", "message/rfc822"}, + {".mhtml", "message/rfc822"}, + {".mix", "application/octet-stream"}, + {".mk", "text/plain"}, + {".mno", "text/xml"}, + {".mod", "video/mpeg"}, + {".mp2v", "video/mpeg"}, + {".mpa", "video/mpeg"}, + {".mpf", "application/vnd.ms-mediapackage"}, + {".mpv2", "video/mpeg"}, + {".mqv", "video/quicktime"}, + {".mso", "application/octet-stream"}, + {".mtx", "application/xml"}, + {".mvc", "application/x-miva-compiled"}, + {".mxp", "application/x-mmxp"}, + {".nws", "message/rfc822"}, + {".ocx", "application/octet-stream"}, + {".odh", "text/plain"}, + {".odl", "text/plain"}, + {".one", "application/onenote"}, + {".onea", "application/onenote"}, + {".orderedtest", "application/xml"}, + {".osdx", "application/opensearchdescription+xml"}, + {".pcast", "application/x-podcast"}, + {".pcz", "application/octet-stream"}, + {".pict", "image/pict"}, + {".pkgdef", "text/plain"}, + {".pkgundef", "text/plain"}, + {".pko", "application/vnd.ms-pki.pko"}, + {".pma", "application/x-perfmon"}, + {".pmc", "application/x-perfmon"}, + {".pmr", "application/x-perfmon"}, + {".pmw", "application/x-perfmon"}, + {".pnt", "image/x-macpaint"}, + {".pntg", "image/x-macpaint"}, + {".pnz", "image/png"}, + {".ppa", "application/vnd.ms-powerpoint"}, + {".prm", "application/octet-stream"}, + {".prx", "application/octet-stream"}, + {".psc1", "application/PowerShell"}, + {".psess", "application/xml"}, + {".psm", "application/octet-stream"}, + {".psp", "application/octet-stream"}, + {".pwz", "application/vnd.ms-powerpoint"}, + {".qht", "text/x-html-insertion"}, + {".qhtm", "text/x-html-insertion"}, + {".qti", "image/x-quicktime"}, + {".qtif", "image/x-quicktime"}, + {".qtl", "application/x-quicktimeplayer"}, + {".rat", "application/rat-file"}, + {".rc", "text/plain"}, + {".rc2", "text/plain"}, + {".rct", "text/plain"}, + {".rdlc", "application/xml"}, + {".resx", "application/xml"}, + {".rf", "image/vnd.rn-realflash"}, + {".rgs", "text/plain"}, + {".rpm", "audio/x-pn-realaudio-plugin"}, + {".rqy", "text/x-ms-rqy"}, + {".ruleset", "application/xml"}, + {".safariextz", "application/x-safari-safariextz"}, + {".sct", "text/scriptlet"}, + {".sd2", "audio/x-sd2"}, + {".sea", "application/octet-stream"}, + {".searchConnector-ms", "application/windows-search-connector+xml"}, + {".settings", "application/xml"}, + {".sgimb", "application/x-sgimb"}, + {".shtml", "text/html"}, + {".sitemap", "application/xml"}, + {".skin", "application/xml"}, + {".slk", "application/vnd.ms-excel"}, + {".sln", "text/plain"}, + {".slupkg-ms", "application/x-ms-license"}, + {".smd", "audio/x-smd"}, + {".smx", "audio/x-smd"}, + {".smz", "audio/x-smd"}, + {".snippet", "application/xml"}, + {".snp", "application/octet-stream"}, + {".sol", "text/plain"}, + {".sor", "text/plain"}, + {".srf", "text/plain"}, + {".SSISDeploymentManifest", "text/xml"}, + {".ssm", "application/streamingmedia"}, + {".sst", "application/vnd.ms-pki.certstore"}, + {".testrunconfig", "application/xml"}, + {".testsettings", "application/xml"}, + {".tgz", "application/x-compressed"}, + {".thn", "application/octet-stream"}, + {".tlh", "text/plain"}, + {".tli", "text/plain"}, + {".toc", "application/octet-stream"}, + {".trx", "application/xml"}, + {".ts", "video/vnd.dlna.mpeg-tts"}, + {".tts", "video/vnd.dlna.mpeg-tts"}, + {".uls", "text/iuls"}, + {".user", "text/plain"}, + {".vb", "text/plain"}, + {".vbdproj", "text/plain"}, + {".vbk", "video/mpeg"}, + {".vbproj", "text/plain"}, + {".vbs", "text/vbscript"}, + {".vcproj", "Application/xml"}, + {".vcxproj", "Application/xml"}, + {".vddproj", "text/plain"}, + {".vdp", "text/plain"}, + {".vdproj", "text/plain"}, + {".vdx", "application/vnd.ms-visio.viewer"}, + {".vml", "text/xml"}, + {".vscontent", "application/xml"}, + {".vsct", "text/xml"}, + {".vsi", "application/ms-vsi"}, + {".vsix", "application/vsix"}, + {".vsixlangpack", "text/xml"}, + {".vsixmanifest", "text/xml"}, + {".vsmdi", "application/xml"}, + {".vspscc", "text/plain"}, + {".vsscc", "text/plain"}, + {".vssettings", "text/xml"}, + {".vssscc", "text/plain"}, + {".vstemplate", "text/xml"}, + {".vsto", "application/x-ms-vsto"}, + {".vsx", "application/vnd.visio"}, + {".vtx", "application/vnd.visio"}, + {".wave", "audio/wav"}, + {".wbk", "application/msword"}, + {".webarchive", "application/x-safari-webarchive"}, + {".webtest", "application/xml"}, + {".wiq", "application/xml"}, + {".wiz", "application/msword"}, + {".WLMP", "application/wlmoviemaker"}, + {".wlpginstall", "application/x-wlpg-detect"}, + {".wlpginstall3", "application/x-wlpg3-detect"}, + {".wmp", "video/x-ms-wmp"}, + {".wrz", "x-world/x-vrml"}, + {".wsc", "text/scriptlet"}, + {".x", "application/directx"}, + {".xaf", "x-world/x-vrml"}, + {".xdr", "text/plain"}, + {".xld", "application/vnd.ms-excel"}, + {".xlk", "application/vnd.ms-excel"}, + {".xll", "application/vnd.ms-excel"}, + {".xmta", "application/xml"}, + {".xof", "x-world/x-vrml"}, + {".XOML", "text/plain"}, + {".xrm-ms", "text/xml"}, + {".xsc", "application/xml"}, + {".xsd", "text/xml"}, + {".xsf", "text/xml"}, + {".xsn", "application/octet-stream"}, + {".xss", "application/xml"}, + {".xtp", "application/octet-stream"}, + {".z", "application/x-compress"}, + }; + #endregion + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/MixinMatch.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/MixinMatch.cs new file mode 100755 index 0000000..76f6a17 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/MixinMatch.cs @@ -0,0 +1,10 @@ +namespace dotless.Core.Parser.Infrastructure +{ + public enum MixinMatch + { + ArgumentMismatch = 1, + Pass = 2, + GuardFail = 3, + Default = 4 + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/NamedArgument.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/NamedArgument.cs new file mode 100755 index 0000000..2d9e2fd --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/NamedArgument.cs @@ -0,0 +1,10 @@ +namespace dotless.Core.Parser.Infrastructure +{ + using Tree; + + public class NamedArgument + { + public string Name { get; set; } + public Expression Value { get; set; } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/BooleanNode.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/BooleanNode.cs new file mode 100755 index 0000000..d04d2d0 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/BooleanNode.cs @@ -0,0 +1,25 @@ +namespace dotless.Core.Parser.Infrastructure.Nodes +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + public class BooleanNode : Node + { + public bool Value { get; set; } + + public BooleanNode(bool value) + { + Value = value; + } + + protected override Node CloneCore() { + return new BooleanNode(Value); + } + + public override string ToString() { + return Value.ToString().ToLowerInvariant(); + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/CharMatchResult.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/CharMatchResult.cs new file mode 100755 index 0000000..b12dafe --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/CharMatchResult.cs @@ -0,0 +1,12 @@ +namespace dotless.Core.Parser.Infrastructure.Nodes +{ + public class CharMatchResult : TextNode + { + public char Char { get; set; } + + public CharMatchResult(char c) : base(c.ToString()) + { + Char = c; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/Node.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/Node.cs new file mode 100755 index 0000000..cf94006 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/Node.cs @@ -0,0 +1,170 @@ +using dotless.Core.Plugins; + +namespace dotless.Core.Parser.Infrastructure.Nodes +{ + using System; + + public abstract class Node + { + private bool isReference; + // Should this node be treated as a reference node + // i.e. has this been added to the parse tree via + // @import (reference) + public bool IsReference { + get { return isReference; } + set { isReference = value; } + } + + public NodeLocation Location { get; set; } + + public NodeList PreComments { get; set; } + public NodeList PostComments { get; set; } + + #region Boolean Operators + + public static implicit operator bool(Node node) + { + return node != null; + } + + public static bool operator true(Node n) + { + return n != null; + } + + public static bool operator false(Node n) + { + return n == null; + } + + public static bool operator !(Node n) + { + return n == null; + } + + public static Node operator &(Node n1, Node n2) + { + return n1 != null ? n2 : null; + } + + public static Node operator |(Node n1, Node n2) + { + return n1 ?? n2; + } + + #endregion + + /// + /// Copies common properties when evaluating multiple nodes into one + /// + /// Type to return - for convenience + /// The nodes this new node is derived from + /// The new node + public T ReducedFrom(params Node[] nodes) where T : Node + { + foreach (var node in nodes) + { + if (node == this) + { + continue; + } + + Location = node.Location; + + if (node.PreComments) + { + if (PreComments) + { + PreComments.AddRange(node.PreComments); + } + else + { + PreComments = node.PreComments; + } + } + + if (node.PostComments) + { + if (PostComments) + { + PostComments.AddRange(node.PostComments); + } + else + { + PostComments = node.PostComments; + } + } + + IsReference = node.IsReference; + } + + return (T)this; + } + + public virtual Node Clone() { + return CloneCore().ReducedFrom(this); + } + protected abstract Node CloneCore(); + + public virtual void AppendCSS(Env env) + { + //if there is no implementation then it will be a node that + //evalutes to something. + //ideally this shouldn't be called, but when creating error message it may be + Evaluate(env).AppendCSS(env); + } + + public virtual string ToCSS(Env env) + { + return env.Output + .Push() + .Append(this) + .Pop() + .ToString(); + } + + public virtual Node Evaluate(Env env) + { + return this; + } + + public virtual bool IgnoreOutput() + { + return + this is RegexMatchResult || + this is CharMatchResult; + } + + public virtual void Accept(IVisitor visitor) {} + + /// + /// Visits the node and throw an exception if the replacement mode isn't the right type, or the replacement is null + /// + public T VisitAndReplace(T nodeToVisit, IVisitor visitor) where T : Node + { + return VisitAndReplace(nodeToVisit, visitor, false); + } + + /// + /// Visits the node and throw an exception if the replacement mode isn't the right type + /// The allowNull parameter determines if a null is allowed to be returned + /// + public T VisitAndReplace(T nodeToVisit, IVisitor visitor, bool allowNull) where T : Node + { + if (nodeToVisit == null) + { + return null; + } + + Node replacement = visitor.Visit(nodeToVisit); + + T typedReplacement = replacement as T; + if (typedReplacement != null || (allowNull && replacement == null)) + { + return typedReplacement; + } + + throw new Exception("Not allowed null for node of type "+typeof(T).ToString()); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/NodeList.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/NodeList.cs new file mode 100755 index 0000000..48d24cc --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/NodeList.cs @@ -0,0 +1,155 @@ +using System.Linq; + +namespace dotless.Core.Parser.Infrastructure.Nodes +{ + using System.Collections; + using System.Collections.Generic; + + public class NodeList : NodeList + { + public NodeList() + { + Inner = new List(); + } + + public NodeList(params Node[] nodes) + : this((IEnumerable) nodes) + { + } + + public NodeList(IEnumerable nodes) + { + Inner = new List(nodes); + } + + public NodeList(NodeList nodes) : this((IEnumerable)nodes) + { + IsReference = nodes.IsReference; + } + } + + public class NodeList : Node, IList + where TNode : Node + { + protected List Inner; + + public NodeList() + { + Inner = new List(); + } + + public NodeList(params TNode[] nodes) + : this((IEnumerable) nodes) + { + } + + public NodeList(IEnumerable nodes) + { + Inner = new List(nodes); + } + + protected override Node CloneCore() { + return new NodeList(Inner.Select(i => i.Clone())); + } + + public override void AppendCSS(Env env) + { + env.Output.AppendMany(Inner); + } + + public override void Accept(Plugins.IVisitor visitor) + { + List newInner = new List(Inner.Count); + + foreach (TNode inner in Inner) + { + TNode node = VisitAndReplace(inner, visitor, true); + + if (node == null) + { + continue; + } + + newInner.Add(node); + } + + Inner = newInner; + } + + public void AddRange(IEnumerable nodes) + { + Inner.AddRange(nodes); + } + + public IEnumerator GetEnumerator() + { + return Inner.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void InsertRange(int index, IEnumerable collection) + { + Inner.InsertRange(index, collection); + } + + public void Add(TNode item) + { + Inner.Add(item); + } + + public void Clear() + { + Inner.Clear(); + } + + public bool Contains(TNode item) + { + return Inner.Contains(item); + } + + public void CopyTo(TNode[] array, int arrayIndex) + { + Inner.CopyTo(array, arrayIndex); + } + + public bool Remove(TNode item) + { + return Inner.Remove(item); + } + + public int Count + { + get { return Inner.Count; } + } + + public bool IsReadOnly + { + get { return ((IList) Inner).IsReadOnly; } + } + + public int IndexOf(TNode item) + { + return Inner.IndexOf(item); + } + + public void Insert(int index, TNode item) + { + Inner.Insert(index, item); + } + + public void RemoveAt(int index) + { + Inner.RemoveAt(index); + } + + public TNode this[int index] + { + get { return Inner[index]; } + set { Inner[index] = value; } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/RegexMatchResult.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/RegexMatchResult.cs new file mode 100755 index 0000000..5a62862 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/RegexMatchResult.cs @@ -0,0 +1,23 @@ +namespace dotless.Core.Parser.Infrastructure.Nodes +{ + using System.Text.RegularExpressions; + + public class RegexMatchResult : TextNode + { + public Match Match { get; set; } + + public RegexMatchResult(Match match) : base(match.Value) + { + Match = match; + } + + public string this[int index] + { + get + { + var value = Match.Groups[index].Value; + return string.IsNullOrEmpty(value) ? null : value; + } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/TextNode.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/TextNode.cs new file mode 100755 index 0000000..f42057d --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Nodes/TextNode.cs @@ -0,0 +1,47 @@ +using System; +namespace dotless.Core.Parser.Infrastructure.Nodes +{ + public class TextNode : Node, IComparable + { + public string Value { get; set; } + + public TextNode(string contents) + { + Value = contents; + } + + public static TextNode operator &(TextNode n1, TextNode n2) + { + return n1 != null ? n2 : null; + } + + public static TextNode operator |(TextNode n1, TextNode n2) + { + return n1 ?? n2; + } + + protected override Node CloneCore() { + return new TextNode(Value); + } + + public override void AppendCSS(Env env) + { + env.Output.Append(env.Compress ? Value.Trim() : Value); + } + + public override string ToString() + { + return Value; + } + + public virtual int CompareTo(object obj) + { + if (obj == null) + { + return -1; + } + + return obj.ToString().CompareTo(ToString()); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Output.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Output.cs new file mode 100755 index 0000000..39b57c1 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Infrastructure/Output.cs @@ -0,0 +1,241 @@ +namespace dotless.Core.Parser.Infrastructure +{ + using System; + using System.Globalization; + using System.Collections.Generic; + using System.Text; + using Nodes; + + public class Output + { + private Env Env { get; set; } + private StringBuilder Builder { get; set; } + private Stack BuilderStack { get; set; } + + public Output(Env env) + { + Env = env; + BuilderStack = new Stack(); + + Push(); + } + + public Output Push() + { + Builder = new StringBuilder(); + + BuilderStack.Push(Builder); + + return this; + } + + public StringBuilder Pop() + { + if (BuilderStack.Count == 1) + throw new InvalidOperationException(); + + var sb = BuilderStack.Pop(); + + Builder = BuilderStack.Peek(); + + return sb; + } + + public void Reset(string s) + { + Builder = new StringBuilder(s); + + BuilderStack.Pop(); + BuilderStack.Push(Builder); + } + + public Output PopAndAppend() + { + return Append(Pop()); + } + + public Output Append(Node node) + { + if (node != null) + { + if (node.PreComments) + node.PreComments.AppendCSS(Env); + + node.AppendCSS(Env); + + if (node.PostComments) + node.PostComments.AppendCSS(Env); + } + + return this; + } + + public Output Append(string s) + { + Builder.Append(s); + + return this; + } + + public Output Append(char? s) + { + Builder.Append(s); + + return this; + } + + public Output Append(StringBuilder sb) + { + Builder.Append(sb); + + return this; + } + + public Output AppendMany(IEnumerable nodes) + where TNode : Node + { + return AppendMany(nodes, null); + } + + public Output AppendMany(IEnumerable nodes, string join) + where TNode : Node + { + return AppendMany(nodes, n => Env.Output.Append(n), join); + } + + public Output AppendMany(IEnumerable list, string join) + { + return AppendMany(list, (item, sb) => sb.Append(item), join); + } + + public Output AppendMany(IEnumerable list, Func toString, string join) + { + return AppendMany(list, (item, sb) => sb.Append(toString(item)), join); + } + + public Output AppendMany(IEnumerable list, Action toString, string join) + { + return AppendMany(list, (item, sb) => toString(item), join); + } + + public Output AppendMany(IEnumerable list, Action toString, string join) + { + var first = true; + var hasJoinString = !string.IsNullOrEmpty(join); + + foreach (var item in list) + { + if (!first && hasJoinString) + Builder.Append(join); + + first = false; + toString(item, Builder); + } + + return this; + } + + public Output AppendMany(IEnumerable buildersToAppend) + { + return AppendMany(buildersToAppend, null); + } + + public Output AppendMany(IEnumerable buildersToAppend, string join) + { + return AppendMany(buildersToAppend, (b, output) => output.Append(b), join); + } + + public Output AppendFormat(string format, params object[] values) { + return AppendFormat(CultureInfo.InvariantCulture, format, values); + } + + public Output AppendFormat(IFormatProvider formatProvider, string format, params object[] values) + { + Builder.AppendFormat(formatProvider, format, values); + + return this; + } + + public Output Indent(int amount) + { + if (amount > 0) + { + var indentation = new string(' ', amount); + Builder.Replace("\n", "\n" + indentation); + Builder.Insert(0, indentation); + } + + return this; + } + + /// + /// Trims whitespace + /// + public Output Trim() + { + return this.TrimLeft(null).TrimRight(null); + } + + /// + /// Trims the character passed or whitespace if it has no value from the left + /// + public Output TrimLeft(char? c) + { + int trimLLength = 0; + int length = Builder.Length; + + if (length == 0) + { + return this; + } + + while (trimLLength < length && + (c.HasValue ? Builder[trimLLength] == c.Value : + char.IsWhiteSpace(Builder[trimLLength]))) + { + trimLLength++; + } + + if (trimLLength > 0) + { + Builder.Remove(0, trimLLength); + } + + return this; + } + + /// + /// Trims the character passed or whitespace if it has no value from the left + /// + public Output TrimRight(char? c) + { + int trimRLength = 0; + int length = Builder.Length; + + if (length == 0) + { + return this; + } + + while (trimRLength < length && + (c.HasValue ? Builder[length - (trimRLength + 1)] == c.Value : + char.IsWhiteSpace(Builder[length - (trimRLength + 1)]))) + { + trimRLength++; + } + + if (trimRLength > 0) + { + Builder.Remove(length - trimRLength, trimRLength); + } + + return this; + } + + + public override string ToString() + { + return Builder.ToString(); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Parser.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Parser.cs new file mode 100755 index 0000000..c33a1ce --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Parser.cs @@ -0,0 +1,173 @@ +// ReSharper disable InconsistentNaming + +namespace dotless.Core.Parser +{ + using Exceptions; + using Importers; + using Infrastructure; + using Stylizers; + using Tree; + + // + // less.js - parser + // + // A relatively straight-forward predictive parser. + // There is no tokenization/lexing stage, the input is parsed + // in one sweep. + // + // To make the parser fast enough to run in the browser, several + // optimization had to be made: + // + // - Instead of the more commonly used technique of slicing the + // input string on every match, we use global regexps (/g), + // and move the `lastIndex` pointer on match, foregoing `slice()` + // completely. This gives us a 3x speed-up. + // + // - Matching on a huge input is often cause of slowdowns. + // The solution to that is to chunkify the input into + // smaller strings. + // + // - In many cases, we don't need to match individual tokens; + // for example, if a value doesn't hold any variables, operations + // or dynamic references, the parser can effectively 'skip' it, + // treating it as a literal. + // An example would be '1px solid #000' - which evaluates to itself, + // we don't need to know what the individual components are. + // The drawback, of course is that you don't get the benefits of + // syntax-checking on the CSS. This gives us a 50% speed-up in the parser, + // and a smaller speed-up in the code-gen. + // + // + // Token matching is done with the `Match` function, which either takes + // a terminal string or regexp, or a non-terminal function to call. + // It also takes care of moving all the indices forwards. + // + // + public class Parser + { + public Tokenizer Tokenizer { get; set; } + public IStylizer Stylizer { get; set; } + public string FileName { get; set; } + public bool Debug { get; set; } + + public string CurrentDirectory + { + get { return Importer.CurrentDirectory; } + set { Importer.CurrentDirectory = value; } + } + + private INodeProvider _nodeProvider; + public INodeProvider NodeProvider + { + get { return _nodeProvider ?? (_nodeProvider = new DefaultNodeProvider()); } + set { _nodeProvider = value; } + } + + private IImporter _importer; + public IImporter Importer + { + get { return _importer; } + set + { + _importer = value; + _importer.Parser = () => new Parser(Tokenizer.Optimization, Stylizer, _importer) + { + NodeProvider = NodeProvider, + Debug = Debug, + CurrentDirectory = CurrentDirectory, + StrictMath = StrictMath + }; + } + } + + public bool StrictMath { get; set; } + + private const int defaultOptimization = 1; + private const bool defaultDebug = false; + + public Parser() + : this(defaultOptimization, defaultDebug) + { + } + + public Parser(bool debug) + : this(defaultOptimization, debug) + { + } + + public Parser(int optimization) + : this(optimization, new PlainStylizer(), new Importer(), defaultDebug) + { + } + + public Parser(int optimization, bool debug) + : this(optimization, new PlainStylizer(), new Importer(), debug) + { + } + + public Parser(IStylizer stylizer, IImporter importer) + : this(defaultOptimization, stylizer, importer, defaultDebug) + { + } + + public Parser(IStylizer stylizer, IImporter importer, bool debug) + : this(defaultOptimization, stylizer, importer, debug) + { + } + + public Parser(int optimization, IStylizer stylizer, IImporter importer) + : this(optimization, stylizer, importer, defaultDebug) + { + } + + public Parser(int optimization, IStylizer stylizer, IImporter importer, bool debug) + { + Stylizer = stylizer; + Importer = importer; + Debug = debug; + Tokenizer = new Tokenizer(optimization); + } + + public Parser(dotless.Core.configuration.DotlessConfiguration config, IStylizer stylizer, IImporter importer) + : this(config.Optimization, stylizer, importer, config.Debug) + { + + } + + public Ruleset Parse(string input, string fileName) + { + Ruleset root; + FileName = fileName; + + try + { + Tokenizer.SetupInput(input, fileName); + + var parsers = new Parsers(NodeProvider); + root = new Root(parsers.Primary(this), GenerateParserError); + } + catch (ParsingException e) + { + throw GenerateParserError(e); + } + + if (!Tokenizer.HasCompletedParsing()) + throw GenerateParserError(new ParsingException("Content after finishing parsing (missing opening bracket?)", Tokenizer.GetNodeLocation(Tokenizer.Location.Index))); + + return root; + } + + private ParserException GenerateParserError(ParsingException parsingException) + { + var errorLocation = parsingException.Location; + var error = parsingException.Message; + var call = parsingException.CallLocation; + + var zone = new Zone(errorLocation, error, call != null ? new Zone(call) : null); + + var message = Stylizer.Stylize(zone); + + return new ParserException(message, parsingException, zone); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/ParserLocation.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/ParserLocation.cs new file mode 100755 index 0000000..5ed47ff --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/ParserLocation.cs @@ -0,0 +1,18 @@ +namespace dotless.Core.Parser +{ + using System; + + public class NodeLocation + { + public int Index { get; set; } + public string Source { get; set; } + public string FileName { get; set; } + + public NodeLocation(int index, string source, string filename) + { + Index = index; + Source = source; + FileName = filename; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Parsers.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Parsers.cs new file mode 100755 index 0000000..8c6be6d --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Parsers.cs @@ -0,0 +1,2071 @@ +// +// Here in, the parsing rules/functions +// +// The basic structure of the syntax tree generated is as follows: +// +// Ruleset -> Rule -> Value -> Expression -> Entity +// +// Here's some LESS code: +// +// .class { +// color: #fff; +// border: 1px solid #000; +// width: @w + 4px; +// > .child {...} +// } +// +// And here's what the parse tree might look like: +// +// Ruleset (Selector '.class', [ +// Rule ("color", Value ([Expression [Color #fff]])) +// Rule ("border", Value ([Expression [Number 1px][Keyword "solid"][Color #000]])) +// Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Number 4px]]])) +// Ruleset (Selector [Element '>', '.child'], [...]) +// ]) +// +// In general, most rules will try to parse a token with the `$()` function, and if the return +// value is truly, will return a new node, of the relevant type. Sometimes, we need to check +// first, before parsing, that's when we use `peek()`. +// + + +using System; +using System.Text.RegularExpressions; + +#pragma warning disable 665 +// ReSharper disable RedundantNameQualifier + +namespace dotless.Core.Parser +{ + using System.Collections.Generic; + using System.Linq; + using Exceptions; + using Infrastructure; + using Infrastructure.Nodes; + using Tree; + + public class Parsers + { + public INodeProvider NodeProvider { get; set; } + + public Parsers(INodeProvider nodeProvider) + { + NodeProvider = nodeProvider; + } + + // + // The `primary` rule is the *entry* and *exit* point of the parser. + // The rules here can appear at any level of the parse tree. + // + // The recursive nature of the grammar is an interplay between the `block` + // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule, + // as represented by this simplified grammar: + // + // primary → (ruleset | rule)+ + // ruleset → selector+ block + // block → '{' primary '}' + // + // Only at one point is the primary rule not called from the + // block rule: at the root level. + // + public NodeList Primary(Parser parser) + { + Node node; + var root = new NodeList(); + + GatherComments(parser); + while (node = MixinDefinition(parser) || ExtendRule(parser) || Rule(parser) || PullComments() || GuardedRuleset(parser) || Ruleset(parser) || + MixinCall(parser) || Directive(parser)) + { + NodeList comments; + if (comments = PullComments()) + { + root.AddRange(comments); + } + + comments = node as NodeList; + if (comments) + { + foreach (Comment c in comments) + { + c.IsPreSelectorComment = true; + } + root.AddRange(comments); + } + else + root.Add(node); + + GatherComments(parser); + } + return root; + } + + private NodeList CurrentComments { get; set; } + + /// + /// Gathers the comments and put them on the stack + /// + private void GatherComments(Parser parser) + { + Comment comment; + while (comment = Comment(parser)) + { + if (CurrentComments == null) + { + CurrentComments = new NodeList(); + } + CurrentComments.Add(comment); + } + } + + /// + /// Collects comments from the stack retrived when gathering comments + /// + private NodeList PullComments() + { + NodeList comments = CurrentComments; + CurrentComments = null; + return comments; + } + + /// + /// The equivalent of gathering any more comments and pulling everything on the stack + /// + private NodeList GatherAndPullComments(Parser parser) + { + GatherComments(parser); + return PullComments(); + } + + private Stack CommentsStack = new Stack(); + + /// + /// Pushes comments on to a stack for later use + /// + private void PushComments() + { + CommentsStack.Push(PullComments()); + } + + /// + /// Pops the comments stack + /// + private void PopComments() + { + CurrentComments = CommentsStack.Pop(); + } + + // We create a Comment node for CSS comments `/* */`, + // but keep the LeSS comments `//` silent, by just skipping + // over them.e + public Comment Comment(Parser parser) + { + var index = parser.Tokenizer.Location.Index; + string comment = parser.Tokenizer.GetComment(); + + if (comment != null) + { + return NodeProvider.Comment(comment, parser.Tokenizer.GetNodeLocation(index)); + } + + return null; + } + + // + // Entities are tokens which can be found inside an Expression + // + + // + // A string, which supports escaping " and ' + // + // "milky way" 'he\'s the one!' + // + public Quoted Quoted(Parser parser) + { + var index = parser.Tokenizer.Location.Index; + var escaped = false; + var quote = parser.Tokenizer.CurrentChar; + + if (parser.Tokenizer.CurrentChar == '~') + { + escaped = true; + quote = parser.Tokenizer.NextChar; + } + if (quote != '"' && quote != '\'') + return null; + + if (escaped) + parser.Tokenizer.Match('~'); + + string str = parser.Tokenizer.GetQuotedString(); + + if (str == null) + return null; + + return NodeProvider.Quoted(str, str.Substring(1, str.Length - 2), escaped, parser.Tokenizer.GetNodeLocation(index)); + } + + // + // A catch-all word, such as: + // + // black border-collapse + // + public Keyword Keyword(Parser parser) + { + var index = parser.Tokenizer.Location.Index; + + var k = parser.Tokenizer.Match(@"[A-Za-z0-9_-]+"); + if (k) + return NodeProvider.Keyword(k.Value, parser.Tokenizer.GetNodeLocation(index)); + + return null; + } + + // + // A function call + // + // rgb(255, 0, 255) + // + // We also try to catch IE's `alpha()`, but let the `alpha` parser + // deal with the details. + // + // The arguments are parsed with the `entities.arguments` parser. + // + public Call Call(Parser parser) + { + var memo = Remember(parser); + var index = parser.Tokenizer.Location.Index; + + var name = parser.Tokenizer.Match(@"(%|[a-zA-Z0-9_-]+|progid:[\w\.]+)\("); + + if (!name) + return null; + + if (name[1].ToLowerInvariant() == "alpha") + { + var alpha = Alpha(parser); + if (alpha != null) + return alpha; + } + + var args = Arguments(parser); + + if (!parser.Tokenizer.Match(')')) + { + Recall(parser, memo); + return null; + } + + return NodeProvider.Call(name[1], args, parser.Tokenizer.GetNodeLocation(index)); + } + + public NodeList Arguments(Parser parser) + { + var args = new NodeList(); + Node arg; + + while ((arg = Assignment(parser)) || (arg = Expression(parser))) + { + args.Add(arg); + if (!parser.Tokenizer.Match(',')) + break; + } + return args; + } + + // Assignments are argument entities for calls. + // They are present in ie filter properties as shown below. + // + // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* ) + // + public Assignment Assignment(Parser parser) + { + var key = parser.Tokenizer.Match(@"\w+(?=\s?=)"); + + if (!key || !parser.Tokenizer.Match('=')) + { + return null; + } + + var value = Entity(parser); + + if (value) { + return NodeProvider.Assignment(key.Value, value, key.Location); + } + + return null; + } + + public Node Literal(Parser parser) + { + return Dimension(parser) || + Color(parser) || + Quoted(parser); + } + + // + // Parse url() tokens + // + // We use a specific rule for urls, because they don't really behave like + // standard function calls. The difference is that the argument doesn't have + // to be enclosed within a string, so it can't be parsed as an Expression. + // + public Url Url(Parser parser) + { + var index = parser.Tokenizer.Location.Index; + + if (parser.Tokenizer.CurrentChar != 'u' || !parser.Tokenizer.Match(@"url\(")) + return null; + + GatherComments(parser); + + Node value = Quoted(parser); + + if (!value) + { + var memo = Remember(parser); + value = Expression(parser); + + if (value && !parser.Tokenizer.Peek(')')) + { + value = null; + Recall(parser, memo); + } + } + else + { + value.PreComments = PullComments(); + value.PostComments = GatherAndPullComments(parser); + } + + if (!value) + { + value = parser.Tokenizer.MatchAny(@"[^\)""']*") || new TextNode(""); + } + + Expect(parser, ')'); + + return NodeProvider.Url(value, parser.Importer, parser.Tokenizer.GetNodeLocation(index)); + } + + // + // A Variable entity, such as `@fink`, in + // + // width: @fink + 2px + // + // We use a different parser for variable definitions, + // see `parsers.variable`. + // + public Variable Variable(Parser parser) + { + RegexMatchResult name; + var index = parser.Tokenizer.Location.Index; + + if (parser.Tokenizer.CurrentChar == '@' && (name = parser.Tokenizer.Match(@"@(@?[a-zA-Z0-9_-]+)"))) + return NodeProvider.Variable(name.Value, parser.Tokenizer.GetNodeLocation(index)); + + return null; + } + + // + // An interpolated Variable entity, such as `@{foo}`, in + // + // [@{foo}="value"] + // + public Variable InterpolatedVariable(Parser parser) { + RegexMatchResult name; + var index = parser.Tokenizer.Location.Index; + + if (parser.Tokenizer.CurrentChar == '@' && (name = parser.Tokenizer.Match(@"@\{(?@?[a-zA-Z0-9_-]+)\}"))) + return NodeProvider.Variable("@" + name.Match.Groups["name"].Value, parser.Tokenizer.GetNodeLocation(index)); + + return null; + } + + // + // A Variable entity as like in a selector e.g. + // + // @{var} { + // } + // + public Variable VariableCurly(Parser parser) + { + RegexMatchResult name; + var index = parser.Tokenizer.Location.Index; + + if (parser.Tokenizer.CurrentChar == '@' && (name = parser.Tokenizer.Match(@"@\{([a-zA-Z0-9_-]+)\}"))) + return NodeProvider.Variable("@" + name.Match.Groups[1].Value, parser.Tokenizer.GetNodeLocation(index)); + + return null; + } + + /// + /// A guarded ruleset placed inside another e.g. + /// + /// & when (@x = true) { + /// } + /// + public GuardedRuleset GuardedRuleset(Parser parser) + { + var selectors = new NodeList(); + + var memo = Remember(parser); + var index = memo.TokenizerLocation.Index; + + Selector s; + while (s = Selector(parser)) + { + selectors.Add(s); + if (!parser.Tokenizer.Match(',')) + break; + + GatherComments(parser); + } + + if (parser.Tokenizer.Match(@"when")) + { + GatherAndPullComments(parser); + + var condition = Expect(Conditions(parser), "Expected conditions after when (guard)", parser); + var rules = Block(parser); + + return NodeProvider.GuardedRuleset(selectors, rules, condition, parser.Tokenizer.GetNodeLocation(index)); + } + + Recall(parser, memo); + + return null; + } + + /// + /// An extend statement placed at the end of a rule + /// + /// + /// + public Extend ExtendRule(Parser parser) + { + RegexMatchResult extendKeyword; + var index = parser.Tokenizer.Location.Index; + + if ((extendKeyword = parser.Tokenizer.Match(@"\&?:extend\(")) != null) + { + var exact = new List(); + var partial = new List(); + + Selector s; + while (s = Selector(parser)) + { + if (s.Elements.Count == 1 && s.Elements.First().Value == null) + { + continue; + } + + if (s.Elements.Count > 1 && s.Elements.Last().Value == "all") + { + s.Elements.Remove(s.Elements.Last()); + partial.Add(s); + } + else + { + exact.Add(s); + } + + if (!parser.Tokenizer.Match(',')) + { + break; + } + } + + if (!parser.Tokenizer.Match(')')) + { + throw new ParsingException(@"Extend rule not correctly terminated",parser.Tokenizer.GetNodeLocation(index)); + } + + if (extendKeyword.Match.Value[0] == '&') + { + parser.Tokenizer.Match(';'); + } + + if (partial.Count == 0 && exact.Count == 0) + { + return null; + } + + return NodeProvider.Extend(exact,partial, parser.Tokenizer.GetNodeLocation(index)); + } + return null; + } + + // + // A Hexadecimal color + // + // #4F3C2F + // + // `rgb` and `hsl` colors are parsed through the `entities.call` parser. + // + public Color Color(Parser parser) + { + RegexMatchResult hex; + + var index = parser.Tokenizer.Location.Index; + + if (parser.Tokenizer.CurrentChar == '#' && + (hex = parser.Tokenizer.Match(@"#([a-fA-F0-9]{8}|[a-fA-F0-9]{6}|[a-fA-F0-9]{3})"))) + return NodeProvider.Color(hex[1], parser.Tokenizer.GetNodeLocation(index)); + + return null; + } + + // + // A Dimension, that is, a number and a unit + // + // 0.5em 95% + // + public Number Dimension(Parser parser) + { + var c = parser.Tokenizer.CurrentChar; + if (!(char.IsNumber(c) || c == '.' || c == '-' || c == '+')) + return null; + + var index = parser.Tokenizer.Location.Index; + + var value = parser.Tokenizer.Match(@"([+-]?[0-9]*\.?[0-9]+)(px|%|em|pc|ex|in|deg|s|ms|pt|cm|mm|ch|rem|vw|vh|vmin|vm(ax)?|grad|rad|fr|gr|Hz|kHz|dpi|dpcm|dppx)?", true); + + if (value) + return NodeProvider.Number(value[1], value[2], parser.Tokenizer.GetNodeLocation(index)); + + return null; + } + + // + // C# code to be evaluated + // + // `` + // + public Script Script(Parser parser) + { + if (parser.Tokenizer.CurrentChar != '`') + return null; + + var index = parser.Tokenizer.Location.Index; + + var script = parser.Tokenizer.MatchAny(@"`[^`]*`"); + + if (!script) + { + return null; + } + + return NodeProvider.Script(script.Value, parser.Tokenizer.GetNodeLocation(index)); + } + + + // + // The variable part of a variable definition. Used in the `rule` parser + // + // @fink: + // + public string VariableName(Parser parser) + { + var variable = Variable(parser); + + if (variable != null) + return variable.Name; + + return null; + } + + // + // A font size/line-height shorthand + // + // small/12px + // + // We need to peek first, or we'll match on keywords and dimensions + // + public Shorthand Shorthand(Parser parser) + { + if (!parser.Tokenizer.Peek(@"[@%\w.-]+\/[@%\w.-]+")) + return null; + + var index = parser.Tokenizer.Location.Index; + + Node a = null; + Node b = null; + if ((a = Entity(parser)) && parser.Tokenizer.Match('/') && (b = Entity(parser))) + return NodeProvider.Shorthand(a, b, parser.Tokenizer.GetNodeLocation(index)); + + return null; + } + + // + // Mixins + // + + // + // A Mixin call, with an optional argument list + // + // #mixins > .square(#fff); + // .rounded(4px, black); + // .button; + // + // The `while` loop is there because mixins can be + // namespaced, but we only support the child and descendant + // selector for now. + // + public MixinCall MixinCall(Parser parser) + { + var elements = new NodeList(); + var index = parser.Tokenizer.Location.Index; + bool important = false; + + RegexMatchResult e; + Combinator c = null; + + PushComments(); + + for (var i = parser.Tokenizer.Location.Index; e = parser.Tokenizer.Match(@"[#.][a-zA-Z0-9_-]+"); i = parser.Tokenizer.Location.Index) + { + elements.Add(NodeProvider.Element(c, e, parser.Tokenizer.GetNodeLocation(index))); + + i = parser.Tokenizer.Location.Index; + var match = parser.Tokenizer.Match('>'); + c = match != null ? NodeProvider.Combinator(match.Value, parser.Tokenizer.GetNodeLocation(index)) : null; + } + + if (elements.Count == 0) + { + PopComments(); + return null; + } + + var args = new List(); + if (parser.Tokenizer.Peek('(')) { + var location = Remember(parser); + const string balancedParenthesesRegex = @"\([^()]*(?>(?>(?'open'\()[^()]*)*(?>(?'-open'\))[^()]*)*)+(?(open)(?!))\)"; + + var argumentList = parser.Tokenizer.Match(balancedParenthesesRegex); + bool argumentListIsSemicolonSeparated = argumentList != null && argumentList.Value.Contains(';'); + + char expectedSeparator = argumentListIsSemicolonSeparated ? ';' : ','; + + Recall(parser, location); + parser.Tokenizer.Match('('); + + Expression arg; + while (arg = Expression(parser, argumentListIsSemicolonSeparated)) + { + var value = arg; + string name = null; + + if (arg.Value.Count == 1 && arg.Value[0] is Variable) + { + if (parser.Tokenizer.Match(':')) + { + value = Expect(Expression(parser), "expected value", parser); + name = (arg.Value[0] as Variable).Name; + } + } + + args.Add(new NamedArgument { Name = name, Value = value }); + + if (!parser.Tokenizer.Match(expectedSeparator)) + break; + } + Expect(parser, ')'); + } + + GatherComments(parser); + + if (!string.IsNullOrEmpty(Important(parser))) + { + important = true; + } + + // if elements then we've picked up chars so don't need to worry about remembering + var postComments = GatherAndPullComments(parser); + + if (End(parser)) + { + var mixinCall = NodeProvider.MixinCall(elements, args, important, parser.Tokenizer.GetNodeLocation(index)); + mixinCall.PostComments = postComments; + PopComments(); + return mixinCall; + } + + PopComments(); + return null; + } + + private Expression Expression(Parser parser, bool allowList) + { + return allowList ? ExpressionOrExpressionList(parser) : Expression(parser); + } + + // + // A Mixin definition, with a list of parameters + // + // .rounded (@radius: 2px, @color) { + // ... + // } + // + // Until we have a finer grained state-machine, we have to + // do a look-ahead, to make sure we don't have a mixin call. + // See the `rule` function for more information. + // + // We start by matching `.rounded (`, and then proceed on to + // the argument list, which has optional default values. + // We store the parameters in `params`, with a `value` key, + // if there is a value, such as in the case of `@radius`. + // + // Once we've got our params list, and a closing `)`, we parse + // the `{...}` block. + // + public MixinDefinition MixinDefinition(Parser parser) + { + if ((parser.Tokenizer.CurrentChar != '.' && parser.Tokenizer.CurrentChar != '#') || + parser.Tokenizer.Peek(@"[^{]*}")) + return null; + + var index = parser.Tokenizer.Location.Index; + + var memo = Remember(parser); + + var match = parser.Tokenizer.Match(@"([#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+)\s*\("); + if (!match) + return null; + + //mixin definition ignores comments before it - a css hack can't be part of a mixin definition, + //so it may as well be a rule before the definition + PushComments(); + GatherAndPullComments(parser); // no store as mixin definition not output + + var name = match[1]; + bool variadic = false; + var parameters = new NodeList(); + RegexMatchResult param = null; + Node param2 = null; + Condition condition = null; + int i; + while (true) + { + i = parser.Tokenizer.Location.Index; + if (parser.Tokenizer.CurrentChar == '.' && parser.Tokenizer.Match("\\.{3}")) + { + variadic = true; + break; + } + + if (param = parser.Tokenizer.Match(@"@[a-zA-Z0-9_-]+")) + { + GatherAndPullComments(parser); + + if (parser.Tokenizer.Match(':')) + { + GatherComments(parser); + var value = Expect(Expression(parser), "Expected value", parser); + + parameters.Add(NodeProvider.Rule(param.Value, value, parser.Tokenizer.GetNodeLocation(i))); + } + else if (parser.Tokenizer.Match("\\.{3}")) + { + variadic = true; + parameters.Add(NodeProvider.Rule(param.Value, null, true, parser.Tokenizer.GetNodeLocation(i))); + break; + } + else + parameters.Add(NodeProvider.Rule(param.Value, null, parser.Tokenizer.GetNodeLocation(i))); + + } else if (param2 = Literal(parser) || Keyword(parser)) + { + parameters.Add(NodeProvider.Rule(null, param2, parser.Tokenizer.GetNodeLocation(i))); + } else + break; + + GatherAndPullComments(parser); + + if (!(parser.Tokenizer.Match(',') || parser.Tokenizer.Match(';'))) + break; + + GatherAndPullComments(parser); + } + + if (!parser.Tokenizer.Match(')')) + { + Recall(parser, memo); + } + + GatherAndPullComments(parser); + + if (parser.Tokenizer.Match("when")) + { + GatherAndPullComments(parser); + + condition = Expect(Conditions(parser), "Expected conditions after when (mixin guards)", parser); + } + + var rules = Block(parser); + + PopComments(); + + if (rules != null) + return NodeProvider.MixinDefinition(name, parameters, rules, condition, variadic, parser.Tokenizer.GetNodeLocation(index)); + + Recall(parser, memo); + + return null; + } + + /// + /// a list of , seperated conditions (, == OR) + /// + public Condition Conditions(Parser parser) + { + Condition condition, nextCondition; + + if (condition = Condition(parser)) { + while(parser.Tokenizer.Match(',')) { + nextCondition = Expect(Condition(parser), ", without recognised condition", parser); + + condition = NodeProvider.Condition(condition, "or", nextCondition, false, parser.Tokenizer.GetNodeLocation()); + } + return condition; + } + + return null; + } + + /// + /// A condition is used for mixin definitions and is made up + /// of left operation right + /// + public Condition Condition(Parser parser) + { + int index = parser.Tokenizer.Location.Index; + bool negate = false; + Condition condition; + //var a, b, c, op, index = i, negate = false; + + if (parser.Tokenizer.Match("not")) + { + negate = true; + } + + Expect(parser, '('); + + Node left = Expect(Operation(parser) || Keyword(parser) || Quoted(parser), "unrecognised condition", parser); + + var op = parser.Tokenizer.Match("(>=|=<|[<=>])"); + + if (op) + { + Node right = Expect(Operation(parser) || Keyword(parser) || Quoted(parser), "unrecognised right hand side condition expression", parser); + + condition = NodeProvider.Condition(left, op.Value, right, negate, parser.Tokenizer.GetNodeLocation(index)); + } + else + { + condition = NodeProvider.Condition(left, "=", NodeProvider.Keyword("true", parser.Tokenizer.GetNodeLocation(index)), negate, parser.Tokenizer.GetNodeLocation(index)); + } + + Expect(parser, ')'); + + if (parser.Tokenizer.Match("and")) + { + return NodeProvider.Condition(condition, "and", Condition(parser), false, parser.Tokenizer.GetNodeLocation(index)); + } + + return condition; + } + + // + // Entities are the smallest recognized token, + // and can be found inside a rule's value. + // + public Node Entity(Parser parser) + { + return Literal(parser) || Variable(parser) || Url(parser) || + Call(parser) || Keyword(parser) || Script(parser); + } + + private Expression ExpressionOrExpressionList(Parser parser) + { + var memo = Remember(parser); + + List entities = new List(); + + Expression entity; + while (entity = Expression(parser)) + { + entities.Add(entity); + + if (!parser.Tokenizer.Match(',')) + { + break; + } + } + + if (entities.Count == 0) + { + Recall(parser, memo); + return null; + } + + if (entities.Count == 1) + { + return entities[0]; + } + + return new Expression(entities.Cast(), true); + } + + // + // A Rule terminator. Note that we use `Peek()` to check for '}', + // because the `block` rule will be expecting it, but we still need to make sure + // it's there, if ';' was ommitted. + // Also note that there might be multiple semicolons between consecutive + // declarations, and those semicolons may be separated by whitespace. + public bool End(Parser parser) + { + // The regular expression searches for a semicolon which may be followed by whitespace and other semicolons. + const string SemicolonSearch = @";[;\s]*"; + + return parser.Tokenizer.Match(SemicolonSearch) || parser.Tokenizer.Peek('}'); + } + + // + // IE's alpha function + // + // alpha(opacity=88) + // + public Alpha Alpha(Parser parser) + { + Node value; + + var index = parser.Tokenizer.Location.Index; + + // Allow for whitespace on both sides of the equals sign since IE seems to allow it too + if (!parser.Tokenizer.Match(@"opacity\s*=\s*", true)) + return null; + + if (value = parser.Tokenizer.Match(@"[0-9]+") || Variable(parser)) + { + Expect(parser, ')'); + + return NodeProvider.Alpha(value, parser.Tokenizer.GetNodeLocation(index)); + } + + return null; + } + + // + // A Selector Element + // + // div + // + h1 + // #socks + // input[type="text"] + // + // Elements are the building blocks for Selectors, + // they are made out of a `Combinator` (see combinator rule), + // and an element name, such as a tag a class, or `*`. + // + public Element Element(Parser parser) + { + var index = parser.Tokenizer.Location.Index; + + GatherComments(parser); + + Combinator c = Combinator(parser); + + const string parenthesizedTokenRegex = @"\(((?\()|(?<-N>\))|[^()@]*)+\)"; + + PushComments(); + GatherComments(parser); // to collect, combinator must have picked up something which would require memory anyway + + if (parser.Tokenizer.Peek("when")) + { + return null; + } + + Node e = ExtendRule(parser) + || NonPseudoClassSelector(parser) + || PseudoClassSelector(parser) + || PseudoElementSelector(parser) + || parser.Tokenizer.Match('*') + || parser.Tokenizer.Match('&') + || Attribute(parser) + || parser.Tokenizer.MatchAny(parenthesizedTokenRegex) + || parser.Tokenizer.Match(@"[\.#](?=@\{)") + || VariableCurly(parser); + + if (!e) + { + if (parser.Tokenizer.Match('(')) { + var variable = Variable(parser) ?? VariableCurly(parser); + if (variable) + { + parser.Tokenizer.Match(')'); + e = NodeProvider.Paren(variable, parser.Tokenizer.GetNodeLocation(index)); + } + } + } + + if (e) + { + c.PostComments = PullComments(); + PopComments(); + c.PreComments = PullComments(); + + return NodeProvider.Element(c, e, parser.Tokenizer.GetNodeLocation(index)); + } + + PopComments(); + return null; + } + + private static RegexMatchResult PseudoClassSelector(Parser parser) { + return parser.Tokenizer.Match(@":(\\.|[a-zA-Z0-9_-])+"); + } + + private static RegexMatchResult PseudoElementSelector(Parser parser) { + return parser.Tokenizer.Match(@"::(\\.|[a-zA-Z0-9_-])+"); + } + + private Node NonPseudoClassSelector(Parser parser) { + var memo = Remember(parser); + var match = parser.Tokenizer.Match(@"[.#]?(\\.|[a-zA-Z0-9_-])+"); + if (!match) { + return null; + } + + if (parser.Tokenizer.Match('(')) { + // Argument list implies that we actually matched a mixin call + // Rewind back to where we started and return a null match + Recall(parser, memo); + return null; + } + + return match; + } + + // + // Combinators combine elements together, in a Selector. + // + // Because our parser isn't white-space sensitive, special care + // has to be taken, when parsing the descendant combinator, ` `, + // as it's an empty space. We have to check the previous character + // in the input, to see if it's a ` ` character. More info on how + // we deal with this in *combinator.js*. + // + public Combinator Combinator(Parser parser) + { + var index = parser.Tokenizer.Location.Index; + + Node match; + if (match = parser.Tokenizer.Match(@"[+>~]")) + return NodeProvider.Combinator(match.ToString(), parser.Tokenizer.GetNodeLocation(index)); + + return NodeProvider.Combinator(char.IsWhiteSpace(parser.Tokenizer.GetPreviousCharIgnoringComments()) ? " " : null, parser.Tokenizer.GetNodeLocation(index)); + } + + // + // A CSS Selector + // + // .class > div + h1 + // li a:hover + // + // Selectors are made out of one or more Elements, see above. + // + public Selector Selector(Parser parser) + { + Element e; + int realElements = 0; + + var elements = new NodeList(); + var index = parser.Tokenizer.Location.Index; + + GatherComments(parser); + PushComments(); + + if (parser.Tokenizer.Match('(')) + { + var sel = Entity(parser); + Expect(parser, ')'); + return NodeProvider.Selector(new NodeList() { NodeProvider.Element(null, sel, parser.Tokenizer.GetNodeLocation(index)) }, parser.Tokenizer.GetNodeLocation(index)); + } + + while (true) + { + e = Element(parser); + if (!e) + break; + + realElements++; + elements.Add(e); + } + + if (realElements > 0) + { + var selector = NodeProvider.Selector(elements, parser.Tokenizer.GetNodeLocation(index)); + selector.PostComments = GatherAndPullComments(parser); + PopComments(); + selector.PreComments = PullComments(); + + return selector; + } + + PopComments(); + //We have lost comments we have absorbed here. + //But comments should be absorbed before selectors... + return null; + } + + public Node Tag(Parser parser) + { + return parser.Tokenizer.Match(@"[a-zA-Z][a-zA-Z-]*[0-9]?") || parser.Tokenizer.Match('*'); + } + + public Node Attribute(Parser parser) + { + var index = parser.Tokenizer.Location.Index; + + if (!parser.Tokenizer.Match('[')) + return null; + + Node key = InterpolatedVariable(parser) || parser.Tokenizer.Match(@"(\\.|[a-z0-9_-])+", true) || Quoted(parser); + + if (!key) + { + return null; + } + + Node op = parser.Tokenizer.Match(@"[|~*$^]?="); + Node val = Quoted(parser) || parser.Tokenizer.Match(@"[\w-]+"); + + Expect(parser, ']'); + + return NodeProvider.Attribute(key, op, val, parser.Tokenizer.GetNodeLocation(index)); + } + + // + // The `block` rule is used by `ruleset` and `mixin.definition`. + // It's a wrapper around the `primary` rule, with added `{}`. + // + public NodeList Block(Parser parser) + { + if (!parser.Tokenizer.Match('{')) + return null; + + var content = Expect(Primary(parser), "Expected content inside block", parser); + + Expect(parser, '}'); + + return content; + } + + // + // div, .class, body > p {...} + // + public Ruleset Ruleset(Parser parser) + { + var selectors = new NodeList(); + + var memo = Remember(parser); + var index = memo.TokenizerLocation.Index; + + Selector s; + while (s = Selector(parser)) + { + selectors.Add(s); + if (!parser.Tokenizer.Match(',')) + break; + + GatherComments(parser); + } + + NodeList rules; + + if (selectors.Count > 0 && (rules = Block(parser)) != null) + { + return NodeProvider.Ruleset(selectors, rules, parser.Tokenizer.GetNodeLocation(index)); + } + + Recall(parser, memo); + + return null; + } + + public Rule Rule(Parser parser) + { + var memo = Remember(parser); + PushComments(); + + Variable variable = null; + string name = Property(parser); + bool interpolatedName = false; + + if (string.IsNullOrEmpty(name)) { + variable = Variable(parser); + if (variable != null) { + name = variable.Name; + } else { + var interpolation = InterpolatedVariable(parser); + if (interpolation != null) { + interpolatedName = true; + name = interpolation.Name; + } + } + } + + var postNameComments = GatherAndPullComments(parser); + + if (name != null && parser.Tokenizer.Match(':')) + { + Node value; + + var preValueComments = GatherAndPullComments(parser); + + if (name == "font") + { + value = Font(parser); + } + else if (MatchesProperty("filter", name)) + { + value = FilterExpressionList(parser) || Value(parser); + } + else + { + value = Value(parser); + } + + + // It's definitely a variable, but we couldn't parse the value to anything meaningful. + // However, the value might still be useful in another context, e.g. as part of a selector + // so let's catch the whole shebang: + if (variable != null && value == null) { + value = parser.Tokenizer.Match("[^;]*"); + } + + var postValueComments = GatherAndPullComments(parser); + + if (End(parser)) + { + if (value == null) + throw new ParsingException(name + " is incomplete", parser.Tokenizer.GetNodeLocation()); + + value.PreComments = preValueComments; + value.PostComments = postValueComments; + + var rule = NodeProvider.Rule(name, value, + parser.Tokenizer.GetNodeLocation(memo.TokenizerLocation.Index)); + if (interpolatedName) { + rule.InterpolatedName = true; + rule.Variable = false; + } + rule.PostNameComments = postNameComments; + PopComments(); + return rule; + } + } + + PopComments(); + Recall(parser, memo); + + return null; + } + + private bool MatchesProperty(string expectedPropertyName, string actualPropertyName) + { + if (string.Equals(expectedPropertyName, actualPropertyName)) + { + return true; + } + + return Regex.IsMatch(actualPropertyName, string.Format(@"-(\w+)-{0}", expectedPropertyName)); + } + + private CssFunctionList FilterExpressionList(Parser parser) + { + var list = new CssFunctionList(); + Node expression; + while (expression = FilterExpression(parser)) + { + list.Add(expression); + } + + if (!list.Any()) + { + return null; + } + + return list; + } + + private Node FilterExpression(Parser parser) + { + const string functionNameRegex = + @"\s*(blur|brightness|contrast|drop-shadow|grayscale|hue-rotate|invert|opacity|saturate|sepia|url)\s*\("; + + var index = parser.Tokenizer.Location.Index; + + GatherComments(parser); + + var url = Url(parser); + if (url) + { + return url; + } + + var nameToken = parser.Tokenizer.Match(functionNameRegex); + if (nameToken == null) + { + return null; + } + + var value = Value(parser); + if (value == null) + { + return null; + } + + Expect(parser, ')'); + + var result = NodeProvider.CssFunction(nameToken.Match.Groups[1].Value.Trim(), value, parser.Tokenizer.GetNodeLocation(index)); + result.PreComments = PullComments(); + result.PostComments = GatherAndPullComments(parser); + return result; + } + + // + // An @import directive + // + // @import "lib"; + // + // Depending on our environemnt, importing is done differently: + // In the browser, it's an XHR request, in Node, it would be a + // file-system operation. The function used for importing is + // stored in `import`, which we pass to the Import constructor. + // + public Import Import(Parser parser) + { + + var index = parser.Tokenizer.Location.Index; + + var importMatch = parser.Tokenizer.Match(@"@import(-(once))?\s+"); + if (!importMatch) { + return null; + } + + ImportOptions option = ParseOptions(parser); + + Node path = Quoted(parser) || Url(parser); + if (!path) { + return null; + } + + var features = MediaFeatures(parser); + + Expect(parser, ';', "Expected ';' (possibly unrecognised media sequence)"); + + if (path is Quoted) + return NodeProvider.Import(path as Quoted, features, option, parser.Tokenizer.GetNodeLocation(index)); + + if (path is Url) + return NodeProvider.Import(path as Url, features, option, parser.Tokenizer.GetNodeLocation(index)); + + throw new ParsingException("unrecognised @import format", parser.Tokenizer.GetNodeLocation(index)); + } + + private static ImportOptions ParseOptions(Parser parser) + { + var index = parser.Tokenizer.Location.Index; + var optionsMatch = parser.Tokenizer.Match(@"\((?.*)\)"); + if (!optionsMatch) { + return ImportOptions.Once; + } + + var allKeywords = optionsMatch.Match.Groups["keywords"].Value; + var keywords = allKeywords.Split(',').Select(kw => kw.Trim()); + + ImportOptions options = 0; + foreach (var keyword in keywords) + { + try + { + ImportOptions value = (ImportOptions) Enum.Parse(typeof (ImportOptions), keyword, true); + options |= value; + } + catch (ArgumentException) + { + throw new ParsingException(string.Format("unrecognized @import option '{0}'", keyword), parser.Tokenizer.GetNodeLocation(index)); + } + } + + CheckForConflictingOptions(parser, options, allKeywords, index); + + return options; + } + + private static readonly ImportOptions[][] illegalOptionCombinations = + { + new[] {ImportOptions.Css, ImportOptions.Less}, + new[] {ImportOptions.Inline, ImportOptions.Css}, + new[] {ImportOptions.Inline, ImportOptions.Less}, + new[] {ImportOptions.Inline, ImportOptions.Reference}, + new[] {ImportOptions.Once, ImportOptions.Multiple}, + new[] {ImportOptions.Reference, ImportOptions.Css}, + }; + private static void CheckForConflictingOptions(Parser parser, ImportOptions options, string allKeywords, int index) + { + foreach (var illegalCombination in illegalOptionCombinations) + { + if (IsOptionSet(options, illegalCombination[0]) && IsOptionSet(options, illegalCombination[1])) + { + throw new ParsingException( + string.Format( + "invalid combination of @import options ({0}) -- specify either {1} or {2}, but not both", + allKeywords, + illegalCombination[0].ToString().ToLowerInvariant(), + illegalCombination[1].ToString().ToLowerInvariant() + ), + parser.Tokenizer.GetNodeLocation(index)); + } + } + } + + private static bool IsOptionSet(ImportOptions options, ImportOptions test) + { + return (options & test) == test; + } + + // + // A CSS Directive + // + // @charset "utf-8"; + // + public Node Directive(Parser parser) + { + if (parser.Tokenizer.CurrentChar != '@') + return null; + + var import = Import(parser); + if (import) + return import; + + var media = Media(parser); + if (media) + return media; + + GatherComments(parser); + + var index = parser.Tokenizer.Location.Index; + + var name = parser.Tokenizer.MatchString(@"@[-a-z]+"); + if (string.IsNullOrEmpty(name)) + { + return null; + } + bool hasIdentifier = false, hasBlock = false, isKeyFrame = false; + NodeList rules, preRulesComments = null, preComments = null; + string identifierRegEx = @"[^{]+"; + string nonVendorSpecificName = name; + + if (name.StartsWith("@-") && name.IndexOf('-', 2) > 0) + { + nonVendorSpecificName = "@" + name.Substring(name.IndexOf('-', 2) + 1); + } + + switch (nonVendorSpecificName) + { + case "@font-face": + hasBlock = true; + break; + case "@page": + case "@document": + case "@supports": + hasBlock = true; + hasIdentifier = true; + break; + case "@viewport": + case "@top-left": + case "@top-left-corner": + case "@top-center": + case "@top-right": + case "@top-right-corner": + case "@bottom-left": + case "@bottom-left-corner": + case "@bottom-center": + case "@bottom-right": + case "@bottom-right-corner": + case "@left-top": + case "@left-middle": + case "@left-bottom": + case "@right-top": + case "@right-middle": + case "@right-bottom": + hasBlock = true; + break; + case "@keyframes": + isKeyFrame = true; + hasIdentifier = true; + break; + } + + string identifier = ""; + + preComments = PullComments(); + + if (hasIdentifier) + { + GatherComments(parser); + + var identifierRegResult = parser.Tokenizer.MatchAny(identifierRegEx); + if (identifierRegResult != null) + { + identifier = identifierRegResult.Value.Trim(); + } + } + + preRulesComments = GatherAndPullComments(parser); + + if (hasBlock) + { + rules = Block(parser); + + if (rules != null) { + rules.PreComments = preRulesComments; + return NodeProvider.Directive(name, identifier, rules, parser.Tokenizer.GetNodeLocation(index)); + } + } + else if (isKeyFrame) + { + var keyframeblock = KeyFrameBlock(parser, name, identifier, index); + keyframeblock.PreComments = preRulesComments; + return keyframeblock; + } + else + { + Node value; + if (value = Expression(parser)) { + value.PreComments = preRulesComments; + value.PostComments = GatherAndPullComments(parser); + + Expect(parser, ';', "missing semicolon in expression"); + + var directive = NodeProvider.Directive(name, value, parser.Tokenizer.GetNodeLocation(index)); + directive.PreComments = preComments; + return directive; + } + } + + throw new ParsingException("directive block with unrecognised format", parser.Tokenizer.GetNodeLocation(index)); + } + + public Expression MediaFeature(Parser parser) + { + NodeList features = new NodeList(); + var outerIndex = parser.Tokenizer.Location.Index; + + while (true) + { + GatherComments(parser); + + var keyword = Keyword(parser); + if (keyword) + { + keyword.PreComments = PullComments(); + keyword.PostComments = GatherAndPullComments(parser); + features.Add(keyword); + } + else if (parser.Tokenizer.Match('(')) + { + GatherComments(parser); + + var memo = Remember(parser); + var index = parser.Tokenizer.Location.Index; + var property = Property(parser); + + var preComments = GatherAndPullComments(parser); + + // in order to support (color) and have rule/*comment*/: we need to keep : + // out of property + if (!string.IsNullOrEmpty(property) && !parser.Tokenizer.Match(':')) + { + Recall(parser, memo); + property = null; + } + + GatherComments(parser); + + memo = Remember(parser); + + var entity = Entity(parser); + + if (!entity || !parser.Tokenizer.Match(')')) + { + Recall(parser, memo); + + // match "3/2" for instance + var unrecognised = parser.Tokenizer.Match(@"[^\){]+"); + if (unrecognised) + { + entity = NodeProvider.TextNode(unrecognised.Value, parser.Tokenizer.GetNodeLocation()); + Expect(parser, ')'); + } + } + + if (!entity) + { + return null; + } + + entity.PreComments = PullComments(); + entity.PostComments = GatherAndPullComments(parser); + + if (!string.IsNullOrEmpty(property)) + { + var rule = NodeProvider.Rule(property, entity, parser.Tokenizer.GetNodeLocation(index)); + rule.IsSemiColonRequired = false; + features.Add(NodeProvider.Paren(rule, parser.Tokenizer.GetNodeLocation(index))); + } + else + { + features.Add(NodeProvider.Paren(entity, parser.Tokenizer.GetNodeLocation(index))); + } + } + else + { + break; + } + } + + if (features.Count == 0) + return null; + + return NodeProvider.Expression(features, parser.Tokenizer.GetNodeLocation(outerIndex)); + } + + public Value MediaFeatures(Parser parser) + { + List features = new List(); + int index = parser.Tokenizer.Location.Index; + + while (true) + { + Node feature = MediaFeature(parser) || Variable(parser); + if (!feature) + { + return null; + } + + features.Add(feature); + + if (!parser.Tokenizer.Match(",")) + break; + } + + return NodeProvider.Value(features, null, parser.Tokenizer.GetNodeLocation(index)); + } + + public Media Media(Parser parser) + { + if (!parser.Tokenizer.Match("@media")) + return null; + + var index = parser.Tokenizer.Location.Index; + + var features = MediaFeatures(parser); + + var preRulesComments = GatherAndPullComments(parser); + + var rules = Expect(Block(parser), "@media block with unrecognised format", parser); + + rules.PreComments = preRulesComments; + return NodeProvider.Media(rules, features, parser.Tokenizer.GetNodeLocation(index)); + } + + public Directive KeyFrameBlock(Parser parser, string name, string identifier, int index) + { + if (!parser.Tokenizer.Match('{')) + return null; + + NodeList keyFrames = new NodeList(); + const string identifierRegEx = "from|to|([0-9\\.]+%)"; + + while (true) + { + GatherComments(parser); + + NodeList keyFrameElements = new NodeList(); + + while(true) { + RegexMatchResult keyFrameIdentifier; + + if (keyFrameElements.Count > 0) + { + keyFrameIdentifier = Expect(parser.Tokenizer.Match(identifierRegEx), "@keyframe block unknown identifier", parser); + } + else + { + keyFrameIdentifier = parser.Tokenizer.Match(identifierRegEx); + if (!keyFrameIdentifier) + { + break; + } + } + + keyFrameElements.Add(new Element(null, keyFrameIdentifier)); + + GatherComments(parser); + + if(!parser.Tokenizer.Match(",")) + break; + + GatherComments(parser); + } + + if (keyFrameElements.Count == 0) + break; + + var preComments = GatherAndPullComments(parser); + + var block = Expect(Block(parser), "Expected css block after key frame identifier", parser); + + block.PreComments = preComments; + block.PostComments = GatherAndPullComments(parser); + + keyFrames.Add(NodeProvider.KeyFrame(keyFrameElements, block, parser.Tokenizer.GetNodeLocation())); + } + + Expect(parser, '}', "Expected start, finish, % or '}}' but got {1}"); + + return NodeProvider.Directive(name, identifier, keyFrames, parser.Tokenizer.GetNodeLocation(index)); + } + + public Value Font(Parser parser) + { + var value = new NodeList(); + var expression = new NodeList(); + Node e; + + var index = parser.Tokenizer.Location.Index; + + while (e = Shorthand(parser) || Entity(parser)) + { + expression.Add(e); + } + value.Add(NodeProvider.Expression(expression, parser.Tokenizer.GetNodeLocation(index))); + + if (parser.Tokenizer.Match(',')) + { + while (e = Expression(parser)) + { + value.Add(e); + if (!parser.Tokenizer.Match(',')) + break; + } + } + return NodeProvider.Value(value, Important(parser), parser.Tokenizer.GetNodeLocation(index)); + } + + // + // A Value is a comma-delimited list of Expressions + // + // font-family: Baskerville, Georgia, serif; + // + // In a Rule, a Value represents everything after the `:`, + // and before the `;`. + // + public Value Value(Parser parser) + { + var expressions = new NodeList(); + + var index = parser.Tokenizer.Location.Index; + + Node e; + while (e = Expression(parser)) + { + expressions.Add(e); + if (!parser.Tokenizer.Match(',')) + break; + } + + GatherComments(parser); + + var important = string.Join( + " ", + new[] + { + IESlash9Hack(parser), + Important(parser) + }.Where(x => x != "").ToArray() + ); + + if (expressions.Count > 0 || parser.Tokenizer.Match(';')) + { + var value = NodeProvider.Value(expressions, important, parser.Tokenizer.GetNodeLocation(index)); + + if (!string.IsNullOrEmpty(important)) + { + value.PreImportantComments = PullComments(); + } + + return value; + } + + return null; + } + + public string Important(Parser parser) + { + var important = parser.Tokenizer.Match(@"!\s*important"); + + return important == null ? "" : important.Value; + } + + public string IESlash9Hack(Parser parser) + { + var slashNine = parser.Tokenizer.Match(@"\\9"); + return slashNine == null ? "" : slashNine.Value; + } + + public Expression Sub(Parser parser) + { + if (!parser.Tokenizer.Match('(')) + return null; + + var memo = Remember(parser); + + var e = Expression(parser); + if (e != null && parser.Tokenizer.Match(')')) + return e; + + Recall(parser, memo); + + return null; + } + + public Node Multiplication(Parser parser) + { + GatherComments(parser); + + var m = Operand(parser); + if (!m) + return null; + + Node operation = m; + + while (true) + { + GatherComments(parser); // after left operand + + var index = parser.Tokenizer.Location.Index; + var op = parser.Tokenizer.Match(@"[\/*]"); + + GatherComments(parser); // after operation + + Node a = null; + if (op && (a = Operand(parser))) + operation = NodeProvider.Operation(op.Value, operation, a, parser.Tokenizer.GetNodeLocation(index)); + else + break; + } + return operation; + } + + public Node UnicodeRange(Parser parser) + { + const string rangeRegex = "(U\\+[0-9a-f]+(-[0-9a-f]+))"; + const string valueOrWildcard = "(U\\+[0-9a-f?]+)"; + return parser.Tokenizer.Match(rangeRegex, true) + ?? parser.Tokenizer.Match(valueOrWildcard, true); + } + + public Node Operation(Parser parser) + { + bool isStrictMathMode = parser.StrictMath; + try + { + // Set Strict Math to false so as not to require extra parens in nested expressions + parser.StrictMath = false; + if (isStrictMathMode) + { + var beginParen = parser.Tokenizer.Match('('); + if (beginParen == null) + { + return null; + } + } + + var m = Multiplication(parser); + if (!m) + return null; + + Operation operation = null; + while (true) + { + GatherComments(parser); + + var index = parser.Tokenizer.Location.Index; + var op = parser.Tokenizer.Match(@"[-+]\s+"); + if (!op && !char.IsWhiteSpace(parser.Tokenizer.GetPreviousCharIgnoringComments())) + op = parser.Tokenizer.Match(@"[-+]"); + + Node a = null; + if (op && (a = Multiplication(parser))) + operation = NodeProvider.Operation(op.Value, operation ?? m, a, + parser.Tokenizer.GetNodeLocation(index)); + else + break; + } + + if (isStrictMathMode) + { + Expect(parser, ')', "Missing closing paren."); + } + + return operation ?? m; + } + finally + { + parser.StrictMath = isStrictMathMode; + } + } + + // + // An operand is anything that can be part of an operation, + // such as a Color, or a Variable + // + public Node Operand(Parser parser) + { + CharMatchResult negate = null; + + if (parser.Tokenizer.CurrentChar == '-' && parser.Tokenizer.Peek(@"-[@\(]")) + { + negate = parser.Tokenizer.Match('-'); + GatherComments(parser); + } + + var operand = Sub(parser) ?? + Dimension(parser) ?? + Color(parser) ?? + (Node)Variable(parser); + + if (operand != null) + { + return negate ? + NodeProvider.Operation("*", NodeProvider.Number("-1", "", negate.Location), operand, negate.Location) : + operand; + } + + if (parser.Tokenizer.CurrentChar == 'u' && parser.Tokenizer.Peek(@"url\(")) + return null; + + return Call(parser) || Keyword(parser); + } + + // + // Expressions either represent mathematical operations, + // or white-space delimited Entities. + // + // 1px solid black + // @var * 2 + // + public Expression Expression(Parser parser) + { + Node e; + var entities = new NodeList(); + + var index = parser.Tokenizer.Location.Index; + +#if CSS3EXPERIMENTAL + while (e = RepeatPattern(parser) || Operation(parser) || Entity(parser)) +#else + while (e = UnicodeRange(parser) || Operation(parser) || Entity(parser) || parser.Tokenizer.Match(@"[-+*/]")) +#endif + { + e.PostComments = PullComments(); + entities.Add(e); + } + + if (entities.Count > 0) + return NodeProvider.Expression(entities, parser.Tokenizer.GetNodeLocation(index)); + + return null; + } + +#if CSS3EXPERIMENTAL + /// + /// A repeat entity.. such as "(0.5in * *)[2]" + /// + public Node RepeatPattern(Parser parser) + { + if (parser.Tokenizer.Peek(@"\([^;{}\)]+\)\[")) { + var index = parser.Tokenizer.Location.Index; + + parser.Tokenizer.Match('('); + var value = Expression(parser); + Expect(parser, ')'); + Expect(parser, '['); + var repeat = Expect(Entity(parser), "Expected repeat entity", parser); + Expect(parser, ']'); + + return NodeProvider.RepeatEntity(NodeProvider.Paren(value, index), repeat, index); + } + + return null; + } +#endif + + public string Property(Parser parser) + { + var name = parser.Tokenizer.Match(@"\*?-?[-_a-zA-Z][-_a-z0-9A-Z]*"); + + if (name) + return name.Value; + + return null; + } + + public void Expect(Parser parser, char expectedString) + { + Expect(parser, expectedString, null); + } + + public void Expect(Parser parser, char expectedString, string message) + { + if (parser.Tokenizer.Match(expectedString)) + return; + + message = message ?? "Expected '{0}' but found '{1}'"; + + throw new ParsingException(string.Format(message, expectedString, parser.Tokenizer.NextChar), parser.Tokenizer.GetNodeLocation()); + } + + public T Expect(T node, string message, Parser parser) where T:Node + { + if (node) + return node; + + throw new ParsingException(message, parser.Tokenizer.GetNodeLocation()); + } + + + public class ParserLocation + { + public NodeList Comments { get; set; } + public Location TokenizerLocation { get; set; } + } + + public ParserLocation Remember(Parser parser) + { + return new ParserLocation() { Comments = CurrentComments, TokenizerLocation = parser.Tokenizer.Location }; + } + + public void Recall(Parser parser, ParserLocation location) + { + CurrentComments = location.Comments; + parser.Tokenizer.Location = location.TokenizerLocation; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tokenizer.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tokenizer.cs new file mode 100755 index 0000000..889f16d --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tokenizer.cs @@ -0,0 +1,563 @@ +using System.Diagnostics; + +namespace dotless.Core.Parser +{ + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using Exceptions; + using Infrastructure.Nodes; + using Utils; + + [DebuggerDisplay("{Remaining}")] + public class Tokenizer + { + public int Optimization { get; set; } + + private string _input; // LeSS input string + private List _chunks; // chunkified input + private int _i; // current index in `input` + private int _j; // current chunk + private int _current; // index of current chunk, in `input` + private int _lastCommentStart = -1; // the start of the last collection of comments + private int _lastCommentEnd = -1; // the end of the last collection of comments + private int _inputLength; + private readonly string _commentRegEx = @"(//[^\n]*|(/\*(.|[\r\n])*?\*/))"; + private readonly string _quotedRegEx = @"(""((?:[^""\\\r\n]|\\.)*)""|'((?:[^'\\\r\n]|\\.)*)')"; + private string _fileName; + + //Increasing throughput through tracing of Regex + private IDictionary regexCache = new Dictionary(); + + public Tokenizer(int optimization) + { + Optimization = optimization; + } + + public void SetupInput(string input, string fileName) + { + _fileName = fileName; + _i = _j = _current = 0; + _chunks = new List(); + _input = input.Replace("\r\n", "\n"); + _inputLength = _input.Length; + + // Split the input into chunks, + // Either delimited by /\n\n/ or + // delmited by '\n}' (see rationale above), + // depending on the level of optimization. + + if(Optimization == 0) + _chunks.Add(new Chunk(_input)); + else + { + var skip = new Regex(@"\G(@\{[a-zA-Z0-9_-]+\}|[^\""'{}/\\\(\)]+)"); + + var comment = GetRegex(this._commentRegEx, RegexOptions.None); + var quotedstring = GetRegex(this._quotedRegEx, RegexOptions.None); + var level = 0; + var lastBlock = 0; + var inParam = false; + + int i = 0; + while(i < _inputLength) + { + var match = skip.Match(_input, i); + if(match.Success) + { + Chunk.Append(match.Value, _chunks); + i += match.Length; + continue; + } + + var c = _input[i]; + + if(i < _inputLength - 1 && c == '/') + { + var cc = _input[i + 1]; + if ((!inParam && cc == '/') || cc == '*') + { + match = comment.Match(_input, i); + if(match.Success) + { + i += match.Length; + _chunks.Add(new Chunk(match.Value, ChunkType.Comment)); + continue; + } else + { + throw new ParsingException("Missing closing comment", GetNodeLocation(i)); + } + } + } + + if(c == '"' || c == '\'') + { + match = quotedstring.Match(_input, i); + if(match.Success) + { + i += match.Length; + _chunks.Add(new Chunk(match.Value, ChunkType.QuotedString)); + continue; + } else + { + throw new ParsingException(string.Format("Missing closing quote ({0})", c), GetNodeLocation(i)); + } + } + + // we are not in a quoted string or comment - process '{' level + if(!inParam && c == '{') + { + level++; + lastBlock = i; + } + else if (!inParam && c == '}') + { + level--; + + if(level < 0) + throw new ParsingException("Unexpected '}'", GetNodeLocation(i)); + + Chunk.Append(c, _chunks, true); + i++; + continue; + } if (c == '(') + { + inParam = true; + } + else if (c == ')') + { + inParam = false; + } + + Chunk.Append(c, _chunks); + i++; + } + + if(level > 0) + throw new ParsingException("Missing closing '}'", GetNodeLocation(lastBlock)); + + _input = Chunk.CommitAll(_chunks); + + _inputLength = _input.Length; + } + + Advance(0); // skip any whitespace characters at the start. + } + + public string GetComment() + { + // if we've hit the end we might still be looking at a valid chunk, so return early + if (_i == _inputLength) { + return null; + } + + string val; + int startI = _i; + int endI = 0; + + if (Optimization == 0) + { + if (this.CurrentChar != '/') + return null; + + var comment = this.Match(this._commentRegEx); + if (comment == null) + { + return null; + } + val = comment.Value; + endI = startI + comment.Value.Length; + } + else + { + if (_chunks[_j].Type == ChunkType.Comment) + { + val = _chunks[_j].Value; + endI = _i + _chunks[_j].Value.Length; + Advance(_chunks[_j].Value.Length); + } + else + { + return null; + } + } + + if (_lastCommentEnd != startI) + { + _lastCommentStart = startI; + } + + _lastCommentEnd = endI; + + return val; + } + + public string GetQuotedString() + { + // if we've hit the end we might still be looking at a valid chunk, so return early + if (_i == _inputLength) { + return null; + } + + if (Optimization == 0) { + if (this.CurrentChar != '"' && this.CurrentChar != '\'') + return null; + + var quotedstring = this.Match(this._quotedRegEx); + return quotedstring.Value; + } else { + if (_chunks[_j].Type == ChunkType.QuotedString) { + string val = _chunks[_j].Value; + Advance(_chunks[_j].Value.Length); + return val; + } + } + return null; + } + + public string MatchString(char tok) + { + var c = Match(tok); + + return c == null ? null : c.Value; + } + + public string MatchString(string tok) + { + var match = Match(tok); + + return match == null ? null : match.Value; + } + + // + // Parse from a token, regexp or string, and move forward if match + // + + public CharMatchResult Match(char tok) + { + if (_i == _inputLength || _chunks[_j].Type != ChunkType.Text) { + return null; + } + + if (_input[_i] == tok) + { + var index = _i; + + Advance(1); + + return new CharMatchResult(tok) { Location = GetNodeLocation(index) }; + } + + return null; + } + + + public RegexMatchResult Match(string tok) + { + return Match(tok, false); + } + + public RegexMatchResult Match(string tok, bool caseInsensitive) + { + if (_i == _inputLength || _chunks[_j].Type != ChunkType.Text) { + return null; + } + + var options = RegexOptions.None; + if (caseInsensitive) + options |= RegexOptions.IgnoreCase; + + var regex = GetRegex(tok, options); + + var match = regex.Match(_chunks[_j].Value, _i - _current); + + if (!match.Success) + return null; + + var index = _i; + + Advance(match.Length); + + return new RegexMatchResult(match) {Location = GetNodeLocation(index)}; + } + + // Match a string, but include the possibility of matching quoted and comments + public RegexMatchResult MatchAny(string tok) + { + if (_i == _inputLength) { + return null; + } + + var regex = GetRegex(tok, RegexOptions.None); + + var match = regex.Match(_input, _i); + + if (!match.Success) + return null; + + Advance(match.Length); + + if (_i > _current && _i < _current + _chunks[_j].Value.Length) + { + //If we absorbed the start of an inline comment then turn it into text so the rest can be absorbed + if (_chunks[_j].Type == ChunkType.Comment && _chunks[_j].Value.StartsWith("//")) + { + _chunks[_j].Type = ChunkType.Text; + } + } + + return new RegexMatchResult(match); + } + + public void Advance(int length) + { + if (_i == _inputLength) //only for empty cases as there may not be any chunks + return; + + // The match is confirmed, add the match length to `i`, + // and consume any extra white-space characters (' ' || '\n') + // which come after that. The reason for this is that LeSS's + // grammar is mostly white-space insensitive. + _i += length; + var endIndex = _current + _chunks[_j].Value.Length; + + while (true) + { + if(_i == _inputLength) + break; + + if (_i >= endIndex) + { + if (_j < _chunks.Count - 1) + { + _current = endIndex; + endIndex += _chunks[++_j].Value.Length; + continue; // allow skipping multiple chunks + } + else + break; + } + + if (!char.IsWhiteSpace(_input[_i])) + break; + + _i++; + } + } + + // Same as Match, but don't change the state of the parser, + // just return the match. + + public bool Peek(char tok) + { + if (_i == _inputLength) + return false; + + return _input[_i] == tok; + } + + public bool Peek(string tok) + { + var regex = GetRegex(tok, RegexOptions.None); + + var match = regex.Match(_input, _i); + + return match.Success; + } + + public bool PeekAfterComments(char tok) + { + var memo = this.Location; + + while(GetComment() != null); + + var peekSuccess = Peek(tok); + + this.Location = memo; + + return peekSuccess; + } + + private Regex GetRegex(string pattern, RegexOptions options) + { + if (!regexCache.ContainsKey(pattern)) + regexCache.Add(pattern, new Regex(@"\G" + pattern, options)); + + return regexCache[pattern]; + } + + public char GetPreviousCharIgnoringComments() + { + if (_i == 0) { + return '\0'; + } + + if (_i != _lastCommentEnd) { + return PreviousChar; + } + + int i = _lastCommentStart - 1; + + if (i < 0) { + return '\0'; + } + + return _input[i]; + } + + public char PreviousChar + { + get { return _i == 0 ? '\0' : _input[_i - 1]; } + } + + public char CurrentChar + { + get { return _i == _inputLength ? '\0' : _input[_i]; } + } + + public char NextChar + { + get { return _i + 1 == _inputLength ? '\0' : _input[_i + 1]; } + } + + public bool HasCompletedParsing() + { + return _i == _inputLength; + } + + public Location Location + { + get + { + return new Location + { + Index = _i, + CurrentChunk = _j, + CurrentChunkIndex = _current + }; + } + set + { + _i = value.Index; + _j = value.CurrentChunk; + _current = value.CurrentChunkIndex; + } + } + + public NodeLocation GetNodeLocation(int index) + { + return new NodeLocation(index, this._input, this._fileName); + } + + public NodeLocation GetNodeLocation() + { + return GetNodeLocation(this.Location.Index); + } + + private enum ChunkType + { + Text, + Comment, + QuotedString + } + + private class Chunk + { + private StringBuilder _builder; + + public Chunk(string val) + { + Value = val; + Type = ChunkType.Text; + } + + public Chunk(string val, ChunkType type) + { + Value = val; + Type = type; + } + + public Chunk() + { + _builder = new StringBuilder(); + Type = ChunkType.Text; + } + + public ChunkType Type { get; set; } + + public string Value { get; set; } + + private bool _final; + + public void Append(string str) + { + _builder.Append(str); + } + + public void Append(char c) + { + _builder.Append(c); + } + + private static Chunk ReadyForText(List chunks) + { + Chunk last = chunks.LastOrDefault(); + if (last == null || last.Type != ChunkType.Text || last._final == true) + { + last = new Chunk(); + chunks.Add(last); + } + return last; + } + + public static void Append(char c, List chunks, bool final) + { + Chunk chunk = ReadyForText(chunks); + chunk.Append(c); + chunk._final = final; + } + + public static void Append(char c, List chunks) + { + Chunk chunk = ReadyForText(chunks); + chunk.Append(c); + } + + public static void Append(string s, List chunks) + { + Chunk chunk = ReadyForText(chunks); + chunk.Append(s); + } + + public static string CommitAll(List chunks) + { + StringBuilder all = new StringBuilder(); + foreach(Chunk chunk in chunks) + { + if (chunk._builder != null) + { + string val = chunk._builder.ToString(); + chunk._builder = null; + chunk.Value = val; + } + + all.Append(chunk.Value); + } + return all.ToString(); + } + } + + private string Remaining + { + get { return _input.Substring(_i); } + } + } + + public class Location + { + public int Index { get; set; } + public int CurrentChunk { get; set; } + public int CurrentChunkIndex { get; set; } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Alpha.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Alpha.cs new file mode 100755 index 0000000..886a45b --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Alpha.cs @@ -0,0 +1,35 @@ +namespace dotless.Core.Parser.Tree +{ + using Infrastructure; + using Infrastructure.Nodes; + + public class Alpha : Call + { + public Node Value { get; set; } + + public Alpha(Node value) + { + Value = value; + } + + public override Node Evaluate(Env env) + { + Value = Value.Evaluate(env); + + return this; + } + + public override void AppendCSS(Env env) + { + env.Output + .Append("alpha(opacity=") + .Append(Value) + .Append(")"); + } + + public override void Accept(Plugins.IVisitor visitor) + { + Value = VisitAndReplace(Value, visitor); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Assignment.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Assignment.cs new file mode 100755 index 0000000..aeabc61 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Assignment.cs @@ -0,0 +1,39 @@ +namespace dotless.Core.Parser.Tree +{ + using Infrastructure; + using Infrastructure.Nodes; + + public class Assignment : Node + { + public string Key { get; set; } + public Node Value { get; set; } + + public Assignment(string key, Node value) + { + Key = key; + Value = value; + } + + public override Node Evaluate(Env env) + { + return new Assignment(Key, Value.Evaluate(env)) {Location = Location}; + } + + protected override Node CloneCore() { + return new Assignment(Key, Value.Clone()); + } + + public override void AppendCSS(Env env) + { + env.Output + .Append(Key) + .Append("=") + .Append(Value); + } + + public override void Accept(Plugins.IVisitor visitor) + { + Value = VisitAndReplace(Value, visitor); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Attribute.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Attribute.cs new file mode 100755 index 0000000..a1ab74e --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Attribute.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using dotless.Core.Parser.Infrastructure; +using dotless.Core.Parser.Infrastructure.Nodes; + +namespace dotless.Core.Parser.Tree { + public class Attribute : Node + { + public Node Name { get; set; } + public Node Op { get; set; } + public Node Value { get; set; } + + public Attribute(Node name, Node op, Node value) + { + Name = name; + Op = op; + Value = value; + } + + protected override Node CloneCore() { + return new Attribute(Name.Clone(), Op.Clone(), Value.Clone()); + } + + public override Node Evaluate(Env env) + { + return new TextNode(string.Format("[{0}{1}{2}]", + Name.Evaluate(env).ToCSS(env), + Op == null ? "" : Op.Evaluate(env).ToCSS(env), + Value == null ? "" : Value.Evaluate(env).ToCSS(env))); + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Call.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Call.cs new file mode 100755 index 0000000..477804f --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Call.cs @@ -0,0 +1,65 @@ +using System; + +namespace dotless.Core.Parser.Tree +{ + using System.Linq; + using Infrastructure; + using Infrastructure.Nodes; + using Plugins; + + public class Call : Node + { + public string Name { get; set; } + public NodeList Arguments { get; set; } + + public Call(string name, NodeList arguments) + { + Name = name; + Arguments = arguments; + } + + protected Call() + { + } + + protected override Node CloneCore() { + return new Call(Name, (NodeList) Arguments.Clone()); + } + + public override Node Evaluate(Env env) + { + if (env == null) + { + throw new ArgumentNullException("env"); + } + + var args = Arguments.Select(a => a.Evaluate(env)); + + var function = env.GetFunction(Name); + + if (function != null) + { + function.Name = Name; + function.Location = Location; + return function.Call(env, args).ReducedFrom(this); + } + + env.Output.Push(); + + env.Output + .Append(Name) + .Append("(") + .AppendMany(args, env.Compress ? "," : ", ") + .Append(")"); + + var css = env.Output.Pop(); + + return new TextNode(css.ToString()).ReducedFrom(this); + } + + public override void Accept(IVisitor visitor) + { + Arguments = VisitAndReplace(Arguments, visitor); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Color.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Color.cs new file mode 100755 index 0000000..1ee1baf --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Color.cs @@ -0,0 +1,467 @@ +namespace dotless.Core.Parser.Tree +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using Exceptions; + using Infrastructure; + using Infrastructure.Nodes; + using Utils; + + public class Color : Node, IOperable, IComparable + { + private static readonly Dictionary Html4Colors = new Dictionary + { + {"aliceblue", 0xf0f8ff}, + {"antiquewhite", 0xfaebd7}, + {"aqua", 0x00ffff}, + {"aquamarine", 0x7fffd4}, + {"azure", 0xf0ffff}, + {"beige", 0xf5f5dc}, + {"bisque", 0xffe4c4}, + {"black", 0x000000}, + {"blanchedalmond", 0xffebcd}, + {"blue", 0x0000ff}, + {"blueviolet", 0x8a2be2}, + {"brown", 0xa52a2a}, + {"burlywood", 0xdeb887}, + {"cadetblue", 0x5f9ea0}, + {"chartreuse", 0x7fff00}, + {"chocolate", 0xd2691e}, + {"coral", 0xff7f50}, + {"cornflowerblue", 0x6495ed}, + {"cornsilk", 0xfff8dc}, + {"crimson", 0xdc143c}, + {"cyan", 0x00ffff}, + {"darkblue", 0x00008b}, + {"darkcyan", 0x008b8b}, + {"darkgoldenrod", 0xb8860b}, + {"darkgray", 0xa9a9a9}, + {"darkgrey", 0xa9a9a9}, + {"darkgreen", 0x006400}, + {"darkkhaki", 0xbdb76b}, + {"darkmagenta", 0x8b008b}, + {"darkolivegreen", 0x556b2f}, + {"darkorange", 0xff8c00}, + {"darkorchid", 0x9932cc}, + {"darkred", 0x8b0000}, + {"darksalmon", 0xe9967a}, + {"darkseagreen", 0x8fbc8f}, + {"darkslateblue", 0x483d8b}, + {"darkslategray", 0x2f4f4f}, + {"darkslategrey", 0x2f4f4f}, + {"darkturquoise", 0x00ced1}, + {"darkviolet", 0x9400d3}, + {"deeppink", 0xff1493}, + {"deepskyblue", 0x00bfff}, + {"dimgray", 0x696969}, + {"dimgrey", 0x696969}, + {"dodgerblue", 0x1e90ff}, + {"firebrick", 0xb22222}, + {"floralwhite", 0xfffaf0}, + {"forestgreen", 0x228b22}, + {"fuchsia", 0xff00ff}, + {"gainsboro", 0xdcdcdc}, + {"ghostwhite", 0xf8f8ff}, + {"gold", 0xffd700}, + {"goldenrod", 0xdaa520}, + {"gray", 0x808080}, + {"grey", 0x808080}, + {"green", 0x008000}, + {"greenyellow", 0xadff2f}, + {"honeydew", 0xf0fff0}, + {"hotpink", 0xff69b4}, + {"indianred", 0xcd5c5c}, + {"indigo", 0x4b0082}, + {"ivory", 0xfffff0}, + {"khaki", 0xf0e68c}, + {"lavender", 0xe6e6fa}, + {"lavenderblush", 0xfff0f5}, + {"lawngreen", 0x7cfc00}, + {"lemonchiffon", 0xfffacd}, + {"lightblue", 0xadd8e6}, + {"lightcoral", 0xf08080}, + {"lightcyan", 0xe0ffff}, + {"lightgoldenrodyellow", 0xfafad2}, + {"lightgray", 0xd3d3d3}, + {"lightgrey", 0xd3d3d3}, + {"lightgreen", 0x90ee90}, + {"lightpink", 0xffb6c1}, + {"lightsalmon", 0xffa07a}, + {"lightseagreen", 0x20b2aa}, + {"lightskyblue", 0x87cefa}, + {"lightslategray", 0x778899}, + {"lightslategrey", 0x778899}, + {"lightsteelblue", 0xb0c4de}, + {"lightyellow", 0xffffe0}, + {"lime", 0x00ff00}, + {"limegreen", 0x32cd32}, + {"linen", 0xfaf0e6}, + {"magenta", 0xff00ff}, + {"maroon", 0x800000}, + {"mediumaquamarine", 0x66cdaa}, + {"mediumblue", 0x0000cd}, + {"mediumorchid", 0xba55d3}, + {"mediumpurple", 0x9370d8}, + {"mediumseagreen", 0x3cb371}, + {"mediumslateblue", 0x7b68ee}, + {"mediumspringgreen", 0x00fa9a}, + {"mediumturquoise", 0x48d1cc}, + {"mediumvioletred", 0xc71585}, + {"midnightblue", 0x191970}, + {"mintcream", 0xf5fffa}, + {"mistyrose", 0xffe4e1}, + {"moccasin", 0xffe4b5}, + {"navajowhite", 0xffdead}, + {"navy", 0x000080}, + {"oldlace", 0xfdf5e6}, + {"olive", 0x808000}, + {"olivedrab", 0x6b8e23}, + {"orange", 0xffa500}, + {"orangered", 0xff4500}, + {"orchid", 0xda70d6}, + {"palegoldenrod", 0xeee8aa}, + {"palegreen", 0x98fb98}, + {"paleturquoise", 0xafeeee}, + {"palevioletred", 0xd87093}, + {"papayawhip", 0xffefd5}, + {"peachpuff", 0xffdab9}, + {"peru", 0xcd853f}, + {"pink", 0xffc0cb}, + {"plum", 0xdda0dd}, + {"powderblue", 0xb0e0e6}, + {"purple", 0x800080}, + {"red", 0xff0000}, + {"rosybrown", 0xbc8f8f}, + {"royalblue", 0x4169e1}, + {"saddlebrown", 0x8b4513}, + {"salmon", 0xfa8072}, + {"sandybrown", 0xf4a460}, + {"seagreen", 0x2e8b57}, + {"seashell", 0xfff5ee}, + {"sienna", 0xa0522d}, + {"silver", 0xc0c0c0}, + {"skyblue", 0x87ceeb}, + {"slateblue", 0x6a5acd}, + {"slategray", 0x708090}, + {"slategrey", 0x708090}, + {"snow", 0xfffafa}, + {"springgreen", 0x00ff7f}, + {"steelblue", 0x4682b4}, + {"tan", 0xd2b48c}, + {"teal", 0x008080}, + {"thistle", 0xd8bfd8}, + {"tomato", 0xff6347}, + {"turquoise", 0x40e0d0}, + {"violet", 0xee82ee}, + {"wheat", 0xf5deb3}, + {"white", 0xffffff}, + {"whitesmoke", 0xf5f5f5}, + {"yellow", 0xffff00}, + {"yellowgreen", 0x9acd32} + }; + + private static readonly Dictionary Html4ColorsReverse = + Html4Colors.GroupBy(kvp => kvp.Value).ToDictionary(g => g.Key, g => g.First().Key); + + public static Color From(string keywordOrHex) + { + return GetColorFromKeyword(keywordOrHex) ?? FromHex(keywordOrHex); + } + + public static Color GetColorFromKeyword(string keyword) + { + if (keyword == "transparent") + { + return new Color(0, 0, 0, 0.0, keyword); + } + + int rgb; + if (Html4Colors.TryGetValue(keyword, out rgb)) + { + var r = (rgb >> 16) & 0xFF; + var g = (rgb >> 8) & 0xFF; + var b = rgb & 0xFF; + return new Color(r, g, b, 1.0, keyword); + } + + return null; + } + + public static string GetKeyword(int[] rgb) { + var color = (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]; + + string keyword; + if (Html4ColorsReverse.TryGetValue(color, out keyword)) { + return keyword; + } + + return null; + } + + public static Color FromHex(string hex) + { + hex = hex.TrimStart('#'); + double[] rgb; + var alpha = 1.0; + var text = '#' + hex; + + if (hex.Length == 8) + { + rgb = ParseRgb(hex.Substring(2)); + alpha = Parse(hex.Substring(0, 2))/255.0; + } + else if (hex.Length == 6) + { + rgb = ParseRgb(hex); + } + else + { + rgb = hex.ToCharArray().Select(c => Parse("" + c + c)).ToArray(); + } + + return new Color(rgb, alpha, text); + } + + private static double[] ParseRgb(string hex) + { + return Enumerable.Range(0, 3) + .Select(i => hex.Substring(i*2, 2)) + .Select(Parse) + .ToArray(); + } + + private static double Parse(string hex) + { + return int.Parse(hex, NumberStyles.HexNumber); + } + + private readonly string _text; + + public Color(int color) { + RGB = new double[3]; + + B = color & 0xff; + color >>= 8; + G = color & 0xff; + color >>= 8; + R = color & 0xff; + Alpha = 1; + } + + public Color(string hex) { + hex = hex.TrimStart('#'); + double[] rgb; + var alpha = 1.0; + var text = '#' + hex; + + if (hex.Length == 8) + { + rgb = ParseRgb(hex.Substring(2)); + alpha = Parse(hex.Substring(0, 2))/255.0; + } + else if (hex.Length == 6) + { + rgb = ParseRgb(hex); + } + else + { + rgb = hex.ToCharArray().Select(c => Parse("" + c + c)).ToArray(); + } + + R = rgb[0]; + G = rgb[1]; + B = rgb[2]; + Alpha = alpha; + + _text = text; + } + + public Color(IEnumerable rgb, Number alpha) { + RGB = rgb.Select(d => d.Normalize()).ToArray(); + + Alpha = alpha.Normalize(); + } + + public Color(double r, double g, double b) : this(r, g, b, 1) { + + } + + public Color(double r, double g, double b, double alpha) : this(new[] {r, g, b}, alpha) { + + } + + public Color(double[] rgb) : this(rgb, 1) { + + } + + public Color(double[] rgb, double alpha) : this(rgb, alpha, null) { + + } + + public Color(double[] rgb, double alpha, string text) + { + RGB = rgb.Select(c => NumberExtensions.Normalize(c, 255.0)).ToArray(); + Alpha = NumberExtensions.Normalize(alpha, 1.0); + _text = text; + } + + public Color(double red, double green, double blue, double alpha = 1.0, string text = null) + : this(new[] {red, green, blue}, alpha, text) + { + } + + // TODO: A RGB color should really be represented by int[], or better: a compressed int. + public readonly double[] RGB = new double[3]; + public readonly double Alpha; + + public double R + { + get { return RGB[0]; } + set { RGB[0] = value; } + } + + public double G + { + get { return RGB[1]; } + set { RGB[1] = value; } + } + + public double B + { + get { return RGB[2]; } + set { RGB[2] = value; } + } + + /// + /// Transforms the linear to SRBG. Formula derivation decscribed here + /// + /// The linear channel, for example R/255 + /// The sRBG value for the given channel + private double TransformLinearToSrbg(double linearChannel) + { + const double decodingGamma = 2.4; + const double phi = 12.92; + const double alpha = .055; + return (linearChannel <= 0.03928) ? linearChannel / phi : Math.Pow(((linearChannel + alpha) / (1 + alpha)), decodingGamma); + } + + /// + /// Calculates the luma value based on the W3 Standard + /// + /// + /// The luma value for the current color + /// + public double Luma + { + get + { + var linearR = R / 255; + var linearG = G / 255; + var linearB = B / 255; + + var red = TransformLinearToSrbg(linearR); + var green = TransformLinearToSrbg(linearG); + var blue = TransformLinearToSrbg(linearB); + + return 0.2126 * red + 0.7152 * green + 0.0722 * blue; + } + } + + protected override Node CloneCore() { + return new Color(RGB.ToArray(), Alpha); + } + + public override void AppendCSS(Env env) + { + if (_text != null) + { + env.Output.Append(_text); + return; + } + + var rgb = ConvertToInt(RGB); + + if (Alpha < 1.0) + { + env.Output.AppendFormat("rgba({0}, {1}, {2}, {3})", rgb[0], rgb[1], rgb[2], Alpha); + return; + } + + var hex = GetHexString(rgb); + env.Output.Append(hex); + } + + private List ConvertToInt(IEnumerable rgb) + { + return rgb.Select(d => (int) Math.Round(d, MidpointRounding.AwayFromZero)).ToList(); + } + + private string GetHexString(IEnumerable rgb) + { + return '#' + rgb.Select(i => i.ToString("x2")).JoinStrings(""); + } + + public Node Operate(Operation op, Node other) + { + var color = other as Color; + var operable = other as IOperable; + + if (color == null && operable == null) + { + var msg = string.Format("Unable to convert right hand side of {0} to a color", op.Operator); + throw new ParsingException(msg, op.Location); + } + + color = color ?? operable.ToColor(); + + var rgb = Enumerable.Range(0, 3) + .Select(i => Operation.Operate(op.Operator, RGB[i], color.RGB[i])) + .ToArray(); + + return new Color(rgb, 1.0).ReducedFrom(this, other); + } + + public Color ToColor() + { + return this; + } + + /// + /// Returns in the IE ARGB format e.g ##FF001122 = rgba(0x00, 0x11, 0x22, 1) + /// + /// + public string ToArgb() + { + var values = new[] {Alpha*255}.Concat(RGB); + var argb = ConvertToInt(values); + return GetHexString(argb); + } + + public int CompareTo(object obj) + { + var col = obj as Color; + + if (col == null) + { + return -1; + } + + if (col.R == R && col.G == G && col.B == B && col.Alpha == Alpha) + { + return 0; + } + + return (((256 * 3) - (col.R + col.G + col.B)) * col.Alpha) < (((256 * 3) - (R + G + B)) * Alpha) ? 1 : -1; + } + + public static explicit operator System.Drawing.Color(Color color) + { + if (color == null) + throw new ArgumentNullException("color"); + + return System.Drawing.Color.FromArgb((int) Math.Round(color.Alpha * 255d), (int) color.R, (int) color.G, (int) color.B); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Combinator.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Combinator.cs new file mode 100755 index 0000000..523abc1 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Combinator.cs @@ -0,0 +1,43 @@ +namespace dotless.Core.Parser.Tree +{ + using System.Collections.Generic; + using Infrastructure; + using Infrastructure.Nodes; + + public class Combinator : Node + { + public string Value { get; set; } + + public Combinator(string value) + { + if (string.IsNullOrEmpty(value)) + Value = ""; + else if (value == " ") + Value = " "; + else + Value = value.Trim(); + } + + protected override Node CloneCore() { + return new Combinator(Value); + } + + public override void AppendCSS(Env env) + { + env.Output.Append(GetValue(env)); + } + + private string GetValue(Env env) { + switch (Value) { + case "+": + return env.Compress ? "+" : " + "; + case "~": + return env.Compress ? "~" : " ~ "; + case ">": + return env.Compress ? ">" : " > "; + default: + return Value; + } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Comment.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Comment.cs new file mode 100755 index 0000000..c59cb05 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Comment.cs @@ -0,0 +1,40 @@ +namespace dotless.Core.Parser.Tree +{ + using Infrastructure; + using Infrastructure.Nodes; + + public class Comment : Node + { + public string Value { get; set; } + public bool IsValidCss { get; set; } + public bool IsSpecialCss { get; set; } + public bool IsPreSelectorComment { get; set; } + private bool IsCSSHack { get; set; } + + public Comment(string value) + { + Value = value; + IsValidCss = !value.StartsWith("//"); + IsSpecialCss = value.StartsWith("/**") || value.StartsWith("/*!"); + IsCSSHack = value == "/**/" || value == "/*\\*/"; + } + + protected override Node CloneCore() { + return new Comment(Value); + } + + public override void AppendCSS(Env env) + { + if (IsReference || env.IsCommentSilent(IsValidCss, IsCSSHack, IsSpecialCss)) { + return; + } + + env.Output.Append(Value); + + if (!IsCSSHack && IsPreSelectorComment) + { + env.Output.Append("\n"); + } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Condition.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Condition.cs new file mode 100755 index 0000000..226c730 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Condition.cs @@ -0,0 +1,139 @@ +using System.Linq; + +namespace dotless.Core.Parser.Tree +{ + using Infrastructure; + using Infrastructure.Nodes; + using Plugins; + using System; + using dotless.Core.Exceptions; + + public class Condition : Node + { + public Node Left { get; set; } + public Node Right { get; set; } + public string Operation { get; set; } + public bool Negate { get; set; } + + public bool IsDefault { get; private set; } + + public Condition(Node left, string operation, Node right, bool negate) + { + Left = left; + Right = right; + Operation = operation.Trim(); + Negate = negate; + } + + protected override Node CloneCore() { + return new Condition(Left.Clone(), Operation, Right.Clone(), Negate); + } + + public override void AppendCSS(Env env) + { + } + + public override Node Evaluate(Env env) + { + var leftCall = Left as Call; + if (leftCall != null && leftCall.Name == "default") { + IsDefault = true; + } + + var lValue = Left.Evaluate(env); + var rValue = Right.Evaluate(env); + + bool value = Evaluate(lValue, Operation, rValue); + + if (Negate) + { + value = !value; + } + + return new BooleanNode(value); + } + + private bool Evaluate(Node lValue, string operation, Node rValue) + { + switch (operation) + { + case "or": + return ToBool(lValue) || ToBool(rValue); + case "and": + return ToBool(lValue) && ToBool(rValue); + default: + int result; + IComparable lValueComparable = lValue as IComparable; + if (lValueComparable != null) + { + result = lValueComparable.CompareTo(rValue); + } + else + { + IComparable rValueComparable = rValue as IComparable; + if (rValueComparable != null) + { + result = rValueComparable.CompareTo(lValue); + + // had to compare right to left so reverse + if (result < 0) + { + result = 1; + } + else if (result > 0) + { + result = -1; + } + } + else + { + throw new ParsingException("Cannot compare objects in mixin guard condition", Location); + } + } + + if (result == 0) + { + return operation == "=" || operation == ">=" || operation == "=<"; + } + else if (result < 0) + { + return operation == "<" || operation == "=<"; + } + else if (result > 0) + { + return operation == ">" || operation == ">="; + } + break; + } + + throw new ParsingException("C# compiler can't work out it is impossible to get here", Location); + } + + /// + /// Evaluates and returns if the condition passes true + /// + public bool Passes(Env env) + { + return ToBool(Evaluate(env)); + } + + /// + /// At the moment should only need to be called on the eval'd result of a Condition + /// + private static bool ToBool(Node node) + { + BooleanNode bNode = node as BooleanNode; + + if (bNode == null) + return false; + + return bNode.Value; + } + + public override void Accept(IVisitor visitor) + { + Left = VisitAndReplace(Left, visitor); + Right = VisitAndReplace(Right, visitor); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/CssFunction.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/CssFunction.cs new file mode 100755 index 0000000..9564525 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/CssFunction.cs @@ -0,0 +1,46 @@ +using System.Linq; +using dotless.Core.Parser.Infrastructure; +using dotless.Core.Parser.Infrastructure.Nodes; + +namespace dotless.Core.Parser.Tree +{ + public class CssFunction : Node + { + public string Name { get; set; } + + public Node Value { get; set; } + + protected override Node CloneCore() { + return new CssFunction() {Name = Name, Value = Value.Clone()}; + } + + public override void AppendCSS(Env env) + { + env.Output.Append(string.Format("{0}({1})", Name, Value.ToCSS(env))); + } + + public override Node Evaluate(Env env) { + var node = (CssFunction)Clone(); + node.Value = Value.Evaluate(env); + return node; + } + } + + public class CssFunctionList : NodeList + { + public override void AppendCSS(Env env) + { + env.Output.AppendMany(Inner, " "); + } + + protected override Node CloneCore() { + return new CssFunctionList(); + } + + public override Node Evaluate(Env env) { + var node = (CssFunctionList) Clone(); + node.Inner = Inner.Select(i => i.Evaluate(env)).ToList(); + return node; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Directive.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Directive.cs new file mode 100755 index 0000000..20d3319 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Directive.cs @@ -0,0 +1,104 @@ +namespace dotless.Core.Parser.Tree +{ + using System.Collections.Generic; + using System.Linq; + using Infrastructure; + using Infrastructure.Nodes; + + public class Directive : Ruleset + { + public string Name { get; set; } + public string Identifier { get; set; } + public Node Value { get; set; } + + public Directive(string name, string identifier, NodeList rules) + { + Name = name; + Rules = rules; + Identifier = identifier; + } + + public Directive(string name, Node value) + { + Name = name; + Value = value; + } + + protected Directive() + { + } + + protected override Node CloneCore() { + if (Rules != null) { + return new Directive(Name, Identifier, (NodeList) Rules.Clone()); + } + return new Directive(Name, Value.Clone()); + } + + public override void Accept(Plugins.IVisitor visitor) + { + Rules = VisitAndReplace(Rules, visitor); + Value = VisitAndReplace(Value, visitor); + } + + public override Node Evaluate(Env env) + { + env.Frames.Push(this); + + Directive evaldDirective; + + if (Rules != null) + { + evaldDirective = new Directive(Name, Identifier, new NodeList(Rules.Select(r => r.Evaluate(env))).ReducedFrom(Rules)); + } + else + { + evaldDirective = new Directive(Name, Value.Evaluate(env)); + } + + evaldDirective.IsReference = IsReference; + + env.Frames.Pop(); + + return evaldDirective; + } + + public override void AppendCSS(Env env, Context context) + { + if (IsReference) + { + return; + } + + if (env.Compress && Rules != null && !Rules.Any()) + return; + + env.Output.Append(Name); + + if (!string.IsNullOrEmpty(Identifier)) + { + env.Output.Append(" "); + env.Output.Append(Identifier); + } + + if (Rules != null) + { + // Append pre comments as we out put each rule ourselves + if (Rules.PreComments) + { + env.Output.Append(Rules.PreComments); + } + + AppendRules(env); + env.Output.Append("\n"); + } + else + { + env.Output + .Append(" ") + .Append(Value) + .Append(";\n"); + } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Element.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Element.cs new file mode 100755 index 0000000..cc3e3ed --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Element.cs @@ -0,0 +1,87 @@ +namespace dotless.Core.Parser.Tree +{ + using Infrastructure; + using Infrastructure.Nodes; + using Plugins; + + public class Element : Node + { + public Combinator Combinator { get; set; } + public string Value { get; set; } + public Node NodeValue { get; set; } + + public Element(Combinator combinator, string textValue) : this(combinator) + { + Value = textValue.Trim(); + } + + public Element(Combinator combinator, Node value) : this(combinator) + { + if (value is TextNode textValue && !(value is Quoted)) + { + Value = textValue.Value.Trim(); + } + else + { + NodeValue = value; + } + } + + private Element(Combinator combinator) + { + Combinator = combinator ?? new Combinator(""); + } + + public override Node Evaluate(Env env) + { + if (NodeValue != null) + { + var newNodeValue = NodeValue.Evaluate(env); + + return new Element(Combinator, newNodeValue) + .ReducedFrom(this); + } + else + return this; + } + + protected override Node CloneCore() { + if (NodeValue != null) { + return new Element((Combinator) Combinator.Clone(), NodeValue.Clone()); + } + + return new Element((Combinator) Combinator.Clone(), Value); + } + + public override void AppendCSS(Env env) + { + env.Output + .Append(Combinator) + .Push(); + + if (NodeValue != null) + { + env.Output.Append(NodeValue) + .Trim(); + } + else + { + env.Output.Append(Value); + } + + env.Output + .PopAndAppend(); + } + + public override void Accept(IVisitor visitor) + { + Combinator = VisitAndReplace(Combinator, visitor); + + NodeValue = VisitAndReplace(NodeValue, visitor, true); + } + + internal Element Clone() { + return new Element(Combinator) { Value = Value, NodeValue = NodeValue}; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Expression.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Expression.cs new file mode 100755 index 0000000..4c816c3 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Expression.cs @@ -0,0 +1,53 @@ +namespace dotless.Core.Parser.Tree +{ + using Infrastructure; + using Infrastructure.Nodes; + using System.Linq; + using System.Collections.Generic; + using Plugins; + + public class Expression : Node + { + public NodeList Value { get; set; } + private bool IsExpressionList { get; set; } + + public Expression(IEnumerable value) : this(value, false) + { + } + + public Expression(IEnumerable value, bool isExpressionList) + { + IsExpressionList = isExpressionList; + + if(value is NodeList) + Value = value as NodeList; + else + Value = new NodeList(value); + } + + public override Node Evaluate(Env env) + { + if (Value.Count > 1) + return new Expression(new NodeList(Value.Select(e => e.Evaluate(env))), IsExpressionList).ReducedFrom(this); + + if (Value.Count == 1) + return Value[0].Evaluate(env).ReducedFrom(this); + + return this; + } + + protected override Node CloneCore() { + return new Expression((NodeList)Value.Clone(), IsExpressionList); + } + + public override void AppendCSS(Env env) + { + env.Output.AppendMany(Value, IsExpressionList ? ", " : " "); + } + + public override void Accept(IVisitor visitor) + { + Value = VisitAndReplace(Value, visitor); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Extend.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Extend.cs new file mode 100755 index 0000000..0176bf7 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Extend.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using dotless.Core.Parser.Infrastructure; +using dotless.Core.Parser.Infrastructure.Nodes; + +namespace dotless.Core.Parser.Tree +{ + public class Extend : Node + { + public Extend(List exact, List partial) + { + Exact = exact; + Partial = partial; + } + + public List Exact{ get; set; } + public List Partial { get; set; } + + + public override Node Evaluate(Env env) + { + var newExact = new List(); + foreach (var e in Exact) + { + var childContext = env.CreateChildEnv(); + e.AppendCSS(childContext); + var selector = new Selector(new []{new Element(e.Elements.First().Combinator,childContext.Output.ToString().Trim())}); + selector.IsReference = IsReference; + newExact.Add(selector); + } + + var newPartial = new List(); + foreach (var e in Partial) + { + var childContext = env.CreateChildEnv(); + e.AppendCSS(childContext); + var selector = new Selector(new[] { new Element(e.Elements.First().Combinator, childContext.Output.ToString().Trim()) }); + selector.IsReference = IsReference; + newPartial.Add(selector); + } + + return new Extend(newExact,newPartial) { IsReference = IsReference, Location = Location }; + } + + protected override Node CloneCore() { + return new Extend(Exact.Select(e => (Selector)e.Clone()).ToList(), Partial.Select(e => (Selector)e.Clone()).ToList()); + } + + public override void AppendCSS(Env env) + { + + } + + public override bool IgnoreOutput() { + return true; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/GuardedRuleset.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/GuardedRuleset.cs new file mode 100755 index 0000000..ce7deb6 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/GuardedRuleset.cs @@ -0,0 +1,37 @@ +using System.Linq; +using dotless.Core.Parser.Infrastructure; +using dotless.Core.Parser.Infrastructure.Nodes; +using dotless.Core.Plugins; + +namespace dotless.Core.Parser.Tree +{ + public class GuardedRuleset : Ruleset + { + public GuardedRuleset(NodeList selectors, NodeList rules, Condition condition) + : base(selectors, rules) + { + this.Condition = condition; + } + + public Condition Condition { get; set; } + + public override Node Evaluate(Env env) + { + if (Condition.Passes(env)) + { + return EvaluateRulesForFrame(this, env); + } + return new NodeList(); + } + + public override void Accept(IVisitor visitor) + { + base.Accept(visitor); + Condition = VisitAndReplace(Condition, visitor); + } + + public override void AppendCSS(Env env) + { + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Import.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Import.cs new file mode 100755 index 0000000..1ccfd70 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Import.cs @@ -0,0 +1,239 @@ +using System; +using System.Linq; +using dotless.Core.Plugins; + +namespace dotless.Core.Parser.Tree +{ + using System.IO; + using Importers; + using Infrastructure; + using Infrastructure.Nodes; + using Utils; + using dotless.Core.Exceptions; + + public class Import : Directive + { + private readonly ReferenceVisitor referenceVisitor = new ReferenceVisitor(true); + /// + /// The original path node + /// + protected Node OriginalPath { get; set; } + + public string Path { get; set; } + + /// + /// The inner root - if the action is ImportLess + /// + public Ruleset InnerRoot { get; set; } + + /// + /// The inner content - if the action is ImportCss + /// + public string InnerContent { get; set; } + + /// + /// The media features (if present) + /// + public Node Features { get; set; } + + /// + /// The type of import: reference, inline, less, css, once, multiple or optional + /// + public ImportOptions ImportOptions { get; set; } + + /// + /// The action to perform with this node + /// + private ImportAction? _importAction; + + public Import(Quoted path, Value features, ImportOptions option) + : this((Node)path, features, option) + { + OriginalPath = path; + } + + public Import(Url path, Value features, ImportOptions option) + : this((Node)path, features, option) + { + OriginalPath = path; + Path = path.GetUnadjustedUrl(); + } + + /// + /// Create a evaluated node that will render a @import + /// + /// + /// + private Import(Node originalPath, Node features) + { + OriginalPath = originalPath; + Features = features; + _importAction = ImportAction.LeaveImport; + } + + private Import(Node path, Value features, ImportOptions option) + { + if (path == null) + throw new ParserException("Imports do not allow expressions"); + + OriginalPath = path; + Features = features; + ImportOptions = option; + } + + private ImportAction GetImportAction(IImporter importer) + { + if (!_importAction.HasValue) + { + _importAction = importer.Import(this); + } + + return _importAction.Value; + } + + public override void AppendCSS(Env env, Context context) + { + ImportAction action = GetImportAction(env.Parser.Importer); + if (action == ImportAction.ImportNothing) + { + return; + } + + if (action == ImportAction.ImportCss) + { + env.Output.Append(InnerContent); + return; + } + + env.Output.Append("@import ") + .Append(OriginalPath.ToCSS(env)); + + if (Features) + { + env.Output + .Append(" ") + .Append(Features); + } + env.Output.Append(";"); + + if (!env.Compress) + { + env.Output.Append("\n"); + } + } + + public override void Accept(Plugins.IVisitor visitor) + { + Features = VisitAndReplace(Features, visitor, true); + + if (_importAction == ImportAction.ImportLess) + { + InnerRoot = VisitAndReplace(InnerRoot, visitor); + } + } + + public override Node Evaluate(Env env) + { + OriginalPath = OriginalPath.Evaluate(env); + var quoted = OriginalPath as Quoted; + if (quoted != null) + { + Path = quoted.Value; + } + + ImportAction action = GetImportAction(env.Parser.Importer); + if (action == Importers.ImportAction.ImportNothing) + { + return new NodeList().ReducedFrom(this); + } + + Node features = null; + + if (Features) + features = Features.Evaluate(env); + + if (action == ImportAction.LeaveImport) + return new Import(OriginalPath, features); + + if (action == ImportAction.ImportCss) + { + var importCss = new Import(OriginalPath, null) { _importAction = ImportAction.ImportCss, InnerContent = InnerContent }; + if (features) + return new Media(features, new NodeList() { importCss }); + return importCss; + } + + using (env.Parser.Importer.BeginScope(this)) + { + if (IsReference || IsOptionSet(ImportOptions, ImportOptions.Reference)) + { + // Walk the parse tree and mark all nodes as references. + IsReference = true; + + Accept(referenceVisitor); + } + NodeHelper.RecursiveExpandNodes(env, InnerRoot); + } + + var rulesList = new NodeList(InnerRoot.Rules).ReducedFrom(this); + if (features) + { + return new Media(features, rulesList); + } + + return rulesList; + } + private bool IsOptionSet(ImportOptions options, ImportOptions test) + { + return (options & test) == test; + } + } + + public class ReferenceVisitor : IVisitor { + private readonly bool isReference; + + public ReferenceVisitor(bool isReference) { + this.isReference = isReference; + } + + public Node Visit(Node node) { + var ruleset = node as Ruleset; + if (ruleset != null) { + if (ruleset.Selectors != null) { + ruleset.Selectors.Accept(this); + ruleset.Selectors.IsReference = isReference; + } + + if (ruleset.Rules != null) { + ruleset.Rules.Accept(this); + ruleset.Rules.IsReference = isReference; + } + } + + var media = node as Media; + if (media != null) { + media.Ruleset.Accept(this); + media.Ruleset.IsReference = isReference; + } + + var nodeList = node as NodeList; + if (nodeList != null) { + nodeList.Accept(this); + } + node.IsReference = isReference; + + return node; + } + } + + [Flags] + public enum ImportOptions { + Once = 1, + Multiple = 2, + Optional = 4, + Css = 8, + Less = 16, + Inline = 32, + Reference = 64 + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/KeyFrame.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/KeyFrame.cs new file mode 100755 index 0000000..1b3a789 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/KeyFrame.cs @@ -0,0 +1,48 @@ +namespace dotless.Core.Parser.Tree +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using dotless.Core.Parser.Infrastructure.Nodes; + using dotless.Core.Parser.Infrastructure; + + public class KeyFrame : Ruleset + { + public NodeList Identifiers { get; set; } + + public KeyFrame(NodeList identifiers, NodeList rules) + { + Identifiers = identifiers; + Rules = rules; + } + + public override Node Evaluate(Env env) + { + env.Frames.Push(this); + + Rules = new NodeList(Rules.Select(r => r.Evaluate(env))).ReducedFrom(Rules); + + env.Frames.Pop(); + return this; + } + + public override void Accept(Plugins.IVisitor visitor) + { + Rules = VisitAndReplace(Rules, visitor); + } + + public override void AppendCSS(Env env, Context context) + { + env.Output.AppendMany(Identifiers, env.Compress ? "," : ", "); + + // Append pre comments as we out put each rule ourselves + if (Rules.PreComments) + { + env.Output.Append(Rules.PreComments); + } + + AppendRules(env); + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Keyword.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Keyword.cs new file mode 100755 index 0000000..f0680da --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Keyword.cs @@ -0,0 +1,44 @@ +namespace dotless.Core.Parser.Tree +{ + using Infrastructure; + using Infrastructure.Nodes; + using System; + + public class Keyword : Node, IComparable + { + public string Value { get; set; } + + public Keyword(string value) + { + Value = value; + } + + public override Node Evaluate(Env env) + { + return ((Node) Color.GetColorFromKeyword(Value) ?? this).ReducedFrom(this); + } + + protected override Node CloneCore() { + return new Keyword(Value); + } + + public override void AppendCSS(Env env) + { + env.Output.Append(Value); + } + + public override string ToString() + { + return Value; + } + + public int CompareTo(object obj) + { + if (obj == null) + { + return -1; + } + return obj.ToString().CompareTo(ToString()); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Media.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Media.cs new file mode 100755 index 0000000..a695c6c --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Media.cs @@ -0,0 +1,315 @@ +using System; +using dotless.Core.Utils; + +namespace dotless.Core.Parser.Tree +{ + using System.Collections.Generic; + using System.Linq; + using Infrastructure; + using Infrastructure.Nodes; + + public class Media : Ruleset + { + public Node Features { get; set; } + public Ruleset Ruleset { get; set; } + public List Extensions { get; set; } + + public Media(Node features, NodeList rules) + : this(features, new Ruleset(GetEmptySelector(), rules),null) + { + } + + public Media(Node features, Ruleset ruleset, List extensions) + { + Features = features; + Ruleset = ruleset; + Extensions = extensions ?? new List(); + } + + protected override Node CloneCore() { + return new Media(Features.Clone(), (Ruleset)Ruleset.Clone(), Extensions); + } + + public static NodeList GetEmptySelector() + { + return new NodeList() { new Selector(new NodeList() { new Element(new Combinator(""), "&") }) }; + } + + public override void Accept(Plugins.IVisitor visitor) + { + Features = VisitAndReplace(Features, visitor); + + Ruleset = VisitAndReplace(Ruleset, visitor); + } + + public override Node Evaluate(Env env) + { + int blockIndex = env.MediaBlocks.Count; + env.MediaBlocks.Add(this); + env.MediaPath.Push(this); + + env.Frames.Push(Ruleset); + NodeHelper.ExpandNodes(env, Ruleset.Rules); + env.Frames.Pop(); + Features = Features.Evaluate(env); + var ruleset = Ruleset.Evaluate(env) as Ruleset; + + var media = new Media(Features, ruleset,Extensions).ReducedFrom(this); + + env.MediaPath.Pop(); + env.MediaBlocks[blockIndex] = media; + + if (env.MediaPath.Count == 0) + { + return media.EvalTop(env); + } + else + { + return media.EvalNested(env, Features, ruleset); + } + } + + /// + /// Simple case for the top media node being evaluated + /// + protected Node EvalTop(Env env) + { + Node result; + + // Render all dependent Media blocks. + if (env.MediaBlocks.Count > 1) + { + result = new Ruleset(GetEmptySelector(), new NodeList(env.MediaBlocks.Cast())) { MultiMedia = true } + .ReducedFrom(this); + } + else + { + result = env.MediaBlocks[0]; + } + + env.MediaPath.Clear(); + env.MediaBlocks.Clear(); + + return result; + } + + /// + /// Evaluate when you have a media inside another media + /// + protected Node EvalNested(Env env, Node features, Ruleset ruleset) + { + var path = new NodeList(env.MediaPath.ToList()); + path.Add(this); + + NodeList derivedPath = new NodeList(); + + // Extract the media-query conditions separated with `,` (OR). + for (int i = 0; i < path.Count; i++) + { + Node pathComponent; + Value value = path[i].Features as Value; + if (value != null) + { + pathComponent = value.Values; + } + else + { + pathComponent = path[i].Features; + } + + derivedPath.Add((pathComponent as NodeList) ?? new NodeList() { pathComponent }); + } + + // Trace all permutations to generate the resulting media-query. + // + // (a, b and c) with nested (d, e) -> + // a and d + // a and e + // b and c and d + // b and c and e + + NodeList pathWithAnds = new NodeList(); + + foreach (NodeList node in derivedPath) + { + pathWithAnds.Add(node); + pathWithAnds.Add(new NodeList() { new TextNode("and") }); + } + + pathWithAnds.RemoveAt(pathWithAnds.Count - 1); + + Features = new Value(Permute(pathWithAnds), null); + + // Fake a tree-node that doesn't output anything. + return new Ruleset(new NodeList(), new NodeList()); + } + + /// + /// Flattens a list of nodes seperated by comma so that all the conditions are on the bottom. + /// e.g. + /// (A) and (B) and (C) => A and B and C + /// (A, B) and (D) and (C) => A and D and C, B and D and C + /// (A) and (B) and (C, D) => A and B and C, A and B and D + /// + /// It does this by generating a list of permutations for the last n-1 then n-2 + /// and with each call it multiplies out the OR'd elements + /// + private NodeList Permute(NodeList arr) + { + // in simple cases return + if (arr.Count == 0) + return new NodeList(); + + if (arr.Count == 1) + { + return arr[0]; + } + + NodeList returner = new NodeList(); + + // run permute on the next n-1 + NodeList sliced = new NodeList(arr.Skip(1)); + NodeList rest = Permute(sliced); + + //now multiply + for (int i = 0; i < rest.Count; i++) + { + NodeList inner = arr[0]; + for (int j = 0; j < inner.Count; j++) + { + NodeList newl = new NodeList(); + newl.Add(inner[j]); + + NodeList addition = rest[i] as NodeList; + if (addition) + { + newl.AddRange(addition); + } + else + { + newl.Add(rest[i]); + } + + //add an expression so it seperated by spaces + returner.Add(new Expression(newl)); + } + } + + return returner; + } + + /// + /// Copies selectors to media statements currently bubbling up, so they are not lost + /// + /// + public void BubbleSelectors(NodeList selectors) + { + Ruleset = new Ruleset(new NodeList(selectors), new NodeList() { Ruleset }); + } + + public override void AppendCSS(Env env, Context ctx) + { + if (env.Compress && !Ruleset.Rules.Any()) + return; + + // first deal with the contents, in case its empty + env.Output.Push(); + + Ruleset.IsRoot = ctx.Count == 0; + + //Track the last media block being appended for extender filters + env.ExtendMediaScope.Push(this); + + // Set the current feeatures to filter extenders + Ruleset.AppendCSS(env, ctx); + + env.ExtendMediaScope.Pop(); + + if (!env.Compress) + env.Output.Trim().Indent(2); + + var contents = env.Output.Pop(); + + // If we're the result of a reference import and none of the rulesets + // have been unmarked as references, skip the whole block. + if (IsReference && Ruleset.Rules.All(r => r.IsReference)) + { + return; + } + + // if we have no contents, skip + if (env.Compress && contents.Length == 0) + return; + + // go ahead and output + env.Output.Append("@media"); + + if (Features) + { + env.Output.Append(' '); + env.Output.Append(Features); + } + + if (env.Compress) + env.Output.Append('{'); + else + env.Output.Append(" {\n"); + + + env.Output.Append(contents); + + if (env.Compress) + env.Output.Append('}'); + else + env.Output.Append("\n}\n"); + } + + public void AddExtension(Selector selector, Extend extends, Env env) + { + foreach (var extending in extends.Exact) + { + Extender match = null; + if ((match = Extensions.OfType().FirstOrDefault(e => e.BaseSelector.ToString().Trim() == extending.ToString().Trim())) == null) + { + match = new ExactExtender(extending, extends); + Extensions.Add(match); + } + + match.AddExtension(selector, env); + } + + foreach (var extending in extends.Partial) + { + Extender match = null; + if ((match = Extensions.OfType().FirstOrDefault(e => e.BaseSelector.ToString().Trim() == extending.ToString().Trim())) == null) + { + match = new PartialExtender(extending, extends); + Extensions.Add(match); + } + + match.AddExtension(selector, env); + } + } + public IEnumerable FindUnmatchedExtensions() { + return Extensions.Where(e => !e.IsMatched); + } + + public ExactExtender FindExactExtension(string selection) + { + return Extensions.OfType().FirstOrDefault(e => e.BaseSelector.ToString().Trim() == selection); + } + + public PartialExtender[] FindPartialExtensions(Context selection) + { + return Extensions.OfType() + .WhereExtenderMatches(selection) + .ToArray(); + } + + [Obsolete("This method doesn't return the correct results. Use FindPartialExtensions(Context) instead.", false)] + public PartialExtender[] FindPartialExtensions(string selection) + { + return Extensions.OfType().Where(e => selection.Contains(e.BaseSelector.ToString().Trim())).ToArray(); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/MixinCall.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/MixinCall.cs new file mode 100755 index 0000000..8490ff0 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/MixinCall.cs @@ -0,0 +1,201 @@ +namespace dotless.Core.Parser.Tree +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Exceptions; + using Infrastructure; + using Infrastructure.Nodes; + using Utils; + using Plugins; + + public class MixinCall : Node + { + public List Arguments { get; set; } + public Selector Selector { get; set; } + public bool Important { get; set; } + + public MixinCall(NodeList elements, List arguments, bool important) + { + Important = important; + Selector = new Selector(elements); + Arguments = arguments; + } + + protected override Node CloneCore() { + return new MixinCall( + new NodeList(Selector.Elements.Select(e => e.Clone())), + Arguments, + Important); + } + + public override Node Evaluate(Env env) + { + var closures = env.FindRulesets(Selector); + if (closures == null) + throw new ParsingException(Selector.ToCSS(env).Trim() + " is undefined", Location); + + env.Rule = this; + + var rules = new NodeList(); + + if (PreComments) + rules.AddRange(PreComments); + + var rulesetList = closures.ToList(); + + // To address bug https://github.com/dotless/dotless/issues/136, where a mixin and ruleset selector may have the same name, we + // need to favour matching a MixinDefinition with the required Selector and only fall back to considering other Ruleset types + // if no match is found. + // However, in order to support having a regular ruleset with the same name as a parameterized + // mixin (see https://github.com/dotless/dotless/issues/387), we need to take argument counts into account, so we make the + // decision after evaluating for argument match. + + var mixins = rulesetList.Where(c => c.Ruleset is MixinDefinition).ToList(); + + var defaults = new List(); + + bool foundMatches = false, foundExactMatches = false, foundDefaultMatches = false; + foreach (var closure in mixins) + { + var ruleset = (MixinDefinition)closure.Ruleset; + var matchType = ruleset.MatchArguments(Arguments, env); + if (matchType == MixinMatch.ArgumentMismatch) + { + continue; + } + + if (matchType == MixinMatch.Default) { + defaults.Add(closure); + foundDefaultMatches = true; + + continue; + } + + foundMatches = true; + + if (matchType == MixinMatch.GuardFail) + { + continue; + } + + foundExactMatches = true; + + try + { + var closureEnvironment = env.CreateChildEnvWithClosure(closure); + rules.AddRange(ruleset.Evaluate(Arguments, closureEnvironment).Rules); + } + catch (ParsingException e) + { + throw new ParsingException(e.Message, e.Location, Location); + } + } + + if (!foundExactMatches && foundDefaultMatches) { + foreach (var closure in defaults) { + try { + var closureEnvironment = env.CreateChildEnvWithClosure(closure); + var ruleset = (MixinDefinition) closure.Ruleset; + rules.AddRange(ruleset.Evaluate(Arguments, closureEnvironment).Rules); + } catch (ParsingException e) { + throw new ParsingException(e.Message, e.Location, Location); + } + } + foundMatches = true; + } + + if (!foundMatches) + { + var regularRulesets = rulesetList.Except(mixins); + + foreach (var closure in regularRulesets) + { + if (closure.Ruleset.Rules != null) { + var nodes = (NodeList)closure.Ruleset.Rules.Clone(); + NodeHelper.ExpandNodes(env, nodes); + + rules.AddRange(nodes); + } + + foundMatches = true; + } + } + + if (PostComments) + rules.AddRange(PostComments); + + env.Rule = null; + + if (!foundMatches) + { + var message = String.Format("No matching definition was found for `{0}({1})`", + Selector.ToCSS(env).Trim(), + Arguments.Select(a => a.Value.ToCSS(env)).JoinStrings(env.Compress ? "," : ", ")); + throw new ParsingException(message, Location); + } + + rules.Accept(new ReferenceVisitor(IsReference)); + + if (Important) + { + return MakeRulesImportant(rules); + } + + return rules; + } + + public override void Accept(IVisitor visitor) + { + Selector = VisitAndReplace(Selector, visitor); + + foreach (var a in Arguments) + { + a.Value = VisitAndReplace(a.Value, visitor); + } + } + + private Ruleset MakeRulesetImportant(Ruleset ruleset) + { + return new Ruleset(ruleset.Selectors, MakeRulesImportant(ruleset.Rules)).ReducedFrom(ruleset); + } + + private NodeList MakeRulesImportant(NodeList rules) + { + var importantRules = new NodeList(); + foreach (var node in rules) + { + if (node is MixinCall) + { + var original = (MixinCall)node; + importantRules.Add(new MixinCall(original.Selector.Elements, new List(original.Arguments), true).ReducedFrom(node)); + } + else if (node is Rule) + { + importantRules.Add(MakeRuleImportant((Rule) node)); + } + else if (node is Ruleset) + { + importantRules.Add(MakeRulesetImportant((Ruleset) node)); + } + else + { + importantRules.Add(node); + } + } + return importantRules; + } + + private Rule MakeRuleImportant(Rule rule) + { + var valueNode = rule.Value; + var value = valueNode as Value; + value = value != null + ? new Value(value.Values, "!important").ReducedFrom(value) + : new Value(new NodeList {valueNode}, "!important"); + + var importantRule = new Rule(rule.Name, value).ReducedFrom(rule); + return importantRule; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/MixinDefinition.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/MixinDefinition.cs new file mode 100755 index 0000000..0c74073 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/MixinDefinition.cs @@ -0,0 +1,195 @@ +namespace dotless.Core.Parser.Tree +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Exceptions; + using Infrastructure; + using Infrastructure.Nodes; + using Utils; + + public class MixinDefinition : Ruleset + { + private int _required; + private int _arity; + public string Name { get; set; } + public NodeList Params { get; set; } + public Condition Condition { get; set; } + public bool Variadic { get; set; } + + public MixinDefinition(string name, NodeList parameters, NodeList rules, Condition condition, bool variadic) + { + Name = name; + Params = parameters; + Rules = rules; + Condition = condition; + Variadic = variadic; + Selectors = new NodeList {new Selector(new NodeList(new Element(null, name)))}; + + _arity = Params.Count; + _required = Params.Count(r => String.IsNullOrEmpty(r.Name) || r.Value == null); + } + + public override Node Evaluate(Env env) + { + return this; + } + + public Ruleset EvaluateParams(Env env, List args) + { + var arguments = new Dictionary(); + args = args ?? new List(); + + var hasNamedArgs = false; + foreach (var arg in args) + { + if (!string.IsNullOrEmpty(arg.Name)) + { + hasNamedArgs = true; + + arguments[arg.Name] = new Rule(arg.Name, arg.Value.Evaluate(env)) { Location = arg.Value.Location }; + } + else if (hasNamedArgs) + throw new ParsingException("Positional arguments must appear before all named arguments.", arg.Value.Location); + } + + for (var i = 0; i < Params.Count; i++) + { + if (String.IsNullOrEmpty(Params[i].Name)) + continue; + + if (arguments.ContainsKey(Params[i].Name)) + continue; + + Node val; + if (i < args.Count && string.IsNullOrEmpty(args[i].Name)) + val = args[i].Value; + else + { + //evaluate in scope of mixin definition? + val = Params[i].Value; + } + + if (val) + { + Node argRuleValue; + if (Params[i].Variadic) + { + NodeList varArgs = new NodeList(); + for (int j = i; j < args.Count; j++) + { + varArgs.Add(args[j].Value.Evaluate(env)); + } + + argRuleValue = (new Expression(varArgs)).Evaluate(env); + } + else + { + argRuleValue = val.Evaluate(env); + } + arguments[Params[i].Name] = new Rule(Params[i].Name, argRuleValue) { Location = val.Location }; + } + else + throw new ParsingException( + String.Format("wrong number of arguments for {0} ({1} for {2})", Name, + args != null ? args.Count : 0, _arity), Location); + } + + var argumentNodes = new List(); + + for(var i = 0; i < Math.Max(Params.Count, args.Count); i++) + { + argumentNodes.Add(i < args.Count ? args[i].Value : Params[i].Value); + } + + var frame = new Ruleset(new NodeList(), new NodeList()); + + frame.Rules.Insert(0, new Rule("@arguments", new Expression(argumentNodes.Where(a => a != null)).Evaluate(env))); + + foreach (var arg in arguments) + { + frame.Rules.Add(arg.Value); + } + + return frame; + } + + [Obsolete("This method will be removed in a future release. Use Evaluate(List, Env) instead.", false)] + public Ruleset Evaluate(List args, Env env, List closureFrames) + { + var childEnv = env.CreateChildEnvWithClosure(new Closure() {Context = closureFrames, Ruleset = this}); + return Evaluate(args, childEnv); + } + + public Ruleset Evaluate(List args, Env env) + { + var frame = EvaluateParams(env, args); + + var context = env.CreateChildEnv(); + context.Frames.Push(this); + context.Frames.Push(frame); + + var result = EvaluateRulesForFrame(frame, context); + + context.Frames.Pop(); + context.Frames.Pop(); + + return result; + } + + public override MixinMatch MatchArguments(List arguments, Env env) + { + var argsLength = arguments != null ? arguments.Count : 0; + + if (!Variadic) + { + if (argsLength < _required) + return MixinMatch.ArgumentMismatch; + if (argsLength > _arity) + return MixinMatch.ArgumentMismatch; + } + + if (Condition) + { + env.Frames.Push(EvaluateParams(env, arguments)); + + bool isPassingConditions = Condition.Passes(env); + + env.Frames.Pop(); + if (Condition.IsDefault) { + return MixinMatch.Default; + } + + + if (!isPassingConditions) + return MixinMatch.GuardFail; + } + + for (var i = 0; i < Math.Min(argsLength, _arity); i++) + { + if (String.IsNullOrEmpty(Params[i].Name)) + { + if (arguments[i].Value.Evaluate(env).ToCSS(env) != Params[i].Value.Evaluate(env).ToCSS(env)) + { + return MixinMatch.ArgumentMismatch; + } + } + } + + return MixinMatch.Pass; + } + + public override void Accept(Plugins.IVisitor visitor) + { + base.Accept(visitor); + + Params = VisitAndReplace(Params, visitor); + Condition = VisitAndReplace(Condition, visitor, true); + } + + public override void AppendCSS(Env env, Context context) + { + + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Number.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Number.cs new file mode 100755 index 0000000..93ef07a --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Number.cs @@ -0,0 +1,140 @@ +namespace dotless.Core.Parser.Tree +{ + using System.Globalization; + using Infrastructure; + using Infrastructure.Nodes; + using Utils; + using System; + + public class Number : Node, IOperable, IComparable + { + public double Value { get; set; } + public string Unit { get; set; } + + private bool preferUnitFromSecondOperand; + + public Number(string value, string unit) + { + Value = double.Parse(value, CultureInfo.InvariantCulture); + Unit = unit; + } + + public Number(double value, string unit) + { + Value = value; + Unit = unit; + } + + public Number(double value) + : this(value, "") + { + } + + /// + /// Formats the value (no unit) based on precision. + /// + private string FormatValue() + { + return Value.ToString("0." +new string('#', GetPrecision()), CultureInfo.InvariantCulture); + } + + /// + /// Gets the precision for the value based on the unit + /// + private int GetPrecision() + { + //TODO: It would be nice to look up which units have sensible precision. + // e.g. do sub pixels or sub points make sense? + // e.g. does it make sense for anything other than em to have more precision? Radians? + // + // For now, follow less.js and allow any number of decimals + return 9; + } + + protected override Node CloneCore() { + return new Number(Value, Unit); + } + + public override void AppendCSS(Env env) + { + env.Output + .Append(FormatValue()) + .Append(Unit); + } + + + // In an operation between two Dimensions, + // we default to the first Number's unit, + // so `1px + 2em` will yield `3px`. + // In the future, we could implement some unit + // conversions such that `100cm + 10mm` would yield + // `101cm`. + public Node Operate(Operation op, Node other) + { + Guard.ExpectNode(other, "right hand side of " + op.Operator, op.Location); + + var dim = (Number) other; + + var unit = Unit; + var otherUnit = dim.Unit; + + if (preferUnitFromSecondOperand && !string.IsNullOrEmpty(otherUnit)) { + unit = otherUnit; + } else if (string.IsNullOrEmpty(unit)) { + unit = otherUnit; + } else if (!string.IsNullOrEmpty(otherUnit)) { + // convert units + } + + return new Number(Operation.Operate(op.Operator, Value, dim.Value), unit) { + // less.js treats division as a special case: if it's the only operation, + // units are kept. However, if the result is then operated on again, and + // the second operand has a unit, it gets the unit from that operand. + preferUnitFromSecondOperand = unit == otherUnit && op.Operator == "/" + }.ReducedFrom(this, other); + } + + public Color ToColor() + { + return new Color(Value, Value, Value); + } + + public double ToNumber() + { + return ToNumber(1d); + } + + public double ToNumber(double max) + { + return Unit == "%" ? Value*max/100d : Value; + } + + public static Number operator -(Number n) + { + return new Number(-n.Value, n.Unit); + } + + public int CompareTo(object obj) + { + Number n = obj as Number; + + if (n) + { + if (n.Value > Value) + { + return -1; + } + else if (n.Value < Value) + { + return 1; + } + else + { + return 0; + } + } + + return -1; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Operation.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Operation.cs new file mode 100755 index 0000000..305434e --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Operation.cs @@ -0,0 +1,87 @@ +namespace dotless.Core.Parser.Tree +{ + using System; + using Exceptions; + using Infrastructure; + using Infrastructure.Nodes; + using Plugins; + + public class Operation : Node + { + public Node First { get; set; } + public Node Second { get; set; } + public string Operator { get; set; } + + public Operation(string op, Node first, Node second) + { + First = first; + Second = second; + Operator = op.Trim(); + } + + protected override Node CloneCore() { + return new Operation(Operator, First.Clone(), Second.Clone()); + } + + public override Node Evaluate(Env env) + { + var a = First.Evaluate(env); + var b = Second.Evaluate(env); + + if (a is Number && b is Color) + { + if (Operator == "*" || Operator == "+") + { + var temp = b; + b = a; + a = temp; + } + else + throw new ParsingException("Can't substract or divide a color from a number", Location); + } + + try + { + var operable = a as IOperable; + if (operable != null) + return operable.Operate(this, b).ReducedFrom(this); + + throw new ParsingException(string.Format("Cannot apply operator {0} to the left hand side: {1}", Operator, a.ToCSS(env)), Location); + } + catch (DivideByZeroException e) + { + throw new ParsingException(e, Location); + } + catch (InvalidOperationException e) + { + throw new ParsingException(e, Location); + } + } + + public static double Operate(string op, double first, double second) + { + if(op == "/" && second == 0) + throw new DivideByZeroException(); + + switch (op) + { + case "+": + return first + second; + case "-": + return first - second; + case "*": + return first * second; + case "/": + return first / second; + default: + throw new InvalidOperationException("Unknown operator"); + } + } + + public override void Accept(IVisitor visitor) + { + First = VisitAndReplace(First, visitor); + Second = VisitAndReplace(Second, visitor); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Paren.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Paren.cs new file mode 100755 index 0000000..55540c4 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Paren.cs @@ -0,0 +1,38 @@ +namespace dotless.Core.Parser.Tree +{ + using Infrastructure; + using Infrastructure.Nodes; + using Plugins; + + public class Paren : Node + { + public Node Value { get; set; } + + public Paren(Node value) + { + Value = value; + } + + protected override Node CloneCore() { + return new Paren(Value.Clone()); + } + + public override void AppendCSS(Env env) + { + env.Output + .Append('(') + .Append(Value) + .Append(')'); + } + + public override Node Evaluate(Env env) + { + return new Paren(Value.Evaluate(env)); + } + + public override void Accept(IVisitor visitor) + { + Value = VisitAndReplace(Value, visitor); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Quoted.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Quoted.cs new file mode 100755 index 0000000..5bdf1d9 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Quoted.cs @@ -0,0 +1,90 @@ +namespace dotless.Core.Parser.Tree +{ + using System.Text.RegularExpressions; + using Infrastructure; + using Infrastructure.Nodes; +using System.Text; + + public class Quoted : TextNode + { + public char? Quote { get; set; } + public bool Escaped { get; set; } + + public Quoted(string value, char? quote) + : base(value) + { + Quote = quote; + } + + public Quoted(string value, char? quote, bool escaped) + : base(value) + { + Escaped = escaped; + Quote = quote; + } + + public Quoted(string value, string contents, bool escaped) + : base(contents) + { + Escaped = escaped; + Quote = value[0]; + } + + public Quoted(string value, bool escaped) + : base(value) + { + Escaped = escaped; + Quote = null; + } + + protected override Node CloneCore() + { + return new Quoted(Value, Quote, Escaped); + } + + public override void AppendCSS(Env env) + { + env.Output + .Append(RenderString()); + } + + public StringBuilder RenderString() + { + if (Escaped) + { + return new StringBuilder(UnescapeContents()); + } + + return new StringBuilder() + .Append(Quote) + .Append(Value) + .Append(Quote); + } + + public override string ToString() + { + return RenderString().ToString(); + } + + public override Node Evaluate(Env env) + { + var value = Regex.Replace(Value, @"@\{([\w-]+)\}", + m => + { + var v = new Variable('@' + m.Groups[1].Value) + { Location = new NodeLocation(Location.Index + m.Index, Location.Source, Location.FileName) } + .Evaluate(env); + return v is TextNode ? (v as TextNode).Value : v.ToCSS(env); + }); + + return new Quoted(value, Quote, Escaped).ReducedFrom(this); + } + + private readonly Regex _unescape = new Regex(@"(^|[^\\])\\(['""])"); + + public string UnescapeContents() + { + return _unescape.Replace(Value, @"$1$2"); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/RepeatEntity.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/RepeatEntity.cs new file mode 100755 index 0000000..f807604 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/RepeatEntity.cs @@ -0,0 +1,42 @@ +#if CSS3EXPERIMENTAL +namespace dotless.Core.Parser.Tree +{ + using Infrastructure; + using Infrastructure.Nodes; + using Plugins; + + public class RepeatEntity : Node + { + public Node Value { get; set; } + public Node RepeatCount { get; set; } + + public RepeatEntity(Node value, Node repeatCount) + { + Value = value; + RepeatCount = repeatCount; + } + + public override Node Evaluate(Env env) + { + var value = Value.Evaluate(env); + var repeatCount = RepeatCount.Evaluate(env); + return new RepeatEntity(value, repeatCount); + } + + public override void AppendCSS(Env env) + { + env.Output + .Append(Value) + .Append('[') + .Append(RepeatCount) + .Append(']'); + } + + public override void Accept(IVisitor visitor) + { + Value = VisitAndReplace(Value, visitor); + RepeatCount = VisitAndReplace(RepeatCount, visitor); + } + } +} +#endif \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Root.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Root.cs new file mode 100755 index 0000000..1e04dc3 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Root.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; + +namespace dotless.Core.Parser.Tree +{ + using System; + using System.Linq; + using Exceptions; + using Infrastructure; + using Infrastructure.Nodes; + using Plugins; + using dotless.Core.Utils; + + public class Root : Ruleset + { + public Func Error { get; set; } + + public Root(NodeList rules, Func error) + : this(rules, error, null) + { + } + + protected Root(NodeList rules, Func error, Ruleset master) + : base(new NodeList(), rules, master) + { + Error = error; + IsRoot = true; + } + + public override void AppendCSS(Env env) + { + try + { + if (Rules == null || !Rules.Any()) + return; + + var evaluated = (Root) Evaluate(env); + evaluated.Rules.InsertRange(0, evaluated.CollectImports().Cast()); + evaluated.AppendCSS(env, new Context()); + } + catch (ParsingException e) + { + throw Error(e); + } + } + + + /// + /// Gather the import statements from this instance, remove them from the list of rules and return them. + /// Used for hoisting imports to the top of the file. + /// + private IList CollectImports() { + var imports = Rules.OfType().ToList(); + foreach (var import in imports) { + Rules.Remove(import); + } + return imports; + } + + private Root DoVisiting(Root node, Env env, VisitorPluginType pluginType) + { + return env.VisitorPlugins + .Where(p => p.AppliesTo == pluginType) + .Aggregate(node, (current, plugin) => + { + try + { + plugin.OnPreVisiting(env); + Root r = plugin.Apply(current); + plugin.OnPostVisiting(env); + return r; + } + catch (Exception ex) + { + string message = string.Format("Plugin '{0}' failed during visiting with error '{1}'", plugin.GetName(), ex.Message); + throw new ParserException(message, ex); + } + }); + + } + + public override Node Evaluate(Env env) + { + if(Evaluated) return this; + + try + { + env = env ?? new Env(); + + env.Frames.Push(this); + NodeHelper.ExpandNodes(env, Rules); + env.Frames.Pop(); + + var clone = new Root(new NodeList(Rules), Error, OriginalRuleset); + + clone = DoVisiting(clone, env, VisitorPluginType.BeforeEvaluation); + + clone.ReducedFrom(this); + clone.EvaluateRules(env); + clone.Evaluated = true; + + clone = DoVisiting(clone, env, VisitorPluginType.AfterEvaluation); + return clone; + } + catch (ParsingException e) + { + throw Error(e); + } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Rule.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Rule.cs new file mode 100755 index 0000000..0c3edb8 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Rule.cs @@ -0,0 +1,109 @@ +namespace dotless.Core.Parser.Tree +{ + using Exceptions; + using Infrastructure; + using Infrastructure.Nodes; + using Plugins; + using System.Text.RegularExpressions; + + public class Rule : Node + { + public string Name { get; set; } + public Node Value { get; set; } + public bool Variable { get; set; } + public NodeList PostNameComments { get; set; } + public bool IsSemiColonRequired { get; set; } + public bool Variadic { get; set; } + public bool InterpolatedName { get; set; } + + public Rule(string name, Node value) : this(name, value, false) + { + } + + public Rule(string name, Node value, bool variadic) + { + Name = name; + Value = value; + Variable = !string.IsNullOrEmpty(name) && name[0] == '@'; + IsSemiColonRequired = true; + Variadic = variadic; + } + + public override Node Evaluate(Env env) + { + env.Rule = this; + + if (Value == null) + { + throw new ParsingException("No value found for rule " + Name, Location); + } + + var rule = new Rule(EvaluateName(env), Value.Evaluate(env)).ReducedFrom(this); + rule.IsSemiColonRequired = this.IsSemiColonRequired; + rule.PostNameComments = this.PostNameComments; + + env.Rule = null; + + return rule; + } + + private string EvaluateName(Env env) { + if (!InterpolatedName) { + return Name; + } + + var evaluatedVariable = env.FindVariable(Name).Evaluate(env) as Rule; + if (evaluatedVariable == null) { + throw new ParsingException("Invalid variable value for property name", Location); + } + + var evaluatedValue = evaluatedVariable.Value as Keyword; + + if (evaluatedValue == null) { + throw new ParsingException("Invalid variable value for property name", Location); + } + + return evaluatedValue.ToCSS(env); + } + + protected override Node CloneCore() { + return new Rule(Name, Value.Clone(), Variadic) { + IsSemiColonRequired = IsSemiColonRequired, + Variable = Variable + }; + } + + public override void AppendCSS(Env env) + { + if (Variable) + return; + + var value = Value; + + env.Output + .Append(Name) + .Append(PostNameComments) + .Append(env.Compress ? ":" : ": "); + + env.Output.Push() + .Append(value); + + if (env.Compress) + { + env.Output.Reset(Regex.Replace(env.Output.ToString(), @"(\s)+", " ").Replace(", ", ",")); + } + + env.Output.PopAndAppend(); + + if (IsSemiColonRequired) + { + env.Output.Append(";"); + } + } + + public override void Accept(IVisitor visitor) + { + Value = VisitAndReplace(Value, visitor); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Ruleset.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Ruleset.cs new file mode 100755 index 0000000..fe36ac9 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Ruleset.cs @@ -0,0 +1,505 @@ +using System; +using dotless.Core.Exceptions; + +namespace dotless.Core.Parser.Tree +{ + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Infrastructure; + using Infrastructure.Nodes; + using Utils; + using Plugins; + + public class Ruleset : Node + { + public NodeList Selectors { get; set; } + public NodeList Rules { get; set; } + public bool Evaluated { get; protected set; } + public bool IsRoot { get; set; } + public bool MultiMedia { get; set; } + + /// + /// The original Ruleset this was cloned from during evaluation + /// (may == this if this is the orginal) + /// + public Ruleset OriginalRuleset + { + get; + set; + } + + private Dictionary> _lookups; + + public Ruleset(NodeList selectors, NodeList rules) + : this(selectors, rules, null) + { + } + + protected Ruleset(NodeList selectors, NodeList rules, Ruleset originalRuleset) + : this() + { + Selectors = selectors; + Rules = rules; + OriginalRuleset = originalRuleset ?? this; + } + + protected Ruleset() + { + _lookups = new Dictionary>(); + OriginalRuleset = this; + } + + /// + /// returns whether this rulset is equal to or cloned from another node + /// + public bool IsEqualOrClonedFrom(Node node) + { + Ruleset ruleset = node as Ruleset; + if (ruleset) + { + return IsEqualOrClonedFrom(ruleset); + } + return false; + } + + /// + /// returns whether this rulset is equal to or cloned from another ruleset + /// + public bool IsEqualOrClonedFrom(Ruleset ruleset) + { + return ruleset.OriginalRuleset == OriginalRuleset; + } + + public Rule Variable(string name, Node startNode) + { + Ruleset startNodeRuleset = startNode as Ruleset; + + return Rules + .TakeWhile(r => r != startNode && (startNodeRuleset == null || !startNodeRuleset.IsEqualOrClonedFrom(r))) + .OfType() + .Where(r => r.Variable) + .Reverse() + .FirstOrDefault(r => r.Name == name); + } + + public List Rulesets() + { + return (Rules ?? Enumerable.Empty()).OfType().ToList(); + } + + public List Find(Env env, Selector selector, Ruleset self) where TRuleset : Ruleset + { + var context = new Context(); + context.AppendSelectors(new Context(), Selectors ?? new NodeList()); + + var namespacedSelectorContext = new Context(); + namespacedSelectorContext.AppendSelectors(context, new NodeList(selector)); + + var namespacedSelector = + namespacedSelectorContext + .Select(selectors => new Selector(selectors.SelectMany(s => s.Elements))) + .First(); + + return FindInternal(env, namespacedSelector, self, context).ToList(); + } + + private IEnumerable FindInternal(Env env, Selector selector, Ruleset self, Context context) + { + if (!selector.Elements.Any()) + { + return Enumerable.Empty(); + } + + string selectorCss = selector.ToCSS(env); + var key = selectorCss; + if (_lookups.ContainsKey(key)) + return _lookups[key]; + + self = self ?? this; + var rules = new List(); + + var bestMatch = context.Select(selectors => new Selector(selectors.SelectMany(s => s.Elements))) + .Where(m => m.Elements.IsSubsequenceOf(selector.Elements, ElementValuesEqual)) + .OrderByDescending(m => m.Elements.Count) + .FirstOrDefault(); + + if (bestMatch != null && bestMatch.Elements.Count == selector.Elements.Count) + { + // exact match, good to go + rules.Add(new Closure { Context = new List { this }, Ruleset = this }); + } + + + var validRulesets = Rulesets().Where(rule => + { + if (rule != self) + return true; + + MixinDefinition mixinRule = rule as MixinDefinition; + + if (mixinRule != null) + { + return mixinRule.Condition != null; + } + + return false; + }); + + foreach (var rule in validRulesets) + { + if (rule.Selectors == null) + { + continue; + } + + var childContext = new Context(); + childContext.AppendSelectors(context, rule.Selectors); + + var closures = rule.FindInternal(env, selector, self, childContext); + foreach (var closure in closures) + { + closure.Context.Insert(0, this); + rules.Add(closure); + } + + } + return _lookups[key] = rules; + } + + private static bool ElementValuesEqual(Element e1, Element e2) + { + if (e1.Value == null && e2.Value == null) + { + return true; + } + + if (e1.Value == null || e2.Value == null) + { + return false; + } + + return string.Equals(e1.Value.Trim(), e2.Value.Trim()); + } + + public virtual MixinMatch MatchArguments(List arguments, Env env) + { + return (arguments == null || arguments.Count == 0) ? MixinMatch.Pass : MixinMatch.ArgumentMismatch; + } + + public override Node Evaluate(Env env) + { + if (Evaluated) return this; + + // create a clone so it is non destructive + var clone = Clone().ReducedFrom(this); + + clone.EvaluateRules(env); + clone.Evaluated = true; + + return clone; + } + + private Ruleset Clone() { + return new Ruleset(new NodeList(Selectors), new NodeList(Rules), OriginalRuleset) { + IsReference = IsReference + }; + } + + protected override Node CloneCore() { + return new Ruleset( + new NodeList(Selectors), + new NodeList(Rules), + OriginalRuleset) { + Evaluated = Evaluated, + IsRoot = IsRoot, + MultiMedia = MultiMedia + }; + } + + public override void Accept(IVisitor visitor) + { + Selectors = VisitAndReplace(Selectors, visitor); + + Rules = VisitAndReplace(Rules, visitor); + } + + /// + /// Evaluate Rules. Must only be run on a copy of the ruleset otherwise it will + /// overwrite defintions that might be required later.. + /// + protected void EvaluateRules(Env env) + { + env.Frames.Push(this); + + for (var i = 0; i < Selectors.Count; i++) { + var evaluatedSelector = Selectors[i].Evaluate(env); + var expandedSelectors = evaluatedSelector as IEnumerable; + if (expandedSelectors != null) { + Selectors.RemoveAt(i); + Selectors.InsertRange(i, expandedSelectors); + } else { + Selectors[i] = evaluatedSelector as Selector; + } + } + + int mediaBlocks = env.MediaBlocks.Count; + + NodeHelper.ExpandNodes(env, Rules); + NodeHelper.ExpandNodes(env, Rules); + + foreach (var r in Rules.OfType().ToArray()) + { + foreach (var s in this.Selectors) + { + //If we're in a media block, then extenders can only apply to that media block + if (env.MediaPath.Any()) + { + env.MediaPath.Peek().AddExtension(s, (Extend) r.Evaluate(env), env); + } + else //Global extend + { + env.AddExtension(s, (Extend) r.Evaluate(env), env); + } + } + } + + for (var i = 0; i < Rules.Count; i++) + { + Rules[i] = Rules[i].Evaluate(env); + } + + // if media blocks are inside this ruleset we have to "catch" the bubbling and + // make sure they get this rulesets selectors + for (var j = mediaBlocks; j < env.MediaBlocks.Count; j++) + { + env.MediaBlocks[j].BubbleSelectors(Selectors); + } + + env.Frames.Pop(); + } + + protected Ruleset EvaluateRulesForFrame(Ruleset frame, Env context) + { + var newRules = new NodeList(); + + foreach (var rule in Rules) + { + if (rule is MixinDefinition) + { + var mixin = rule as MixinDefinition; + var parameters = Enumerable.Concat(mixin.Params, frame.Rules.Cast()); + newRules.Add(new MixinDefinition(mixin.Name, new NodeList(parameters), mixin.Rules, mixin.Condition, mixin.Variadic)); + } + else if (rule is Import) + { + var potentiolNodeList = rule.Evaluate(context); + var nodeList = potentiolNodeList as NodeList; + if (nodeList != null) + { + newRules.AddRange(nodeList); + } + else + { + newRules.Add(potentiolNodeList); + } + } + else if (rule is Directive || rule is Media) + { + newRules.Add(rule.Evaluate(context)); + } + else if (rule is Ruleset) + { + var ruleset = (rule as Ruleset); + + newRules.Add(ruleset.Evaluate(context)); + } + else if (rule is MixinCall) + { + newRules.AddRange((NodeList)rule.Evaluate(context)); + } + else + { + newRules.Add(rule.Evaluate(context)); + } + } + + return new Ruleset(Selectors, newRules); + } + + public override void AppendCSS(Env env) + { + if (Rules == null || !Rules.Any()) + return; + + ((Ruleset) Evaluate(env)).AppendCSS(env, new Context()); + } + + /// + /// Append the rules in a {} block. Used by sub classes. + /// + protected void AppendRules(Env env) + { + if (env.Compress && Rules.Count == 0) + { + return; + } + + env.Output + .Append(env.Compress ? "{" : " {\n") + + .Push() + .AppendMany(Rules, "\n") + .Trim() + .Indent(env.Compress ? 0 : 2) + .PopAndAppend(); + + if (env.Compress) + { + env.Output.TrimRight(';'); + } + + env.Output.Append(env.Compress ? "}" : "\n}"); + } + + public virtual void AppendCSS(Env env, Context context) + { + var rules = new List(); // node.Ruleset instances + int nonCommentRules = 0; + var paths = new Context(); // Current selectors + + if (!IsRoot) + { + if (!env.Compress && env.Debug && Location != null) + { + env.Output.Append(string.Format("/* {0}:L{1} */\n", Location.FileName, Zone.GetLineNumber(Location))); + } + paths.AppendSelectors(context, Selectors); + } + + env.Output.Push(); + + bool hasNonReferenceChildRulesets = false; + foreach (var node in Rules) + { + if (node.IgnoreOutput()) + continue; + + var comment = node as Comment; + if (comment != null && !comment.IsValidCss) + continue; + + var ruleset = node as Ruleset; + if (ruleset != null) + { + ruleset.AppendCSS(env, paths); + if (!ruleset.IsReference) + { + hasNonReferenceChildRulesets = true; + } + } + else + { + var rule = node as Rule; + + if (rule && rule.Variable) + continue; + + if (!IsRoot) + { + if (!comment) + nonCommentRules++; + + env.Output.Push() + .Append(node); + rules.Add(env.Output.Pop()); + } + else + { + env.Output + .Append(node); + + if (!env.Compress) + { + env.Output + .Append("\n"); + } + } + } + } + + var rulesetOutput = env.Output.Pop(); + + var hasExtenders = AddExtenders(env, context, paths); + if (hasExtenders) + { + IsReference = false; + } + + // If this is the root node, we don't render + // a selector, or {}. + // Otherwise, only output if this ruleset has rules. + if (!IsReference) { + if (IsRoot) { + env.Output.AppendMany(rules, env.Compress ? "" : "\n"); + } else { + if (nonCommentRules > 0) { + paths.AppendCSS(env); + + env.Output.Append(env.Compress ? "{" : " {\n "); + + env.Output.AppendMany(rules.ConvertAll(stringBuilder => stringBuilder.ToString()).Distinct(), + env.Compress ? "" : "\n "); + + if (env.Compress) { + env.Output.TrimRight(';'); + } + + env.Output.Append(env.Compress ? "}" : "\n}\n"); + } + } + } + + if (!IsReference || hasNonReferenceChildRulesets) + { + env.Output.Append(rulesetOutput); + } + } + + private bool AddExtenders(Env env, Context context, Context paths) { + bool hasNonReferenceExtenders = false; + foreach (var s in Selectors.Where(s => s.Elements.First().Value != null)) { + var local = context.Clone(); + local.AppendSelectors(context, new[] {s}); + var finalString = local.ToCss(env); + var exactExtension = env.FindExactExtension(finalString); + if (exactExtension != null) { + exactExtension.IsMatched = true; + paths.AppendSelectors(context.Clone(), exactExtension.ExtendedBy); + } + + var partials = env.FindPartialExtensions(local); + if (partials != null) { + foreach (var partialExtension in partials) { + partialExtension.IsMatched = true; + } + paths.AppendSelectors(context.Clone(), partials.SelectMany(p => p.Replacements(finalString))); + } + + bool newExactExtenders = exactExtension != null && exactExtension.ExtendedBy.Any(e => !e.IsReference); + bool newPartialExtenders = partials != null && partials.Any(p => p.ExtendedBy.Any(e => !e.IsReference)); + + hasNonReferenceExtenders = hasNonReferenceExtenders || newExactExtenders || newPartialExtenders; + } + return hasNonReferenceExtenders; + } + + public override string ToString() + { + var format = "{0}{{{1}}}"; + return Selectors != null && Selectors.Count > 0 + ? string.Format(format, Selectors.Select(s => s.ToCSS(new Env(null))).JoinStrings(""), Rules.Count) + : string.Format(format, "*", Rules.Count); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Script.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Script.cs new file mode 100755 index 0000000..8fddf01 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Script.cs @@ -0,0 +1,24 @@ +namespace dotless.Core.Parser.Tree +{ + using Infrastructure; + using Infrastructure.Nodes; + + public class Script : Node + { + public string Expression { get; set; } + + public Script(string script) + { + Expression = script; + } + + protected override Node CloneCore() { + return new Script(Expression); + } + + public override Node Evaluate(Env env) + { + return new TextNode("[script unsupported]"); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Selector.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Selector.cs new file mode 100755 index 0000000..94908e9 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Selector.cs @@ -0,0 +1,126 @@ +using System; + +namespace dotless.Core.Parser.Tree +{ + using Infrastructure; + using Infrastructure.Nodes; + using System.Collections.Generic; + using System.Linq; + using Plugins; + + public class Selector : Node + { + public NodeList Elements { get; set; } + + public Selector(IEnumerable elements) + { + if (elements is NodeList) + Elements = elements as NodeList; + else + Elements = new NodeList(elements); + } + + public bool Match(Selector other) + { + return + other.Elements.Count <= Elements.Count && + Elements[0].Value == other.Elements[0].Value; + } + + + [ThreadStatic] + private static Parser parser; + + [ThreadStatic] + private static Parsers parsers; + + + private Parser Parser { + get { + if (parser == null) { + parser = new Parser(); + } + + return parser; + } + } + + + private Parsers Parsers { + get { + if (parsers == null) { + parsers = new Parsers(Parser.NodeProvider); + } + + return parsers; + } + } + + public override Node Evaluate(Env env) + { + NodeList evaldElements = new NodeList(); + foreach (Element element in Elements) + { + if (element.NodeValue is Extend) + { + if (env.MediaPath.Any()) + { + env.MediaPath.Peek().AddExtension(this, (Extend)(((Extend)element.NodeValue).Evaluate(env)),env); + } + else //Global extend + { + env.AddExtension(this, (Extend)(((Extend)element.NodeValue).Evaluate(env)), env); + } + } + else + { + evaldElements.Add(element.Evaluate(env) as Element); + } + } + var evaluatedSelector = new Selector(evaldElements).ReducedFrom(this); + if (evaluatedSelector.Elements.All(e => e.NodeValue == null)) { + return evaluatedSelector; + } + + Parser.Tokenizer.SetupInput(evaluatedSelector.ToCSS(env), ""); + + var result = new NodeList(); + Selector selector; + while (selector = Parsers.Selector(Parser)) { + selector.IsReference = IsReference; + result.Add(selector.Evaluate(env) as Selector); + + if (!Parser.Tokenizer.Match(',')) { + break; + } + } + + return result; + } + + protected override Node CloneCore() { + return new Selector(Elements.Select(e => e.Clone())); + } + + public override void AppendCSS(Env env) + { + env.Output.Push(); + + if (Elements[0].Combinator.Value == "") + env.Output.Append(' '); + + env.Output.Append(Elements); + env.Output.Append(env.Output.Pop().ToString()); + } + + public override void Accept(IVisitor visitor) + { + Elements = VisitAndReplace(Elements, visitor); + } + + public override string ToString() + { + return ToCSS(new Env(null)); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Shorthand.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Shorthand.cs new file mode 100755 index 0000000..bc8c6a5 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Shorthand.cs @@ -0,0 +1,40 @@ +namespace dotless.Core.Parser.Tree +{ + using Infrastructure; + using Infrastructure.Nodes; + using Plugins; + + public class Shorthand : Node + { + public Node First { get; set; } + public Node Second { get; set; } + + public Shorthand(Node first, Node second) + { + First = first; + Second = second; + } + + public override Node Evaluate(Env env) { + return new Shorthand(First.Evaluate(env), Second.Evaluate(env)); + } + + protected override Node CloneCore() { + return new Shorthand(First.Clone(), Second.Clone()); + } + + public override void AppendCSS(Env env) + { + env.Output + .Append(First) + .Append("/") + .Append(Second); + } + + public override void Accept(IVisitor visitor) + { + First = VisitAndReplace(First, visitor); + Second = VisitAndReplace(Second, visitor); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Url.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Url.cs new file mode 100755 index 0000000..96b5941 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Url.cs @@ -0,0 +1,87 @@ +namespace dotless.Core.Parser.Tree +{ + using System.Collections.Generic; + using System.Linq; + using System.Text.RegularExpressions; + using Infrastructure; + using Infrastructure.Nodes; + using Utils; + using Exceptions; + using Plugins; + using dotless.Core.Importers; + + public class Url : Node + { + public Node Value { get; set; } + public List ImportPaths { get; set; } + public IImporter Importer { get; set; } + + public Url(Node value, IImporter importer) + { + Importer = importer; + ImportPaths = importer.GetCurrentPathsClone(); + + Value = value; + } + + public Url(Node value) + { + Value = value; + } + + /// + /// Gets the url value, unadjusted, as in the less + /// If the url does not contain a text node this will return null + /// + /// + public string GetUnadjustedUrl() + { + var textValue = Value as TextNode; + if (textValue != null) + { + return textValue.Value; + } + + return null; + } + + private Node AdjustUrlPath(Node value) + { + var textValue = value as TextNode; + if (textValue != null) + return AdjustUrlPath(textValue); + return value; + } + + private TextNode AdjustUrlPath(TextNode textValue) + { + if (Importer != null) + { + textValue.Value = Importer.AlterUrl(textValue.Value, ImportPaths); + } + return textValue; + } + + public override Node Evaluate(Env env) + { + return new Url(AdjustUrlPath(Value.Evaluate(env)), Importer); + } + + protected override Node CloneCore() { + return new Url(Value.Clone(), Importer); + } + + public override void AppendCSS(Env env) + { + env.Output + .Append("url(") + .Append(Value) + .Append(")"); + } + + public override void Accept(IVisitor visitor) + { + Value = VisitAndReplace(Value, visitor); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Value.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Value.cs new file mode 100755 index 0000000..c5c006b --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Value.cs @@ -0,0 +1,69 @@ +namespace dotless.Core.Parser.Tree +{ + using System.Collections.Generic; + using System.Linq; + using Infrastructure; + using Infrastructure.Nodes; + using Plugins; + using System; + + public class Value : Node + { + public NodeList Values { get; set; } + public NodeList PreImportantComments { get; set; } + public string Important { get; set; } + + public Value(IEnumerable values, string important) + { + Values = new NodeList(values); + Important = important; + } + + protected override Node CloneCore() { + return new Value((NodeList)Values.Clone(), Important); + } + + public override void AppendCSS(Env env) + { + env.Output.AppendMany(Values, env.Compress ? "," : ", "); + + if (!string.IsNullOrEmpty(Important)) + { + if (PreImportantComments) + { + env.Output.Append(PreImportantComments); + } + + env.Output + .Append(" ") + .Append(Important); + } + } + + public override string ToString() + { + return ToCSS(new Env(null)); // only used during debugging. + } + + public override Node Evaluate(Env env) + { + Node returnNode = null; + Value value; + + if (Values.Count == 1 && string.IsNullOrEmpty(Important)) + returnNode = Values[0].Evaluate(env); + else + { + returnNode = value = new Value(Values.Select(n => n.Evaluate(env)), Important); + value.PreImportantComments = this.PreImportantComments; + } + + return returnNode.ReducedFrom(this); + } + + public override void Accept(IVisitor visitor) + { + Values = VisitAndReplace(Values, visitor); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Variable.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Variable.cs new file mode 100755 index 0000000..874d21d --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Tree/Variable.cs @@ -0,0 +1,42 @@ +namespace dotless.Core.Parser.Tree +{ + using Exceptions; + using Infrastructure; + using Infrastructure.Nodes; + + public class Variable : Node + { + public string Name { get; set; } + + public Variable(string name) + { + Name = name; + } + + protected override Node CloneCore() { + return new Variable(Name); + } + + public override Node Evaluate(Env env) + { + var name = Name; + if (name.StartsWith("@@")) + { + var v = new Variable(name.Substring(1)).Evaluate(env); + name = '@' + (v is TextNode ? (v as TextNode).Value : v.ToCSS(env)); + } + + if (env.IsEvaluatingVariable(name)) { + throw new ParsingException("Recursive variable definition for " + name, Location); + } + + var variable = env.FindVariable(name); + + if (variable) { + return variable.Value.Evaluate(env.CreateVariableEvaluationEnv(name)); + } + + throw new ParsingException("variable " + name + " is undefined", Location); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Parser/Zone.cs b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Zone.cs new file mode 100755 index 0000000..7024adc --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Parser/Zone.cs @@ -0,0 +1,83 @@ +namespace dotless.Core.Parser +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + public class Zone + { + public Zone(NodeLocation location) + : this(location, null, null) + { + } + + public Zone(NodeLocation location, string error, Zone callZone) + { + var input = location.Source; + var index = location.Index; + + if (index > input.Length) + { + index = input.Length; + } + + int lineNumber, position; + GetLineNumber(location, out lineNumber, out position); + + var lines = input.Split('\n'); + + FileName = location.FileName; + Message = error; + CallZone = callZone; + LineNumber = lineNumber + 1; + Position = position; + Extract = new Extract(lines, lineNumber); + } + + public static int GetLineNumber(NodeLocation location) + { + int lineNumber, position; + GetLineNumber(location, out lineNumber, out position); + return lineNumber + 1; + } + + private static void GetLineNumber(NodeLocation location, out int lineNumber, out int position) + { + var input = location.Source; + var index = location.Index; + + if (location.Index > input.Length) + { + index = input.Length; + } + + var first = input.Substring(0, index); + + var start = first.LastIndexOf('\n') + 1; + lineNumber = first.Count(c => c == '\n'); + position = index - start; + } + + public int LineNumber { get; set; } + public int Position { get; set; } + public Extract Extract { get; set; } + public string Message { get; set; } + public string FileName { get; set; } + public Zone CallZone { get; set; } + } + + public class Extract + { + public Extract(string[] lines, int line) + { + Before = line > 0 ? lines[line - 1] : "/beginning of file"; + Line = lines[line]; + After = line + 1 < lines.Length ? lines[line + 1] : "/end of file"; + } + + public string After { get; set; } + public string Before { get; set; } + public string Line { get; set; } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Plugins/ColorSpinPlugin.cs b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/ColorSpinPlugin.cs new file mode 100755 index 0000000..0b09df1 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/ColorSpinPlugin.cs @@ -0,0 +1,37 @@ +namespace dotless.Core.Plugins +{ + using Parser.Infrastructure.Nodes; + using Parser.Tree; + using Utils; + using System.ComponentModel; + + [Description("Automatically spins all colors in a less file"), DisplayName("ColorSpin")] + public class ColorSpinPlugin : VisitorPlugin + { + public double Spin { get; set; } + + public ColorSpinPlugin(double spin) + { + Spin = spin; + } + + public override VisitorPluginType AppliesTo + { + get { return VisitorPluginType.AfterEvaluation; } + } + + public override Node Execute(Node node, out bool visitDeeper) + { + visitDeeper = true; + + var color = node as Color; + if (color == null) return node; + + var hslColor = HslColor.FromRgbColor(color); + hslColor.Hue += Spin/360.0d; + var newColor = hslColor.ToRgbColor(); + + return newColor.ReducedFrom(color); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Plugins/DelegateVisitor.cs b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/DelegateVisitor.cs new file mode 100755 index 0000000..0e8b537 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/DelegateVisitor.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using dotless.Core.Parser.Infrastructure.Nodes; + +namespace dotless.Core.Plugins +{ + public class DelegateVisitor : IVisitor + { + private readonly Func visitor; + + public DelegateVisitor(Func visitor) + { + this.visitor = visitor; + } + + public Node Visit(Node node) { + var list = node as IList; + if (list != null) { + for (var i = 0; i < list.Count; i++) { + list[i] = Visit(list[i]); + } + } + return visitor(node); + } + + public static IVisitor For(Func projection) where TNode : Node + { + return new DelegateVisitor(node => + { + var typed = node as TNode; + if (typed == null) + { + return node; + } + return projection(typed); + }); + } + public static IVisitor For(Action action) where TNode : Node + { + return new DelegateVisitor(node => + { + var typed = node as TNode; + if (typed != null) + { + action(typed); + } + return node; + }); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Plugins/GenericPluginConfigurator.cs b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/GenericPluginConfigurator.cs new file mode 100755 index 0000000..fe497bd --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/GenericPluginConfigurator.cs @@ -0,0 +1,140 @@ +namespace dotless.Core.Plugins +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Reflection; + using System.ComponentModel; + + public class GenericPluginConfigurator : IPluginConfigurator where T : IPlugin + { + public string Name + { + get + { + return PluginFinder.GetName(typeof(T)); + } + } + + public string Description + { + get + { + return PluginFinder.GetDescription(typeof(T)); + } + } + + public Type Configurates + { + get + { + return typeof(T); + } + } + + private Func _pluginCreator = null; + public void SetParameterValues(IEnumerable pluginParameters) + { + ConstructorInfo defaultConstructor; + ConstructorInfo parameterConstructor; + GetConstructorInfos(out parameterConstructor, out defaultConstructor); + + if (pluginParameters == null || pluginParameters.Count() == 0 || pluginParameters.All(parameter => parameter.Value == null)) + { + if (defaultConstructor == null) + { + throw new Exception("No parameters provided but no default constructor"); + } + _pluginCreator = () => (T)defaultConstructor.Invoke(new object[] { }); + } + else + { + var constructorArguments = parameterConstructor.GetParameters() + .OrderBy(parameter => parameter.Position) + .Select(parameter => + { + var p = pluginParameters.FirstOrDefault(pluginParameter => pluginParameter.Name == parameter.Name); + if (p == null) + { + if (parameter.ParameterType.IsValueType) + { + return Activator.CreateInstance(parameter.ParameterType); + } + return null; + } + return p.Value; + }) + .ToArray(); + + _pluginCreator = () => (T)parameterConstructor.Invoke(constructorArguments); + } + } + + public IPlugin CreatePlugin() + { + if (_pluginCreator == null) + { + SetParameterValues(null); + } + + return _pluginCreator(); + } + + private class ConstructorParameterSet + { + public ParameterInfo[] Parameter { get; set; } + public int Count { get; set; } + } + + private void GetConstructorInfos(out ConstructorInfo parameterConstructor, out ConstructorInfo defaultConstructor) + { + List constructors = typeof(T).GetConstructors() + .Where(constructorInfo => constructorInfo.IsPublic && !constructorInfo.IsStatic).ToList(); + + if (constructors.Count > 2 || constructors.Count == 0) + { + throw new Exception("Generic plugin configurator doesn't support less than 1 or more than 2 constructors. Add your own IPluginConfigurator to the assembly."); + } else if (constructors.Count == 2) + { + if (constructors[0].GetParameters().Length == 0) + { + defaultConstructor = constructors[0]; + parameterConstructor = constructors[1]; + } else if (constructors[1].GetParameters().Length == 0) + { + defaultConstructor = constructors[1]; + parameterConstructor = constructors[0]; + } else + { + throw new Exception("Generic plugin configurator only supports 1 parameterless constructor and 1 with parameters. Add your own IPluginConfigurator to the assembly."); + } + } else { + if (constructors[0].GetParameters().Length == 0) + { + defaultConstructor = constructors[0]; + parameterConstructor = null; + } + else + { + defaultConstructor = null; + parameterConstructor = constructors[0]; + } + } + } + + public IEnumerable GetParameters() + { + ConstructorInfo defaultConstructor; + ConstructorInfo parameterConstructor; + GetConstructorInfos(out parameterConstructor, out defaultConstructor); + if (parameterConstructor == null) + { + return new List(); + } + + return parameterConstructor.GetParameters().Select(parameter => (IPluginParameter)new PluginParameter( + parameter.Name, parameter.ParameterType, defaultConstructor == null)).ToList(); + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IFunctionPlugin.cs b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IFunctionPlugin.cs new file mode 100755 index 0000000..992f449 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IFunctionPlugin.cs @@ -0,0 +1,11 @@ +namespace dotless.Core.Plugins +{ + using System; + using System.Collections.Generic; + using dotless.Core.Parser.Infrastructure; + + public interface IFunctionPlugin : IPlugin + { + Dictionary GetFunctions(); + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IPlugin.cs b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IPlugin.cs new file mode 100755 index 0000000..6467e22 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IPlugin.cs @@ -0,0 +1,6 @@ +namespace dotless.Core.Plugins +{ + public interface IPlugin + { + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IPluginConfigurator.cs b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IPluginConfigurator.cs new file mode 100755 index 0000000..2e99009 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IPluginConfigurator.cs @@ -0,0 +1,20 @@ +namespace dotless.Core.Plugins +{ + using System; + using System.Collections.Generic; + + public interface IPluginConfigurator + { + IPlugin CreatePlugin(); + + IEnumerable GetParameters(); + + void SetParameterValues(IEnumerable parameters); + + string Name { get; } + + string Description { get; } + + Type Configurates { get; } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IPluginParameter.cs b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IPluginParameter.cs new file mode 100755 index 0000000..7a236bb --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IPluginParameter.cs @@ -0,0 +1,17 @@ +namespace dotless.Core.Plugins +{ + using System; + + public interface IPluginParameter + { + string Name { get; } + + bool IsMandatory { get; } + + object Value { get; } + + string TypeDescription { get; } + + void SetValue(string value); + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IVisitor.cs b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IVisitor.cs new file mode 100755 index 0000000..3eac501 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IVisitor.cs @@ -0,0 +1,9 @@ +namespace dotless.Core.Plugins +{ + using Parser.Infrastructure.Nodes; + + public interface IVisitor + { + Node Visit(Node node); + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IVisitorPlugin.cs b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IVisitorPlugin.cs new file mode 100755 index 0000000..acc506d --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/IVisitorPlugin.cs @@ -0,0 +1,21 @@ +namespace dotless.Core.Plugins +{ + using Parser.Tree; + using dotless.Core.Parser.Infrastructure; + + public interface IVisitorPlugin : IPlugin + { + Root Apply(Root tree); + + VisitorPluginType AppliesTo { get; } + + void OnPreVisiting(Env env); + void OnPostVisiting(Env env); + } + + public enum VisitorPluginType + { + BeforeEvaluation, + AfterEvaluation + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Plugins/PluginFinder.cs b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/PluginFinder.cs new file mode 100755 index 0000000..45e76b8 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/PluginFinder.cs @@ -0,0 +1,115 @@ +namespace dotless.Core.Plugins +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Reflection; + using System.IO; + using System.ComponentModel; + + public static class PluginFinder + { + /// + /// Gets a plugins name + /// + public static string GetName(this IPlugin plugin) + { + return GetName(plugin.GetType()); + } + + /// + /// Gets a plugins description + /// + public static string GetDescription(this IPlugin plugin) + { + return GetName(plugin.GetType()); + } + + /// + /// Gets a plugins description from its type + /// + public static string GetDescription(Type pluginType) + { + DescriptionAttribute description = pluginType + .GetCustomAttributes(typeof(DescriptionAttribute), true) + .FirstOrDefault() as DescriptionAttribute; + + if (description != null) + return description.Description; + else + return "No Description"; + } + + /// + /// Gets a plugins name from its type + /// + public static string GetName(Type pluginType) + { + DisplayNameAttribute name = pluginType + .GetCustomAttributes(typeof(DisplayNameAttribute), true) + .FirstOrDefault() as DisplayNameAttribute; + + if (name != null) + return name.DisplayName; + else + return pluginType.Name; + } + + /// + /// Gets plugin configurators for all plugins, optionally scanning referenced assemblies and + /// a plugins folder underneath the executing assembly + /// + /// Look for a plugins folder and if exists, load plugins from it + /// + public static IEnumerable GetConfigurators(bool scanPluginsFolder) + { + List> pluginConfigurators = new List>(); + + pluginConfigurators.Add(GetConfigurators(Assembly.GetAssembly(typeof(PluginFinder)))); + + if (scanPluginsFolder) + { + string assemblyLocation = Assembly.GetEntryAssembly().Location; + string pluginsFolder = Path.Combine(Path.GetDirectoryName(assemblyLocation), "plugins"); + + if (Directory.Exists(pluginsFolder)) + { + DirectoryInfo pluginsFolderDirectoryInfo = new DirectoryInfo(pluginsFolder); + foreach(FileInfo pluginAssembly in pluginsFolderDirectoryInfo.GetFiles("*.dll")) + { + pluginConfigurators.Add(GetConfigurators(Assembly.LoadFile(pluginAssembly.FullName))); + } + } + } + + return pluginConfigurators.Aggregate((group1, group2) => group1.Union(group2)); + } + + /// + /// Gets plugin configurators for every plugin in an assembly + /// + /// + /// + public static IEnumerable GetConfigurators(Assembly assembly) + { + IEnumerable types = assembly.GetTypes().Where( + type => !type.IsAbstract && !type.IsGenericType && !type.IsInterface); + + IEnumerable pluginConfigurators = types + .Where(type => typeof(IPluginConfigurator).IsAssignableFrom(type)) + .Select(type => (IPluginConfigurator)type.GetConstructor(new Type[] {}).Invoke(new object[]{})); + + IEnumerable pluginsConfigurated = pluginConfigurators.Select(pluginConfigurator => pluginConfigurator.Configurates); + + Type genericPluginConfiguratorType = typeof(GenericPluginConfigurator<>); + + IEnumerable plugins = types + .Where(type => typeof(IPlugin).IsAssignableFrom(type)) + .Where(type => !pluginsConfigurated.Contains(type)) + .Select(type => (IPluginConfigurator)Activator.CreateInstance(genericPluginConfiguratorType.MakeGenericType(type))); + + return plugins.Union(pluginConfigurators); + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Plugins/PluginParameter.cs b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/PluginParameter.cs new file mode 100755 index 0000000..d027b39 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/PluginParameter.cs @@ -0,0 +1,65 @@ +namespace dotless.Core.Plugins +{ + using System; + using System.Globalization; + + public class PluginParameter : IPluginParameter + { + public PluginParameter(string name, Type type, bool isMandatory) + { + Name = name; + IsMandatory = isMandatory; + Type = type; + } + + public string Name + { + get; + private set; + } + + public bool IsMandatory + { + get; + private set; + } + + public object Value + { + get; + private set; + } + + private Type Type + { + get; + set; + } + + public string TypeDescription + { + get { return Type.Name; } + } + + public void SetValue(string stringValue) + { + if (Type.Equals(typeof(Boolean))) + { + if (stringValue.Equals("true", StringComparison.InvariantCultureIgnoreCase) || + stringValue.Equals("t", StringComparison.InvariantCultureIgnoreCase) || + stringValue.Equals("1", StringComparison.InvariantCultureIgnoreCase)) + { + Value = true; + } + else + { + Value = false; + } + } + else + { + Value = Convert.ChangeType(stringValue, Type, CultureInfo.InvariantCulture); + } + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Plugins/RtlPlugin.cs b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/RtlPlugin.cs new file mode 100755 index 0000000..718a567 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/RtlPlugin.cs @@ -0,0 +1,316 @@ +namespace dotless.Core.Plugins +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using dotless.Core.Parser.Infrastructure.Nodes; + using dotless.Core.Parser.Tree; + using System.Globalization; + using System.Text.RegularExpressions; + using System.ComponentModel; + + [DisplayName("Rtl"), Description("Reverses some css when in rtl mode")] + public class RtlPlugin : VisitorPlugin + { + public RtlPlugin(bool onlyReversePrefixedRules, bool forceRtlTransform) : this() + { + OnlyReversePrefixedRules = onlyReversePrefixedRules; + ForceRtlTransform = forceRtlTransform; + } + + public RtlPlugin() + { + PropertiesToReverse = new List() + { + "border-left", + "border-right", + "border-top-left-radius", + "border-top-right-radius", + "border-bottom-left-radius", + "border-bottom-right-radius", + "border-width", + "margin", + "padding", + "float", + "right", + "left", + "text-align" + }; + } + + public bool OnlyReversePrefixedRules + { + get; + set; + } + + public bool ForceRtlTransform + { + get; + set; + } + + public IEnumerable PropertiesToReverse + { + get; + set; + } + + public override VisitorPluginType AppliesTo + { + get { return VisitorPluginType.AfterEvaluation; } + } + + public override void OnPreVisiting(Parser.Infrastructure.Env env) + { + base.OnPreVisiting(env); + + bool isRtl = ForceRtlTransform || CultureInfo.CurrentCulture.TextInfo.IsRightToLeft; + + PrefixesToProcess = new List(); + + if (!OnlyReversePrefixedRules && isRtl) + { + foreach (string property in PropertiesToReverse) + { + PrefixesToProcess.Add(new Prefix() + { + KeepRule = true, + PrefixString = property, + RemovePrefix = false, + Reverse = true + }); + } + } + + PrefixesToProcess.Add(new Prefix() + { + PrefixString = "-rtl-reverse-", + RemovePrefix = true, + KeepRule = true, + Reverse = isRtl + }); + + PrefixesToProcess.Add(new Prefix() + { + PrefixString = "-ltr-reverse-", + RemovePrefix = true, + KeepRule = true, + Reverse = !isRtl + }); + + PrefixesToProcess.Add(new Prefix() + { + PrefixString = "-rtl-ltr-", + RemovePrefix = true, + KeepRule = true, + Reverse = false + }); + + PrefixesToProcess.Add(new Prefix() + { + PrefixString = "-ltr-rtl-", + RemovePrefix = true, + KeepRule = true, + Reverse = false + }); + + PrefixesToProcess.Add(new Prefix() + { + PrefixString = "-rtl-", + RemovePrefix = true, + KeepRule = isRtl + }); + + PrefixesToProcess.Add(new Prefix() + { + PrefixString = "-ltr-", + RemovePrefix = true, + KeepRule = !isRtl + }); + } + + public override Node Execute(Node node, out bool visitDeeper) + { + Rule rule = node as Rule; + if (rule != null) + { + visitDeeper = false; + + string ruleName = (rule.Name ?? "").ToLowerInvariant(); + + foreach (Prefix prefix in PrefixesToProcess) + { + if (ruleName.StartsWith(prefix.PrefixString)) + { + if (!prefix.KeepRule) + { + return null; + } + + if (prefix.RemovePrefix) + { + rule.Name = rule.Name.Substring(prefix.PrefixString.Length); + } + + if (prefix.Reverse) + { + if (rule.Name.IndexOf("right", StringComparison.InvariantCultureIgnoreCase) >= 0) + { + rule.Name = Replace(rule.Name, "right", "left", StringComparison.InvariantCultureIgnoreCase); + return rule; + } + + if (rule.Name.IndexOf("left", StringComparison.InvariantCultureIgnoreCase) >= 0) + { + rule.Name = Replace(rule.Name, "left", "right", StringComparison.InvariantCultureIgnoreCase); + return rule; + } + + if (rule.Name.IndexOf("top", StringComparison.InvariantCultureIgnoreCase) >= 0 || + rule.Name.IndexOf("bottom", StringComparison.InvariantCultureIgnoreCase) >= 0) + { + return rule; + } + + ValuesReverserVisitor reverser = new ValuesReverserVisitor(); + return reverser.ReverseRule(rule); + } + else + { + return rule; + } + } + } + } + visitDeeper = true; + return node; + } + + private string Replace(string haystack, string needle, string replacement, StringComparison comparisonType) + { + int index = haystack.IndexOf(needle, comparisonType); + if (index < 0) + { + return haystack; + } + return haystack.Substring(0, index) + replacement + haystack.Substring(index + needle.Length); + + } + + private List PrefixesToProcess { get; set; } + + private class Prefix + { + public string PrefixString { get; set; } + public bool KeepRule { get; set; } + public bool Reverse { get; set; } + public bool RemovePrefix { get; set; } + } + + private class ValuesReverserVisitor : IVisitor + { + private StringBuilder _textContent = new StringBuilder(); + private List _nodeContent = new List(); + + public Rule ReverseRule(Rule rule) + { + rule.Accept(this); + + // we have the values.. now we can reverse it + string content = _textContent.ToString(); + string important = ""; + + var value = rule.Value as Value; + if (value != null) + { + important = value.Important; + } + + bool valueChanged = false; + + if (_nodeContent.Count > 1) + { + if (_nodeContent.Count == 4) + { + Node tmp = _nodeContent[1]; + _nodeContent[1] = _nodeContent[3]; + _nodeContent[3] = tmp; + + return new Rule(rule.Name, new Value(new[] { new Expression(_nodeContent)}, important)).ReducedFrom(rule); + } + } + else + { + if (content == "left") + { + content = ("right " + important).TrimEnd(); + valueChanged = true; + } + else if (content == "right") + { + content = ("left " + important).TrimEnd(); + valueChanged = true; + } + else + { + string[] items = content.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + if (items.Length == 4) + { + string temp = items[1]; + items[1] = items[3]; + items[3] = temp; + content = String.Join(" ", items); + valueChanged = true; + } + } + + if (valueChanged) + { + return new Rule(rule.Name, new TextNode(content)).ReducedFrom(rule); + } + } + + return rule; + } + + #region IVisitor Members + + public Node Visit(Node node) + { + TextNode tn = node as TextNode; + + if (tn != null) + { + _textContent.Append(tn.Value); + _nodeContent.Add(tn); + return node; + } + + Number number = node as Number; + + if (number != null) + { + _nodeContent.Add(number); + return node; + } + + Keyword keyword = node as Keyword; + + if (keyword != null) + { + _nodeContent.Add(keyword); + _textContent.Append(keyword.Value); + return node; + } + + node.Accept(this); + + return node; + } + + #endregion + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Plugins/VisitorPlugin.cs b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/VisitorPlugin.cs new file mode 100755 index 0000000..f0a5b20 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Plugins/VisitorPlugin.cs @@ -0,0 +1,39 @@ +namespace dotless.Core.Plugins +{ + using System; + using Parser.Infrastructure.Nodes; + using Parser.Tree; + using dotless.Core.Parser.Infrastructure; + + public abstract class VisitorPlugin : IVisitorPlugin, IVisitor + { + public Root Apply(Root tree) + { + Visit(tree); + + return tree; + } + + public abstract VisitorPluginType AppliesTo { get; } + + public Node Visit(Node node) + { + bool visitDeeper; + node = Execute(node, out visitDeeper); + if (visitDeeper && node != null) + node.Accept(this); + + return node; + } + + public abstract Node Execute(Node node, out bool visitDeeper); + + public virtual void OnPreVisiting(Env env) + { + } + + public virtual void OnPostVisiting(Env env) + { + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Stylizers/ConsoleStylizer.cs b/dotnet/Applications/Mocha.Web.Server/Core/Stylizers/ConsoleStylizer.cs new file mode 100755 index 0000000..7cd9ffa --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Stylizers/ConsoleStylizer.cs @@ -0,0 +1,50 @@ +using System.Text; + +namespace dotless.Core.Stylizers +{ + using System.Collections.Generic; + using Parser; + + public class ConsoleStylizer : IStylizer + { + private Dictionary styles; + + public ConsoleStylizer() + { + styles = new Dictionary + { + {"bold", new[] {1, 22}}, + {"inverse", new[] {7, 27}}, + {"underline", new[] {4, 24}}, + {"yellow", new[] {33, 39}}, + {"green", new[] {32, 39}}, + {"red", new[] {31, 39}}, + {"grey", new[] {90, 39}}, + {"reset", new[] {0, 0}} + }; + } + + private string Stylize(string str, string style) + { + return "\x1b[" + styles[style][0] + "m" + str + + "\x1b[" + styles[style][1] + "m"; + } + + public string Stylize(Zone zone) + { + var extract = zone.Extract; + var errorPosition = zone.Position; + + var errorBefore = extract.Line.Substring(0, errorPosition); + var errorAfter = extract.Line.Substring(errorPosition + 1); + + var styledString = new StringBuilder(); + styledString.Append(Stylize(extract.Before, "grey")); + styledString.Append(Stylize(errorBefore, "green")); + styledString.Append(Stylize(Stylize(extract.Line[errorPosition].ToString(), "inverse") + errorAfter, "yellow")); + styledString.Append(Stylize(extract.After, "grey")); + styledString.Append(Stylize("", "reset")); + return styledString.ToString(); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Stylizers/HtmlStylizer.cs b/dotnet/Applications/Mocha.Web.Server/Core/Stylizers/HtmlStylizer.cs new file mode 100755 index 0000000..f5b17dd --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Stylizers/HtmlStylizer.cs @@ -0,0 +1,35 @@ +namespace dotless.Core.Stylizers +{ + using Parser; + + public class HtmlStylizer : IStylizer + { + public string Stylize(Zone zone) + { + var fileStr = string.IsNullOrEmpty(zone.FileName) ? "" : string.Format(" in '{0}'", zone.FileName); + + return string.Format(@" +
+

There is an error{0}

+

{1} on line {3}, column {5}

+
+
{2}{6}
+
{3}{7}{8}{9}
+
{4}{10}
+
+
+", + fileStr, + zone.Message, + zone.LineNumber - 1, + zone.LineNumber, + zone.LineNumber + 1, + zone.Position, + zone.Extract.Before, + zone.Extract.Line.Substring(0, zone.Position), + zone.Extract.Line[zone.Position], + zone.Extract.Line.Substring(zone.Position + 1), + zone.Extract.After); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Stylizers/IStylizer.cs b/dotnet/Applications/Mocha.Web.Server/Core/Stylizers/IStylizer.cs new file mode 100755 index 0000000..ab580e9 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Stylizers/IStylizer.cs @@ -0,0 +1,9 @@ +namespace dotless.Core.Stylizers +{ + using Parser; + + public interface IStylizer + { + string Stylize(Zone zone); + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Stylizers/PlainStylizer.cs b/dotnet/Applications/Mocha.Web.Server/Core/Stylizers/PlainStylizer.cs new file mode 100755 index 0000000..d0fcf20 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Stylizers/PlainStylizer.cs @@ -0,0 +1,48 @@ +namespace dotless.Core.Stylizers +{ + using Parser; + + public class PlainStylizer : IStylizer + { + public string Stylize(Zone zone) + { + var fileStr = string.IsNullOrEmpty(zone.FileName) ? "" : string.Format(" in file '{0}'", zone.FileName); + + var callStr = ""; + + if(zone.CallZone != null) + { + var callFile = ""; + + if (zone.CallZone.FileName != zone.FileName && !string.IsNullOrEmpty(zone.CallZone.FileName)) + { + callFile = string.Format(@" in file '{0}'", zone.CallZone.FileName); + } + + callStr = string.Format(@" +from line {0}{2}: +{0,5:[#]}: {1}", + zone.CallZone.LineNumber, + zone.CallZone.Extract.Line, + callFile); + } + + return string.Format(@" +{1} on line {4}{0}: +{2,5:[#]}: {3} +{4,5:[#]}: {5} + {6}^ +{7,5:[#]}: {8}{9}", + fileStr, + zone.Message, + zone.LineNumber - 1, + zone.Extract.Before, + zone.LineNumber, + zone.Extract.Line, + new string('-', zone.Position), + zone.LineNumber + 1, + zone.Extract.After, + callStr); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Utils/EnumerableExtensions.cs b/dotnet/Applications/Mocha.Web.Server/Core/Utils/EnumerableExtensions.cs new file mode 100755 index 0000000..602cd8d --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Utils/EnumerableExtensions.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; + +namespace dotless.Core.Utils +{ + internal static class EnumerableExtensions + { + internal static bool IsSubsequenceOf(this IList subsequence, IList sequence) + { + return subsequence.IsSubsequenceOf(sequence, (element1, element2) => Equals(element1, element2)); + } + + internal static bool IsSubsequenceOf( + this IList subsequence, + IList sequence, Func areEqual) + { + return subsequence.IsSubsequenceOf(sequence, (_, element1, __, element2) => areEqual(element1, element2)); + } + + /// + /// Helper method for checking whether the elements in one IList are a subsequence of the elements in another. + /// The parameters of the equality function are: + /// - Index of the subsequence element within the subsequence + /// - The subsequence element + /// - Index of the parent sequence element within the parent sequence + /// - The parent sequence element + /// + /// This allows for equality comparisons where the conditions are different based on the position of each element. + /// + internal static bool IsSubsequenceOf( + this IList subsequence, + IList sequence, Func areEqual) + { + // Trivial case: empty sequence is a subsequence of any sequence, including the empty sequence + if (subsequence.Count == 0) + { + return true; + } + + // Another trivial case: potential subsequence was not empty, but the sequence to test against is + // so we know it can't contain the subsequence. + if (sequence.Count == 0) + { + return false; + } + + // Iterate seqEnumerator until we find an element that matches the first element of subEnumerator + int sequenceIndex = 0; + while (!areEqual(0, subsequence[0], sequenceIndex, sequence[sequenceIndex])) + { + sequenceIndex++; + if (sequenceIndex >= sequence.Count) + { + return false; + } + } + + int subsequenceIndex = 0; + // Now, we've got two enumerators that are at a matching item + while (true) + { + sequenceIndex += 1; + subsequenceIndex += 1; + + if (subsequenceIndex >= subsequence.Count) + { + // Ran out of subsequence to test against, so it's a match + return true; + } + + if (sequenceIndex >= sequence.Count) + { + // Ran out of sequence to test against, so it's not a match + return false; + } + + if (!areEqual(subsequenceIndex, subsequence[subsequenceIndex], sequenceIndex, sequence[sequenceIndex])) + { + // Current elements differ, so it's not a match + return false; + } + } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Utils/Guard.cs b/dotnet/Applications/Mocha.Web.Server/Core/Utils/Guard.cs new file mode 100755 index 0000000..5d31080 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Utils/Guard.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; + +namespace dotless.Core.Utils +{ + using System.Collections.Generic; + using Exceptions; + using Parser.Infrastructure; + using Parser.Infrastructure.Nodes; + using Parser; + + public static class Guard + { + public static void Expect(string expected, string actual, object @in, NodeLocation location) + { + if (actual == expected) + return; + + var message = string.Format("Expected '{0}' in {1}, found '{2}'", expected, @in, actual); + + throw new ParsingException(message, location); + } + + [Obsolete("Use Expect(bool, string, NodeLocation) instead")] + public static void Expect(Func condition, string message, NodeLocation location) + { + if (condition()) + return; + + throw new ParsingException(message, location); + } + + public static void Expect(bool condition, string message, NodeLocation location) + { + if (condition) + return; + + throw new ParsingException(message, location); + } + + public static TExpected ExpectNode(Node actual, object @in, NodeLocation location) where TExpected : Node + { + if (actual is TExpected) + return (TExpected) actual; + + var expected = typeof (TExpected).Name.ToLowerInvariant(); + + var message = string.Format("Expected {0} in {1}, found {2}", expected, @in, actual.ToCSS(new Env(null))); + + throw new ParsingException(message, location); + } + + public static void ExpectNodeToBeOneOf(Node actual, object @in, NodeLocation location) where TExpected1 : Node where TExpected2 : Node + { + if (actual is TExpected1 || actual is TExpected2) + return; + + var expected1 = typeof(TExpected1).Name.ToLowerInvariant(); + var expected2 = typeof(TExpected2).Name.ToLowerInvariant(); + + var message = string.Format("Expected {0} or {1} in {2}, found {3}", expected1, expected2, @in, actual.ToCSS(new Env(null))); + + throw new ParsingException(message, location); + } + + public static List ExpectAllNodes(IEnumerable actual, object @in, NodeLocation location) where TExpected : Node + { + return actual.Select(node => ExpectNode(node, @in, location)).ToList(); + } + + public static void ExpectNumArguments(int expected, int actual, object @in, NodeLocation location) + { + if (actual == expected) + return; + + var message = string.Format("Expected {0} arguments in {1}, found {2}", expected, @in, actual); + + throw new ParsingException(message, location); + } + + public static void ExpectMinArguments(int expected, int actual, object @in, NodeLocation location) + { + if (actual >= expected) + return; + + var message = string.Format("Expected at least {0} arguments in {1}, found {2}", expected, @in, actual); + + throw new ParsingException(message, location); + } + + public static void ExpectMaxArguments(int expected, int actual, object @in, NodeLocation location) + { + if (actual <= expected) + return; + + var message = string.Format("Expected at most {0} arguments in {1}, found {2}", expected, @in, actual); + + throw new ParsingException(message, location); + } + + + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Utils/HslColor.cs b/dotnet/Applications/Mocha.Web.Server/Core/Utils/HslColor.cs new file mode 100755 index 0000000..44d187f --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Utils/HslColor.cs @@ -0,0 +1,151 @@ +namespace dotless.Core.Utils +{ + using System; + using System.Linq; + using Parser.Tree; + + public class HslColor + { + // Note: To avoid converting back and forth between HlsColor and Color, HlsColor should perhaps inherit from color and only cenvert when needed. + + private double _hue; + + public double Hue + { + get { return _hue; } + set { _hue = value%1d; } + } + + private double _saturation; + + public double Saturation + { + get { return _saturation; } + set { _saturation = NumberExtensions.Normalize(value); } + } + + private double _lightness; + + public double Lightness + { + get { return _lightness; } + set { _lightness = NumberExtensions.Normalize(value); } + } + + public double Alpha { get; set; } + + public HslColor(double hue, double saturation, double lightness) + : this(hue, saturation, lightness, 1) + { + } + + public HslColor(double hue, double saturation, double lightness, double alpha) + { + Hue = hue; + Saturation = saturation; + Lightness = lightness; + Alpha = alpha; + } + + public HslColor(Number hue, Number saturation, Number lightness, Number alpha) + { + Hue = (hue.ToNumber()/360d)%1d; + Saturation = saturation.Normalize(100d)/100d; + Lightness = lightness.Normalize(100d)/100d; + Alpha = alpha.Normalize(); + } + + public static HslColor FromRgbColor(Color color) + { + // Note: this algorithm from http://www.easyrgb.com/index.php?X=MATH&H=18#text18 + + const int R = 0; + const int G = 1; + const int B = 2; + + var rgb = color.RGB.Select(x => x/255d).ToArray(); + + var min = rgb.Min(); + var max = rgb.Max(); + var range = max - min; + + var lightness = (max + min)/2; + + double saturation = 0; + double hue = 0; + + if (range != 0) + { + if (lightness < 0.5) + saturation = range/(max + min); + else + saturation = range/(2 - max - min); + + var deltas = rgb.Select(x => (((max - x)/6) + (range/2))/range).ToArray(); + + if (rgb[R] == max) + hue = deltas[B] - deltas[G]; + else if (rgb[G] == max) + hue = (1d/3) + deltas[R] - deltas[B]; + else if (rgb[B] == max) + hue = (2d/3) + deltas[G] - deltas[R]; + + if (hue < 0) hue += 1; + if (hue > 1) hue -= 1; + } + + return new HslColor(hue, saturation, lightness, color.Alpha); + } + + + public Color ToRgbColor() + { + // Note: this algorithm from http://www.easyrgb.com/index.php?X=MATH&H=19#text19 + + if (Saturation == 0) + { + var grey = Math.Round(Lightness*255); + return new Color(grey, grey, grey, Alpha); + } + + double q; + if (Lightness < 0.5) + q = Lightness*(1 + Saturation); + else + q = (Lightness + Saturation) - (Saturation*Lightness); + + var p = 2*Lightness - q; + + var red = 255*Hue_2_RGB(p, q, Hue + (1d/3)); + var green = 255*Hue_2_RGB(p, q, Hue); + var blue = 255*Hue_2_RGB(p, q, Hue - (1d/3)); + + return new Color(red, green, blue, Alpha); + } + + private static double Hue_2_RGB(double v1, double v2, double vH) + { + if (vH < 0) vH += 1; + if (vH > 1) vH -= 1; + if ((6*vH) < 1) return (v1 + (v2 - v1)*6*vH); + if ((2*vH) < 1) return (v2); + if ((3*vH) < 2) return (v1 + (v2 - v1)*((2d/3) - vH)*6); + return (v1); + } + + public Number GetHueInDegrees() + { + return new Number(Hue*360, "deg"); + } + + public Number GetSaturation() + { + return new Number(Saturation*100, "%"); + } + + public Number GetLightness() + { + return new Number(Lightness*100, "%"); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Utils/NodeHelper.cs b/dotnet/Applications/Mocha.Web.Server/Core/Utils/NodeHelper.cs new file mode 100755 index 0000000..c11cbc9 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Utils/NodeHelper.cs @@ -0,0 +1,95 @@ +using dotless.Core.Parser.Tree; + +namespace dotless.Core.Utils +{ + using System.Collections.Generic; + using Parser.Infrastructure; + using Parser.Infrastructure.Nodes; + + internal class NodeHelper + { + public static void ExpandNodes(Env env, NodeList rules) + where TNode : Node + { + for (var i = 0; i < rules.Count; i++) + { + var node = rules[i]; + + if (node is TNode) + { + var evaluated = node.Evaluate(env); + var nodes = evaluated as IEnumerable; + if (nodes != null) + { + rules.InsertRange(i + 1, nodes); + rules.RemoveAt(i); + i--; + } + else + { + rules[i] = evaluated; + } + } + } + } + + public static void RecursiveExpandNodes(Env env, Ruleset parentRuleset) + where TNode : Node + { + + env.Frames.Push(parentRuleset); + + for (var i = 0; i < parentRuleset.Rules.Count; i++) + { + var node = parentRuleset.Rules[i]; + + if (node is TNode) + { + var evaluated = node.Evaluate(env); + var nodes = evaluated as IEnumerable; + if (nodes != null) + { + parentRuleset.Rules.InsertRange(i + 1, nodes); + parentRuleset.Rules.RemoveAt(i); + i--; + } + else + { + parentRuleset.Rules[i] = evaluated; + } + } + else + { + var ruleset = node as Ruleset; + if (ruleset != null && ruleset.Rules != null) + { + RecursiveExpandNodes(env, ruleset); + } + } + } + + env.Frames.Pop(); + } + + public static IEnumerable NonDestructiveExpandNodes(Env env, NodeList rules) + where TNode : Node + { + foreach (var node in rules) + { + if (node is TNode) + { + var expandedNodes = (IEnumerable)node.Evaluate(env); + + foreach (var expandedNode in expandedNodes) + { + yield return expandedNode; + } + } + else + { + yield return node; + } + } + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Utils/NumberExtensions.cs b/dotnet/Applications/Mocha.Web.Server/Core/Utils/NumberExtensions.cs new file mode 100755 index 0000000..aedd349 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Utils/NumberExtensions.cs @@ -0,0 +1,38 @@ +namespace dotless.Core.Utils +{ + using Parser.Tree; + + public static class NumberExtensions + { + public static double Normalize(this Number value) + { + return value.Normalize(1d); + } + + public static double Normalize(this Number value, double max) + { + return value.Normalize(0d, max); + } + + public static double Normalize(this Number value, double min, double max) + { + var val = value.ToNumber(max); + return Normalize(val, min, max); + } + + public static double Normalize(double value) + { + return Normalize(value, 1d); + } + + public static double Normalize(double value, double max) + { + return Normalize(value, 0d, max); + } + + public static double Normalize(double value, double min, double max) + { + return value < min ? min : value > max ? max : value; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Utils/ObjectExtensions.cs b/dotnet/Applications/Mocha.Web.Server/Core/Utils/ObjectExtensions.cs new file mode 100755 index 0000000..477659f --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Utils/ObjectExtensions.cs @@ -0,0 +1,21 @@ +using System; + +namespace dotless.Core.Utils +{ + public static class ObjectExtensions + { + /// + /// Helper extension for chaining intermediate actions into expressions. + /// Invokes on and then returns obj. + /// + /// + /// + /// + /// + public static T Do(this T obj, Action action) + { + action(obj); + return obj; + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/Utils/StringExtensions.cs b/dotnet/Applications/Mocha.Web.Server/Core/Utils/StringExtensions.cs new file mode 100755 index 0000000..708288f --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/Utils/StringExtensions.cs @@ -0,0 +1,97 @@ +namespace dotless.Core.Utils +{ + using System.Collections.Generic; + using System.IO; + using System.Linq; + using Parser.Infrastructure; + using Parser.Infrastructure.Nodes; + using System; + + public static class StringExtensions + { + public static string JoinStrings(this IEnumerable source, string separator) + { + return string.Join(separator, source.ToArray()); + } + + public static string AggregatePaths(this IEnumerable source, string currentDirectory) + { + if (!source.Any()) + return ""; + + var path = source.Aggregate("", Path.Combine); + + return CanonicalizePath(path, currentDirectory); + } + + /// + /// Splits the given path into segments and resolves any parent path references + /// it can (eg. "foo/../bar" becomes "bar" whereas "../foo" is left as-is). + /// + private static string CanonicalizePath(string path, string currentDirectory) + { + var pathStack = new Stack(); + + // Split assuming we might get any combination of backward and forward slashes + string[] segments = path.Split('\\', '/'); + foreach (string segment in segments) + { + // If the parent reference is the first one on the stack, do nothing + // (because there's nothing we can do, and removing it would break the path) + if (segment.Equals("..") && pathStack.Count > 0 && pathStack.Peek() != "..") + pathStack.Pop(); + else + pathStack.Push(segment); + } + + IEnumerable pathList = pathStack.Reverse().ToList(); + + // if the imported file is outside the path of the first file then + // path re-writing won't work.. so we can check to see if we can further + // reduce the path. e.g. + // + // base/css/a.less + // @import "../../theme/b.less"; + // theme/b.less + // url("../base/file.png") + // + // then we end up with ../../base/file.png + // which we re-write as ../file.png + if (pathList.First().Equals("..")) + { + // get the total number of parent segments (../) in the path list + var numberOfParents = pathList.TakeWhile(segment => segment.Equals("..")).Count(); + // get the relevant part of the current directory that the ../ goes down too + var currentPathList = currentDirectory + .Split('\\', '/') + .Reverse() + .Take(numberOfParents) + .Reverse(); + // now see how many match going outwards + int numberOfMatchingParents = 0, i = numberOfParents; + + foreach (var currentPathSegment in currentPathList) + { + if (i < pathList.Count() && string.Equals(currentPathSegment, pathList.ElementAt(i++), StringComparison.InvariantCultureIgnoreCase)) + { + numberOfMatchingParents++; + } + else + { + // once a higher up element of the path doesn't match it is invalid to look any deeper + break; + } + } + + // skip out the ../ that match directories we are already in + pathList = pathList.Take(numberOfParents - numberOfMatchingParents) + .Concat(pathList.Skip((numberOfParents - numberOfMatchingParents) + (numberOfMatchingParents*2))); + } + + // Recombine the path segments. Note that there is a difference between doing this + // and pathStack.Reverse().Aggregate("", Path.Combine), which would discard empty path + // segments (and therefore strip leading slashes) + return string.Join("/", pathList.ToArray()); + } + } +} \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/Core/configuration/DotlessConfiguration.cs b/dotnet/Applications/Mocha.Web.Server/Core/configuration/DotlessConfiguration.cs new file mode 100755 index 0000000..8005a92 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Core/configuration/DotlessConfiguration.cs @@ -0,0 +1,216 @@ +namespace dotless.Core.configuration +{ + using System; + using Input; + using Loggers; + using Plugins; + using System.Collections.Generic; + + public enum DotlessSessionStateMode + { + /// + /// Session is not used. + /// + Disabled, + + /// + /// Session is loaded for each request to Dotless HTTP handler. + /// + Enabled, + + /// + /// Session is loaded when URL QueryString parameter specified by is truthy. + /// + QueryParam + } + + public class DotlessConfiguration + { + public const string DEFAULT_SESSION_QUERY_PARAM_NAME = "sstate"; + public const int DefaultHttpExpiryInMinutes = 10080; //7 days + + public static DotlessConfiguration GetDefault() + { + return new DotlessConfiguration(); + } + + public static DotlessConfiguration GetDefaultWeb() + { + return new DotlessConfiguration + { + Web = true + }; + } + + public DotlessConfiguration() + { + LessSource = typeof (FileReader); + MinifyOutput = false; + Debug = false; + CacheEnabled = true; + HttpExpiryInMinutes = DefaultHttpExpiryInMinutes; + Web = false; + SessionMode = DotlessSessionStateMode.Disabled; + SessionQueryParamName = DEFAULT_SESSION_QUERY_PARAM_NAME; + Logger = null; + LogLevel = LogLevel.Error; + Optimization = 1; + Plugins = new List(); + MapPathsToWeb = true; + HandleWebCompression = true; + KeepFirstSpecialComment = false; + RootPath = ""; + StrictMath = false; + } + + public DotlessConfiguration(DotlessConfiguration config) + { + LessSource = config.LessSource; + MinifyOutput = config.MinifyOutput; + Debug = config.Debug; + CacheEnabled = config.CacheEnabled; + Web = config.Web; + SessionMode = config.SessionMode; + SessionQueryParamName = config.SessionQueryParamName; + Logger = null; + LogLevel = config.LogLevel; + Optimization = config.Optimization; + Plugins = new List(); + Plugins.AddRange(config.Plugins); + MapPathsToWeb = config.MapPathsToWeb; + DisableUrlRewriting = config.DisableUrlRewriting; + InlineCssFiles = config.InlineCssFiles; + ImportAllFilesAsLess = config.ImportAllFilesAsLess; + HandleWebCompression = config.HandleWebCompression; + DisableParameters = config.DisableParameters; + KeepFirstSpecialComment = config.KeepFirstSpecialComment; + RootPath = config.RootPath; + StrictMath = config.StrictMath; + } + + /// + /// Keep first comment begining /** + /// + public bool KeepFirstSpecialComment { get; set; } + + /// + /// Disable using parameters + /// + public bool DisableParameters { get; set; } + + /// + /// Stops URL's being adjusted depending on the imported file location + /// + public bool DisableUrlRewriting { get; set; } + + /// + /// Allows you to add a path to every generated import and url in your output css. + /// This corresponds to 'rootpath' option of lessc. + /// + public string RootPath { get; set; } + + /// + /// Disables variables being redefined, so less will search from the bottom of the input up. + /// Makes dotless behave like less.js with regard variables + /// + [Obsolete("The Variable Redefines feature has been removed to align with less.js")] + public bool DisableVariableRedefines { get; set; } + + /// + /// Disables hex color shortening + /// + [Obsolete("The Color Compression feature has been removed to align with less.js")] + public bool DisableColorCompression { get; set; } + + /// + /// Inlines css files into the less output + /// + public bool InlineCssFiles { get; set; } + + /// + /// import all files (even if ending in .css) as less files + /// + public bool ImportAllFilesAsLess { get; set; } + + /// + /// When this is a web configuration, whether to map the paths to the website or just + /// be relative to the current directory + /// + public bool MapPathsToWeb { get; set; } + + /// + /// Whether to minify the ouput + /// + public bool MinifyOutput { get; set; } + + /// + /// Prints helpful comments in the output while debugging. + /// + public bool Debug { get; set; } + + /// + /// For web handlers output in a cached mode. Reccommended on. + /// + public bool CacheEnabled { get; set; } + + /// + /// When is set to true, use this parameter to set how far in the future the expires header will be set. + /// For example, to have the browser cache the CSS for five minutes, set this property to 5. + /// + public int HttpExpiryInMinutes { get; set; } + + /// + /// IFileReader type to use to get imported files + /// + public Type LessSource { get; set; } + + /// + /// Whether this is used in a web context or not + /// + public bool Web { get; set; } + + /// + /// Specifies the mode the HttpContext.Session is loaded. + /// + public DotlessSessionStateMode SessionMode { get; set; } + + /// + /// Gets or sets the URL QueryString parameter name used in conjunction with set to . + /// + public string SessionQueryParamName { get; set; } + + /// + /// The ILogger type + /// + public Type Logger { get; set; } + + /// + /// The Log level + /// + public LogLevel LogLevel { get; set; } + + /// + /// Optimisation int + /// 0 - do not chunk up the input + /// > 0 - chunk up output + /// + /// Recommended value - 1 + /// + public int Optimization { get; set; } + + /// + /// Whether to handle the compression (e.g. look at Accept-Encoding) - true or leave it to IIS - false + /// + public bool HandleWebCompression { get; set; } + + /// + /// Plugins to use + /// + public List Plugins { get; private set; } + + /// + /// Whether to only evaluate mathematical expressions when they are wrapped in an extra set of parentheses. + /// + public bool StrictMath { get; set; } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/InteropExtensions.cs b/dotnet/Applications/Mocha.Web.Server/InteropExtensions.cs new file mode 100755 index 0000000..b2f7399 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/InteropExtensions.cs @@ -0,0 +1,31 @@ +// +// InteropExtensions.cs +// +// Author: +// beckermj <> +// +// Copyright (c) 2023 ${CopyrightHolder} +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +using System; +namespace Mocha.Web.Server +{ + public static class InteropExtensions + { + public static void WriteFixedLengthString(this System.IO.StreamWriter sw, string value) + { + sw.Write(value); + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/Mocha.Web.Server.csproj b/dotnet/Applications/Mocha.Web.Server/Mocha.Web.Server.csproj new file mode 100755 index 0000000..d7dd6eb --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/Mocha.Web.Server.csproj @@ -0,0 +1,241 @@ + + + + Debug + AnyCPU + {6317262E-0B06-486B-AC1F-4494A708493D} + Exe + Mocha.Web.Server + Mocha.Web.Server + v4.7 + + + true + full + false + ..\..\Output\Debug + DEBUG; + prompt + 4 + true + + + true + ..\..\Output\Release + prompt + 4 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {8D3211A6-B2D6-4A26-ABE3-5B57636A4196} + Mocha.Core + + + {8588928F-2868-4248-8993-533307A05C0C} + Mocha.OMS + + + {A4B8D6D8-3365-49FC-8123-58B3749A5427} + Mocha.Storage.Local + + + {C9A3AE1D-0658-4A70-BC48-9D8DEF9C205B} + Mocha.Storage + + + {2D4737E6-6D95-408A-90DB-8DFF38147E85} + UniversalEditor.Core + + + {00266B21-35C9-4A7F-A6BA-D54D7FDCC25C} + MBS.Framework + + + + \ No newline at end of file diff --git a/dotnet/Applications/Mocha.Web.Server/PageBuilder.cs b/dotnet/Applications/Mocha.Web.Server/PageBuilder.cs new file mode 100755 index 0000000..2ec69c3 --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/PageBuilder.cs @@ -0,0 +1,440 @@ +// +// PageBuilder.cs +// +// Author: +// Michael Becker +// +// Copyright (c) 2021 Mike Becker's Software +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +using System; +using System.Web.UI; + +using MBS.Web; +using MBS.Web.Controls; +using Mocha.Core; +using Mocha.OMS; +using Mocha.OMS.OMSComponents; +using Mocha.Web.Controls; + +namespace Mocha.Web.Server +{ + public class PageBuilder + { + public Oms OMS { get; } = null; + public PageBuilder(Oms oms) + { + OMS = oms; + } + + public Control RenderOMSComponent(OMSComponent comp) + { + if (comp is OMSDetailComponent) + { + ListView lvReport = new ListView(); + + // PageBuilder component : Detail Page Component + // gets columns from: Detail Page Component.has column source Method + // gets rows from: Detail Page Component.has row source Method + + OMSDetailComponent cmp = (comp as OMSDetailComponent); + foreach (OMSDetailComponent.OMSDetailColumn col in cmp.Columns) + { + ListViewColumn lvc = new ListViewColumn(); + lvc.ID = "ReportColumn_" + col.InstanceID.ToString(); + lvc.Title = col.Title; + lvc.Attributes.Add("data-instance-id", col.InstanceID.ToString()); // report column inst id + lvReport.Columns.Add(lvc); + } + foreach (OMSDetailComponent.OMSDetailRow row in cmp.Rows) + { + ListViewItem lvi = new ListViewItem(); + lvi.Attributes.Add("data-instance-id", row.InstanceID.ToString()); + foreach (OMSDetailComponent.OMSDetailRowColumn col in row.Columns) + { + ListViewItemColumn lvic = null; + if (col.InstanceIDs != null) + { + lvic = new Mocha.Web.Controls.ListViewItemColumnInstance(); + lvic.Attributes.Add("data-rcid", col.ColumnInstanceID.ToString()); // report column inst id + for (int i = 0; i < col.InstanceIDs.Length; i++) + { + (lvic as Mocha.Web.Controls.ListViewItemColumnInstance).InstanceIDs.Add(col.InstanceIDs[i]); + } + (lvic as Mocha.Web.Controls.ListViewItemColumnInstance).DisplayAsCount = col.DisplayAsCount; + // (lvic as Mocha.Web.Controls.ListViewItemColumnInstance).Text = col.Value; + } + else + { + lvic = new ListViewItemColumn(); + lvic.Value = col.Value; + } + lvic.ColumnID = "ReportColumn_" + col.ColumnInstanceID.ToString(); + lvi.Columns.Add(lvic); + } + lvReport.Items.Add(lvi); + } + return lvReport; + } + else if (comp is OMSSummaryComponent) + { + OMSSummaryComponent cmp = (comp as OMSSummaryComponent); + FormView fv = new FormView(); + fv.CssClass = "mcx-summary"; + fv.Attributes["data-instance-id"] = cmp.InstanceID.ToString(); + foreach (OMSSummaryComponent.OMSSummaryField field in cmp.Fields) + { + if (field is OMSSummaryComponent.OMSSummaryFieldText) + { + OMSSummaryComponent.OMSSummaryFieldText fld = (field as OMSSummaryComponent.OMSSummaryFieldText); + FormViewItemText fvi = new FormViewItemText(); + fvi.Attributes["data-instance-id"] = field.InstanceID.ToString(); + fvi.Title = fld.Title; + fvi.ReadOnly = fld.ReadOnly; + fvi.Value = fld.Value?.ToString(); + fv.Items.Add(fvi); + } + else if (field is OMSSummaryComponent.OMSSummaryFieldInstance) + { + OMSSummaryComponent.OMSSummaryFieldInstance fld = (field as OMSSummaryComponent.OMSSummaryFieldInstance); + FormViewItemInstance fvi = new FormViewItemInstance(); + fvi.Attributes["data-instance-id"] = field.InstanceID.ToString(); + fvi.Title = fld.Title; + fvi.ReadOnly = fld.ReadOnly; + fvi.Value = fld.Value?.ToString(); + for (int i = 0; i < fld.ValidClassIDs.Count; i++) + { + fvi.ValidClassIDs.Add(fld.ValidClassIDs[i]); + } + fv.Items.Add(fvi); + } + else if (field is OMSSummaryComponent.OMSSummaryFieldDateTime) + { + OMSSummaryComponent.OMSSummaryFieldDateTime fld = (field as OMSSummaryComponent.OMSSummaryFieldDateTime); + FormViewItemDateTime fvi = new FormViewItemDateTime(); + fvi.Attributes["data-instance-id"] = field.InstanceID.ToString(); + fvi.Title = fld.Title; + fvi.ReadOnly = fld.ReadOnly; + fvi.Value = fld.Value?.ToString(); + fv.Items.Add(fvi); + } + else if (field is OMSSummaryComponent.OMSSummaryFieldBoolean) + { + OMSSummaryComponent.OMSSummaryFieldBoolean fld = (field as OMSSummaryComponent.OMSSummaryFieldBoolean); + FormViewItemBoolean fvi = new FormViewItemBoolean(); + fvi.Attributes["data-instance-id"] = field.InstanceID.ToString(); + fvi.Title = fld.Title; + fvi.ReadOnly = fld.ReadOnly; + fvi.Value = fld.Value?.ToString(); + fv.Items.Add(fvi); + } + fv.Items[fv.Items.Count - 1].Name = String.Format("SummaryComponent_{0}_{1}", cmp.InstanceID.ToString(), field.InstanceID.ToString()); + } + return fv; + } + else if (comp is OMSTabContainerComponent) + { + TabContainer tbs = new TabContainer(); + OMSTabContainerComponent cmp = (comp as OMSTabContainerComponent); + foreach (OMSTabContainerComponent.TabPage page in cmp.TabPages) + { + TabPage pg = new TabPage(); + pg.Title = page.Title; + foreach (OMSComponent comp1 in page.Components) + { + Control ctl1 = RenderOMSComponent(comp1); + pg.Controls.Add(ctl1); + } + tbs.TabPages.Add(pg); + } + return tbs; + } + else if (comp is OMSSequentialContainerComponent) + { + System.Web.UI.HtmlControls.HtmlGenericControl div = new System.Web.UI.HtmlControls.HtmlGenericControl("div"); + div.AddCssClass("uwt-layout uwt-layout-box"); + div.AddCssClass("uwt-orientation-vertical"); + + foreach (OMSComponent comp2 in ((OMSContainerComponent)comp).Components) + { + System.Web.UI.HtmlControls.HtmlGenericControl div2 = new System.Web.UI.HtmlControls.HtmlGenericControl("div"); + div2.AddCssClass("uwt-layout-item"); + + Control ctl2 = RenderOMSComponent(comp2); + if (ctl2 == null) + { + // MADI error: PageBuilder failed to create component () + continue; + } + div2.Controls.Add(ctl2); + + div.Controls.Add(div2); + } + + return div; + } + else if (comp is OMSHeaderComponent) + { + int level = ((OMSHeaderComponent)comp).Level; + if (level < 1 || level > 6) + { + // add MADI error: level must be between 1 and 6 inclusive + return null; + } + + System.Web.UI.HtmlControls.HtmlGenericControl h = new System.Web.UI.HtmlControls.HtmlGenericControl(String.Format("h{0}", level)); + h.InnerText = ((OMSHeaderComponent)comp).Text; + return h; + } + else if (comp is OMSParagraphComponent) + { + System.Web.UI.HtmlControls.HtmlGenericControl h = new System.Web.UI.HtmlControls.HtmlGenericControl("p"); + h.InnerText = ((OMSParagraphComponent)comp).Text; + return h; + } + else if (comp is OMSImageComponent) + { + InstanceKey targetFile = ((OMSImageComponent)comp).TargetFileInstanceID; + if (targetFile == InstanceKey.Empty) + return null; + + Instance instTargetFile = OMS.GetInstance(targetFile); + + System.Web.UI.WebControls.Image image = new System.Web.UI.WebControls.Image(); + if (instTargetFile != null) + { + image.ImageUrl = String.Format("~/Images/Uploads/{0}.png", instTargetFile.GlobalIdentifier.ToString("B").ToLower()); + } + return image; + } + else if (comp is OMSPanelComponent) + { + Panel panel = new Panel(); + OMSPanelComponent pcomp = (OMSPanelComponent)comp; + for (int i = 0; i < pcomp.HeaderComponents.Count; i++) + { + Control ctl = RenderOMSComponent(pcomp.HeaderComponents[i]); + if (ctl == null) + continue; + + panel.HeaderControls.Controls.Add(ctl); + } + for (int i = 0; i < pcomp.ContentComponents.Count; i++) + { + Control ctl = RenderOMSComponent(pcomp.ContentComponents[i]); + if (ctl == null) + continue; + + panel.ContentControls.Controls.Add(ctl); + } + for (int i = 0; i < pcomp.FooterComponents.Count; i++) + { + Control ctl = RenderOMSComponent(pcomp.FooterComponents[i]); + if (ctl == null) + continue; + + panel.FooterControls.Controls.Add(ctl); + } + return panel; + } + return null; + } + + public void RenderPage(Instance instPage, Control control) + { + OmsContext context = new OmsContext(); + Instance[] instPageComponents = OMS.GetRelatedInstances(instPage, KnownRelationshipGuids.Page__has__Page_Component); + for (int i = 0; i < instPageComponents.Length; i++) + { + Control ctl = CreatePageComponent(instPageComponents[i], context); + if (ctl != null) + { + control.Controls.Add(ctl); + } + } + } + + internal void RenderResponse(IOmsResponse resp, Control page) + { + for (int i = 0; i < resp.Components.Count; i++) + { + Control comp = RenderOMSComponent(resp.Components[i]); + if (comp == null) continue; + + page.Controls.Add(comp); + } + } + + public Control CreatePageComponent(Instance inst, OmsContext context) + { + Instance parentClassInstance = OMS.GetParentClass(inst); + if (parentClassInstance.GlobalIdentifier == KnownInstanceGuids.Classes.SequentialContainerPageComponent) + { + return CreateSequentialContainerPageComponent(inst, context); + } + else if (parentClassInstance.GlobalIdentifier == KnownInstanceGuids.Classes.ImagePageComponent) + { + return CreateImagePageComponent(inst, context); + } + else if (parentClassInstance.GlobalIdentifier == KnownInstanceGuids.Classes.HeadingPageComponent) + { + return CreateHeadingPageComponent(inst, context); + } + else if (parentClassInstance.GlobalIdentifier == KnownInstanceGuids.Classes.ParagraphPageComponent) + { + System.Web.UI.HtmlControls.HtmlGenericControl p = new System.Web.UI.HtmlControls.HtmlGenericControl("p"); + Instance instText = OMS.GetRelatedInstance(inst, KnownRelationshipGuids.Content_Page_Component__gets_content_from__Method); + p.InnerHtml = OMS.ExecuteMethodReturningTextOrTranslation(instText, context); + return p; + } + else if (parentClassInstance.GlobalIdentifier == KnownInstanceGuids.Classes.PanelPageComponent) + { + MBS.Web.Controls.Panel panel = new MBS.Web.Controls.Panel(); + + Instance[] instHeaderComponents = OMS.GetRelatedInstances(inst, KnownRelationshipGuids.Panel_Page_Component__has_header__Page_Component); + for (int i = 0; i < instHeaderComponents.Length; i++) + { + Control ctl = CreatePageComponent(instHeaderComponents[i], context); + if (ctl == null) continue; + + panel.HeaderControls.Controls.Add(ctl); + } + Instance[] instContentComponents = OMS.GetRelatedInstances(inst, KnownRelationshipGuids.Panel_Page_Component__has_content__Page_Component); + for (int i = 0; i < instContentComponents.Length; i++) + { + Control ctl = CreatePageComponent(instContentComponents[i], context); + if (ctl == null) continue; + + panel.ContentControls.Controls.Add(ctl); + } + Instance[] instFooterComponents = OMS.GetRelatedInstances(inst, KnownRelationshipGuids.Panel_Page_Component__has_footer__Page_Component); + for (int i = 0; i < instFooterComponents.Length; i++) + { + Control ctl = CreatePageComponent(instFooterComponents[i], context); + if (ctl == null) continue; + + panel.FooterControls.Controls.Add(ctl); + } + + return panel; + } + else if (parentClassInstance.GlobalIdentifier == KnownInstanceGuids.Classes.ButtonPageComponent) + { + System.Web.UI.WebControls.Button item = new System.Web.UI.WebControls.Button(); + item.Text = OMS.GetTranslationValue(inst, KnownRelationshipGuids.Button_Page_Component__has_text__Translatable_Text_Constant); + return item; + } + else if (parentClassInstance.GlobalIdentifier == KnownInstanceGuids.Classes.DetailPageComponent) + { + MBS.Web.Controls.ListView lv = new ListView(); + + Instance instColumnSourceRSMB = OMS.GetRelatedInstance(inst, KnownRelationshipGuids.Detail_Page_Component__has_column_source__Method_Binding); + // column source can return an Instance Set of columns, or a method returning an instance set of columns + object columnSourceValue = OMS.ExecuteMethod(instColumnSourceRSMB, context); + + if (columnSourceValue is Instance[]) + { + Instance[] reportColumns = (Instance[])columnSourceValue; + foreach (Instance instColumn in reportColumns) + { + ListViewColumn lvc = CreateListViewColumnForReportColumn(instColumn); + if (lvc != null) + lv.Columns.Add(lvc); + } + } + + Instance instRowSourceRSMB = OMS.GetRelatedInstance(inst, KnownRelationshipGuids.Detail_Page_Component__has_row_source__Method_Binding); + + lv.Title = OMS.GetTranslationValue(inst, KnownRelationshipGuids.Detail_Page_Component__has_caption__Translation); + return lv; + } + else if (parentClassInstance.GlobalIdentifier == KnownInstanceGuids.Classes.SummaryPageComponent) + { + MBS.Web.Controls.FormView item = new MBS.Web.Controls.FormView(); + item.CssClass = "mcx-summary"; + item.Attributes["data-instance-id"] = OMS.GetInstanceKey(inst).ToString(); + + object bEditable = OMS.GetAttributeValue(inst, KnownAttributeGuids.Boolean.Editable, false); + // item.Editable = (bool)bEditable; + return item; + } + throw new NotSupportedException(); + } + + private ListViewColumn CreateListViewColumnForReportColumn(Instance instColumn) + { + ListViewColumn lvc = new ListViewColumn(); + InstanceKey ik = OMS.GetInstanceKey(instColumn); + lvc.ID = "ReportColumn_" + ik.ToString(); + lvc.Title = OMS.GetInstanceText(instColumn); + lvc.Attributes.Add("data-instance-id", ik.ToString()); // report column inst id + return lvc; + } + + + private System.Web.UI.WebControls.Panel CreateSequentialContainerPageComponent(Instance inst, OmsContext context) + { + System.Web.UI.WebControls.Panel panel = new System.Web.UI.WebControls.Panel(); + + Instance instOrientation = OMS.GetRelatedInstance(inst, KnownRelationshipGuids.Sequential_Container_Page_Component__has__Sequential_Container_Orientation); + // TODO: set the orientation of the Panel for the Sequential Container Page Component + + Instance[] instComponents = OMS.GetRelatedInstances(inst, KnownRelationshipGuids.Container_Page_Component__has__Page_Component); + for (int i = 0; i < instComponents.Length; i++) + { + Control ctl = CreatePageComponent(instComponents[i], context); + if (ctl == null) continue; + + panel.Controls.Add(ctl); + } + + return panel; + } + private System.Web.UI.HtmlControls.HtmlGenericControl CreateHeadingPageComponent(Instance inst, OmsContext context) + { + System.Web.UI.HtmlControls.HtmlGenericControl ctl = null; + object jlevel = OMS.GetAttributeValue(inst, KnownAttributeGuids.Numeric.Level); + ctl = new System.Web.UI.HtmlControls.HtmlGenericControl("h" + jlevel.ToString()); + + string value = OMS.GetTranslationValue(inst, new Guid("{C5027DC2-53EE-4FC0-9BA6-F2B883F7DAD8}")); + ctl.InnerHtml = value; + return ctl; + } + + // FIXME: BIG PROBLEM!!! + // right now the CLIENT is bouncing back and forth requesting EVERY LITTLE INSTANCE From the server + // especially when loading Pages, the CLIENT should make ONE SINGLE REQUEST for the page + // the server should then push out EVERYTHING The client needs to make sense of the page + // (e.g. push out an entire JSON response representing all the page components on the page) + // rather than the client having to do this... handling 1000+ requests to and from the OMS each time. + + // tl;dr : THERE SHOULD ONLY BE ONE REQUEST TO THE OMS PER PAGE!!! + + private System.Web.UI.WebControls.Image CreateImagePageComponent(Instance inst, OmsContext context) + { + System.Web.UI.WebControls.Image image = new System.Web.UI.WebControls.Image(); + + Instance instTargetFile = OMS.GetRelatedInstance(inst, KnownRelationshipGuids.Image_Page_Component__has_source__Method); + if (instTargetFile == null) + return null; + + string attfmt = (OMS.GetAttributeValue(instTargetFile, KnownAttributeGuids.Text.ContentType) as string); + string attvalue = (OMS.GetAttributeValue(instTargetFile, KnownAttributeGuids.Text.Value) as string); + // image.ImageUrl = String.Format("data:{0};base64,{1}", attfmt, attvalue); + image.ImageUrl = String.Format("~/Images/Uploads/{0}.png", instTargetFile.GlobalIdentifier.ToString("b")); + + return image; + } + } +} diff --git a/dotnet/Applications/Mocha.Web.Server/PageBuilder2.cs b/dotnet/Applications/Mocha.Web.Server/PageBuilder2.cs new file mode 100755 index 0000000..6fc0bcc --- /dev/null +++ b/dotnet/Applications/Mocha.Web.Server/PageBuilder2.cs @@ -0,0 +1,548 @@ +// +// PageBuilder2.cs +// +// Author: +// beckermj <> +// +// Copyright (c) 2022 ${CopyrightHolder} +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +using System; +using System.Collections.Generic; +using System.IO; +using Mocha.Core; +using Mocha.OMS; +using UniversalEditor.IO; + +namespace Mocha.Web.Server +{ + public class PageBuilder2 + { + private Mocha.OMS.Oms oms = null; + private SessionContext sess = null; + public PageBuilder2(Mocha.OMS.Oms oms, SessionContext sess) + { + this.oms = oms; + this.sess = sess; + } + + private void RenderBeginTag(Instance inst, StreamWriter sw) + { + InstanceKey instId = oms.GetInstanceKey(inst); + Instance instParentClass = oms.GetParentClass(inst); + if (instParentClass.GlobalIdentifier == KnownInstanceGuids.Classes.ImagePageComponent) + { + Instance instImage = oms.GetRelatedInstance(inst, KnownRelationshipGuids.Image_Page_Component__has_source__Method); + + OmsContext ctx = new OmsContext(); + object val = oms.ExecuteMethod(instImage, ctx); + + string imgurl = sess.GetAttachmentUrl(((Instance[])val)[0], sess.GetOmsAttachmentEntropy()); + sw.WriteLine(String.Format("", instId, imgurl)); + } + else if (instParentClass.GlobalIdentifier == KnownInstanceGuids.Classes.PanelPageComponent) + { + sw.WriteLine(String.Format("
", instId)); + + sw.WriteLine("
"); + Instance[] instHeaderComponents = oms.GetRelatedInstances(inst, KnownRelationshipGuids.Panel_Page_Component__has_header__Page_Component); + foreach (Instance inst2 in instHeaderComponents) + { + RenderPageComponent(inst2, sw); + } + sw.WriteLine("
"); + sw.WriteLine("
"); + Instance[] instContentComponents = oms.GetRelatedInstances(inst, KnownRelationshipGuids.Panel_Page_Component__has_content__Page_Component); + foreach (Instance inst2 in instContentComponents) + { + RenderPageComponent(inst2, sw); + } + sw.WriteLine("
"); + sw.WriteLine("
"); + Instance[] instFooterComponents = oms.GetRelatedInstances(inst, KnownRelationshipGuids.Panel_Page_Component__has_footer__Page_Component); + foreach (Instance inst2 in instFooterComponents) + { + RenderPageComponent(inst2, sw); + } + sw.WriteLine("
"); + } + else if (instParentClass.GlobalIdentifier == KnownInstanceGuids.Classes.ButtonPageComponent) + { + sw.WriteFixedLengthString(String.Format("