├── .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 | ![Font Metric](./readme/text-tools-font-metrics.gif) 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 | ![Align text](./readme/text-tools-align.gif) 30 | ![Align text](./readme/text-tools-alignment-panel.png) 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 | ![Baseline Layer](./readme/text-tools-baseline-layer.gif) 46 | ![Baseline Layer](./readme/text-tools-baseline.png) 47 | ![Baseline Layer](./readme/text-tools-baseline-panel.png) 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 | ![Columnize](./readme/text-tools-columnize.gif) 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"; --------------------------------------------------------------------------------