<commit_before>define([
    'require',
    'underscore',
    'xpath',
    'vellum/tree',
    'vellum/logic',
    'vellum/widgets',
    'vellum/util'
], function (
    require,
    _,
    xpath,
    Tree,
    logic,
    widgets,
    util
) {
    // Load these dependencies in the background after all other run-time
    // dependencies have been resolved, since they shouldn't be necessary
    // initially.
    var writer,
        exporter;
    require(['vellum/writer', 'vellum/exporter'], function (w, e) {
        writer = w;
        exporter = e;
    });

    var FormError = function (options) {
        var that = {};
        that.message = options.message;
        // the key is how uniqueness is determined
        that.key = options.key; 
        that.level = options.level || "form-warning";
        that.options = options;
        
        that.isMatch = function (other) {
            if (this.key && other.key) {
                return this.key === other.key;
            }
            return false;
        };
        
        return that;
    };
    
    function processInstance(instance) {
        instance.defaultId = instance.defaultId || convertToId(instance.sourceUri);
        instance.id = instance.defaultId;
        _.each(instance.levels, function (level) {
            var i = 1,
                mappedSubsets = {};
            _.each(level.subsets, function (subset) {
                subset.id = i++;
                subset.selector = normalizeXPathExpr(subset.selector);
                mappedSubsets[subset.id] = subset;
            });
            level.subsets = mappedSubsets;
        });
        return instance;
    }
    
    // Parsing the instance selectors using the XPath models and comparing the
    // parsed expressions might be a better approach that these hacky functions.
    function normalizeXPathExpr(str) {
        return normalizeToSingleQuotes(removeSpaces(str));
    }

    // remove spaces around = and []
    function removeSpaces(str) {
        return str.replace(/\s*([=\[\]])\s*/g, function (match, p1) {
            return p1;
        });
    }

    // Change any top-level double quotes to single quotes. (Assumes no
    // top-level escaped double quotes).  This may not correctly handle escaped
    // quotes within a quote.  Moving on.
    function normalizeToSingleQuotes(str) {
        var ret = '';
        eachCharByQuotedStatus(str,
            function (c) {
                ret += c;
            },
            function (c) {
                if (c === '"') {
                    c = "'";
                }
                ret += c;
            });
        return ret;
    }

    // abstracted this because I was using it for two things before
    function eachCharByQuotedStatus(str, quoted, unquoted) {
        var prevIsBackslash = false,
            inSingleQuote = false,
            inDoubleQuote = false;

        for (var i=0, l=str.length; i < l; i++) {
            var c = str[i],
                inQuote = inSingleQuote || inDoubleQuote;
          
            if (!prevIsBackslash && ((inSingleQuote && c === "'") ||
                                     (inDoubleQuote && c === '"'))) {
                inQuote = false;
            }
            (inQuote ? quoted : unquoted)(c);

            if (!prevIsBackslash) {
                if (c === "'" && !inDoubleQuote) {
                    inSingleQuote = !inSingleQuote;
                } else if (c === '"' && !inSingleQuote) {
                    inDoubleQuote = !inDoubleQuote;
                }
            }

            if (c === '\\') {
                prevIsBackslash = !prevIsBackslash;
            } else {
                prevIsBackslash = false;
            }
        }
    }
    
    function convertToId(str) {
        return str
            .toLowerCase()
            .replace(/ /g,'_')
            .replace(/[^\w-]+/g,'');
    }
    
    var InstanceMetadata = function (attributes, children, ref) {
        var that = {};
        that.attributes = attributes;
        that.children = children || [];
        that.refs = {};
        if (ref !== null && !_.isUndefined(ref)) { that.refs[ref] = null; }
        return that;
    };

    function Form (opts, vellum, mugTypes) {
        var _this = this;

        this.setValues = [];
        this._setValueId = 1;

        this._logicManager = new logic.LogicManager(this, {
                allowedDataNodeReferences: opts.allowedDataNodeReferences
            });

        // Some things in the form and mug methods depend on the UI.  These
        // should eventually be factored out.
        this.vellum = vellum;
        this.mugTypes = mugTypes;

        this.formName = 'New Form';
        this.mugMap = {};
        this.dataTree = new Tree('data', 'data');
        this.dataTree.on('change', function (e) {
            _this.fireChange(e.mug);
        });
        this.controlTree = new Tree('data', 'control');
        this.controlTree.on('change', function (e) {
            _this.fireChange(e.mug);
        });
        this.instanceMetadata = [InstanceMetadata({})];
        this.enableInstanceRefCounting = opts.enableInstanceRefCounting;
        this.errors = [];
        
        this.externalInstances = _.indexBy(
            _.map(opts.externalInstances || [], processInstance), 'id');
        this.question_counter = 1;
        
        //make the object event aware
        util.eventuality(this);
    }

    Form.prototype = {
        getBasePath: function () {
            // the choice to use dataTree instead of controlTree is arbitrary
            return "/" + this.dataTree.getRootNode().getID() + "/";
        },
        fireChange: function (mug) {
            this.fire({
                type: 'change',
                mug: mug
            });
        },
        addInstance: function (instance) {
            // NOTE this does something very different from addInstanceIfNotExists
            this.externalInstances[instance.id] = instance;
        },
        /**
         * Add an instance if it is not already on the form
         *
         * @param attrs - Instance attributes. The `src` attribute is used to
         *          match other instances on the form, and is required. The
         *          instance will be unconditionally added and it's id returned
         *          if the instance has no `src` attribute.
         * @param ref - Optional, a unique key of the thing that references
         *          the instance. If provided, this will add a reference to
         *          the added or existing instance unless ref counting is
         *          disabled for the form or instance.
         * @returns - The `id` of the added or already-existing instance.
         */
        addInstanceIfNotExists: function (attrs, ref) {
            if (attrs.src === null || _.isUndefined(attrs.src)) {
                // no ref counting for instances without a `src` since they cannot be dropped
                this.instanceMetadata.push(InstanceMetadata({
                    src: attrs.src,
                    id: attrs.id
                }));
                return attrs.id;
            }

            var meta = _.find(this.instanceMetadata, function (m) {
                return m.attributes.src === attrs.src;
            });
            if (!meta) {
                this.instanceMetadata.push(InstanceMetadata({
                    src: attrs.src,
                    id: attrs.id
                }, null, this.enableInstanceRefCounting ? ref : null));
                return attrs.id;
            } else if (meta.refs && this.enableInstanceRefCounting) {
                if (_.isUndefined(ref)) {
                    // disable ref counting for this instance. how unfortunate
                    meta.refs = null;
                } else {
                    meta.refs[ref] = null;
                }
            }
            return meta.attributes.id;
        },
        /**
         * Add a reference to the instance with matching id
         *
         * @returns - The InstanceMetadata if found, otherwise null. Note
         *            that this will always return the found instance, even if
         *            ref counting is disabled.
         */
        referenceInstance: function (id, ref) {
            var meta = _.find(this.instanceMetadata, function (meta) {
                    return meta.attributes.id;
                });
            if (meta && meta.refs && this.enableInstanceRefCounting) {
                meta.refs[ref] = null;
            }
            return meta || null;
        },
        /**
         * Drop instance reference, possibly removing the instance
         *
         * @param src - The instance `src` attribute value.
         * @param ref - The unique key of the thing that no longer references<commit_msg>Fix bug in Form.referenceInstance<commit_after>define([
    'require',
    'underscore',
    'xpath',
    'vellum/tree',
    'vellum/logic',
    'vellum/widgets',
    'vellum/util'
], function (
    require,
    _,
    xpath,
    Tree,
    logic,
    widgets,
    util
) {
    // Load these dependencies in the background after all other run-time
    // dependencies have been resolved, since they shouldn't be necessary
    // initially.
    var writer,
        exporter;
    require(['vellum/writer', 'vellum/exporter'], function (w, e) {
        writer = w;
        exporter = e;
    });

    var FormError = function (options) {
        var that = {};
        that.message = options.message;
        // the key is how uniqueness is determined
        that.key = options.key; 
        that.level = options.level || "form-warning";
        that.options = options;
        
        that.isMatch = function (other) {
            if (this.key && other.key) {
                return this.key === other.key;
            }
            return false;
        };
        
        return that;
    };
    
    function processInstance(instance) {
        instance.defaultId = instance.defaultId || convertToId(instance.sourceUri);
        instance.id = instance.defaultId;
        _.each(instance.levels, function (level) {
            var i = 1,
                mappedSubsets = {};
            _.each(level.subsets, function (subset) {
                subset.id = i++;
                subset.selector = normalizeXPathExpr(subset.selector);
                mappedSubsets[subset.id] = subset;
            });
            level.subsets = mappedSubsets;
        });
        return instance;
    }
    
    // Parsing the instance selectors using the XPath models and comparing the
    // parsed expressions might be a better approach that these hacky functions.
    function normalizeXPathExpr(str) {
        return normalizeToSingleQuotes(removeSpaces(str));
    }

    // remove spaces around = and []
    function removeSpaces(str) {
        return str.replace(/\s*([=\[\]])\s*/g, function (match, p1) {
            return p1;
        });
    }

    // Change any top-level double quotes to single quotes. (Assumes no
    // top-level escaped double quotes).  This may not correctly handle escaped
    // quotes within a quote.  Moving on.
    function normalizeToSingleQuotes(str) {
        var ret = '';
        eachCharByQuotedStatus(str,
            function (c) {
                ret += c;
            },
            function (c) {
                if (c === '"') {
                    c = "'";
                }
                ret += c;
            });
        return ret;
    }

    // abstracted this because I was using it for two things before
    function eachCharByQuotedStatus(str, quoted, unquoted) {
        var prevIsBackslash = false,
            inSingleQuote = false,
            inDoubleQuote = false;

        for (var i=0, l=str.length; i < l; i++) {
            var c = str[i],
                inQuote = inSingleQuote || inDoubleQuote;
          
            if (!prevIsBackslash && ((inSingleQuote && c === "'") ||
                                     (inDoubleQuote && c === '"'))) {
                inQuote = false;
            }
            (inQuote ? quoted : unquoted)(c);

            if (!prevIsBackslash) {
                if (c === "'" && !inDoubleQuote) {
                    inSingleQuote = !inSingleQuote;
                } else if (c === '"' && !inSingleQuote) {
                    inDoubleQuote = !inDoubleQuote;
                }
            }

            if (c === '\\') {
                prevIsBackslash = !prevIsBackslash;
            } else {
                prevIsBackslash = false;
            }
        }
    }
    
    function convertToId(str) {
        return str
            .toLowerCase()
            .replace(/ /g,'_')
            .replace(/[^\w-]+/g,'');
    }
    
    var InstanceMetadata = function (attributes, children, ref) {
        var that = {};
        that.attributes = attributes;
        that.children = children || [];
        that.refs = {};
        if (ref !== null && !_.isUndefined(ref)) { that.refs[ref] = null; }
        return that;
    };

    function Form (opts, vellum, mugTypes) {
        var _this = this;

        this.setValues = [];
        this._setValueId = 1;

        this._logicManager = new logic.LogicManager(this, {
                allowedDataNodeReferences: opts.allowedDataNodeReferences
            });

        // Some things in the form and mug methods depend on the UI.  These
        // should eventually be factored out.
        this.vellum = vellum;
        this.mugTypes = mugTypes;

        this.formName = 'New Form';
        this.mugMap = {};
        this.dataTree = new Tree('data', 'data');
        this.dataTree.on('change', function (e) {
            _this.fireChange(e.mug);
        });
        this.controlTree = new Tree('data', 'control');
        this.controlTree.on('change', function (e) {
            _this.fireChange(e.mug);
        });
        this.instanceMetadata = [InstanceMetadata({})];
        this.enableInstanceRefCounting = opts.enableInstanceRefCounting;
        this.errors = [];
        
        this.externalInstances = _.indexBy(
            _.map(opts.externalInstances || [], processInstance), 'id');
        this.question_counter = 1;
        
        //make the object event aware
        util.eventuality(this);
    }

    Form.prototype = {
        getBasePath: function () {
            // the choice to use dataTree instead of controlTree is arbitrary
            return "/" + this.dataTree.getRootNode().getID() + "/";
        },
        fireChange: function (mug) {
            this.fire({
                type: 'change',
                mug: mug
            });
        },
        addInstance: function (instance) {
            // NOTE this does something very different from addInstanceIfNotExists
            this.externalInstances[instance.id] = instance;
        },
        /**
         * Add an instance if it is not already on the form
         *
         * @param attrs - Instance attributes. The `src` attribute is used to
         *          match other instances on the form, and is required. The
         *          instance will be unconditionally added and it's id returned
         *          if the instance has no `src` attribute.
         * @param ref - Optional, a unique key of the thing that references
         *          the instance. If provided, this will add a reference to
         *          the added or existing instance unless ref counting is
         *          disabled for the form or instance.
         * @returns - The `id` of the added or already-existing instance.
         */
        addInstanceIfNotExists: function (attrs, ref) {
            if (attrs.src === null || _.isUndefined(attrs.src)) {
                // no ref counting for instances without a `src` since they cannot be dropped
                this.instanceMetadata.push(InstanceMetadata({
                    src: attrs.src,
                    id: attrs.id
                }));
                return attrs.id;
            }

            var meta = _.find(this.instanceMetadata, function (m) {
                return m.attributes.src === attrs.src;
            });
            if (!meta) {
                this.instanceMetadata.push(InstanceMetadata({
                    src: attrs.src,
                    id: attrs.id
                }, null, this.enableInstanceRefCounting ? ref : null));
                return attrs.id;
            } else if (meta.refs && this.enableInstanceRefCounting) {
                if (_.isUndefined(ref)) {
                    // disable ref counting for this instance. how unfortunate
                    meta.refs = null;
                } else {
                    meta.refs[ref] = null;
                }
            }
            return meta.attributes.id;
        },
        /**
         * Add a reference to the instance with matching id
         *
         * @returns - The InstanceMetadata if found, otherwise null. Note
         *            that this will always return the found instance, even if
         *            ref counting is disabled.
         */
        referenceInstance: function (id, ref) {
            var meta = _.find(this.instanceMetadata, function (meta) {
                    return meta.attributes.id === id;
                });
            if (meta && meta.refs && this.enableInstanceRefCounting) {
                meta.refs[ref] = null;
            }
            return meta || null;
        },
        /**
         * Drop instance reference, possibly removing the instance
         *
         * @param src - The instance `src` attribute value.
         * @param ref - The unique key of the thing that no longer references
{'fraction_non_alphanumeric': 0.10627800147450803, 'fraction_numerical': 0.0006805421652583225, 'mean_word_length': 1.4945536851039751, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3974438', 'n_tokens_mistral': 4669, 'n_tokens_neox': 4480, 'n_words': 1281}