├── .gitignore
├── README.md
├── readme
├── text-tools-align.gif
├── text-tools-alignment-panel.png
├── text-tools-baseline-layer.gif
├── text-tools-baseline-panel.png
├── text-tools-baseline.png
├── text-tools-columnize.gif
└── text-tools-font-metrics.gif
├── test
└── sketch-text-tools-test.sketch
└── text-tools.sketchplugin
└── Contents
├── Resources
├── align_bottom.tiff
├── align_left.tiff
├── align_right.tiff
├── align_top.tiff
└── align_vertically.tiff
└── Sketch
├── align.cocoascript
├── columnize.cocoascript
├── count.cocoascript
├── create-baseline-layer.cocoascript
├── create-font-metrics.cocoascript
├── library.cocoascript
├── manifest.json
└── shared.cocoascript
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_STORE
2 | .idea/
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sketch-text-tools
2 |
3 | ***Compatible with Sketch 43.1***
4 |
5 | This plugin eases working with typography in Sketch. It enables displaying font metrics, aligning text-layers to text-layers or other layers relative to baseline, x-height and cap-height. Baseline grid reference layers can be extracted from text-layers or build with custom configurations. Text-layers can be split into several columns with specific gutter widths.
6 |
7 | ### Commands
8 |
9 | [**1. Create Font Metrics**](#1)
10 | [**2. Align Text**](#2)
11 | [**3. Create Baseline Layer**](#3)
12 | [**4. Count Characters Per Line**](#4)
13 | [**5. Columnize**](#5)
14 |
15 | ---
16 |
17 |
18 |
19 | ## 1. Create Font Metrics
20 |
21 | 
22 |
23 | Extracts font metrics from text layer fonts. Creates a reference layer displaying the fonts baseline, ascent, descent, x-height and cap-height as well as the default line-height relative to the font-size used ( *blue* ). Furthermore the x-height and cap-height center get displayed ( *red* ). Metrics get extracted for the first line of a text layer. width equals text-layer width.
24 |
25 |
26 |
27 | ## 2. Align Text
28 |
29 | 
30 | 
31 |
32 | Aligns selected text-layers and non-text-layers. Alignment is based on a metric reference, e.g. centering all layers on a shared baseline or aligning all layers at the x-height top.
33 |
34 | Option | Description
35 | ------------------- | ---------------------------------------------------------------
36 | Reference | The reference metric to be used. (Baseline,X-Height,Cap-Height)
37 | Reference Alignment | Alignment to the reference. (e.g. to x-height center or top)
38 | Layer Alignment | Alignment of text-layers and non-text-layers
39 | Pixel Precision | Precision of resulting layer y position px
40 |
41 |
42 |
43 | ## 3. Create Baseline Layer
44 |
45 | 
46 | 
47 | 
48 |
49 | Creates a baseline reference layer either from text-layers or from configuration.
50 |
51 | Option | Description
52 | ------------ | --------------------------------------------------------------------------------------
53 | Layer Width | The width of the baseline layer ('auto' on text-layers sets width to text-layer width)
54 | Line Height | The line height ('auto' on text-layers sets line-height to text-layer line-height )
55 | Num Lines | The number of lines to display ('auto' on text-layers sets number of lines relative to text-layer height)
56 | ½ Step | If enabled an additional guide displaying half the line-height will be added
57 | Shared Style | Choose a custom styling via shared styles
58 |
59 |
60 |
61 | ## 4. Count Characters Per Line
62 |
63 | Counts characters of all lines within a text layer. Shows the minimum and maximum amount of characters, the line indices for both values as well as the total amount of characters.
64 |
65 |
66 |
67 | ## 5. Columnize
68 |
69 | 
70 |
71 | Splits a text-layer into multiple columns. Number of columns, gutter width and column height can be specified.
72 |
73 | ***Sorry, no hyphenation at the moment.***
74 |
--------------------------------------------------------------------------------
/readme/text-tools-align.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/automat/sketch-text-tools/d5ac4ca25ad64bc4976826ccd8b40cdbc38a5fc9/readme/text-tools-align.gif
--------------------------------------------------------------------------------
/readme/text-tools-alignment-panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/automat/sketch-text-tools/d5ac4ca25ad64bc4976826ccd8b40cdbc38a5fc9/readme/text-tools-alignment-panel.png
--------------------------------------------------------------------------------
/readme/text-tools-baseline-layer.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/automat/sketch-text-tools/d5ac4ca25ad64bc4976826ccd8b40cdbc38a5fc9/readme/text-tools-baseline-layer.gif
--------------------------------------------------------------------------------
/readme/text-tools-baseline-panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/automat/sketch-text-tools/d5ac4ca25ad64bc4976826ccd8b40cdbc38a5fc9/readme/text-tools-baseline-panel.png
--------------------------------------------------------------------------------
/readme/text-tools-baseline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/automat/sketch-text-tools/d5ac4ca25ad64bc4976826ccd8b40cdbc38a5fc9/readme/text-tools-baseline.png
--------------------------------------------------------------------------------
/readme/text-tools-columnize.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/automat/sketch-text-tools/d5ac4ca25ad64bc4976826ccd8b40cdbc38a5fc9/readme/text-tools-columnize.gif
--------------------------------------------------------------------------------
/readme/text-tools-font-metrics.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/automat/sketch-text-tools/d5ac4ca25ad64bc4976826ccd8b40cdbc38a5fc9/readme/text-tools-font-metrics.gif
--------------------------------------------------------------------------------
/test/sketch-text-tools-test.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/automat/sketch-text-tools/d5ac4ca25ad64bc4976826ccd8b40cdbc38a5fc9/test/sketch-text-tools-test.sketch
--------------------------------------------------------------------------------
/text-tools.sketchplugin/Contents/Resources/align_bottom.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/automat/sketch-text-tools/d5ac4ca25ad64bc4976826ccd8b40cdbc38a5fc9/text-tools.sketchplugin/Contents/Resources/align_bottom.tiff
--------------------------------------------------------------------------------
/text-tools.sketchplugin/Contents/Resources/align_left.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/automat/sketch-text-tools/d5ac4ca25ad64bc4976826ccd8b40cdbc38a5fc9/text-tools.sketchplugin/Contents/Resources/align_left.tiff
--------------------------------------------------------------------------------
/text-tools.sketchplugin/Contents/Resources/align_right.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/automat/sketch-text-tools/d5ac4ca25ad64bc4976826ccd8b40cdbc38a5fc9/text-tools.sketchplugin/Contents/Resources/align_right.tiff
--------------------------------------------------------------------------------
/text-tools.sketchplugin/Contents/Resources/align_top.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/automat/sketch-text-tools/d5ac4ca25ad64bc4976826ccd8b40cdbc38a5fc9/text-tools.sketchplugin/Contents/Resources/align_top.tiff
--------------------------------------------------------------------------------
/text-tools.sketchplugin/Contents/Resources/align_vertically.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/automat/sketch-text-tools/d5ac4ca25ad64bc4976826ccd8b40cdbc38a5fc9/text-tools.sketchplugin/Contents/Resources/align_vertically.tiff
--------------------------------------------------------------------------------
/text-tools.sketchplugin/Contents/Sketch/align.cocoascript:
--------------------------------------------------------------------------------
1 | @import 'library.cocoascript'
2 | @import 'shared.cocoascript'
3 |
4 | var Reference = {
5 | BASELINE : 'Baseline',
6 | X_HEIGHT : 'X-Height',
7 | CAP_HEIGHT : 'Cap-Height'
8 | };
9 |
10 | var Alignment = {
11 | TOP : 'alignmentPositionTop',
12 | CENTER : 'alignmentPositionCenter',
13 | BOTTOM : 'alignmentPositionBottom'
14 | };
15 |
16 | var Precision = {
17 | SUB_PIXEL : 'Sub-Pixel',
18 | FLOOR_NEAREST : 'Floor Nearest',
19 | ROUND_NEAREST : 'Round Nearest'
20 | };
21 |
22 | function align(context,reference,alignmentReference,alignmentLayer,precision){
23 | var selection = lib.getSelectionSimple(context);
24 | var currentSelection = selection.currentSelection;
25 | var lenSelection = currentSelection.count();
26 |
27 | /*----------------------------------------------------------------------------------------------------------------*/
28 | // utils position + offset
29 | /*----------------------------------------------------------------------------------------------------------------*/
30 |
31 | function getLayerOffset(layer){
32 | return lib.objTypeOf(layer,MSTextLayer) ? getTextLayerOffset(layer) : getNonTextLayerOffset(layer);
33 | }
34 |
35 | function getNonTextLayerOffset(layer){
36 | var height = layer.frame().height();
37 |
38 | var y;
39 | switch (alignmentLayer){
40 | case Alignment.TOP:
41 | y = 0;
42 | break;
43 | case Alignment.CENTER:
44 | y = height * 0.5;
45 | break;
46 | case Alignment.BOTTOM:
47 | y = height;
48 | break;
49 | }
50 | return y;
51 | }
52 |
53 | function getTextLayerOffset(layer){
54 | var y;
55 | var metrics = lib.relToAbsMetrics(lib.getFontMetrics(layer.font()));
56 |
57 | switch (reference){
58 | case Reference.BASELINE:
59 | switch (alignmentReference){
60 | case Alignment.TOP:
61 | case Alignment.CENTER:
62 | case Alignment.BOTTOM:
63 | y = metrics.baselineHeight;
64 | break;
65 | }
66 | break;
67 | case Reference.X_HEIGHT:
68 | switch (alignmentReference){
69 | case Alignment.TOP:
70 | y = metrics.xHeight;
71 | break;
72 | case Alignment.CENTER:
73 | y = metrics.xHeightCenter;
74 | break;
75 | case Alignment.BOTTOM:
76 | y = metrics.baselineHeight;
77 | break;
78 | }
79 | break;
80 | case Reference.CAP_HEIGHT:
81 | switch (alignmentReference){
82 | case Alignment.TOP:
83 | y = metrics.capHeight;
84 | break;
85 | case Alignment.CENTER:
86 | y = metrics.capHeightCenter;
87 | break;
88 | case Alignment.BOTTOM:
89 | y = metrics.baselineHeight;
90 | break;
91 | }
92 | break;
93 | }
94 | return y;
95 | }
96 |
97 | function getLayerYBounds(layer){
98 | var bounds = [0,0];
99 | var y = layer.absolutePosition().y;
100 |
101 | if(lib.objTypeOf(layer,MSTextLayer)){
102 | var metrics = lib.relToAbsMetrics(lib.getFontMetrics(layer.font()));
103 |
104 | bounds[0] = y - getTextLayerOffset(layer);
105 | bounds[1] = y - metrics.baselineHeight;
106 | } else {
107 | bounds[0] = y;
108 | bounds[1] = y + layer.frame().height();
109 | }
110 | return bounds;
111 | }
112 |
113 | function setLayerY(layer,y){
114 | switch (precision){
115 | case Precision.SUB_PIXEL:
116 | break;
117 | case Precision.FLOOR_NEAREST:
118 | y = Math.floor(y);
119 | break;
120 | case Precision.ROUND_NEAREST:
121 | y = Math.round(y);
122 | break;
123 | }
124 | layer.setAbsolutePosition_(CGPointMake(layer.absolutePosition().x,y));
125 | }
126 |
127 | /*----------------------------------------------------------------------------------------------------------------*/
128 | // positioning
129 | /*----------------------------------------------------------------------------------------------------------------*/
130 |
131 | if(lenSelection == 1){
132 | var layer = currentSelection[0];
133 | var artboard = selection.currentArtboard;
134 | setLayerY(layer,artboard.absolutePosition().y + getNonTextLayerOffset(artboard) - getTextLayerOffset(layer));
135 | return;
136 | }
137 |
138 | var axis;
139 | var offsets;
140 | var index;
141 |
142 | switch (alignmentLayer){
143 |
144 | /*------------------------------------------------------------------------------------------------------------*/
145 | // Layer Alignment Top
146 | /*------------------------------------------------------------------------------------------------------------*/
147 |
148 | case Alignment.TOP:
149 | axis = Number.MAX_VALUE;
150 | offsets = new Array(lenSelection);
151 |
152 | for(var i = 0, item, offset, yOrigin; i < lenSelection; ++i){
153 | item = currentSelection[i];
154 | offset = offsets[i] = getLayerOffset(item);
155 | yOrigin = item.absolutePosition().y;
156 |
157 | if(yOrigin - offset < axis){
158 | axis = yOrigin + offset;
159 | index = i;
160 | }
161 | }
162 |
163 | for(var i = 0; i < lenSelection; ++i){
164 | if(index == i){
165 | continue;
166 | }
167 | setLayerY(currentSelection[i],axis - offsets[i]);
168 | }
169 |
170 | break;
171 |
172 | /*------------------------------------------------------------------------------------------------------------*/
173 | // Layer Alignment Center
174 | /*------------------------------------------------------------------------------------------------------------*/
175 |
176 | case Alignment.CENTER:
177 | var container = null;
178 | for(var i = 0, item; i < lenSelection; ++i){
179 | item = currentSelection[i];
180 | var containsAll = true;
181 | for(var j = 0; j < lenSelection; ++j){
182 | if(j == i){
183 | continue;
184 | }
185 | if(!lib.containsElementY(item, currentSelection[j])){
186 | containsAll = false;
187 | break;
188 | }
189 | }
190 | if(containsAll){
191 | container = item;
192 | break;
193 | }
194 | }
195 |
196 | if(container != null){
197 | axis = container.absolutePosition().y + getLayerOffset(container);
198 |
199 | for(var i = 0, item; i < lenSelection; ++i){
200 | item = currentSelection[i];
201 | if(item == container){
202 | continue;
203 | }
204 |
205 | setLayerY(item, axis - getLayerOffset(item));
206 | }
207 | return;
208 | }
209 |
210 | offsets = new Array(lenSelection);
211 | var axisMin = Number.MAX_VALUE;
212 | var axisMax = -Number.MAX_VALUE;
213 |
214 | for(var i = 0, bounds; i < lenSelection; ++i){
215 | bounds = getLayerYBounds(currentSelection[i]);
216 |
217 | axisMin = Math.min(bounds[0],axisMin);
218 | axisMax = Math.max(bounds[1],axisMax);
219 | }
220 |
221 | axis = axisMin + (axisMax - axisMin) * 0.5;
222 | for(var i = 0; i < lenSelection; ++i){
223 | setLayerY(currentSelection[i], axis - getLayerOffset(currentSelection[i]));
224 | }
225 |
226 | break;
227 |
228 | /*------------------------------------------------------------------------------------------------------------*/
229 | // Layer Alignment Bottom
230 | /*------------------------------------------------------------------------------------------------------------*/
231 |
232 | case Alignment.BOTTOM:
233 | axis = -Number.MAX_VALUE;
234 | offsets = new Array(lenSelection);
235 |
236 | for(var i = 0, item, offset; i < lenSelection; ++i){
237 | item = currentSelection[i];
238 | offset = offsets[i] = getLayerOffset(item);
239 | axis = Math.max(axis,item.absolutePosition().y + offset);
240 | }
241 |
242 | for(var i = 0, item; i < lenSelection; ++i){
243 | setLayerY(currentSelection[i],axis - offsets[i]);
244 | }
245 |
246 | break;
247 | }
248 | }
249 |
250 | function alignText(context){
251 | var selection = lib.getSelectionSimple(context);
252 | var currentSelection = selection.currentSelection;
253 | var hasTextLayer = lib.layerArrayHasClassOfType(currentSelection,MSTextLayer);
254 | var lenSelection = currentSelection.count();
255 |
256 | if(lenSelection == 0){
257 | lib.warn(context,'Align: Nothing selected.');
258 | return;
259 |
260 | } else if(!hasTextLayer){
261 | lib.warn(context,'Align: No Text Layer selected.');
262 | return;
263 |
264 | }
265 |
266 | //Danger!
267 | var arrReference = lib.objValuesToArr(Reference);
268 | var arrAlignment = lib.objValuesToArr(Alignment);
269 | var arrPrecision = lib.objValuesToArr(Precision);
270 |
271 | lib.createPluginDefaults(PLUGIN_ID,{
272 | reference : Reference.BASELINE,
273 | referenceAlignment : Alignment.BOTTOM,
274 | layerAlignment : Alignment.CENTER,
275 | precision : Precision.SUB_PIXEL
276 | },'align');
277 |
278 | var settings = lib.getPluginSettingsObj(PLUGIN_ID,'align');
279 |
280 | var viewWidth = 320;
281 | var viewHeight = 140;
282 |
283 | var labelWidth = 130;
284 | var inputWidth = 120;
285 | var inputOffset = 4;
286 |
287 | var compStep = 29;
288 | var compOffsetV = viewHeight - 10;
289 | var compHeight = 25;
290 |
291 | function createLabel(name){
292 | return lib.createLabel(name,NSMakeRect(0,compOffsetV,labelWidth,compHeight));
293 | }
294 |
295 | function createSegmentControl(selected){
296 | var frame = NSMakeRect(labelWidth,compOffsetV + inputOffset, inputWidth, compHeight);
297 | return lib.createImageSegmentedControl(context,3,frame,[
298 | 'align_top.tiff','align_vertically.tiff','align_bottom.tiff'
299 | ],selected);
300 | }
301 |
302 | function createSelect(values,initialValue){
303 | var frame = NSMakeRect(labelWidth,compOffsetV + inputOffset, inputWidth, compHeight);
304 | return lib.createSelect(values,frame,initialValue);
305 | }
306 |
307 | compOffsetV -= compStep;
308 | var labelReference = createLabel('Reference');
309 | var inputReference = createSelect(arrReference,settings.reference);
310 |
311 | compOffsetV -= compStep;
312 | var labelReferenceAlignment = createLabel('Reference Alignment');
313 | var inputReferenceAlignment = createSegmentControl(arrAlignment.indexOf("" + settings.referenceAlignment));
314 |
315 | compOffsetV -= compStep;
316 | var labelLayerAlignment = createLabel('Layer Alignment');
317 | var inputLayerAlignment = createSegmentControl(arrAlignment.indexOf("" +settings.layerAlignment));
318 |
319 | compOffsetV -= compStep;
320 | var labelPixelPrecision = createLabel('Pixel Precision');
321 | var inputPixelPrecision = createSelect(arrPrecision,settings.precision);
322 |
323 | var view = lib.createViewWithSubviews(
324 | NSMakeRect(0, 0, viewWidth, viewHeight), [
325 | labelReference, inputReference,
326 | labelReferenceAlignment, inputReferenceAlignment,
327 | labelLayerAlignment, inputLayerAlignment,
328 | labelPixelPrecision, inputPixelPrecision
329 | ]
330 | );
331 |
332 | if(!lib.runModalAlert(view, 'Text Tools', 'Align Text Layer')){
333 | return;
334 | }
335 |
336 | var reference = "" + inputReference.titleOfSelectedItem();
337 | var referenceAlignment = arrAlignment[inputReferenceAlignment.selectedSegment()];
338 | var layerAlignment = arrAlignment[inputLayerAlignment.selectedSegment()];
339 | var precision = "" + inputPixelPrecision.titleOfSelectedItem();
340 |
341 | lib.synchronizePluginDefaults(PLUGIN_ID,{
342 | reference : reference,
343 | referenceAlignment : referenceAlignment,
344 | layerAlignment : layerAlignment,
345 | precision : precision
346 | },'align');
347 |
348 | align(context,reference,referenceAlignment,layerAlignment,precision);
349 | }
--------------------------------------------------------------------------------
/text-tools.sketchplugin/Contents/Sketch/columnize.cocoascript:
--------------------------------------------------------------------------------
1 | @import 'library.cocoascript'
2 |
3 | /**
4 | * Splits a text layer into multiple text layers.
5 | * @param {MSTextLayer} layer - Target text layer
6 | * @param {number} numColumns - Number of columns
7 | * @param {number} gutterWidth - Width of gutter between columns
8 | * @param {number} columnsHeight - Height of the columns
9 | */
10 | function columnizeTextLayer(layer,numColumns,gutterWidth,columnsHeight){
11 | if(numColumns * gutterWidth >= layer.frame().width){
12 | return;
13 | }
14 | var numColumnsAdded = 1;
15 |
16 | var frame = layer.frame();
17 | var width = (frame.width() - gutterWidth * (numColumns - 1)) / numColumns;
18 | var tokens = layer.stringValue().split(' ');
19 |
20 | layer.stringValue = tokens.shift();
21 | frame.width = width;
22 | frame.height = 0;
23 |
24 | while(true){
25 | if(tokens.length == 0){
26 | break;
27 | }
28 |
29 | layer.stringValue = layer.stringValue() + ' ' + tokens.shift();
30 |
31 | // column break
32 | if(frame.height() > columnsHeight){
33 | // remove last token added
34 | var stringValue = layer.stringValue();
35 | var index = stringValue.lastIndexOf(' ');
36 | var token = stringValue.substring(index + 1,stringValue.length());
37 | layer.stringValue = stringValue.substring(0,index);
38 |
39 | // number of columns exceed columns specified
40 | if(numColumnsAdded >= numColumns){
41 | break;
42 | }
43 |
44 | // create next column text layer
45 | layer = layer.duplicate();
46 | var offset = frame.x() + width + gutterWidth;
47 | frame = layer.frame();
48 | frame.x = offset;
49 |
50 | // add last token removed
51 | layer.stringValue = token;
52 |
53 | numColumnsAdded++;
54 | }
55 | }
56 | }
57 |
58 | /**
59 | * 'Columnize Text' - Action
60 | * @param context
61 | */
62 | function columnize(context){
63 | var selection = lib.getSelectionSimple(context);
64 |
65 | if(!selection.hasSelection()){
66 | lib.warn(context,'Columnize: Nothing selected.');
67 | return;
68 | }
69 |
70 | var selectionFiltered = lib.filterLayersByClass(selection.currentSelection,MSTextLayer);
71 |
72 | if(selectionFiltered.length == 0){
73 | lib.warn(context,'Columnize: No Text Layer selected.');
74 | return;
75 | }
76 |
77 | var viewWidth = 300;
78 | var viewHeight = 120;
79 |
80 | var labelWidth = 110;
81 | var inputWidth = 120;
82 | var inputOffset = 4;
83 |
84 | var compStep = 26;
85 | var compOffsetV = viewHeight - 10;
86 | var compHeight = 20;
87 |
88 | function createLabel(name){
89 | return lib.createLabel(name,NSMakeRect(0,compOffsetV,labelWidth,compHeight));
90 | }
91 |
92 | function createInput(value){
93 | return lib.createTextField(value,NSMakeRect(labelWidth,compOffsetV + inputOffset,inputWidth,compHeight));
94 | }
95 |
96 | compOffsetV -= compStep;
97 | var labelNumColumns = createLabel('Num Columns');
98 | var inputNumColumns = createInput(2);
99 |
100 | compOffsetV -= compStep;
101 | var labelColumnHeight = createLabel('Column Height');
102 | var inputColumnHeight = createInput(200);
103 |
104 | compOffsetV -= compStep;
105 | var labelColumnGutter = createLabel('Gutter');
106 | var inputColumnGutter = createInput(30);
107 |
108 | var alert = lib.createAlertWithView(
109 | "Text Tools",
110 | lib.createViewWithSubviews(
111 | NSMakeRect(0,0,viewWidth,viewHeight),[
112 | labelNumColumns,inputNumColumns,
113 | labelColumnHeight,inputColumnHeight,
114 | labelColumnGutter,inputColumnGutter
115 | ]
116 | ),
117 | 'Columnize Text Layer.'
118 | );
119 |
120 | alert.addButtonWithTitle_('OK');
121 | alert.addButtonWithTitle_('Cancel');
122 |
123 | if(alert.runModal() !== NSAlertFirstButtonReturn){
124 | return;
125 | }
126 |
127 | var numColumns = Math.max(0,+inputNumColumns.stringValue());
128 | var columnHeight = Math.max(0,+inputColumnHeight.stringValue());
129 | var gutter = Math.max(0,+inputColumnGutter.stringValue());
130 |
131 | for(var i = 0; i < selectionFiltered.length; ++i){
132 | var layer = selectionFiltered[i];
133 | if(numColumns * gutter >= layer.frame().width()){
134 | lib.warn(context,'Columnize: Configuration exceeds Text Layer width.');
135 | continue;
136 | }
137 | if(layer.stringValue().length() == 0){
138 | lib.warn(context,"Columnize: Text Layer has no content.");
139 | continue;
140 | }
141 | columnizeTextLayer(layer,numColumns,gutter,columnHeight);
142 | }
143 | }
--------------------------------------------------------------------------------
/text-tools.sketchplugin/Contents/Sketch/count.cocoascript:
--------------------------------------------------------------------------------
1 | @import 'library.cocoascript'
2 |
3 | /**
4 | * 'Count Character Per Line' - Action
5 | * @param context
6 | */
7 | function count(context){
8 | var selection = lib.getSelectionSimple(context);
9 | var currentSelection = selection.currentSelection;
10 | var selectionFiltered = lib.filterLayersByClass(currentSelection,MSTextLayer);
11 |
12 | if(currentSelection.length == 0){
13 | lib.warn(context,'Count: Nothing selected.');
14 | return;
15 |
16 | } else if(selectionFiltered.length == 0){
17 | lib.warn(context,'Count: No Text Layer selected.');
18 | return;
19 |
20 | } else if(selectionFiltered.length > 1){
21 | lib.warn(context,'Count: Multiple Text Layers selected. Min and max number of characters ')
22 | }
23 |
24 | var layer = currentSelection[0];
25 |
26 | var storage = layer.createTextStorage();
27 | var manager = storage.layoutManagers()[0];
28 | var container = manager.textContainers()[0];
29 |
30 | var lineRange = NSMakeRange(0,0);
31 | var glyphRange = manager.glyphRangeForTextContainer_(container);
32 | var lineRangePtr = MOPointer.alloc().initWithValue_(lineRange);
33 |
34 | var numCharsPerLine = [];
35 |
36 | var yCurr = 0;
37 | var yLast = -Number.MAX_VALUE;
38 |
39 | var charIndex = 0;
40 | var charIndexLast = 0;
41 | var numLines = 0;
42 |
43 | var numGlyphs = NSMaxRange(glyphRange);
44 |
45 | while(charIndex < numGlyphs){
46 | yCurr = manager.lineFragmentRectForGlyphAtIndex_effectiveRange_(charIndex, lineRangePtr).origin.y;
47 |
48 | if(yCurr > yLast){
49 | numLines++;
50 | yLast = yCurr;
51 | }
52 |
53 | charIndexLast = charIndex - numLines + 1; //linebreaks char
54 | charIndex = lineRange.location = NSMaxRange(lineRangePtr.value());
55 |
56 | numCharsPerLine.push((charIndex - numLines) - charIndexLast);
57 | }
58 |
59 | numCharsPerLine[numCharsPerLine.length - 1]++;
60 |
61 | var min = Number.MAX_VALUE;
62 | var max = -Number.MAX_VALUE;
63 | var indexMin = 0;
64 | var indexMax = 1;
65 |
66 | for(var i = 0; i < numCharsPerLine.length; ++i){
67 | var num = numCharsPerLine[i];
68 | if(num < min){
69 | min = num;
70 | indexMin = i + 1;
71 | }
72 | if(num > max){
73 | max = num;
74 | indexMax = i + 1;
75 | }
76 | }
77 |
78 | lib.warn(context,
79 | 'Count: (Minimum: ' + min + ' @ Line: ' + indexMin + ')' +
80 | ', (Maximum: ' + max + ' @ Line: ' + indexMax + ')' +
81 | ', (Total: ' + numGlyphs + ')'
82 | );
83 | }
--------------------------------------------------------------------------------
/text-tools.sketchplugin/Contents/Sketch/create-baseline-layer.cocoascript:
--------------------------------------------------------------------------------
1 | @import 'library.cocoascript'
2 | @import 'shared.cocoascript'
3 |
4 | var DEFAULT_STYLE_PRIMARY = {fill : ['#979797',0.35]};
5 | var DEFAULT_STYLE_SECONDARY = {fill : ['#979797',0.135]};
6 | var NO_STYLE = 'No Style';
7 | var MODE_AUTO = 'auto';
8 |
9 | /**
10 | * Creates path with stacked rect representing the baselines.
11 | * @param width
12 | * @param height
13 | * @param numLines
14 | */
15 | function createGuidePath(width,height,numLines){
16 | var path = lib.createBezierPath();
17 | for(var i = 0; i < numLines; ++i){
18 | lib.pathRect(path,0, i * height, width, height - 1);
19 | }
20 | return path;
21 | }
22 |
23 | /**
24 | * Creates a baseline layer group.
25 | * @param width
26 | * @param lineHeight
27 | * @param numLines
28 | * @param halfStep
29 | * @param style
30 | */
31 | function createGuideGroup(width,lineHeight,numLines,halfStep,style){
32 | var layers = [];
33 | var useDefaultStyle = style == NO_STYLE;
34 | style = useDefaultStyle ? lib.createStyle(DEFAULT_STYLE_PRIMARY) : style.newInstance();
35 |
36 | // create primary baseline layer
37 | var layer = lib.createShapeFromPath(createGuidePath(width,lineHeight,numLines));
38 | layer.setStyle_(style);
39 | layer.setName_("guide");
40 | layers.push(layer);
41 |
42 | // create secondary baseline layer
43 | if(halfStep){
44 | var offset = Math.floor(lineHeight * 0.5);
45 | layer = lib.createShapeFromPath(createGuidePath(width,lineHeight,numLines-1));
46 | layer.setStyle_(useDefaultStyle ? lib.createStyle(DEFAULT_STYLE_SECONDARY) : style);
47 | layer.frame().setY_(offset);
48 | layer.setName_('guide-1/2');
49 | layers.push(layer);
50 | }
51 |
52 | // create group
53 | var group = lib.createGroupFromLayers(layers);
54 | group.setName_('baseline-guide@' + lineHeight + 'px');
55 | return group;
56 | }
57 |
58 | /**
59 | * Creates a bseline layer group from text layer.
60 | * @param {MSTextLayer} layer - The text layer reference
61 | * @param {number|string} width - The width, if set 'auto' equals text layer width.
62 | * @param {number|string} lineHeight - The line height, if set 'auto' equals text layer height.
63 | * @param {number|string} numLines - The number of baseline guides, if if set 'auto' equals text layer num layers.
64 | * @param {boolean} halfStep - If true an additional guide at half the baseline height will be created.
65 | * @param {MSStyle} [style] - The style to be used.
66 | */
67 | function createBaselineGroupFromText(layer,width,lineHeight,numLines,halfStep,style){
68 | if(width == 0 || numLines == 0){
69 | return;
70 | }
71 | var frame = layer.frame();
72 | var metrics = lib.relToAbsMetrics(lib.getFontMetrics(layer.font()));
73 | var baselineOffsets = layer.baselineOffsets();
74 |
75 | // width
76 | if(width == MODE_AUTO){
77 | width = frame.width();
78 | }
79 |
80 | // line height
81 | if(lineHeight == MODE_AUTO){
82 | var layerLineHeight = layer.lineHeight();
83 | if(baselineOffsets.length == 1){
84 | lineHeight = layerLineHeight || metrics.defaultLineHeight;
85 | } else {
86 | lineHeight = baselineOffsets[1] - baselineOffsets[0];
87 | }
88 | }
89 |
90 | // number of lines
91 | if(numLines == MODE_AUTO){
92 | numLines = baselineOffsets.length;
93 | }
94 |
95 | // create layer
96 | var offset = lineHeight - Math.floor(layer.firstBaselineOffset());
97 | var group = createGuideGroup(width,lineHeight,numLines,halfStep,style);
98 | group.frame().setX_(frame.x());
99 | group.frame().setY_(frame.y() - offset + 1);
100 | layer.parentGroup().insertLayers_beforeLayer_([group],layer);
101 | }
102 |
103 | /**
104 | * "Create Baseline Layer" - Action
105 | * @param context
106 | */
107 | function createBaselineLayer(context){
108 | var selection = lib.getSelectionSimple(context);
109 |
110 | if(!selection.hasSelection() && selection.currentArtboard == null){
111 | lib.warn(context,'Create Baseline Layer: No Artboard selected');
112 | return;
113 | }
114 |
115 | var selectionFiltered = !selection.hasSelection() ? [] : lib.filterLayersByClass(selection.currentSelection,MSTextLayer);
116 | var selectionHasTextLayers = selectionFiltered.length != 0;
117 |
118 | var viewWidth = 300;
119 | var viewHeight = 150;
120 |
121 | var labelWidth = 110;
122 | var inputWidth = 120;
123 | var inputOffset = 4;
124 |
125 | var compStep = 26;
126 | var compOffsetV = viewHeight - 10;
127 | var compHeight = 20;
128 |
129 | function createLabel(name){
130 | return lib.createLabel(name,NSMakeRect(0,compOffsetV,labelWidth,compHeight));
131 | }
132 |
133 | function createInput(value){
134 | return lib.createTextField(value,NSMakeRect(labelWidth,compOffsetV + inputOffset,inputWidth,compHeight));
135 | }
136 |
137 | function createSelect(values,initialValue){
138 | var frame = NSMakeRect(labelWidth,compOffsetV + inputOffset - 2,inputWidth,compHeight + 2);
139 | return lib.createSelect(values,frame,initialValue);
140 | }
141 |
142 | var defaults = {
143 | width : 200,
144 | lineHeight : 24,
145 | numLines : 10,
146 | halfStep : false,
147 | style : NO_STYLE
148 | };
149 |
150 | lib.createPluginDefaults(PLUGIN_ID,defaults,'baseline');
151 |
152 | var settings = lib.getPluginSettingsObj(PLUGIN_ID,'baseline');
153 |
154 | var sharedStylesContainer = selection.document.documentData().layerStyles();
155 | var sharedStyles = new Array(sharedStylesContainer.numberOfSharedStyles());
156 | var sharedStyleNames = new Array(sharedStyles.length);
157 |
158 | //style name?
159 | function getNameFromDescription(description){
160 | description = "" + description;
161 |
162 | var indexBegin = lib.indicesOf(description,'>')[0] + 1;
163 | var indexEnd = lib.indicesOf(description,'(');
164 | indexEnd = indexEnd[indexEnd.length - 1] - 1;
165 |
166 | return description.substring(indexBegin + 1,indexEnd);
167 | }
168 |
169 | for(var i = 0,l = sharedStyles.length; i < l; ++i){
170 | sharedStyles[i] = sharedStylesContainer.sharedStyleAtIndex(i);
171 | sharedStyleNames[i] = getNameFromDescription(sharedStyles[i].description());
172 | }
173 |
174 | if(selectionHasTextLayers){
175 | settings.width = MODE_AUTO;
176 | settings.lineHeight = MODE_AUTO;
177 | settings.numLines = MODE_AUTO;
178 | }else{
179 | settings.width = ("" + settings.width) == MODE_AUTO ? defaults.width : settings.width;
180 | settings.lineHeight = ("" + settings.lineHeight) == MODE_AUTO ? defaults.lineHeight : settings.lineHeight;
181 | settings.numLines = ("" + settings.numLines) == MODE_AUTO ? defaults.numLines : settings.numLines;
182 | }
183 |
184 | settings.style = sharedStyleNames.indexOf("" + settings.style) != -1 ? settings.style : NO_STYLE;
185 |
186 | compOffsetV -= compStep;
187 | var labelLayerWidth = createLabel('Layer Width');
188 | var inputLayerWidth = createInput(settings.width);
189 |
190 | compOffsetV -= compStep;
191 | var labelLineHeight = createLabel('Line Height');
192 | var inputLineHeight = createInput(settings.lineHeight);
193 |
194 | compOffsetV -= compStep;
195 | var labelNumLines = createLabel('Num Lines');
196 | var inputNumLines = createInput(settings.numLines);
197 |
198 | compOffsetV -= compStep;
199 | var labelLineHeightHalfStep = createLabel('Line Height ½ Step');
200 | var checkboxLineHeightHalfStep = lib.createCheckBox('',NSMakeRect(labelWidth,compOffsetV + inputOffset,20,18),settings.halfStep);
201 |
202 | compOffsetV -= compStep;
203 | var labelSharedStyle = createLabel('Shared Style');
204 | var selectSharedStyle = createSelect([NO_STYLE].concat(sharedStyleNames),settings.style);
205 |
206 | var view = lib.createViewWithSubviews(
207 | NSMakeRect(0,0,viewWidth,viewHeight),[
208 | labelLayerWidth,inputLayerWidth,
209 | labelLineHeight,inputLineHeight,
210 | labelNumLines,inputNumLines,
211 | labelLineHeightHalfStep,checkboxLineHeightHalfStep,
212 | labelSharedStyle,selectSharedStyle
213 | ]
214 | );
215 |
216 | if(!lib.runModalAlert(view,'Text Tools','Create Baseline Layer')){
217 | return;
218 | }
219 |
220 | var width = "" + inputLayerWidth.stringValue();
221 | var lineHeight = "" + inputLineHeight.stringValue();
222 | var numLines = "" + inputNumLines.stringValue();
223 | var halfStep = checkboxLineHeightHalfStep.state() == NSOnState;
224 | var style = "" + selectSharedStyle.titleOfSelectedItem();
225 |
226 | width = width != MODE_AUTO ? +width : width;
227 | lineHeight = lineHeight != MODE_AUTO ? +lineHeight : lineHeight;
228 | numLines = numLines != MODE_AUTO ? Math.floor(+numLines) : numLines;
229 |
230 | lib.synchronizePluginDefaults(PLUGIN_ID,{
231 | width : width,
232 | lineHeight : lineHeight,
233 | numLines : numLines,
234 | halfStep : halfStep,
235 | style : style
236 | },'baseline');
237 |
238 | style = style != NO_STYLE ? sharedStyles[sharedStyleNames.indexOf(style)] : style;
239 |
240 | // create baseline group from settings
241 | if(!selectionHasTextLayers){
242 | var group = createGuideGroup(width, lineHeight, numLines, halfStep, style);
243 | selection.currentArtboard.addLayers_([group]);
244 | lib.centerElementToElement(group,selection.currentArtboard,true);
245 | return;
246 | }
247 |
248 | // create baseline group from text layers
249 | for(var i = 0; i < selectionFiltered.length; ++i){
250 | createBaselineGroupFromText(selectionFiltered[i], width, lineHeight, numLines, halfStep, style);
251 | }
252 | }
--------------------------------------------------------------------------------
/text-tools.sketchplugin/Contents/Sketch/create-font-metrics.cocoascript:
--------------------------------------------------------------------------------
1 | @import 'library.cocoascript'
2 |
3 | /**
4 | * Create font metrics group.
5 | * @param [MsLayer] layer - Target layer
6 | */
7 | function createFontMetricsGroup(layer){
8 | var font = layer.font();
9 | var fontName = font.fontName();
10 | var fontSize = layer.fontSize();
11 | var metrics = lib.relToAbsMetrics(lib.getFontMetrics(font));
12 | var frame = layer.frame();
13 | var width = frame.width();
14 | var offset = metrics.defaultLineHeight - layer.firstBaselineOffset();
15 |
16 | // create guides metrics
17 | var path = lib.createBezierPath();
18 | lib.pathLineH(path,0,width,0);
19 | lib.pathLineH(path,0,width,metrics.capHeight);
20 | lib.pathLineH(path,0,width,metrics.xHeight);
21 | lib.pathLineH(path,0,width,metrics.baselineHeight);
22 | lib.pathLineH(path,0,width,metrics.descentHeight);
23 | lib.pathLineH(path,0,width,metrics.defaultLineHeight);
24 |
25 | var guideMetrics = lib.createShapeFromPath(path);
26 | guideMetrics.setStyle_(lib.createStyle({border: ['#0000ff', 0.35]}));
27 | guideMetrics.setName_('guides');
28 |
29 | // create guide centers
30 | path = lib.createBezierPath();
31 | lib.pathLineH(path,0,width,metrics.capHeightCenter);
32 | lib.pathLineH(path,0,width,metrics.xHeightCenter);
33 |
34 | var guideCenters = lib.createShapeFromPath(path);
35 | guideCenters.setStyle_(lib.createStyle({border: ['#ff0000', 0.35]}));
36 | guideCenters.setName_('guides-centers');
37 |
38 | //out
39 | var group = lib.createGroupFromLayers([guideMetrics,guideCenters]);
40 | group.frame().setX_(frame.x());
41 | group.frame().setY_(frame.y() - offset);
42 | group.setName_(fontName + ' – metrics@' + fontSize + 'px');
43 | layer.parentGroup().insertLayers_beforeLayer_([group],layer);
44 | }
45 |
46 | /**
47 | * 'Create Font Metrics' - Action
48 | * @param context
49 | */
50 | function createTextFontMetrics(context){
51 | var selection = lib.getSelectionSimple(context);
52 |
53 | if(!selection.hasSelection()){
54 | lib.warn(context,'Create Font Metrics: Nothing selected.');
55 | return;
56 | }
57 |
58 | var selectionFiltered = lib.filterLayersByClass(selection.currentSelection,MSTextLayer);
59 | if(selection.currentSelection.count() == 1 && selectionFiltered.length == 0){
60 | lib.warn(context,'Create Font Metrics: Selection not of type Text Layer.');
61 | return;
62 | } else if(selectionFiltered.length == 0) {
63 | lib.warn(context,'Create Font Metrics: Selection does not contain any Text Layers.');
64 | return;
65 | }
66 |
67 | for(var i = 0; i < selectionFiltered.length; ++i){
68 | createFontMetricsGroup(selectionFiltered[i]);
69 | }
70 | }
--------------------------------------------------------------------------------
/text-tools.sketchplugin/Contents/Sketch/library.cocoascript:
--------------------------------------------------------------------------------
1 | function printv(args){
2 | for(var i = 0, l = arguments.length; i < l; ++i){
3 | print(arguments[i]);
4 | }
5 | }
6 |
7 |
8 | var lib = {};
9 |
10 | lib.warn = function(context,msg){
11 | context.document.showMessage_(msg);
12 | };
13 |
14 | /*--------------------------------------------------------------------------------------------------------------------*/
15 | // obj utitls
16 | /*--------------------------------------------------------------------------------------------------------------------*/
17 |
18 | lib.objTypeOf = function(obj,class_){
19 | return obj.class() == class_;
20 | };
21 |
22 | lib.objValuesToArr = function(obj){
23 | var out = [];
24 | for(var p in obj){
25 | out.push(obj[p]);
26 | }
27 | return out;
28 | };
29 |
30 | lib.CGPointToObj = function(point){
31 | return {
32 | x : point.x,
33 | y : point.y
34 | }
35 | };
36 |
37 | lib.CGSizeToObj = function(size){
38 | return {
39 | width : size.width,
40 | height: size.height
41 | }
42 | };
43 |
44 | lib.CGRectToObj = function(rect){
45 | var origin = rect.origin;
46 | var size = rect.size;
47 | return {
48 | x : origin.x,
49 | y : origin.y,
50 | width: size.width,
51 | height: size.height
52 | };
53 | };
54 |
55 | lib.createDict = function(objects,keys){
56 | return NSDictionary.dictionaryWithObjects_forKeys(objects, keys);
57 | };
58 |
59 | lib.objToDict = function(obj){
60 | var keys = Object.keys(obj);
61 | var values = new Array(keys.length);
62 |
63 | for(var i = 0, l = keys.length; i < l; ++i){
64 | values[i] = obj[""+keys[i]];
65 | }
66 |
67 | return lib.createDict(values,keys);
68 | };
69 |
70 | lib.dictToObj = function(dict){
71 | var obj = {};
72 | var keys = dict.allKeys();
73 | for(var i = 0, l = keys.count(), key; i < l; ++i){
74 | key = keys[i];
75 | obj[key] = dict.objectForKey_(key);
76 | }
77 | return obj;
78 | };
79 |
80 | lib.indicesOf = function(obj,element){
81 | var indices = [];
82 | for(var i = 0, l = obj.length; i < l; ++i){
83 | if(obj[i] == element){
84 | indices.push(i);
85 | }
86 | }
87 | return indices;
88 | };
89 |
90 | /*--------------------------------------------------------------------------------------------------------------------*/
91 | // Style
92 | /*--------------------------------------------------------------------------------------------------------------------*/
93 |
94 | lib.createStyleFromDescription = function(description){
95 | var style = MSStyle.alloc().init();
96 | var fill = description.fill;
97 | var border = description.border;
98 |
99 | if(fill){
100 | var styleFill = style.addStylePartOfType(0);
101 | var styleFillColor = MSColor.colorWithRed_green_blue_alpha(fill[0].r, fill[0].g, fill[0].b, fill[0].a);
102 | styleFillColor.alpha = fill[1] || 1.0;
103 | styleFill.color = styleFillColor;
104 | }
105 |
106 | if(border){
107 | var styleBorder = style.addStylePartOfType(1);
108 | var styleBorderColor = MSColor.colorWithRed_green_blue_alpha(border[0].r, border[0].g, border[0].b, border[0].a);
109 | styleBorderColor.alpha = border[1] || 1.0;
110 | styleBorder.color = styleBorderColor;
111 | }
112 |
113 | return style;
114 | };
115 |
116 | /*--------------------------------------------------------------------------------------------------------------------*/
117 | // Shapes
118 | /*--------------------------------------------------------------------------------------------------------------------*/
119 |
120 | lib.createBezierPath = function(){
121 | return NSBezierPath.bezierPath();
122 | };
123 |
124 | lib.createShapeFromPathWithStyle = function(path,style){
125 | var shape = MSShapeGroup.shapeWithBezierPath(path);
126 | shape.setStyle(style);
127 | return shape;
128 | };
129 |
130 | lib.pathMoveTo = function(path,x,y){
131 | path.moveToPoint(NSMakePoint(x,y));
132 | };
133 |
134 | lib.pathLineTo = function(path,x,y){
135 | path.lineToPoint(NSMakePoint(x,y));
136 | };
137 |
138 | lib.pathLine = function(path,x0,y0,x1,y1){
139 | path.moveToPoint(NSMakePoint(x0,y0));
140 | path.lineToPoint(NSMakePoint(x1,y1));
141 | };
142 |
143 | lib.pathLineH = function(path,x0,x1,y){
144 | this.pathLine(path,x0,y,x1,y);
145 | };
146 |
147 | lib.pathLineV = function(path,x,y0,y1){
148 | this.pathLine(path,x,y0,x,y1);
149 | };
150 |
151 | lib.pathRect = function(path,x,y,width,height){
152 | path.appendBezierPathWithRect(NSMakeRect(x,y,width,height));
153 | };
154 |
155 | /*--------------------------------------------------------------------------------------------------------------------*/
156 | // Groups & Layers
157 | /*--------------------------------------------------------------------------------------------------------------------*/
158 |
159 | lib.filterLayersByClass = function(layers,class_){
160 | var out = [];
161 | var item;
162 | var l = layers.objectEnumerator ? layers.count() : layers.length;
163 |
164 | for(var i = 0; i < l; ++i){
165 | item = layers[i];
166 | if(!this.objTypeOf(item,class_)){
167 | continue;
168 | }
169 | out.push(item);
170 | }
171 | return out;
172 | };
173 |
174 | lib.layerArrayHasClassOfType = function(layers,class_){
175 | var hasClass = false;
176 | var l = layers.objectEnumerator ? layers.count() : layers.length;
177 |
178 | for(var i = 0; i < l; ++i){
179 | if(this.objTypeOf(layers[i],class_)){
180 | hasClass = true;
181 | break;
182 | }
183 | }
184 |
185 | return hasClass;
186 | };
187 |
188 | lib.containsElementY = function(elementA,elementB){
189 | var frameA = elementA.frame();
190 | var frameB = elementB.frame();
191 |
192 | return (frameB.y() >= frameA.y()) && ((frameB.y() + frameB.height()) <= (frameA.y() + frameA.height()));
193 | };
194 |
195 | lib.centerElementToElement = function(elementA,elementB,floor){
196 | var frameA = elementA.frame();
197 | var frameB = elementB.frame();
198 | var x = frameB.width() * 0.5 - frameA.width() * 0.5;
199 | var y = frameB.height() * 0.5 - frameA.height() * 0.5;
200 | if(floor){
201 | x = Math.floor(x);
202 | y = Math.floor(y);
203 | }
204 | frameA.setX_(x);
205 | frameA.setY_(y);
206 | };
207 |
208 | lib.createGroupFromLayers = function(layers){
209 | var group = MSLayerGroup.new();
210 | group.addLayers(layers);
211 | group.resizeToFitChildrenWithOption(0);
212 | return group;
213 | };
214 |
215 | lib.getBoundsFromLayers = function(layers){
216 | return MSLayerGroup.groupBoundsForLayers(layers);
217 | };
218 |
219 | /*--------------------------------------------------------------------------------------------------------------------*/
220 | // Interface
221 | /*--------------------------------------------------------------------------------------------------------------------*/
222 |
223 | lib.createViewWithSubviews = function(frame,subviews){
224 | var view = NSView.alloc().initWithFrame_(frame);
225 | view.setSubviews(subviews);
226 | return view;
227 | };
228 |
229 | lib.createAlertWithView = function(messageText,view,informativeText){
230 | var alert = NSAlert.alloc().init();
231 | alert.setMessageText_(messageText);
232 | alert.setAccessoryView_(view);
233 | if(informativeText !== undefined){
234 | alert.setInformativeText_(informativeText);
235 | }
236 | return alert;
237 | };
238 |
239 | lib.runModalAlert = function(view,messageText,invormativeText){
240 | var alert = lib.createAlertWithView(messageText, view, invormativeText);
241 |
242 | alert.addButtonWithTitle_('OK');
243 | alert.addButtonWithTitle_('Cancel');
244 |
245 | return alert.runModal() == NSAlertFirstButtonReturn
246 | };
247 |
248 | lib.createLabel = function(name,frame,fontSize){
249 | fontSize = fontSize === undefined ? 11 : fontSize;
250 |
251 | var label = NSTextField.alloc().initWithFrame_(frame);
252 | label.setEditable_(false);
253 | label.setSelectable_(false);
254 | label.setBezeled_(false);
255 | label.setDrawsBackground_(false);
256 | label.setFont(NSFont.systemFontOfSize_(fontSize));
257 | label.setStringValue_(name);
258 | return label;
259 | };
260 |
261 | lib.createTextField = function(value,frame,placeholderValue){
262 | var textfield = NSTextField.alloc().initWithFrame_(frame);
263 | if(value !== null){
264 | textfield.setStringValue_(""+value);
265 | }
266 | if(placeholderValue !== undefined){
267 | textfield.setPlaceholderString_(""+value);
268 | }
269 | return textfield;
270 | };
271 |
272 | lib.createImageSegmentedControl = function(context,numSegments,frame,imagePaths,selectedSegment){
273 | var control = NSSegmentedControl.alloc().initWithFrame_(frame);
274 | control.setSegmentCount_(numSegments);
275 |
276 | var segWidth = frame.size.width / numSegments - (numSegments - 1); //pixel divider
277 | var plugin = context.plugin;
278 |
279 | var images = new Array(numSegments);
280 | for(var i = 0, image; i < numSegments; ++i){
281 | image = images[i] = NSImage.alloc().initByReferencingFile_(
282 | plugin.urlForResourceNamed_(imagePaths[i]).path()
283 | );
284 | image.setTemplate_(true);
285 | control.setImage_forSegment_(image,i);
286 | control.setWidth_forSegment_(segWidth,i);
287 | }
288 |
289 | if(selectedSegment !== undefined){
290 | control.setSelectedSegment_(selectedSegment);
291 | }
292 |
293 | return control;
294 | };
295 |
296 | lib.createSelect = function(values, frame, initialState) {
297 | var select = NSPopUpButton.alloc().initWithFrame_(frame);
298 | select.addItemsWithTitles_(values);
299 | select.setFont(NSFont.systemFontOfSize_(11));
300 | select.selectItemWithTitle_(initialState);
301 | return select;
302 | };
303 |
304 | lib.createCheckBox = function(name, frame, initialState) {
305 | var btn = NSButton.alloc().initWithFrame_(frame);
306 | btn.setButtonType_(NSSwitchButton);
307 | btn.setState_(initialState || NSOffState);
308 | btn.setTitle_(name);
309 | return btn;
310 | };
311 |
312 | lib.createRadioButton = function(name,frame, initialState){
313 | var btn = NSButton.alloc().initWithFrame_(frame);
314 | btn.setState_(initialState || NSOffState);
315 | btn.setButtonType_(NSRadioButton);
316 | };
317 |
318 | /*--------------------------------------------------------------------------------------------------------------------*/
319 | // Font
320 | /*--------------------------------------------------------------------------------------------------------------------*/
321 |
322 | lib.getFontMetrics = function(font){
323 | return {
324 | ascent : font.ascender(),
325 | descent : font.descender(),
326 | capHeight : font.capHeight(),
327 | xHeight : font.xHeight(),
328 | defaultLineHeight : font.defaultLineHeightForFont(),
329 | italicAngle : font.italicAngle(),
330 | maxAdvancement : this.CGSizeToObj(font.maximumAdvancement()),
331 | boundingRect : this.CGRectToObj(font.boundingRectForFont())
332 | }
333 | };
334 |
335 | lib.relToAbsMetrics = function(metrics){
336 | var defaultLineHeight = metrics.defaultLineHeight;
337 | var baselineHeight = metrics.ascent;
338 |
339 | return {
340 | defaultLineHeight : defaultLineHeight,
341 | baselineHeight : baselineHeight,
342 | descentHeight : defaultLineHeight - metrics.descent,
343 | capHeight : baselineHeight - metrics.capHeight,
344 | xHeight : baselineHeight - metrics.xHeight,
345 |
346 | capHeightCenter : baselineHeight - metrics.capHeight * 0.5,
347 | xHeightCenter : baselineHeight - metrics.xHeight * 0.5,
348 |
349 | italicAngle : metrics.italicAngle,
350 | maxAdvancement : metrics.maxAdvancement,
351 | boundingRect : metrics.boundingRect
352 | };
353 | };
354 |
355 | lib.getFontCharMetrics = function(font,char){
356 | var charCode = char.charCodeAt(0);
357 | return {
358 | advancement : this.CGSizeToObj(font.advancementForGlyph_(charCode)),
359 | boundingRect : this.CGRectToObj(font.boundingRectForGlyph_(charCode))
360 | }
361 | };
362 |
363 | lib.getFontLeading = function(metrics,lineHeight){
364 | return lineHeight - (metrics.ascent + metrics.descent);
365 | };
366 |
367 | /*--------------------------------------------------------------------------------------------------------------------*/
368 | // Selection
369 | /*--------------------------------------------------------------------------------------------------------------------*/
370 |
371 | lib.getSelectionSimple = function(context){
372 | var document = context.document;
373 | var view = document.currentView();
374 | var currentPage = document.currentPage();
375 | var currentArtboard = currentPage.currentArtboard();
376 | var currentSelection = context.selection;
377 | var hasSelection = currentSelection.count() != 0;
378 |
379 | return {
380 | document : document,
381 | view : view,
382 | currentPage : currentPage,
383 | currentArtboard : currentArtboard,
384 | currentSelection : currentSelection,
385 | hasSelection : function(){
386 | return hasSelection;
387 | }
388 | };
389 | };
390 |
391 | /*--------------------------------------------------------------------------------------------------------------------*/
392 | // User Defaults
393 | /*--------------------------------------------------------------------------------------------------------------------*/
394 |
395 | lib.createPluginDefaults = function(pluginId,defaults,group){
396 | var userDefaults = NSUserDefaults.standardUserDefaults();
397 | var userDefaultsPlugin = userDefaults.objectForKey_(pluginId);
398 |
399 | //userDefaults.removeObjectForKey_(pluginId);
400 |
401 | if(!userDefaultsPlugin){
402 | var entries = lib.objToDict(defaults);
403 | userDefaults.setObject_forKey_(group ? lib.createDict([entries],[group]) : entries ,pluginId);
404 | return;
405 | }
406 |
407 | if(group){
408 | if(!userDefaultsPlugin.objectForKey_(group)){
409 | var dict = NSMutableDictionary.alloc().init();
410 | dict.setDictionary_(userDefaultsPlugin);
411 | dict.setValue_forKey_(lib.objToDict(defaults),group);
412 |
413 | userDefaults.setObject_forKey_(dict,pluginId);
414 | userDefaults.synchronize();
415 | }
416 | }
417 | };
418 |
419 | lib.synchronizePluginDefaults = function(pluginId,valueMap,group){
420 | var userDefaults = NSUserDefaults.standardUserDefaults();
421 | var userDefaultsPlugin = userDefaults.objectForKey_(pluginId);
422 |
423 | var dict = NSMutableDictionary.alloc().init();
424 | dict.setDictionary_(userDefaultsPlugin);
425 |
426 | if(group){
427 | dict.setObject_forKey_(lib.objToDict(valueMap),group);
428 | } else {
429 | for(var p in valueMap){
430 | dict.setValue_forKey_(valueMap[p],p);
431 | }
432 | }
433 |
434 | userDefaults.setObject_forKey_(dict,pluginId);
435 | userDefaults.synchronize();
436 | };
437 |
438 | lib.getPluginSettingsObj = function(pluginId,group){
439 | var userDefaultsPlugin = NSUserDefaults.standardUserDefaults().objectForKey_(pluginId);
440 | return group ? lib.dictToObj(userDefaultsPlugin.objectForKey_(group)) : lib.dictToObj(userDefaultsPlugin);
441 | };
442 |
--------------------------------------------------------------------------------
/text-tools.sketchplugin/Contents/Sketch/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Text Tools",
3 | "identifier": "io.shifted.text-tools",
4 | "version": "0.1.1",
5 | "description": "",
6 | "author": "Henryk Wollik",
7 | "authorEmail": "hwollik@hotmail.com",
8 | "compatibleVersion": 3,
9 | "bundleVersion": 1,
10 | "commands": [
11 | {
12 | "script": "create-font-metrics.cocoascript",
13 | "handler": "createTextFontMetrics",
14 | "name": "Create Font Metrics",
15 | "identifier": "create-font-metrics"
16 | },
17 | {
18 | "script": "create-baseline-layer.cocoascript",
19 | "handler": "createBaselineLayer",
20 | "name": "Create Baseline Layer",
21 | "identifier": "create-baseline-layer"
22 | },
23 | {
24 | "script": "align.cocoascript",
25 | "handler": "alignText",
26 | "name": "Align Text",
27 | "identifier": "align-text"
28 | },
29 | {
30 | "script": "count.cocoascript",
31 | "handler": "count",
32 | "name": "Count Characters Per Line",
33 | "identifier": "count-characters"
34 | },
35 | {
36 | "script": "columnize.cocoascript",
37 | "handler": "columnize",
38 | "name": "Columnize Text",
39 | "identifier": "columnize-text"
40 | }
41 | ],
42 | "menu": {
43 | "items": [
44 | "align-text",
45 | "columnize-text",
46 | "create-font-metrics",
47 | "create-baseline-layer",
48 | "count-characters"
49 | ],
50 | "title": "Text Tools"
51 | }
52 | }
--------------------------------------------------------------------------------
/text-tools.sketchplugin/Contents/Sketch/shared.cocoascript:
--------------------------------------------------------------------------------
1 | var PLUGIN_ID = "io.shifted.text-tools";
--------------------------------------------------------------------------------