<commit_before>const _ = require('lodash');
const extendDefined = require('../util/extendDefined');
const urlTools = require('urltools');
const urlModule = require('url');

/**
 * In graph terminology a relation represents a directed edge, a
 * reference from one asset to another. For the purpose of being able
 * to treat all relations equally, there's a subclass for each
 * supported relation type, encapsulating the details of how to
 * retrieve, update, and (optionally) inline the asset being pointed
 * to.
 *
 * These are some examples of included subclasses:
 *
 *    - `relations.HtmlAnchor`         An anchor tag in an HTML document `<a href='...'>`.
 *    - `relations.HtmlImage`          An `<img src='...'>` tag in an HTML document.
 *    - `relations.CssImport`          An `@import` declaration in a CSS asset.
 *    - `relations.CacheManifestEntry` A line in a cache manifest.
 */
class Relation {
  /**
   * Create a new Relation instance. For existing assets the
   * instantiation of relations happens automatically if you use the
   * `populate` transform. You only need to create relations manually
   * when you need to introduce new ones.
   *
   * Note that the base Relation class should be considered
   * abstract. Please instantiate the appropriate subclass.
   *
   * Options:
   *
   *  - `from` The source asset of the relation.
   *  - `to`   The target asset of the relation, or an asset configuration
   *           object if the target asset hasn't yet been resolved and created.
   *
   * @constructor
   * @param {Object} config The relation configuration
   */
  constructor(config) {
    if (config.hrefType) {
      this._hrefType = config.hrefType;
      config.hrefType = undefined;
    }
    if (config.to) {
      this._to = config.to;
      config.to = undefined;
    }
    extendDefined(this, config);
    this.id = '' + _.uniqueId();
  }

  /**
   * The source asset of the relation.
   *
   * @member {Asset} Relation#from
   */

  /**
   * The target asset of the relation.
   *
   * @type {Asset}
   */
  get to() {
    return this._to;
  }

  set to(to) {
    if (this.to && this.to.incomingInlineRelation === this) {
      this.to.incomingInlineRelation = undefined;
    }
    this._to = this.from.assetGraph.addAsset(to, this);
    this.refreshHref();
  }

  /**
   * The ATS/DOM-node where the Relation originates in the parent document
   *
   * @member {Object} Relation#node
   */

  /**
   * Get or set the href of the relation. The relation must be
   * attached to an asset.
   *
   * What is actually retrieved or updated depends on the relation
   * type. For `HtmlImage` the `src` attribute of the HTML element
   * is changed, for `CssImport` the parsed representation of
   * the @import rule is updated, etc.
   *
   * Most of the time you don't need to think about this property,
   * as the href is automatically updated when the url of the source
   * or target asset is changed, or an intermediate asset is
   * inlined.
   *
   * @member {String} Relation#href
   */

  /**
   * Get or set the fragment identifier part of the href of the relation.
   *
   * Setting a fragment with a non-empty value requires the value to begin with `#`
   *
   * @type {String}
   */
  get fragment() {
    const href = this.href;
    if (typeof href === 'string') {
      const indexOfFragment = href.indexOf('#');
      if (indexOfFragment === -1) {
        return '';
      } else {
        return href.substr(indexOfFragment);
      }
    }
  }

  set fragment(fragment) {
    if (fragment && !/^#/.test(fragment)) {
      throw new Error('The fragment must begin with a # or be empty');
    }
    const href = this.href;
    if (href) {
      let currentHrefWithoutFragment;
      const indexOfFragment = href.indexOf('#');
      if (indexOfFragment === -1) {
        currentHrefWithoutFragment = href;
      } else {
        currentHrefWithoutFragment = href.substr(0, indexOfFragment);
      }
      this.href = currentHrefWithoutFragment + (fragment || '');
      this.from.markDirty();
    }
  }

  /**
   * Update `href` of a relation to make sure it points at the
   * current url of its target asset.
   *
   * It's not necessary to call this function manually as long as
   * the source and target assets of the relation have only been
   * moved by having their `url` property changed (the recommended
   * way), but some transforms will need this after some low-level
   * surgery, such as attaching an existing relation to a different
   * asset.
   *
   * @return {Relation} The relation itself (chaining-friendly).
   */
  refreshHref() {
    const hrefType = this.hrefType;
    if (hrefType === 'inline') {
      this.inline();
    } else {
      const currentHref = this.href;
      if (/^#/.test(currentHref)) {
        // Pure fragment url, nothing to do
        return;
      }
      const targetUrl = this.to.url;
      const assetGraph = this.from.assetGraph;
      const canonical = this.canonical;
      let href;
      if (hrefType === 'rootRelative' && !canonical) {
        href = urlTools.buildRootRelativeUrl(
          this.baseUrl,
          targetUrl,
          assetGraph.root
        );
      } else if (hrefType === 'relative' && !canonical) {
        href = urlTools.buildRelativeUrl(this.baseUrl, targetUrl);
      } else if (hrefType === 'protocolRelative') {
        href = urlTools.buildProtocolRelativeUrl(this.baseUrl, targetUrl);
      } else {
        // Absolute
        href = targetUrl;
      }
      // Hack: Avoid adding index.html to an href pointing at file://.../index.html if it's not already there:
      if (
        /^file:\/\/.*\/index\.html(?:[?#]|$)/.test(targetUrl) &&
        !/(?:^|\/)index\.html(?:[?#]:|$)/.test(this.href)
      ) {
        href = href.replace(/(^|\/)index\.html(?=[?#]|$)/, '$1');
      }
      if (canonical) {
        href = href.replace(assetGraph.root, assetGraph.canonicalRoot);
      }
      const currentFragment = this.fragment;
      if (currentFragment) {
        href += currentFragment;
      }
      if (currentHref !== href) {
        this.href = href;
        this.from.markDirty();
      }
    }
    return this;
  }

  /**
   * Get the relations cross origins status defined as
   * differences in protocol or hostname.
   *
   * This property is quite useful as part of a population
   * query to ensure that the graph doesn't expand beyond
   * the current host or file system
   *
   * @type {Boolean}
   */
  get crossorigin() {
    const fromUrl = this.from.nonInlineAncestor.url;
    const toUrl = this.to.url;
    if (!toUrl) {
      // Inline
      return false;
    }
    if (this.canonical) {
      return false;
    }
    const fromUrlObj = urlModule.parse(fromUrl);
    const toUrlObj = urlModule.parse(urlModule.resolve(fromUrl, toUrl));
    if (
      fromUrlObj.protocol !== toUrlObj.protocol ||
      fromUrlObj.hostname !== toUrlObj.hostname
    ) {
      return true;
    }
    const fromPort = fromUrlObj.port
      ? parseInt(fromUrlObj.port, 10)
      : { 'http:': 80, 'https:': 443 }[fromUrlObj.protocol];
    const toPort = toUrlObj.port
      ? parseInt(toUrlObj.port, 10)
      : { 'http:': 80, 'https:': 443 }[toUrlObj.protocol];
    return fromPort !== toPort;
  }

  /**
   * Get or set the canonical state of a Relation
   *
   * If [AssetGraph]{@link AssetGraph} has a [`canonicalRoot`]{@link AssetGraph#canonicalRoot}
   * property, AssetGraph will detect absolute URLs matching `AssetGraph.canonicalRoot`
   * as if they were of [`hrefType`]{@link Relation#hrefType} `rootRelative`.
   *
   * The getter tell you if [`Relation.href`]{@link Relation#href} will be prepended
   * with `Assetgraph.canonicalRoot`.
   *
   * The setter will change the Relation's `href` to be prefixed with `AssetGraph.canonicalRoot`
   * if `true`, and without if `false`
   *
   * @type {Boolean}
   */
  get canonical() {
    if (typeof this._canonical === 'undefined') {
      let canonical = false;

      if (
        this.href &&
        this.from &&
        this.from.assetGraph &&
        this.from.assetGraph.canonicalRoot
      ) {
        const canonicalRootObj = urlModule.parse(
          this.from.assetGraph.canonicalRoot,
          false,
          true
        );
        const hrefObj = urlModule.parse(this.href, false, true);

        canonical =
          hrefObj.slashes === true &&
          ['http:', 'https:', null].includes(hrefObj.protocol) &&
          canonicalRootObj.host === hrefObj.host &&
          hrefObj.path.startsWith(canonicalRootObj.path) &&
          (canonicalRootObj.protocol === hrefObj.protocol ||
            canonicalRootObj.protocol === null);
      }

      this._canonical = canonical;
    }

    return this._canonical;
  }

  set canonical(isCanonical) {
    if (
      this.from &&
      this.from.assetGraph &&
      this.from.assetGraph.canonicalRoot
    ) {
      isCanonical = !!isCanonical;
      if (this._canonical !== isCanonical) {
        this._canonical = isCanonical;

        if (
          !isCanonical &&
          (this._hrefType === 'absolute' ||
            this._hrefType !== 'protocolRelative')
        ) {
          // We're switching to non-canonical mode. Degrade the href type
          // to rootRelative so we won't issue absolute file:// urls
          // This is based on guesswork, though.
          this._hrefType = 'rootRelative';
        }
        this.refreshHref();
      }
    }
  }

  /**
   * Get the url of the first non-inlined ancestor of the `from`-Asset
   *
   * @type {String}
   */
  get baseUrl() {
    const nonInlineAncestor = this.from.nonInlineAncestor;
    return nonInlineAncestor && nonInlineAncestor.url;
  }

  /**
   * Either `'absolute'`, `'rootRelative'`, `'protocolRelative'`, `'relative'`, or `'inline'`.
   * Decides what "degree" of relative url [`refreshHref()`]{@link Relation#refreshHref} tries to issue, except for
   * `'inline'` which means that the target asset is contained in a `data:` url or similar.
   *
   * @type {String}
   */
  get hrefType() {
    const to = this.to;
    if (to.isInline && to !== this.from) {
      return 'inline';
    }
    if (!this._hrefType) {
      const href = (this.href || '').trim();
      if (/^\/\//.test(href)) {
        this._hrefType = 'protocolRelative';
      } else if (/^\//.test(href)) {
        this._hrefType = 'rootRelative';
      } else if (/^[a-z+]+:/i.test(href)) {
        this._hrefType = 'absolute';
      } else {
        this._hrefType = 'relative';
      }
    }
    return this._hrefType;
  }

  set hrefType(hrefType) {
    const existingHrefType = this.hrefType;
    if (hrefType === 'inline') {
      this.inline();
    } else if (hrefType !== existingHrefType) {
      this._hrefType = hrefType;
      if (existingHrefType === 'inline') {
        this.to.externalize();
      } else {
        this.refreshHref();
      }
    }
  }

  /**
   * Inline the relation. This is only supported by certain relation
   * types and will produce different results depending on the type
   * (`data:` url, inline script, inline stylesheet...).
   *
   * Will make a clone of the target asset if it has more incoming
   * relations than this one.
   *
   * @return {Relation} The relation itself (chaining-friendly).
   */
  inline() {
    if (
      this.to.isAsset &&
      this.to.incomingRelations.filter(r => r._to !== r.from).length > 1
    ) {
      // This isn't the only incoming relation to the asset, clone before inlining.
      this.to.clone(this);
    }
    this.to.incomingInlineRelation = this;
    if (!this.to.isInline) {
      this.to.url = null;
    }
    return this;
  }

  /**
   * Attaches the relation to an asset.
   *
   * The ordering of certain relation types is significant
   * (`HtmlScript`, for instance), so it's important that the order
   * isn't scrambled in the indices. Therefore the caller must
   * explicitly specify a position at which to insert the object.
   *
   * @param {String} position `"first"`, `"last"`, `"before"`, or `"after"`.
   * @param {Relation} adjacentRelation The adjacent relation, mandatory if the position is `"before"` or `"after"`.
   * @return {Relation} The relation itself (chaining-friendly).
   */
  attach(position, adjacentRelation) {
    this.from.markDirty();
    this.addToOutgoingRelations(position, adjacentRelation);
    if (this.to && this.to.url) {
      this.refreshHref();
    }
    return this;
  }

  addToOutgoingRelations(position, adjacentRelation) {
    const outgoingRelations = this.from.outgoingRelations;
    const existingIndex = outgoingRelations.indexOf(this);
    if (existingIndex !== -1) {
      outgoingRelations.splice(existingIndex, 1);
    }
    if (position === 'last') {
      outgoingRelations.push(this);
    } else if (position === 'first') {
      outgoingRelations.unshift(this);
    } else if (position === 'before' || position === 'after') {
      // Assume 'before' or 'after'
      if (!adjacentRelation || !adjacentRelation.isRelation) {
        throw new Error(
          `addRelation: Adjacent relation is not a relation: ${adjacentRelation}`
        );
      }
      const i =
        outgoingRelations.indexOf(adjacentRelation) +
        (position === 'after' ? 1 : 0);
      if (i === -1) {
        throw new Error(
          `addRelation: Adjacent relation ${adjacentRelation.toString()} is not among the outgoing relations of ${
            this.urlOrDescription
          }`
        );
      }
      outgoingRelations.splice(i, 0, this);
    } else {
      throw new Error(`addRelation: Illegal 'position' argument: ${position}`);
    }
  }

  /**
   * Detaches the relation from the asset it is currently attached
   * to. If the relation is currently part of a graph, it will
   * removed from it.
   *
   * Detaching implies that the tag/statement/declaration
   * representing the relation is physically removed from the
   * referring asset. Not all relation types support this.
   *
   * @return {Relation} The relation itself (chaining-friendly).
   */
  detach() {
    this.from.markDirty();
    this.remove();
    return this;
  }

  /**
   * Removes the relation from the graph it's currently part
   * of. Doesn't detach the relation (compare with
   * [`relation.detach()`]{@link Relation#detach}).
   *
   * @return {Relation} The relation itself (chaining-friendly).
   * @api public
   */
  remove() {
    this.from.removeRelation(this);
    return this;
  }

  /**
   * Get a brief text containing the type, id of the relation. Will
   * also contain the `.toString()` of the relation's source and
   * target assets if available.
   *
   * @return {String} The string, eg. `"[HtmlAnchor/141: [Html/40 file:///foo/bar/index.html] => [Html/76 file:///foo/bar/otherpage.html]]"``
   */
  toString() {
    return (
      '[' +
      this.type +
      '/' +
      this.id +
      ': ' +
      (this.from && this.to
        ? this.from.toString() +
          ' => ' +
          (this.to.isAsset
            ? this.to.toString()
            : this.to.url || this.to.type || '?')
        : 'unattached') +
      ']'
    );
  }
}

Object.assign(Relation.prototype, {
  /**
   * Property that's true for all relation instances. Avoids
   * reliance on the `instanceof` operator.
   *
   * @constant
   * @type {Boolean}
   * @memberOf Relation#
   */
  isRelation: true
});

module.exports = Relation;
<commit_msg>Maintain incomingInlineRelation when removing a relation<commit_after>const _ = require('lodash');
const extendDefined = require('../util/extendDefined');
const urlTools = require('urltools');
const urlModule = require('url');

/**
 * In graph terminology a relation represents a directed edge, a
 * reference from one asset to another. For the purpose of being able
 * to treat all relations equally, there's a subclass for each
 * supported relation type, encapsulating the details of how to
 * retrieve, update, and (optionally) inline the asset being pointed
 * to.
 *
 * These are some examples of included subclasses:
 *
 *    - `relations.HtmlAnchor`         An anchor tag in an HTML document `<a href='...'>`.
 *    - `relations.HtmlImage`          An `<img src='...'>` tag in an HTML document.
 *    - `relations.CssImport`          An `@import` declaration in a CSS asset.
 *    - `relations.CacheManifestEntry` A line in a cache manifest.
 */
class Relation {
  /**
   * Create a new Relation instance. For existing assets the
   * instantiation of relations happens automatically if you use the
   * `populate` transform. You only need to create relations manually
   * when you need to introduce new ones.
   *
   * Note that the base Relation class should be considered
   * abstract. Please instantiate the appropriate subclass.
   *
   * Options:
   *
   *  - `from` The source asset of the relation.
   *  - `to`   The target asset of the relation, or an asset configuration
   *           object if the target asset hasn't yet been resolved and created.
   *
   * @constructor
   * @param {Object} config The relation configuration
   */
  constructor(config) {
    if (config.hrefType) {
      this._hrefType = config.hrefType;
      config.hrefType = undefined;
    }
    if (config.to) {
      this._to = config.to;
      config.to = undefined;
    }
    extendDefined(this, config);
    this.id = '' + _.uniqueId();
  }

  /**
   * The source asset of the relation.
   *
   * @member {Asset} Relation#from
   */

  /**
   * The target asset of the relation.
   *
   * @type {Asset}
   */
  get to() {
    return this._to;
  }

  set to(to) {
    if (this.to && this.to.incomingInlineRelation === this) {
      this.to.incomingInlineRelation = undefined;
    }
    this._to = this.from.assetGraph.addAsset(to, this);
    this.refreshHref();
  }

  /**
   * The ATS/DOM-node where the Relation originates in the parent document
   *
   * @member {Object} Relation#node
   */

  /**
   * Get or set the href of the relation. The relation must be
   * attached to an asset.
   *
   * What is actually retrieved or updated depends on the relation
   * type. For `HtmlImage` the `src` attribute of the HTML element
   * is changed, for `CssImport` the parsed representation of
   * the @import rule is updated, etc.
   *
   * Most of the time you don't need to think about this property,
   * as the href is automatically updated when the url of the source
   * or target asset is changed, or an intermediate asset is
   * inlined.
   *
   * @member {String} Relation#href
   */

  /**
   * Get or set the fragment identifier part of the href of the relation.
   *
   * Setting a fragment with a non-empty value requires the value to begin with `#`
   *
   * @type {String}
   */
  get fragment() {
    const href = this.href;
    if (typeof href === 'string') {
      const indexOfFragment = href.indexOf('#');
      if (indexOfFragment === -1) {
        return '';
      } else {
        return href.substr(indexOfFragment);
      }
    }
  }

  set fragment(fragment) {
    if (fragment && !/^#/.test(fragment)) {
      throw new Error('The fragment must begin with a # or be empty');
    }
    const href = this.href;
    if (href) {
      let currentHrefWithoutFragment;
      const indexOfFragment = href.indexOf('#');
      if (indexOfFragment === -1) {
        currentHrefWithoutFragment = href;
      } else {
        currentHrefWithoutFragment = href.substr(0, indexOfFragment);
      }
      this.href = currentHrefWithoutFragment + (fragment || '');
      this.from.markDirty();
    }
  }

  /**
   * Update `href` of a relation to make sure it points at the
   * current url of its target asset.
   *
   * It's not necessary to call this function manually as long as
   * the source and target assets of the relation have only been
   * moved by having their `url` property changed (the recommended
   * way), but some transforms will need this after some low-level
   * surgery, such as attaching an existing relation to a different
   * asset.
   *
   * @return {Relation} The relation itself (chaining-friendly).
   */
  refreshHref() {
    const hrefType = this.hrefType;
    if (hrefType === 'inline') {
      this.inline();
    } else {
      const currentHref = this.href;
      if (/^#/.test(currentHref)) {
        // Pure fragment url, nothing to do
        return;
      }
      const targetUrl = this.to.url;
      const assetGraph = this.from.assetGraph;
      const canonical = this.canonical;
      let href;
      if (hrefType === 'rootRelative' && !canonical) {
        href = urlTools.buildRootRelativeUrl(
          this.baseUrl,
          targetUrl,
          assetGraph.root
        );
      } else if (hrefType === 'relative' && !canonical) {
        href = urlTools.buildRelativeUrl(this.baseUrl, targetUrl);
      } else if (hrefType === 'protocolRelative') {
        href = urlTools.buildProtocolRelativeUrl(this.baseUrl, targetUrl);
      } else {
        // Absolute
        href = targetUrl;
      }
      // Hack: Avoid adding index.html to an href pointing at file://.../index.html if it's not already there:
      if (
        /^file:\/\/.*\/index\.html(?:[?#]|$)/.test(targetUrl) &&
        !/(?:^|\/)index\.html(?:[?#]:|$)/.test(this.href)
      ) {
        href = href.replace(/(^|\/)index\.html(?=[?#]|$)/, '$1');
      }
      if (canonical) {
        href = href.replace(assetGraph.root, assetGraph.canonicalRoot);
      }
      const currentFragment = this.fragment;
      if (currentFragment) {
        href += currentFragment;
      }
      if (currentHref !== href) {
        this.href = href;
        this.from.markDirty();
      }
    }
    return this;
  }

  /**
   * Get the relations cross origins status defined as
   * differences in protocol or hostname.
   *
   * This property is quite useful as part of a population
   * query to ensure that the graph doesn't expand beyond
   * the current host or file system
   *
   * @type {Boolean}
   */
  get crossorigin() {
    const fromUrl = this.from.nonInlineAncestor.url;
    const toUrl = this.to.url;
    if (!toUrl) {
      // Inline
      return false;
    }
    if (this.canonical) {
      return false;
    }
    const fromUrlObj = urlModule.parse(fromUrl);
    const toUrlObj = urlModule.parse(urlModule.resolve(fromUrl, toUrl));
    if (
      fromUrlObj.protocol !== toUrlObj.protocol ||
      fromUrlObj.hostname !== toUrlObj.hostname
    ) {
      return true;
    }
    const fromPort = fromUrlObj.port
      ? parseInt(fromUrlObj.port, 10)
      : { 'http:': 80, 'https:': 443 }[fromUrlObj.protocol];
    const toPort = toUrlObj.port
      ? parseInt(toUrlObj.port, 10)
      : { 'http:': 80, 'https:': 443 }[toUrlObj.protocol];
    return fromPort !== toPort;
  }

  /**
   * Get or set the canonical state of a Relation
   *
   * If [AssetGraph]{@link AssetGraph} has a [`canonicalRoot`]{@link AssetGraph#canonicalRoot}
   * property, AssetGraph will detect absolute URLs matching `AssetGraph.canonicalRoot`
   * as if they were of [`hrefType`]{@link Relation#hrefType} `rootRelative`.
   *
   * The getter tell you if [`Relation.href`]{@link Relation#href} will be prepended
   * with `Assetgraph.canonicalRoot`.
   *
   * The setter will change the Relation's `href` to be prefixed with `AssetGraph.canonicalRoot`
   * if `true`, and without if `false`
   *
   * @type {Boolean}
   */
  get canonical() {
    if (typeof this._canonical === 'undefined') {
      let canonical = false;

      if (
        this.href &&
        this.from &&
        this.from.assetGraph &&
        this.from.assetGraph.canonicalRoot
      ) {
        const canonicalRootObj = urlModule.parse(
          this.from.assetGraph.canonicalRoot,
          false,
          true
        );
        const hrefObj = urlModule.parse(this.href, false, true);

        canonical =
          hrefObj.slashes === true &&
          ['http:', 'https:', null].includes(hrefObj.protocol) &&
          canonicalRootObj.host === hrefObj.host &&
          hrefObj.path.startsWith(canonicalRootObj.path) &&
          (canonicalRootObj.protocol === hrefObj.protocol ||
            canonicalRootObj.protocol === null);
      }

      this._canonical = canonical;
    }

    return this._canonical;
  }

  set canonical(isCanonical) {
    if (
      this.from &&
      this.from.assetGraph &&
      this.from.assetGraph.canonicalRoot
    ) {
      isCanonical = !!isCanonical;
      if (this._canonical !== isCanonical) {
        this._canonical = isCanonical;

        if (
          !isCanonical &&
          (this._hrefType === 'absolute' ||
            this._hrefType !== 'protocolRelative')
        ) {
          // We're switching to non-canonical mode. Degrade the href type
          // to rootRelative so we won't issue absolute file:// urls
          // This is based on guesswork, though.
          this._hrefType = 'rootRelative';
        }
        this.refreshHref();
      }
    }
  }

  /**
   * Get the url of the first non-inlined ancestor of the `from`-Asset
   *
   * @type {String}
   */
  get baseUrl() {
    const nonInlineAncestor = this.from.nonInlineAncestor;
    return nonInlineAncestor && nonInlineAncestor.url;
  }

  /**
   * Either `'absolute'`, `'rootRelative'`, `'protocolRelative'`, `'relative'`, or `'inline'`.
   * Decides what "degree" of relative url [`refreshHref()`]{@link Relation#refreshHref} tries to issue, except for
   * `'inline'` which means that the target asset is contained in a `data:` url or similar.
   *
   * @type {String}
   */
  get hrefType() {
    const to = this.to;
    if (to.isInline && to !== this.from) {
      return 'inline';
    }
    if (!this._hrefType) {
      const href = (this.href || '').trim();
      if (/^\/\//.test(href)) {
        this._hrefType = 'protocolRelative';
      } else if (/^\//.test(href)) {
        this._hrefType = 'rootRelative';
      } else if (/^[a-z+]+:/i.test(href)) {
        this._hrefType = 'absolute';
      } else {
        this._hrefType = 'relative';
      }
    }
    return this._hrefType;
  }

  set hrefType(hrefType) {
    const existingHrefType = this.hrefType;
    if (hrefType === 'inline') {
      this.inline();
    } else if (hrefType !== existingHrefType) {
      this._hrefType = hrefType;
      if (existingHrefType === 'inline') {
        this.to.externalize();
      } else {
        this.refreshHref();
      }
    }
  }

  /**
   * Inline the relation. This is only supported by certain relation
   * types and will produce different results depending on the type
   * (`data:` url, inline script, inline stylesheet...).
   *
   * Will make a clone of the target asset if it has more incoming
   * relations than this one.
   *
   * @return {Relation} The relation itself (chaining-friendly).
   */
  inline() {
    if (
      this.to.isAsset &&
      this.to.incomingRelations.filter(r => r._to !== r.from).length > 1
    ) {
      // This isn't the only incoming relation to the asset, clone before inlining.
      this.to.clone(this);
    }
    this.to.incomingInlineRelation = this;
    if (!this.to.isInline) {
      this.to.url = null;
    }
    return this;
  }

  /**
   * Attaches the relation to an asset.
   *
   * The ordering of certain relation types is significant
   * (`HtmlScript`, for instance), so it's important that the order
   * isn't scrambled in the indices. Therefore the caller must
   * explicitly specify a position at which to insert the object.
   *
   * @param {String} position `"first"`, `"last"`, `"before"`, or `"after"`.
   * @param {Relation} adjacentRelation The adjacent relation, mandatory if the position is `"before"` or `"after"`.
   * @return {Relation} The relation itself (chaining-friendly).
   */
  attach(position, adjacentRelation) {
    this.from.markDirty();
    this.addToOutgoingRelations(position, adjacentRelation);
    if (this.to && this.to.url) {
      this.refreshHref();
    }
    return this;
  }

  addToOutgoingRelations(position, adjacentRelation) {
    const outgoingRelations = this.from.outgoingRelations;
    const existingIndex = outgoingRelations.indexOf(this);
    if (existingIndex !== -1) {
      outgoingRelations.splice(existingIndex, 1);
    }
    if (position === 'last') {
      outgoingRelations.push(this);
    } else if (position === 'first') {
      outgoingRelations.unshift(this);
    } else if (position === 'before' || position === 'after') {
      // Assume 'before' or 'after'
      if (!adjacentRelation || !adjacentRelation.isRelation) {
        throw new Error(
          `addRelation: Adjacent relation is not a relation: ${adjacentRelation}`
        );
      }
      const i =
        outgoingRelations.indexOf(adjacentRelation) +
        (position === 'after' ? 1 : 0);
      if (i === -1) {
        throw new Error(
          `addRelation: Adjacent relation ${adjacentRelation.toString()} is not among the outgoing relations of ${
            this.urlOrDescription
          }`
        );
      }
      outgoingRelations.splice(i, 0, this);
    } else {
      throw new Error(`addRelation: Illegal 'position' argument: ${position}`);
    }
  }

  /**
   * Detaches the relation from the asset it is currently attached
   * to. If the relation is currently part of a graph, it will
   * removed from it.
   *
   * Detaching implies that the tag/statement/declaration
   * representing the relation is physically removed from the
   * referring asset. Not all relation types support this.
   *
   * @return {Relation} The relation itself (chaining-friendly).
   */
  detach() {
    this.from.markDirty();
    this.remove();
    return this;
  }

  /**
   * Removes the relation from the graph it's currently part
   * of. Doesn't detach the relation (compare with
   * [`relation.detach()`]{@link Relation#detach}).
   *
   * @return {Relation} The relation itself (chaining-friendly).
   * @api public
   */
  remove() {
    if (this.to && this.to.incomingInlineRelation === this) {
      this.to.incomingInlineRelation = undefined;
    }
    this.from.removeRelation(this);
    return this;
  }

  /**
   * Get a brief text containing the type, id of the relation. Will
   * also contain the `.toString()` of the relation's source and
   * target assets if available.
   *
   * @return {String} The string, eg. `"[HtmlAnchor/141: [Html/40 file:///foo/bar/index.html] => [Html/76 file:///foo/bar/otherpage.html]]"``
   */
  toString() {
    return (
      '[' +
      this.type +
      '/' +
      this.id +
      ': ' +
      (this.from && this.to
        ? this.from.toString() +
          ' => ' +
          (this.to.isAsset
            ? this.to.toString()
            : this.to.url || this.to.type || '?')
        : 'unattached') +
      ']'
    );
  }
}

Object.assign(Relation.prototype, {
  /**
   * Property that's true for all relation instances. Avoids
   * reliance on the `instanceof` operator.
   *
   * @constant
   * @type {Boolean}
   * @memberOf Relation#
   */
  isRelation: true
});

module.exports = Relation;
<|endoftext|>


<commit_before>/*
 * Licensed to Cloudkick, Inc ('Cloudkick') under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Cloudkick licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

var util = require('util');
var path = require('path');

var sprintf = require('sprintf').sprintf;

var TestReporter = require('./base').TestReporter;
var testUtil = require('./../../util');

function TapReporter(tests, options) {
  TestReporter.call(this, tests, options);

  this._tempResults = {};
  this._successes = 0;
  this._failures = 0;
  this._timeouts = 0;
}

util.inherits(TapReporter, TestReporter);

TapReporter.prototype.handleTestsStart = function() {
};

TapReporter.prototype.handleTestFileStart = function(filePath) {
};

TapReporter.prototype.handleTestFileComplete = function(filePath,
                                                        stdout, stderr) {
  var tests, test, key;
  var fileName = path.basename(filePath);

  stdout = stdout || '';
  stderr = stderr || '';
  tests = this._testResults[filePath];

  for (key in tests) {
    if (tests.hasOwnProperty(key)) {
      test = tests[key];

      if (test.error) {
        this._addFailure(fileName, test.name);
      }
      else if (test.timeout) {
        this._addTimeout(fileName, 'timeout');
      }
      else if (test.status === 'skipped') {
        continue;
      }
      else {
        this._addSuccess(fileName, test.name);
      }
    }
  }
  /*if (error) {
    // Test file does not exist or an exception was thrown before the tests were
    // even run
    this._addFailure(fileName, error.name);
    return;
  }*/
};

TapReporter.prototype.handleTestsComplete = function() {
  this._reportResults();

  return this._failures + this._timeouts;
};

TapReporter.prototype._addSuccess = function(testFile, testName) {
  this._successes++;
  this._addResult(testFile, testName, 'success');
};

TapReporter.prototype._addFailure = function(testFile, testName) {
  this._failures++;
  this._addResult(testFile, testName, 'failure');
};

TapReporter.prototype._addTimeout = function(testFile, testName) {
  this._timeouts++;
  this._addResult(testFile, testName, 'timeout');
};

TapReporter.prototype._addResult = function(testFile, testName, testStatus) {
  if (!this._tempResults.hasOwnProperty(testFile)) {
    this._tempResults[testFile] = {};
  }

  this._tempResults[testFile][testName] = {
    'file': testFile,
    'name': testName,
    'status': testStatus
  };
};

TapReporter.prototype._reportResults = function() {
  var self = this;
  var startNum, i, files, file, filesLen, testsLen, tests, test, testResult, testNum;
  files = Object.keys(this._tempResults);
  filesLen = files.length;

  function getFileTestLen(file) {
    return Object.keys(self._tempResults[file]).length;
  }

  testsLen = files.map(getFileTestLen).reduce(function (a, b) { return a + b; });

  if (testsLen === 0) {
    startNum = 0;
  }
  else {
    startNum = 1;
  }

  console.log('  %d..%d', startNum, testsLen);

  testNum = 0;
  for (i = 0; i < filesLen; i++) {
    file = files[i];
    tests = this._tempResults[file];

    for (test in tests) {
      if (tests.hasOwnProperty(test)) {
        testNum++;
        testResult = tests[test];

        if (testResult.status === 'success') {
          console.log('  ok %d - %s: %s', testNum, testResult.file, testResult.name);
        }
        else if (testResult.status === 'failure') {
          console.log('  not ok %d - %s: %s', testNum, testResult.file, testResult.name);
        }
        else if (testResult.status === 'timeout') {
          console.log('  not ok %d - %s: %s (timeout)', testNum, testResult.file,
                       testResult.name);
        }
      }
    }
  }
};

exports.name = 'tap';
exports.klass = TapReporter;
<commit_msg>Simpler tap formatting<commit_after>/*
 * Licensed to Cloudkick, Inc ('Cloudkick') under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Cloudkick licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

var util = require('util');
var path = require('path');

var sprintf = require('sprintf').sprintf;

var TestReporter = require('./base').TestReporter;
var testUtil = require('./../../util');

function TapReporter(tests, options) {
  TestReporter.call(this, tests, options);

  this._tempResults = {};
  this._successes = 0;
  this._failures = 0;
  this._timeouts = 0;
}

util.inherits(TapReporter, TestReporter);

TapReporter.prototype.handleTestsStart = function() {
};

TapReporter.prototype.handleTestFileStart = function(filePath) {
};

TapReporter.prototype.handleTestFileComplete = function(filePath,
                                                        stdout, stderr) {
  var tests, test, key;
  var fileName = path.basename(filePath);

  stdout = stdout || '';
  stderr = stderr || '';
  tests = this._testResults[filePath];

  for (key in tests) {
    if (tests.hasOwnProperty(key)) {
      test = tests[key];

      if (test.error) {
        this._addFailure(fileName, test.name);
      }
      else if (test.timeout) {
        this._addTimeout(fileName, 'timeout');
      }
      else if (test.status === 'skipped') {
        continue;
      }
      else {
        this._addSuccess(fileName, test.name);
      }
    }
  }
  /*if (error) {
    // Test file does not exist or an exception was thrown before the tests were
    // even run
    this._addFailure(fileName, error.name);
    return;
  }*/
};

TapReporter.prototype.handleTestsComplete = function() {
  this._reportResults();

  return this._failures + this._timeouts;
};

TapReporter.prototype._addSuccess = function(testFile, testName) {
  this._successes++;
  this._addResult(testFile, testName, 'success');
};

TapReporter.prototype._addFailure = function(testFile, testName) {
  this._failures++;
  this._addResult(testFile, testName, 'failure');
};

TapReporter.prototype._addTimeout = function(testFile, testName) {
  this._timeouts++;
  this._addResult(testFile, testName, 'timeout');
};

TapReporter.prototype._addResult = function(testFile, testName, testStatus) {
  if (!this._tempResults.hasOwnProperty(testFile)) {
    this._tempResults[testFile] = {};
  }

  this._tempResults[testFile][testName] = {
    'file': testFile,
    'name': testName,
    'status': testStatus
  };
};

TapReporter.prototype._reportResults = function() {
  var self = this;
  var startNum, i, files, file, filesLen, testsLen, tests, test, testResult, testNum;
  files = Object.keys(this._tempResults);
  filesLen = files.length;

  function getFileTestLen(file) {
    return Object.keys(self._tempResults[file]).length;
  }

  testsLen = files.map(getFileTestLen).reduce(function (a, b) { return a + b; });

  if (testsLen === 0) {
    startNum = 0;
  }
  else {
    startNum = 1;
  }

  console.log('%d..%d', startNum, testsLen);

  testNum = 0;
  for (i = 0; i < filesLen; i++) {
    file = files[i];
    tests = this._tempResults[file];

    for (test in tests) {
      if (tests.hasOwnProperty(test)) {
        testNum++;
        testResult = tests[test];

        if (testResult.status === 'success') {
          console.log('ok %d - %s: %s', testNum, testResult.file, testResult.name);
        }
        else if (testResult.status === 'failure') {
          console.log('not ok %d - %s: %s', testNum, testResult.file, testResult.name);
        }
        else if (testResult.status === 'timeout') {
          console.log('not ok %d - %s: %s (timeout)', testNum, testResult.file,
                       testResult.name);
        }
      }
    }
  }
};

exports.name = 'tap';
exports.klass = TapReporter;
<|endoftext|>


<commit_before>'use strict';

const passport = require('passport');
const FacebookStrategy = require('passport-facebook');

module.exports = function (config, userData, env) {
  if (env !== 'development') {
    const facebookStrategy = new FacebookStrategy({
      clientID: config.facebookAppId,
      clientSecret: config.facebookAppSecret,
      callbackURL: config.facebookCallbackUrl
    },
      function (accessToken, refreshToken, profile, next) {
        userData.getUserByFacebookId(profile.id)
          .then((user) => {
            if (user) {
              return next(null, user);
            }

            return userData.createUserFromFacebookUser(profile);
          })
          .then((user) => {
            next(null, user);
          })
          .catch((err) => {
            console.log(err);
            next(err, false);
          });
      });

    passport.use(facebookStrategy);
  }
};<commit_msg>removed console log<commit_after>'use strict';

const passport = require('passport');
const FacebookStrategy = require('passport-facebook');

module.exports = function (config, userData, env) {
  if (env !== 'development') {
    const facebookStrategy = new FacebookStrategy({
      clientID: config.facebookAppId,
      clientSecret: config.facebookAppSecret,
      callbackURL: config.facebookCallbackUrl
    },
      function (accessToken, refreshToken, profile, next) {
        userData.getUserByFacebookId(profile.id)
          .then((user) => {
            if (user) {
              return next(null, user);
            }

            return userData.createUserFromFacebookUser(profile);
          })
          .then((user) => {
            next(null, user);
          })
          .catch((err) => {
            next(err, false);
          });
      });

    passport.use(facebookStrategy);
  }
};<|endoftext|>


<commit_before>module.exports = function () {

/*
* Format of a permission {
* role: 'basic-user' || {'basic-user':{match: true}}
* customValidator: functionName
* }
*/

//  TODO:50 ensure is_own_dojo for dojo-admin && belongs_to for champion

  return {
    'cd-dojos':{
      'get_dojo_config': [{
        role: 'none'
      }],
      'search_bounding_box': [{
        role: 'none'
      }],
      //Used by front-e resolver, must restrict fields
      'find': [{
        role: 'none'
      }],
      'search': [{
        role: 'cdf-admin'
      }],
      // TODO : perm for ctrl:'dojo', cmd: 'save'
      'update': [{
        role: 'basic-user',
        customValidator: [{
            role: 'cd-dojos',
            cmd: 'have_permissions_on_dojo',
            perm: 'dojo-admin'
        }]
      }],
      'update_image': [{
        role: 'basic-user',
        customValidator: [{
            role: 'cd-dojos',
            cmd: 'have_permissions_on_dojo',
            perm: 'dojo-admin'
        }]
      }],
      'dojos_by_country': [{
        role: 'none'
      }],
      'list': [{
        role: 'none',
      }],

      'load': [{
        role: 'none'
      }],
      'bulk_delete': [{
        role: 'cdf-admin'
      }],

      'get_stats': [{
        role: 'cdf-admin'
      }],

      'load_setup_dojo_steps': [{
        role: 'basic-user'
      }],

      //  TODO:120 split as "belongs_to_dojo"
      'load_usersdojos': [{
        role: 'basic-user'
      }],

      'load_dojo_users': [{
        role: 'basic-user',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'have_permissions_on_dojo',
          perm: 'dojo-admin'
        }]
      }],

      'dojos_for_user': [{
        role: 'basic-user',
        customValidator:[{
          role: 'cd-users',
          cmd: 'is_self'
        }]
      }, {
        role: 'basic-user',
        customValidator: [{
          role: 'cd-users',
          cmd: 'is_parent_of',
        }]
      }],

      //  TODO:130 split join from manage userdojos
      'save_usersdojos': [{
        role: 'basic-user'
      }],
      'remove_usersdojos': [ {
        role: 'basic-user',
        customValidator:[{
          role: 'cd-users',
          cmd: 'is_self'
        }]
      }, {
        role: 'basic-user',
        customValidator:[{
          role: 'cd-dojos',
          cmd: 'have_permissions_on_dojo',
          perm: 'dojo-admin'
        }]
      }, {
        role: 'basic-user',
        userType: 'parent-guardian',
        customValidator:[{
          role: 'cd-users',
          cmd: 'is_parent_of'
        }]
      }
    ],
      'export_dojo_users': [{
        role: 'basic-user',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'have_permissions_on_dojo',
          perm: 'dojo-admin'
        }]
      }],
      'generate_user_invite_token': [{
        role: 'basic-user',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'have_permissions_on_dojo',
          perm: 'dojo-admin'
        }]
      }],
      'accept_user_invite': [{
        role: 'basic-user',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'is_own_invite'
        }]
      }],
      'request_user_invite': [{
        role: 'basic-user'
      }],
      'accept_user_request': [{
        // No validator here as check is post-recovery of data
        role: 'basic-user'
      }],
      'search_join_requests': [{
        role: 'basic-user',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'have_permissions_on_dojo',
          perm: 'dojo-admin'
        }]
      }],
      'decline_user_request': [{
        // No validator here as check is post-recovery of data
        role: 'basic-user'
      }],

      'update_founder': [{
        role: 'basic-user',
        userType: 'champion',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'is_founder'
        }]
      }],

      'notify_all_members': [
      {
        role: 'basic-user',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'have_permissions_on_dojo',
          perm: 'dojo-admin'
        }]
      }],

      //  It returns an static object, don't be afraid
      'get_user_permissions': [{
        role: 'none',
      }],
      'get_user_types': [{
        role: 'none'
      }],

      'search_nearest_dojos': [{
        role: 'none'
      }],

      'list_countries': [{
        role: 'none'
      }],
      'list_places': [{
        role: 'none'
      }],
      //  Not even used
      'countries_lat_long': [{
        role: 'none'
      }],
      'continent_codes': [{
        role: 'none'
      }],
      'poll_count': [{
        role: 'none'
      }],
      // It's by design, but we know it's insecure as it could allow to vote as as another dojo
      'save_poll_result': [{
        role: 'none'
      }],
      'get_poll_setup': [{
        role: 'none'
      }],
      'get_poll_results': [{
        role: 'none'
      }],
      'remove_poll_result': [{
        role: 'cdf-admin',
      }],
      'send_test_email_poll': [{
        role: 'cdf-admin'
      }],
      'start_poll': [{
        role: 'cdf-admin'
      }],
      'queue_email_poll': [{
        role: 'cdf-admin'
      }],
      'dojo': {
        'bulkVerify': [{
          role: 'cdf-admin'
        }],
        'verify': [{
          role: 'cdf-admin'
        }],
        'joinedDojos': [{
          role: 'basic-user'
        }],
        'delete': [{
          role: 'cdf-admin'
        }]
      },
      'load_dojo_lead': [{
        role: 'basic-user',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'is_own_lead'
        }]
      }],
      'lead': {
        'search': [{
          role: 'basic-user',
          customValidator: [{
            role: 'cd-dojos',
            cmd: 'is_own_lead'
          }]
        }, {
          role: 'basic-user',
          customValidator: [{
            role: 'cd-users',
            cmd: 'is_self'
          }]
        }],
        'save': [{
          role: 'basic-user',
          customValidator: [{
            role: 'cd-dojos',
            cmd: 'is_own_lead'
          }]
        }],
        'submit': [{
          role: 'basic-user',
          customValidator: [{
            role: 'cd-dojos',
            cmd: 'is_own_lead'
          }]
        }],
        'delete': [{
          role: 'cdf-admin'
        }]
      }
    }
  };
};
<commit_msg>Refine polls permissions (#402)<commit_after>module.exports = function () {

/*
* Format of a permission {
* role: 'basic-user' || {'basic-user':{match: true}}
* customValidator: functionName
* }
*/

//  TODO:50 ensure is_own_dojo for dojo-admin && belongs_to for champion

  return {
    'cd-dojos':{
      'get_dojo_config': [{
        role: 'none'
      }],
      'search_bounding_box': [{
        role: 'none'
      }],
      //Used by front-e resolver, must restrict fields
      'find': [{
        role: 'none'
      }],
      'search': [{
        role: 'cdf-admin'
      }],
      // TODO : perm for ctrl:'dojo', cmd: 'save'
      'update': [{
        role: 'basic-user',
        customValidator: [{
            role: 'cd-dojos',
            cmd: 'have_permissions_on_dojo',
            perm: 'dojo-admin'
        }]
      }],
      'update_image': [{
        role: 'basic-user',
        customValidator: [{
            role: 'cd-dojos',
            cmd: 'have_permissions_on_dojo',
            perm: 'dojo-admin'
        }]
      }],
      'dojos_by_country': [{
        role: 'none'
      }],
      'list': [{
        role: 'none',
      }],

      'load': [{
        role: 'none'
      }],
      'bulk_delete': [{
        role: 'cdf-admin'
      }],

      'get_stats': [{
        role: 'cdf-admin'
      }],

      'load_setup_dojo_steps': [{
        role: 'basic-user'
      }],

      //  TODO:120 split as "belongs_to_dojo"
      'load_usersdojos': [{
        role: 'basic-user'
      }],

      'load_dojo_users': [{
        role: 'basic-user',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'have_permissions_on_dojo',
          perm: 'dojo-admin'
        }]
      }],

      'dojos_for_user': [{
        role: 'basic-user',
        customValidator:[{
          role: 'cd-users',
          cmd: 'is_self'
        }]
      }, {
        role: 'basic-user',
        customValidator: [{
          role: 'cd-users',
          cmd: 'is_parent_of',
        }]
      }],

      //  TODO:130 split join from manage userdojos
      'save_usersdojos': [{
        role: 'basic-user'
      }],
      'remove_usersdojos': [ {
        role: 'basic-user',
        customValidator:[{
          role: 'cd-users',
          cmd: 'is_self'
        }]
      }, {
        role: 'basic-user',
        customValidator:[{
          role: 'cd-dojos',
          cmd: 'have_permissions_on_dojo',
          perm: 'dojo-admin'
        }]
      }, {
        role: 'basic-user',
        userType: 'parent-guardian',
        customValidator:[{
          role: 'cd-users',
          cmd: 'is_parent_of'
        }]
      }
    ],
      'export_dojo_users': [{
        role: 'basic-user',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'have_permissions_on_dojo',
          perm: 'dojo-admin'
        }]
      }],
      'generate_user_invite_token': [{
        role: 'basic-user',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'have_permissions_on_dojo',
          perm: 'dojo-admin'
        }]
      }],
      'accept_user_invite': [{
        role: 'basic-user',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'is_own_invite'
        }]
      }],
      'request_user_invite': [{
        role: 'basic-user'
      }],
      'accept_user_request': [{
        // No validator here as check is post-recovery of data
        role: 'basic-user'
      }],
      'search_join_requests': [{
        role: 'basic-user',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'have_permissions_on_dojo',
          perm: 'dojo-admin'
        }]
      }],
      'decline_user_request': [{
        // No validator here as check is post-recovery of data
        role: 'basic-user'
      }],

      'update_founder': [{
        role: 'basic-user',
        userType: 'champion',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'is_founder'
        }]
      }],

      'notify_all_members': [
      {
        role: 'basic-user',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'have_permissions_on_dojo',
          perm: 'dojo-admin'
        }]
      }],

      //  It returns an static object, don't be afraid
      'get_user_permissions': [{
        role: 'none',
      }],
      'get_user_types': [{
        role: 'none'
      }],

      'search_nearest_dojos': [{
        role: 'none'
      }],

      'list_countries': [{
        role: 'none'
      }],
      'list_places': [{
        role: 'none'
      }],
      //  Not even used
      'countries_lat_long': [{
        role: 'none'
      }],
      'continent_codes': [{
        role: 'none'
      }],
      'poll_count': [{
        role: 'none'
      }],
      // It's by design, but we know it's insecure as it could allow to vote as as another dojo
      'save_poll_result': [{
        role: 'none'
      }],
      'save_poll_setup': [{
        role: 'cdf-admin'
      }],
      'get_poll_setup': [{
        role: 'none'
      }],
      'get_poll_results': [{
        role: 'none'
      }],
      'remove_poll_result': [{
        role: 'cdf-admin',
      }],
      'send_test_email_poll': [{
        role: 'cdf-admin'
      }],
      'start_poll': [{
        role: 'cdf-admin'
      }],
      'queue_email_poll': [{
        role: 'cdf-admin'
      }],
      'dojo': {
        'bulkVerify': [{
          role: 'cdf-admin'
        }],
        'verify': [{
          role: 'cdf-admin'
        }],
        'joinedDojos': [{
          role: 'basic-user'
        }],
        'delete': [{
          role: 'cdf-admin'
        }]
      },
      'load_dojo_lead': [{
        role: 'basic-user',
        customValidator: [{
          role: 'cd-dojos',
          cmd: 'is_own_lead'
        }]
      }],
      'lead': {
        'search': [{
          role: 'basic-user',
          customValidator: [{
            role: 'cd-dojos',
            cmd: 'is_own_lead'
          }]
        }, {
          role: 'basic-user',
          customValidator: [{
            role: 'cd-users',
            cmd: 'is_self'
          }]
        }],
        'save': [{
          role: 'basic-user',
          customValidator: [{
            role: 'cd-dojos',
            cmd: 'is_own_lead'
          }]
        }],
        'submit': [{
          role: 'basic-user',
          customValidator: [{
            role: 'cd-dojos',
            cmd: 'is_own_lead'
          }]
        }],
        'delete': [{
          role: 'cdf-admin'
        }]
      }
    }
  };
};
<|endoftext|>


<commit_before>Monocle.Dimensions.Columns = function (pageDiv) {

  var API = { constructor: Monocle.Dimensions.Columns }
  var k = API.constants = API.constructor;
  var p = API.properties = {
    page: pageDiv,
    reader: pageDiv.m.reader,
    length: 0,
    width: 0
  }

  // Logically, forceColumn browsers can't have a gap, because that would
  // make the minWidth > 200%. But how much greater? Not worth the effort.
  k.GAP = Monocle.Browser.env.forceColumns ? 0 : 20;

  function update(callback) {
    setColumnWidth();
    Monocle.defer(function () {
      p.length = columnCount();
      if (Monocle.DEBUG) {
        console.log(
          'page['+p.page.m.pageIndex+'] -> '+p.length+
          ' ('+p.page.m.activeFrame.m.component.properties.id+')'
        );
      }
      callback(p.length);
    });
  }

  function setColumnWidth() {
    var pdims = pageDimensions();
    var ce = columnedElement();

    p.width = pdims.width;

    var cer = Monocle.Styles.rulesToString(k.STYLE["columned"]);
    cer += Monocle.Browser.css.toCSSDeclaration('column-width', pdims.col+'px');
    cer += Monocle.Browser.css.toCSSDeclaration('column-gap', k.GAP+'px');
    cer += Monocle.Browser.css.toCSSDeclaration('column-fill', 'auto');
    cer += Monocle.Browser.css.toCSSDeclaration('transform', 'none');

    if (Monocle.Browser.env.forceColumns && ce.scrollHeight > pdims.height) {
      cer += Monocle.Styles.rulesToString(k.STYLE['column-force']);
      if (Monocle.DEBUG) {
        console.warn("Force columns ("+ce.scrollHeight+" > "+pdims.height+")");
      }
    }

    var rules = [
      'html#RS\\:monocle body { '+cer+' }',
      'html#RS\\:monocle * {',
        'max-width: '+pdims.col+'px !important;',
      '}',
      'img, video, audio, object, svg {',
        'max-height: '+pdims.height+'px !important;',
      '}'
    ]

    // IE10 hack.
    if (Monocle.Browser.env.documentElementHasScrollbars) {
      rules.push('html { overflow: hidden !important; }');
    }

    rules = rules.join('\n');

    var doc = p.page.m.activeFrame.contentDocument;
    var head = doc.querySelector('head');
    var sty = head.querySelector('style#monocle_column_rules');
    if (!sty) {
      sty = doc.createElement('style');
      sty.id = 'monocle_column_rules';
      head.appendChild(sty);
    }

    if (sty.innerHTML != rules) {
      // Update offset because we're translating to zero.
      p.page.m.offset = 0;

      // Apply body style changes.
      sty.innerHTML = rules;

      if (Monocle.Browser.env.scrollToApplyStyle) {
        ce.scrollLeft = 0;
      }
    }
  }

  // Returns the element to which columns CSS should be applied.
  //
  function columnedElement() {
    return p.page.m.activeFrame.contentDocument.body;
  }

  // Returns the width of the offsettable area of the columned element. By
  // definition, the number of pages is always this divided by the
  // width of a single page (eg, the client area of the columned element).
  //
  function columnedWidth() {
    var bd = columnedElement();
    var de = p.page.m.activeFrame.contentDocument.documentElement;

    var w = Math.max(bd.scrollWidth, de.scrollWidth);

    // Add one because the final column doesn't have right gutter.
    // w += k.GAP;

    if (!Monocle.Browser.env.widthsIgnoreTranslate && p.page.m.offset) {
      w += p.page.m.offset;
    }
    return w;
  }

  function pageDimensions() {
    var elem = p.page.m.sheafDiv;
    var w = elem.clientWidth;
    if (elem.getBoundingClientRect) { w = elem.getBoundingClientRect().width; }
    if (Monocle.Browser.env.roundPageDimensions) { w = Math.round(w); }
    return { col: w, width: w + k.GAP, height: elem.clientHeight }
  }

  function columnCount() {
    return Math.ceil(columnedWidth() / pageDimensions().width)
  }

  function locusToOffset(locus) {
    return pageDimensions().width * (locus.page - 1);
  }

  // Moves the columned element to the offset implied by the locus.
  //
  // The 'transition' argument is optional, allowing the translation to be
  // animated. If not given, no change is made to the columned element's
  // transition property.
  //
  function translateToLocus(locus, transition) {
    var offset = locusToOffset(locus);
    p.page.m.offset = offset;
    translateToOffset(offset, transition);
    return offset;
  }

  function translateToOffset(offset, transition) {
    var ce = columnedElement();
    if (transition) {
      Monocle.Styles.affix(ce, "transition", transition);
    }
    // NB: can't use setX as it causes a flicker on iOS.
    Monocle.Styles.affix(ce, "transform", "translateX(-"+offset+"px)");
  }

  function percentageThroughOfNode(target) {
    if (!target) { return 0; }
    var doc = p.page.m.activeFrame.contentDocument;
    var offset = 0;
    if (Monocle.Browser.env.findNodesByScrolling) {
      // First, remove translation...
      translateToOffset(0);

      // Store scroll offsets for all windows.
      var win, s;
      win = s = p.page.m.activeFrame.contentWindow;
      var scrollers = [
        [win, win.scrollX, win.scrollY],
        [window, window.scrollX, window.scrollY]
      ];
      //while (s != s.parent) { scrollers.push([s, s.scrollX]); s = s.parent; }

      var scroller, x;
      if (Monocle.Browser.env.sheafIsScroller) {
        scroller = p.page.m.sheafDiv;
        x = scroller.scrollLeft;
        target.scrollIntoView();
        offset = scroller.scrollLeft;
      } else {
        scroller = win;
        x = scroller.scrollX;
        target.scrollIntoView();
        offset = scroller.scrollX;
      }

      // Restore scroll offsets for all windows.
      while (s = scrollers.shift()) {
        s[0].scrollTo(s[1], s[2]);
      }

      // ... finally, replace translation.
      translateToOffset(p.page.m.offset);
    } else {
      offset = target.getBoundingClientRect().left;
      offset -= doc.body.getBoundingClientRect().left;
    }

    // We know at least 1px will be visible, and offset should not be 0.
    offset += 1;

    // Percent is the offset divided by the total width of the component.
    var percent = offset / (p.length * p.width);

    // Page number would be offset divided by the width of a single page.
    // var pageNum = Math.ceil(offset / pageDimensions().width);

    return percent;
  }

  API.update = update;
  API.percentageThroughOfNode = percentageThroughOfNode;

  API.locusToOffset = locusToOffset;
  API.translateToLocus = translateToLocus;

  return API;
}

Monocle.Dimensions.Columns.STYLE = {
  // Most of these are already applied to body, but they're repeated here
  // in case columnedElement() is ever anything other than body.
  "columned": {
    "margin": "0",
    "padding": "0",
    "height": "100%",
    "width": "100%",
    "position": "absolute"
  },
  "column-force": {
    "min-width": "200%",
    "overflow": "hidden"
  }
}
<commit_msg>Set columnedElement style directly, to ensure accurate page counts.<commit_after>Monocle.Dimensions.Columns = function (pageDiv) {

  var API = { constructor: Monocle.Dimensions.Columns }
  var k = API.constants = API.constructor;
  var p = API.properties = {
    page: pageDiv,
    reader: pageDiv.m.reader,
    length: 0,
    width: 0
  }

  // Logically, forceColumn browsers can't have a gap, because that would
  // make the minWidth > 200%. But how much greater? Not worth the effort.
  k.GAP = Monocle.Browser.env.forceColumns ? 0 : 20;

  function update(callback) {
    setColumnWidth();
    Monocle.defer(function () {
      p.length = columnCount();
      if (Monocle.DEBUG) {
        console.log(
          'page['+p.page.m.pageIndex+'] -> '+p.length+
          ' ('+p.page.m.activeFrame.m.component.properties.id+')'
        );
      }
      callback(p.length);
    });
  }

  function setColumnWidth() {
    var pdims = pageDimensions();
    var ce = columnedElement();

    p.width = pdims.width;

    var cer = Monocle.Styles.rulesToString(k.STYLE["columned"]);
    cer += Monocle.Browser.css.toCSSDeclaration('column-width', pdims.col+'px');
    cer += Monocle.Browser.css.toCSSDeclaration('column-gap', k.GAP+'px');
    cer += Monocle.Browser.css.toCSSDeclaration('column-fill', 'auto');
    cer += Monocle.Browser.css.toCSSDeclaration('transform', 'none');

    if (Monocle.Browser.env.forceColumns && ce.scrollHeight > pdims.height) {
      cer += Monocle.Styles.rulesToString(k.STYLE['column-force']);
      if (Monocle.DEBUG) {
        console.warn("Force columns ("+ce.scrollHeight+" > "+pdims.height+")");
      }
    }

    var rules = [
      'html#RS\\:monocle * {',
        'max-width: '+pdims.col+'px !important;',
      '}',
      'img, video, audio, object, svg {',
        'max-height: '+pdims.height+'px !important;',
      '}'
    ]

    // IE10 hack.
    if (Monocle.Browser.env.documentElementHasScrollbars) {
      rules.push('html { overflow: hidden !important; }');
    }

    rules = rules.join('\n');

    var doc = p.page.m.activeFrame.contentDocument;
    var head = doc.querySelector('head');
    var sty = head.querySelector('style#monocle_column_rules');
    if (!sty) {
      sty = doc.createElement('style');
      sty.id = 'monocle_column_rules';
      head.appendChild(sty);
    }

    if (sty.innerHTML != rules) {
      // Update offset because we're translating to zero.
      p.page.m.offset = 0;

      // Apply style changes.
      ce.style.cssText = cer;
      sty.innerHTML = rules;

      if (Monocle.Browser.env.scrollToApplyStyle) {
        ce.scrollLeft = 0;
      }
    }
  }

  // Returns the element to which columns CSS should be applied.
  //
  function columnedElement() {
    return p.page.m.activeFrame.contentDocument.body;
  }

  // Returns the width of the offsettable area of the columned element. By
  // definition, the number of pages is always this divided by the
  // width of a single page (eg, the client area of the columned element).
  //
  function columnedWidth() {
    var bd = columnedElement();
    var de = p.page.m.activeFrame.contentDocument.documentElement;

    var w = Math.max(bd.scrollWidth, de.scrollWidth);

    // Add one because the final column doesn't have right gutter.
    // w += k.GAP;

    if (!Monocle.Browser.env.widthsIgnoreTranslate && p.page.m.offset) {
      w += p.page.m.offset;
    }
    return w;
  }

  function pageDimensions() {
    var elem = p.page.m.sheafDiv;
    var w = elem.clientWidth;
    if (elem.getBoundingClientRect) { w = elem.getBoundingClientRect().width; }
    if (Monocle.Browser.env.roundPageDimensions) { w = Math.round(w); }
    return { col: w, width: w + k.GAP, height: elem.clientHeight }
  }

  function columnCount() {
    return Math.ceil(columnedWidth() / pageDimensions().width)
  }

  function locusToOffset(locus) {
    return pageDimensions().width * (locus.page - 1);
  }

  // Moves the columned element to the offset implied by the locus.
  //
  // The 'transition' argument is optional, allowing the translation to be
  // animated. If not given, no change is made to the columned element's
  // transition property.
  //
  function translateToLocus(locus, transition) {
    var offset = locusToOffset(locus);
    p.page.m.offset = offset;
    translateToOffset(offset, transition);
    return offset;
  }

  function translateToOffset(offset, transition) {
    var ce = columnedElement();
    if (transition) {
      Monocle.Styles.affix(ce, "transition", transition);
    }
    // NB: can't use setX as it causes a flicker on iOS.
    Monocle.Styles.affix(ce, "transform", "translateX(-"+offset+"px)");
  }

  function percentageThroughOfNode(target) {
    if (!target) { return 0; }
    var doc = p.page.m.activeFrame.contentDocument;
    var offset = 0;
    if (Monocle.Browser.env.findNodesByScrolling) {
      // First, remove translation...
      translateToOffset(0);

      // Store scroll offsets for all windows.
      var win, s;
      win = s = p.page.m.activeFrame.contentWindow;
      var scrollers = [
        [win, win.scrollX, win.scrollY],
        [window, window.scrollX, window.scrollY]
      ];
      //while (s != s.parent) { scrollers.push([s, s.scrollX]); s = s.parent; }

      var scroller, x;
      if (Monocle.Browser.env.sheafIsScroller) {
        scroller = p.page.m.sheafDiv;
        x = scroller.scrollLeft;
        target.scrollIntoView();
        offset = scroller.scrollLeft;
      } else {
        scroller = win;
        x = scroller.scrollX;
        target.scrollIntoView();
        offset = scroller.scrollX;
      }

      // Restore scroll offsets for all windows.
      while (s = scrollers.shift()) {
        s[0].scrollTo(s[1], s[2]);
      }

      // ... finally, replace translation.
      translateToOffset(p.page.m.offset);
    } else {
      offset = target.getBoundingClientRect().left;
      offset -= doc.body.getBoundingClientRect().left;
    }

    // We know at least 1px will be visible, and offset should not be 0.
    offset += 1;

    // Percent is the offset divided by the total width of the component.
    var percent = offset / (p.length * p.width);

    // Page number would be offset divided by the width of a single page.
    // var pageNum = Math.ceil(offset / pageDimensions().width);

    return percent;
  }

  API.update = update;
  API.percentageThroughOfNode = percentageThroughOfNode;

  API.locusToOffset = locusToOffset;
  API.translateToLocus = translateToLocus;

  return API;
}

Monocle.Dimensions.Columns.STYLE = {
  // Most of these are already applied to body, but they're repeated here
  // in case columnedElement() is ever anything other than body.
  "columned": {
    "margin": "0",
    "padding": "0",
    "height": "100%",
    "width": "100%",
    "position": "absolute"
  },
  "column-force": {
    "min-width": "200%",
    "overflow": "hidden"
  }
}
<|endoftext|>


<commit_before>import { Text, View, StyleSheet, ScrollView, Platform } from 'react-native'
import React, { Component, PropTypes } from 'react'

import { ListItem, Subheader, Toolbar } from 'react-native-material-ui'

import { PROVIDER_GOOGLE, PROVIDER_DEFAULT } from 'react-native-maps'
import MapView from 'react-native-maps'

import Container from 'ReactNotes/ui/components/Container'

const styles = StyleSheet.create({
    container: {
       ...StyleSheet.absoluteFillObject,
       height: 400,
       width: 400,
       justifyContent: 'flex-end',
       alignItems: 'center',
     },
     map: {
       ...StyleSheet.absoluteFillObject,
     },
})

export default class MapSpec extends Component {

    static propTypes = {
        navigator: PropTypes.object.isRequired,
        route: PropTypes.object.isRequired,
    }

    render() {
        return (
            <Container>
                <Toolbar
                    leftElement="arrow-back"
                    onLeftElementPress={() => this.props.navigator.pop()}
                    centerElement={this.props.route.title}
                />

                <View style ={styles.container}>
                    <MapView
                         provider={PROVIDER_GOOGLE}
                         style={styles.map}
                         region={{
                           latitude: 20.941305,
                           longitude: 105.90358,
                           latitudeDelta: 0.015,
                           longitudeDelta: 0.0121,
                         }}
                    />
                </View>

            </Container>
        )
    }
}

<commit_msg>Added docs for mobile maps<commit_after>import { Text, View, StyleSheet, ScrollView, Platform } from 'react-native'
import React, { Component, PropTypes } from 'react'

import { ListItem, Subheader, Toolbar } from 'react-native-material-ui'

import { PROVIDER_GOOGLE, PROVIDER_DEFAULT } from 'react-native-maps'
import MapView from 'react-native-maps'

import Container from 'ReactNotes/ui/components/Container'

const styles = StyleSheet.create({
    container: {
       ...StyleSheet.absoluteFillObject,
       justifyContent: 'flex-end',
       alignItems: 'center',
     },
     map: {
       ...StyleSheet.absoluteFillObject,
     },
})

export default class MapSpec extends Component {

    static propTypes = {
        navigator: PropTypes.object.isRequired,
        route: PropTypes.object.isRequired,
    }

    render() {
        return (
            <Container>
                <Toolbar
                    leftElement="arrow-back"
                    onLeftElementPress={() => this.props.navigator.pop()}
                    centerElement={this.props.route.title}
                />

                <View style ={styles.container}>
                    <MapView
                         provider={PROVIDER_GOOGLE}
                         style={styles.map}
                         region={{
                           latitude: 20.941305,
                           longitude: 105.90358,
                           latitudeDelta: 0.015,
                           longitudeDelta: 0.0121,
                         }}
                    />
                </View>

            </Container>
        )
    }
}

<|endoftext|>


<commit_before>import * as DS from './delaney';
import * as props from './properties';
import * as derived from './derived';
import { covers } from './covers';

import { rationals as opsQ } from '../arithmetic/types';

const assert = (condition, message) => {
  if (!condition)
    throw new Error(message || 'assertion error');
};

const sum = numbers => numbers.reduce((a, x) => opsQ.plus(a, x), 0);

function* orbits1d(ds) {
  for (let i = 0; i < ds.dim; ++i) {
    for (let j = i + 1; j <= ds.dim; ++j) {
      for (const D of DS.orbitReps2(ds, i, j))
        yield [i, j, D];
    }
  }
};

const loopless = (ds, i, j, D) =>
  ds.orbit2(i, j, D).every(E => ds.s(i, E) != E && ds.s(j, E) != E);

const orbitTypes = ds => {
  const result = [];
  for (const [i, j, D] of orbits1d(ds))
    result.push([ds.v(i, j, D), loopless(ds, i, j, D)]);
  return result;
};

const fullyBranched = ds => orbitTypes(ds).every(([v]) => !!v);

export const curvature = (ds, vDefault=1) => {
  assert(DS.dim(ds) == 2, 'must be two-dimensional');

  let sum = 0;

  for (const [v, loopless] of orbitTypes(ds)) {
    const numerator = loopless ? 2 : 1;
    const denominator = v || vDefault;
    sum = opsQ.plus(sum, opsQ.div(numerator, denominator));
  }

  return opsQ.minus(sum, ds.size);
};

const signOfCurvature = ds => opsQ.sgn(curvature(ds));

export const isProtoEuclidean = ds => signOfCurvature(ds) >= 0;
export const isProtoSpherical = ds => signOfCurvature(ds) > 0;
export const isEuclidean = ds => fullyBranched(ds) && signOfCurvature(ds) == 0;
export const isHyperbolic = ds => fullyBranched(ds) && signOfCurvature(ds) < 0;

export const isSpherical = ds => {
  if (!fullyBranched(ds) || signOfCurvature(ds) <= 0)
    return false;
  else {
    const dso = derived.orientedCover(ds);
    const cones = orbitTypes(dso).map(([v]) => v).filter(v => v > 1);
    const n = cones.length;

    return !(n == 1 || (n == 2 && cones[0] != cones[1]));
  }
};

export const toroidalCover = ds => {
  assert(isEuclidean(ds), 'must be euclidean');

  const dso = derived.orientedCover(ds);
  const degree = Math.max(...orbitTypes(dso).map(([v]) => v));

  for (const cov of covers(dso, degree)) {
    if (orbitTypes(cov).every(([v]) => v == 1))
      return cov;
  };
};

export const orbifoldSymbol = ds => {
  //TODO correctly handle multiple boundary components

  const types = orbitTypes(ds);
  const cones = types.filter(([v, c]) => v > 1 && c).map(([v]) => v);
  const corners = types.filter(([v, c]) => v > 1 && !c).map(([v]) => v);

  const cost = opsQ.minus(2, sum([
    opsQ.div(curvature(ds), 2),
    sum(cones.map(v => opsQ.div(v - 1, v))),
    sum(corners.map(v => opsQ.div(v - 1, 2*v))),
    (props.isLoopless(ds) ? 0 : 1)
  ]));

  assert(opsQ.typeOf(cost) == 'Integer',
          'residual cost should be an integer, got ${cost}');

  const repeat = (c, n) => new Array(n).fill(c);

  const sym = [].concat(
    cones.sort().reverse(),
    (props.isLoopless(ds) ? [] : ['*']),
    corners.sort().reverse(),
    (props.isWeaklyOriented(ds) ? repeat('o', cost/2) : repeat('x', cost))
  ).join('');

  if (sym == 'x' || sym == '*' || sym == '')
    return '1'+sym;
  else
    return sym;
};

const eulerCharacteristic = ds => {
  const nrLoops = i => ds.elements().filter(D => ds.s(i, D) == D).length;
  const nrOrbits = (i, j) => DS.orbitReps2(ds, i, j).length;

  const nf = ds.size;
  const ne = (3 * nf + nrLoops(0) + nrLoops(1) + nrLoops(2)) / 2;
  const nv = nrOrbits(0, 1) + nrOrbits(0, 2) + nrOrbits(1, 2);

  return nf - ne + nv;
};

const cutsOffDisk = (ds, cut, allow2Cone) => {
  const checkCones = cones => (
    cones.length == 0 || (allow2Cone && cones.length == 1 && cones[0] == 2)
  );

  const pairs = [];
  for (const D of cut) {
    const E = ds.s(1, D);
    pairs.push([D, D]);
    pairs.push([E, E]);
  }
  const tmp = DS.withPairings(ds, 1, pairs);
  const patch = derived.subsymbol(tmp, [0, 1, 2], cut[0]);

  if (patch.size == cut.length)
    return false;

  if (eulerCharacteristic(ds) > 0) {
    if (patch.size == ds.size) {
      const vs = [ds.v(0, 1, cut[0]), ds.v(1, 2, cut[0])];
      if (cut.length > 2)
        vs.push(ds.v(1, 2, cut[1]));

      if (checkCones(vs.filter(v => v > 1)))
        return false;
    }

    if (patch.size == ds.size - cut.length) {
      if (
        cut.every(D => ds.v(1, 2, D) == 1)
          && cut.every(D => ds.v(0, 1, D) == 1)
      ) {
        const rest = derived.subsymbol(tmp, [0, 1, 2], ds.s(1, cut[0]));
        const types = orbitTypes(rest);

        if (checkCones(types.filter(([v, c]) => v > 1 && c).map(([v]) => v)))
          return false;
      }
    }
  }

  if (!props.isWeaklyOriented(patch))
    return false;

  if (eulerCharacteristic(patch) != 1)
    return false;

  const types = orbitTypes(patch);

  return checkCones(types.filter(([v, c]) => v > 1 && c).map(([v]) => v));
};

export const isPseudoConvex = ds => {
  assert(DS.dim(ds) == 2, 'must be two-dimensional');
  assert(props.isConnected(ds), 'must be connected');

  const log = () => {};
  //const log = console.log;

  ds = derived.canonical(derived.orientedCover(ds));
  log(`isPseudoConvex(${ds})`);
  const ori = props.partialOrientation(ds);

  for (const A1 of ds.elements().filter(D => ori[D] > 0)) {
    log(`  A1 = ${A1}`);
    const onTrail1 = Array(ds.size + 1).fill(0);
    onTrail1[A1] = 1;
    let A2 = ds.s(0, A1);

    while (!onTrail1[A2]) {
      log(`    A2 = ${A2}`);
      const onTrail2 = onTrail1.slice();
      onTrail2[A2] = 1;
      let B2 = ds.s(2, A2);

      while (B2 == A1 || !onTrail2[B2]) {
        log(`      B2 = ${B2}`);
        if (B2 == A1) {
          if (cutsOffDisk(ds, [A1, A2], true))
            return false;
          else
            break;
        }

        const onTrail3 = onTrail2.slice();
        onTrail3[B2] = 1;
        let B1 = ds.s(0, B2);

        while (!onTrail3[B1]) {
          log(`        B1 = ${B1}`);

          const onTrail4 = onTrail3.slice();
          onTrail4[B1] = 1;
          let T = ds.s(2, B1);

          while (T != A1 && ds.s(1, T) != B1 && !onTrail4[T]) {
            onTrail4[T] = onTrail4[ds.s(1, T)] = 1;
            T = ds.s(2, ds.s(1, T));
          }
          log(`          T = ${T}`);

          if (T == A1 && cutsOffDisk(ds, [A1, A2, B2, B1], false))
            return false;

          onTrail3[B1] = onTrail3[ds.s(1, B1)] = 1;
          B1 = ds.s(0, ds.s(1, B1));
        }

        onTrail2[B2] = onTrail2[ds.s(1, B2)] = 1;
        B2 = ds.s(2, ds.s(1, B2));
      }

      onTrail1[A2] = onTrail1[ds.s(1, A2)] = 1;
      A2 = ds.s(0, ds.s(1, A2));
    }
  }

  return true;
};

if (require.main == module) {
  const test = ds => {
    const is = fn => fn(ds) ? 'is' : 'is not';

    console.log(`ds = ${ds}`);
    console.log(`  curvature is ${curvature(ds)}`);
    console.log(`  symbol ${is(isProtoEuclidean)} proto-euclidean`);
    console.log(`  symbol ${is(isProtoSpherical)} proto-spherical`);
    console.log(`  symbol ${is(isEuclidean)} euclidean`);
    console.log(`  symbol ${is(isHyperbolic)} hyperbolic`);
    console.log(`  symbol ${is(isSpherical)} spherical`);
    console.log(`  symbol ${is(isPseudoConvex)} pseudo-convex`);
    console.log(`  orbifold symbol = ${orbifoldSymbol(ds)}`);

    if (isEuclidean(ds)) {
      const dst = toroidalCover(ds);
      console.log(`  toroidal cover = ${dst}`);

      const curv = curvature(dst);
      const orbs = orbifoldSymbol(dst);

      if (opsQ.eq(curv, 0) && orbs == 'o')
        console.log('    (curvature and orbifold symbol okay)');
      else
        console.error(`    !!!! curvature ${curv}, orbifold symbol ${orbs}`);
    }
    console.log();
  };

  test(DS.parse('<1.1:3:1 2 3,1 3,2 3:4 0,0>'));
  test(DS.parse('<1.1:3:1 2 3,1 3,2 3:4 8,0>'));
  test(DS.parse('<1.1:3:1 2 3,1 3,2 3:4 8,3>'));
  test(DS.parse('<1.1:1:1,1,1:5,3>'));
  test(DS.parse('<1.1:1:1,1,1:6,3>'));
  test(DS.parse('<1.1:1:1,1,1:7,3>'));
  test(DS.parse('<1.1:2:2,1 2,1 2:2,4 4>'));
  test(DS.parse('<1.1:2:2,1 2,1 2:2,4 5>'));
  test(DS.parse('<1.1:8:2 4 6 8,8 3 5 7,6 5 8 7:4,4>'));
  test(DS.parse('<1.1:8:2 4 6 8,8 3 5 7,5 6 8 7:4,4>'));
  test(DS.parse('<1.1:5:2 4 5,1 2 3 5,3 4 5:8 3,8 3>')); // not pseudo-convex
  test(DS.parse('<1.1:4:2 4,1 3 4,3 4:4,4>'));

  test(DS.parse('<1.1:12:1 3 5 8 10 11 12,2 3 6 7 10 12 11,1 4 5 9 7 11 10 12:3 3 3,6 3 3>'));
}
<commit_msg>delaney2d: clean up global helpers<commit_after>import * as DS from './delaney';
import * as props from './properties';
import * as derived from './derived';
import { covers } from './covers';

import { rationals as opsQ } from '../arithmetic/types';

const assert = (condition, message) => {
  if (!condition)
    throw new Error(message || 'assertion error');
};

const loopless = (ds, i, j, D) =>
  ds.orbit2(i, j, D).every(E => ds.s(i, E) != E && ds.s(j, E) != E);

const orbitTypes = ds => {
  const result = [];

  for (let i = 0; i < ds.dim; ++i) {
    for (let j = i + 1; j <= ds.dim; ++j) {
      for (const D of DS.orbitReps2(ds, i, j))
        result.push([ds.v(i, j, D), loopless(ds, i, j, D)]);
    }
  }

  return result;
};

export const curvature = (ds, vDefault=1) => {
  assert(DS.dim(ds) == 2, 'must be two-dimensional');

  let sum = 0;

  for (const [v, loopless] of orbitTypes(ds)) {
    const numerator = loopless ? 2 : 1;
    const denominator = v || vDefault;
    sum = opsQ.plus(sum, opsQ.div(numerator, denominator));
  }

  return opsQ.minus(sum, ds.size);
};

const fullyBranched = ds => orbitTypes(ds).every(([v]) => !!v);
const signOfCurvature = ds => opsQ.sgn(curvature(ds));

export const isProtoEuclidean = ds => signOfCurvature(ds) >= 0;
export const isProtoSpherical = ds => signOfCurvature(ds) > 0;
export const isEuclidean = ds => fullyBranched(ds) && signOfCurvature(ds) == 0;
export const isHyperbolic = ds => fullyBranched(ds) && signOfCurvature(ds) < 0;

export const isSpherical = ds => {
  if (!fullyBranched(ds) || signOfCurvature(ds) <= 0)
    return false;
  else {
    const dso = derived.orientedCover(ds);
    const cones = orbitTypes(dso).map(([v]) => v).filter(v => v > 1);
    const n = cones.length;

    return !(n == 1 || (n == 2 && cones[0] != cones[1]));
  }
};

export const toroidalCover = ds => {
  assert(isEuclidean(ds), 'must be euclidean');

  const dso = derived.orientedCover(ds);
  const degree = Math.max(...orbitTypes(dso).map(([v]) => v));

  for (const cov of covers(dso, degree)) {
    if (orbitTypes(cov).every(([v]) => v == 1))
      return cov;
  };
};

export const orbifoldSymbol = ds => {
  //TODO correctly handle multiple boundary components

  const sum = numbers => numbers.reduce((a, x) => opsQ.plus(a, x), 0);
  const types = orbitTypes(ds);
  const cones = types.filter(([v, c]) => v > 1 && c).map(([v]) => v);
  const corners = types.filter(([v, c]) => v > 1 && !c).map(([v]) => v);

  const cost = opsQ.minus(2, sum([
    opsQ.div(curvature(ds), 2),
    sum(cones.map(v => opsQ.div(v - 1, v))),
    sum(corners.map(v => opsQ.div(v - 1, 2*v))),
    (props.isLoopless(ds) ? 0 : 1)
  ]));

  assert(opsQ.typeOf(cost) == 'Integer',
          'residual cost should be an integer, got ${cost}');

  const repeat = (c, n) => new Array(n).fill(c);

  const sym = [].concat(
    cones.sort().reverse(),
    (props.isLoopless(ds) ? [] : ['*']),
    corners.sort().reverse(),
    (props.isWeaklyOriented(ds) ? repeat('o', cost/2) : repeat('x', cost))
  ).join('');

  if (sym == 'x' || sym == '*' || sym == '')
    return '1'+sym;
  else
    return sym;
};

const eulerCharacteristic = ds => {
  const nrLoops = i => ds.elements().filter(D => ds.s(i, D) == D).length;
  const nrOrbits = (i, j) => DS.orbitReps2(ds, i, j).length;

  const nf = ds.size;
  const ne = (3 * nf + nrLoops(0) + nrLoops(1) + nrLoops(2)) / 2;
  const nv = nrOrbits(0, 1) + nrOrbits(0, 2) + nrOrbits(1, 2);

  return nf - ne + nv;
};

const cutsOffDisk = (ds, cut, allow2Cone) => {
  const checkCones = cones => (
    cones.length == 0 || (allow2Cone && cones.length == 1 && cones[0] == 2)
  );

  const pairs = [];
  for (const D of cut) {
    const E = ds.s(1, D);
    pairs.push([D, D]);
    pairs.push([E, E]);
  }
  const tmp = DS.withPairings(ds, 1, pairs);
  const patch = derived.subsymbol(tmp, [0, 1, 2], cut[0]);

  if (patch.size == cut.length)
    return false;

  if (eulerCharacteristic(ds) > 0) {
    if (patch.size == ds.size) {
      const vs = [ds.v(0, 1, cut[0]), ds.v(1, 2, cut[0])];
      if (cut.length > 2)
        vs.push(ds.v(1, 2, cut[1]));

      if (checkCones(vs.filter(v => v > 1)))
        return false;
    }

    if (patch.size == ds.size - cut.length) {
      if (
        cut.every(D => ds.v(1, 2, D) == 1)
          && cut.every(D => ds.v(0, 1, D) == 1)
      ) {
        const rest = derived.subsymbol(tmp, [0, 1, 2], ds.s(1, cut[0]));
        const types = orbitTypes(rest);

        if (checkCones(types.filter(([v, c]) => v > 1 && c).map(([v]) => v)))
          return false;
      }
    }
  }

  if (!props.isWeaklyOriented(patch))
    return false;

  if (eulerCharacteristic(patch) != 1)
    return false;

  const types = orbitTypes(patch);

  return checkCones(types.filter(([v, c]) => v > 1 && c).map(([v]) => v));
};

export const isPseudoConvex = ds => {
  assert(DS.dim(ds) == 2, 'must be two-dimensional');
  assert(props.isConnected(ds), 'must be connected');

  const log = () => {};
  //const log = console.log;

  ds = derived.canonical(derived.orientedCover(ds));
  log(`isPseudoConvex(${ds})`);
  const ori = props.partialOrientation(ds);

  for (const A1 of ds.elements().filter(D => ori[D] > 0)) {
    log(`  A1 = ${A1}`);
    const onTrail1 = Array(ds.size + 1).fill(0);
    onTrail1[A1] = 1;
    let A2 = ds.s(0, A1);

    while (!onTrail1[A2]) {
      log(`    A2 = ${A2}`);
      const onTrail2 = onTrail1.slice();
      onTrail2[A2] = 1;
      let B2 = ds.s(2, A2);

      while (B2 == A1 || !onTrail2[B2]) {
        log(`      B2 = ${B2}`);
        if (B2 == A1) {
          if (cutsOffDisk(ds, [A1, A2], true))
            return false;
          else
            break;
        }

        const onTrail3 = onTrail2.slice();
        onTrail3[B2] = 1;
        let B1 = ds.s(0, B2);

        while (!onTrail3[B1]) {
          log(`        B1 = ${B1}`);

          const onTrail4 = onTrail3.slice();
          onTrail4[B1] = 1;
          let T = ds.s(2, B1);

          while (T != A1 && ds.s(1, T) != B1 && !onTrail4[T]) {
            onTrail4[T] = onTrail4[ds.s(1, T)] = 1;
            T = ds.s(2, ds.s(1, T));
          }
          log(`          T = ${T}`);

          if (T == A1 && cutsOffDisk(ds, [A1, A2, B2, B1], false))
            return false;

          onTrail3[B1] = onTrail3[ds.s(1, B1)] = 1;
          B1 = ds.s(0, ds.s(1, B1));
        }

        onTrail2[B2] = onTrail2[ds.s(1, B2)] = 1;
        B2 = ds.s(2, ds.s(1, B2));
      }

      onTrail1[A2] = onTrail1[ds.s(1, A2)] = 1;
      A2 = ds.s(0, ds.s(1, A2));
    }
  }

  return true;
};

if (require.main == module) {
  const test = ds => {
    const is = fn => fn(ds) ? 'is' : 'is not';

    console.log(`ds = ${ds}`);
    console.log(`  curvature is ${curvature(ds)}`);
    console.log(`  symbol ${is(isProtoEuclidean)} proto-euclidean`);
    console.log(`  symbol ${is(isProtoSpherical)} proto-spherical`);
    console.log(`  symbol ${is(isEuclidean)} euclidean`);
    console.log(`  symbol ${is(isHyperbolic)} hyperbolic`);
    console.log(`  symbol ${is(isSpherical)} spherical`);
    console.log(`  symbol ${is(isPseudoConvex)} pseudo-convex`);
    console.log(`  orbifold symbol = ${orbifoldSymbol(ds)}`);

    if (isEuclidean(ds)) {
      const dst = toroidalCover(ds);
      console.log(`  toroidal cover = ${dst}`);

      const curv = curvature(dst);
      const orbs = orbifoldSymbol(dst);

      if (opsQ.eq(curv, 0) && orbs == 'o')
        console.log('    (curvature and orbifold symbol okay)');
      else
        console.error(`    !!!! curvature ${curv}, orbifold symbol ${orbs}`);
    }
    console.log();
  };

  test(DS.parse('<1.1:3:1 2 3,1 3,2 3:4 0,0>'));
  test(DS.parse('<1.1:3:1 2 3,1 3,2 3:4 8,0>'));
  test(DS.parse('<1.1:3:1 2 3,1 3,2 3:4 8,3>'));
  test(DS.parse('<1.1:1:1,1,1:5,3>'));
  test(DS.parse('<1.1:1:1,1,1:6,3>'));
  test(DS.parse('<1.1:1:1,1,1:7,3>'));
  test(DS.parse('<1.1:2:2,1 2,1 2:2,4 4>'));
  test(DS.parse('<1.1:2:2,1 2,1 2:2,4 5>'));
  test(DS.parse('<1.1:8:2 4 6 8,8 3 5 7,6 5 8 7:4,4>'));
  test(DS.parse('<1.1:8:2 4 6 8,8 3 5 7,5 6 8 7:4,4>'));
  test(DS.parse('<1.1:5:2 4 5,1 2 3 5,3 4 5:8 3,8 3>')); // not pseudo-convex
  test(DS.parse('<1.1:4:2 4,1 3 4,3 4:4,4>'));

  test(DS.parse('<1.1:12:1 3 5 8 10 11 12,2 3 6 7 10 12 11,1 4 5 9 7 11 10 12:3 3 3,6 3 3>'));
}
<|endoftext|>