<commit_before>const {Fragment} = require("./fragment")
const {Mark} = require("./mark")
const {Slice, replace} = require("./replace")
const {ResolvedPos} = require("./resolvedpos")
const {compareDeep} = require("./comparedeep")

const emptyAttrs = Object.create(null)

// ::- This class represents a node in the tree that makes up a
// ProseMirror document. So a document is an instance of `Node`, with
// children that are also instances of `Node`.
//
// Nodes are persistent data structures. Instead of changing them, you
// create new ones with the content you want. Old ones keep pointing
// at the old document shape. This is made cheaper by sharing
// structure between the old and new data as much as possible, which a
// tree shape like this (without back pointers) makes easy.
//
// **Never** directly mutate the properties of a `Node` object. See
// [this guide](guide/doc.html) for more information.
class Node {
  constructor(type, attrs, content, marks) {
    // :: NodeType
    // The type of node that this is.
    this.type = type

    // :: Object
    // An object mapping attribute names to values. The kind of
    // attributes allowed and required are determined by the node
    // type.
    this.attrs = attrs

    // :: Fragment
    // A container holding the node's children.
    this.content = content || Fragment.empty

    // :: [Mark]
    // The marks (things like whether it is emphasized or part of a
    // link) associated with this node.
    this.marks = marks || Mark.none
  }

  // text:: ?string
  // For text nodes, this contains the node's text content.

  // :: number
  // The size of this node, as defined by the integer-based [indexing
  // scheme](guide/doc.html#indexing). For text nodes, this is the
  // amount of characters. For other leaf nodes, it is one. And for
  // non-leaf nodes, it is the size of the content plus two (the start
  // and end token).
  get nodeSize() { return this.isLeaf ? 1 : 2 + this.content.size }

  // :: number
  // The number of children that the node has.
  get childCount() { return this.content.childCount }

  // :: (number) → Node
  // Get the child node at the given index. Raises an error when the
  // index is out of range.
  child(index) { return this.content.child(index) }

  // :: (number) → ?Node
  // Get the child node at the given index, if it exists.
  maybeChild(index) { return this.content.maybeChild(index) }

  // :: ((node: Node, offset: number, index: number))
  // Call `f` for every child node, passing the node, its offset
  // into this parent node, and its index.
  forEach(f) { this.content.forEach(f) }

  // :: (?number, ?number, (node: Node, pos: number, parent: Node, index: number))
  // Invoke a callback for all descendant nodes recursively between
  // the given two positions that are relative to start of this node's content.
  // The callback is invoked with the node, its parent-relative position,
  // its parent node, and its child index. If the callback returns false,
  // the current node's children will not be recursed over.
  nodesBetween(from, to, f, pos = 0) {
    this.content.nodesBetween(from, to, f, pos, this)
  }

  // :: ((node: Node, pos: number, parent: Node))
  // Call the given callback for every descendant node.
  descendants(f) {
    this.nodesBetween(0, this.content.size, f)
  }

  // :: string
  // Concatenates all the text nodes found in this fragment and its
  // children.
  get textContent() { return this.textBetween(0, this.content.size, "") }

  // :: (number, number, ?string, ?string) → string
  // Get all text between positions `from` and `to`. When
  // `blockSeparator` is given, it will be inserted whenever a new
  // block node is started. When `leafText` is given, it'll be
  // inserted for every non-text leaf node encountered.
  textBetween(from, to, blockSeparator, leafText) {
    return this.content.textBetween(from, to, blockSeparator, leafText)
  }

  // :: ?Node
  // Returns this node's first child, or `null` if there are no
  // children.
  get firstChild() { return this.content.firstChild }

  // :: ?Node
  // Returns this node's last child, or `null` if there are no
  // children.
  get lastChild() { return this.content.lastChild }

  // :: (Node) → bool
  // Test whether two nodes represent the same content.
  eq(other) {
    return this == other || (this.sameMarkup(other) && this.content.eq(other.content))
  }

  // :: (Node) → bool
  // Compare the markup (type, attributes, and marks) of this node to
  // those of another. Returns `true` if both have the same markup.
  sameMarkup(other) {
    return this.hasMarkup(other.type, other.attrs, other.marks)
  }

  // :: (NodeType, ?Object, ?[Mark]) → bool
  // Check whether this node's markup correspond to the given type,
  // attributes, and marks.
  hasMarkup(type, attrs, marks) {
    return this.type == type &&
      compareDeep(this.attrs, attrs || type.defaultAttrs || emptyAttrs) &&
      Mark.sameSet(this.marks, marks || Mark.none)
  }

  // :: (?Fragment) → Node
  // Create a new node with the same markup as this node, containing
  // the given content (or empty, if no content is given).
  copy(content = null) {
    if (content == this.content) return this
    return new this.constructor(this.type, this.attrs, content, this.marks)
  }

  // :: ([Mark]) → Node
  // Create a copy of this node, with the given set of marks instead
  // of the node's own marks.
  mark(marks) {
    return marks == this.marks ? this : new this.constructor(this.type, this.attrs, this.content, marks)
  }

  // :: (number, ?number) → Node
  // Create a copy of this node with only the content between the
  // given offsets. If `to` is not given, it defaults to the end of
  // the node.
  cut(from, to) {
    if (from == 0 && to == this.content.size) return this
    return this.copy(this.content.cut(from, to))
  }

  // :: (number, ?number) → Slice
  // Cut out the part of the document between the given positions, and
  // return it as a `Slice` object.
  slice(from, to = this.content.size, includeParents = false) {
    if (from == to) return Slice.empty

    let $from = this.resolve(from), $to = this.resolve(to)
    let depth = includeParents ? 0 : $from.sharedDepth(to)
    let start = $from.start(depth), node = $from.node(depth)
    let content = node.content.cut($from.pos - start, $to.pos - start)
    return new Slice(content, $from.depth - depth, $to.depth - depth)
  }

  // :: (number, number, Slice) → Node
  // Replace the part of the document between the given positions with
  // the given slice. The slice must 'fit', meaning its open sides
  // must be able to connect to the surrounding content, and its
  // content nodes must be valid children for the node they are placed
  // into. If any of this is violated, an error of type
  // [`ReplaceError`](#model.ReplaceError) is thrown.
  replace(from, to, slice) {
    return replace(this.resolve(from), this.resolve(to), slice)
  }

  // :: (number) → ?Node
  // Find the node after the given position.
  nodeAt(pos) {
    for (let node = this;;) {
      let {index, offset} = node.content.findIndex(pos)
      node = node.maybeChild(index)
      if (!node) return null
      if (offset == pos || node.isText) return node
      pos -= offset + 1
    }
  }

  // :: (number) → {node: ?Node, index: number, offset: number}
  // Find the (direct) child node after the given offset, if any,
  // and return it along with its index and offset relative to this
  // node.
  childAfter(pos) {
    let {index, offset} = this.content.findIndex(pos)
    return {node: this.content.maybeChild(index), index, offset}
  }

  // :: (number) → {node: ?Node, index: number, offset: number}
  // Find the (direct) child node before the given offset, if any,
  // and return it along with its index and offset relative to this
  // node.
  childBefore(pos) {
    if (pos == 0) return {node: null, index: 0, offset: 0}
    let {index, offset} = this.content.findIndex(pos)
    if (offset < pos) return {node: this.content.child(index), index, offset}
    let node = this.content.child(index - 1)
    return {node, index: index - 1, offset: offset - node.nodeSize}
  }

  // :: (number) → ResolvedPos
  // Resolve the given position in the document, returning an object
  // describing its path through the document.
  resolve(pos) { return ResolvedPos.resolveCached(this, pos) }

  resolveNoCache(pos) { return ResolvedPos.resolve(this, pos) }

  // :: (number, ?bool) → [Mark]
  // Get the marks at the given position factoring in the surrounding
  // marks' inclusiveRight property. If the position is at the start
  // of a non-empty node, or `useAfter` is true, the marks of the node
  // after it are returned.
  marksAt(pos, useAfter) {
    let $pos = this.resolve(pos), parent = $pos.parent, index = $pos.index()

    // In an empty parent, return the empty array
    if (parent.content.size == 0) return Mark.none
    // When inside a text node or at the start of the parent node, return the node's marks
    if (useAfter || index == 0 || $pos.textOffset) return parent.child(index).marks

    let marks = parent.child(index - 1).marks
    for (var i = 0; i < marks.length; i++) if (marks[i].type.spec.inclusiveRight === false)
      marks = marks[i--].removeFromSet(marks)
    return marks
  }

  // :: (?number, ?number, MarkType) → bool
  // Test whether a mark of the given type occurs in this document
  // between the two given positions.
  rangeHasMark(from, to, type) {
    let found = false
    this.nodesBetween(from, to, node => {
      if (type.isInSet(node.marks)) found = true
      return !found
    })
    return found
  }

  // :: bool
  // True when this is a block (non-inline node)
  get isBlock() { return this.type.isBlock }

  // :: bool
  // True when this is a textblock node, a block node with inline
  // content.
  get isTextblock() { return this.type.isTextblock }

  // :: bool
  // True when this is an inline node (a text node or a node that can
  // appear among text).
  get isInline() { return this.type.isInline }

  // :: bool
  // True when this is a text node.
  get isText() { return this.type.isText }

  // :: bool
  // True when this is a leaf node.
  get isLeaf() { return this.type.isLeaf }

  // :: () → string
  // Return a string representation of this node for debugging
  // purposes.
  toString() {
    let name = this.type.name
    if (this.content.size)
      name += "(" + this.content.toStringInner() + ")"
    return wrapMarks(this.marks, name)
  }

  // :: (number) → ContentMatch
  // Get the content match in this node at the given index.
  contentMatchAt(index) {
    return this.type.contentExpr.getMatchAt(this.attrs, this.content, index)
  }

  // :: (number, number, ?Fragment, ?number, ?number) → bool
  // Test whether replacing the range `from` to `to` (by index) with
  // the given replacement fragment (which defaults to the empty
  // fragment) would leave the node's content valid. You can
  // optionally pass `start` and `end` indices into the replacement
  // fragment.
  canReplace(from, to, replacement, start, end) {
    return this.type.contentExpr.checkReplace(this.attrs, this.content, from, to, replacement, start, end)
  }

  // :: (number, number, NodeType, ?[Mark]) → bool
  // Test whether replacing the range `from` to `to` (by index) with a
  // node of the given type with the given attributes and marks would
  // be valid.
  canReplaceWith(from, to, type, attrs, marks) {
    return this.type.contentExpr.checkReplaceWith(this.attrs, this.content, from, to, type, attrs, marks || Mark.none)
  }

  // :: (Node) → bool
  // Test whether the given node's content could be appended to this
  // node. If that node is empty, this will only return true if there
  // is at least one node type that can appear in both nodes (to avoid
  // merging completely incompatible nodes).
  canAppend(other) {
    if (other.content.size) return this.canReplace(this.childCount, this.childCount, other.content)
    else return this.type.compatibleContent(other.type)
  }

  defaultContentType(at) {
    let elt = this.contentMatchAt(at).nextElement
    return elt && elt.defaultType()
  }

  // :: () → Object
  // Return a JSON-serializeable representation of this node.
  toJSON() {
    let obj = {type: this.type.name}
    for (let _ in this.attrs) {
      obj.attrs = this.attrs
      break
    }
    if (this.content.size)
      obj.content = this.content.toJSON()
    if (this.marks.length)
      obj.marks = this.marks.map(n => n.toJSON())
    return obj
  }

  // :: (Schema, Object) → Node
  // Deserialize a node from its JSON representation.
  static fromJSON(schema, json) {
    let marks = json.marks && json.marks.map(schema.markFromJSON)
    if (json.type == "text") return schema.text(json.text, marks)
    return schema.nodeType(json.type).create(json.attrs, Fragment.fromJSON(schema, json.content), marks)
  }
}
exports.Node = Node

class TextNode extends Node {
  constructor(type, attrs, content, marks) {
    super(type, attrs, null, marks)

    if (!content) throw new RangeError("Empty text nodes are not allowed")

    this.text = content
  }

  toString() { return wrapMarks(this.marks, JSON.stringify(this.text)) }

  get textContent() { return this.text }

  textBetween(from, to) { return this.text.slice(from, to) }

  get nodeSize() { return this.text.length }

  mark(marks) {
    return new TextNode(this.type, this.attrs, this.text, marks)
  }

  withText(text) {
    if (text == this.text) return this
    return new TextNode(this.type, this.attrs, text, this.marks)
  }

  cut(from = 0, to = this.text.length) {
    if (from == 0 && to == this.text.length) return this
    return this.withText(this.text.slice(from, to))
  }

  eq(other) {
    return this.sameMarkup(other) && this.text == other.text
  }

  toJSON() {
    let base = super.toJSON()
    base.text = this.text
    return base
  }
}
exports.TextNode = TextNode

function wrapMarks(marks, str) {
  for (let i = marks.length - 1; i >= 0; i--)
    str = marks[i].type.name + "(" + str + ")"
  return str
}
<commit_msg>Fix out of bounds access in marksAt<commit_after>const {Fragment} = require("./fragment")
const {Mark} = require("./mark")
const {Slice, replace} = require("./replace")
const {ResolvedPos} = require("./resolvedpos")
const {compareDeep} = require("./comparedeep")

const emptyAttrs = Object.create(null)

// ::- This class represents a node in the tree that makes up a
// ProseMirror document. So a document is an instance of `Node`, with
// children that are also instances of `Node`.
//
// Nodes are persistent data structures. Instead of changing them, you
// create new ones with the content you want. Old ones keep pointing
// at the old document shape. This is made cheaper by sharing
// structure between the old and new data as much as possible, which a
// tree shape like this (without back pointers) makes easy.
//
// **Never** directly mutate the properties of a `Node` object. See
// [this guide](guide/doc.html) for more information.
class Node {
  constructor(type, attrs, content, marks) {
    // :: NodeType
    // The type of node that this is.
    this.type = type

    // :: Object
    // An object mapping attribute names to values. The kind of
    // attributes allowed and required are determined by the node
    // type.
    this.attrs = attrs

    // :: Fragment
    // A container holding the node's children.
    this.content = content || Fragment.empty

    // :: [Mark]
    // The marks (things like whether it is emphasized or part of a
    // link) associated with this node.
    this.marks = marks || Mark.none
  }

  // text:: ?string
  // For text nodes, this contains the node's text content.

  // :: number
  // The size of this node, as defined by the integer-based [indexing
  // scheme](guide/doc.html#indexing). For text nodes, this is the
  // amount of characters. For other leaf nodes, it is one. And for
  // non-leaf nodes, it is the size of the content plus two (the start
  // and end token).
  get nodeSize() { return this.isLeaf ? 1 : 2 + this.content.size }

  // :: number
  // The number of children that the node has.
  get childCount() { return this.content.childCount }

  // :: (number) → Node
  // Get the child node at the given index. Raises an error when the
  // index is out of range.
  child(index) { return this.content.child(index) }

  // :: (number) → ?Node
  // Get the child node at the given index, if it exists.
  maybeChild(index) { return this.content.maybeChild(index) }

  // :: ((node: Node, offset: number, index: number))
  // Call `f` for every child node, passing the node, its offset
  // into this parent node, and its index.
  forEach(f) { this.content.forEach(f) }

  // :: (?number, ?number, (node: Node, pos: number, parent: Node, index: number))
  // Invoke a callback for all descendant nodes recursively between
  // the given two positions that are relative to start of this node's content.
  // The callback is invoked with the node, its parent-relative position,
  // its parent node, and its child index. If the callback returns false,
  // the current node's children will not be recursed over.
  nodesBetween(from, to, f, pos = 0) {
    this.content.nodesBetween(from, to, f, pos, this)
  }

  // :: ((node: Node, pos: number, parent: Node))
  // Call the given callback for every descendant node.
  descendants(f) {
    this.nodesBetween(0, this.content.size, f)
  }

  // :: string
  // Concatenates all the text nodes found in this fragment and its
  // children.
  get textContent() { return this.textBetween(0, this.content.size, "") }

  // :: (number, number, ?string, ?string) → string
  // Get all text between positions `from` and `to`. When
  // `blockSeparator` is given, it will be inserted whenever a new
  // block node is started. When `leafText` is given, it'll be
  // inserted for every non-text leaf node encountered.
  textBetween(from, to, blockSeparator, leafText) {
    return this.content.textBetween(from, to, blockSeparator, leafText)
  }

  // :: ?Node
  // Returns this node's first child, or `null` if there are no
  // children.
  get firstChild() { return this.content.firstChild }

  // :: ?Node
  // Returns this node's last child, or `null` if there are no
  // children.
  get lastChild() { return this.content.lastChild }

  // :: (Node) → bool
  // Test whether two nodes represent the same content.
  eq(other) {
    return this == other || (this.sameMarkup(other) && this.content.eq(other.content))
  }

  // :: (Node) → bool
  // Compare the markup (type, attributes, and marks) of this node to
  // those of another. Returns `true` if both have the same markup.
  sameMarkup(other) {
    return this.hasMarkup(other.type, other.attrs, other.marks)
  }

  // :: (NodeType, ?Object, ?[Mark]) → bool
  // Check whether this node's markup correspond to the given type,
  // attributes, and marks.
  hasMarkup(type, attrs, marks) {
    return this.type == type &&
      compareDeep(this.attrs, attrs || type.defaultAttrs || emptyAttrs) &&
      Mark.sameSet(this.marks, marks || Mark.none)
  }

  // :: (?Fragment) → Node
  // Create a new node with the same markup as this node, containing
  // the given content (or empty, if no content is given).
  copy(content = null) {
    if (content == this.content) return this
    return new this.constructor(this.type, this.attrs, content, this.marks)
  }

  // :: ([Mark]) → Node
  // Create a copy of this node, with the given set of marks instead
  // of the node's own marks.
  mark(marks) {
    return marks == this.marks ? this : new this.constructor(this.type, this.attrs, this.content, marks)
  }

  // :: (number, ?number) → Node
  // Create a copy of this node with only the content between the
  // given offsets. If `to` is not given, it defaults to the end of
  // the node.
  cut(from, to) {
    if (from == 0 && to == this.content.size) return this
    return this.copy(this.content.cut(from, to))
  }

  // :: (number, ?number) → Slice
  // Cut out the part of the document between the given positions, and
  // return it as a `Slice` object.
  slice(from, to = this.content.size, includeParents = false) {
    if (from == to) return Slice.empty

    let $from = this.resolve(from), $to = this.resolve(to)
    let depth = includeParents ? 0 : $from.sharedDepth(to)
    let start = $from.start(depth), node = $from.node(depth)
    let content = node.content.cut($from.pos - start, $to.pos - start)
    return new Slice(content, $from.depth - depth, $to.depth - depth)
  }

  // :: (number, number, Slice) → Node
  // Replace the part of the document between the given positions with
  // the given slice. The slice must 'fit', meaning its open sides
  // must be able to connect to the surrounding content, and its
  // content nodes must be valid children for the node they are placed
  // into. If any of this is violated, an error of type
  // [`ReplaceError`](#model.ReplaceError) is thrown.
  replace(from, to, slice) {
    return replace(this.resolve(from), this.resolve(to), slice)
  }

  // :: (number) → ?Node
  // Find the node after the given position.
  nodeAt(pos) {
    for (let node = this;;) {
      let {index, offset} = node.content.findIndex(pos)
      node = node.maybeChild(index)
      if (!node) return null
      if (offset == pos || node.isText) return node
      pos -= offset + 1
    }
  }

  // :: (number) → {node: ?Node, index: number, offset: number}
  // Find the (direct) child node after the given offset, if any,
  // and return it along with its index and offset relative to this
  // node.
  childAfter(pos) {
    let {index, offset} = this.content.findIndex(pos)
    return {node: this.content.maybeChild(index), index, offset}
  }

  // :: (number) → {node: ?Node, index: number, offset: number}
  // Find the (direct) child node before the given offset, if any,
  // and return it along with its index and offset relative to this
  // node.
  childBefore(pos) {
    if (pos == 0) return {node: null, index: 0, offset: 0}
    let {index, offset} = this.content.findIndex(pos)
    if (offset < pos) return {node: this.content.child(index), index, offset}
    let node = this.content.child(index - 1)
    return {node, index: index - 1, offset: offset - node.nodeSize}
  }

  // :: (number) → ResolvedPos
  // Resolve the given position in the document, returning an object
  // describing its path through the document.
  resolve(pos) { return ResolvedPos.resolveCached(this, pos) }

  resolveNoCache(pos) { return ResolvedPos.resolve(this, pos) }

  // :: (number, ?bool) → [Mark]
  // Get the marks at the given position factoring in the surrounding
  // marks' inclusiveRight property. If the position is at the start
  // of a non-empty node, or `useAfter` is true, the marks of the node
  // after it are returned.
  marksAt(pos, useAfter) {
    let $pos = this.resolve(pos), parent = $pos.parent, index = $pos.index()

    // In an empty parent, return the empty array
    if (parent.content.size == 0) return Mark.none
    // When inside a text node or at the start of the parent node, return the node's marks
    if ((useAfter && index < parent.childCount) || index == 0 || $pos.textOffset)
      return parent.child(index).marks

    let marks = parent.child(index - 1).marks
    for (var i = 0; i < marks.length; i++) if (marks[i].type.spec.inclusiveRight === false)
      marks = marks[i--].removeFromSet(marks)
    return marks
  }

  // :: (?number, ?number, MarkType) → bool
  // Test whether a mark of the given type occurs in this document
  // between the two given positions.
  rangeHasMark(from, to, type) {
    let found = false
    this.nodesBetween(from, to, node => {
      if (type.isInSet(node.marks)) found = true
      return !found
    })
    return found
  }

  // :: bool
  // True when this is a block (non-inline node)
  get isBlock() { return this.type.isBlock }

  // :: bool
  // True when this is a textblock node, a block node with inline
  // content.
  get isTextblock() { return this.type.isTextblock }

  // :: bool
  // True when this is an inline node (a text node or a node that can
  // appear among text).
  get isInline() { return this.type.isInline }

  // :: bool
  // True when this is a text node.
  get isText() { return this.type.isText }

  // :: bool
  // True when this is a leaf node.
  get isLeaf() { return this.type.isLeaf }

  // :: () → string
  // Return a string representation of this node for debugging
  // purposes.
  toString() {
    let name = this.type.name
    if (this.content.size)
      name += "(" + this.content.toStringInner() + ")"
    return wrapMarks(this.marks, name)
  }

  // :: (number) → ContentMatch
  // Get the content match in this node at the given index.
  contentMatchAt(index) {
    return this.type.contentExpr.getMatchAt(this.attrs, this.content, index)
  }

  // :: (number, number, ?Fragment, ?number, ?number) → bool
  // Test whether replacing the range `from` to `to` (by index) with
  // the given replacement fragment (which defaults to the empty
  // fragment) would leave the node's content valid. You can
  // optionally pass `start` and `end` indices into the replacement
  // fragment.
  canReplace(from, to, replacement, start, end) {
    return this.type.contentExpr.checkReplace(this.attrs, this.content, from, to, replacement, start, end)
  }

  // :: (number, number, NodeType, ?[Mark]) → bool
  // Test whether replacing the range `from` to `to` (by index) with a
  // node of the given type with the given attributes and marks would
  // be valid.
  canReplaceWith(from, to, type, attrs, marks) {
    return this.type.contentExpr.checkReplaceWith(this.attrs, this.content, from, to, type, attrs, marks || Mark.none)
  }

  // :: (Node) → bool
  // Test whether the given node's content could be appended to this
  // node. If that node is empty, this will only return true if there
  // is at least one node type that can appear in both nodes (to avoid
  // merging completely incompatible nodes).
  canAppend(other) {
    if (other.content.size) return this.canReplace(this.childCount, this.childCount, other.content)
    else return this.type.compatibleContent(other.type)
  }

  defaultContentType(at) {
    let elt = this.contentMatchAt(at).nextElement
    return elt && elt.defaultType()
  }

  // :: () → Object
  // Return a JSON-serializeable representation of this node.
  toJSON() {
    let obj = {type: this.type.name}
    for (let _ in this.attrs) {
      obj.attrs = this.attrs
      break
    }
    if (this.content.size)
      obj.content = this.content.toJSON()
    if (this.marks.length)
      obj.marks = this.marks.map(n => n.toJSON())
    return obj
  }

  // :: (Schema, Object) → Node
  // Deserialize a node from its JSON representation.
  static fromJSON(schema, json) {
    let marks = json.marks && json.marks.map(schema.markFromJSON)
    if (json.type == "text") return schema.text(json.text, marks)
    return schema.nodeType(json.type).create(json.attrs, Fragment.fromJSON(schema, json.content), marks)
  }
}
exports.Node = Node

class TextNode extends Node {
  constructor(type, attrs, content, marks) {
    super(type, attrs, null, marks)

    if (!content) throw new RangeError("Empty text nodes are not allowed")

    this.text = content
  }

  toString() { return wrapMarks(this.marks, JSON.stringify(this.text)) }

  get textContent() { return this.text }

  textBetween(from, to) { return this.text.slice(from, to) }

  get nodeSize() { return this.text.length }

  mark(marks) {
    return new TextNode(this.type, this.attrs, this.text, marks)
  }

  withText(text) {
    if (text == this.text) return this
    return new TextNode(this.type, this.attrs, text, this.marks)
  }

  cut(from = 0, to = this.text.length) {
    if (from == 0 && to == this.text.length) return this
    return this.withText(this.text.slice(from, to))
  }

  eq(other) {
    return this.sameMarkup(other) && this.text == other.text
  }

  toJSON() {
    let base = super.toJSON()
    base.text = this.text
    return base
  }
}
exports.TextNode = TextNode

function wrapMarks(marks, str) {
  for (let i = marks.length - 1; i >= 0; i--)
    str = marks[i].type.name + "(" + str + ")"
  return str
}
<|endoftext|>


<commit_before>import timezone from './timezones'
import { decode, convert, parseHeaderValue, mimeWordsDecode } from 'emailjs-mime-codec'
import { decode as decodeBase64 } from 'emailjs-base64'
import parseAddress from 'emailjs-addressparser'

export default function MimeNode (parentNode) {
  this.header = [] // An array of unfolded header lines
  this.headers = {} // An object that holds header key=value pairs
  this.bodystructure = ''
  this.childNodes = [] // If this is a multipart or message/rfc822 mime part, the value will be converted to array and hold all child nodes for this node
  this.raw = '' // Stores the raw content of this node

  // Private properties
  this._parentNode = parentNode // Parent node for this specific node
  this._state = 'HEADER' // Current state, always starts out with HEADER
  this._bodyBuffer = '' // Body buffer
  this._lineCount = 0 // Line counter bor the body part
  this._currentChild = false // Active child node (if available)
  this._lineRemainder = '' // Remainder string when dealing with base64 and qp values
  this._isMultipart = false // Indicates if this is a multipart node
  this._multipartBoundary = false // Stores boundary value for current multipart node
  this._isRfc822 = false // Indicates if this is a message/rfc822 node
}

MimeNode.prototype.writeLine = function (line) {
  this.raw += (this.raw ? '\n' : '') + line

  if (this._state === 'HEADER') {
    this._processHeaderLine(line)
  } else if (this._state === 'BODY') {
    this._processBodyLine(line)
  }
}

MimeNode.prototype.finalize = function () {
  if (this._isRfc822) {
    this._currentChild.finalize()
  } else {
    this._emitBody()
  }
}

/**
 * Processes a line in the HEADER state. It the line is empty, change state to BODY
 *
 * @param {String} line Entire input line as 'binary' string
 */
MimeNode.prototype._processHeaderLine = function (line) {
  if (!line) {
    this._parseHeaders()
    this.bodystructure += this.header.join('\n') + '\n\n'
    this._state = 'BODY'
    return
  }

  if (line.match(/^\s/) && this.header.length) {
    this.header[this.header.length - 1] += '\n' + line
  } else {
    this.header.push(line)
  }
}

/**
 * Joins folded header lines and calls Content-Type and Transfer-Encoding processors
 */
MimeNode.prototype._parseHeaders = function () {
  // Join header lines
  var key, value, hasBinary

  for (var i = 0, len = this.header.length; i < len; i++) {
    value = this.header[i].split(':')
    key = (value.shift() || '').trim().toLowerCase()
    value = (value.join(':') || '').replace(/\n/g, '').trim()

    if (value.match(/[\u0080-\uFFFF]/)) {
      if (!this.charset) {
        hasBinary = true
      }
      // use default charset at first and if the actual charset is resolved, the conversion is re-run
      value = decode(convert(str2arr(value), this.charset || 'iso-8859-1'))
    }

    if (!this.headers[key]) {
      this.headers[key] = [this._parseHeaderValue(key, value)]
    } else {
      this.headers[key].push(this._parseHeaderValue(key, value))
    }

    if (!this.charset && key === 'content-type') {
      this.charset = this.headers[key][this.headers[key].length - 1].params.charset
    }

    if (hasBinary && this.charset) {
      // reset values and start over once charset has been resolved and 8bit content has been found
      hasBinary = false
      this.headers = {}
      i = -1 // next iteration has i == 0
    }
  }

  this._processContentType()
  this._processContentTransferEncoding()
}

/**
 * Parses single header value
 * @param {String} key Header key
 * @param {String} value Value for the key
 * @return {Object} parsed header
 */
MimeNode.prototype._parseHeaderValue = function (key, value) {
  let parsedValue
  let isAddress = false

  switch (key) {
    case 'content-type':
    case 'content-transfer-encoding':
    case 'content-disposition':
    case 'dkim-signature':
      parsedValue = parseHeaderValue(value)
      break
    case 'from':
    case 'sender':
    case 'to':
    case 'reply-to':
    case 'cc':
    case 'bcc':
    case 'abuse-reports-to':
    case 'errors-to':
    case 'return-path':
    case 'delivered-to':
      isAddress = true
      parsedValue = {
        value: [].concat(parseAddress(value) || [])
      }
      break
    case 'date':
      parsedValue = {
        value: this._parseDate(value)
      }
      break
    default:
      parsedValue = {
        value: value
      }
  }
  parsedValue.initial = value

  this._decodeHeaderCharset(parsedValue, {
    isAddress: isAddress
  })

  return parsedValue
}

/**
 * Checks if a date string can be parsed. Falls back replacing timezone
 * abbrevations with timezone values
 *
 * @param {String} str Date header
 * @returns {String} UTC date string if parsing succeeded, otherwise returns input value
 */
MimeNode.prototype._parseDate = function (str) {
  str = (str || '').toString().trim()

  var date = new Date(str)

  if (this._isValidDate(date)) {
    return date.toUTCString().replace(/GMT/, '+0000')
  }

  // Assume last alpha part is a timezone
  // Ex: "Date: Thu, 15 May 2014 13:53:30 EEST"
  str = str.replace(/\b[a-z]+$/i, function (tz) {
    tz = tz.toUpperCase()
    if (timezone.hasOwnProperty(tz)) {
      return timezone[tz]
    }
    return tz
  })

  date = new Date(str)

  if (this._isValidDate(date)) {
    return date.toUTCString().replace(/GMT/, '+0000')
  } else {
    return str
  }
}

/**
 * Checks if a value is a Date object and it contains an actual date value
 * @param {Date} date Date object to check
 * @returns {Boolean} True if the value is a valid date
 */
MimeNode.prototype._isValidDate = function (date) {
  return Object.prototype.toString.call(date) === '[object Date]' && date.toString() !== 'Invalid Date'
}

MimeNode.prototype._decodeHeaderCharset = function (parsed, options) {
  options = options || {}

    // decode default value
  if (typeof parsed.value === 'string') {
    parsed.value = mimeWordsDecode(parsed.value)
  }

    // decode possible params
  Object.keys(parsed.params || {}).forEach(function (key) {
    if (typeof parsed.params[key] === 'string') {
      parsed.params[key] = mimeWordsDecode(parsed.params[key])
    }
  })

    // decode addresses
  if (options.isAddress && Array.isArray(parsed.value)) {
    parsed.value.forEach(function (addr) {
      if (addr.name) {
        addr.name = mimeWordsDecode(addr.name)
        if (Array.isArray(addr.group)) {
          this._decodeHeaderCharset({
            value: addr.group
          }, {
            isAddress: true
          })
        }
      }
    }.bind(this))
  }

  return parsed
}

/**
 * Parses Content-Type value and selects following actions.
 */
MimeNode.prototype._processContentType = function () {
  var contentDisposition

  this.contentType = (this.headers['content-type'] && this.headers['content-type'][0]) || parseHeaderValue('text/plain')
  this.contentType.value = (this.contentType.value || '').toLowerCase().trim()
  this.contentType.type = (this.contentType.value.split('/').shift() || 'text')

  if (this.contentType.params && this.contentType.params.charset && !this.charset) {
    this.charset = this.contentType.params.charset
  }

  if (this.contentType.type === 'multipart' && this.contentType.params.boundary) {
    this.childNodes = []
    this._isMultipart = (this.contentType.value.split('/').pop() || 'mixed')
    this._multipartBoundary = this.contentType.params.boundary
  }

  if (this.contentType.value === 'message/rfc822') {
    /**
     * Parse message/rfc822 only if the mime part is not marked with content-disposition: attachment,
     * otherwise treat it like a regular attachment
     */
    contentDisposition = (this.headers['content-disposition'] && this.headers['content-disposition'][0]) || parseHeaderValue('')
    if ((contentDisposition.value || '').toLowerCase().trim() !== 'attachment') {
      this.childNodes = []
      this._currentChild = new MimeNode(this)
      this.childNodes.push(this._currentChild)
      this._isRfc822 = true
    }
  }
}

/**
 * Parses Content-Transfer-Encoding value to see if the body needs to be converted
 * before it can be emitted
 */
MimeNode.prototype._processContentTransferEncoding = function () {
  this.contentTransferEncoding = (this.headers['content-transfer-encoding'] && this.headers['content-transfer-encoding'][0]) || parseHeaderValue('7bit')
  this.contentTransferEncoding.value = (this.contentTransferEncoding.value || '').toLowerCase().trim()
}

/**
 * Processes a line in the BODY state. If this is a multipart or rfc822 node,
 * passes line value to child nodes.
 *
 * @param {String} line Entire input line as 'binary' string
 */
MimeNode.prototype._processBodyLine = function (line) {
  var curLine, match

  this._lineCount++

  if (this._isMultipart) {
    if (line === '--' + this._multipartBoundary) {
      this.bodystructure += line + '\n'
      if (this._currentChild) {
        this._currentChild.finalize()
      }
      this._currentChild = new MimeNode(this)
      this.childNodes.push(this._currentChild)
    } else if (line === '--' + this._multipartBoundary + '--') {
      this.bodystructure += line + '\n'
      if (this._currentChild) {
        this._currentChild.finalize()
      }
      this._currentChild = false
    } else if (this._currentChild) {
      this._currentChild.writeLine(line)
    } else {
            // Ignore multipart preamble
    }
  } else if (this._isRfc822) {
    this._currentChild.writeLine(line)
  } else {
    switch (this.contentTransferEncoding.value) {
      case 'base64':
        curLine = this._lineRemainder + line.trim()

        if (curLine.length % 4) {
          this._lineRemainder = curLine.substr(-curLine.length % 4)
          curLine = curLine.substr(0, curLine.length - this._lineRemainder.length)
        } else {
          this._lineRemainder = ''
        }

        if (curLine.length) {
          this._bodyBuffer += decodeBase64(curLine)
        }

        break
      case 'quoted-printable':
        curLine = this._lineRemainder + (this._lineCount > 1 ? '\n' : '') + line

        if ((match = curLine.match(/=[a-f0-9]{0,1}$/i))) {
          this._lineRemainder = match[0]
          curLine = curLine.substr(0, curLine.length - this._lineRemainder.length)
        } else {
          this._lineRemainder = ''
        }

        this._bodyBuffer += curLine.replace(/=(\r?\n|$)/g, '').replace(/=([a-f0-9]{2})/ig, function (m, code) {
          return String.fromCharCode(parseInt(code, 16))
        })
        break
                // case '7bit':
                // case '8bit':
      default:
        this._bodyBuffer += (this._lineCount > 1 ? '\n' : '') + line
        break
    }
  }
}

/**
 * Emits a chunk of the body
 *
 * @param {Boolean} forceEmit If set to true does not keep any remainders
 */
MimeNode.prototype._emitBody = function () {
  var contentDisposition = (this.headers['content-disposition'] && this.headers['content-disposition'][0]) || parseHeaderValue('')
  var delSp

  if (this._isMultipart || !this._bodyBuffer) {
    return
  }

    // Process flowed text before emitting it
  if (/^text\/(plain|html)$/i.test(this.contentType.value) &&
        this.contentType.params && /^flowed$/i.test(this.contentType.params.format)) {
    delSp = /^yes$/i.test(this.contentType.params.delsp)

    this._bodyBuffer = this._bodyBuffer
        .split('\n')
        // remove soft linebreaks
        // soft linebreaks are added after space symbols
        .reduce(function (previousValue, currentValue) {
          var body = previousValue
          if (delSp) {
            // delsp adds spaces to text to be able to fold it
            // these spaces can be removed once the text is unfolded
            body = body.replace(/[ ]+$/, '')
          }
          if (/ $/.test(previousValue) && !/(^|\n)-- $/.test(previousValue)) {
            return body + currentValue
          } else {
            return body + '\n' + currentValue
          }
        })
        // remove whitespace stuffing
        // http://tools.ietf.org/html/rfc3676#section-4.4
        .replace(/^ /gm, '')
  }

  this.content = str2arr(this._bodyBuffer)

  if (/^text\/(plain|html)$/i.test(this.contentType.value) && !/^attachment$/i.test(contentDisposition.value)) {
    if (!this.charset && /^text\/html$/i.test(this.contentType.value)) {
      this.charset = this._detectHTMLCharset(this._bodyBuffer)
    }

    // decode "binary" string to an unicode string
    if (!/^utf[-_]?8$/i.test(this.charset)) {
      this.content = convert(str2arr(this._bodyBuffer), this.charset || 'iso-8859-1')
    }

    // override charset for text nodes
    this.charset = this.contentType.params.charset = 'utf-8'
  }
  this._bodyBuffer = ''
}

/**
 * Detect charset from a html file
 *
 * @param {String} html Input HTML
 * @returns {String} Charset if found or undefined
 */
MimeNode.prototype._detectHTMLCharset = function (html) {
  var charset, input, meta

  if (typeof html !== 'string') {
    html = html.toString('ascii')
  }

  html = html.replace(/\r?\n|\r/g, ' ')

  if ((meta = html.match(/<meta\s+http-equiv=["'\s]*content-type[^>]*?>/i))) {
    input = meta[0]
  }

  if (input) {
    charset = input.match(/charset\s?=\s?([a-zA-Z\-_:0-9]*);?/)
    if (charset) {
      charset = (charset[1] || '').trim().toLowerCase()
    }
  }

  if (!charset && (meta = html.match(/<meta\s+charset=["'\s]*([^"'<>/\s]+)/i))) {
    charset = (meta[1] || '').trim().toLowerCase()
  }

  return charset
}

const str2arr = str => new Uint8Array(str.split('').map(char => char.charCodeAt(0)))
<commit_msg>Remove parent node<commit_after>import timezone from './timezones'
import { decode, convert, parseHeaderValue, mimeWordsDecode } from 'emailjs-mime-codec'
import { decode as decodeBase64 } from 'emailjs-base64'
import parseAddress from 'emailjs-addressparser'

export default function MimeNode () {
  this.header = [] // An array of unfolded header lines
  this.headers = {} // An object that holds header key=value pairs
  this.bodystructure = ''
  this.childNodes = [] // If this is a multipart or message/rfc822 mime part, the value will be converted to array and hold all child nodes for this node
  this.raw = '' // Stores the raw content of this node

  // Private properties
  this._state = 'HEADER' // Current state, always starts out with HEADER
  this._bodyBuffer = '' // Body buffer
  this._lineCount = 0 // Line counter bor the body part
  this._currentChild = false // Active child node (if available)
  this._lineRemainder = '' // Remainder string when dealing with base64 and qp values
  this._isMultipart = false // Indicates if this is a multipart node
  this._multipartBoundary = false // Stores boundary value for current multipart node
  this._isRfc822 = false // Indicates if this is a message/rfc822 node
}

MimeNode.prototype.writeLine = function (line) {
  this.raw += (this.raw ? '\n' : '') + line

  if (this._state === 'HEADER') {
    this._processHeaderLine(line)
  } else if (this._state === 'BODY') {
    this._processBodyLine(line)
  }
}

MimeNode.prototype.finalize = function () {
  if (this._isRfc822) {
    this._currentChild.finalize()
  } else {
    this._emitBody()
  }
}

/**
 * Processes a line in the HEADER state. It the line is empty, change state to BODY
 *
 * @param {String} line Entire input line as 'binary' string
 */
MimeNode.prototype._processHeaderLine = function (line) {
  if (!line) {
    this._parseHeaders()
    this.bodystructure += this.header.join('\n') + '\n\n'
    this._state = 'BODY'
    return
  }

  if (line.match(/^\s/) && this.header.length) {
    this.header[this.header.length - 1] += '\n' + line
  } else {
    this.header.push(line)
  }
}

/**
 * Joins folded header lines and calls Content-Type and Transfer-Encoding processors
 */
MimeNode.prototype._parseHeaders = function () {
  // Join header lines
  var key, value, hasBinary

  for (var i = 0, len = this.header.length; i < len; i++) {
    value = this.header[i].split(':')
    key = (value.shift() || '').trim().toLowerCase()
    value = (value.join(':') || '').replace(/\n/g, '').trim()

    if (value.match(/[\u0080-\uFFFF]/)) {
      if (!this.charset) {
        hasBinary = true
      }
      // use default charset at first and if the actual charset is resolved, the conversion is re-run
      value = decode(convert(str2arr(value), this.charset || 'iso-8859-1'))
    }

    if (!this.headers[key]) {
      this.headers[key] = [this._parseHeaderValue(key, value)]
    } else {
      this.headers[key].push(this._parseHeaderValue(key, value))
    }

    if (!this.charset && key === 'content-type') {
      this.charset = this.headers[key][this.headers[key].length - 1].params.charset
    }

    if (hasBinary && this.charset) {
      // reset values and start over once charset has been resolved and 8bit content has been found
      hasBinary = false
      this.headers = {}
      i = -1 // next iteration has i == 0
    }
  }

  this._processContentType()
  this._processContentTransferEncoding()
}

/**
 * Parses single header value
 * @param {String} key Header key
 * @param {String} value Value for the key
 * @return {Object} parsed header
 */
MimeNode.prototype._parseHeaderValue = function (key, value) {
  let parsedValue
  let isAddress = false

  switch (key) {
    case 'content-type':
    case 'content-transfer-encoding':
    case 'content-disposition':
    case 'dkim-signature':
      parsedValue = parseHeaderValue(value)
      break
    case 'from':
    case 'sender':
    case 'to':
    case 'reply-to':
    case 'cc':
    case 'bcc':
    case 'abuse-reports-to':
    case 'errors-to':
    case 'return-path':
    case 'delivered-to':
      isAddress = true
      parsedValue = {
        value: [].concat(parseAddress(value) || [])
      }
      break
    case 'date':
      parsedValue = {
        value: this._parseDate(value)
      }
      break
    default:
      parsedValue = {
        value: value
      }
  }
  parsedValue.initial = value

  this._decodeHeaderCharset(parsedValue, {
    isAddress: isAddress
  })

  return parsedValue
}

/**
 * Checks if a date string can be parsed. Falls back replacing timezone
 * abbrevations with timezone values
 *
 * @param {String} str Date header
 * @returns {String} UTC date string if parsing succeeded, otherwise returns input value
 */
MimeNode.prototype._parseDate = function (str) {
  str = (str || '').toString().trim()

  var date = new Date(str)

  if (this._isValidDate(date)) {
    return date.toUTCString().replace(/GMT/, '+0000')
  }

  // Assume last alpha part is a timezone
  // Ex: "Date: Thu, 15 May 2014 13:53:30 EEST"
  str = str.replace(/\b[a-z]+$/i, function (tz) {
    tz = tz.toUpperCase()
    if (timezone.hasOwnProperty(tz)) {
      return timezone[tz]
    }
    return tz
  })

  date = new Date(str)

  if (this._isValidDate(date)) {
    return date.toUTCString().replace(/GMT/, '+0000')
  } else {
    return str
  }
}

/**
 * Checks if a value is a Date object and it contains an actual date value
 * @param {Date} date Date object to check
 * @returns {Boolean} True if the value is a valid date
 */
MimeNode.prototype._isValidDate = function (date) {
  return Object.prototype.toString.call(date) === '[object Date]' && date.toString() !== 'Invalid Date'
}

MimeNode.prototype._decodeHeaderCharset = function (parsed, options) {
  options = options || {}

    // decode default value
  if (typeof parsed.value === 'string') {
    parsed.value = mimeWordsDecode(parsed.value)
  }

    // decode possible params
  Object.keys(parsed.params || {}).forEach(function (key) {
    if (typeof parsed.params[key] === 'string') {
      parsed.params[key] = mimeWordsDecode(parsed.params[key])
    }
  })

    // decode addresses
  if (options.isAddress && Array.isArray(parsed.value)) {
    parsed.value.forEach(function (addr) {
      if (addr.name) {
        addr.name = mimeWordsDecode(addr.name)
        if (Array.isArray(addr.group)) {
          this._decodeHeaderCharset({
            value: addr.group
          }, {
            isAddress: true
          })
        }
      }
    }.bind(this))
  }

  return parsed
}

/**
 * Parses Content-Type value and selects following actions.
 */
MimeNode.prototype._processContentType = function () {
  var contentDisposition

  this.contentType = (this.headers['content-type'] && this.headers['content-type'][0]) || parseHeaderValue('text/plain')
  this.contentType.value = (this.contentType.value || '').toLowerCase().trim()
  this.contentType.type = (this.contentType.value.split('/').shift() || 'text')

  if (this.contentType.params && this.contentType.params.charset && !this.charset) {
    this.charset = this.contentType.params.charset
  }

  if (this.contentType.type === 'multipart' && this.contentType.params.boundary) {
    this.childNodes = []
    this._isMultipart = (this.contentType.value.split('/').pop() || 'mixed')
    this._multipartBoundary = this.contentType.params.boundary
  }

  if (this.contentType.value === 'message/rfc822') {
    /**
     * Parse message/rfc822 only if the mime part is not marked with content-disposition: attachment,
     * otherwise treat it like a regular attachment
     */
    contentDisposition = (this.headers['content-disposition'] && this.headers['content-disposition'][0]) || parseHeaderValue('')
    if ((contentDisposition.value || '').toLowerCase().trim() !== 'attachment') {
      this.childNodes = []
      this._currentChild = new MimeNode(this)
      this.childNodes.push(this._currentChild)
      this._isRfc822 = true
    }
  }
}

/**
 * Parses Content-Transfer-Encoding value to see if the body needs to be converted
 * before it can be emitted
 */
MimeNode.prototype._processContentTransferEncoding = function () {
  this.contentTransferEncoding = (this.headers['content-transfer-encoding'] && this.headers['content-transfer-encoding'][0]) || parseHeaderValue('7bit')
  this.contentTransferEncoding.value = (this.contentTransferEncoding.value || '').toLowerCase().trim()
}

/**
 * Processes a line in the BODY state. If this is a multipart or rfc822 node,
 * passes line value to child nodes.
 *
 * @param {String} line Entire input line as 'binary' string
 */
MimeNode.prototype._processBodyLine = function (line) {
  var curLine, match

  this._lineCount++

  if (this._isMultipart) {
    if (line === '--' + this._multipartBoundary) {
      this.bodystructure += line + '\n'
      if (this._currentChild) {
        this._currentChild.finalize()
      }
      this._currentChild = new MimeNode(this)
      this.childNodes.push(this._currentChild)
    } else if (line === '--' + this._multipartBoundary + '--') {
      this.bodystructure += line + '\n'
      if (this._currentChild) {
        this._currentChild.finalize()
      }
      this._currentChild = false
    } else if (this._currentChild) {
      this._currentChild.writeLine(line)
    } else {
            // Ignore multipart preamble
    }
  } else if (this._isRfc822) {
    this._currentChild.writeLine(line)
  } else {
    switch (this.contentTransferEncoding.value) {
      case 'base64':
        curLine = this._lineRemainder + line.trim()

        if (curLine.length % 4) {
          this._lineRemainder = curLine.substr(-curLine.length % 4)
          curLine = curLine.substr(0, curLine.length - this._lineRemainder.length)
        } else {
          this._lineRemainder = ''
        }

        if (curLine.length) {
          this._bodyBuffer += decodeBase64(curLine)
        }

        break
      case 'quoted-printable':
        curLine = this._lineRemainder + (this._lineCount > 1 ? '\n' : '') + line

        if ((match = curLine.match(/=[a-f0-9]{0,1}$/i))) {
          this._lineRemainder = match[0]
          curLine = curLine.substr(0, curLine.length - this._lineRemainder.length)
        } else {
          this._lineRemainder = ''
        }

        this._bodyBuffer += curLine.replace(/=(\r?\n|$)/g, '').replace(/=([a-f0-9]{2})/ig, function (m, code) {
          return String.fromCharCode(parseInt(code, 16))
        })
        break
                // case '7bit':
                // case '8bit':
      default:
        this._bodyBuffer += (this._lineCount > 1 ? '\n' : '') + line
        break
    }
  }
}

/**
 * Emits a chunk of the body
 *
 * @param {Boolean} forceEmit If set to true does not keep any remainders
 */
MimeNode.prototype._emitBody = function () {
  var contentDisposition = (this.headers['content-disposition'] && this.headers['content-disposition'][0]) || parseHeaderValue('')
  var delSp

  if (this._isMultipart || !this._bodyBuffer) {
    return
  }

    // Process flowed text before emitting it
  if (/^text\/(plain|html)$/i.test(this.contentType.value) &&
        this.contentType.params && /^flowed$/i.test(this.contentType.params.format)) {
    delSp = /^yes$/i.test(this.contentType.params.delsp)

    this._bodyBuffer = this._bodyBuffer
        .split('\n')
        // remove soft linebreaks
        // soft linebreaks are added after space symbols
        .reduce(function (previousValue, currentValue) {
          var body = previousValue
          if (delSp) {
            // delsp adds spaces to text to be able to fold it
            // these spaces can be removed once the text is unfolded
            body = body.replace(/[ ]+$/, '')
          }
          if (/ $/.test(previousValue) && !/(^|\n)-- $/.test(previousValue)) {
            return body + currentValue
          } else {
            return body + '\n' + currentValue
          }
        })
        // remove whitespace stuffing
        // http://tools.ietf.org/html/rfc3676#section-4.4
        .replace(/^ /gm, '')
  }

  this.content = str2arr(this._bodyBuffer)

  if (/^text\/(plain|html)$/i.test(this.contentType.value) && !/^attachment$/i.test(contentDisposition.value)) {
    if (!this.charset && /^text\/html$/i.test(this.contentType.value)) {
      this.charset = this._detectHTMLCharset(this._bodyBuffer)
    }

    // decode "binary" string to an unicode string
    if (!/^utf[-_]?8$/i.test(this.charset)) {
      this.content = convert(str2arr(this._bodyBuffer), this.charset || 'iso-8859-1')
    }

    // override charset for text nodes
    this.charset = this.contentType.params.charset = 'utf-8'
  }
  this._bodyBuffer = ''
}

/**
 * Detect charset from a html file
 *
 * @param {String} html Input HTML
 * @returns {String} Charset if found or undefined
 */
MimeNode.prototype._detectHTMLCharset = function (html) {
  var charset, input, meta

  if (typeof html !== 'string') {
    html = html.toString('ascii')
  }

  html = html.replace(/\r?\n|\r/g, ' ')

  if ((meta = html.match(/<meta\s+http-equiv=["'\s]*content-type[^>]*?>/i))) {
    input = meta[0]
  }

  if (input) {
    charset = input.match(/charset\s?=\s?([a-zA-Z\-_:0-9]*);?/)
    if (charset) {
      charset = (charset[1] || '').trim().toLowerCase()
    }
  }

  if (!charset && (meta = html.match(/<meta\s+charset=["'\s]*([^"'<>/\s]+)/i))) {
    charset = (meta[1] || '').trim().toLowerCase()
  }

  return charset
}

const str2arr = str => new Uint8Array(str.split('').map(char => char.charCodeAt(0)))
<|endoftext|>


<commit_before>var fs = require('fs');
var url = require('url');
var path = require('path');
var coffee = require('coffee-script');
var less = require('less');
var async = require('async');
var uglify = require('uglify-js');
var cleanCSS = require('clean-css');
var glob = require('glob');

var isUndefined = function(x) {
  return typeof x == 'undefined';
};
var compileCoffee = function(filePath, encoding, callback) {
  fs.readFile(filePath, encoding, function(err, content) {
    var code = null;

    if (err) {
      callback(err);
      return;
    }

    try {
      code = coffee.compile(content);
    } catch (e) {
      callback(e);
      return;
    }

    callback(null, code);
  });
};
var compileLess = function(filePath, paths, encoding, callback) {
  fs.readFile(filePath, encoding, function(err, content) {
    if (err) {
      callback(err);
      return;
    }
    less.render(content, { paths: paths.concat([path.dirname(filePath)]) }, callback);
  });
};
var uglifier = function(code) {
  var jsp = uglify.parser;
  var pro = uglify.uglify;

  var ast = jsp.parse(code);
  ast = pro.ast_mangle(ast);
  ast = pro.ast_squeeze(ast);
  return pro.gen_code(ast);
};
var endsWith = function(str, ends) {
  return ends.some(function(end) {
    return end == str.slice(-end.length);
  });
};
var safeReplace = function(str, target, newString) {
  var i = str.indexOf(target);
  return str.slice(0, i) + newString + str.slice(i + target.length);
};
var safeReplaceAll = function(str, target, newString) {
  while (true) {
    var i = str.indexOf(target);

    if (i === -1) {
      return str;
    }

    str = str.slice(0, i) + newString + str.slice(i + target.length);
  }
};
var execAll = function(regexp, str) {
  var match;
  var matches = [];

  while ((match = regexp.exec(str)) != null) {
    matches.push(match);
  }

  return matches;
};
var removeIndex = function(array, index) {
  var a = array.slice(0);
  a.splice(index, 1);
  return a;
};
var removeElement = function(array, element) {
  var i = array.indexOf(element);
  if (i !== -1) {
    return removeIndex(array, i);
  }
  return array;
};
var arrayContains = function(array, element) {
  return array.indexOf(element) !== -1;
};
var createTag = function(name, attributes, content) {
  return "<" + name + Object.keys(attributes).filter(function(key) {
    return !isUndefined(attributes[key]);
  }).map(function(key) {
    return " " + key + '="' + attributes[key] + '"';
  }).join('') + (typeof content == 'string' ? ">" + content + "</" + name + ">" : " />");
};

var build = function(indexFile, settings, callback) {

  if (!callback && typeof settings == 'function') {
    callback = settings;
    settings = {};
  }
  settings = settings || {};

  var indexFileDir = path.resolve(process.cwd(), path.dirname(indexFile));
  var encoding = settings.encoding || 'utf8';
  var assetRoot = path.resolve(settings.assetRoot || indexFileDir);

  var globalFlags = {
    concat: settings.concat,
    inline: settings.inline,
    compress: settings.compress,
    paths: settings.paths,
    ids: settings.ids
  };

  var filetype = function(filename) {
    if (endsWith(filename, ['.css', '.less'])) {
      return 'css';
    }
    if (endsWith(filename, ['.js', '.coffee'])) {
      return 'js';
    }
    return 'other';
  };
  var iewrap = function(params) {
    if (arrayContains(params, "ie7")) {
      return "ie7";
    }
    return undefined;
  };
  var wrappIE = function(params, str) {
    if (iewrap(params) == 'ie7') {
      return "<!--[if IE 7]>" + str + "<![endif]-->";
    }
    return str;
  };
  var paramsToMediaType = function(params) {
    if (arrayContains(params, "screen")) {
      return 'screen';
    }
    if (arrayContains(params, "print")) {
      return 'print';
    }
    return undefined;
  };

  var getMatches = function(content, prefix, postfix) {
    var reg = new RegExp(" *" + prefix + "( +[^\n]*)?\n([^>]*)" + postfix, "g");

    return execAll(reg, content).map(function(x) {
      return {
        str: x[0],
        params: (x[1] || '').split(' ').map(function(x) {
          return x.trim();
        }).filter(function(x) {
          return x;
        })
      };
    }).map(function(matchData) {
      var filename = undefined;

      if (matchData.params.length >= 1 && endsWith(matchData.params[0], ['.js', '.css'])) {
        filename = matchData.params[0];
      }

      return {
        match: matchData.str,
        filename: filename,
        spaces: matchData.str.match(/^\s*/)[0],
        params: matchData.params,
        files: matchData.str.replace(prefix, "").replace(postfix, "").split('\n').slice(1).map(function(s) {
          return s.split('#')[0];
        }).filter(function(s) {
          return s.trim();
        }).map(function(s) {
          var data = s.split('@');
          return {
            name: data[0].trim(),
            params: data.length > 1 ? data[1].split(' ').map(function(p) { return p.trim(); }).filter(function(x) { return x; }) : [],
            spaces: s.match(/^\s*/)[0] || ''
          };
        })
      };
    });
  };
  var globMatches = function(matches, callback) {
    async.map(matches, function(match, callback) {
      async.mapSeries(match.files, function(file, callback) {
        var basePath = file.name && file.name[0] == '/' ? assetRoot : indexFileDir;
        glob(file.name, { nonull: false, root: assetRoot, cwd: indexFileDir  }, function(err, globbedFiles) {
          if (err) {
            callback(err);
            return;
          }
          if (!globbedFiles) {
            console.error("Found no matches for pattern: " + file.name);
          }

          var files = globbedFiles.map(function(globbedFile) {
            return {
              name: globbedFile && globbedFile[0] == '/' ? '/' + path.relative(basePath, globbedFile) : globbedFile,
              params: file.params,
              spaces: file.spaces
            };
          });

          callback(null, files);
        });
      }, function(err, result) {
        callback(err, {
          match: match.match,
          spaces: match.spaces,
          filename: match.filename,
          params: match.params,
          files: (result || []).reduce(function(mem, item) {
            return mem.concat(item);
          }, [])
        });
      });
    }, callback);
  };

  var tagify = function(data) {
    return data.map(function(d) {

      var spaces = d.file.spaces.slice(2);
      d.content = d.content.trim().split('\n').map(function(s) {
        return d.file.spaces + s;
      }).join('\n');
      d.content = "\n" + d.content + "\n" + spaces;

      if (arrayContains(d.file.params, 'compress')) {
        if (filetype(d.file.name) == 'css') {
          d.content = cleanCSS.process(d.content);
        } else if (filetype(d.file.name) == 'js') {
          d.content = uglifier(d.content);
        }
      }

      var csstag = createTag('style', {
        type: 'text/css',
        media: paramsToMediaType(d.file.params),
        'data-path': arrayContains(d.file.params, 'paths') ? d.file.name : undefined
      }, d.content);
      var jstag = createTag('script', {
        type: filetype(d.file.name) == 'js' ? 'text/javascript' : 'text/x-opra',
        id: filetype(d.file.name) != 'js' && arrayContains(d.file.params, 'ids') ? "opra-" + path.basename(d.file.name).split('.')[0] : undefined,
        'data-path': arrayContains(d.file.params, 'paths') ? d.file.name : undefined
      }, d.content);
      return spaces + wrappIE(d.file.params, filetype(d.file.name) == 'css' ? csstag : jstag);
    }).join('\n');
  };

  var filesToMultipleInclude = function(filename, spaces, shouldConcat, fileParams, files, callback) {
    var result = files.filter(function(file) {
      return filetype(file.name) != 'other';
    }).map(function(file) {
      var isCss = endsWith(file.name, ['.css', '.less']);
      var css = createTag('link', { rel: 'stylesheet', type: 'text/css', media: paramsToMediaType(file.params), href: file.name });
      var js = createTag('script', { type: 'text/javascript', src: file.name }, '');
      return file.spaces.slice(2) + wrappIE(file.params, isCss ? css : js);
    }).join('\n');
    callback(null, result);
  };
  var filesToInline = function(filename, spaces, shouldConcat, fileParams, files, callback) {
    filesToInlineBasic(filename, spaces, shouldConcat, fileParams, files, function(err, data) {
      if (err) {
        callback(err);
        return;
      }
      callback(null, tagify(data));
    });
  };
  var filesToInlineBasic = function(filename, spaces, shouldConcat, fileParams, files, callback) {
    async.mapSeries(files, function(file, callback) {

      var basePath = file.name && file.name[0] == '/' ? assetRoot : indexFileDir;
      var filePath = path.join(basePath, file.name);

      var actualCallback = function(err, data) {
        callback(err, { file: file, content: data });
      };

      if (endsWith(file.name, ['.less'])) {
        compileLess(filePath, [assetRoot], encoding, actualCallback);
      } else if (endsWith(file.name, ['.coffee'])) {
        compileCoffee(filePath, encoding, actualCallback);
      } else {
        fs.readFile(filePath, encoding, actualCallback);
      }
    }, function(err, data) {
      if (err) {
        callback(err);
        return;
      }

      if (shouldConcat) {
        var re = data.reduce(function(groups, d) {
          if (groups.length === 0) {
            groups.push([d]);
          } else {
            var last = groups.slice(-1)[0][0];
            if (filetype(d.file.name) == filetype(last.file.name) &&
              iewrap(d.file.params) == iewrap(last.file.params) &&
              (d.file.params.indexOf('compress') === -1) == (last.file.params.indexOf('compress') === -1) &&
              paramsToMediaType(d.file.params) == paramsToMediaType(last.file.params)) {
              groups.slice(-1)[0].push(d);
            } else {
              groups.push([d]);
            }
          }
          return groups;
        }, []);

        var d2 = re.map(function(g) {
          return { file: g[0].file, content: g.map(function(x) {
            return x.content.trim();
          }).join(endsWith(g[0].file.name, ['.js']) ? ';\n' : '\n') };
        });

        callback(null, d2);
      } else {
        callback(err, data);
      }
    });
  };
  var concatToFiles = function(filename, spaces, shouldConcat, fileParams, files, callback) {
    var ft = filetype(filename);
    filesToInlineBasic(filename, spaces, shouldConcat, fileParams, files, function(err, data) {
      if (err) {
        callback(err);
        return;
      }
      if (data.length !== 1) {
        callback("Invalid number of files produced while concatenating");
        return;
      }

      var outFile = path.join(assetRoot, filename);
      var content = data[0].content;

      if (arrayContains(data[0].file.params, 'compress')) {
        if (filetype(outFile) == 'css') {
          content = cleanCSS.process(content);
        } else if (filetype(outFile) == 'js') {
          content = uglifier(content);
        }
      }

      fs.writeFile(outFile, content, encoding, function(err) {
        if (err) {
          callback(err);
          return;
        }

        if (ft == 'js') {
          var js = spaces + createTag('script', { type: 'text/javascript', src: filename }, '');
          callback(null, js);
        } else if (ft == 'css') {
          var css = spaces + createTag('link', { rel: 'stylesheet', type: 'text/css', href: filename });
          callback(null, css);
        } else {
          callback("Invalid filetype '" + fileType + "'! Use 'js' or 'css'.");
        }
      });
    });
  };

  fs.readFile(indexFile, encoding, function(err, content) {
    if (err) {
      callback(err);
      return;
    }

    globMatches(getMatches(content, "<!--OPRA", "-->"), function(err, matches) {
      if (err) {
        callback(err);
        return;
      }

      matches.forEach(function(f) {
        ['concat', 'inline'].forEach(function(n) {

          if (arrayContains(f.params, 'always-' + n)) {
            f.params.push(n);
          } else if (arrayContains(f.params, 'never-' + n)) {
            f.params = removeElement(f.params, n);
          } else if (!isUndefined(globalFlags[n])) {
            if (globalFlags[n]) {
              f.params.push(n);
            } else {
              f.params = removeElement(f.params, n);
            }
          }

          f.params = removeElement(f.params, 'always-' + n);
          f.params = removeElement(f.params, 'never-' + n);
        });
      });

      matches.forEach(function(m) {
        m.files.forEach(function(f) {
          ['compress', 'paths', 'ids'].forEach(function(n) {
            if (arrayContains(f.params, 'always-' + n) || (!arrayContains(f.params, 'never-' + n) && globalFlags[n])) {
              f.params.push(n);
            }

            f.params = removeElement(f.params, 'always-' + n);
            f.params = removeElement(f.params, 'never-' + n);
          });
        });
      });

      async.reduce(matches, content, function(next_content, d, callback) {
        var shouldConcat = arrayContains(d.params, 'concat');
        var f = arrayContains(d.params, 'inline') ? filesToInline : (shouldConcat && d.filename ? concatToFiles : filesToMultipleInclude);

        f(d.filename, d.spaces, shouldConcat, d.params, d.files, function(err, data, outfiles) {
          if (err) {
            callback(err);
            return;
          }

          async.forEach(outfiles || [], function(file, callback) {
            fs.writeFile(file.name, file.content, encoding, callback);
          }, function(err) {
            callback(err, safeReplace(next_content, d.match, data));
          });
        });
      }, callback);
    });
  });
};
var serve = function(path, settings) {
  settings = settings || {};
  settings.url = settings.url || '/index.html';

  if (isUndefined(settings.assetRoot)) {
    settings.assetRoot = path;
  }

  return function(req, res, next) {
    var pathname = url.parse(req.url).pathname;

    if (endsWith(pathname, ['.html'])) {
      build(path + pathname, settings, function(err, result) {
        if (err) {
          console.log("OPRA ERROR: While compiling " + pathname + " the following was caught:", err);
          next();
          return;
        }
        res.setHeader('Content-Type', 'text/html');
        res.setHeader('Content-Length', Buffer.byteLength(result));
        res.end(result);
      });
    } else {
      next();
    }
  };
};

exports.build = build;
exports.serve = serve;
<commit_msg>Serve-method no falls back to index.html when requesting a directory<commit_after>var fs = require('fs');
var url = require('url');
var path = require('path');
var coffee = require('coffee-script');
var less = require('less');
var async = require('async');
var uglify = require('uglify-js');
var cleanCSS = require('clean-css');
var glob = require('glob');

var isUndefined = function(x) {
  return typeof x == 'undefined';
};
var compileCoffee = function(filePath, encoding, callback) {
  fs.readFile(filePath, encoding, function(err, content) {
    var code = null;

    if (err) {
      callback(err);
      return;
    }

    try {
      code = coffee.compile(content);
    } catch (e) {
      callback(e);
      return;
    }

    callback(null, code);
  });
};
var compileLess = function(filePath, paths, encoding, callback) {
  fs.readFile(filePath, encoding, function(err, content) {
    if (err) {
      callback(err);
      return;
    }
    less.render(content, { paths: paths.concat([path.dirname(filePath)]) }, callback);
  });
};
var uglifier = function(code) {
  var jsp = uglify.parser;
  var pro = uglify.uglify;

  var ast = jsp.parse(code);
  ast = pro.ast_mangle(ast);
  ast = pro.ast_squeeze(ast);
  return pro.gen_code(ast);
};
var endsWith = function(str, ends) {
  return ends.some(function(end) {
    return end == str.slice(-end.length);
  });
};
var safeReplace = function(str, target, newString) {
  var i = str.indexOf(target);
  return str.slice(0, i) + newString + str.slice(i + target.length);
};
var safeReplaceAll = function(str, target, newString) {
  while (true) {
    var i = str.indexOf(target);

    if (i === -1) {
      return str;
    }

    str = str.slice(0, i) + newString + str.slice(i + target.length);
  }
};
var execAll = function(regexp, str) {
  var match;
  var matches = [];

  while ((match = regexp.exec(str)) != null) {
    matches.push(match);
  }

  return matches;
};
var removeIndex = function(array, index) {
  var a = array.slice(0);
  a.splice(index, 1);
  return a;
};
var removeElement = function(array, element) {
  var i = array.indexOf(element);
  if (i !== -1) {
    return removeIndex(array, i);
  }
  return array;
};
var arrayContains = function(array, element) {
  return array.indexOf(element) !== -1;
};
var createTag = function(name, attributes, content) {
  return "<" + name + Object.keys(attributes).filter(function(key) {
    return !isUndefined(attributes[key]);
  }).map(function(key) {
    return " " + key + '="' + attributes[key] + '"';
  }).join('') + (typeof content == 'string' ? ">" + content + "</" + name + ">" : " />");
};

var build = function(indexFile, settings, callback) {

  if (!callback && typeof settings == 'function') {
    callback = settings;
    settings = {};
  }
  settings = settings || {};

  var indexFileDir = path.resolve(process.cwd(), path.dirname(indexFile));
  var encoding = settings.encoding || 'utf8';
  var assetRoot = path.resolve(settings.assetRoot || indexFileDir);

  var globalFlags = {
    concat: settings.concat,
    inline: settings.inline,
    compress: settings.compress,
    paths: settings.paths,
    ids: settings.ids
  };

  var filetype = function(filename) {
    if (endsWith(filename, ['.css', '.less'])) {
      return 'css';
    }
    if (endsWith(filename, ['.js', '.coffee'])) {
      return 'js';
    }
    return 'other';
  };
  var iewrap = function(params) {
    if (arrayContains(params, "ie7")) {
      return "ie7";
    }
    return undefined;
  };
  var wrappIE = function(params, str) {
    if (iewrap(params) == 'ie7') {
      return "<!--[if IE 7]>" + str + "<![endif]-->";
    }
    return str;
  };
  var paramsToMediaType = function(params) {
    if (arrayContains(params, "screen")) {
      return 'screen';
    }
    if (arrayContains(params, "print")) {
      return 'print';
    }
    return undefined;
  };

  var getMatches = function(content, prefix, postfix) {
    var reg = new RegExp(" *" + prefix + "( +[^\n]*)?\n([^>]*)" + postfix, "g");

    return execAll(reg, content).map(function(x) {
      return {
        str: x[0],
        params: (x[1] || '').split(' ').map(function(x) {
          return x.trim();
        }).filter(function(x) {
          return x;
        })
      };
    }).map(function(matchData) {
      var filename = undefined;

      if (matchData.params.length >= 1 && endsWith(matchData.params[0], ['.js', '.css'])) {
        filename = matchData.params[0];
      }

      return {
        match: matchData.str,
        filename: filename,
        spaces: matchData.str.match(/^\s*/)[0],
        params: matchData.params,
        files: matchData.str.replace(prefix, "").replace(postfix, "").split('\n').slice(1).map(function(s) {
          return s.split('#')[0];
        }).filter(function(s) {
          return s.trim();
        }).map(function(s) {
          var data = s.split('@');
          return {
            name: data[0].trim(),
            params: data.length > 1 ? data[1].split(' ').map(function(p) { return p.trim(); }).filter(function(x) { return x; }) : [],
            spaces: s.match(/^\s*/)[0] || ''
          };
        })
      };
    });
  };
  var globMatches = function(matches, callback) {
    async.map(matches, function(match, callback) {
      async.mapSeries(match.files, function(file, callback) {
        var basePath = file.name && file.name[0] == '/' ? assetRoot : indexFileDir;
        glob(file.name, { nonull: false, root: assetRoot, cwd: indexFileDir  }, function(err, globbedFiles) {
          if (err) {
            callback(err);
            return;
          }
          if (!globbedFiles) {
            console.error("Found no matches for pattern: " + file.name);
          }

          var files = globbedFiles.map(function(globbedFile) {
            return {
              name: globbedFile && globbedFile[0] == '/' ? '/' + path.relative(basePath, globbedFile) : globbedFile,
              params: file.params,
              spaces: file.spaces
            };
          });

          callback(null, files);
        });
      }, function(err, result) {
        callback(err, {
          match: match.match,
          spaces: match.spaces,
          filename: match.filename,
          params: match.params,
          files: (result || []).reduce(function(mem, item) {
            return mem.concat(item);
          }, [])
        });
      });
    }, callback);
  };

  var tagify = function(data) {
    return data.map(function(d) {

      var spaces = d.file.spaces.slice(2);
      d.content = d.content.trim().split('\n').map(function(s) {
        return d.file.spaces + s;
      }).join('\n');
      d.content = "\n" + d.content + "\n" + spaces;

      if (arrayContains(d.file.params, 'compress')) {
        if (filetype(d.file.name) == 'css') {
          d.content = cleanCSS.process(d.content);
        } else if (filetype(d.file.name) == 'js') {
          d.content = uglifier(d.content);
        }
      }

      var csstag = createTag('style', {
        type: 'text/css',
        media: paramsToMediaType(d.file.params),
        'data-path': arrayContains(d.file.params, 'paths') ? d.file.name : undefined
      }, d.content);
      var jstag = createTag('script', {
        type: filetype(d.file.name) == 'js' ? 'text/javascript' : 'text/x-opra',
        id: filetype(d.file.name) != 'js' && arrayContains(d.file.params, 'ids') ? "opra-" + path.basename(d.file.name).split('.')[0] : undefined,
        'data-path': arrayContains(d.file.params, 'paths') ? d.file.name : undefined
      }, d.content);
      return spaces + wrappIE(d.file.params, filetype(d.file.name) == 'css' ? csstag : jstag);
    }).join('\n');
  };

  var filesToMultipleInclude = function(filename, spaces, shouldConcat, fileParams, files, callback) {
    var result = files.filter(function(file) {
      return filetype(file.name) != 'other';
    }).map(function(file) {
      var isCss = endsWith(file.name, ['.css', '.less']);
      var css = createTag('link', { rel: 'stylesheet', type: 'text/css', media: paramsToMediaType(file.params), href: file.name });
      var js = createTag('script', { type: 'text/javascript', src: file.name }, '');
      return file.spaces.slice(2) + wrappIE(file.params, isCss ? css : js);
    }).join('\n');
    callback(null, result);
  };
  var filesToInline = function(filename, spaces, shouldConcat, fileParams, files, callback) {
    filesToInlineBasic(filename, spaces, shouldConcat, fileParams, files, function(err, data) {
      if (err) {
        callback(err);
        return;
      }
      callback(null, tagify(data));
    });
  };
  var filesToInlineBasic = function(filename, spaces, shouldConcat, fileParams, files, callback) {
    async.mapSeries(files, function(file, callback) {

      var basePath = file.name && file.name[0] == '/' ? assetRoot : indexFileDir;
      var filePath = path.join(basePath, file.name);

      var actualCallback = function(err, data) {
        callback(err, { file: file, content: data });
      };

      if (endsWith(file.name, ['.less'])) {
        compileLess(filePath, [assetRoot], encoding, actualCallback);
      } else if (endsWith(file.name, ['.coffee'])) {
        compileCoffee(filePath, encoding, actualCallback);
      } else {
        fs.readFile(filePath, encoding, actualCallback);
      }
    }, function(err, data) {
      if (err) {
        callback(err);
        return;
      }

      if (shouldConcat) {
        var re = data.reduce(function(groups, d) {
          if (groups.length === 0) {
            groups.push([d]);
          } else {
            var last = groups.slice(-1)[0][0];
            if (filetype(d.file.name) == filetype(last.file.name) &&
              iewrap(d.file.params) == iewrap(last.file.params) &&
              (d.file.params.indexOf('compress') === -1) == (last.file.params.indexOf('compress') === -1) &&
              paramsToMediaType(d.file.params) == paramsToMediaType(last.file.params)) {
              groups.slice(-1)[0].push(d);
            } else {
              groups.push([d]);
            }
          }
          return groups;
        }, []);

        var d2 = re.map(function(g) {
          return { file: g[0].file, content: g.map(function(x) {
            return x.content.trim();
          }).join(endsWith(g[0].file.name, ['.js']) ? ';\n' : '\n') };
        });

        callback(null, d2);
      } else {
        callback(err, data);
      }
    });
  };
  var concatToFiles = function(filename, spaces, shouldConcat, fileParams, files, callback) {
    var ft = filetype(filename);
    filesToInlineBasic(filename, spaces, shouldConcat, fileParams, files, function(err, data) {
      if (err) {
        callback(err);
        return;
      }
      if (data.length !== 1) {
        callback("Invalid number of files produced while concatenating");
        return;
      }

      var outFile = path.join(assetRoot, filename);
      var content = data[0].content;

      if (arrayContains(data[0].file.params, 'compress')) {
        if (filetype(outFile) == 'css') {
          content = cleanCSS.process(content);
        } else if (filetype(outFile) == 'js') {
          content = uglifier(content);
        }
      }

      fs.writeFile(outFile, content, encoding, function(err) {
        if (err) {
          callback(err);
          return;
        }

        if (ft == 'js') {
          var js = spaces + createTag('script', { type: 'text/javascript', src: filename }, '');
          callback(null, js);
        } else if (ft == 'css') {
          var css = spaces + createTag('link', { rel: 'stylesheet', type: 'text/css', href: filename });
          callback(null, css);
        } else {
          callback("Invalid filetype '" + fileType + "'! Use 'js' or 'css'.");
        }
      });
    });
  };

  fs.readFile(indexFile, encoding, function(err, content) {
    if (err) {
      callback(err);
      return;
    }

    globMatches(getMatches(content, "<!--OPRA", "-->"), function(err, matches) {
      if (err) {
        callback(err);
        return;
      }

      matches.forEach(function(f) {
        ['concat', 'inline'].forEach(function(n) {

          if (arrayContains(f.params, 'always-' + n)) {
            f.params.push(n);
          } else if (arrayContains(f.params, 'never-' + n)) {
            f.params = removeElement(f.params, n);
          } else if (!isUndefined(globalFlags[n])) {
            if (globalFlags[n]) {
              f.params.push(n);
            } else {
              f.params = removeElement(f.params, n);
            }
          }

          f.params = removeElement(f.params, 'always-' + n);
          f.params = removeElement(f.params, 'never-' + n);
        });
      });

      matches.forEach(function(m) {
        m.files.forEach(function(f) {
          ['compress', 'paths', 'ids'].forEach(function(n) {
            if (arrayContains(f.params, 'always-' + n) || (!arrayContains(f.params, 'never-' + n) && globalFlags[n])) {
              f.params.push(n);
            }

            f.params = removeElement(f.params, 'always-' + n);
            f.params = removeElement(f.params, 'never-' + n);
          });
        });
      });

      async.reduce(matches, content, function(next_content, d, callback) {
        var shouldConcat = arrayContains(d.params, 'concat');
        var f = arrayContains(d.params, 'inline') ? filesToInline : (shouldConcat && d.filename ? concatToFiles : filesToMultipleInclude);

        f(d.filename, d.spaces, shouldConcat, d.params, d.files, function(err, data, outfiles) {
          if (err) {
            callback(err);
            return;
          }

          async.forEach(outfiles || [], function(file, callback) {
            fs.writeFile(file.name, file.content, encoding, callback);
          }, function(err) {
            callback(err, safeReplace(next_content, d.match, data));
          });
        });
      }, callback);
    });
  });
};
var serve = function(rootpath, settings) {
  settings = settings || {};
  settings.url = settings.url || '/index.html';

  if (isUndefined(settings.assetRoot)) {
    settings.assetRoot = rootpath;
  }

  return function(req, res, next) {
    var pathname = url.parse(req.url).pathname;
    var filepath = path.join(rootpath, pathname);

    fs.stat(filepath, function(err, stat) {

      if (stat.isDirectory()) {
        pathname = path.join(pathname, 'index.html');
        filepath = path.join(filepath, 'index.html');
      }

      if (endsWith(pathname, ['.html'])) {
        build(filepath, settings, function(err, result) {
          if (err) {
            console.log("OPRA ERROR: While compiling " + pathname + " the following was caught:", err);
            next();
            return;
          }
          res.setHeader('Content-Type', 'text/html');
          res.setHeader('Content-Length', Buffer.byteLength(result));
          res.end(result);
        });
      } else {
        next();
      }
    });
  };
};

exports.build = build;
exports.serve = serve;
<|endoftext|>


<commit_before>var _ = require('./util');
var Node = require('./node');
var svg = require('./svg');

var Path = function() {
  Node.apply(this, arguments);
};

Path.prototype = _.extend(Path.prototype, Node.prototype, {

  // Much from Fabric.js - https://github.com/kangax/fabric.js/blob/master/src/shapes/path.class.js
  sketch: function(ctx) {
    var pathCommands = this.commandCache || (this.commandCache = svg.parse(this.path));
    svg.render(ctx, pathCommands);
  },

  draw: function(ctx) {
    if (!this.visible) {
      return;
    }

    ctx.save();
    ctx.translate(this.x || 0, this.y || 0);

    if (this.fillStyle) {
      ctx.fillStyle = this.fillStyle;
    }

    if (this.stroke) {
      ctx.strokeStyle = this.stroke;
      ctx.lineWidth = this.lineWidth || 1;
      ctx.lineCap = this.lineCap || 'butt';
      ctx.lineJoin = this.lineJoin || 'miter';
    }

    this.sketch(ctx);

    if (this.stroke) {
      ctx.stroke();
    }
    if (this.fillStyle) {
      ctx.fill();
    }

    ctx.restore();
  },

  pick: function(ctx, x, y, lx, ly) {
    var isHit = false;

    if (!this.visible) {
      return;
    }

    ctx.save();
    ctx.translate(this.x || 0, this.y || 0);

    this.sketch(ctx);

    if (this.fillStyle) {
      isHit = ctx.isPointInPath(x,y);
    }
    if (!isHit && this.stroke) {
      isHit = ctx.isPointInStroke(x,y);
    }

    ctx.restore();
    return isHit && this;
  }
});

module.exports = Path;<commit_msg>Allow path svg string to be redefined+interpreted<commit_after>var _ = require('./util');
var Node = require('./node');
var svg = require('./svg');

var Path = function() {
  Node.apply(this, arguments);
};

Path.prototype = _.extend(Path.prototype, Node.prototype, {

  attr: function(attributes) {
    Node.prototype.attr.call(this, attributes);
    if (attributes.path) {
      this.commandCache = null;
    }
    return this;
  },

  // Much from Fabric.js - https://github.com/kangax/fabric.js/blob/master/src/shapes/path.class.js
  sketch: function(ctx) {
    if (this.path && this.path.length > 0) {
      var pathCommands = this.commandCache || (this.commandCache = svg.parse(this.path));
      svg.render(ctx, pathCommands);
    }
  },

  draw: function(ctx) {
    if (!this.visible) {
      return;
    }

    ctx.save();
    ctx.translate(this.x || 0, this.y || 0);

    if (this.fillStyle) {
      ctx.fillStyle = this.fillStyle;
    }

    if (this.stroke) {
      ctx.strokeStyle = this.stroke;
      ctx.lineWidth = this.lineWidth || 1;
      ctx.lineCap = this.lineCap || 'butt';
      ctx.lineJoin = this.lineJoin || 'miter';
    }

    this.sketch(ctx);

    if (this.stroke) {
      ctx.stroke();
    }
    if (this.fillStyle) {
      ctx.fill();
    }

    ctx.restore();
  },

  pick: function(ctx, x, y, lx, ly) {
    var isHit = false;

    if (!this.visible) {
      return;
    }

    ctx.save();
    ctx.translate(this.x || 0, this.y || 0);

    this.sketch(ctx);

    if (this.fillStyle) {
      isHit = ctx.isPointInPath(x,y);
    }
    if (!isHit && this.stroke) {
      isHit = ctx.isPointInStroke(x,y);
    }

    ctx.restore();
    return isHit && this;
  }
});

module.exports = Path;<|endoftext|>