substance
Version:
Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing systems.
201 lines (179 loc) • 5.86 kB
JavaScript
import { isString, isFunction, Registry, encodeXMLEntities } from '../util'
import Fragmenter from './Fragmenter'
class DOMExporter {
constructor(config, context) {
this.context = context || {}
if (!config.converters) {
throw new Error('config.converters is mandatory')
}
if (!config.converters._isRegistry) {
this.converters = new Registry()
config.converters.forEach(function(Converter) {
let converter = isFunction(Converter) ? new Converter() : Converter
if (!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
if (!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) {
if (!container) {
throw new Error('Illegal arguments: container is mandatory.')
}
const doc = container.getDocument()
this.state.doc = doc
let elements = []
container.getContent().forEach((id) => {
const node = doc.get(id)
const nodeEl = this.convertNode(node)
elements.push(nodeEl)
})
return elements
}
convertNode(node) {
if (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)`
if (node.isPropertyAnnotation() && (!converter || !converter.export)) {
return this._convertPropertyAnnotation(node)
}
if (!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)
if (!converter) {
converter = self.getDefaultPropertyAnnotationConverter()
}
var el
if (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