UNPKG

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
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