├── README.md
├── README
├── download-btn.png
└── header.png
└── Sketch-Highlighter.sketchplugin
└── Contents
└── Sketch
├── manifest.json
└── script.js
/README.md:
--------------------------------------------------------------------------------
1 | 
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 | };
--------------------------------------------------------------------------------