From 31a8bcd69c3bdc7cc743f3eafffaa212c9737ed3 Mon Sep 17 00:00:00 2001 From: John Kenny Date: Fri, 1 Dec 2023 16:19:28 -0800 Subject: [PATCH 01/20] Added mergeText plugin. --- plugins/mergeText.js | 306 ++++++++++++++++++++++++++++++++++ plugins/plugins-types.d.ts | 1 + test/plugins/mergeText.01.svg | 18 ++ test/plugins/mergeText.02.svg | 18 ++ test/plugins/mergeText.03.svg | 21 +++ test/plugins/mergeText.04.svg | 14 ++ test/plugins/mergeText.05.svg | 13 ++ test/plugins/mergeText.06.svg | 18 ++ test/plugins/mergeText.07.svg | 20 +++ test/plugins/mergeText.08.svg | 17 ++ 10 files changed, 446 insertions(+) create mode 100644 plugins/mergeText.js create mode 100644 test/plugins/mergeText.01.svg create mode 100644 test/plugins/mergeText.02.svg create mode 100644 test/plugins/mergeText.03.svg create mode 100644 test/plugins/mergeText.04.svg create mode 100644 test/plugins/mergeText.05.svg create mode 100644 test/plugins/mergeText.06.svg create mode 100644 test/plugins/mergeText.07.svg create mode 100644 test/plugins/mergeText.08.svg diff --git a/plugins/mergeText.js b/plugins/mergeText.js new file mode 100644 index 000000000..c7d202ddc --- /dev/null +++ b/plugins/mergeText.js @@ -0,0 +1,306 @@ +'use strict'; + +/** + * @typedef {import('../lib/types').XastElement} XastElement + * @typedef {import('../lib/types').XastChild} XastChild + */ + +exports.name = 'mergeText'; +exports.description = 'merges elements and children where possible'; + +/** + * @typedef {{ + * x:string, + * y:string, + * attributes:Map, + * children:XastChild[] + * }} TspanData + */ + +/** + * @typedef {{ + * x:string, + * y:string, + * children:TspanData[], + * } + * }TextData + */ + +/** + * Merge elements and children where possible, and minimize duplication of attributes. + * + * @author John Kenny + * + * @type {import('./plugins-types').Plugin<'mergeText'>} + */ + +exports.fn = () => { + let deoptimized = false; + + return { + element: { + enter: (node) => { + // Don't collapse if styles are present. + if (node.name === 'style' && node.children.length !== 0) { + deoptimized = true; + } + }, + + exit: (node) => { + if (deoptimized) { + return; + } + + // See if the node has children. + /** @type Map */ + const mergeableChildren = new Map(); + for (let index = 0; index < node.children.length; index++) { + const child = node.children[index]; + if (child.type === 'element' && child.name === 'text') { + const mergeData = getMergeData(child); + if (mergeData) { + mergeableChildren.set(index, mergeData); + } + } + } + + // If nothing to merge, return. + if (mergeableChildren.size === 0) { + return; + } + + // Create new child nodes. + /** @type XastChild[] */ + const newChildren = []; + for (let index = 0; index < node.children.length; index++) { + if (!mergeableChildren.has(index)) { + // This is not a element; do not process. + newChildren.push(node.children[index]); + continue; + } + if (mergeableChildren.has(index - 1)) { + // The previous child was a mergeable element; assume this one was already merged into it. + continue; + } + // Merge and insert text elements. + newChildren.push(mergeTextElements(mergeableChildren, index)); + } + + // Update children. + node.children = newChildren; + }, + }, + }; +}; + +/** + * + * @param {XastElement} textEl + * @returns {any} + */ +function getMergeData(textEl) { + /**@type TextData */ + const data = {}; + /**@type Map */ + const textAttributes = new Map(); + data.children = []; + + // Gather all attributes. + for (const [k, v] of Object.entries(textEl.attributes)) { + switch (k) { + case 'x': + case 'y': + data[k] = v; + break; + default: + textAttributes.set(k, v); + break; + } + } + + // Check all children of element. + for (const child of textEl.children) { + if (child.type === 'text') { + if (isWhiteSpace(child.value)) { + // Ignore nodes that are all whitespace. + continue; + } + } + if (child.type !== 'element' || child.name !== 'tspan') { + // Don't transform unless all children are elements. + return; + } + for (const tspanChild of child.children) { + // Don't transform unless has only text nodes and s as children. + if (tspanChild.type === 'text') { + continue; + } + if (tspanChild.type === 'element' && tspanChild.name === 'tspan') { + continue; + } + return; + } + + /** @type TspanData */ + const tspanData = {}; + + // Copy all attributes to the tspan data. + tspanData.attributes = new Map(textAttributes); + + // Merge all attributes. + for (const [k, v] of Object.entries(child.attributes)) { + switch (k) { + case 'x': + case 'y': + tspanData[k] = v; + break; + case 'color': + case 'fill': + case 'fill-opacity': + case 'fill-rule': + case 'opacity': + case 'stroke': + case 'stroke-dasharray': + case 'stroke-dashoffset': + case 'stroke-linecap': + case 'stroke-linejoin': + case 'stroke-miterlimit': + case 'stroke-opacity': + case 'stroke-width': + tspanData.attributes.set(k, v); + break; + default: + // Don't transform if has unrecognized attributes. + return; + } + } + + // Don't transform unless all children have x and y attributes. + if (!tspanData.x || !tspanData.y) { + return; + } + + tspanData.children = child.children; + + data.children.push(tspanData); + } + + return data; +} + +/** + * + * @param {TspanData[]} tspans + * @returns {{}} + */ +function getTextAttributes(tspans) { + // Figure out what attributes and values we have. + const allAttributes = new Map(); + for (const tspanElement of tspans) { + for (const [attName, attValue] of tspanElement.attributes) { + let values = allAttributes.get(attName); + if (!values) { + values = new Map(); + allAttributes.set(attName, values); + } + if (!values.has(attValue)) { + values.set(attValue, 1); + } else { + values.set(attValue, values.get(attValue) + 1); + } + } + } + + // Figure out which ones to use as attributes. + /** @type Object. */ + const textAttributes = {}; + for (const [attName, values] of allAttributes) { + // Check all values. If there are fewer values than children, at least one child does not have the attribute; + // in this case leave attribute off of . Otherwise, set text attribute to the most-used value. + let total = 0; + let maxCount = 0; + let textAttValue; + for (const [value, count] of values) { + total += count; + if (count > maxCount) { + maxCount = count; + textAttValue = value; + } + } + if (total === tspans.length && textAttValue) { + textAttributes[attName] = textAttValue; + } + } + + return textAttributes; +} + +/** + * @param {string} s + * @returns {boolean} + */ +function isWhiteSpace(s) { + return /^\s*$/.test(s); +} + +/** + * + * @param {Map} mergeableChildren + * @param {number} index + * @returns XastElement + */ +function mergeTextElements(mergeableChildren, index) { + /** @type {XastElement} */ + const textElement = { + type: 'element', + name: 'text', + attributes: {}, + children: [], + }; + + // Find all child data. + const tspans = []; + for (; ; index++) { + const textData = mergeableChildren.get(index); + if (!textData) { + break; + } + tspans.push(...textData.children); + } + + // Find the default attributes for the element. + textElement.attributes = getTextAttributes(tspans); + + // If there is only one , merge content into . + if (tspans.length === 1) { + const tspanData = tspans[0]; + textElement.attributes.x = tspanData.x; + textElement.attributes.y = tspanData.y; + textElement.children = tspanData.children; + return textElement; + } + + // Generate elements. + const textChildren = []; + for (const tspanData of tspans) { + /** @type {XastElement} */ + const tspanElement = { + type: 'element', + name: 'tspan', + attributes: { x: tspanData.x, y: tspanData.y }, + children: tspanData.children, + }; + + // Add any attributes that are different from attributes. + for (const [k, v] of tspanData.attributes) { + if (textElement.attributes[k] !== v) { + tspanElement.attributes[k] = v; + } + } + + textChildren.push(tspanElement); + } + + textElement.children = textChildren; + return textElement; +} diff --git a/plugins/plugins-types.d.ts b/plugins/plugins-types.d.ts index 8e3972c1c..3a114689d 100644 --- a/plugins/plugins-types.d.ts +++ b/plugins/plugins-types.d.ts @@ -136,6 +136,7 @@ type DefaultPlugins = { }; }; + mergeText:void; moveElemsAttrsToGroup: void; moveGroupAttrsToElems: void; removeComments: { diff --git a/test/plugins/mergeText.01.svg b/test/plugins/mergeText.01.svg new file mode 100644 index 000000000..8da31fbef --- /dev/null +++ b/test/plugins/mergeText.01.svg @@ -0,0 +1,18 @@ +Combine adjacent elements. + +=== + + + + Part one + + + Part two + + + +@@@ + + + Part onePart two + diff --git a/test/plugins/mergeText.02.svg b/test/plugins/mergeText.02.svg new file mode 100644 index 000000000..1ae4c47ee --- /dev/null +++ b/test/plugins/mergeText.02.svg @@ -0,0 +1,18 @@ +Combine adjacent elements. + +=== + + + + Part one + + + Part two + + + +@@@ + + + Part onePart two + diff --git a/test/plugins/mergeText.03.svg b/test/plugins/mergeText.03.svg new file mode 100644 index 000000000..63d6a3fcb --- /dev/null +++ b/test/plugins/mergeText.03.svg @@ -0,0 +1,21 @@ +Don't merge elements with text node children. + +=== + + + + Part one + + + xxxPart twoooo + + + +@@@ + + + Part one + + xxxPart twoooo + + diff --git a/test/plugins/mergeText.04.svg b/test/plugins/mergeText.04.svg new file mode 100644 index 000000000..76395f96d --- /dev/null +++ b/test/plugins/mergeText.04.svg @@ -0,0 +1,14 @@ +Collapse single child of + +=== + + + this is a test + + +@@@ + + + this is a test + diff --git a/test/plugins/mergeText.05.svg b/test/plugins/mergeText.05.svg new file mode 100644 index 000000000..f8a3d97ac --- /dev/null +++ b/test/plugins/mergeText.05.svg @@ -0,0 +1,13 @@ +Collapse single child of , where single has multiple children. + +=== + + + This is some text + + +@@@ + + + This is some text + diff --git a/test/plugins/mergeText.06.svg b/test/plugins/mergeText.06.svg new file mode 100644 index 000000000..92e8a9424 --- /dev/null +++ b/test/plugins/mergeText.06.svg @@ -0,0 +1,18 @@ +Do not collapse if styles are present. + +=== + + + this is a test + + + +@@@ + + + this is a test + + diff --git a/test/plugins/mergeText.07.svg b/test/plugins/mergeText.07.svg new file mode 100644 index 000000000..0b2284a19 --- /dev/null +++ b/test/plugins/mergeText.07.svg @@ -0,0 +1,20 @@ +Do not collapse if present. + +=== + + + + + this is a test + + + +@@@ + + + + + this is a test + + diff --git a/test/plugins/mergeText.08.svg b/test/plugins/mergeText.08.svg new file mode 100644 index 000000000..2feb9474f --- /dev/null +++ b/test/plugins/mergeText.08.svg @@ -0,0 +1,17 @@ +Don't merge elements with children. + +=== + +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 210 120"> + <text xml:space="preserve" x="45.869" y="38.606" fill="red" stroke-width=".265" font-family="Sans" font-size="6.35"> + <tspan x="45.869" y="38.606"><title>xxxPart one + + + +@@@ + + + + xxxPart one + + From 45a62884ba2e7cc3df6ef227c85d54adaf862f08 Mon Sep 17 00:00:00 2001 From: John Kenny Date: Fri, 1 Dec 2023 16:23:43 -0800 Subject: [PATCH 02/20] Added mergeText plugin to builtin.js. --- lib/builtin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/builtin.js b/lib/builtin.js index 28a380ec9..1ab975bef 100644 --- a/lib/builtin.js +++ b/lib/builtin.js @@ -20,6 +20,7 @@ exports.builtin = [ require('../plugins/mergeStyles.js'), require('../plugins/inlineStyles.js'), require('../plugins/mergePaths.js'), + require('../plugins/mergeText.js'), require('../plugins/minifyStyles.js'), require('../plugins/moveElemsAttrsToGroup.js'), require('../plugins/moveGroupAttrsToElems.js'), From c379a5f66cefd18dafe7b12704d50aeceb79c783 Mon Sep 17 00:00:00 2001 From: John Kenny Date: Sun, 24 Dec 2023 10:37:30 -0800 Subject: [PATCH 03/20] Changed to use inheritableAttrs from collections.js. --- plugins/mergeText.js | 441 +++++++++++++++++++++---------------------- 1 file changed, 216 insertions(+), 225 deletions(-) diff --git a/plugins/mergeText.js b/plugins/mergeText.js index c7d202ddc..bbe0766a0 100644 --- a/plugins/mergeText.js +++ b/plugins/mergeText.js @@ -1,5 +1,7 @@ 'use strict'; +const { inheritableAttrs } = require( './_collections' ); + /** * @typedef {import('../lib/types').XastElement} XastElement * @typedef {import('../lib/types').XastChild} XastChild @@ -35,62 +37,62 @@ exports.description = 'merges elements and children where possible'; */ exports.fn = () => { - let deoptimized = false; - - return { - element: { - enter: (node) => { - // Don't collapse if styles are present. - if (node.name === 'style' && node.children.length !== 0) { - deoptimized = true; - } - }, - - exit: (node) => { - if (deoptimized) { - return; - } - - // See if the node has children. - /** @type Map */ - const mergeableChildren = new Map(); - for (let index = 0; index < node.children.length; index++) { - const child = node.children[index]; - if (child.type === 'element' && child.name === 'text') { - const mergeData = getMergeData(child); - if (mergeData) { - mergeableChildren.set(index, mergeData); - } - } - } - - // If nothing to merge, return. - if (mergeableChildren.size === 0) { - return; - } - - // Create new child nodes. - /** @type XastChild[] */ - const newChildren = []; - for (let index = 0; index < node.children.length; index++) { - if (!mergeableChildren.has(index)) { - // This is not a element; do not process. - newChildren.push(node.children[index]); - continue; - } - if (mergeableChildren.has(index - 1)) { - // The previous child was a mergeable element; assume this one was already merged into it. - continue; - } - // Merge and insert text elements. - newChildren.push(mergeTextElements(mergeableChildren, index)); - } - - // Update children. - node.children = newChildren; - }, - }, - }; + let deoptimized = false; + + return { + element: { + enter: ( node ) => { + // Don't collapse if styles are present. + if ( node.name === 'style' && node.children.length !== 0 ) { + deoptimized = true; + } + }, + + exit: ( node ) => { + if ( deoptimized ) { + return; + } + + // See if the node has children. + /** @type Map */ + const mergeableChildren = new Map(); + for ( let index = 0; index < node.children.length; index++ ) { + const child = node.children[ index ]; + if ( child.type === 'element' && child.name === 'text' ) { + const mergeData = getMergeData( child ); + if ( mergeData ) { + mergeableChildren.set( index, mergeData ); + } + } + } + + // If nothing to merge, return. + if ( mergeableChildren.size === 0 ) { + return; + } + + // Create new child nodes. + /** @type XastChild[] */ + const newChildren = []; + for ( let index = 0; index < node.children.length; index++ ) { + if ( !mergeableChildren.has( index ) ) { + // This is not a element; do not process. + newChildren.push( node.children[ index ] ); + continue; + } + if ( mergeableChildren.has( index - 1 ) ) { + // The previous child was a mergeable element; assume this one was already merged into it. + continue; + } + // Merge and insert text elements. + newChildren.push( mergeTextElements( mergeableChildren, index ) ); + } + + // Update children. + node.children = newChildren; + }, + }, + }; }; /** @@ -98,94 +100,83 @@ exports.fn = () => { * @param {XastElement} textEl * @returns {any} */ -function getMergeData(textEl) { - /**@type TextData */ - const data = {}; - /**@type Map */ - const textAttributes = new Map(); - data.children = []; - - // Gather all attributes. - for (const [k, v] of Object.entries(textEl.attributes)) { - switch (k) { - case 'x': - case 'y': - data[k] = v; - break; - default: - textAttributes.set(k, v); - break; - } - } - - // Check all children of element. - for (const child of textEl.children) { - if (child.type === 'text') { - if (isWhiteSpace(child.value)) { - // Ignore nodes that are all whitespace. - continue; - } - } - if (child.type !== 'element' || child.name !== 'tspan') { - // Don't transform unless all children are elements. - return; - } - for (const tspanChild of child.children) { - // Don't transform unless has only text nodes and s as children. - if (tspanChild.type === 'text') { - continue; - } - if (tspanChild.type === 'element' && tspanChild.name === 'tspan') { - continue; - } - return; +function getMergeData( textEl ) { + /**@type TextData */ + const data = {}; + /**@type Map */ + const textAttributes = new Map(); + data.children = []; + + // Gather all attributes. + for ( const [ k, v ] of Object.entries( textEl.attributes ) ) { + switch ( k ) { + case 'x': + case 'y': + data[ k ] = v; + break; + default: + textAttributes.set( k, v ); + break; + } } - /** @type TspanData */ - const tspanData = {}; - - // Copy all attributes to the tspan data. - tspanData.attributes = new Map(textAttributes); - - // Merge all attributes. - for (const [k, v] of Object.entries(child.attributes)) { - switch (k) { - case 'x': - case 'y': - tspanData[k] = v; - break; - case 'color': - case 'fill': - case 'fill-opacity': - case 'fill-rule': - case 'opacity': - case 'stroke': - case 'stroke-dasharray': - case 'stroke-dashoffset': - case 'stroke-linecap': - case 'stroke-linejoin': - case 'stroke-miterlimit': - case 'stroke-opacity': - case 'stroke-width': - tspanData.attributes.set(k, v); - break; - default: - // Don't transform if has unrecognized attributes. - return; - } - } + // Check all children of element. + for ( const child of textEl.children ) { + if ( child.type === 'text' ) { + if ( isWhiteSpace( child.value ) ) { + // Ignore nodes that are all whitespace. + continue; + } + } + if ( child.type !== 'element' || child.name !== 'tspan' ) { + // Don't transform unless all children are elements. + return; + } + for ( const tspanChild of child.children ) { + // Don't transform unless has only text nodes and s as children. + if ( tspanChild.type === 'text' ) { + continue; + } + if ( tspanChild.type === 'element' && tspanChild.name === 'tspan' ) { + continue; + } + return; + } - // Don't transform unless all children have x and y attributes. - if (!tspanData.x || !tspanData.y) { - return; - } + /** @type TspanData */ + const tspanData = {}; + + // Copy all attributes to the tspan data. + tspanData.attributes = new Map( textAttributes ); + + // Merge all attributes. + for ( const [ k, v ] of Object.entries( child.attributes ) ) { + switch ( k ) { + case 'x': + case 'y': + tspanData[ k ] = v; + break; + default: + if ( !inheritableAttrs.includes( k ) ) { + // Don't transform if has unrecognized attributes. + return; + } + tspanData.attributes.set( k, v ); + break; + } + } - tspanData.children = child.children; + // Don't transform unless all children have x and y attributes. + if ( !tspanData.x || !tspanData.y ) { + return; + } - data.children.push(tspanData); - } + tspanData.children = child.children; - return data; + data.children.push( tspanData ); + } + + return data; } /** @@ -193,54 +184,54 @@ function getMergeData(textEl) { * @param {TspanData[]} tspans * @returns {{}} */ -function getTextAttributes(tspans) { - // Figure out what attributes and values we have. - const allAttributes = new Map(); - for (const tspanElement of tspans) { - for (const [attName, attValue] of tspanElement.attributes) { - let values = allAttributes.get(attName); - if (!values) { - values = new Map(); - allAttributes.set(attName, values); - } - if (!values.has(attValue)) { - values.set(attValue, 1); - } else { - values.set(attValue, values.get(attValue) + 1); - } - } - } - - // Figure out which ones to use as attributes. - /** @type Object. */ - const textAttributes = {}; - for (const [attName, values] of allAttributes) { - // Check all values. If there are fewer values than children, at least one child does not have the attribute; - // in this case leave attribute off of . Otherwise, set text attribute to the most-used value. - let total = 0; - let maxCount = 0; - let textAttValue; - for (const [value, count] of values) { - total += count; - if (count > maxCount) { - maxCount = count; - textAttValue = value; - } +function getTextAttributes( tspans ) { + // Figure out what attributes and values we have. + const allAttributes = new Map(); + for ( const tspanElement of tspans ) { + for ( const [ attName, attValue ] of tspanElement.attributes ) { + let values = allAttributes.get( attName ); + if ( !values ) { + values = new Map(); + allAttributes.set( attName, values ); + } + if ( !values.has( attValue ) ) { + values.set( attValue, 1 ); + } else { + values.set( attValue, values.get( attValue ) + 1 ); + } + } } - if (total === tspans.length && textAttValue) { - textAttributes[attName] = textAttValue; + + // Figure out which ones to use as attributes. + /** @type Object. */ + const textAttributes = {}; + for ( const [ attName, values ] of allAttributes ) { + // Check all values. If there are fewer values than children, at least one child does not have the attribute; + // in this case leave attribute off of . Otherwise, set text attribute to the most-used value. + let total = 0; + let maxCount = 0; + let textAttValue; + for ( const [ value, count ] of values ) { + total += count; + if ( count > maxCount ) { + maxCount = count; + textAttValue = value; + } + } + if ( total === tspans.length && textAttValue ) { + textAttributes[ attName ] = textAttValue; + } } - } - return textAttributes; + return textAttributes; } /** * @param {string} s * @returns {boolean} */ -function isWhiteSpace(s) { - return /^\s*$/.test(s); +function isWhiteSpace( s ) { + return /^\s*$/.test( s ); } /** @@ -249,58 +240,58 @@ function isWhiteSpace(s) { * @param {number} index * @returns XastElement */ -function mergeTextElements(mergeableChildren, index) { - /** @type {XastElement} */ - const textElement = { - type: 'element', - name: 'text', - attributes: {}, - children: [], - }; - - // Find all child data. - const tspans = []; - for (; ; index++) { - const textData = mergeableChildren.get(index); - if (!textData) { - break; - } - tspans.push(...textData.children); - } - - // Find the default attributes for the element. - textElement.attributes = getTextAttributes(tspans); - - // If there is only one , merge content into . - if (tspans.length === 1) { - const tspanData = tspans[0]; - textElement.attributes.x = tspanData.x; - textElement.attributes.y = tspanData.y; - textElement.children = tspanData.children; - return textElement; - } - - // Generate elements. - const textChildren = []; - for (const tspanData of tspans) { +function mergeTextElements( mergeableChildren, index ) { /** @type {XastElement} */ - const tspanElement = { - type: 'element', - name: 'tspan', - attributes: { x: tspanData.x, y: tspanData.y }, - children: tspanData.children, + const textElement = { + type: 'element', + name: 'text', + attributes: {}, + children: [], }; - // Add any attributes that are different from attributes. - for (const [k, v] of tspanData.attributes) { - if (textElement.attributes[k] !== v) { - tspanElement.attributes[k] = v; - } + // Find all child data. + const tspans = []; + for ( ; ; index++ ) { + const textData = mergeableChildren.get( index ); + if ( !textData ) { + break; + } + tspans.push( ...textData.children ); + } + + // Find the default attributes for the element. + textElement.attributes = getTextAttributes( tspans ); + + // If there is only one , merge content into . + if ( tspans.length === 1 ) { + const tspanData = tspans[ 0 ]; + textElement.attributes.x = tspanData.x; + textElement.attributes.y = tspanData.y; + textElement.children = tspanData.children; + return textElement; } - textChildren.push(tspanElement); - } + // Generate elements. + const textChildren = []; + for ( const tspanData of tspans ) { + /** @type {XastElement} */ + const tspanElement = { + type: 'element', + name: 'tspan', + attributes: { x: tspanData.x, y: tspanData.y }, + children: tspanData.children, + }; + + // Add any attributes that are different from attributes. + for ( const [ k, v ] of tspanData.attributes ) { + if ( textElement.attributes[ k ] !== v ) { + tspanElement.attributes[ k ] = v; + } + } + + textChildren.push( tspanElement ); + } - textElement.children = textChildren; - return textElement; + textElement.children = textChildren; + return textElement; } From f72ff44698f7af95b66bfc6aa6b2e393fb1a22ab Mon Sep 17 00:00:00 2001 From: John Kenny Date: Mon, 25 Dec 2023 08:34:10 -0800 Subject: [PATCH 04/20] Fixed formatting. --- plugins/mergeText.js | 428 ++++++++++++++++++------------------- plugins/plugins-types.d.ts | 2 +- 2 files changed, 215 insertions(+), 215 deletions(-) diff --git a/plugins/mergeText.js b/plugins/mergeText.js index bbe0766a0..9af46fde5 100644 --- a/plugins/mergeText.js +++ b/plugins/mergeText.js @@ -1,6 +1,6 @@ 'use strict'; -const { inheritableAttrs } = require( './_collections' ); +const { inheritableAttrs } = require('./_collections'); /** * @typedef {import('../lib/types').XastElement} XastElement @@ -37,62 +37,62 @@ exports.description = 'merges elements and children where possible'; */ exports.fn = () => { - let deoptimized = false; - - return { - element: { - enter: ( node ) => { - // Don't collapse if styles are present. - if ( node.name === 'style' && node.children.length !== 0 ) { - deoptimized = true; - } - }, - - exit: ( node ) => { - if ( deoptimized ) { - return; - } - - // See if the node has children. - /** @type Map */ - const mergeableChildren = new Map(); - for ( let index = 0; index < node.children.length; index++ ) { - const child = node.children[ index ]; - if ( child.type === 'element' && child.name === 'text' ) { - const mergeData = getMergeData( child ); - if ( mergeData ) { - mergeableChildren.set( index, mergeData ); - } - } - } - - // If nothing to merge, return. - if ( mergeableChildren.size === 0 ) { - return; - } - - // Create new child nodes. - /** @type XastChild[] */ - const newChildren = []; - for ( let index = 0; index < node.children.length; index++ ) { - if ( !mergeableChildren.has( index ) ) { - // This is not a element; do not process. - newChildren.push( node.children[ index ] ); - continue; - } - if ( mergeableChildren.has( index - 1 ) ) { - // The previous child was a mergeable element; assume this one was already merged into it. - continue; - } - // Merge and insert text elements. - newChildren.push( mergeTextElements( mergeableChildren, index ) ); - } - - // Update children. - node.children = newChildren; - }, - }, - }; + let deoptimized = false; + + return { + element: { + enter: (node) => { + // Don't collapse if styles are present. + if (node.name === 'style' && node.children.length !== 0) { + deoptimized = true; + } + }, + + exit: (node) => { + if (deoptimized) { + return; + } + + // See if the node has children. + /** @type Map */ + const mergeableChildren = new Map(); + for (let index = 0; index < node.children.length; index++) { + const child = node.children[index]; + if (child.type === 'element' && child.name === 'text') { + const mergeData = getMergeData(child); + if (mergeData) { + mergeableChildren.set(index, mergeData); + } + } + } + + // If nothing to merge, return. + if (mergeableChildren.size === 0) { + return; + } + + // Create new child nodes. + /** @type XastChild[] */ + const newChildren = []; + for (let index = 0; index < node.children.length; index++) { + if (!mergeableChildren.has(index)) { + // This is not a element; do not process. + newChildren.push(node.children[index]); + continue; + } + if (mergeableChildren.has(index - 1)) { + // The previous child was a mergeable element; assume this one was already merged into it. + continue; + } + // Merge and insert text elements. + newChildren.push(mergeTextElements(mergeableChildren, index)); + } + + // Update children. + node.children = newChildren; + }, + }, + }; }; /** @@ -100,83 +100,83 @@ exports.fn = () => { * @param {XastElement} textEl * @returns {any} */ -function getMergeData( textEl ) { - /**@type TextData */ - const data = {}; - /**@type Map */ - const textAttributes = new Map(); - data.children = []; - - // Gather all attributes. - for ( const [ k, v ] of Object.entries( textEl.attributes ) ) { - switch ( k ) { - case 'x': - case 'y': - data[ k ] = v; - break; - default: - textAttributes.set( k, v ); - break; - } +function getMergeData(textEl) { + /**@type TextData */ + const data = {}; + /**@type Map */ + const textAttributes = new Map(); + data.children = []; + + // Gather all attributes. + for (const [k, v] of Object.entries(textEl.attributes)) { + switch (k) { + case 'x': + case 'y': + data[k] = v; + break; + default: + textAttributes.set(k, v); + break; + } + } + + // Check all children of element. + for (const child of textEl.children) { + if (child.type === 'text') { + if (isWhiteSpace(child.value)) { + // Ignore nodes that are all whitespace. + continue; + } + } + if (child.type !== 'element' || child.name !== 'tspan') { + // Don't transform unless all children are elements. + return; + } + for (const tspanChild of child.children) { + // Don't transform unless has only text nodes and s as children. + if (tspanChild.type === 'text') { + continue; + } + if (tspanChild.type === 'element' && tspanChild.name === 'tspan') { + continue; + } + return; } - // Check all children of element. - for ( const child of textEl.children ) { - if ( child.type === 'text' ) { - if ( isWhiteSpace( child.value ) ) { - // Ignore nodes that are all whitespace. - continue; - } - } - if ( child.type !== 'element' || child.name !== 'tspan' ) { - // Don't transform unless all children are elements. + /** @type TspanData */ + const tspanData = {}; + + // Copy all attributes to the tspan data. + tspanData.attributes = new Map(textAttributes); + + // Merge all attributes. + for (const [k, v] of Object.entries(child.attributes)) { + switch (k) { + case 'x': + case 'y': + tspanData[k] = v; + break; + default: + if (!inheritableAttrs.includes(k)) { + // Don't transform if has unrecognized attributes. return; - } - for ( const tspanChild of child.children ) { - // Don't transform unless has only text nodes and s as children. - if ( tspanChild.type === 'text' ) { - continue; - } - if ( tspanChild.type === 'element' && tspanChild.name === 'tspan' ) { - continue; - } - return; - } - - /** @type TspanData */ - const tspanData = {}; - - // Copy all attributes to the tspan data. - tspanData.attributes = new Map( textAttributes ); - - // Merge all attributes. - for ( const [ k, v ] of Object.entries( child.attributes ) ) { - switch ( k ) { - case 'x': - case 'y': - tspanData[ k ] = v; - break; - default: - if ( !inheritableAttrs.includes( k ) ) { - // Don't transform if has unrecognized attributes. - return; - } - tspanData.attributes.set( k, v ); - break; - } - } + } + tspanData.attributes.set(k, v); + break; + } + } - // Don't transform unless all children have x and y attributes. - if ( !tspanData.x || !tspanData.y ) { - return; - } + // Don't transform unless all children have x and y attributes. + if (!tspanData.x || !tspanData.y) { + return; + } - tspanData.children = child.children; + tspanData.children = child.children; - data.children.push( tspanData ); - } + data.children.push(tspanData); + } - return data; + return data; } /** @@ -184,54 +184,54 @@ function getMergeData( textEl ) { * @param {TspanData[]} tspans * @returns {{}} */ -function getTextAttributes( tspans ) { - // Figure out what attributes and values we have. - const allAttributes = new Map(); - for ( const tspanElement of tspans ) { - for ( const [ attName, attValue ] of tspanElement.attributes ) { - let values = allAttributes.get( attName ); - if ( !values ) { - values = new Map(); - allAttributes.set( attName, values ); - } - if ( !values.has( attValue ) ) { - values.set( attValue, 1 ); - } else { - values.set( attValue, values.get( attValue ) + 1 ); - } - } +function getTextAttributes(tspans) { + // Figure out what attributes and values we have. + const allAttributes = new Map(); + for (const tspanElement of tspans) { + for (const [attName, attValue] of tspanElement.attributes) { + let values = allAttributes.get(attName); + if (!values) { + values = new Map(); + allAttributes.set(attName, values); + } + if (!values.has(attValue)) { + values.set(attValue, 1); + } else { + values.set(attValue, values.get(attValue) + 1); + } } - - // Figure out which ones to use as attributes. - /** @type Object. */ - const textAttributes = {}; - for ( const [ attName, values ] of allAttributes ) { - // Check all values. If there are fewer values than children, at least one child does not have the attribute; - // in this case leave attribute off of . Otherwise, set text attribute to the most-used value. - let total = 0; - let maxCount = 0; - let textAttValue; - for ( const [ value, count ] of values ) { - total += count; - if ( count > maxCount ) { - maxCount = count; - textAttValue = value; - } - } - if ( total === tspans.length && textAttValue ) { - textAttributes[ attName ] = textAttValue; - } + } + + // Figure out which ones to use as attributes. + /** @type Object. */ + const textAttributes = {}; + for (const [attName, values] of allAttributes) { + // Check all values. If there are fewer values than children, at least one child does not have the attribute; + // in this case leave attribute off of . Otherwise, set text attribute to the most-used value. + let total = 0; + let maxCount = 0; + let textAttValue; + for (const [value, count] of values) { + total += count; + if (count > maxCount) { + maxCount = count; + textAttValue = value; + } + } + if (total === tspans.length && textAttValue) { + textAttributes[attName] = textAttValue; } + } - return textAttributes; + return textAttributes; } /** * @param {string} s * @returns {boolean} */ -function isWhiteSpace( s ) { - return /^\s*$/.test( s ); +function isWhiteSpace(s) { + return /^\s*$/.test(s); } /** @@ -240,58 +240,58 @@ function isWhiteSpace( s ) { * @param {number} index * @returns XastElement */ -function mergeTextElements( mergeableChildren, index ) { - /** @type {XastElement} */ - const textElement = { - type: 'element', - name: 'text', - attributes: {}, - children: [], - }; - - // Find all child data. - const tspans = []; - for ( ; ; index++ ) { - const textData = mergeableChildren.get( index ); - if ( !textData ) { - break; - } - tspans.push( ...textData.children ); +function mergeTextElements(mergeableChildren, index) { + /** @type {XastElement} */ + const textElement = { + type: 'element', + name: 'text', + attributes: {}, + children: [], + }; + + // Find all child data. + const tspans = []; + for (; ; index++) { + const textData = mergeableChildren.get(index); + if (!textData) { + break; } + tspans.push(...textData.children); + } + + // Find the default attributes for the element. + textElement.attributes = getTextAttributes(tspans); + + // If there is only one , merge content into . + if (tspans.length === 1) { + const tspanData = tspans[0]; + textElement.attributes.x = tspanData.x; + textElement.attributes.y = tspanData.y; + textElement.children = tspanData.children; + return textElement; + } - // Find the default attributes for the element. - textElement.attributes = getTextAttributes( tspans ); + // Generate elements. + const textChildren = []; + for (const tspanData of tspans) { + /** @type {XastElement} */ + const tspanElement = { + type: 'element', + name: 'tspan', + attributes: { x: tspanData.x, y: tspanData.y }, + children: tspanData.children, + }; - // If there is only one , merge content into . - if ( tspans.length === 1 ) { - const tspanData = tspans[ 0 ]; - textElement.attributes.x = tspanData.x; - textElement.attributes.y = tspanData.y; - textElement.children = tspanData.children; - return textElement; + // Add any attributes that are different from attributes. + for (const [k, v] of tspanData.attributes) { + if (textElement.attributes[k] !== v) { + tspanElement.attributes[k] = v; + } } - // Generate elements. - const textChildren = []; - for ( const tspanData of tspans ) { - /** @type {XastElement} */ - const tspanElement = { - type: 'element', - name: 'tspan', - attributes: { x: tspanData.x, y: tspanData.y }, - children: tspanData.children, - }; - - // Add any attributes that are different from attributes. - for ( const [ k, v ] of tspanData.attributes ) { - if ( textElement.attributes[ k ] !== v ) { - tspanElement.attributes[ k ] = v; - } - } - - textChildren.push( tspanElement ); - } + textChildren.push(tspanElement); + } - textElement.children = textChildren; - return textElement; + textElement.children = textChildren; + return textElement; } diff --git a/plugins/plugins-types.d.ts b/plugins/plugins-types.d.ts index f789fa7a6..caadce489 100644 --- a/plugins/plugins-types.d.ts +++ b/plugins/plugins-types.d.ts @@ -136,7 +136,7 @@ type DefaultPlugins = { }; }; - mergeText:void; + mergeText: void; moveElemsAttrsToGroup: void; moveGroupAttrsToElems: void; removeComments: { From 2cccb0e73be4cf123b6d24587ca0cc4e23d0fdb4 Mon Sep 17 00:00:00 2001 From: John Kenny Date: Mon, 25 Dec 2023 15:39:03 -0800 Subject: [PATCH 05/20] Added mergeText documentation. --- docs/03-plugins/merge-text.mdx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/03-plugins/merge-text.mdx diff --git a/docs/03-plugins/merge-text.mdx b/docs/03-plugins/merge-text.mdx new file mode 100644 index 000000000..38b19d2ae --- /dev/null +++ b/docs/03-plugins/merge-text.mdx @@ -0,0 +1,15 @@ +--- +title: Merge Text +svgo: + pluginId: mergeText +--- + +Merge adjacent `` elements and remove `` when it is the only child of a `` element. + +If the SVG contains any ` + + +@@@ + + + this is a test + + diff --git a/test/plugins/mergeText.07.svg b/test/plugins/mergeText.07.svg new file mode 100644 index 000000000..0b2284a19 --- /dev/null +++ b/test/plugins/mergeText.07.svg @@ -0,0 +1,20 @@ +Do not collapse if present. + +=== + + + + + this is a test + + + +@@@ + + + + + this is a test + + diff --git a/test/plugins/mergeText.08.svg b/test/plugins/mergeText.08.svg new file mode 100644 index 000000000..2feb9474f --- /dev/null +++ b/test/plugins/mergeText.08.svg @@ -0,0 +1,17 @@ +Don't merge elements with children. + +=== + +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 210 120"> + <text xml:space="preserve" x="45.869" y="38.606" fill="red" stroke-width=".265" font-family="Sans" font-size="6.35"> + <tspan x="45.869" y="38.606"><title>xxxPart one + + + +@@@ + + + + xxxPart one + + From a94c45aeb70f103f274f81df4a43520e60f8f1d7 Mon Sep 17 00:00:00 2001 From: John Kenny Date: Fri, 1 Dec 2023 16:23:43 -0800 Subject: [PATCH 16/20] Added mergeText plugin to builtin.js. --- lib/builtin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/builtin.js b/lib/builtin.js index 28a380ec9..1ab975bef 100644 --- a/lib/builtin.js +++ b/lib/builtin.js @@ -20,6 +20,7 @@ exports.builtin = [ require('../plugins/mergeStyles.js'), require('../plugins/inlineStyles.js'), require('../plugins/mergePaths.js'), + require('../plugins/mergeText.js'), require('../plugins/minifyStyles.js'), require('../plugins/moveElemsAttrsToGroup.js'), require('../plugins/moveGroupAttrsToElems.js'), From 65c4d7f6c87ee5607b69039f95df87ce4612673b Mon Sep 17 00:00:00 2001 From: John Kenny Date: Sun, 24 Dec 2023 10:37:30 -0800 Subject: [PATCH 17/20] Changed to use inheritableAttrs from collections.js. --- plugins/mergeText.js | 441 +++++++++++++++++++++---------------------- 1 file changed, 216 insertions(+), 225 deletions(-) diff --git a/plugins/mergeText.js b/plugins/mergeText.js index c7d202ddc..bbe0766a0 100644 --- a/plugins/mergeText.js +++ b/plugins/mergeText.js @@ -1,5 +1,7 @@ 'use strict'; +const { inheritableAttrs } = require( './_collections' ); + /** * @typedef {import('../lib/types').XastElement} XastElement * @typedef {import('../lib/types').XastChild} XastChild @@ -35,62 +37,62 @@ exports.description = 'merges elements and children where possible'; */ exports.fn = () => { - let deoptimized = false; - - return { - element: { - enter: (node) => { - // Don't collapse if styles are present. - if (node.name === 'style' && node.children.length !== 0) { - deoptimized = true; - } - }, - - exit: (node) => { - if (deoptimized) { - return; - } - - // See if the node has children. - /** @type Map */ - const mergeableChildren = new Map(); - for (let index = 0; index < node.children.length; index++) { - const child = node.children[index]; - if (child.type === 'element' && child.name === 'text') { - const mergeData = getMergeData(child); - if (mergeData) { - mergeableChildren.set(index, mergeData); - } - } - } - - // If nothing to merge, return. - if (mergeableChildren.size === 0) { - return; - } - - // Create new child nodes. - /** @type XastChild[] */ - const newChildren = []; - for (let index = 0; index < node.children.length; index++) { - if (!mergeableChildren.has(index)) { - // This is not a element; do not process. - newChildren.push(node.children[index]); - continue; - } - if (mergeableChildren.has(index - 1)) { - // The previous child was a mergeable element; assume this one was already merged into it. - continue; - } - // Merge and insert text elements. - newChildren.push(mergeTextElements(mergeableChildren, index)); - } - - // Update children. - node.children = newChildren; - }, - }, - }; + let deoptimized = false; + + return { + element: { + enter: ( node ) => { + // Don't collapse if styles are present. + if ( node.name === 'style' && node.children.length !== 0 ) { + deoptimized = true; + } + }, + + exit: ( node ) => { + if ( deoptimized ) { + return; + } + + // See if the node has children. + /** @type Map */ + const mergeableChildren = new Map(); + for ( let index = 0; index < node.children.length; index++ ) { + const child = node.children[ index ]; + if ( child.type === 'element' && child.name === 'text' ) { + const mergeData = getMergeData( child ); + if ( mergeData ) { + mergeableChildren.set( index, mergeData ); + } + } + } + + // If nothing to merge, return. + if ( mergeableChildren.size === 0 ) { + return; + } + + // Create new child nodes. + /** @type XastChild[] */ + const newChildren = []; + for ( let index = 0; index < node.children.length; index++ ) { + if ( !mergeableChildren.has( index ) ) { + // This is not a element; do not process. + newChildren.push( node.children[ index ] ); + continue; + } + if ( mergeableChildren.has( index - 1 ) ) { + // The previous child was a mergeable element; assume this one was already merged into it. + continue; + } + // Merge and insert text elements. + newChildren.push( mergeTextElements( mergeableChildren, index ) ); + } + + // Update children. + node.children = newChildren; + }, + }, + }; }; /** @@ -98,94 +100,83 @@ exports.fn = () => { * @param {XastElement} textEl * @returns {any} */ -function getMergeData(textEl) { - /**@type TextData */ - const data = {}; - /**@type Map */ - const textAttributes = new Map(); - data.children = []; - - // Gather all attributes. - for (const [k, v] of Object.entries(textEl.attributes)) { - switch (k) { - case 'x': - case 'y': - data[k] = v; - break; - default: - textAttributes.set(k, v); - break; - } - } - - // Check all children of element. - for (const child of textEl.children) { - if (child.type === 'text') { - if (isWhiteSpace(child.value)) { - // Ignore nodes that are all whitespace. - continue; - } - } - if (child.type !== 'element' || child.name !== 'tspan') { - // Don't transform unless all children are elements. - return; - } - for (const tspanChild of child.children) { - // Don't transform unless has only text nodes and s as children. - if (tspanChild.type === 'text') { - continue; - } - if (tspanChild.type === 'element' && tspanChild.name === 'tspan') { - continue; - } - return; +function getMergeData( textEl ) { + /**@type TextData */ + const data = {}; + /**@type Map */ + const textAttributes = new Map(); + data.children = []; + + // Gather all attributes. + for ( const [ k, v ] of Object.entries( textEl.attributes ) ) { + switch ( k ) { + case 'x': + case 'y': + data[ k ] = v; + break; + default: + textAttributes.set( k, v ); + break; + } } - /** @type TspanData */ - const tspanData = {}; - - // Copy all attributes to the tspan data. - tspanData.attributes = new Map(textAttributes); - - // Merge all attributes. - for (const [k, v] of Object.entries(child.attributes)) { - switch (k) { - case 'x': - case 'y': - tspanData[k] = v; - break; - case 'color': - case 'fill': - case 'fill-opacity': - case 'fill-rule': - case 'opacity': - case 'stroke': - case 'stroke-dasharray': - case 'stroke-dashoffset': - case 'stroke-linecap': - case 'stroke-linejoin': - case 'stroke-miterlimit': - case 'stroke-opacity': - case 'stroke-width': - tspanData.attributes.set(k, v); - break; - default: - // Don't transform if has unrecognized attributes. - return; - } - } + // Check all children of element. + for ( const child of textEl.children ) { + if ( child.type === 'text' ) { + if ( isWhiteSpace( child.value ) ) { + // Ignore nodes that are all whitespace. + continue; + } + } + if ( child.type !== 'element' || child.name !== 'tspan' ) { + // Don't transform unless all children are elements. + return; + } + for ( const tspanChild of child.children ) { + // Don't transform unless has only text nodes and s as children. + if ( tspanChild.type === 'text' ) { + continue; + } + if ( tspanChild.type === 'element' && tspanChild.name === 'tspan' ) { + continue; + } + return; + } - // Don't transform unless all children have x and y attributes. - if (!tspanData.x || !tspanData.y) { - return; - } + /** @type TspanData */ + const tspanData = {}; + + // Copy all attributes to the tspan data. + tspanData.attributes = new Map( textAttributes ); + + // Merge all attributes. + for ( const [ k, v ] of Object.entries( child.attributes ) ) { + switch ( k ) { + case 'x': + case 'y': + tspanData[ k ] = v; + break; + default: + if ( !inheritableAttrs.includes( k ) ) { + // Don't transform if has unrecognized attributes. + return; + } + tspanData.attributes.set( k, v ); + break; + } + } - tspanData.children = child.children; + // Don't transform unless all children have x and y attributes. + if ( !tspanData.x || !tspanData.y ) { + return; + } - data.children.push(tspanData); - } + tspanData.children = child.children; - return data; + data.children.push( tspanData ); + } + + return data; } /** @@ -193,54 +184,54 @@ function getMergeData(textEl) { * @param {TspanData[]} tspans * @returns {{}} */ -function getTextAttributes(tspans) { - // Figure out what attributes and values we have. - const allAttributes = new Map(); - for (const tspanElement of tspans) { - for (const [attName, attValue] of tspanElement.attributes) { - let values = allAttributes.get(attName); - if (!values) { - values = new Map(); - allAttributes.set(attName, values); - } - if (!values.has(attValue)) { - values.set(attValue, 1); - } else { - values.set(attValue, values.get(attValue) + 1); - } - } - } - - // Figure out which ones to use as attributes. - /** @type Object. */ - const textAttributes = {}; - for (const [attName, values] of allAttributes) { - // Check all values. If there are fewer values than children, at least one child does not have the attribute; - // in this case leave attribute off of . Otherwise, set text attribute to the most-used value. - let total = 0; - let maxCount = 0; - let textAttValue; - for (const [value, count] of values) { - total += count; - if (count > maxCount) { - maxCount = count; - textAttValue = value; - } +function getTextAttributes( tspans ) { + // Figure out what attributes and values we have. + const allAttributes = new Map(); + for ( const tspanElement of tspans ) { + for ( const [ attName, attValue ] of tspanElement.attributes ) { + let values = allAttributes.get( attName ); + if ( !values ) { + values = new Map(); + allAttributes.set( attName, values ); + } + if ( !values.has( attValue ) ) { + values.set( attValue, 1 ); + } else { + values.set( attValue, values.get( attValue ) + 1 ); + } + } } - if (total === tspans.length && textAttValue) { - textAttributes[attName] = textAttValue; + + // Figure out which ones to use as attributes. + /** @type Object. */ + const textAttributes = {}; + for ( const [ attName, values ] of allAttributes ) { + // Check all values. If there are fewer values than children, at least one child does not have the attribute; + // in this case leave attribute off of . Otherwise, set text attribute to the most-used value. + let total = 0; + let maxCount = 0; + let textAttValue; + for ( const [ value, count ] of values ) { + total += count; + if ( count > maxCount ) { + maxCount = count; + textAttValue = value; + } + } + if ( total === tspans.length && textAttValue ) { + textAttributes[ attName ] = textAttValue; + } } - } - return textAttributes; + return textAttributes; } /** * @param {string} s * @returns {boolean} */ -function isWhiteSpace(s) { - return /^\s*$/.test(s); +function isWhiteSpace( s ) { + return /^\s*$/.test( s ); } /** @@ -249,58 +240,58 @@ function isWhiteSpace(s) { * @param {number} index * @returns XastElement */ -function mergeTextElements(mergeableChildren, index) { - /** @type {XastElement} */ - const textElement = { - type: 'element', - name: 'text', - attributes: {}, - children: [], - }; - - // Find all child data. - const tspans = []; - for (; ; index++) { - const textData = mergeableChildren.get(index); - if (!textData) { - break; - } - tspans.push(...textData.children); - } - - // Find the default attributes for the element. - textElement.attributes = getTextAttributes(tspans); - - // If there is only one , merge content into . - if (tspans.length === 1) { - const tspanData = tspans[0]; - textElement.attributes.x = tspanData.x; - textElement.attributes.y = tspanData.y; - textElement.children = tspanData.children; - return textElement; - } - - // Generate elements. - const textChildren = []; - for (const tspanData of tspans) { +function mergeTextElements( mergeableChildren, index ) { /** @type {XastElement} */ - const tspanElement = { - type: 'element', - name: 'tspan', - attributes: { x: tspanData.x, y: tspanData.y }, - children: tspanData.children, + const textElement = { + type: 'element', + name: 'text', + attributes: {}, + children: [], }; - // Add any attributes that are different from attributes. - for (const [k, v] of tspanData.attributes) { - if (textElement.attributes[k] !== v) { - tspanElement.attributes[k] = v; - } + // Find all child data. + const tspans = []; + for ( ; ; index++ ) { + const textData = mergeableChildren.get( index ); + if ( !textData ) { + break; + } + tspans.push( ...textData.children ); + } + + // Find the default attributes for the element. + textElement.attributes = getTextAttributes( tspans ); + + // If there is only one , merge content into . + if ( tspans.length === 1 ) { + const tspanData = tspans[ 0 ]; + textElement.attributes.x = tspanData.x; + textElement.attributes.y = tspanData.y; + textElement.children = tspanData.children; + return textElement; } - textChildren.push(tspanElement); - } + // Generate elements. + const textChildren = []; + for ( const tspanData of tspans ) { + /** @type {XastElement} */ + const tspanElement = { + type: 'element', + name: 'tspan', + attributes: { x: tspanData.x, y: tspanData.y }, + children: tspanData.children, + }; + + // Add any attributes that are different from attributes. + for ( const [ k, v ] of tspanData.attributes ) { + if ( textElement.attributes[ k ] !== v ) { + tspanElement.attributes[ k ] = v; + } + } + + textChildren.push( tspanElement ); + } - textElement.children = textChildren; - return textElement; + textElement.children = textChildren; + return textElement; } From 5de49974d5a027c9d35a8dbfae33ead1374731fa Mon Sep 17 00:00:00 2001 From: John Kenny Date: Mon, 25 Dec 2023 08:34:10 -0800 Subject: [PATCH 18/20] Fixed formatting. --- plugins/mergeText.js | 428 ++++++++++++++++++------------------- plugins/plugins-types.d.ts | 2 +- 2 files changed, 215 insertions(+), 215 deletions(-) diff --git a/plugins/mergeText.js b/plugins/mergeText.js index bbe0766a0..9af46fde5 100644 --- a/plugins/mergeText.js +++ b/plugins/mergeText.js @@ -1,6 +1,6 @@ 'use strict'; -const { inheritableAttrs } = require( './_collections' ); +const { inheritableAttrs } = require('./_collections'); /** * @typedef {import('../lib/types').XastElement} XastElement @@ -37,62 +37,62 @@ exports.description = 'merges elements and children where possible'; */ exports.fn = () => { - let deoptimized = false; - - return { - element: { - enter: ( node ) => { - // Don't collapse if styles are present. - if ( node.name === 'style' && node.children.length !== 0 ) { - deoptimized = true; - } - }, - - exit: ( node ) => { - if ( deoptimized ) { - return; - } - - // See if the node has children. - /** @type Map */ - const mergeableChildren = new Map(); - for ( let index = 0; index < node.children.length; index++ ) { - const child = node.children[ index ]; - if ( child.type === 'element' && child.name === 'text' ) { - const mergeData = getMergeData( child ); - if ( mergeData ) { - mergeableChildren.set( index, mergeData ); - } - } - } - - // If nothing to merge, return. - if ( mergeableChildren.size === 0 ) { - return; - } - - // Create new child nodes. - /** @type XastChild[] */ - const newChildren = []; - for ( let index = 0; index < node.children.length; index++ ) { - if ( !mergeableChildren.has( index ) ) { - // This is not a element; do not process. - newChildren.push( node.children[ index ] ); - continue; - } - if ( mergeableChildren.has( index - 1 ) ) { - // The previous child was a mergeable element; assume this one was already merged into it. - continue; - } - // Merge and insert text elements. - newChildren.push( mergeTextElements( mergeableChildren, index ) ); - } - - // Update children. - node.children = newChildren; - }, - }, - }; + let deoptimized = false; + + return { + element: { + enter: (node) => { + // Don't collapse if styles are present. + if (node.name === 'style' && node.children.length !== 0) { + deoptimized = true; + } + }, + + exit: (node) => { + if (deoptimized) { + return; + } + + // See if the node has children. + /** @type Map */ + const mergeableChildren = new Map(); + for (let index = 0; index < node.children.length; index++) { + const child = node.children[index]; + if (child.type === 'element' && child.name === 'text') { + const mergeData = getMergeData(child); + if (mergeData) { + mergeableChildren.set(index, mergeData); + } + } + } + + // If nothing to merge, return. + if (mergeableChildren.size === 0) { + return; + } + + // Create new child nodes. + /** @type XastChild[] */ + const newChildren = []; + for (let index = 0; index < node.children.length; index++) { + if (!mergeableChildren.has(index)) { + // This is not a element; do not process. + newChildren.push(node.children[index]); + continue; + } + if (mergeableChildren.has(index - 1)) { + // The previous child was a mergeable element; assume this one was already merged into it. + continue; + } + // Merge and insert text elements. + newChildren.push(mergeTextElements(mergeableChildren, index)); + } + + // Update children. + node.children = newChildren; + }, + }, + }; }; /** @@ -100,83 +100,83 @@ exports.fn = () => { * @param {XastElement} textEl * @returns {any} */ -function getMergeData( textEl ) { - /**@type TextData */ - const data = {}; - /**@type Map */ - const textAttributes = new Map(); - data.children = []; - - // Gather all attributes. - for ( const [ k, v ] of Object.entries( textEl.attributes ) ) { - switch ( k ) { - case 'x': - case 'y': - data[ k ] = v; - break; - default: - textAttributes.set( k, v ); - break; - } +function getMergeData(textEl) { + /**@type TextData */ + const data = {}; + /**@type Map */ + const textAttributes = new Map(); + data.children = []; + + // Gather all attributes. + for (const [k, v] of Object.entries(textEl.attributes)) { + switch (k) { + case 'x': + case 'y': + data[k] = v; + break; + default: + textAttributes.set(k, v); + break; + } + } + + // Check all children of element. + for (const child of textEl.children) { + if (child.type === 'text') { + if (isWhiteSpace(child.value)) { + // Ignore nodes that are all whitespace. + continue; + } + } + if (child.type !== 'element' || child.name !== 'tspan') { + // Don't transform unless all children are elements. + return; + } + for (const tspanChild of child.children) { + // Don't transform unless has only text nodes and s as children. + if (tspanChild.type === 'text') { + continue; + } + if (tspanChild.type === 'element' && tspanChild.name === 'tspan') { + continue; + } + return; } - // Check all children of element. - for ( const child of textEl.children ) { - if ( child.type === 'text' ) { - if ( isWhiteSpace( child.value ) ) { - // Ignore nodes that are all whitespace. - continue; - } - } - if ( child.type !== 'element' || child.name !== 'tspan' ) { - // Don't transform unless all children are elements. + /** @type TspanData */ + const tspanData = {}; + + // Copy all attributes to the tspan data. + tspanData.attributes = new Map(textAttributes); + + // Merge all attributes. + for (const [k, v] of Object.entries(child.attributes)) { + switch (k) { + case 'x': + case 'y': + tspanData[k] = v; + break; + default: + if (!inheritableAttrs.includes(k)) { + // Don't transform if has unrecognized attributes. return; - } - for ( const tspanChild of child.children ) { - // Don't transform unless has only text nodes and s as children. - if ( tspanChild.type === 'text' ) { - continue; - } - if ( tspanChild.type === 'element' && tspanChild.name === 'tspan' ) { - continue; - } - return; - } - - /** @type TspanData */ - const tspanData = {}; - - // Copy all attributes to the tspan data. - tspanData.attributes = new Map( textAttributes ); - - // Merge all attributes. - for ( const [ k, v ] of Object.entries( child.attributes ) ) { - switch ( k ) { - case 'x': - case 'y': - tspanData[ k ] = v; - break; - default: - if ( !inheritableAttrs.includes( k ) ) { - // Don't transform if has unrecognized attributes. - return; - } - tspanData.attributes.set( k, v ); - break; - } - } + } + tspanData.attributes.set(k, v); + break; + } + } - // Don't transform unless all children have x and y attributes. - if ( !tspanData.x || !tspanData.y ) { - return; - } + // Don't transform unless all children have x and y attributes. + if (!tspanData.x || !tspanData.y) { + return; + } - tspanData.children = child.children; + tspanData.children = child.children; - data.children.push( tspanData ); - } + data.children.push(tspanData); + } - return data; + return data; } /** @@ -184,54 +184,54 @@ function getMergeData( textEl ) { * @param {TspanData[]} tspans * @returns {{}} */ -function getTextAttributes( tspans ) { - // Figure out what attributes and values we have. - const allAttributes = new Map(); - for ( const tspanElement of tspans ) { - for ( const [ attName, attValue ] of tspanElement.attributes ) { - let values = allAttributes.get( attName ); - if ( !values ) { - values = new Map(); - allAttributes.set( attName, values ); - } - if ( !values.has( attValue ) ) { - values.set( attValue, 1 ); - } else { - values.set( attValue, values.get( attValue ) + 1 ); - } - } +function getTextAttributes(tspans) { + // Figure out what attributes and values we have. + const allAttributes = new Map(); + for (const tspanElement of tspans) { + for (const [attName, attValue] of tspanElement.attributes) { + let values = allAttributes.get(attName); + if (!values) { + values = new Map(); + allAttributes.set(attName, values); + } + if (!values.has(attValue)) { + values.set(attValue, 1); + } else { + values.set(attValue, values.get(attValue) + 1); + } } - - // Figure out which ones to use as attributes. - /** @type Object. */ - const textAttributes = {}; - for ( const [ attName, values ] of allAttributes ) { - // Check all values. If there are fewer values than children, at least one child does not have the attribute; - // in this case leave attribute off of . Otherwise, set text attribute to the most-used value. - let total = 0; - let maxCount = 0; - let textAttValue; - for ( const [ value, count ] of values ) { - total += count; - if ( count > maxCount ) { - maxCount = count; - textAttValue = value; - } - } - if ( total === tspans.length && textAttValue ) { - textAttributes[ attName ] = textAttValue; - } + } + + // Figure out which ones to use as attributes. + /** @type Object. */ + const textAttributes = {}; + for (const [attName, values] of allAttributes) { + // Check all values. If there are fewer values than children, at least one child does not have the attribute; + // in this case leave attribute off of . Otherwise, set text attribute to the most-used value. + let total = 0; + let maxCount = 0; + let textAttValue; + for (const [value, count] of values) { + total += count; + if (count > maxCount) { + maxCount = count; + textAttValue = value; + } + } + if (total === tspans.length && textAttValue) { + textAttributes[attName] = textAttValue; } + } - return textAttributes; + return textAttributes; } /** * @param {string} s * @returns {boolean} */ -function isWhiteSpace( s ) { - return /^\s*$/.test( s ); +function isWhiteSpace(s) { + return /^\s*$/.test(s); } /** @@ -240,58 +240,58 @@ function isWhiteSpace( s ) { * @param {number} index * @returns XastElement */ -function mergeTextElements( mergeableChildren, index ) { - /** @type {XastElement} */ - const textElement = { - type: 'element', - name: 'text', - attributes: {}, - children: [], - }; - - // Find all child data. - const tspans = []; - for ( ; ; index++ ) { - const textData = mergeableChildren.get( index ); - if ( !textData ) { - break; - } - tspans.push( ...textData.children ); +function mergeTextElements(mergeableChildren, index) { + /** @type {XastElement} */ + const textElement = { + type: 'element', + name: 'text', + attributes: {}, + children: [], + }; + + // Find all child data. + const tspans = []; + for (; ; index++) { + const textData = mergeableChildren.get(index); + if (!textData) { + break; } + tspans.push(...textData.children); + } + + // Find the default attributes for the element. + textElement.attributes = getTextAttributes(tspans); + + // If there is only one , merge content into . + if (tspans.length === 1) { + const tspanData = tspans[0]; + textElement.attributes.x = tspanData.x; + textElement.attributes.y = tspanData.y; + textElement.children = tspanData.children; + return textElement; + } - // Find the default attributes for the element. - textElement.attributes = getTextAttributes( tspans ); + // Generate elements. + const textChildren = []; + for (const tspanData of tspans) { + /** @type {XastElement} */ + const tspanElement = { + type: 'element', + name: 'tspan', + attributes: { x: tspanData.x, y: tspanData.y }, + children: tspanData.children, + }; - // If there is only one , merge content into . - if ( tspans.length === 1 ) { - const tspanData = tspans[ 0 ]; - textElement.attributes.x = tspanData.x; - textElement.attributes.y = tspanData.y; - textElement.children = tspanData.children; - return textElement; + // Add any attributes that are different from attributes. + for (const [k, v] of tspanData.attributes) { + if (textElement.attributes[k] !== v) { + tspanElement.attributes[k] = v; + } } - // Generate elements. - const textChildren = []; - for ( const tspanData of tspans ) { - /** @type {XastElement} */ - const tspanElement = { - type: 'element', - name: 'tspan', - attributes: { x: tspanData.x, y: tspanData.y }, - children: tspanData.children, - }; - - // Add any attributes that are different from attributes. - for ( const [ k, v ] of tspanData.attributes ) { - if ( textElement.attributes[ k ] !== v ) { - tspanElement.attributes[ k ] = v; - } - } - - textChildren.push( tspanElement ); - } + textChildren.push(tspanElement); + } - textElement.children = textChildren; - return textElement; + textElement.children = textChildren; + return textElement; } diff --git a/plugins/plugins-types.d.ts b/plugins/plugins-types.d.ts index 0b05ece73..9d30515c2 100644 --- a/plugins/plugins-types.d.ts +++ b/plugins/plugins-types.d.ts @@ -137,7 +137,7 @@ type DefaultPlugins = { }; }; - mergeText:void; + mergeText: void; moveElemsAttrsToGroup: void; moveGroupAttrsToElems: void; removeComments: { From 8cb03da0b06db7eadabdc43c9675f4346b7c4b86 Mon Sep 17 00:00:00 2001 From: John Kenny Date: Mon, 25 Dec 2023 15:39:03 -0800 Subject: [PATCH 19/20] Added mergeText documentation. --- docs/03-plugins/merge-text.mdx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/03-plugins/merge-text.mdx diff --git a/docs/03-plugins/merge-text.mdx b/docs/03-plugins/merge-text.mdx new file mode 100644 index 000000000..38b19d2ae --- /dev/null +++ b/docs/03-plugins/merge-text.mdx @@ -0,0 +1,15 @@ +--- +title: Merge Text +svgo: + pluginId: mergeText +--- + +Merge adjacent `` elements and remove `` when it is the only child of a `` element. + +If the SVG contains any `