<commit_before>// Copyright 2015, University of Colorado Boulder

/**
 * Creates a composite report of all of the 3rd party images, code, audio and other media used by all of the published
 * PhET Simulations. The reports is published at: https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
 *
 * Usage:
 * grunt report-third-party
 * // then push the updated license file to sherpa
 *
 * Third party entries are parsed from the HTML files for all simulations published on the PhET website, see
 * getLicenseEntry.js
 *
 * @author Sam Reid
 */

// Load shared constants
var ChipperConstants = require( '../../../chipper/js/common/ChipperConstants' );
var ChipperStringUtils = require( '../../../chipper/js/common/ChipperStringUtils' );
var assert = require( 'assert' );

// constants
var SHERPA = '../sherpa';  // The relative path to sherpa, from the chipper path

// node modules
var fs = require( 'fs' );
var http = require( 'http' );

var _ = require( '../../' + SHERPA + '/lib/lodash-2.4.1.min' ); // eslint-disable-line require-statement-match

/**
 * @param grunt - the grunt instance
 */
module.exports = function( grunt ) {
  'use strict';

  // The file where the report will be written
  var outputFilename = SHERPA + '/third-party-licenses.md';

  // Since we download the HTML files, this task must wait until it is complete
  var gruntDone = grunt.task.current.async();

  // Prepare a directory where the HTML files will be downloaded.
  // This directory should have been deleted at the end of the last task, but this could be useful if the task was
  // interrupted before cleanup.
  if ( grunt.file.exists( 'downloaded-sims' ) ) {
    grunt.file.delete( 'downloaded-sims' );
  }
  grunt.file.mkdir( 'downloaded-sims' );

  // Download a URL to a local file
  // http://stackoverflow.com/questions/11944932/how-to-download-a-file-with-node-js-without-using-third-party-libraries
  var download = function( url, dest, cb ) {
    var file = fs.createWriteStream( dest );
    http.get( url, function( response ) {
      response.pipe( file );
      file.on( 'finish', function() {
        file.close( cb );  // close() is async, call cb after close completes.
      } );
    } ).on( 'error', function( err ) { // Handle errors
      fs.unlink( dest ); // Delete the file async. (But we don't check the result)
      if ( cb ) {
        cb( err.message );
      }
    } );
  };

  // After the downloads are complete, write the report based on the metadata in the files.
  var writeReport = function() {

    // Aggregate results for each of the license types
    var compositeCode = {};
    var compositeMedia = {};

    // List of all of the repository names, so that we can detect which libraries are used by all-sims
    var simTitles = [];

    /**
     * Given an HTML text, find the title attribute by parsing for <title>
     * @param html
     * @returns {string}
     */
    var parseTitle = function( html ) {
      var startKey = '<title>';
      var endKey = '</title>';

      var startIndex = html.indexOf( startKey );
      var endIndex = html.indexOf( endKey );

      return html.substring( startIndex + startKey.length, endIndex ).trim();
    };

    /**
     * Add the source (images/audio/media or code) entries to the destination object, keyed by name.
     * @param {string} repositoryName - the name of the repository, such as 'energy-skate-park-basics'
     * @param {Object} source - the object from which to read the entry
     * @param {Object} destination - the object to which to append the entry
     */
    var augment = function( repositoryName, source, destination ) {
      for ( var entry in source ) {
        if ( source.hasOwnProperty( entry ) ) {
          if ( !destination.hasOwnProperty( entry ) ) {
            destination[ entry ] = source[ entry ];//overwrites
            destination[ entry ].usedBy = [];
          }
          destination[ entry ].usedBy.push( repositoryName );
        }
      }
    };

    grunt.file.recurse( 'downloaded-sims', function( abspath, rootdir, subdir, filename ) {
      if ( ChipperStringUtils.endsWith( filename.toLowerCase(), '.html' ) ) {

        // load the file into a string
        var html = grunt.file.read( abspath ).trim();

        if ( html.indexOf( 'was not found on this server' ) < 0 || html.indexOf( 'was not found on this server' ) > 400 ) {

          var startIndex = html.indexOf( ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES );
          var endIndex = html.indexOf( ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES );
          var substring = html.substring( startIndex, endIndex );

          var firstCurlyBrace = substring.indexOf( '{' );
          var lastCurlyBrace = substring.lastIndexOf( '}' );
          var jsonString = substring.substring( firstCurlyBrace, lastCurlyBrace + 1 );

          var json = JSON.parse( jsonString );

          var title = parseTitle( html );
          if ( !title || title.indexOf( 'undefined' ) === 0 || title.indexOf( 'TITLE' ) >= 0 ) {
            grunt.log.writeln( 'title not found for ' + abspath );
            title = filename;
          }
          augment( title, json.lib, compositeCode );
          augment( title, json.audio, compositeMedia );
          augment( title, json.images, compositeMedia );

          simTitles.push( title );
        }
      }
    } );

    // Sort to easily compare lists of repositoryNames with usedBy columns, to see which resources are used by everything.
    simTitles.sort();

    // If anything is used by every sim indicate that here
    for ( var entry in compositeCode ) {
      if ( compositeCode.hasOwnProperty( entry ) ) {
        compositeCode[ entry ].usedBy.sort();
        if ( _.isEqual( simTitles, compositeCode[ entry ].usedBy ) ) {
          compositeCode[ entry ].usedBy = 'all-sims';// Confusing that this matches a repo name, but since that repo isn't actually a sim, perhaps this will be ok
        }
      }
    }

    var licenseJSON = grunt.file.readJSON( SHERPA + '/lib/license.json' );

    var codeOutput = [];
    var codeLicensesUsed = [];
    var mediaLicensesUsed = [];

    // Get a list of the library names
    var libraryNames = [];
    for ( var lib in licenseJSON ) {
      if ( licenseJSON.hasOwnProperty( lib ) ) {
        libraryNames.push( lib );
      }
    }

    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    libraryNames.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Add info for each library to the MD report
    for ( var i = 0; i < libraryNames.length; i++ ) {
      var library = libraryNames[ i ];

      var lineElementsForLibrary = [
        '**' + library + '**',
        licenseJSON[ library ].text.join( '
' ),
        licenseJSON[ library ].projectURL,
        'License: [' + licenseJSON[ library ].license + '](licenses/' + library + '.txt)',
        'Notes: ' + licenseJSON[ library ].notes
      ];

      if ( licenseJSON[ library ].dependencies ) {
        lineElementsForLibrary.push( 'Dependencies: **' + licenseJSON[ library ].dependencies + '**' );
      }

      if ( compositeCode.hasOwnProperty( library ) && compositeCode[ library ].usedBy instanceof Array ) {
        lineElementsForLibrary.push( 'Used by: ' + compositeCode[ library ].usedBy.join( ', ' ) );
      }

      // \n worked well when viewing GitHub markdown as an issue comment, but for unknown reasons
 is necessary when
      // viewing from https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
      codeOutput.push( lineElementsForLibrary.join( '
' ) );

      if ( codeLicensesUsed.indexOf( licenseJSON[ library ].license ) < 0 ) {
        codeLicensesUsed.push( licenseJSON[ library ].license );
      }
    }

    var mediaOutput = [];
    var mediaKeys = [];
    for ( var imageAudioEntry in compositeMedia ) {
      if ( compositeMedia.hasOwnProperty( imageAudioEntry ) ) {
        mediaKeys.push( imageAudioEntry );
      }
    }
    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    mediaKeys.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Create the text for the image and audio, and keep track of which licenses were used by them.
    for ( i = 0; i < mediaKeys.length; i++ ) {
      var mediaKey = mediaKeys[ i ];

      var copyrightStatement = compositeMedia[ mediaKey ].text.join( '
' ).trim();
      var projectURL = compositeMedia[ mediaKey ].projectURL.trim();

      if ( copyrightStatement.length === 0 ) {
        copyrightStatement = '(no text)';
      }

      if ( projectURL.length === 0 ) {
        projectURL = '(no project url)';
      }

      var notes = compositeMedia[ mediaKey ].notes.trim();
      if ( notes.length === 0 ) {
        notes = '(no notes)';
      }

      var license = compositeMedia[ mediaKey ].license.trim();
      assert && assert( license.length > 0, 'All media entries must have a license' );

      var mediaEntryLines = [
        '**' + mediaKey + '**',
        copyrightStatement,
        projectURL,
        'License: ' + license,
        'Notes: ' + notes
      ];

      // PhET has temporarily chosen to publish John Travoltage with incompatible licenses, so the reasons for
      // making the exceptions are noted here.  The new artwork is beind developed now and the simulation
      // will be republished without exception cases soon.
      // This code will remain in case we have other exception cases in the future.
      if ( compositeMedia[ mediaKey ].exception ) {
        mediaEntryLines.push( 'Exception: ' + compositeMedia[ mediaKey ].exception );
      }

      // TODO: is this a real license?  Why is there logic here but nowhere else?  Perhaps files annotated with
      // contact phethelp@colorado.edu are leaking into the HTML files unnecessarily
      if ( license !== 'contact phethelp@colorado.edu' ) {
        mediaOutput.push( mediaEntryLines.join( '
' ) );

        if ( mediaLicensesUsed.indexOf( license ) < 0 ) {
          mediaLicensesUsed.push( license );
        }
      }
    }

    // Summarize licenses used
    var fileList = simTitles.join( '\n* ' );

    var outputString =
      'This report enumerates the third-party resources (code, images, audio, etc) used in a set of simulations.\n' +
      '* [Third-party Code](#third-party-code)\n' +
      '* [Third-party Code License Summary](#third-party-code-license-summary)\n' +
      '* [Third-party Media](#third-party-media)\n' +
      '* [Third-party Media License Summary](#third-party-media-license-summary)\n' +
      '\n' +
      'This report is for the following simulations: \n\n* ' + fileList + '\n\nTo see the third party resources used in a particular published ' +
      'simulation, inspect the HTML file between the `' + ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES + '` and `' + ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES + '` ' +
      '(only exists in sim publications after Aug 7, 2015).\n' +
      '# Third-party Code:
\n' +
      codeOutput.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Code License Summary:
\n' +
      codeLicensesUsed.join( '
' ) + '\n\n' +

      '---\n' +

      '# Third-party Media:
\n' +
      mediaOutput.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Media License Summary:
\n' +
      mediaLicensesUsed.join( '
' ) + '\n\n';

    // Compare the file output to the existing file, and write & git commit only if different
    if ( !grunt.file.exists( outputFilename ) || grunt.file.read( outputFilename ) !== outputString ) {
      grunt.log.writeln( 'File output changed, writing file ' + outputFilename );
      grunt.file.write( outputFilename, outputString );
    }
    else {
      grunt.log.writeln( outputFilename + ' contents are the same.  No need to save.' );
    }

    // Delete the temporarily downloaded files when task complete
    if ( grunt.file.exists( 'downloaded-sims' ) ) {
      grunt.file.delete( 'downloaded-sims' );
    }

    gruntDone();
  };

  // Download all of the simulations.  For simulations that are in "active-sims" but not published,
  // download them anyways but skip them in the report.
  var activeSimsString = fs.readFileSync( '../chipper/data/active-sims', 'utf-8' ).trim();
  var activeSimsArray = activeSimsString.split( '\n' );

  // Download files one at a time so we can make sure we get everything.
  var downloadNext = function( index ) {
    var sim = activeSimsArray[ index ];

    var url = 'http://phet.colorado.edu/sims/html/' + sim + '/latest/' + sim + '_en.html';
    console.log( 'downloading ' + (index + 1) + '/' + activeSimsArray.length + ': ' + url );

    download( url, 'downloaded-sims/' + sim + '_en.html', function( err ) {
      assert && assert( !err, 'Error during download: ' + err );

      var next = index + 1;

      if ( next < activeSimsArray.length ) {
        downloadNext( next );
      }
      else {
        writeReport();
      }
    } );
  };

  downloadNext( 0 );
};<commit_msg>Renamed copyrightStatement to text, since Public Domain things have no copyright statement, see https://github.com/phetsims/sherpa/issues/64<commit_after>// Copyright 2015, University of Colorado Boulder

/**
 * Creates a composite report of all of the 3rd party images, code, audio and other media used by all of the published
 * PhET Simulations. The reports is published at: https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
 *
 * Usage:
 * grunt report-third-party
 * // then push the updated license file to sherpa
 *
 * Third party entries are parsed from the HTML files for all simulations published on the PhET website, see
 * getLicenseEntry.js
 *
 * @author Sam Reid
 */

// Load shared constants
var ChipperConstants = require( '../../../chipper/js/common/ChipperConstants' );
var ChipperStringUtils = require( '../../../chipper/js/common/ChipperStringUtils' );
var assert = require( 'assert' );

// constants
var SHERPA = '../sherpa';  // The relative path to sherpa, from the chipper path

// node modules
var fs = require( 'fs' );
var http = require( 'http' );

var _ = require( '../../' + SHERPA + '/lib/lodash-2.4.1.min' ); // eslint-disable-line require-statement-match

/**
 * @param grunt - the grunt instance
 */
module.exports = function( grunt ) {
  'use strict';

  // The file where the report will be written
  var outputFilename = SHERPA + '/third-party-licenses.md';

  // Since we download the HTML files, this task must wait until it is complete
  var gruntDone = grunt.task.current.async();

  // Prepare a directory where the HTML files will be downloaded.
  // This directory should have been deleted at the end of the last task, but this could be useful if the task was
  // interrupted before cleanup.
  if ( grunt.file.exists( 'downloaded-sims' ) ) {
    grunt.file.delete( 'downloaded-sims' );
  }
  grunt.file.mkdir( 'downloaded-sims' );

  // Download a URL to a local file
  // http://stackoverflow.com/questions/11944932/how-to-download-a-file-with-node-js-without-using-third-party-libraries
  var download = function( url, dest, cb ) {
    var file = fs.createWriteStream( dest );
    http.get( url, function( response ) {
      response.pipe( file );
      file.on( 'finish', function() {
        file.close( cb );  // close() is async, call cb after close completes.
      } );
    } ).on( 'error', function( err ) { // Handle errors
      fs.unlink( dest ); // Delete the file async. (But we don't check the result)
      if ( cb ) {
        cb( err.message );
      }
    } );
  };

  // After the downloads are complete, write the report based on the metadata in the files.
  var writeReport = function() {

    // Aggregate results for each of the license types
    var compositeCode = {};
    var compositeMedia = {};

    // List of all of the repository names, so that we can detect which libraries are used by all-sims
    var simTitles = [];

    /**
     * Given an HTML text, find the title attribute by parsing for <title>
     * @param html
     * @returns {string}
     */
    var parseTitle = function( html ) {
      var startKey = '<title>';
      var endKey = '</title>';

      var startIndex = html.indexOf( startKey );
      var endIndex = html.indexOf( endKey );

      return html.substring( startIndex + startKey.length, endIndex ).trim();
    };

    /**
     * Add the source (images/audio/media or code) entries to the destination object, keyed by name.
     * @param {string} repositoryName - the name of the repository, such as 'energy-skate-park-basics'
     * @param {Object} source - the object from which to read the entry
     * @param {Object} destination - the object to which to append the entry
     */
    var augment = function( repositoryName, source, destination ) {
      for ( var entry in source ) {
        if ( source.hasOwnProperty( entry ) ) {
          if ( !destination.hasOwnProperty( entry ) ) {
            destination[ entry ] = source[ entry ];//overwrites
            destination[ entry ].usedBy = [];
          }
          destination[ entry ].usedBy.push( repositoryName );
        }
      }
    };

    grunt.file.recurse( 'downloaded-sims', function( abspath, rootdir, subdir, filename ) {
      if ( ChipperStringUtils.endsWith( filename.toLowerCase(), '.html' ) ) {

        // load the file into a string
        var html = grunt.file.read( abspath ).trim();

        if ( html.indexOf( 'was not found on this server' ) < 0 || html.indexOf( 'was not found on this server' ) > 400 ) {

          var startIndex = html.indexOf( ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES );
          var endIndex = html.indexOf( ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES );
          var substring = html.substring( startIndex, endIndex );

          var firstCurlyBrace = substring.indexOf( '{' );
          var lastCurlyBrace = substring.lastIndexOf( '}' );
          var jsonString = substring.substring( firstCurlyBrace, lastCurlyBrace + 1 );

          var json = JSON.parse( jsonString );

          var title = parseTitle( html );
          if ( !title || title.indexOf( 'undefined' ) === 0 || title.indexOf( 'TITLE' ) >= 0 ) {
            grunt.log.writeln( 'title not found for ' + abspath );
            title = filename;
          }
          augment( title, json.lib, compositeCode );
          augment( title, json.audio, compositeMedia );
          augment( title, json.images, compositeMedia );

          simTitles.push( title );
        }
      }
    } );

    // Sort to easily compare lists of repositoryNames with usedBy columns, to see which resources are used by everything.
    simTitles.sort();

    // If anything is used by every sim indicate that here
    for ( var entry in compositeCode ) {
      if ( compositeCode.hasOwnProperty( entry ) ) {
        compositeCode[ entry ].usedBy.sort();
        if ( _.isEqual( simTitles, compositeCode[ entry ].usedBy ) ) {
          compositeCode[ entry ].usedBy = 'all-sims';// Confusing that this matches a repo name, but since that repo isn't actually a sim, perhaps this will be ok
        }
      }
    }

    var licenseJSON = grunt.file.readJSON( SHERPA + '/lib/license.json' );

    var codeOutput = [];
    var codeLicensesUsed = [];
    var mediaLicensesUsed = [];

    // Get a list of the library names
    var libraryNames = [];
    for ( var lib in licenseJSON ) {
      if ( licenseJSON.hasOwnProperty( lib ) ) {
        libraryNames.push( lib );
      }
    }

    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    libraryNames.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Add info for each library to the MD report
    for ( var i = 0; i < libraryNames.length; i++ ) {
      var library = libraryNames[ i ];

      var lineElementsForLibrary = [
        '**' + library + '**',
        licenseJSON[ library ].text.join( '
' ),
        licenseJSON[ library ].projectURL,
        'License: [' + licenseJSON[ library ].license + '](licenses/' + library + '.txt)',
        'Notes: ' + licenseJSON[ library ].notes
      ];

      if ( licenseJSON[ library ].dependencies ) {
        lineElementsForLibrary.push( 'Dependencies: **' + licenseJSON[ library ].dependencies + '**' );
      }

      if ( compositeCode.hasOwnProperty( library ) && compositeCode[ library ].usedBy instanceof Array ) {
        lineElementsForLibrary.push( 'Used by: ' + compositeCode[ library ].usedBy.join( ', ' ) );
      }

      // \n worked well when viewing GitHub markdown as an issue comment, but for unknown reasons
 is necessary when
      // viewing from https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
      codeOutput.push( lineElementsForLibrary.join( '
' ) );

      if ( codeLicensesUsed.indexOf( licenseJSON[ library ].license ) < 0 ) {
        codeLicensesUsed.push( licenseJSON[ library ].license );
      }
    }

    var mediaOutput = [];
    var mediaKeys = [];
    for ( var imageAudioEntry in compositeMedia ) {
      if ( compositeMedia.hasOwnProperty( imageAudioEntry ) ) {
        mediaKeys.push( imageAudioEntry );
      }
    }
    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    mediaKeys.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Create the text for the image and audio, and keep track of which licenses were used by them.
    for ( i = 0; i < mediaKeys.length; i++ ) {
      var mediaKey = mediaKeys[ i ];

      var text = compositeMedia[ mediaKey ].text.join( '
' ).trim();
      var projectURL = compositeMedia[ mediaKey ].projectURL.trim();

      if ( text.length === 0 ) {
        text = '(no text)';
      }

      if ( projectURL.length === 0 ) {
        projectURL = '(no project url)';
      }

      var notes = compositeMedia[ mediaKey ].notes.trim();
      if ( notes.length === 0 ) {
        notes = '(no notes)';
      }

      var license = compositeMedia[ mediaKey ].license.trim();
      assert && assert( license.length > 0, 'All media entries must have a license' );

      var mediaEntryLines = [
        '**' + mediaKey + '**',
        text,
        projectURL,
        'License: ' + license,
        'Notes: ' + notes
      ];

      // PhET has temporarily chosen to publish John Travoltage with incompatible licenses, so the reasons for
      // making the exceptions are noted here.  The new artwork is beind developed now and the simulation
      // will be republished without exception cases soon.
      // This code will remain in case we have other exception cases in the future.
      if ( compositeMedia[ mediaKey ].exception ) {
        mediaEntryLines.push( 'Exception: ' + compositeMedia[ mediaKey ].exception );
      }

      // TODO: is this a real license?  Why is there logic here but nowhere else?  Perhaps files annotated with
      // contact phethelp@colorado.edu are leaking into the HTML files unnecessarily
      if ( license !== 'contact phethelp@colorado.edu' ) {
        mediaOutput.push( mediaEntryLines.join( '
' ) );

        if ( mediaLicensesUsed.indexOf( license ) < 0 ) {
          mediaLicensesUsed.push( license );
        }
      }
    }

    // Summarize licenses used
    var fileList = simTitles.join( '\n* ' );

    var outputString =
      'This report enumerates the third-party resources (code, images, audio, etc) used in a set of simulations.\n' +
      '* [Third-party Code](#third-party-code)\n' +
      '* [Third-party Code License Summary](#third-party-code-license-summary)\n' +
      '* [Third-party Media](#third-party-media)\n' +
      '* [Third-party Media License Summary](#third-party-media-license-summary)\n' +
      '\n' +
      'This report is for the following simulations: \n\n* ' + fileList + '\n\nTo see the third party resources used in a particular published ' +
      'simulation, inspect the HTML file between the `' + ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES + '` and `' + ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES + '` ' +
      '(only exists in sim publications after Aug 7, 2015).\n' +
      '# Third-party Code:
\n' +
      codeOutput.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Code License Summary:
\n' +
      codeLicensesUsed.join( '
' ) + '\n\n' +

      '---\n' +

      '# Third-party Media:
\n' +
      mediaOutput.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Media License Summary:
\n' +
      mediaLicensesUsed.join( '
' ) + '\n\n';

    // Compare the file output to the existing file, and write & git commit only if different
    if ( !grunt.file.exists( outputFilename ) || grunt.file.read( outputFilename ) !== outputString ) {
      grunt.log.writeln( 'File output changed, writing file ' + outputFilename );
      grunt.file.write( outputFilename, outputString );
    }
    else {
      grunt.log.writeln( outputFilename + ' contents are the same.  No need to save.' );
    }

    // Delete the temporarily downloaded files when task complete
    if ( grunt.file.exists( 'downloaded-sims' ) ) {
      grunt.file.delete( 'downloaded-sims' );
    }

    gruntDone();
  };

  // Download all of the simulations.  For simulations that are in "active-sims" but not published,
  // download them anyways but skip them in the report.
  var activeSimsString = fs.readFileSync( '../chipper/data/active-sims', 'utf-8' ).trim();
  var activeSimsArray = activeSimsString.split( '\n' );

  // Download files one at a time so we can make sure we get everything.
  var downloadNext = function( index ) {
    var sim = activeSimsArray[ index ];

    var url = 'http://phet.colorado.edu/sims/html/' + sim + '/latest/' + sim + '_en.html';
    console.log( 'downloading ' + (index + 1) + '/' + activeSimsArray.length + ': ' + url );

    download( url, 'downloaded-sims/' + sim + '_en.html', function( err ) {
      assert && assert( !err, 'Error during download: ' + err );

      var next = index + 1;

      if ( next < activeSimsArray.length ) {
        downloadNext( next );
      }
      else {
        writeReport();
      }
    } );
  };

  downloadNext( 0 );
};<|endoftext|>


<commit_before>// Copyright 2015, University of Colorado Boulder

/**
 * Creates a composite report of all of the 3rd party images, code, audio and other media used by all of the PhET
 * Simulations which appear in a directory (required argument) and updates the online version by automatically adding and
 * pushing the changes to GitHub.  One of the reports is published at:
 * https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
 *
 * Usage:
 * cd chipper
 * grunt report-third-party
 * // then push the updated license file to sherpa
 *
 * NOTE: sometimes for unknown reasons a file is missed or skipped, please inspect the third-party-licenses.md before
 * committing to make sure it is complete.
 *
 * Third party entries are parsed from HTML, see getLicenseEntry.js
 *
 * If the --active-runnables grunt option is supplied, the task iterates over the active-runnables and copies each built HTML file
 * into the directory specified with --input before running the report.  If any HTML files are missing, the report will fail.
 * Before using this options, the developer should run `grunt-all.sh` to make sure all of the HTML files are up-to-date.
 * The directory is neither automatically created nor automatically cleaned, this is the responsibility of the developer.
 * (Note that if you fail to manually clean the directory, you may end up with stale HTML files).
 *
 * See https://github.com/phetsims/chipper/issues/162
 *
 * @author Sam Reid
 */

// Load shared constants
var ChipperConstants = require( '../../../chipper/js/common/ChipperConstants' );
var ChipperStringUtils = require( '../../../chipper/js/common/ChipperStringUtils' );

// constants
var SHERPA = '../sherpa';  // The relative path to sherpa, from the chipper path

// node modules
var fs = require( 'fs' );
var http = require( 'http' );

var _ = require( '../../../sherpa/lib/lodash-2.4.1.min' ); // eslint-disable-line require-statement-match

/**
 * @param grunt - the grunt instance
 */
module.exports = function( grunt ) {
  'use strict';

  // The file where the report will be written
  var output = '../sherpa/third-party-licenses.md';

  // Since we download the HTML files, this task must wait until it is complete
  var gruntDone = grunt.task.current.async();

  // Prepare a directory where the HTML files will be downloaded
  if ( grunt.file.exists( 'downloaded-sims' ) ) {
    grunt.file.delete( 'downloaded-sims' );
  }
  grunt.file.mkdir( 'downloaded-sims' );

  // Download a URL to a local file
  // http://stackoverflow.com/questions/11944932/how-to-download-a-file-with-node-js-without-using-third-party-libraries
  var download = function( url, dest, cb ) {
    var file = fs.createWriteStream( dest );
    http.get( url, function( response ) {
      response.pipe( file );
      file.on( 'finish', function() {
        file.close( cb );  // close() is async, call cb after close completes.
      } );
    } ).on( 'error', function( err ) { // Handle errors
      fs.unlink( dest ); // Delete the file async. (But we don't check the result)
      if ( cb ) {
        cb( err.message );
      }
    } );
  };

  // After the downloads are complete, write the report based on the metadata in the files.
  var writeReport = function() {

    // Aggregate results for each of the license types
    var compositeCode = {};
    var compositeMedia = {};

    // List of all of the repository names, so that we can detect which libraries are used by all-sims
    var simTitles = [];

    /**
     * Given an HTML text, find the title attribute by parsing for <title>
     * @param html
     * @returns {string}
     */
    var parseTitle = function( html ) {
      var startKey = '<title>';
      var endKey = '</title>';

      var startIndex = html.indexOf( startKey );
      var endIndex = html.indexOf( endKey );

      return html.substring( startIndex + startKey.length, endIndex ).trim();
    };

    /**
     * Add the source (images/audio/media or code) entries to the destination object, keyed by name.
     * @param {string} repositoryName - the name of the repository, such as 'energy-skate-park-basics'
     * @param {Object} source - the object from which to read the entry
     * @param {Object} destination - the object to which to append the entry
     */
    var augment = function( repositoryName, source, destination ) {
      for ( var entry in source ) {
        if ( source.hasOwnProperty( entry ) ) {
          if ( !destination.hasOwnProperty( entry ) ) {
            destination[ entry ] = source[ entry ];//overwrites
            destination[ entry ].usedBy = [];
          }
          destination[ entry ].usedBy.push( repositoryName );
        }
      }
    };

    grunt.file.recurse( 'downloaded-sims', function( abspath, rootdir, subdir, filename ) {
      if ( ChipperStringUtils.endsWith( filename.toLowerCase(), '.html' ) &&

           // shortcut to avoid looking at the -iframe.html files when testing on a build directory.
           !ChipperStringUtils.endsWith( filename.toLowerCase(), '-iframe.html' ) ) {

        // load the file into a string
        var html = grunt.file.read( abspath ).trim();

        if ( html.indexOf( 'was not found on this server' ) < 0 || html.indexOf( 'was not found on this server' ) > 400 ) {

          var startIndex = html.indexOf( ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES );
          var endIndex = html.indexOf( ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES );
          var substring = html.substring( startIndex, endIndex );

          var firstCurlyBrace = substring.indexOf( '{' );
          var lastCurlyBrace = substring.lastIndexOf( '}' );
          var jsonString = substring.substring( firstCurlyBrace, lastCurlyBrace + 1 );

          var json = JSON.parse( jsonString );

          var title = parseTitle( html );
          if ( !title || title.indexOf( 'undefined' ) === 0 || title.indexOf( 'TITLE' ) >= 0 ) {
            grunt.log.writeln( 'title not found for ' + abspath );
            title = filename;
          }
          augment( title, json.lib, compositeCode );
          augment( title, json.audio, compositeMedia );
          augment( title, json.images, compositeMedia );

          simTitles.push( title );
        }
      }
    } );

    // Sort to easily compare lists of repositoryNames with usedBy columns, to see which resources are used by everything.
    simTitles.sort();

    // If anything is used by every sim indicate that here
    for ( var entry in compositeCode ) {
      if ( compositeCode.hasOwnProperty( entry ) ) {
        compositeCode[ entry ].usedBy.sort();
        if ( _.isEqual( simTitles, compositeCode[ entry ].usedBy ) ) {
          compositeCode[ entry ].usedBy = 'all-sims';// Confusing that this matches a repo name, but since that repo isn't actually a sim, perhaps this will be ok
        }
      }
    }

    var json = grunt.file.readJSON( SHERPA + '/lib/license.json' );

    var entries = [];
    var codeLicensesUsed = [];
    var mediaLicensesUsed = [];

    // Get a list of the library names
    var libraries = [];
    for ( var lib in json ) {
      if ( json.hasOwnProperty( lib ) ) {
        libraries.push( lib );
      }
    }

    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    libraries.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Add info for each library to the MD report
    for ( var i = 0; i < libraries.length; i++ ) {
      var library = libraries[ i ];

      var lines = [
        '**' + library + '**',
        json[ library ].text.join( '
' ),
        json[ library ].projectURL,
        'License: [' + json[ library ].license + '](licenses/' + library + '.txt)',
        'Notes: ' + json[ library ].notes
      ];

      if ( json[ library ].dependencies ) {
        lines.push( 'Dependencies: **' + json[ library ].dependencies + '**' );
      }

      if ( compositeCode.hasOwnProperty( library ) && compositeCode[ library ].usedBy instanceof Array ) {
        lines.push( 'Used by: ' + compositeCode[ library ].usedBy.join( ', ' ) );
      }

      // \n worked well when viewing GitHub markdown as an issue comment, but for unknown reasons
 is necessary when
      // viewing from https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
      entries.push( lines.join( '
' ) );

      if ( codeLicensesUsed.indexOf( json[ library ].license ) < 0 ) {
        codeLicensesUsed.push( json[ library ].license );
      }
    }

    var mediaOutput = [];
    var mediaKeys = [];
    for ( var imageAudioEntry in compositeMedia ) {
      if ( compositeMedia.hasOwnProperty( imageAudioEntry ) ) {
        mediaKeys.push( imageAudioEntry );
      }
    }
    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    mediaKeys.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Create the text for the image and audio, and keep track of which licenses were used by them.
    for ( i = 0; i < mediaKeys.length; i++ ) {
      var mediaKey = mediaKeys[ i ];

      var copyrightStatement = compositeMedia[ mediaKey ].text.join( '
' ).trim();
      var projectURL = compositeMedia[ mediaKey ].projectURL.trim();

      if ( copyrightStatement.length === 0 ) {
        copyrightStatement = '(no text)';
      }
      if ( projectURL.length === 0 ) {
        projectURL = '(no project url)';
      }
      var notes = compositeMedia[ mediaKey ].notes.trim();
      if ( notes.length === 0 ) {
        notes = '(no notes)';
      }
      var license = compositeMedia[ mediaKey ].license.trim();
      if ( license.length === 0 ) {
        license = '(no license)';
      }
      var mediaEntryLines = [
        '**' + mediaKey + '**',
        copyrightStatement,
        projectURL,
        'License: ' + license,
        'Notes: ' + notes
      ];
      if ( compositeMedia[ mediaKey ].exception ) {
        mediaEntryLines.push( 'Exception: ' + compositeMedia[ mediaKey ].exception );
      }

      // TODO: is this a real license?  Why is there logic here but nowhere else?  Perhaps files annotated with
      // contact phethelp@colorado.edu are leaking into the HTML files unnecessarily
      if ( license !== 'contact phethelp@colorado.edu' ) {
        mediaOutput.push( mediaEntryLines.join( '
' ) );

        if ( mediaLicensesUsed.indexOf( license ) < 0 ) {
          mediaLicensesUsed.push( license );
        }
      }
    }

    // Summarize licenses used
    var fileList = simTitles.join( '\n* ' );

    var outputString =
      'This report enumerates the third-party resources (code, images, audio, etc) used in a set of simulations.\n' +
      '* [Third-party Code](#third-party-code)\n' +
      '* [Third-party Code License Summary](#third-party-code-license-summary)\n' +
      '* [Third-party Media](#third-party-media)\n' +
      '* [Third-party Media License Summary](#third-party-media-license-summary)\n' +
      '\n' +
      'This report is for the following simulations: \n\n* ' + fileList + '\n\nTo see the third party resources used in a particular published ' +
      'simulation, inspect the HTML file between the `' + ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES + '` and `' + ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES + '` ' +
      '(only exists in sim publications after Aug 7, 2015).\n' +
      '# Third-party Code:
\n' +
      entries.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Code License Summary:
\n' +
      codeLicensesUsed.join( '
' ) + '\n\n' +

      '---\n' +

      '# Third-party Media:
\n' +
      mediaOutput.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Media License Summary:
\n' +
      mediaLicensesUsed.join( '
' ) + '\n\n';

    // Compare the file output to the existing file, and write & git commit only if different
    if ( !grunt.file.exists( output ) || grunt.file.read( output ) !== outputString ) {
      grunt.log.writeln( 'File output changed, writing file ' + output );
      grunt.file.write( output, outputString );
    }
    else {
      grunt.log.writeln( output + ' contents are the same.  No need to save.' );
    }

    gruntDone();
  };

  // Download all of the simulations.  For simulations that are in "active-sims" but not published,
  // download them anyways but skip them in the report.
  var activeSimsString = fs.readFileSync( '../chipper/data/active-sims', 'utf-8' ).trim();
  var activeSimsArray = activeSimsString.split( '\n' );
  var count = 0;
  activeSimsArray.forEach( function( sim ) {
    var url = 'http://phet.colorado.edu/sims/html/' + sim + '/latest/' + sim + '_en.html';
    console.log( 'downloading ' + url );

    download( url, 'downloaded-sims/' + sim + '_en.html', function() {
      count++;
      console.log( 'Finished downloading: ' + count + '/' + activeSimsArray.length );
      if ( count === activeSimsArray.length ) {
        writeReport();
      }
    } );
  } );
};<commit_msg>Download files one at a time so we can make sure we get everything, see https://github.com/phetsims/sherpa/issues/64<commit_after>// Copyright 2015, University of Colorado Boulder

/**
 * Creates a composite report of all of the 3rd party images, code, audio and other media used by all of the PhET
 * Simulations which appear in a directory (required argument) and updates the online version by automatically adding and
 * pushing the changes to GitHub.  One of the reports is published at:
 * https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
 *
 * Usage:
 * cd chipper
 * grunt report-third-party
 * // then push the updated license file to sherpa
 *
 * NOTE: sometimes for unknown reasons a file is missed or skipped, please inspect the third-party-licenses.md before
 * committing to make sure it is complete.
 *
 * Third party entries are parsed from HTML, see getLicenseEntry.js
 *
 * If the --active-runnables grunt option is supplied, the task iterates over the active-runnables and copies each built HTML file
 * into the directory specified with --input before running the report.  If any HTML files are missing, the report will fail.
 * Before using this options, the developer should run `grunt-all.sh` to make sure all of the HTML files are up-to-date.
 * The directory is neither automatically created nor automatically cleaned, this is the responsibility of the developer.
 * (Note that if you fail to manually clean the directory, you may end up with stale HTML files).
 *
 * See https://github.com/phetsims/chipper/issues/162
 *
 * @author Sam Reid
 */

// Load shared constants
var ChipperConstants = require( '../../../chipper/js/common/ChipperConstants' );
var ChipperStringUtils = require( '../../../chipper/js/common/ChipperStringUtils' );

// constants
var SHERPA = '../sherpa';  // The relative path to sherpa, from the chipper path

// node modules
var fs = require( 'fs' );
var http = require( 'http' );

var _ = require( '../../../sherpa/lib/lodash-2.4.1.min' ); // eslint-disable-line require-statement-match

/**
 * @param grunt - the grunt instance
 */
module.exports = function( grunt ) {
  'use strict';

  // The file where the report will be written
  var output = '../sherpa/third-party-licenses.md';

  // Since we download the HTML files, this task must wait until it is complete
  var gruntDone = grunt.task.current.async();

  // Prepare a directory where the HTML files will be downloaded
  if ( grunt.file.exists( 'downloaded-sims' ) ) {
    grunt.file.delete( 'downloaded-sims' );
  }
  grunt.file.mkdir( 'downloaded-sims' );

  // Download a URL to a local file
  // http://stackoverflow.com/questions/11944932/how-to-download-a-file-with-node-js-without-using-third-party-libraries
  var download = function( url, dest, cb ) {
    var file = fs.createWriteStream( dest );
    http.get( url, function( response ) {
      response.pipe( file );
      file.on( 'finish', function() {
        file.close( cb );  // close() is async, call cb after close completes.
      } );
    } ).on( 'error', function( err ) { // Handle errors
      fs.unlink( dest ); // Delete the file async. (But we don't check the result)
      if ( cb ) {
        cb( err.message );
      }
    } );
  };

  // After the downloads are complete, write the report based on the metadata in the files.
  var writeReport = function() {

    // Aggregate results for each of the license types
    var compositeCode = {};
    var compositeMedia = {};

    // List of all of the repository names, so that we can detect which libraries are used by all-sims
    var simTitles = [];

    /**
     * Given an HTML text, find the title attribute by parsing for <title>
     * @param html
     * @returns {string}
     */
    var parseTitle = function( html ) {
      var startKey = '<title>';
      var endKey = '</title>';

      var startIndex = html.indexOf( startKey );
      var endIndex = html.indexOf( endKey );

      return html.substring( startIndex + startKey.length, endIndex ).trim();
    };

    /**
     * Add the source (images/audio/media or code) entries to the destination object, keyed by name.
     * @param {string} repositoryName - the name of the repository, such as 'energy-skate-park-basics'
     * @param {Object} source - the object from which to read the entry
     * @param {Object} destination - the object to which to append the entry
     */
    var augment = function( repositoryName, source, destination ) {
      for ( var entry in source ) {
        if ( source.hasOwnProperty( entry ) ) {
          if ( !destination.hasOwnProperty( entry ) ) {
            destination[ entry ] = source[ entry ];//overwrites
            destination[ entry ].usedBy = [];
          }
          destination[ entry ].usedBy.push( repositoryName );
        }
      }
    };

    grunt.file.recurse( 'downloaded-sims', function( abspath, rootdir, subdir, filename ) {
      if ( ChipperStringUtils.endsWith( filename.toLowerCase(), '.html' ) &&

           // shortcut to avoid looking at the -iframe.html files when testing on a build directory.
           !ChipperStringUtils.endsWith( filename.toLowerCase(), '-iframe.html' ) ) {

        // load the file into a string
        var html = grunt.file.read( abspath ).trim();

        if ( html.indexOf( 'was not found on this server' ) < 0 || html.indexOf( 'was not found on this server' ) > 400 ) {

          var startIndex = html.indexOf( ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES );
          var endIndex = html.indexOf( ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES );
          var substring = html.substring( startIndex, endIndex );

          var firstCurlyBrace = substring.indexOf( '{' );
          var lastCurlyBrace = substring.lastIndexOf( '}' );
          var jsonString = substring.substring( firstCurlyBrace, lastCurlyBrace + 1 );

          var json = JSON.parse( jsonString );

          var title = parseTitle( html );
          if ( !title || title.indexOf( 'undefined' ) === 0 || title.indexOf( 'TITLE' ) >= 0 ) {
            grunt.log.writeln( 'title not found for ' + abspath );
            title = filename;
          }
          augment( title, json.lib, compositeCode );
          augment( title, json.audio, compositeMedia );
          augment( title, json.images, compositeMedia );

          simTitles.push( title );
        }
      }
    } );

    // Sort to easily compare lists of repositoryNames with usedBy columns, to see which resources are used by everything.
    simTitles.sort();

    // If anything is used by every sim indicate that here
    for ( var entry in compositeCode ) {
      if ( compositeCode.hasOwnProperty( entry ) ) {
        compositeCode[ entry ].usedBy.sort();
        if ( _.isEqual( simTitles, compositeCode[ entry ].usedBy ) ) {
          compositeCode[ entry ].usedBy = 'all-sims';// Confusing that this matches a repo name, but since that repo isn't actually a sim, perhaps this will be ok
        }
      }
    }

    var json = grunt.file.readJSON( SHERPA + '/lib/license.json' );

    var entries = [];
    var codeLicensesUsed = [];
    var mediaLicensesUsed = [];

    // Get a list of the library names
    var libraries = [];
    for ( var lib in json ) {
      if ( json.hasOwnProperty( lib ) ) {
        libraries.push( lib );
      }
    }

    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    libraries.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Add info for each library to the MD report
    for ( var i = 0; i < libraries.length; i++ ) {
      var library = libraries[ i ];

      var lines = [
        '**' + library + '**',
        json[ library ].text.join( '
' ),
        json[ library ].projectURL,
        'License: [' + json[ library ].license + '](licenses/' + library + '.txt)',
        'Notes: ' + json[ library ].notes
      ];

      if ( json[ library ].dependencies ) {
        lines.push( 'Dependencies: **' + json[ library ].dependencies + '**' );
      }

      if ( compositeCode.hasOwnProperty( library ) && compositeCode[ library ].usedBy instanceof Array ) {
        lines.push( 'Used by: ' + compositeCode[ library ].usedBy.join( ', ' ) );
      }

      // \n worked well when viewing GitHub markdown as an issue comment, but for unknown reasons
 is necessary when
      // viewing from https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
      entries.push( lines.join( '
' ) );

      if ( codeLicensesUsed.indexOf( json[ library ].license ) < 0 ) {
        codeLicensesUsed.push( json[ library ].license );
      }
    }

    var mediaOutput = [];
    var mediaKeys = [];
    for ( var imageAudioEntry in compositeMedia ) {
      if ( compositeMedia.hasOwnProperty( imageAudioEntry ) ) {
        mediaKeys.push( imageAudioEntry );
      }
    }
    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    mediaKeys.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Create the text for the image and audio, and keep track of which licenses were used by them.
    for ( i = 0; i < mediaKeys.length; i++ ) {
      var mediaKey = mediaKeys[ i ];

      var copyrightStatement = compositeMedia[ mediaKey ].text.join( '
' ).trim();
      var projectURL = compositeMedia[ mediaKey ].projectURL.trim();

      if ( copyrightStatement.length === 0 ) {
        copyrightStatement = '(no text)';
      }
      if ( projectURL.length === 0 ) {
        projectURL = '(no project url)';
      }
      var notes = compositeMedia[ mediaKey ].notes.trim();
      if ( notes.length === 0 ) {
        notes = '(no notes)';
      }
      var license = compositeMedia[ mediaKey ].license.trim();
      if ( license.length === 0 ) {
        license = '(no license)';
      }
      var mediaEntryLines = [
        '**' + mediaKey + '**',
        copyrightStatement,
        projectURL,
        'License: ' + license,
        'Notes: ' + notes
      ];
      if ( compositeMedia[ mediaKey ].exception ) {
        mediaEntryLines.push( 'Exception: ' + compositeMedia[ mediaKey ].exception );
      }

      // TODO: is this a real license?  Why is there logic here but nowhere else?  Perhaps files annotated with
      // contact phethelp@colorado.edu are leaking into the HTML files unnecessarily
      if ( license !== 'contact phethelp@colorado.edu' ) {
        mediaOutput.push( mediaEntryLines.join( '
' ) );

        if ( mediaLicensesUsed.indexOf( license ) < 0 ) {
          mediaLicensesUsed.push( license );
        }
      }
    }

    // Summarize licenses used
    var fileList = simTitles.join( '\n* ' );

    var outputString =
      'This report enumerates the third-party resources (code, images, audio, etc) used in a set of simulations.\n' +
      '* [Third-party Code](#third-party-code)\n' +
      '* [Third-party Code License Summary](#third-party-code-license-summary)\n' +
      '* [Third-party Media](#third-party-media)\n' +
      '* [Third-party Media License Summary](#third-party-media-license-summary)\n' +
      '\n' +
      'This report is for the following simulations: \n\n* ' + fileList + '\n\nTo see the third party resources used in a particular published ' +
      'simulation, inspect the HTML file between the `' + ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES + '` and `' + ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES + '` ' +
      '(only exists in sim publications after Aug 7, 2015).\n' +
      '# Third-party Code:
\n' +
      entries.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Code License Summary:
\n' +
      codeLicensesUsed.join( '
' ) + '\n\n' +

      '---\n' +

      '# Third-party Media:
\n' +
      mediaOutput.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Media License Summary:
\n' +
      mediaLicensesUsed.join( '
' ) + '\n\n';

    // Compare the file output to the existing file, and write & git commit only if different
    if ( !grunt.file.exists( output ) || grunt.file.read( output ) !== outputString ) {
      grunt.log.writeln( 'File output changed, writing file ' + output );
      grunt.file.write( output, outputString );
    }
    else {
      grunt.log.writeln( output + ' contents are the same.  No need to save.' );
    }

    gruntDone();
  };

  // Download all of the simulations.  For simulations that are in "active-sims" but not published,
  // download them anyways but skip them in the report.
  var activeSimsString = fs.readFileSync( '../chipper/data/active-sims', 'utf-8' ).trim();
  var activeSimsArray = activeSimsString.split( '\n' );

  // Download files one at a time so we can make sure we get everything.
  var downloadNext = function( index ) {
    var sim = activeSimsArray[ index ];

    var url = 'http://phet.colorado.edu/sims/html/' + sim + '/latest/' + sim + '_en.html';
    console.log( 'downloading ' + (index + 1) + '/' + activeSimsArray.length + ': ' + url );

    download( url, 'downloaded-sims/' + sim + '_en.html', function() {
      var next = index + 1;

      if ( next < activeSimsArray.length ) {
        downloadNext( next );
      }
      else {
        writeReport();
      }
    } );
  };

  downloadNext( 0 );

  activeSimsArray.forEach( function( sim ) {

  } );
};<|endoftext|>


<commit_before>/*
 * JavaScript Load Image Orientation
 * https://github.com/blueimp/JavaScript-Load-Image
 *
 * Copyright 2013, Sebastian Tschan
 * https://blueimp.net
 *
 * Licensed under the MIT license:
 * https://opensource.org/licenses/MIT
 */

/*

Exif orientation values to correctly display the letter F:

  1       2       3       4          5           6           7           8

██████  ██████      ██  ██      ██████████  ██                  ██  ██████████
██          ██      ██  ██      ██  ██      ██  ██          ██  ██      ██  ██
████      ████    ████  ████    ██          ██████████  ██████████          ██
██          ██      ██  ██
██          ██  ██████  ██████

*/

/* global define, module, require */

;(function (factory) {
  'use strict'
  if (typeof define === 'function' && define.amd) {
    // Register as an anonymous AMD module:
    define(['./load-image', './load-image-scale', './load-image-meta'], factory)
  } else if (typeof module === 'object' && module.exports) {
    factory(
      require('./load-image'),
      require('./load-image-scale'),
      require('./load-image-meta')
    )
  } else {
    // Browser globals:
    factory(window.loadImage)
  }
})(function (loadImage) {
  'use strict'

  var originalTransform = loadImage.transform
  var originalHasCanvasOption = loadImage.hasCanvasOption
  var originalHasMetaOption = loadImage.hasMetaOption
  var originalTransformCoordinates = loadImage.transformCoordinates
  var originalGetTransformedOptions = loadImage.getTransformedOptions

  ;(function () {
    // black 2x1 JPEG, with the following meta information set:
    // - EXIF Orientation: 6 (Rotated 90° CCW)
    var testImageURL =
      'data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' +
      'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' +
      'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' +
      'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/x' +
      'ABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAA' +
      'AAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q=='
    var img = document.createElement('img')
    img.onload = function () {
      // Check if browser supports automatic image orientation:
      loadImage.orientation = img.width === 1 && img.height === 2
    }
    img.src = testImageURL
  })()

  loadImage.transform = function (img, options, callback, file, data) {
    originalTransform.call(
      loadImage,
      img,
      options,
      function (img, data) {
        if (data) {
          var exifOrientation = data.exif && data.exif.get('Orientation')
          if (
            loadImage.orientation &&
            exifOrientation > 4 &&
            exifOrientation < 9
          ) {
            // Automatic image orientation switched image dimensions
            var originalWidth = data.originalWidth
            var originalHeight = data.originalHeight
            data.originalWidth = originalHeight
            data.originalHeight = originalWidth
          }
        }
        callback(img, data)
      },
      file,
      data
    )
  }

  // Determines if the target image should be a canvas element:
  loadImage.hasCanvasOption = function (options) {
    return (
      (!!options.orientation === true && !loadImage.orientation) ||
      (options.orientation > 1 && options.orientation < 9) ||
      originalHasCanvasOption.call(loadImage, options)
    )
  }

  // Determines if meta data should be loaded automatically:
  loadImage.hasMetaOption = function (options) {
    return (
      (options && options.orientation === true && !loadImage.orientation) ||
      originalHasMetaOption.call(loadImage, options)
    )
  }

  // Transform image orientation based on the given EXIF orientation option:
  loadImage.transformCoordinates = function (canvas, options) {
    originalTransformCoordinates.call(loadImage, canvas, options)
    var orientation = options.orientation
    if (!(orientation > 1 && orientation < 9)) {
      return
    }
    var ctx = canvas.getContext('2d')
    var width = canvas.width
    var height = canvas.height
    var styleWidth = canvas.style.width
    var styleHeight = canvas.style.height
    if (orientation > 4) {
      canvas.width = height
      canvas.height = width
      canvas.style.width = styleHeight
      canvas.style.height = styleWidth
    }
    switch (orientation) {
      case 2:
        // horizontal flip
        ctx.translate(width, 0)
        ctx.scale(-1, 1)
        break
      case 3:
        // 180° rotate left
        ctx.translate(width, height)
        ctx.rotate(Math.PI)
        break
      case 4:
        // vertical flip
        ctx.translate(0, height)
        ctx.scale(1, -1)
        break
      case 5:
        // vertical flip + 90° rotate right
        ctx.rotate(0.5 * Math.PI)
        ctx.scale(1, -1)
        break
      case 6:
        // 90° rotate right
        ctx.rotate(0.5 * Math.PI)
        ctx.translate(0, -height)
        break
      case 7:
        // horizontal flip + 90° rotate right
        ctx.rotate(0.5 * Math.PI)
        ctx.translate(width, -height)
        ctx.scale(-1, 1)
        break
      case 8:
        // 90° rotate left
        ctx.rotate(-0.5 * Math.PI)
        ctx.translate(-width, 0)
        break
    }
  }

  // Transforms coordinate and dimension options
  // based on the given orientation option:
  loadImage.getTransformedOptions = function (img, opts, data) {
    var options = originalGetTransformedOptions.call(loadImage, img, opts)
    var orientation = options.orientation
    var newOptions
    var i
    if (orientation === true) {
      if (loadImage.orientation) {
        // Browser supports automatic image orientation
        return options
      }
      orientation = data && data.exif && data.exif.get('Orientation')
    }
    if (!(orientation > 1 && orientation < 9)) {
      return options
    }
    newOptions = {}
    for (i in options) {
      if (Object.prototype.hasOwnProperty.call(options, i)) {
        newOptions[i] = options[i]
      }
    }
    newOptions.orientation = orientation
    switch (orientation) {
      case 2:
        // horizontal flip
        newOptions.left = options.right
        newOptions.right = options.left
        break
      case 3:
        // 180° rotate left
        newOptions.left = options.right
        newOptions.top = options.bottom
        newOptions.right = options.left
        newOptions.bottom = options.top
        break
      case 4:
        // vertical flip
        newOptions.top = options.bottom
        newOptions.bottom = options.top
        break
      case 5:
        // vertical flip + 90° rotate right
        newOptions.left = options.top
        newOptions.top = options.left
        newOptions.right = options.bottom
        newOptions.bottom = options.right
        break
      case 6:
        // 90° rotate right
        newOptions.left = options.top
        newOptions.top = options.right
        newOptions.right = options.bottom
        newOptions.bottom = options.left
        break
      case 7:
        // horizontal flip + 90° rotate right
        newOptions.left = options.bottom
        newOptions.top = options.right
        newOptions.right = options.top
        newOptions.bottom = options.left
        break
      case 8:
        // 90° rotate left
        newOptions.left = options.bottom
        newOptions.top = options.left
        newOptions.right = options.top
        newOptions.bottom = options.right
        break
    }
    if (newOptions.orientation > 4) {
      newOptions.maxWidth = options.maxHeight
      newOptions.maxHeight = options.maxWidth
      newOptions.minWidth = options.minHeight
      newOptions.minHeight = options.minWidth
      newOptions.sourceWidth = options.sourceHeight
      newOptions.sourceHeight = options.sourceWidth
    }
    return newOptions
  }
})
<commit_msg>Move transformCoordinates declaration to the end.<commit_after>/*
 * JavaScript Load Image Orientation
 * https://github.com/blueimp/JavaScript-Load-Image
 *
 * Copyright 2013, Sebastian Tschan
 * https://blueimp.net
 *
 * Licensed under the MIT license:
 * https://opensource.org/licenses/MIT
 */

/*

Exif orientation values to correctly display the letter F:

  1       2       3       4          5           6           7           8

██████  ██████      ██  ██      ██████████  ██                  ██  ██████████
██          ██      ██  ██      ██  ██      ██  ██          ██  ██      ██  ██
████      ████    ████  ████    ██          ██████████  ██████████          ██
██          ██      ██  ██
██          ██  ██████  ██████

*/

/* global define, module, require */

;(function (factory) {
  'use strict'
  if (typeof define === 'function' && define.amd) {
    // Register as an anonymous AMD module:
    define(['./load-image', './load-image-scale', './load-image-meta'], factory)
  } else if (typeof module === 'object' && module.exports) {
    factory(
      require('./load-image'),
      require('./load-image-scale'),
      require('./load-image-meta')
    )
  } else {
    // Browser globals:
    factory(window.loadImage)
  }
})(function (loadImage) {
  'use strict'

  var originalTransform = loadImage.transform
  var originalHasCanvasOption = loadImage.hasCanvasOption
  var originalHasMetaOption = loadImage.hasMetaOption
  var originalTransformCoordinates = loadImage.transformCoordinates
  var originalGetTransformedOptions = loadImage.getTransformedOptions

  ;(function () {
    // black 2x1 JPEG, with the following meta information set:
    // - EXIF Orientation: 6 (Rotated 90° CCW)
    var testImageURL =
      'data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' +
      'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' +
      'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' +
      'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/x' +
      'ABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAA' +
      'AAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q=='
    var img = document.createElement('img')
    img.onload = function () {
      // Check if browser supports automatic image orientation:
      loadImage.orientation = img.width === 1 && img.height === 2
    }
    img.src = testImageURL
  })()

  loadImage.transform = function (img, options, callback, file, data) {
    originalTransform.call(
      loadImage,
      img,
      options,
      function (img, data) {
        if (data) {
          var exifOrientation = data.exif && data.exif.get('Orientation')
          if (
            loadImage.orientation &&
            exifOrientation > 4 &&
            exifOrientation < 9
          ) {
            // Automatic image orientation switched image dimensions
            var originalWidth = data.originalWidth
            var originalHeight = data.originalHeight
            data.originalWidth = originalHeight
            data.originalHeight = originalWidth
          }
        }
        callback(img, data)
      },
      file,
      data
    )
  }

  // Determines if the target image should be a canvas element:
  loadImage.hasCanvasOption = function (options) {
    return (
      (!!options.orientation === true && !loadImage.orientation) ||
      (options.orientation > 1 && options.orientation < 9) ||
      originalHasCanvasOption.call(loadImage, options)
    )
  }

  // Determines if meta data should be loaded automatically:
  loadImage.hasMetaOption = function (options) {
    return (
      (options && options.orientation === true && !loadImage.orientation) ||
      originalHasMetaOption.call(loadImage, options)
    )
  }

  // Transforms coordinate and dimension options
  // based on the given orientation option:
  loadImage.getTransformedOptions = function (img, opts, data) {
    var options = originalGetTransformedOptions.call(loadImage, img, opts)
    var orientation = options.orientation
    var newOptions
    var i
    if (orientation === true) {
      if (loadImage.orientation) {
        // Browser supports automatic image orientation
        return options
      }
      orientation = data && data.exif && data.exif.get('Orientation')
    }
    if (!(orientation > 1 && orientation < 9)) {
      return options
    }
    newOptions = {}
    for (i in options) {
      if (Object.prototype.hasOwnProperty.call(options, i)) {
        newOptions[i] = options[i]
      }
    }
    newOptions.orientation = orientation
    switch (orientation) {
      case 2:
        // horizontal flip
        newOptions.left = options.right
        newOptions.right = options.left
        break
      case 3:
        // 180° rotate left
        newOptions.left = options.right
        newOptions.top = options.bottom
        newOptions.right = options.left
        newOptions.bottom = options.top
        break
      case 4:
        // vertical flip
        newOptions.top = options.bottom
        newOptions.bottom = options.top
        break
      case 5:
        // vertical flip + 90° rotate right
        newOptions.left = options.top
        newOptions.top = options.left
        newOptions.right = options.bottom
        newOptions.bottom = options.right
        break
      case 6:
        // 90° rotate right
        newOptions.left = options.top
        newOptions.top = options.right
        newOptions.right = options.bottom
        newOptions.bottom = options.left
        break
      case 7:
        // horizontal flip + 90° rotate right
        newOptions.left = options.bottom
        newOptions.top = options.right
        newOptions.right = options.top
        newOptions.bottom = options.left
        break
      case 8:
        // 90° rotate left
        newOptions.left = options.bottom
        newOptions.top = options.left
        newOptions.right = options.top
        newOptions.bottom = options.right
        break
    }
    if (newOptions.orientation > 4) {
      newOptions.maxWidth = options.maxHeight
      newOptions.maxHeight = options.maxWidth
      newOptions.minWidth = options.minHeight
      newOptions.minHeight = options.minWidth
      newOptions.sourceWidth = options.sourceHeight
      newOptions.sourceHeight = options.sourceWidth
    }
    return newOptions
  }

  // Transform image orientation based on the given EXIF orientation option:
  loadImage.transformCoordinates = function (canvas, options) {
    originalTransformCoordinates.call(loadImage, canvas, options)
    var orientation = options.orientation
    if (!(orientation > 1 && orientation < 9)) {
      return
    }
    var ctx = canvas.getContext('2d')
    var width = canvas.width
    var height = canvas.height
    var styleWidth = canvas.style.width
    var styleHeight = canvas.style.height
    if (orientation > 4) {
      canvas.width = height
      canvas.height = width
      canvas.style.width = styleHeight
      canvas.style.height = styleWidth
    }
    switch (orientation) {
      case 2:
        // horizontal flip
        ctx.translate(width, 0)
        ctx.scale(-1, 1)
        break
      case 3:
        // 180° rotate left
        ctx.translate(width, height)
        ctx.rotate(Math.PI)
        break
      case 4:
        // vertical flip
        ctx.translate(0, height)
        ctx.scale(1, -1)
        break
      case 5:
        // vertical flip + 90° rotate right
        ctx.rotate(0.5 * Math.PI)
        ctx.scale(1, -1)
        break
      case 6:
        // 90° rotate right
        ctx.rotate(0.5 * Math.PI)
        ctx.translate(0, -height)
        break
      case 7:
        // horizontal flip + 90° rotate right
        ctx.rotate(0.5 * Math.PI)
        ctx.translate(width, -height)
        ctx.scale(-1, 1)
        break
      case 8:
        // 90° rotate left
        ctx.rotate(-0.5 * Math.PI)
        ctx.translate(-width, 0)
        break
    }
  }
})
<|endoftext|>


<commit_before>$(function() {

  // Storage of note model/properties.
  var notes = {};

  var currentQuery;
  var lgQuery = window.matchMedia("(min-width:1200px)");
  var mdQuery = window.matchMedia("(min-width:980px) and (max-width:1199px)");
  var smQuery = window.matchMedia("(min-width:768px) and (max-width:979px)");
  var xsQuery = window.matchMedia("(max-width:767px)");

  var resizeTimer; //set resizeTimer to empty so it resets on page load

  var init = function() {
    findNotes();
    //on resize, run the function and reset the timeout
    //250 is the delay in milliseconds. Can be adjust as you see fit.
    $(window).resize(function() {
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(checkSize, 250);
    });
  }

  var findNotes = function() {
    $(".DC-note-container").each(function() {
      getCoordinates(this); /*swapURL(this);*/
    });
  }

  //link notes to wapo embedder pages instead of document cloud
  //var swapURL = function(note) {
  //    var note = $(note);
  //    if (note.data('wapo')) {
  //        var newLink = note.data('wapo');
  //        note.find('a').attr('href', newLink);
  //        note.find('a').attr('target', '_blank');
  //    }
  //}

  // gets default/canonical positioning data for note & stores it.
  var getCoordinates = function(noteEl) {
    var noteID = $(noteEl).attr('id');

    var wrapWidth = $("#" + noteID + " .DC-note-excerpt").width(),
      docWidth = $("#" + noteID + " .DC-note-image").width(),
      docTop = $("#" + noteID + " .DC-note-image").position().top,
      lCoverWidth = $("#" + noteID + " .DC-left-cover").width(),
      rCoverWidth = $("#" + noteID + " .DC-right-cover").width(),
      excerptHeight = $("#" + noteID + " .DC-note-excerpt").height(),
      excerptLeft = $("#" + noteID + " .DC-note-excerpt").position().left;
    excerptWidth = $("#" + noteID + " .DC-note-excerpt").width();

    if (docWidth != 0 && !(notes[noteID])) { //don't store notes that are set to display none
      var x1 = lCoverWidth,
        y1 = (docTop) * -1;

      var x2 = x1 + (750 - (lCoverWidth + rCoverWidth)),
        y2 = y1 + excerptHeight;

      var noteWidth = (x2 - x1);

      var note = {
        x1: x1,
        x2: x2,
        y1: y1,
        y2: y2,
        noteID: noteID,
        wrapWidth: wrapWidth,
        docWidth: docWidth,
        docTop: docTop,
        lCoverWidth: lCoverWidth,
        rCoverWidth: rCoverWidth,
        excerptHeight: excerptHeight,
        excerptLeft: excerptLeft,
        excerptWidth: excerptWidth,
        noteWidth: noteWidth
      }

      notes[noteID] = note;
    }
    setMatchMedia();
  }

  var setMatchMedia = function() {
    //initial call of handleMatchMedia for each breakpoint
    handleMatchMedia(lgQuery);
    handleMatchMedia(mdQuery);
    handleMatchMedia(smQuery);
    handleMatchMedia(xsQuery);

    //add listeners for when viewport size changes
    lgQuery.addListener(handleMatchMedia);
    mdQuery.addListener(handleMatchMedia);
    smQuery.addListener(handleMatchMedia);
    xsQuery.addListener(handleMatchMedia);

    //orientation change listener
    window.addEventListener("orientationchange", function() {
      console.debug("orientation change = " + window.orientation);
      checkSize();
    }, false);
  }

  var handleMatchMedia = function(mediaQuery) {
    if (mediaQuery.matches) {
      console.log("mediaQuery.media = " + mediaQuery.media);
      currentQuery = mediaQuery.media;
      checkSize();
    }
  }

  var checkSize = function() {
    $.each(notes, function() {
      var noteElem = $("#" + this.noteID); // the notes lookup should just cache the element.
      var newWrapWidth = noteElem.find('.DC-note-excerpt-wrap').width(); // Get viewable width
      var checkWidth = this.noteWidth / newWrapWidth; // calculate the ration of the note's width to the veiwable width

      // If we're at the smallest media width size just resize the doc/note
      // otherwise check whether the note is larger or smaller than the viewerable width
      if (currentQuery != xsQuery.media) {
        if (checkWidth > 1) { // note width is larger than viewable width
          resizeDoc(this.noteID);
        } else if (checkWidth < 1) { // note width is smaller than viewable width
          restoreDoc(this.noteID);
        }
      } else {
        resizeDoc(this.noteID);
      }
    });
  }

  var resizeDoc = function(noteID) {
    //original values
    var natNoteWidth = notes[noteID].noteWidth;
    var natWrapWidth = notes[noteID].wrapWidth;
    var natDocWidth  = notes[noteID].docWidth;

    //new values after resize of browser
    var newWrapWidth = $("#" + noteID + " .DC-note-excerpt-wrap").width();

    //calculate new width of document image
    var w             = (natNoteWidth - newWrapWidth); // delta between natural width & current viewable width (e.g. the # of pixels cut out of the viewable area.)
    var a             = w / natNoteWidth;              // fraction of image that has been cutoff
    var x             = (natNoteWidth / natDocWidth);  // note width as fraction of document width
    var b             = a * x;                         // ???? this is the viewable cutoff as a fraction of document (we think)
    var newDocWidth   = (natDocWidth * (1 - b)) - 70;  // scale natural doc width by 1 - viewable cutoff as fraction of document (minus margin?)
    //var newDocWidth = natDocWidth-(w*x);

    var z = newDocWidth / natDocWidth;
    var newExcerptWidth = (notes[noteID].excerptWidth) * z;
    var newExcerptHeight = (notes[noteID].excerptHeight) * z;

    //new x1,x2,y1,y2
    var newX1 = (notes[noteID].x1) * z;
    var newX2 = (notes[noteID].x2) * z;

    //apply calculations to elements
    $("#" + noteID + " .DC-note-image").width(newDocWidth);
    $("#" + noteID + " .DC-note-image").css('top', (notes[noteID].docTop) * z);
    $("#" + noteID + " .DC-note-excerpt").width(newExcerptWidth);
    $("#" + noteID + " .DC-note-excerpt").height(newExcerptHeight);
    $("#" + noteID + " .DC-note-excerpt").css('left', -1 * newX1);
    $("#" + noteID + " .DC-left-cover").width(newX1);
    $("#" + noteID + " .DC-right-cover").width(newExcerptWidth - newX2);
    $("#" + noteID + " .DC-left-cover").height(newExcerptHeight);
    $("#" + noteID + " .DC-right-cover").height(newExcerptHeight);
  }

  var restoreDoc = function(noteID) {
    $("#" + noteID + " .DC-note-image").width(notes[noteID].docWidth);
    $("#" + noteID + " .DC-note-image").css('top', notes[noteID].docTop);
    $("#" + noteID + " .DC-note-excerpt").width(notes[noteID].excerptWidth);
    $("#" + noteID + " .DC-note-excerpt").height(notes[noteID].excerptHeight);
    $("#" + noteID + " .DC-note-excerpt").css('left', notes[noteID].excerptLeft);
    $("#" + noteID + " .DC-left-cover").width(notes[noteID].lCoverWidth);
    $("#" + noteID + " .DC-right-cover").width(notes[noteID].rCoverWidth);
    $("#" + noteID + " .DC-left-cover").height(notes[noteID].excerptHeight);
    $("#" + noteID + " .DC-right-cover").height(notes[noteID].excerptHeight);
  }

  //footnote click
  $(document).on('click', '.link-fn', function() {
    var fnLink = $(this);
    var fnLinkIcon = $(this).find('.fa');
    var fnID = $(this).data('fn');
    var footnote = $('#footnote' + fnID);
    if (footnote.hasClass('show')) {
      footnote.removeClass('show');
      footnote.slideUp({
        duration: 800,
        easing: 'swing'
      });
      fnLink.removeClass('reverse');
    } else {
      footnote.addClass('show');
      footnote.slideDown({
        duration: 800,
        easing: 'swing'
      });
      fnLink.addClass('reverse');
      //add note to notes and run subsequent functions
      var note = $(footnote).find('.DC-note-container');
      getCoords(note);
    }
    if (fnLinkIcon.hasClass('fa-caret-down')) {
      fnLinkIcon.removeClass('fa-caret-down').addClass('fa-caret-up');
    } else {
      fnLinkIcon.removeClass('fa-caret-up').addClass('fa-caret-down');
    }
  });

  $(window).load(function() {
    init();
  });

});
<commit_msg>testing math (formula simplification)<commit_after>$(function() {

  // Storage of note model/properties.
  var notes = {};

  var currentQuery;
  var lgQuery = window.matchMedia("(min-width:1200px)");
  var mdQuery = window.matchMedia("(min-width:980px) and (max-width:1199px)");
  var smQuery = window.matchMedia("(min-width:768px) and (max-width:979px)");
  var xsQuery = window.matchMedia("(max-width:767px)");

  var resizeTimer; //set resizeTimer to empty so it resets on page load

  var init = function() {
    findNotes();
    //on resize, run the function and reset the timeout
    //250 is the delay in milliseconds. Can be adjust as you see fit.
    $(window).resize(function() {
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(checkSize, 250);
    });
  }

  var findNotes = function() {
    $(".DC-note-container").each(function() {
      getCoordinates(this); /*swapURL(this);*/
    });
  }

  //link notes to wapo embedder pages instead of document cloud
  //var swapURL = function(note) {
  //    var note = $(note);
  //    if (note.data('wapo')) {
  //        var newLink = note.data('wapo');
  //        note.find('a').attr('href', newLink);
  //        note.find('a').attr('target', '_blank');
  //    }
  //}

  // gets default/canonical positioning data for note & stores it.
  var getCoordinates = function(noteEl) {
    var noteID = $(noteEl).attr('id');

    var wrapWidth = $("#" + noteID + " .DC-note-excerpt").width(),
      docWidth = $("#" + noteID + " .DC-note-image").width(),
      docTop = $("#" + noteID + " .DC-note-image").position().top,
      lCoverWidth = $("#" + noteID + " .DC-left-cover").width(),
      rCoverWidth = $("#" + noteID + " .DC-right-cover").width(),
      excerptHeight = $("#" + noteID + " .DC-note-excerpt").height(),
      excerptLeft = $("#" + noteID + " .DC-note-excerpt").position().left;
    excerptWidth = $("#" + noteID + " .DC-note-excerpt").width();

    if (docWidth != 0 && !(notes[noteID])) { //don't store notes that are set to display none
      var x1 = lCoverWidth,
        y1 = (docTop) * -1;

      var x2 = x1 + (750 - (lCoverWidth + rCoverWidth)),
        y2 = y1 + excerptHeight;

      var noteWidth = (x2 - x1);

      var note = {
        x1: x1,
        x2: x2,
        y1: y1,
        y2: y2,
        noteID: noteID,
        wrapWidth: wrapWidth,
        docWidth: docWidth,
        docTop: docTop,
        lCoverWidth: lCoverWidth,
        rCoverWidth: rCoverWidth,
        excerptHeight: excerptHeight,
        excerptLeft: excerptLeft,
        excerptWidth: excerptWidth,
        noteWidth: noteWidth
      }

      notes[noteID] = note;
    }
    setMatchMedia();
  }

  var setMatchMedia = function() {
    //initial call of handleMatchMedia for each breakpoint
    handleMatchMedia(lgQuery);
    handleMatchMedia(mdQuery);
    handleMatchMedia(smQuery);
    handleMatchMedia(xsQuery);

    //add listeners for when viewport size changes
    lgQuery.addListener(handleMatchMedia);
    mdQuery.addListener(handleMatchMedia);
    smQuery.addListener(handleMatchMedia);
    xsQuery.addListener(handleMatchMedia);

    //orientation change listener
    window.addEventListener("orientationchange", function() {
      console.debug("orientation change = " + window.orientation);
      checkSize();
    }, false);
  }

  var handleMatchMedia = function(mediaQuery) {
    if (mediaQuery.matches) {
      console.log("mediaQuery.media = " + mediaQuery.media);
      currentQuery = mediaQuery.media;
      checkSize();
    }
  }

  var checkSize = function() {
    $.each(notes, function() {
      var noteElem = $("#" + this.noteID); // the notes lookup should just cache the element.
      var newWrapWidth = noteElem.find('.DC-note-excerpt-wrap').width(); // Get viewable width
      var checkWidth = this.noteWidth / newWrapWidth; // calculate the ration of the note's width to the veiwable width

      // If we're at the smallest media width size just resize the doc/note
      // otherwise check whether the note is larger or smaller than the viewerable width
      if (currentQuery != xsQuery.media) {
        if (checkWidth > 1) { // note width is larger than viewable width
          resizeDoc(this.noteID);
        } else if (checkWidth < 1) { // note width is smaller than viewable width
          restoreDoc(this.noteID);
        }
      } else {
        resizeDoc(this.noteID);
      }
    });
  }

  var resizeDoc = function(noteID) {
    //original values
    var natNoteWidth = notes[noteID].noteWidth;
    var natWrapWidth = notes[noteID].wrapWidth;
    var natDocWidth  = notes[noteID].docWidth;

    //new values after resize of browser
    var newWrapWidth = $("#" + noteID + " .DC-note-excerpt-wrap").width();

    //calculate new width of document image
    var w             = (natNoteWidth - newWrapWidth); // delta between natural width & current viewable width (e.g. the # of pixels cut out of the viewable area.)
    var a             = w / natNoteWidth;              // fraction of image that has been cutoff
    var x             = (natNoteWidth / natDocWidth);  // note width as fraction of document width
    var b             = a * x;                         // ???? this is the viewable cutoff as a fraction of document (we think)
    var newDocWidth   = (natDocWidth * (1 - b)) - 70;  // scale natural doc width by 1 - viewable cutoff as fraction of document (minus margin?)
    //var newDocWidth = natDocWidth-(w*x);

    extraNewDocWidth = natDocWidth - natNoteWidth + newWrapWidth - 70; // this is the simplifed version of the formulae above.
    console.log("resize doc:", newDocWidth, extraNewDocWidth);

    var z = newDocWidth / natDocWidth;
    var newExcerptWidth = (notes[noteID].excerptWidth) * z;
    var newExcerptHeight = (notes[noteID].excerptHeight) * z;

    //new x1,x2,y1,y2
    var newX1 = (notes[noteID].x1) * z;
    var newX2 = (notes[noteID].x2) * z;

    //apply calculations to elements
    $("#" + noteID + " .DC-note-image").width(newDocWidth);
    $("#" + noteID + " .DC-note-image").css('top', (notes[noteID].docTop) * z);
    $("#" + noteID + " .DC-note-excerpt").width(newExcerptWidth);
    $("#" + noteID + " .DC-note-excerpt").height(newExcerptHeight);
    $("#" + noteID + " .DC-note-excerpt").css('left', -1 * newX1);
    $("#" + noteID + " .DC-left-cover").width(newX1);
    $("#" + noteID + " .DC-right-cover").width(newExcerptWidth - newX2);
    $("#" + noteID + " .DC-left-cover").height(newExcerptHeight);
    $("#" + noteID + " .DC-right-cover").height(newExcerptHeight);
  }

  var restoreDoc = function(noteID) {
    $("#" + noteID + " .DC-note-image").width(notes[noteID].docWidth);
    $("#" + noteID + " .DC-note-image").css('top', notes[noteID].docTop);
    $("#" + noteID + " .DC-note-excerpt").width(notes[noteID].excerptWidth);
    $("#" + noteID + " .DC-note-excerpt").height(notes[noteID].excerptHeight);
    $("#" + noteID + " .DC-note-excerpt").css('left', notes[noteID].excerptLeft);
    $("#" + noteID + " .DC-left-cover").width(notes[noteID].lCoverWidth);
    $("#" + noteID + " .DC-right-cover").width(notes[noteID].rCoverWidth);
    $("#" + noteID + " .DC-left-cover").height(notes[noteID].excerptHeight);
    $("#" + noteID + " .DC-right-cover").height(notes[noteID].excerptHeight);
  }

  //footnote click
  $(document).on('click', '.link-fn', function() {
    var fnLink = $(this);
    var fnLinkIcon = $(this).find('.fa');
    var fnID = $(this).data('fn');
    var footnote = $('#footnote' + fnID);
    if (footnote.hasClass('show')) {
      footnote.removeClass('show');
      footnote.slideUp({
        duration: 800,
        easing: 'swing'
      });
      fnLink.removeClass('reverse');
    } else {
      footnote.addClass('show');
      footnote.slideDown({
        duration: 800,
        easing: 'swing'
      });
      fnLink.addClass('reverse');
      //add note to notes and run subsequent functions
      var note = $(footnote).find('.DC-note-container');
      getCoords(note);
    }
    if (fnLinkIcon.hasClass('fa-caret-down')) {
      fnLinkIcon.removeClass('fa-caret-down').addClass('fa-caret-up');
    } else {
      fnLinkIcon.removeClass('fa-caret-up').addClass('fa-caret-down');
    }
  });

  $(window).load(function() {
    init();
  });

});
<|endoftext|>


<commit_before>/////////////////////////////////////////////////////////////////////
// FileWidget
// 13 January 2016
//
//
// Developed by :
// Diego Guidotti
// Michele Mammini
/////////////////////////////////////////////////////////////////////

Dbmng.FileWidget = Dbmng.AbstractWidget.extend({
  createWidget: function(){
    this.aField.value = this.getFieldValue();
    if(typeof jQuery().fileupload !== 'undefined'){
      this.aField.field_type='hidden';
    }
    else{
      this.aField.field_type='file';
    }
    return this.theme.getInput(this.aField);
    /*
    var self=this;

    var fld=this._super();
    fld.type='file';
    fld.name="files[]";
    //fld.setAttribute('data-url',url);
    fld.multiple=true;
    return fld;
    */
  },

  createField: function(data_val){
    var self=this;
    console.log(self);

    var url='server/php/';
    if(self.aParam.url){
      url=self.aParam.url+"file/"+self.field+"/";
    }
    if(self.aField.url){
      url=self.aField.url;
    }
    var weburl_file=url+"files/";
    if(self.aField.weburl_file){
      weburl_file=self.aField.weburl_file;
    }

    var el = this._super(data_val);

    if(typeof jQuery().fileupload !== 'undefined'){

      var aVField = {};
      aVField.field = this.aField.field + '_file';

      var elv = this.theme.getInput(aVField);
      elv.type='file';
      elv.name="files[]";

      var opt={};
      var btn_class="btn btn-success";
      var btn_icon="glyphicon glyphicon-plus";
      var btn_label="Select file...";
      var uploading_text="Uploading...";

      if( self.aField.class ) {
        btn_class = self.aField.class;
      }
      if( self.aField.add_icon ) {
        btn_icon = self.aField.add_icon;
      }
      if( self.aField.label_file ) {
        btn_label = self.aField.label_file;
      }
      if( self.aField.uploading_text ) {
        uploading_text = self.aField.uploading_text;
      }

      opt.class = btn_class;
      opt.icon  = btn_icon;
      opt.label_file = btn_label;

      /*

      */

      el.appendChild(this.theme.createFileUploadField(elv,btn_label, opt));

      var el_progress=jQuery(el).find('.progress');

      var info=jQuery(el).find('.dbmng_fileupload_container');
      if(typeof data_val !== 'undefined' && data_val!=='' && data_val !==null ){
        self.addFile(info, weburl_file, data_val);
      }

      jQuery(elv).fileupload({
          url: url,
          dataType: 'json',
          add: function (e, data) {
            console.log(data);
            info.html(uploading_text);
            data.submit();
          },
          progressall: function (e, data) {
              var progress = parseInt(data.loaded / data.total * 100, 10);
              console.log(progress);
              jQuery(el_progress).find(".progress-bar").css(
                  'width',
                  progress + '%'
              );
          },
          fail:function (e, data) {
            console.log(data);
            info.html(self.theme.alertMessage("Error"));
          },
          done: function (e, data) {
            console.log(data);
            if(data.result.files){
              if(data.result.files.length>0){
                info.html("");
                jQuery.each(data.result.files, function (index, file) {
                  if(file.error){
                    info.append(self.theme.alertMessage(file.error));
                  }
                  else{
                    self.addFile(info, weburl_file, file.name);
                    self.setValue(file.name);
                  }
                });
              }
              else{
                  info.html("");
                  jQuery.each(data.messages, function(k,v){
                    info.append(self.theme.alertMessage(v));
                  });
              }
            }
            else{
              info.append(self.theme.alertMessage(data.result.message));
            }
          }
      });
    }

    return el;
  },

  addFile:function(info, weburl_file, file){
    var self=this;

    var btn_icon="glyphicon glyphicon-remove";
    if( self.aField.remove_icon ) {
      btn_icon = self.aField.remove_icon;
    }

    info.append(""+this.assignFileTypeIcon(file)+" "+file+" ");
    var del=this.theme.getDeleteButton("delete", btn_icon);
    info.append(del);

    jQuery(del).click(function(){
      info.html("");
      self.setValue("");
    });
  },

  assignFileTypeIcon: function( file ) {
    var aFile = file.split('.');
    var file_type_icon = "";
    if( aFile[1] == 'pdf' ) {
      file_type_icon = '';
    }
    return file_type_icon;
  }
});
<commit_msg>added the title parameter to delete button<commit_after>/////////////////////////////////////////////////////////////////////
// FileWidget
// 13 January 2016
//
//
// Developed by :
// Diego Guidotti
// Michele Mammini
/////////////////////////////////////////////////////////////////////

Dbmng.FileWidget = Dbmng.AbstractWidget.extend({
  createWidget: function(){
    this.aField.value = this.getFieldValue();
    if(typeof jQuery().fileupload !== 'undefined'){
      this.aField.field_type='hidden';
    }
    else{
      this.aField.field_type='file';
    }
    return this.theme.getInput(this.aField);
    /*
    var self=this;

    var fld=this._super();
    fld.type='file';
    fld.name="files[]";
    //fld.setAttribute('data-url',url);
    fld.multiple=true;
    return fld;
    */
  },

  createField: function(data_val){
    var self=this;
    console.log(self);

    var url='server/php/';
    if(self.aParam.url){
      url=self.aParam.url+"file/"+self.field+"/";
    }
    if(self.aField.url){
      url=self.aField.url;
    }
    var weburl_file=url+"files/";
    if(self.aField.weburl_file){
      weburl_file=self.aField.weburl_file;
    }

    var el = this._super(data_val);

    if(typeof jQuery().fileupload !== 'undefined'){

      var aVField = {};
      aVField.field = this.aField.field + '_file';

      var elv = this.theme.getInput(aVField);
      elv.type='file';
      elv.name="files[]";

      var opt={};
      var btn_class="btn btn-success";
      var btn_icon="glyphicon glyphicon-plus";
      var btn_label="Select file...";
      var uploading_text="Uploading...";

      if( self.aField.class ) {
        btn_class = self.aField.class;
      }
      if( self.aField.add_icon ) {
        btn_icon = self.aField.add_icon;
      }
      if( self.aField.label_file ) {
        btn_label = self.aField.label_file;
      }
      if( self.aField.uploading_text ) {
        uploading_text = self.aField.uploading_text;
      }

      opt.class = btn_class;
      opt.icon  = btn_icon;
      opt.label_file = btn_label;

      /*

      */

      el.appendChild(this.theme.createFileUploadField(elv,btn_label, opt));

      var el_progress=jQuery(el).find('.progress');

      var info=jQuery(el).find('.dbmng_fileupload_container');
      if(typeof data_val !== 'undefined' && data_val!=='' && data_val !==null ){
        self.addFile(info, weburl_file, data_val);
      }

      jQuery(elv).fileupload({
          url: url,
          dataType: 'json',
          add: function (e, data) {
            console.log(data);
            info.html(uploading_text);
            data.submit();
          },
          progressall: function (e, data) {
              var progress = parseInt(data.loaded / data.total * 100, 10);
              console.log(progress);
              jQuery(el_progress).find(".progress-bar").css(
                  'width',
                  progress + '%'
              );
          },
          fail:function (e, data) {
            console.log(data);
            info.html(self.theme.alertMessage("Error"));
          },
          done: function (e, data) {
            console.log(data);
            if(data.result.files){
              if(data.result.files.length>0){
                info.html("");
                jQuery.each(data.result.files, function (index, file) {
                  if(file.error){
                    info.append(self.theme.alertMessage(file.error));
                  }
                  else{
                    self.addFile(info, weburl_file, file.name);
                    self.setValue(file.name);
                  }
                });
              }
              else{
                  info.html("");
                  jQuery.each(data.messages, function(k,v){
                    info.append(self.theme.alertMessage(v));
                  });
              }
            }
            else{
              info.append(self.theme.alertMessage(data.result.message));
            }
          }
      });
    }

    return el;
  },

  addFile:function(info, weburl_file, file){
    var self=this;

    var btn_icon="glyphicon glyphicon-remove";
    if( self.aField.remove_icon ) {
      btn_icon = self.aField.remove_icon;
    }
    var btn_title = "Delete";
    if( self.aField.remove_title_icon ) {
      btn_title = self.aField.remove_title_icon;
    }

    info.append(""+this.assignFileTypeIcon(file)+" "+file+" ");
    var del=this.theme.getDeleteButton(btn_title, btn_icon);
    info.append(del);

    jQuery(del).click(function(){
      info.html("");
      self.setValue("");
    });
  },

  assignFileTypeIcon: function( file ) {
    var aFile = file.split('.');
    var file_type_icon = "";
    if( aFile[1] == 'pdf' ) {
      file_type_icon = '';
    }
    return file_type_icon;
  }
});
<|endoftext|>