<commit_before>import React, { Component } from "react";
import MediaQuery from "react-responsive";
import moment from "moment-timezone";
import { css } from "glamor";

import EventScheduleCompressedTable from "./EventScheduleCompressedTable";
import EventScheduleFullSizeTable from "./EventScheduleFullSizeTable";
import ContentWrapper from "./ContentWrapper";
import DaySwitcher from "./DaySwitcher";

moment.tz.setDefault("America/Kingston");

// duration in minutes, startDate as ISO 8601
const schedule = [
  {
    startDate: "2020-01-31T17:00:00",
    endDate: "",
    eventName: "Buses Arrive",
    subtitle: " ",
    location: "Mitchell Hall"
    },
    {
    startDate: "2020-01-31T17:00:00",
    endDate: "2020-01-31T19:00:00",
    eventName: "Registration",
    subtitle: "",
    location: "Event Commons, Mitchell Hall"
    },
    {
    startDate: "2020-01-31T17:00:00",
    endDate: "2020-01-31T19:00:00",
    eventName: "Dinner",
    subtitle: "",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-01-31T18:30:00",
    endDate: "2020-01-31T20:30:00",
    eventName: "Opening Ceremony",
    subtitle: "",
    location: "Bartlett Gym, Mitchell Hall"
    },
    {
    startDate: "2020-01-31T21:45:00",
    endDate: "2020-01-31T22:30:00",
    eventName: "Team Formation & Setup",
    subtitle: "Find a Space to Hack!",
    location: "Rooms 215, 225, and 235, Mitchell Hall"
    },
    {
    startDate: "2020-01-31T22:00:00",
    endDate: "",
    eventName: "Hacking Begins!",
    subtitle: "",
    location: ""
    },
    {
    startDate: "2020-01-31T23:00:00",
    endDate: "2020-01-31T23:45:00",
    eventName: "Workshop | Nicole Fitzgerald - Microsoft",
    subtitle: "",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-02-01T00:00:00",
    endDate: "",
    eventName: "Hardware Lounge Opens",
    subtitle: "",
    location: "TBD"
    },
    {
    startDate: "2020-02-01T00:00:00",
    endDate: "2020-02-01T00:45:00",
    eventName: "Midnight Snack",
    subtitle: "Chips and Dips",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T02:00:00",
    endDate: "2020-02-01T02:45:00",
    eventName: "Interactive Event",
    subtitle: "MLH Cup Stacking",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T07:00:00",
    endDate: "2020-02-01T07:45:00",
    eventName: "Workshop | MMIE",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T08:00:00",
    endDate: "2020-02-01T08:45:00",
    eventName: "Workshop | Merlin",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T08:00:00",
    endDate: "2020-02-01T09:30:00",
    eventName: "Breakfast ",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T09:00:00",
    endDate: "2020-02-01T09:45:00",
    eventName: "Workshop | Scotiabank - Skills of the Trade",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T10:00:00",
    endDate: "2020-02-01T11:00:00",
    eventName: "Mini Event",
    subtitle: "Morning Stretch and Exercise",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T11:00:00",
    endDate: "2020-02-01T11:45:00",
    eventName: "Fireside Chat | Cameron Gorrie and David Hariri",
    subtitle: "",
    location: "McLaughlin Hall"
    },
    {
    startDate: "2020-02-01T12:00:00",
    endDate: "2020-02-01T13:00:00",
    eventName: "Lunch",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T15:00:00",
    endDate: "2020-02-01T15:45:00",
    eventName: "Workshop | Sunlife Financial",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T16:00:00",
    endDate: "2020-02-01T16:45:00",
    eventName: "Mini Event",
    subtitle: "MLH Slideshow Karaoke",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T17:00:00",
    endDate: "2020-02-01T17:45:00",
    eventName: "Workshop | Resume Roast",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T18:00:00",
    endDate: "2020-02-01T19:00:00",
    eventName: "Dinner",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T20:00:00",
    endDate: "2020-02-01T20:45:00",
    eventName: "Workshop | Cameron Gorrie - Top Hat",
    subtitle: "",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-02-01T22:00:00",
    endDate: "2020-02-01T23:45:00",
    eventName: "Mini Event",
    subtitle: "Candy Bar and Decorating Competition",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-02-02T00:00:00",
    endDate: "2020-02-02T01:30:00",
    eventName: "Midnight Snack",
    subtitle: "",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-02-02T00:00:00",
    endDate: "2020-02-02T00:45:00",
    eventName: "Mini Event",
    subtitle: "Blind Coding & Spicy Software ",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-02-02T08:00:00",
    endDate: "2020-02-02T10:00:00",
    eventName: "Breakfast ",
    subtitle: "",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-02-02T09:00:00",
    endDate: "",
    eventName: "Hacking Ends!",
    subtitle: "",
    location: ""
    },
    {
    startDate: "2020-02-02T09:30:00",
    endDate: "2020-02-02T11:30:00",
    eventName: "Demo Time & Judging",
    subtitle: "",
    location: "Engineering Active Learning Rooms"
    },
    {
    startDate: "2020-02-02T11:00:00",
    endDate: "2020-02-02T12:00:00",
    eventName: "Lunch ",
    subtitle: "",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-02-02T12:30:00",
    endDate: "2020-02-02T14:30:00",
    eventName: "Closing Ceremony",
    subtitle: "",
    location: "Grant Hall "
    },
    {
    startDate: "2020-02-02T15:00:00",
    endDate: "",
    eventName: "Buses Leave",
    subtitle: "",
    location: "Mitchell Hall"
    },
];

const eventDays = [];

for (const event of schedule) {
  const start = moment(event.startDate).format("YYYY-MM-DD");
  if (!eventDays.includes(start)) {
    eventDays.push(start);
  }
}

eventDays.sort((a, b) => {
  return (
    moment(a, "YYYY-MM-DD").format("X") - moment(b, "YYYY-MM-DD").format("X")
  );
});

class EventSchedule extends Component {
  state = {
    selectedDay: eventDays[0]
  };

  addDurations(events) {
    return events.map((item) => {
      if (!item.endDate) {
        return item;
      }
      let duration = moment(item.endDate).diff(item.startDate, "minutes", true);
      if (duration > 59) {
        duration = Math.round((duration / 60) * 100) / 100;
        item.duration = `${duration} hr`;
      } else {
        item.duration = `${duration} min`;
      }
      return duration !== 1 ? { ...item, duration: `${item.duration}s` } : item;
    });
  }

  changeDay(selectedDay) {
    this.setState({ selectedDay });
  }

  render() {
    const eventsOnSelectedDay = schedule.filter(
      (item) =>
        (moment(item.startDate).format("YYYY-MM-DD") ===
          this.state.selectedDay &&
          Number(moment(item.startDate).format("H")) > 5) ||
        (moment(item.startDate)
          .subtract("days", 1)
          .format("YYYY-MM-DD") === this.state.selectedDay &&
          Number(moment(item.startDate).format("H")) <= 5)
    );

    const data = this.addDurations(eventsOnSelectedDay);

    return (
      <ContentWrapper>

            Event Schedule*

            Download as Calendar

          <DaySwitcher
            days={eventDays}
            changeDay={(day) => this.changeDay(day)}
          />

            {moment(this.state.selectedDay, "YYYY-MM-DD").format(
              "dddd, MMMM Do, YYYY"
            )}

          <MediaQuery query="screen and (min-width: 760px)">
            <EventScheduleFullSizeTable data={data} />
          </MediaQuery>
          <MediaQuery query="screen and (max-width: 760px)">
            <EventScheduleCompressedTable data={data} />
          </MediaQuery>

            *Subject to Change

      </ContentWrapper>
    );
  }
}

export default EventSchedule;
<commit_msg>updated schedule and ics file<commit_after>import React, { Component } from "react";
import MediaQuery from "react-responsive";
import moment from "moment-timezone";
import { css } from "glamor";

import EventScheduleCompressedTable from "./EventScheduleCompressedTable";
import EventScheduleFullSizeTable from "./EventScheduleFullSizeTable";
import ContentWrapper from "./ContentWrapper";
import DaySwitcher from "./DaySwitcher";

moment.tz.setDefault("America/Kingston");

// duration in minutes, startDate as ISO 8601
const schedule = [
  {
    startDate: "2020-01-31T17:00:00",
    endDate: "",
    eventName: "Buses Arrive",
    subtitle: " ",
    location: "Mitchell Hall"
    },
    {
    startDate: "2020-01-31T17:00:00",
    endDate: "2020-01-31T19:00:00",
    eventName: "Registration",
    subtitle: "",
    location: "Event Commons, Mitchell Hall"
    },
    {
    startDate: "2020-01-31T17:00:00",
    endDate: "2020-01-31T19:00:00",
    eventName: "Dinner",
    subtitle: "",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-01-31T19:30:00",
    endDate: "2020-01-31T21:30:00",
    eventName: "Opening Ceremony",
    subtitle: "",
    location: "Bartlett Gym, Mitchell Hall"
    },
    {
    startDate: "2020-01-31T21:45:00",
    endDate: "2020-01-31T22:30:00",
    eventName: "Team Formation & Setup",
    subtitle: "Find a Space to Hack!",
    location: "Rooms 215, 225, and 235, Mitchell Hall"
    },
    {
    startDate: "2020-01-31T22:00:00",
    endDate: "",
    eventName: "Hacking Begins!",
    subtitle: "",
    location: ""
    },
    {
    startDate: "2020-01-31T23:00:00",
    endDate: "2020-01-31T23:45:00",
    eventName: "Workshop | Nicole Fitzgerald - Microsoft",
    subtitle: "",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-02-01T00:00:00",
    endDate: "",
    eventName: "Hardware Lounge Opens",
    subtitle: "",
    location: "TBD"
    },
    {
    startDate: "2020-02-01T00:00:00",
    endDate: "2020-02-01T00:45:00",
    eventName: "Midnight Snack",
    subtitle: "Chips and Dips",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T02:00:00",
    endDate: "2020-02-01T02:45:00",
    eventName: "Interactive Event",
    subtitle: "MLH Cup Stacking",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T07:00:00",
    endDate: "2020-02-01T07:45:00",
    eventName: "Workshop | MMIE",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T08:00:00",
    endDate: "2020-02-01T08:45:00",
    eventName: "Workshop | Merlin",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T08:00:00",
    endDate: "2020-02-01T09:30:00",
    eventName: "Breakfast ",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T09:00:00",
    endDate: "2020-02-01T09:45:00",
    eventName: "Workshop | Scotiabank - Skills of the Trade",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T10:00:00",
    endDate: "2020-02-01T11:00:00",
    eventName: "Mini Event",
    subtitle: "Morning Stretch and Exercise",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T11:00:00",
    endDate: "2020-02-01T11:45:00",
    eventName: "Fireside Chat | Cameron Gorrie and David Hariri",
    subtitle: "",
    location: "McLaughlin Hall"
    },
    {
    startDate: "2020-02-01T12:00:00",
    endDate: "2020-02-01T13:00:00",
    eventName: "Lunch",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T15:00:00",
    endDate: "2020-02-01T15:45:00",
    eventName: "Workshop | Sunlife Financial",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T16:00:00",
    endDate: "2020-02-01T16:45:00",
    eventName: "Mini Event",
    subtitle: "MLH Slideshow Karaoke",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T17:00:00",
    endDate: "2020-02-01T17:45:00",
    eventName: "Workshop | Resume Roast",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T18:00:00",
    endDate: "2020-02-01T19:00:00",
    eventName: "Dinner",
    subtitle: "",
    location: "John Orr Room, JDUC"
    },
    {
    startDate: "2020-02-01T20:00:00",
    endDate: "2020-02-01T20:45:00",
    eventName: "Workshop | Cameron Gorrie - Top Hat",
    subtitle: "",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-02-01T22:00:00",
    endDate: "2020-02-01T23:45:00",
    eventName: "Mini Event",
    subtitle: "Candy Bar and Decorating Competition",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-02-02T00:00:00",
    endDate: "2020-02-02T01:30:00",
    eventName: "Midnight Snack",
    subtitle: "",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-02-02T00:00:00",
    endDate: "2020-02-02T00:45:00",
    eventName: "Mini Event",
    subtitle: "Blind Coding & Spicy Software ",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-02-02T08:00:00",
    endDate: "2020-02-02T10:00:00",
    eventName: "Breakfast ",
    subtitle: "",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-02-02T09:00:00",
    endDate: "",
    eventName: "Hacking Ends!",
    subtitle: "",
    location: ""
    },
    {
    startDate: "2020-02-02T09:30:00",
    endDate: "2020-02-02T11:30:00",
    eventName: "Demo Time & Judging",
    subtitle: "",
    location: "Engineering Active Learning Rooms"
    },
    {
    startDate: "2020-02-02T11:00:00",
    endDate: "2020-02-02T12:00:00",
    eventName: "Lunch ",
    subtitle: "",
    location: "Wallace Hall, JDUC"
    },
    {
    startDate: "2020-02-02T12:30:00",
    endDate: "2020-02-02T14:30:00",
    eventName: "Closing Ceremony",
    subtitle: "",
    location: "Grant Hall "
    },
    {
    startDate: "2020-02-02T15:00:00",
    endDate: "",
    eventName: "Buses Leave",
    subtitle: "",
    location: "Mitchell Hall"
    },
];

const eventDays = [];

for (const event of schedule) {
  const start = moment(event.startDate).format("YYYY-MM-DD");
  if (!eventDays.includes(start)) {
    eventDays.push(start);
  }
}

eventDays.sort((a, b) => {
  return (
    moment(a, "YYYY-MM-DD").format("X") - moment(b, "YYYY-MM-DD").format("X")
  );
});

class EventSchedule extends Component {
  state = {
    selectedDay: eventDays[0]
  };

  addDurations(events) {
    return events.map((item) => {
      if (!item.endDate) {
        return item;
      }
      let duration = moment(item.endDate).diff(item.startDate, "minutes", true);
      if (duration > 59) {
        duration = Math.round((duration / 60) * 100) / 100;
        item.duration = `${duration} hr`;
      } else {
        item.duration = `${duration} min`;
      }
      return duration !== 1 ? { ...item, duration: `${item.duration}s` } : item;
    });
  }

  changeDay(selectedDay) {
    this.setState({ selectedDay });
  }

  render() {
    const eventsOnSelectedDay = schedule.filter(
      (item) =>
        (moment(item.startDate).format("YYYY-MM-DD") ===
          this.state.selectedDay &&
          Number(moment(item.startDate).format("H")) > 5) ||
        (moment(item.startDate)
          .subtract("days", 1)
          .format("YYYY-MM-DD") === this.state.selectedDay &&
          Number(moment(item.startDate).format("H")) <= 5)
    );

    const data = this.addDurations(eventsOnSelectedDay);

    return (
      <ContentWrapper>

            Event Schedule*

            Download as Calendar

          <DaySwitcher
            days={eventDays}
            changeDay={(day) => this.changeDay(day)}
          />

            {moment(this.state.selectedDay, "YYYY-MM-DD").format(
              "dddd, MMMM Do, YYYY"
            )}

          <MediaQuery query="screen and (min-width: 760px)">
            <EventScheduleFullSizeTable data={data} />
          </MediaQuery>
          <MediaQuery query="screen and (max-width: 760px)">
            <EventScheduleCompressedTable data={data} />
          </MediaQuery>

            *Subject to Change

      </ContentWrapper>
    );
  }
}

export default EventSchedule;
<|endoftext|>


<commit_before>/*jshint regexp:false */

/**
 * StateRegistry factory.
 * This factory might return null if the routing is disabled (by the config or if the address is not compatible with the browser).
 * This class provides access to the state registry as a service.
 */
define([
    'spoon/core/StateRegistry/StateRegistry',
    'services/address',
    'app-config',
    'amd-utils/lang/isString',
    'amd-utils/lang/isObject',
    'amd-utils/object/mixIn',
    'amd-utils/object/size',
    'has'
], function (StateRegistry, address, config, isString, isObject, mixIn, size, has) {

    'use strict';

    config = config || {};
    config = config.state || {};

    var registry = new StateRegistry(),
        states = config.states || [],
        curr,
        key,
        obj,
        pattern,
        fullPattern,
        constraints,
        priority,
        value,
        isLeaf,
        slashRegExp = /\/+$/g,
        paramsRegExp = /\(.+?\)/g,
        x,
        length,
        queue = [],
        arr = [];

    /**
     * Standardizes a pattern.
     * It ensures that it starts with a / and does not end with a /.
     *
     * @param {String} pattern The pattern to standardize
     *
     * @return {String} The standardized pattern
     */
    function standardizePattern(pattern) {
        if (pattern === '/') {
            return pattern;
        }

        if (pattern.charAt(0) !== '/') {
            pattern = '/' + pattern;
        }

        return pattern.replace(slashRegExp, '');
    }

    // Process the states and add them to the registry
    // The code bellow uses a stack (deep first) to avoid recursion
    queue.push({ obj: states });

    while (queue.length) {
        curr = queue.shift();
        obj = curr.obj;
        pattern = standardizePattern((curr.pattern ? curr.pattern : '') + (obj.$pattern || obj.key || ''));
        fullPattern = obj.$fullPattern ? standardizePattern(obj.$fullPattern) : null;
        constraints = mixIn(curr.constraints || {}, obj.$constraints);
        priority = obj.$priority || 0;
        delete obj.$pattern;
        delete obj.$constraints;
        delete obj.$fullPattern;
        delete obj.$priority;

        if (!size(constraints)) {
            constraints = null;
        }

        isLeaf = true;
        for (key in obj) {
            value = obj[key];
            key = key.replace(paramsRegExp, '');    // Remove the parentheses if any
            isLeaf = false;

            // Boolean falsy -> state has no route
            if (!value) {
                // We can add it already because the priority only apply to states with routes
                registry.register(curr.state ? curr.state + '.' + key : key);
            // String -> state has a route
            } else if (isString(obj[key])) {
                // Add to the array to be sorted later
                arr.push({
                    state: curr.state ? curr.state + '.' + key : key,
                    pattern:  standardizePattern(pattern + standardizePattern(value)),
                    constraints: constraints,
                    priority: priority
                });
            // Object -> add to the processing queue
            } else if (isObject(obj[key])) {
                queue.unshift({
                    obj: value,
                    state: curr.state ? curr.state + '.' + key : key,
                    key: key,
                    pattern: pattern,
                    constraints: constraints
                });
            } else if (has('debug')) {
                throw new Error('Unexpected "' + key + '" while parsing states.');
            }
        }

        if (curr.state) {
            arr.push({
                state: curr.state,
                pattern: fullPattern || pattern,
                constraints: constraints,
                priority: priority
            });
        }
    }

    // Sort the array according to the priority
    arr.sort(function (val1, val2) {
        if (val1.priority === val2.priority) {
            return 0;
        }

        if (val1.priority > val2.priority) {
            return -1;
        }

        return 1;
    });

    // Add the sorted array to the registry
    length = arr.length;
    for (x = 0; x < length; x += 1) {
        curr = arr[x];
        registry.register(curr.state, curr.pattern, curr.constraints);
    }

    // Inject the address if the routing is enabled
    if (!!config.routing) {
        registry.setAddress(address);
    }

    return registry;
});<commit_msg>Fix patterns not being correctly extracted.<commit_after>/*jshint regexp:false */

/**
 * StateRegistry factory.
 * This factory might return null if the routing is disabled (by the config or if the address is not compatible with the browser).
 * This class provides access to the state registry as a service.
 */
define([
    'spoon/core/StateRegistry/StateRegistry',
    'services/address',
    'app-config',
    'amd-utils/lang/isString',
    'amd-utils/lang/isObject',
    'amd-utils/object/mixIn',
    'amd-utils/object/size',
    'has'
], function (StateRegistry, address, config, isString, isObject, mixIn, size, has) {

    'use strict';

    config = config || {};
    config = config.state || {};

    var registry = new StateRegistry(),
        states = config.states || [],
        curr,
        key,
        obj,
        pattern,
        fullPattern,
        constraints,
        priority,
        value,
        isLeaf,
        trimSlashRegExp = /\/+$/g,
        cleanSlashRegExp = /\/\/+/g,
        paramsRegExp = /\(.+?\)/g,
        x,
        length,
        queue = [],
        arr = [];

    /**
     * Standardizes a pattern.
     * It ensures that it starts with a / and does not end with a /.
     *
     * @param {String} pattern The pattern to standardize
     *
     * @return {String} The standardized pattern
     */
    function patternJoin(pattern1, pattern2) {
        var joined;

        pattern1 = pattern1.replace(trimSlashRegExp, '');
        pattern2 = pattern2.replace(trimSlashRegExp, '');

        joined = pattern1 + '/' + pattern2;

        if (joined.charAt(0) !== '/') {
            joined = '/' + joined;
        }

        return joined.replace(cleanSlashRegExp, '/')
                     .replace(trimSlashRegExp, '');
    }

    // Process the states and add them to the registry
    // The code bellow uses a stack (deep first) to avoid recursion
    queue.push({ obj: states });

    while (queue.length) {
        curr = queue.shift();
        obj = curr.obj;
        pattern = patternJoin(curr.pattern ? curr.pattern : '', obj.$pattern || curr.key || '');
        fullPattern = obj.$fullPattern;
        constraints = mixIn(curr.constraints || {}, obj.$constraints);
        priority = obj.$priority || 0;
        delete obj.$pattern;
        delete obj.$constraints;
        delete obj.$fullPattern;
        delete obj.$priority;

        if (!size(constraints)) {
            constraints = null;
        }

        isLeaf = true;
        for (key in obj) {
            value = obj[key];
            key = key.replace(paramsRegExp, '');    // Remove the parentheses if any
            isLeaf = false;

            // Boolean falsy -> state has no route
            if (!value) {
                // We can add it already because the priority only apply to states with routes
                registry.register(curr.state ? curr.state + '.' + key : key);
            // String -> state has a route
            } else if (isString(obj[key])) {
                // Add to the array to be sorted later
                arr.push({
                    state: curr.state ? curr.state + '.' + key : key,
                    pattern: patternJoin(pattern, value),
                    constraints: constraints,
                    priority: priority
                });
            // Object -> add to the processing queue
            } else if (isObject(obj[key])) {
                queue.unshift({
                    obj: value,
                    state: curr.state ? curr.state + '.' + key : key,
                    key: key,
                    pattern: pattern,
                    constraints: constraints
                });
            } else if (has('debug')) {
                throw new Error('Unexpected "' + key + '" while parsing states.');
            }
        }

        if (curr.state) {
            arr.push({
                state: curr.state,
                pattern: fullPattern || pattern,
                constraints: constraints,
                priority: priority
            });
        }
    }

    // Sort the array according to the priority
    arr.sort(function (val1, val2) {
        if (val1.priority === val2.priority) {
            return 0;
        }

        if (val1.priority > val2.priority) {
            return -1;
        }

        return 1;
    });

    // Add the sorted array to the registry
    length = arr.length;
    for (x = 0; x < length; x += 1) {
        curr = arr[x];
        registry.register(curr.state, curr.pattern, curr.constraints);
    }

    // Inject the address if the routing is enabled
    if (!!config.routing) {
        registry.setAddress(address);
    }

    return registry;
});<|endoftext|>


<commit_before>"use strict";

Cu.import("resource://gre/modules/PlacesUIUtils.jsm");

let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]
      .getService(Ci.nsINavBookmarksService);
let fs = Cc["@mozilla.org/browser/favicon-service;1"]
      .getService(Ci.nsIFaviconService);
let hs = Cc["@mozilla.org/browser/nav-history-service;1"]
      .getService(Ci.nsINavHistoryService);
let ts = Cc["@mozilla.org/browser/tagging-service;1"]
      .getService(Ci.nsITaggingService);
let as = Cc["@mozilla.org/browser/annotation-service;1"]
      .getService(Ci.nsIAnnotationService);
let ios = Services.io;

let bm = {
  DOMAIN_REGEX: /:\/\/([^/]*)/,
  KEEP_TITLE_ANN: "bmreplace/keep-title",
  WRONG_TITLE: 10,

  /**
   * Checks if there is a bookmark with given URL and title.
   * @param {String} url
   * @param {String} title
   * @return true if both match, WRONG_TITLE if the title doesn't match.
   */
  isBookmarked: function(url, title) {
    let id = this.firstBookmarkFor(url);
    if (id) {
      if (title == bms.getItemTitle(id)) {
        return true;
      } else {
        return this.WRONG_TITLE;
      }
    }
    return false;
  },

  /**
   * Finds bookmarks related to the given url, sorts them by
   * the length of the longest common prefix in the path or title similarity,
   * depending on whether the domain is special.
   * @return [{title, uri, id, weight}, ...].
   */
  getRelatedBookmarks: function(url, title) {
    let domain = this.DOMAIN_REGEX.exec(url)[1],
        altDomain = /^www\./.test(domain) ? domain.slice(4) : "www." + domain,
        isSpecial = this.isDomainSpecial(domain);
    let lst = this.getBookmarksOn(domain).concat(this.getBookmarksOn(altDomain));
    lst.forEach(function(item) {
      if (isSpecial) {
        item.weight = bm.matchWeight(title, item.title);
      } else {
        item.weight = bm.matchWeight(url, item.uri, true);
      }
    });
    lst.sort(function(a, b) b.weight - a.weight); // better matches first
    return lst;
  },

  getBookmarksOn: function(domain) {
    let keepTitleDefault = getPref(PREF_KEEP_TITLE),
        query = hs.getNewQuery();
    query.domain = domain;
    query.domainIsHost = true;
    query.onlyBookmarked = true;
    let options = hs.getNewQueryOptions();
    options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
    let root = hs.executeQuery(query, options).root;
    root.containerOpen = true;
    let lst = [];
    for (var i = 0, len = root.childCount; i < len; ++i) {
      let child = root.getChild(i),
          id = child.itemId;
      lst.push({
        title: child.title,
        uri: child.uri,
        id: id,
        checked: bm.shouldKeepTitle(id, keepTitleDefault)
      });
    };
    root.containerOpen = false;
    return lst;
  },

  isDomainSpecial: function(domain) {
    return getPref(PREF_SPECIAL_DOMAINS).split(/, */).some(function(d) {
      return new RegExp("(^|\\.)" + d.replace(/\./, "\\."), "i").test(domain);
    });
  },

  matchWeight: function(u, v, comparePaths) {
    if (comparePaths) {
      u = this.urlPath(u);
      v = this.urlPath(v);
    }
    let max = Math.min(u.length, v.length);
    for (var i = 0; i < max && u[i] == v[i]; ++i) {}

    if (i == 0 && !comparePaths) {
      i = this.longestCommonSubstring(u, v) / 3;
    }

    return i;
  },

  urlPath: function(url) {
    let match = this.DOMAIN_REGEX.exec(url);
    return url.slice(match.index + match[0].length);
  },

  /**
   * http://en.wikibooks.org/wiki/Algorithm_implementation
   *   /Strings/Longest_common_substring#JavaScript
   */
  longestCommonSubstring: function(string1, string2) {
    // init max value
    var longestCommonSubstring = 0;
    // init 2D array with 0
    var table = Array(string1.length);
    for (let a = 0; a <= string1.length; a++) {
      table[a] = Array(string2.length);
      for (let b = 0; b <= string2.length; b++) {
        table[a][b] = 0;
      }
    }
    // fill table
    for (let i = 0; i < string1.length; i++) {
      for (let j = 0; j < string2.length; j++) {
        if (string1[i] == string2[j]) {
          if (table[i][j] == 0) {
            table[i+1][j+1] = 1;
          } else {
            table[i+1][j+1] = table[i][j] + 1;
          }
          if (table[i+1][j+1] > longestCommonSubstring) {
            longestCommonSubstring = table[i+1][j+1];
          }
        } else {
          table[i+1][j+1] = 0;
        }
      }
    }
    return longestCommonSubstring;
  },

  /**
   * Replaces bookmark's title and URL with new ones.
   * Retains the folder, bookmark's position in it, and
   * moves the tags from the old URI to the new one.
   * @param id Bookmark ID.
   * @param url URL string.
   * @param title New title. Optional.
   * @param description New description. Optional.
   */
  replaceBookmark: function(id, url, title, description) {
    let oldUri = bms.getBookmarkURI(id),
        tags = ts.getTagsForURI(oldUri, {}),
        uri = ios.newURI(url, null, null);
    ts.tagURI(uri, tags);
    ts.untagURI(oldUri, tags);
    bms.changeBookmarkURI(id, uri);
    if (title) {
      bms.setItemTitle(id, title);
    }
    if (description) {
      this.setDescription(id, description);
    }
    try {
      let favUri = fs.getFaviconForPage(oldUri);
      fs.setAndLoadFaviconForPage(uri, favUri, false);
    } catch (e) {/*NS_ERROR_NOT_AVAILABLE*/}
  },

  firstBookmarkFor: function(url) {
    let uri = ios.newURI(url, null, null),
        ids = bms.getBookmarkIdsForURI(uri);
    return ids && ids[0];
  },

  setTitle: function(url, title) {
    bms.setItemTitle(this.firstBookmarkFor(url), title);
  },

  /*
   * Shows Places "Add Bookmark" dialog.
   */
  showAddBookmark: function(url, title, parentWindow) {
    PlacesUIUtils.showBookmarkDialog({
      uri: ios.newURI(url, null, null),
      title: title,
      action: "add",
      type: "bookmark",
      hiddenRows: ["description", "location", "keyword", "loadInSidebar"]
    }, parentWindow);
  },

  /*
   * Checks if we should keep the old bookmark title when replacing.
   */
  shouldKeepTitle: function(id, defaultValue) {
    try {
      return as.getItemAnnotation(id, bm.KEEP_TITLE_ANN);
    } catch(e) {
      return defaultValue;
    }
  },

  setKeepTitle: function(id, value) {
    as.setItemAnnotation(id, bm.KEEP_TITLE_ANN, value,
                         0, Ci.nsIAnnotationService.EXPIRE_NEVER);
  },

  setDescription: function(id, value) {
    as.setItemAnnotation(id, PlacesUIUtils.DESCRIPTION_ANNO, value,
                         0, Ci.nsIAnnotationService.EXPIRE_NEVER);
  }
};
<commit_msg>Appease the wonky test runner<commit_after>"use strict";

Cu.import("resource://gre/modules/PlacesUIUtils.jsm");

let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]
      .getService(Ci.nsINavBookmarksService);
let fs = Cc["@mozilla.org/browser/favicon-service;1"]
      .getService(Ci.nsIFaviconService);
let hs = Cc["@mozilla.org/browser/nav-history-service;1"]
      .getService(Ci.nsINavHistoryService);
let ts = Cc["@mozilla.org/browser/tagging-service;1"]
      .getService(Ci.nsITaggingService);
let as = Cc["@mozilla.org/browser/annotation-service;1"]
      .getService(Ci.nsIAnnotationService);
let ios = Services.io;

let bm = {
  DOMAIN_REGEX: /:\/\/([^/]*)/,
  KEEP_TITLE_ANN: "bmreplace/keep-title",
  WRONG_TITLE: 10,

  /**
   * Checks if there is a bookmark with given URL and title.
   * @param {String} url
   * @param {String} title
   * @return true if both match, WRONG_TITLE if the title doesn't match.
   */
  isBookmarked: function(url, title) {
    let id = this.firstBookmarkFor(url);
    if (id) {
      if (title == bms.getItemTitle(id)) {
        return true;
      } else {
        return this.WRONG_TITLE;
      }
    }
    return false;
  },

  /**
   * Finds bookmarks related to the given url, sorts them by
   * the length of the longest common prefix in the path or title similarity,
   * depending on whether the domain is special.
   * @return [{title, uri, id, weight}, ...].
   */
  getRelatedBookmarks: function(url, title) {
    let domain = this.DOMAIN_REGEX.exec(url)[1],
        altDomain = /^www\./.test(domain) ? domain.slice(4) : "www." + domain,
        isSpecial = this.isDomainSpecial(domain);
    let lst = this.getBookmarksOn(domain).concat(this.getBookmarksOn(altDomain));
    lst.forEach(function(item) {
      if (isSpecial) {
        item.weight = bm.matchWeight(title, item.title);
      } else {
        item.weight = bm.matchWeight(url, item.uri, true);
      }
    });
    lst.sort(function(a, b) b.weight - a.weight); // better matches first
    return lst;
  },

  getBookmarksOn: function(domain) {
    let keepTitleDefault = getPref(PREF_KEEP_TITLE),
        query = hs.getNewQuery();
    query.domain = domain;
    query.domainIsHost = true;
    query.onlyBookmarked = true;
    let options = hs.getNewQueryOptions();
    options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
    let root = hs.executeQuery(query, options).root;
    root.containerOpen = true;
    let lst = [];
    for (var i = 0, len = root.childCount; i < len; ++i) {
      let child = root.getChild(i),
          id = child.itemId;
      lst.push({
        title: child.title,
        uri: child.uri,
        id: id,
        checked: bm.shouldKeepTitle(id, keepTitleDefault)
      });
    };
    root.containerOpen = false;
    return lst;
  },

  isDomainSpecial: function(domain) {
    return getPref(PREF_SPECIAL_DOMAINS).split(/, */).some(function(d) {
      return new RegExp("(^|\\.)" + d.replace(/\./, "\\."), "i").test(domain);
    });
  },

  matchWeight: function(u, v, comparePaths) {
    if (comparePaths) {
      u = this.urlPath(u);
      v = this.urlPath(v);
    }
    let max = Math.min(u.length, v.length);
    for (var i = 0; i < max && u[i] == v[i]; ++i) {}

    if (i == 0 && !comparePaths) {
      i = this.longestCommonSubstring(u, v) / 3;
    }

    return i;
  },

  urlPath: function(url) {
    let match = this.DOMAIN_REGEX.exec(url);
    return url.slice(match.index + match[0].length);
  },

  /**
   * http://en.wikibooks.org/wiki/Algorithm_implementation
   *   /Strings/Longest_common_substring#JavaScript
   */
  longestCommonSubstring: function(string1, string2) {
    var longestCommonSubstring = 0;
    /* init 2D array with 0 */
    var table = Array(string1.length);
    for (let a = 0; a <= string1.length; a++) {
      table[a] = Array(string2.length);
      for (let b = 0; b <= string2.length; b++) {
        table[a][b] = 0;
      }
    }
    /* fill table */
    for (let i = 0; i < string1.length; i++) {
      for (let j = 0; j < string2.length; j++) {
        if (string1[i] == string2[j]) {
          if (table[i][j] == 0) {
            table[i+1][j+1] = 1;
          } else {
            table[i+1][j+1] = table[i][j] + 1;
          }
          if (table[i+1][j+1] > longestCommonSubstring) {
            longestCommonSubstring = table[i+1][j+1];
          }
        } else {
          table[i+1][j+1] = 0;
        }
      }
    }
    return longestCommonSubstring;
  },

  /**
   * Replaces bookmark's title and URL with new ones.
   * Retains the folder, bookmark's position in it, and
   * moves the tags from the old URI to the new one.
   * @param id Bookmark ID.
   * @param url URL string.
   * @param title New title. Optional.
   * @param description New description. Optional.
   */
  replaceBookmark: function(id, url, title, description) {
    let oldUri = bms.getBookmarkURI(id),
        tags = ts.getTagsForURI(oldUri, {}),
        uri = ios.newURI(url, null, null);
    ts.tagURI(uri, tags);
    ts.untagURI(oldUri, tags);
    bms.changeBookmarkURI(id, uri);
    if (title) {
      bms.setItemTitle(id, title);
    }
    if (description) {
      this.setDescription(id, description);
    }
    try {
      let favUri = fs.getFaviconForPage(oldUri);
      fs.setAndLoadFaviconForPage(uri, favUri, false);
    } catch (e) {/*NS_ERROR_NOT_AVAILABLE*/}
  },

  firstBookmarkFor: function(url) {
    let uri = ios.newURI(url, null, null),
        ids = bms.getBookmarkIdsForURI(uri);
    return ids && ids[0];
  },

  setTitle: function(url, title) {
    bms.setItemTitle(this.firstBookmarkFor(url), title);
  },

  /*
   * Shows Places "Add Bookmark" dialog.
   */
  showAddBookmark: function(url, title, parentWindow) {
    PlacesUIUtils.showBookmarkDialog({
      uri: ios.newURI(url, null, null),
      title: title,
      action: "add",
      type: "bookmark",
      hiddenRows: ["description", "location", "keyword", "loadInSidebar"]
    }, parentWindow);
  },

  /*
   * Checks if we should keep the old bookmark title when replacing.
   */
  shouldKeepTitle: function(id, defaultValue) {
    try {
      return as.getItemAnnotation(id, bm.KEEP_TITLE_ANN);
    } catch(e) {
      return defaultValue;
    }
  },

  setKeepTitle: function(id, value) {
    as.setItemAnnotation(id, bm.KEEP_TITLE_ANN, value,
                         0, Ci.nsIAnnotationService.EXPIRE_NEVER);
  },

  setDescription: function(id, value) {
    as.setItemAnnotation(id, PlacesUIUtils.DESCRIPTION_ANNO, value,
                         0, Ci.nsIAnnotationService.EXPIRE_NEVER);
  }
};
<|endoftext|>


<commit_before>var URL = require('url'),
    queryString = require('querystring'),
    _ = require('underscore'),
    seq = require('seq'),
    passError = require('passerror'),
    packers = require('./packers'),
    Canvas;

try {
    Canvas = require('canvas');
} catch (e) {}

function getCanvasImageFromImageAsset(imageAsset, cb) {
    var canvasImage = new Canvas.Image();
    canvasImage.onerror = function (err) {
        process.nextTick(function () {
            err.message += ' (' + imageAsset.urlOrDescription + ')';
            cb(err);
        });
    };
    canvasImage.onload = function () {
        process.nextTick(function () {
            cb(null, canvasImage);
        });
    };
    canvasImage.src = imageAsset.rawSrc;
}

function getImageAssetFromCanvas(canvas, assetType, assetGraph, cb) {
    if (assetType === 'Png') {
        canvas.toBuffer(passError(cb, function (pngBuffer) {
            cb(null, new assetGraph.Png({
                rawSrc: pngBuffer
            }));
        }));
    } else {
        var jpegChunks = [];
        canvas.createJPEGStream().on('data', function (chunk) {
            jpegChunks.push(chunk);
        }).on('end', function () {
            cb(null, new assetGraph.Jpeg({
                rawSrc: Buffer.concat(jpegChunks)
            }));
        }).on('error', cb);
    }
}

function calculateSpritePadding(paddingStr, asset) {
    var padding;
    if (paddingStr) {
        // Strip units ('px' assumed)
        var tokens = [];
        paddingStr.split(/[,+]|\s+/).forEach(function (token) {
            var num = parseInt(token.replace(/[a-z]+$/, ''), 10);
            if (!isNaN(num)) {
                tokens.push(num);
            }
        });
        if (tokens.length === 4) {
            padding = tokens;
        } else if (tokens.length === 3) {
            padding = [tokens[0], tokens[1], tokens[2], tokens[1]]; // T, L+R, B
        } else if (tokens.length === 2) {
            padding = [tokens[0], tokens[1], tokens[0], tokens[1]]; // T+B, L+R
        } else if (tokens.length === 1) {
            padding = [tokens[0], tokens[0], tokens[0], tokens[0]];
        }
    } else {
        padding = [0, 0, 0, 0];
    }

    return padding.map(function (size) {
        return Math.max(size, Math.max(asset.devicePixelRatio) - 1);
    });
}

function getRelationSpriteInfoFromIncomingRelation(incomingRelation) {
    var parsedQueryString = queryString.parse(incomingRelation.href.match(/\?([^#]*)/)[1]);
    return {
        groupName: parsedQueryString.sprite || 'default',
        noGroup: 'spriteNoGroup' in parsedQueryString,
        padding: calculateSpritePadding(parsedQueryString.padding, incomingRelation.to),
        asset: incomingRelation.to
    };
}

function extractInfoFromCssRule(cssRule, propertyNamePrefix) {
    var info = {};
    for (var i = 0 ; i < cssRule.style.length ; i += 1) {
        var propertyName = cssRule.style[i];
        if (!propertyNamePrefix || propertyName.indexOf(propertyNamePrefix) === 0) {
            var keyName = propertyName.substr(propertyNamePrefix.length).replace(/-([a-z])/g, function ($0, $1) {
                return $1.toUpperCase();
            });
            info[keyName] = cssRule.style[propertyName].replace(/^([\'\"])(.*)\1$/, '$2');
        }
    }
    return info;
}

module.exports = function () {
    return function spriteBackgroundImages(assetGraph, cb) {
        if (!Canvas) {
            assetGraph.emit('warn', new Error('assetgraph-sprite: Canvas not found, skipping'));
            return cb();
        }

        // Waiting for https://github.com/LearnBoost/node-canvas/issues/52
        var cairoVersion = Canvas.cairoVersion.split('.').map(function (str) {
            return parseInt(str, 10);
        });
        if (cairoVersion[0] < 1 || cairoVersion[1] < 10) {
            assetGraph.emit('warn', new Error('assetgraph-sprite: Cannot create sprites due to missing canvas.getContext("2d").drawImage() support. Please compile node-canvas with Cairo version 1.10.0 or above.'));
            return cb();
        }

        var spriteGroups = {};

        // Find sprite annotated images and create a data structure with their information
        assetGraph.findRelations({type: 'CssImage', to: {isImage: true}, href: /\?(?:|[^#]*&)sprite(?:[=&#]|$)/}).forEach(function (relation) {
            var relationSpriteInfo = getRelationSpriteInfoFromIncomingRelation(relation),
                spriteGroup = (spriteGroups[relationSpriteInfo.groupName] = spriteGroups[relationSpriteInfo.groupName] || {
                    imageInfosById: {}
                }),
                imageInfo = spriteGroup.imageInfosById[relationSpriteInfo.asset.id];
            if (!imageInfo) {
                relationSpriteInfo.incomingRelations = [relation];
                spriteGroup.imageInfosById[relationSpriteInfo.asset.id] = relationSpriteInfo;
            } else {
                imageInfo.incomingRelations.push(relation);
                for (var i = 0 ; i < 4 ; i += 1) {
                    imageInfo.padding[i] = Math.max(relationSpriteInfo.padding[i], imageInfo.padding[i]);
                }
            }
        });

        // Extract sprite grouping information va -sprite- prefixed properties in stylesheets
        assetGraph.findAssets({type: 'Css'}).forEach(function (cssAsset) {
            cssAsset.eachRuleInParseTree(function (cssRule) {
                if (cssRule.type !== 1) { // cssom.CSSRule.STYLE_RULE
                    return;
                }
                if ('-sprite-selector-for-group' in cssRule.style) {
                    var spriteInfo = extractInfoFromCssRule(cssRule, '-sprite-'),
                        spriteGroupName = spriteInfo.selectorForGroup;
                    if ('location' in spriteInfo) {
                        var matchLocation = spriteInfo.location.match(/^url\((['"]|)(.*?)\1\)$/);
                        if (matchLocation) {
                            spriteInfo.location = matchLocation[2];
                        }
                    }
                    if (spriteGroupName in spriteGroups) {
                        var group = spriteGroups[spriteGroupName];

                        if (!Array.isArray(group.placeHolders)) {
                            group.placeHolders = [];
                        }

                        if (group.placeHolders.length > 0) {
                            var err;

                            if (Object.keys(group.placeHolders[0]).every(function (key) {
                                if (['asset', 'cssRule'].indexOf(key) !== -1) {
                                    return true;
                                }
                                return group.placeHolders[0][key] === spriteInfo[key];
                            })) {
                                err = new Error('assetgraph-sprite: Multiple identical definitions of ' + spriteGroupName + ' sprite.\nThis might happen if you duplicate CSS using a preprocessor.');
                                err.asset = cssAsset;

                                assetGraph.emit('info', err);

                                group.placeHolders.push(_.extend(spriteInfo, {
                                    asset: cssAsset,
                                    cssRule: cssRule
                                }));
                            } else {
                                err = new Error('assetgraph-sprite: Multiple differing definitions of ' + spriteGroupName + ' sprite.\nThis is most likely an error.');
                                err.asset = cssAsset;

                                assetGraph.emit('warn', err);
                            }

                        } else {
                            group.placeHolders.push(_.extend(spriteInfo, {
                                asset: cssAsset,
                                cssRule: cssRule
                            }));
                        }

                    }
                }
            });
        });

        seq(Object.keys(spriteGroups))
            .seqEach(function (spriteGroupName) {
                var callback = this,
                    spriteGroup = spriteGroups[spriteGroupName],
                    imageInfos = _.values(spriteGroup.imageInfosById),
                    spriteInfo = spriteGroup.placeHolders && spriteGroup.placeHolders[0] || {},
                    packingData;

                seq(imageInfos)
                    .parMap(function (imageInfo) {
                        getCanvasImageFromImageAsset(imageInfo.asset, this);
                    })
                    .seqEach(function (canvasImage, i) {
                        _.extend(imageInfos[i], {
                            canvasImage: canvasImage,
                            width: canvasImage.width,
                            height: canvasImage.height
                        });
                        setImmediate(this);
                    })
                    .seq(function () {
                        var packerName = {
                            'jim-scott': 'jimScott',
                            horizontal: 'horizontal',
                            vertical: 'vertical'
                        }[spriteInfo.packer] || 'tryAll';

                        packingData =  packers[packerName].pack(imageInfos);

                        var canvas = new Canvas(packingData.width, packingData.height),
                            ctx = canvas.getContext('2d');

                        imageInfos = packingData.imageInfos;
                        if ('backgroundColor' in spriteInfo) {
                            ctx.fillStyle = spriteInfo.backgroundColor;
                            ctx.fillRect(0, 0, canvas.width, canvas.height);
                        }
                        imageInfos.forEach(function (imageInfo) {
                            ctx.drawImage(imageInfo.canvasImage, imageInfo.x, imageInfo.y, imageInfo.width, imageInfo.height);
                        });
                        getImageAssetFromCanvas(canvas, /^jpe?g$/.test(spriteInfo.imageFormat) ? 'Jpeg' : 'Png', assetGraph, this);
                    })
                    .seq(function (spriteAsset) {
                        var baseUrl = assetGraph.root,
                            href = ['sprite', spriteGroupName, imageInfos.length, spriteAsset.id].join('-') + spriteAsset.defaultExtension;

                        if (Array.isArray(spriteGroup.placeHolders)) {
                            var location = spriteGroup.placeHolders[0].location;

                            if (location) {
                                if (/^[?#]/.test(location)) {
                                    href += location;
                                } else {
                                    href = location;
                                }
                            }
                        }

                        spriteAsset.url = URL.resolve(baseUrl, href);
                        assetGraph.addAsset(spriteAsset);
                        if (Array.isArray(spriteGroup.placeHolders)) {
                            spriteGroup.placeHolders.forEach(function (spriteInfo) {
                                var cssRule = spriteInfo.cssRule,
                                    relation = new assetGraph.CssImage({
                                        cssRule: cssRule,
                                        from: spriteInfo.asset,
                                        to: spriteAsset
                                    });

                                ['background-image', 'background' ].forEach(function (propertyName) {
                                    if (!relation.propertyName && cssRule.style[propertyName]) {
                                        relation.propertyName = propertyName;
                                        var propertyValue = cssRule.style.getPropertyValue(propertyName),
                                            propertyPriority = cssRule.style.getPropertyPriority(propertyName);
                                        if (propertyValue === '!important') {
                                            // Hack so that an existing value of "!important" will DTRT
                                            propertyPriority = 'important';
                                            propertyValue = 'url(...)';
                                        } else if (/^\s*$/.test(propertyValue)) {
                                            propertyValue = 'url(...)';
                                        } else {
                                            var existingUrlTokens = propertyValue.match(relation.tokenRegExp);
                                            if (existingUrlTokens) {
                                                relation.tokenNumber = existingUrlTokens.length;
                                            }
                                            propertyValue += ' url(...)';
                                        }
                                        cssRule.style.setProperty(propertyName, propertyValue, propertyPriority);
                                    }
                                });
                                if (!relation.propertyName) {
                                    relation.propertyName = 'background-image';
                                    cssRule.style.setProperty('background-image', 'url(...)');
                                }
                                // I can't see why the ordering of CssImage relations should be significant...
                                assetGraph.addRelation(relation, 'last');
                                relation.refreshHref();
                                ['selector-for-group', 'packer', 'image-format', 'background-color', 'important'].forEach(function (propertyName) {
                                    spriteInfo.cssRule.style.removeProperty('-sprite-' + propertyName);
                                });

                                // If background-size is set, we should update it, The correct size is now the sprites size
                                if (spriteInfo.cssRule.style.getPropertyValue('background-size')) {
                                    spriteInfo.cssRule.style.setProperty('background-size', packingData.width + 'px ' + packingData.height + 'px');
                                }
                            });
                        }
                        imageInfos.forEach(function (imageInfo) {
                            imageInfo.incomingRelations.forEach(function (incomingRelation) {
                                incomingRelation.from.markDirty();
                                var relationSpriteInfo = getRelationSpriteInfoFromIncomingRelation(incomingRelation),
                                    style = incomingRelation.cssRule.style,
                                    existingBackgroundPositionValue = style['background-position'],
                                    existingBackgroundValue = style.background,
                                    backgroundOffsetsWereUpdated = false,
                                    offsets = [
                                        Math.round(imageInfo.x / imageInfo.asset.devicePixelRatio), // FIXME: Rounding issues?
                                        Math.round(imageInfo.y / imageInfo.asset.devicePixelRatio)
                                    ],
                                    existingOffsets;

                                if (existingBackgroundValue) {
                                    var backgroundTokens = existingBackgroundValue.split(/\s+/),
                                        positionTokenIndices = [];
                                    existingOffsets = [];
                                    backgroundTokens.forEach(function (existingBackgroundValueToken, i) {
                                        if (/^(?:-?\d+px|0)$/i.test(existingBackgroundValueToken)) {
                                            positionTokenIndices.push(i);
                                            existingOffsets.push(parseInt(existingBackgroundValueToken, 10));
                                        }
                                    });
                                    if (existingOffsets.length === 2) {
                                        // Patch up the existing background property by replacing the old offsets with corrected ones:
                                        offsets.forEach(function (offset, i) {
                                            offset -= existingOffsets[i];
                                            backgroundTokens.splice(positionTokenIndices[i], 1, offset ? -offset + 'px' : '0');
                                        });

                                        style.setProperty('background',
                                                          backgroundTokens.join(' '),
                                                          style.getPropertyPriority('background'));
                                        backgroundOffsetsWereUpdated = true;
                                    }
                                }

                                if (!backgroundOffsetsWereUpdated) {
                                    // There was no 'background' property, or it didn't contain something that looked like offsets.
                                    // Create or update the background-position property instead:
                                    var backgroundPositionPriority;
                                    if (existingBackgroundPositionValue === '!important') {
                                        // Hack so that an existing value of "!important" will DTRT
                                        backgroundPositionPriority = 'important';
                                    } else if (existingBackgroundPositionValue) {
                                        backgroundPositionPriority = style.getPropertyPriority('background-position');
                                        // FIXME: Silently ignores other units than px
                                        existingOffsets = existingBackgroundPositionValue.split(' ').map(function (item) {
                                            return parseInt(item, 10);
                                        });

                                        if (existingOffsets.length !== 2 || isNaN(existingOffsets[0]) || isNaN(existingOffsets[1])) {
                                            var err = new Error('WARNING: trying to sprite ' + imageInfo.asset.url + ' with background-position: ' + existingBackgroundPositionValue);
                                            assetGraph.emit('warn', err);
                                        } else {
                                            offsets[0] -= existingOffsets[0];
                                            offsets[1] -= existingOffsets[1];
                                        }
                                    }

                                    style.setProperty('background-position', offsets.map(function (item) {
                                        return item ? -item + 'px' : '0';
                                    }).join(' '), backgroundPositionPriority);
                                }
                                ['group', 'padding', 'no-group-selector', 'important'].forEach(function (propertyName) {
                                    style.removeProperty('-sprite-' + propertyName);
                                });

                                // Background-sizes change when spriting, upadte appropriately
                                if (imageInfo.asset.devicePixelRatio === 1) {
                                    // Device pixel ratio is default. Remove property and let the defaults rule
                                    incomingRelation.cssRule.style.removeProperty('background-size');
                                } else {
                                    // Device pixel ratio is non-default, Set it explicitly with the ratio applied
                                    var dpr = incomingRelation.to.devicePixelRatio;

                                    // TODO: Figure out if rounding might become a problem
                                    var width = packingData.width / dpr;
                                    var height = packingData.height / dpr;
                                    incomingRelation.cssRule.style.setProperty('background-size', width + 'px ' + height + 'px');
                                }

                                if (relationSpriteInfo.noGroup || !spriteGroup.placeHolders) {
                                    // The user specified that this selector needs its own background-image/background
                                    // property pointing at the sprite rather than relying on the Html elements also being
                                    // matched by the sprite group's "main" selector, which would have been preferable.
                                    var relation = new assetGraph.CssImage({
                                        cssRule: incomingRelation.cssRule,
                                        propertyName: incomingRelation.propertyName,
                                        from: incomingRelation.from,
                                        to: spriteAsset
                                    });
                                    assetGraph.addRelation(relation, 'before', incomingRelation);
                                    relation.refreshHref();
                                    assetGraph.removeRelation(incomingRelation);
                                } else {
                                    incomingRelation.detach();
                                }

                                // Remove the original image if it has become an orphan:
                                if (!assetGraph.findRelations({to: incomingRelation.to}).length) {
                                    assetGraph.removeAsset(incomingRelation.to);
                                }
                            });
                        });
                        callback();
                    })['catch'](callback);
            })
            .seq(function () {
                cb();
            })['catch'](cb);
    };
};
<commit_msg>Queue spriteGroupName exact duplication error messages to be less noisy when workign with large code bases<commit_after>var URL = require('url'),
    queryString = require('querystring'),
    _ = require('underscore'),
    seq = require('seq'),
    passError = require('passerror'),
    packers = require('./packers'),
    Canvas;

try {
    Canvas = require('canvas');
} catch (e) {}

function getCanvasImageFromImageAsset(imageAsset, cb) {
    var canvasImage = new Canvas.Image();
    canvasImage.onerror = function (err) {
        process.nextTick(function () {
            err.message += ' (' + imageAsset.urlOrDescription + ')';
            cb(err);
        });
    };
    canvasImage.onload = function () {
        process.nextTick(function () {
            cb(null, canvasImage);
        });
    };
    canvasImage.src = imageAsset.rawSrc;
}

function getImageAssetFromCanvas(canvas, assetType, assetGraph, cb) {
    if (assetType === 'Png') {
        canvas.toBuffer(passError(cb, function (pngBuffer) {
            cb(null, new assetGraph.Png({
                rawSrc: pngBuffer
            }));
        }));
    } else {
        var jpegChunks = [];
        canvas.createJPEGStream().on('data', function (chunk) {
            jpegChunks.push(chunk);
        }).on('end', function () {
            cb(null, new assetGraph.Jpeg({
                rawSrc: Buffer.concat(jpegChunks)
            }));
        }).on('error', cb);
    }
}

function calculateSpritePadding(paddingStr, asset) {
    var padding;
    if (paddingStr) {
        // Strip units ('px' assumed)
        var tokens = [];
        paddingStr.split(/[,+]|\s+/).forEach(function (token) {
            var num = parseInt(token.replace(/[a-z]+$/, ''), 10);
            if (!isNaN(num)) {
                tokens.push(num);
            }
        });
        if (tokens.length === 4) {
            padding = tokens;
        } else if (tokens.length === 3) {
            padding = [tokens[0], tokens[1], tokens[2], tokens[1]]; // T, L+R, B
        } else if (tokens.length === 2) {
            padding = [tokens[0], tokens[1], tokens[0], tokens[1]]; // T+B, L+R
        } else if (tokens.length === 1) {
            padding = [tokens[0], tokens[0], tokens[0], tokens[0]];
        }
    } else {
        padding = [0, 0, 0, 0];
    }

    return padding.map(function (size) {
        return Math.max(size, Math.max(asset.devicePixelRatio) - 1);
    });
}

function getRelationSpriteInfoFromIncomingRelation(incomingRelation) {
    var parsedQueryString = queryString.parse(incomingRelation.href.match(/\?([^#]*)/)[1]);
    return {
        groupName: parsedQueryString.sprite || 'default',
        noGroup: 'spriteNoGroup' in parsedQueryString,
        padding: calculateSpritePadding(parsedQueryString.padding, incomingRelation.to),
        asset: incomingRelation.to
    };
}

function extractInfoFromCssRule(cssRule, propertyNamePrefix) {
    var info = {};
    for (var i = 0 ; i < cssRule.style.length ; i += 1) {
        var propertyName = cssRule.style[i];
        if (!propertyNamePrefix || propertyName.indexOf(propertyNamePrefix) === 0) {
            var keyName = propertyName.substr(propertyNamePrefix.length).replace(/-([a-z])/g, function ($0, $1) {
                return $1.toUpperCase();
            });
            info[keyName] = cssRule.style[propertyName].replace(/^([\'\"])(.*)\1$/, '$2');
        }
    }
    return info;
}

module.exports = function () {
    return function spriteBackgroundImages(assetGraph, cb) {
        if (!Canvas) {
            assetGraph.emit('warn', new Error('assetgraph-sprite: Canvas not found, skipping'));
            return cb();
        }

        // Waiting for https://github.com/LearnBoost/node-canvas/issues/52
        var cairoVersion = Canvas.cairoVersion.split('.').map(function (str) {
            return parseInt(str, 10);
        });
        if (cairoVersion[0] < 1 || cairoVersion[1] < 10) {
            assetGraph.emit('warn', new Error('assetgraph-sprite: Cannot create sprites due to missing canvas.getContext("2d").drawImage() support. Please compile node-canvas with Cairo version 1.10.0 or above.'));
            return cb();
        }

        var spriteGroups = {};

        // Find sprite annotated images and create a data structure with their information
        assetGraph.findRelations({type: 'CssImage', to: {isImage: true}, href: /\?(?:|[^#]*&)sprite(?:[=&#]|$)/}).forEach(function (relation) {
            var relationSpriteInfo = getRelationSpriteInfoFromIncomingRelation(relation),
                spriteGroup = (spriteGroups[relationSpriteInfo.groupName] = spriteGroups[relationSpriteInfo.groupName] || {
                    imageInfosById: {}
                }),
                imageInfo = spriteGroup.imageInfosById[relationSpriteInfo.asset.id];
            if (!imageInfo) {
                relationSpriteInfo.incomingRelations = [relation];
                spriteGroup.imageInfosById[relationSpriteInfo.asset.id] = relationSpriteInfo;
            } else {
                imageInfo.incomingRelations.push(relation);
                for (var i = 0 ; i < 4 ; i += 1) {
                    imageInfo.padding[i] = Math.max(relationSpriteInfo.padding[i], imageInfo.padding[i]);
                }
            }
        });

        var redefinitionErrors = {};

        // Extract sprite grouping information va -sprite- prefixed properties in stylesheets
        assetGraph.findAssets({type: 'Css'}).forEach(function (cssAsset) {
            cssAsset.eachRuleInParseTree(function (cssRule) {
                if (cssRule.type !== 1) { // cssom.CSSRule.STYLE_RULE
                    return;
                }
                if ('-sprite-selector-for-group' in cssRule.style) {
                    var spriteInfo = extractInfoFromCssRule(cssRule, '-sprite-'),
                        spriteGroupName = spriteInfo.selectorForGroup;
                    if ('location' in spriteInfo) {
                        var matchLocation = spriteInfo.location.match(/^url\((['"]|)(.*?)\1\)$/);
                        if (matchLocation) {
                            spriteInfo.location = matchLocation[2];
                        }
                    }
                    if (spriteGroupName in spriteGroups) {
                        var group = spriteGroups[spriteGroupName];

                        if (!Array.isArray(group.placeHolders)) {
                            group.placeHolders = [];
                        }

                        if (group.placeHolders.length > 0) {
                            var err;

                            if (Object.keys(group.placeHolders[0]).every(function (key) {
                                if (['asset', 'cssRule'].indexOf(key) !== -1) {
                                    return true;
                                }
                                return group.placeHolders[0][key] === spriteInfo[key];
                            })) {
                                // Queue up these errors as they tend to come in quite big bunches
                                if (!Array.isArray(redefinitionErrors[spriteGroupName])) {
                                    redefinitionErrors[spriteGroupName] = [];
                                }
                                redefinitionErrors[spriteGroupName].push(cssAsset);

                                group.placeHolders.push(_.extend(spriteInfo, {
                                    asset: cssAsset,
                                    cssRule: cssRule
                                }));
                            } else {
                                err = new Error('assetgraph-sprite: Multiple differing definitions of ' + spriteGroupName + ' sprite.\nThis is most likely an error.');
                                err.asset = cssAsset;

                                assetGraph.emit('warn', err);
                            }

                        } else {
                            group.placeHolders.push(_.extend(spriteInfo, {
                                asset: cssAsset,
                                cssRule: cssRule
                            }));
                        }

                    }
                }
            });
        });

        Object.keys(redefinitionErrors).forEach(function (spriteGroupName) {
            var message = [
                'assetgraph-sprite: Multiple identical definitions of ' + spriteGroupName + ' sprite.',
                'This might happen if you duplicate CSS using a preprocessor.',
                redefinitionErrors[spriteGroupName].map(function (asset) {
                    return '\t' + asset.urlOrDescription;
                }).join('\n')
            ].join('\n');

            var err = new Error(message);

            assetGraph.emit('info', err);
        });

        seq(Object.keys(spriteGroups))
            .seqEach(function (spriteGroupName) {
                var callback = this,
                    spriteGroup = spriteGroups[spriteGroupName],
                    imageInfos = _.values(spriteGroup.imageInfosById),
                    spriteInfo = spriteGroup.placeHolders && spriteGroup.placeHolders[0] || {},
                    packingData;

                seq(imageInfos)
                    .parMap(function (imageInfo) {
                        getCanvasImageFromImageAsset(imageInfo.asset, this);
                    })
                    .seqEach(function (canvasImage, i) {
                        _.extend(imageInfos[i], {
                            canvasImage: canvasImage,
                            width: canvasImage.width,
                            height: canvasImage.height
                        });
                        setImmediate(this);
                    })
                    .seq(function () {
                        var packerName = {
                            'jim-scott': 'jimScott',
                            horizontal: 'horizontal',
                            vertical: 'vertical'
                        }[spriteInfo.packer] || 'tryAll';

                        packingData =  packers[packerName].pack(imageInfos);

                        var canvas = new Canvas(packingData.width, packingData.height),
                            ctx = canvas.getContext('2d');

                        imageInfos = packingData.imageInfos;
                        if ('backgroundColor' in spriteInfo) {
                            ctx.fillStyle = spriteInfo.backgroundColor;
                            ctx.fillRect(0, 0, canvas.width, canvas.height);
                        }
                        imageInfos.forEach(function (imageInfo) {
                            ctx.drawImage(imageInfo.canvasImage, imageInfo.x, imageInfo.y, imageInfo.width, imageInfo.height);
                        });
                        getImageAssetFromCanvas(canvas, /^jpe?g$/.test(spriteInfo.imageFormat) ? 'Jpeg' : 'Png', assetGraph, this);
                    })
                    .seq(function (spriteAsset) {
                        var baseUrl = assetGraph.root,
                            href = ['sprite', spriteGroupName, imageInfos.length, spriteAsset.id].join('-') + spriteAsset.defaultExtension;

                        if (Array.isArray(spriteGroup.placeHolders)) {
                            var location = spriteGroup.placeHolders[0].location;

                            if (location) {
                                if (/^[?#]/.test(location)) {
                                    href += location;
                                } else {
                                    href = location;
                                }
                            }
                        }

                        spriteAsset.url = URL.resolve(baseUrl, href);
                        assetGraph.addAsset(spriteAsset);
                        if (Array.isArray(spriteGroup.placeHolders)) {
                            spriteGroup.placeHolders.forEach(function (spriteInfo) {
                                var cssRule = spriteInfo.cssRule,
                                    relation = new assetGraph.CssImage({
                                        cssRule: cssRule,
                                        from: spriteInfo.asset,
                                        to: spriteAsset
                                    });

                                ['background-image', 'background' ].forEach(function (propertyName) {
                                    if (!relation.propertyName && cssRule.style[propertyName]) {
                                        relation.propertyName = propertyName;
                                        var propertyValue = cssRule.style.getPropertyValue(propertyName),
                                            propertyPriority = cssRule.style.getPropertyPriority(propertyName);
                                        if (propertyValue === '!important') {
                                            // Hack so that an existing value of "!important" will DTRT
                                            propertyPriority = 'important';
                                            propertyValue = 'url(...)';
                                        } else if (/^\s*$/.test(propertyValue)) {
                                            propertyValue = 'url(...)';
                                        } else {
                                            var existingUrlTokens = propertyValue.match(relation.tokenRegExp);
                                            if (existingUrlTokens) {
                                                relation.tokenNumber = existingUrlTokens.length;
                                            }
                                            propertyValue += ' url(...)';
                                        }
                                        cssRule.style.setProperty(propertyName, propertyValue, propertyPriority);
                                    }
                                });
                                if (!relation.propertyName) {
                                    relation.propertyName = 'background-image';
                                    cssRule.style.setProperty('background-image', 'url(...)');
                                }
                                // I can't see why the ordering of CssImage relations should be significant...
                                assetGraph.addRelation(relation, 'last');
                                relation.refreshHref();
                                ['selector-for-group', 'packer', 'image-format', 'background-color', 'important'].forEach(function (propertyName) {
                                    spriteInfo.cssRule.style.removeProperty('-sprite-' + propertyName);
                                });

                                // If background-size is set, we should update it, The correct size is now the sprites size
                                if (spriteInfo.cssRule.style.getPropertyValue('background-size')) {
                                    spriteInfo.cssRule.style.setProperty('background-size', packingData.width + 'px ' + packingData.height + 'px');
                                }
                            });
                        }
                        imageInfos.forEach(function (imageInfo) {
                            imageInfo.incomingRelations.forEach(function (incomingRelation) {
                                incomingRelation.from.markDirty();
                                var relationSpriteInfo = getRelationSpriteInfoFromIncomingRelation(incomingRelation),
                                    style = incomingRelation.cssRule.style,
                                    existingBackgroundPositionValue = style['background-position'],
                                    existingBackgroundValue = style.background,
                                    backgroundOffsetsWereUpdated = false,
                                    offsets = [
                                        Math.round(imageInfo.x / imageInfo.asset.devicePixelRatio), // FIXME: Rounding issues?
                                        Math.round(imageInfo.y / imageInfo.asset.devicePixelRatio)
                                    ],
                                    existingOffsets;

                                if (existingBackgroundValue) {
                                    var backgroundTokens = existingBackgroundValue.split(/\s+/),
                                        positionTokenIndices = [];
                                    existingOffsets = [];
                                    backgroundTokens.forEach(function (existingBackgroundValueToken, i) {
                                        if (/^(?:-?\d+px|0)$/i.test(existingBackgroundValueToken)) {
                                            positionTokenIndices.push(i);
                                            existingOffsets.push(parseInt(existingBackgroundValueToken, 10));
                                        }
                                    });
                                    if (existingOffsets.length === 2) {
                                        // Patch up the existing background property by replacing the old offsets with corrected ones:
                                        offsets.forEach(function (offset, i) {
                                            offset -= existingOffsets[i];
                                            backgroundTokens.splice(positionTokenIndices[i], 1, offset ? -offset + 'px' : '0');
                                        });

                                        style.setProperty('background',
                                                          backgroundTokens.join(' '),
                                                          style.getPropertyPriority('background'));
                                        backgroundOffsetsWereUpdated = true;
                                    }
                                }

                                if (!backgroundOffsetsWereUpdated) {
                                    // There was no 'background' property, or it didn't contain something that looked like offsets.
                                    // Create or update the background-position property instead:
                                    var backgroundPositionPriority;
                                    if (existingBackgroundPositionValue === '!important') {
                                        // Hack so that an existing value of "!important" will DTRT
                                        backgroundPositionPriority = 'important';
                                    } else if (existingBackgroundPositionValue) {
                                        backgroundPositionPriority = style.getPropertyPriority('background-position');
                                        // FIXME: Silently ignores other units than px
                                        existingOffsets = existingBackgroundPositionValue.split(' ').map(function (item) {
                                            return parseInt(item, 10);
                                        });

                                        if (existingOffsets.length !== 2 || isNaN(existingOffsets[0]) || isNaN(existingOffsets[1])) {
                                            var err = new Error('WARNING: trying to sprite ' + imageInfo.asset.url + ' with background-position: ' + existingBackgroundPositionValue);
                                            assetGraph.emit('warn', err);
                                        } else {
                                            offsets[0] -= existingOffsets[0];
                                            offsets[1] -= existingOffsets[1];
                                        }
                                    }

                                    style.setProperty('background-position', offsets.map(function (item) {
                                        return item ? -item + 'px' : '0';
                                    }).join(' '), backgroundPositionPriority);
                                }
                                ['group', 'padding', 'no-group-selector', 'important'].forEach(function (propertyName) {
                                    style.removeProperty('-sprite-' + propertyName);
                                });

                                // Background-sizes change when spriting, upadte appropriately
                                if (imageInfo.asset.devicePixelRatio === 1) {
                                    // Device pixel ratio is default. Remove property and let the defaults rule
                                    incomingRelation.cssRule.style.removeProperty('background-size');
                                } else {
                                    // Device pixel ratio is non-default, Set it explicitly with the ratio applied
                                    var dpr = incomingRelation.to.devicePixelRatio;

                                    // TODO: Figure out if rounding might become a problem
                                    var width = packingData.width / dpr;
                                    var height = packingData.height / dpr;
                                    incomingRelation.cssRule.style.setProperty('background-size', width + 'px ' + height + 'px');
                                }

                                if (relationSpriteInfo.noGroup || !spriteGroup.placeHolders) {
                                    // The user specified that this selector needs its own background-image/background
                                    // property pointing at the sprite rather than relying on the Html elements also being
                                    // matched by the sprite group's "main" selector, which would have been preferable.
                                    var relation = new assetGraph.CssImage({
                                        cssRule: incomingRelation.cssRule,
                                        propertyName: incomingRelation.propertyName,
                                        from: incomingRelation.from,
                                        to: spriteAsset
                                    });
                                    assetGraph.addRelation(relation, 'before', incomingRelation);
                                    relation.refreshHref();
                                    assetGraph.removeRelation(incomingRelation);
                                } else {
                                    incomingRelation.detach();
                                }

                                // Remove the original image if it has become an orphan:
                                if (!assetGraph.findRelations({to: incomingRelation.to}).length) {
                                    assetGraph.removeAsset(incomingRelation.to);
                                }
                            });
                        });
                        callback();
                    })['catch'](callback);
            })
            .seq(function () {
                cb();
            })['catch'](cb);
    };
};
<|endoftext|>