// // ORIGINAL CODE BASE IS Copyright (C) 2006-2010 by Alphons van der Heijden. // The code was donated on 2010-04-28 by Alphons van der Heijden to Brandon 'Dimentox Travanti' Husbands & // Malcolm J. Kudra, who in turn License under the GPLv2 in agreement with Alphons van der Heijden's wishes. // // The community would like to thank Alphons for all of his hard work, blood sweat and tears. Without his work // the community would be stuck with crappy editors. // // The source code in this file ("Source Code") is provided by The LSLEditor Group to you under the terms of the GNU // General Public License, version 2.0 ("GPL"), unless you have obtained a separate licensing agreement ("Other // License"), formally executed by you and The LSLEditor Group. // Terms of the GPL can be found in the gplv2.txt document. // // GPLv2 Header // ************ // LSLEditor, a External editor for the LSL Language. // Copyright (C) 2010 The LSLEditor Group. // // This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public // License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any // later version. // // This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied // warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more // details. // // You should have received a copy of the GNU General Public License along with this program; if not, write to the Free // Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. // ******************************************************************************************************************** // The above copyright notice and this permission notice shall be included in copies or substantial portions of the // Software. // ******************************************************************************************************************** // // // // This class is used to convert LSLI to LSL and the other way around. // Created by Jasper Wiggerink // 13-11-2017 // using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Windows.Forms; namespace LSLEditor.Helpers { class LSLIConverter { private EditForm editForm; private const string BEGIN = "//#BEGIN"; private const string END = "//#END"; private const string INCLUDE = "//#include"; public const string EXPANDED_SUBEXT = ".expanded"; public const string LSL_EXT = ".lsl"; public const string LSLI_EXT = ".lsli"; public static List validExtensions = new List() { LSLI_EXT, LSL_EXT }; private const string INCLUDE_REGEX = "(\n|^)\\s*" + INCLUDE + "\\(\".*?\"\\).*"; private const string BEGIN_REGEX = "(\\s+|^)" + BEGIN; private const string END_REGEX = "(\\s+|^)" + END; private const string EMPTY_OR_WHITESPACE_REGEX = "^\\s*$"; private const string PATH_OF_INCLUDE_REGEX = "\".*?\""; private const string EMPTY_SCRIPT = "// Empty script\n"; private List implementedIncludes = new List(); private int includeDepth = 0; public LSLIConverter() { } /// /// Searches for a file with one of the validExtensions based on a name or path. Also searches in the IncludeDirectories /// /// /// File path private static string SearchFile(string file) { // Search in optional include directories foreach (string directory in Properties.Settings.Default.IncludeDirectories) { string pFile; if (file.ToLower().Contains(directory.ToLower())) { pFile = file; } else { pFile = directory + file; } if (File.Exists(pFile)) { return pFile; } if (Path.GetExtension(file) == "") { foreach (string extension in validExtensions) { if (file.Contains(directory)) { pFile = file + extension; } else { pFile = directory + file + extension; } if (File.Exists(pFile)) { return pFile; } } } } // Search for file relative to the script if (File.Exists(file)) { return file; } if (Path.GetExtension(file) == "") { string pFile = ""; foreach (string extension in validExtensions) { pFile = file + extension; if (File.Exists(pFile)) { return pFile; } } } return ""; } /// /// Returns the path of the file /// /// /// /// private string GetFilePath(string pathOfInclude, string pathOfScript) { // Step 1 (optional). Search from include directories // Step 2. Search from relative path from script string pathOfIncludeOriginal = pathOfInclude; pathOfInclude = SearchFile(pathOfInclude); if (pathOfInclude == "") { // If path is relative and no includedirectories if (!Path.IsPathRooted(pathOfIncludeOriginal)) { pathOfInclude = LSLIPathHelper.GetRelativePath(pathOfScript, Environment.CurrentDirectory) + pathOfIncludeOriginal; } else if (this.implementedIncludes.Count > 0) // If there are already includes, the relative path is already correct { pathOfInclude = Path.GetDirectoryName(this.implementedIncludes.LastOrDefault()) + '\\' + pathOfIncludeOriginal; } else { pathOfInclude = pathOfIncludeOriginal; } // If path is absolute it will stay the pathOfInclude pathOfInclude = SearchFile(pathOfInclude); } return pathOfInclude; } /// /// Finds all indexes of a value in a string /// /// /// /// public static List AllIndexesOf(string str, string value) { if (!String.IsNullOrEmpty(value)) { List indexes = new List(); for (int index = 0; ; index += value.Length) { index = str.IndexOf(value, index); if (index == -1) return indexes; indexes.Add(index); } } return null; } /// /// Compares 2 paths and returns true if they are different, false if they're the same. /// Warning: This doesn't compare extensions. /// /// /// /// public static bool IsDifferentScript(string pathOfInclude, string pathOfScript) { string pathOfScriptNoExt = LSLIPathHelper.RemoveExpandedSubExtension(Path.GetFileNameWithoutExtension(pathOfScript)); string pathOfIncludeNoExt = LSLIPathHelper.RemoveExpandedSubExtension(Path.GetFileNameWithoutExtension(pathOfInclude)); // Compare paths return !pathOfScriptNoExt.EndsWith(pathOfIncludeNoExt); } /// /// This is a hack to get the correct line, since problems arose in WriteAfterLine when inserting index-based. /// It checks for each occurance of lineBefore (when it's an include statement) if it has a BEGIN after it. /// /// /// private int GetCorrectIndexOfLine(string lineBefore, string context) { string trimmedLine = Regex.Replace(lineBefore, @"\s+", ""); string matchString = Regex.Match(trimmedLine, INCLUDE_REGEX).ToString(); // Tussen de één na laatste en de laatste moet de include statement staan, of na de laatste int lastButOneNewLineIndex = lineBefore.TrimEnd('\n').LastIndexOf('\n') > -1 ? lineBefore.TrimEnd('\n').LastIndexOf('\n') : 0; string lineBeforeAfterLastButOneNewLine = lineBefore.Substring(lastButOneNewLineIndex).TrimEnd('\n'); // Best variable name ever? if (Regex.IsMatch(lineBefore.TrimEnd('\n'), INCLUDE_REGEX) && Regex.IsMatch(lineBeforeAfterLastButOneNewLine, INCLUDE_REGEX)) // Line before this line is an include statement, that means this is a BEGIN statement //lineBefore.TrimEnd('\n').EndsWith(matchString) { // Get all matches with this linebefore List allIndexes = AllIndexesOf(context, lineBefore); foreach (int index in allIndexes) { // Check wether there is already a begin statement string targetText = context.Substring(index + lineBefore.Length); // This is the text after lineBefore targetText = Regex.Replace(targetText, @"\s+", ""); if (targetText.StartsWith(BEGIN)) // If the targetted text starts with BEGIN, then we should keep searching { continue; } else { return index; // Found a free spot! Return the index } } } return context.LastIndexOf(lineBefore); // If the lineBefore is not an include statement, simply return the last index of it. } /// /// Creates a new line in the context after another line /// /// /// /// /// Context with the new line private StringBuilder WriteAfterLine(StringBuilder context, string newLine, string lineBefore) { string ctx = context.ToString(); int lastIndexOfLineBefore = GetCorrectIndexOfLine(lineBefore, ctx); int includeIndex = lastIndexOfLineBefore + lineBefore.Length; string hasSeperator = lineBefore.Substring(lineBefore.Length - 1, 1); if (hasSeperator != "\n") { newLine = "\n" + newLine; } hasSeperator = newLine.Substring(newLine.Length - 1, 1); if (hasSeperator != "\n") { newLine += "\n"; } context.Insert(includeIndex, newLine); return context; } /// /// Shows an 'Oops...' messagebox with the message and verboses it. /// /// private void ShowError(string message) { if (!editForm.verboseQueue.Contains(message)) { MessageBox.Show(message, "Oops...", MessageBoxButtons.OK, MessageBoxIcon.Error); editForm.verboseQueue.Add(message); } } /// /// Returns the amount of tabs for an include depth /// /// /// /// private string GetTabsForIncludeDepth(int includeDepth, bool OneLess = false) { string tabs = ""; if(OneLess && includeDepth != 0) { includeDepth--; } for(int i = 0; i < includeDepth; i++) { tabs += "\t"; } return tabs; } /// /// Returns the amount of tabs in front of an include statement /// /// /// private string GetTabsForIndentedInclude(string includeLine, int includeDepth, bool isDebug = false) { if(includeLine.Contains('\t')) { int includeIndex = Regex.Match(includeLine, INCLUDE).Index; string beforeInclude = includeLine.Substring(0, includeIndex); if(beforeInclude.Contains('\n')) { // Last '\n' before includeIndex int lastIndexNewLine = beforeInclude.LastIndexOf('\n'); beforeInclude = beforeInclude.Substring(lastIndexNewLine, beforeInclude.Length - lastIndexNewLine); } int tabCount = 0; // Count the tabs between the start of the line and the begin of the include. if (isDebug) { tabCount = beforeInclude.Count(f => f == '\t') - (includeDepth - 1); // The tabcount should be without the includeDepth, because this was already added. } else { tabCount = beforeInclude.Count(f => f == '\t'); } string tabs = ""; for (int i = 0; i < tabCount; i++) { tabs += "\t"; } return tabs; } return ""; } /// /// Returns the amount of spaces in front of an include statement /// /// /// private string GetSpacesForIndentedInclude(string includeLine, int includeDepth, bool isDebug = false) { if (includeLine.Contains(" ")) { int includeIndex = Regex.Match(includeLine, INCLUDE).Index; string beforeInclude = includeLine.Substring(0, includeIndex); int spaceCount = 0; // Count the space between the start of the line and the begin of the include. if (isDebug) { // The spacecount should be without the includeDepth, because this was already added. spaceCount = beforeInclude.Count(f => f == ' ') - (includeDepth - 1); } else { spaceCount = beforeInclude.Count(f => f == ' '); } string spaces = ""; for (int i = 0; i < spaceCount; i++) { spaces += " "; } return spaces; } return ""; } /// /// Returns the amount of tabs/spaces for an include script /// /// /// /// /// private string GetTabsForIncludeScript(string includeLine, int includeDepth, bool OneLess = false) { string includeDepthTabs = GetTabsForIncludeDepth(includeDepth, OneLess); string indentedIncludeTabs = GetTabsForIndentedInclude(includeLine, includeDepth, true); string spacesForIndentedInclude = GetSpacesForIndentedInclude(includeLine, includeDepth, true); return includeDepthTabs + indentedIncludeTabs + spacesForIndentedInclude; } /// /// Returns the line of the match within a context /// /// /// /// private string GetLineOfMatch(string context, Match m) { string contentAfterMatchValue = context.Substring(m.Index + m.Value.Length); int indexOfNewLine = contentAfterMatchValue.IndexOf('\n') + m.Index + m.Value.Length + 1; // Index of the first occurence of \n after this match return context.Substring(m.Index, indexOfNewLine - m.Index); // Get full line } /// /// Inserts an included script and writes the expanded script for export. /// /// /// /// /// private StringBuilder WriteExportScript(string pathOfInclude, StringBuilder sb, string includeLine) { string script = GetTabsForIndentedInclude(includeLine, includeDepth) + EMPTY_SCRIPT; using (StreamReader sr = new StreamReader(pathOfInclude)) { this.implementedIncludes.Add(Path.GetFullPath(pathOfInclude)); string scriptRaw = sr.ReadToEnd(); scriptRaw = GetTabsForIndentedInclude(includeLine, includeDepth) + scriptRaw.Replace("\n", "\n" + GetTabsForIndentedInclude(includeLine, includeDepth)); // If there are includes in the included script if (Regex.IsMatch(scriptRaw, INCLUDE_REGEX)) { // Then import these scripts too script = ImportScripts(scriptRaw, pathOfInclude, false) + "\n"; } else if (!Regex.IsMatch(scriptRaw, EMPTY_OR_WHITESPACE_REGEX)) { script = scriptRaw + "\n"; } } this.WriteAfterLine(sb, script, includeLine); string ctx = sb.ToString(); return new StringBuilder(ctx.Remove(ctx.IndexOf(includeLine.TrimStart('\n')), includeLine.TrimStart('\n').Length)); // Deletes the include statement } /// /// Inserts an included script and writes it to the expanded script for debug. /// /// /// /// /// private StringBuilder WriteDebugScript(string pathOfInclude, StringBuilder sb, string includeLine) { sb = this.WriteAfterLine(sb, GetTabsForIncludeScript(includeLine, includeDepth, true) + BEGIN, includeLine); // Insert included script string script = GetTabsForIncludeScript(includeLine, includeDepth) + EMPTY_SCRIPT; using (StreamReader sr = new StreamReader(pathOfInclude)) { this.implementedIncludes.Add(Path.GetFullPath(pathOfInclude)); string scriptRaw = sr.ReadToEnd(); scriptRaw = GetTabsForIncludeScript(includeLine, includeDepth) + scriptRaw.Replace("\n", "\n" + GetTabsForIncludeScript(includeLine, includeDepth)); // If there are includes in the included script if (Regex.IsMatch(scriptRaw, INCLUDE_REGEX)) { // Then import these scripts too script = "\n" + ImportScripts(scriptRaw, pathOfInclude) + "\n"; } else if (!Regex.IsMatch(scriptRaw, EMPTY_OR_WHITESPACE_REGEX))// Check if its not empty or whitespace { script = scriptRaw + "\n"; } } this.WriteAfterLine(sb, script, BEGIN + "\n"); this.WriteAfterLine(sb, GetTabsForIncludeScript(includeLine, includeDepth, true) + END, script); return sb; } /// /// Imports scripts from //#include statements /// /// Sourcecode /// Path of the source code of the script /// Sourcecode with imported scripts private string ImportScripts(string strC, string pathOfScript, bool ShowBeginEnd = true) { if(!LSLIPathHelper.IsLSLI(pathOfScript)) { // If it's not an LSLI script, it can't import a script return strC; } StringBuilder sb = new StringBuilder(strC); MatchCollection mIncludes = Regex.Matches(strC, INCLUDE_REGEX); // Find includes foreach (Match m in mIncludes) { if (this.includeDepth == 0) { this.implementedIncludes = new List(); } includeDepth++; string line = GetLineOfMatch(strC, m); int lineNumber = strC.Take(m.Index + line.Length - 1).Count(c => c == '\n') + 1; string pathOfIncludeOriginal = Regex.Match(line, PATH_OF_INCLUDE_REGEX).Value.Trim('"'); string pathOfInclude = pathOfIncludeOriginal; string ext = Path.GetExtension(pathOfInclude).ToLower(); if ((validExtensions.Contains(ext) || ext == "") && IsDifferentScript(pathOfInclude, pathOfScript)) { pathOfInclude = GetFilePath(pathOfInclude, pathOfScript); if (pathOfInclude != "" && !this.implementedIncludes.Contains(Path.GetFullPath(pathOfInclude))) { if (!ShowBeginEnd) { sb = WriteExportScript(pathOfInclude, sb, line); } else { sb = WriteDebugScript(pathOfInclude, sb, line); } } else if (pathOfInclude != "" && this.implementedIncludes.Contains(Path.GetFullPath(pathOfInclude))) { string message = "Error: Recursive include loop detected: \"" + Path.GetFullPath(pathOfInclude) + "\". In script \"" + Path.GetFileName(pathOfScript) + "\". Line " + lineNumber + "."; ShowError(message); } else { string relativeToPathOfScript = LSLIPathHelper.GetRelativePath(pathOfScript, Environment.CurrentDirectory); string correctPath = Path.GetFullPath(relativeToPathOfScript) + pathOfIncludeOriginal; string message = "Error: Unable to find file \"" + correctPath + "\". In script \"" + Path.GetFileName(pathOfScript) + "\". Line " + lineNumber + "."; ShowError(message); } } includeDepth--; if(this.implementedIncludes.Count > 0) { this.implementedIncludes.Remove(this.implementedIncludes.Last()); } } return sb.ToString(); } /// /// Removes included scripts /// /// Sourcecode /// Sourcecode without imported scripts private static string RemoveScripts(string strC) { StringBuilder sb = new StringBuilder(strC); int indexOfFirstBeginStatement = -1; uint depth = 0; int readIndex = 0; using (StringReader sr = new StringReader(strC)) { int amountOfLines = strC.Split('\n').Length; for (int i = 1; i < amountOfLines; i++) { string line = sr.ReadLine(); if (Regex.IsMatch(line, BEGIN_REGEX)) { if (depth == 0) { indexOfFirstBeginStatement = readIndex; } depth++; } readIndex += line.Length + 1; if (Regex.IsMatch(line, END_REGEX)) { depth--; if (depth == 0) { sb.Remove(indexOfFirstBeginStatement, (readIndex - indexOfFirstBeginStatement)); readIndex -= readIndex - indexOfFirstBeginStatement; indexOfFirstBeginStatement = -1; } } } } return sb.ToString(); } /// /// Call this to collapse LSL to LSLI /// /// /// LSLI public static string CollapseToLSLI(string source) { string sourceCode = RemoveScripts(source); return sourceCode; } /// /// Call this to collapse LSL to LSLI /// /// /// LSLI public string CollapseToLSLIFromEditform(EditForm editform) { this.editForm = editform; return CollapseToLSLI(editform.SourceCode); } /// /// Call this to collapse LSL to LSLI /// /// /// LSLI public static string CollapseToLSLIFromPath(string path) { string sourceCode = ""; using(StreamReader sr = new StreamReader(path)) { sourceCode = sr.ReadToEnd(); } return CollapseToLSLI(sourceCode); } /// /// Call this to expand LSLI to LSL /// /// /// LSL public string ExpandToLSL(EditForm editForm, bool ShowBeginEnd = true) { editForm.verboseQueue = new List(); this.editForm = editForm; string strC = editForm.SourceCode; string fullPathName = editForm.FullPathName; if (LSLIPathHelper.IsExpandedLSL(editForm.ScriptName)) { // Collapse first, to ensure it is expanded showing or not showing begin/end. strC = CollapseToLSLI(strC); // Mimic LSLI file fullPathName = LSLIPathHelper.CreateCollapsedPathAndScriptName(fullPathName); } string sourceCode = ImportScripts(strC, fullPathName, ShowBeginEnd); return sourceCode; } } }