UNPKG

svgicons2svgfont

Version:

Read a set of SVG icons and output a SVG font

776 lines (679 loc) 20.4 kB
import { describe, test, expect } from '@jest/globals'; import assert from 'assert'; import { Readable } from 'node:stream'; import fs from 'node:fs'; import { mkdir } from 'node:fs/promises'; import { join } from 'node:path'; import { SVGIcons2SVGFontStream } from '../index.js'; import { SVGIconsDirStream, type SVGIconStream } from '../iconsdir.js'; import streamtest from 'streamtest'; import { BufferStream } from 'bufferstreams'; try { await mkdir(join('fixtures', 'results')); } catch (err) { // empty } const codepoint = JSON.parse( fs.readFileSync('./fixtures/expected/test-codepoint.json').toString(), ); // Helpers async function generateFontToFile(options, fileSuffix?, startUnicode?, files?) { const dest = join( 'fixtures', 'results', `${options.fontName + (fileSuffix || '')}.svg`, ); let resolve; let reject; const promise = new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject; }); options.log = () => {}; options.round = options.round || 1e3; const svgFontStream = new SVGIcons2SVGFontStream(options); svgFontStream.pipe(fs.createWriteStream(dest)).on('finish', () => { try { expect(fs.readFileSync(dest, { encoding: 'utf8' })).toEqual( fs.readFileSync( join( 'fixtures', 'expected', `${options.fontName + (fileSuffix || '')}.svg`, ), { encoding: 'utf8' }, ), ); resolve(); } catch (err) { reject(err); } }); new SVGIconsDirStream(files || join('fixtures', 'icons', options.fontName), { startUnicode: startUnicode || 0xe001, }).pipe(svgFontStream); return await promise; } async function generateFontToMemory(options, files?, startUnicode?) { options.log = () => {}; options.round = options.round || 1e3; options.callback = (glyphs) => { const fontName = options.fontName; expect(glyphs).toEqual(codepoint[fontName]); }; const svgFontStream = new SVGIcons2SVGFontStream(options); const promise = bufferStream(svgFontStream); new SVGIconsDirStream(files || join('fixtures', 'icons', options.fontName), { startUnicode: startUnicode || 0xe001, }).pipe(svgFontStream); expect((await promise).toString()).toEqual( fs.readFileSync(join('fixtures', 'expected', `${options.fontName}.svg`), { encoding: 'utf8', }), ); } // Tests describe('Generating fonts to files', () => { test('should work for simple SVG', async () => { await generateFontToFile({ fontName: 'originalicons', }); }); test('should work for simple fixedWidth and normalize option', async () => { await generateFontToFile( { fontName: 'originalicons', fixedWidth: true, normalize: true, }, 'n', ); }); test('should work for simple SVG', async () => { await generateFontToFile({ fontName: 'cleanicons', }); }); test('should work for simple SVG and custom ascent', async () => { await generateFontToFile( { fontName: 'cleanicons', ascent: 100, }, '-ascent', ); }); test('should work for simple SVG and custom properties', async () => { await generateFontToFile( { fontName: 'cleanicons', fontStyle: 'italic', fontWeight: 'bold', }, '-stw', ); }); test('should work for codepoint mapped SVG icons', async () => { await generateFontToFile({ fontName: 'prefixedicons', callback: () => {}, }); }); test('should work with multipath SVG icons', async () => { await generateFontToFile({ fontName: 'multipathicons', }); }); test('should work with simple shapes SVG icons', async () => { await generateFontToFile({ fontName: 'shapeicons', }); }); test('should work with variable height icons', async () => { await generateFontToFile({ fontName: 'variableheighticons', }); }); test('should work with variable height icons and the normalize option', async () => { await generateFontToFile( { fontName: 'variableheighticons', normalize: true, }, 'n', ); }); test('should work with variable height icons, the normalize option and the preserveAspectRatio option', async () => { await generateFontToFile( { fontName: 'variableheighticons', normalize: true, preserveAspectRatio: true, }, 'np', ); }); test('should work with variable width icons', async () => { await generateFontToFile({ fontName: 'variablewidthicons', }); }); test('should work with centered variable width icons and the fixed width option', async () => { await generateFontToFile( { fontName: 'variablewidthicons', fixedWidth: true, centerHorizontally: true, }, 'n', ); }); test('should calculate bounds when not specified in the svg file', async () => { await generateFontToFile({ fontName: 'calcbounds', }); }); test('should work with a font id', async () => { await generateFontToFile( { fontName: 'variablewidthicons', fixedWidth: true, centerHorizontally: true, fontId: 'plop', }, 'id', ); }); test('should work with scaled icons', async () => { await generateFontToFile({ fontName: 'scaledicons', fixedWidth: true, centerHorizontally: true, fontId: 'plop', }); }); test('should not display hidden paths', async () => { await generateFontToFile({ fontName: 'hiddenpathesicons', }); }); test('should work with real world icons', async () => { await generateFontToFile({ fontName: 'realicons', }); }); test('should work with rendering test SVG icons', async () => { await generateFontToFile({ fontName: 'rendricons', }); }); test('should work with a single SVG icon', async () => { await generateFontToFile({ fontName: 'singleicon', }); }); test('should work with transformed SVG icons', async () => { await generateFontToFile({ fontName: 'transformedicons', }); }); test('should work when horizontally centering SVG icons', async () => { await generateFontToFile({ fontName: 'tocentericons', centerHorizontally: true, }); }); test('should work when vertically centering SVG icons', async () => { await generateFontToFile({ fontName: 'toverticalcentericons', centerVertically: true, }); }); test('should work with a icons with path with fill none', async () => { await generateFontToFile({ fontName: 'pathfillnone', }); }); test('should work with shapes with rounded corners', async () => { await generateFontToFile({ fontName: 'roundedcorners', }); }); test('should work with realworld icons', async () => { await generateFontToFile({ fontName: 'realworld', }); }); test('should work with a lot of icons', async () => { await generateFontToFile( { fontName: 'lotoficons', }, '', 0, [ 'fixtures/icons/cleanicons/account.svg', 'fixtures/icons/cleanicons/arrow-down.svg', 'fixtures/icons/cleanicons/arrow-left.svg', 'fixtures/icons/cleanicons/arrow-right.svg', 'fixtures/icons/cleanicons/arrow-up.svg', 'fixtures/icons/cleanicons/basket.svg', 'fixtures/icons/cleanicons/close.svg', 'fixtures/icons/cleanicons/minus.svg', 'fixtures/icons/cleanicons/plus.svg', 'fixtures/icons/cleanicons/search.svg', 'fixtures/icons/hiddenpathesicons/sound--off.svg', 'fixtures/icons/hiddenpathesicons/sound--on.svg', 'fixtures/icons/multipathicons/kikoolol.svg', 'fixtures/icons/originalicons/mute.svg', 'fixtures/icons/originalicons/sound.svg', 'fixtures/icons/originalicons/speaker.svg', 'fixtures/icons/realicons/diegoliv.svg', 'fixtures/icons/realicons/hannesjohansson.svg', 'fixtures/icons/realicons/roelvanhitum.svg', 'fixtures/icons/realicons/safety-icon.svg', 'fixtures/icons/realicons/sb-icon.svg', 'fixtures/icons/realicons/settings-icon.svg', 'fixtures/icons/realicons/track-icon.svg', 'fixtures/icons/realicons/web-icon.svg', 'fixtures/icons/roundedcorners/roundedrect.svg', 'fixtures/icons/shapeicons/circle.svg', 'fixtures/icons/shapeicons/ellipse.svg', 'fixtures/icons/shapeicons/lines.svg', 'fixtures/icons/shapeicons/polygon.svg', 'fixtures/icons/shapeicons/polyline.svg', 'fixtures/icons/shapeicons/rect.svg', 'fixtures/icons/tocentericons/bottomleft.svg', 'fixtures/icons/tocentericons/center.svg', 'fixtures/icons/tocentericons/topright.svg', ], ); }); test('should work with rotated rectangle icon', async () => { await generateFontToFile({ fontName: 'rotatedrectangle', }); }); /** * Issue #6 * icon by @paesku * https://github.com/nfroidure/svgicons2svgfont/issues/6#issuecomment-125545925 */ test('should work with complicated nested transforms', async () => { await generateFontToFile({ fontName: 'paesku', round: 1e3, }); }); /** * Issue #76 * https://github.com/nfroidure/svgicons2svgfont/issues/76#issue-259831969 */ test('should work with transform=translate(x) without y', async () => { await generateFontToFile({ fontName: 'translatex', round: 1e3, }); }); test('should work with skew', async () => { await generateFontToFile({ fontName: 'skew', }); }); test('should work when only rx is present', async () => { await generateFontToFile({ fontName: 'onlywithrx', }); }); test('should work when only ry is present', async () => { await generateFontToFile({ fontName: 'onlywithry', }); }); }); describe('Generating fonts to memory', () => { test('should work for simple SVG', async () => { await generateFontToMemory({ fontName: 'originalicons', }); }); test('should work for simple SVG', async () => { await generateFontToMemory({ fontName: 'cleanicons', }); }); test('should work for codepoint mapped SVG icons', async () => { await generateFontToMemory({ fontName: 'prefixedicons', }); }); test('should work with multipath SVG icons', async () => { await generateFontToMemory({ fontName: 'multipathicons', }); }); test('should work with simple shapes SVG icons', async () => { await generateFontToMemory({ fontName: 'shapeicons', }); }); }); describe('Using options', () => { test('should work with fixedWidth option set to true', async () => { await generateFontToFile( { fontName: 'originalicons', fixedWidth: true, }, '2', ); }); test('should work with custom fontHeight option', async () => { await generateFontToFile( { fontName: 'originalicons', fontHeight: 800, }, '3', ); }); test('should work with custom descent option', async () => { await generateFontToFile( { fontName: 'originalicons', descent: 200, }, '4', ); }); test('should work with fixedWidth set to true and with custom fontHeight option', async () => { await generateFontToFile( { fontName: 'originalicons', fontHeight: 800, fixedWidth: true, }, '5', ); }); test( 'should work with fixedWidth and centerHorizontally set to true and with' + ' custom fontHeight option', async () => { await generateFontToFile( { fontName: 'originalicons', fontHeight: 800, fixedWidth: true, centerHorizontally: true, round: 1e5, }, '6', ); }, ); test( 'should work with fixedWidth, normalize and centerHorizontally set to' + ' true and with custom fontHeight option', async () => { await generateFontToFile( { fontName: 'originalicons', fontHeight: 800, normalize: true, fixedWidth: true, centerHorizontally: true, round: 1e5, }, '7', ); }, ); test( 'should work with fixedWidth, normalize and centerHorizontally set to' + ' true and with a large custom fontHeight option', async () => { await generateFontToFile( { fontName: 'originalicons', fontHeight: 5000, normalize: true, fixedWidth: true, centerHorizontally: true, round: 1e5, }, '8', ); }, ); test('should work with nested icons', async () => { await generateFontToFile( { fontName: 'nestedicons', }, '', 0xea01, ); }); }); describe('Passing code points', () => { test('should work with multiple unicode values for a single icon', async () => { const svgFontStream = new SVGIcons2SVGFontStream({ round: 1e3 }); const svgIconStream = fs.createReadStream( join('fixtures', 'icons', 'cleanicons', 'account.svg'), ) as unknown as SVGIconStream; svgIconStream.metadata = { name: 'account', unicode: ['\uE001', '\uE002'], }; const promise = bufferStream(svgFontStream); svgFontStream.write(svgIconStream); svgFontStream.end(); assert.equal( await promise, fs.readFileSync(join('fixtures', 'expected', 'cleanicons-multi.svg'), { encoding: 'utf8', }), ); }); test('should work with ligatures', async () => { const svgFontStream = new SVGIcons2SVGFontStream({ round: 1e3 }); const svgIconStream = fs.createReadStream( join('fixtures', 'icons', 'cleanicons', 'account.svg'), ) as unknown as SVGIconStream; svgIconStream.metadata = { name: 'account', unicode: ['\uE001\uE002'], }; const promise = bufferStream(svgFontStream); svgFontStream.write(svgIconStream); svgFontStream.end(); assert.equal( await promise, fs.readFileSync(join('fixtures', 'expected', 'cleanicons-lig.svg'), { encoding: 'utf8', }), ); }); test('should work with high code points', async () => { const svgFontStream = new SVGIcons2SVGFontStream({ round: 1e3 }); const svgIconStream = fs.createReadStream( join('fixtures', 'icons', 'cleanicons', 'account.svg'), ) as unknown as SVGIconStream; svgIconStream.metadata = { name: 'account', unicode: ['\u{1f63a}'], }; const promise = bufferStream(svgFontStream); svgFontStream.write(svgIconStream); svgFontStream.end(); assert.equal( (await promise).toString(), fs.readFileSync(join('fixtures', 'expected', 'cleanicons-high.svg'), { encoding: 'utf8', }), ); }); }); describe('Providing bad glyphs', () => { test('should fail when not providing glyph name', async () => { const svgIconStream = fs.createReadStream( join('fixtures', 'icons', 'cleanicons', 'account.svg'), ) as unknown as SVGIconStream; svgIconStream.metadata = { name: undefined as unknown as string, unicode: '\uE001', }; new SVGIcons2SVGFontStream({ round: 1e3 }) .on('error', (err) => { assert.equal(err instanceof Error, true); assert.equal( err.message, 'Please provide a name for the glyph at index 0', ); }) .write(svgIconStream); }); test('should fail when not providing codepoints', async () => { const svgIconStream = fs.createReadStream( join('fixtures', 'icons', 'cleanicons', 'account.svg'), ) as unknown as SVGIconStream; svgIconStream.metadata = { name: 'test', unicode: undefined as unknown as string[], }; new SVGIcons2SVGFontStream({ round: 1e3 }) .on('error', (err) => { assert.equal(err instanceof Error, true); assert.equal( err.message, 'Please provide a codepoint for the glyph "test"', ); }) .write(svgIconStream); }); test('should fail when providing unicode value with duplicates', async () => { const svgIconStream = fs.createReadStream( join('fixtures', 'icons', 'cleanicons', 'account.svg'), ) as unknown as SVGIconStream; svgIconStream.metadata = { name: 'test', unicode: ['\uE002', '\uE002'], }; new SVGIcons2SVGFontStream({ round: 1e3 }) .on('error', (err) => { assert.equal(err instanceof Error, true); assert.equal( err.message, 'Given codepoints for the glyph "test" contain duplicates.', ); }) .write(svgIconStream); }); test('should fail when providing the same codepoint twice', async () => { const svgIconStream = fs.createReadStream( join('fixtures', 'icons', 'cleanicons', 'account.svg'), ) as unknown as SVGIconStream; const svgIconStream2 = fs.createReadStream( join('fixtures', 'icons', 'cleanicons', 'account.svg'), ) as unknown as SVGIconStream; const svgFontStream = new SVGIcons2SVGFontStream({ round: 1e3, }); svgIconStream.metadata = { name: 'test', unicode: '\uE002', }; svgIconStream2.metadata = { name: 'test2', unicode: '\uE002', }; svgFontStream.on('error', (err) => { assert.equal(err instanceof Error, true); assert.equal( err.message, 'The glyph "test2" codepoint seems to be used already elsewhere.', ); }); svgFontStream.write(svgIconStream); svgFontStream.write(svgIconStream2); }); test('should fail when providing the same name twice', async () => { const svgIconStream = fs.createReadStream( join('fixtures', 'icons', 'cleanicons', 'account.svg'), ) as unknown as SVGIconStream; const svgIconStream2 = fs.createReadStream( join('fixtures', 'icons', 'cleanicons', 'account.svg'), ) as unknown as SVGIconStream; const svgFontStream = new SVGIcons2SVGFontStream({ round: 1e3 }); svgIconStream.metadata = { name: 'test', unicode: '\uE001', }; svgIconStream2.metadata = { name: 'test', unicode: '\uE002', }; svgFontStream.on('error', (err) => { assert.equal(err instanceof Error, true); assert.equal(err.message, 'The glyph name "test" must be unique.'); }); svgFontStream.write(svgIconStream); svgFontStream.write(svgIconStream2); }); test('should fail when providing bad pathdata', async () => { const svgIconStream = fs.createReadStream( join('fixtures', 'icons', 'badicons', 'pathdata.svg'), ) as unknown as SVGIconStream; svgIconStream.metadata = { name: 'test', unicode: ['\uE002'], }; new SVGIcons2SVGFontStream({ round: 1e3 }) .on('error', (err) => { assert.equal(err instanceof Error, true); assert.equal( err.message, 'Got an error parsing the glyph "test":' + ' Expected a flag, got "20" at index "23".', ); }) .on('end', () => {}) .write(svgIconStream); }); test('should fail when providing bad XML', async () => { const svgIconStream = streamtest.fromChunks([ Buffer.from('bad'), Buffer.from('xml'), ]) as unknown as SVGIconStream; svgIconStream.metadata = { name: 'test', unicode: ['\uE002'], }; let firstError = true; new SVGIcons2SVGFontStream({ round: 1e3 }) .on('error', (err) => { assert.equal(err instanceof Error, true); if (firstError) { firstError = false; assert.equal( err.message, 'Non-whitespace before first tag.\nLine: 0\nColumn: 1\nChar: b', ); } }) .write(svgIconStream); }); }); async function bufferStream(readableStream: Readable) { return await new Promise<Buffer>((resolve, reject) => { readableStream.pipe( new BufferStream((err, buf) => { if (err) { return reject(err); } resolve(buf); }), ); }); }