diff --git a/Assets/CsvParser.cs b/Assets/CsvParser.cs new file mode 100755 index 00000000..7d702aad --- /dev/null +++ b/Assets/CsvParser.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Ideafixxxer.CsvParser +{ + public class CsvParser + { + private const char CommaCharacter = ','; + private const char QuoteCharacter = '"'; + + #region Nested types + + private abstract class ParserState + { + public static readonly LineStartState LineStartState = new LineStartState(); + public static readonly ValueStartState ValueStartState = new ValueStartState(); + public static readonly ValueState ValueState = new ValueState(); + public static readonly QuotedValueState QuotedValueState = new QuotedValueState(); + public static readonly QuoteState QuoteState = new QuoteState(); + + public abstract ParserState AnyChar(char ch, ParserContext context); + public abstract ParserState Comma(ParserContext context); + public abstract ParserState Quote(ParserContext context); + public abstract ParserState EndOfLine(ParserContext context); + } + + private class LineStartState : ParserState + { + public override ParserState AnyChar(char ch, ParserContext context) + { + context.AddChar(ch); + return ValueState; + } + + public override ParserState Comma(ParserContext context) + { + context.AddValue(); + return ValueStartState; + } + + public override ParserState Quote(ParserContext context) + { + return QuotedValueState; + } + + public override ParserState EndOfLine(ParserContext context) + { + context.AddLine(); + return LineStartState; + } + } + + private class ValueStartState : LineStartState + { + public override ParserState EndOfLine(ParserContext context) + { + context.AddValue(); + context.AddLine(); + return LineStartState; + } + } + + private class ValueState : ParserState + { + public override ParserState AnyChar(char ch, ParserContext context) + { + context.AddChar(ch); + return ValueState; + } + + public override ParserState Comma(ParserContext context) + { + context.AddValue(); + return ValueStartState; + } + + public override ParserState Quote(ParserContext context) + { + context.AddChar(QuoteCharacter); + return ValueState; + } + + public override ParserState EndOfLine(ParserContext context) + { + context.AddValue(); + context.AddLine(); + return LineStartState; + } + } + + private class QuotedValueState : ParserState + { + public override ParserState AnyChar(char ch, ParserContext context) + { + context.AddChar(ch); + return QuotedValueState; + } + + public override ParserState Comma(ParserContext context) + { + context.AddChar(CommaCharacter); + return QuotedValueState; + } + + public override ParserState Quote(ParserContext context) + { + return QuoteState; + } + + public override ParserState EndOfLine(ParserContext context) + { + context.AddChar('\r'); + context.AddChar('\n'); + return QuotedValueState; + } + } + + private class QuoteState : ParserState + { + public override ParserState AnyChar(char ch, ParserContext context) + { + //undefined, ignore " + context.AddChar(ch); + return QuotedValueState; + } + + public override ParserState Comma(ParserContext context) + { + context.AddValue(); + return ValueStartState; + } + + public override ParserState Quote(ParserContext context) + { + context.AddChar(QuoteCharacter); + return QuotedValueState; + } + + public override ParserState EndOfLine(ParserContext context) + { + context.AddValue(); + context.AddLine(); + return LineStartState; + } + } + + private class ParserContext + { + private readonly StringBuilder _currentValue = new StringBuilder(); + private readonly List _lines = new List(); + private readonly List _currentLine = new List(); + + public void AddChar(char ch) + { + _currentValue.Append(ch); + } + + public void AddValue() + { + _currentLine.Add(_currentValue.ToString()); + _currentValue.Remove(0, _currentValue.Length); + } + + public void AddLine() + { + _lines.Add(_currentLine.ToArray()); + _currentLine.Clear(); + } + + public List GetAllLines() + { + if (_currentValue.Length > 0) + { + AddValue(); + } + if (_currentLine.Count > 0) + { + AddLine(); + } + return _lines; + } + } + + #endregion + + public string[][] Parse(string csvData) + { + var context = new ParserContext(); + + string[] lines = csvData.Split('\n'); + + ParserState currentState = ParserState.LineStartState; + foreach (string next in lines) + { + foreach (char ch in next) + { + switch (ch) + { + case CommaCharacter: + currentState = currentState.Comma(context); + break; + case QuoteCharacter: + currentState = currentState.Quote(context); + break; + default: + currentState = currentState.AnyChar(ch, context); + break; + } + } + currentState = currentState.EndOfLine(context); + } + List allLines = context.GetAllLines(); + return allLines.ToArray(); + } + } +} \ No newline at end of file diff --git a/Assets/CsvParser.cs.meta b/Assets/CsvParser.cs.meta new file mode 100644 index 00000000..8c45d08a --- /dev/null +++ b/Assets/CsvParser.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 803c0d4d8bc9447d1b20e1f4fb86120f +timeCreated: 1428171346 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Fungus/Flowchart/Scripts/CSVSupport.cs b/Assets/Fungus/Flowchart/Scripts/CSVSupport.cs index f92ec78a..95921102 100644 --- a/Assets/Fungus/Flowchart/Scripts/CSVSupport.cs +++ b/Assets/Fungus/Flowchart/Scripts/CSVSupport.cs @@ -39,32 +39,7 @@ namespace Fungus return s; } - - public static string[] SplitCSVLine(string line) - { - - // '(?[^'\\]*(?:\\[\S\s][^'\\]*)*)' # Either $1: Single quoted string, - - string pattern = @" - # Match one value in valid CSV string. - (?!\s*$) # Don't match empty last value. - \s* # Strip whitespace before value. - (?: # Group for value alternatives. - | ""(?[^""\\]*(?:\\[\S\s][^""\\]*)*)"" # or $2: Double quoted string, - | (?[^,'""\s\\]*(?:\s+[^,'""\s\\]+)*) # or $3: Non-comma, non-quote stuff. - ) # End group of value alternatives. - \s* # Strip whitespace after value. - (?:,|$) # Field ends on comma or EOS. - "; - - string[] values = (from Match m in Regex.Matches(line, pattern, - RegexOptions.ExplicitCapture | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline) - select m.Groups[1].Value).ToArray(); - - return values; - } - - + private const string QUOTE = "\""; private const string ESCAPED_QUOTE = "\"\""; // private static char[] CHARACTERS_THAT_MUST_BE_QUOTED = { ',', '"', '\n' }; diff --git a/Assets/Fungus/Flowchart/Scripts/Language.cs b/Assets/Fungus/Flowchart/Scripts/Language.cs index 7f2bc4c6..b55170ed 100644 --- a/Assets/Fungus/Flowchart/Scripts/Language.cs +++ b/Assets/Fungus/Flowchart/Scripts/Language.cs @@ -2,23 +2,12 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; +using Ideafixxxer.CsvParser; namespace Fungus { - interface ILocalizable - { - string GetLocalizationID(); - - string GetStandardText(); - - void SetStandardText(string standardText); - - string GetTimestamp(); - - string GetDescription(); - } - /** * Multi-language localization support. */ @@ -36,7 +25,6 @@ namespace Fungus */ protected class LanguageItem { - public string timeStamp; public string description; public string standardText; public Dictionary localizedStrings = new Dictionary(); @@ -73,7 +61,7 @@ namespace Fungus } // Build CSV header row and a list of the language codes currently in use - string csvHeader = "Key,Timestamp,Description,Standard"; + string csvHeader = "Key,Description,Standard"; List languageCodes = new List(); foreach (LanguageItem languageItem in languageItems.Values) { @@ -94,7 +82,6 @@ namespace Fungus LanguageItem languageItem = languageItems[stringId]; string row = CSVSupport.Escape(stringId); - row += "," + CSVSupport.Escape(languageItem.timeStamp); row += "," + CSVSupport.Escape(languageItem.description); row += "," + CSVSupport.Escape(languageItem.standardText); @@ -121,6 +108,7 @@ namespace Fungus Dictionary languageItems = new Dictionary(); // Export all Say and Menu commands in the scene + // To make it easier to localize, we preserve the command order in each exported block. Flowchart[] flowcharts = GameObject.FindObjectsOfType(); foreach (Flowchart flowchart in flowcharts) { @@ -131,19 +119,22 @@ namespace Fungus { string stringID = ""; string standardText = ""; - + string description = ""; + System.Type type = command.GetType(); if (type == typeof(Say)) { stringID = "SAY." + flowchart.name + "." + command.itemId; Say sayCommand = command as Say; standardText = sayCommand.storyText; + description = sayCommand.description; } else if (type == typeof(Menu)) { stringID = "MENU." + flowchart.name + "." + command.itemId; Menu menuCommand = command as Menu; standardText = menuCommand.text; + description = menuCommand.description; } else { @@ -162,9 +153,8 @@ namespace Fungus } // Update basic properties,leaving localised strings intact - languageItem.timeStamp = "10/10/2015"; - languageItem.description = "Note"; languageItem.standardText = standardText; + languageItem.description = description; } } } @@ -174,33 +164,37 @@ namespace Fungus protected virtual void AddLocalisedStrings(Dictionary languageItems, string csvData) { - // Split into lines - // Excel on Mac exports csv files with \r line endings, so we need to support that too. - string[] lines = csvData.Split('\n', '\r'); + CsvParser csvParser = new CsvParser(); + string[][] csvTable = csvParser.Parse(csvData); - if (lines.Length == 0) + if (csvTable.Length <= 1) { - // Early out if no data in file + // No data rows in file return; } - + // Parse header row - string[] columnNames = CSVSupport.SplitCSVLine(lines[0]); + string[] columnNames = csvTable[0]; - for (int i = 1; i < lines.Length; ++i) + for (int i = 1; i < csvTable.Length; ++i) { - string line = lines[i]; - - string[] fields = CSVSupport.SplitCSVLine(line); + string[] fields = csvTable[i]; if (fields.Length < 4) { + // No localized string fields present continue; } string stringId = fields[0]; + if (!languageItems.ContainsKey(stringId)) + { + continue; + } + // Store localized strings for this string id - for (int j = 4; j < fields.Length; ++j) + LanguageItem languageItem = languageItems[stringId]; + for (int j = 3; j < fields.Length; ++j) { if (j >= columnNames.Length) { @@ -211,10 +205,7 @@ namespace Fungus if (languageEntry.Length > 0) { - if (languageItems.ContainsKey(stringId)) - { - languageItems[stringId].localizedStrings[languageCode] = languageEntry; - } + languageItem.localizedStrings[languageCode] = languageEntry; } } } @@ -230,18 +221,17 @@ namespace Fungus localizedStrings.Clear(); - // Split into lines - // Excel on Mac exports csv files with \r line endings, so we need to support that too. - string[] lines = csvData.Split('\n', '\r'); - - if (lines.Length == 0) + CsvParser csvParser = new CsvParser(); + string[][] csvTable = csvParser.Parse(csvData); + + if (csvTable.Length <= 1) { // No data rows in file return; } - + // Parse header row - string[] columnNames = CSVSupport.SplitCSVLine(lines[0]); + string[] columnNames = csvTable[0]; if (columnNames.Length < 5) { @@ -250,7 +240,7 @@ namespace Fungus } int languageIndex = -1; - for (int i = 4; i < columnNames.Length; ++i) + for (int i = 3; i < columnNames.Length; ++i) { if (columnNames[i] == languageCode) { @@ -265,11 +255,10 @@ namespace Fungus return; } - for (int i = 1; i < lines.Length; ++i) + for (int i = 1; i < csvTable.Length; ++i) { - string line = lines[i]; - - string[] fields = CSVSupport.SplitCSVLine(line); + string[] fields = csvTable[i]; + if (fields.Length < languageIndex + 1) { continue; @@ -342,64 +331,6 @@ namespace Fungus } } } - - /** - * Import strings from a CSV file. - * 1. Any changes to standard text items will be applied to the corresponding scene object. - * 2. Any localized strings will be added to the localization dictionary. - */ - public virtual void ImportCSV(string csvData) - { - /* - // Split into lines - // Excel on Mac exports csv files with \r line endings, so we need to support that too. - string[] lines = csvData.Split('\n', '\r'); - - if (lines.Length == 0) - { - return; - } - - localizedStrings.Clear(); - - // Parse header row - string[] columnNames = CSVSupport.SplitCSVLine(lines[0]); - - for (int i = 1; i < lines.Length; ++i) - { - string line = lines[i]; - - string[] fields = CSVSupport.SplitCSVLine(line); - if (fields.Length < 4) - { - continue; - } - - string stringId = fields[0]; - // Ignore timestamp & notes fields - string standardText = CSVSupport.Unescape(fields[3]); - - PopulateGameString(stringId, standardText); - - // Store localized string in stringDict - for (int j = 4; j < fields.Length; ++j) - { - if (j >= columnNames.Length) - { - continue; - } - string languageCode = columnNames[j]; - string languageEntry = CSVSupport.Unescape(fields[j]); - - if (languageEntry.Length > 0) - { - // The dictionary key is the basic string id with . appended - localizedStrings[stringId + "." + languageCode] = languageEntry; - } - } - } - */ - } } } \ No newline at end of file diff --git a/Assets/Fungus/Narrative/Scripts/Character.cs b/Assets/Fungus/Narrative/Scripts/Character.cs index de99b3f2..82d9e319 100644 --- a/Assets/Fungus/Narrative/Scripts/Character.cs +++ b/Assets/Fungus/Narrative/Scripts/Character.cs @@ -8,7 +8,7 @@ namespace Fungus { [ExecuteInEditMode] - public class Character : MonoBehaviour, ILocalizable + public class Character : MonoBehaviour { public string nameText; // We need a separate name as the object name is used for character variations (e.g. "Smurf Happy", "Smurf Sad") public Color nameColor = Color.white; @@ -36,33 +36,6 @@ namespace Fungus { activeCharacters.Remove(this); } - - // ILocalizable methods - - public virtual string GetLocalizationID() - { - return "CHARACTER." + nameText; - } - - public virtual string GetStandardText() - { - return nameText; - } - - public virtual void SetStandardText(string standardText) - { - nameText = standardText; - } - - public virtual string GetTimestamp() - { - return DateTime.Now.ToShortDateString(); - } - - public virtual string GetDescription() - { - return description; - } } } \ No newline at end of file diff --git a/Assets/Fungus/Narrative/Scripts/Commands/Menu.cs b/Assets/Fungus/Narrative/Scripts/Commands/Menu.cs index b82cd41b..5ae26e31 100644 --- a/Assets/Fungus/Narrative/Scripts/Commands/Menu.cs +++ b/Assets/Fungus/Narrative/Scripts/Commands/Menu.cs @@ -11,7 +11,7 @@ namespace Fungus "Menu", "Displays a multiple choice menu")] [AddComponentMenu("")] - public class Menu : Command, ILocalizable + public class Menu : Command { // Menu displays a menu button which will execute the target block when clicked @@ -116,33 +116,6 @@ namespace Fungus { return false; } - - // ILocalizable methods - - public virtual string GetLocalizationID() - { - return "MENU." + itemId.ToString(); - } - - public virtual string GetStandardText() - { - return text; - } - - public virtual void SetStandardText(string standardText) - { - text = standardText; - } - - public virtual string GetTimestamp() - { - return DateTime.Now.ToShortDateString(); - } - - public virtual string GetDescription() - { - return description; - } } } \ No newline at end of file diff --git a/Assets/Fungus/Narrative/Scripts/Commands/Say.cs b/Assets/Fungus/Narrative/Scripts/Commands/Say.cs index f1982852..0a75aced 100644 --- a/Assets/Fungus/Narrative/Scripts/Commands/Say.cs +++ b/Assets/Fungus/Narrative/Scripts/Commands/Say.cs @@ -9,7 +9,7 @@ namespace Fungus "Say", "Writes text in a dialog box.")] [AddComponentMenu("")] - public class Say : Command, ILocalizable + public class Say : Command { [TextArea(5,10)] public string storyText = ""; @@ -164,33 +164,6 @@ namespace Fungus { executionCount = 0; } - - // ILocalizable methods - - public virtual string GetLocalizationID() - { - return "SAY." + itemId.ToString(); - } - - public virtual string GetStandardText() - { - return storyText; - } - - public virtual void SetStandardText(string standardText) - { - storyText = standardText; - } - - public virtual string GetTimestamp() - { - return DateTime.Now.ToShortDateString(); - } - - public virtual string GetDescription() - { - return description; - } } } \ No newline at end of file