From 81a09a9173ec25fbc7360dc569ba0119c1313da3 Mon Sep 17 00:00:00 2001 From: chris48s Date: Wed, 11 Aug 2021 16:37:58 +0100 Subject: [PATCH 01/13] start changelog entry for v4 --- badge-maker/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/badge-maker/CHANGELOG.md b/badge-maker/CHANGELOG.md index 2d99050baff6c..3cf6c3c37f06f 100644 --- a/badge-maker/CHANGELOG.md +++ b/badge-maker/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 4.0.0 [WIP] + +- Drop compatibility with Node 10 + ## 3.3.1 - Improve font measuring in for-the-badge and social styles From b8822c313874669bf1b0b833f1356e48d61fca04 Mon Sep 17 00:00:00 2001 From: chris48s Date: Wed, 11 Aug 2021 16:38:01 +0100 Subject: [PATCH 02/13] migrate Flat/FlatSquare/Plastic to use XmlElement --- badge-maker/lib/badge-renderers.js | 432 +++++++++++++++++------------ badge-maker/lib/xml.js | 27 +- 2 files changed, 281 insertions(+), 178 deletions(-) diff --git a/badge-maker/lib/badge-renderers.js b/badge-maker/lib/badge-renderers.js index 6aba74624ac16..07ff32a8116ec 100644 --- a/badge-maker/lib/badge-renderers.js +++ b/badge-maker/lib/badge-renderers.js @@ -2,14 +2,14 @@ const anafanafo = require('anafanafo') const { brightness } = require('./color') -const { XmlElement, escapeXml } = require('./xml') +const { XmlElement, NullElement, ElementList, escapeXml } = require('./xml') // 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 WIDTH_FONT = '11px Verdana' const socialFontFamily = 'font-family="Helvetica Neue,Helvetica,Arial,sans-serif"' const brightnessThreshold = 0.69 @@ -86,68 +86,6 @@ function renderLogo({ } } -function renderLink({ - link, - height, - textLength, - horizPadding, - leftMargin, - renderedText, -}) { - const rectHeight = height - const rectWidth = textLength + horizPadding * 2 - const rectX = leftMargin > 1 ? leftMargin + 1 : 0 - return ` - - ${renderedText} - ` -} - -function renderText({ - leftMargin, - horizPadding = 0, - content, - link, - height, - verticalMargin = 0, - shadow = false, - color, -}) { - if (!content.length) { - return { renderedText: '', width: 0 } - } - - const textLength = preferredWidthOf(content, { font: '11px Verdana' }) - const escapedContent = escapeXml(content) - - const shadowMargin = 150 + verticalMargin - const textMargin = 140 + verticalMargin - - const outTextLength = 10 * textLength - const x = 10 * (leftMargin + 0.5 * textLength + horizPadding) - - let renderedText = '' - const { textColor, shadowColor } = colorsForBackground(color) - if (shadow) { - renderedText = `` - } - renderedText += `${escapedContent}` - - return { - renderedText: link - ? renderLink({ - link, - height, - textLength, - horizPadding, - leftMargin, - renderedText, - }) - : renderedText, - width: textLength, - } -} - function renderBadge( { links, leftWidth, rightWidth, height, accessibleText }, main @@ -170,10 +108,6 @@ function renderBadge( } class Badge { - static get fontFamily() { - throw new Error('Not implemented') - } - static get height() { throw new Error('Not implemented') } @@ -197,41 +131,25 @@ class Badge { labelColor, }) { const horizPadding = 5 - const { hasLogo, totalLogoWidth, renderedLogo } = renderLogo({ - logo, - badgeHeight: this.constructor.height, - horizPadding, - logoWidth, - logoPadding, - }) + const hasLogo = !!logo + const totalLogoWidth = logoWidth + logoPadding + const accessibleText = createAccessibleText({ label, message }) + const hasLabel = label.length || labelColor if (labelColor == null) { labelColor = '#555' } - - const [leftLink, rightLink] = links - labelColor = hasLabel || hasLogo ? labelColor : color - labelColor = escapeXml(labelColor) - color = escapeXml(color) const labelMargin = totalLogoWidth + 1 - - const { renderedText: renderedLabel, width: labelWidth } = renderText({ - leftMargin: labelMargin, - horizPadding, - content: label, - link: !shouldWrapBodyWithLink({ links }) && leftLink, - height: this.constructor.height, - verticalMargin: this.constructor.verticalMargin, - shadow: this.constructor.shadow, - color: labelColor, - }) - + const labelWidth = label.length + ? preferredWidthOf(label, { font: WIDTH_FONT }) + : 0 const leftWidth = hasLabel ? labelWidth + 2 * horizPadding + totalLogoWidth : 0 + const messageWidth = preferredWidthOf(message, { font: WIDTH_FONT }) let messageMargin = leftWidth - (message.length ? 1 : 0) if (!hasLabel) { if (hasLogo) { @@ -240,18 +158,6 @@ class Badge { messageMargin = messageMargin + 1 } } - - const { renderedText: renderedMessage, width: messageWidth } = renderText({ - leftMargin: messageMargin, - horizPadding, - content: message, - link: rightLink, - height: this.constructor.height, - verticalMargin: this.constructor.verticalMargin, - shadow: this.constructor.shadow, - color, - }) - let rightWidth = messageWidth + 2 * horizPadding if (hasLogo && !hasLabel) { rightWidth += totalLogoWidth + horizPadding - 1 @@ -259,9 +165,13 @@ class Badge { const width = leftWidth + rightWidth - const accessibleText = createAccessibleText({ label, message }) - + this.horizPadding = horizPadding + this.hasLogo = hasLogo + this.labelMargin = labelMargin + this.messageMargin = messageMargin this.links = links + this.labelWidth = labelWidth + this.messageWidth = messageWidth this.leftWidth = leftWidth this.rightWidth = rightWidth this.width = width @@ -270,25 +180,188 @@ class Badge { this.label = label this.message = message this.accessibleText = accessibleText - this.renderedLogo = renderedLogo - this.renderedLabel = renderedLabel - this.renderedMessage = renderedMessage + this.logo = logo + this.logoWidth = logoWidth + + this.logoElement = this.getLogoElement() + this.foregroundGroupElement = this.getForegroundGroupElement() } static render(params) { return new this(params).render() } + getLogoElement() { + const logoHeight = 14 + if (!this.hasLogo) { + return new NullElement() + } + return new XmlElement({ + name: 'image', + attrs: { + x: this.horizPadding, + y: 0.5 * (this.constructor.height - logoHeight), + width: this.logoWidth, + height: logoHeight, + 'xlink:href': this.logo, + }, + }) + } + + getTextElement({ leftMargin, content, link, color, textWidth, linkWidth }) { + if (!content.length) { + return new NullElement() + } + + const { textColor, shadowColor } = colorsForBackground(color) + const x = + FONT_SCALE_UP_FACTOR * (leftMargin + 0.5 * textWidth + this.horizPadding) + + const text = new XmlElement({ + name: 'text', + content: [content], + attrs: { + x, + y: 140 + this.constructor.verticalMargin, + transform: FONT_SCALE_DOWN_VALUE, + fill: textColor, + textLength: FONT_SCALE_UP_FACTOR * textWidth, + }, + }) + + const shadowText = new XmlElement({ + name: 'text', + content: [content], + attrs: { + 'aria-hidden': 'true', + x, + y: 150 + this.constructor.verticalMargin, + fill: shadowColor, + 'fill-opacity': '.3', + transform: FONT_SCALE_DOWN_VALUE, + textLength: FONT_SCALE_UP_FACTOR * textWidth, + }, + }) + const shadow = this.constructor.shadow ? shadowText : new NullElement() + + if (!link) { + return new ElementList({ content: [shadow, text] }) + } + + const rect = new XmlElement({ + name: 'rect', + attrs: { + width: linkWidth, + x: leftMargin > 1 ? leftMargin + 1 : 0, + height: this.constructor.height, + fill: 'rgba(0,0,0,0)', + }, + }) + return new XmlElement({ + name: 'a', + content: [rect, shadow, text], + attrs: { target: '_blank', 'xlink:href': link }, + }) + } + + getLabelElement() { + const leftLink = this.links[0] + return this.getTextElement({ + leftMargin: this.labelMargin, + content: this.label, + link: !shouldWrapBodyWithLink({ links: this.links }) && leftLink, + color: this.labelColor, + textWidth: this.labelWidth, + linkWidth: this.leftWidth, + }) + } + + getMessageElement() { + const rightLink = this.links[1] + return this.getTextElement({ + leftMargin: this.messageMargin, + content: this.message, + link: rightLink, + color: this.messageColor, + textWidth: this.messageWidth, + linkWidth: this.rightWidth, + }) + } + + getClipPathElement(rx) { + return new XmlElement({ + name: 'clipPath', + content: [ + new XmlElement({ + name: 'rect', + attrs: { + width: this.width, + height: this.constructor.height, + rx, + fill: '#fff', + }, + }), + ], + attrs: { id: 'r' }, + }) + } + + getBackgroundGroupElement({ withGradient, attrs }) { + const leftRect = new XmlElement({ + name: 'rect', + attrs: { + width: this.leftWidth, + height: this.constructor.height, + fill: this.labelColor, + }, + }) + const rightRect = new XmlElement({ + name: 'rect', + attrs: { + x: this.leftWidth, + width: this.rightWidth, + height: this.constructor.height, + fill: this.color, + }, + }) + const gradient = new XmlElement({ + name: 'rect', + attrs: { + width: this.width, + height: this.constructor.height, + fill: 'url(#s)', + }, + }) + const content = withGradient + ? [leftRect, rightRect, gradient] + : [leftRect, rightRect] + return new XmlElement({ name: 'g', content, attrs }) + } + + getForegroundGroupElement() { + return new XmlElement({ + name: 'g', + content: [ + this.logoElement, + this.getLabelElement(), + this.getMessageElement(), + ], + attrs: { + fill: '#fff', + 'text-anchor': 'middle', + 'font-family': FONT_FAMILY, + 'text-rendering': 'geometricPrecision', + 'font-size': '110', + }, + }) + } + render() { throw new Error('Not implemented') } } class Plastic extends Badge { - static get fontFamily() { - return fontFamily - } - static get height() { return 18 } @@ -302,6 +375,36 @@ class Plastic extends Badge { } render() { + const gradient = new XmlElement({ + name: 'linearGradient', + content: [ + new XmlElement({ + name: 'stop', + attrs: { offset: '0', 'stop-color': '#fff', 'stop-opacity': '.7' }, + }), + new XmlElement({ + name: 'stop', + attrs: { offset: '.1', 'stop-color': '#aaa', 'stop-opacity': '.1' }, + }), + new XmlElement({ + name: 'stop', + attrs: { offset: '.9', 'stop-color': '#000', 'stop-opacity': '.3' }, + }), + new XmlElement({ + name: 'stop', + attrs: { offset: '1', 'stop-color': '#000', 'stop-opacity': '.5' }, + }), + ], + attrs: { id: 's', x2: '0', y2: '100%' }, + }) + + const clipPath = this.getClipPathElement(4) + + const backgroundGroup = this.getBackgroundGroupElement({ + withGradient: true, + attrs: { 'clip-path': 'url(#r)' }, + }) + return renderBadge( { links: this.links, @@ -310,38 +413,17 @@ class Plastic extends Badge { accessibleText: this.accessibleText, height: this.constructor.height, }, - ` - - - - - - - - - - - - - - - - - - - ${this.renderedLogo} - ${this.renderedLabel} - ${this.renderedMessage} - ` + [ + gradient.render(), + clipPath.render(), + backgroundGroup.render(), + this.foregroundGroupElement.render(), + ].join('') ) } } class Flat extends Badge { - static get fontFamily() { - return fontFamily - } - static get height() { return 20 } @@ -355,6 +437,28 @@ class Flat extends Badge { } render() { + const gradient = new XmlElement({ + name: 'linearGradient', + content: [ + new XmlElement({ + name: 'stop', + attrs: { offset: '0', 'stop-color': '#bbb', 'stop-opacity': '.1' }, + }), + new XmlElement({ + name: 'stop', + attrs: { offset: '1', 'stop-opacity': '.1' }, + }), + ], + attrs: { id: 's', x2: '0', y2: '100%' }, + }) + + const clipPath = this.getClipPathElement(3) + + const backgroundGroup = this.getBackgroundGroupElement({ + withGradient: true, + attrs: { 'clip-path': 'url(#r)' }, + }) + return renderBadge( { links: this.links, @@ -363,36 +467,17 @@ class Flat extends Badge { accessibleText: this.accessibleText, height: this.constructor.height, }, - ` - - - - - - - - - - - - - - - - - ${this.renderedLogo} - ${this.renderedLabel} - ${this.renderedMessage} - ` + [ + gradient.render(), + clipPath.render(), + backgroundGroup.render(), + this.foregroundGroupElement.render(), + ].join('') ) } } class FlatSquare extends Badge { - static get fontFamily() { - return fontFamily - } - static get height() { return 20 } @@ -406,6 +491,11 @@ class FlatSquare extends Badge { } render() { + const backgroundGroup = this.getBackgroundGroupElement({ + withGradient: false, + attrs: { 'shape-rendering': 'crispEdges' }, + }) + return renderBadge( { links: this.links, @@ -414,17 +504,7 @@ class FlatSquare extends Badge { accessibleText: this.accessibleText, height: this.constructor.height, }, - ` - - - - - - - ${this.renderedLogo} - ${this.renderedLabel} - ${this.renderedMessage} - ` + [backgroundGroup.render(), this.foregroundGroupElement.render()].join('') ) } } diff --git a/badge-maker/lib/xml.js b/badge-maker/lib/xml.js index 1d30fa501ea67..d238c7ac35bc7 100644 --- a/badge-maker/lib/xml.js +++ b/badge-maker/lib/xml.js @@ -58,7 +58,7 @@ class XmlElement { if (this.content.length > 0) { const content = this.content .map(function (el) { - if (el instanceof XmlElement) { + if (typeof el.render === 'function') { return el.render() } else { return escapeXml(el) @@ -73,4 +73,27 @@ class XmlElement { } } -module.exports = { escapeXml, stripXmlWhitespace, XmlElement } +class NullElement { + // TODO: we can remove this later + render() { + return '' + } +} + +class ElementList { + constructor({ content = [] }) { + this.content = content + } + + render() { + return this.content.reduce((acc, el) => acc + el.render(), '') + } +} + +module.exports = { + escapeXml, + stripXmlWhitespace, + XmlElement, + NullElement, + ElementList, +} From a820225f64f26933064ee6d06d9b71e112600752 Mon Sep 17 00:00:00 2001 From: chris48s Date: Wed, 11 Aug 2021 16:38:05 +0100 Subject: [PATCH 03/13] move brightnessThreshold into colorsForBackground --- badge-maker/lib/badge-renderers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/badge-maker/lib/badge-renderers.js b/badge-maker/lib/badge-renderers.js index 07ff32a8116ec..82d8c32daf8b9 100644 --- a/badge-maker/lib/badge-renderers.js +++ b/badge-maker/lib/badge-renderers.js @@ -12,13 +12,13 @@ const FONT_FAMILY = 'Verdana,Geneva,DejaVu Sans,sans-serif' const WIDTH_FONT = '11px Verdana' const socialFontFamily = 'font-family="Helvetica Neue,Helvetica,Arial,sans-serif"' -const brightnessThreshold = 0.69 function capitalize(s) { return `${s.charAt(0).toUpperCase()}${s.slice(1)}` } function colorsForBackground(color) { + const brightnessThreshold = 0.69 if (brightness(color) <= brightnessThreshold) { return { textColor: '#fff', shadowColor: '#010101' } } else { From 01f24c40523f55cffdfa49d1ae378aa3468e8f4c Mon Sep 17 00:00:00 2001 From: chris48s Date: Wed, 11 Aug 2021 16:38:08 +0100 Subject: [PATCH 04/13] move old renderLogo function inline into social() this is the only place it is now used --- badge-maker/lib/badge-renderers.js | 46 +++++++++++++++--------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/badge-maker/lib/badge-renderers.js b/badge-maker/lib/badge-renderers.js index 82d8c32daf8b9..67126aca92bd5 100644 --- a/badge-maker/lib/badge-renderers.js +++ b/badge-maker/lib/badge-renderers.js @@ -63,29 +63,6 @@ function renderTitle({ accessibleText, links }) { return hasLink ? '' : `${escapeXml(accessibleText)}` } -function renderLogo({ - logo, - badgeHeight, - horizPadding, - logoWidth = 14, - logoPadding = 0, -}) { - if (logo) { - const logoHeight = 14 - const y = (badgeHeight - logoHeight) / 2 - const x = horizPadding - return { - hasLogo: true, - totalLogoWidth: logoWidth + logoPadding, - renderedLogo: ``, - } - } else { - return { hasLogo: false, totalLogoWidth: 0, renderedLogo: '' } - } -} - function renderBadge( { links, leftWidth, rightWidth, height, accessibleText }, main @@ -523,6 +500,29 @@ function social({ // width can be measured using the correct characters. label = capitalize(label) + function renderLogo({ + logo, + badgeHeight, + horizPadding, + logoWidth = 14, + logoPadding = 0, + }) { + if (logo) { + const logoHeight = 14 + const y = (badgeHeight - logoHeight) / 2 + const x = horizPadding + return { + hasLogo: true, + totalLogoWidth: logoWidth + logoPadding, + renderedLogo: ``, + } + } else { + return { hasLogo: false, totalLogoWidth: 0, renderedLogo: '' } + } + } + const externalHeight = 20 const internalHeight = 19 const labelHorizPadding = 5 From fd14c14e111e7fbe905b4ab4e6a827dbd43235f6 Mon Sep 17 00:00:00 2001 From: chris48s Date: Wed, 11 Aug 2021 16:38:10 +0100 Subject: [PATCH 05/13] use XmlElement in social() --- badge-maker/lib/badge-renderers.js | 322 ++++++++++++++++++++--------- 1 file changed, 224 insertions(+), 98 deletions(-) diff --git a/badge-maker/lib/badge-renderers.js b/badge-maker/lib/badge-renderers.js index 67126aca92bd5..245d77c1b59a5 100644 --- a/badge-maker/lib/badge-renderers.js +++ b/badge-maker/lib/badge-renderers.js @@ -10,8 +10,7 @@ const FONT_SCALE_DOWN_VALUE = 'scale(.1)' const FONT_FAMILY = 'Verdana,Geneva,DejaVu Sans,sans-serif' const WIDTH_FONT = '11px Verdana' -const socialFontFamily = - 'font-family="Helvetica Neue,Helvetica,Arial,sans-serif"' +const SOCIAL_FONT_FAMILY = 'Helvetica Neue,Helvetica,Arial,sans-serif' function capitalize(s) { return `${s.charAt(0).toUpperCase()}${s.slice(1)}` @@ -500,41 +499,13 @@ function social({ // width can be measured using the correct characters. label = capitalize(label) - function renderLogo({ - logo, - badgeHeight, - horizPadding, - logoWidth = 14, - logoPadding = 0, - }) { - if (logo) { - const logoHeight = 14 - const y = (badgeHeight - logoHeight) / 2 - const x = horizPadding - return { - hasLogo: true, - totalLogoWidth: logoWidth + logoPadding, - renderedLogo: ``, - } - } else { - return { hasLogo: false, totalLogoWidth: 0, renderedLogo: '' } - } - } - const externalHeight = 20 const internalHeight = 19 const labelHorizPadding = 5 const messageHorizPadding = 4 const horizGutter = 6 - const { totalLogoWidth, renderedLogo } = renderLogo({ - logo, - badgeHeight: externalHeight, - horizPadding: labelHorizPadding, - logoWidth, - logoPadding, - }) + const logoHeight = 14 + const totalLogoWidth = logoWidth + logoPadding const hasMessage = message.length const font = 'bold 11px Helvetica' @@ -543,75 +514,243 @@ function social({ const labelRectWidth = labelTextWidth + totalLogoWidth + 2 * labelHorizPadding const messageRectWidth = messageTextWidth + 2 * messageHorizPadding - let [leftLink, rightLink] = links - leftLink = escapeXml(leftLink) - rightLink = escapeXml(rightLink) + const [leftLink, rightLink] = links const { hasLeftLink, hasRightLink, hasLink } = hasLinks({ links }) const accessibleText = createAccessibleText({ label, message }) - function renderMessageBubble() { + function getMessageBubble() { + if (!hasMessage) { + return new NullElement() + } + const messageBubbleMainX = labelRectWidth + horizGutter + 0.5 const messageBubbleNotchX = labelRectWidth + horizGutter - return ` - - - - ` + const content = [ + new XmlElement({ + name: 'rect', + attrs: { + x: `${messageBubbleMainX}`, + y: '0.5', + width: `${messageRectWidth}`, + height: `${internalHeight}`, + rx: '2', + fill: '#fafafa', + }, + }), + new XmlElement({ + name: 'rect', + attrs: { + x: `${messageBubbleNotchX}`, + y: '7.5', + width: '0.5', + height: '5', + stroke: '#fafafa', + }, + }), + new XmlElement({ + name: 'path', + attrs: { + d: `M${messageBubbleMainX} 6.5 l-3 3v1 l3 3`, + stroke: 'd5d5d5', + fill: '#fafafa', + }, + }), + ] + return new ElementList({ content }) } - function renderLabelText() { + function getLabelText() { const labelTextX = 10 * (totalLogoWidth + labelTextWidth / 2 + labelHorizPadding) const labelTextLength = 10 * labelTextWidth - const escapedLabel = escapeXml(label) const shouldWrapWithLink = hasLeftLink && !shouldWrapBodyWithLink({ links }) - const rect = `` - const shadow = `` - const text = `${escapedLabel}` + const rect = new XmlElement({ + name: 'rect', + attrs: { + id: 'llink', + stroke: '#d5d5d5', + fill: 'url(#a)', + x: '.5', + y: '.5', + width: labelRectWidth, + height: internalHeight, + rx: 2, + }, + }) + const shadow = new XmlElement({ + name: 'text', + content: [label], + attrs: { + 'aria-hidden': 'true', + x: labelTextX, + y: 150, + fill: '#fff', + transform: 'scale(.1)', + textLength: labelTextLength, + }, + }) + const text = new XmlElement({ + name: 'text', + content: [label], + attrs: { + x: labelTextX, + y: 140, + transform: 'scale(.1)', + textLength: labelTextLength, + }, + }) return shouldWrapWithLink - ? ` - - ${shadow} - ${text} - ${rect} - - ` - : ` - ${rect} - ${shadow} - ${text} - ` - } - - function renderMessageText() { + ? new XmlElement({ + name: 'a', + content: [shadow, text, rect], + attrs: { target: '_blank', 'xlink:href': leftLink }, + }) + : new ElementList({ content: [rect, shadow, text] }) + } + + function getMessageText() { + if (!hasMessage) { + return new NullElement() + } + const messageTextX = 10 * (labelRectWidth + horizGutter + messageRectWidth / 2) const messageTextLength = 10 * messageTextWidth - const escapedMessage = escapeXml(message) - const rect = `` - const shadow = `` - const text = `${escapedMessage}` + const rect = new XmlElement({ + name: 'rect', + attrs: { + width: messageRectWidth + 1, + x: labelRectWidth + horizGutter, + height: internalHeight + 1, + fill: 'rgba(0,0,0,0)', + }, + }) + const shadow = new XmlElement({ + name: 'text', + content: [message], + attrs: { + 'aria-hidden': 'true', + x: messageTextX, + y: 150, + fill: '#fff', + transform: 'scale(.1)', + textLength: messageTextLength, + }, + }) + const text = new XmlElement({ + name: 'text', + content: [message], + attrs: { + id: 'rlink', + x: messageTextX, + y: 140, + transform: 'scale(.1)', + textLength: messageTextLength, + }, + }) return hasRightLink - ? ` - - ${rect} - ${shadow} - ${text} - - ` - : ` - ${shadow} - ${text} - ` + ? new XmlElement({ + name: 'a', + content: [rect, shadow, text], + attrs: { target: '_blank', 'xlink:href': rightLink }, + }) + : new ElementList({ content: [shadow, text] }) } + const style = new XmlElement({ + name: 'style', + content: [ + 'a:hover #llink{fill:url(#b);stroke:#ccc}a:hover #rlink{fill:#4183c4}', + ], + }) + const gradients = new ElementList({ + content: [ + new XmlElement({ + name: 'linearGradient', + content: [ + new XmlElement({ + name: 'stop', + attrs: { + offset: '0', + 'stop-color': '#fcfcfc', + 'stop-opacity': '0', + }, + }), + new XmlElement({ + name: 'stop', + attrs: { offset: '1', 'stop-opacity': '.1' }, + }), + ], + attrs: { id: 'a', x2: '0', y2: '100%' }, + }), + new XmlElement({ + name: 'linearGradient', + content: [ + new XmlElement({ + name: 'stop', + attrs: { offset: '0', 'stop-color': '#ccc', 'stop-opacity': '.1' }, + }), + new XmlElement({ + name: 'stop', + attrs: { offset: '1', 'stop-opacity': '.1' }, + }), + ], + attrs: { id: 'b', x2: '0', y2: '100%' }, + }), + ], + }) + const labelRect = new XmlElement({ + name: 'rect', + attrs: { + stroke: 'none', + fill: '#fcfcfc', + x: '0.5', + y: '0.5', + width: labelRectWidth, + height: internalHeight, + rx: 2, + }, + }) + const messageBubble = getMessageBubble() + const labelText = getLabelText() + const messageText = getMessageText() + const backgroundGroup = new XmlElement({ + name: 'g', + content: [labelRect, messageBubble], + attrs: { stroke: '#d5d5d5' }, + }) + const foregroundGroup = new XmlElement({ + name: 'g', + content: [labelText, messageText], + attrs: { + 'aria-hidden': `${!hasLink}`, + fill: '#333', + 'text-anchor': 'middle', + 'font-family': SOCIAL_FONT_FAMILY, + 'text-rendering': 'geometricPrecision', + 'font-weight': 700, + 'font-size': '110px', + 'line-height': '14px', + }, + }) + const logoElement = !logo + ? new NullElement() + : new XmlElement({ + name: 'image', + attrs: { + x: labelHorizPadding, + y: 0.5 * (externalHeight - logoHeight), + width: logoWidth, + height: logoHeight, + 'xlink:href': logo, + }, + }) + return renderBadge( { links, @@ -620,26 +759,13 @@ function social({ accessibleText, height: externalHeight, }, - ` - - - - - - - - - - - - ${hasMessage ? renderMessageBubble() : ''} - - ${renderedLogo} - - ${renderLabelText()} - ${hasMessage ? renderMessageText() : ''} - - ` + [ + style.render(), + gradients.render(), + backgroundGroup.render(), + logoElement.render(), + foregroundGroup.render(), + ].join('') ) } From 66383ea47670891312eca89dbe3e409b5cc44170 Mon Sep 17 00:00:00 2001 From: chris48s Date: Wed, 11 Aug 2021 16:38:13 +0100 Subject: [PATCH 06/13] don't quote numbers if we don't need to --- badge-maker/lib/badge-renderers.js | 50 +++++++++++++++--------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/badge-maker/lib/badge-renderers.js b/badge-maker/lib/badge-renderers.js index 245d77c1b59a5..e6f79c52ba5ce 100644 --- a/badge-maker/lib/badge-renderers.js +++ b/badge-maker/lib/badge-renderers.js @@ -327,7 +327,7 @@ class Badge { 'text-anchor': 'middle', 'font-family': FONT_FAMILY, 'text-rendering': 'geometricPrecision', - 'font-size': '110', + 'font-size': 110, }, }) } @@ -356,7 +356,7 @@ class Plastic extends Badge { content: [ new XmlElement({ name: 'stop', - attrs: { offset: '0', 'stop-color': '#fff', 'stop-opacity': '.7' }, + attrs: { offset: 0, 'stop-color': '#fff', 'stop-opacity': '.7' }, }), new XmlElement({ name: 'stop', @@ -368,10 +368,10 @@ class Plastic extends Badge { }), new XmlElement({ name: 'stop', - attrs: { offset: '1', 'stop-color': '#000', 'stop-opacity': '.5' }, + attrs: { offset: 1, 'stop-color': '#000', 'stop-opacity': '.5' }, }), ], - attrs: { id: 's', x2: '0', y2: '100%' }, + attrs: { id: 's', x2: 0, y2: '100%' }, }) const clipPath = this.getClipPathElement(4) @@ -418,14 +418,14 @@ class Flat extends Badge { content: [ new XmlElement({ name: 'stop', - attrs: { offset: '0', 'stop-color': '#bbb', 'stop-opacity': '.1' }, + attrs: { offset: 0, 'stop-color': '#bbb', 'stop-opacity': '.1' }, }), new XmlElement({ name: 'stop', - attrs: { offset: '1', 'stop-opacity': '.1' }, + attrs: { offset: 1, 'stop-opacity': '.1' }, }), ], - attrs: { id: 's', x2: '0', y2: '100%' }, + attrs: { id: 's', x2: 0, y2: '100%' }, }) const clipPath = this.getClipPathElement(3) @@ -530,21 +530,21 @@ function social({ new XmlElement({ name: 'rect', attrs: { - x: `${messageBubbleMainX}`, - y: '0.5', - width: `${messageRectWidth}`, - height: `${internalHeight}`, - rx: '2', + x: messageBubbleMainX, + y: 0.5, + width: messageRectWidth, + height: internalHeight, + rx: 2, fill: '#fafafa', }, }), new XmlElement({ name: 'rect', attrs: { - x: `${messageBubbleNotchX}`, - y: '7.5', - width: '0.5', - height: '5', + x: messageBubbleNotchX, + y: 7.5, + width: 0.5, + height: 5, stroke: '#fafafa', }, }), @@ -676,31 +676,31 @@ function social({ new XmlElement({ name: 'stop', attrs: { - offset: '0', + offset: 0, 'stop-color': '#fcfcfc', - 'stop-opacity': '0', + 'stop-opacity': 0, }, }), new XmlElement({ name: 'stop', - attrs: { offset: '1', 'stop-opacity': '.1' }, + attrs: { offset: 1, 'stop-opacity': '.1' }, }), ], - attrs: { id: 'a', x2: '0', y2: '100%' }, + attrs: { id: 'a', x2: 0, y2: '100%' }, }), new XmlElement({ name: 'linearGradient', content: [ new XmlElement({ name: 'stop', - attrs: { offset: '0', 'stop-color': '#ccc', 'stop-opacity': '.1' }, + attrs: { offset: 0, 'stop-color': '#ccc', 'stop-opacity': '.1' }, }), new XmlElement({ name: 'stop', - attrs: { offset: '1', 'stop-opacity': '.1' }, + attrs: { offset: 1, 'stop-opacity': '.1' }, }), ], - attrs: { id: 'b', x2: '0', y2: '100%' }, + attrs: { id: 'b', x2: 0, y2: '100%' }, }), ], }) @@ -709,8 +709,8 @@ function social({ attrs: { stroke: 'none', fill: '#fcfcfc', - x: '0.5', - y: '0.5', + x: 0.5, + y: 0.5, width: labelRectWidth, height: internalHeight, rx: 2, From b577a0328392353b2df1f393261d2a7efaa279cc Mon Sep 17 00:00:00 2001 From: chris48s Date: Wed, 11 Aug 2021 16:38:15 +0100 Subject: [PATCH 07/13] remove intermediate calls to .render() leave everything as XmlElement objects right till the end then make one final call to .render() which cascades aaall the way through the tree --- badge-maker/lib/badge-renderers.js | 86 ++++++++++++++---------------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/badge-maker/lib/badge-renderers.js b/badge-maker/lib/badge-renderers.js index e6f79c52ba5ce..79beb418c85b4 100644 --- a/badge-maker/lib/badge-renderers.js +++ b/badge-maker/lib/badge-renderers.js @@ -2,7 +2,7 @@ const anafanafo = require('anafanafo') const { brightness } = require('./color') -const { XmlElement, NullElement, ElementList, escapeXml } = require('./xml') +const { XmlElement, NullElement, ElementList } = require('./xml') // https://github.com/badges/shields/pull/1132 const FONT_SCALE_UP_FACTOR = 10 @@ -52,35 +52,43 @@ function shouldWrapBodyWithLink({ links }) { return hasLeftLink && !hasRightLink } -function renderAriaAttributes({ accessibleText, links }) { - const { hasLink } = hasLinks({ links }) - return hasLink ? '' : `role="img" aria-label="${escapeXml(accessibleText)}"` -} - -function renderTitle({ accessibleText, links }) { - const { hasLink } = hasLinks({ links }) - return hasLink ? '' : `${escapeXml(accessibleText)}` -} - function renderBadge( { links, leftWidth, rightWidth, height, accessibleText }, - main + content ) { const width = leftWidth + rightWidth - const leftLink = escapeXml(links[0]) - - return ` - - - ${renderTitle({ accessibleText, links })} - ${ - shouldWrapBodyWithLink({ links }) - ? `${main}` - : main - } - ` + const leftLink = links[0] + const { hasLink } = hasLinks({ links }) + + const title = hasLink + ? new NullElement() + : new XmlElement({ name: 'title', content: [accessibleText] }) + + const body = shouldWrapBodyWithLink({ links }) + ? new XmlElement({ + name: 'a', + content, + attrs: { target: '_blank', 'xlink:href': leftLink }, + }) + : new ElementList({ content }) + + const svgAttrs = { + xmlns: 'http://www.w3.org/2000/svg', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + width, + height, + } + if (!hasLink) { + svgAttrs.role = 'img' + svgAttrs['aria-label'] = accessibleText + } + + const svg = new XmlElement({ + name: 'svg', + content: [title, body], + attrs: svgAttrs, + }) + return svg.render() } class Badge { @@ -389,12 +397,7 @@ class Plastic extends Badge { accessibleText: this.accessibleText, height: this.constructor.height, }, - [ - gradient.render(), - clipPath.render(), - backgroundGroup.render(), - this.foregroundGroupElement.render(), - ].join('') + [gradient, clipPath, backgroundGroup, this.foregroundGroupElement] ) } } @@ -443,12 +446,7 @@ class Flat extends Badge { accessibleText: this.accessibleText, height: this.constructor.height, }, - [ - gradient.render(), - clipPath.render(), - backgroundGroup.render(), - this.foregroundGroupElement.render(), - ].join('') + [gradient, clipPath, backgroundGroup, this.foregroundGroupElement] ) } } @@ -480,7 +478,7 @@ class FlatSquare extends Badge { accessibleText: this.accessibleText, height: this.constructor.height, }, - [backgroundGroup.render(), this.foregroundGroupElement.render()].join('') + [backgroundGroup, this.foregroundGroupElement] ) } } @@ -759,13 +757,7 @@ function social({ accessibleText, height: externalHeight, }, - [ - style.render(), - gradients.render(), - backgroundGroup.render(), - logoElement.render(), - foregroundGroup.render(), - ].join('') + [style, gradients, backgroundGroup, logoElement, foregroundGroup] ) } @@ -1000,7 +992,7 @@ function forTheBadge({ accessibleText: createAccessibleText({ label, message }), height: BADGE_HEIGHT, }, - [backgroundGroup.render(), foregroundGroup.render()].join('') + [backgroundGroup, foregroundGroup] ) } From 23123fe7cbef2bb131f07edf145a9d49d3ca1cfc Mon Sep 17 00:00:00 2001 From: chris48s Date: Wed, 11 Aug 2021 16:38:18 +0100 Subject: [PATCH 08/13] factor out code for assembling logo element --- badge-maker/lib/badge-renderers.js | 78 +++++++++++++----------------- 1 file changed, 33 insertions(+), 45 deletions(-) diff --git a/badge-maker/lib/badge-renderers.js b/badge-maker/lib/badge-renderers.js index 79beb418c85b4..da83853cfea42 100644 --- a/badge-maker/lib/badge-renderers.js +++ b/badge-maker/lib/badge-renderers.js @@ -52,6 +52,21 @@ function shouldWrapBodyWithLink({ links }) { return hasLeftLink && !hasRightLink } +function getLogoElement({ logo, horizPadding, badgeHeight, logoWidth }) { + const logoHeight = 14 + if (!logo) return new NullElement() + return new XmlElement({ + name: 'image', + attrs: { + x: horizPadding, + y: 0.5 * (badgeHeight - logoHeight), + width: logoWidth, + height: logoHeight, + 'xlink:href': logo, + }, + }) +} + function renderBadge( { links, leftWidth, rightWidth, height, accessibleText }, content @@ -150,7 +165,6 @@ class Badge { const width = leftWidth + rightWidth this.horizPadding = horizPadding - this.hasLogo = hasLogo this.labelMargin = labelMargin this.messageMargin = messageMargin this.links = links @@ -164,10 +178,13 @@ class Badge { this.label = label this.message = message this.accessibleText = accessibleText - this.logo = logo - this.logoWidth = logoWidth - this.logoElement = this.getLogoElement() + this.logoElement = getLogoElement({ + logo, + horizPadding, + badgeHeight: this.constructor.height, + logoWidth, + }) this.foregroundGroupElement = this.getForegroundGroupElement() } @@ -175,23 +192,6 @@ class Badge { return new this(params).render() } - getLogoElement() { - const logoHeight = 14 - if (!this.hasLogo) { - return new NullElement() - } - return new XmlElement({ - name: 'image', - attrs: { - x: this.horizPadding, - y: 0.5 * (this.constructor.height - logoHeight), - width: this.logoWidth, - height: logoHeight, - 'xlink:href': this.logo, - }, - }) - } - getTextElement({ leftMargin, content, link, color, textWidth, linkWidth }) { if (!content.length) { return new NullElement() @@ -502,7 +502,6 @@ function social({ const labelHorizPadding = 5 const messageHorizPadding = 4 const horizGutter = 6 - const logoHeight = 14 const totalLogoWidth = logoWidth + logoPadding const hasMessage = message.length @@ -736,18 +735,12 @@ function social({ 'line-height': '14px', }, }) - const logoElement = !logo - ? new NullElement() - : new XmlElement({ - name: 'image', - attrs: { - x: labelHorizPadding, - y: 0.5 * (externalHeight - logoHeight), - width: logoWidth, - height: logoHeight, - 'xlink:href': logo, - }, - }) + const logoElement = getLogoElement({ + logo, + horizPadding: labelHorizPadding, + badgeHeight: externalHeight, + logoWidth, + }) return renderBadge( { @@ -772,7 +765,6 @@ function forTheBadge({ }) { 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 @@ -839,15 +831,11 @@ function forTheBadge({ } } - const logoElement = new XmlElement({ - name: 'image', - attrs: { - x: logoMinX, - y: 0.5 * (BADGE_HEIGHT - LOGO_HEIGHT), - width: logoWidth, - height: LOGO_HEIGHT, - 'xlink:href': logo, - }, + const logoElement = getLogoElement({ + logo, + horizPadding: logoMinX, + badgeHeight: BADGE_HEIGHT, + logoWidth, }) function getLabelElement() { @@ -970,7 +958,7 @@ function forTheBadge({ const foregroundGroup = new XmlElement({ name: 'g', content: [ - logo ? logoElement : '', + logoElement, hasLabel ? getLabelElement() : '', getMessageElement(), ], From 138c525899c1b2a4525e14f9134f83d683707567 Mon Sep 17 00:00:00 2001 From: chris48s Date: Wed, 11 Aug 2021 16:38:20 +0100 Subject: [PATCH 09/13] use scale consts in social --- badge-maker/lib/badge-renderers.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/badge-maker/lib/badge-renderers.js b/badge-maker/lib/badge-renderers.js index da83853cfea42..bba5178bc8e66 100644 --- a/badge-maker/lib/badge-renderers.js +++ b/badge-maker/lib/badge-renderers.js @@ -559,8 +559,9 @@ function social({ function getLabelText() { const labelTextX = - 10 * (totalLogoWidth + labelTextWidth / 2 + labelHorizPadding) - const labelTextLength = 10 * labelTextWidth + FONT_SCALE_UP_FACTOR * + (totalLogoWidth + labelTextWidth / 2 + labelHorizPadding) + const labelTextLength = FONT_SCALE_UP_FACTOR * labelTextWidth const shouldWrapWithLink = hasLeftLink && !shouldWrapBodyWithLink({ links }) const rect = new XmlElement({ @@ -584,7 +585,7 @@ function social({ x: labelTextX, y: 150, fill: '#fff', - transform: 'scale(.1)', + transform: FONT_SCALE_DOWN_VALUE, textLength: labelTextLength, }, }) @@ -594,7 +595,7 @@ function social({ attrs: { x: labelTextX, y: 140, - transform: 'scale(.1)', + transform: FONT_SCALE_DOWN_VALUE, textLength: labelTextLength, }, }) @@ -614,8 +615,9 @@ function social({ } const messageTextX = - 10 * (labelRectWidth + horizGutter + messageRectWidth / 2) - const messageTextLength = 10 * messageTextWidth + FONT_SCALE_UP_FACTOR * + (labelRectWidth + horizGutter + messageRectWidth / 2) + const messageTextLength = FONT_SCALE_UP_FACTOR * messageTextWidth const rect = new XmlElement({ name: 'rect', @@ -634,7 +636,7 @@ function social({ x: messageTextX, y: 150, fill: '#fff', - transform: 'scale(.1)', + transform: FONT_SCALE_DOWN_VALUE, textLength: messageTextLength, }, }) @@ -645,7 +647,7 @@ function social({ id: 'rlink', x: messageTextX, y: 140, - transform: 'scale(.1)', + transform: FONT_SCALE_DOWN_VALUE, textLength: messageTextLength, }, }) From f9820392e4375631e0cfe94f50aff0b6a70e70ff Mon Sep 17 00:00:00 2001 From: chris48s Date: Wed, 11 Aug 2021 16:38:23 +0100 Subject: [PATCH 10/13] remove NullElement now we've removed all the intermediate calls to render() we can just use an empty string --- badge-maker/lib/badge-renderers.js | 20 +++++++------------- badge-maker/lib/xml.js | 27 ++++++++++++--------------- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/badge-maker/lib/badge-renderers.js b/badge-maker/lib/badge-renderers.js index bba5178bc8e66..3ba68b619fccd 100644 --- a/badge-maker/lib/badge-renderers.js +++ b/badge-maker/lib/badge-renderers.js @@ -2,7 +2,7 @@ const anafanafo = require('anafanafo') const { brightness } = require('./color') -const { XmlElement, NullElement, ElementList } = require('./xml') +const { XmlElement, ElementList } = require('./xml') // https://github.com/badges/shields/pull/1132 const FONT_SCALE_UP_FACTOR = 10 @@ -54,7 +54,7 @@ function shouldWrapBodyWithLink({ links }) { function getLogoElement({ logo, horizPadding, badgeHeight, logoWidth }) { const logoHeight = 14 - if (!logo) return new NullElement() + if (!logo) return '' return new XmlElement({ name: 'image', attrs: { @@ -76,7 +76,7 @@ function renderBadge( const { hasLink } = hasLinks({ links }) const title = hasLink - ? new NullElement() + ? '' : new XmlElement({ name: 'title', content: [accessibleText] }) const body = shouldWrapBodyWithLink({ links }) @@ -193,9 +193,7 @@ class Badge { } getTextElement({ leftMargin, content, link, color, textWidth, linkWidth }) { - if (!content.length) { - return new NullElement() - } + if (!content.length) return '' const { textColor, shadowColor } = colorsForBackground(color) const x = @@ -226,7 +224,7 @@ class Badge { textLength: FONT_SCALE_UP_FACTOR * textWidth, }, }) - const shadow = this.constructor.shadow ? shadowText : new NullElement() + const shadow = this.constructor.shadow ? shadowText : '' if (!link) { return new ElementList({ content: [shadow, text] }) @@ -517,9 +515,7 @@ function social({ const accessibleText = createAccessibleText({ label, message }) function getMessageBubble() { - if (!hasMessage) { - return new NullElement() - } + if (!hasMessage) return '' const messageBubbleMainX = labelRectWidth + horizGutter + 0.5 const messageBubbleNotchX = labelRectWidth + horizGutter @@ -610,9 +606,7 @@ function social({ } function getMessageText() { - if (!hasMessage) { - return new NullElement() - } + if (!hasMessage) return '' const messageTextX = FONT_SCALE_UP_FACTOR * diff --git a/badge-maker/lib/xml.js b/badge-maker/lib/xml.js index d238c7ac35bc7..1916113c47317 100644 --- a/badge-maker/lib/xml.js +++ b/badge-maker/lib/xml.js @@ -73,27 +73,24 @@ class XmlElement { } } -class NullElement { - // TODO: we can remove this later - render() { - return '' - } -} - +/** + * Convenience class. Sometimes it is useful to return an object that behaves + * like an XmlElement but renders multiple XML tags (not wrapped in a ). + */ class ElementList { constructor({ content = [] }) { this.content = content } render() { - return this.content.reduce((acc, el) => acc + el.render(), '') + return this.content.reduce( + (acc, el) => + typeof el.render === 'function' + ? acc + el.render() + : acc + escapeXml(el), + '' + ) } } -module.exports = { - escapeXml, - stripXmlWhitespace, - XmlElement, - NullElement, - ElementList, -} +module.exports = { escapeXml, stripXmlWhitespace, XmlElement, ElementList } From b6c5a10548a2ac9471cf0785f18f7b64c5870bdf Mon Sep 17 00:00:00 2001 From: chris48s Date: Sat, 21 Aug 2021 19:56:18 +0100 Subject: [PATCH 11/13] Update badge-maker/lib/badge-renderers.js Co-authored-by: Caleb Cartwright --- badge-maker/lib/badge-renderers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/badge-maker/lib/badge-renderers.js b/badge-maker/lib/badge-renderers.js index 3ba68b619fccd..f8eae2bf4062e 100644 --- a/badge-maker/lib/badge-renderers.js +++ b/badge-maker/lib/badge-renderers.js @@ -251,7 +251,7 @@ class Badge { return this.getTextElement({ leftMargin: this.labelMargin, content: this.label, - link: !shouldWrapBodyWithLink({ links: this.links }) && leftLink, + link: leftLink && !shouldWrapBodyWithLink({ links: this.links }), color: this.labelColor, textWidth: this.labelWidth, linkWidth: this.leftWidth, From 05f4f80fd6a37695e03606f811eed7f57af83488 Mon Sep 17 00:00:00 2001 From: chris48s Date: Sat, 21 Aug 2021 20:07:18 +0100 Subject: [PATCH 12/13] Revert "Update badge-maker/lib/badge-renderers.js" This reverts commit b6c5a10548a2ac9471cf0785f18f7b64c5870bdf. --- badge-maker/lib/badge-renderers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/badge-maker/lib/badge-renderers.js b/badge-maker/lib/badge-renderers.js index f8eae2bf4062e..3ba68b619fccd 100644 --- a/badge-maker/lib/badge-renderers.js +++ b/badge-maker/lib/badge-renderers.js @@ -251,7 +251,7 @@ class Badge { return this.getTextElement({ leftMargin: this.labelMargin, content: this.label, - link: leftLink && !shouldWrapBodyWithLink({ links: this.links }), + link: !shouldWrapBodyWithLink({ links: this.links }) && leftLink, color: this.labelColor, textWidth: this.labelWidth, linkWidth: this.leftWidth, From 6df7949adfe6194ff438c7ff7d80194436389e7a Mon Sep 17 00:00:00 2001 From: chris48s Date: Sat, 21 Aug 2021 20:26:44 +0100 Subject: [PATCH 13/13] write leftlink so it doesn't look like a bool --- badge-maker/lib/badge-renderers.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/badge-maker/lib/badge-renderers.js b/badge-maker/lib/badge-renderers.js index 3ba68b619fccd..9d841f228d9fd 100644 --- a/badge-maker/lib/badge-renderers.js +++ b/badge-maker/lib/badge-renderers.js @@ -251,7 +251,9 @@ class Badge { return this.getTextElement({ leftMargin: this.labelMargin, content: this.label, - link: !shouldWrapBodyWithLink({ links: this.links }) && leftLink, + link: !shouldWrapBodyWithLink({ links: this.links }) + ? leftLink + : undefined, color: this.labelColor, textWidth: this.labelWidth, linkWidth: this.leftWidth,