| 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)  );};
 |