all files / model/ DOMExporter.js

75.58% Statements 65/86
66.67% Branches 26/39
68.42% Functions 13/19
75.58% Lines 65/86
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201            401× 401×     401× 401× 401× 4390× 4390×       4390×           401×     401×     401× 401×     401×                                                               12×     12× 12× 12× 12× 32× 32× 32×   12×       58×       58×   58×     58×     58×     58× 58× 30×   28×   58× 58× 44×   14×   58×                     64× 64× 64× 64×       64×                               98×       64×   64× 64× 76×   64×         64×               64× 64× 64×                                      
import { isString, isFunction, Registry, encodeXMLEntities } from '../util'
import Fragmenter from './Fragmenter'
 
class DOMExporter {
 
  constructor(config, context) {
    this.context = context || {}
    Iif (!config.converters) {
      throw new Error('config.converters is mandatory')
    }
    Eif (!config.converters._isRegistry) {
      this.converters = new Registry()
      config.converters.forEach(function(Converter) {
        let converter = isFunction(Converter) ? new Converter() : Converter
        Iif (!converter.type) {
          console.error('Converter must provide the type of the associated node.', converter)
          return
        }
        this.converters.add(converter.type, converter)
      }.bind(this))
    } else {
      this.converters = config.converters
    }
 
    this.state = {
      doc: null
    }
    this.config = config
    // NOTE: Subclasses (HTMLExporter and XMLExporter) must initialize this
    // with a proper DOMElement instance which is used to create new elements.
    this._elementFactory = config.elementFactory
    Iif (!this._elementFactory) {
      throw new Error("'elementFactory' is mandatory")
    }
    this.$$ = this.createElement.bind(this)
  }
 
  exportDocument(doc) {
    // TODO: this is no left without much functionality
    // still, it would be good to have a consistent top-level API
    // i.e. converter.importDocument(el) and converter.exportDocument(doc)
    // On the other side, the 'internal' API methods are named this.convert*.
    return this.convertDocument(doc)
  }
 
  /**
    @param {Document}
    @returns {DOMElement|DOMElement[]} The exported document as DOM or an array of elements
             if exported as partial, which depends on the actual implementation
             of `this.convertDocument()`.
 
    @abstract
    @example
 
    convertDocument(doc) {
      var elements = this.convertContainer(doc, this.state.containerId)
      var out = elements.map(function(el) {
        return el.outerHTML
      })
      return out.join('')
    }
  */
  convertDocument(doc) { // eslint-disable-line
    throw new Error('This method is abstract')
  }
 
  convertContainer(container) {
    Iif (!container) {
      throw new Error('Illegal arguments: container is mandatory.')
    }
    var doc = container.getDocument()
    this.state.doc = doc
    var elements = []
    container.nodes.forEach(function(id) {
      var node = doc.get(id)
      var nodeEl = this.convertNode(node)
      elements.push(nodeEl)
    }.bind(this))
    return elements
  }
 
  convertNode(node) {
    Iif (isString(node)) {
      // Assuming this.state.doc has been set by convertDocument
      node = this.state.doc.get(node)
    } else {
      this.state.doc = node.getDocument()
    }
    var converter = this.getNodeConverter(node)
    // special treatment for annotations, i.e. if someone calls
    // `exporter.convertNode(anno)`
    Iif (node._isPropertyAnnotation && (!converter || !converter.export)) {
      return this._convertPropertyAnnotation(node)
    }
    Iif (!converter) {
      converter = this.getDefaultBlockConverter()
    }
    var el
    if (converter.tagName) {
      el = this.$$(converter.tagName)
    } else {
      el = this.$$('div')
    }
    el.attr(this.config.idAttribute, node.id)
    if (converter.export) {
      el = converter.export(node, el, this) || el
    } else {
      el = this.getDefaultBlockConverter().export(node, el, this) || el
    }
    return el
  }
 
  convertProperty(doc, path, options) {
    this.initialize(doc, options)
    var wrapper = this.$$('div')
      .append(this.annotatedText(path))
    return wrapper.innerHTML
  }
 
  annotatedText(path) {
    var doc = this.state.doc
    var text = doc.get(path)
    var annotations = doc.getIndex('annotations').get(path)
    return this._annotatedText(text, annotations)
  }
 
  getNodeConverter(node) {
    return this.converters.get(node.type)
  }
 
  getDefaultBlockConverter() {
    throw new Error('This method is abstract.')
  }
 
  getDefaultPropertyAnnotationConverter() {
    throw new Error('This method is abstract.')
  }
 
  getDocument() {
    return this.state.doc
  }
 
  createElement(str) {
    return this._elementFactory.createElement(str)
  }
 
  _annotatedText(text, annotations) {
    var self = this
 
    var annotator = new Fragmenter()
    annotator.onText = function(context, text) {
      context.children.push(encodeXMLEntities(text))
    }
    annotator.onEnter = function(fragment) {
      var anno = fragment.node
      return {
        annotation: anno,
        children: []
      }
    }
    annotator.onExit = function(fragment, context, parentContext) {
      var anno = context.annotation
      var converter = self.getNodeConverter(anno)
      Iif (!converter) {
        converter = self.getDefaultPropertyAnnotationConverter()
      }
      var el
      Eif (converter.tagName) {
        el = this.$$(converter.tagName)
      } else {
        el = this.$$('span')
      }
      el.attr(this.config.idAttribute, anno.id)
      el.append(context.children)
      if (converter.export) {
        el = converter.export(anno, el, self) || el
      }
      parentContext.children.push(el)
    }.bind(this)
    var wrapper = { children: [] }
    annotator.start(wrapper, text, annotations)
    return wrapper.children
  }
 
  /*
    This is used when someone calls `exporter.convertNode(anno)`
    Usually, annotations are converted by calling exporter.annotatedText(path).
    Still it makes sense to be able to export just a fragment containing just
    the annotation element.
  */
  _convertPropertyAnnotation(anno) {
    // take only the annotations within the range of the anno
    var wrapper = this.$$('div').append(this.annotatedText(anno.path))
    var el = wrapper.find('['+this.config.idAttribute+'="'+anno.id+'"]')
    return el
  }
 
}
 
export default DOMExporter