UNPKG

svg-kit

Version:

A SVG toolkit, with its own Vector Graphics structure, multiple renderers (svg text, DOM svg, canvas), and featuring Flowing Text.

449 lines (342 loc) 13.3 kB
/* SVG Kit Copyright (c) 2017 - 2024 Cédric Ronvel The MIT License (MIT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ "use strict" ; const fs = require( 'fs' ).promises ; const domKit = require( 'dom-kit' ) ; const escape = require( 'string-kit/lib/escape.js' ) ; const core = {} ; module.exports = core ; /* load( url , [options] ) * url: the URL of the .svg file * options: (optional) object of options, transmitted to .inject() and .patch() Return a promise resolving to the SVG DOM document. */ core.load = async function( url , options = {} ) { var content , $doc , $svg ; if ( ! process.browser ) { // Use Node.js 'fs' module if ( url.substring( 0 , 7 ) === 'file://' ) { url = url.slice( 7 ) ; } content = await fs.readFile( url , 'utf8' ) ; $doc = domKit.fromXml( content ) ; } else { // Use an AJAX HTTP Request $doc = await core.ajax( url ) ; } if ( options.removeComments ) { domKit.removeComments( $doc ) ; delete options.removeComments ; } $svg = $doc.documentElement ; core.inject( $svg , options ) ; return $svg ; } ; core.loadFromString = function( content , options = {} ) { var $doc = domKit.fromXml( content ) ; if ( options.removeComments ) { domKit.removeComments( $doc ) ; delete options.removeComments ; } var $svg = $doc.documentElement ; core.inject( $svg , options ) ; return $svg ; } ; core.ajax = function( url ) { return new Promise( ( resolve , reject ) => { var xhr = new XMLHttpRequest() ; xhr.responseType = 'document' ; xhr.onreadystatechange = () => { // From MDN: In the event of a communication error (such as the webserver going down), // an exception will be thrown when attempting to access the 'status' property. try { if ( xhr.readyState === 4 ) { if ( xhr.status === 200 ) { resolve( xhr.responseXML ) ; } else if ( xhr.status === 0 && xhr.responseXML ) { // Yay, loading with file:// does not provide any status... resolve( xhr.responseXML ) ; } else if ( xhr.status ) { reject( xhr.status ) ; } else { reject( new Error( "[svg-kit] ajaxStatus(): Error with falsy status" ) ) ; } } } catch ( error ) { reject( error ) ; } } ; xhr.open( 'GET' , url ) ; xhr.send() ; } ) ; } ; /* Fix few <svg> things in order to inject it in the dom * $svg: the svg element * options: options object, where: * into: `DOMElement` an element where the .svg file should be loaded into * as: `DOMElement` a <svg> element where the .svg file should replace, almost like the "into" option, useful when a <svg> tag should be created synchronously to start doing stuffs on it, and let the asynchronous loading works in the background * all other options are passed to .patch() */ core.inject = function( $svg , options ) { core.patch( $svg , options ) ; if ( options.into ) { options.into.appendChild( $svg ) ; } // Better to avoid to check the tag name: // it's too defensive and it prevents from loading inside a <g> tag if ( options.as ) { //&& options.as.tagName.toLowerCase() === 'svg' ) domKit.moveAttributes( $svg , options.as ) ; domKit.moveChildrenInto( $svg , options.as ) ; } } ; /* Fix few <svg> things. * $svg: the svg element * options: options object, where: * id: `string` the id attribute of the <svg> tag (recommanded) * removeIds: `boolean` remove all 'id' attributes * prefixIds: `string` prefix all IDs and patch url #ref * hidden: `boolean` turn the svg hidden (useful to apply modification before the show) * class: `string` or `object` (key=class, value=true/false) to add/remove on the <svg> tag * removeSize: `boolean` remove the width and height attribute and style from the <svg> tag * removeSvgStyle: `boolean` remove the top-level style attribute of the <svg> tag * removeDefaultStyles: `boolean` used to removed meaningless style pollution * css: `object` a css object to apply on the <svg> tag * colorClass: `boolean` a very specialized option. It moves all stroke and fill color inline styles to attribute on all drawing elements and add the "primary" class to those that are class-less. Since CSS has a greater priority than attributes (but lesser than inline styles), this allows us to controle color using CSS. * removeComments: `boolean` remove all comment nodes * removeWhiteSpaces: `boolean` remove all white-space * removeWhiteLines: `boolean` remove all empty lines * removeExoticNamespaces: `boolean` remove all tag and attributes that have a namespace different than svg, the svg namespace is stripped */ core.patch = function( $svg , options ) { var viewBox , width , height ; core.lightCleanup( $svg ) ; // Fix id, if necessary if ( options.id !== undefined ) { if ( typeof options.id === 'string' ) { $svg.setAttribute( 'id' , options.id ) ; } else if ( ! options.id ) { $svg.removeAttribute( 'id' ) ; } } if ( options.class ) { if ( typeof options.class === 'string' ) { $svg.classList.add( options.class ) ; } else if ( typeof options.class === 'object' ) { domKit.class( $svg , options.class ) ; } } if ( options.hidden ) { $svg.style.visibility = 'hidden' ; } if ( options.prefixIds ) { domKit.prefixIds( $svg , options.prefixIds ) ; } if ( options.removeIds ) { domKit.removeAllAttributes( $svg , 'id' ) ; } if ( options.removeSvgStyle ) { $svg.removeAttribute( 'style' ) ; } if ( options.removeDefaultStyles ) { core.removeDefaultStyles( $svg ) ; } if ( options.removeComments ) { domKit.removeComments( $svg ) ; } if ( options.removeExoticNamespaces ) { domKit.filterByNamespace( $svg , { primary: 'svg' , whitelist: [] } ) ; } if ( options.removeSize ) { // Save and remove the width and height attribute width = $svg.getAttribute( 'width' ) || $svg.style.width ; height = $svg.getAttribute( 'height' ) || $svg.style.height ; $svg.removeAttribute( 'height' ) ; $svg.removeAttribute( 'width' ) ; $svg.style.width = null ; $svg.style.height = null ; // if the $svg don't have a viewBox attribute, set it now from the width and height (it works most of time) if ( ! $svg.getAttribute( 'viewBox' ) && width && height ) { viewBox = '0 0 ' + width + ' ' + height ; $svg.setAttribute( 'viewBox' , viewBox ) ; } } if ( options.css ) { domKit.css( $svg , options.css ) ; } if ( options.colorClass ) { core.colorClass( $svg ) ; } if ( options.removeWhiteSpaces ) { domKit.removeWhiteSpaces( $svg ) ; } else if ( options.removeWhiteLines ) { domKit.removeWhiteSpaces( $svg , true ) ; } } ; core.patchDocument = function( $doc , options ) { var removeWhiteSpaces = options.removeWhiteSpaces , removeWhiteLines = options.removeWhiteLines , removeComments = options.removeComments ; delete options.removeWhiteSpaces ; delete options.removeWhiteLines ; delete options.removeComments ; if ( removeComments ) { domKit.removeComments( $doc ) ; } core.patch( $doc.documentElement , options ) ; if ( removeWhiteSpaces ) { domKit.removeWhiteSpaces( $doc ) ; } else if ( removeWhiteLines ) { domKit.removeWhiteSpaces( $doc , true ) ; } } ; core.lightCleanup = function( $svg ) { domKit.removeAllTags( $svg , 'metadata' ) ; domKit.removeAllTags( $svg , 'script' ) ; domKit.removeAllTags( $svg , 'defs' , true ) ; // all empty defs } ; // List of svg tags that actually display things const drawingTags = [ 'path' , 'circle' , 'ellipse' , 'line' , 'rect' , 'polyline' , 'polygone' , 'text' , 'textPath' ] ; const defaultStyles = [ [ 'font-style' , 'normal' ] , [ 'font-weight' , 'normal' ] , [ 'font-variant' , 'normal' ] , [ 'font-stretch' , 'normal' ] , [ 'font-size' , 'medium' ] , [ 'line-height' , 'normal' ] , [ 'font-variant-ligatures' , 'normal' ] , //[ 'font-family' , 'sans-serif' ] , [ 'font-variant-position' , 'normal' ] , [ 'font-variant-caps' , 'normal' ] , [ 'font-variant-numeric' , 'normal' ] , [ 'font-variant-alternates' , 'normal' ] , [ 'font-variant-east-asian' , 'normal' ] , [ 'font-feature-settings' , 'normal' ] , [ 'text-indent' , '0' ] , [ 'text-align' , 'start' ] , [ 'text-decoration' , 'none' ] , [ 'text-decoration-line' , 'none' ] , [ 'text-decoration-style' , 'solid' ] , [ 'text-decoration-color' , '#000000' ] , [ 'letter-spacing' , 'normal' ] , [ 'word-spacing' , 'normal' ] , [ 'text-transform' , 'none' ] , [ 'writing-mode' , 'lr-tb' ] , [ 'direction' , 'ltr' ] , [ 'text-orientation' , 'mixed' ] , [ 'dominant-baseline' , 'auto' ] , [ 'baseline-shift' , 'baseline' ] , [ 'text-anchor' , 'start' ] , [ 'white-space' , 'normal' ] , [ 'shape-padding' , '0' ] , [ 'display' , 'inline' ] , [ 'visibility' , 'visible' ] , [ 'overflow' , 'visible' ] , [ 'opacity' , '1' ] , [ 'isolation' , 'auto' ] , [ 'mix-blend-mode' , 'normal' ] , [ 'color-interpolation' , 'sRGB' ] , [ 'color-interpolation-filters' , 'linearRGB' ] , [ 'solid-color' , '#000000' ] , [ 'solid-opacity' , '1' ] , [ 'vector-effect' , 'none' ] , [ 'fill-rule' , 'nonzero' ] , [ 'clip-rule' , 'nonzero' ] , [ 'color-rendering' , 'auto' ] , [ 'image-rendering' , 'auto' ] , [ 'shape-rendering' , 'auto' ] , [ 'text-rendering' , 'auto' ] , [ 'enable-background' , 'accumulate' ] , [ 'stroke-dasharray' , 'none' ] , [ 'stroke-dashoffset' , '0' ] , [ 'paint-order' , 'normal' ] , [ 'paint-order' , 'fill stroke markers' ] ] ; core.colorClass = function( $svg ) { drawingTags.forEach( ( tagName ) => { Array.from( $svg.getElementsByTagName( tagName ) ).forEach( ( $element ) => { // Beware, $element.className does not work as expected for SVG if ( ! $element.getAttribute( 'class' ) ) { $element.classList.add( 'primary' ) ; } // move style to attribute if they are not 'none' domKit.styleToAttribute( $element , 'fill' , [ 'none' ] ) ; domKit.styleToAttribute( $element , 'stroke' , [ 'none' ] ) ; } ) ; } ) ; } ; // Remove styles set to a default/unused value core.removeDefaultStyles = function( $svg ) { drawingTags.forEach( ( tagName ) => { Array.from( $svg.getElementsByTagName( tagName ) ).forEach( ( $element ) => { var styles = $element.getAttribute( 'style' ) ; defaultStyles.forEach( array => { var k = array[ 0 ] ; var v = array[ 1 ] ; styles = styles.replace( new RegExp( '(^|;) *' + escape.regExp( k ) + ' *: *' + escape.regExp( v ) + ' *(?:;|$)' ) , ( full , pre ) => pre ) ; } ) ; $element.setAttribute( 'style' , styles ) ; } ) ; } ) ; } ; // Should remove all tags and attributes that have non-registered namespace, // e.g.: sodipodi, inkscape, etc... //core.heavyCleanup = function( svgElement ) {} ; core.getViewBox = function( $svg ) { var raw = $svg.getAttribute( 'viewBox' ) ; if ( ! raw ) { return null ; } var array = raw.split( / +/ ) ; return { x: parseFloat( array[ 0 ] , 10 ) , y: parseFloat( array[ 1 ] , 10 ) , width: parseFloat( array[ 2 ] , 10 ) , height: parseFloat( array[ 3 ] , 10 ) } ; } ; core.setViewBox = function( $svg , viewBox ) { $svg.setAttribute( 'viewBox' , viewBox.x + ' ' + viewBox.y + ' ' + viewBox.width + ' ' + viewBox.height ) ; } ; core.toAreaArray = function( object ) { if ( object.min && object.max ) { // Math Kit BoundingBox2D return [ object.min.x , object.min.y , object.max.x - object.min.x , object.max.y - object.min.y ] ; } else if ( object.xmin !== undefined && object.xmax !== undefined && object.ymin !== undefined && object.ymax !== undefined ) { return [ object.xmin , object.ymin , object.xmax - object.xmin , object.ymax - object.ymin ] ; } else if ( object.x !== undefined && object.y !== undefined && object.width !== undefined && object.height !== undefined ) { return [ object.x , object.y , object.width , object.height ] ; } return [ 0 , 0 , 100 , 100 ] ; } ; core.standalone = function( content , viewBox ) { var output = '<?xml version="1.0" encoding="UTF-8"?>\n' ; if ( ! Array.isArray( viewBox ) ) { viewBox = core.toAreaArray( viewBox ) ; } output += '<svg xmlns="http://www.w3.org/2000/svg" viewBox="' + viewBox.join( ' ' ) + '">\n' ; // ? // width="500" // height="500" output += content ; output += '\n</svg>\n' ; return output ; } ;