Skip to content

Commit ed3edf9

Browse files
committed
Initial commit
0 parents  commit ed3edf9

3 files changed

Lines changed: 196 additions & 0 deletions

File tree

icon@2x.png

3.35 KB
Loading

main.js

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Copyright (c) 2018 Peter Flynn
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a
5+
* copy of this software and associated documentation files (the "Software"),
6+
* to deal in the Software without restriction, including without limitation
7+
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
8+
* and/or sell copies of the Software, and to permit persons to whom the
9+
* Software is furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20+
* DEALINGS IN THE SOFTWARE.
21+
*/
22+
23+
/*jshint esnext: true */
24+
/*globals console, require, exports */
25+
26+
var sg = require("scenegraph");
27+
var clipboard = require("clipboard");
28+
29+
function styleToWeight(fontStyle) {
30+
if (fontStyle.match(/\bBold\b/i)) {
31+
return "bold";
32+
} else if (fontStyle.match(/\bBlack\b/i) || fontStyle.match(/\bHeavy\b/i)) { // TODO: "extra bold"? (move precedence higher if so)
33+
return 900;
34+
} else if (fontStyle.match(/\bSemi[- ]?bold\b/i) || fontStyle.match(/\bDemi[- ]?bold\b/i)) {
35+
return 600;
36+
} else if (fontStyle.match(/\bMedium\b/i)) {
37+
return 500;
38+
} else if (fontStyle.match(/\bLight\b/i)) {
39+
return 300;
40+
} else if (fontStyle.match(/\bUltra[- ]light\b/i)) {
41+
return 200;
42+
} else {
43+
return "normal";
44+
}
45+
}
46+
47+
function styleIsItalic(fontStyle) {
48+
return (fontStyle.match(/\bItalic\b/i) || fontStyle.match(/\bOblique\b/i));
49+
}
50+
51+
function colorToCSS(solidColor) {
52+
if (solidColor.a !== 255) {
53+
return `rgba(${solidColor.r}, ${solidColor.g}, ${solidColor.b}, ${num(solidColor.a/255)})`;
54+
} else {
55+
return solidColor.toHex();
56+
}
57+
}
58+
59+
function num(value) {
60+
return Math.round(value * 100) / 100;
61+
}
62+
// TODO: omit "px" suffix from 0s
63+
64+
function eq(num1, num2) {
65+
return (Math.abs(num1 - num2) < 0.001);
66+
}
67+
68+
function copy(selection) {
69+
var node = selection.items[0];
70+
if (!node) {
71+
return;
72+
}
73+
74+
var css = "";
75+
76+
// Size - for anything except point text
77+
if (!(node instanceof sg.Text && !node.areaBox)) {
78+
var bounds = node.localBounds;
79+
css += `width: ${num(bounds.width)}px;\n`;
80+
css += `height: ${num(bounds.height)}px;\n`;
81+
}
82+
83+
// Corner metrics
84+
if (node.hasRoundedCorners) {
85+
var corners = node.effectiveCornerRadii;
86+
var tlbr = eq(corners.topLeft, corners.bottomRight);
87+
var trbl = eq(corners.topRight, corners.bottomLeft);
88+
if (tlbr && trbl) {
89+
if (eq(corners.topLeft, corners.topRight)) {
90+
css += `border-radius: ${num(corners.topLeft)}px;\n`;
91+
} else {
92+
css += `border-radius: ${num(corners.topLeft)}px ${num(corners.topRight)}px;\n`;
93+
}
94+
} else {
95+
css += `border-radius: ${num(corners.topLeft)}px ${num(corners.topRight)}px ${num(corners.bottomRight)}px ${num(corners.bottomLeft)}px;\n`;
96+
}
97+
}
98+
99+
// Text styles
100+
if (node instanceof sg.Text) {
101+
var textStyles = node.styleRanges[0];
102+
if (textStyles.fontFamily.includes(" ")) {
103+
css += `font-family: "${textStyles.fontFamily}";\n`;
104+
} else {
105+
css += `font-family: ${textStyles.fontFamily};\n`;
106+
}
107+
css += `font-weight: ${styleToWeight(textStyles.fontStyle)};\n`;
108+
if (styleIsItalic(textStyles.fontStyle)) {
109+
css += `font-style: italic;\n`;
110+
}
111+
if (textStyles.underline) {
112+
css += `text-decoration: underline;\n`;
113+
}
114+
css += `font-size: ${num(textStyles.fontSize)}px;\n`;
115+
if (textStyles.charSpacing !== 0) {
116+
css += `letter-spacing: ${num(textStyles.charSpacing / 1000)}em;\n`;
117+
}
118+
if (node.lineSpacing !== 0) {
119+
css += `line-height: ${num(node.lineSpacing)}px;\n`;
120+
}
121+
css += `text-align: ${node.textAlign};\n`;
122+
}
123+
124+
// Fill
125+
var fillName = (node instanceof sg.Text)? "color" : "background";
126+
if (node.fill && node.fillEnabled) {
127+
var fill = node.fill;
128+
if (fill instanceof sg.Color) {
129+
css += `${fillName}: ${colorToCSS(fill)};\n`;
130+
} else if (fill.colorStops) {
131+
var stops = fill.colorStops.map(stop => {
132+
return colorToCSS(stop.color) + " " + num(stop.stop * 100) + "%";
133+
});
134+
css += `${fillName}: linear-gradient(${ stops.join(", ") });\n`; // TODO: gradient direction!
135+
}
136+
// TODO: image fills
137+
} else {
138+
css += `${fillName}: transparent;\n`;
139+
}
140+
141+
// Stroke
142+
if (node.stroke && node.strokeEnabled) {
143+
var stroke = node.stroke;
144+
css += `border: ${num(node.strokeWidth)}px solid ${colorToCSS(stroke)};\n`;
145+
// TODO: dashed lines!
146+
}
147+
148+
// Opacity
149+
if (node.opacity !== 1) {
150+
css += `opacity: ${num(node.opacity)};\n`;
151+
}
152+
153+
// Dropshadow
154+
if (node.shadow && node.shadow.visible) {
155+
var shadow = node.shadow;
156+
var shadowSettings = `${num(shadow.x)}px ${num(shadow.y)}px ${num(shadow.blur)}px ${colorToCSS(shadow.color)}`;
157+
if (node instanceof sg.Text) {
158+
css += `text-shadow: ${shadowSettings};\n`;
159+
} else if (node instanceof sg.Rectangle) {
160+
css += `box-shadow: ${shadowSettings};\n`;
161+
} else {
162+
css += `filter: drop-shadow(${shadowSettings});\n`;
163+
}
164+
}
165+
166+
clipboard.copyText(css);
167+
// console.log(css);
168+
}
169+
170+
exports.commands = {
171+
copy: copy
172+
};

manifest.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"id": "db5e3c23",
3+
"name": "Copy CSS to Clipboard",
4+
"version": "1.0.0",
5+
6+
"description": "Quickly copy CSS code for text styles, colors, gradients, shadows, etc. to the clipboard.\n\nThis plugin is open source and less than 200 lines of code!",
7+
"icons": [
8+
{ "width": 96, "height": 96, "path": "icon@2x.png" }
9+
],
10+
11+
"host": {
12+
"app": "XD",
13+
"minVersion": "13.0.0"
14+
},
15+
16+
"uiEntryPoints": [
17+
{
18+
"type": "menu",
19+
"label": "Copy CSS",
20+
"commandId": "copy",
21+
"shortcut": { "mac": "Cmd+Shift+Alt+C", "win": "Ctrl+Shift+Alt+C" }
22+
}
23+
]
24+
}

0 commit comments

Comments
 (0)