3
\$\begingroup\$

Given a Dialogue Script for SDT (NSFW! Google at own risk), parse said dialogue script for validation with checkers. A dialogue script consists of a set of lines.

There's a special line, initial_settings, formatted like such:

initial_settings:{"variable1":numericvalue,"variable2":"stringvalue"}

The rest of the lines follow this format:

linename:"content[TRIGGER]*insertion*" {"attribute":"value","complexattribute":{"variable":numericvalue,"variable2":"stringvalue"}}

Class variables:

//The lines of the dialogue, stored in order of appearance.
private var lines:Array;
//A dictionary of the lines. Structured like so:
//Indexed by linename. Contents are an array of indexes for the lines array.
private var linesDictionary:Array;
//The entire dialogue in string format, if available.
private var dialogue:String;
//String-indexed.
private var variables:Array;

Parsing a dialogue...

public function parseDialogueFromString(dialogueText:String):void {
    var a:Array = dialogueText.split("\r"); //Maybe I should split by \r\n first, then by \n\r, then by \r, then by \n?
    if (a.length == 1) {
        a = dialogueText.split("\n");
    }
    //Parse each line individually...
    for (var i:uint = 0, isize:uint = a.length; i < isize; i++) {
        var line:Line = parseLine(a[i], i);
        //Then place them in the dictionary and main array.
        if (line != null) {
            if (linesDictionary[line.getLineName()] == null) {
                linesDictionary[line.getLineName()] = new Array();
            }
            linesDictionary[line.getLineName()].push(lines.push(line) - 1);
        }
    }
}

line by line.

/**
 * Parses a single Line of Dialogue from a String.
 * Note that since SDT still parses commented lines, the DialogueChecker has to do so as well.
 * Certain severe issues don't care that a line is unused, for instance.
 * @param   lineText the string that should be converted to a Line object.
 * @param   lineIndex the index of the line in the array - used for determining lineNumber.
 * @return a line object if possible, nothing otherwise.
 */
private function parseLine(lineText:String, lineIndex:uint):Line {
    var lineNumber:uint = lineIndex + 1;
    var lineName:String = "";
    var rawLine:String = lineText;
    var lineContent:Array = new Array();
    var lineExcess:Array = ["", "", ""];
    var lineAttribute:LineAttribute = null;
    var lineAttributeValueArray:Array = new Array();
    var lineAttributes:Array = new Array();
    var lineAttributesString:String = "";
    var lineContentString:String = "";
    var line:Line = null;

    //1|intro:"Hi!" {"style":"Him"}
    //2|general : "Hello"
    //3|help"me"{"next":"pls"}
    //4|held:Let go!{"held":"true"}
    //5|under_line"Testing!"
    //6|font:Arial
    var source:String = lineText;
    var nameEnd:uint = 0;
    if (source.indexOf(":") != -1 || source.indexOf("\"") != -1) {
        //valid for all 6 examples.
        //Needs : or " to be a line. Otherwise it's not parsable.
        if (source.indexOf(":") != -1) {
            nameEnd = source.indexOf(":");
        } else {
            nameEnd = source.indexOf("\"");
        }
    } else {
        return null; //Comment Filter!
    }
    lineName = source.substr(0, nameEnd);
    source = source.substring(nameEnd);
    //1|:"Hi!" {"style":"Him"}
    //2|: "Hello"
    //3|:"pls"}
    //4|:Let go!{"held":"true"}
    //5|"Testing!"
    //6|:Arial
    var contentStart:int = 0;
    if (source.indexOf(":") == 0) {
        source = source.substring(1);
        //1|"Hi!" {"style":"Him"}
        //2| "Hello"
        //3|"pls"}
        //4|Let go!{"held":"true"}
        //6|Arial
        contentStart = source.indexOf("\"");
    }
    if (contentStart >= 0 && lineName != "initial_settings") {
        lineExcess[0] = source.substr(0, contentStart);
        source = source.substr(contentStart);
    }
    //1|"Hi!" {"style":"Him"}
    //2|"Hello"
    //3|"pls"}
    //4|"held":"true"}
    //5|"Testing!"
    //6|Arial
    var contentString:String = "";
    if (source.indexOf("\"") == 0 && source.indexOf("\"", 1) != -1 && lineName != "initial_settings") {
        contentString = source.substr(0, source.indexOf("\"", 1) + 1);
        source = source.substr(source.indexOf("\"", 1) + 1);
    }
    //source:
    //1| {"style":"Him"}
    //2|
    //3|}
    //4|:"true"}
    //5|
    //6|Arial

    //contentString:
    //1|"Hi!"
    //2|"Hello"
    //3|"pls"
    //4|"held"
    //5|"Testing!"
    //6|
    if (contentString == "" && lineName != "initial_settings") {
        contentString = source;
        source = "";
    }
    lineContentString = contentString;
    //Parse contentString
    //When parsing a contentString, you're treating everything as normal string until...
    //Б - change to delayString (new normal string, keep until different character)
    //% - change to special char (new normal string, parse next 2 chars, keep that as pattern, then parse until pattern is broken)
    //* - change to VariableInsertion/VariableSubstitution (Treat as Insertion until YOU/ME/YOUR/MY/FINISHES is detected)
    //[ - change to Trigger
    var contentParseMode:String = "Normal"; //Remember kids, Stringly-typed code is bad! What I'm doing here is bad! So don't do it! (Even although it makes things easier sometimes...)
    var currentContentSegment:String = "";

    while (contentString != "") {
        if (contentParseMode == "Normal") {
            //The problem: 4 functions give 4 values. Which function gives the lowest positive value?
            var checkArray:Array = new Array();
            checkArray.push([contentString.indexOf("Б"), "Б"]);
            checkArray.push([contentString.indexOf("%"), "%"]);
            checkArray.push([contentString.indexOf("*"), "*"]);
            checkArray.push([contentString.indexOf("["), "["]);
            checkArray.sortOn([0], [Array.NUMERIC]);
            var nextIssue:int = -1;
            var nextChar:String = "";
            for (var i:uint = 0, isize:uint = checkArray.length; i < isize; i++) {
                if (checkArray[i][0] != -1) {
                    nextIssue = checkArray[i][0];
                    nextChar = checkArray[i][1];
                    i = isize;
                }
            }
            if (nextChar == "Б" || nextChar == "%" || nextChar == "*" || nextChar == "[") {

                if (nextChar == "Б") {
                    contentParseMode = "Delay";
                } else if (nextChar == "*" || (nextChar == "%" && contentString.indexOf("%2A") == nextIssue)) {
                    contentParseMode = "Insertion";
                } else if (nextChar == "%") {
                    //trace(contentString.indexOf("%2A")); //commented because it was just extra noise for me as dev and I don't know why I had put this in (v2.13)
                    contentParseMode = "SpecialChar";
                } else if (nextChar == "[") {
                    contentParseMode = "Trigger";
                }
                currentContentSegment = contentString.substr(0, nextIssue);
                lineContent.push(currentContentSegment);
                currentContentSegment = "";
                contentString = contentString.substr(nextIssue);
            } else {
                currentContentSegment = contentString;
                lineContent.push(currentContentSegment);
                currentContentSegment = "";
                contentString = "";
            }
        } else if (contentParseMode == "Delay") {
            //Loop until next char is not delay char. Add all looped chars to content array, strip from content string. Evaluate next section.
            while (contentString.length > 0 && contentString.indexOf("Б") == 0) {
                currentContentSegment += "Б";
                contentString = contentString.substr(1);
            }
            lineContent.push(currentContentSegment);
            currentContentSegment = "";
            if (contentString.charAt(0) == "%" && contentString.indexOf("%2A") != 0) {
                contentParseMode = "SpecialChar";
            } else if (contentString.charAt(0) == "[") {
                contentParseMode = "Trigger";
            } else if (contentString.charAt(0) == "*" || contentString.indexOf("%2A") == 0) {
                contentParseMode = "Insertion";
            } else {
                contentParseMode = "Normal";
            }
        } else if (contentParseMode == "SpecialChar") {
            //Identify special char type, loop until pattern is not next section of string. Add all looped chars to content array, strip from content string. Evaluate next section.
            var specialCharPattern:String = contentString.substr(0, 3);
            //check if it's actually a percent encoded value here
            var hexString:String = "0123456789ABCDEFabcdef";
            if (!(specialCharPattern.length == 3 && hexString.indexOf(specialCharPattern.charAt(1)) != -1 && hexString.indexOf(specialCharPattern.charAt(2)) != -1)) {
                //it's not a proper percent encoded value.
                //treat the % as %25, and strip only 1 character.

                currentContentSegment = specialCharPattern;
                contentString = contentString.substring(1); //strip 1 char
            } else {
                while (contentString.length > 0 && contentString.indexOf(specialCharPattern) == 0) {
                    currentContentSegment += specialCharPattern;
                    contentString = contentString.substr(specialCharPattern.length);
                }
            }
            lineContent.push(currentContentSegment);
            currentContentSegment = "";
            if (contentString.charAt(0) == "%" && contentString.indexOf("%2A") != 0) {
                contentParseMode = "SpecialChar";
            } else if (contentString.charAt(0) == "[") {
                contentParseMode = "Trigger";
            } else if (contentString.charAt(0) == "*" || contentString.indexOf("%2A") == 0) {
                contentParseMode = "Insertion";
            } else if (contentString.charAt(0) == "Б") {
                contentParseMode = "Delay";
            } else {
                contentParseMode = "Normal";
            }
        } else if (contentParseMode == "Trigger") {
            //Find trigger and identify trigger if possible...?
            //TODO: Identify variable insertions within triggers
            if (contentString.indexOf("[") != 0) {
                ErrorManager.showDevelopmentError("Line parsing - trigger doesn't start with [");
                contentString = "";
                break;
            }
            var trigger:String = "";
            if (contentString.indexOf("]") != -1) {
                trigger = contentString.substr(1, contentString.indexOf("]") - 1);
                contentString = contentString.substr(contentString.indexOf("]") + 1);
            } else {
                //user error, so ignore it. Act like rest of line is trigger.
                trigger = contentString.substr(1);
                contentString = "";
            }
            var type:TriggerType = TriggerType.identifyType(trigger);
            var triggerObject:Trigger = new Trigger(trigger, type);
            lineContent.push(triggerObject);
            if (contentString.charAt(0) == "%" && contentString.indexOf("%2A") != 0) {
                contentParseMode = "SpecialChar";
            } else if (contentString.charAt(0) == "[") {
                contentParseMode = "Trigger";
            } else if (contentString.charAt(0) == "*" || contentString.indexOf("%2A") == 0) {
                contentParseMode = "Insertion";
            } else if (contentString.charAt(0) == "Б") {
                contentParseMode = "Delay";
            } else {
                contentParseMode = "Normal";
            }
        } else if (contentParseMode == "Insertion") {
            //Find insertion and determine type.
            if (contentString.indexOf("*") != 0 && contentString.indexOf("%2A") != 0) {
                ErrorManager.showDevelopmentError("Line parsing - insertion doesn't start with *");
                contentString = "";
                break;
            }
            var insertion:String = "";
            var indexOfAsterisk:int = contentString.indexOf("*");
            var indexOfPercent2A:int = contentString.indexOf("%2A");
            var indexOfSecondAsterisk:int = contentString.indexOf("*", 1);
            var indexOfSecondPercent2A:int = contentString.indexOf("%2A", 1);
            if (Math.max(indexOfSecondAsterisk, indexOfSecondPercent2A) != -1) {
                var insertionIndex:int = indexOfAsterisk;
                var insertionEndIndex:int = indexOfSecondAsterisk;
                var insertionStartLength:uint = 1;
                var insertionEndLength:uint = 1;
                if (indexOfAsterisk == -1 || (indexOfPercent2A < indexOfAsterisk && indexOfPercent2A != -1)) {
                    insertionIndex = indexOfPercent2A;
                    insertionStartLength = 3;
                }
                if (indexOfSecondAsterisk == -1 || (indexOfSecondPercent2A < indexOfSecondAsterisk && indexOfSecondPercent2A != -1)) {
                    insertionEndIndex = indexOfSecondPercent2A;
                    insertionEndLength = 3;
                }
                insertion = contentString.substr(insertionStartLength, insertionEndIndex - insertionStartLength);
                contentString = contentString.substr(insertionEndIndex + insertionEndLength);
            } else {
                //user error, so ignore it. Act like rest of line is insertion.
                insertion = contentString.substr(1);
                contentString = "";
            }
            var insertionObject:LineElement = null;
            if (insertion.indexOf("YOU") != -1 || insertion.indexOf("ME") != -1 || insertion.indexOf("MY") != -1 || insertion.indexOf("FINISHES") != -1) { //There's also YOUR, but that's already covered by checking for YOU.
                insertionObject = new VariableSubstitution(insertion);
            } else {
                var variable:Variable = getVariable("\"" + insertion + "\"");
                if (variable == null) {
                    variable = new Variable(VariableType.getEnum("UNKNOWN"), "\"" + insertion + "\"");
                    addVariable(variable);
                }
                variable.setInsertedInDialogue(true);
                insertionObject = new VariableInsertion(variable, insertion);
            }
            lineContent.push(insertionObject);
            if (contentString.charAt(0) == "%" && contentString.indexOf("%2A") != 0) {
                contentParseMode = "SpecialChar";
            } else if (contentString.charAt(0) == "[") {
                contentParseMode = "Trigger";
            } else if (contentString.charAt(0) == "*" || contentString.indexOf("%2A") == 0) {
                contentParseMode = "Insertion";
            } else if (contentString.charAt(0) == "Б") {
                contentParseMode = "Delay";
            } else {
                contentParseMode = "Normal";
            }
        } else {
            contentString = "";
            ErrorManager.showDevelopmentError("Line parsing infinite loop");
        }
    }
    //source:
    //1| {"style":"Him"}
    //2|
    //3|}
    //4|:"true"}
    //5|
    //6|

    //contentString:
    //1|"Hi!"
    //2|"Hello"
    //3|"pls"
    //4|"held"
    //5|"Testing!"
    //6|Arial
    var attributeStart:int = 0;
    var hasAttributes:Boolean = false;
    if (source.indexOf("{") != -1) {
        hasAttributes = true;
        attributeStart = source.indexOf("{");
        lineExcess[1] = source.substr(0, source.indexOf("{"));
    }
    //source:
    //1|{"style":"Him"}
    //2|
    //3|}
    //4|:"true"}
    //5|
    //6|
    var attributeString:String = "";
    if (hasAttributes) {
        var endIndex:Number = Math.max(source.lastIndexOf("{"), source.lastIndexOf("}"));
        attributeString = source.substr(attributeStart, endIndex + 1);
        source = source.substr(endIndex + 1);
    }
    //Parse attributeString
    lineAttributesString = attributeString;
    var attributeParseMode:String = "Start"; //Oh no Pim, more stringly-typed coding! What are you doing?! I'm trying to make things work, that's what I'm doing.
    var attributeName:String = "";
    var attributeValue:Object; //LineAttributes can be multiple variable references.
    var attributeName2:String = "";
    var attributeValue2:String = "";
    while (attributeString != "") {
        //I really should have included an initial_settings line in my examples.
        if (attributeParseMode == "Start") {
            if (attributeString.indexOf("{") != 0) {
                //Not sure who is to blame here. Me or the user?
                attributeString = "";
            } else {
                attributeString = attributeString.substr(1);
                attributeParseMode = "NextVar";
            }
        } else if (attributeParseMode == "NextVar") {
            //nextvar, reset.
            attributeName = "";
            attributeValue = null;
            attributeName2 = "";
            attributeValue2 = "";
            var start:int = attributeString.indexOf("\"");
            if (start != -1) {
                var end:int = attributeString.indexOf("\"", start + 1);
                attributeName = attributeString.substring(start, end + 1);
                attributeString = attributeString.substr(end + 1);
                attributeParseMode = "NextVal";
            } else {
                if (attributeString.indexOf("}") == 0) {
                    attributeString = attributeString.substr(1);
                }
                //No more variables, so this is the end.
                //source = attributeString + source; //<- I had that line of code, but lineAttributesString makes it unnecessary. I think. Gotta test it first.
                attributeString = "";
            }
        } else if (attributeParseMode == "NextVal") {
            if (attributeString.indexOf(":") != 0) {
                //user error. Now what.
                //Decided to add string version of lineAttributes, so I can always detect shit like missing }.
                //Same goes for this. whatever.
                attributeString = "";
            } else {
                attributeString = attributeString.substr(1);
                if (attributeString.charAt(0) == "\"") {
                    if (attributeString.indexOf("\"", 1) != -1) {
                        attributeValue = attributeString.substring(0, attributeString.indexOf("\"", 1) + 1);
                        attributeString = attributeString.substring(attributeString.indexOf("\"", 1) + 1);
                    } else {
                        attributeValue = attributeString;
                    }
                    lineAttribute = new LineAttribute(attributeName, attributeValue);
                    lineAttributes.push(lineAttribute);
                    if (lineName == "initial_settings") {
                        var variable:Variable = getVariable(attributeName);
                        if (variable == null) {
                            if (attributeValue == "\"false\"" || attributeValue == "\"true\"") {
                                variable = new Variable(VariableType.getEnum("BOOLEAN"), attributeName);
                            } else {
                                variable = new Variable(VariableType.getEnum("STRING"), attributeName);
                            }
                            addVariable(variable);
                        } else if (variable.getType() == VariableType.getEnum("UNKNOWN")) {
                            if (attributeValue == "\"false\"" || attributeValue == "\"true\"") {
                                variable.setType(VariableType.getEnum("BOOLEAN"));
                            } else {
                                variable.setType(VariableType.getEnum("STRING"));
                            }
                        }
                        variable.setInitialValue(attributeValue);
                    }
                    attributeParseMode = "NextVar";
                } else if (attributeString.indexOf("{") == 0) {
                    //Array.
                    attributeString = attributeString.substr(1);
                    lineAttributeValueArray = new Array();
                    attributeParseMode = "NextSubVar";
                } else if (attributeString.indexOf(",") != -1 || attributeString.indexOf("}") != -1) {
                    //Numeric.
                    var nextcomma:int = attributeString.indexOf(",");
                    var nextbackaccolade:int = attributeString.indexOf("}");
                    if (nextcomma != -1) {
                        if (nextbackaccolade != -1 && nextbackaccolade < nextcomma) { //I don't know either.
                            attributeValue = attributeString.substr(0, nextbackaccolade);
                            attributeString = attributeString.substr(nextbackaccolade);
                        } else {
                            attributeValue = attributeString.substr(0, nextcomma);
                            attributeString = attributeString.substr(nextcomma);
                        }
                    } else {
                        attributeValue = attributeString.substr(0, nextbackaccolade);
                        attributeString = attributeString.substr(nextbackaccolade);
                    }
                    lineAttribute = new LineAttribute(attributeName, attributeValue);
                    lineAttributes.push(lineAttribute);
                    if (lineName == "initial_settings") {
                        var variable:Variable = getVariable(attributeName);
                        if (variable == null) {
                            variable = new Variable(VariableType.getEnum("NUMERIC"), attributeName);
                            addVariable(variable);
                        }
                        variable.setInitialValue(attributeValue);
                    }
                    attributeParseMode = "NextVar";
                } else {
                    //Yeaahh... damn users again.
                    //Unknown type, but lets register the attribute anyway.
                    attributeValue = attributeString;
                    attributeString = "";
                    lineAttribute = new LineAttribute(attributeName, attributeValue);
                    lineAttributes.push(lineAttribute);
                    if (lineName == "initial_settings") {
                        var variable:Variable = getVariable(attributeName);
                        if (variable == null) {
                            variable = new Variable(VariableType.getEnum("UNKNOWN"), attributeName);
                            addVariable(variable);
                        }
                        variable.setInitialValue(attributeValue);
                    }
                    attributeParseMode = "NextVar";
                }
            }
        } else if (attributeParseMode == "NextSubVar") {
            //"name":??? (value)
            attributeName2 = "";
            attributeValue2 = "";
            var nextSegmentArray:Array = new Array();
            nextSegmentArray.push([attributeString.indexOf("}"), "}"]);
            nextSegmentArray.push([attributeString.indexOf("\""), "\""]);
            nextSegmentArray.sortOn([0], [Array.NUMERIC]);
            var nextSegmentLocation:int = -1;
            var nextSegmentChar:String = "";
            for (var i:uint = 0, isize:uint = nextSegmentArray.length; i < isize; i++) {
                if (nextSegmentArray[i][0] != -1) {
                    nextSegmentLocation = nextSegmentArray[i][0];
                    nextSegmentChar = nextSegmentArray[i][1];
                    i = isize;
                }
            }
            if (nextSegmentChar == "\"") {
                attributeString = attributeString.substr(nextSegmentLocation); //I hope that didn't contain anything important.
                if (attributeString.indexOf("\"") == 0) {
                    if (attributeString.indexOf("\"", 1) != -1) {
                        //This segment - NextSubVar name String, correct
                        attributeName2 = attributeString.substr(0, attributeString.indexOf("\"", 1) + 1);
                        attributeString = attributeString.substr(attributeString.indexOf("\"", 1) + 1);
                    } else if (attributeString.indexOf(":", 1) != -1) {
                        //This segment - NextSubVar name String, missing end double quote
                        attributeName2 = attributeString.substr(0, attributeString.indexOf(":", 1));
                        attributeString = attributeString.substr(attributeString.indexOf(":", 1));
                    } else {
                        //fuck you, learn to write dialogues for once.
                        //I mean, how inept do you have to be to keep making all these mistakes - 
                        //given a line like {"set":{"var":"value","var2":0}}
                        //Here's where I have to watch out for...
                        //{"set:{"var":"value","var2":0}}
                        //{"set"{"var":"value","var2":0}}
                        //{"set": {"var":"value","var2":0}}
                        //{"set":{ "var":"value","var2":0}}
                        //{"set":{"var""value","var2":0}}
                        //{"set":{"var":value","var2":0}}
                        //etc. how hard is it to just, yknow, write the line properly?
                        attributeString = "";
                    }
                }
                attributeParseMode = "NextSubVal";
            } else if (nextSegmentChar == "}") {
                attributeString = attributeString.substr(nextSegmentLocation + 1);
                attributeParseMode = "NextVar";
                lineAttribute = new LineAttribute(attributeName, lineAttributeValueArray);
                lineAttributes.push(lineAttribute);
            }
        } else if (attributeParseMode == "NextSubVal") {
            //This segment - NextSubVal
            //Does it start with "? Find next ", that is value. Else, find next , or }
            //Does it start with something else? Find next , or }.
            if (attributeString.indexOf(":") == 0) {
                attributeString = attributeString.substr(1);
                var nextSegmentArray:Array = new Array();
                nextSegmentArray.push([attributeString.indexOf("}"), "}"]);
                nextSegmentArray.push([attributeString.indexOf("\""), "\""]);
                nextSegmentArray.push([attributeString.indexOf(","), ","]);
                nextSegmentArray.sortOn([0], [Array.NUMERIC]);
                var nextSegmentLocation:int = -1;
                var nextSegmentChar:String = "";
                for (var i:uint = 0, isize:uint = nextSegmentArray.length; i < isize; i++) {
                    if (nextSegmentArray[i][0] != -1) {
                        nextSegmentLocation = nextSegmentArray[i][0];
                        nextSegmentChar = nextSegmentArray[i][1];
                        i = isize;
                    }
                }
                if (nextSegmentChar == "\"") {
                    attributeValue2 = attributeString.substr(0, nextSegmentLocation);
                    attributeString = attributeString.substr(nextSegmentLocation);
                    if (attributeString.indexOf("\"") == 0) {
                        if (attributeString.indexOf("\"", 1) != -1) {
                            attributeValue2 += attributeString.substr(0, attributeString.indexOf("\"", 1) + 1);
                            attributeString = attributeString.substr(attributeString.indexOf("\"", 1) + 1);
                            var variableReference:VariableReference = new VariableReference(attributeName2, attributeValue2);
                            if (attributeName == "\"set\"") {
                                addSetValueToVariable(attributeName2, attributeValue2);
                            } else if (attributeName == "\"check\"") {
                                addCheckedValueToVariable(attributeName2, attributeValue2);
                            }
                            lineAttributeValueArray.push(variableReference);
                            attributeParseMode = "NextSubVar";
                        } else {
                            //find next comma or }
                            var nextCommaIndex:int = attributeString.indexOf(",");
                            var nextBackAccoladeIndex:int = attributeString.indexOf("}");
                            if (nextCommaIndex != -1) {
                                if (nextBackAccoladeIndex != -1 && nextBackAccoladeIndex < nextCommaIndex) {
                                    attributeValue2 += attributeString.substr(0, attributeString.indexOf("}"));
                                    attributeString = attributeString.substr(attributeString.indexOf("}") + 1);
                                    var variableReference:VariableReference = new VariableReference(attributeName2, attributeValue2);
                                    if (attributeName == "\"set\"") {
                                        addSetValueToVariable(attributeName2, attributeValue2);
                                    } else if (attributeName == "\"check\"") {
                                        addCheckedValueToVariable(attributeName2, attributeValue2);
                                    }
                                    lineAttributeValueArray.push(variableReference);
                                    lineAttribute = new LineAttribute(attributeName, lineAttributeValueArray);
                                    lineAttributes.push(lineAttribute);
                                    attributeParseMode = "NextVar";
                                } else {
                                    attributeValue2 += attributeString.substr(0, attributeString.indexOf(","));
                                    attributeString = attributeString.substr(attributeString.indexOf(",") + 1);
                                    var variableReference:VariableReference = new VariableReference(attributeName2, attributeValue2);
                                    if (attributeName == "\"set\"") {
                                        addSetValueToVariable(attributeName2, attributeValue2);
                                    } else if (attributeName == "\"check\"") {
                                        addCheckedValueToVariable(attributeName2, attributeValue2);
                                    }
                                    lineAttributeValueArray.push(variableReference);
                                    attributeParseMode = "NextSubVar";
                                }
                            } else if (nextBackAccoladeIndex != -1) {
                                attributeValue2 += attributeString.substr(0, attributeString.indexOf("}"));
                                attributeString = attributeString.substr(attributeString.indexOf("}") + 1);
                                var variableReference:VariableReference = new VariableReference(attributeName2, attributeValue2);
                                if (attributeName == "\"set\"") {
                                    addSetValueToVariable(attributeName2, attributeValue2);
                                } else if (attributeName == "\"check\"") {
                                    addCheckedValueToVariable(attributeName2, attributeValue2);
                                }
                                lineAttributeValueArray.push(variableReference);
                                lineAttribute = new LineAttribute(attributeName, lineAttributeValueArray);
                                lineAttributes.push(lineAttribute);
                                attributeParseMode = "NextVar";
                            } else {
                                //we fucked
                                attributeValue2 += attributeString;
                                attributeString = "";
                                var variableReference:VariableReference = new VariableReference(attributeName2, attributeValue2);
                                if (attributeName == "\"set\"") {
                                    addSetValueToVariable(attributeName2, attributeValue2);
                                } else if (attributeName == "\"check\"") {
                                    addCheckedValueToVariable(attributeName2, attributeValue2);
                                }
                                lineAttributeValueArray.push(variableReference);
                                lineAttribute = new LineAttribute(attributeName, lineAttributeValueArray);
                                lineAttributes.push(lineAttribute);
                                attributeParseMode = "NextVar"; //safety measure
                            }
                        }
                    } else {
                        //yeah so now what
                        attributeString = "";
                    }
                } else if (nextSegmentChar == ",") {
                    attributeValue2 = attributeString.substr(0, attributeString.indexOf(","));
                    attributeString = attributeString.substr(attributeString.indexOf(",") + 1);
                    var variableReference:VariableReference = new VariableReference(attributeName2, attributeValue2);
                    if (attributeName == "\"set\"") {
                        addSetValueToVariable(attributeName2, attributeValue2);
                    } else if (attributeName == "\"check\"") {
                        addCheckedValueToVariable(attributeName2, attributeValue2);
                    }
                    lineAttributeValueArray.push(variableReference);
                    attributeParseMode = "NextSubVar";
                } else if (nextSegmentChar == "}") {
                    attributeValue2 = attributeString.substr(0, attributeString.indexOf("}"));
                    attributeString = attributeString.substr(attributeString.indexOf("}") + 1);
                    var variableReference:VariableReference = new VariableReference(attributeName2, attributeValue2);
                    if (attributeName == "\"set\"") {
                        addSetValueToVariable(attributeName2, attributeValue2);
                    } else if (attributeName == "\"check\"") {
                        addCheckedValueToVariable(attributeName2, attributeValue2);
                    }
                    lineAttributeValueArray.push(variableReference);
                    lineAttribute = new LineAttribute(attributeName, lineAttributeValueArray);
                    lineAttributes.push(lineAttribute);
                    attributeParseMode = "NextVar";
                } else {
                    //stop fucking things up, man
                    attributeString = "";
                }
            } else {
                //I've had it.
                var variableReference:VariableReference = new VariableReference(attributeName2, null);
                lineAttributeValueArray.push(variableReference);
                lineAttribute = new LineAttribute(attributeName, lineAttributeValueArray);
                lineAttributes.push(lineAttribute);
                attributeString = "";
            }

        }
    }
    //source:
    //1|
    //2|
    //3|}
    //4|:"true"}
    //5|
    //6|
    //atributeString:
    //1|{"style":"Him"}
    //2|
    //3|
    //4|
    //5|
    //6|
    lineExcess[2] = source;
    line = new Line(lineNumber, lineName, rawLine, lineContent, lineContentString, lineExcess, lineAttributes, lineAttributesString);
    return line;
}

I'm looking for a way to split up my MASSIVE parse function into something more manageable. Improvements on a smaller scale are also welcomed.

\$\endgroup\$
1

1 Answer 1

1
\$\begingroup\$

Duplication

        if (contentString.charAt(0) == "%" && contentString.indexOf("%2A") != 0) {
            contentParseMode = "SpecialChar";
        } else if (contentString.charAt(0) == "[") {
            contentParseMode = "Trigger";
        } else if (contentString.charAt(0) == "*" || contentString.indexOf("%2A") == 0) {
            contentParseMode = "Insertion";
        } else if (contentString.charAt(0) == "Б") {
            contentParseMode = "Delay";
        } else {
            contentParseMode = "Normal";
        }

This, or variations there of, show up 4 different times in your code! I think you'd be best off by making this a separate function. If you could identify several more areas like this, you could eventually reduce the noise and length of the parseLine function, making it easier to come up with a way to split up line parsing in general.

Here's one way to do that:

private function determineNextParseMode(remainingContent:String):String {
    var hasPercentAsteriskAtIndexZero:Boolean = contentString.indexOf("%2A") == 0;
    if (hasPercentAsteriskAtIndexZero) {
        return "Insertion";
    }
    var firstChar:String = contentString.charAt(0);
    if (firstChar == "%") {
        return "SpecialChar";
    } else if (firstChar == "[") {
        return "Trigger";
    } else if (firstChar == "*") {
        return "Insertion";
    } else if (firstChar == "Б") {
        return "Delay";
    }
    return "Normal";
}

I've taken the liberty to move some of the work done to separate variables, rather than redoing it per each case.


        var checkArray:Array = new Array();
        checkArray.push([contentString.indexOf("Б"), "Б"]);
        checkArray.push([contentString.indexOf("%"), "%"]);
        checkArray.push([contentString.indexOf("*"), "*"]);
        checkArray.push([contentString.indexOf("["), "["]);
        checkArray.sortOn([0], [Array.NUMERIC]);
        var nextIssue:int = -1;
        var nextChar:String = "";
        for (var i:uint = 0, isize:uint = checkArray.length; i < isize; i++) {
            if (checkArray[i][0] != -1) {
                nextIssue = checkArray[i][0];
                nextChar = checkArray[i][1];
                i = isize;
            }
        }
        if (nextChar == "Б" || nextChar == "%" || nextChar == "*" || nextChar == "[") {

You check 4 different cases here... but instead of checking if one of them happens to be the case, why not check if it's "not the default"?

That is, you define nextChar to be an empty String, and then set a value to it if you find a special character coming up. So if nextChar is not empty, then it's a special char! And aside from that, don't manipulate i to break a loop. Just use break for that.

        var checkArray:Array = new Array();
        checkArray.push([contentString.indexOf("Б"), "Б"]);
        checkArray.push([contentString.indexOf("%"), "%"]);
        checkArray.push([contentString.indexOf("*"), "*"]);
        checkArray.push([contentString.indexOf("["), "["]);
        checkArray.sortOn([0], [Array.NUMERIC]);
        var nextIssue:int = -1;
        var nextChar:String = "";
        for (var i:uint = 0, isize:uint = checkArray.length; i < isize; i++) {
            if (checkArray[i][0] != -1) {
                nextIssue = checkArray[i][0];
                nextChar = checkArray[i][1];
                break;
            }
        }
        if (nextChar != "") {

A few lines down,

        if (nextChar != "") {

            if (nextChar == "Б") {
                contentParseMode = "Delay";
            } else if (nextChar == "*" || (nextChar == "%" && contentString.indexOf("%2A") == nextIssue)) {
                contentParseMode = "Insertion";
            } else if (nextChar == "%") {
                //trace(contentString.indexOf("%2A")); //commented because it was just extra noise for me as dev and I don't know why I had put this in (v2.13)
                contentParseMode = "SpecialChar";
            } else if (nextChar == "[") {
                contentParseMode = "Trigger";
            }
            currentContentSegment = contentString.substr(0, nextIssue);
            lineContent.push(currentContentSegment);
            currentContentSegment = "";
            contentString = contentString.substr(nextIssue);
        } else {
            currentContentSegment = contentString;
            lineContent.push(currentContentSegment);
            currentContentSegment = "";
            contentString = "";
        }

well, I've explained the duplication in nextChar already, after that you've got even more duplication!

            currentContentSegment = contentString.substr(0, nextIssue);
            lineContent.push(currentContentSegment);
            currentContentSegment = "";
            contentString = contentString.substr(nextIssue);
        } else {
            currentContentSegment = contentString;
            lineContent.push(currentContentSegment);
            currentContentSegment = "";
            contentString = "";
        }

If you can change nextIssue to point to the end of the string by default, then you don't even need to deal with this statement!

Making this block as small as the following:

        var checkArray:Array = new Array();
        checkArray.push([contentString.indexOf("Б"), "Б"]);
        checkArray.push([contentString.indexOf("%"), "%"]);
        checkArray.push([contentString.indexOf("*"), "*"]);
        checkArray.push([contentString.indexOf("["), "["]);
        checkArray.sortOn([0], [Array.NUMERIC]);
        var nextIssue:int = contentString.length;
        var nextChar:String = "";
        for (var i:uint = 0, isize:uint = checkArray.length; i < isize; i++) {
            if (checkArray[i][0] != -1) {
                nextIssue = checkArray[i][0];
                nextChar = checkArray[i][1];
                break;
            }
        }

        if (nextChar != "") {
            contentParseMode = determineNextParseMode(contentString);
        }
        lineContent.push(contentString.substr(0, nextIssue));
        contentString = contentString.substr(nextIssue);

Performance

        while (contentString.length > 0 && contentString.indexOf("Б") == 0) {
            currentContentSegment += "Б";
            contentString = contentString.substr(1);
        }

This isn't the smartest move. Constantly rearranging strings, searching for delay characters...

Better would be to have a loop like this:

var delayCharacters:uint = 0;
while(contentString.length > delayCharacters && contentString.charAt(delayCharacters) == "Б"){ 
    delayCharacters++;
}

and then substring the appropriate amount.

currentContentSegment = contentString.substring(0, delayCharacters);
contentString = contentString.substr(delayCharacters);

Given that directly afterwards, you push and clear:

lineContent.push(currentContentSegment);
currentContentSegment = "";

You can skip the currentContentSegment variable.

var delayCharacters:uint = 0;
while(contentString.length > delayCharacters && contentString.charAt(delayCharacters) == "Б"){ 
    delayCharacters++;
}
lineContent.push(contentString.substring(0, delayCharacters));
contentString = contentString.substr(delayCharacters);

A bit later you do the same thing:

while (contentString.length > 0 && contentString.indexOf(specialCharPattern) == 0) {
    currentContentSegment += specialCharPattern;
    contentString = contentString.substr(specialCharPattern.length);
}

Again, here you can rewrite the loop:

var occurances:uint = 0;
while (contentString.length > ((occurances*3) + 2) && contentString.substring(occurances*3, (occurances*3)+2) == specialCharPattern) {
    occurances++;
}
currentContentSegment += contentString.substring(0, (occurances*3)+2);

It might even be better to make occurances go up by 3 at a time, but this seems like a dirty hack, and in a long function like this, it's probably better to focus on readability where you can.


    } else if (contentParseMode == "Trigger") {
        //Find trigger and identify trigger if possible...?
        //TODO: Identify variable insertions within triggers
        if (contentString.indexOf("[") != 0) {
            ErrorManager.showDevelopmentError("Line parsing - trigger doesn't start with [");
            contentString = "";
            break;
        }

According to your comments, "Triggers" must start with the [ character. However, due to the way the code will look after you implement determineNextParseMode, this is nothing more than an assertion. Personally, I feel you should remove it.


        if (contentString.indexOf("]") != -1) {
            trigger = contentString.substr(1, contentString.indexOf("]") - 1);
            contentString = contentString.substr(contentString.indexOf("]") + 1);

Try to reuse the result of indexOf here. You call it three times in a row and that can't be good for performance.

    } else if (contentParseMode == "Insertion") {
        //Find insertion and determine type.
        if (contentString.indexOf("*") != 0 && contentString.indexOf("%2A") != 0) {
            ErrorManager.showDevelopmentError("Line parsing - insertion doesn't start with *");
            contentString = "";
            break;
        }

Another assertion. This can be removed.

Further shortening

We can always go further.

Right now we determine the next contentParseMode at the end of the line. That's a bit sloppy, because this means we can start with a empty normal segment... which doesn't make any sense.

Better would be to put that bit of parsing at the start. Sure, it does cause a double parse for normal, but it saves us some duplication. Maybe we can clean up normal later.

        contentParseMode = determineNextParseMode(contentString);
        if (contentParseMode == "Normal") {
            //The problem: 4 functions give 4 values. Which function gives the lowest positive value?
            var checkArray:Array = new Array();
            checkArray.push([contentString.indexOf("Б"), "Б"]);
            checkArray.push([contentString.indexOf("%"), "%"]);
            checkArray.push([contentString.indexOf("*"), "*"]);
            checkArray.push([contentString.indexOf("["), "["]);
            checkArray.sortOn([0], [Array.NUMERIC]);
            var nextIssue:int = contentString.length;
            var nextChar:String = "";
            for (var i:uint = 0, isize:uint = checkArray.length; i < isize; i++) {
                if (checkArray[i][0] != -1) {
                    nextIssue = checkArray[i][0];
                    nextChar = checkArray[i][1];
                    break;
                }
            }

            if (nextChar != "") {

            }
            lineContent.push(contentString.substr(0, nextIssue));
            contentString = contentString.substr(nextIssue);
        }

Hang on, if we don't have to determine WHAT comes next, then we only want to know how long the normal text section of the string is. This vastly simplifies the code!

            var checkArray = new Array();
            checkArray.push(contentString.indexOf("Б"));
            checkArray.push(contentString.indexOf("%"));
            checkArray.push(contentString.indexOf("*"));
            checkArray.push(contentString.indexOf("["));
            var nextIssue:int = contentString.length;
            for (var i:uint = 0, isize:uint = checkArray.length; i < isize; i++) {
                if (checkArray[i] != -1 && nextIssue > checkArray[i]) {
                    nextIssue = checkArray[i];
                }
            }
            lineContent.push(contentString.substr(0, nextIssue));
            contentString = contentString.substr(nextIssue);

Final code

I can't fit the whole thing in here, obviously, but here's the helper function, and the contentString segment:

private function determineNextParseMode(remainingContent:String):String {
    var hasPercentAsteriskAtIndexZero:Boolean = contentString.indexOf("%2A") == 0;
    if (hasPercentAsteriskAtIndexZero) {
        return "Insertion";
    }
    var firstChar:String = contentString.charAt(0);
    if (firstChar == "%") {
        return "SpecialChar";
    } else if (firstChar == "[") {
        return "Trigger";
    } else if (firstChar == "*") {
        return "Insertion";
    } else if (firstChar == "Б") {
        return "Delay";
    }
    return "Normal";
}

contentString

    //Parse contentString
    //When parsing a contentString, you're treating everything as normal string until...
    //Б - change to delayString (new normal string, keep until different character)
    //% - change to special char (new normal string, parse next 2 chars, keep that as pattern, then parse until pattern is broken)
    //* - change to VariableInsertion/VariableSubstitution (Treat as Insertion until YOU/ME/YOUR/MY/FINISHES is detected)
    //[ - change to Trigger
    var contentParseMode:String = "Normal"; //Remember kids, Stringly-typed code is bad! What I'm doing here is bad! So don't do it! (Even although it makes things easier sometimes...)
    var currentContentSegment:String = "";
    while (contentString != "") {
        contentParseMode = determineNextParseMode(contentString);
        if (contentParseMode == "Normal") {
            //The problem: 4 functions give 4 values. Which function gives the lowest positive value?
            var checkArray = new Array();
            checkArray.push(contentString.indexOf("Б"));
            checkArray.push(contentString.indexOf("%"));
            checkArray.push(contentString.indexOf("*"));
            checkArray.push(contentString.indexOf("["));
            var nextIssue:int = contentString.length;
            for (var i:uint = 0, isize:uint = checkArray.length; i < isize; i++) {
                if (checkArray[i] != -1 && nextIssue > checkArray[i]) {
                    nextIssue = checkArray[i];
                }
            }
            lineContent.push(contentString.substr(0, nextIssue));
            contentString = contentString.substr(nextIssue);
        } else if (contentParseMode == "Delay") {
            //Add all continuous delay characters as contentpiece
            var delayCharacters:uint = 0;
            while(contentString.length > delayCharacters && contentString.charAt(delayCharacters) == "Б"){ 
                delayCharacters++;
            }
            lineContent.push(contentString.substring(0, delayCharacters));
            contentString = contentString.substr(delayCharacters);
        } else if (contentParseMode == "SpecialChar") {
            //Identify special char type, loop until pattern is not next section of string. Add all looped chars to content array, strip from content string. Evaluate next section.
            var specialCharPattern:String = contentString.substr(0, 3);
            //check if it's actually a percent encoded value here
            var hexString:String = "0123456789ABCDEFabcdef";
            if (!(specialCharPattern.length == 3 && hexString.indexOf(specialCharPattern.charAt(1)) != -1 && hexString.indexOf(specialCharPattern.charAt(2)) != -1)) {
                //it's not a proper percent encoded value.
                //treat the % as %25, and strip only 1 character.

                currentContentSegment = specialCharPattern;
                contentString = contentString.substring(1); //strip 1 char
            } else {
                var occurances:uint = 0;
                while (contentString.length > ((occurances*3) + 2) && contentString.substring(occurances*3, (occurances*3)+2) == specialCharPattern) {
                    occurances++;
                }
                currentContentSegment = contentString.substring(0, (occurances*3)+2);
                contentString = contentString.substr(occurances * 3);
            }
            lineContent.push(currentContentSegment);
        } else if (contentParseMode == "Trigger") {
            //TODO: Identify variable insertions within triggers
            var trigger:String = "";
            var rightBracketIndex:int = contentString.indexOf("]");
            if (rightBracketIndex != -1) {
                trigger = contentString.substr(1, rightBracketIndex - 1);
                contentString = contentString.substr(rightBracketIndex + 1);
            } else {
                //user error, so ignore it. Act like rest of line is trigger.
                trigger = contentString.substr(1);
                contentString = "";
            }
            var type:TriggerType = TriggerType.identifyType(trigger);
            var triggerObject:Trigger = new Trigger(trigger, type);
            lineContent.push(triggerObject);
        } else if (contentParseMode == "Insertion") {
            //Find insertion and determine type.
            var insertion:String = "";
            var indexOfAsterisk:int = contentString.indexOf("*");
            var indexOfPercent2A:int = contentString.indexOf("%2A");
            var indexOfSecondAsterisk:int = contentString.indexOf("*", 1);
            var indexOfSecondPercent2A:int = contentString.indexOf("%2A", 1);
            if (Math.max(indexOfSecondAsterisk, indexOfSecondPercent2A) != -1) {
                var insertionIndex:int = indexOfAsterisk;
                var insertionEndIndex:int = indexOfSecondAsterisk;
                var insertionStartLength:uint = 1;
                var insertionEndLength:uint = 1;
                if (indexOfAsterisk == -1 || (indexOfPercent2A < indexOfAsterisk && indexOfPercent2A != -1)) {
                    insertionIndex = indexOfPercent2A;
                    insertionStartLength = 3;
                }
                if (indexOfSecondAsterisk == -1 || (indexOfSecondPercent2A < indexOfSecondAsterisk && indexOfSecondPercent2A != -1)) {
                    insertionEndIndex = indexOfSecondPercent2A;
                    insertionEndLength = 3;
                }
                insertion = contentString.substr(insertionStartLength, insertionEndIndex - insertionStartLength);
                contentString = contentString.substr(insertionEndIndex + insertionEndLength);
            } else {
                //user error, so ignore it. Act like rest of line is insertion.
                insertion = contentString.substr(1);
                contentString = "";
            }
            var insertionObject:LineElement = null;
            if (insertion.indexOf("YOU") != -1 || insertion.indexOf("ME") != -1 || insertion.indexOf("MY") != -1 || insertion.indexOf("FINISHES") != -1) { //There's also YOUR, but that's already covered by checking for YOU.
                insertionObject = new VariableSubstitution(insertion);
            } else {
                var variable:Variable = getVariable("\"" + insertion + "\"");
                if (variable == null) {
                    variable = new Variable(VariableType.getEnum("UNKNOWN"), "\"" + insertion + "\"");
                    addVariable(variable);
                }
                variable.setInsertedInDialogue(true);
                insertionObject = new VariableInsertion(variable, insertion);
            }
            lineContent.push(insertionObject);
        } else {
            contentString = "";
            ErrorManager.showDevelopmentError("Line parsing infinite loop");
        }
    }
\$\endgroup\$
3
  • \$\begingroup\$ Why not var char = contentString.charAt(0); switch (char) { case "%" ...; and so on instead. \$\endgroup\$
    – ferada
    Commented Feb 16, 2015 at 12:45
  • \$\begingroup\$ @ferada I thought it wasn't possible to use a switch since I had various boolean conditions in there, but I moved them around and now it would be possible, I guess... \$\endgroup\$
    – Pimgd
    Commented Feb 16, 2015 at 13:19
  • \$\begingroup\$ (I can safely say that I should remove the assertions because they've been inserted when I just built the parser, and in 6 months of time they've never tripped once...) \$\endgroup\$
    – Pimgd
    Commented Feb 16, 2015 at 16:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.