Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

badge-maker

Package Overview
Dependencies
Maintainers
3
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

badge-maker - npm Package Compare versions

Comparing version 3.3.0 to 3.3.1

lib/xml.js

5

CHANGELOG.md
# Changelog
## 3.3.1
- Improve font measuring in for-the-badge and social styles
- Make for-the-badge letter spacing more predictable
## 3.3.0

@@ -4,0 +9,0 @@

468

lib/badge-renderers.js

@@ -5,4 +5,10 @@ 'use strict'

const { brightness } = require('./color')
const { XmlElement, escapeXml } = require('./xml')
const fontFamily = 'font-family="Verdana,Geneva,DejaVu Sans,sans-serif"'
// https://github.com/badges/shields/pull/1132
const FONT_SCALE_UP_FACTOR = 10
const FONT_SCALE_DOWN_VALUE = 'scale(.1)'
const FONT_FAMILY = 'Verdana,Geneva,DejaVu Sans,sans-serif'
const fontFamily = `font-family="${FONT_FAMILY}"`
const socialFontFamily =

@@ -18,23 +24,5 @@ 'font-family="Helvetica Neue,Helvetica,Arial,sans-serif"'

if (brightness(color) <= brightnessThreshold) {
return {
textColor: '#fff',
shadowColor: '#010101',
}
}
return {
textColor: '#333',
shadowColor: '#ccc',
}
}
function escapeXml(s) {
if (s === undefined || typeof s !== 'string') {
return undefined
return { textColor: '#fff', shadowColor: '#010101' }
} else {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
return { textColor: '#333', shadowColor: '#ccc' }
}

@@ -44,17 +32,10 @@ }

function roundUpToOdd(val) {
// Increase chances of pixel grid alignment.
return val % 2 === 0 ? val + 1 : val
}
function preferredWidthOf(str) {
return roundUpToOdd((anafanafo(str) / 10) | 0)
function preferredWidthOf(str, options) {
// Increase chances of pixel grid alignment.
return roundUpToOdd(anafanafo(str, options) | 0)
}
function computeWidths({ label, message }) {
return {
labelWidth: preferredWidthOf(label),
messageWidth: preferredWidthOf(message),
}
}
function createAccessibleText({ label, message }) {

@@ -95,19 +76,16 @@ const labelPrefix = label ? `${label}: ` : ''

}) {
if (!logo) {
if (logo) {
const logoHeight = 14
const y = (badgeHeight - logoHeight) / 2
const x = horizPadding
return {
hasLogo: false,
totalLogoWidth: 0,
renderedLogo: '',
hasLogo: true,
totalLogoWidth: logoWidth + logoPadding,
renderedLogo: `<image x="${x}" y="${y}" width="${logoWidth}" height="${logoHeight}" xlink:href="${escapeXml(
logo
)}"/>`,
}
} else {
return { hasLogo: false, totalLogoWidth: 0, renderedLogo: '' }
}
const logoHeight = 14
const y = (badgeHeight - logoHeight) / 2
const x = horizPadding
return {
hasLogo: true,
totalLogoWidth: logoWidth + logoPadding,
renderedLogo: `<image x="${x}" y="${y}" width="${logoWidth}" height="14" xlink:href="${escapeXml(
logo
)}"/>`,
}
}

@@ -146,3 +124,3 @@

const textLength = preferredWidthOf(content)
const textLength = preferredWidthOf(content, { font: '11px Verdana' })
const escapedContent = escapeXml(content)

@@ -199,6 +177,2 @@

function stripXmlWhitespace(xml) {
return xml.replace(/>\s+/g, '>').replace(/<\s+/g, '<').trim()
}
class Badge {

@@ -310,2 +284,6 @@ static get fontFamily() {

static render(params) {
return new this(params).render()
}
render() {

@@ -461,26 +439,2 @@ throw new Error('Not implemented')

function plastic(params) {
const badge = new Plastic(params)
if (params.minify) {
return stripXmlWhitespace(badge.render())
}
return badge.render()
}
function flat(params) {
const badge = new Flat(params)
if (params.minify) {
return stripXmlWhitespace(badge.render())
}
return badge.render()
}
function flatSquare(params) {
const badge = new FlatSquare(params)
if (params.minify) {
return stripXmlWhitespace(badge.render())
}
return badge.render()
}
function social({

@@ -495,3 +449,2 @@ label,

labelColor = '#555',
minify,
}) {

@@ -504,7 +457,9 @@ // Social label is styled with a leading capital. Convert to caps here so

const internalHeight = 19
const horizPadding = 5
const labelHorizPadding = 5
const messageHorizPadding = 4
const horizGutter = 6
const { totalLogoWidth, renderedLogo } = renderLogo({
logo,
badgeHeight: externalHeight,
horizPadding,
horizPadding: labelHorizPadding,
logoWidth,

@@ -515,11 +470,8 @@ logoPadding,

let { labelWidth, messageWidth } = computeWidths({ label, message })
labelWidth += 10 + totalLogoWidth
messageWidth += 10
messageWidth -= 4
const font = 'bold 11px Helvetica'
const labelTextWidth = preferredWidthOf(label, { font })
const messageTextWidth = preferredWidthOf(message, { font })
const labelRectWidth = labelTextWidth + totalLogoWidth + 2 * labelHorizPadding
const messageRectWidth = messageTextWidth + 2 * messageHorizPadding
const labelTextX = ((labelWidth + totalLogoWidth) / 2) * 10
const labelTextLength = (labelWidth - (10 + totalLogoWidth)) * 10
const escapedLabel = escapeXml(label)
let [leftLink, rightLink] = links

@@ -533,6 +485,6 @@ leftLink = escapeXml(leftLink)

function renderMessageBubble() {
const messageBubbleMainX = labelWidth + 6.5
const messageBubbleNotchX = labelWidth + 6
const messageBubbleMainX = labelRectWidth + horizGutter + 0.5
const messageBubbleNotchX = labelRectWidth + horizGutter
return `
<rect x="${messageBubbleMainX}" y="0.5" width="${messageWidth}" height="${internalHeight}" rx="2" fill="#fafafa"/>
<rect x="${messageBubbleMainX}" y="0.5" width="${messageRectWidth}" height="${internalHeight}" rx="2" fill="#fafafa"/>
<rect x="${messageBubbleNotchX}" y="7.5" width="0.5" height="5" stroke="#fafafa"/>

@@ -544,7 +496,14 @@ <path d="M${messageBubbleMainX} 6.5 l-3 3v1 l3 3" stroke="d5d5d5" fill="#fafafa"/>

function renderLabelText() {
const rect = `<rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="${labelWidth}" height="${internalHeight}" rx="2" />`
const labelTextX =
10 * (totalLogoWidth + labelTextWidth / 2 + labelHorizPadding)
const labelTextLength = 10 * labelTextWidth
const escapedLabel = escapeXml(label)
const shouldWrapWithLink = hasLeftLink && !shouldWrapBodyWithLink({ links })
const rect = `<rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="${labelRectWidth}" height="${internalHeight}" rx="2" />`
const shadow = `<text aria-hidden="true" x="${labelTextX}" y="150" fill="#fff" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>`
const text = `<text x="${labelTextX}" y="140" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>`
if (hasLeftLink && !shouldWrapBodyWithLink({ links })) {
return `
return shouldWrapWithLink
? `
<a target="_blank" xlink:href="${leftLink}">

@@ -556,4 +515,3 @@ ${shadow}

`
}
return `
: `
${rect}

@@ -566,12 +524,15 @@ ${shadow}

function renderMessageText() {
const messageTextX = (labelWidth + messageWidth / 2 + 6) * 10
const messageTextLength = (messageWidth - 8) * 10
const messageTextX =
10 * (labelRectWidth + horizGutter + messageRectWidth / 2)
const messageTextLength = 10 * messageTextWidth
const escapedMessage = escapeXml(message)
const rect = `<rect width="${messageWidth + 1}" x="${
labelWidth + 6
const rect = `<rect width="${messageRectWidth + 1}" x="${
labelRectWidth + horizGutter
}" height="${internalHeight + 1}" fill="rgba(0,0,0,0)" />`
const shadow = `<text aria-hidden="true" x="${messageTextX}" y="150" fill="#fff" transform="scale(.1)" textLength="${messageTextLength}">${escapedMessage}</text>`
const text = `<text id="rlink" x="${messageTextX}" y="140" transform="scale(.1)" textLength="${messageTextLength}">${escapedMessage}</text>`
if (hasRightLink) {
return `
return hasRightLink
? `
<a target="_blank" xlink:href="${rightLink}">

@@ -583,4 +544,3 @@ ${rect}

`
}
return `
: `
${shadow}

@@ -591,7 +551,7 @@ ${text}

const badge = renderBadge(
return renderBadge(
{
links,
leftWidth: labelWidth + 1,
rightWidth: hasMessage ? messageWidth + 6 : 0,
leftWidth: labelRectWidth + 1,
rightWidth: hasMessage ? horizGutter + messageRectWidth : 0,
accessibleText,

@@ -611,3 +571,3 @@ height: externalHeight,

<g stroke="#d5d5d5">
<rect stroke="none" fill="#fcfcfc" x="0.5" y="0.5" width="${labelWidth}" height="${internalHeight}" rx="2"/>
<rect stroke="none" fill="#fcfcfc" x="0.5" y="0.5" width="${labelRectWidth}" height="${internalHeight}" rx="2"/>
${hasMessage ? renderMessageBubble() : ''}

@@ -622,7 +582,2 @@ </g>

)
if (minify) {
return stripXmlWhitespace(badge)
}
return badge
}

@@ -636,126 +591,237 @@

logoWidth,
logoPadding,
color = '#4c1',
labelColor,
minify,
}) {
// For the Badge is styled in all caps. Convert to caps here so widths can
// be measured using the correct characters.
const FONT_SIZE = 10
const BADGE_HEIGHT = 28
const LOGO_HEIGHT = 14
const TEXT_MARGIN = 12
const LOGO_MARGIN = 9
const LOGO_TEXT_GUTTER = 6
const LETTER_SPACING = 1.25
// Prepare content. For the Badge is styled in all caps. It's important to to
// convert to uppercase first so the widths can be measured using the correct
// symbols.
label = label.toUpperCase()
message = message.toUpperCase()
let { labelWidth, messageWidth } = computeWidths({ label, message })
const height = 28
const hasLabel = label.length || labelColor
if (labelColor == null) {
labelColor = '#555'
const [leftLink, rightLink] = links
const { hasLeftLink, hasRightLink } = hasLinks({ links })
const outLabelColor = labelColor || '#555'
// Compute text width.
// TODO: This really should count the symbols rather than just using `.length`.
// https://mathiasbynens.be/notes/javascript-unicode
// This is not using `preferredWidthOf()` as it tends to produce larger
// inconsistencies in the letter spacing. The badges look fine, however if you
// replace `textLength` with `letterSpacing` in the rendered SVG, you can see
// the discrepancy. Ideally, swapping out `textLength` for `letterSpacing`
// should not affect the appearance.
const labelTextWidth = label.length
? (anafanafo(label, { font: `${FONT_SIZE}px Verdana` }) | 0) +
LETTER_SPACING * label.length
: 0
const messageTextWidth = message.length
? (anafanafo(message, { font: `bold ${FONT_SIZE}px Verdana` }) | 0) +
LETTER_SPACING * message.length
: 0
// Compute horizontal layout.
// If a `labelColor` is set, the logo is always set against it, even when
// there is no label. When `needsLabelRect` is true, render a label rect and a
// message rect; when false, only a message rect.
const hasLabel = Boolean(label.length)
const needsLabelRect = hasLabel || (logo && labelColor)
let logoMinX, labelTextMinX
if (logo) {
logoMinX = LOGO_MARGIN
labelTextMinX = logoMinX + logoWidth + LOGO_TEXT_GUTTER
} else {
labelTextMinX = TEXT_MARGIN
}
const horizPadding = 9
const { hasLogo, totalLogoWidth, renderedLogo } = renderLogo({
logo,
badgeHeight: height,
horizPadding,
logoWidth,
logoPadding,
})
labelWidth += 10 + totalLogoWidth
if (label.length) {
labelWidth += 10 + label.length * 1.5
} else if (hasLogo) {
let labelRectWidth, messageTextMinX, messageRectWidth
if (needsLabelRect) {
if (hasLabel) {
labelWidth += 7
labelRectWidth = labelTextMinX + labelTextWidth + TEXT_MARGIN
} else {
labelWidth -= 7
labelRectWidth = 2 * LOGO_MARGIN + logoWidth
}
messageTextMinX = labelRectWidth + TEXT_MARGIN
messageRectWidth = 2 * TEXT_MARGIN + messageTextWidth
} else {
labelWidth -= 11
if (logo) {
messageTextMinX = TEXT_MARGIN + logoWidth + LOGO_TEXT_GUTTER
messageRectWidth =
2 * TEXT_MARGIN + logoWidth + LOGO_TEXT_GUTTER + messageTextWidth
} else {
messageTextMinX = TEXT_MARGIN
messageRectWidth = 2 * TEXT_MARGIN + messageTextWidth
}
}
messageWidth += 10
messageWidth += 10 + message.length * 2
const leftWidth = hasLogo && !hasLabel ? 0 : labelWidth
const rightWidth =
hasLogo && !hasLabel ? messageWidth + labelWidth : messageWidth
const logoElement = new XmlElement({
name: 'image',
attrs: {
x: logoMinX,
y: 0.5 * (BADGE_HEIGHT - LOGO_HEIGHT),
width: logoWidth,
height: LOGO_HEIGHT,
'xlink:href': logo,
},
})
labelColor = hasLabel || hasLogo ? labelColor : color
function getLabelElement() {
const { textColor } = colorsForBackground(outLabelColor)
const midX = labelTextMinX + 0.5 * labelTextWidth
const text = new XmlElement({
name: 'text',
content: [label],
attrs: {
transform: FONT_SCALE_DOWN_VALUE,
x: FONT_SCALE_UP_FACTOR * midX,
y: 175,
textLength: FONT_SCALE_UP_FACTOR * labelTextWidth,
fill: textColor,
},
})
color = escapeXml(color)
labelColor = escapeXml(labelColor)
let [leftLink, rightLink] = links
leftLink = escapeXml(leftLink)
rightLink = escapeXml(rightLink)
const { hasLeftLink, hasRightLink } = hasLinks({ links })
const accessibleText = createAccessibleText({ label, message })
function renderLabelText() {
const { textColor } = colorsForBackground(labelColor)
const labelTextX = ((labelWidth + totalLogoWidth) / 2) * 10
const labelTextLength = (labelWidth - (24 + totalLogoWidth)) * 10
const escapedLabel = escapeXml(label)
const text = `<text fill="${textColor}" x="${labelTextX}" y="175" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>`
if (hasLeftLink && !shouldWrapBodyWithLink({ links })) {
return `
<a target="_blank" xlink:href="${leftLink}">
<rect width="${leftWidth}" height="${height}" fill="rgba(0,0,0,0)"/>
${text}
</a>
`
const rect = new XmlElement({
name: 'rect',
attrs: {
width: labelRectWidth,
height: BADGE_HEIGHT,
fill: 'rgba(0,0,0,0)',
},
})
return new XmlElement({
name: 'a',
content: [rect, text],
attrs: {
target: '_blank',
'xlink:href': leftLink,
},
})
} else {
return text
}
return text
}
function renderMessageText() {
function getMessageElement() {
const { textColor } = colorsForBackground(color)
const text = `<text fill="${textColor}" x="${
(labelWidth + messageWidth / 2) * 10
}" y="175" font-weight="bold" transform="scale(.1)" textLength="${
(messageWidth - 24) * 10
}">
${escapeXml(message)}</text>`
const midX = messageTextMinX + 0.5 * messageTextWidth
const text = new XmlElement({
name: 'text',
content: [message],
attrs: {
transform: FONT_SCALE_DOWN_VALUE,
x: FONT_SCALE_UP_FACTOR * midX,
y: 175,
textLength: FONT_SCALE_UP_FACTOR * messageTextWidth,
fill: textColor,
'font-weight': 'bold',
},
})
if (hasRightLink) {
return `
<a target="_blank" xlink:href="${rightLink}">
<rect width="${rightWidth}" height="${height}" x="${labelWidth}" fill="rgba(0,0,0,0)"/>
${text}
</a>
`
const rect = new XmlElement({
name: 'rect',
attrs: {
width: messageRectWidth,
height: BADGE_HEIGHT,
x: labelRectWidth || 0,
fill: 'rgba(0,0,0,0)',
},
})
return new XmlElement({
name: 'a',
content: [rect, text],
attrs: {
target: '_blank',
'xlink:href': rightLink,
},
})
} else {
return text
}
return text
}
const badge = renderBadge(
let backgroundContent
if (needsLabelRect) {
backgroundContent = [
new XmlElement({
name: 'rect',
attrs: {
width: labelRectWidth,
height: BADGE_HEIGHT,
fill: outLabelColor,
},
}),
new XmlElement({
name: 'rect',
attrs: {
x: labelRectWidth,
width: messageRectWidth,
height: BADGE_HEIGHT,
fill: color,
},
}),
]
} else {
backgroundContent = [
new XmlElement({
name: 'rect',
attrs: {
width: messageRectWidth,
height: BADGE_HEIGHT,
fill: color,
},
}),
]
}
const backgroundGroup = new XmlElement({
name: 'g',
content: backgroundContent,
attrs: {
'shape-rendering': 'crispEdges',
},
})
const foregroundGroup = new XmlElement({
name: 'g',
content: [
logo ? logoElement : '',
hasLabel ? getLabelElement() : '',
getMessageElement(),
],
attrs: {
fill: '#fff',
'text-anchor': 'middle',
'font-family': FONT_FAMILY,
'text-rendering': 'geometricPrecision',
'font-size': FONT_SCALE_UP_FACTOR * FONT_SIZE,
},
})
// Render.
return renderBadge(
{
links,
leftWidth,
rightWidth,
accessibleText,
height,
leftWidth: labelRectWidth || 0,
rightWidth: messageRectWidth,
accessibleText: createAccessibleText({ label, message }),
height: BADGE_HEIGHT,
},
`
<g shape-rendering="crispEdges">
<rect width="${leftWidth}" height="${height}" fill="${labelColor}"/>
<rect x="${leftWidth}" width="${rightWidth}" height="${height}" fill="${color}"/>
</g>
<g fill="#fff" text-anchor="middle" ${fontFamily} text-rendering="geometricPrecision" font-size="100">
${renderedLogo}
${hasLabel ? renderLabelText() : ''}
${renderMessageText()}
</g>`
[backgroundGroup.render(), foregroundGroup.render()].join('')
)
if (minify) {
return stripXmlWhitespace(badge)
}
return badge
}
module.exports = {
plastic,
flat,
plastic: params => Plastic.render(params),
flat: params => Flat.render(params),
'flat-square': params => FlatSquare.render(params),
social,
'flat-square': flatSquare,
'for-the-badge': forTheBadge,
}

@@ -54,10 +54,4 @@ 'use strict'

// convert "public" format to "internal" format
cleaned.text = [cleaned.label || '', cleaned.message]
delete cleaned.label
delete cleaned.message
if ('style' in cleaned) {
cleaned.template = cleaned.style
delete cleaned.style
}
// Legacy.
cleaned.label = cleaned.label || ''

@@ -64,0 +58,0 @@ return cleaned

@@ -5,2 +5,3 @@ 'use strict'

const badgeRenderers = require('./badge-renderers')
const { stripXmlWhitespace } = require('./xml')

@@ -13,4 +14,5 @@ /*

format,
template = 'flat',
text,
style = 'flat',
label,
message,
color,

@@ -24,6 +26,5 @@ labelColor,

// String coercion and whitespace removal.
text = text.map(value => `${value}`.trim())
label = `${label}`.trim()
message = `${message}`.trim()
const [label, message] = text
// This ought to be the responsibility of the server, not `makeBadge`.

@@ -45,5 +46,5 @@ if (format === 'json') {

const render = badgeRenderers[template]
const render = badgeRenderers[style]
if (!render) {
throw new Error(`Unknown template: '${template}'`)
throw new Error(`Unknown badge style: '${style}'`)
}

@@ -53,14 +54,15 @@

return render({
label,
message,
links,
logo,
logoPosition,
logoWidth,
logoPadding: logo && label.length ? 3 : 0,
color: toSvgColor(color),
labelColor: toSvgColor(labelColor),
minify: true,
})
return stripXmlWhitespace(
render({
label,
message,
links,
logo,
logoPosition,
logoWidth,
logoPadding: logo && label.length ? 3 : 0,
color: toSvgColor(color),
labelColor: toSvgColor(labelColor),
})
)
}
{
"name": "badge-maker",
"version": "3.3.0",
"version": "3.3.1",
"description": "Shields.io badge library",

@@ -38,3 +38,3 @@ "keywords": [

"dependencies": {
"anafanafo": "^1.0.0",
"anafanafo": "2.0.0",
"css-color-converter": "^2.0.0"

@@ -41,0 +41,0 @@ },

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc