<commit_before>import reducerUtils from './utils/reducerUtils';
import constants, {prefix} from './constants';
import getIn from '@mzvonar/getin';
import setIn from '@mzvonar/setin';
import mergeIn from '@mzvonar/mergein';
import {validateInput} from './validators';

const isMRFAction = action => (
    action &&
    action.type &&
    action.type.length > prefix.length &&
    action.type.substring(0, prefix.length) === prefix
);

const createEmptyInputState = (config, initialValue, initialErrors) => {
    const state = {
        pristine: true,
        dirty: false,
        touched: false,
        valid: true,
        errors: null,
        validate: config.validate || [],
        asyncValidation: false,
        asyncErrors: [],
        initialErrors: initialErrors && (Object.keys(initialErrors).length > 0 ? initialErrors : null),
        value: initialValue
    };

    if(config.required === true) {
        state.validate.unshift('required');
    }

    return state;
};

const createEmtpyFormState = (state) => Object.assign({
    pristine: true,
    valid: true,
    touched: false,
    submitting: false,
    submitted: false,
    submitSuccess: false,
    asyncValidation: null,
    initialValues: {},
    initialFormErrors: null,
    initialInputErrors: null,
    submitError: null
}, state);

function registerForm(state, form, config = {}) {
    return setIn(state, [form], createEmtpyFormState({
        initialValues: config.initialValues || {},
        initialFormErrors: config.initialFormErrors && (config.initialFormErrors.length > 0 ? config.initialFormErrors : null),
        initialInputErrors: config.initialInputErrors
    }));
}

function removeForm(state, form) {
    // return null;
    const newState = Object.assign({}, state);
    delete newState[form];

    return newState;
}

function getAllValues(inputs) {
    const values = {};

    for(const key in inputs) {
        if(Object.prototype.hasOwnProperty.call(inputs, key)) {
            values[key] = inputs[key].value;
        }
    }

    return values;
}

function registerInput(state, name, config, initialValue, initialErrors) {
    const input = getIn(state, ['inputs', name], createEmptyInputState(config, initialValue, initialErrors));

    input.errors = validateInput(input, input.value, getAllValues(getIn(state, 'inputs')));
    input.valid = input.errors.length === 0;

    return setIn(state, ['inputs', name], input);
}

// TODO: remove input cant remove inout from state, it has to only mark it as disabled

function handleInputChange(state, touch, name, value) {
    const input = getIn(state, ['inputs', name]);

    const newValue = (value === '' && input.initialValue === undefined) ? undefined : value;
    const errors = validateInput(input, newValue, getAllValues(getIn(state, 'inputs')));
    const valid = !errors || errors.length === 0;

    return mergeIn(state, ['inputs', name], {
        valid,
        errors: errors.length > 0 ? errors : null,
        pristine: false,
        dirty: true,
        touched: touch ? true : input.touched,
        value: newValue,
        asyncErrors: []
    });
}

function handleInputBlur(state, touch, name) {
    const input = getIn(state, ['inputs', name]);

    let valid = input.valid;
    let errors = input.errors || [];

    if(touch) {
        errors = validateInput(input, input.value, getAllValues(getIn(state, 'inputs')));
        valid = errors.length === 0;
    }

    return mergeIn(state, ['inputs', name], {
        valid,
        errors,
        touched: touch ? true : input.touched
    });
}

function handleFormChange(state) {
    let valid = true;
    let pristine = true;
    let touched = false;

    for(const key in state.inputs) {
        if(Object.prototype.hasOwnProperty.call(state.inputs, key)) {
            const input = state.inputs[key];

            if(!input.valid) {
                valid = false;
            }
            if(!input.pristine) {
                pristine = false;
            }
            if(input.touched) {
                touched = true;
            }
        }
    }

    return mergeIn(state, {
        valid,
        pristine,
        touched
    });
}

function handleSubmitSuccess(state) {
    // In case form is already removed
    if(!state) {
        return state;
    }

    return mergeIn(state, {
        submitting: false,
        submitted: true,
        submitSuccess: true,
        valid: true,
        submitError: null
    });
}

function handleSubmitError(state, error) {
    state = mergeIn(state, {
        submitting: false,
        submitted: true,
        submitSuccess: false,
        submitError: error
    });

    if(error && error.inputs) {
        for(const key in error.inputs) {
            if(Object.prototype.hasOwnProperty.call(error.inputs, key)) {
                if(getIn(state, ['inputs', key])) {
                    let asyncErrors = error.inputs[key];

                    if(Object.prototype.toString.call(asyncErrors) !== '[object Array]') {
                        asyncErrors = [asyncErrors];
                    }

                    state = setIn(state, ['inputs', key, 'asyncErrors'], asyncErrors);
                }
            }
        }
    }

    return state;
}

function handleAsyncValidateStart(state, name) {
    let newState = setIn(state, 'asyncValidation', true);

    if(name) {
       newState = setIn(newState, ['inputs', name, 'asyncValidation'], true);
    }

    return newState;
}

function handleAsyncValidateFinished(state, name, errors) {
    let newState = setIn(state, 'asyncValidation', false);

    if(errors) {
        if(name) {
            errors = {
                [name]: errors[name]
            };
        }

        for(const key in errors) {
            if(Object.prototype.hasOwnProperty.call(errors, key) && getIn(newState, ['inputs', key])) {
                let inputErrors = errors[key];
                if(inputErrors) {
                    if(typeof inputErrors === 'string') {
                        inputErrors = [inputErrors];
                    }
                    if(Object.prototype.toString.call(inputErrors) !== '[object Array]') {
                        throw new Error(`Errors should be of type string or array. ${typeof inputErrors} was given`);
                    }
                }
                else {
                    inputErrors = [];
                }

                newState = mergeIn(newState, ['inputs', key], {
                    asyncValidation: false,
                    asyncErrors: inputErrors,
                    valid: inputErrors.length !== 0 ? false : getIn(newState, ['inputs', key, 'valid'])
                });
            }

        }
    }

    return newState;
}

const reducer = (state, action) => {
    switch(action.type) {
        case constants.REGISTER_INPUT:
            return handleFormChange(registerInput(state, action.payload.name, action.payload.config, action.payload.initialValue, action.payload.initialErrors));

        case constants.INPUT_CHANGE:
            return handleFormChange(handleInputChange(state, action.meta.touch, action.meta.name, action.payload.value));

        case constants.INPUT_BLUR:
            return handleFormChange(handleInputBlur(state, action.meta.touch, action.meta.name));

        case constants.SUBMITTING:
            return mergeIn(state, {
                submitting: true,
                submitError: null
            });

        case constants.SUBMIT_SUCCESS:
            return handleSubmitSuccess(state);

        case constants.SUBMIT_ERROR:
            return handleSubmitError(state, action.payload);

        case constants.ASYNC_VALIDATE_START:
            return handleAsyncValidateStart(state, action.meta.name);

        case constants.ASYNC_VALIDATE_FINISHED:
            return handleFormChange(handleAsyncValidateFinished(state, action.meta.name, action.payload));

        default:
            return state;
    }
};

const commonReducer = (state, action) => {
    switch(action.type) {
        case constants.REGISTER_FORM:
            return registerForm(state, action.meta.form, action.payload);

        case constants.REMOVE_FORM:
            if(action.meta && action.meta.form) {
                return removeForm(state, action.meta.form);
            }
            else {
                return state;
            }
            break;

        default:
            return state;
    }
};

const byForm = reducer => (state = {}, action = {}) => {
    const form = action && action.meta && action.meta.form;
    if (!form || !isMRFAction(action)) {
        return state
    }

    const formState = state[form];

    if(!formState) {
        return state;
    }

    const result = reducer(formState, action);

    return result === formState ? state : setIn(state, form, result);
};

export default reducerUtils.composeReducers(byForm(reducer), commonReducer);<commit_msg>input blur empty errors fix<commit_after>import reducerUtils from './utils/reducerUtils';
import constants, {prefix} from './constants';
import getIn from '@mzvonar/getin';
import setIn from '@mzvonar/setin';
import mergeIn from '@mzvonar/mergein';
import {validateInput} from './validators';

const isMRFAction = action => (
    action &&
    action.type &&
    action.type.length > prefix.length &&
    action.type.substring(0, prefix.length) === prefix
);

const createEmptyInputState = (config, initialValue, initialErrors) => {
    const state = {
        pristine: true,
        dirty: false,
        touched: false,
        valid: true,
        errors: null,
        validate: config.validate || [],
        asyncValidation: false,
        asyncErrors: [],
        initialErrors: initialErrors && (Object.keys(initialErrors).length > 0 ? initialErrors : null),
        value: initialValue
    };

    if(config.required === true) {
        state.validate.unshift('required');
    }

    return state;
};

const createEmtpyFormState = (state) => Object.assign({
    pristine: true,
    valid: true,
    touched: false,
    submitting: false,
    submitted: false,
    submitSuccess: false,
    asyncValidation: null,
    initialValues: {},
    initialFormErrors: null,
    initialInputErrors: null,
    submitError: null
}, state);

function registerForm(state, form, config = {}) {
    return setIn(state, [form], createEmtpyFormState({
        initialValues: config.initialValues || {},
        initialFormErrors: config.initialFormErrors && (config.initialFormErrors.length > 0 ? config.initialFormErrors : null),
        initialInputErrors: config.initialInputErrors
    }));
}

function removeForm(state, form) {
    // return null;
    const newState = Object.assign({}, state);
    delete newState[form];

    return newState;
}

function getAllValues(inputs) {
    const values = {};

    for(const key in inputs) {
        if(Object.prototype.hasOwnProperty.call(inputs, key)) {
            values[key] = inputs[key].value;
        }
    }

    return values;
}

function registerInput(state, name, config, initialValue, initialErrors) {
    const input = getIn(state, ['inputs', name], createEmptyInputState(config, initialValue, initialErrors));

    input.errors = validateInput(input, input.value, getAllValues(getIn(state, 'inputs')));
    input.valid = input.errors.length === 0;

    return setIn(state, ['inputs', name], input);
}

// TODO: remove input cant remove inout from state, it has to only mark it as disabled

function handleInputChange(state, touch, name, value) {
    const input = getIn(state, ['inputs', name]);

    const newValue = (value === '' && input.initialValue === undefined) ? undefined : value;
    const errors = validateInput(input, newValue, getAllValues(getIn(state, 'inputs')));
    const valid = !errors || errors.length === 0;

    return mergeIn(state, ['inputs', name], {
        valid,
        errors: errors.length > 0 ? errors : null,
        pristine: false,
        dirty: true,
        touched: touch ? true : input.touched,
        value: newValue,
        asyncErrors: []
    });
}

function handleInputBlur(state, touch, name) {
    const input = getIn(state, ['inputs', name]);

    let valid = input.valid;
    let errors = input.errors || [];

    if(touch) {
        errors = validateInput(input, input.value, getAllValues(getIn(state, 'inputs')));
        valid = errors.length === 0;
    }

    return mergeIn(state, ['inputs', name], {
        valid,
        errors: errors.length > 0 ? errors : null,
        touched: touch ? true : input.touched
    });
}

function handleFormChange(state) {
    let valid = true;
    let pristine = true;
    let touched = false;

    for(const key in state.inputs) {
        if(Object.prototype.hasOwnProperty.call(state.inputs, key)) {
            const input = state.inputs[key];

            if(!input.valid) {
                valid = false;
            }
            if(!input.pristine) {
                pristine = false;
            }
            if(input.touched) {
                touched = true;
            }
        }
    }

    return mergeIn(state, {
        valid,
        pristine,
        touched
    });
}

function handleSubmitSuccess(state) {
    // In case form is already removed
    if(!state) {
        return state;
    }

    return mergeIn(state, {
        submitting: false,
        submitted: true,
        submitSuccess: true,
        valid: true,
        submitError: null
    });
}

function handleSubmitError(state, error) {
    state = mergeIn(state, {
        submitting: false,
        submitted: true,
        submitSuccess: false,
        submitError: error
    });

    if(error && error.inputs) {
        for(const key in error.inputs) {
            if(Object.prototype.hasOwnProperty.call(error.inputs, key)) {
                if(getIn(state, ['inputs', key])) {
                    let asyncErrors = error.inputs[key];

                    if(Object.prototype.toString.call(asyncErrors) !== '[object Array]') {
                        asyncErrors = [asyncErrors];
                    }

                    state = setIn(state, ['inputs', key, 'asyncErrors'], asyncErrors);
                }
            }
        }
    }

    return state;
}

function handleAsyncValidateStart(state, name) {
    let newState = setIn(state, 'asyncValidation', true);

    if(name) {
       newState = setIn(newState, ['inputs', name, 'asyncValidation'], true);
    }

    return newState;
}

function handleAsyncValidateFinished(state, name, errors) {
    let newState = setIn(state, 'asyncValidation', false);

    if(errors) {
        if(name) {
            errors = {
                [name]: errors[name]
            };
        }

        for(const key in errors) {
            if(Object.prototype.hasOwnProperty.call(errors, key) && getIn(newState, ['inputs', key])) {
                let inputErrors = errors[key];
                if(inputErrors) {
                    if(typeof inputErrors === 'string') {
                        inputErrors = [inputErrors];
                    }
                    if(Object.prototype.toString.call(inputErrors) !== '[object Array]') {
                        throw new Error(`Errors should be of type string or array. ${typeof inputErrors} was given`);
                    }
                }
                else {
                    inputErrors = [];
                }

                newState = mergeIn(newState, ['inputs', key], {
                    asyncValidation: false,
                    asyncErrors: inputErrors,
                    valid: inputErrors.length !== 0 ? false : getIn(newState, ['inputs', key, 'valid'])
                });
            }

        }
    }

    return newState;
}

const reducer = (state, action) => {
    switch(action.type) {
        case constants.REGISTER_INPUT:
            return handleFormChange(registerInput(state, action.payload.name, action.payload.config, action.payload.initialValue, action.payload.initialErrors));

        case constants.INPUT_CHANGE:
            return handleFormChange(handleInputChange(state, action.meta.touch, action.meta.name, action.payload.value));

        case constants.INPUT_BLUR:
            return handleFormChange(handleInputBlur(state, action.meta.touch, action.meta.name));

        case constants.SUBMITTING:
            return mergeIn(state, {
                submitting: true,
                submitError: null
            });

        case constants.SUBMIT_SUCCESS:
            return handleSubmitSuccess(state);

        case constants.SUBMIT_ERROR:
            return handleSubmitError(state, action.payload);

        case constants.ASYNC_VALIDATE_START:
            return handleAsyncValidateStart(state, action.meta.name);

        case constants.ASYNC_VALIDATE_FINISHED:
            return handleFormChange(handleAsyncValidateFinished(state, action.meta.name, action.payload));

        default:
            return state;
    }
};

const commonReducer = (state, action) => {
    switch(action.type) {
        case constants.REGISTER_FORM:
            return registerForm(state, action.meta.form, action.payload);

        case constants.REMOVE_FORM:
            if(action.meta && action.meta.form) {
                return removeForm(state, action.meta.form);
            }
            else {
                return state;
            }
            break;

        default:
            return state;
    }
};

const byForm = reducer => (state = {}, action = {}) => {
    const form = action && action.meta && action.meta.form;
    if (!form || !isMRFAction(action)) {
        return state
    }

    const formState = state[form];

    if(!formState) {
        return state;
    }

    const result = reducer(formState, action);

    return result === formState ? state : setIn(state, form, result);
};

export default reducerUtils.composeReducers(byForm(reducer), commonReducer);<|endoftext|>


<commit_before><commit_msg>Version 0.3.3<commit_after><|endoftext|>


<commit_before><commit_msg>fix(http): secure /values route with jwt token<commit_after><|endoftext|>


<commit_before><commit_msg>Removed use of container because it causes performance degradation in browsers<commit_after><|endoftext|>


<commit_before>/**
 * 每位工程师都有保持代码优雅的义务
 * Each engineer has a duty to keep the code elegant
**/

const request = require('superagent');
const debug = require('debug')('request');
const md5 = require('md5');
const Promise = require('./promise');
const Cache = require('./cache');
const AVError = require('./error');
const AV = require('./av');

// 计算 X-LC-Sign 的签名方法
const sign = (key, isMasterKey) => {
  const now = new Date().getTime();
  const signature = md5(now + key);
  if (isMasterKey) {
    return `${signature},${now},master`;
  }
  return `${signature},${now}`;
};

const checkRouter = (router) => {
  const routerList = [
    'batch',
    'classes',
    'files',
    'date',
    'functions',
    'call',
    'login',
    'push',
    'search/select',
    'requestPasswordReset',
    'requestEmailVerify',
    'requestPasswordResetBySmsCode',
    'resetPasswordBySmsCode',
    'requestMobilePhoneVerify',
    'requestLoginSmsCode',
    'verifyMobilePhone',
    'requestSmsCode',
    'verifySmsCode',
    'users',
    'usersByMobilePhone',
    'cloudQuery',
    'qiniu',
    'fileTokens',
    'statuses',
    'bigquery',
    'search/select',
    'subscribe/statuses/count',
    'subscribe/statuses',
    'installations',
  ];

  if (routerList.indexOf(router) === -1 &&
    !(/users\/[^\/]+\/updatePassword/.test(router)) &&
    !(/users\/[^\/]+\/friendship\/[^\/]+/.test(router))
  ) {
    throw new Error(`Bad router: ${router}.`);
  }
};

const ajax = (method, resourceUrl, data, headers = {}, onprogress) => {
  debug(method, resourceUrl, data, headers);

  const promise = new Promise();
  const req = request(method, resourceUrl)
    .set(headers)
    .send(data)
    .end((err, res) => {
      if (res) {
        debug(res.status, res.body, res.text);
      }
      if (err) {
        if (res) {
          err.statusCode = res.status;
          err.responseText = res.text;
          err.response = res.body;
        }
        return promise.reject(err);
      }
      return promise.resolve(res.body, res.status, res);
    });
  if (onprogress) {
    req.on('progress', onprogress);
  }
  return promise;
};

const setHeaders = (sessionToken) => {
  const headers = {
    'X-LC-Id': AV.applicationId,
    'Content-Type': 'application/json;charset=UTF-8',
  };
  if (AV.masterKey && AV._useMasterKey) {
    headers['X-LC-Sign'] = sign(AV.masterKey, true);
  } else {
    headers['X-LC-Sign'] = sign(AV.applicationKey);
  }
  if (!AV._config.applicationProduction === null) {
    headers['X-LC-Prod'] = AV._config.applicationProduction;
  }
  if (!AV._config.isNode) {
    headers['X-LC-UA'] = `AV/${AV.version}`;
  } else {
    // LeanEngine need use AV._config.userAgent
    headers['User-Agent'] = AV._config.userAgent || `AV/${AV.version}; Node.js/${process.version}`;
  }

  const promise = new Promise();

  // Pass the session token
  if (sessionToken) {
    headers['X-LC-Session'] = sessionToken;
    promise.resolve(headers);
  } else if (!AV._config.disableCurrentUser) {
    AV.User.currentAsync().then((currentUser) => {
      if (currentUser && currentUser._sessionToken) {
        headers['X-LC-Session'] = currentUser._sessionToken;
      }
      promise.resolve(headers);
    });
  } else {
    promise.resolve(headers);
  }

  return promise;
};

const createApiUrl = (route, className, objectId, method, dataObject) => {
  // TODO: 兼容 AV.serverURL 旧方式设置 API Host，后续去掉
  if (AV.serverURL) {
    AV._config.APIServerURL = AV.serverURL;
    console.warn('Please use AV._config.APIServerURL to replace AV.serverURL, and it is an internal interface.');
  }

  let apiURL = AV._config.APIServerURL;

  if (apiURL.charAt(apiURL.length - 1) !== '/') {
    apiURL += '/';
  }
  apiURL += `1.1/${route}`;
  if (className) {
    apiURL += `/${className}`;
  }
  if (objectId) {
    apiURL += `/${objectId}`;
  }
  if ((route === 'users' || route === 'classes') && dataObject) {
    apiURL += '?';
    if (dataObject._fetchWhenSave) {
      delete dataObject._fetchWhenSave;
      apiURL += '&new=true';
    }
    if (dataObject._where) {
      apiURL += `&where=${encodeURIComponent(JSON.stringify(dataObject._where))}`;
      delete dataObject._where;
    }
  }

  if (method.toLowerCase() === 'get') {
    if (apiURL.indexOf('?') === -1) {
      apiURL += '?';
    }
    for (const k in dataObject) {
      if (typeof dataObject[k] === 'object') {
        dataObject[k] = JSON.stringify(dataObject[k]);
      }
      apiURL += `&${k}=${encodeURIComponent(dataObject[k])}`;
    }
  }

  return apiURL;
};

const cacheServerURL = (serverURL, ttl) => {
  if (typeof ttl !== 'number') {
    ttl = 3600;
  }
  Cache.set('APIServerURL', serverURL, ttl * 1000);
};

// handle AV._request Error
const handleError = (res) => {
  const promise = new Promise();
  /**
    When API request need to redirect to the right location,
    can't use browser redirect by http status 307, as the reason of CORS,
    so API server response http status 410 and the param "location" for this case.
  */
  if (res.statusCode === 410) {
    cacheServerURL(res.response.api_server, res.response.ttl);
    promise.resolve(res.response.location);
  } else {
    let errorJSON = { code: -1, error: res.responseText };
    if (res.response && res.response.code) {
      errorJSON = res.response;
    } else if (res.responseText) {
      try {
        errorJSON = JSON.parse(res.responseText);
      } catch (e) {
        // If we fail to parse the error text, that's okay.
      }
    }

    // Transform the error into an instance of AVError by trying to parse
    // the error string as JSON.
    const error = new AVError(errorJSON.code, errorJSON.error);
    promise.reject(error);
  }
  return promise;
};

const setServerUrlByRegion = (region = 'cn') => {
  // 服务器请求的节点 host
  const API_HOST = {
    cn: 'https://api.leancloud.cn',
    us: 'https://us-api.leancloud.cn',
  };

  const AVConfig = AV._config;
  AVConfig.region = region;
  // 如果用户在 init 之前设置了 APIServerURL，则跳过请求 router
  if (AVConfig.APIServerURL) {
    return;
  }
  AVConfig.APIServerURL = API_HOST[region];
  if (region === 'cn') {
    Cache.get('APIServerURL').then(cachedServerURL => {
      if (cachedServerURL) {
        return cachedServerURL;
      } else {
        return ajax('get', `https://app-router.leancloud.cn/1/route?appId=${AV.applicationId}`)
          .then(servers => {
            if (servers.api_server) {
              cacheServerURL(servers.api_server, servers.ttl);
              return servers.api_server;
            }
          });
      }
    }).then(serverURL => {
      // 如果用户在 init 之后设置了 APIServerURL，保持用户设置
      if (AVConfig.APIServerURL === API_HOST[region]) {
        AVConfig.APIServerURL = `https://${serverURL}`;
      }
    });
  }
};

/**
 * route is classes, users, login, etc.
 * objectId is null if there is no associated objectId.
 * method is the http method for the REST API.
 * dataObject is the payload as an object, or null if there is none.
 * @ignore
 */
const AVRequest = (route, className, objectId, method, dataObject = {}, sessionToken) => {
  if (!AV.applicationId) {
    throw new Error('You must specify your applicationId using AV.init()');
  }

  if (!AV.applicationKey && !AV.masterKey) {
    throw new Error('You must specify a AppKey using AV.init()');
  }

  checkRouter(route);
  const apiURL = createApiUrl(route, className, objectId, method, dataObject);

  return setHeaders(sessionToken).then(
    headers => ajax(method, apiURL, dataObject, headers)
      .then(
        null,
        res => handleError(res)
          .then(location => ajax(method, location, dataObject, headers))
      )
  );
};

module.exports = {
  ajax,
  request: AVRequest,
  setServerUrlByRegion,
};
<commit_msg>AV.applicationProduction -> AV._config.applicationProduction (#290)<commit_after>/**
 * 每位工程师都有保持代码优雅的义务
 * Each engineer has a duty to keep the code elegant
**/

const request = require('superagent');
const debug = require('debug')('request');
const md5 = require('md5');
const Promise = require('./promise');
const Cache = require('./cache');
const AVError = require('./error');
const AV = require('./av');

// 计算 X-LC-Sign 的签名方法
const sign = (key, isMasterKey) => {
  const now = new Date().getTime();
  const signature = md5(now + key);
  if (isMasterKey) {
    return `${signature},${now},master`;
  }
  return `${signature},${now}`;
};

const checkRouter = (router) => {
  const routerList = [
    'batch',
    'classes',
    'files',
    'date',
    'functions',
    'call',
    'login',
    'push',
    'search/select',
    'requestPasswordReset',
    'requestEmailVerify',
    'requestPasswordResetBySmsCode',
    'resetPasswordBySmsCode',
    'requestMobilePhoneVerify',
    'requestLoginSmsCode',
    'verifyMobilePhone',
    'requestSmsCode',
    'verifySmsCode',
    'users',
    'usersByMobilePhone',
    'cloudQuery',
    'qiniu',
    'fileTokens',
    'statuses',
    'bigquery',
    'search/select',
    'subscribe/statuses/count',
    'subscribe/statuses',
    'installations',
  ];

  if (routerList.indexOf(router) === -1 &&
    !(/users\/[^\/]+\/updatePassword/.test(router)) &&
    !(/users\/[^\/]+\/friendship\/[^\/]+/.test(router))
  ) {
    throw new Error(`Bad router: ${router}.`);
  }
};

const ajax = (method, resourceUrl, data, headers = {}, onprogress) => {
  debug(method, resourceUrl, data, headers);

  const promise = new Promise();
  const req = request(method, resourceUrl)
    .set(headers)
    .send(data)
    .end((err, res) => {
      if (res) {
        debug(res.status, res.body, res.text);
      }
      if (err) {
        if (res) {
          err.statusCode = res.status;
          err.responseText = res.text;
          err.response = res.body;
        }
        return promise.reject(err);
      }
      return promise.resolve(res.body, res.status, res);
    });
  if (onprogress) {
    req.on('progress', onprogress);
  }
  return promise;
};

const setHeaders = (sessionToken) => {
  const headers = {
    'X-LC-Id': AV.applicationId,
    'Content-Type': 'application/json;charset=UTF-8',
  };
  if (AV.masterKey && AV._useMasterKey) {
    headers['X-LC-Sign'] = sign(AV.masterKey, true);
  } else {
    headers['X-LC-Sign'] = sign(AV.applicationKey);
  }
  if (AV._config.applicationProduction !== null) {
    headers['X-LC-Prod'] = AV._config.applicationProduction;
  }
  if (!AV._config.isNode) {
    headers['X-LC-UA'] = `AV/${AV.version}`;
  } else {
    // LeanEngine need use AV._config.userAgent
    headers['User-Agent'] = AV._config.userAgent || `AV/${AV.version}; Node.js/${process.version}`;
  }

  const promise = new Promise();

  // Pass the session token
  if (sessionToken) {
    headers['X-LC-Session'] = sessionToken;
    promise.resolve(headers);
  } else if (!AV._config.disableCurrentUser) {
    AV.User.currentAsync().then((currentUser) => {
      if (currentUser && currentUser._sessionToken) {
        headers['X-LC-Session'] = currentUser._sessionToken;
      }
      promise.resolve(headers);
    });
  } else {
    promise.resolve(headers);
  }

  return promise;
};

const createApiUrl = (route, className, objectId, method, dataObject) => {
  // TODO: 兼容 AV.serverURL 旧方式设置 API Host，后续去掉
  if (AV.serverURL) {
    AV._config.APIServerURL = AV.serverURL;
    console.warn('Please use AV._config.APIServerURL to replace AV.serverURL, and it is an internal interface.');
  }

  let apiURL = AV._config.APIServerURL;

  if (apiURL.charAt(apiURL.length - 1) !== '/') {
    apiURL += '/';
  }
  apiURL += `1.1/${route}`;
  if (className) {
    apiURL += `/${className}`;
  }
  if (objectId) {
    apiURL += `/${objectId}`;
  }
  if ((route === 'users' || route === 'classes') && dataObject) {
    apiURL += '?';
    if (dataObject._fetchWhenSave) {
      delete dataObject._fetchWhenSave;
      apiURL += '&new=true';
    }
    if (dataObject._where) {
      apiURL += `&where=${encodeURIComponent(JSON.stringify(dataObject._where))}`;
      delete dataObject._where;
    }
  }

  if (method.toLowerCase() === 'get') {
    if (apiURL.indexOf('?') === -1) {
      apiURL += '?';
    }
    for (const k in dataObject) {
      if (typeof dataObject[k] === 'object') {
        dataObject[k] = JSON.stringify(dataObject[k]);
      }
      apiURL += `&${k}=${encodeURIComponent(dataObject[k])}`;
    }
  }

  return apiURL;
};

const cacheServerURL = (serverURL, ttl) => {
  if (typeof ttl !== 'number') {
    ttl = 3600;
  }
  Cache.set('APIServerURL', serverURL, ttl * 1000);
};

// handle AV._request Error
const handleError = (res) => {
  const promise = new Promise();
  /**
    When API request need to redirect to the right location,
    can't use browser redirect by http status 307, as the reason of CORS,
    so API server response http status 410 and the param "location" for this case.
  */
  if (res.statusCode === 410) {
    cacheServerURL(res.response.api_server, res.response.ttl);
    promise.resolve(res.response.location);
  } else {
    let errorJSON = { code: -1, error: res.responseText };
    if (res.response && res.response.code) {
      errorJSON = res.response;
    } else if (res.responseText) {
      try {
        errorJSON = JSON.parse(res.responseText);
      } catch (e) {
        // If we fail to parse the error text, that's okay.
      }
    }

    // Transform the error into an instance of AVError by trying to parse
    // the error string as JSON.
    const error = new AVError(errorJSON.code, errorJSON.error);
    promise.reject(error);
  }
  return promise;
};

const setServerUrlByRegion = (region = 'cn') => {
  // 服务器请求的节点 host
  const API_HOST = {
    cn: 'https://api.leancloud.cn',
    us: 'https://us-api.leancloud.cn',
  };

  const AVConfig = AV._config;
  AVConfig.region = region;
  // 如果用户在 init 之前设置了 APIServerURL，则跳过请求 router
  if (AVConfig.APIServerURL) {
    return;
  }
  AVConfig.APIServerURL = API_HOST[region];
  if (region === 'cn') {
    Cache.get('APIServerURL').then(cachedServerURL => {
      if (cachedServerURL) {
        return cachedServerURL;
      } else {
        return ajax('get', `https://app-router.leancloud.cn/1/route?appId=${AV.applicationId}`)
          .then(servers => {
            if (servers.api_server) {
              cacheServerURL(servers.api_server, servers.ttl);
              return servers.api_server;
            }
          });
      }
    }).then(serverURL => {
      // 如果用户在 init 之后设置了 APIServerURL，保持用户设置
      if (AVConfig.APIServerURL === API_HOST[region]) {
        AVConfig.APIServerURL = `https://${serverURL}`;
      }
    });
  }
};

/**
 * route is classes, users, login, etc.
 * objectId is null if there is no associated objectId.
 * method is the http method for the REST API.
 * dataObject is the payload as an object, or null if there is none.
 * @ignore
 */
const AVRequest = (route, className, objectId, method, dataObject = {}, sessionToken) => {
  if (!AV.applicationId) {
    throw new Error('You must specify your applicationId using AV.init()');
  }

  if (!AV.applicationKey && !AV.masterKey) {
    throw new Error('You must specify a AppKey using AV.init()');
  }

  checkRouter(route);
  const apiURL = createApiUrl(route, className, objectId, method, dataObject);

  return setHeaders(sessionToken).then(
    headers => ajax(method, apiURL, dataObject, headers)
      .then(
        null,
        res => handleError(res)
          .then(location => ajax(method, location, dataObject, headers))
      )
  );
};

module.exports = {
  ajax,
  request: AVRequest,
  setServerUrlByRegion,
};
<|endoftext|>


<commit_before>import {identity} from "./util.js";

//Export math constants
export const pi = Math.PI;
export const tau = 2 * Math.PI;
export const epsilon = Number.EPSILON;

//Restrict the value to be in the [min, max] range
export function clamp (value, min, max) {
    if (value < min) {
        return min;
    }
    else if (value > max) {
        return max;
    }
    //Default: return the value
    return value;
}

//Generate an array with an algorithmic progression
//Based on https://docs.python.org/3/library/stdtypes.html#range
export function range (start, end, step) {
    //Check for no start value
    if (typeof start !== "number") {
        return [];
    }
    //Check for no end value
    if (typeof end !== "number") {
        end = start;
        start = 0;
        // Same as calling return range(0, start, 1);
    }
    //Check if start < end and if start is not negative
    if (0 <= start && start < end) {
        //Check if step is not defined or if step is a negative or zero number
        if (typeof step !== "number" || step <= 0) {
            step = 1;
        }
        //Calculate the number of elements of the new array
        let count = Math.floor((end - start) / step);
        return Array(count).fill().map(function (value, index) {
            return start + (index * step);
        });
    }
    else {
        //Start or end values not valid, return an empty array
        return [];
    }
}

//Returns the q-quantile value of a given SORTED list of numbers
//https://en.wikipedia.org/wiki/Quantile
//https://en.wikipedia.org/wiki/Quantile#Estimating_the_quantiles_of_a_population
export function quantile (q, values, valueOf) {
    //Check for undefined valueOf method
    if (typeof valueOf !== "function") {
        valueOf = identity; //Save it as a function that returns the same value
    }
    //Check for no values
    if (values.length === 0) {
        return null;
    }
    //Check for negative values of q or for more than 2 values in the list
    if (q <= 0 || values.length < 2) {
        return valueOf(values[0]);
    }
    //Check for numbers of q >= 1
    if (q >= 1) {
        return valueOf(values[values.length -1]);
    }
    //Calculate tue quantile
    let h = (values.length - 1) * q;
    let rh = Math.floor(h);
    return valueOf(values[rh]) + (valueOf(values[rh + 1]) - valueOf(values[rh])) * (h - rh);
}

//Calculate the hypotenuse without danger of overflow in taking the square root
// hypo = sqrt(max*max + min*min)
// hypo = sqrt(max*max*(1 + min*min/(max*max)))
// hypo = max*sqrt(1 + min*min/(max*max))
//Inspired in from https://www.johndcook.com/blog/2010/06/02/whats-so-hard-about-finding-a-hypotenuse/
export function hypo (x, y) {
    let max = Math.max(Math.abs(x), Math.abs(y));
    let min = Math.min(Math.abs(x), Math.abs(y));
    let division = min / max;
    return max * Math.sqrt(1 + division*division);
}

//Get the X coordinate of the conversion from polar to cartesian
export function polarToCartesianX (radius, angle, center) {
    return center + radius * Math.cos(angle);
}

//Get the Y coordinate of the conversion from polar to cartesian
export function polarToCartesianY (radius, angle, center) {
    return center + radius * Math.sin(angle);
}

//Convert polar to cartesian
export function polarToCartesian(radius, angle, centerX, centerY) {
    //Return the conversion to cartesian coordinates
    return {
        "x": polarToCartesianX(radius, angle, centerX),
        "y": polarToCartesianY(radius, angle, centerY)
    };
}

//Convert cartesian to polar
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2
export function cartesianToPolar(x, y, centerX, centerY) {
    //Check for no center point provided
    if (typeof centerX !== "number" || typeof centerY !== "number") {
        centerX = 0; centerY = 0;
    }
    //Return the polar coordinates
    return {
        "radius": hypo(x - center.x, y - center.y),
        "angle": Math.atan2(y - center.y, x - center.x)
    };
}

//Format size prefixes
let formatSizePrefix = ["", "K", "M", "G", "T", "P"];

//Format a number
export function format (value, digits) {
    //Check for no digits provided
    if (typeof digits !== "number") {
        digits = 2;
    }
    //Get the size prefix index
    let i = parseInt(Math.floor(Math.log(value) / Math.log(1000)));
    //Return the formated value
    return (value / Math.pow(1000, i)).toFixed(digits) + "" + formatSizePrefix[i];
}

//Returns a random number between `min` and `max` (not included)
//If this function is called only with one argumet, it returns a random number between `0` and that number.
export function random (min, max) {
    if (typeof min !== "number") {
        return Math.random(); //Return a random number between 0 and 1
    }
    else if (typeof max !== "number") {
        return Math.random() * min; //Use min as the max value
    }
    //Default
    return min + Math.random()*(max - min);
}

//Nice number for Heckbert algorithm
export function niceNumber (x, round) {
    let exp = Math.floor(Math.log10(x));
    let f = x / Math.pow(10, exp);
    let nf = 0;
    if (round === true) {
        nf = (f < 1.5) ? 1 : ((f < 3) ? 2 : ((f < 7) ? 5 : 10));
    }
    else  {
        nf = (f <= 1) ? 1 : ((f <= 2) ? 2 : ((f <= 5) ? 5 : 10));
    }
    //Return the nice number
    return nf * Math.pow(10, exp);
}

//Generate values in the provided range using the Heckbert algorithm
export function ticks (start, end, n, tight) {
    //Check for the same range values
    if (start === end) {
        return [start];
    }
    //Check if end < start --> call this method with the reversed arguments
    if (end < start) {
        return ticks(end, start, n, tight);
    }
    let range = niceNumber(end - start, false);
    let step = niceNumber(range / (n - 1), true); //Ticks separation
    let ticksStart = Math.floor(start / step) * step; //Ticks start
    let ticksEnd = Math.ceil(end / step) * step; //Ticks end
    let nfrac = Math.max(-Math.floor(Math.log10(step)), 0); //number of fractional digits to show;
    let ticksValues = []; //Output ticks values
    for (let value = ticksStart; value <= ticksEnd; value = value + step) {
        ticksValues.push(parseFloat(value.toFixed(8)));
    }
    //Check for tight option --> remove ticks outside of the [start, end] interval
    //and add start and end values
    if (typeof tight === "boolean" && tight === true) {
        ticksValues.filter(function (value) {
            return start < value && value < end;
        });
        //Insert start and end values
        ticksValues.unshift(start); //Insert start as the first item
        ticksValues.push(end); //Insert end as the last item
    }
    //Return ticks values
    return ticksValues;
}

//Sort an array of objects
export function sort (list, keys, order) {
    return list.sort(function (a, b) {
        for(let i = 0; i < keys.length; i++) {
            let key = keys[i]; //Get list field
            if (typeof a[key] === "undefined" || typeof b[key] === "undefined") {
                continue; //Undefined value
            }
            //Check for the same value
            if (a[key] === b[key]) {
                continue; //Use next field
            }
            let isNum = !isNaN(+a[key] - +b[key]);
            let aValue = (isNum === true) ? +a[key] : a[key].toLowerCase();
            let bValue = (isNum === true) ? +b[key] : b[key].toLowerCase();
            if (aValue < bValue) {
                return (order[i] === "asc") ? -1 : 1;
            }
            else if (aValue > bValue) {
                return (order[i] === "asc") ? 1 : -1;
            }
        }
        return 0; //Default --> return 0
    });
}

//Find all divisors of a number
export function divisors (n) {
    let result = []; //List with all divisors
    let m = Math.ceil(n / 2);
    for (let i = 1; i <= m; i++) {
        if (n % i === 0) {
            result.push([i, n / i]);
        }
    }
    //Return list of divisors
    return result;
}

//Logaritmic function
export function log (x, base) {
    return Math.log(x) / Math.log(base);
}

//Check if a number is integer
export function isInteger (value) {
    return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
}

//Check if a number is natural
export function isNatural (value) {
    return value >= 0 && isInteger(value);
}

//Parse a nth number
let nthValue = function (sign, value) {
    if (typeof value === "undefined") {
        return 0; //No value to parse
    }
    else if (value === "") {
        value = 1; //It matches, but there is no value --> use 1
    }
    //Default --> parse the value and the sign
    return (sign === "-") ? ((-1) * parseInt(value)) : parseInt(value);
};

//Parse a nth expression (an + b)
let nthExpr = /^(?:([+\-]?)\s*(\d*)n)?\s*(?:([+\-]?)\s*(\d+))?$/; //To check for an expression
export function nthParse (expr) {
    expr = expr.trim().toLowerCase();
    let match = null;
    if (expr === "*") {
        return [1, 0]; //Catch all
    }
    else if (expr === "even") {
        return [2, 0]; //Return for even nth checks
    }
    else if (expr === "odd") {
        return [2, 1]; //Return for odd nth checks
    }
    else if (expr === "n") {
        return [1, 0]; //Return a catch-all
    }
    else if ((match = expr.match(nthExpr)) !== null) {
        return [nthValue(match[1], match[2]), nthValue(match[3], match[4])];
    }
    //Default --> not valid expression
    throw new SyntaxError(`Invalid nth expression ('${expr}')`);
}

//Check a nth expression
export function nthCheck (value, attr) {
    let a = attr[0], b = attr[1];
    //If a === 0 --> only check the b value
    if (a === 0) {
        return b === value;
    }
    //Default --> check if (value - b) / a is a positive integer
    return isNatural((value - b) / a);
}

//Parse + check a nth expression
export function nth (value, expr) {
    return nthCheck(value, nthParse(expr));
}

<commit_msg>math: added binary search tree manager<commit_after>import {identity} from "./util.js";

//Export math constants
export const pi = Math.PI;
export const tau = 2 * Math.PI;
export const epsilon = Number.EPSILON;

//Restrict the value to be in the [min, max] range
export function clamp (value, min, max) {
    if (value < min) {
        return min;
    }
    else if (value > max) {
        return max;
    }
    //Default: return the value
    return value;
}

//Generate an array with an algorithmic progression
//Based on https://docs.python.org/3/library/stdtypes.html#range
export function range (start, end, step) {
    //Check for no start value
    if (typeof start !== "number") {
        return [];
    }
    //Check for no end value
    if (typeof end !== "number") {
        end = start;
        start = 0;
        // Same as calling return range(0, start, 1);
    }
    //Check if start < end and if start is not negative
    if (0 <= start && start < end) {
        //Check if step is not defined or if step is a negative or zero number
        if (typeof step !== "number" || step <= 0) {
            step = 1;
        }
        //Calculate the number of elements of the new array
        let count = Math.floor((end - start) / step);
        return Array(count).fill().map(function (value, index) {
            return start + (index * step);
        });
    }
    else {
        //Start or end values not valid, return an empty array
        return [];
    }
}

//Returns the q-quantile value of a given SORTED list of numbers
//https://en.wikipedia.org/wiki/Quantile
//https://en.wikipedia.org/wiki/Quantile#Estimating_the_quantiles_of_a_population
export function quantile (q, values, valueOf) {
    //Check for undefined valueOf method
    if (typeof valueOf !== "function") {
        valueOf = identity; //Save it as a function that returns the same value
    }
    //Check for no values
    if (values.length === 0) {
        return null;
    }
    //Check for negative values of q or for more than 2 values in the list
    if (q <= 0 || values.length < 2) {
        return valueOf(values[0]);
    }
    //Check for numbers of q >= 1
    if (q >= 1) {
        return valueOf(values[values.length -1]);
    }
    //Calculate tue quantile
    let h = (values.length - 1) * q;
    let rh = Math.floor(h);
    return valueOf(values[rh]) + (valueOf(values[rh + 1]) - valueOf(values[rh])) * (h - rh);
}

//Calculate the hypotenuse without danger of overflow in taking the square root
// hypo = sqrt(max*max + min*min)
// hypo = sqrt(max*max*(1 + min*min/(max*max)))
// hypo = max*sqrt(1 + min*min/(max*max))
//Inspired in from https://www.johndcook.com/blog/2010/06/02/whats-so-hard-about-finding-a-hypotenuse/
export function hypo (x, y) {
    let max = Math.max(Math.abs(x), Math.abs(y));
    let min = Math.min(Math.abs(x), Math.abs(y));
    let division = min / max;
    return max * Math.sqrt(1 + division*division);
}

//Get the X coordinate of the conversion from polar to cartesian
export function polarToCartesianX (radius, angle, center) {
    return center + radius * Math.cos(angle);
}

//Get the Y coordinate of the conversion from polar to cartesian
export function polarToCartesianY (radius, angle, center) {
    return center + radius * Math.sin(angle);
}

//Convert polar to cartesian
export function polarToCartesian(radius, angle, centerX, centerY) {
    //Return the conversion to cartesian coordinates
    return {
        "x": polarToCartesianX(radius, angle, centerX),
        "y": polarToCartesianY(radius, angle, centerY)
    };
}

//Convert cartesian to polar
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2
export function cartesianToPolar(x, y, centerX, centerY) {
    //Check for no center point provided
    if (typeof centerX !== "number" || typeof centerY !== "number") {
        centerX = 0; centerY = 0;
    }
    //Return the polar coordinates
    return {
        "radius": hypo(x - center.x, y - center.y),
        "angle": Math.atan2(y - center.y, x - center.x)
    };
}

//Format size prefixes
let formatSizePrefix = ["", "K", "M", "G", "T", "P"];

//Format a number
export function format (value, digits) {
    //Check for no digits provided
    if (typeof digits !== "number") {
        digits = 2;
    }
    //Get the size prefix index
    let i = parseInt(Math.floor(Math.log(value) / Math.log(1000)));
    //Return the formated value
    return (value / Math.pow(1000, i)).toFixed(digits) + "" + formatSizePrefix[i];
}

//Returns a random number between `min` and `max` (not included)
//If this function is called only with one argumet, it returns a random number between `0` and that number.
export function random (min, max) {
    if (typeof min !== "number") {
        return Math.random(); //Return a random number between 0 and 1
    }
    else if (typeof max !== "number") {
        return Math.random() * min; //Use min as the max value
    }
    //Default
    return min + Math.random()*(max - min);
}

//Nice number for Heckbert algorithm
export function niceNumber (x, round) {
    let exp = Math.floor(Math.log10(x));
    let f = x / Math.pow(10, exp);
    let nf = 0;
    if (round === true) {
        nf = (f < 1.5) ? 1 : ((f < 3) ? 2 : ((f < 7) ? 5 : 10));
    }
    else  {
        nf = (f <= 1) ? 1 : ((f <= 2) ? 2 : ((f <= 5) ? 5 : 10));
    }
    //Return the nice number
    return nf * Math.pow(10, exp);
}

//Generate values in the provided range using the Heckbert algorithm
export function ticks (start, end, n, tight) {
    //Check for the same range values
    if (start === end) {
        return [start];
    }
    //Check if end < start --> call this method with the reversed arguments
    if (end < start) {
        return ticks(end, start, n, tight);
    }
    let range = niceNumber(end - start, false);
    let step = niceNumber(range / (n - 1), true); //Ticks separation
    let ticksStart = Math.floor(start / step) * step; //Ticks start
    let ticksEnd = Math.ceil(end / step) * step; //Ticks end
    let nfrac = Math.max(-Math.floor(Math.log10(step)), 0); //number of fractional digits to show;
    let ticksValues = []; //Output ticks values
    for (let value = ticksStart; value <= ticksEnd; value = value + step) {
        ticksValues.push(parseFloat(value.toFixed(8)));
    }
    //Check for tight option --> remove ticks outside of the [start, end] interval
    //and add start and end values
    if (typeof tight === "boolean" && tight === true) {
        ticksValues.filter(function (value) {
            return start < value && value < end;
        });
        //Insert start and end values
        ticksValues.unshift(start); //Insert start as the first item
        ticksValues.push(end); //Insert end as the last item
    }
    //Return ticks values
    return ticksValues;
}

//Sort an array of objects
export function sort (list, keys, order) {
    return list.sort(function (a, b) {
        for(let i = 0; i < keys.length; i++) {
            let key = keys[i]; //Get list field
            if (typeof a[key] === "undefined" || typeof b[key] === "undefined") {
                continue; //Undefined value
            }
            //Check for the same value
            if (a[key] === b[key]) {
                continue; //Use next field
            }
            let isNum = !isNaN(+a[key] - +b[key]);
            let aValue = (isNum === true) ? +a[key] : a[key].toLowerCase();
            let bValue = (isNum === true) ? +b[key] : b[key].toLowerCase();
            if (aValue < bValue) {
                return (order[i] === "asc") ? -1 : 1;
            }
            else if (aValue > bValue) {
                return (order[i] === "asc") ? 1 : -1;
            }
        }
        return 0; //Default --> return 0
    });
}

//Find all divisors of a number
export function divisors (n) {
    let result = []; //List with all divisors
    let m = Math.ceil(n / 2);
    for (let i = 1; i <= m; i++) {
        if (n % i === 0) {
            result.push([i, n / i]);
        }
    }
    //Return list of divisors
    return result;
}

//Logaritmic function
export function log (x, base) {
    return Math.log(x) / Math.log(base);
}

//Check if a number is integer
export function isInteger (value) {
    return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
}

//Check if a number is natural
export function isNatural (value) {
    return value >= 0 && isInteger(value);
}

//Parse a nth number
let nthValue = function (sign, value) {
    if (typeof value === "undefined") {
        return 0; //No value to parse
    }
    else if (value === "") {
        value = 1; //It matches, but there is no value --> use 1
    }
    //Default --> parse the value and the sign
    return (sign === "-") ? ((-1) * parseInt(value)) : parseInt(value);
};

//Parse a nth expression (an + b)
let nthExpr = /^(?:([+\-]?)\s*(\d*)n)?\s*(?:([+\-]?)\s*(\d+))?$/; //To check for an expression
export function nthParse (expr) {
    expr = expr.trim().toLowerCase();
    let match = null;
    if (expr === "*") {
        return [1, 0]; //Catch all
    }
    else if (expr === "even") {
        return [2, 0]; //Return for even nth checks
    }
    else if (expr === "odd") {
        return [2, 1]; //Return for odd nth checks
    }
    else if (expr === "n") {
        return [1, 0]; //Return a catch-all
    }
    else if ((match = expr.match(nthExpr)) !== null) {
        return [nthValue(match[1], match[2]), nthValue(match[3], match[4])];
    }
    //Default --> not valid expression
    throw new SyntaxError(`Invalid nth expression ('${expr}')`);
}

//Check a nth expression
export function nthCheck (value, attr) {
    let a = attr[0], b = attr[1];
    //If a === 0 --> only check the b value
    if (a === 0) {
        return b === value;
    }
    //Default --> check if (value - b) / a is a positive integer
    return isNatural((value - b) / a);
}

//Parse + check a nth expression
export function nth (value, expr) {
    return nthCheck(value, nthParse(expr));
}

//Binary search tree
let BinarySearchTreeHandler = function () {
    this.root = null; //Initial node
};

//Prototype
BinarySearchTreeHandler.prototype = {
    //Add a new node to the tree
    "add": function (value) {
        let node = {"value": value, "left": null, "right": null};
        if (this.root === null) {
            this.root = node;
            return true; //Node has been inserted
        }
        //Search in the tree
        let current = this.root;
        while (true) {
            //Check if there is no
            if (node.value.end < current.value.start) {
                if (current.left === null) {
                    current.left = node;
                    return true;
                }
                //Now check the left node
                current = current.left;
            }
            else if (current.value.end < node.value.start) {
                if (current.right === null) {
                    current.right = node;
                    return true;
                }
                //Now check the right side
                current = current.right;
            }
            else {
                //This node intersects the current node --> reject
                return false;
            }
        }
    }
};

//Create a new binary search tree
export function binarySearchTree () {
    return new BinarySearchTreeHandler();
}

<|endoftext|>


<commit_before>"use strict";

var m    = require("mithril"),
    data = require("./data");

module.exports = function() {
    return [
        m("a.menu-link[href='#menu'][id='menuLink']",
            m("span")
        ),
        m("[id='menu']",
            m(".pure-menu",
                m("a.pure-menu-heading[href='#']", "Falco"),
                m("ul.pure-menu-list",
                    data.get("order").map(function(key) {
                        var list = data.get("lists")[key];

                        return m("li.pure-menu-item",
                            m("a.pure-menu-link", {
                                    href : "/lists/" + key,
                                    config : m.route
                                },
                                list.name + "(" + list.tweets.length + ")"
                            )
                        );
                    })
                )
            )
        )
    ];
};
<commit_msg>Spacing<commit_after>"use strict";

var m    = require("mithril"),
    data = require("./data");

module.exports = function() {
    return [
        m("a.menu-link[href='#menu'][id='menuLink']",
            m("span")
        ),
        m("[id='menu']",
            m(".pure-menu",
                m("a.pure-menu-heading[href='#']", "Falco"),
                m("ul.pure-menu-list",
                    data.get("order").map(function(key) {
                        var list = data.get("lists")[key];

                        return m("li.pure-menu-item",
                            m("a.pure-menu-link", {
                                    href : "/lists/" + key,
                                    config : m.route
                                },
                                list.name + " (" + list.tweets.length + ")"
                            )
                        );
                    })
                )
            )
        )
    ];
};
<|endoftext|>


<commit_before>// Display modal text.

import { renderUI } from 'lib/cEl';
import { createRectangle } from 'lib/geometry';
import { inputs } from 'controls';
import { canvas, ctx, viewHeight, viewWidth } from 'dom';

// Menu box traits.
// Flexible size.
const stroke = 2;
let menuWidth = 0;
let menuHeight = 0;
const strokeColor = `#fff`;
const bgColor = `#00f`;

// Text traits.
const textSize = 12;
const lineHeight = textSize * 1.2;
const textWidth = menuWidth - stroke * 4;
const textHeight = menuHeight - stroke * 4;
const textColor = `#fff`;

const Command = (label, handler)=> {
  menuWidth = Math.max(menuWidth, ctx.measureText(label).width);

  return {
    geometry: createRectangle([viewWidth / 2, viewHeight - menuHeight / 2], 0, menuWidth, menuHeight),

    render: ({ fillText }, { geometry })=> {
      fillText({ style: textColor }, [0, 0], label);
    },

    interact: {
      onMouseDown: handler
    }
  };
};

export class Menu {
  constructor() {
    this.commands = new Set();
  }

  add(label, handler) {
    menuHeight += lineHeight;
    const cmd = Command(label, handler);
    this.commands.add(cmd);
    return cmd;
  }

  delete(cmd) {
    menuHeight -= lineHeight;
    this.commands.delete(cmd);
  }

  render() {
    return {
      geometry: createRectangle([viewWidth / 2, viewHeight - menuHeight / 2], 0, menuWidth, menuHeight),

      children: this.commands,

      render: (palette, { geometry }) => {
        const { ctx, fillRectangle, strokeRectangle, fillText } = palette;
        fillRectangle(bgColor, [0, -stroke/2], geometry.width, geometry.height);
        strokeRectangle(strokeColor, stroke, [0, -stroke/2], geometry.width, geometry.height);

      },
      interact: {
        onMouseDown: () => {

        },
      }
    };
  }
}
<commit_msg>Mannu working sans keyboard interactions.<commit_after>// Display modal text.

import { renderUI } from 'lib/cEl';
import { createRectangle } from 'lib/geometry';
import { inputs } from 'controls';
import { canvas, ctx, viewHeight, viewWidth } from 'dom';

// Menu box traits.
// Flexible size.
const stroke = 2;
let menuWidth = viewWidth/3;
let menuHeight = 0;
let menuX = -viewWidth/2 + menuWidth/2 + stroke/2;
let menuY = viewHeight/2 - menuHeight/2 - stroke/2 | 0;
const strokeColor = `#fff`;
const bgColor = `#00f`;

// Text traits.
const textSize = 12;
const lineHeight = textSize * 1.2;
const textWidth = menuWidth - stroke * 4;
const textHeight = menuHeight - stroke * 4;
const textColor = `#fff`;

const Command = (label, handler, index)=> {
  label = label.toUpperCase();
  const txtMetrics = ctx.measureText(label);
  menuWidth = Math.max(menuWidth, txtMetrics.width + stroke*4);
  menuX = -viewWidth/2 + menuWidth/2 + stroke/2;

  return {
    geometry: createRectangle([
      -menuWidth/2 + stroke*2,
      lineHeight/4 + menuHeight/2 - lineHeight/2 - index*lineHeight | 0
    ], 0, menuWidth, lineHeight),

    render: ({ fillText }, { geometry })=> {
      geometry.position = [
        -menuWidth/2 + stroke*2,
        lineHeight/4 + menuHeight/2 - lineHeight/2 - index*lineHeight | 0
      ];

      fillText({ style: textColor }, [0, 0], label);
    },

    interact: {
      onMouseDown: handler
    }
  };
};

export default class Menu {
  constructor() {
    this.commands = new Set();
  }

  add(label, handler) {
    const cmd = Command(label, handler, this.commands.size);
    this.commands.add(cmd);
    menuHeight = lineHeight*this.commands.size + stroke | 0;
    menuY = viewHeight/2 - menuHeight/2 - stroke/2 | 0;
    return cmd;
  }

  delete(cmd) {
    this.commands.delete(cmd);
    menuHeight = lineHeight*this.commands.size + stroke | 0;
    menuY = viewHeight/2 - menuHeight/2 - stroke/2 | 0;
  }

  render() {
    return {
      geometry: createRectangle([menuX, menuY], 0, menuWidth, menuHeight),

      children: this.commands,

      render: (palette, { geometry }) => {
        geometry.position = [menuX, menuY];

        const { fillRectangle, strokeRectangle } = palette;
        fillRectangle(bgColor, [0,0], menuWidth, menuHeight);
        strokeRectangle(strokeColor, stroke, [0,0], menuWidth, menuHeight);
      }
    };
  }
}
<|endoftext|>


<commit_before><commit_msg>Filter out non-tweenable keys within tweenAttr<commit_after><|endoftext|>


<commit_before>const {Fragment} = require("./fragment")
const {Mark} = require("./mark")
const {Slice, replace} = require("./replace")
const {ResolvedPos} = require("./resolvedpos")
const {compareDeep} = require("./comparedeep")

const emptyAttrs = Object.create(null)

let warnedAboutMarksAt = false

// ::- This class represents a node in the tree that makes up a
// ProseMirror document. So a document is an instance of `Node`, with
// children that are also instances of `Node`.
//
// Nodes are persistent data structures. Instead of changing them, you
// create new ones with the content you want. Old ones keep pointing
// at the old document shape. This is made cheaper by sharing
// structure between the old and new data as much as possible, which a
// tree shape like this (without back pointers) makes easy.
//
// **Never** directly mutate the properties of a `Node` object. See
// [this guide](guide/doc.html) for more information.
class Node {
  constructor(type, attrs, content, marks) {
    // :: NodeType
    // The type of node that this is.
    this.type = type

    // :: Object
    // An object mapping attribute names to values. The kind of
    // attributes allowed and required are determined by the node
    // type.
    this.attrs = attrs

    // :: Fragment
    // A container holding the node's children.
    this.content = content || Fragment.empty

    // :: [Mark]
    // The marks (things like whether it is emphasized or part of a
    // link) associated with this node.
    this.marks = marks || Mark.none
  }

  // text:: ?string
  // For text nodes, this contains the node's text content.

  // :: number
  // The size of this node, as defined by the integer-based [indexing
  // scheme](guide/doc.html#indexing). For text nodes, this is the
  // amount of characters. For other leaf nodes, it is one. And for
  // non-leaf nodes, it is the size of the content plus two (the start
  // and end token).
  get nodeSize() { return this.isLeaf ? 1 : 2 + this.content.size }

  // :: number
  // The number of children that the node has.
  get childCount() { return this.content.childCount }

  // :: (number) → Node
  // Get the child node at the given index. Raises an error when the
  // index is out of range.
  child(index) { return this.content.child(index) }

  // :: (number) → ?Node
  // Get the child node at the given index, if it exists.
  maybeChild(index) { return this.content.maybeChild(index) }

  // :: ((node: Node, offset: number, index: number))
  // Call `f` for every child node, passing the node, its offset
  // into this parent node, and its index.
  forEach(f) { this.content.forEach(f) }

  // :: (?number, ?number, (node: Node, pos: number, parent: Node, index: number))
  // Invoke a callback for all descendant nodes recursively between
  // the given two positions that are relative to start of this node's content.
  // The callback is invoked with the node, its parent-relative position,
  // its parent node, and its child index. If the callback returns false,
  // the current node's children will not be recursed over.
  nodesBetween(from, to, f, pos = 0) {
    this.content.nodesBetween(from, to, f, pos, this)
  }

  // :: ((node: Node, pos: number, parent: Node))
  // Call the given callback for every descendant node.
  descendants(f) {
    this.nodesBetween(0, this.content.size, f)
  }

  // :: string
  // Concatenates all the text nodes found in this fragment and its
  // children.
  get textContent() { return this.textBetween(0, this.content.size, "") }

  // :: (number, number, ?string, ?string) → string
  // Get all text between positions `from` and `to`. When
  // `blockSeparator` is given, it will be inserted whenever a new
  // block node is started. When `leafText` is given, it'll be
  // inserted for every non-text leaf node encountered.
  textBetween(from, to, blockSeparator, leafText) {
    return this.content.textBetween(from, to, blockSeparator, leafText)
  }

  // :: ?Node
  // Returns this node's first child, or `null` if there are no
  // children.
  get firstChild() { return this.content.firstChild }

  // :: ?Node
  // Returns this node's last child, or `null` if there are no
  // children.
  get lastChild() { return this.content.lastChild }

  // :: (Node) → bool
  // Test whether two nodes represent the same content.
  eq(other) {
    return this == other || (this.sameMarkup(other) && this.content.eq(other.content))
  }

  // :: (Node) → bool
  // Compare the markup (type, attributes, and marks) of this node to
  // those of another. Returns `true` if both have the same markup.
  sameMarkup(other) {
    return this.hasMarkup(other.type, other.attrs, other.marks)
  }

  // :: (NodeType, ?Object, ?[Mark]) → bool
  // Check whether this node's markup correspond to the given type,
  // attributes, and marks.
  hasMarkup(type, attrs, marks) {
    return this.type == type &&
      compareDeep(this.attrs, attrs || type.defaultAttrs || emptyAttrs) &&
      Mark.sameSet(this.marks, marks || Mark.none)
  }

  // :: (?Fragment) → Node
  // Create a new node with the same markup as this node, containing
  // the given content (or empty, if no content is given).
  copy(content = null) {
    if (content == this.content) return this
    return new this.constructor(this.type, this.attrs, content, this.marks)
  }

  // :: ([Mark]) → Node
  // Create a copy of this node, with the given set of marks instead
  // of the node's own marks.
  mark(marks) {
    return marks == this.marks ? this : new this.constructor(this.type, this.attrs, this.content, marks)
  }

  // :: (number, ?number) → Node
  // Create a copy of this node with only the content between the
  // given offsets. If `to` is not given, it defaults to the end of
  // the node.
  cut(from, to) {
    if (from == 0 && to == this.content.size) return this
    return this.copy(this.content.cut(from, to))
  }

  // :: (number, ?number) → Slice
  // Cut out the part of the document between the given positions, and
  // return it as a `Slice` object.
  slice(from, to = this.content.size, includeParents = false) {
    if (from == to) return Slice.empty

    let $from = this.resolve(from), $to = this.resolve(to)
    let depth = includeParents ? 0 : $from.sharedDepth(to)
    let start = $from.start(depth), node = $from.node(depth)
    let content = node.content.cut($from.pos - start, $to.pos - start)
    return new Slice(content, $from.depth - depth, $to.depth - depth)
  }

  // :: (number, number, Slice) → Node
  // Replace the part of the document between the given positions with
  // the given slice. The slice must 'fit', meaning its open sides
  // must be able to connect to the surrounding content, and its
  // content nodes must be valid children for the node they are placed
  // into. If any of this is violated, an error of type
  // [`ReplaceError`](#model.ReplaceError) is thrown.
  replace(from, to, slice) {
    return replace(this.resolve(from), this.resolve(to), slice)
  }

  // :: (number) → ?Node
  // Find the node after the given position.
  nodeAt(pos) {
    for (let node = this;;) {
      let {index, offset} = node.content.findIndex(pos)
      node = node.maybeChild(index)
      if (!node) return null
      if (offset == pos || node.isText) return node
      pos -= offset + 1
    }
  }

  // :: (number) → {node: ?Node, index: number, offset: number}
  // Find the (direct) child node after the given offset, if any,
  // and return it along with its index and offset relative to this
  // node.
  childAfter(pos) {
    let {index, offset} = this.content.findIndex(pos)
    return {node: this.content.maybeChild(index), index, offset}
  }

  // :: (number) → {node: ?Node, index: number, offset: number}
  // Find the (direct) child node before the given offset, if any,
  // and return it along with its index and offset relative to this
  // node.
  childBefore(pos) {
    if (pos == 0) return {node: null, index: 0, offset: 0}
    let {index, offset} = this.content.findIndex(pos)
    if (offset < pos) return {node: this.content.child(index), index, offset}
    let node = this.content.child(index - 1)
    return {node, index: index - 1, offset: offset - node.nodeSize}
  }

  // :: (number) → ResolvedPos
  // Resolve the given position in the document, returning an object
  // describing its path through the document.
  resolve(pos) { return ResolvedPos.resolveCached(this, pos) }

  resolveNoCache(pos) { return ResolvedPos.resolve(this, pos) }

  marksAt(pos, useAfter) {
    if (!warnedAboutMarksAt && typeof console != "undefined" && console.warn) {
      warnedAboutMarksAt = true
      console.warn("Node.marksAt is deprecated. Use ResolvedPos.marks instead.")
    }
    return this.resolve(pos).marks(useAfter)
  }

  // :: (?number, ?number, MarkType) → bool
  // Test whether a mark of the given type occurs in this document
  // between the two given positions.
  rangeHasMark(from, to, type) {
    let found = false
    this.nodesBetween(from, to, node => {
      if (type.isInSet(node.marks)) found = true
      return !found
    })
    return found
  }

  // :: bool
  // True when this is a block (non-inline node)
  get isBlock() { return this.type.isBlock }

  // :: bool
  // True when this is a textblock node, a block node with inline
  // content.
  get isTextblock() { return this.type.isTextblock }

  // :: bool
  // True when this is an inline node (a text node or a node that can
  // appear among text).
  get isInline() { return this.type.isInline }

  // :: bool
  // True when this is a text node.
  get isText() { return this.type.isText }

  // :: bool
  // True when this is a leaf node.
  get isLeaf() { return this.type.isLeaf }

  // :: () → string
  // Return a string representation of this node for debugging
  // purposes.
  toString() {
    let name = this.type.name
    if (this.content.size)
      name += "(" + this.content.toStringInner() + ")"
    return wrapMarks(this.marks, name)
  }

  // :: (number) → ContentMatch
  // Get the content match in this node at the given index.
  contentMatchAt(index) {
    return this.type.contentExpr.getMatchAt(this.attrs, this.content, index)
  }

  // :: (number, number, ?Fragment, ?number, ?number) → bool
  // Test whether replacing the range `from` to `to` (by index) with
  // the given replacement fragment (which defaults to the empty
  // fragment) would leave the node's content valid. You can
  // optionally pass `start` and `end` indices into the replacement
  // fragment.
  canReplace(from, to, replacement, start, end) {
    return this.type.contentExpr.checkReplace(this.attrs, this.content, from, to, replacement, start, end)
  }

  // :: (number, number, NodeType, ?[Mark]) → bool
  // Test whether replacing the range `from` to `to` (by index) with a
  // node of the given type with the given attributes and marks would
  // be valid.
  canReplaceWith(from, to, type, attrs, marks) {
    return this.type.contentExpr.checkReplaceWith(this.attrs, this.content, from, to, type, attrs, marks || Mark.none)
  }

  // :: (Node) → bool
  // Test whether the given node's content could be appended to this
  // node. If that node is empty, this will only return true if there
  // is at least one node type that can appear in both nodes (to avoid
  // merging completely incompatible nodes).
  canAppend(other) {
    if (other.content.size) return this.canReplace(this.childCount, this.childCount, other.content)
    else return this.type.compatibleContent(other.type)
  }

  defaultContentType(at) {
    let elt = this.contentMatchAt(at).nextElement
    return elt && elt.defaultType()
  }

  // :: ()
  // Check whether this node and its descendants conform to the
  // schema, and raise error when they do not.
  check() {
    if (!this.type.validContent(this.content, this.attrs))
      throw new RangeError(`Invalid content for node ${this.type.name}: ${this.content.toString().slice(0, 50)}`)
    this.content.forEach(node => node.check())
  }

  // :: () → Object
  // Return a JSON-serializeable representation of this node.
  toJSON() {
    let obj = {type: this.type.name}
    for (let _ in this.attrs) {
      obj.attrs = this.attrs
      break
    }
    if (this.content.size)
      obj.content = this.content.toJSON()
    if (this.marks.length)
      obj.marks = this.marks.map(n => n.toJSON())
    return obj
  }

  // :: (Schema, Object) → Node
  // Deserialize a node from its JSON representation.
  static fromJSON(schema, json) {
    let marks = json.marks && json.marks.map(schema.markFromJSON)
    if (json.type == "text") return schema.text(json.text, marks)
    let type = schema.nodeType(json.type)
    if (!type) throw new RangeError(`There is no node type ${json.type} in this schema`)
    return type.create(json.attrs, Fragment.fromJSON(schema, json.content), marks)
  }
}
exports.Node = Node

class TextNode extends Node {
  constructor(type, attrs, content, marks) {
    super(type, attrs, null, marks)

    if (!content) throw new RangeError("Empty text nodes are not allowed")

    this.text = content
  }

  toString() { return wrapMarks(this.marks, JSON.stringify(this.text)) }

  get textContent() { return this.text }

  textBetween(from, to) { return this.text.slice(from, to) }

  get nodeSize() { return this.text.length }

  mark(marks) {
    return new TextNode(this.type, this.attrs, this.text, marks)
  }

  withText(text) {
    if (text == this.text) return this
    return new TextNode(this.type, this.attrs, text, this.marks)
  }

  cut(from = 0, to = this.text.length) {
    if (from == 0 && to == this.text.length) return this
    return this.withText(this.text.slice(from, to))
  }

  eq(other) {
    return this.sameMarkup(other) && this.text == other.text
  }

  toJSON() {
    let base = super.toJSON()
    base.text = this.text
    return base
  }
}
exports.TextNode = TextNode

function wrapMarks(marks, str) {
  for (let i = marks.length - 1; i >= 0; i--)
    str = marks[i].type.name + "(" + str + ")"
  return str
}
<commit_msg>Remove backwards-compat shim for marksAt<commit_after>const {Fragment} = require("./fragment")
const {Mark} = require("./mark")
const {Slice, replace} = require("./replace")
const {ResolvedPos} = require("./resolvedpos")
const {compareDeep} = require("./comparedeep")

const emptyAttrs = Object.create(null)

// ::- This class represents a node in the tree that makes up a
// ProseMirror document. So a document is an instance of `Node`, with
// children that are also instances of `Node`.
//
// Nodes are persistent data structures. Instead of changing them, you
// create new ones with the content you want. Old ones keep pointing
// at the old document shape. This is made cheaper by sharing
// structure between the old and new data as much as possible, which a
// tree shape like this (without back pointers) makes easy.
//
// **Never** directly mutate the properties of a `Node` object. See
// [this guide](guide/doc.html) for more information.
class Node {
  constructor(type, attrs, content, marks) {
    // :: NodeType
    // The type of node that this is.
    this.type = type

    // :: Object
    // An object mapping attribute names to values. The kind of
    // attributes allowed and required are determined by the node
    // type.
    this.attrs = attrs

    // :: Fragment
    // A container holding the node's children.
    this.content = content || Fragment.empty

    // :: [Mark]
    // The marks (things like whether it is emphasized or part of a
    // link) associated with this node.
    this.marks = marks || Mark.none
  }

  // text:: ?string
  // For text nodes, this contains the node's text content.

  // :: number
  // The size of this node, as defined by the integer-based [indexing
  // scheme](guide/doc.html#indexing). For text nodes, this is the
  // amount of characters. For other leaf nodes, it is one. And for
  // non-leaf nodes, it is the size of the content plus two (the start
  // and end token).
  get nodeSize() { return this.isLeaf ? 1 : 2 + this.content.size }

  // :: number
  // The number of children that the node has.
  get childCount() { return this.content.childCount }

  // :: (number) → Node
  // Get the child node at the given index. Raises an error when the
  // index is out of range.
  child(index) { return this.content.child(index) }

  // :: (number) → ?Node
  // Get the child node at the given index, if it exists.
  maybeChild(index) { return this.content.maybeChild(index) }

  // :: ((node: Node, offset: number, index: number))
  // Call `f` for every child node, passing the node, its offset
  // into this parent node, and its index.
  forEach(f) { this.content.forEach(f) }

  // :: (?number, ?number, (node: Node, pos: number, parent: Node, index: number))
  // Invoke a callback for all descendant nodes recursively between
  // the given two positions that are relative to start of this node's content.
  // The callback is invoked with the node, its parent-relative position,
  // its parent node, and its child index. If the callback returns false,
  // the current node's children will not be recursed over.
  nodesBetween(from, to, f, pos = 0) {
    this.content.nodesBetween(from, to, f, pos, this)
  }

  // :: ((node: Node, pos: number, parent: Node))
  // Call the given callback for every descendant node.
  descendants(f) {
    this.nodesBetween(0, this.content.size, f)
  }

  // :: string
  // Concatenates all the text nodes found in this fragment and its
  // children.
  get textContent() { return this.textBetween(0, this.content.size, "") }

  // :: (number, number, ?string, ?string) → string
  // Get all text between positions `from` and `to`. When
  // `blockSeparator` is given, it will be inserted whenever a new
  // block node is started. When `leafText` is given, it'll be
  // inserted for every non-text leaf node encountered.
  textBetween(from, to, blockSeparator, leafText) {
    return this.content.textBetween(from, to, blockSeparator, leafText)
  }

  // :: ?Node
  // Returns this node's first child, or `null` if there are no
  // children.
  get firstChild() { return this.content.firstChild }

  // :: ?Node
  // Returns this node's last child, or `null` if there are no
  // children.
  get lastChild() { return this.content.lastChild }

  // :: (Node) → bool
  // Test whether two nodes represent the same content.
  eq(other) {
    return this == other || (this.sameMarkup(other) && this.content.eq(other.content))
  }

  // :: (Node) → bool
  // Compare the markup (type, attributes, and marks) of this node to
  // those of another. Returns `true` if both have the same markup.
  sameMarkup(other) {
    return this.hasMarkup(other.type, other.attrs, other.marks)
  }

  // :: (NodeType, ?Object, ?[Mark]) → bool
  // Check whether this node's markup correspond to the given type,
  // attributes, and marks.
  hasMarkup(type, attrs, marks) {
    return this.type == type &&
      compareDeep(this.attrs, attrs || type.defaultAttrs || emptyAttrs) &&
      Mark.sameSet(this.marks, marks || Mark.none)
  }

  // :: (?Fragment) → Node
  // Create a new node with the same markup as this node, containing
  // the given content (or empty, if no content is given).
  copy(content = null) {
    if (content == this.content) return this
    return new this.constructor(this.type, this.attrs, content, this.marks)
  }

  // :: ([Mark]) → Node
  // Create a copy of this node, with the given set of marks instead
  // of the node's own marks.
  mark(marks) {
    return marks == this.marks ? this : new this.constructor(this.type, this.attrs, this.content, marks)
  }

  // :: (number, ?number) → Node
  // Create a copy of this node with only the content between the
  // given offsets. If `to` is not given, it defaults to the end of
  // the node.
  cut(from, to) {
    if (from == 0 && to == this.content.size) return this
    return this.copy(this.content.cut(from, to))
  }

  // :: (number, ?number) → Slice
  // Cut out the part of the document between the given positions, and
  // return it as a `Slice` object.
  slice(from, to = this.content.size, includeParents = false) {
    if (from == to) return Slice.empty

    let $from = this.resolve(from), $to = this.resolve(to)
    let depth = includeParents ? 0 : $from.sharedDepth(to)
    let start = $from.start(depth), node = $from.node(depth)
    let content = node.content.cut($from.pos - start, $to.pos - start)
    return new Slice(content, $from.depth - depth, $to.depth - depth)
  }

  // :: (number, number, Slice) → Node
  // Replace the part of the document between the given positions with
  // the given slice. The slice must 'fit', meaning its open sides
  // must be able to connect to the surrounding content, and its
  // content nodes must be valid children for the node they are placed
  // into. If any of this is violated, an error of type
  // [`ReplaceError`](#model.ReplaceError) is thrown.
  replace(from, to, slice) {
    return replace(this.resolve(from), this.resolve(to), slice)
  }

  // :: (number) → ?Node
  // Find the node after the given position.
  nodeAt(pos) {
    for (let node = this;;) {
      let {index, offset} = node.content.findIndex(pos)
      node = node.maybeChild(index)
      if (!node) return null
      if (offset == pos || node.isText) return node
      pos -= offset + 1
    }
  }

  // :: (number) → {node: ?Node, index: number, offset: number}
  // Find the (direct) child node after the given offset, if any,
  // and return it along with its index and offset relative to this
  // node.
  childAfter(pos) {
    let {index, offset} = this.content.findIndex(pos)
    return {node: this.content.maybeChild(index), index, offset}
  }

  // :: (number) → {node: ?Node, index: number, offset: number}
  // Find the (direct) child node before the given offset, if any,
  // and return it along with its index and offset relative to this
  // node.
  childBefore(pos) {
    if (pos == 0) return {node: null, index: 0, offset: 0}
    let {index, offset} = this.content.findIndex(pos)
    if (offset < pos) return {node: this.content.child(index), index, offset}
    let node = this.content.child(index - 1)
    return {node, index: index - 1, offset: offset - node.nodeSize}
  }

  // :: (number) → ResolvedPos
  // Resolve the given position in the document, returning an object
  // describing its path through the document.
  resolve(pos) { return ResolvedPos.resolveCached(this, pos) }

  resolveNoCache(pos) { return ResolvedPos.resolve(this, pos) }

  // :: (?number, ?number, MarkType) → bool
  // Test whether a mark of the given type occurs in this document
  // between the two given positions.
  rangeHasMark(from, to, type) {
    let found = false
    this.nodesBetween(from, to, node => {
      if (type.isInSet(node.marks)) found = true
      return !found
    })
    return found
  }

  // :: bool
  // True when this is a block (non-inline node)
  get isBlock() { return this.type.isBlock }

  // :: bool
  // True when this is a textblock node, a block node with inline
  // content.
  get isTextblock() { return this.type.isTextblock }

  // :: bool
  // True when this is an inline node (a text node or a node that can
  // appear among text).
  get isInline() { return this.type.isInline }

  // :: bool
  // True when this is a text node.
  get isText() { return this.type.isText }

  // :: bool
  // True when this is a leaf node.
  get isLeaf() { return this.type.isLeaf }

  // :: () → string
  // Return a string representation of this node for debugging
  // purposes.
  toString() {
    let name = this.type.name
    if (this.content.size)
      name += "(" + this.content.toStringInner() + ")"
    return wrapMarks(this.marks, name)
  }

  // :: (number) → ContentMatch
  // Get the content match in this node at the given index.
  contentMatchAt(index) {
    return this.type.contentExpr.getMatchAt(this.attrs, this.content, index)
  }

  // :: (number, number, ?Fragment, ?number, ?number) → bool
  // Test whether replacing the range `from` to `to` (by index) with
  // the given replacement fragment (which defaults to the empty
  // fragment) would leave the node's content valid. You can
  // optionally pass `start` and `end` indices into the replacement
  // fragment.
  canReplace(from, to, replacement, start, end) {
    return this.type.contentExpr.checkReplace(this.attrs, this.content, from, to, replacement, start, end)
  }

  // :: (number, number, NodeType, ?[Mark]) → bool
  // Test whether replacing the range `from` to `to` (by index) with a
  // node of the given type with the given attributes and marks would
  // be valid.
  canReplaceWith(from, to, type, attrs, marks) {
    return this.type.contentExpr.checkReplaceWith(this.attrs, this.content, from, to, type, attrs, marks || Mark.none)
  }

  // :: (Node) → bool
  // Test whether the given node's content could be appended to this
  // node. If that node is empty, this will only return true if there
  // is at least one node type that can appear in both nodes (to avoid
  // merging completely incompatible nodes).
  canAppend(other) {
    if (other.content.size) return this.canReplace(this.childCount, this.childCount, other.content)
    else return this.type.compatibleContent(other.type)
  }

  defaultContentType(at) {
    let elt = this.contentMatchAt(at).nextElement
    return elt && elt.defaultType()
  }

  // :: ()
  // Check whether this node and its descendants conform to the
  // schema, and raise error when they do not.
  check() {
    if (!this.type.validContent(this.content, this.attrs))
      throw new RangeError(`Invalid content for node ${this.type.name}: ${this.content.toString().slice(0, 50)}`)
    this.content.forEach(node => node.check())
  }

  // :: () → Object
  // Return a JSON-serializeable representation of this node.
  toJSON() {
    let obj = {type: this.type.name}
    for (let _ in this.attrs) {
      obj.attrs = this.attrs
      break
    }
    if (this.content.size)
      obj.content = this.content.toJSON()
    if (this.marks.length)
      obj.marks = this.marks.map(n => n.toJSON())
    return obj
  }

  // :: (Schema, Object) → Node
  // Deserialize a node from its JSON representation.
  static fromJSON(schema, json) {
    let marks = json.marks && json.marks.map(schema.markFromJSON)
    if (json.type == "text") return schema.text(json.text, marks)
    let type = schema.nodeType(json.type)
    if (!type) throw new RangeError(`There is no node type ${json.type} in this schema`)
    return type.create(json.attrs, Fragment.fromJSON(schema, json.content), marks)
  }
}
exports.Node = Node

class TextNode extends Node {
  constructor(type, attrs, content, marks) {
    super(type, attrs, null, marks)

    if (!content) throw new RangeError("Empty text nodes are not allowed")

    this.text = content
  }

  toString() { return wrapMarks(this.marks, JSON.stringify(this.text)) }

  get textContent() { return this.text }

  textBetween(from, to) { return this.text.slice(from, to) }

  get nodeSize() { return this.text.length }

  mark(marks) {
    return new TextNode(this.type, this.attrs, this.text, marks)
  }

  withText(text) {
    if (text == this.text) return this
    return new TextNode(this.type, this.attrs, text, this.marks)
  }

  cut(from = 0, to = this.text.length) {
    if (from == 0 && to == this.text.length) return this
    return this.withText(this.text.slice(from, to))
  }

  eq(other) {
    return this.sameMarkup(other) && this.text == other.text
  }

  toJSON() {
    let base = super.toJSON()
    base.text = this.text
    return base
  }
}
exports.TextNode = TextNode

function wrapMarks(marks, str) {
  for (let i = marks.length - 1; i >= 0; i--)
    str = marks[i].type.name + "(" + str + ")"
  return str
}
<|endoftext|>