<commit_before>/**
 * Copyright 2019 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed 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.
 */

/**
 * @fileoverview Detects <amp-img> elements to set the `lightbox` attribute
 * automatically.
 *
 * This extension is not meant to be included explicitly, e.g. as a script tag.
 * Instead, the runtime loads it when encountering an <amp-img>.
 */

import {AmpEvents} from '../../../src/amp-events';
import {AutoLightboxEvents} from '../../../src/auto-lightbox';
import {CarouselCriteria} from './carousel-criteria';
import {CommonSignals} from '../../../src/common-signals';
import {Services} from '../../../src/services';
import {
  closestAncestorElementBySelector,
  matches,
  whenUpgradedToCustomElement,
} from '../../../src/dom';
import {dev} from '../../../src/log';
import {getMode} from '../../../src/mode';
import {resolveFalse, resolveTrue} from './utils/promise';
import {toArray} from '../../../src/types';
import {tryParseJson} from '../../../src/json';

const TAG = 'amp-auto-lightbox';

export const REQUIRED_EXTENSION = 'amp-lightbox-gallery';
export const LIGHTBOXABLE_ATTR = 'lightbox';

/** Attribute to mark scanned lightbox candidates as not to revisit. */
export const VISITED_ATTR = 'i-amphtml-auto-lightbox-visited';

/**
 * Types of document by LD+JSON schema `@type` field where auto-lightbox should
 * be enabled.
 * @private @const {!Object<string|undefined, boolean>}
 */
export const ENABLED_LD_JSON_TYPES = {
  'Article': true,
  'NewsArticle': true,
  'BlogPosting': true,
  'LiveBlogPosting': true,
  'DiscussionForumPosting': true,
};

/**
 * Only of document type by Open Graph `<meta property="og:type">` where
 * auto-lightbox should be enabled. Top-level og:type set is tiny, and `article`
 * covers all required types.
 */
export const ENABLED_OG_TYPE_ARTICLE = 'article';

/** Factor of naturalArea vs renderArea to lightbox. */
export const RENDER_AREA_RATIO = 1.2;

/** Factor of renderArea vs viewportArea to lightbox. */
export const VIEWPORT_AREA_RATIO = 0.25;

/** @const {!Array<string>} */
const CANDIDATES = ['amp-img', 'amp-carousel'];

/**
 * Selector for subnodes by attribute for which the auto-lightbox treatment
 * does not apply. These can be set directly on the candidate or on an ancestor.
 */
const DISABLED_BY_ATTR = [
  // Runtime-specific.
  '[placeholder]',

  // Explicitly opted out.
  '[data-amp-auto-lightbox-disable]',

  // Considered "actionable", i.e. that are bound to a default
  // onclick action(e.g. `button`) or where it cannot be determined whether
  // they're actionable or not (e.g. `amp-script`).
  'amp-selector [option]',
].join(',');

/**
 * Selector for subnodes for which the auto-lightbox treatment does not apply.
 */
const DISABLED_ANCESTORS = [
  // Ancestors considered "actionable", i.e. that are bound to a default
  // onclick action(e.g. `button`) or where it cannot be determined whether
  // they're actionable or not (e.g. `amp-script`).
  'a[href]',
  'amp-script',
  'amp-story',
  'button',

  // No nested lightboxes.
  'amp-lightbox',

  // Special treatment.
  'amp-carousel',
].join(',');

const SCRIPT_LD_JSON = 'script[type="application/ld+json"]';
const META_OG_TYPE = 'meta[property="og:type"]';

const NOOP = () => {};

/**
 * For better minification.
 * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
 * @return {!Document|!ShadowRoot}
 */
const getRootNode = ampdoc => ampdoc.getRootNode();

/** @visibleForTesting */
export class Criteria {

  /**
   * @param {!Element} element
   * @return {!Promise<boolean>}
   */
  static meetsAll(element) {
    if (!Criteria.meetsSimpleCriteria(element) ||
        !Criteria.meetsTreeShapeCriteria(element)) {
      return resolveFalse();
    }
    return Criteria.meetsComplexCriteria(element);
  }

  /**
   * Criteria that is "simple", ie runs quickly and discards elements in order
   * to shortcircuit.
   * @param {!Element} element
   * @return {boolean}
   */
  static meetsSimpleCriteria(element) {
    if (element.tagName.toUpperCase() == 'AMP-IMG') {
      return ImageCriteria.meetsSizingCriteria(element);
    }
    return true;
  }

  /**
   * Criteria that is "complex", ie takes longer to run and discards elements
   * after they're likely to be good candidates per previous conditions.
   * @param {!Element} element
   * @return {!Promise<boolean>}
   */
  static meetsComplexCriteria(element) {
    if (element.tagName.toUpperCase() == 'AMP-CAROUSEL') {
      return CarouselCriteria.meetsAll(element);
    }
    return resolveTrue();
  }

  /**
   * @param {!Element} element
   * @return {boolean}
   */
  static meetsTreeShapeCriteria(element) {
    const disabledSelector = `${DISABLED_ANCESTORS},${DISABLED_BY_ATTR}`;
    const disabledAncestor =
        closestAncestorElementBySelector(element, disabledSelector);
    // since we lookup both amp-img and amp-carousel at the same level, and
    // we'd like to give amp-carousel special treatment by containing amp-img's,
    // we need to filter out images inside carousels, but not carousels
    // themselves.
    if (disabledAncestor &&
        (disabledAncestor != element ||
        matches(disabledAncestor, DISABLED_BY_ATTR))) {
      return false;
    }
    const actions = Services.actionServiceForDoc(element);
    return !actions.hasResolvableAction(element, 'tap');
  }
}

class ImageCriteria {
  /**
   * @param {!Element} element
   * @return {boolean}
   */
  static meetsSizingCriteria(element) {
    const {naturalWidth, naturalHeight} =
        dev().assertElement(element.querySelector('img'));

    const {width: renderWidth, height: renderHeight} = element.getLayoutBox();

    const viewport = Services.viewportForDoc(element);
    const {width: vw, height: vh} = viewport.getSize();

    return meetsSizingCriteria(
        renderWidth,
        renderHeight,
        naturalWidth,
        naturalHeight,
        vw,
        vh);
  }
}

/**
 * @param {number} renderWidth
 * @param {number} renderHeight
 * @param {number} naturalWidth
 * @param {number} naturalHeight
 * @param {number} vw
 * @param {number} vh
 * @return {boolean}
 * @visibleForTesting
 */
export function meetsSizingCriteria(
  renderWidth,
  renderHeight,
  naturalWidth,
  naturalHeight,
  vw,
  vh) {

  const viewportArea = vw * vh;
  const naturalArea = naturalWidth * naturalHeight;
  const renderArea = renderWidth * renderHeight;

  const isShrunk =
    (naturalArea / renderArea) >= RENDER_AREA_RATIO;

  const isCoveringSignificantArea =
    (renderArea / viewportArea) >= VIEWPORT_AREA_RATIO;

  const isLargerThanViewport = naturalWidth > vw || naturalHeight > vh;

  return isShrunk ||
    isLargerThanViewport ||
    isCoveringSignificantArea;
}

/**
 * Marks a lightbox candidate as visited as not to rescan on DOM update.
 * @param {!Element} candidate
 * @return {!Promise<!Element>}
 */
function markAsVisited(candidate) {
  return Mutation.mutate(candidate, () => {
    candidate.setAttribute(VISITED_ATTR, '');
  });
}

/**
 * @param {string} tagName
 * @return {string}
 */
function candidateSelector(tagName) {
  return `${tagName}:not([${LIGHTBOXABLE_ATTR}]):not([${VISITED_ATTR}])`;
}

/**
 * @param {!Element} element
 * @return {!Promise<!Element>}
 */
function whenLoaded(element) {
  return element.signals().whenSignal(CommonSignals.LOAD_END);
}

/** @visibleForTesting */
export class Scanner {

  /**
   * Gets all unvisited lightbox candidates.
   * @param {!Document|!Element} root
   * @return {!Array<!Element>}
   */
  static getCandidates(root) {
    const selector = CANDIDATES.map(candidateSelector).join(',');
    const candidates = toArray(root.querySelectorAll(selector));
    candidates.forEach(markAsVisited);
    return candidates;
  }
}

/**
 * Parses document metadata annotations as defined by either LD+JSON schema or
 * Open Graph <meta> tags.
 * @visibleForTesting
 */
export class DocMetaAnnotations {

  /**
   * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
   * @return {string|undefined}
   */
  static getOgType(ampdoc) {
    const tag = getRootNode(ampdoc).querySelector(META_OG_TYPE);
    if (tag) {
      return tag.getAttribute('content');
    }
  }

  /**
   * Determines wheter the document type as defined by Open Graph meta tag
   * e.g. `<meta property="og:type">` is valid.
   * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
   * @return {boolean}
   */
  static hasValidOgType(ampdoc) {
    return DocMetaAnnotations.getOgType(ampdoc) == ENABLED_OG_TYPE_ARTICLE;
  }

  /**
   * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
   * @return {!Array<string>}
   */
  static getAllLdJsonTypes(ampdoc) {
    return toArray(getRootNode(ampdoc).querySelectorAll(SCRIPT_LD_JSON))
        .map(({textContent}) => ((tryParseJson(textContent) || {})['@type']))
        .filter(typeOrUndefined => typeOrUndefined);
  }

  /**
   * Determines wheter one of the document types (field `@type`) defined in
   * LD+JSON schema is in ENABLED_LD_JSON_TYPES.
   * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
   * @return {boolean}
   */
  static hasValidLdJsonType(ampdoc) {
    return DocMetaAnnotations.getAllLdJsonTypes(ampdoc)
        .some(type => ENABLED_LD_JSON_TYPES[type]);
  }
}

/**
 * Wrapper for an element-implementation-mutate sequence for readability and
 * mocking in tests.
 * @visibleForTesting
 */
export class Mutation {
  /**
   * @param {!Element} ampEl
   * @param {function()} mutator
   * @return {!Promise}
   */
  static mutate(ampEl, mutator) {
    return whenUpgradedToCustomElement(ampEl)
        .then(ampEl => ampEl.getResources().mutateElement(ampEl, mutator));
  }
}

/**
 * Determines whether a document uses `amp-lightbox-gallery` explicitly by
 * including the extension and explicitly lightboxing at least one element.
 * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
 * @return {boolean}
 */
function usesLightboxExplicitly(ampdoc) {
  // TODO(alanorozco): Backport into Extensions service.
  const requiredExtensionSelector =
      `script[custom-element="${REQUIRED_EXTENSION}"]`;

  const lightboxedElementsSelector =
      `[${LIGHTBOXABLE_ATTR}]:not([${VISITED_ATTR}])`;

  const exists = selector => !!getRootNode(ampdoc).querySelector(selector);

  return exists(requiredExtensionSelector) &&
      exists(lightboxedElementsSelector);
}

/**
 * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
 * @return {boolean}
 */
function isProxyOriginOrLocalDev(ampdoc) {
  // Allow `localDev` in lieu of proxy origin for manual testing, except in
  // tests where we need to actually perform the check.
  const {win} = ampdoc;
  if (getMode(win).localDev && !getMode(win).test) {
    return true;
  }

  // An attached node is required for proxy origin check. If no elements are
  // present, short-circuit.
  const {firstElementChild} = ampdoc.getBody();
  if (!firstElementChild) {
    return false;
  }

  // TODO(alanorozco): Additionally check for transformed, webpackaged flag.
  // See git.io/fhQ0a (#20359) for details.
  return Services.urlForDoc(firstElementChild).isProxyOrigin(win.location);
}

/**
 * Determines whether auto-lightbox is enabled for a document.
 * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
 * @return {boolean}
 * @visibleForTesting
 */
export function isEnabledForDoc(ampdoc) {
  if (usesLightboxExplicitly(ampdoc)) {
    return false;
  }
  if (!DocMetaAnnotations.hasValidOgType(ampdoc) &&
      !DocMetaAnnotations.hasValidLdJsonType(ampdoc)) {
    return false;
  }
  return isProxyOriginOrLocalDev(ampdoc);
}

/** @private {number} */
let uid = 0;

/**
 * Generates a unique id for lightbox grouping.
 * @return {string}
 */
function generateLightboxUid() {
  return `i-amphtml-auto-lightbox-${uid++}`;
}

/**
 * Lightboxes an element.
 * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
 * @param {!Element} element
 * @return {!Promise<!Element>}
 * @visibleForTesting
 */
export function apply(ampdoc, element) {
  return Mutation.mutate(element, () => {
    element.setAttribute(LIGHTBOXABLE_ATTR, generateLightboxUid());
  }).then(() => {
    Services.extensionsFor(ampdoc.win)
        .installExtensionForDoc(ampdoc, REQUIRED_EXTENSION);

    element.dispatchCustomEvent(AutoLightboxEvents.NEWLY_SET);

    return element;
  });
}

/**
 * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
 * @param {!Array<!Element>} candidates
 * @return {!Array<!Promise<!Element|undefined>>}
 */
export function runCandidates(ampdoc, candidates) {
  return candidates.map(candidate =>
    whenLoaded(candidate).then(() => {
      return Criteria.meetsAll(candidate).then(meetsAll => {
        if (!meetsAll) {
          return;
        }
        dev().info(TAG, 'apply', candidate);
        return apply(ampdoc, candidate);
      });
    }, NOOP));
}

/**
 * Scans a document on initialization to lightbox elements that meet criteria.
 * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
 * @param {!Element=} opt_root
 * @return {!Array<!Promise>|undefined}
 */
export function scan(ampdoc, opt_root) {
  if (!isEnabledForDoc(ampdoc)) {
    dev().info(TAG, 'disabled');
    return;
  }
  const root = opt_root || ampdoc.win.document;
  return runCandidates(ampdoc, Scanner.getCandidates(root));
}

AMP.extension(TAG, '0.1', ({ampdoc}) => {
  ampdoc.whenReady().then(() => {
    getRootNode(ampdoc).addEventListener(AmpEvents.DOM_UPDATE, ({target}) => {
      scan(ampdoc, dev().assertElement(target));
    });
    scan(ampdoc);
  });
});
<commit_msg>🐛 Fix race condition for `signals` (#21237)<commit_after>/**
 * Copyright 2019 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed 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.
 */

/**
 * @fileoverview Detects <amp-img> elements to set the `lightbox` attribute
 * automatically.
 *
 * This extension is not meant to be included explicitly, e.g. as a script tag.
 * Instead, the runtime loads it when encountering an <amp-img>.
 */

import {AmpEvents} from '../../../src/amp-events';
import {AutoLightboxEvents} from '../../../src/auto-lightbox';
import {CarouselCriteria} from './carousel-criteria';
import {CommonSignals} from '../../../src/common-signals';
import {Services} from '../../../src/services';
import {
  closestAncestorElementBySelector,
  matches,
  whenUpgradedToCustomElement,
} from '../../../src/dom';
import {dev} from '../../../src/log';
import {getMode} from '../../../src/mode';
import {resolveFalse, resolveTrue} from './utils/promise';
import {toArray} from '../../../src/types';
import {tryParseJson} from '../../../src/json';

const TAG = 'amp-auto-lightbox';

export const REQUIRED_EXTENSION = 'amp-lightbox-gallery';
export const LIGHTBOXABLE_ATTR = 'lightbox';

/** Attribute to mark scanned lightbox candidates as not to revisit. */
export const VISITED_ATTR = 'i-amphtml-auto-lightbox-visited';

/**
 * Types of document by LD+JSON schema `@type` field where auto-lightbox should
 * be enabled.
 * @private @const {!Object<string|undefined, boolean>}
 */
export const ENABLED_LD_JSON_TYPES = {
  'Article': true,
  'NewsArticle': true,
  'BlogPosting': true,
  'LiveBlogPosting': true,
  'DiscussionForumPosting': true,
};

/**
 * Only of document type by Open Graph `<meta property="og:type">` where
 * auto-lightbox should be enabled. Top-level og:type set is tiny, and `article`
 * covers all required types.
 */
export const ENABLED_OG_TYPE_ARTICLE = 'article';

/** Factor of naturalArea vs renderArea to lightbox. */
export const RENDER_AREA_RATIO = 1.2;

/** Factor of renderArea vs viewportArea to lightbox. */
export const VIEWPORT_AREA_RATIO = 0.25;

/** @const {!Array<string>} */
const CANDIDATES = ['amp-img', 'amp-carousel'];

/**
 * Selector for subnodes by attribute for which the auto-lightbox treatment
 * does not apply. These can be set directly on the candidate or on an ancestor.
 */
const DISABLED_BY_ATTR = [
  // Runtime-specific.
  '[placeholder]',

  // Explicitly opted out.
  '[data-amp-auto-lightbox-disable]',

  // Considered "actionable", i.e. that are bound to a default
  // onclick action(e.g. `button`) or where it cannot be determined whether
  // they're actionable or not (e.g. `amp-script`).
  'amp-selector [option]',
].join(',');

/**
 * Selector for subnodes for which the auto-lightbox treatment does not apply.
 */
const DISABLED_ANCESTORS = [
  // Ancestors considered "actionable", i.e. that are bound to a default
  // onclick action(e.g. `button`) or where it cannot be determined whether
  // they're actionable or not (e.g. `amp-script`).
  'a[href]',
  'amp-script',
  'amp-story',
  'button',

  // No nested lightboxes.
  'amp-lightbox',

  // Special treatment.
  'amp-carousel',
].join(',');

const SCRIPT_LD_JSON = 'script[type="application/ld+json"]';
const META_OG_TYPE = 'meta[property="og:type"]';

const NOOP = () => {};

/**
 * For better minification.
 * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
 * @return {!Document|!ShadowRoot}
 */
const getRootNode = ampdoc => ampdoc.getRootNode();

/** @visibleForTesting */
export class Criteria {

  /**
   * @param {!Element} element
   * @return {!Promise<boolean>}
   */
  static meetsAll(element) {
    if (!Criteria.meetsSimpleCriteria(element) ||
        !Criteria.meetsTreeShapeCriteria(element)) {
      return resolveFalse();
    }
    return Criteria.meetsComplexCriteria(element);
  }

  /**
   * Criteria that is "simple", ie runs quickly and discards elements in order
   * to shortcircuit.
   * @param {!Element} element
   * @return {boolean}
   */
  static meetsSimpleCriteria(element) {
    if (element.tagName.toUpperCase() == 'AMP-IMG') {
      return ImageCriteria.meetsSizingCriteria(element);
    }
    return true;
  }

  /**
   * Criteria that is "complex", ie takes longer to run and discards elements
   * after they're likely to be good candidates per previous conditions.
   * @param {!Element} element
   * @return {!Promise<boolean>}
   */
  static meetsComplexCriteria(element) {
    if (element.tagName.toUpperCase() == 'AMP-CAROUSEL') {
      return CarouselCriteria.meetsAll(element);
    }
    return resolveTrue();
  }

  /**
   * @param {!Element} element
   * @return {boolean}
   */
  static meetsTreeShapeCriteria(element) {
    const disabledSelector = `${DISABLED_ANCESTORS},${DISABLED_BY_ATTR}`;
    const disabledAncestor =
        closestAncestorElementBySelector(element, disabledSelector);
    // since we lookup both amp-img and amp-carousel at the same level, and
    // we'd like to give amp-carousel special treatment by containing amp-img's,
    // we need to filter out images inside carousels, but not carousels
    // themselves.
    if (disabledAncestor &&
        (disabledAncestor != element ||
        matches(disabledAncestor, DISABLED_BY_ATTR))) {
      return false;
    }
    const actions = Services.actionServiceForDoc(element);
    return !actions.hasResolvableAction(element, 'tap');
  }
}

class ImageCriteria {
  /**
   * @param {!Element} element
   * @return {boolean}
   */
  static meetsSizingCriteria(element) {
    const {naturalWidth, naturalHeight} =
        dev().assertElement(element.querySelector('img'));

    const {width: renderWidth, height: renderHeight} = element.getLayoutBox();

    const viewport = Services.viewportForDoc(element);
    const {width: vw, height: vh} = viewport.getSize();

    return meetsSizingCriteria(
        renderWidth,
        renderHeight,
        naturalWidth,
        naturalHeight,
        vw,
        vh);
  }
}

/**
 * @param {number} renderWidth
 * @param {number} renderHeight
 * @param {number} naturalWidth
 * @param {number} naturalHeight
 * @param {number} vw
 * @param {number} vh
 * @return {boolean}
 * @visibleForTesting
 */
export function meetsSizingCriteria(
  renderWidth,
  renderHeight,
  naturalWidth,
  naturalHeight,
  vw,
  vh) {

  const viewportArea = vw * vh;
  const naturalArea = naturalWidth * naturalHeight;
  const renderArea = renderWidth * renderHeight;

  const isShrunk =
    (naturalArea / renderArea) >= RENDER_AREA_RATIO;

  const isCoveringSignificantArea =
    (renderArea / viewportArea) >= VIEWPORT_AREA_RATIO;

  const isLargerThanViewport = naturalWidth > vw || naturalHeight > vh;

  return isShrunk ||
    isLargerThanViewport ||
    isCoveringSignificantArea;
}

/**
 * Marks a lightbox candidate as visited as not to rescan on DOM update.
 * @param {!Element} candidate
 * @return {!Promise}
 */
function markAsVisited(candidate) {
  return Mutation.mutate(candidate, () => {
    candidate.setAttribute(VISITED_ATTR, '');
  });
}

/**
 * @param {string} tagName
 * @return {string}
 */
function candidateSelector(tagName) {
  return `${tagName}:not([${LIGHTBOXABLE_ATTR}]):not([${VISITED_ATTR}])`;
}

/**
 * @param {!Element} element
 * @return {!Promise}
 */
function whenLoaded(element) {
  return whenUpgradedToCustomElement(element).then(element =>
    element.signals().whenSignal(CommonSignals.LOAD_END));
}

/** @visibleForTesting */
export class Scanner {

  /**
   * Gets all unvisited lightbox candidates.
   * @param {!Document|!Element} root
   * @return {!Array<!Element>}
   */
  static getCandidates(root) {
    const selector = CANDIDATES.map(candidateSelector).join(',');
    const candidates = toArray(root.querySelectorAll(selector));
    candidates.forEach(markAsVisited);
    return candidates;
  }
}

/**
 * Parses document metadata annotations as defined by either LD+JSON schema or
 * Open Graph <meta> tags.
 * @visibleForTesting
 */
export class DocMetaAnnotations {

  /**
   * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
   * @return {string|undefined}
   */
  static getOgType(ampdoc) {
    const tag = getRootNode(ampdoc).querySelector(META_OG_TYPE);
    if (tag) {
      return tag.getAttribute('content');
    }
  }

  /**
   * Determines wheter the document type as defined by Open Graph meta tag
   * e.g. `<meta property="og:type">` is valid.
   * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
   * @return {boolean}
   */
  static hasValidOgType(ampdoc) {
    return DocMetaAnnotations.getOgType(ampdoc) == ENABLED_OG_TYPE_ARTICLE;
  }

  /**
   * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
   * @return {!Array<string>}
   */
  static getAllLdJsonTypes(ampdoc) {
    return toArray(getRootNode(ampdoc).querySelectorAll(SCRIPT_LD_JSON))
        .map(({textContent}) => ((tryParseJson(textContent) || {})['@type']))
        .filter(typeOrUndefined => typeOrUndefined);
  }

  /**
   * Determines wheter one of the document types (field `@type`) defined in
   * LD+JSON schema is in ENABLED_LD_JSON_TYPES.
   * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
   * @return {boolean}
   */
  static hasValidLdJsonType(ampdoc) {
    return DocMetaAnnotations.getAllLdJsonTypes(ampdoc)
        .some(type => ENABLED_LD_JSON_TYPES[type]);
  }
}

/**
 * Wrapper for an element-implementation-mutate sequence for readability and
 * mocking in tests.
 * @visibleForTesting
 */
export class Mutation {
  /**
   * @param {!Element} ampEl
   * @param {function()} mutator
   * @return {!Promise}
   */
  static mutate(ampEl, mutator) {
    return whenUpgradedToCustomElement(ampEl)
        .then(ampEl => ampEl.getResources().mutateElement(ampEl, mutator));
  }
}

/**
 * Determines whether a document uses `amp-lightbox-gallery` explicitly by
 * including the extension and explicitly lightboxing at least one element.
 * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
 * @return {boolean}
 */
function usesLightboxExplicitly(ampdoc) {
  // TODO(alanorozco): Backport into Extensions service.
  const requiredExtensionSelector =
      `script[custom-element="${REQUIRED_EXTENSION}"]`;

  const lightboxedElementsSelector =
      `[${LIGHTBOXABLE_ATTR}]:not([${VISITED_ATTR}])`;

  const exists = selector => !!getRootNode(ampdoc).querySelector(selector);

  return exists(requiredExtensionSelector) &&
      exists(lightboxedElementsSelector);
}

/**
 * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
 * @return {boolean}
 */
function isProxyOriginOrLocalDev(ampdoc) {
  // Allow `localDev` in lieu of proxy origin for manual testing, except in
  // tests where we need to actually perform the check.
  const {win} = ampdoc;
  if (getMode(win).localDev && !getMode(win).test) {
    return true;
  }

  // An attached node is required for proxy origin check. If no elements are
  // present, short-circuit.
  const {firstElementChild} = ampdoc.getBody();
  if (!firstElementChild) {
    return false;
  }

  // TODO(alanorozco): Additionally check for transformed, webpackaged flag.
  // See git.io/fhQ0a (#20359) for details.
  return Services.urlForDoc(firstElementChild).isProxyOrigin(win.location);
}

/**
 * Determines whether auto-lightbox is enabled for a document.
 * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
 * @return {boolean}
 * @visibleForTesting
 */
export function isEnabledForDoc(ampdoc) {
  if (usesLightboxExplicitly(ampdoc)) {
    return false;
  }
  if (!DocMetaAnnotations.hasValidOgType(ampdoc) &&
      !DocMetaAnnotations.hasValidLdJsonType(ampdoc)) {
    return false;
  }
  return isProxyOriginOrLocalDev(ampdoc);
}

/** @private {number} */
let uid = 0;

/**
 * Generates a unique id for lightbox grouping.
 * @return {string}
 */
function generateLightboxUid() {
  return `i-amphtml-auto-lightbox-${uid++}`;
}

/**
 * Lightboxes an element.
 * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
 * @param {!Element} element
 * @return {!Promise<!Element>}
 * @visibleForTesting
 */
export function apply(ampdoc, element) {
  return Mutation.mutate(element, () => {
    element.setAttribute(LIGHTBOXABLE_ATTR, generateLightboxUid());
  }).then(() => {
    Services.extensionsFor(ampdoc.win)
        .installExtensionForDoc(ampdoc, REQUIRED_EXTENSION);

    element.dispatchCustomEvent(AutoLightboxEvents.NEWLY_SET);

    return element;
  });
}

/**
 * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
 * @param {!Array<!Element>} candidates
 * @return {!Array<!Promise<!Element|undefined>>}
 */
export function runCandidates(ampdoc, candidates) {
  return candidates.map(candidate =>
    whenLoaded(candidate).then(() => {
      return Criteria.meetsAll(candidate).then(meetsAll => {
        if (!meetsAll) {
          return;
        }
        dev().info(TAG, 'apply', candidate);
        return apply(ampdoc, candidate);
      });
    }, NOOP));
}

/**
 * Scans a document on initialization to lightbox elements that meet criteria.
 * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
 * @param {!Element=} opt_root
 * @return {!Array<!Promise>|undefined}
 */
export function scan(ampdoc, opt_root) {
  if (!isEnabledForDoc(ampdoc)) {
    dev().info(TAG, 'disabled');
    return;
  }
  const root = opt_root || ampdoc.win.document;
  return runCandidates(ampdoc, Scanner.getCandidates(root));
}

AMP.extension(TAG, '0.1', ({ampdoc}) => {
  ampdoc.whenReady().then(() => {
    getRootNode(ampdoc).addEventListener(AmpEvents.DOM_UPDATE, ({target}) => {
      scan(ampdoc, dev().assertElement(target));
    });
    scan(ampdoc);
  });
});
<|endoftext|>


<commit_before>/***
Adds the app switcher to a TiddlySpace app.

Makes use of tw.Stylesheet
Triple licensed under the BSD, MIT and GPL licenses:
  http://www.opensource.org/licenses/bsd-license.php
  http://www.opensource.org/licenses/mit-license.php
  http://www.gnu.org/licenses/gpl.html
***/
(function() {

// Add or replace a style sheet
// css argument is a string of CSS rule sets
// options.id is an optional name identifying the style sheet
// options.doc is an optional document reference
// N.B.: Uses DOM methods instead of jQuery to ensure cross-browser comaptibility.
var twStylesheet = function(css, options) {
	options = options || {};
	var id = options.id || "backstageStyleSheet";
	var doc = options.doc || document;
	var el = doc.getElementById(id);
	if(doc.createStyleSheet) { // IE-specific handling
		if(el) {
			el.parentNode.removeChild(el);
		}
		doc.getElementsByTagName("head")[0].insertAdjacentHTML("beforeEnd",
			' '); // fails without
	} else { // modern browsers
		if(el) {
			el.replaceChild(doc.createTextNode(css), el.firstChild);
		} else {
			el = doc.createElement("style");
			el.type = "text/css";
			el.id = id;
			el.appendChild(doc.createTextNode(css));
			doc.getElementsByTagName("head")[0].appendChild(el);
		}
	}
};

var stylesheet = ["iframe {",
"	height: 320px;",
"	z-index: 1000;",
"	position: relative;",
"}",
"",
"#app-picker {",
"	cursor: pointer;",
"	position: absolute;",
"	right: 24px;",
"	top: 0px;",
"	width: 24px;",
"	height: 24px;",
"	background-size: 24px 24px;",
"	background-image: url(/bags/common/tiddlers/backstageIcon.png);",
"	text-indent: -999px;",
"	overflow: hidden;",
"	z-index: 2000;",
"	border: none;",
"	opacity: 0.5;",
"}",
"",
"#app-picker:hover {",
"	background-color: none !important;",
"	opacity: 1;",
"}",
"",
".bs-popup {",
"	width: 100%;",
"	height: 100%;",
"	position: absolute;",
"	z-index: 1000;",
"	right: 10px;",
"	top: 52px;",
"}",
"",
".bubble .description {",
"	margin-left: 70px;",
"	margin-top: 2px;",
"}",
"",
".bubble {",
"	float: right;",
"	font-size: 0.9em;",
"	font-family: Georgia;",
"	position: relative;",
"	width: 300px;",
"	margin: 0px auto 0px auto;",
"	margin: top right bottom left;",
"	padding: 20px 20px 20px 10px;",
"	border: solid 2px rgb(255, 255, 255);",
"	border-radius: 8px 8px;",
"	-webkit-box-shadow: #F4C4E0 0px 0px 8px;",
"	-moz-box-shadow: #F4C4E0 0px 0px 8px;",
"	-o-box-shadow: #F4C4E0 0px 0px 8px;",
"	-ms-box-shadow: #F4C4E0 0px 0px 8px;",
"	box-shadow: #F4C4E0 0px 0px 8px;",
"	background-color: rgb(220, 231, 241);",
"}",
"",
".bubble .tail:before {",
"	content: \"\";",
"	position:absolute;",
"	border-style:solid;",
"	display:block; ",
"	width:0;",
"	top:-24px;",
"	right:5px;",
"	bottom:auto;",
"	left:auto;",
"	opacity: 0.5;",
"	border-width:0 20px 20px;",
"	border-color: rgba(244, 196, 224, 0.6) transparent;",
"}",
"",
".bubble .tail:after {",
"	content:\"\";",
"	position:absolute;",
"	border-style:solid;",
"	display:block; ",
"	width:0;",
"	top:-15px;",
"	right:10px;",
"	bottom:auto;",
"	left:auto;",
"	border-width:0 15px 15px;",
"	border-color: #DCE7F1 transparent;",
"}"
].join("\n");
function addEventListener(node, event, handler) {
	if (node.addEventListener){
		node.addEventListener(event, handler, false);
	} else if (node.attachEvent){
		event = event == "click" ? "onclick" : event;
		event = event == "load" ? "onload" : event;
		node.attachEvent(event, handler);
	}
};

var loadEvent = function() {
	var link = document.createElement("a");
	link.setAttribute("id", "app-picker");
	link.setAttribute("class", "app-picker");
	link.appendChild(document.createTextNode("tiddlyspace"));

	var body = document.getElementsByTagName("BODY")[0];
	body.insertBefore(link, body.firstChild);
	var html = [
	'
',
	    '',
	    '

',
	'
'].join("");
	var bubble = document.createElement("div");
	bubble.setAttribute("id", "bs-popup");
	bubble.setAttribute("style", "display:none;");
	bubble.setAttribute("class", "bs-popup");
	bubble.innerHTML = html;
	body.insertBefore(bubble, link);

	twStylesheet(stylesheet);

	var bubbleFadeInterval;
	function fade(el, fadeIn) {
		var opacity = fadeIn ? 0 : 1;
		if(bubbleFadeInterval) {
			clearInterval(bubbleFadeInterval);
		}
		bubbleFadeInterval = setInterval(function() {
			// TODO: IE does not support opacity
			el.style.cssText = "opacity:" + opacity;
			opacity = fadeIn ? opacity + 0.1 : opacity - 0.1;
			if(opacity < 0 || opacity > 1) {
				clearInterval(bubbleFadeInterval);
				el.style.cssText = fadeIn ? "" : "display:none";
			}
		}, 50);
	}

	addEventListener(link, "mousedown", function(ev) {
		ev.preventDefault();
	}, false);

	var bubbleOpen = false;
	var toggleBubble = function(ev) {
		if(ev.stopPropagation) {
			ev.stopPropagation();
		} else {
			ev.cancelBubble = false;
		}
		if(bubbleOpen) {
			fade(bubble, false);
		} else {
			fade(bubble, true);
		};
		bubbleOpen = !bubbleOpen;
	}

	addEventListener(link, "click", toggleBubble);

	addEventListener(window, "click",
		function(ev) {
			if(bubbleOpen) {
				toggleBubble(ev);
			}
		}, false);

	addEventListener(bubble, "click", function(ev) {
		if(ev.stopPropagation) {
			ev.stopPropagation();
		} else {
			ev.cancelBubble = false;
		}
	});
};

addEventListener(window, "load", loadEvent);

})();
<commit_msg>set style and class so it works in ie6<commit_after>/***
Adds the app switcher to a TiddlySpace app.

Makes use of tw.Stylesheet
Triple licensed under the BSD, MIT and GPL licenses:
  http://www.opensource.org/licenses/bsd-license.php
  http://www.opensource.org/licenses/mit-license.php
  http://www.gnu.org/licenses/gpl.html
***/
(function() {

// Add or replace a style sheet
// css argument is a string of CSS rule sets
// options.id is an optional name identifying the style sheet
// options.doc is an optional document reference
// N.B.: Uses DOM methods instead of jQuery to ensure cross-browser comaptibility.
var twStylesheet = function(css, options) {
	options = options || {};
	var id = options.id || "backstageStyleSheet";
	var doc = options.doc || document;
	var el = doc.getElementById(id);
	if(doc.createStyleSheet) { // IE-specific handling
		if(el) {
			el.parentNode.removeChild(el);
		}
		doc.getElementsByTagName("head")[0].insertAdjacentHTML("beforeEnd",
			' '); // fails without
	} else { // modern browsers
		if(el) {
			el.replaceChild(doc.createTextNode(css), el.firstChild);
		} else {
			el = doc.createElement("style");
			el.type = "text/css";
			el.id = id;
			el.appendChild(doc.createTextNode(css));
			doc.getElementsByTagName("head")[0].appendChild(el);
		}
	}
};

var stylesheet = ["iframe {",
"	height: 320px;",
"	z-index: 1000;",
"	position: relative;",
"}",
"",
"#app-picker {",
"	cursor: pointer;",
"	position: absolute;",
"	right: 24px;",
"	top: 0px;",
"	width: 24px;",
"	height: 24px;",
"	background-size: 24px 24px;",
"	background-image: url(/bags/common/tiddlers/backstageIcon.png);",
"	text-indent: -999px;",
"	overflow: hidden;",
"	z-index: 2000;",
"	border: none;",
"	opacity: 0.5;",
"}",
"",
"#app-picker:hover {",
"	background-color: none !important;",
"	opacity: 1;",
"}",
"",
".bs-popup {",
"	width: 100%;",
"	height: 100%;",
"	position: absolute;",
"	z-index: 1000;",
"	right: 10px;",
"	top: 52px;",
"}",
"",
".bubble .description {",
"	margin-left: 70px;",
"	margin-top: 2px;",
"}",
"",
".bubble {",
"	float: right;",
"	font-size: 0.9em;",
"	font-family: Georgia;",
"	position: relative;",
"	width: 300px;",
"	margin: 0px auto 0px auto;",
"	margin: top right bottom left;",
"	padding: 20px 20px 20px 10px;",
"	border: solid 2px rgb(255, 255, 255);",
"	border-radius: 8px 8px;",
"	-webkit-box-shadow: #F4C4E0 0px 0px 8px;",
"	-moz-box-shadow: #F4C4E0 0px 0px 8px;",
"	-o-box-shadow: #F4C4E0 0px 0px 8px;",
"	-ms-box-shadow: #F4C4E0 0px 0px 8px;",
"	box-shadow: #F4C4E0 0px 0px 8px;",
"	background-color: rgb(220, 231, 241);",
"}",
"",
".bubble .tail:before {",
"	content: \"\";",
"	position:absolute;",
"	border-style:solid;",
"	display:block; ",
"	width:0;",
"	top:-24px;",
"	right:5px;",
"	bottom:auto;",
"	left:auto;",
"	opacity: 0.5;",
"	border-width:0 20px 20px;",
"	border-color: rgba(244, 196, 224, 0.6) transparent;",
"}",
"",
".bubble .tail:after {",
"	content:\"\";",
"	position:absolute;",
"	border-style:solid;",
"	display:block; ",
"	width:0;",
"	top:-15px;",
"	right:10px;",
"	bottom:auto;",
"	left:auto;",
"	border-width:0 15px 15px;",
"	border-color: #DCE7F1 transparent;",
"}"
].join("\n");
function addEventListener(node, event, handler) {
	if (node.addEventListener){
		node.addEventListener(event, handler, false);
	} else if (node.attachEvent){
		event = event == "click" ? "onclick" : event;
		event = event == "load" ? "onload" : event;
		node.attachEvent(event, handler);
	}
};

var loadEvent = function() {
	var link = document.createElement("a");
	link.setAttribute("id", "app-picker");
	link.setAttribute("class", "app-picker");
	link.appendChild(document.createTextNode("tiddlyspace"));

	var body = document.getElementsByTagName("BODY")[0];
	body.insertBefore(link, body.firstChild);
	var html = [
	'
',
	    '',
	    '

',
	'
'].join("");
	var bubble = document.createElement("div");
	bubble.setAttribute("id", "bs-popup");
	bubble.style.cssText = "display:none;";
	bubble.className = "bs-popup";
	bubble.innerHTML = html;
	body.insertBefore(bubble, link);

	twStylesheet(stylesheet);

	var bubbleFadeInterval;
	function fade(el, fadeIn) {
		var opacity = fadeIn ? 0 : 1;
		if(bubbleFadeInterval) {
			clearInterval(bubbleFadeInterval);
		}
		bubbleFadeInterval = setInterval(function() {
			// TODO: IE does not support opacity
			el.style.cssText = "opacity:" + opacity;
			opacity = fadeIn ? opacity + 0.1 : opacity - 0.1;
			if(opacity < 0 || opacity > 1) {
				clearInterval(bubbleFadeInterval);
				el.style.cssText = fadeIn ? "" : "display:none;";
			}
		}, 50);
	}

	addEventListener(link, "mousedown", function(ev) {
		ev.preventDefault();
	}, false);

	var bubbleOpen = false;
	var toggleBubble = function(ev) {
		if(ev.stopPropagation) {
			ev.stopPropagation();
		} else {
			ev.cancelBubble = false;
		}
		if(bubbleOpen) {
			fade(bubble, false);
		} else {
			fade(bubble, true);
		};
		bubbleOpen = !bubbleOpen;
	}

	addEventListener(link, "click", toggleBubble);

	addEventListener(window, "click",
		function(ev) {
			if(bubbleOpen) {
				toggleBubble(ev);
			}
		}, false);

	addEventListener(bubble, "click", function(ev) {
		if(ev.stopPropagation) {
			ev.stopPropagation();
		} else {
			ev.cancelBubble = false;
		}
	});
};

addEventListener(window, "load", loadEvent);

})();
<|endoftext|>


<commit_before>/***
Adds the app switcher to a TiddlySpace app.

Makes use of tw.Stylesheet
Triple licensed under the BSD, MIT and GPL licenses:
  http://www.opensource.org/licenses/bsd-license.php
  http://www.opensource.org/licenses/mit-license.php
  http://www.gnu.org/licenses/gpl.html
***/
(function() {

// Add or replace a style sheet
// css argument is a string of CSS rule sets
// options.id is an optional name identifying the style sheet
// options.doc is an optional document reference
// N.B.: Uses DOM methods instead of jQuery to ensure cross-browser comaptibility.
var twStylesheet = function(css, options) {
	options = options || {};
	var id = options.id || "backstageStyleSheet";
	var doc = options.doc || document;
	var el = doc.getElementById(id);
	if(doc.createStyleSheet) { // IE-specific handling
		if(el) {
			el.parentNode.removeChild(el);
		}
		doc.getElementsByTagName("head")[0].insertAdjacentHTML("beforeEnd",
			' '); // fails without
	} else { // modern browsers
		if(el) {
			el.replaceChild(doc.createTextNode(css), el.firstChild);
		} else {
			el = doc.createElement("style");
			el.type = "text/css";
			el.id = id;
			el.appendChild(doc.createTextNode(css));
			doc.getElementsByTagName("head")[0].appendChild(el);
		}
	}
};

// detect background-size support
// in <IE9 need to fallback to msfilter property
function hasBgSizing() {
	var supported,
		elem = document.createElement('div');

	document.body.appendChild(elem);
	elem.style.cssText = "background-size: cover;";
	supported = (elem.style.backgroundSize === undefined || elem.style.backgroundSize === null) ? false : true;
	// clean up
	elem.parentNode.removeChild(elem);
	return supported;
}
var backgroundSizeSupported = hasBgSizing();

// ms filters as fix for not supporting background-size property
var msfilter_in = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/bags/tiddlyspace/tiddlers/privateAndPublicIcon', sizingMethod='scale')",
	msfilter_out = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/bags/tiddlyspace/tiddlers/publicIcon', sizingMethod='scale')";

var stylesheet = ["iframe {",
"	height: 256px; /* need to dynamically resize this so all opts fit */",
"	z-index: 1000;",
"	position: relative;",
"}",
"",
"#app-picker {",
"	cursor: pointer;",
"	position: absolute;",
"	right: 24px;",
"	top: 0px;",
"	width: 24px;",
"	height: 24px;",
"	background-size: 24px 24px;",
"	text-indent: -999px;",
"	overflow: hidden;",
"	z-index: 2000;",
"	border: none;",
"	opacity: 0.5;",
"}",
"",
"#app-picker:hover {",
"	background-color: none !important;",
"	opacity: 1;",
"}",
"",
".bs-popup {",
"	width: 100%;",
"	position: absolute;",
"	z-index: 1000;",
"	right: 10px;",
"	top: 36px;",
"}",
"",
".bubble .description {",
"	margin-left: 70px;",
"	margin-top: 2px;",
"}",
"",
".bubble {",
"	float: right;",
"	font-size: 0.9em;",
"	font-family: Georgia;",
"	position: relative;",
"	width: 300px;",
"	margin: 0px auto 0px auto;",
"	margin: top right bottom left;",
"	border: solid 1px rgb(200, 200, 200);",
"	border-radius: 4px;",
"	-webkit-box-shadow: 0px 0px 4px rgba(0,0,0,.2);",
"	-moz-box-shadow: 0px 0px 4px rgba(0,0,0,.2);",
"	-o-box-shadow: 0px 0px 4px rgba(0,0,0,.2);",
"	-ms-box-shadow: 0px 0px 4px rgba(0,0,0,.2);",
"	box-shadow: 0px 0px 4px rgba(0,0,0,.2);",
"	background-color: #F0F4F8;",
"}",
"",
".ts-logout {",
"	display: none;",
"}",
".ts-loggedin .ts-logout {",
"	display: block;",
"}",
".arrow {",
"	border-width: 0 10px 10px;",
"	border-style: dashed dashed solid;",
"	width: 0;",
"	height: 0;",
"	border-color: transparent;",
"	display: inline-block;",
"	position: absolute;",
"	top: -10px;",
"	right: 16px;",
"	border-bottom-color: #fff;",
"}",
".bubble div.whitearrow {",
"	top: -11px;",
"	border-bottom-color: rgba(0,0,0,0.25);",
"}"
].join("\n");

function addEventListener(node, event, handler, bubble) {
	if (node.addEventListener){
		node.addEventListener(event, handler, bubble);
	} else if (node.attachEvent){
		event = event == "click" ? "onclick" : event;
		event = event == "load" ? "onload" : event;
		node.attachEvent(event, handler);
	}
}

var loadEvent = function() {
	var link = document.createElement("a");
	link.setAttribute("id", "app-picker");
	link.setAttribute("class", "app-picker");
	link.setAttribute("title", "Click to navigate around tiddlyspace");
	link.appendChild(document.createTextNode("tiddlyspace"));

        // Quite a hack. GUEST does not have a csrf token.
        if (/csrf_token=\d+:\w+:\w+/.test(document.cookie)) {
			if( backgroundSizeSupported ) {
				link.style.backgroundImage = 'url(/bags/tiddlyspace/tiddlers/privateAndPublicIcon)';
			} else {
				link.style.filter = msfilter_in;
			}
        } else {
			if( backgroundSizeSupported ) {
				link.style.backgroundImage = 'url(/bags/tiddlyspace/tiddlers/publicIcon)';
			} else {
				link.style.filter = msfilter_out;
			}
			stylesheet = stylesheet.replace('height: 180px;', 'height: 156px;');
        }

	var body = document.getElementsByTagName("BODY")[0];
	body.insertBefore(link, body.firstChild);
	var html = [
	'
',
		'',
		'

',
		'

',
	'
'].join("");
	var bubble = document.createElement("div");
	bubble.setAttribute("id", "bs-popup");
	bubble.style.cssText = "visibility:hidden;";
	bubble.className = "bs-popup";
	bubble.innerHTML = html;
	body.insertBefore(bubble, link);

	twStylesheet(stylesheet);

	var bubbleFadeInterval;
	function fade(el, fadeIn) {
		var opacity = fadeIn ? 0 : 1;
		if(bubbleFadeInterval) {
			clearInterval(bubbleFadeInterval);
		}
		bubbleFadeInterval = setInterval(function() {
			// TODO: IE does not support opacity
			el.style.cssText = "opacity:" + opacity;
			opacity = fadeIn ? opacity + 0.1 : opacity - 0.1;
			if(opacity < 0 || opacity > 1) {
				clearInterval(bubbleFadeInterval);
				el.style.cssText = fadeIn ? "" : "visibility:hidden;";
			}
		}, 25);
	}

	addEventListener(link, "mousedown", function(ev) {
		ev.preventDefault();
	}, false);

	var bubbleOpen = false;
	var toggleBubble = function(ev) {
		if(ev.stopPropagation) {
			ev.stopPropagation();
		} else {
			ev.cancelBubble = false;
		}
		if(bubbleOpen) {
			fade(bubble, false);
		} else {
			fade(bubble, true);
		}
		bubbleOpen = !bubbleOpen;
	};

	addEventListener(link, "click", toggleBubble);

	addEventListener(window.document.body, "click",
		function(ev) {
			var targ,
			ev = ev || window.event;

			if (ev.target) targ = ev.target;
			else if (ev.srcElement) targ = ev.srcElement;
			if(targ == link) {
				return;
			}
			if(bubbleOpen) {
				toggleBubble(ev);
			}
		}, true);

	addEventListener(bubble, "click", function(ev) {
		if(ev.stopPropagation) {
			ev.stopPropagation();
		} else {
			ev.cancelBubble = false;
		}
	});
};

if(window.top == window) { // only add the backstage when NOT in an iframe (top window)
	addEventListener(window, "load", loadEvent);
}

})();
<commit_msg>Delay testing for background-size in backstage.js<commit_after>/***
Adds the app switcher to a TiddlySpace app.

Makes use of tw.Stylesheet
Triple licensed under the BSD, MIT and GPL licenses:
  http://www.opensource.org/licenses/bsd-license.php
  http://www.opensource.org/licenses/mit-license.php
  http://www.gnu.org/licenses/gpl.html
***/
(function() {

// Add or replace a style sheet
// css argument is a string of CSS rule sets
// options.id is an optional name identifying the style sheet
// options.doc is an optional document reference
// N.B.: Uses DOM methods instead of jQuery to ensure cross-browser comaptibility.
var twStylesheet = function(css, options) {
	options = options || {};
	var id = options.id || "backstageStyleSheet";
	var doc = options.doc || document;
	var el = doc.getElementById(id);
	if(doc.createStyleSheet) { // IE-specific handling
		if(el) {
			el.parentNode.removeChild(el);
		}
		doc.getElementsByTagName("head")[0].insertAdjacentHTML("beforeEnd",
			' '); // fails without
	} else { // modern browsers
		if(el) {
			el.replaceChild(doc.createTextNode(css), el.firstChild);
		} else {
			el = doc.createElement("style");
			el.type = "text/css";
			el.id = id;
			el.appendChild(doc.createTextNode(css));
			doc.getElementsByTagName("head")[0].appendChild(el);
		}
	}
};

// detect background-size support
// in <IE9 need to fallback to msfilter property
function hasBgSizing() {
	var supported,
		elem = document.createElement('div');

	document.body.appendChild(elem);
	elem.style.cssText = "background-size: cover;";
	supported = (elem.style.backgroundSize === undefined || elem.style.backgroundSize === null) ? false : true;
	// clean up
	elem.parentNode.removeChild(elem);
	return supported;
}

// ms filters as fix for not supporting background-size property
var msfilter_in = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/bags/tiddlyspace/tiddlers/privateAndPublicIcon', sizingMethod='scale')",
	msfilter_out = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/bags/tiddlyspace/tiddlers/publicIcon', sizingMethod='scale')";

var stylesheet = ["iframe {",
"	height: 256px; /* need to dynamically resize this so all opts fit */",
"	z-index: 1000;",
"	position: relative;",
"}",
"",
"#app-picker {",
"	cursor: pointer;",
"	position: absolute;",
"	right: 24px;",
"	top: 0px;",
"	width: 24px;",
"	height: 24px;",
"	background-size: 24px 24px;",
"	text-indent: -999px;",
"	overflow: hidden;",
"	z-index: 2000;",
"	border: none;",
"	opacity: 0.5;",
"}",
"",
"#app-picker:hover {",
"	background-color: none !important;",
"	opacity: 1;",
"}",
"",
".bs-popup {",
"	width: 100%;",
"	position: absolute;",
"	z-index: 1000;",
"	right: 10px;",
"	top: 36px;",
"}",
"",
".bubble .description {",
"	margin-left: 70px;",
"	margin-top: 2px;",
"}",
"",
".bubble {",
"	float: right;",
"	font-size: 0.9em;",
"	font-family: Georgia;",
"	position: relative;",
"	width: 300px;",
"	margin: 0px auto 0px auto;",
"	margin: top right bottom left;",
"	border: solid 1px rgb(200, 200, 200);",
"	border-radius: 4px;",
"	-webkit-box-shadow: 0px 0px 4px rgba(0,0,0,.2);",
"	-moz-box-shadow: 0px 0px 4px rgba(0,0,0,.2);",
"	-o-box-shadow: 0px 0px 4px rgba(0,0,0,.2);",
"	-ms-box-shadow: 0px 0px 4px rgba(0,0,0,.2);",
"	box-shadow: 0px 0px 4px rgba(0,0,0,.2);",
"	background-color: #F0F4F8;",
"}",
"",
".ts-logout {",
"	display: none;",
"}",
".ts-loggedin .ts-logout {",
"	display: block;",
"}",
".arrow {",
"	border-width: 0 10px 10px;",
"	border-style: dashed dashed solid;",
"	width: 0;",
"	height: 0;",
"	border-color: transparent;",
"	display: inline-block;",
"	position: absolute;",
"	top: -10px;",
"	right: 16px;",
"	border-bottom-color: #fff;",
"}",
".bubble div.whitearrow {",
"	top: -11px;",
"	border-bottom-color: rgba(0,0,0,0.25);",
"}"
].join("\n");

function addEventListener(node, event, handler, bubble) {
	if (node.addEventListener){
		node.addEventListener(event, handler, bubble);
	} else if (node.attachEvent){
		event = event == "click" ? "onclick" : event;
		event = event == "load" ? "onload" : event;
		node.attachEvent(event, handler);
	}
}

var loadEvent = function() {
	var link = document.createElement("a");
	link.setAttribute("id", "app-picker");
	link.setAttribute("class", "app-picker");
	link.setAttribute("title", "Click to navigate around tiddlyspace");
	link.appendChild(document.createTextNode("tiddlyspace"));
	var backgroundSizeSupported = hasBgSizing();

        // Quite a hack. GUEST does not have a csrf token.
        if (/csrf_token=\d+:\w+:\w+/.test(document.cookie)) {
			if( backgroundSizeSupported ) {
				link.style.backgroundImage = 'url(/bags/tiddlyspace/tiddlers/privateAndPublicIcon)';
			} else {
				link.style.filter = msfilter_in;
			}
        } else {
			if( backgroundSizeSupported ) {
				link.style.backgroundImage = 'url(/bags/tiddlyspace/tiddlers/publicIcon)';
			} else {
				link.style.filter = msfilter_out;
			}
			stylesheet = stylesheet.replace('height: 180px;', 'height: 156px;');
        }

	var body = document.getElementsByTagName("BODY")[0];
	body.insertBefore(link, body.firstChild);
	var html = [
	'
',
		'',
		'

',
		'

',
	'
'].join("");
	var bubble = document.createElement("div");
	bubble.setAttribute("id", "bs-popup");
	bubble.style.cssText = "visibility:hidden;";
	bubble.className = "bs-popup";
	bubble.innerHTML = html;
	body.insertBefore(bubble, link);

	twStylesheet(stylesheet);

	var bubbleFadeInterval;
	function fade(el, fadeIn) {
		var opacity = fadeIn ? 0 : 1;
		if(bubbleFadeInterval) {
			clearInterval(bubbleFadeInterval);
		}
		bubbleFadeInterval = setInterval(function() {
			// TODO: IE does not support opacity
			el.style.cssText = "opacity:" + opacity;
			opacity = fadeIn ? opacity + 0.1 : opacity - 0.1;
			if(opacity < 0 || opacity > 1) {
				clearInterval(bubbleFadeInterval);
				el.style.cssText = fadeIn ? "" : "visibility:hidden;";
			}
		}, 25);
	}

	addEventListener(link, "mousedown", function(ev) {
		ev.preventDefault();
	}, false);

	var bubbleOpen = false;
	var toggleBubble = function(ev) {
		if(ev.stopPropagation) {
			ev.stopPropagation();
		} else {
			ev.cancelBubble = false;
		}
		if(bubbleOpen) {
			fade(bubble, false);
		} else {
			fade(bubble, true);
		}
		bubbleOpen = !bubbleOpen;
	};

	addEventListener(link, "click", toggleBubble);

	addEventListener(window.document.body, "click",
		function(ev) {
			var targ,
			ev = ev || window.event;

			if (ev.target) targ = ev.target;
			else if (ev.srcElement) targ = ev.srcElement;
			if(targ == link) {
				return;
			}
			if(bubbleOpen) {
				toggleBubble(ev);
			}
		}, true);

	addEventListener(bubble, "click", function(ev) {
		if(ev.stopPropagation) {
			ev.stopPropagation();
		} else {
			ev.cancelBubble = false;
		}
	});
};

if(window.top == window) { // only add the backstage when NOT in an iframe (top window)
	addEventListener(window, "load", loadEvent);
}

})();
<|endoftext|>


<commit_before>'use strict'; // eslint-disable-line strict
const async = require('async');

const Logger = require('werelogs').Logger;
const redisClient = require('../../replication/utils/getRedisClient')();

const FailedCRRProducer = require('./FailedCRRProducer');
const BackbeatConsumer = require('../../../lib/BackbeatConsumer');
const BackbeatTask = require('../../../lib/tasks/BackbeatTask');
const config = require('../../../conf/Config');

// BackbeatConsumer constant defaults
const CONSUMER_FETCH_MAX_BYTES = 5000020;
const CONCURRENCY = 10;

class FailedCRRConsumer {
    /**
     * Create the retry consumer.
     */
    constructor() {
        this._repConfig = config.extensions.replication;
        this._kafkaConfig = config.kafka;
        this._topic = config.extensions.replication.replicationFailedTopic;
        this.logger = new Logger('Backbeat:FailedCRRConsumer');
        this._failedCRRProducer = new FailedCRRProducer(this.kafkaConfig);
        this._backbeatTask = new BackbeatTask();
    }

    /**
     * Start the retry consumer by subscribing to the retry kafka topic. Setup
     * the failed CRR producer for pushing any failed redis operations back to
     * the queue.
     * @param {Function} cb - The callback function
     * @return {undefined}
     */
    start(cb) {
        let consumerReady = false;
        const consumer = new BackbeatConsumer({
            kafka: { hosts: this._kafkaConfig.hosts },
            topic: this._topic,
            groupId: 'backbeat-retry-group',
            concurrency: CONCURRENCY,
            queueProcessor: this.processKafkaEntry.bind(this),
            fetchMaxBytes: CONSUMER_FETCH_MAX_BYTES,
        });
        consumer.on('error', err => {
            if (!consumerReady) {
                this.logger.fatal('could not setup a backbeat consumer', {
                    method: 'FailedCRRConsumer.start',
                    error: err,
                });
                process.exit(1);
            }
        });
        consumer.on('ready', () => {
            consumerReady = true;
            consumer.subscribe();
            this.logger.info('retry consumer is ready to consume entries');
        });
        return this._failedCRRProducer.setupProducer(err => {
            if (err) {
                this.logger.error('could not setup producer', {
                    method: 'FailedCRRConsumer.processKafkaEntry',
                    error: err,
                });
                return cb(err);
            }
            return cb();
        });
    }

    /**
     * Process an entry from the retry topic, and set the data in a Redis hash.
     * @param {Object} kafkaEntry - The entry from the retry topic
     * @param {function} cb - The callback function
     * @return {undefined}
     */
    processKafkaEntry(kafkaEntry, cb) {
        const log = this.logger.newRequestLogger();
        let data;
        try {
            data = JSON.parse(kafkaEntry.value);
        } catch (err) {
            log.error('error processing retry entry', {
                method: 'FailedCRRConsumer.processKafkaEntry',
                error: err,
            });
            log.end();
            return cb();
        }
        return async.series([
            next => this._removePreexistingKeys(data, err => {
                if (err) {
                    this.logger.error('error deleting preexisting CRR object ' +
                    'metrics redis keys', {
                        method: 'FailedCRRConsumer.processKafkaEntry',
                        error: err,
                    });
                }
                return next();
            }),
            next => this._setRedisKey(data, kafkaEntry, log, next),
        ], cb);
    }

    /**
     * Delete any pre-existing object CRR metrics keys. This way the object
     * metrics during the retry will not be affected by the prior attempt.
     * @param {Object} data - The kafka entry value
     * @param {Function} cb - callback to call
     * @return {undefined}
     */
    _removePreexistingKeys(data, cb) {
        // eslint-disable-next-line no-unused-vars
        const [service, extension, status, bucketName, objectKey, versionId,
            site] = data.key.split(':');
        // CRR object-level metrics key schema.
        const queryString = `${site}:${bucketName}:${objectKey}:${versionId}:` +
            `${service}:${extension}:*`;
        return redisClient.scan(queryString, undefined, (err, keys) => {
            if (err) {
                return cb(err);
            }
            if (keys === 0) {
                return cb();
            }
            return redisClient.batch([['del', ...keys]], (err, res) => {
                if (err) {
                    return cb(err);
                }
                const [cmdErr] = res[0];
                if (cmdErr) {
                    return cb(cmdErr);
                }
                return cb();
            });
        });
    }

    /**
     * Attempt to set the Redis hash, using an exponential backoff should the
     * key set fail. If the backoff time is exceeded, push the entry back into
     * the retry entry topic for a later reattempt.
     * @param {Object} data - The field and value for the Redis hash
     * @param {Object} kafkaEntry - The entry from the retry topic
     * @param {Werelogs} log - The werelogs logger
     * @param {Function} cb - The callback function
     * @return {undefined}
     */
    _setRedisKey(data, kafkaEntry, log, cb) {
        this._backbeatTask.retry({
            actionDesc: 'set redis key',
            logFields: {},
            actionFunc: done => this._setRedisKeyOnce(data, log, done),
            shouldRetryFunc: err => err.retryable,
            log,
        }, err => {
            if (err && err.retryable === true) {
                log.info('publishing entry back into the kafka queue');
                const entry = Buffer.from(kafkaEntry.value).toString();
                return this._failedCRRProducer.publishFailedCRREntry(entry, cb);
            }
            log.info('successfully set redis key');
            return cb();
        });
    }

    /**
     * Attempt to set the Redis hash.
     * @param {Object} data - The key and value for the Redis key
     * @param {Werelogs} log - The werelogs logger
     * @param {Function} cb - The callback function
     * @return {undefined}
     */
    _setRedisKeyOnce(data, log, cb) {
        const { key, value } = data;
        const expiry = this._repConfig.monitorReplicationFailureExpiryTimeS;
        const cmd = ['set', key, value, 'EX', expiry];
        return redisClient.batch([cmd], (err, res) => {
            if (err) {
                return cb({ retryable: true });
            }
            const [cmdErr] = res[0];
            return cb({ retryable: cmdErr !== null });
        });
    }
}

module.exports = FailedCRRConsumer;
<commit_msg>bugfix: ZENKO-683 Check length of array<commit_after>'use strict'; // eslint-disable-line strict
const async = require('async');

const Logger = require('werelogs').Logger;
const redisClient = require('../../replication/utils/getRedisClient')();

const FailedCRRProducer = require('./FailedCRRProducer');
const BackbeatConsumer = require('../../../lib/BackbeatConsumer');
const BackbeatTask = require('../../../lib/tasks/BackbeatTask');
const config = require('../../../conf/Config');

// BackbeatConsumer constant defaults
const CONSUMER_FETCH_MAX_BYTES = 5000020;
const CONCURRENCY = 10;

class FailedCRRConsumer {
    /**
     * Create the retry consumer.
     */
    constructor() {
        this._repConfig = config.extensions.replication;
        this._kafkaConfig = config.kafka;
        this._topic = config.extensions.replication.replicationFailedTopic;
        this.logger = new Logger('Backbeat:FailedCRRConsumer');
        this._failedCRRProducer = new FailedCRRProducer(this.kafkaConfig);
        this._backbeatTask = new BackbeatTask();
    }

    /**
     * Start the retry consumer by subscribing to the retry kafka topic. Setup
     * the failed CRR producer for pushing any failed redis operations back to
     * the queue.
     * @param {Function} cb - The callback function
     * @return {undefined}
     */
    start(cb) {
        let consumerReady = false;
        const consumer = new BackbeatConsumer({
            kafka: { hosts: this._kafkaConfig.hosts },
            topic: this._topic,
            groupId: 'backbeat-retry-group',
            concurrency: CONCURRENCY,
            queueProcessor: this.processKafkaEntry.bind(this),
            fetchMaxBytes: CONSUMER_FETCH_MAX_BYTES,
        });
        consumer.on('error', err => {
            if (!consumerReady) {
                this.logger.fatal('could not setup a backbeat consumer', {
                    method: 'FailedCRRConsumer.start',
                    error: err,
                });
                process.exit(1);
            }
        });
        consumer.on('ready', () => {
            consumerReady = true;
            consumer.subscribe();
            this.logger.info('retry consumer is ready to consume entries');
        });
        return this._failedCRRProducer.setupProducer(err => {
            if (err) {
                this.logger.error('could not setup producer', {
                    method: 'FailedCRRConsumer.processKafkaEntry',
                    error: err,
                });
                return cb(err);
            }
            return cb();
        });
    }

    /**
     * Process an entry from the retry topic, and set the data in a Redis hash.
     * @param {Object} kafkaEntry - The entry from the retry topic
     * @param {function} cb - The callback function
     * @return {undefined}
     */
    processKafkaEntry(kafkaEntry, cb) {
        const log = this.logger.newRequestLogger();
        let data;
        try {
            data = JSON.parse(kafkaEntry.value);
        } catch (err) {
            log.error('error processing retry entry', {
                method: 'FailedCRRConsumer.processKafkaEntry',
                error: err,
            });
            log.end();
            return cb();
        }
        return async.series([
            next => this._removePreexistingKeys(data, err => {
                if (err) {
                    this.logger.error('error deleting preexisting CRR object ' +
                    'metrics redis keys', {
                        method: 'FailedCRRConsumer.processKafkaEntry',
                        error: err,
                    });
                }
                return next();
            }),
            next => this._setRedisKey(data, kafkaEntry, log, next),
        ], cb);
    }

    /**
     * Delete any pre-existing object CRR metrics keys. This way the object
     * metrics during the retry will not be affected by the prior attempt.
     * @param {Object} data - The kafka entry value
     * @param {Function} cb - callback to call
     * @return {undefined}
     */
    _removePreexistingKeys(data, cb) {
        // eslint-disable-next-line no-unused-vars
        const [service, extension, status, bucketName, objectKey, versionId,
            site] = data.key.split(':');
        // CRR object-level metrics key schema.
        const queryString = `${site}:${bucketName}:${objectKey}:${versionId}:` +
            `${service}:${extension}:*`;
        return redisClient.scan(queryString, undefined, (err, keys) => {
            if (err) {
                return cb(err);
            }
            if (keys.length === 0) {
                return cb();
            }
            return redisClient.batch([['del', ...keys]], (err, res) => {
                if (err) {
                    return cb(err);
                }
                const [cmdErr] = res[0];
                if (cmdErr) {
                    return cb(cmdErr);
                }
                return cb();
            });
        });
    }

    /**
     * Attempt to set the Redis hash, using an exponential backoff should the
     * key set fail. If the backoff time is exceeded, push the entry back into
     * the retry entry topic for a later reattempt.
     * @param {Object} data - The field and value for the Redis hash
     * @param {Object} kafkaEntry - The entry from the retry topic
     * @param {Werelogs} log - The werelogs logger
     * @param {Function} cb - The callback function
     * @return {undefined}
     */
    _setRedisKey(data, kafkaEntry, log, cb) {
        this._backbeatTask.retry({
            actionDesc: 'set redis key',
            logFields: {},
            actionFunc: done => this._setRedisKeyOnce(data, log, done),
            shouldRetryFunc: err => err.retryable,
            log,
        }, err => {
            if (err && err.retryable === true) {
                log.info('publishing entry back into the kafka queue');
                const entry = Buffer.from(kafkaEntry.value).toString();
                return this._failedCRRProducer.publishFailedCRREntry(entry, cb);
            }
            log.info('successfully set redis key');
            return cb();
        });
    }

    /**
     * Attempt to set the Redis hash.
     * @param {Object} data - The key and value for the Redis key
     * @param {Werelogs} log - The werelogs logger
     * @param {Function} cb - The callback function
     * @return {undefined}
     */
    _setRedisKeyOnce(data, log, cb) {
        const { key, value } = data;
        const expiry = this._repConfig.monitorReplicationFailureExpiryTimeS;
        const cmd = ['set', key, value, 'EX', expiry];
        return redisClient.batch([cmd], (err, res) => {
            if (err) {
                return cb({ retryable: true });
            }
            const [cmdErr] = res[0];
            return cb({ retryable: cmdErr !== null });
        });
    }
}

module.exports = FailedCRRConsumer;
<|endoftext|>


<commit_before>var
	config = require('../config'),
	paths = {
		css: require('../utils/paths')('css'),
		styleguide: require('../utils/paths')('styleguide'),
	},
	src,
	styleguide,
	task,

	directory = require('directory-tree'),
	ejs = require('ejs'),
	fm = require('front-matter'),
	fs = require('fs'),
	gulp = require('gulp'),
	gutil = require('gulp-util'),
	marked = require('marked'),
	path = require('path'),
	postcss = require('gulp-postcss'),
	syntax = require('postcss-scss'),
	titleCase = require('title-case'),

	options = {
		postcss: {
			syntax: syntax,
		},
	},
	src = path.join(
		config.root.src,
		config.tasks.css.src
	),
	template = path.resolve(
		__dirname,
		'../..',
		path.join(
			config.root.src,
			config.tasks.styleguide.src,
			'index.html'
		)
	),

	processor = function(css) {
		var relPath = path.relative(process.cwd(), css.source.input.file);

		css.walkDecls(function(decl) {
			var
				variable = decl.prop.match(/\$(\w+(-\w+)?)(--(\w+(-\w+)?))?/),
				type = variable && variable[1];

			if (!variable) {
				return;
			}

			if (!styleguide.variables[type]) {
				styleguide.variables[type] = [];
			}

			styleguide.variables[type].push({
				modifier: variable && (variable[4] || false),
				value: decl.value,
			});
		});

		css.walkComments(function(comment) {
			var content;

			// skip non-styleguide comments
			if (comment.text.substr(0, 3) !== '---') {
				return;
			}

			content = fm(comment.text);

			if (content.body.substr(0, 3) === '---') {
				content.body = content.body.substr(3);
			}

			if (!styleguide.components[relPath]) {
				styleguide.components[relPath] = [];
			}

			styleguide.components[relPath].push({
				attributes: content.attributes,
				html: content.body.trim(),
			});
		});
	},

	titleCaseFilename = function(filename) {
		return titleCase(filename.match(/_?([^\.]+)/)[1]);
	},

	parseDocs = function(node) {
		return {
			filename: titleCaseFilename(node.name),
			children: node.children ? node.children.map(parseDocs) : [],
			html: !node.children && marked(fs.readFileSync(node.path, { encoding: 'utf8' }))
		};
	},

	parseNode = function(node) {
		return {
			filename: titleCaseFilename(node.name),
			components: styleguide.components[node.path] || [],
			children: node.children ? node.children.map(parseNode) : []
		};
	},

	filterNode = function(node) {
		node.children = node.children.filter(filterNode);
		return node.children.length || node.components.length;
	};

gulp.task('styleguide.parse', function() {
	styleguide = {
		variables: {},
		components: {}
	};

	return gulp
		.src(paths.css.src)
		.pipe(postcss([processor], options.postcss));
});

gulp.task('styleguide.generate', function(done) {
	var data = {
		variables: styleguide.variables,
		nodes: {
			docs: directory('src/styleguide/docs', ['.md']).children.map(parseDocs),
			vars: [],
			components: directory('src/scss').children.map(parseNode).filter(filterNode)
		}
	};

	ejs.renderFile(template, data, null, function(error, html) {
		if (error) {

			gutil.log(gutil.colors.red(error.message));
			done();

		} else {

			fs.writeFile(paths.styleguide.dest, html, 'utf8', done);

		}
	});
});

task = gulp.series('styleguide.parse', 'styleguide.generate');
gulp.task('styleguide', task);
module.exports = task;
<commit_msg>Updates function names.<commit_after>var
	config = require('../config'),
	paths = {
		css: require('../utils/paths')('css'),
		styleguide: require('../utils/paths')('styleguide'),
	},
	src,
	styleguide,
	task,

	directory = require('directory-tree'),
	ejs = require('ejs'),
	fm = require('front-matter'),
	fs = require('fs'),
	gulp = require('gulp'),
	gutil = require('gulp-util'),
	marked = require('marked'),
	path = require('path'),
	postcss = require('gulp-postcss'),
	syntax = require('postcss-scss'),
	titleCase = require('title-case'),

	options = {
		postcss: {
			syntax: syntax,
		},
	},
	src = path.join(
		config.root.src,
		config.tasks.css.src
	),
	template = path.resolve(
		__dirname,
		'../..',
		path.join(
			config.root.src,
			config.tasks.styleguide.src,
			'index.html'
		)
	),

	processor = function(css) {
		var relPath = path.relative(process.cwd(), css.source.input.file);

		css.walkDecls(function(decl) {
			var
				variable = decl.prop.match(/\$(\w+(-\w+)?)(--(\w+(-\w+)?))?/),
				type = variable && variable[1];

			if (!variable) {
				return;
			}

			if (!styleguide.variables[type]) {
				styleguide.variables[type] = [];
			}

			styleguide.variables[type].push({
				modifier: variable && (variable[4] || false),
				value: decl.value,
			});
		});

		css.walkComments(function(comment) {
			var content;

			// skip non-styleguide comments
			if (comment.text.substr(0, 3) !== '---') {
				return;
			}

			content = fm(comment.text);

			if (content.body.substr(0, 3) === '---') {
				content.body = content.body.substr(3);
			}

			if (!styleguide.components[relPath]) {
				styleguide.components[relPath] = [];
			}

			styleguide.components[relPath].push({
				attributes: content.attributes,
				html: content.body.trim(),
			});
		});
	},

	titleCaseFilename = function(filename) {
		return titleCase(filename.match(/_?([^\.]+)/)[1]);
	},

	parseDoc = function(node) {
		return {
			filename: titleCaseFilename(node.name),
			children: node.children ? node.children.map(parseDoc) : [],
			html: !node.children && marked(fs.readFileSync(node.path, { encoding: 'utf8' }))
		};
	},

	parseComponent = function(node) {
		return {
			filename: titleCaseFilename(node.name),
			components: styleguide.components[node.path] || [],
			children: node.children ? node.children.map(parseComponent) : []
		};
	},

	filterComponent = function(node) {
		node.children = node.children.filter(filterComponent);
		return node.children.length || node.components.length;
	};

gulp.task('styleguide.parse', function() {
	styleguide = {
		variables: {},
		components: {}
	};

	return gulp
		.src(paths.css.src)
		.pipe(postcss([processor], options.postcss));
});

gulp.task('styleguide.generate', function(done) {
	var data = {
		variables: styleguide.variables,
		nodes: {
			docs: directory('src/styleguide/docs', ['.md']).children.map(parseDoc),
			vars: [],
			components: directory('src/scss').children.map(parseComponent).filter(filterComponent)
		}
	};

	ejs.renderFile(template, data, null, function(error, html) {
		if (error) {

			gutil.log(gutil.colors.red(error.message));
			done();

		} else {

			fs.writeFile(paths.styleguide.dest, html, 'utf8', done);

		}
	});
});

task = gulp.series('styleguide.parse', 'styleguide.generate');
gulp.task('styleguide', task);
module.exports = task;
<|endoftext|>


<commit_before>const fs = require('fs')
const path = require('path')
const { EOL } = require('os')
const opentype = require('opentype.js')

const ROOT_DIR = path.resolve(__dirname, '..')
const ICONFONT_DIR = path.resolve(ROOT_DIR, 'iconfont')
const CODEPOINTS_JSON = path.resolve(ROOT_DIR, '_data', 'codepoints.json')
const CODEPOINTS_SCSS = path.resolve(ROOT_DIR, 'css', '_codepoints.scss')

const main = () => {
  const existing = getExistingCodepoints()
  const latest = getLatestCodepoints()
  logChanges(existing, latest)
  save(latest)
}

const getExistingCodepoints = () => {
  const json = fs.readFileSync(CODEPOINTS_JSON).toString()
  return JSON.parse(json)
}

const getLatestCodepoints = () => {
  const codepoints = {}
  const styles = ['', 'outlined', 'round', 'sharp', 'two-tone']
  styles.reverse() // last processed font overwrites previous ones
  styles.forEach((style) => {
    const file = `${ICONFONT_DIR}/material-icons${style && `-${style}`}.woff`
    processFont(file, codepoints)
  })
  return sortMap(codepoints)
}

const processFont = (file, codepoints) => {
  const font = opentype.loadSync(path.resolve(file))
  const glyphToCodepoints = {}
  const { glyphIndexMap } = font.tables.cmap
  for (const [codepoint, glyph] of Object.entries(glyphIndexMap)) {
    glyphToCodepoints[glyph] = parseInt(codepoint)
  }
  font.tables.gsub.lookups.forEach(({ lookupType, subtables }) => {
    if (lookupType === 4) {
      subtables.forEach(({ coverage: { ranges }, ligatureSets }) => {
        const prefixes = []
        ranges.forEach(({ start, end }) => {
          for (let i = start; i <= end; i++) {
            prefixes.push(i)
          }
        })
        ligatureSets.forEach((ligatureSet, i) => {
          ligatureSet.forEach(({ ligGlyph, components }) => {
            const ligature = [prefixes[i], ...components]
              .map((v) => String.fromCharCode(glyphToCodepoints[v]))
              .join('')
            const codepoint = glyphToCodepoints[ligGlyph]
            codepoints[ligature] = codepoint.toString(16)
          })
        })
      })
    }
  })
}

const save = (codepoints) => {
  codepoints = sortMap(codepoints)
  const sassMap = createSassMap(codepoints)
  fs.writeFileSync(CODEPOINTS_JSON, JSON.stringify(codepoints, null, 2))
  fs.writeFileSync(CODEPOINTS_SCSS, sassMap)
}

const createSassMap = (codepoints) => {
  const mapName = '$material-icons-codepoints'
  let map = ''
  for (const [name, codepoint] of Object.entries(codepoints)) {
    map += `  "${name}": ${codepoint},${EOL}`
  }

  map = map.replace(/,\s*$/, '')
  map = `${mapName}: () !default;
${mapName}: map-merge((
${map}
), ${mapName});
`
  return map
}

const logChanges = (existing, latest) => {
  Object.keys(existing).forEach((name) => {
    if (!latest[name]) {
      console.error(`Removed "${name}"`)
    }
  })
  Object.keys(existing).forEach((name) => {
    if (latest[name] && latest[name] !== existing[name]) {
      console.warn(`Changed "${name}"`)
    }
  })
  Object.keys(latest).forEach((name) => {
    if (!existing[name]) {
      console.info(`Added "${name}"`)
    }
  })
}

const sortMap = (map) => {
  return Object.fromEntries(Object.entries(map).sort())
}

main()
<commit_msg>Change EOL to '\n' for codepoints<commit_after>const fs = require('fs')
const path = require('path')
const opentype = require('opentype.js')

const ROOT_DIR = path.resolve(__dirname, '..')
const ICONFONT_DIR = path.resolve(ROOT_DIR, 'iconfont')
const CODEPOINTS_JSON = path.resolve(ROOT_DIR, '_data', 'codepoints.json')
const CODEPOINTS_SCSS = path.resolve(ROOT_DIR, 'css', '_codepoints.scss')

const main = () => {
  const existing = getExistingCodepoints()
  const latest = getLatestCodepoints()
  logChanges(existing, latest)
  save(latest)
}

const getExistingCodepoints = () => {
  const json = fs.readFileSync(CODEPOINTS_JSON).toString()
  return JSON.parse(json)
}

const getLatestCodepoints = () => {
  const codepoints = {}
  const styles = ['', 'outlined', 'round', 'sharp', 'two-tone']
  styles.reverse() // last processed font overwrites previous ones
  styles.forEach((style) => {
    const file = `${ICONFONT_DIR}/material-icons${style && `-${style}`}.woff`
    processFont(file, codepoints)
  })
  return sortMap(codepoints)
}

const processFont = (file, codepoints) => {
  const font = opentype.loadSync(path.resolve(file))
  const glyphToCodepoints = {}
  const { glyphIndexMap } = font.tables.cmap
  for (const [codepoint, glyph] of Object.entries(glyphIndexMap)) {
    glyphToCodepoints[glyph] = parseInt(codepoint)
  }
  font.tables.gsub.lookups.forEach(({ lookupType, subtables }) => {
    if (lookupType === 4) {
      subtables.forEach(({ coverage: { ranges }, ligatureSets }) => {
        const prefixes = []
        ranges.forEach(({ start, end }) => {
          for (let i = start; i <= end; i++) {
            prefixes.push(i)
          }
        })
        ligatureSets.forEach((ligatureSet, i) => {
          ligatureSet.forEach(({ ligGlyph, components }) => {
            const ligature = [prefixes[i], ...components]
              .map((v) => String.fromCharCode(glyphToCodepoints[v]))
              .join('')
            const codepoint = glyphToCodepoints[ligGlyph]
            codepoints[ligature] = codepoint.toString(16)
          })
        })
      })
    }
  })
}

const save = (codepoints) => {
  codepoints = sortMap(codepoints)
  const sassMap = createSassMap(codepoints)
  fs.writeFileSync(CODEPOINTS_JSON, JSON.stringify(codepoints, null, 2))
  fs.writeFileSync(CODEPOINTS_SCSS, sassMap)
}

const createSassMap = (codepoints) => {
  const mapName = '$material-icons-codepoints'
  let map = ''
  for (const [name, codepoint] of Object.entries(codepoints)) {
    map += `  "${name}": ${codepoint},\n`
  }

  map = map.replace(/,\s*$/, '')
  map = `${mapName}: () !default;
${mapName}: map-merge((
${map}
), ${mapName});
`
  return map
}

const logChanges = (existing, latest) => {
  Object.keys(existing).forEach((name) => {
    if (!latest[name]) {
      console.error(`Removed "${name}"`)
    }
  })
  Object.keys(existing).forEach((name) => {
    if (latest[name] && latest[name] !== existing[name]) {
      console.warn(`Changed "${name}"`)
    }
  })
  Object.keys(latest).forEach((name) => {
    if (!existing[name]) {
      console.info(`Added "${name}"`)
    }
  })
}

const sortMap = (map) => {
  return Object.fromEntries(Object.entries(map).sort())
}

main()
<|endoftext|>


<commit_before>"use strict";

module.exports = Object.freeze({

    // App-ID. TODO: set to your own Skill App ID from the developer portal.
    appId : 'amzn1.ask.skill.c2368a37-fa99-4024-b61e-5c390c8b628b',

    //  DynamoDB Table name
    dynamoDBTableName : 'DeployBotSession',
    repoTable : {
        region: 'us-west-2',
        name: 'build-bot-state',
        repoName: 'myrepo'
    },

    /*
     *  States:
     *  START_MODE : Welcome state when the audio list has not begun.
     *  PLAY_MODE :  When a playlist is being played. Does not imply only active play.
     *               It remains in the state as long as the playlist is not finished.
     *  RESUME_DECISION_MODE : When a user invokes the skill in PLAY_MODE with a LaunchRequest,
     *                         the skill provides an option to resume from last position, or to start over the playlist.
     */
    states : {
        START_MODE : '',
        PLAY_MODE : '_PLAY_MODE',
        RESUME_DECISION_MODE : '_RESUME_DECISION_MODE'
    }
});
<commit_msg>remove console logs<commit_after>"use strict";

module.exports = Object.freeze({

    // App-ID. TODO: set to your own Skill App ID from the developer portal.
    appId : 'amzn1.ask.skill.c2368a37-fa99-4024-b61e-5c390c8b628b',

    //  DynamoDB Table name
    dynamoDBTableName : 'DeployBotSession',
    repoTable : {
        region: 'us-west-2',
        name: 'build-bot-state',
        repoName: 'hello-johnny5'
    },

    /*
     *  States:
     *  START_MODE : Welcome state when the audio list has not begun.
     *  PLAY_MODE :  When a playlist is being played. Does not imply only active play.
     *               It remains in the state as long as the playlist is not finished.
     *  RESUME_DECISION_MODE : When a user invokes the skill in PLAY_MODE with a LaunchRequest,
     *                         the skill provides an option to resume from last position, or to start over the playlist.
     */
    states : {
        START_MODE : '',
        PLAY_MODE : '_PLAY_MODE',
        RESUME_DECISION_MODE : '_RESUME_DECISION_MODE'
    }
});
<|endoftext|>


<commit_before>
/**
 * @namespace
 *
 * @description
 * Analytics is a module to track events sending it to a webapp callback.
 * It's used internally in Stargate to track events like MFP get.
 * Before using it you need to set the callback calling {@link Stargate#setAnalyticsCallback}
 *
 */
var analytics = (function(){

	var cb;
	var ana = {};

	/**
     * @name analytics#track
     * @memberof analytics
     *
     * @description Send an event to webapp analytics callback if it's defined
     *
     * @param {object} event
     */
	ana.track = function(event) {

		if (typeof cb !== 'function') {
			return log("[analytics] callback not set!");
		}

		// send it
		cb(event);
	};

	/**
     * @name analytics#setCallback
     * @memberof analytics
     *
     * @description Save webapp analytics callback to be called when an event is tracked
     *
     * @param {function} callback
     */
	ana.setCallback = function(callback) {
		cb = callback;
	};

	return ana;
})();

/**
 * @name Stargate#setAnalyticsCallback
 * @memberof Stargate
 *
 * @description Save webapp analytics callback to be called when an event inside Stargaed need to be tracked
 *
 * @param {function} callback
 */
stargatePublic.setAnalyticsCallback = function(callback) {

	analytics.setCallback(callback);
};
<commit_msg>try catch calling callback<commit_after>
/**
 * @namespace
 * @protected
 *
 * @description
 * Analytics is a module to track events sending it to a webapp callback.
 * It's used internally in Stargate to track events like MFP get.
 * Before using it you need to set the callback calling {@link Stargate#setAnalyticsCallback}
 *
 */
var analytics = (function(){

	var cb;
	var ana = {};

	/**
     * @name analytics#track
     * @memberof analytics
     *
     * @description Send an event to webapp analytics callback if it's defined
     *
     * @param {object} event
     */
	ana.track = function(trackedEvent) {

		if (typeof cb !== 'function') {
			return log("[analytics] callback not set!");
		}

		// send it
		try {
			cb(trackedEvent);
		}
		catch (error) {
			err("[analytics] callback error: "+error, error);
		}
	};

	/**
     * @name analytics#setCallback
     * @memberof analytics
     *
     * @description Save webapp analytics callback to be called when an event is tracked
     *
     * @param {function} callback
     */
	ana.setCallback = function(callback) {
		cb = callback;
	};

	return ana;
})();

/**
 * @name Stargate#setAnalyticsCallback
 * @memberof Stargate
 *
 * @description Save webapp analytics callback to be called when an event inside Stargaed need to be tracked
 *
 * @param {function} callback
 */
stargatePublic.setAnalyticsCallback = function(callback) {

	analytics.setCallback(callback);
};
<|endoftext|>


<commit_before>(function() {
    var Ext = window.Ext4 || window.Ext;
    var appAutoScroll = Ext.isIE7 || Ext.isIE8;
    var gridAutoScroll = !appAutoScroll;

    Ext.define('Rally.apps.grid.GridApp', {
        extend: 'Rally.app.App',
        layout: 'fit',

        requires: [
            'Rally.data.util.Sorter',
            'Rally.data.wsapi.Filter',
            'Rally.ui.grid.Grid',
            'Rally.data.ModelFactory',
            'Rally.ui.grid.plugin.PercentDonePopoverPlugin'
        ],

        config: {
            defaultSettings: {
                types: 'hierarchicalrequirement'
            }
        },

        autoScroll: appAutoScroll,

        launch: function() {
            var context = this.getContext(),
                pageSize = this.getSetting('pageSize'),
                fetch = this.getSetting('fetch'),
                columns = this._getColumns(fetch);

            this.add({
                xtype: 'rallygrid',
                columnCfgs: columns,
                enableColumnHide: false,
                enableRanking: true,
                enableBulkEdit: context.isFeatureEnabled("EXT4_GRID_BULK_EDIT"),
                autoScroll: gridAutoScroll,
                plugins: this._getPlugins(columns),
                context: this.getContext(),
                storeConfig: {
                    fetch: fetch,
                    models: this.getSetting('types').split(','),
                    filters: this._getFilters(),
                    pageSize: pageSize,
                    sorters: Rally.data.util.Sorter.sorters(this.getSetting('order')),
                    listeners: {
                        load: this._updateAppContainerSize,
                        scope: this
                    }
                },
                pagingToolbarCfg: {
                    pageSizes: [pageSize]
                }
            });
        },

        onTimeboxScopeChange: function(newTimeboxScope) {
            this.callParent(arguments);

            this.down('rallygrid').getStore().reload({
                filters: this._getFilters()
            });
        },

        _getFilters: function() {
            var filters = [],
                query = this.getSetting('query'),
                timeboxScope = this.getContext().getTimeboxScope();
            if(query) {
                try {
                    query = new Ext.Template(query).apply({
                        user: Rally.util.Ref.getRelativeUri(this.getContext().getUser())
                    });
                } catch(e) {}
                filters.push(Rally.data.wsapi.Filter.fromQueryString(query));
            }

            if(timeboxScope && _.every(this.getSetting('types').split(','), this._isSchedulableType, this)) {
                filters.push(timeboxScope.getQueryFilter());
            }
            return filters;
        },

        _isSchedulableType: function(type) {
            return _.contains(['hierarchicalrequirement', 'task', 'defect', 'defectsuite', 'testset'], type);
        },

        _getFetchOnlyFields:function(){
            return ['LatestDiscussionAgeInMinutes'];
        },

        _updateAppContainerSize: function() {
            if (this.appContainer) {
                var grid = this.down('rallygrid');
                grid.el.setHeight('auto');
                grid.body.setHeight('auto');
                grid.view.el.setHeight('auto');
                this.setSize({height: grid.getHeight() + _.reduce(grid.getDockedItems(), function(acc, item) {
                    return acc + item.getHeight() + item.el.getMargin('tb');
                }, 0)});
                this.appContainer.setPanelHeightToAppHeight();
            }
        },

        _getColumns: function(fetch){
            if (fetch) {
                return Ext.Array.difference(fetch.split(','), this._getFetchOnlyFields());
            }
            return [];
        },

        _getPlugins: function(columns) {
            var plugins = [];

            if (Ext.Array.intersect(columns, ['PercentDoneByStoryPlanEstimate','PercentDoneByStoryCount']).length > 0) {
                plugins.push('rallypercentdonepopoverplugin');
            }

            return plugins;
        }
    });
})();
<commit_msg>fixing object type lowercase issue for grids on timebox filtered dashboards<commit_after>(function() {
    var Ext = window.Ext4 || window.Ext;
    var appAutoScroll = Ext.isIE7 || Ext.isIE8;
    var gridAutoScroll = !appAutoScroll;

    Ext.define('Rally.apps.grid.GridApp', {
        extend: 'Rally.app.App',
        layout: 'fit',

        requires: [
            'Rally.data.util.Sorter',
            'Rally.data.wsapi.Filter',
            'Rally.ui.grid.Grid',
            'Rally.data.ModelFactory',
            'Rally.ui.grid.plugin.PercentDonePopoverPlugin'
        ],

        config: {
            defaultSettings: {
                types: 'hierarchicalrequirement'
            }
        },

        autoScroll: appAutoScroll,

        launch: function() {
            var context = this.getContext(),
                pageSize = this.getSetting('pageSize'),
                fetch = this.getSetting('fetch'),
                columns = this._getColumns(fetch);

            this.add({
                xtype: 'rallygrid',
                columnCfgs: columns,
                enableColumnHide: false,
                enableRanking: true,
                enableBulkEdit: context.isFeatureEnabled("EXT4_GRID_BULK_EDIT"),
                autoScroll: gridAutoScroll,
                plugins: this._getPlugins(columns),
                context: this.getContext(),
                storeConfig: {
                    fetch: fetch,
                    models: this.getSetting('types').split(','),
                    filters: this._getFilters(),
                    pageSize: pageSize,
                    sorters: Rally.data.util.Sorter.sorters(this.getSetting('order')),
                    listeners: {
                        load: this._updateAppContainerSize,
                        scope: this
                    }
                },
                pagingToolbarCfg: {
                    pageSizes: [pageSize]
                }
            });
        },

        onTimeboxScopeChange: function(newTimeboxScope) {
            this.callParent(arguments);

            this.down('rallygrid').getStore().reload({
                filters: this._getFilters()
            });
        },

        _getFilters: function() {
            var filters = [],
                query = this.getSetting('query'),
                timeboxScope = this.getContext().getTimeboxScope();
            if(query) {
                try {
                    query = new Ext.Template(query).apply({
                        user: Rally.util.Ref.getRelativeUri(this.getContext().getUser())
                    });
                } catch(e) {}
                filters.push(Rally.data.wsapi.Filter.fromQueryString(query));
            }

            if(timeboxScope && _.every(this.getSetting('types').split(','), this._isSchedulableType, this)) {
                filters.push(timeboxScope.getQueryFilter());
            }
            return filters;
        },

        _isSchedulableType: function(type) {
            return _.contains(['hierarchicalrequirement', 'task', 'defect', 'defectsuite', 'testset'], type.toLowerCase());
        },

        _getFetchOnlyFields:function(){
            return ['LatestDiscussionAgeInMinutes'];
        },

        _updateAppContainerSize: function() {
            if (this.appContainer) {
                var grid = this.down('rallygrid');
                grid.el.setHeight('auto');
                grid.body.setHeight('auto');
                grid.view.el.setHeight('auto');
                this.setSize({height: grid.getHeight() + _.reduce(grid.getDockedItems(), function(acc, item) {
                    return acc + item.getHeight() + item.el.getMargin('tb');
                }, 0)});
                this.appContainer.setPanelHeightToAppHeight();
            }
        },

        _getColumns: function(fetch){
            if (fetch) {
                return Ext.Array.difference(fetch.split(','), this._getFetchOnlyFields());
            }
            return [];
        },

        _getPlugins: function(columns) {
            var plugins = [];

            if (Ext.Array.intersect(columns, ['PercentDoneByStoryPlanEstimate','PercentDoneByStoryCount']).length > 0) {
                plugins.push('rallypercentdonepopoverplugin');
            }

            return plugins;
        }
    });
})();
<|endoftext|>