├── .gitignore ├── Clausewitz.sln.DotSettings.user ├── LICENSE ├── README.md ├── Tamar.Clausewitz.CLI ├── CLI.cs ├── Tamar.Clausewitz.CLI.csproj └── Test │ └── input.txt ├── Tamar.Clausewitz.sln ├── Tamar.Clausewitz.sln.DotSettings.user └── Tamar.Clausewitz ├── Constructs ├── Binding.cs ├── Construct.cs ├── Extensions.cs ├── Pragma.cs ├── Scope.cs └── Token.cs ├── IO ├── Directory.cs ├── Extensions.cs ├── FileScope.cs └── IExplorable.cs ├── Interpreter.cs ├── Log.cs └── Tamar.Clausewitz.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | bin 3 | obj 4 | *.bat -------------------------------------------------------------------------------- /Clausewitz.sln.DotSettings.user: -------------------------------------------------------------------------------- 1 |  2 | ForceIncluded 3 | ForceIncluded 4 | ForceIncluded 5 | ForceIncluded 6 | ForceIncluded 7 | ForceIncluded 8 | ForceIncluded 9 | ForceIncluded 10 | ForceIncluded 11 | ForceIncluded 12 | ForceIncluded 13 | ForceIncluded 14 | ForceIncluded 15 | ForceIncluded 16 | ForceIncluded 17 | ForceIncluded 18 | ForceIncluded 19 | ForceIncluded 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A .NET Interpreter for Clausewitz Engine 2 | ## Introduction 3 | This is a .NET interpreter for Clausewitz's scripting language. The interpreter helps with reading, writing, editing and querying the contents of standard Clausewitz files made by Paradox Interactive for their various soft-coded games. 4 | 5 | This interpreter uses an abstract data tree structure when tokenizing the Clausewitz files, and offers pragma commands for sorting and indenting files, and enforces comment association to prevent their loss. With these features, it may be used as a cleanup tool for messy projects that were already made around Clausewitz files. 6 | 7 | ## Dependencies 8 | 1. Currently set to .NET 6.0. 9 | 2. The CLI requires my **[ANSITerm](https://github.com/david-tamar/ansi-term)** library[1](#WhyANSITerm). If your IDE does not resolve this dependency from Nuget, then you may either git clone that library and attach it to this solution, or you may download the package `Tamar.ANSITerm` itself from Nuget's website. 10 | 11 | ## I/O results 12 | 13 | Input: **[input.txt](Tamar.Clausewitz.CLI%2FTest%2Finput.txt)** 14 | 15 | Output: **[output.txt](Tamar.Clausewitz.CLI%2FTest%2Foutput.txt)** 16 | 17 | ## Notes: 18 | 1: I created my own ANSI-Compliant System.Console implementation because .NET's default System.Console could not display 24-bit colors properly on Linux terminals which support ANSI escape codes. 19 | -------------------------------------------------------------------------------- /Tamar.Clausewitz.CLI/CLI.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.IO; 3 | using System.Linq; 4 | using Tamar.ANSITerm; 5 | using Tamar.Clausewitz.Constructs; 6 | using Directory = Tamar.Clausewitz.IO.Directory; 7 | using FileScope = Tamar.Clausewitz.IO.FileScope; 8 | 9 | namespace Tamar.Clausewitz.CLI 10 | { 11 | /// 12 | /// The CLI: "Command Line Interface" of the Clausewitz interpreter helps the 13 | /// user/developer to interact with the 14 | /// interpreter through a console interface for some basic commands. 15 | /// 16 | public static class CLI 17 | { 18 | /// Main entry point. 19 | public static void Main() 20 | { 21 | Console.WriteLine("A .NET Interpreter for Clausewitz Engine.\nWritten by David von Tamar, LGPLv3"); 22 | Log.MessageSent += LogMessage; 23 | Console.CursorVisible = true; 24 | var input = Interpreter.ReadFile(@"Test\input.txt"); 25 | if (!errorOccured) 26 | { 27 | PrettyPrint(input.Parent.Parent); 28 | input.Name = "output.txt"; 29 | input.Write(); 30 | } 31 | Log.Send("Press any key to exit."); 32 | Console.ReadKey(); 33 | } 34 | 35 | /// Draws tree structure to the left. 36 | /// 37 | /// The initial node. (will stop looking for parents beyond this member) 38 | /// 39 | /// Current construct. 40 | /// 41 | /// Whether this line opens a new node from its parent scope, or rather drawn above 42 | /// or beneath a node. 43 | /// 44 | private static string ConcatTree(object root, object current, Alignment alignment = Alignment.Inner) 45 | { 46 | if (root == current) 47 | return string.Empty; 48 | var tree = string.Empty; 49 | var isLast = false; 50 | switch (current) 51 | { 52 | // Inside a scope/file: 53 | case Construct construct: 54 | { 55 | if (construct is FileScope file) 56 | { 57 | var parent = file.Parent; 58 | if (parent.Explorables.Last() == file) 59 | isLast = true; 60 | else if (parent.Explorables.First() == file) 61 | { 62 | } 63 | tree += ConcatTree(root, parent); 64 | } 65 | else 66 | { 67 | var parent = construct.Parent; 68 | if (parent.Members.Last() == construct) 69 | isLast = true; 70 | tree += ConcatTree(root, parent); 71 | } 72 | break; 73 | } 74 | 75 | // Inside a directory: 76 | case Directory directory: 77 | { 78 | var parent = directory.Parent; 79 | if (parent.Explorables.Last() == directory) 80 | isLast = true; 81 | tree += ConcatTree(root, parent); 82 | break; 83 | } 84 | 85 | // End of switch body. 86 | } 87 | switch (alignment) 88 | { 89 | case Alignment.Before: 90 | tree += "│ "; 91 | break; 92 | case Alignment.After: 93 | case Alignment.Inner: 94 | { 95 | if (isLast) 96 | tree += " "; 97 | else 98 | tree += "│ "; 99 | break; 100 | } 101 | case Alignment.Node when isLast: 102 | tree += "└─"; 103 | break; 104 | case Alignment.Node: 105 | tree += "├─"; 106 | break; 107 | } 108 | return tree; 109 | } 110 | 111 | /// Handles messages from the log. 112 | /// Message sent. 113 | private static void LogMessage(Log.Message message) 114 | { 115 | switch (message.Type) 116 | { 117 | case Log.Message.Types.Error: 118 | errorOccured = true; 119 | Console.Write("ERROR", Color.White, Color.Red); 120 | break; 121 | case Log.Message.Types.Info: 122 | Console.Write("Info", Color.Cyan, Color.Blue); 123 | break; 124 | default: 125 | Console.Write("Message", Color.White, Color.DarkGray); 126 | break; 127 | } 128 | Console.WriteLine(' ' + message.Text, Color.White, Color.DarkBlue); 129 | if (string.IsNullOrWhiteSpace(message.Details)) 130 | return; 131 | Console.WriteLine(message.Details, Color.White); 132 | } 133 | 134 | /// Pretty-prints a Clausewitz scope/file/directory. 135 | /// Initial scope (file, directory or just a random scope). 136 | /// Used for recursive iteration through inner scopes. 137 | private static void PrettyPrint(object root, object current = null) 138 | { 139 | if (current == null) 140 | current = root; 141 | if (current is Construct construct) 142 | { 143 | foreach (var comment in construct.Comments) 144 | { 145 | // Preceding comments: 146 | Console.Write(ConcatTree(root, construct, Alignment.Before), TreeFore, DefaultBack); 147 | Console.WriteLine(comment, CommentFore); 148 | } 149 | } 150 | Console.Write(ConcatTree(root, current, Alignment.Node), TreeFore, DefaultBack); 151 | switch (current) 152 | { 153 | case Binding binding: 154 | Console.Write(binding.Name, TokenFore, TokenBack); 155 | Console.Write(" = ", BindingFore, DefaultBack); 156 | Console.WriteLine(binding.Value, TokenFore, TokenBack); 157 | break; 158 | case Scope scope: 159 | if (scope is FileScope) 160 | Console.WriteLine(scope.Name, FileFore, FileBack); 161 | else if (!string.IsNullOrWhiteSpace(scope.Name)) 162 | Console.WriteLine(scope.Name, ScopeFore, ScopeBack); 163 | else 164 | { 165 | Console.WriteLine(scope.Members.Count > 0 ? 166 | "┐" : 167 | "─", TreeFore, DefaultBack); 168 | } 169 | foreach (var member in scope.Members) 170 | PrettyPrint(root, member); 171 | foreach (var comment in scope.EndComments) 172 | { 173 | // End comments: 174 | Console.Write(ConcatTree(root, scope, Alignment.After), TreeFore, DefaultBack); 175 | Console.WriteLine(" " + comment, CommentFore); 176 | } 177 | break; 178 | case Directory directory: 179 | if (!string.IsNullOrWhiteSpace(directory.Name)) 180 | Console.WriteLine(directory.Name + Path.DirectorySeparatorChar, DirectoryFore, DirectoryBack); 181 | foreach (var subDirectory in directory.Directories) 182 | PrettyPrint(root, subDirectory); 183 | foreach (var file in directory.Files) 184 | PrettyPrint(root, file); 185 | break; 186 | case Token token: 187 | Console.WriteLine(token.Value, TokenFore, TokenBack); 188 | break; 189 | } 190 | Console.ResetColor(); 191 | } 192 | 193 | private static readonly Color BindingFore = Color.Magenta; 194 | private static readonly Color CommentFore = Color.FromArgb(0, 255, 0); 195 | private static readonly Color DefaultBack = Color.Black; 196 | private static readonly Color DirectoryBack = Color.FromArgb(128, 128, 0); 197 | private static readonly Color DirectoryFore = Color.Yellow; 198 | 199 | /// 200 | /// Indicates that an error occured during the interpretation time. 201 | /// 202 | private static bool errorOccured; 203 | 204 | private static readonly Color FileBack = Color.DarkCyan; 205 | private static readonly Color FileFore = Color.Cyan; 206 | private static readonly Color ScopeBack = Color.Blue; 207 | private static readonly Color ScopeFore = Color.Cyan; 208 | private static readonly Color TokenBack = Color.DarkBlue; 209 | private static readonly Color TokenFore = Color.White; 210 | private static readonly Color TreeFore = Color.DarkGray; 211 | 212 | /// 213 | /// Special enum for pretty-printing which determines the junctions at the tree 214 | /// hierarchy. 215 | /// 216 | private enum Alignment 217 | { 218 | Before, 219 | Node, 220 | After, 221 | Inner 222 | } 223 | } 224 | } -------------------------------------------------------------------------------- /Tamar.Clausewitz.CLI/Tamar.Clausewitz.CLI.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | net6.0 5 | Tamar.Clausewitz.CLI 6 | false 7 | Tamar.Clausewitz.CLI 8 | default 9 | 10 | 11 | full 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | 23 | 24 | 25 | PreserveNewest 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Tamar.Clausewitz.CLI/Test/input.txt: -------------------------------------------------------------------------------- 1 | # File comment. 2 | # Another file comment. 3 | 4 | # Yet another one. 5 | 6 | #tricky_comment = { 7 | #} 8 | 9 | # Not file comment. 10 | #[sort all] 11 | named_scope = { 12 | #[unindent] 13 | OR = { 14 | B 15 | # Comment 16 | A C # Attached comment 17 | # Inner end comment 18 | } 19 | # Penrose 20 | { 21 | # Say 22 | Hi = {{{ I_like_stairs }} A = { B = { C }}} 23 | { 24 | # Lonely comment 25 | } 26 | } 27 | # Outer end comment 28 | } 29 | # Unnamed scope 30 | { 31 | text = "\"escaped quote" 32 | 1 = 2 33 | "English" 34 | #[unindent all] 35 | colors = { 36 | { 255 255 255 } 37 | # Nameless 38 | { 39 | # Just a color 40 | 255 # R 41 | 255 # G 42 | 255 # B 43 | } 44 | } 45 | } 46 | # File end comments. -------------------------------------------------------------------------------- /Tamar.Clausewitz.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tamar.Clausewitz.CLI", "Tamar.Clausewitz.CLI\Tamar.Clausewitz.CLI.csproj", "{4C7B4026-C67D-4B9B-97E2-DBB270FDBE20}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tamar.Clausewitz", "Tamar.Clausewitz\Tamar.Clausewitz.csproj", "{8D89E5C6-35D0-44D9-AAB8-495C137B876A}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tamar.ANSITerm", "..\ansi-term\Tamar.ANSITerm\Tamar.ANSITerm.csproj", "{792A9B03-ED7F-46EC-98F1-CE1B270C82A4}" 8 | EndProject 9 | Global 10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 11 | Debug|Any CPU = Debug|Any CPU 12 | Release|Any CPU = Release|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {4C7B4026-C67D-4B9B-97E2-DBB270FDBE20}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {4C7B4026-C67D-4B9B-97E2-DBB270FDBE20}.Release|Any CPU.Build.0 = Release|Any CPU 17 | {4C7B4026-C67D-4B9B-97E2-DBB270FDBE20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {4C7B4026-C67D-4B9B-97E2-DBB270FDBE20}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {8D89E5C6-35D0-44D9-AAB8-495C137B876A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {8D89E5C6-35D0-44D9-AAB8-495C137B876A}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {8D89E5C6-35D0-44D9-AAB8-495C137B876A}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {8D89E5C6-35D0-44D9-AAB8-495C137B876A}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {792A9B03-ED7F-46EC-98F1-CE1B270C82A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {792A9B03-ED7F-46EC-98F1-CE1B270C82A4}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {792A9B03-ED7F-46EC-98F1-CE1B270C82A4}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {792A9B03-ED7F-46EC-98F1-CE1B270C82A4}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /Tamar.Clausewitz.sln.DotSettings.user: -------------------------------------------------------------------------------- 1 |  2 | ForceIncluded 3 | ForceIncluded 4 | ForceIncluded 5 | ForceIncluded 6 | ForceIncluded 7 | ForceIncluded 8 | ForceIncluded 9 | ForceIncluded 10 | ForceIncluded 11 | ForceIncluded 12 | ForceIncluded 13 | ForceIncluded 14 | ForceIncluded 15 | ForceIncluded 16 | ForceIncluded 17 | ForceIncluded 18 | ForceIncluded 19 | ForceIncluded -------------------------------------------------------------------------------- /Tamar.Clausewitz/Constructs/Binding.cs: -------------------------------------------------------------------------------- 1 | namespace Tamar.Clausewitz.Constructs; 2 | 3 | /// 4 | /// Any statement which includes the assignment operator '=' in Clausewitz. 5 | /// Including most commands, conditions 6 | /// and triggers which come in a single line. 7 | /// 8 | public class Binding : Construct 9 | { 10 | /// Left side. 11 | public string Name; 12 | 13 | /// Right side. 14 | public string Value; 15 | 16 | /// Primary constructor. 17 | /// Parent scope. 18 | /// Left side. 19 | /// Right side. 20 | internal Binding(Scope parent, string name, string value) : base(parent) 21 | { 22 | Name = name; 23 | Value = value; 24 | } 25 | } -------------------------------------------------------------------------------- /Tamar.Clausewitz/Constructs/Construct.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace Tamar.Clausewitz.Constructs; 5 | 6 | /// Basic Clausewitz language construct. 7 | public abstract class Construct 8 | { 9 | /// Associated comments. 10 | public readonly List Comments = new(); 11 | 12 | /// Construct type & parent must be defined when created. 13 | /// Parent scope. 14 | protected Construct(Scope parent) 15 | { 16 | Parent = parent; 17 | } 18 | 19 | /// Scope's depth level within the containing file. 20 | public int Level 21 | { 22 | get 23 | { 24 | // This recursive function retrieves the count of all parents up to the root. 25 | var parentScopes = 0; 26 | var currentScope = Parent; 27 | while (true) 28 | { 29 | if (currentScope == null) 30 | return parentScopes; 31 | parentScopes++; 32 | currentScope = currentScope.Parent; 33 | } 34 | } 35 | } 36 | 37 | /// The parent scope. 38 | public Scope Parent { get; internal set; } 39 | 40 | /// 41 | /// Extracts pragmas from associated comments within brackets, which are separated 42 | /// by commas, and their keywords 43 | /// which are separated by spaces. 44 | /// 45 | public IEnumerable Pragmas 46 | { 47 | get 48 | { 49 | var allComments = new List(); 50 | var @return = new HashSet(); 51 | if (Comments != null) 52 | allComments.AddRange(Comments); 53 | if (this is Scope scope) 54 | if (scope.EndComments != null) 55 | allComments.AddRange(scope.EndComments); 56 | if (allComments.Count == 0) 57 | return @return; 58 | foreach (var comment in allComments) 59 | { 60 | if (!(comment.Contains('[') && comment.Contains(']'))) 61 | continue; 62 | 63 | // All pragmas are guaranteed to be lower case and trimmed. 64 | var pragmas = Regex.Replace(comment.Split('[', ']')[1], @"\s+", " ").ToLower().Split(','); 65 | foreach (var pragma in pragmas) 66 | @return.Add(new Pragma(pragma.Split(' '))); 67 | } 68 | 69 | return @return; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /Tamar.Clausewitz/Constructs/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Tamar.Clausewitz.Constructs; 4 | 5 | /// 6 | /// Extension class for all language constructs. 7 | /// 8 | public static class Extensions 9 | { 10 | /// 11 | /// Checks if a collection of pragmas has the requested pragma with the specified 12 | /// keywords. 13 | /// 14 | /// Extended. 15 | /// Keywords (all keywords). 16 | /// Returns true if a pragma with the said keywords was found. 17 | public static bool Contains(this IEnumerable pragmas, params string[] keywords) 18 | { 19 | foreach (var pragma in pragmas) 20 | if (pragma.Contains(keywords)) 21 | return true; 22 | return false; 23 | } 24 | 25 | /// 26 | /// Formats a keyword to lower case and trims it. 27 | /// 28 | /// Extended. 29 | /// Formatted 30 | internal static string FormatKeyword(this string keyword) 31 | { 32 | return keyword.Trim().ToLower(); 33 | } 34 | 35 | /// 36 | /// Formats all keywords with lazy evaluation. 37 | /// 38 | /// Extended. 39 | /// Formatted. 40 | internal static IEnumerable FormatKeywords(this IEnumerable keywords) 41 | { 42 | foreach (var keyword in keywords) 43 | yield return keyword.FormatKeyword(); 44 | } 45 | } -------------------------------------------------------------------------------- /Tamar.Clausewitz/Constructs/Pragma.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Tamar.Clausewitz.Constructs; 4 | 5 | /// 6 | /// Every Clausewitz construct may have pragmas within the associated comments. 7 | /// Each pragma includes a set of 8 | /// keywords. 9 | /// 10 | public struct Pragma 11 | { 12 | /// User-friendly constructor. 13 | /// Keywords. 14 | public Pragma(params string[] keywords) 15 | { 16 | Keywords = new HashSet(keywords); 17 | } 18 | 19 | /// Primary constructor. 20 | /// Keywords. 21 | public Pragma(HashSet keywords) 22 | { 23 | Keywords = keywords; 24 | } 25 | 26 | /// Checks if a pragma has all of the specified keywords. 27 | /// Keywords. 28 | /// Boolean. 29 | public bool Contains(IEnumerable keywords) 30 | { 31 | return Keywords.IsSupersetOf(keywords.FormatKeywords()); 32 | } 33 | 34 | /// Checks if a pragma has all of the specified keywords. 35 | /// Keywords. 36 | /// Boolean. 37 | public bool Contains(params string[] keywords) 38 | { 39 | return Keywords.IsSupersetOf(keywords.FormatKeywords()); 40 | } 41 | 42 | /// 43 | /// Keywords are separated by spaces within each pragma, and their order does not 44 | /// matter. 45 | /// 46 | public readonly HashSet Keywords; 47 | } -------------------------------------------------------------------------------- /Tamar.Clausewitz/Constructs/Scope.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Tamar.Clausewitz.Constructs; 4 | 5 | /// Scopes are files, directories, and clauses. 6 | public class Scope : Construct 7 | { 8 | /// Child members. 9 | public readonly List Members = new(); 10 | 11 | /// Comments located at the end of the scope. 12 | public List EndComments = new(); 13 | 14 | /// 15 | /// Special scope constructor for files (which has no parent scope, but parent 16 | /// directory). 17 | /// 18 | /// Scope name. 19 | protected Scope(string name) : this(null, name) 20 | { 21 | // implemented at primary constructor. 22 | } 23 | 24 | /// Primary constructor. 25 | /// Parent scope. 26 | /// Optional name. 27 | private Scope(Scope parent, string name = null) : base(parent) 28 | { 29 | if (name != null) 30 | Name = name; 31 | } 32 | 33 | /// If false, all members within this scope will come in a single line. 34 | public bool Indented 35 | { 36 | get 37 | { 38 | if (Pragmas.Contains("indent")) 39 | return true; 40 | if (Pragmas.Contains("unindent")) 41 | return false; 42 | if (IndentedParent(Parent)) 43 | return true; 44 | if (UnindentedParent(Parent)) 45 | return false; 46 | if (AllTokens() && Members.Count > 20) 47 | return false; 48 | return true; 49 | 50 | bool IndentedParent(Scope parent) 51 | { 52 | while (true) 53 | { 54 | if (parent == null) 55 | return false; 56 | if (parent.Pragmas.Contains("indent", "all")) 57 | return true; 58 | parent = parent.Parent; 59 | } 60 | } 61 | 62 | bool UnindentedParent(Scope parent) 63 | { 64 | while (true) 65 | { 66 | if (parent == null) 67 | return false; 68 | if (parent.Pragmas.Contains("unindent", "all")) 69 | return true; 70 | parent = parent.Parent; 71 | } 72 | } 73 | 74 | bool AllTokens() 75 | { 76 | foreach (var member in Members) 77 | if (!(member is Token)) 78 | return false; 79 | return true; 80 | } 81 | } 82 | } 83 | 84 | /// Optional scope name (not all scopes have names in Clausewitz) 85 | public string Name { get; set; } 86 | 87 | /// If true, all members within this scope will be sorted alphabetically. 88 | public bool Sorted 89 | { 90 | get 91 | { 92 | return SortedParent(Parent) || Pragmas.Contains("sort"); 93 | 94 | bool SortedParent(Scope parent) 95 | { 96 | while (true) 97 | { 98 | if (parent == null) 99 | return false; 100 | if (parent.Pragmas.Contains("sort", "all")) 101 | return true; 102 | parent = parent.Parent; 103 | } 104 | } 105 | } 106 | } 107 | 108 | /// 109 | /// Creates a new binding within this scope. (Automatically assigns the 110 | /// parent) 111 | /// 112 | /// Left side. 113 | /// Right side. 114 | /// New binding. 115 | public Binding NewBinding(string name, string value) 116 | { 117 | var binding = new Binding(this, name, value); 118 | Members.Add(binding); 119 | return binding; 120 | } 121 | 122 | /// 123 | /// Creates a new scope within this scope. (Automatically assigns the 124 | /// parent) 125 | /// 126 | /// Optional name. 127 | /// New scope. 128 | public Scope NewScope(string name = null) 129 | { 130 | var scope = new Scope(this, name); 131 | Members.Add(scope); 132 | return scope; 133 | } 134 | 135 | /// 136 | /// Creates a new token within this scope. (Automatically assigns the 137 | /// parent) 138 | /// 139 | /// Token symbol/string/value. 140 | /// New token. 141 | public Token NewToken(string value) 142 | { 143 | var token = new Token(this, value); 144 | Members.Add(token); 145 | return token; 146 | } 147 | 148 | /// 149 | /// Sorts members alphabetically. 150 | /// 151 | public void Sort() 152 | { 153 | Members.Sort((first, second) => 154 | { 155 | var constructs = new[] 156 | { 157 | first, 158 | second 159 | }; 160 | var values = new string[2]; 161 | for (var index = 0; index < constructs.Length; index++) 162 | { 163 | var construct = constructs[index]; 164 | switch (construct) 165 | { 166 | case Binding binding: 167 | values[index] = binding.Name; 168 | break; 169 | case Scope scope: 170 | values[index] = scope.Name; 171 | break; 172 | case Token token: 173 | values[index] = token.Value; 174 | break; 175 | } 176 | } 177 | 178 | return string.CompareOrdinal(values[0], values[1]); 179 | }); 180 | } 181 | 182 | /// 183 | /// Finds recursively a scope by its exact name. 184 | /// 185 | public Scope FindScope(string name) 186 | { 187 | foreach (var member in Members) 188 | if (member is Scope scope) 189 | if (scope.Name == name) 190 | return scope; 191 | else 192 | { 193 | var found = scope.FindScope(name); 194 | if (found != null) 195 | return found; 196 | } 197 | return null; 198 | } 199 | /// 200 | /// Finds a binding in this scope by its exact name. 201 | /// 202 | public Binding FindBinding(string name) 203 | { 204 | foreach (var member in Members) 205 | if (member is Binding binding && binding.Name == name) 206 | return binding; 207 | return null; 208 | } 209 | 210 | /// 211 | /// Returns true if the scope contains the given token. 212 | /// 213 | public bool HasToken(string value) 214 | { 215 | foreach(var member in Members) 216 | if (member is Token token && token.Value == value) 217 | return true; 218 | return false; 219 | } 220 | } -------------------------------------------------------------------------------- /Tamar.Clausewitz/Constructs/Token.cs: -------------------------------------------------------------------------------- 1 | namespace Tamar.Clausewitz.Constructs; 2 | 3 | /// A single token, sometimes a number, a string or just a symbol. 4 | public class Token : Construct 5 | { 6 | /// The actual symbol/value of the token. 7 | public string Value; 8 | 9 | /// Primary constructor. 10 | /// Parent scope. 11 | /// The token itself. 12 | internal Token(Scope parent, string value) : base(parent) 13 | { 14 | Value = value; 15 | } 16 | } -------------------------------------------------------------------------------- /Tamar.Clausewitz/IO/Directory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Tamar.Clausewitz.IO; 4 | 5 | /// 6 | /// Corresponds to a file directory. (Renamed from 'Directory' to 'Folder' due to 7 | /// name conflicts with 8 | /// 'System.IO.Directory', Then renamed again back to 'Directory' after using an 9 | /// alias names for the .NET static 10 | /// classes. 11 | /// 12 | public class Directory : IExplorable 13 | { 14 | /// Sub-directories 15 | public readonly List Directories = new(); 16 | 17 | /// Files. 18 | public readonly List Files = new(); 19 | 20 | /// Primary constructor 21 | /// Parent directory. 22 | /// Directory name. 23 | internal Directory(Directory parent, string name) 24 | { 25 | Name = name; 26 | Parent = parent; 27 | } 28 | 29 | /// 30 | /// Returns all directories and then all files within this directory in a single 31 | /// list. 32 | /// 33 | public IEnumerable Explorables 34 | { 35 | get 36 | { 37 | var explorables = new List(); 38 | explorables.AddRange(Directories); 39 | explorables.AddRange(Files); 40 | return explorables; 41 | } 42 | } 43 | 44 | /// 45 | /// Returns true if this directory has no parent. (Typically "C:\") 46 | /// 47 | public bool IsRoot => Parent == null; 48 | 49 | /// 50 | public string Address => this.GetAddress(); 51 | 52 | /// Directory name. 53 | public string Name { get; set; } 54 | 55 | /// 56 | public Directory Parent { get; internal set; } 57 | 58 | /// 59 | /// Creates a new directory within this directory. (Automatically assigns the 60 | /// parent) 61 | /// 62 | /// Directory name. 63 | /// New directory. 64 | public Directory NewDirectory(string name) 65 | { 66 | var directory = new Directory(this, name); 67 | Directories.Add(directory); 68 | return directory; 69 | } 70 | 71 | /// 72 | /// Creates a new file within this directory. (Automatically assigns the 73 | /// parent) 74 | /// 75 | /// File name with extension 76 | /// New file. 77 | public FileScope NewFile(string name) 78 | { 79 | var file = new FileScope(this, name); 80 | Files.Add(file); 81 | return file; 82 | } 83 | } -------------------------------------------------------------------------------- /Tamar.Clausewitz/IO/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | // ReSharper disable UnusedMember.Global 6 | 7 | namespace Tamar.Clausewitz.IO; 8 | 9 | /// 10 | /// Extension class for various interfaces. These extensions are used only within 11 | /// this library to implement a 12 | /// similar pattern to Multiple-Inheritances in C#. Implement corresponding 13 | /// properties within the derived classes of 14 | /// these interfaces that call these extensions. 15 | /// 16 | internal static class Extensions 17 | { 18 | /// 19 | /// Retrieves the parent directory connected to all grandparents of parents without 20 | /// any sibling entries. 21 | /// 22 | /// Extended. 23 | /// Closest parent. 24 | public static Directory DefineParents(this string address) 25 | { 26 | if (!Path.IsPathFullyQualified(address)) 27 | address = Path.Combine(Environment.CurrentDirectory, address); 28 | 29 | var root = new Directory(null, Path.GetPathRoot(address)); 30 | var parent = root; 31 | var remaining = address.Remove(0, root.Address.Length); 32 | while (remaining.Contains(Path.DirectorySeparatorChar)) 33 | { 34 | var name = remaining.Substring(0, remaining.IndexOf(Path.DirectorySeparatorChar)); 35 | remaining = remaining.Remove(0, name.Length + 1); 36 | parent = parent.NewDirectory(name); 37 | } 38 | 39 | return parent; 40 | } 41 | 42 | /// 43 | /// Checks if the the given directory is included somewhere within the extended 44 | /// directory. 45 | /// 46 | /// Extended. 47 | /// Suspected parent. 48 | /// True if included. 49 | public static bool IsSubDirectoryOf(this string candidate, string parent) 50 | { 51 | var isChild = false; 52 | var candidateInfo = new DirectoryInfo(candidate); 53 | var parentInfo = new DirectoryInfo(parent); 54 | 55 | while (candidateInfo.Parent != null) 56 | if (candidateInfo.Parent.FullName == parentInfo.FullName) 57 | { 58 | isChild = true; 59 | break; 60 | } 61 | else 62 | { 63 | candidateInfo = candidateInfo.Parent; 64 | } 65 | 66 | return isChild; 67 | } 68 | 69 | /// Retrieves the full address. 70 | /// Extended. 71 | /// Full address. 72 | internal static string GetAddress(this IExplorable explorable) 73 | { 74 | var address = string.Empty; 75 | var currentExplorable = explorable; 76 | while (true) 77 | { 78 | if (currentExplorable == null) 79 | return address; 80 | if (currentExplorable.GetType() == typeof(Directory)) 81 | address = Path.Combine(currentExplorable.Name, address); 82 | else 83 | address = currentExplorable.Name; 84 | currentExplorable = currentExplorable.Parent; 85 | } 86 | } 87 | 88 | /// 89 | /// Ensures the address is fully qualified using the correct directory and drive 90 | /// separators for each platform. 91 | /// 92 | /// Relative or fully qualified path. 93 | internal static string ToFullyQualifiedAddress(this string address) 94 | { 95 | // Replace slashes with platform specific separators. 96 | if (Environment.OSVersion.Platform == PlatformID.Unix) 97 | address = address.Replace('\\', Path.DirectorySeparatorChar); 98 | if (Environment.OSVersion.Platform == PlatformID.Win32NT) 99 | { 100 | address = address.Replace('/', Path.DirectorySeparatorChar); 101 | } 102 | else 103 | { 104 | address = address.Replace('/', Path.DirectorySeparatorChar); 105 | address = address.Replace('\\', Path.DirectorySeparatorChar); 106 | } 107 | 108 | // Replace pairs of separators with single ones: 109 | var separatorPair = string.Empty + Path.DirectorySeparatorChar + Path.DirectorySeparatorChar; 110 | while (address.Contains(separatorPair)) 111 | address = address.Replace(separatorPair, 112 | string.Empty + Path.DirectorySeparatorChar); 113 | 114 | // This checks whether the address is local or full: 115 | if (!Path.IsPathFullyQualified(address)) 116 | address = Environment.CurrentDirectory + Path.DirectorySeparatorChar + address; 117 | return address; 118 | } 119 | } -------------------------------------------------------------------------------- /Tamar.Clausewitz/IO/FileScope.cs: -------------------------------------------------------------------------------- 1 | using Tamar.Clausewitz.Constructs; 2 | 3 | namespace Tamar.Clausewitz.IO; 4 | 5 | /// 6 | /// An extended type of scope, which enforces file name and parent 7 | /// directory. 8 | /// 9 | public class FileScope : Scope, IExplorable 10 | { 11 | /// Primary constructor. 12 | /// Parent directory. 13 | /// File name with extension. 14 | internal FileScope(Directory parent, string name) : base(name) 15 | { 16 | Name = name; 17 | Parent = parent; 18 | } 19 | 20 | /// 21 | public string Address => this.GetAddress(); 22 | 23 | /// 24 | public new Directory Parent { get; internal set; } 25 | 26 | /// 27 | /// Retrieves all text within this file. 28 | /// 29 | /// File contents. 30 | public string ReadText() 31 | { 32 | return System.IO.File.ReadAllText(Address); 33 | } 34 | 35 | /// 36 | /// Writes the given text into this file. 37 | /// 38 | /// 39 | internal void WriteText(string data) 40 | { 41 | if (!System.IO.Directory.Exists(Parent.Address)) 42 | System.IO.Directory.CreateDirectory(Parent.Address); 43 | System.IO.File.WriteAllText(Address, data); 44 | } 45 | } -------------------------------------------------------------------------------- /Tamar.Clausewitz/IO/IExplorable.cs: -------------------------------------------------------------------------------- 1 | namespace Tamar.Clausewitz.IO; 2 | 3 | /// Scopes which can be explored through a file manager. 4 | public interface IExplorable 5 | { 6 | /// Full address. 7 | string Address { get; } 8 | 9 | /// Name. 10 | string Name { get; } 11 | 12 | /// Parent directory. 13 | Directory Parent { get; } 14 | } -------------------------------------------------------------------------------- /Tamar.Clausewitz/Interpreter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | using Tamar.Clausewitz.Constructs; 7 | using Tamar.Clausewitz.IO; 8 | using Directory = Tamar.Clausewitz.IO.Directory; 9 | 10 | // ReSharper disable UnusedMember.Global 11 | 12 | namespace Tamar.Clausewitz; 13 | 14 | /// The Clausewitz interpreter. 15 | public static class Interpreter 16 | { 17 | /// 18 | /// Regex rule for valid Clausewitz values. Includes: identifiers, numerical 19 | /// values, and ':' variable binding 20 | /// operator. 21 | /// 22 | private const string ValueRegexRule = @"[a-zA-Z0-9_.:""]+"; 23 | 24 | /// Reads a directory in the given address. 25 | /// Relative or fully qualified path. 26 | /// Explorable directory. 27 | public static Directory ReadDirectory(string address) 28 | { 29 | // This checks whether the address is local or full: 30 | address = address.ToFullyQualifiedAddress(); 31 | 32 | // Interpret all files and directories found within this directory: 33 | var directory = address.DefineParents().NewDirectory(Path.GetFileName(address)); 34 | 35 | // If doesn't exist, notify an error. 36 | if (!System.IO.Directory.Exists(address)) 37 | { 38 | Log.SendError("Could not locate the directory.", address); 39 | return null; 40 | } 41 | 42 | // Read the directory: 43 | ReadAll(directory); 44 | return directory; 45 | } 46 | 47 | /// Reads a file in the given address. 48 | /// Relative or fully qualified path. 49 | /// File scope. 50 | public static FileScope ReadFile(string address) 51 | { 52 | // This checks whether the address is local or full: 53 | address = address.ToFullyQualifiedAddress(); 54 | 55 | // Read the file: 56 | var file = address.DefineParents().NewFile(Path.GetFileName(address)); 57 | return TryInterpret(file); 58 | } 59 | 60 | /// 61 | /// Translates data back into Clausewitz syntax and writes down to the 62 | /// actual file. 63 | /// 64 | /// Extended. 65 | public static void Write(this FileScope fileScope) 66 | { 67 | fileScope.WriteText(Translate(fileScope)); 68 | Log.Send("File saved: \"" + Path.GetFileName(fileScope.Address) + "\"."); 69 | } 70 | 71 | /// 72 | /// Translates data back into Clausewitz syntax and writes down all files within 73 | /// this directory. 74 | /// 75 | /// Extended 76 | public static void Write(this Directory directory) 77 | { 78 | foreach (var file in directory.Files) 79 | file.Write(); 80 | foreach (var subDirectory in directory.Directories) 81 | subDirectory.Write(); 82 | } 83 | 84 | /// 85 | /// Checks if a token is a valid value in Clausewitz syntax standards for both 86 | /// names & values. 87 | /// 88 | /// Token. 89 | /// Boolean. 90 | internal static bool IsValidValue(string token) 91 | { 92 | return Regex.IsMatch(token, @"\d") || token == "---" || Regex.IsMatch(token, ValueRegexRule); 93 | } 94 | 95 | /// Tokenizes a file. 96 | /// Clausewitz file. 97 | /// Token list. 98 | internal static List<(string token, int line)> Tokenize(FileScope fileScope) 99 | { 100 | // The actual text data, character by character. 101 | var data = fileScope.ReadText(); 102 | 103 | // The current token so far recorded since the last token-breaking character. 104 | var token = string.Empty; 105 | 106 | // All tokenized tokens within this file so far. 107 | var tokens = new List<(string token, int line)>(); 108 | 109 | // Indicates a delimited string token. 110 | var @string = false; 111 | 112 | // Indicates a delimited comment token. 113 | var comment = false; 114 | 115 | // Indicates a new line. 116 | var newline = false; 117 | 118 | // Counts each newline. 119 | var line = 1; 120 | 121 | // Keeps track of the previous char. 122 | var prevChar = '\0'; 123 | 124 | // Tokenization loop: 125 | foreach (var @char in data) 126 | { 127 | // Count a new line. 128 | if (newline) 129 | { 130 | line++; 131 | newline = false; 132 | } 133 | 134 | // Keep tokenizing a string unless a switching delimiter comes outside escape. 135 | if (@string && !(@char == '"' && prevChar != '\\')) 136 | goto concat; 137 | 138 | // Keep tokenizing a comment unless a switching delimiter comes. 139 | if (comment && !(@char == '\r' || @char == '\n')) 140 | goto concat; 141 | 142 | // Standard tokenizer: 143 | var charToken = '\0'; 144 | switch (@char) 145 | { 146 | // Newline: (also comment delimiter) 147 | case '\r': 148 | case '\n': 149 | 150 | // Switch comments: 151 | if (comment) 152 | { 153 | comment = false; 154 | 155 | // Add empty comments: 156 | if (token.Length == 0) 157 | tokens.Add((string.Empty, line)); 158 | } 159 | 160 | // Cross-platform compatibility for newlines: 161 | if (prevChar == '\r' && @char == '\n') 162 | break; 163 | newline = true; 164 | break; 165 | 166 | // Whitespace (which breaks tokens): 167 | case ' ': 168 | case '\t': 169 | break; 170 | 171 | // String delimiter: 172 | case '"': 173 | @string = !@string; 174 | token += @char; 175 | break; 176 | 177 | // Comment delimiter: 178 | case '#': 179 | comment = true; 180 | charToken = @char; 181 | break; 182 | 183 | // Scope clauses & binding operator: 184 | case '}': 185 | case '{': 186 | case '=': 187 | charToken = @char; 188 | break; 189 | 190 | // Any other character: 191 | default: 192 | goto concat; 193 | } 194 | 195 | // Add new tokens to the list: 196 | if (token.Length > 0 && !@string) 197 | { 198 | tokens.Add((token, line)); 199 | token = string.Empty; 200 | } 201 | 202 | if (charToken != '\0') 203 | tokens.Add((new string(charToken, 1), line)); 204 | prevChar = @char; 205 | continue; 206 | 207 | // Concat characters to unfinished numbers/words/comments/strings. 208 | concat: 209 | token += @char; 210 | prevChar = @char; 211 | } 212 | 213 | // EOF & last token: 214 | if (token.Length > 0 && !@string) 215 | tokens.Add((token, line)); 216 | return tokens; 217 | } 218 | 219 | /// Translates data back into Clausewitz syntax. 220 | /// Root scope (file scope) 221 | /// Clausewitz script. 222 | internal static string Translate(Scope root) 223 | { 224 | var data = string.Empty; 225 | var newline = Environment.NewLine; 226 | var tabs = new string('\t', root.Level); 227 | 228 | // Files include their own comments at the beginning followed by an empty line. 229 | if (root is FileScope) 230 | if (root.Comments.Count > 0) 231 | { 232 | foreach (var comment in root.Comments) 233 | data += tabs + "# " + comment + newline; 234 | data += newline; 235 | } 236 | 237 | // Translate scope members: 238 | foreach (var construct in root.Members) 239 | { 240 | foreach (var comment in construct.Comments) 241 | data += tabs + "# " + comment + newline; 242 | 243 | // Translate the actual type: 244 | switch (construct) 245 | { 246 | case Scope scope: 247 | if (string.IsNullOrWhiteSpace(scope.Name)) 248 | data += tabs + '{'; 249 | else 250 | data += tabs + scope.Name + " = {"; 251 | if (scope.Members.Count > 0) 252 | { 253 | data += newline + Translate(scope); 254 | foreach (var comment in scope.EndComments) 255 | data += tabs + "\t" + "# " + comment + newline; 256 | data += tabs + '}' + newline; 257 | } 258 | else 259 | { 260 | data += '}' + newline; 261 | } 262 | 263 | break; 264 | case Binding binding: 265 | data += tabs + binding.Name + " = " + binding.Value + newline; 266 | break; 267 | case Token token: 268 | if (root.Indented) 269 | { 270 | data += tabs + token.Value + newline; 271 | } 272 | else 273 | { 274 | var preceding = " "; 275 | var following = string.Empty; 276 | 277 | // Preceding characters: 278 | if (root.Members.First() == token) 279 | preceding = tabs; 280 | else if (token.Comments.Count > 0) 281 | preceding = tabs; 282 | else if (root.Members.First() != token) 283 | if (!(root.Members[root.Members.IndexOf(token) - 1] is Token)) 284 | preceding = tabs; 285 | 286 | // Following characters: 287 | if (root.Members.Last() != token) 288 | { 289 | var next = root.Members[root.Members.IndexOf(token) + 1]; 290 | if (!(next is Token)) 291 | following = newline; 292 | if (next.Comments.Count > 0) 293 | following = newline; 294 | } 295 | else if (root.Members.Last() == token) 296 | { 297 | following = newline; 298 | } 299 | 300 | data += preceding + token.Value + following; 301 | } 302 | 303 | break; 304 | } 305 | } 306 | 307 | // Append end comments at files: 308 | if (root is FileScope) 309 | foreach (var comment in root.EndComments) 310 | data += newline + "# " + comment; 311 | return data; 312 | } 313 | 314 | /// Interprets a file and all of its inner scopes recursively. 315 | /// A Clausewitz file. 316 | /// 317 | /// A syntax error was encountered during 318 | /// interpretation. 319 | /// 320 | private static void Interpret(FileScope fileScope) 321 | { 322 | // Tokenize the file: 323 | var tokens = Tokenize(fileScope); 324 | 325 | // All associated comments so far. 326 | var comments = new List<(string text, int line)>(); 327 | 328 | // Current scope. 329 | Scope scope = fileScope; 330 | 331 | // Interpretation loop: 332 | for (var index = 0; index < tokens.Count; index++) 333 | { 334 | // All current information: 335 | var token = tokens[index].token; 336 | var nextToken = index < tokens.Count - 1 ? tokens[index + 1].token : string.Empty; 337 | var prevToken = index > 0 ? tokens[index - 1].token : string.Empty; 338 | var prevPrevToken = index > 1 ? tokens[index - 2].token : string.Empty; 339 | var line = tokens[index].line; 340 | 341 | // Interpret tokens: 342 | // Enter a new scope: 343 | if (token == "{" && prevToken != "#") 344 | { 345 | // Participants: 346 | var name = prevPrevToken; 347 | var binding = prevToken; 348 | 349 | // Syntax check: 350 | if (binding == "=") 351 | { 352 | if (IsValidValue(name)) 353 | scope = scope.NewScope(name); 354 | else 355 | throw new SyntaxException("Invalid name at scope binding.", fileScope, line, token); 356 | } 357 | else 358 | { 359 | scope = scope.NewScope(); 360 | } 361 | 362 | AssociateComments(scope); 363 | } 364 | // Exit the current scope: 365 | else if (token == "}" && prevToken != "#") 366 | { 367 | // Associate end comments: 368 | AssociateComments(scope, true); 369 | 370 | // Check if the current scope is the file, if so, then notify an error of a missing opening "{". 371 | if (!(scope is FileScope)) 372 | { 373 | if (scope.Sorted) 374 | scope.Sort(); 375 | scope = scope.Parent; 376 | } 377 | else 378 | { 379 | throw new SyntaxException("Missing an opening '{' for a scope", fileScope, line, 380 | token); 381 | } 382 | } 383 | // Binding operator: 384 | else if (token == "=" && prevToken != "#") 385 | { 386 | // Participants: 387 | var name = prevToken; 388 | var value = nextToken; 389 | 390 | // Skip scope binding: (handled at "{" case, otherwise will claim as a syntax error.) 391 | if (value == "{") 392 | continue; 393 | 394 | // Syntax check: 395 | if (!IsValidValue(name)) 396 | throw new SyntaxException("Invalid name at binding.", fileScope, line, token); 397 | if (!IsValidValue(value)) 398 | throw new SyntaxException("Invalid value at binding.", fileScope, line, token); 399 | scope.NewBinding(name, value); 400 | AssociateComments(); 401 | } 402 | // Comment/pragma: 403 | else if (token == "#") 404 | { 405 | // Attached means the comment comes at the same line with another language construct: 406 | // If the comment comes at the same line with another construct, then it will be associated to that construct. 407 | // If the comment takes a whole line then it will be stacked and associated with the next construct when it is created. 408 | // If there was an empty line after the comment at the beginning of the file, then it will be associated with the file itself. 409 | // Comments are responsible for pragmas as well when utilizing square brackets. 410 | var lineOfPrevToken = index > 0 ? tokens[index - 1].line : -1; 411 | var isAttached = line == lineOfPrevToken; 412 | 413 | // Associate attached comments HERE: 414 | if (isAttached) 415 | { 416 | if (prevToken != "{") 417 | scope.Members.Last().Comments.Add(nextToken.Trim()); 418 | else 419 | scope.Comments.Add(nextToken.Trim()); 420 | } 421 | else 422 | { 423 | comments.Add((nextToken.Trim(), line)); 424 | } 425 | } 426 | // Unattached value/word token: 427 | else 428 | { 429 | // Check if bound: 430 | var isBound = prevToken.Contains('=') || nextToken.Contains('='); 431 | 432 | // Check if commented: 433 | var isComment = prevToken.Contains('#'); 434 | 435 | // Skip those cases: 436 | if (!isBound && !isComment) 437 | { 438 | if (IsValidValue(token)) 439 | { 440 | scope.NewToken(token); 441 | AssociateComments(); 442 | } 443 | else 444 | { 445 | throw new SyntaxException("Unexpected token.", fileScope, line, token); 446 | } 447 | } 448 | } 449 | } 450 | 451 | // Missing a closing "{" for scopes above the file level: 452 | if (scope != fileScope) 453 | throw new SyntaxException("Missing a closing '}' for a scope.", fileScope, tokens.Last().line, 454 | tokens.Last().token); 455 | 456 | // Associate end-comments (of the file): 457 | AssociateComments(scope, true); 458 | 459 | // This local method helps with associating the stacking comments with the latest language construct. 460 | void AssociateComments(Construct construct = null, bool endComments = false) 461 | { 462 | // No comments, exit. 463 | if (comments.Count == 0) 464 | return; 465 | 466 | // Associate with last construct if parameter is null. 467 | // ReSharper disable once ConvertIfStatementToSwitchStatement 468 | if (construct == null && scope.Members.Count == 0) 469 | return; 470 | if (construct == null) 471 | construct = scope.Members.Last(); 472 | 473 | // Leading comments at the beginning of a file: 474 | if (!endComments && construct.Parent is FileScope && construct.Parent.Members.First() == construct) 475 | { 476 | var associatedWithFile = new List(); 477 | var associatedWithConstruct = new List(); 478 | var associateWithFile = false; 479 | 480 | // Reverse iteration: 481 | for (var index = comments.Count - 1; index >= 0; index--) 482 | { 483 | if (!associateWithFile) 484 | { 485 | var prevCommentLine = index < comments.Count - 1 ? comments[index + 1].line : -1; 486 | var commentLine = comments[index].line; 487 | if (prevCommentLine > 1 && prevCommentLine - commentLine != 1) 488 | associateWithFile = true; 489 | } 490 | 491 | if (associateWithFile) 492 | associatedWithFile.Add(comments[index].text); 493 | else 494 | associatedWithConstruct.Add(comments[index].text); 495 | } 496 | 497 | // Reverse & append: 498 | construct.Parent.Comments.AddRange(associatedWithFile.Reverse()); 499 | construct.Comments.AddRange(associatedWithConstruct.Reverse()); 500 | } 501 | else if (!endComments) 502 | { 503 | foreach (var comment in comments) 504 | construct.Comments.Add(comment.text); 505 | } 506 | 507 | else if (construct is Scope commentScope) 508 | { 509 | foreach (var comment in comments) 510 | commentScope.EndComments.Add(comment.text); 511 | } 512 | 513 | comments.Clear(); 514 | } 515 | } 516 | 517 | /// 518 | /// Reads & interprets all files or data found in the given address. It will 519 | /// attempt to load & interpret each 520 | /// file, however if an error has occurred it will skip the specific files. 521 | /// 522 | /// Parent directory. 523 | private static void ReadAll(Directory parent) 524 | { 525 | // Read files: 526 | foreach (var file in System.IO.Directory.GetFiles(parent.Address)) 527 | if (file.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) 528 | { 529 | var newFile = parent.NewFile(Path.GetFileName(file)); 530 | var interpretedFile = TryInterpret(newFile); 531 | if (interpretedFile == null) 532 | parent.Files.Remove(newFile); 533 | } 534 | 535 | // Read Directories: 536 | foreach (var directory in System.IO.Directory.GetDirectories(parent.Address)) 537 | ReadAll(parent.NewDirectory(Path.GetFileNameWithoutExtension(directory))); 538 | } 539 | 540 | /// 541 | /// This method will try to interpret a file and handle potential syntax 542 | /// exceptions. If something went wrong 543 | /// during the interpretation it will rather not load the file at all, the user 544 | /// will be notified through an error 545 | /// message in the log, and the application will continue to run routinely. 546 | /// 547 | /// Clausewitz file. 548 | /// Interpreted file or null if an error occurred. 549 | private static FileScope TryInterpret(FileScope fileScope) 550 | { 551 | if (!File.Exists(fileScope.Address)) 552 | { 553 | Log.SendError("Could not locate the file.", fileScope.Address); 554 | return null; 555 | } 556 | 557 | try 558 | { 559 | Interpret(fileScope); 560 | Log.Send("Loaded file: \"" + Path.GetFileName(fileScope.Address) + "\"."); 561 | return fileScope; 562 | } 563 | catch (SyntaxException syntaxException) 564 | { 565 | syntaxException.Send(); 566 | Log.Send("File was not loaded: \"" + Path.GetFileName(fileScope.Address) + "\"."); 567 | } 568 | 569 | return null; 570 | } 571 | 572 | /// 573 | /// Creates a new empty Clausewitz file. 574 | /// 575 | /// Relative or fully qualified path. 576 | /// The newly created file. 577 | public static FileScope NewFile(string address) 578 | { 579 | // This checks whether the address is local or full: 580 | address = address.ToFullyQualifiedAddress(); 581 | 582 | // Read the file: 583 | return address.DefineParents().NewFile(Path.GetFileName(address)); 584 | } 585 | 586 | /// 587 | /// Thrown when syntax-related errors occur during interpretation time which result 588 | /// in a broken & meaningless interpretation. 589 | /// 590 | public class SyntaxException : Exception 591 | { 592 | /// The file where the exception occurred. 593 | public readonly FileScope FileScope; 594 | 595 | /// The line at which the exception occurred. 596 | public readonly int Line; 597 | 598 | /// The token responsible for the exception. 599 | public readonly string Token; 600 | 601 | /// Primary constructor. 602 | /// Message. 603 | /// The file where the exception occurred. 604 | /// The line at which the exception occurred. 605 | /// The token responsible for the exception. 606 | internal SyntaxException(string message, FileScope fileScope, int line, string token) : base(message) 607 | { 608 | FileScope = fileScope; 609 | Line = line; 610 | Token = token; 611 | } 612 | 613 | /// Retrieves all detailed information in a formatted string. 614 | public string Details => 615 | $"Token: '{Token}'\nLine: {Line}\nFile: {FileScope.Address.Remove(0, Environment.CurrentDirectory.Length)}"; 616 | } 617 | } -------------------------------------------------------------------------------- /Tamar.Clausewitz/Log.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Tamar.Clausewitz; 5 | 6 | /// 7 | /// Logs messages during operation time for the Clausewitz interpreter. Note that 8 | /// this class does not write a log 9 | /// file nor notify the user (or the developer) for any messages. Use the event 10 | /// handler to keep track of the messages, 11 | /// or write down to file the entire messages list. 12 | /// 13 | public static class Log 14 | { 15 | /// Special delegate to deliver the message as an event argument. 16 | /// The message that was sent. 17 | public delegate void MessageHandler(Message message); 18 | 19 | /// Contains all messages. 20 | public static readonly List Messages = new(); 21 | 22 | /// Sends a new message to the log. 23 | /// Main text line. 24 | /// More details. 25 | public static void Send(string text, string details = "") 26 | { 27 | Send(new Message(Message.Types.Info, text, details)); 28 | } 29 | 30 | /// 31 | /// Sends an exception to the log. 32 | /// 33 | /// Extended. 34 | public static void Send(this Exception exception) 35 | { 36 | var details = string.Empty; 37 | if (exception is Interpreter.SyntaxException syntaxException) 38 | details = syntaxException.Details; 39 | var message = new Message(Message.Types.Error, exception.Message, details, exception); 40 | Send(message); 41 | } 42 | 43 | /// Sends a new message to the log. 44 | /// Log message. 45 | public static void Send(Message message) 46 | { 47 | Messages.Add(message); 48 | MessageSent?.Invoke(message); 49 | } 50 | 51 | /// Sends a new error message to the log. 52 | /// Main text line. 53 | /// More details. 54 | /// Exception thrown. 55 | public static void SendError(string text, string details = "", Exception exception = null) 56 | { 57 | Send(new Message(Message.Types.Error, text, details, exception)); 58 | } 59 | 60 | /// 61 | /// Fires when a new message is sent. Use this event at Console applications or 62 | /// elsewhere to track log messages at 63 | /// runtime. 64 | /// 65 | public static event MessageHandler MessageSent; 66 | 67 | /// Log.Message struct. 68 | public struct Message 69 | { 70 | /// Primary constructor. 71 | /// Message type. 72 | /// Main text line. 73 | /// More details. 74 | /// Exception thrown. 75 | public Message(Types type, string text, string details = "", Exception exception = null) 76 | { 77 | Exception = exception; 78 | Details = details; 79 | Text = text; 80 | Type = type; 81 | } 82 | 83 | /// More details such as filename. 84 | public string Details; 85 | 86 | /// Bound exception for errors. 87 | public Exception Exception; 88 | 89 | /// Leading text. 90 | public string Text; 91 | 92 | /// Message type. 93 | public Types Type; 94 | 95 | /// Message types. 96 | public enum Types 97 | { 98 | Info, 99 | Error 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /Tamar.Clausewitz/Tamar.Clausewitz.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | Tamar.Clausewitz 5 | Tamar.Clausewitz 6 | Tamar.Clausewitz 7 | David von Tamar 8 | 2023 David von Tamar, LGPLv3 9 | 0.2.0 10 | https://github.com/david-tamar/clausewitz.net 11 | https://github.com/david-tamar/clausewitz.net/blob/master/LICENSE 12 | https://github.com/david-tamar/clausewitz.net 13 | git 14 | clausewitz, interpreter 15 | Tamar.Clausewitz 16 | David von Tamar 17 | default 18 | 19 | 20 | bin\Release\netcoreapp2.0\Clausewitz interpreter.xml 21 | 22 | --------------------------------------------------------------------------------