This is the mirror image of #2170.

### Checklists

<!-- Note on checklists:
     If you think some of these steps are not necessary for your PR,
     just remove those checkboxes, or mark them as checked. Otherwise,
     if some checkboxes are left unmarked, we might assume that your PR
     is not ready to be merged and we might keep you waiting -->

- [x] Add a [CHANGELOG](https://github.com/pyodide/pyodide/blob/main/docs/project/changelog.md) entry
- [x] Add / update tests
- [x] Add new / update outdated documentation

Add `default_converter` argument to `to_js` and `toJs`. This is the mirror image of #2170.

### Checklists

<!-- Note on checklists:
     If you think some of these steps are not necessary for your PR,
     just remove those checkboxes, or mark them as checked. Otherwise,
     if some checkboxes are left unmarked, we might assume that your PR
     is not ready to be merged and we might keep you waiting -->

- [x] Add a [CHANGELOG](https://github.com/pyodide/pyodide/blob/main/docs/project/changelog.md) entry
- [x] Add / update tests
- [x] Add new / update outdated documentation

JS_FILE(js2python_init, () => {
  0, 0; /* Magic, see include_js_file.h */
  let PropagateError = Module._PropagatePythonError;
  function js2python_string(value) {
    // The general idea here is to allocate a Python string and then
    // have JavaScript write directly into its buffer.  We first need
    // to determine if is needs to be a 1-, 2- or 4-byte string, since
    // Python handles all 3.
    let max_code_point = 0;
    let num_code_points = 0;
    for (let c of value) {
      num_code_points++;
      let code_point = c.codePointAt(0);
      max_code_point =
        code_point > max_code_point ? code_point : max_code_point;
    }

    let result = _PyUnicode_New(num_code_points, max_code_point);
    if (result === 0) {
      throw new PropagateError();
    }

    let ptr = _PyUnicode_Data(result);
    if (max_code_point > 0xffff) {
      for (let c of value) {
        HEAPU32[ptr / 4] = c.codePointAt(0);
        ptr += 4;
      }
    } else if (max_code_point > 0xff) {
      for (let c of value) {
        HEAPU16[ptr / 2] = c.codePointAt(0);
        ptr += 2;
      }
    } else {
      for (let c of value) {
        HEAPU8[ptr] = c.codePointAt(0);
        ptr += 1;
      }
    }

    return result;
  }

  function js2python_bigint(value) {
    let value_orig = value;
    let length = 0;
    if (value < 0) {
      value = -value;
    }
    while (value) {
      length++;
      value >>= BigInt(32);
    }
    let stackTop = stackSave();
    let ptr = stackAlloc(length * 4);
    value = value_orig;
    for (let i = 0; i < length; i++) {
      ASSIGN_U32(ptr, i, Number(value & BigInt(0xffffffff)));
      value >>= BigInt(32);
    }
    let result = __PyLong_FromByteArray(
      ptr,
      length * 4 /* length in bytes */,
      true /* little endian */,
      true /* signed? */
    );
    stackRestore(stackTop);
    return result;
  }

  /**
   * This function converts immutable types. numbers, bigints, strings,
   * booleans, undefined, and null are converted. PyProxies are unwrapped.
   *
   * If `value` is of any other type then `undefined` is returned.
   *
   * If `value` is one of those types but an error is raised during conversion,
   * we throw a PropagateError to propogate the error out to C. This causes
   * special handling in the EM_JS wrapper.
   */
  function js2python_convertImmutable(value) {
    let result = js2python_convertImmutableInner(value);
    if (result === 0) {
      throw new PropagateError();
    }
    return result;
  }
  // js2python_convertImmutable is used from js2python.c so we need to add it
  // to Module.
  Module.js2python_convertImmutable = js2python_convertImmutable;

  /**
   * Returns a pointer to a Python object, 0, or undefined.
   *
   * If we return 0 it means we tried to convert but an error occurred, if we
   * return undefined, no conversion was attempted.
   */
  function js2python_convertImmutableInner(value) {
    let type = typeof value;
    if (type === "string") {
      return js2python_string(value);
    } else if (type === "number") {
      if (Number.isSafeInteger(value)) {
        return _PyLong_FromDouble(value);
      } else {
        return _PyFloat_FromDouble(value);
      }
    } else if (type === "bigint") {
      return js2python_bigint(value);
    } else if (value === undefined || value === null) {
      return __js2python_none();
    } else if (value === true) {
      return __js2python_true();
    } else if (value === false) {
      return __js2python_false();
    } else if (API.isPyProxy(value)) {
      return __js2python_pyproxy(Module.PyProxy_getPtr(value));
    }
    return undefined;
  }

  function js2python_convertList(obj, context) {
    let list = _PyList_New(obj.length);
    if (list === 0) {
      return 0;
    }
    let entryid = 0;
    let item = 0;
    try {
      context.cache.set(obj, list);
      for (let i = 0; i < obj.length; i++) {
        entryid = Hiwire.new_value(obj[i]);
        item = js2python_convert_with_context(entryid, context);
        // PyList_SetItem steals a reference to item no matter what
        _Py_IncRef(item);
        if (_PyList_SetItem(list, i, item) === -1) {
          throw new PropagateError();
        }
        Hiwire.decref(entryid);
        entryid = 0;
        _Py_DecRef(item);
        item = 0;
      }
    } catch (e) {
      Hiwire.decref(entryid);
      _Py_DecRef(item);
      _Py_DecRef(list);
      throw e;
    }

    return list;
  }

  function js2python_convertMap(obj, entries, context) {
    let dict = _PyDict_New();
    if (dict === 0) {
      return 0;
    }
    let key_py = 0;
    let value_id = 0;
    let value_py = 0;
    try {
      context.cache.set(obj, dict);
      for (let [key_js, value_js] of entries) {
        key_py = js2python_convertImmutable(key_js);
        if (key_py === undefined) {
          let key_type =
            (key_js.constructor && key_js.constructor.name) || typeof key_js;
          throw new Error(
            `Cannot use key of type ${key_type} as a key to a Python dict`
          );
        }
        value_id = Hiwire.new_value(value_js);
        value_py = js2python_convert_with_context(value_id, context);

        if (_PyDict_SetItem(dict, key_py, value_py) === -1) {
          throw new PropagateError();
        }
        _Py_DecRef(key_py);
        key_py = 0;
        Hiwire.decref(value_id);
        value_id = 0;
        _Py_DecRef(value_py);
        value_py = 0;
      }
    } catch (e) {
      _Py_DecRef(key_py);
      Hiwire.decref(value_id);
      _Py_DecRef(value_py);
      _Py_DecRef(dict);
      throw e;
    }
    return dict;
  }

  function js2python_convertSet(obj, context) {
    let set = _PySet_New(0);
    if (set === 0) {
      return 0;
    }
    let key_py = 0;
    try {
      context.cache.set(obj, set);
      for (let key_js of obj) {
        key_py = js2python_convertImmutable(key_js);
        if (key_py === undefined) {
          let key_type =
            (key_js.constructor && key_js.constructor.name) || typeof key_js;
          throw new Error(
            `Cannot use key of type ${key_type} as a key to a Python set`
          );
        }
        let errcode = _PySet_Add(set, key_py);
        if (errcode === -1) {
          throw new PropagateError();
        }
        _Py_DecRef(key_py);
        key_py = 0;
      }
    } catch (e) {
      _Py_DecRef(key_py);
      _Py_DecRef(set);
      throw e;
    }
    return set;
  }

  function checkBoolIntCollision(obj, ty) {
    if (obj.has(1) && obj.has(true)) {
      throw new Error(
        `Cannot faithfully convert ${ty} into Python since it ` +
          "contains both 1 and true as keys."
      );
    }
    if (obj.has(0) && obj.has(false)) {
      throw new Error(
        `Cannot faithfully convert ${ty} into Python since it ` +
          "contains both 0 and false as keys."
      );
    }
  }

  /**
   * Convert mutable types: Array, Map, Set, and Objects whose prototype is
   * either null or the default. Anything else is wrapped in a Proxy. This
   * should only be used on values for which js2python_convertImmutable
   * returned `undefined`.
   */
  function js2python_convertOther(id, value, context) {
    let toStringTag = Object.prototype.toString.call(value);
    if (
      Array.isArray(value) ||
      value === "[object HTMLCollection]" ||
      value === "[object NodeList]"
    ) {
      return js2python_convertList(value, context);
    }
    if (toStringTag === "[object Map]" || value instanceof Map) {
      checkBoolIntCollision(value, "Map");
      return js2python_convertMap(value, value.entries(), context);
    }
    if (toStringTag === "[object Set]" || value instanceof Set) {
      checkBoolIntCollision(value, "Set");
      return js2python_convertSet(value, context);
    }
    if (
      toStringTag === "[object Object]" &&
      (value.constructor === undefined || value.constructor.name === "Object")
    ) {
      return js2python_convertMap(value, Object.entries(value), context);
    }
    if (toStringTag === "[object ArrayBuffer]" || ArrayBuffer.isView(value)) {
      let [format_utf8, itemsize] = Module.get_buffer_datatype(value);
      return _JsBuffer_CopyIntoMemoryView(
        id,
        value.byteLength,
        format_utf8,
        itemsize
      );
    }
    return undefined;
  }

  /**
   * Convert a JavaScript object to Python to a given depth.
   */
  function js2python_convert_with_context(id, context) {
    let value = Hiwire.get_value(id);
    let result;
    result = js2python_convertImmutable(value);
    if (result !== undefined) {
      return result;
    }
    if (context.depth === 0) {
      return _JsProxy_create(id);
    }
    result = context.cache.get(value);
    if (result !== undefined) {
      return result;
    }
    context.depth--;
    try {
      result = js2python_convertOther(id, value, context);
      if (result !== undefined) {
        return result;
      }
      if (context.defaultConverter === undefined) {
        return _JsProxy_create(id);
      }
      let result_js = context.defaultConverter(
        value,
        context.converter,
        context.cacheConversion
      );
      if (result_js === undefined) {
        return _JsProxy_create(id);
      }
      result = js2python_convertImmutable(result_js);
      if (API.isPyProxy(result_js)) {
        result_js.destroy();
      }
      if (result !== undefined) {
        return result;
      }
      let result_id = Module.hiwire.new_value(result_js);
      result = _JsProxy_create(result_id);
      Module.hiwire.decref(result_id);
      return result;
    } finally {
      context.depth++;
    }
  }

  /**
   * Convert a JavaScript object to Python to a given depth.
   */
  function js2python_convert(id, { depth, defaultConverter }) {
    let context = {
      cache: new Map(),
      depth,
      defaultConverter,
      // arguments for defaultConverter
      converter(x) {
        let id = Module.hiwire.new_value(x);
        try {
          return Module.pyproxy_new(
            js2python_convert_with_context(id, context)
          );
        } finally {
          Module.hiwire.decref(id);
        }
      },
      cacheConversion(input, output) {
        if (API.isPyProxy(output)) {
          context.cache.set(input, Module.PyProxy_getPtr(output));
        } else {
          throw new Error("Second argument should be a PyProxy!");
        }
      },
    };
    return js2python_convert_with_context(id, context);
  }

  Module.js2python_convert = js2python_convert;
});

JS_FILE(js2python_init, () => {
  0, 0; /* Magic, see include_js_file.h */
  let PropagateError = Module._PropagatePythonError;
  function js2python_string(value) {
    // The general idea here is to allocate a Python string and then
    // have JavaScript write directly into its buffer.  We first need
    // to determine if is needs to be a 1-, 2- or 4-byte string, since
    // Python handles all 3.
    let max_code_point = 0;
    let num_code_points = 0;
    for (let c of value) {
      num_code_points++;
      let code_point = c.codePointAt(0);
      max_code_point =
        code_point > max_code_point ? code_point : max_code_point;
    }

    let result = _PyUnicode_New(num_code_points, max_code_point);
    if (result === 0) {
      throw new PropagateError();
    }

    let ptr = _PyUnicode_Data(result);
    if (max_code_point > 0xffff) {
      for (let c of value) {
        HEAPU32[ptr / 4] = c.codePointAt(0);
        ptr += 4;
      }
    } else if (max_code_point > 0xff) {
      for (let c of value) {
        HEAPU16[ptr / 2] = c.codePointAt(0);
        ptr += 2;
      }
    } else {
      for (let c of value) {
        HEAPU8[ptr] = c.codePointAt(0);
        ptr += 1;
      }
    }

    return result;
  }

  function js2python_bigint(value) {
    let value_orig = value;
    let length = 0;
    if (value < 0) {
      value = -value;
    }
    while (value) {
      length++;
      value >>= BigInt(32);
    }
    let stackTop = stackSave();
    let ptr = stackAlloc(length * 4);
    value = value_orig;
    for (let i = 0; i < length; i++) {
      ASSIGN_U32(ptr, i, Number(value & BigInt(0xffffffff)));
      value >>= BigInt(32);
    }
    let result = __PyLong_FromByteArray(
      ptr,
      length * 4 /* length in bytes */,
      true /* little endian */,
      true /* signed? */
    );
    stackRestore(stackTop);
    return result;
  }

  /**
   * This function converts immutable types. numbers, bigints, strings,
   * booleans, undefined, and null are converted. PyProxies are unwrapped.
   *
   * If `value` is of any other type then `undefined` is returned.
   *
   * If `value` is one of those types but an error is raised during conversion,
   * we throw a PropagateError to propogate the error out to C. This causes
   * special handling in the EM_JS wrapper.
   */
  function js2python_convertImmutable(value) {
    let result = js2python_convertImmutableInner(value);
    if (result === 0) {
      throw new PropagateError();
    }
    return result;
  }
  // js2python_convertImmutable is used from js2python.c so we need to add it
  // to Module.
  Module.js2python_convertImmutable = js2python_convertImmutable;

  /**
   * Returns a pointer to a Python object, 0, or undefined.
   *
   * If we return 0 it means we tried to convert but an error occurred, if we
   * return undefined, no conversion was attempted.
   */
  function js2python_convertImmutableInner(value) {
    let type = typeof value;
    if (type === "string") {
      return js2python_string(value);
    } else if (type === "number") {
      if (Number.isSafeInteger(value)) {
        return _PyLong_FromDouble(value);
      } else {
        return _PyFloat_FromDouble(value);
      }
    } else if (type === "bigint") {
      return js2python_bigint(value);
    } else if (value === undefined || value === null) {
      return __js2python_none();
    } else if (value === true) {
      return __js2python_true();
    } else if (value === false) {
      return __js2python_false();
    } else if (API.isPyProxy(value)) {
      return __js2python_pyproxy(Module.PyProxy_getPtr(value));
    }
    return undefined;
  }

  function js2python_convertList(obj, context) {
    let list = _PyList_New(obj.length);
    if (list === 0) {
      return 0;
    }
    let entryid = 0;
    let item = 0;
    try {
      context.cache.set(obj, list);
      for (let i = 0; i < obj.length; i++) {
        entryid = Hiwire.new_value(obj[i]);
        item = js2python_convert_with_context(entryid, context);
        // PyList_SetItem steals a reference to item no matter what
        _Py_IncRef(item);
        if (_PyList_SetItem(list, i, item) === -1) {
          throw new PropagateError();
        }
        Hiwire.decref(entryid);
        entryid = 0;
        _Py_DecRef(item);
        item = 0;
      }
    } catch (e) {
      Hiwire.decref(entryid);
      _Py_DecRef(item);
      _Py_DecRef(list);
      throw e;
    }

    return list;
  }

  function js2python_convertMap(obj, entries, context) {
    let dict = _PyDict_New();
    if (dict === 0) {
      return 0;
    }
    let key_py = 0;
    let value_id = 0;
    let value_py = 0;
    try {
      context.cache.set(obj, dict);
      for (let [key_js, value_js] of entries) {
        key_py = js2python_convertImmutable(key_js);
        if (key_py === undefined) {
          let key_type =
            (key_js.constructor && key_js.constructor.name) || typeof key_js;
          throw new Error(
            `Cannot use key of type ${key_type} as a key to a Python dict`
          );
        }
        value_id = Hiwire.new_value(value_js);
        value_py = js2python_convert_with_context(value_id, context);

        if (_PyDict_SetItem(dict, key_py, value_py) === -1) {
          throw new PropagateError();
        }
        _Py_DecRef(key_py);
        key_py = 0;
        Hiwire.decref(value_id);
        value_id = 0;
        _Py_DecRef(value_py);
        value_py = 0;
      }
    } catch (e) {
      _Py_DecRef(key_py);
      Hiwire.decref(value_id);
      _Py_DecRef(value_py);
      _Py_DecRef(dict);
      throw e;
    }
    return dict;
  }

  function js2python_convertSet(obj, context) {
    let set = _PySet_New(0);
    if (set === 0) {
      return 0;
    }
    let key_py = 0;
    try {
      context.cache.set(obj, set);
      for (let key_js of obj) {
        key_py = js2python_convertImmutable(key_js);
        if (key_py === undefined) {
          let key_type =
            (key_js.constructor && key_js.constructor.name) || typeof key_js;
          throw new Error(
            `Cannot use key of type ${key_type} as a key to a Python set`
          );
        }
        let errcode = _PySet_Add(set, key_py);
        if (errcode === -1) {
          throw new PropagateError();
        }
        _Py_DecRef(key_py);
        key_py = 0;
      }
    } catch (e) {
      _Py_DecRef(key_py);
      _Py_DecRef(set);
      throw e;
    }
    return set;
  }

  function checkBoolIntCollision(obj, ty) {
    if (obj.has(1) && obj.has(true)) {
      throw new Error(
        `Cannot faithfully convert ${ty} into Python since it ` +
          "contains both 1 and true as keys."
      );
    }
    if (obj.has(0) && obj.has(false)) {
      throw new Error(
        `Cannot faithfully convert ${ty} into Python since it ` +
          "contains both 0 and false as keys."
      );
    }
  }

  /**
   * Convert mutable types: Array, Map, Set, and Objects whose prototype is
   * either null or the default. Anything else is wrapped in a Proxy. This
   * should only be used on values for which js2python_convertImmutable
   * returned `undefined`.
   */
  function js2python_convertOther(id, value, context) {
    let toStringTag = Object.prototype.toString.call(value);
    if (
      Array.isArray(value) ||
      value === "[object HTMLCollection]" ||
      value === "[object NodeList]"
    ) {
      return js2python_convertList(value, context);
    }
    if (toStringTag === "[object Map]" || value instanceof Map) {
      checkBoolIntCollision(value, "Map");
      return js2python_convertMap(value, value.entries(), context);
    }
    if (toStringTag === "[object Set]" || value instanceof Set) {
      checkBoolIntCollision(value, "Set");
      return js2python_convertSet(value, context);
    }
    if (
      toStringTag === "[object Object]" &&
      (value.constructor === undefined || value.constructor.name === "Object")
    ) {
      return js2python_convertMap(value, Object.entries(value), context);
    }
    if (toStringTag === "[object ArrayBuffer]" || ArrayBuffer.isView(value)) {
      let [format_utf8, itemsize] = Module.get_buffer_datatype(value);
      return _JsBuffer_CopyIntoMemoryView(
        id,
        value.byteLength,
        format_utf8,
        itemsize
      );
    }
    return undefined;
  }

  /**
   * Convert a JavaScript object to Python to a given depth.
   */
  function js2python_convert_with_context(id, context) {
    let value = Hiwire.get_value(id);
    let result;
    result = js2python_convertImmutable(value);
    if (result !== undefined) {
      return result;
    }
    if (context.depth === 0) {
      return _JsProxy_create(id);
    }
    result = context.cache.get(value);
    if (result !== undefined) {
      return result;
    }
    context.depth--;
    try {
      result = js2python_convertOther(id, value, context);
      if (result !== undefined) {
        return result;
      }
      if (context.defaultConverter === undefined) {
        return _JsProxy_create(id);
      }
      let result_js = context.defaultConverter(
        value,
        context.converter,
        context.cacheConversion
      );
      result = js2python_convertImmutable(result_js);
      if (API.isPyProxy(result_js)) {
        result_js.destroy();
      }
      if (result !== undefined) {
        return result;
      }
      let result_id = Module.hiwire.new_value(result_js);
      result = _JsProxy_create(result_id);
      Module.hiwire.decref(result_id);
      return result;
    } finally {
      context.depth++;
    }
  }

  /**
   * Convert a JavaScript object to Python to a given depth.
   */
  function js2python_convert(id, { depth, defaultConverter }) {
    let context = {
      cache: new Map(),
      depth,
      defaultConverter,
      // arguments for defaultConverter
      converter(x) {
        let id = Module.hiwire.new_value(x);
        try {
          return Module.pyproxy_new(
            js2python_convert_with_context(id, context)
          );
        } finally {
          Module.hiwire.decref(id);
        }
      },
      cacheConversion(input, output) {
        if (API.isPyProxy(output)) {
          context.cache.set(input, Module.PyProxy_getPtr(output));
        } else {
          throw new Error("Second argument should be a PyProxy!");
        }
      },
    };
    return js2python_convert_with_context(id, context);
  }

  Module.js2python_convert = js2python_convert;
});
