-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add cleanupTextElements plugin (#69)
- Loading branch information
1 parent
a5b3fd3
commit 827538b
Showing
11 changed files
with
358 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
export const name = 'cleanupTextElements'; | ||
export const description = 'simplify <text> elements and content'; | ||
|
||
/** | ||
* @type {import('./plugins-types.js').Plugin<'cleanupTextElements'>} | ||
*/ | ||
export const fn = (root, params, info) => { | ||
const styleData = info.docData.getStyles(); | ||
if ( | ||
info.docData.hasScripts() || | ||
styleData === null || | ||
styleData.hasStyles() | ||
) { | ||
return; | ||
} | ||
|
||
/** @type {Map<import('./inlineStyles.js').XastParent,Set<import('./collapseGroups.js').XastElement>>} */ | ||
const textElsToHoist = new Map(); | ||
|
||
return { | ||
element: { | ||
exit: (element) => { | ||
if (element.name !== 'text') { | ||
return; | ||
} | ||
|
||
// Remove xml:space="preserve" if possible. | ||
if (element.attributes['xml:space'] === 'preserve') { | ||
if (canRemovePreserve(element)) { | ||
delete element.attributes['xml:space']; | ||
} | ||
} | ||
|
||
// Remove any pure whitespace children. | ||
const childrenToDelete = new Set(); | ||
for (const child of element.children) { | ||
switch (child.type) { | ||
case 'cdata': | ||
case 'text': | ||
if (isOnlyWhiteSpace(child.value)) { | ||
childrenToDelete.add(child); | ||
} | ||
break; | ||
} | ||
} | ||
if (childrenToDelete.size > 0) { | ||
element.children = element.children.filter( | ||
(c) => !childrenToDelete.has(c), | ||
); | ||
} | ||
|
||
// If there is a single child whose content can be hoisted, do so. | ||
const hoistableChild = getHoistableChild(element); | ||
if (hoistableChild) { | ||
element.children = hoistableChild.children; | ||
for (const child of element.children) { | ||
child.parentNode = element; | ||
} | ||
for (const attributeName of ['x', 'y']) { | ||
if (hoistableChild.attributes[attributeName] !== undefined) { | ||
element.attributes[attributeName] = | ||
hoistableChild.attributes[attributeName]; | ||
} | ||
} | ||
} | ||
|
||
// If the <text> element has x/y, and so do all children, remove x/y from <text>. | ||
if ( | ||
element.attributes.x !== undefined && | ||
element.attributes.y !== undefined && | ||
childrenAllHaveXY(element) | ||
) { | ||
delete element.attributes.x; | ||
delete element.attributes.y; | ||
} | ||
|
||
// If the <text> has no attributes, and all of the children can be hoisted, add this element to the list to be updated | ||
// at the end. | ||
if (Object.keys(element.attributes).length === 0) { | ||
if ( | ||
element.parentNode.type === 'element' && | ||
element.parentNode.name !== 'switch' | ||
) { | ||
if ( | ||
element.children.every( | ||
(child) => isHoistable(child) !== undefined, | ||
) | ||
) { | ||
let textEls = textElsToHoist.get(element.parentNode); | ||
if (!textEls) { | ||
textEls = new Set(); | ||
textElsToHoist.set(element.parentNode, textEls); | ||
} | ||
textEls.add(element); | ||
} | ||
} | ||
} | ||
}, | ||
}, | ||
root: { | ||
exit: () => { | ||
for (const [parent, textEls] of textElsToHoist.entries()) { | ||
/** @type {import('../lib/types.js').XastChild[]} */ | ||
const newChildren = []; | ||
for (const child of parent.children) { | ||
if (child.type !== 'element' || !textEls.has(child)) { | ||
newChildren.push(child); | ||
continue; | ||
} | ||
// Promote all children to <text> elements. | ||
for (const textChild of child.children) { | ||
if (textChild.type !== 'element') { | ||
throw new Error(); | ||
} | ||
textChild.parentNode = parent; | ||
textChild.name = 'text'; | ||
newChildren.push(textChild); | ||
} | ||
} | ||
parent.children = newChildren; | ||
} | ||
}, | ||
}, | ||
}; | ||
}; | ||
|
||
/** | ||
* @param {import('../lib/types.js').XastElement} element | ||
* @returns {boolean} | ||
*/ | ||
function canRemovePreserve(element) { | ||
for (const child of element.children) { | ||
switch (child.type) { | ||
case 'cdata': | ||
case 'text': | ||
if (hasSignificantWhiteSpace(child.value)) { | ||
return false; | ||
} | ||
break; | ||
case 'element': | ||
switch (child.name) { | ||
case 'tspan': | ||
if (!canRemovePreserve(child)) { | ||
return false; | ||
} | ||
break; | ||
default: | ||
return false; | ||
} | ||
break; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
/** | ||
* @param {import('../lib/types.js').XastElement} element | ||
* @returns {boolean} | ||
*/ | ||
function childrenAllHaveXY(element) { | ||
for (const child of element.children) { | ||
if (child.type !== 'element') { | ||
return false; | ||
} | ||
if (child.name !== 'tspan') { | ||
return false; | ||
} | ||
if (child.attributes.x === undefined || child.attributes.y === undefined) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
/** | ||
* @param {import('../lib/types.js').XastElement} element | ||
* @returns {import('../lib/types.js').XastElement|undefined} | ||
*/ | ||
function getHoistableChild(element) { | ||
if (element.children.length !== 1) { | ||
return; | ||
} | ||
const child = element.children[0]; | ||
return isHoistable(child); | ||
} | ||
|
||
/** | ||
* @param {string} str | ||
* @returns {boolean} | ||
*/ | ||
export function hasSignificantWhiteSpace(str) { | ||
let isStart = true; | ||
let lastIsSpace = false; | ||
for (const char of str) { | ||
switch (char) { | ||
case ' ': | ||
case '\n': | ||
case '\t': | ||
if (!isStart) { | ||
// Consective space within text is significant. | ||
if (lastIsSpace) { | ||
return true; | ||
} | ||
} | ||
lastIsSpace = true; | ||
break; | ||
default: | ||
if (isStart) { | ||
if (lastIsSpace) { | ||
// There is space at beginning of string. | ||
return true; | ||
} | ||
isStart = false; | ||
} | ||
lastIsSpace = false; | ||
break; | ||
} | ||
} | ||
if (isStart) { | ||
return false; | ||
} | ||
return lastIsSpace; | ||
} | ||
|
||
/** | ||
* @param {import('../lib/types.js').XastChild} child | ||
* @returns {import('./collapseGroups.js').XastElement|undefined} | ||
*/ | ||
function isHoistable(child) { | ||
if (child.type !== 'element') { | ||
return; | ||
} | ||
if (child.children.length !== 1) { | ||
return; | ||
} | ||
if (child.children[0].type !== 'text') { | ||
return; | ||
} | ||
for (const attributeName of Object.keys(child.attributes)) { | ||
switch (attributeName) { | ||
case 'x': | ||
case 'y': | ||
break; | ||
default: | ||
return; | ||
} | ||
} | ||
return child; | ||
} | ||
|
||
/** | ||
* @param {string} str | ||
* @returns {boolean} | ||
*/ | ||
function isOnlyWhiteSpace(str) { | ||
for (const char of str) { | ||
switch (char) { | ||
case ' ': | ||
case '\n': | ||
case '\t': | ||
continue; | ||
default: | ||
return false; | ||
} | ||
} | ||
return true; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { hasSignificantWhiteSpace } from '../../plugins/cleanupTextElements.js'; | ||
|
||
describe('test for significant whitespace', () => { | ||
/** @type {{in:string,out:boolean}[]} */ | ||
const testCases = [ | ||
{ in: '', out: false }, | ||
{ in: ' \n\n\t', out: false }, | ||
{ in: ' x', out: true }, | ||
{ in: 'xxxx', out: false }, | ||
{ in: 'xx xx', out: false }, | ||
{ in: 'xx xx', out: true }, | ||
{ in: 'xxxx\n', out: true }, | ||
]; | ||
|
||
for (const testCase of testCases) { | ||
it(`${JSON.stringify(testCase.in)}`, () => { | ||
expect(hasSignificantWhiteSpace(testCase.in)).toBe(testCase.out); | ||
}); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
Remove xml:space when not needed. | ||
|
||
=== | ||
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 300"> | ||
<text xml:space="preserve" x="35" y="45"> | ||
<tspan x="35" y="45"> Here is some text</tspan> | ||
<tspan x="35" y="55">And Here is some more</tspan> | ||
</text> | ||
<text xml:space="preserve" x="105" y="185"> | ||
<tspan x="105" y="185">Also down here</tspan> | ||
</text> | ||
</svg> | ||
|
||
@@@ | ||
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 300"> | ||
<text xml:space="preserve"><tspan x="35" y="45"> Here is some text</tspan><tspan x="35" y="55">And Here is some more</tspan></text> | ||
<text x="105" y="185">Also down here</text> | ||
</svg> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
Hoist multiple <tspan>s if possible. | ||
|
||
=== | ||
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 300"> | ||
<text> | ||
<tspan x="35" y="45">Here is some text</tspan> | ||
<tspan x="35" y="55">And Here is some more</tspan> | ||
</text> | ||
</svg> | ||
|
||
@@@ | ||
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 300"> | ||
<text x="35" y="45">Here is some text</text> | ||
<text x="35" y="55">And Here is some more</text> | ||
</svg> |
Oops, something went wrong.