<commit_before>/**
 * @overview ccm component for fill-in-the-blank texts
 * @author André Kless <andre.kless@web.de> 2017
 * @license The MIT License (MIT)
 *
 * Notes
 * - disadvantage of bit operation: possible positions for given letters in a word are 0-31
 * - onFinish could also be a destination object
 */

ccm.component( {

  name: 'cloze',

  config: {

    text: 'Hello, [[(W)o(rl)d]]!',
    html_templates: {
      main: {
        id: 'main',
        inner: [
          { id: 'keywords' },
          { id: 'text' },
          { id: 'button' },
          { id: 'timer' }
        ]
      },
      keyword: {
        class: 'keyword',
        inner: '%%'
      },
      button: {
        tag: 'button',
        inner: '%caption%',
        onclick: '%click%'
      },
      timer: {
        tag: 'span',
        inner: '%%'
      }
    },
    css_layout: [ ccm.load, '../cloze/layouts/default.css' ],
    ccm_helper: [ ccm.load, '../../ccm-developer-no-jquery/ccm/ccm-helper.js' ],
    button_caption: 'finish',
    onFinish: function ( instance, result ) { console.log( result ); }

//  blank: true,
//  ignore_case: true,
//  keywords: [ 'keyword1', 'keyword2', ... ],
//  points_per_gap: 1,
//  time: 60,
//  user: [ ccm.instance, '../user/ccm.user.js' ]

  },

  Instance: function () {

    var self = this;
    var my;             // contains privatized instance members
    var keywords = [];  // information data for each keyword

    this.ready = function ( callback ) {

      // privatize all possible instance members
      my = ccm.helper.privatize( self );

      // fill-in-the-blank text is given via inner HTML? => use it with higher priority
      if ( self.node && self.node.innerHTML.trim() ) my.text = self.node.innerHTML;

      var regex_keyword = /\[\[.+?\]\]/g;   // regular expression for finding all gaps/keywords in the text
      var regex_given = /\(.+?\)/g;         // regular expression for finding all given characters of a keyword

      // iterate all keywords in the text
      ( my.text.match( regex_keyword ) || [] ).map( function ( keyword ) {

        // remove distinguishing characteristic '[[' and ']]'
        keyword = keyword.substr( 2, keyword.length - 4 );

        // replace all given characters of a keywords with '*'
        var keyw__d = keyword.replace( '*', '#' ).replace( regex_given, function ( given ) {
          var length = given.length - 2;
          given = '';
          for ( var i = 0; i < length; i++ ) given += '*';
          return given;
        } );

        // determine given characters and hold this information in a single number (use of bit operations)
        var givens = 0;
        for ( var i = 0; i < keyw__d.length; i++ )
          if ( keyw__d.charAt( i ) === '*' ) givens += Math.pow( 2, i );

        // add keyword information data for current keyword
        keywords.push( {
          word: keyword.replace( regex_given, function ( given ) { return given.substr( 1, given.length - 2 ); } ),
          givens: givens
        } );

      } );

      // replace gaps/keywords with empty span elements
      my.text = my.text.replace( regex_keyword, '' );

      callback();
    };

    this.render = function ( callback ) {

      // initial result data
      var result = { points: 0, max_points: 0, details: [] };

      // prepare main HTML structure
      var main_elem = ccm.helper.html( my.html_templates.main );

      var button_elem  = main_elem.querySelector( '#button' );  // container for finish button
      var  timer_elem  = main_elem.querySelector( '#timer'  );  // container for timer
      var  timer_value = my.time;                               // timer value

      // add content for inner containers
      renderKeywords();
      renderText();
      renderButton();
      if ( my.time ) renderTimer(); else ccm.helper.removeElement( timer_elem );

      // remember start time
      var time = new Date().getTime();

      // set content of own website area
      ccm.helper.setContent( self.element, ccm.helper.protect( main_elem ) );

      if ( callback ) callback();

      /** renders given keywords for text gaps */
      function renderKeywords() {

        var keywords_elem = main_elem.querySelector( '#keywords' );  // container for keywords
        var entries = [];                                            // inner container for each keyword

        // has given keywords? => create inner container for each keyword
        if ( my.keywords )
          ( Array.isArray( my.keywords ) ? my.keywords : keywords ).map( addKeyword );
        else
          return main_elem.removeChild( keywords_elem );  // no given keywords? => remove container for keywords

        // generated keyword list? => sort keywords lexicographical (keyword order gives no hint about correct solution)
        if ( my.keywords === true ) entries.sort( function ( a, b ) { return a.innerHTML.localeCompare( b.innerHTML ) } );

        // add each inner keyword container to container for keywords
        entries.map( function ( entry ) { keywords_elem.appendChild( entry ); } );

        /** adds a inner container for a keyword */
        function addKeyword( keyword ) {
          entries.push( ccm.helper.html( my.html_templates.keyword, Array.isArray( my.keywords ) ? keyword : keyword.word ) );
        }

      }

      /** renders the text with containing gaps */
      function renderText() {

        // render text with containing gaps
        main_elem.querySelector( '#text' ).innerHTML = my.text;

        // render input field in each gap
        ccm.helper.makeIterable( main_elem.querySelectorAll( '.gap' ) ).map( function ( gap_elem, i ) {

          // blank input fields and shown keywords? => input fields should give no hint for the length of the searched word
          if ( my.blank && my.keywords ) return gap_elem.appendChild( ccm.helper.html( { tag: 'input', type: 'text' } ) );

          // shorter access to keyword
          var keyword = keywords[ i ].word;

          // prepare ccm HTML data for input field
          var input = {
            tag: 'input',
            type: 'text',
            maxlength: keyword.length,
            size: keyword.length * 1.5  // works tolerably for words with a length up to 30
          };

          // no blank input fields? => set placeholder attribute
          if ( !my.blank ) {
            input.placeholder = '';
            for ( var j = 0; j < keyword.length; j++ )
              input.placeholder += Math.pow( 2, j ) & keywords[ i ].givens ? keyword.charAt( j ) : '_';
          }

          // render input field in the current gap
          gap_elem.appendChild( ccm.helper.html( input ) );

        } );

      }

      /** renders the finish button */
      function renderButton() {

        ccm.helper.setContent( button_elem, ccm.helper.html( my.html_templates.button, {
          caption: my.button_caption,
          click:   onFinish
        } ) );

      }

      /** renders the timer */
      function renderTimer() {

        timer();  // start timer

        /** updates countdown timer */
        function timer() {

          // already finished? => remove timer
          if ( !main_elem.querySelector( '#timer' ) ) return;

          // (re)render timer value
          ccm.helper.setContent( timer_elem, ccm.helper.html( my.html_templates.timer, timer_value ) );

          // countdown
          if ( timer_value-- )
            ccm.helper.wait( 1000, timer );
          else if ( button_elem )
            onFinish();           // perform finish callback at timeout

        }

      }

      /** onclick callback for finish button */
      function onFinish() {

        time = new Date().getTime() - time;       // calculate result time
        button_elem.innerHTML = '';               // remove button
        ccm.helper.removeElement( timer_elem  );  // remove timer container

        // has user instance? => login user
        if ( self.user ) self.user.login( proceed ); else proceed();

        function proceed() {

          // give visual feedback for correctness
          ccm.helper.makeIterable( main_elem.querySelectorAll( '.gap input' ) ).map( function ( gap, i ) {
            var correct = my.ignore_case ? gap.value.toLowerCase() === keywords[ i ].word.toLowerCase() : gap.value === keywords[ i ].word;
            gap.removeAttribute( 'placeholder' );
            gap.parentNode.className += correct ? ' correct' : ' wrong';

            // correct input? => give points
            if ( my.points_per_gap ) {
              result.max_points += my.points_per_gap;
              if ( correct ) result.points += my.points_per_gap;
            }

            // set details for current gap result
            result.details.push( { input: gap.value, solution: keywords[ i ].word, correct: correct } );

          } );

          // finalize result data
          if ( !my.points_per_gap ) { delete result.points; delete result.max_points; }
          if ( self.user ) result.user = self.user.data().key;
          if (   my.time ) result.time_left = timer_value + 1;
          result.time = time;

          // provide result data
          if ( ccm.helper.isObject( self.onFinish ) )
            ccm.helper.setDataset( result, self.onFinish, self.user );
          else
            self.onFinish( self, result );

        }

      }

    };

  }

} );<commit_msg>update component for fill-in-the-blank texts (ccm.clozes.js)<commit_after>/**
 * @overview ccm component for fill-in-the-blank texts
 * @author André Kless <andre.kless@web.de> 2017
 * @license The MIT License (MIT)
 *
 * Notes
 * - disadvantage of bit operation: possible positions for given letters in a word are 0-31
 * - onFinish could also be a destination object
 */

ccm.component( {

  name: 'cloze',

  config: {

    text: 'Hello, [[(W)o(rl)d]]!',
    html_templates: {
      main: {
        id: 'main',
        inner: [
          { id: 'keywords' },
          { id: 'text' },
          { id: 'button' },
          { id: 'timer' }
        ]
      },
      keyword: {
        class: 'keyword',
        inner: '%%'
      },
      button: {
        tag: 'button',
        inner: '%caption%',
        onclick: '%click%'
      },
      timer: {
        tag: 'span',
        inner: '%%'
      }
    },
    css_layout: [ ccm.load, '../cloze/layouts/default.css' ],
    ccm_helper: [ ccm.load, '../../ccm-developer-no-jquery/ccm/ccm-helper.js' ],
    button_caption: 'finish',
    onFinish: function ( instance, result ) { console.log( result ); }

//  blank: true,
//  ignore_case: true,
//  keywords: [ 'keyword1', 'keyword2', ... ],
//  points_per_gap: 1,
//  time: 60,
//  user: [ ccm.instance, '../user/ccm.user.js' ]

  },

  Instance: function () {

    var self = this;
    var my;             // contains privatized instance members
    var keywords = [];  // information data for each keyword

    this.init = function ( callback ) {

      // fill-in-the-blank text is given via inner HTML? => use it with higher priority
      if ( self.node && self.node.innerHTML.trim() ) self.text = self.node.innerHTML;

      callback();
    };

    this.ready = function ( callback ) {

      // privatize all possible instance members
      my = ccm.helper.privatize( self );

      var regex_keyword = /\[\[.+?\]\]/g;   // regular expression for finding all gaps/keywords in the text
      var regex_given = /\(.+?\)/g;         // regular expression for finding all given characters of a keyword

      // iterate all keywords in the text to determine the information data for each keyword
      ( my.text.match( regex_keyword ) || [] ).map( function ( keyword ) {

        // remove distinguishing characteristic '[[' and ']]'
        keyword = keyword.substr( 2, keyword.length - 4 );

        // replace all given characters of a keywords with '*'
        var keyw__d = keyword.replace( '*', '#' ).replace( regex_given, function ( given ) {
          var length = given.length - 2;
          given = '';
          for ( var i = 0; i < length; i++ ) given += '*';
          return given;
        } );

        // determine given characters and hold this information in a single number (use of bit operations)
        var givens = 0;
        for ( var i = 0; i < keyw__d.length; i++ )
          if ( keyw__d.charAt( i ) === '*' ) givens += Math.pow( 2, i );

        // add keyword information data for current keyword
        keywords.push( {
          word: keyword.replace( regex_given, function ( given ) { return given.substr( 1, given.length - 2 ); } ),
          givens: givens
        } );

      } );

      // replace gaps/keywords with empty span elements
      my.text = my.text.replace( regex_keyword, '' );

      callback();
    };

    this.render = function ( callback ) {

      // initial result data
      var result = { points: 0, max_points: 0, details: [] };

      // prepare main HTML structure
      var main_elem = ccm.helper.html( my.html_templates.main );

      var button_elem  = main_elem.querySelector( '#button' );  // container for finish button
      var  timer_elem  = main_elem.querySelector( '#timer'  );  // container for timer
      var  timer_value = my.time;                               // timer value

      // add content for inner containers
      renderKeywords();
      renderText();
      renderButton();
      if ( my.time ) renderTimer(); else ccm.helper.removeElement( timer_elem );

      // remember start time
      var time = new Date().getTime();

      // set content of own website area
      ccm.helper.setContent( self.element, ccm.helper.protect( main_elem ) );

      if ( callback ) callback();

      /** renders given keywords for text gaps */
      function renderKeywords() {

        var keywords_elem = main_elem.querySelector( '#keywords' );  // container for keywords
        var entries = [];                                            // inner container for each keyword

        // has given keywords? => create inner container for each keyword
        if ( my.keywords )
          ( Array.isArray( my.keywords ) ? my.keywords : keywords ).map( addKeyword );
        else
          return main_elem.removeChild( keywords_elem );  // no given keywords? => remove container for keywords

        // generated keyword list? => sort keywords lexicographical (keyword order gives no hint about correct solution)
        if ( my.keywords === true ) entries.sort( function ( a, b ) { return a.innerHTML.localeCompare( b.innerHTML ) } );

        // add each inner keyword container to container for keywords
        entries.map( function ( entry ) { keywords_elem.appendChild( entry ); } );

        /** adds a inner container for a keyword */
        function addKeyword( keyword ) {
          entries.push( ccm.helper.html( my.html_templates.keyword, Array.isArray( my.keywords ) ? keyword : keyword.word ) );
        }

      }

      /** renders the text with containing gaps */
      function renderText() {

        // render text with containing gaps
        main_elem.querySelector( '#text' ).innerHTML = my.text;

        // render input field in each gap
        ccm.helper.makeIterable( main_elem.querySelectorAll( '.gap' ) ).map( function ( gap_elem, i ) {

          // blank input fields and shown keywords? => input fields should give no hint for the length of the searched word
          if ( my.blank && my.keywords ) return gap_elem.appendChild( ccm.helper.html( { tag: 'input', type: 'text' } ) );

          // shorter access to keyword
          var keyword = keywords[ i ].word;

          // prepare ccm HTML data for input field
          var input = {
            tag: 'input',
            type: 'text',
            maxlength: keyword.length,
            size: keyword.length * 1.5  // works tolerably for words with a length up to 30
          };

          // no blank input fields? => set placeholder attribute
          if ( !my.blank ) {
            input.placeholder = '';
            for ( var j = 0; j < keyword.length; j++ )
              input.placeholder += Math.pow( 2, j ) & keywords[ i ].givens ? keyword.charAt( j ) : '_';
          }

          // render input field in the current gap
          gap_elem.appendChild( ccm.helper.html( input ) );

        } );

      }

      /** renders the finish button */
      function renderButton() {

        ccm.helper.setContent( button_elem, ccm.helper.html( my.html_templates.button, {
          caption: my.button_caption,
          click:   onFinish
        } ) );

      }

      /** renders the timer */
      function renderTimer() {

        timer();  // start timer

        /** updates countdown timer */
        function timer() {

          // already finished? => remove timer
          if ( !main_elem.querySelector( '#timer' ) ) return;

          // (re)render timer value
          ccm.helper.setContent( timer_elem, ccm.helper.html( my.html_templates.timer, timer_value ) );

          // countdown
          if ( timer_value-- )
            ccm.helper.wait( 1000, timer );
          else if ( button_elem )
            onFinish();           // perform finish callback at timeout

        }

      }

      /** onclick callback for finish button */
      function onFinish() {

        time = new Date().getTime() - time;       // calculate result time
        button_elem.innerHTML = '';               // remove button
        ccm.helper.removeElement( timer_elem  );  // remove timer container

        // has user instance? => login user
        if ( self.user ) self.user.login( proceed ); else proceed();

        function proceed() {

          // give visual feedback for correctness
          ccm.helper.makeIterable( main_elem.querySelectorAll( '.gap input' ) ).map( function ( gap, i ) {
            var correct = my.ignore_case ? gap.value.toLowerCase() === keywords[ i ].word.toLowerCase() : gap.value === keywords[ i ].word;
            gap.removeAttribute( 'placeholder' );
            gap.parentNode.className += correct ? ' correct' : ' wrong';

            // correct input? => give points
            if ( my.points_per_gap ) {
              result.max_points += my.points_per_gap;
              if ( correct ) result.points += my.points_per_gap;
            }

            // set details for current gap result
            result.details.push( { input: gap.value, solution: keywords[ i ].word, correct: correct } );

          } );

          // finalize result data
          if ( !my.points_per_gap ) { delete result.points; delete result.max_points; }
          if ( self.user ) result.user = self.user.data().key;
          if (   my.time ) result.time_left = timer_value + 1;
          result.time = time;

          // provide result data
          if ( ccm.helper.isObject( self.onFinish ) )
            ccm.helper.setDataset( result, self.onFinish, self.user );
          else
            self.onFinish( self, result );

        }

      }

    };

  }

} );<|endoftext|>


<commit_before>/**

  compiler.js

  Powerful compiler for script transpilation, templating and CSS preprocessing.

  As a note: we differentiate between "extensions" and "types". The extension
  is the actual file extension, whereas the type is the extension a file will
  be after processing.

  • extractMetadata
  - createCompiler (!!! TODO: NEEDS CACHING)
  • aggregateData
  • createRenderer (!!! TODO: UPDATE WITH SOURCE)
  • createQueryFn
  • addPipeline

**/

/**

  Dependencies and exports

**/

var Bundler     = require("./bundler");
var Metadata    = require("./metadata");
var Utils       = require("./utils");

var Pipelines   = global.shipp.pipelines;

module.exports = {
  extractMetadata: extractMetadata,
  createCompiler: createCompiler,
  aggregateData: aggregateData,
  createRenderer: createRenderer,
  createQueryFn: createQueryFn,
  addPipeline: addPipeline
};

/**

  Extracts metadata from a file if appropriate (HTML-like)

  @param {String} id Source identifier
  @param {String} template String template
  @param {String} type type of file (this may be different than the extension)
  @returns {Object} Metadata object

**/

function extractMetadata(id, template, type) {

  var metadata,
      directives;

  if (Utils.isHTML(type)) {
    // Currently leaving out the PARAM directive (until better planned)
    directives = {
      "DATA"    : "query",
      "QUERY"   : "query",
      "COOKIE"  : "object",
      "SESSION" : "object",
      "CACHE"   : "boolean"
    };
    metadata = Metadata.extract(template.slice(0, 500), directives);
  } else {
    // For non-HTML files default to caching
    metadata = { cache: true };
  }

  return metadata;

}

/**

  Creates a compiler for a file. For bundles, this means webpacking. Otherwise,
  uses the Pipelines engine.

  @param {Object} source object containing file or template
  @returns {Function} Function that compiles a file

**/

function createCompiler(source, type) {
  if (source.file && source.file.bundle)
    return Bundler.fromFile(file, type).get;
  else  {
    var fn
    if (source.template)
      fn = Pipelines.compile.bind(Pipelines, source.ext, source.template);
    else if (source.file)
      fn = Pipelines.compileFile.bind(Pipelines, source.file);
    else
      throw new Error("Cannot create compiler due to invalid source");

    return function(data) {
      return new Promise(function(resolve, reject) {
        fn(data, function(err, compiled) {
          if (err) return reject(err);
          resolve(compiled);
        });
      })
    }
  }
}

/**

  Pulls data from cookies, session, params, query, slug and metadata into
  single data object.

  @param {Object} metadata Metadata object as returns by metadata.js
  @param {Object} params Params object as provided by handler.js
  @returns {Object} The data object

**/

function aggregateData(metadata, params) {

  // Data object will include locals, cookies, session, params, query, slug
  // and if applicable, database query results
  var data = Object.assign({}, global.shipp.locals, params.locals);

  // Attach special variables
  data.$assets  = Object.assign({}, global.shipp.assets);
  data.$body    = params.body;
  data.$cookies = params.cookies;
  data.$params  = params.params;
  data.$query   = params.query;
  data.$session = params.session;

  // Special slug
  if (params.slug)
    data.$slug = params.slug

  // Set cookies
  for (var key in metadata.cookies || {})
    data.$cookies[key] = metadata.cookies[key];

  // Set session
  for (key in metadata.session || {})
    data.$session[key] = metadata.session[key];

  return data;

}

// !!! TODO: compiler must be file as well (accept template or file)
// !!! TODO: refactor aggregateData

/**

  Creates a renderer for a blueprint

  @param {String} id Source identifier
  @param {Object} source Object of form { template, file } with either "template" or "file"
  @param {String} type The type of file
  @param {String} pipeline Pipeline to use for compiler
  @return {Function} Handler of form (params = {}, compressionMethod = "identity", maxCompression = false)

  results {
    compression,
    metadata,
    sourceId,
    type,
    contents
  }

**/

function createRenderer(id, source, type, pipeline) {

  // !!! TODO: Handle HTML here, how to map type
  // !!! TODO: add generic source handler
  var metadata = extractMetadata(source, type) || {};
  var compiler = createCompiler(source, pipeline);

  var tasks = [compiler];
  var isHTML = Utils.isHTML(type);

  // Add data query if necessary
  if (metadata.data)
    tasks.unshift(createQueryFn(metadata.data));

  return function(params, compressionMethod, maxCompression) {

    // params should include relevant information normally on a req or res object
    var data = isHTML ? aggregateData(metadata, params) : {};

    // Default compression to identity to maintain compatibility with BrowserSync and other tools
    compressionMethod = compressionMethod || "identity";
    maxCompression = maxCompression || false;

    return Utils.sequence(tasks, data).then(function(rendered) {

      var results = {
        compression: compressionMethod,
        metadata: metadata,
        sourceId: id,
        type: type
      };

      // Uncompressed
      if ("identity" === compressionMethod) {
        results.contents = rendered;
        return results;

      // Compressed
      } else {
        return new Promise(function(resolve, reject) {
          // Note that we are not defaulting to
          Utils.compress(compressionMethod, rendered, maxCompression, function(err, compressed) {
            if (err) return reject(err)
            results.contents = compressed;
            resolve(results);
          });
        })

      }

    })
  }

}

/**

  Creates a function that runs Universql-style data queries.

  @param {Array} queries The queries to be run
  @returns {Function} Function that performs query and returns promise

**/

function createQueryFn(queries) {
  return function(context) {
    return Utils.makeQueries(queries, context).then(function(data) {
      return Promise.resolve(Object.assign(data, context));
    });
  };
}

/**

  Add a pipeline if it does not exist.

  @param {String} ext The file extension for the pipeline

**/

function addPipeline(ext) {

  if (Pipelines.hasPipeline(ext)) return;

  try {
    Pipelines.addPipeline(ext);
  } catch (err) {
    global.shipp.logger.warn("");
    global.shipp.logger.warn("WARNING! There is no pipeline available for *." + ext + " files.");
    global.shipp.logger.warn("If this is not fixed, these files will not be served nor compiled.");
    global.shipp.logger.warn("You should add an appropriate pipeline to your shipp.json file.\n");
    return;
  }

}

<commit_msg>Compiler#createQueryFn: handles empty data (simplifies other functions)<commit_after>/**

  compiler.js

  Powerful compiler for script transpilation, templating and CSS preprocessing.

  As a note: we differentiate between "extensions" and "types". The extension
  is the actual file extension, whereas the type is the extension a file will
  be after processing.

  • extractMetadata
  - createCompiler (!!! TODO: NEEDS CACHING)
  • aggregateData
  • createRenderer (!!! TODO: UPDATE WITH SOURCE)
  • createQueryFn
  • addPipeline

**/

/**

  Dependencies and exports

**/

var Bundler     = require("./bundler");
var Metadata    = require("./metadata");
var Utils       = require("./utils");

var Pipelines   = global.shipp.pipelines;

module.exports = {
  extractMetadata: extractMetadata,
  createCompiler: createCompiler,
  aggregateData: aggregateData,
  createRenderer: createRenderer,
  createQueryFn: createQueryFn,
  addPipeline: addPipeline
};

/**

  Extracts metadata from a file if appropriate (HTML-like)

  @param {String} id Source identifier
  @param {String} template String template
  @param {String} type type of file (this may be different than the extension)
  @returns {Object} Metadata object

**/

function extractMetadata(id, template, type) {

  var metadata,
      directives;

  if (Utils.isHTML(type)) {
    // Currently leaving out the PARAM directive (until better planned)
    directives = {
      "DATA"    : "query",
      "QUERY"   : "query",
      "COOKIE"  : "object",
      "SESSION" : "object",
      "CACHE"   : "boolean"
    };
    metadata = Metadata.extract(template.slice(0, 500), directives);
  } else {
    // For non-HTML files default to caching
    metadata = { cache: true };
  }

  return metadata;

}

/**

  Creates a compiler for a file. For bundles, this means webpacking. Otherwise,
  uses the Pipelines engine.

  @param {Object} source object containing file or template
  @returns {Function} Function that compiles a file

**/

function createCompiler(source, type) {
  if (source.file && source.file.bundle)
    return Bundler.fromFile(file, type).get;
  else  {
    var fn
    if (source.template)
      fn = Pipelines.compile.bind(Pipelines, source.ext, source.template);
    else if (source.file)
      fn = Pipelines.compileFile.bind(Pipelines, source.file);
    else
      throw new Error("Cannot create compiler due to invalid source");

    return function(data) {
      return new Promise(function(resolve, reject) {
        fn(data, function(err, compiled) {
          if (err) return reject(err);
          resolve(compiled);
        });
      })
    }
  }
}

/**

  Pulls data from cookies, session, params, query, slug and metadata into
  single data object.

  @param {Object} metadata Metadata object as returns by metadata.js
  @param {Object} params Params object as provided by handler.js
  @returns {Object} The data object

**/

function aggregateData(metadata, params) {

  // Data object will include locals, cookies, session, params, query, slug
  // and if applicable, database query results
  var data = Object.assign({}, global.shipp.locals, params.locals);

  // Attach special variables
  data.$assets  = Object.assign({}, global.shipp.assets);
  data.$body    = params.body;
  data.$cookies = params.cookies;
  data.$params  = params.params;
  data.$query   = params.query;
  data.$session = params.session;

  // Special slug
  if (params.slug)
    data.$slug = params.slug

  // Set cookies
  for (var key in metadata.cookies || {})
    data.$cookies[key] = metadata.cookies[key];

  // Set session
  for (key in metadata.session || {})
    data.$session[key] = metadata.session[key];

  return data;

}

// !!! TODO: compiler must be file as well (accept template or file)
// !!! TODO: refactor aggregateData

/**

  Creates a renderer for a blueprint

  @param {String} id Source identifier
  @param {Object} source Object of form { template, file } with either "template" or "file"
  @param {String} type The type of file
  @param {String} pipeline Pipeline to use for compiler
  @return {Function} Handler of form (params = {}, compressionMethod = "identity", maxCompression = false)

  results {
    compression,
    metadata,
    sourceId,
    type,
    contents
  }

**/

function createRenderer(id, source, type, pipeline) {

  // !!! TODO: Handle HTML here, how to map type
  // !!! TODO: add generic source handler
  var metadata = extractMetadata(source, type) || {};
  var compiler = createCompiler(source, pipeline);

  var tasks = [compiler];
  var isHTML = Utils.isHTML(type);

  // Add data query if necessary
  if (metadata.data)
    tasks.unshift(createQueryFn(metadata.data));

  return function(params, compressionMethod, maxCompression) {

    // params should include relevant information normally on a req or res object
    var data = isHTML ? aggregateData(metadata, params) : {};

    // Default compression to identity to maintain compatibility with BrowserSync and other tools
    compressionMethod = compressionMethod || "identity";
    maxCompression = maxCompression || false;

    return Utils.sequence(tasks, data).then(function(rendered) {

      var results = {
        compression: compressionMethod,
        metadata: metadata,
        sourceId: id,
        type: type
      };

      // Uncompressed
      if ("identity" === compressionMethod) {
        results.contents = rendered;
        return results;

      // Compressed
      } else {
        return new Promise(function(resolve, reject) {
          // Note that we are not defaulting to
          Utils.compress(compressionMethod, rendered, maxCompression, function(err, compressed) {
            if (err) return reject(err)
            results.contents = compressed;
            resolve(results);
          });
        })

      }

    })
  }

}

/**

  Creates a function that runs Universql-style data queries.

  @param {Array} queries The queries to be run
  @returns {Function} Function that performs query and returns promise

**/

function createQueryFn(queries) {
  return function(context) {
    if (!queries)
      return Promise.resolve(Object.assign({}, context));
    else
      return Utils.makeQueries(queries, context).then(function(data) {
        return Promise.resolve(Object.assign(data, context));
      });
  };
}

/**

  Add a pipeline if it does not exist.

  @param {String} ext The file extension for the pipeline

**/

function addPipeline(ext) {

  if (Pipelines.hasPipeline(ext)) return;

  try {
    Pipelines.addPipeline(ext);
  } catch (err) {
    global.shipp.logger.warn("");
    global.shipp.logger.warn("WARNING! There is no pipeline available for *." + ext + " files.");
    global.shipp.logger.warn("If this is not fixed, these files will not be served nor compiled.");
    global.shipp.logger.warn("You should add an appropriate pipeline to your shipp.json file.\n");
    return;
  }

}

<|endoftext|>


<commit_before>// This is where it all goes :)

$(document).ready(function() {
  // Hook up annotator events to GA.
  // https://groups.google.com/a/list.hypothes.is/d/msgid/dev/29f8f881-3245-4153-b877-a8c952e17d37%40list.hypothes.is?utm_medium=email&utm_source=footer.
  // Interval checks every 500ms for annotator variable. Cancels interval if
  // found or timeout cancels it after 10 seconds.
  //
  var interval = window.setInterval(attachToAnnotatorEvents, 500);
  function attachToAnnotatorEvents() {
    if (window.annotator && typeof window.annotator !== "undefined" && window.annotator.hasOwnProperty('on')) {
      window.clearInterval(interval);
      function logEvent(type, annotation) {
        window.ga('send', 'event', 'Annotation', type);
      }
      window.annotator.on('annotationCreated', logEvent.bind(null, 'created'));
      window.annotator.on('annotationUpdated', logEvent.bind(null, 'updated'));

      $('button[name="sidebar-toggle"], button[name="highlight-visibility"]').on('click', function(ev) {
        window.ga('send', 'event', 'Annotation', ev.currentTarget.name + '-click');
      });
    }
  }
  window.setTimeout(function() {
    window.clearInterval(interval);
  }, 1000 * 10);

  $('#page-wrapper').append("");
  $.scrollDepth({
    userTiming: false,
    pixelDepth: false,
    elements: ['#page_end'],
  });

  // Modal for citation link clicks.
  $(document).on('click', '.citation, .footnote-ref', function(ev) {
    ev.preventDefault();
    var modal = $('#citation_modal');
    var citation = $(ev.currentTarget);
    modal.find('.modal-body').html(citation.data('ref-html'));
    modal.modal('show');
  });

  // Modal for larger images on image click.
  $(document).on('click', '.book-html .figure img, .book-html figure img', function(ev) {
    var modal = $('#image_modal');
    var image = $(ev.target);
    var caption = image.siblings('.caption, figcaption');
    modal.find('.modal-body').html(image.clone().addClass('img-responsive'));
    modal.find('.modal-footer').html(caption.clone());
    modal.modal('show');
  });

  // Keyboard shortcuts for navigating to previous and next page.
  $(document).on('keyup', function(ev) {
    var targetsToIgnore = /textarea|input|select/i
    if ((ev.keyCode === 37 || ev.keyCode === 39) && !targetsToIgnore.test(ev.target.nodeName)) {
      if (ev.keyCode === 37 && $('.pager .previous a').length > 0) {
        // left: go to previous
        location.href = $('.pager .previous a').attr('href');
      }
      else if (ev.keyCode === 39 && $('.pager .next a').length > 0) {
        // right: go to next
        location.href = $('.pager .next a').attr('href');
      }
    }
  });

  // Check if hypothes.is window is expanded.
  var annotator_frame = $('.annotator-frame');
  function isHypothesisOpen() {
    if (annotator_frame.length === 0) {
      annotator_frame = $('.annotator-frame');
    }
    return annotator_frame.length > 0 && !annotator_frame.hasClass('annotator-collapsed');
  }

  // Toggle body class if hypothes.is is open or closed.
  var body = $('body');
  setInterval(function() {
    body.toggleClass('hypothesis-open', isHypothesisOpen());
  }, 200);

  $('i[data-toggle="collapse"]').click(function(ev) {
    // Prevent click within link to cause new page load.
    ev.preventDefault();
  });

  // Remove alert if user already closed it this session.
  $('.alert[data-alert-name]').each(function(i, el) {
    var $el = $(el);
    var re = new RegExp("(?:(?:^|.*;\\s*)" + $el.data('alert-name') + "_closed" +"\\s*\\=\\s*([^;]*).*$)|^.*$");
    var closed = document.cookie.replace(re, "$1")
    if (closed) {
      $el.remove();
    }
  });
  // Hide all email asks if they've already submitted their email address.
  if (document.cookie.replace(/(?:(?:^|.*;\s*)submittedEmail\s*\=\s*([^;]*).*$)|^.*$/, "$1") === "true") {
    $('.email-cta').not('.always-show').addClass('hide');
  }

  $(document).on('click', '.close-bottom-cta', function(ev) {
    ev.preventDefault();
    $(ev.currentTarget).closest('.email-cta').addClass('hide');
  });

  // Enforce required for browsers that don't.
  $(document).on('submit', 'form', function(ev) {
    var form = $(ev.currentTarget);
    var required = form.find('[required]');
    var has_all_required = true;
    if (required.length === 0) { return true; }

    required.each(function(index, el) {
      if ($(el).val() === "") {
        has_all_required = false;
      }
    });
    if (!has_all_required) {
      ev.preventDefault();
    }
  });

  // Stash cookie with alert name if alert is closed.
  $('.alert[data-alert-name]').on('close.bs.alert', function(ev) {
    var target = $(ev.target);
    document.cookie = target.data('alert-name') + "_closed=true; path=/";
  });

});
<commit_msg>Add ability to link to images<commit_after>// This is where it all goes :)

$(document).ready(function() {
  // Hook up annotator events to GA.
  // https://groups.google.com/a/list.hypothes.is/d/msgid/dev/29f8f881-3245-4153-b877-a8c952e17d37%40list.hypothes.is?utm_medium=email&utm_source=footer.
  // Interval checks every 500ms for annotator variable. Cancels interval if
  // found or timeout cancels it after 10 seconds.
  //
  var interval = window.setInterval(attachToAnnotatorEvents, 500);
  function attachToAnnotatorEvents() {
    if (window.annotator && typeof window.annotator !== "undefined" && window.annotator.hasOwnProperty('on')) {
      window.clearInterval(interval);
      function logEvent(type, annotation) {
        window.ga('send', 'event', 'Annotation', type);
      }
      window.annotator.on('annotationCreated', logEvent.bind(null, 'created'));
      window.annotator.on('annotationUpdated', logEvent.bind(null, 'updated'));

      $('button[name="sidebar-toggle"], button[name="highlight-visibility"]').on('click', function(ev) {
        window.ga('send', 'event', 'Annotation', ev.currentTarget.name + '-click');
      });
    }
  }
  window.setTimeout(function() {
    window.clearInterval(interval);
  }, 1000 * 10);

  $('#page-wrapper').append("");
  $.scrollDepth({
    userTiming: false,
    pixelDepth: false,
    elements: ['#page_end'],
  });

  // Modal for citation link clicks.
  $(document).on('click', '.citation, .footnote-ref', function(ev) {
    ev.preventDefault();
    var modal = $('#citation_modal');
    var citation = $(ev.currentTarget);
    modal.find('.modal-body').html(citation.data('ref-html'));
    modal.modal('show');
  });

  function openImageModal(image) {
    var modal = $('#image_modal');
    if (image.attr('id')) {
      window.location.hash = image.attr('id');
    }
    var caption = image.siblings('.caption, figcaption');
    modal.find('.modal-body').html(image.clone().addClass('img-responsive'));
    modal.find('.modal-footer').html(caption.clone());
    modal.modal('show');
  }

  (function() {
    var image = $('img[id="' + window.location.hash.replace(/^#/, '') + '"]:first');
    if (image.length > 0) {
      openImageModal(image);
    }
  })();

  // Modal for larger images on image click.
  $(document).on('click', '.book-html .figure img, .book-html figure img', function(ev) {
    var image = $(ev.target);
    openImageModal(image);
  });

  // Keyboard shortcuts for navigating to previous and next page.
  $(document).on('keyup', function(ev) {
    var targetsToIgnore = /textarea|input|select/i
    if ((ev.keyCode === 37 || ev.keyCode === 39) && !targetsToIgnore.test(ev.target.nodeName)) {
      if (ev.keyCode === 37 && $('.pager .previous a').length > 0) {
        // left: go to previous
        location.href = $('.pager .previous a').attr('href');
      }
      else if (ev.keyCode === 39 && $('.pager .next a').length > 0) {
        // right: go to next
        location.href = $('.pager .next a').attr('href');
      }
    }
  });

  // Check if hypothes.is window is expanded.
  var annotator_frame = $('.annotator-frame');
  function isHypothesisOpen() {
    if (annotator_frame.length === 0) {
      annotator_frame = $('.annotator-frame');
    }
    return annotator_frame.length > 0 && !annotator_frame.hasClass('annotator-collapsed');
  }

  // Toggle body class if hypothes.is is open or closed.
  var body = $('body');
  setInterval(function() {
    body.toggleClass('hypothesis-open', isHypothesisOpen());
  }, 200);

  $('i[data-toggle="collapse"]').click(function(ev) {
    // Prevent click within link to cause new page load.
    ev.preventDefault();
  });

  // Remove alert if user already closed it this session.
  $('.alert[data-alert-name]').each(function(i, el) {
    var $el = $(el);
    var re = new RegExp("(?:(?:^|.*;\\s*)" + $el.data('alert-name') + "_closed" +"\\s*\\=\\s*([^;]*).*$)|^.*$");
    var closed = document.cookie.replace(re, "$1")
    if (closed) {
      $el.remove();
    }
  });
  // Hide all email asks if they've already submitted their email address.
  if (document.cookie.replace(/(?:(?:^|.*;\s*)submittedEmail\s*\=\s*([^;]*).*$)|^.*$/, "$1") === "true") {
    $('.email-cta').not('.always-show').addClass('hide');
  }

  $(document).on('click', '.close-bottom-cta', function(ev) {
    ev.preventDefault();
    $(ev.currentTarget).closest('.email-cta').addClass('hide');
  });

  // Enforce required for browsers that don't.
  $(document).on('submit', 'form', function(ev) {
    var form = $(ev.currentTarget);
    var required = form.find('[required]');
    var has_all_required = true;
    if (required.length === 0) { return true; }

    required.each(function(index, el) {
      if ($(el).val() === "") {
        has_all_required = false;
      }
    });
    if (!has_all_required) {
      ev.preventDefault();
    }
  });

  // Stash cookie with alert name if alert is closed.
  $('.alert[data-alert-name]').on('close.bs.alert', function(ev) {
    var target = $(ev.target);
    document.cookie = target.data('alert-name') + "_closed=true; path=/";
  });

});
<|endoftext|>


<commit_before>/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
'use strict';

const Hero = require('Hero');
const Metadata = require('Metadata');
const Prism = require('Prism');
const React = require('React');
const ShowcaseAppIcon = require('ShowcaseAppIcon');
const Site = require('Site');

const pinnedApps = Metadata.showcaseApps.filter(app => {
  return app.pinned;
});

const AppList = React.createClass({
  render: function() {
    return (

        {this.props.apps.map(this._renderApp)}

    );
  },

  _renderApp: function(app, i) {
    return (

        <ShowcaseAppIcon
          iconUri={app.icon}
          name={app.name}
          linkUri={app.infoLink} />

    );
  },
});

const index = React.createClass({
  render: function() {
    return (
      <Site>
        <Hero title="React Native" subtitle="Learn once, write anywhere: Build mobile apps with React">

            Get Started
            Take the Tutorial

        </Hero>

Build native mobile apps using JavaScript and React

              React Native lets you build mobile apps using only JavaScript. It uses the same design as React, letting you compose a rich mobile UI from declarative components.

            <Prism>

{`import React, { Component } from 'react';
import { Text, View } from 'react-native';

class WhyReactNativeIsSoGreat extends Component {
  render() {
    return (
      <View>
        <Text>
          If you like React on the web, you'll like React Native.
        </Text>
        <Text>
          You just use native components like 'View' and 'Text',
          instead of web components like 'div' and 'span'.
        </Text>
      </View>
    );
  }
}`}
            </Prism>

A React Native app is a real mobile app

              With React Native, you don't build a “mobile web app”, an “HTML5 app”, or a “hybrid app”. You build a real mobile app that's indistinguishable from an app built using Objective-C or Java. React Native uses the same fundamental UI building blocks as regular iOS and Android apps. You just put those building blocks together using JavaScript and React.

            <Prism>
{`import React, { Component } from 'react';
import { Image, ScrollView, Text } from 'react-native';

class AwkwardScrollingImageWithText extends Component {
  render() {
    return (
      <ScrollView>
        <Image source={{uri: 'https://i.chzbgr.com/full/7345954048/h7E2C65F9/'}} />
        <Text>
          On iOS, a React Native ScrollView uses a native UIScrollView.
          On Android, it uses a native ScrollView.

          On iOS, a React Native Image uses a native UIImageView.
          On Android, it uses a native ImageView.

          React Native wraps the fundamental native components, giving you
          the performance of a native app, plus the clean design of React.
        </Text>
      </ScrollView>
    );
  }
}`}
            </Prism>

Don't waste time recompiling

              React Native lets you build your app faster. Instead of recompiling, you can reload your app instantly. With hot reloading, you can even run new code while retaining your application state. Give it a try - it's a magical experience.

Use native code when you need to

              React Native combines smoothly with components written in Objective-C, Java, or Swift. It's simple to drop down to native code if you need to optimize a few aspects of your application. It's also easy to build part of your app in React Native, and part of your app using native code directly - that's how the Facebook app works.

            <Prism>
{`import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { TheGreatestComponentInTheWorld } from './your-native-code';

class SomethingFast extends Component {
  render() {
    return (
      <View>
        <TheGreatestComponentInTheWorld />
        <Text>
          TheGreatestComponentInTheWorld could use native Objective-C,
          Java, or Swift - the product development process is the same.
        </Text>
      </View>
    );
  }
}`}
            </Prism>

              Get Started with React Native

Who's using React Native?

              Thousands of apps are using React Native, from established Fortune 500 companies to hot new startups. If you're curious to see what can be accomplished with React Native, check out these apps!

            <AppList apps={pinnedApps} />

              Some of these are hybrid native/React Native apps.

              More React Native apps

      </Site>
    );
  }
});

module.exports = index;
<commit_msg>Fix #14566, image in second code sample not rendering<commit_after>/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
'use strict';

const Hero = require('Hero');
const Metadata = require('Metadata');
const Prism = require('Prism');
const React = require('React');
const ShowcaseAppIcon = require('ShowcaseAppIcon');
const Site = require('Site');

const pinnedApps = Metadata.showcaseApps.filter(app => {
  return app.pinned;
});

const AppList = React.createClass({
  render: function() {
    return (

        {this.props.apps.map(this._renderApp)}

    );
  },

  _renderApp: function(app, i) {
    return (

        <ShowcaseAppIcon
          iconUri={app.icon}
          name={app.name}
          linkUri={app.infoLink} />

    );
  },
});

const index = React.createClass({
  render: function() {
    return (
      <Site>
        <Hero title="React Native" subtitle="Learn once, write anywhere: Build mobile apps with React">

            Get Started
            Take the Tutorial

        </Hero>

Build native mobile apps using JavaScript and React

              React Native lets you build mobile apps using only JavaScript. It uses the same design as React, letting you compose a rich mobile UI from declarative components.

            <Prism>

{`import React, { Component } from 'react';
import { Text, View } from 'react-native';

class WhyReactNativeIsSoGreat extends Component {
  render() {
    return (
      <View>
        <Text>
          If you like React on the web, you'll like React Native.
        </Text>
        <Text>
          You just use native components like 'View' and 'Text',
          instead of web components like 'div' and 'span'.
        </Text>
      </View>
    );
  }
}`}
            </Prism>

A React Native app is a real mobile app

              With React Native, you don't build a “mobile web app”, an “HTML5 app”, or a “hybrid app”. You build a real mobile app that's indistinguishable from an app built using Objective-C or Java. React Native uses the same fundamental UI building blocks as regular iOS and Android apps. You just put those building blocks together using JavaScript and React.

            <Prism>
{`import React, { Component } from 'react';
import { Image, ScrollView, Text } from 'react-native';

class AwkwardScrollingImageWithText extends Component {
  render() {
    return (
      <ScrollView>
        <Image
          source={{uri: 'https://i.chzbgr.com/full/7345954048/h7E2C65F9/'}}
          style={{width: 320, height:180}}
        />
        <Text>
          On iOS, a React Native ScrollView uses a native UIScrollView.
          On Android, it uses a native ScrollView.

          On iOS, a React Native Image uses a native UIImageView.
          On Android, it uses a native ImageView.

          React Native wraps the fundamental native components, giving you
          the performance of a native app, plus the clean design of React.
        </Text>
      </ScrollView>
    );
  }
}`}
            </Prism>

Don't waste time recompiling

              React Native lets you build your app faster. Instead of recompiling, you can reload your app instantly. With hot reloading, you can even run new code while retaining your application state. Give it a try - it's a magical experience.

Use native code when you need to

              React Native combines smoothly with components written in Objective-C, Java, or Swift. It's simple to drop down to native code if you need to optimize a few aspects of your application. It's also easy to build part of your app in React Native, and part of your app using native code directly - that's how the Facebook app works.

            <Prism>
{`import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { TheGreatestComponentInTheWorld } from './your-native-code';

class SomethingFast extends Component {
  render() {
    return (
      <View>
        <TheGreatestComponentInTheWorld />
        <Text>
          TheGreatestComponentInTheWorld could use native Objective-C,
          Java, or Swift - the product development process is the same.
        </Text>
      </View>
    );
  }
}`}
            </Prism>

              Get Started with React Native

Who's using React Native?

              Thousands of apps are using React Native, from established Fortune 500 companies to hot new startups. If you're curious to see what can be accomplished with React Native, check out these apps!

            <AppList apps={pinnedApps} />

              Some of these are hybrid native/React Native apps.

              More React Native apps

      </Site>
    );
  }
});

module.exports = index;
<|endoftext|>


<commit_before>class IgnoreHelper {
    constructor(keyPrefix, brain, logger) {
        this.keyPrefix = keyPrefix;
        this.brain     = brain;
        this.logger    = logger;
    }

    toggleIgnore(type, id, callback) {
        if (!this.brain.get) {
            this.logger.error("Brain isn't initialized");
            throw new Error("Brain isn't initialized");
        }

        this.getIgnores(ignored => {
            let index = ignored.findIndex(item => item.type === type && item.id === id);
            if (index === -1) {
                ignored.push({type: type, id: id, ignored: true});
                index = ignored.length - 1;
            } else {
                ignored[index].ignored = !ignored[index].ignored;
            }

            this.saveIgnores(ignored, callback(ignored[index]));
        });
    }

    ignore(type, id, callback) {
        if (!this.brain.get) {
            this.logger.error("Brain isn't initialized");
            throw new Error("Brain isn't initialized");
        }

        this.getIgnores(ignored => {
            let index = ignored.findIndex(item => item.type === type && item.id === id);
            if (index === -1) {
                ignored.push({type: type, id: id, ignored: true});
                index = ignored.length - 1;
            } else {
                ignored[index].ignored = true;
            }

            this.saveIgnores(ignored, callback(ignored[index]));
        });
    }

    batchIgnore(type, ids, callback) {

        if (!this.brain.get) {
            this.logger.error("Brain isn't initialized");
            throw new Error("Brain isn't initialized");
        }

        this.getIgnores(ignored => {
            for (let i in ids) {
                if (!id.hasOwnProperty(index)) {
                    continue;
                }

                let id = ids[i];

                let index = ignored.findIndex(item => item.type === type && item.id === id);
                if (index === -1) {
                    ignored.push({type: type, id: id, ignored: true});
                    index = ignored.length - 1;
                } else {
                    ignored[index].ignored = true;
                }
            }

            this.saveIgnores(ignored, callback(ignored));
        });
    }

    unignore(type, id, callback) {
        if (!this.brain.get) {
            this.logger.error("Brain isn't initialized");
            throw new Error("Brain isn't initialized");
        }

        this.getIgnores(ignored => {
            let index = ignored.findIndex(item => item.type === type && item.id === id);
            if (index === -1) {
                ignored.push({type: type, id: id, ignored: false});
                index = ignored.length - 1;
            } else {
                ignored[index].ignored = false;
            }

            this.saveIgnores(ignored, callback(ignored[index]));
        });
    }

    getIgnores(callback) {
        this.brain.get(this.keyPrefix + '.ignore', (err, results) => {
            if (err) { return this.logger.error(err); }
            let ignored = results === null ? [] : JSON.parse(results);

            return callback(ignored);
        });
    }

    saveIgnores(ignored, callback) {
        this.brain.set(this.keyPrefix + '.ignore', JSON.stringify(ignored), callback);
    }

    reset() {
        this.saveIgnores([]);
    }

    isNotIgnored(message, callback) {
        let isIgnored = false;

        this.getIgnores(ignored => {
            ignored.forEach(item => {
                if (isIgnored || !item.ignored) {
                    return;
                }

                if (item.type === 'server' && item.id == message.server.id) {
                    isIgnored = true;
                }

                if (item.type === 'channel') {
                    let split = item.id.split('-');
                    if (split[0] == message.server.id && split[1] == message.channel.id) {
                        isIgnored = true;
                    }
                }

                if (item.type === 'user' && item.id == message.author.id) {
                    isIgnored = true;
                }
            });

            if (!isIgnored) {
                callback();
            }
        });
    }
}

module.exports = IgnoreHelper;<commit_msg>Adding batch ignore<commit_after>class IgnoreHelper {
    constructor(keyPrefix, brain, logger) {
        this.keyPrefix = keyPrefix;
        this.brain     = brain;
        this.logger    = logger;
    }

    toggleIgnore(type, id, callback) {
        if (!this.brain.get) {
            this.logger.error("Brain isn't initialized");
            throw new Error("Brain isn't initialized");
        }

        this.getIgnores(ignored => {
            let index = ignored.findIndex(item => item.type === type && item.id === id);
            if (index === -1) {
                ignored.push({type: type, id: id, ignored: true});
                index = ignored.length - 1;
            } else {
                ignored[index].ignored = !ignored[index].ignored;
            }

            this.saveIgnores(ignored, callback.call(ignored[index]));
        });
    }

    ignore(type, id, callback) {
        if (!this.brain.get) {
            this.logger.error("Brain isn't initialized");
            throw new Error("Brain isn't initialized");
        }

        this.getIgnores(ignored => {
            let index = ignored.findIndex(item => item.type === type && item.id === id);
            if (index === -1) {
                ignored.push({type: type, id: id, ignored: true});
                index = ignored.length - 1;
            } else {
                ignored[index].ignored = true;
            }

            this.saveIgnores(ignored, callback.call(ignored[index]));
        });
    }

    batchIgnore(type, ids, callback) {

        if (!this.brain.get) {
            this.logger.error("Brain isn't initialized");
            throw new Error("Brain isn't initialized");
        }

        this.getIgnores(ignored => {
            for (let i in ids) {
                if (!id.hasOwnProperty(index)) {
                    continue;
                }

                let id = ids[i];

                let index = ignored.findIndex(item => item.type === type && item.id === id);
                if (index === -1) {
                    ignored.push({type: type, id: id, ignored: true});
                    index = ignored.length - 1;
                } else {
                    ignored[index].ignored = true;
                }
            }

            this.saveIgnores(ignored, callback.call(ignored));
        });
    }

    unignore(type, id, callback) {
        if (!this.brain.get) {
            this.logger.error("Brain isn't initialized");
            throw new Error("Brain isn't initialized");
        }

        this.getIgnores(ignored => {
            let index = ignored.findIndex(item => item.type === type && item.id === id);
            if (index === -1) {
                ignored.push({type: type, id: id, ignored: false});
                index = ignored.length - 1;
            } else {
                ignored[index].ignored = false;
            }

            this.saveIgnores(ignored, callback.call(ignored[index]));
        });
    }

    getIgnores(callback) {
        this.brain.get(this.keyPrefix + '.ignore', (err, results) => {
            if (err) { return this.logger.error(err); }
            let ignored = results === null ? [] : JSON.parse(results);

            return callback(ignored);
        });
    }

    saveIgnores(ignored, callback) {
        this.brain.set(this.keyPrefix + '.ignore', JSON.stringify(ignored), callback);
    }

    reset() {
        this.saveIgnores([]);
    }

    isNotIgnored(message, callback) {
        let isIgnored = false;

        this.getIgnores(ignored => {
            ignored.forEach(item => {
                if (isIgnored || !item.ignored) {
                    return;
                }

                if (item.type === 'server' && item.id == message.server.id) {
                    isIgnored = true;
                }

                if (item.type === 'channel') {
                    let split = item.id.split('-');
                    if (split[0] == message.server.id && split[1] == message.channel.id) {
                        isIgnored = true;
                    }
                }

                if (item.type === 'user' && item.id == message.author.id) {
                    isIgnored = true;
                }
            });

            if (!isIgnored) {
                callback();
            }
        });
    }
}

module.exports = IgnoreHelper;<|endoftext|>


<commit_before>'use strict';

var $ = require('jquery');
var m = require('mithril');
var Raven = require('raven-js');
var Treebeard = require('treebeard');
var citations = require('js/citations');
var clipboard = require('js/clipboard');

var apaStyle = require('raw!styles/apa.csl');

var errorPage = require('raw!citations_load_error.html');

require('css/fangorn.css');

function resolveToggle(item) {
    var toggleMinus = m('i.fa.fa-minus', ' ');
    var togglePlus = m('i.fa.fa-plus', ' ');
    if (item.kind === 'folder') {
        return item.open ? toggleMinus : togglePlus;
    } else {
        return '';
    }
}

function resolveIcon(item) {
    var privateFolder = m('img', {
        src: '/static/img/hgrid/fatcowicons/folder_delete.png'
    });
    var openFolder = m('i.fa fa-folder-open', ' ');
    var closedFolder = m('i.fa fa-folder', ' ');

    if (item.kind === 'folder') {
        return item.open ? openFolder : closedFolder;
    } else if (item.kind === 'message'){
        return '';
    } else if (item.data.icon) {
        return m('i.fa.' + item.data.icon, ' ');
    } else {
        return m('i.fa fa-file-o');
    }
}

var utils = {
    reduce: function(array, dst, fn) {
        for (var i = 0; i < array.length; i++) {
            fn(array[i], dst);
        }
        return dst;
    },
    zip: function(array1, array2) {
        var zipped = [];
        var min = Math.min(array1.length, array2.length);
        for (var i = 0; i < min; i++) {
            zipped.push([array1[i], array2[i]]);
        }
        return zipped;
    }
};

var objectify = function(array, key) {
    key = key || 'id';
    return utils.reduce(
        array, {},
        function(item, acc) {
            return acc[item[key]] = item;
        }
    );
};

var formatResult = function(state) {
    return '
' + state.title + '
';
};

var formatSelection = function(state) {
    return state.title;
};

var mergeConfigs = function() {
    var unloads = [];
    var args = Array.prototype.slice.call(arguments);
    return function(elm, isInit, ctx) {
        for (var i = 0; i < args.length; i++) {
            args[i] && args[i](elm, isInit, ctx);
            ctx.onunload && unloads.push(ctx.onunload);
            ctx.onunload = null;
        }
        ctx.onunload = function() {
            for (var i = 0; i < unloads.length; i++) {
                unloads[i]();
            }
        };
    };
};

var tooltipConfig = function(elm, isInit, ctx) {
    var $elm = $(elm);
    $elm.tooltip({
        container: 'body'
    });
    ctx.onunload = function() {
        $elm.tooltip('destroy');
    };
};

var makeButtons = function(item, col, buttons) {
    return buttons.map(function(button) {
        var self = this;
        return m(
            'a', {
                'data-col': item.id
            }, [
                m(
                    'i', {
                        title: button.tooltip,
                        style: button.style,
                        class: button.css,
                        'data-toggle': 'tooltip',
                        'data-placement': 'bottom',
                        'data-clipboard-target': item.data.csl ? item.data.csl.id : button.clipboard,
                        config: mergeConfigs(button.config, tooltipConfig),
                        onclick: button.onclick ?
                            function(event) {
                                button.onclick.call(self, event, item, col);
                            } : null
                    }, [
                        m(
                            'span', {
                                class: button.icon
                            },
                            button.name
                        )
                    ]
                )
            ]
        );
    });
};

var buildExternalUrl = function(csl) {
    if (csl.URL) {
        return csl.URL;
    } else if (csl.DOI) {
        return 'http://dx.doi.org/' + csl.DOI;
    } else if (csl.PMID) {
        return 'http://www.ncbi.nlm.nih.gov/pubmed/' + csl.PMID;
    }
    return null;
};

var makeClipboardConfig = function(getText) {
    return function(elm, isInit, ctx) {
        var $elm = $(elm);
        if (!elm._client) {
            elm._client = clipboard(elm);
            // Attach `beforecopy` handler to ensure updated clipboard text
            if (getText) {
                elm._client.on('beforecopy', function() {
                    $elm.attr('data-clipboard-text', getText());
                });
            }
        }
        ctx.onunload = function() {
            elm._client && elm._client.destroy();
        };
    };
};

var renderActions = function(item, col) {
    var self = this;
    var buttons = [];
    if (item.kind === 'file') {
        buttons.push({
            name: '',
            icon: 'fa fa-file-o',
            css: 'btn btn-default btn-xs',
            tooltip: 'Copy citation',
            clipboard: self.getCitation(item),
            config: makeClipboardConfig()
        });
        // Add link to external document
        var externalUrl = buildExternalUrl(item.data.csl);
        if (externalUrl) {
            buttons.push({
                name: '',
                icon: 'fa fa-external-link',
                css: 'btn btn-default btn-xs',
                tooltip: 'View original document',
                onclick: function(event) {
                    window.open(externalUrl);
                }
            });
        }
        // Add link to document on reference management service
        if (item.data.serviceUrl) {
            buttons.push({
                name: '',
                icon: 'fa fa-link',
                css: 'btn btn-default btn-xs',
                tooltip: 'View on ' + self.provider,
                onclick: function(event) {
                    window.open(item.data.serviceUrl);
                }
            });
        }
    } else if (item.kind === 'folder' && item.open && item.children.length) {
        buttons.push({
            name: '',
            icon: 'fa fa-file-o',
            css: 'btn btn-default btn-xs',
            tooltip: 'Copy citations',
            config: makeClipboardConfig(function() {
                return self.getCitations(item).join('\n');
            })
        });
        buttons.push({
            name: '',
            icon: 'fa fa-arrow-circle-o-down',
            css: 'btn btn-default btn-xs',
            tooltip: 'Download citations',
            config: function(elm, isInit, ctx) {
                var text = self.getCitations(item).join('\n');
                $(elm).parent('a')
                    .attr('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text))
                    .attr('download', item.data.name + '-' + self.styleName + '.txt');
            }
        });
    }
    return makeButtons(item, col, buttons);
};

var treebeardOptions = {
    rowHeight: 30,
    lazyLoad: true,
    showFilter: false,
    resolveIcon: resolveIcon,
    resolveToggle: resolveToggle,
    lazyLoadPreprocess: function(res) {
        return res.contents;
    },
    columnTitles: function() {
        return [{
            title: 'Citation',
            width: '80%',
            sort: false
        }, {
            title: 'Actions',
            width: '20%',
            sort: false
        }];
    }
};

var CitationGrid = function(provider, gridSelector, styleSelector, apiUrl) {
    var self = this;

    self.provider = provider;
    self.gridSelector = gridSelector;
    self.styleSelector = styleSelector;
    self.apiUrl = apiUrl;

    self.styleName = 'apa';
    self.styleXml = apaStyle;
    self.bibliographies = {};

    self.initTreebeard();
    self.initStyleSelect();
};

CitationGrid.prototype.initTreebeard = function() {
    var self = this;
    var options = $.extend({
            divID: self.gridSelector.replace('#', ''),
            filesData: self.apiUrl,
            resolveLazyloadUrl: function(item) {
                return item.data.urls.fetch;
            },
            // Wrap callback in closure to preserve intended `this`
            resolveRows: function() {
                return self.resolveRowAux.call(self, arguments);
            },
            ondataloaderror: function(err) {
                $(self.gridSelector).html(errorPage);
            }
        },
        treebeardOptions
    );
    var preprocess = options.lazyLoadPreprocess;
    options.lazyLoadPreprocess = function(data){
        data = preprocess(data);
        // TODO remove special case for Zotero
        if (self.provider === 'Zotero') {
            if (data.length >= 200) {
        data.push({
                    name: 'Only 200 citations may be displayed',
                    kind: 'message'
                });
            }
        }
        return data;
    };
    self.treebeard = new Treebeard(options);
};

CitationGrid.prototype.initStyleSelect = function() {
    var self = this;
    var $input = $(self.styleSelector);
    $input.select2({
        allowClear: false,
        formatResult: formatResult,
        formatSelection: formatSelection,
        placeholder: 'Citation Style (e.g. "APA")',
        minimumInputLength: 1,
        ajax: {
            url: '/api/v1/citations/styles/',
            quietMillis: 200,
            data: function(term, page) {
                return {
                    q: term
                };
            },
            results: function(data, page) {
                return {
                    results: data.styles
                };
            },
            cache: true
        }
    }).on('select2-selecting', function(event) {
        var styleUrl = '/static/vendor/bower_components/styles/' + event.val + '.csl';
        $.get(styleUrl).done(function(xml) {
            self.updateStyle(event.val, xml);
        }).fail(function(jqxhr, status, error) {
            Raven.captureMessage('Error while selecting citation style: ' + event.val, {
                url: styleUrl,
                status: status,
                error: error
            });
        });
    });
};

CitationGrid.prototype.updateStyle = function(name, xml) {
    this.styleName = name;
    this.styleXml = xml;
    this.bibliographies = {};
    this.treebeard.tbController.redraw();
};

CitationGrid.prototype.makeBibliography = function(folder) {
    var data = objectify(
        folder.children.filter(function(child) {
            return child.kind === 'file';
        }).map(function(child) {
            return child.data.csl;
        })
    );
    var citeproc = citations.makeCiteproc(this.styleXml, data, 'html');
    var bibliography = citeproc.makeBibliography();
    if (bibliography[0].entry_ids) {
        return utils.reduce(
            utils.zip(bibliography[0].entry_ids, bibliography[1]), {},
            function(pair, acc) {
                return acc[pair[0][0]] = pair[1];
            }
        );
    }
    return {};
};

CitationGrid.prototype.getBibliography = function(folder) {
    this.bibliographies[folder.id] = this.bibliographies[folder.id] || this.makeBibliography(folder);
    return this.bibliographies[folder.id];
};

CitationGrid.prototype.getCitation = function(item) {
    var bibliography = this.getBibliography(item.parent());
    return bibliography[item.data.csl.id];
};

CitationGrid.prototype.getCitations = function(folder) {
    var self = this;
    return folder.children.filter(function(child) {
        return child.kind === 'file';
    }).map(function(child) {
        return self.getCitation(child);
    });
};

CitationGrid.prototype.resolveRowAux = function(item) {
    var self = this;
    return [{
        data: 'csl',
        folderIcons: true,
        custom: function(item) {
            if (item.kind === 'folder'){
                return item.data.name;
            }
            else if (item.kind === 'message'){
                return item.data.name;
            }
            else {
                return m('span', {id: item.data.csl.id}, [
                    m.trust(self.getCitation(item))
                        ]);
            }
        }
    }, {
        // Wrap callback in closure to preserve intended `this`
        custom: function() {
            return renderActions.apply(self, arguments);
        }
    }];
};

module.exports = CitationGrid;
<commit_msg>Parse citations in RTF format for download<commit_after>'use strict';

var $ = require('jquery');
var m = require('mithril');
var Raven = require('raven-js');
var Treebeard = require('treebeard');
var citations = require('js/citations');
var clipboard = require('js/clipboard');

var apaStyle = require('raw!styles/apa.csl');

var errorPage = require('raw!citations_load_error.html');

require('css/fangorn.css');

function resolveToggle(item) {
    var toggleMinus = m('i.fa.fa-minus', ' ');
    var togglePlus = m('i.fa.fa-plus', ' ');
    if (item.kind === 'folder') {
        return item.open ? toggleMinus : togglePlus;
    } else {
        return '';
    }
}

function resolveIcon(item) {
    var privateFolder = m('img', {
        src: '/static/img/hgrid/fatcowicons/folder_delete.png'
    });
    var openFolder = m('i.fa fa-folder-open', ' ');
    var closedFolder = m('i.fa fa-folder', ' ');

    if (item.kind === 'folder') {
        return item.open ? openFolder : closedFolder;
    } else if (item.kind === 'message'){
        return '';
    } else if (item.data.icon) {
        return m('i.fa.' + item.data.icon, ' ');
    } else {
        return m('i.fa fa-file-o');
    }
}

var utils = {
    reduce: function(array, dst, fn) {
        for (var i = 0; i < array.length; i++) {
            fn(array[i], dst);
        }
        return dst;
    },
    zip: function(array1, array2) {
        var zipped = [];
        var min = Math.min(array1.length, array2.length);
        for (var i = 0; i < min; i++) {
            zipped.push([array1[i], array2[i]]);
        }
        return zipped;
    }
};

var objectify = function(array, key) {
    key = key || 'id';
    return utils.reduce(
        array, {},
        function(item, acc) {
            return acc[item[key]] = item;
        }
    );
};

var formatResult = function(state) {
    return '
' + state.title + '
';
};

var formatSelection = function(state) {
    return state.title;
};

var mergeConfigs = function() {
    var unloads = [];
    var args = Array.prototype.slice.call(arguments);
    return function(elm, isInit, ctx) {
        for (var i = 0; i < args.length; i++) {
            args[i] && args[i](elm, isInit, ctx);
            ctx.onunload && unloads.push(ctx.onunload);
            ctx.onunload = null;
        }
        ctx.onunload = function() {
            for (var i = 0; i < unloads.length; i++) {
                unloads[i]();
            }
        };
    };
};

var tooltipConfig = function(elm, isInit, ctx) {
    var $elm = $(elm);
    $elm.tooltip({
        container: 'body'
    });
    ctx.onunload = function() {
        $elm.tooltip('destroy');
    };
};

var makeButtons = function(item, col, buttons) {
    return buttons.map(function(button) {
        var self = this;
        return m(
            'a', {
                'data-col': item.id
            }, [
                m(
                    'i', {
                        title: button.tooltip,
                        style: button.style,
                        class: button.css,
                        'data-toggle': 'tooltip',
                        'data-placement': 'bottom',
                        'data-clipboard-target': item.data.csl ? item.data.csl.id : button.clipboard,
                        config: mergeConfigs(button.config, tooltipConfig),
                        onclick: button.onclick ?
                            function(event) {
                                button.onclick.call(self, event, item, col);
                            } : null
                    }, [
                        m(
                            'span', {
                                class: button.icon
                            },
                            button.name
                        )
                    ]
                )
            ]
        );
    });
};

var buildExternalUrl = function(csl) {
    if (csl.URL) {
        return csl.URL;
    } else if (csl.DOI) {
        return 'http://dx.doi.org/' + csl.DOI;
    } else if (csl.PMID) {
        return 'http://www.ncbi.nlm.nih.gov/pubmed/' + csl.PMID;
    }
    return null;
};

var makeClipboardConfig = function(getText) {
    return function(elm, isInit, ctx) {
        var $elm = $(elm);
        if (!elm._client) {
            elm._client = clipboard(elm);
            // Attach `beforecopy` handler to ensure updated clipboard text
            if (getText) {
                elm._client.on('beforecopy', function() {
                    $elm.attr('data-clipboard-text', getText());
                });
            }
        }
        ctx.onunload = function() {
            elm._client && elm._client.destroy();
        };
    };
};

var renderActions = function(item, col) {
    var self = this;
    var buttons = [];
    if (item.kind === 'file') {
        buttons.push({
            name: '',
            icon: 'fa fa-file-o',
            css: 'btn btn-default btn-xs',
            tooltip: 'Copy citation',
            clipboard: self.getCitation(item),
            config: makeClipboardConfig()
        });
        // Add link to external document
        var externalUrl = buildExternalUrl(item.data.csl);
        if (externalUrl) {
            buttons.push({
                name: '',
                icon: 'fa fa-external-link',
                css: 'btn btn-default btn-xs',
                tooltip: 'View original document',
                onclick: function(event) {
                    window.open(externalUrl);
                }
            });
        }
        // Add link to document on reference management service
        if (item.data.serviceUrl) {
            buttons.push({
                name: '',
                icon: 'fa fa-link',
                css: 'btn btn-default btn-xs',
                tooltip: 'View on ' + self.provider,
                onclick: function(event) {
                    window.open(item.data.serviceUrl);
                }
            });
        }
    } else if (item.kind === 'folder' && item.open && item.children.length) {
        buttons.push({
            name: '',
            icon: 'fa fa-file-o',
            css: 'btn btn-default btn-xs',
            tooltip: 'Copy citations',
            config: makeClipboardConfig(function() {
                return self.getCitations(item).join('\n');
            })
        });
        buttons.push({
            name: '',
            icon: 'fa fa-arrow-circle-o-down',
            css: 'btn btn-default btn-xs',
            tooltip: 'Download citations',
            config: function(elm, isInit, ctx) {
                var text = '{\\rtf1\\ansi\n' + self.getCitations(item, true).join('\n') + '\n}';
                $(elm).parent('a')
                    .attr('href', 'data:text/enriched;charset=utf-8,' + encodeURIComponent(text))
                    .attr('download', item.data.name + '-' + self.styleName + '.rtf');
            }
        });
    }
    return makeButtons(item, col, buttons);
};

var treebeardOptions = {
    rowHeight: 30,
    lazyLoad: true,
    showFilter: false,
    resolveIcon: resolveIcon,
    resolveToggle: resolveToggle,
    lazyLoadPreprocess: function(res) {
        return res.contents;
    },
    columnTitles: function() {
        return [{
            title: 'Citation',
            width: '80%',
            sort: false
        }, {
            title: 'Actions',
            width: '20%',
            sort: false
        }];
    }
};

var CitationGrid = function(provider, gridSelector, styleSelector, apiUrl) {
    var self = this;

    self.provider = provider;
    self.gridSelector = gridSelector;
    self.styleSelector = styleSelector;
    self.apiUrl = apiUrl;

    self.styleName = 'apa';
    self.styleXml = apaStyle;
    self.bibliographies = {};

    self.initTreebeard();
    self.initStyleSelect();
};

CitationGrid.prototype.initTreebeard = function() {
    var self = this;
    var options = $.extend({
            divID: self.gridSelector.replace('#', ''),
            filesData: self.apiUrl,
            resolveLazyloadUrl: function(item) {
                return item.data.urls.fetch;
            },
            // Wrap callback in closure to preserve intended `this`
            resolveRows: function() {
                return self.resolveRowAux.call(self, arguments);
            },
            ondataloaderror: function(err) {
                $(self.gridSelector).html(errorPage);
            }
        },
        treebeardOptions
    );
    var preprocess = options.lazyLoadPreprocess;
    options.lazyLoadPreprocess = function(data){
        data = preprocess(data);
        // TODO remove special case for Zotero
        if (self.provider === 'Zotero') {
            if (data.length >= 200) {
        data.push({
                    name: 'Only 200 citations may be displayed',
                    kind: 'message'
                });
            }
        }
        return data;
    };
    self.treebeard = new Treebeard(options);
};

CitationGrid.prototype.initStyleSelect = function() {
    var self = this;
    var $input = $(self.styleSelector);
    $input.select2({
        allowClear: false,
        formatResult: formatResult,
        formatSelection: formatSelection,
        placeholder: 'Citation Style (e.g. "APA")',
        minimumInputLength: 1,
        ajax: {
            url: '/api/v1/citations/styles/',
            quietMillis: 200,
            data: function(term, page) {
                return {
                    q: term
                };
            },
            results: function(data, page) {
                return {
                    results: data.styles
                };
            },
            cache: true
        }
    }).on('select2-selecting', function(event) {
        var styleUrl = '/static/vendor/bower_components/styles/' + event.val + '.csl';
        $.get(styleUrl).done(function(xml) {
            self.updateStyle(event.val, xml);
        }).fail(function(jqxhr, status, error) {
            Raven.captureMessage('Error while selecting citation style: ' + event.val, {
                url: styleUrl,
                status: status,
                error: error
            });
        });
    });
};

CitationGrid.prototype.updateStyle = function(name, xml) {
    this.styleName = name;
    this.styleXml = xml;
    this.bibliographies = {};
    this.treebeard.tbController.redraw();
};

CitationGrid.prototype.makeBibliography = function(folder) {
    var data = objectify(
        folder.children.filter(function(child) {
            return child.kind === 'file';
        }).map(function(child) {
            return child.data.csl;
        })
    );
    var citeproc = citations.makeCiteproc(this.styleXml, data, 'html');
    var bibliography = citeproc.makeBibliography();
    if (bibliography[0].entry_ids) {
        return utils.reduce(
            utils.zip(bibliography[0].entry_ids, bibliography[1]), {},
            function(pair, acc) {
                return acc[pair[0][0]] = pair[1];
            }
        );
    }
    return {};
};

CitationGrid.prototype.getBibliography = function(folder) {
    this.bibliographies[folder.id] = this.bibliographies[folder.id] || this.makeBibliography(folder);
    return this.bibliographies[folder.id];
};

CitationGrid.prototype.getCitation = function(item) {
    var bibliography = this.getBibliography(item.parent());
    return bibliography[item.data.csl.id];
};

CitationGrid.prototype.parseRTF = function(item) {
    if (item.children.length) {
        for (var i = 0; i < item.children.length; i++){
            return self.parseRTF(item.childNodes[0])
        }
    }

    var rtfCitation = item.outerHTML;

    if (rtfCitation.includes('')) {
        rtfCitation = rtfCitation.replace('', '\\ul ')
            .replace('', '\\ulnone ');
    }

    return rtfCitation.replace('', '\\i ').replace('', '\\i0 ')
        .replace('', '\\b ').replace('', '\\b0 ')
        .replace('', '\\ul ').replace('', '\\ulnone ');
};

CitationGrid.prototype.getRTFCitation = function(item) {
    //Change HTML tags in each citation item to .rtf tags, for exporting
    var citation = this.getBibliography(item.parent())[item.data.csl.id];
    var htmlCitation = $.parseHTML(citation)[0];
    var rtfCitation = '';

    for(var i = 0; i < htmlCitation.childNodes.length; i++) {
        rtfCitation += self.parseRTF(htmlCitation.childNodes[i])
    }

    return rtfCitation.replace('\n', '').trim() + '\\';
};

CitationGrid.prototype.getCitations = function(folder, rtf) {
    var self = this;
    return folder.children.filter(function(child) {
        return child.kind === 'file';
    }).map(function(child) {
        if (rtf) {
            return self.getRTFCitation(child);
        } else {
            return self.getCitation(child);
        }
    });
};

CitationGrid.prototype.resolveRowAux = function(item) {
    var self = this;
    return [{
        data: 'csl',
        folderIcons: true,
        custom: function(item) {
            if (item.kind === 'folder'){
                return item.data.name;
            }
            else if (item.kind === 'message'){
                return item.data.name;
            }
            else {
                return m('span', {id: item.data.csl.id}, [
                    m.trust(self.getCitation(item))
                        ]);
            }
        }
    }, {
        // Wrap callback in closure to preserve intended `this`
        custom: function() {
            return renderActions.apply(self, arguments);
        }
    }];
};

module.exports = CitationGrid;
<|endoftext|>