<commit_before>import sax from 'sax';

class Context {
  constructor() {
    this.stack = [];
    this.namespace = {};
  }
  push(operator, data) {
    let stackFrame = { operator, parent: this.stack[this.stack.length - 1] };
    this.stack.push(stackFrame);
    if (operator.push != null) operator.push.call(this, data, stackFrame);
  }
  pop(data) {
    if (this.stack.length === 0) throw new Error('Unable to pop frame');
    let stackFrame = this.stack.pop();
    let popOp = stackFrame.operator.pop;
    let result = popOp && popOp.call(this, data, stackFrame);
    if (this.stack.length > 0) {
      let parentFrame = this.stack[this.stack.length - 1];
      let popChild = parentFrame.operator.popChild;
      popChild && popChild.call(this, result, parentFrame, stackFrame);
    }
  }
  get() {
    return this.stack[this.stack.length - 1];
  }
  getDelegator(getOp) {
    return (v) => {
      let frame = this.get();
      let func = getOp(frame.operator);
      if (func != null) func.call(this, v, frame);
    };
  }
}

const NOOP = {
  opentag(node) {
    this.push(NOOP, node);
  },
  closetag() {
    this.pop();
  },
};

function resolveSchema(schema) {
  if (typeof schema === 'string') return SCHEMA[schema];
  if (typeof schema === 'function') return schema();
  return schema;
}

function cached(schema, manipulator) {
  // We use cache to make hoisting available.... what?
  let cached = null;
  return () => {
    if (cached !== null) return cached;
    cached = manipulator(resolveSchema(schema));
    return cached;
  };
}

const rename =
  (target, schema) => cached(schema, v => v && Object.assign({ target }, v));
const multiple =
  (schema) => cached(schema, v => v && Object.assign({
    // merge is *always* executed by hierarchy, even though no prev object
    // is available.
    merge: (prev = [], current) => (prev.push(current), prev)
  }, v));
const multipleMap =
  (schema, getKey) => cached(schema, v => v && Object.assign({
    // merge is *always* executed by hierarchy, even though no prev object
    // is available.
    merge: (prev = {}, current, frame) => {
      prev[getKey(current, frame)] = current;
      return prev;
    }
  }, v));
const registerNamespace = {
  push(node, frame) {
    Object.assign(frame.data, node.attributes);
    const { id } = node.attributes;
    if (id != null) {
      frame.id = id;
    }
    frame.namespace = {};
  },
  pop(data, frame) {
    data.namespace = frame.namespace;
    if (frame.id != null) this.namespace[frame.id] = data;
    return data;
  }
};

const registerSid = {
  push(node, frame) {
    Object.assign(frame.data, node.attributes);
    const { id, sid } = node.attributes;
    if (sid != null) {
      let parent = frame.parent;
      while (parent != null && parent.namespace == null) {
        parent = node.parent;
      }
      if (parent != null) {
        frame.namespaceParent = parent;
        frame.sid = sid;
      }
    } else {
      throw new Error('sid is required but was not specified');
    }
    if (id != null) {
      frame.id = id;
    }
  },
  pop(data, frame) {
    if (frame.namespaceParent != null) {
      frame.namespaceParent.namespace[frame.sid] = data;
    }
    if (frame.id != null) this.namespace[frame.id] = data;
    return data;
  }
};

function hoist(children, triggers) {
  let onPush, onPop;
  if (typeof triggers === 'function') onPush = triggers;
  else if (triggers != null) {
    onPush = triggers.push;
    onPop = triggers.pop;
  }
  return {
    push(node, frame) {
      frame.data = {};
      if (onPush != null) onPush.call(this, node, frame);
    },
    opentag(node) {
      let child = children[node.name];
      // Ignore if node name doesn't match
      if (child == null) return this.push(NOOP, node);
      let schema = resolveSchema(child);
      // TODO Remove this
      if (schema == null) return this.push(NOOP, node);
      this.push(schema, node);
    },
    closetag() {
      // TODO This should be default operation if not specified
      this.pop();
    },
    pop(data, frame) {
      if (onPop != null) return onPop.call(this, frame.data, frame);
      return frame.data;
    },
    popChild(data, frame) {
      let result = data;
      frame.data = result;
    }
  };
}

function hierarchy(children, triggers) {
  let onPush, onPop;
  if (typeof triggers === 'function') onPush = triggers;
  else if (triggers != null) {
    onPush = triggers.push;
    onPop = triggers.pop;
  }
  return {
    push(node, frame) {
      // TODO attributes
      frame.data = {};
      if (onPush != null) onPush.call(this, node, frame);
    },
    opentag(node, frame) {
      let child = children[node.name];
      frame.target = null;
      // Ignore if node name doesn't match
      if (child == null) return this.push(NOOP, node);
      let schema = resolveSchema(child);
      // TODO Remove this
      if (schema == null) return this.push(NOOP, node);
      frame.target = schema.target || node.name;
      frame.targetSchema = schema;
      this.push(schema, node);
    },
    closetag() {
      // TODO This should be default operation if not specified
      this.pop();
    },
    pop(data, frame) {
      console.log(frame.data);
      if (onPop != null) return onPop.call(this, frame.data, frame);
      return frame.data;
    },
    popChild(data, frame, childFrame) {
      let prev = frame.data[frame.target];
      let result = data;
      if (frame.target == null) return;
      if (frame.targetSchema && frame.targetSchema.merge != null) {
        result = frame.targetSchema.merge(prev, result, childFrame);
      }
      frame.data[frame.target] = result;
    }
  };
}

function library(nodeName, schema) {
  return {
    push(node, frame) {
      frame.data = [];
    },
    opentag(node) {
      // TODO Maybe we should handle this?
      if (nodeName !== node.name) return this.push(NOOP, node);
      let schemaResolved = resolveSchema(schema);
      if (schemaResolved == null) return this.push(NOOP, node);
      this.push(schemaResolved, node);
    },
    closetag() {
      // TODO This should be default operation if not specified
      this.pop();
    },
    pop(data, frame) {
      return frame.data;
    },
    popChild(data, frame) {
      if (data == null) return;
      frame.data.push(data);
    },
    merge(prev = [], current) {
      return prev.concat(current);
    }
  };
}

function attributes(proc) {
  return {
    push(node, frame) {
      if (proc != null) frame.data = proc(node);
      else frame.data = node.attributes;
    },
    opentag(node) {
      return this.push(NOOP, node);
    },
    closetag() {
      // TODO This should be default operation if not specified
      this.pop();
    },
    pop(data, frame) {
      return frame.data;
    },
    merge(prev = {}, current) {
      if (proc != null) return current;
      return Object.assign(prev, current);
    }
  };
}

function textValue(proc) {
  return {
    push(data, frame) {
      frame.value = null;
    },
    opentag(node) {
      return this.push(NOOP, node);
    },
    closetag() {
      // TODO This should be default operation if not specified
      this.pop();
    },
    text(data, frame) {
      let value = data.trim();
      if (value === '') return;
      frame.value = value;
    },
    pop(data, frame) {
      return proc(frame.value);
    }
  };
}

const MATERIAL_STRUCTURE = {
  emission: 'colorOrTexture',
  ambient: 'colorOrTexture',
  diffuse: 'colorOrTexture',
  specular: 'colorOrTexture',
  shininess: 'floatOrParam',
  reflective: 'colorOrTexture',
  reflectivity: 'floatOrParam',
  transparent: 'colorOrTexture',
  transparency: 'floatOrParam',
  index_of_refraction: rename('refraction', 'floatOrParam')
};

const CORE_PARAM_TYPE = {
  bool: 'boolean',
  bool2: 'booleanArray',
  bool3: 'booleanArray',
  bool4: 'booleanArray',
  int: 'int',
  int2: 'intArray',
  int3: 'intArray',
  int4: 'intArray',
  float: 'float',
  float2: 'floatArray',
  float3: 'floatArray',
  float4: 'floatArray',
  surface: 'surface',
  sampler2D: 'sampler',
  samplerCUBE: 'sampler'
};

for (let i = 1; i <= 4; ++i) {
  for (let j = 1; j <= 4; ++j) {
    CORE_PARAM_TYPE[`float${i}x${j}`] = 'floatArray';
  }
}

const SCHEMA = {
  noop: NOOP,
  attributes: attributes(),
  boolean: textValue(v => v === 'true'),
  booleanArray: textValue(v => v.split(/\s+/).map(v => v === 'true')),
  string: textValue(v => v),
  // Should be the date parsed?
  date: textValue(v => v),
  stringArray: textValue(v => v.split(/\s+/)),
  float: textValue(v => parseFloat(v)),
  floatArray: textValue(v => new Float32Array(v.split(/\s+/).map(parseFloat))),
  int: textValue(v => parseFloat(v)),
  intArray: textValue(v => new Int32Array(v.split(/\s+/).map(parseFloat))),
  COLLADA: hierarchy({
    asset: 'asset',
    library_animations: rename('animations', library('animation', 'animation')),
    library_animation_clips: rename('animationClips', library('animation_clip',
      'animationClip')),
    library_cameras: rename('cameras', library('camera', 'camera')),
    library_controllers: rename('controllers', library('controller',
      'controller')),
    library_geometries: rename('geometries', library('geometry', 'geometry')),
    library_lights: rename('lights', library('light', 'light')),
    library_nodes: rename('nodes', library('node', 'node')),
    library_visual_scenes: rename('visualScenes', library('visual_scene',
      'visualScene')),
    // COLLADA FX
    library_images: rename('images', library('image', 'image')),
    library_effects: rename('effects', library('effect', 'effect')),
    library_materials: rename('materials', library('material', 'material'))
  }, ({ attributes }) => {
    // Check version
    if (attributes.version.slice(0, 3) !== '1.4') {
      throw new Error('COLLADA parser only supports 1.4.x format');
    }
  }),
  asset: hierarchy({
    contributor: multiple('contributor'),
    created: 'date',
    modified: 'date',
    keywords: 'stringArray',
    revision: 'string',
    subject: 'string',
    title: 'string',
    unit: 'attributes',
    up_axis: rename('upAxis', 'string')
  }),
  contributor: hierarchy({
    author: 'string',
    authoring_tool: 'string',
    comments: 'string',
    copyright: 'string',
    source_data: rename('sourceData', 'string')
  }),
  animation: hierarchy({
    animation: rename('children', multiple('animation'))
  }, registerNamespace),
  effect: hierarchy({
    asset: 'asset',
    image: rename('images', multiple('image')),
    // Only accept COMMON profile for now
    profile_COMMON: rename('common', hierarchy({
      asset: 'asset',
      image: rename('images', multiple('image')),
      newparam: rename('params', multiple('newparam')),
      technique: hoist({
        // TODO Accept image / newparam at this point
        // image: rename('images', multiple('image')),
        // newparam: rename('params', multiple('newparam')),
        blinn: hierarchy(MATERIAL_STRUCTURE,
          (node, frame) => frame.data.type = 'blinn'),
        constant: hierarchy(MATERIAL_STRUCTURE,
          (node, frame) => frame.data.type = 'constant'),
        lambert: hierarchy(MATERIAL_STRUCTURE,
          (node, frame) => frame.data.type = 'lambert'),
        phong: hierarchy(MATERIAL_STRUCTURE,
          (node, frame) => frame.data.type = 'phong')
      }, registerSid)
    }, registerNamespace))
  }, {
    push: registerNamespace.push,
    pop(data, frame) {
      if (frame.data.common) {
        let { common } = frame.data;
        let newImages = (frame.data.images || []).concat(common.images || []);
        Object.assign(frame.data, common);
        frame.data.images = newImages;
        delete frame.data.common;
      }
      return registerNamespace.pop.call(this, frame.data, frame);
    }
  }),
  newparam: hoist(CORE_PARAM_TYPE, registerSid),
  colorOrTexture: hoist({
    color: 'floatArray',
    param: attributes(v => v.attributes.ref),
    // Ignore 'texCoord' for now
    texture: attributes(v => v.attributes.texture)
  }),
  floatOrParam: hoist({
    float: 'float',
    param: attributes(v => v.attributes.ref)
  }),
  surface: hierarchy({
    size: 'floatArray',
    mipmap_generate: 'boolean',
    channels: 'string',
    range: 'string',
    // TODO Cube texture
    init_cube: NOOP,
    init_from: 'string'
  }),
  sampler: hierarchy({
    source: 'string',
    wrap_s: 'fxSamplerWrapCommon',
    wrap_t: 'fxSamplerWrapCommon',
    minfilter: 'fxSamplerFilterCommon',
    magfilter: 'fxSamplerFilterCommon',
    mipfilter: 'fxSamplerFilterCommon'
  }),
  material: hoist({
    instance_effect: 'instanceEffect'
  }, registerNamespace),
  instanceEffect: hierarchy({
    setparam: rename('params', multipleMap(
      hoist(CORE_PARAM_TYPE, (node, frame) => {
        frame.ref = node.attributes.ref;
      }),
      (data, frame) => frame.ref
    ))
  }, {
    push(node, frame) {
      frame.data.effect = node.attributes.url;
    }
  })
};

const INITIAL = {
  opentag: function (node) {
    if (node.name !== 'COLLADA') {
      throw new Error('Provided file is not COLLADA format');
    }
    this.push(SCHEMA.COLLADA, node);
  }
};

export default function loadCollada(data) {
  let context = new Context();
  context.push(INITIAL);
  let parser = sax.parser(true);
  parser.onerror = (e) => console.log(e);
  parser.ontext = context.getDelegator(v => v.text);
  parser.onopentag = context.getDelegator(v => v.opentag);
  parser.onclosetag = context.getDelegator(v => v.closetag);
  parser.onattribute = context.getDelegator(v => v.attribute);
  parser.onend = context.getDelegator(v => v.end);
  parser.write(data).close();
  console.log(context.namespace);
}
<commit_msg>Add source parsing in COLLADA<commit_after>import sax from 'sax';

class Context {
  constructor() {
    this.stack = [];
    this.namespace = {};
  }
  push(operator, data) {
    let stackFrame = { operator, parent: this.stack[this.stack.length - 1] };
    this.stack.push(stackFrame);
    if (operator.push != null) operator.push.call(this, data, stackFrame);
  }
  pop(data) {
    if (this.stack.length === 0) throw new Error('Unable to pop frame');
    let stackFrame = this.stack.pop();
    let popOp = stackFrame.operator.pop;
    let result = popOp && popOp.call(this, data, stackFrame);
    if (this.stack.length > 0) {
      let parentFrame = this.stack[this.stack.length - 1];
      let popChild = parentFrame.operator.popChild;
      popChild && popChild.call(this, result, parentFrame, stackFrame);
    }
  }
  get() {
    return this.stack[this.stack.length - 1];
  }
  getDelegator(getOp) {
    return (v) => {
      let frame = this.get();
      let func = getOp(frame.operator);
      if (func != null) func.call(this, v, frame);
    };
  }
  resolveURI(name) {
    if (name.charAt(0) === '#') {
      return this.namespace[name.slice(1)];
    }
    throw new Error('This parser doesn\'t support outside URI (Only XPointer ' +
      'Syntax is supported)');
  }
}

const NOOP = {
  opentag(node) {
    this.push(NOOP, node);
  },
  closetag() {
    this.pop();
  },
};

function resolveSchema(schema) {
  if (typeof schema === 'string') return SCHEMA[schema];
  if (typeof schema === 'function') return schema();
  return schema;
}

function cached(schema, manipulator) {
  // We use cache to make hoisting available.... what?
  let cached = null;
  return () => {
    if (cached !== null) return cached;
    cached = manipulator(resolveSchema(schema));
    return cached;
  };
}

const rename =
  (target, schema) => cached(schema, v => v && Object.assign({ target }, v));
const multiple =
  (schema) => cached(schema, v => v && Object.assign({
    // merge is *always* executed by hierarchy, even though no prev object
    // is available.
    merge: (prev = [], current) => (prev.push(current), prev)
  }, v));
const multipleMap =
  (schema, getKey) => cached(schema, v => v && Object.assign({
    // merge is *always* executed by hierarchy, even though no prev object
    // is available.
    merge: (prev = {}, current, frame) => {
      prev[getKey(current, frame)] = current;
      return prev;
    }
  }, v));
const registerNamespace = {
  push(node, frame) {
    const { id } = node.attributes;
    if (id != null) {
      frame.id = id;
    }
    frame.namespace = {};
  },
  pop(data, frame) {
    data.namespace = frame.namespace;
    if (frame.id != null) {
      this.namespace[frame.id] = data;
      data.id = frame.id;
    }
    return data;
  }
};

const registerSid = {
  push(node, frame) {
    const { id, sid } = node.attributes;
    if (sid != null) {
      let parent = frame.parent;
      while (parent != null && parent.namespace == null) {
        parent = node.parent;
      }
      if (parent != null) {
        frame.namespaceParent = parent;
        frame.sid = sid;
      }
    } else {
      throw new Error('sid is required but was not specified');
    }
    if (id != null) {
      frame.id = id;
    }
  },
  pop(data, frame) {
    if (frame.namespaceParent != null) {
      frame.namespaceParent.namespace[frame.sid] = data;
      data.sid = frame.sid;
    }
    if (frame.id != null) {
      this.namespace[frame.id] = data;
      data.id = frame.id;
    }
    return data;
  }
};

function addTrigger(schema, triggers) {
  let onPush, onPop;
  if (typeof triggers === 'function') onPush = triggers;
  else if (triggers != null) {
    onPush = triggers.push;
    onPop = triggers.pop;
  }
  return cached(schema, v => {
    console.log(v);
    return v && Object.assign({}, v, {
      push(node, frame) {
        v.push(node, frame);
        if (onPush != null) onPush.call(this, node, frame);
      },
      pop(data, frame) {
        let result = v.pop(data, frame);
        if (onPop != null) return onPop.call(this, result, frame);
        return result;
      }
    });
  });
}

function hoist(children, triggers) {
  let onPush, onPop;
  if (typeof triggers === 'function') onPush = triggers;
  else if (triggers != null) {
    onPush = triggers.push;
    onPop = triggers.pop;
  }
  return {
    push(node, frame) {
      frame.data = {};
      if (onPush != null) onPush.call(this, node, frame);
    },
    opentag(node) {
      let child = children[node.name];
      // Ignore if node name doesn't match
      if (child == null) return this.push(NOOP, node);
      let schema = resolveSchema(child);
      // TODO Remove this
      if (schema == null) return this.push(NOOP, node);
      this.push(schema, node);
    },
    closetag() {
      // TODO This should be default operation if not specified
      this.pop();
    },
    pop(data, frame) {
      if (onPop != null) return onPop.call(this, frame.data, frame);
      return frame.data;
    },
    popChild(data, frame) {
      let result = data;
      frame.data = result;
    }
  };
}

function hierarchy(children, triggers) {
  let onPush, onPop;
  if (typeof triggers === 'function') onPush = triggers;
  else if (triggers != null) {
    onPush = triggers.push;
    onPop = triggers.pop;
  }
  return {
    push(node, frame) {
      // TODO attributes
      frame.data = {};
      if (onPush != null) onPush.call(this, node, frame);
    },
    opentag(node, frame) {
      let child = children[node.name];
      frame.target = null;
      // Ignore if node name doesn't match
      if (child == null) return this.push(NOOP, node);
      let schema = resolveSchema(child);
      // TODO Remove this
      if (schema == null) return this.push(NOOP, node);
      frame.target = schema.target || node.name;
      frame.targetSchema = schema;
      this.push(schema, node);
    },
    closetag() {
      // TODO This should be default operation if not specified
      this.pop();
    },
    pop(data, frame) {
      console.log(frame.data);
      if (onPop != null) return onPop.call(this, frame.data, frame);
      return frame.data;
    },
    popChild(data, frame, childFrame) {
      let prev = frame.data[frame.target];
      let result = data;
      if (frame.target == null) return;
      if (frame.targetSchema && frame.targetSchema.merge != null) {
        result = frame.targetSchema.merge(prev, result, childFrame);
      }
      frame.data[frame.target] = result;
    }
  };
}

function library(nodeName, schema) {
  return {
    push(node, frame) {
      frame.data = [];
    },
    opentag(node) {
      // TODO Maybe we should handle this?
      if (nodeName !== node.name) return this.push(NOOP, node);
      let schemaResolved = resolveSchema(schema);
      if (schemaResolved == null) return this.push(NOOP, node);
      this.push(schemaResolved, node);
    },
    closetag() {
      // TODO This should be default operation if not specified
      this.pop();
    },
    pop(data, frame) {
      return frame.data;
    },
    popChild(data, frame) {
      if (data == null) return;
      frame.data.push(data);
    },
    merge(prev = [], current) {
      return prev.concat(current);
    }
  };
}

function attributes(proc) {
  return {
    push(node, frame) {
      if (proc != null) frame.data = proc(node);
      else frame.data = node.attributes;
    },
    opentag(node) {
      return this.push(NOOP, node);
    },
    closetag() {
      // TODO This should be default operation if not specified
      this.pop();
    },
    pop(data, frame) {
      return frame.data;
    },
    merge(prev = {}, current) {
      if (proc != null) return current;
      return Object.assign(prev, current);
    }
  };
}

function textValue(proc) {
  return {
    push(data, frame) {
      frame.value = null;
    },
    opentag(node) {
      return this.push(NOOP, node);
    },
    closetag() {
      // TODO This should be default operation if not specified
      this.pop();
    },
    text(data, frame) {
      let value = data.trim();
      if (value === '') return;
      frame.value = value;
    },
    pop(data, frame) {
      return proc(frame.value);
    }
  };
}

const MATERIAL_STRUCTURE = {
  emission: 'colorOrTexture',
  ambient: 'colorOrTexture',
  diffuse: 'colorOrTexture',
  specular: 'colorOrTexture',
  shininess: 'floatOrParam',
  reflective: 'colorOrTexture',
  reflectivity: 'floatOrParam',
  transparent: 'colorOrTexture',
  transparency: 'floatOrParam',
  index_of_refraction: rename('refraction', 'floatOrParam')
};

const CORE_PARAM_TYPE = {
  bool: 'boolean',
  bool2: 'booleanArray',
  bool3: 'booleanArray',
  bool4: 'booleanArray',
  int: 'int',
  int2: 'intArray',
  int3: 'intArray',
  int4: 'intArray',
  float: 'float',
  float2: 'floatArray',
  float3: 'floatArray',
  float4: 'floatArray',
  surface: 'surface',
  sampler2D: 'sampler',
  samplerCUBE: 'sampler'
};

for (let i = 1; i <= 4; ++i) {
  for (let j = 1; j <= 4; ++j) {
    CORE_PARAM_TYPE[`float${i}x${j}`] = 'floatArray';
  }
}

const SCHEMA = {
  noop: NOOP,
  attributes: attributes(),
  boolean: textValue(v => v === 'true'),
  booleanArray: textValue(v => v.split(/\s+/).map(v => v === 'true')),
  string: textValue(v => v),
  // Should be the date parsed?
  date: textValue(v => v),
  stringArray: textValue(v => v.split(/\s+/)),
  float: textValue(v => parseFloat(v)),
  floatArray: textValue(v => new Float32Array(v.split(/\s+/).map(parseFloat))),
  int: textValue(v => parseFloat(v)),
  intArray: textValue(v => new Int32Array(v.split(/\s+/).map(parseFloat))),
  COLLADA: hierarchy({
    asset: 'asset',
    library_animations: rename('animations', library('animation', 'animation')),
    library_animation_clips: rename('animationClips', library('animation_clip',
      'animationClip')),
    library_cameras: rename('cameras', library('camera', 'camera')),
    library_controllers: rename('controllers', library('controller',
      'controller')),
    library_geometries: rename('geometries', library('geometry', 'geometry')),
    library_lights: rename('lights', library('light', 'light')),
    library_nodes: rename('nodes', library('node', 'node')),
    library_visual_scenes: rename('visualScenes', library('visual_scene',
      'visualScene')),
    // COLLADA FX
    library_images: rename('images', library('image', 'image')),
    library_effects: rename('effects', library('effect', 'effect')),
    library_materials: rename('materials', library('material', 'material'))
  }, ({ attributes }) => {
    // Check version
    if (attributes.version.slice(0, 3) !== '1.4') {
      throw new Error('COLLADA parser only supports 1.4.x format');
    }
  }),
  asset: hierarchy({
    contributor: multiple('contributor'),
    created: 'date',
    modified: 'date',
    keywords: 'stringArray',
    revision: 'string',
    subject: 'string',
    title: 'string',
    unit: 'attributes',
    up_axis: rename('upAxis', 'string')
  }),
  contributor: hierarchy({
    author: 'string',
    authoring_tool: 'string',
    comments: 'string',
    copyright: 'string',
    source_data: rename('sourceData', 'string')
  }),
  animation: hierarchy({
    animation: rename('children', multiple('animation'))
  }, registerNamespace),
  effect: hierarchy({
    asset: 'asset',
    image: rename('images', multiple('image')),
    // Only accept COMMON profile for now
    profile_COMMON: rename('common', hierarchy({
      asset: 'asset',
      image: rename('images', multiple('image')),
      newparam: rename('params', multipleMap('newparam',
        (data, frame) => frame.sid
      )),
      technique: hoist({
        // TODO Accept image / newparam at this point
        // image: rename('images', multiple('image')),
        // newparam: rename('params', multiple('newparam')),
        blinn: hierarchy(MATERIAL_STRUCTURE,
          (node, frame) => frame.data.type = 'blinn'),
        constant: hierarchy(MATERIAL_STRUCTURE,
          (node, frame) => frame.data.type = 'constant'),
        lambert: hierarchy(MATERIAL_STRUCTURE,
          (node, frame) => frame.data.type = 'lambert'),
        phong: hierarchy(MATERIAL_STRUCTURE,
          (node, frame) => frame.data.type = 'phong')
      }, registerSid)
    }, registerNamespace))
  }, {
    push: registerNamespace.push,
    pop(data, frame) {
      if (frame.data.common) {
        let { common } = frame.data;
        let newImages = (frame.data.images || []).concat(common.images || []);
        Object.assign(frame.data, common);
        frame.data.images = newImages;
        delete frame.data.common;
      }
      return registerNamespace.pop.call(this, frame.data, frame);
    }
  }),
  newparam: hoist(CORE_PARAM_TYPE, registerSid),
  colorOrTexture: hoist({
    color: 'floatArray',
    param: attributes(v => v.attributes.ref),
    // Ignore 'texCoord' for now
    texture: attributes(v => v.attributes.texture)
  }),
  floatOrParam: hoist({
    float: 'float',
    param: attributes(v => v.attributes.ref)
  }),
  surface: hierarchy({
    size: 'floatArray',
    mipmap_generate: 'boolean',
    channels: 'string',
    range: 'string',
    // TODO Cube texture
    init_cube: NOOP,
    init_from: 'string'
  }),
  sampler: hierarchy({
    source: 'string',
    wrap_s: 'fxSamplerWrapCommon',
    wrap_t: 'fxSamplerWrapCommon',
    minfilter: 'fxSamplerFilterCommon',
    magfilter: 'fxSamplerFilterCommon',
    mipfilter: 'fxSamplerFilterCommon'
  }),
  material: hoist({
    instance_effect: 'instanceEffect'
  }, registerNamespace),
  instanceEffect: hierarchy({
    setparam: rename('params', multipleMap(
      hoist(CORE_PARAM_TYPE, (node, frame) => {
        frame.ref = node.attributes.ref;
      }),
      (data, frame) => frame.ref
    ))
  }, {
    push(node, frame) {
      frame.data.effect = node.attributes.url;
    }
  }),
  geometry: hoist({
    mesh: hierarchy({
      source: rename('sources', multipleMap('source',
        (data, frame) => frame.id))
    })
  }, registerNamespace),
  source: hierarchy({
    Name_array: rename('data', addTrigger('stringArray', registerNamespace)),
    bool_array: rename('data', addTrigger('booleanArray', registerNamespace)),
    float_array: rename('data', addTrigger('floatArray', registerNamespace)),
    int_array: rename('data', addTrigger('intArray', registerNamespace)),
    // TODO Read param names
    technique_common: rename('options', hoist({
      accessor: attributes()
    }))
  }, {
    push: registerNamespace.push,
    pop(data, frame) {
      // TODO This should be outside XML parser (Should do post processing)
      if (data.options != null) {
        data.axis = parseInt(data.options.stride) || 1;
        if (data.source != null) {
          data.data = this.resolveURI(data.source, frame);
        }
        let offset = parseInt(data.options.offset);
        if (!isNaN(offset)) {
          // TODO We shouldn't care about data structure size...
          data.offset = offset * 4;
        }
        let count = parseInt(data.options.count);
        if (!isNaN(count)) {
          data.count = count;
          // Slice the array
          if (Array.isArray(data.data)) {
            data.data = data.data.slice(0, count * data.axis);
          } else {
            data.data = data.data.subarray(0, count * data.axis);
          }
        }
      }
      return registerNamespace.pop.call(this, data, frame);
    }
  })
};

const INITIAL = {
  opentag: function (node) {
    if (node.name !== 'COLLADA') {
      throw new Error('Provided file is not COLLADA format');
    }
    this.push(SCHEMA.COLLADA, node);
  }
};

export default function loadCollada(data) {
  let context = new Context();
  context.push(INITIAL);
  let parser = sax.parser(true);
  parser.onerror = (e) => console.log(e);
  parser.ontext = context.getDelegator(v => v.text);
  parser.onopentag = context.getDelegator(v => v.opentag);
  parser.onclosetag = context.getDelegator(v => v.closetag);
  parser.onattribute = context.getDelegator(v => v.attribute);
  parser.onend = context.getDelegator(v => v.end);
  parser.write(data).close();
  console.log(context.namespace);
}

{'fraction_non_alphanumeric': 0.1560978884823257, 'fraction_numerical': 0.0021484440565309342, 'mean_word_length': 2.8608087091757386, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 80, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1963670', 'n_tokens_mistral': 9626, 'n_tokens_neox': 9483, 'n_words': 2292}