import AstNode, { Attributes } from './Node';
import Schema from './Schema';
import Writer, { WriterSettings } from './Writer';
export interface HtmlSerializerSettings extends WriterSettings {
inner?: boolean;
validate?: boolean;
}
interface HtmlSerializer {
serialize: (node: AstNode) => string;
}
/**
* This class is used to serialize down the DOM tree into a string using a Writer instance.
*
* @class tinymce.html.Serializer
* @version 3.4
* @example
* tinymce.html.Serializer().serialize(tinymce.html.DomParser().parse('
text
'));
*/
const HtmlSerializer = (settings: HtmlSerializerSettings = {}, schema: Schema = Schema()): HtmlSerializer => {
const writer = Writer(settings);
settings.validate = 'validate' in settings ? settings.validate : true;
/**
* Serializes the specified node into a string.
*
* @method serialize
* @param {tinymce.html.Node} node Node instance to serialize.
* @return {String} String with HTML based on the DOM tree.
* @example
* tinymce.html.Serializer().serialize(tinymce.html.DomParser().parse('text
'));
*/
const serialize = (node: AstNode): string => {
const validate = settings.validate;
const handlers: Record void> = {
// #text
3: (node) => {
writer.text(node.value ?? '', node.raw);
},
// #comment
8: (node) => {
writer.comment(node.value ?? '');
},
// Processing instruction
7: (node) => {
writer.pi(node.name, node.value);
},
// Doctype
10: (node) => {
writer.doctype(node.value ?? '');
},
// CDATA
4: (node) => {
writer.cdata(node.value ?? '');
},
// Document fragment
11: (node) => {
let tempNode: AstNode | null | undefined = node;
if ((tempNode = tempNode.firstChild)) {
do {
walk(tempNode);
} while ((tempNode = tempNode.next));
}
}
};
writer.reset();
const walk = (node: AstNode) => {
const handler = handlers[node.type];
if (!handler) {
const name = node.name;
const isEmpty = name in schema.getVoidElements();
let attrs = node.attributes;
// Sort attributes
if (validate && attrs && attrs.length > 1) {
const sortedAttrs = [] as unknown as Attributes;
(sortedAttrs as any).map = {};
const elementRule = schema.getElementRule(node.name);
if (elementRule) {
for (let i = 0, l = elementRule.attributesOrder.length; i < l; i++) {
const attrName: string = elementRule.attributesOrder[i];
if (attrName in attrs.map) {
const attrValue = attrs.map[attrName];
sortedAttrs.map[attrName] = attrValue;
sortedAttrs.push({ name: attrName, value: attrValue });
}
}
for (let i = 0, l = attrs.length; i < l; i++) {
const attrName: string = attrs[i].name;
if (!(attrName in sortedAttrs.map)) {
const attrValue = attrs.map[attrName];
sortedAttrs.map[attrName] = attrValue;
sortedAttrs.push({ name: attrName, value: attrValue });
}
}
attrs = sortedAttrs;
}
}
writer.start(name, attrs, isEmpty);
if (!isEmpty) {
let child = node.firstChild;
if (child) {
// Pre and textarea elements treat the first newline character as optional and will omit it. As such, if the content starts
// with a newline we need to add in an additional newline to prevent the current newline in the value being treated as optional
// See https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
if ((name === 'pre' || name === 'textarea') && child.type === 3 && child.value?.[0] === '\n') {
writer.text('\n', true);
}
do {
walk(child);
} while ((child = child.next));
}
writer.end(name);
}
} else {
handler(node);
}
};
// Serialize element or text nodes and treat all other nodes as fragments
if (node.type === 1 && !settings.inner) {
walk(node);
} else if (node.type === 3) {
handlers[3](node);
} else {
handlers[11](node);
}
return writer.getContent();
};
return {
serialize
};
};
export default HtmlSerializer;