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 system. It is developed to power our online editing platform [Substance](http://substance.io).

395 lines (365 loc) 12.5 kB
import XMLImporter from './XMLImporter' import XMLExporter from './XMLExporter' import { ATTRIBUTE_PROPERTY_TYPES } from './_SchemaConstants' import _requiresPropertyElements from './_requiresPropertyElements' import { isNil } from '../util' export default function createXmlConverterFactory (rootType, definition) { const converters = [] for (const nodeSpec of definition.nodes.values()) { switch (nodeSpec.parentType) { case '@node': { converters.push(_createNodeConverter(nodeSpec)) break } case '@text': { converters.push(_createTextNodeConverter(nodeSpec)) break } case '@annotation': case '@inlinenode': { converters.push(_createAnnotationConverter(nodeSpec)) break } case '@asset': { converters.push(_createAssetConverter(nodeSpec)) break } default: throw new Error(`Unsupported parent type ${nodeSpec.parentType}`) } } return { createImporter (doc, context) { return new NextDocumentXmlImporter({ converters }, doc, { rootType, definition, context }) }, createExporter (context) { return new NextDocumentXmlExporter({ converters }, { rootType, definition, context }) } } } class NextDocumentXmlImporter extends XMLImporter { constructor (params, doc, options = {}) { super(params, doc, options) this.rootType = options.rootType this.definition = options.definition this.context = options.context || {} } importIntoDocument (xmlDom) { const doc = this.state.doc doc.clear() const rootEl = xmlDom.find(this.rootType) const root = this.convertElement(rootEl) doc.root = root } } class NextDocumentXmlExporter extends XMLExporter { constructor (params, options = {}) { super(params, options) this.rootType = options.rootType this.definition = options.definition this.context = options.context || {} } } function _createBasicNodeConverter (nodeSpec) { class BasicNodeConverter { get type () { return nodeSpec.type } get tagName () { return nodeSpec.type } get nodeSpec () { return nodeSpec } import (el, node, importer) {} export (node, el, exporter) {} _importAttributes (el, node, importer) { for (const [propName, propSpec] of nodeSpec.properties) { this._importAttribute(propName, propSpec, el, node, importer) } } _importAttribute (propName, propSpec, el, node, importer) { const type = propSpec.type const str = el.getAttribute(propName) if (!ATTRIBUTE_PROPERTY_TYPES.has(type) || !str) return let val switch (propSpec.type) { case 'integer': { val = parseInt(str) break } case 'number': { val = Number(str) break } case 'boolean': { val = (str === 'true') break } case 'string': case 'one': { val = str break } case 'string-array': val = str.split(';').map(s => s.trim()) break case 'many': { val = str.split(/\s+/).map(s => s.trim()) break } default: throw new Error('Illegal state') } node[propName] = val } _exportAttributes (node, el, exporter) { for (const [propName, propSpec] of nodeSpec.properties) { this._exportAttribute(propName, propSpec, node, el, exporter) } } _exportAttribute (propName, propSpec, node, el, exporter) { const type = propSpec.type if (!ATTRIBUTE_PROPERTY_TYPES.has(type)) return const isOptional = propSpec.options.optional const val = node.get(propName) let str if (isNil(val)) { // skip if attribute is optional if (isOptional) { return } else { str = '' } } else { switch (propSpec.type) { case 'integer': case 'number': case 'boolean': case 'string': case 'one': { str = String(val) break } case 'string-array': if (val.length === 0 && isOptional) return str = val.join(';') break case 'many': { if (val.length === 0 && isOptional) return str = val.join(' ') break } default: throw new Error('Illegal state') } } el.setAttribute(propName, str) } } return BasicNodeConverter } function _createAnnotationConverter (nodeSpec) { class AnnotationConverter extends _createBasicNodeConverter(nodeSpec) { import (el, node, importer) { this._importAttributes(el, node, importer) } export (node, el, exporter) { this._exportAttributes(node, el, exporter) } } return AnnotationConverter } function _createTextNodeConverter (nodeSpec) { class TextNodeConverter extends _createBasicNodeConverter(nodeSpec) { import (el, node, importer) { this._importAttributes(el, node, importer) node.content = importer.annotatedText(el, [node.id, 'content']) } export (node, el, exporter) { this._exportAttributes(node, el, exporter) el.append(exporter.annotatedText([node.id, 'content'])) } } return TextNodeConverter } function _createNodeConverter (nodeSpec) { class NodeConverter extends _createBasicNodeConverter(nodeSpec) { import (el, node, importer) { this._importAttributes(el, node, importer) this._importChildProperties(el, node, importer) } export (node, el, exporter) { this._exportAttributes(node, el, exporter) this._exportChildProperties(node, el, exporter) } _importChildProperties (el, node, importer) { // TODO: for now we use property elements for all 'structured' nodes const usePropertyEl = _requiresPropertyElements(nodeSpec) if (usePropertyEl) { for (const propEl of el.children) { const propName = propEl.tagName this._importChildProperty(propName, propEl, node, importer) } } else if (nodeSpec.childPropertyNames.length === 1) { this._importChildProperty(nodeSpec.childPropertyNames[0], el, node, importer) } else if (nodeSpec.childPropertyNames.length > 1) { throw new Error("Only one child property allowed when 'omitPropertyElement=true'") } } _importChildProperty (propName, propEl, node, importer) { const propSpec = nodeSpec.properties.get(propName) let val switch (propSpec.type) { case 'child': { const childNode = importer.convertElement(propEl) val = childNode.id break } case 'children': case 'container': { const childNodes = propEl.getChildren().map(childEl => { return importer.convertElement(childEl) }) val = childNodes.map(n => n.id) break } case 'text': { val = importer.annotatedText(propEl, [node.id, propName]) break } default: throw new Error('Illegal state') } node[propName] = val } _exportChildProperties (node, el, exporter) { // TODO: try to simplify this. I don't like how the decision whether to append // to 'el' or use a property element is taken in _exportChildProperty() // TODO: for now we use property elements for all 'structured' nodes const usePropertyEl = _requiresPropertyElements(nodeSpec) if (usePropertyEl) { for (const propName of nodeSpec.childPropertyNames) { const propEl = el.createElement(propName) // ATTENTION: in some cases the propEl is omitted // thus we allow to provide a different element to be appended const childEl = this._exportChildProperty(propName, node, propEl, exporter) el.append(childEl) } } else if (nodeSpec.childPropertyNames.length === 1) { const propEl = el const childEl = this._exportChildProperty(nodeSpec.childPropertyNames[0], node, propEl, exporter) // NOTE: when property elements are omitted generally // then we use el as property element // only if a different element is returned we have to append (which sounds like an edge case) if (childEl !== propEl) { el.append(childEl) } } else if (nodeSpec.childPropertyNames.length > 1) { throw new Error("Only one child property allowed when 'omitPropertyElement=true'") } } _exportChildProperty (propName, node, propEl, exporter) { const propSpec = nodeSpec.properties.get(propName) switch (propSpec.type) { case 'child': { const childNode = node.resolve(propName) if (childNode) { const childEl = exporter.convertNode(childNode) // TODO: we need to find a clearer approach to this // In some cases, e.g. when the property is called the same as the (only) child node type, // it is desired to omit the property element if (childNode.type === propName && propSpec.options && propSpec.options.childTypes.length === 1) { propEl = childEl } else { propEl.append(childEl) } } break } case 'children': case 'container': { const childNodes = node.resolve(propName) // skip empty containers if they are optional if (childNodes.length === 0 && propSpec.options.optional) { return null } propEl.append(childNodes.map(childNode => exporter.convertNode(childNode))) break } case 'text': { propEl.append(exporter.annotatedText([node.id, propName])) break } default: throw new Error('Illegal state') } return propEl } } return NodeConverter } function _createAssetConverter (nodeSpec) { class AssetConverter extends _createNodeConverter(nodeSpec) { _getArchive (imOrExporter) { const archive = imOrExporter.context.archive if (!archive) { throw new Error("'options.context' is required") } return archive } _importAttribute (propName, propSpec, el, node, importer) { // TODO: this is pretty hard coded, maybe we could introduce a property type instead // and make this a general behavior switch (propName) { case 'src': { // NOTE: stored as filename in XML, we need to map to assetId internally const filename = el.getAttribute('src') const archive = this._getArchive(importer) const asset = archive.getAssetForFilename(filename) if (asset) { node.src = asset.id } break } case 'mimetype': { // NOTE: no matter what is stored in XML, we won't gonna use it const filename = el.getAttribute('src') const archive = this._getArchive(importer) const asset = archive.getAssetForFilename(filename) if (asset) { node.mimetype = asset.mimetype } break } default: { return super._importAttribute(propName, propSpec, el, node, importer) } } } _exportAttribute (propName, propSpec, node, el, exporter) { // TODO: this is pretty hard coded, maybe we could introduce a property type instead // and make this a general behavior switch (propName) { case 'src': { // NOTE: internally stores an assetId, and needs to be mapped to filename const archive = this._getArchive(exporter) const asset = archive.getAssetById(node.src) if (asset) { el.attr('src', asset.filename) } else { el.attr('src', '') } break } case 'mimetype': { // NOTE: whatever is stored here, only what the DAR says matters const archive = this._getArchive(exporter) const asset = archive.getAssetById(node.src) if (asset && asset.mimetype) { el.attr('mimetype', asset.mimetype) } break } default: { super._exportAttribute(propName, propSpec, node, el, exporter) } } } } return AssetConverter }