Source: fragments.js

"use strict";

var documents = require('./documents');
var DateUtils = require('./utils/date');
var WithFragments = documents.WithFragments;
var GroupDoc = documents.GroupDoc;

/**
 * Embodies a plain text fragment (beware: not a structured text)
 * @constructor
 * @global
 * @alias Fragments:Text
 */
function Text(data) {
  this.value = data;
}
Text.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function () {
    return "<span>" + this.value + "</span>";
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function() {
    return this.value;
  }
};
/**
 * Embodies a document link fragment (a link that is internal to a prismic.io repository)
 * @constructor
 * @global
 * @alias Fragments:DocumentLink
 */
function DocumentLink(data) {
  this.value = data;

  this.document = data.document;
  /**
   * @field
   * @description the linked document id
   */
  this.id = data.document.id;
  /**
   * @field
   * @description the linked document uid
   */
  this.uid = data.document.uid;
  /**
   * @field
   * @description the linked document tags
   */
  this.tags = data.document.tags;
  /**
   * @field
   * @description the linked document slug
   */
  this.slug = data.document.slug;
  /**
   * @field
   * @description the linked document type
   */
  this.type = data.document.type;
  /**
   * @field
   * @description the linked document language
   */
  this.lang = data.document.lang;

  var fragmentsData = {};
  if (data.document.data) {
    for (var field in data.document.data[data.document.type]) {
      fragmentsData[data.document.type + '.' + field] = data.document.data[data.document.type][field];
    }
  }
  /**
   * @field
   * @description the fragment list, if the fetchLinks parameter was used in at query time
   */
  this.fragments = parseFragments(fragmentsData);
  /**
   * @field
   * @description true if the link is broken, false otherwise
   */
  this.isBroken = data.isBroken;
}

DocumentLink.prototype = Object.create(WithFragments.prototype);

/**
 * Turns the fragment into a useable HTML version of it.
 * If the native HTML code doesn't suit your design, this function is meant to be overriden.
 *
 * @params {object} ctx - mandatory ctx object, with a useable linkResolver function (please read prismic.io online documentation about this)
 * @returns {string} - basic HTML code for the fragment
 */
DocumentLink.prototype.asHtml = function (ctx) {
  return "<a href=\""+this.url(ctx)+"\">"+this.url(ctx)+"</a>";
};

/**
 * Returns the URL of the document link.
 *
 * @params {object} linkResolver - mandatory linkResolver function (please read prismic.io online documentation about this)
 * @returns {string} - the proper URL to use
 */
DocumentLink.prototype.url = function (linkResolver) {
  return linkResolver(this, this.isBroken);
};

/**
 * Turns the fragment into a useable text version of it.
 *
 * @returns {string} - basic text version of the fragment
 */
DocumentLink.prototype.asText = function(linkResolver) {
  return this.url(linkResolver);
};

/**
 * Embodies a web link fragment
 * @constructor
 * @global
 * @alias Fragments:WebLink
 */
function WebLink(data) {
  /**
   * @field
   * @description the JSON object exactly as is returned in the "data" field of the JSON responses (see API documentation: https://developers.prismic.io/documentation/UjBe8bGIJ3EKtgBZ/api-documentation#json-responses)
   */
  this.value = data;
}
WebLink.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function () {
    var target = this.value.target ? 'target="' + this.value.target + '"' : '';
    return "<a " + target + " href=\""+this.url()+"\">"+this.url()+"</a>";
  },
  /**
   * Returns the URL of the link.
   *
   * @returns {string} - the proper URL to use
   */
  url: function() {
    return this.value.url;
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function() {
    return this.url();
  }
};

/**
 * Embodies a file link fragment
 * @constructor
 * @global
 * @alias Fragments:FileLink
 */
function FileLink(data) {
  /**
   * @field
   * @description the JSON object exactly as is returned in the "data" field of the JSON responses (see API documentation: https://developers.prismic.io/documentation/UjBe8bGIJ3EKtgBZ/api-documentation#json-responses)
   */
  this.value = data;
}
FileLink.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function () {
    return "<a href=\""+this.url()+"\">"+this.value.file.name+"</a>";
  },
  /**
   * Returns the URL of the link.
   *
   * @returns {string} - the proper URL to use
   */
  url: function() {
    return this.value.file.url;
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function() {
    return this.url();
  }
};

/**
 * Embodies an image link fragment
 * @constructor
 * @global
 * @alias Fragments:ImageLink
 */
function ImageLink(data) {
  /**
   *
   * @field
   * @description the JSON object exactly as is returned in the "data" field of the JSON responses (see API documentation: https://developers.prismic.io/documentation/UjBe8bGIJ3EKtgBZ/api-documentation#json-responses)
   */
  this.value = data;
}
ImageLink.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function () {
    return "<a href=\"" + this.url() + "\"><img src=\"" + this.url() + "\" alt=\"" + (this.alt || "") + " copyright=\"" + (this.copyright || "") + "\" \"></a>";
  },
  /**
   * Returns the URL of the link.
   *
   * @returns {string} - the proper URL to use
   */
  url: function() {
    return this.value.image.url;
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function() {
    return this.url();
  }
};

/**
 * Embodies a select fragment
 * @constructor
 * @global
 * @alias Fragments:Select
 */
function Select(data) {
  /**
   * @field
   * @description the text value of the fragment
   */
  this.value = data;
}
Select.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function () {
    return "<span>" + this.value + "</span>";
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function() {
    return this.value;
  }
};

/**
 * Embodies a color fragment
 * @constructor
 * @global
 * @alias Fragments:Color
 */
function Color(data) {
  /**
   * @field
   * @description the text value of the fragment
   */
  this.value = data;
}
Color.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function () {
    return "<span>" + this.value + "</span>";
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function() {
    return this.value;
  }
};

/**
 * Embodies a geopoint
 * @constructor
 * @global
 * @alias Fragments:GeoPoint
 */
function GeoPoint(data) {
  /**
   * @field
   * @description the latitude of the geo point
   */
  this.latitude = data.latitude;
  /**
   * @field
   * @description the longitude of the geo point
   */
  this.longitude = data.longitude;
}

GeoPoint.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function () {
    return '<div class="geopoint"><span class="latitude">' + this.latitude + '</span><span class="longitude">' + this.longitude + '</span></div>';
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function() {
    return '(' + this.latitude + "," + this.longitude + ')';
  }
};

/**
 * Embodies a Number fragment
 * @constructor
 * @global
 * @alias Fragments:Num
 */
function Num(data) {
  /**
   * @field
   * @description the integer value of the fragment
   */
  this.value = data;
}
Num.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function () {
    return "<span>" + this.value + "</span>";
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function() {
    if (this.value === null) {
      return null;
    } else {
      return this.value.toString();
    }
  }
};

/**
 * Embodies a Date fragment
 * @constructor
 * @global
 * @alias Fragments:Date
 */
function DateFragment(data) {
  /**
   * @field
   * @description the Date value of the fragment (as a regular JS Date object)
   */
  this.value = new Date(data);
}

DateFragment.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function () {
    return "<time>" + this.value + "</time>";
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function() {
    if (this.value === null) {
      return null;
    } else {
      return this.value.toString();
    }
  }
};

/**
 * Embodies a Timestamp fragment
 * @constructor
 * @global
 * @alias Fragments:Timestamp
 */
function Timestamp(data) {
  /**
   * @field
   * @description the Date value of the fragment (as a regular JS Date object)
   */
  this.value = DateUtils.parse(data);
}

Timestamp.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function () {
    return "<time>" + this.value + "</time>";
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function() {
    if (this.value === null) {
      return null;
    } else {
      return this.value.toString();
    }
  }
};

/**
 * Embodies an embed fragment
 * @constructor
 * @global
 * @alias Fragments:Embed
 */
function Embed(data) {
  /**
   * @field
   * @description the JSON object exactly as is returned in the "data" field of the JSON responses (see API documentation: https://developers.prismic.io/documentation/UjBe8bGIJ3EKtgBZ/api-documentation#json-responses)
   */
  this.value = data;
}

Embed.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function () {
    return this.value.oembed.html;
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function() {
    return "";
  }
};

/**
 * Embodies an Image fragment
 * @constructor
 * @global
 * @alias Fragments:ImageEl
 */
function ImageEl(main, views) {
  /**
   * @field
   * @description the main ImageView for this image
   */
  this.main = main;


  /**
   * @field
   * @description the url of the main ImageView for this image
   */
  this.url = main.url;

  /**
   * @field
   * @description an array of all the other ImageViews for this image
   */
  this.views = views || {};
}
ImageEl.prototype = {
  /**
   * Gets the view of the image, from its name
   *
   * @param {string} name - the name of the view to get
   * @returns {ImageView} - the proper view
   */
  getView: function(name) {
    if (name === "main") {
      return this.main;
    } else {
      return this.views[name];
    }
  },
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function () {
    return this.main.asHtml();
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function() {
    return "";
  }
};

/**
 * Embodies an image view (an image in prismic.io can be defined with several different thumbnail sizes, each size is called a "view")
 * @constructor
 * @global
 * @alias Fragments:ImageView
 */
function ImageView(url, width, height, alt, copyright) {
  /**
   * @field
   * @description the URL of the ImageView (useable as it, in a <img> tag in HTML, for instance)
   */
  this.url = url;
  /**
   * @field
   * @description the width of the ImageView
   */
  this.width = width;
  /**
   * @field
   * @description the height of the ImageView
   */
  this.height = height;
  /**
   * @field
   * @description the alt text for the ImageView
   */
  this.alt = alt;
  /**
   * @field
   * @description the copyright for the ImageView
   */
  this.copyright = copyright;
}
ImageView.prototype = {
  ratio: function () {
    return this.width / this.height;
  },
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function () {
    return '<img src="' + this.url + '" width="' + this.width + '" height="' + this.height + '" alt="' + (this.alt || "") + '" copyright="' + (this.copyright || "") + '">';
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function() {
    return "";
  }
};


/**
 * Embodies a fragment of type "Separator" (only used in Slices)
 * @constructor
 * @global
 * @alias Fragments:Separator
 */
function Separator() {
}
Separator.prototype = {
  asHtml: function() {
    return "<hr/>";
  },
  asText: function() {
    return "----";
  }
};

/**
 * Embodies a fragment of type "Group" (which is a group of subfragments)
 * @constructor
 * @global
 * @alias Fragments:Group
 */
function Group(data) {
  this.value = [];
  for (var i = 0; i < data.length; i++) {
    this.value.push(new GroupDoc(data[i]));
  }
}
Group.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   * @params {function} linkResolver - linkResolver function (please read prismic.io online documentation about this)
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function(linkResolver) {
    var output = "";
    for (var i = 0; i < this.value.length; i++) {
      output += this.value[i].asHtml(linkResolver);
    }
    return output;
  },
  /**
   * Turns the Group fragment into an array in order to access its items (groups of fragments),
   * or to loop through them.
   * @params {object} ctx - mandatory ctx object, with a useable linkResolver function (please read prismic.io online documentation about this)
   * @returns {Array} - the array of groups, each group being a JSON object with subfragment name as keys, and subfragment as values
   */
  toArray: function(){
    return this.value;
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function(linkResolver) {
    var output = "";
    for (var i=0; i<this.value.length; i++) {
      output += this.value[i].asText(linkResolver) + '\n';
    }
    return output;
  },

  getFirstImage: function() {
    return this.toArray().reduce(function(image, fragment) {
      if (image) return image;
      else {
        return fragment.getFirstImage();
      }
    }, null);
  },

  getFirstTitle: function() {
    return this.toArray().reduce(function(st, fragment) {
      if (st) return st;
      else {
        return fragment.getFirstTitle();
      }
    }, null);
  },

  getFirstParagraph: function() {
    return this.toArray().reduce(function(st, fragment) {
      if (st) return st;
      else {
        return fragment.getFirstParagraph();
      }
    }, null);
  }
};


/**
 * Embodies a structured text fragment
 * @constructor
 * @global
 * @alias Fragments:StructuredText
 */
function StructuredText(blocks) {

  this.blocks = blocks;

}

StructuredText.prototype = {

  /**
   * @returns {object} the first heading block in the text
   */
  getTitle: function () {
    for(var i=0; i<this.blocks.length; i++) {
      var block = this.blocks[i];
      if(block.type.indexOf('heading') === 0) {
        return block;
      }
    }
    return null;
  },

  /**
   * @returns {object} the first block of type paragraph
   */
  getFirstParagraph: function() {
    for(var i=0; i<this.blocks.length; i++) {
      var block = this.blocks[i];
      if(block.type == 'paragraph') {
        return block;
      }
    }
    return null;
  },

  /**
   * @returns {array} all paragraphs
   */
  getParagraphs: function() {
    var paragraphs = [];
    for(var i=0; i<this.blocks.length; i++) {
      var block = this.blocks[i];
      if(block.type == 'paragraph') {
        paragraphs.push(block);
      }
    }
    return paragraphs;
  },

  /**
   * @returns {object} the nth paragraph
   */
  getParagraph: function(n) {
    return this.getParagraphs()[n];
  },

  /**
   * @returns {object}
   */
  getFirstImage: function() {
    for(var i=0; i<this.blocks.length; i++) {
      var block = this.blocks[i];
      if(block.type == 'image') {
        return new ImageView(
          block.url,
          block.dimensions.width,
          block.dimensions.height,
          block.alt,
          block.copyright
        );
      }
    }
    return null;
  },

  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   * @params {function} linkResolver - please read prismic.io online documentation about link resolvers
   * @params {function} htmlSerializer optional HTML serializer to customize the output
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function(linkResolver, htmlSerializer) {
    var blockGroups = [],
        blockGroup,
        block,
        html = [];
    if (!isFunction(linkResolver)) {
      // Backward compatibility with the old ctx argument
      var ctx = linkResolver;
      linkResolver = function(doc, isBroken) {
        return ctx.linkResolver(ctx, doc, isBroken);
      };
    }
    if (Array.isArray(this.blocks)) {

      for(var i=0; i < this.blocks.length; i++) {
        block = this.blocks[i];

        // Resolve image links
        if (block.type == "image" && block.linkTo) {
          var link = initField(block.linkTo);
          block.linkUrl = link.url(linkResolver);
        }

        if (block.type !== "list-item" && block.type !== "o-list-item") {
          // it's not a type that groups
          blockGroups.push(block);
          blockGroup = null;
        } else if (!blockGroup || blockGroup.type != ("group-" + block.type)) {
          // it's a new type or no BlockGroup was set so far
          blockGroup = {
            type: "group-" + block.type,
            blocks: [block]
          };
          blockGroups.push(blockGroup);
        } else {
          // it's the same type as before, no touching blockGroup
          blockGroup.blocks.push(block);
        }
      }

      var blockContent = function(block) {
        var content = "";
        if (block.blocks) {
          block.blocks.forEach(function (block2) {
            content = content + serialize(block2, blockContent(block2), htmlSerializer);
          });
        } else {
          content = insertSpans(block.text, block.spans, linkResolver, htmlSerializer);
        }
        return content;
      };

      blockGroups.forEach(function (blockGroup) {
        html.push(serialize(blockGroup, blockContent(blockGroup), htmlSerializer));
      });

    }

    return html.join('');

  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function() {
    var output = [];
    for(var i=0; i<this.blocks.length; i++) {
      var block = this.blocks[i];
      if (block.text) {
        output.push(block.text);
      }
    }
    return output.join(' ');
  }

};

function htmlEscape(input) {
  return input && input.replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/\n/g, "<br>");
}

/**
 * Parses a block that has spans, and inserts the proper HTML code.
 *
 * @param {string} text - the original text of the block
 * @param {object} spans - the spans as returned by the API
 * @param {object} linkResolver - the function to build links that may be in the fragment (please read prismic.io's online documentation about this)
 * @param {function} htmlSerializer - optional serializer
 * @returns {string} - the HTML output
 */
function insertSpans(text, spans, linkResolver, htmlSerializer) {
  if (!spans || !spans.length) {
    return htmlEscape(text);
  }

  var tagsStart = {};
  var tagsEnd = {};

  spans.forEach(function (span) {
    if (!tagsStart[span.start]) { tagsStart[span.start] = []; }
    if (!tagsEnd[span.end]) { tagsEnd[span.end] = []; }

    tagsStart[span.start].push(span);
    tagsEnd[span.end].unshift(span);
  });

  var c;
  var html = "";
  var stack = [];
  for (var pos = 0, len = text.length + 1; pos < len; pos++) { // Looping to length + 1 to catch closing tags
    if (tagsEnd[pos]) {
      tagsEnd[pos].forEach(function () {
        // Close a tag
        var tag = stack.pop();
        // Continue only if block contains content.
        if (typeof tag !== 'undefined') {
          var innerHtml = serialize(tag.span, tag.text, htmlSerializer);
          if (stack.length === 0) {
            // The tag was top level
            html += innerHtml;
          } else {
            // Add the content to the parent tag
            stack[stack.length - 1].text += innerHtml;
          }
        }
      });
    }
    if (tagsStart[pos]) {
      // Sort bigger tags first to ensure the right tag hierarchy
      tagsStart[pos].sort(function (a, b) {
        return (b.end - b.start) - (a.end - a.start);
      });
      tagsStart[pos].forEach(function (span) {
        // Open a tag
        var url = null;
        if (span.type == "hyperlink") {
          var fragment = initField(span.data);
          if (fragment) {
            url = fragment.url(linkResolver);
          } else {
            if (console && console.error) console.error('Impossible to convert span.data as a Fragment', span);
            return;
          }
          span.url = url;
        }
        var elt = {
          span: span,
          text: ""
        };
        stack.push(elt);
      });
    }
    if (pos < text.length) {
      c = text[pos];
      if (stack.length === 0) {
        // Top-level text
        html += htmlEscape(c);
      } else {
        // Inner text of a span
        stack[stack.length - 1].text += htmlEscape(c);
      }
    }
  }

  return html;
}

/**
 * Embodies a simple slice (fragment or group)
 * @constructor
 * @global
 * @alias Fragments:SimpleSlice
 */
function SimpleSlice(sliceType, label, sliceValue) {
  this.sliceType = sliceType;
  this.label = label;
  this.value = initField(sliceValue);
}

SimpleSlice.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function (linkResolver) {
    var classes = ['slice'];
    if (this.label) classes.push(this.label);
    return '<div data-slicetype="' + this.sliceType + '" class="' + classes.join(' ') + '">' +
      this.value.asHtml(linkResolver) +
      '</div>';

  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function(linkResolver) {
    return this.value.asText(linkResolver);
  },

  /**
   * Get the first Image in slice.
   * @returns {object}
   */
  getFirstImage: function() {
    var fragment = this.value;
    if(typeof fragment.getFirstImage === "function") {
      return fragment.getFirstImage();
    } else if (fragment instanceof ImageEl) {
      return fragment;
    } else return null;
  },

  getFirstTitle: function() {
    var fragment = this.value;
    if(typeof fragment.getFirstTitle === "function") {
      return fragment.getFirstTitle();
    } else if (fragment instanceof StructuredText) {
      return fragment.getTitle();
    } else return null;
  },

  getFirstParagraph: function() {
    var fragment = this.value;
    if(typeof fragment.getFirstParagraph === "function") {
      return fragment.getFirstParagraph();
    } else return null;
  }

};

/**
 * Embodies a composite slice with repeatable and non repeatable parts
 * @constructor
 * @global
 * @alias Fragments:CompositeSlice
 */
function CompositeSlice(sliceType, label, sliceValue) {
  this.sliceType = sliceType;
  this.label = label;
  var nonRepeatKeys = Object.keys(sliceValue['non-repeat']);
  this.nonRepeat = nonRepeatKeys.reduce(function(acc, key) {
    var field = initField(sliceValue['non-repeat'][key]);
    acc[key] = field;
    return acc;
  }, {});
  this.repeat = initField({type: "Group", value: sliceValue.repeat});
}

CompositeSlice.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function(linkResolver) {
    var classes = ['slice'];
    if (this.label) classes.push(this.label);

    var self = this;
    var nonRepeatHtml = Object.keys(this.nonRepeat).reduce(function(acc, key) {
      return acc + self.nonRepeat[key].asHtml(linkResolver);
    }, "");

    return '<div data-slicetype="' + this.sliceType + '" class="' + classes.join(' ') + '">' +
      nonRepeatHtml +
      this.repeat.asHtml(linkResolver) +
      '</div>';
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function(linkResolver) {
    var self = this;
    var nonRepeatText = Object.keys(this.nonRepeat).reduce(function(acc, key) {
      return acc + self.nonRepeat[key].asText(linkResolver);
    }, "");
    return nonRepeatText + '\n' + this.repeat.asText(linkResolver);
  },

  /**
   * Get the first Image in slice.
   * @returns {object}
   */
  getFirstImage: function() {
    var self = this;
    var firstImage = Object.keys(this.nonRepeat).reduce(function(image, key) {
      if (image) {
        return image;
      } else {
        var element = self.nonRepeat[key];
        if(typeof element.getFirstImage === "function") {
          return element.getFirstImage();
        } else if (element instanceof ImageEl) {
          return element;
        } else return null;
      }
    }, null);

    if (firstImage) {
      return firstImage;
    } else {
      return this.repeat.getFirstImage();
    }
  },


  getFirstTitle: function() {
    var self = this;
    var firstTitle = Object.keys(this.nonRepeat).reduce(function(title, key) {
      if (title) return title;
      else {
        var fragment = self.nonRepeat[key];
        if(typeof fragment.getFirstTitle === "function") {
          return fragment.getFirstTitle();
        } else if (fragment instanceof StructuredText) {
          return fragment.getTitle();
        } else return null;
      }
    }, null);


    return firstTitle || this.repeat.getFirstTitle();
  },


  getFirstParagraph: function() {
    var self = this;
    var firstParagraph = Object.keys(this.nonRepeat).reduce(function(paragraph, key) {
      if (paragraph) return paragraph;
      else {
        var fragment = self.nonRepeat[key];
        if(typeof fragment.getFirstParagraph === "function") {
          return fragment.getFirstParagraph();
        } else return null;
      }
    }, null);

    return firstParagraph || this.repeat.getFirstParagraph();
  }
};

/**
 * Embodies a SliceZone fragment
 * @constructor
 * @global
 * @alias Fragments:SliceZone
 */
function SliceZone(data) {
  this.value = [];
  for (var i = 0; i < data.length; i++) {
    var sliceType = data[i]['slice_type'];
    var label = data[i]['slice_label'] || null;
    var value = data[i];
    if (sliceType && value) {
      if(value.repeat)
        this.value.push(new CompositeSlice(sliceType, label, value));
      else
        this.value.push(new SimpleSlice(sliceType, label, value.value));
    }
  }
  this.slices = this.value;
}

SliceZone.prototype = {
  /**
   * Turns the fragment into a useable HTML version of it.
   * If the native HTML code doesn't suit your design, this function is meant to be overriden.
   *
   * @returns {string} - basic HTML code for the fragment
   */
  asHtml: function (linkResolver) {
    var output = "";
    for (var i = 0; i < this.value.length; i++) {
      output += this.value[i].asHtml(linkResolver);
    }
    return output;
  },

  /**
   * Turns the fragment into a useable text version of it.
   *
   * @returns {string} - basic text version of the fragment
   */
  asText: function(linkResolver) {
    var output = "";
    for (var i = 0; i < this.value.length; i++) {
      output += this.value[i].asText(linkResolver) + '\n';
    }
    return output;
  },

  getFirstImage: function() {
    return this.value.reduce(function(image, slice) {
      if (image) return image;
      else {
        return slice.getFirstImage();
      }
    }, null);
  },

  getFirstTitle: function() {
    return this.value.reduce(function(text, slice) {
      if (text) return text;
      else {
        return slice.getFirstTitle();
      }
    }, null);
  },

  getFirstParagraph: function() {
    return this.value.reduce(function(paragraph, slice) {
      if (paragraph) return paragraph;
      else {
        return slice.getFirstParagraph();
      }
    }, null);
  }
};

/**
 * From a fragment's name, casts it into the proper object type (like Prismic.Fragments.StructuredText)
 *
 * @private
 * @param {string} field - the fragment's name
 * @returns {object} - the object of the proper Fragments type.
 */
function initField(field) {

  var classForType = {
    "Color": Color,
    "Number": Num,
    "Date": DateFragment,
    "Timestamp": Timestamp,
    "Text": Text,
    "Embed": Embed,
    "GeoPoint": GeoPoint,
    "Select": Select,
    "StructuredText": StructuredText,
    "Link.document": DocumentLink,
    "Link.web": WebLink,
    "Link.file": FileLink,
    "Link.image": ImageLink,
    "Separator": Separator,
    "Group": Group,
    "SliceZone": SliceZone
  };

  if (classForType[field.type]) {
    return new classForType[field.type](field.value);
  }

  if (field.type === "Image") {
    var img = field.value.main;
    var output = new ImageEl(
      new ImageView(
        img.url,
        img.dimensions.width,
        img.dimensions.height,
        img.alt,
        img.copyright
      ),
      {}
    );
    for (var name in field.value.views) {
      img = field.value.views[name];
      output.views[name] = new ImageView(
        img.url,
        img.dimensions.width,
        img.dimensions.height,
        img.alt,
        img.copyright
      );
    }
    return output;
  }

  if (console && console.log) console.log("Fragment type not supported: ", field.type);
  return null;

}

function parseFragments(json) {
  var result = {};
  for (var key in json) {
    if (json.hasOwnProperty(key)) {
      if (Array.isArray(json[key])) {
        result[key] = json[key].map(function (fragment) {
          return initField(fragment);
        });
      } else {
        result[key] = initField(json[key]);
      }
    }
  }
  return result;
}


function isFunction(f) {
  var getType = {};
  return f && getType.toString.call(f) === '[object Function]';
}

function serialize(element, content, htmlSerializer) {
  // Return the user customized output (if available)
  if (htmlSerializer) {
    var custom = htmlSerializer(element, content);
    if (custom) {
      return custom;
    }
  }

  // Fall back to the default HTML output
  var TAG_NAMES = {
    "heading1": "h1",
    "heading2": "h2",
    "heading3": "h3",
    "heading4": "h4",
    "heading5": "h5",
    "heading6": "h6",
    "paragraph": "p",
    "preformatted": "pre",
    "list-item": "li",
    "o-list-item": "li",
    "group-list-item": "ul",
    "group-o-list-item": "ol",
    "strong": "strong",
    "em": "em"
  };

  if (TAG_NAMES[element.type]) {
    var name = TAG_NAMES[element.type];
    var classCode = element.label ? (' class="' + element.label + '"') : '';
    return '<' + name + classCode + '>' + content + '</' + name + '>';
  }

  if (element.type == "image") {
    var label = element.label ? (" " + element.label) : "";
    var imgTag = '<img src="' + element.url + '" alt="' + (element.alt || "") + '" copyright="' + (element.copyright || "") + '">';
    return '<p class="block-img' + label + '">' +
      (element.linkUrl ? ('<a href="' + element.linkUrl + '">' + imgTag + '</a>') : imgTag) +
      '</p>';
  }

  if (element.type == "embed") {
    return '<div data-oembed="'+ element.embed_url +
      '" data-oembed-type="'+ element.type +
      '" data-oembed-provider="'+ element.provider_name +
      (element.label ? ('" class="' + element.label) : '') +
      '">' + element.oembed.html+"</div>";
  }

  if (element.type === 'hyperlink') {
    return '<a href="' + element.url + '">' + content + '</a>';
  }

  if (element.type === 'label') {
    return '<span class="' + element.data.label + '">' + content + '</span>';
  }

  return "<!-- Warning: " + element.type + " not implemented. Upgrade the Developer Kit. -->" + content;
}

module.exports = {
  Embed: Embed,
  Image: ImageEl,
  ImageView: ImageView,
  Text: Text,
  Number: Num,
  Date: DateFragment,
  Timestamp: Timestamp,
  Select: Select,
  Color: Color,
  StructuredText: StructuredText,
  WebLink: WebLink,
  DocumentLink: DocumentLink,
  ImageLink: ImageLink,
  FileLink: FileLink,
  Separator: Separator,
  Group: Group,
  GeoPoint: GeoPoint,
  SliceZone: SliceZone,
  SimpleSlice: SimpleSlice,
  CompositeSlice: CompositeSlice,
  initField: initField,
  parseFragments: parseFragments,
  insertSpans: insertSpans
};