├── README.md ├── README ├── download-btn.png └── header.png └── Sketch-Highlighter.sketchplugin └── Contents └── Sketch ├── manifest.json └── script.js /README.md: -------------------------------------------------------------------------------- 1 | ![](README/header.png) 2 | 3 | --- 4 | 5 | This a Sketch plugin I threw together that generates highlights for selected text layers. It inserts a shape layer behind your text lines, giving them a 'highlighted' look. 6 | 7 | It even works in text edit mode, and can generate highlights only for the portion of text you have selected. You can also specify padding. 8 | 9 | --- 10 | 11 |

12 | 13 |

14 | -------------------------------------------------------------------------------- /README/download-btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt-curtis/Sketch-Highlighter/a5f392f1c0fde5f0d638ae7944e441c4b8bfdfc9/README/download-btn.png -------------------------------------------------------------------------------- /README/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt-curtis/Sketch-Highlighter/a5f392f1c0fde5f0d638ae7944e441c4b8bfdfc9/README/header.png -------------------------------------------------------------------------------- /Sketch-Highlighter.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author" : "Matt Curtis", 3 | "commands" : [ 4 | { 5 | "script" : "script.js", 6 | "name" : "Highlight Text", 7 | "handlers" : { 8 | "run" : "onRun" 9 | }, 10 | "identifier" : "highlight-text" 11 | } 12 | ], 13 | "menu" : { 14 | "items" : [ 15 | "highlight-text" 16 | ], 17 | "title" : "Sketch Text Highlighter" 18 | }, 19 | "identifier" : "com.matt-curtis.sketch-highlighter", 20 | "version" : "1.1.2", 21 | "description" : "Generates a shape layer from the selected text layer that create a highlighted effect.", 22 | "authorEmail" : "", 23 | "name" : "Sketch Text Higlighter" 24 | } -------------------------------------------------------------------------------- /Sketch-Highlighter.sketchplugin/Contents/Sketch/script.js: -------------------------------------------------------------------------------- 1 | var context; 2 | 3 | var getLineRectsForTextLayer = function(textLayer, padding, factorInSelection){ 4 | var textLayerOrigin = textLayer.frame().origin(); 5 | var stringValue = textLayer.stringValue() + ""; 6 | 7 | // Create & size text container 8 | 9 | var textContainer = NSTextContainer.new(); 10 | 11 | textContainer.size = NSMakeSize( 12 | textLayer.frame().size().width, 13 | Number.MAX_VALUE 14 | ); 15 | 16 | // Create layout manager & text storage 17 | 18 | var layoutManager = NSLayoutManager.new(); 19 | var textStorage = NSTextStorage.new(); 20 | 21 | textStorage.setAttributedString(textLayer.attributedStringValue()); 22 | 23 | layoutManager.textStorage = textStorage; 24 | 25 | layoutManager.addTextContainer(textContainer); 26 | 27 | // Prequisites 28 | 29 | var lineRects = []; 30 | var index = 0, numberOfGlyphs = layoutManager.numberOfGlyphs(); 31 | 32 | // Factor in selection 33 | 34 | var currentHandler = context.document.currentHandler(); 35 | 36 | if(factorInSelection && currentHandler.textView){ 37 | var selectedRange = currentHandler.textView().selectedRange(); 38 | 39 | if(selectedRange.length > 0){ 40 | index = selectedRange.location; 41 | numberOfGlyphs = NSMaxRange(selectedRange); 42 | } 43 | } 44 | 45 | // Enumerate lines 46 | 47 | while(index < numberOfGlyphs){ 48 | var endOfLineIndex; 49 | 50 | // Get end of line index 51 | 52 | var lineRangePtr = MOPointer.new(); 53 | 54 | [layoutManager lineFragmentUsedRectForGlyphAtIndex:index effectiveRange:lineRangePtr]; 55 | 56 | endOfLineIndex = NSMaxRange(lineRangePtr.value()); 57 | 58 | // Get bounding rect 59 | // Also ignore empty line ends and hard line breaks 60 | 61 | var rangeLength = Math.min(endOfLineIndex, numberOfGlyphs) - index; 62 | 63 | var lineText = stringValue.substr(index, rangeLength), trimmedLineText = lineText.trimRight(); 64 | 65 | rangeLength -= (lineText.length - trimmedLineText.length); 66 | 67 | var glyphRange = NSMakeRange(index, rangeLength); 68 | var lineRect = [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer]; 69 | 70 | // Update index 71 | 72 | index = endOfLineIndex; 73 | 74 | // Offset from text layer... 75 | 76 | lineRect.origin.x = textLayerOrigin.x; 77 | lineRect.origin.y += textLayerOrigin.y; 78 | 79 | // Apply padding 80 | 81 | // Top & bottom 82 | 83 | lineRect.origin.y -= padding.top; 84 | 85 | lineRect.size.height += padding.top; 86 | 87 | lineRect.size.height += padding.bottom; 88 | 89 | // Left & right 90 | 91 | lineRect.origin.x -= padding.left; 92 | lineRect.size.width += padding.left; 93 | 94 | lineRect.size.width += padding.right; 95 | 96 | // Store rect 97 | 98 | lineRects.push(lineRect); 99 | } 100 | 101 | return lineRects; 102 | }; 103 | 104 | var createLineShapeGroupForTextLayer = function(textLayer, padding, factorInSelection){ 105 | // Create shape group 106 | 107 | var shapeGroup = MSShapeGroup.new(); 108 | 109 | shapeGroup.name = "Lines Shape"; 110 | 111 | // Apply fill 112 | 113 | shapeGroup.style().addStylePartOfType(0); 114 | 115 | // Add line rects as shapes 116 | 117 | var lineRects = getLineRectsForTextLayer(textLayer, padding, factorInSelection); 118 | 119 | for(var i = 0; i < lineRects.length; i++){ 120 | var lineRect = lineRects[i]; 121 | 122 | // Create shape layer 123 | 124 | var bezierPath = NSBezierPath.bezierPathWithRect(lineRect); 125 | var shapeLayer = [MSShapePathLayer shapeWithBezierPath:bezierPath]; 126 | 127 | shapeLayer.booleanOperation = 0; // Union 128 | shapeLayer.name = "Line " + (i + 1); 129 | 130 | // Add shape group to main group 131 | 132 | shapeGroup.addLayer(shapeLayer); 133 | } 134 | 135 | // Fit group to contents, give same origin as textLayer 136 | 137 | shapeGroup.resizeToFitChildrenWithOption(1); 138 | 139 | return shapeGroup; 140 | }; 141 | 142 | var prompt = function(promptTitle, defaultValue){ 143 | if(!defaultValue) defaultValue = ""; 144 | 145 | var alert = [NSAlert 146 | alertWithMessageText:promptTitle 147 | defaultButton:"OK" 148 | alternateButton:"Cancel" 149 | otherButton:nil 150 | informativeTextWithFormat:""]; 151 | 152 | var input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 24)]; 153 | 154 | [input setStringValue:defaultValue]; 155 | [alert setAccessoryView:input]; 156 | [input selectText:nil]; 157 | 158 | var pressedButtonIndex = [alert runModal]; 159 | 160 | if(pressedButtonIndex == NSAlertDefaultReturn){ 161 | [input validateEditing]; 162 | 163 | return [input stringValue]+""; 164 | } else { 165 | return null; 166 | } 167 | }; 168 | 169 | var alert = function(message, title){ 170 | var alert = NSAlert.new(); 171 | 172 | alert.messageText = title || "Alert"; 173 | alert.informativeText = message+""; 174 | 175 | alert.runModal(); 176 | }; 177 | 178 | var SessionStorage = new function(){ 179 | var ns = "com.matt-curtis.sketch-highlighter", nsPrefix = ns + "."; 180 | var dictionary = NSThread.mainThread().threadDictionary(); 181 | 182 | this.get = function(key){ 183 | key = nsPrefix + key; 184 | 185 | return dictionary[key]; 186 | }; 187 | 188 | this.set = function(key, value){ 189 | key = nsPrefix + key; 190 | 191 | dictionary[key] = value; 192 | }; 193 | }; 194 | 195 | var promptUserForAndReturnPadding = function(){ 196 | var paddingString = prompt("Enter padding (i.e. top,right,bottom,left).\nUse negative values to create inset.", "0,0,0,0"); 197 | 198 | if(!paddingString) return null; 199 | 200 | var paddingValueStrings = paddingString.split(","); 201 | 202 | var padding = { 203 | top: parseFloat(paddingValueStrings[0]) || 0, 204 | right: parseFloat(paddingValueStrings[1]) || 0, 205 | bottom: parseFloat(paddingValueStrings[2]) || 0, 206 | left: parseFloat(paddingValueStrings[3]) || 0 207 | }; 208 | 209 | if(paddingValueStrings.length == 1){ 210 | padding.right = padding.bottom = padding.left = padding.top; 211 | } else if(paddingValueStrings.length == 2){ 212 | padding.bottom = padding.top; 213 | padding.left = padding.right; 214 | } 215 | 216 | return padding; 217 | }; 218 | 219 | var onRun = function(_context){ 220 | context = _context; 221 | 222 | // Grab and confirm selected text layer(s) 223 | 224 | var selectedLayers = context.selection; 225 | var foundTextLayers = false; 226 | var padding; 227 | 228 | for(var i = 0; i < selectedLayers.length; i++){ 229 | var layer = selectedLayers[i]; 230 | 231 | if(layer.class() != MSTextLayer.class()) continue; 232 | 233 | // Found text layer - prompt user for padding 234 | 235 | if(!foundTextLayers){ 236 | foundTextLayers = true; 237 | 238 | padding = promptUserForAndReturnPadding(); 239 | 240 | if(!padding) break; 241 | } 242 | 243 | // Insert lines behind text layer 244 | 245 | var parentGroup = layer.parentGroup(); 246 | 247 | var lineShapeGroup = createLineShapeGroupForTextLayer(layer, padding, true); 248 | 249 | var destinationIndex = parentGroup.layers().indexOfObject(layer); 250 | 251 | [parentGroup insertLayer:lineShapeGroup atIndex:destinationIndex]; 252 | } 253 | 254 | if(!foundTextLayers){ 255 | // Hey - no text layers found. 256 | 257 | alert("No text layers in selection."); 258 | } 259 | }; --------------------------------------------------------------------------------