EpubHtml.js

import { set } from 'lodash';
import EpubItem from './EpubItem';
import ItemTypeEnum from './enum/ItemTypeEnum';
import convertXmlStringToJS from './utils/convertXmlStringToJS';
import convertJsObjectToXML from './utils/convertJsObjectToXML';
import addChild from './utils/addChild';
import TemplateTypeEnum from './enum/TemplateTypeEnum';
import findNodesRecursively from './utils/findNodesRecursively';

/**
 * Class that represents an HTML document in the EPUB file.
 *
 * @class
 * @augments EpubItem
 */
class EpubHtml extends EpubItem {
	constructor(
		uid = null,
		fileName = '',
		mediaType = '',
		content = null,
		title = '',
		lang = null,
		direction = null,
		mediaOverlay = null,
		mediaDuration = null,
	) {
		super(uid, fileName, mediaType, content);

		this.title = title;
		this.lang = lang;
		this.direction = direction;

		this.mediaOverlay = mediaOverlay;
		this.mediaDuration = mediaDuration;

		this.links = [];
		this.properties = [];
		this.pages = [];
	}

	/**
	 * Returns true if this document is a chapter and false if it is not
	 *
	 * @returns {boolean} The book value
	 */
	static isChapter() {
		return true;
	}

	/**
	 * Overrides EpubItem's getType method to always return DOCUMENT type
	 *
	 * @returns {ItemTypeEnum} The item's type
	 */
	static getType() {
		return ItemTypeEnum.DOCUMENT;
	}

	/**
	 * Sets the language for this book item. By default it will user the language of the book, but
	 * it can still be overwritten with this method.
	 *
	 * @param {string} lang  The new language code for this book item
	 */
	setLanguage(lang) {
		this.lang = lang;
	}

	/**
	 * Gets the language code for this book item. The language of the book item can be different from the
	 * language settings defined globally for the book.
	 *
	 * @returns {string}  The language code for this book item
	 */
	getLanguage() {
		return this.lang;
	}

	/**
	 * Adds an additional link to the document. Links will be embedded only inside of this document.
	 * NOTE: This link has an undefined structure and is not the same as the Link object in src/epub/Link.js
	 *
	 * @example <caption>Example usage of addLink.</caption>
	 * addLink({href='styles.css', rel='stylesheet', type='text/css'})
	 *
	 * @param {object} link The link to add to the document
	 */
	addLink(link) {
		this.links.push(link);

		if (
			Object.keys(link).includes('type') &&
			link.type === 'text/javascript' &&
			!this.properties.includes('scripted')
		) {
			this.properties.push('scripted');
		}
	}

	/**
	 * Returns the array of additional links defined for this document.
	 * NOTE: These links have an undefined structure and is not the same as the Link object in src/epub/Link.js
	 *
	 * @returns {Array.<object>} An array of links
	 *
	 */
	getLinks() {
		return this.links;
	}

	/**
	 * Returns an array of additional links defined for this document of the given type.
	 * NOTE: These links have an undefined structure and is not the same as the Link object in src/epub/Link.js
	 *
	 * @param {string} linkType  The type of the links to search for (e.g. 'text/javascript')
	 *
	 * @returns {Array.<object>} An array of links
	 *
	 */
	getLinksOfType(linkType) {
		return this.links.filter(
			(link) => Object.keys(link).includes('type') && link.type === linkType,
		);
	}

	/**
	 * Adds an EpubItem to this document. It will create additional links according to the item type.
	 *
	 * @param {EpubItem} item  The EpubItem to add to this document
	 */
	addItem(item) {
		switch (item.getType()) {
			case ItemTypeEnum.STYLE:
				this.addLink({
					href: item.getName(),
					rel: 'stylesheet',
					type: 'text/css',
				});
				break;
			case ItemTypeEnum.SCRIPT:
				this.addLink({
					src: item.getName(),
					type: 'text/javascript',
				});
				break;
			default:
		}
	}

	/**
	 * Gets the content of the Body element for this HTML document. Content will be of type string
	 *
	 * @returns {string}  The body content of this document
	 */
	getBodyContent() {
		try {
			const wrappedContent =
				this.content && this.content.includes('body')
					? this.content
					: `<div>${this.content || ''}</div>`;

			const content = convertXmlStringToJS(wrappedContent);
			const bodyNodes = findNodesRecursively(
				[content],
				(node) => node.name === 'body',
			);

			return bodyNodes.length > 0 ? bodyNodes[0] : content;
		} catch (error) {
			return '';
		}
	}

	/**
	 * Gets the content for this HTML document as a string.
	 *
	 * @returns {string} The content
	 */
	getContent() {
		try {
			const tree = convertXmlStringToJS(
				this.book.getTemplate(TemplateTypeEnum.CHAPTER),
			);

			const treeRoot = tree.elements[tree.elements.length - 1];
			set(treeRoot.attributes, 'lang', this.lang || this.book.language);
			set(treeRoot.attributes, 'xml:lang', this.lang || this.book.language);

			const headElement = {
				type: 'element',
				name: 'head',
				attributes: {},
				elements: [],
			};

			if (this.title !== '') {
				const titleElement = {
					type: 'element',
					name: 'title',
					attributes: {},
					elements: [
						{
							type: 'text',
							text: this.title,
						},
					],
				};
				addChild(headElement, titleElement);
			}

			Object.values(this.links).forEach((link) => {
				switch (link.type) {
					case 'text/javascript': {
						const scriptElement = {
							type: 'element',
							name: 'script',
							attributes: {},
							elements: [],
						};
						set(scriptElement.attributes, 'src', link.src);
						addChild(headElement, scriptElement);
						break;
					}
					case 'text/css': {
						const styleElement = {
							type: 'element',
							name: 'link',
							attributes: {},
							elements: [],
						};
						set(styleElement.attributes, 'rel', link.rel);
						set(styleElement.attributes, 'href', link.href);
						addChild(headElement, styleElement);
						break;
					}
					default:
				}
			});

			addChild(treeRoot, headElement);
			const bodyElement = {
				type: 'element',
				name: 'body',
				attributes: {},
				elements: [],
			};

			if (this.direction) {
				set(bodyElement.attributes, 'dir', this.direction);
				set(treeRoot.attributes, 'dir', this.direction);
			}

			const bodyContent = this.getBodyContent();

			if (bodyContent) {
				bodyContent.elements.forEach((node) => addChild(bodyElement, node));
			}

			addChild(treeRoot, bodyElement);
			tree.elements[tree.elements.length - 1] = treeRoot;

			return convertJsObjectToXML(tree);
		} catch (error) {
			return '';
		}
	}
}

EpubHtml.prototype.toString = () => `<EpubHtml:${this.id}:${this.fileName}>`;

export default EpubHtml;