123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- 'use strict';
- /**
- * @typedef {import('./types').XastParent} XastParent
- * @typedef {import('./types').XastRoot} XastRoot
- * @typedef {import('./types').XastElement} XastElement
- * @typedef {import('./types').XastInstruction} XastInstruction
- * @typedef {import('./types').XastDoctype} XastDoctype
- * @typedef {import('./types').XastText} XastText
- * @typedef {import('./types').XastCdata} XastCdata
- * @typedef {import('./types').XastComment} XastComment
- * @typedef {import('./types').StringifyOptions} StringifyOptions
- */
- const { textElems } = require('../plugins/_collections.js');
- /**
- * @typedef {{
- * width: void | string,
- * height: void | string,
- * indent: string,
- * textContext: null | XastElement,
- * indentLevel: number,
- * }} State
- */
- /**
- * @typedef {Required<StringifyOptions>} Options
- */
- /**
- * @type {(char: string) => string}
- */
- const encodeEntity = (char) => {
- return entities[char];
- };
- /**
- * @type {Options}
- */
- const defaults = {
- doctypeStart: '<!DOCTYPE',
- doctypeEnd: '>',
- procInstStart: '<?',
- procInstEnd: '?>',
- tagOpenStart: '<',
- tagOpenEnd: '>',
- tagCloseStart: '</',
- tagCloseEnd: '>',
- tagShortStart: '<',
- tagShortEnd: '/>',
- attrStart: '="',
- attrEnd: '"',
- commentStart: '<!--',
- commentEnd: '-->',
- cdataStart: '<![CDATA[',
- cdataEnd: ']]>',
- textStart: '',
- textEnd: '',
- indent: 4,
- regEntities: /[&'"<>]/g,
- regValEntities: /[&"<>]/g,
- encodeEntity: encodeEntity,
- pretty: false,
- useShortTags: true,
- eol: 'lf',
- finalNewline: false,
- };
- /**
- * @type {Record<string, string>}
- */
- const entities = {
- '&': '&',
- "'": ''',
- '"': '"',
- '>': '>',
- '<': '<',
- };
- /**
- * convert XAST to SVG string
- *
- * @type {(data: XastRoot, config: StringifyOptions) => {
- * data: string,
- * info: {
- * width: void | string,
- * height: void | string
- * }
- * }}
- */
- const stringifySvg = (data, userOptions = {}) => {
- /**
- * @type {Options}
- */
- const config = { ...defaults, ...userOptions };
- const indent = config.indent;
- let newIndent = ' ';
- if (typeof indent === 'number' && Number.isNaN(indent) === false) {
- newIndent = indent < 0 ? '\t' : ' '.repeat(indent);
- } else if (typeof indent === 'string') {
- newIndent = indent;
- }
- /**
- * @type {State}
- */
- const state = {
- // TODO remove width and height in v3
- width: undefined,
- height: undefined,
- indent: newIndent,
- textContext: null,
- indentLevel: 0,
- };
- const eol = config.eol === 'crlf' ? '\r\n' : '\n';
- if (config.pretty) {
- config.doctypeEnd += eol;
- config.procInstEnd += eol;
- config.commentEnd += eol;
- config.cdataEnd += eol;
- config.tagShortEnd += eol;
- config.tagOpenEnd += eol;
- config.tagCloseEnd += eol;
- config.textEnd += eol;
- }
- let svg = stringifyNode(data, config, state);
- if (config.finalNewline && svg.length > 0 && svg[svg.length - 1] !== '\n') {
- svg += eol;
- }
- return {
- data: svg,
- info: {
- width: state.width,
- height: state.height,
- },
- };
- };
- exports.stringifySvg = stringifySvg;
- /**
- * @type {(node: XastParent, config: Options, state: State) => string}
- */
- const stringifyNode = (data, config, state) => {
- let svg = '';
- state.indentLevel += 1;
- for (const item of data.children) {
- if (item.type === 'element') {
- svg += stringifyElement(item, config, state);
- }
- if (item.type === 'text') {
- svg += stringifyText(item, config, state);
- }
- if (item.type === 'doctype') {
- svg += stringifyDoctype(item, config);
- }
- if (item.type === 'instruction') {
- svg += stringifyInstruction(item, config);
- }
- if (item.type === 'comment') {
- svg += stringifyComment(item, config);
- }
- if (item.type === 'cdata') {
- svg += stringifyCdata(item, config, state);
- }
- }
- state.indentLevel -= 1;
- return svg;
- };
- /**
- * create indent string in accordance with the current node level.
- *
- * @type {(config: Options, state: State) => string}
- */
- const createIndent = (config, state) => {
- let indent = '';
- if (config.pretty && state.textContext == null) {
- indent = state.indent.repeat(state.indentLevel - 1);
- }
- return indent;
- };
- /**
- * @type {(node: XastDoctype, config: Options) => string}
- */
- const stringifyDoctype = (node, config) => {
- return config.doctypeStart + node.data.doctype + config.doctypeEnd;
- };
- /**
- * @type {(node: XastInstruction, config: Options) => string}
- */
- const stringifyInstruction = (node, config) => {
- return (
- config.procInstStart + node.name + ' ' + node.value + config.procInstEnd
- );
- };
- /**
- * @type {(node: XastComment, config: Options) => string}
- */
- const stringifyComment = (node, config) => {
- return config.commentStart + node.value + config.commentEnd;
- };
- /**
- * @type {(node: XastCdata, config: Options, state: State) => string}
- */
- const stringifyCdata = (node, config, state) => {
- return (
- createIndent(config, state) +
- config.cdataStart +
- node.value +
- config.cdataEnd
- );
- };
- /**
- * @type {(node: XastElement, config: Options, state: State) => string}
- */
- const stringifyElement = (node, config, state) => {
- // beautiful injection for obtaining SVG information :)
- if (
- node.name === 'svg' &&
- node.attributes.width != null &&
- node.attributes.height != null
- ) {
- state.width = node.attributes.width;
- state.height = node.attributes.height;
- }
- // empty element and short tag
- if (node.children.length === 0) {
- if (config.useShortTags) {
- return (
- createIndent(config, state) +
- config.tagShortStart +
- node.name +
- stringifyAttributes(node, config) +
- config.tagShortEnd
- );
- } else {
- return (
- createIndent(config, state) +
- config.tagShortStart +
- node.name +
- stringifyAttributes(node, config) +
- config.tagOpenEnd +
- config.tagCloseStart +
- node.name +
- config.tagCloseEnd
- );
- }
- // non-empty element
- } else {
- let tagOpenStart = config.tagOpenStart;
- let tagOpenEnd = config.tagOpenEnd;
- let tagCloseStart = config.tagCloseStart;
- let tagCloseEnd = config.tagCloseEnd;
- let openIndent = createIndent(config, state);
- let closeIndent = createIndent(config, state);
- if (state.textContext) {
- tagOpenStart = defaults.tagOpenStart;
- tagOpenEnd = defaults.tagOpenEnd;
- tagCloseStart = defaults.tagCloseStart;
- tagCloseEnd = defaults.tagCloseEnd;
- openIndent = '';
- } else if (textElems.includes(node.name)) {
- tagOpenEnd = defaults.tagOpenEnd;
- tagCloseStart = defaults.tagCloseStart;
- closeIndent = '';
- state.textContext = node;
- }
- const children = stringifyNode(node, config, state);
- if (state.textContext === node) {
- state.textContext = null;
- }
- return (
- openIndent +
- tagOpenStart +
- node.name +
- stringifyAttributes(node, config) +
- tagOpenEnd +
- children +
- closeIndent +
- tagCloseStart +
- node.name +
- tagCloseEnd
- );
- }
- };
- /**
- * @type {(node: XastElement, config: Options) => string}
- */
- const stringifyAttributes = (node, config) => {
- let attrs = '';
- for (const [name, value] of Object.entries(node.attributes)) {
- // TODO remove attributes without values support in v3
- if (value !== undefined) {
- const encodedValue = value
- .toString()
- .replace(config.regValEntities, config.encodeEntity);
- attrs += ' ' + name + config.attrStart + encodedValue + config.attrEnd;
- } else {
- attrs += ' ' + name;
- }
- }
- return attrs;
- };
- /**
- * @type {(node: XastText, config: Options, state: State) => string}
- */
- const stringifyText = (node, config, state) => {
- return (
- createIndent(config, state) +
- config.textStart +
- node.value.replace(config.regEntities, config.encodeEntity) +
- (state.textContext ? '' : config.textEnd)
- );
- };
|