├── .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 |
--------------------------------------------------------------------------------