feat(GUI): separate svg path and content attributes (#1677)

We separate the SVG component path and content into attributes
`paths` and `contents` which take lists of strings that are
tried until one succeeds. `contents` takes precedence over `paths`,
i.e. it is tried first.

Change-Type: patch
Changelog-Entry: Separate SVG component's path and content attributes.
This commit is contained in:
Benedict Aas 2018-02-20 09:51:13 +00:00 committed by GitHub
parent 91719435d9
commit 8b577ca12f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 123 additions and 41 deletions

View File

@ -29,6 +29,7 @@ const propTypes = require('prop-types')
const react2angular = require('react2angular').react2angular
const path = require('path')
const fs = require('fs')
const analytics = require('../modules/analytics')
const MODULE_NAME = 'Etcher.Components.SVGIcon'
const angularSVGIcon = angular.module(MODULE_NAME, [])
@ -37,6 +38,29 @@ const DEFAULT_SIZE = '40px'
const domParser = new window.DOMParser()
/**
* @summary Try to parse SVG contents and return it data encoded
*
* @param {String} contents - SVG XML contents
* @returns {String|null}
*
* @example
* const encodedSVG = tryParseSVGContents('<svg><path></path></svg>')
*
* img.src = encodedSVG
*/
const tryParseSVGContents = (contents) => {
const doc = domParser.parseFromString(contents, 'image/svg+xml')
const parserError = doc.querySelector('parsererror')
const svg = doc.querySelector('svg')
if (!parserError && svg) {
return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`
}
return null
}
/**
* @summary SVG element that takes both filepaths and file contents
* @type {Object}
@ -59,31 +83,52 @@ class SVGIcon extends react.Component {
// eslint-disable-next-line no-underscore-dangle
: global.__dirname
// This means the path to the icon should be
// relative to *this directory*.
// TODO: There might be a way to compute the path
// relatively to the `index.html`.
const imagePath = path.join(baseDirectory, 'assets', this.props.path)
let svgData = ''
let contents = ''
_.find(this.props.contents, (content) => {
const attempt = tryParseSVGContents(content)
if (_.startsWith(this.props.path, '<')) {
contents = this.props.path
} else {
contents = fs.readFileSync(imagePath, {
encoding: 'utf8'
if (attempt) {
svgData = attempt
return true
}
return false
})
if (!svgData) {
_.find(this.props.paths, (relativePath) => {
// This means the path to the icon should be
// relative to *this directory*.
// TODO: There might be a way to compute the path
// relatively to the `index.html`.
const imagePath = path.join(baseDirectory, 'assets', relativePath)
const contents = _.attempt(() => {
return fs.readFileSync(imagePath, {
encoding: 'utf8'
})
})
if (_.isError(contents)) {
analytics.logException(contents)
return false
}
const parsed = _.attempt(tryParseSVGContents, contents)
if (parsed) {
svgData = parsed
return true
}
return false
})
}
const width = this.props.width || DEFAULT_SIZE
const height = this.props.height || DEFAULT_SIZE
const doc = domParser.parseFromString(contents, 'image/svg+xml')
const parserError = doc.querySelector('parsererror')
const svg = doc.querySelector('svg')
const svgXml = svg && _.isNil(parserError) ? svg.outerHTML : ''
const svgData = `data:image/svg+xml,${encodeURIComponent(svgXml)}`
return react.createElement('img', {
className: 'svg-icon',
style: {
@ -108,9 +153,14 @@ class SVGIcon extends react.Component {
SVGIcon.propTypes = {
/**
* @summary SVG contents or path to the resource
* @summary Paths to SVG files to be tried in succession if any fails
*/
path: propTypes.string.isRequired,
paths: propTypes.array,
/**
* @summary List of embedded SVG contents to be tried in succession if any fails
*/
contents: propTypes.array,
/**
* @summary SVG image width unit

View File

@ -38,7 +38,7 @@
ng-hide="state.currentName === 'success'">
<span os-open-external="https://etcher.io?ref=etcher_footer"
tabindex="100">
<svg-icon path="'../../assets/etcher.svg'"
<svg-icon paths="[ '../../assets/etcher.svg' ]"
width="'83px'"
height="'13px'"></svg-icon>
</span>
@ -51,7 +51,7 @@
<span os-open-external="https://resin.io?ref=etcher"
tabindex="102">
<svg-icon path="'../../assets/resin.svg'"
<svg-icon paths="[ '../../assets/resin.svg' ]"
width="'79px'"
height="'23px'"></svg-icon>
</span>

View File

@ -16,7 +16,7 @@
<div class="fallback-banner">
<div class="caption caption-big">Thanks for using
<span os-open-external="https://etcher.io?ref=etcher_offline_banner">
<svg-icon path="'../../assets/etcher.svg'"
<svg-icon paths="[ '../../assets/etcher.svg' ]"
width="'150px'"
height="'auto'">
</svg-icon>
@ -24,12 +24,12 @@
</div>
<div class="caption caption-small">
made with
<svg-icon path="'../../assets/love.svg'"
<svg-icon paths="[ '../../assets/love.svg' ]"
width="'20px'"
height="'auto'"></svg-icon>
by
<span os-open-external="https://resin.io?ref=etcher_success">
<svg-icon path="'../../assets/resin.svg'"
<svg-icon paths="[ '../../assets/resin.svg' ]"
width="'100px'"
height="'auto'">
</svg-icon>

View File

@ -3,7 +3,7 @@
<div class="box text-center relative" os-dropzone="image.selectImageByPath($file)">
<div class="center-block">
<svg-icon path="main.selection.getImageLogo() || '../../assets/image.svg'"></svg-icon>
<svg-icon contents="main.selection.getImageLogo()" paths="[ '../../assets/image.svg' ]"></svg-icon>
</div>
<div class="space-vertical-large">
@ -44,7 +44,7 @@
<div class="step-border-right" ng-disabled="main.shouldFlashStepBeDisabled()"></div>
<div class="center-block">
<svg-icon path="'../../assets/drive.svg'"
<svg-icon paths="[ '../../assets/drive.svg' ]"
disabled="main.shouldDriveStepBeDisabled()"></svg-icon>
</div>
@ -87,7 +87,7 @@
<div class="col-xs" ng-controller="FlashController as flash">
<div class="box text-center">
<div class="center-block">
<svg-icon path="'../../assets/flash.svg'"
<svg-icon paths="[ '../../assets/flash.svg' ]"
disabled="main.shouldFlashStepBeDisabled()"></svg-icon>
</div>

View File

@ -35,11 +35,12 @@ describe('Browser: SVGIcon', function () {
beforeEach(angular.mock.inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_
$rootScope = _$rootScope_
this.iconPath = '../../../lib/gui/assets/etcher.svg'
}))
it('should inline the svg contents in the element', function () {
const icon = '../../../gui/assets/etcher.svg'
let iconContents = _.split(fs.readFileSync(path.join(__dirname, '../../../lib/gui/assets/etcher.svg'), {
let iconContents = _.split(fs.readFileSync(path.join(__dirname, this.iconPath), {
encoding: 'utf8'
}), /\r?\n/)
@ -48,7 +49,7 @@ describe('Browser: SVGIcon', function () {
iconContents[0] = `<!--${iconContents[0].slice(1, iconContents[0].length - 1)}-->`
iconContents = iconContents.join('\n')
const element = $compile(`<svg-icon path="'${icon}'">Resin.io</svg-icon>`)($rootScope)
const element = $compile(`<svg-icon paths="['${this.iconPath}']">Resin.io</svg-icon>`)($rootScope)
$rootScope.$digest()
// We parse the SVGs to get rid of discrepancies caused by string differences
@ -62,12 +63,47 @@ describe('Browser: SVGIcon', function () {
m.chai.expect(compiledDoc.outerHTML).to.equal(originalDoc.outerHTML)
})
it('should accept an SVG in the path attribute', function () {
it('should try next path if previous was not found', function () {
let iconContents = _.split(fs.readFileSync(path.join(__dirname, this.iconPath), {
encoding: 'utf8'
}), /\r?\n/)
// Injecting XML as HTML causes the XML header to be commented out.
// Modify here to ease assertions later on.
iconContents[0] = `<!--${iconContents[0].slice(1, iconContents[0].length - 1)}-->`
iconContents = iconContents.join('\n')
const element = $compile(`<svg-icon paths="['i-dont-exist', '${this.iconPath}']">Resin.io</svg-icon>`)($rootScope)
$rootScope.$digest()
// We parse the SVGs to get rid of discrepancies caused by string differences
// in the outputs; the XML trees are still equal, as proven here.
const originalSVGParser = new DOMParser()
const originalDoc = originalSVGParser.parseFromString(iconContents, 'image/svg+xml')
const compiledSVGParser = new DOMParser()
const compiledContents = decodeURIComponent(element.children()[0].src.substr(19))
const compiledDoc = compiledSVGParser.parseFromString(compiledContents, 'image/svg+xml')
m.chai.expect(compiledDoc.outerHTML).to.equal(originalDoc.outerHTML)
})
it('should accept an SVG in the contents attribute', function () {
const iconContents = '<svg><rect x="10" y="10" height="100" width="100" style="stroke:red;fill:blue;"/></svg>'
const imgData = `data:image/svg+xml,${encodeURIComponent(iconContents)}`
$rootScope.iconContents = iconContents
const element = $compile('<svg-icon path="iconContents">Resin.io</svg-icon>')($rootScope)
const element = $compile('<svg-icon contents="[iconContents]">Resin.io</svg-icon>')($rootScope)
$rootScope.$digest()
m.chai.expect(element.children().attr('src')).to.equal(imgData)
})
it('should prioritise the contents attribute over the paths attribute', function () {
const iconContents = '<svg><rect x="10" y="10" height="100" width="100" style="stroke:red;fill:blue;"/></svg>'
const imgData = `data:image/svg+xml,${encodeURIComponent(iconContents)}`
$rootScope.iconContents = iconContents
const svg = `<svg-icon contents="[iconContents]" paths="[ '${this.iconPath}' ]">Resin.io</svg-icon>`
const element = $compile(svg)($rootScope)
$rootScope.$digest()
m.chai.expect(element.children().attr('src')).to.equal(imgData)
})
@ -75,33 +111,29 @@ describe('Browser: SVGIcon', function () {
it('should use an empty src if there is a parsererror', function () {
// The following is invalid, because there's no closing tag for `foreignObject`
const iconContents = '<svg><foreignObject></svg>'
const imgData = 'data:image/svg+xml,'
$rootScope.iconContents = iconContents
const element = $compile('<svg-icon path="iconContents">Resin.io</svg-icon>')($rootScope)
const element = $compile('<svg-icon contents="[iconContents]">Resin.io</svg-icon>')($rootScope)
$rootScope.$digest()
m.chai.expect(element.children().attr('src')).to.equal(imgData)
m.chai.expect(element.children().attr('src')).to.be.empty
})
it('should default the size to 40x40 pixels', function () {
const icon = '../../../gui/assets/etcher.svg'
const element = $compile(`<svg-icon path="'${icon}'">Resin.io</svg-icon>`)($rootScope)
const element = $compile(`<svg-icon paths="[ '${this.iconPath}' ]">Resin.io</svg-icon>`)($rootScope)
$rootScope.$digest()
m.chai.expect(element.children().css('width')).to.equal('40px')
m.chai.expect(element.children().css('height')).to.equal('40px')
})
it('should be able to set a custom width', function () {
const icon = '../../../gui/assets/etcher.svg'
const element = $compile(`<svg-icon path="'${icon}'" width="'20px'">Resin.io</svg-icon>`)($rootScope)
const element = $compile(`<svg-icon paths="[ '${this.iconPath}' ]" width="'20px'">Resin.io</svg-icon>`)($rootScope)
$rootScope.$digest()
m.chai.expect(element.children().css('width')).to.equal('20px')
m.chai.expect(element.children().css('height')).to.equal('40px')
})
it('should be able to set a custom height', function () {
const icon = '../../../gui/assets/etcher.svg'
const element = $compile(`<svg-icon path="'${icon}'" height="'20px'">Resin.io</svg-icon>`)($rootScope)
const element = $compile(`<svg-icon paths="[ '${this.iconPath}' ]" height="'20px'">Resin.io</svg-icon>`)($rootScope)
$rootScope.$digest()
m.chai.expect(element.children().css('width')).to.equal('40px')
m.chai.expect(element.children().css('height')).to.equal('20px')