Reformat SVG files to reduce
git diff
noise
The impetus for GSVG was the SVG output of Inkscape which comes so close to being Git-friendly but doesn't quite make it. The goal of GSVG is to create a consistent, deterministic formatting for SVG files. This is valuable no matter what SVG editor you use. The formatting that GSVG creates is modeled after Inkscape's output, it just fixes some inconsistencies.
First, some great things about how Inkscape formats its files:
- Each attribute of each tag is on its own line. Great for version control systems like Git.
- Attributes are indented a
shiftwidth
+ 1 space from their start tag (the extra space is not added to the next nested tag). This is great for humans: It's easy to see which tag an attribute belongs to.
<g
class="bar"
id="foo">
<defs
id="baz" />
</g>
However, there are some less ideal Inkscape formatting issues which GSVG addresses:
- The order of attributes in Inkscape's output is non-deterministic. When you re-save a file, Inkscape usually re-orders attributes in tags unrelated to your edit. This leads to noisy and unclear
git diff
output. GSVG enforces a consistent order of attributes. - Inkscape mixes spaces and tabs for indentation which is bad form no matter what side of the tabs vs. spaces debate you're on. GSVG creates consistent (and customizable) indentation.
First, make sure you have Node.js installed. Node also comes with npm (Node Package Manager) which is the easiest way to install GSVG.
- For command line usage:
npm install --global gsvg
- As a package dependency:
npm install --save gsvg
GSVG uses Semantic Versioning. At this point, GSVG hasn't reached version 1.0.0 and the API is not yet stable.
gsvg infile.svg
: Print Git-friendly version of infile.svg to stdoutgsvg infile.svg outfile.svg
: Write output to a filegsvg -i infile.svg
: Overwrite original fileecho "<svg></svg>" | gsvg
: Use stdin, print to stdout
Usage
$ gsvg [--help|--version]
$ gsvg [flags/options] [<infile> [<outfile>]]
$ echo "<svg></svg>" | gsvg [flags/options] [<outfile>]
<infile> Input filename
If a string is piped to GSVG, that will be used as the input and
the first positional argument will be treated as <outfile> instead.
<outfile> Output filename
The file will be overwritten if it exists.
If <outfile> is omitted, output will be piped to stdout.
Flags
-h, --help, Display this message
-v, --version Display version number
-i, --in-place Change file in place
Requires: <infile>
Excludes: piping, <outfile>
Options
-s, --shiftwidth <integer|string> Shiftwidth used for indentation
<integer> (Default: 2) Number of spaces (0 or more)
<string> Spaces (" ") or "t" (converted to "\t")
-a, --attr-extra-indent <integer|string> Added to indent for attributes
<integer> (Default: 1) Number of spaces (0 or more)
<string> Spaces (" ") or "t" (converted to "\t")
Examples
$ gsvg --shiftwidth t sample.svg
$ gsvg -i sample.svg
$ cat sample.svg | gsvg -a 0 > sample.gsvg.svg
var gsvg = require('gsvg');
// get a string
gsvg('<g></g>').then(console.log);
//=> '<g>\n</g>\n'
// get an array
gsvg.array('<g></g>').then(console.log);
//=> ['<g>', '</g>']
// example with nesting and indentation
gsvg.array('<g id="foo"><g></g></g>').then(console.log);
/*=>
[
'<g',
' id="foo"',
' <g>',
' </g>',
'</g>'
]
*/
// change default indentation settings
gsvg.array('<g id="foo"><g></g></g>', {
shiftwidth: '\t',
attrExtraIndent: 2
}).then(console.log);
/*=>
[
'<g',
'\t id="foo"',
'\t<g>',
'\t</g>',
'</g>'
]
*/
Returns a promise which resolves with the resulting Git-friendly string.
Returns a promise that resolves with an array of strings, one for each line.
Type: string
Required: yes
The SVG string you want to process. All tags must be properly nested.
Type: Object
Required: no
Change default settings by passing them in this object.
-
shiftwidth
Type:number
string
Default:2
-
number
: Integer number of spaces by which to increase indentation for each level of nested tags. Zero or more. -
string
: The actual string to use forshiftwidth
. Must be all spaces (' '
) or all tabs ('\t'
).
-
-
attrExtraIndent
Type:number
string
Default:1
-
number
: Integer number of spaces by which to increase indentation for each level of nested tags. Zero or more. -
string
: The actual string to use forshiftwidth
. Must be all spaces (' '
) or all tabs ('\t'
).
-
- Git: A pre-commit hook would be an excellent place for a GSVG run.
- Vim: Just use
:%!gsvg
to filter the current buffer contents through GSVG.
GSVG is not an SVG linter of any sort. The minimal error checking it has is from using sax's "strict mode". Errors will be thrown for tags which are improperly nested or improperly opened/closed, but not a whole lot else.
We use bluebird for promises.
GSVG is designed and optimized for SVG, but it can be used with other XML as well. The Roadmap includes plans for customizing some of the SVG-specific defaults which may be valuable for filtering XML that isn't SVG. The main situation where you could run into an issue is if you have a text node that's entirely whitespace which should be preserved. Please file an issue if you run into this.
GSVG always outputs Unix line endings ('\n'
), UTF-8 encoding, and it includes a newline character and the end of the file. If you need something other than that, you can use the gsvg.array()
method and convert it however you'd like or you can filter the CLI output in the pipeline.
- Sort attributes in a logical order (to humans). Currently they're just alphabetized. Also allow customization of that sort order.
- Add line breaks at each command in path definitions (d attributes of
<path>
and<glyph>
tags). - Inkscape often changes relative URIs to absolute ones, breaking your
<use xlink:href="other-file.svg#some-id" />
tags (among others) when uploaded or simply moved to a different folder. So: Make paths relative if the absolute URI points to the local file system (and if we know where the files are relative to each other). - Add more tests.
- Flesh out Contributing guidelines.
- Remove Inkscape-specific attributes and css properties.
- Stabilize API and release version 1.0.0.
- Make sure GSVG is usable in the browser. It may be there already when using browserify (no native modules are used outside of the CLI), but I haven't verified that.
Contributions are always welcome! Before you jump in, please read the Code of Conduct.
Make sure that npm test
is successful before you submit a pull request. Here's what that command runs:
Linter: XO which uses eslint under the hood. I've made just a few changes to XO's default setup, most notably: indent is 4 spaces and arrow functions always require parentheses (the test files were left with XO's default AVA setting of omitting the parens when there's only one argument).
Test runner: AVA. AVA runs asynchronous tests concurrently so it's nice and fast. It runs its test files through babel which means you can use things like async
await
within the test files.
Most pull requests that affect code should be accompanied by relevant test cases.
The ideal way to report bugs is to submit a pull request with a failing test. When submitting a failing test, use test.failing()
as shown below. Then XO will verify that the test actually fails and report on it, but will still give an exit code of 0
.
// your test showing that frobs fail to result in wobbles might look like this:
test.failing('frobs result in wobbles', async t => {
var result = await gsvg('frobs');
t.is(result, 'wobbles');
});
markdown-it, particularly its use of a tokens array instead of an AST.