diff --git a/lib/parser.js b/lib/parser.js index 1cade1419..6f97f4971 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -102,6 +102,7 @@ export const parseSvg = (data, from) => { * @type {XastParent[]} */ const stack = [root]; + let foreignLevel = 0; /** * @type {(node: XastChild) => void} @@ -157,7 +158,7 @@ export const parseSvg = (data, from) => { */ const node = { type: 'comment', - value: comment.trim(), + value: foreignLevel > 0 ? comment : comment.trim(), }; pushToContent(node); }; @@ -189,12 +190,15 @@ export const parseSvg = (data, from) => { pushToContent(element); current = element; stack.push(element); + if (data.name === 'foreignObject') { + foreignLevel++; + } }; sax.ontext = (text) => { if (current.type === 'element') { // prevent trimming of meaningful whitespace inside textual tags - if (textElems.has(current.name)) { + if (foreignLevel > 0 || textElems.has(current.name)) { /** * @type {XastText} */ @@ -216,9 +220,12 @@ export const parseSvg = (data, from) => { } }; - sax.onclosetag = () => { + sax.onclosetag = (tagName) => { stack.pop(); current = stack[stack.length - 1]; + if (tagName === 'foreignObject') { + foreignLevel--; + } }; sax.onerror = (e) => { diff --git a/lib/stringifier.js b/lib/stringifier.js index 83b8e12f3..4b7400a07 100644 --- a/lib/stringifier.js +++ b/lib/stringifier.js @@ -14,6 +14,8 @@ import { textElems } from '../plugins/_collections.js'; * indent: string, * textContext: ?XastElement, * indentLevel: number, + * foreignLevel: number, + * eolLen: number * }} State * @typedef {Required} Options */ @@ -81,14 +83,6 @@ export const stringifySvg = (data, userOptions = {}) => { } else if (typeof indent === 'string') { newIndent = indent; } - /** - * @type {State} - */ - const state = { - indent: newIndent, - textContext: null, - indentLevel: 0, - }; const eol = config.eol === 'crlf' ? '\r\n' : '\n'; if (config.pretty) { config.doctypeEnd += eol; @@ -100,6 +94,16 @@ export const stringifySvg = (data, userOptions = {}) => { config.tagCloseEnd += eol; config.textEnd += eol; } + /** + * @type {State} + */ + const state = { + indent: newIndent, + textContext: null, + indentLevel: 0, + foreignLevel: 0, + eolLen: eol.length, + }; let svg = stringifyNode(data, config, state); if (config.finalNewline && svg.length > 0 && !svg.endsWith('\n')) { svg += eol; @@ -116,20 +120,15 @@ const stringifyNode = (data, config, state) => { for (const item of data.children) { if (item.type === 'element') { svg += stringifyElement(item, config, state); - } - if (item.type === 'text') { + } else if (item.type === 'text') { svg += stringifyText(item, config, state); - } - if (item.type === 'doctype') { + } else if (item.type === 'doctype') { svg += stringifyDoctype(item, config); - } - if (item.type === 'instruction') { + } else if (item.type === 'instruction') { svg += stringifyInstruction(item, config); - } - if (item.type === 'comment') { - svg += stringifyComment(item, config); - } - if (item.type === 'cdata') { + } else if (item.type === 'comment') { + svg += stringifyComment(item, config, state); + } else if (item.type === 'cdata') { svg += stringifyCdata(item, config, state); } } @@ -144,12 +143,25 @@ const stringifyNode = (data, config, state) => { */ const createIndent = (config, state) => { let indent = ''; - if (config.pretty && state.textContext == null) { + if (config.pretty && state.textContext == null && state.foreignLevel === 0) { indent = state.indent.repeat(state.indentLevel - 1); } return indent; }; +/** + * Trim newline at end of tag if format is "pretty" and in a foreignObject. + * @param {string} tagEnd + * @param {StringifyOptions} config + * @param {State} state + */ +const formatEndTag = (tagEnd, config, state) => { + if (config.pretty && state.foreignLevel > 0) { + return tagEnd.substring(0, tagEnd.length - state.eolLen); + } + return tagEnd; +}; + /** * @type {(node: XastDoctype, config: Options) => string} */ @@ -167,10 +179,14 @@ const stringifyInstruction = (node, config) => { }; /** - * @type {(node: XastComment, config: Options) => string} + * @type {(node: XastComment, config: Options,state:State) => string} */ -const stringifyComment = (node, config) => { - return config.commentStart + node.value + config.commentEnd; +const stringifyComment = (node, config, state) => { + return ( + config.commentStart + + node.value + + formatEndTag(config.commentEnd, config, state) + ); }; /** @@ -181,7 +197,7 @@ const stringifyCdata = (node, config, state) => { createIndent(config, state) + config.cdataStart + node.value + - config.cdataEnd + formatEndTag(config.cdataEnd, config, state) ); }; @@ -197,7 +213,7 @@ const stringifyElement = (node, config, state) => { config.tagShortStart + node.name + stringifyAttributes(node, config) + - config.tagShortEnd + formatEndTag(config.tagShortEnd, config, state) ); } else { return ( @@ -205,10 +221,10 @@ const stringifyElement = (node, config, state) => { config.tagShortStart + node.name + stringifyAttributes(node, config) + - config.tagOpenEnd + + formatEndTag(config.tagOpenEnd, config, state) + config.tagCloseStart + node.name + - config.tagCloseEnd + formatEndTag(config.tagCloseEnd, config, state) ); } // non-empty element @@ -218,7 +234,7 @@ const stringifyElement = (node, config, state) => { let tagCloseStart = config.tagCloseStart; let tagCloseEnd = config.tagCloseEnd; let openIndent = createIndent(config, state); - let closeIndent = createIndent(config, state); + let enableCloseIndent = true; if (state.textContext) { tagOpenStart = defaults.tagOpenStart; @@ -229,28 +245,34 @@ const stringifyElement = (node, config, state) => { } else if (textElems.has(node.name)) { tagOpenEnd = defaults.tagOpenEnd; tagCloseStart = defaults.tagCloseStart; - closeIndent = ''; + enableCloseIndent = false; state.textContext = node; } + if (node.name === 'foreignObject') { + state.foreignLevel++; + } const children = stringifyNode(node, config, state); if (state.textContext === node) { state.textContext = null; } - return ( + const s = openIndent + tagOpenStart + node.name + stringifyAttributes(node, config) + - tagOpenEnd + + formatEndTag(tagOpenEnd, config, state) + children + - closeIndent + + (enableCloseIndent ? createIndent(config, state) : '') + tagCloseStart + - node.name + - tagCloseEnd - ); + node.name; + if (node.name === 'foreignObject') { + state.foreignLevel--; + } + + return s + formatEndTag(tagCloseEnd, config, state); } }; @@ -281,6 +303,6 @@ const stringifyText = (node, config, state) => { createIndent(config, state) + config.textStart + node.value.replace(config.regEntities, config.encodeEntity) + - (state.textContext ? '' : config.textEnd) + (state.textContext ? '' : formatEndTag(config.textEnd, config, state)) ); }; diff --git a/plugins/_collections.js b/plugins/_collections.js index 194b39fb6..74a059620 100644 --- a/plugins/_collections.js +++ b/plugins/_collections.js @@ -103,10 +103,8 @@ export const elemsGroups = { /** * Elements where adding or removing whitespace may effect rendering, metadata, * or semantic meaning. - * - * @see https://developer.mozilla.org/docs/Web/HTML/Element/pre */ -export const textElems = new Set([...elemsGroups.textContent, 'pre', 'title']); +export const textElems = new Set([...elemsGroups.textContent, 'title']); export const pathElems = new Set(['glyph', 'missing-glyph', 'path']); diff --git a/test/plugins/inlineStyles.17.svg.txt b/test/plugins/inlineStyles.17.svg.txt index ae3adc010..01a3a8a27 100644 --- a/test/plugins/inlineStyles.17.svg.txt +++ b/test/plugins/inlineStyles.17.svg.txt @@ -9,13 +9,7 @@ - - -
- hello, world -
- -
+ +
hello, world
+ diff --git a/test/plugins/mergeStyles.12.svg.txt b/test/plugins/mergeStyles.12.svg.txt index 884b58f58..1a3d1f83d 100644 --- a/test/plugins/mergeStyles.12.svg.txt +++ b/test/plugins/mergeStyles.12.svg.txt @@ -17,10 +17,10 @@ Skip styles inside foreignObject element - - + + diff --git a/test/svgo/_index.test.js b/test/svgo/_index.test.js index 6bdddc19b..deea80db5 100644 --- a/test/svgo/_index.test.js +++ b/test/svgo/_index.test.js @@ -102,4 +102,17 @@ describe('svgo', () => { }); expect(normalize(result.data)).toStrictEqual(expected); }); + + it('should preserve comments and cdata in foreign object', async () => { + const [original, expected] = await parseFixture( + 'foreign-comments-and-cdata-pretty.svg.txt', + ); + // Disable plugins so comments aren't removed. + const result = optimize(original, { + path: 'input.svg', + plugins: [], + js2svg: { pretty: true }, + }); + expect(normalize(result.data)).toStrictEqual(expected); + }); }); diff --git a/test/svgo/foreign-comments-and-cdata-pretty.svg.txt b/test/svgo/foreign-comments-and-cdata-pretty.svg.txt new file mode 100644 index 000000000..58e9b7f22 --- /dev/null +++ b/test/svgo/foreign-comments-and-cdata-pretty.svg.txt @@ -0,0 +1,27 @@ + + +
+ + Some random text + + +
+
+
+ +@@@ + + + +
+ + Some random text + + +
+
+
\ No newline at end of file diff --git a/test/svgo/pre-element-pretty.svg.txt b/test/svgo/pre-element-pretty.svg.txt index 692bbcef1..82d027def 100644 --- a/test/svgo/pre-element-pretty.svg.txt +++ b/test/svgo/pre-element-pretty.svg.txt @@ -20,8 +20,8 @@ M M A A IIIII N N T A A IIIII N N EEEEE R R -
-
 OOO   PPPP   EEEEE  N   N  SSSSS   OOO   U   U  RRRR    CCCC  EEEEE
+    
+
 OOO   PPPP   EEEEE  N   N  SSSSS   OOO   U   U  RRRR    CCCC  EEEEE
 O   O  P   P  E      NN  N  SS     O   O  U   U  R   R  C      E    
 O   O  PPPP   EEE    N N N   SSS   O   O  U   U  RRRR   C      EEE  
 O   O  P      E      N  NN     SS  O   O  U   U  R R    C      E    
@@ -32,6 +32,6 @@ MM MM  A   A    I    NN  N    T    A   A    I    NN  N  E      R   R
 M M M  AAAAA    I    N N N    T    AAAAA    I    N N N  EEE    RRRR 
 M   M  A   A    I    N  NN    T    A   A    I    N  NN  E      R R  
 M   M  A   A  IIIII  N   N    T    A   A  IIIII  N   N  EEEEE  R  R 
-
- +
+
diff --git a/test/svgo/pre-element.svg.txt b/test/svgo/pre-element.svg.txt index 71a6f9525..fc2e0f43b 100644 --- a/test/svgo/pre-element.svg.txt +++ b/test/svgo/pre-element.svg.txt @@ -18,7 +18,9 @@ M M A A IIIII N N T A A IIIII N N EEEEE R R @@@ -
 OOO   PPPP   EEEEE  N   N  SSSSS   OOO   U   U  RRRR    CCCC  EEEEE
+
+    
+
 OOO   PPPP   EEEEE  N   N  SSSSS   OOO   U   U  RRRR    CCCC  EEEEE
 O   O  P   P  E      NN  N  SS     O   O  U   U  R   R  C      E    
 O   O  PPPP   EEE    N N N   SSS   O   O  U   U  RRRR   C      EEE  
 O   O  P      E      N  NN     SS  O   O  U   U  R R    C      E    
@@ -28,4 +30,7 @@ M   M   AAA   IIIII  N   N  TTTTT   AAA   IIIII  N   N  EEEEE  RRRR
 MM MM  A   A    I    NN  N    T    A   A    I    NN  N  E      R   R
 M M M  AAAAA    I    N N N    T    AAAAA    I    N N N  EEE    RRRR 
 M   M  A   A    I    N  NN    T    A   A    I    N  NN  E      R R  
-M   M  A   A  IIIII  N   N    T    A   A  IIIII  N   N  EEEEE  R  R 
+M M A A IIIII N N T A A IIIII N N EEEEE R R
+
+
+