├── .eslintignore
├── .eslintrc.json
├── .gitattributes
├── .gitignore
├── .npmrc
├── LICENSE
├── README.md
├── Style Master.sketchplugin
└── Contents
│ ├── Resources
│ ├── UIBundle
│ │ └── Contents
│ │ │ └── Resources
│ │ │ ├── RenameStyles.json
│ │ │ └── RenameStyles.nib
│ ├── icon.png
│ ├── rename-styles.png
│ └── rename-styles@2x.png
│ └── Sketch
│ ├── manifest.json
│ └── plugin.js
├── appcast.xml
├── docs
├── all-styles.png
├── dialog-start.png
├── matching-styles.png
└── show-only-matching-styles.gif
├── package.json
├── pnpm-lock.yaml
├── resources
├── icon.png
├── rename-styles.png
└── rename-styles@2x.png
├── scripts
├── build-plugin-js.js
├── enable-sketch-plugin-hotloading.js
├── install.js
├── link.js
├── postversion.js
├── publish.sh
└── utils.js
├── src
├── commands
│ └── rename-styles.js
├── lib
│ ├── MochaJSDelegate.js
│ ├── shared-style-renamer.js
│ ├── sketch-nibui.js
│ └── utils.js
├── manifest.json
├── nib
│ ├── RenameStyles.m
│ ├── RenameStyles.xib
│ └── SketchNibUI.xcodeproj
│ │ └── project.pbxproj
└── plugin.js
├── webpack-lib
├── config-helpers.js
├── loaders
│ ├── sketch-xib-connection-loader.js
│ ├── sketch-xib-loader-utils.js
│ └── sketch-xib-loader.js
└── plugins
│ └── webpack-export-sketch-commands-plugin.js
└── webpack.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.sketchplugin
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "ecmaVersion": 2017,
4 | "sourceType": "module",
5 | "ecmaFeatures": {
6 | }
7 | },
8 |
9 | "env": {
10 | "es6": true,
11 | "node": true
12 | },
13 |
14 | "extends": "eslint:recommended",
15 |
16 | "globals": {
17 | "NSObject": true,
18 | "NSControl": true,
19 | "NSTextField": true,
20 | "NSMakeSize": true,
21 | "NSFont": true,
22 | "NSAlert": true,
23 | "NSImage": true,
24 | "NSApp": true,
25 | "NSMakePoint": true,
26 | "NSColor": true,
27 | "NSBezelBorder": true,
28 | "NSScrollView": true,
29 | "NSScroller": true,
30 | "NSRegularControlSize": true,
31 | "NSScrollerStyleOverlay": true,
32 | "NSCell": true,
33 | "NSMatrix": true,
34 | "NSMakeRect": true,
35 | "NSListModeMatrix": true,
36 | "CGSizeMake": true,
37 | "NSString": true,
38 | "NSUUID": true,
39 | "NSBundle": true,
40 | "NSClassFromString": true,
41 | "MOClassDescription": true,
42 | "MOProtocolDescription": true,
43 | "NSSelectorFromString": true,
44 | "MOPointer": true,
45 | "NSDictionary": true,
46 | "NSWindow": true,
47 | "NSUTF8StringEncoding": true,
48 | "log": true
49 | },
50 |
51 | "rules": {
52 | "no-console": 0,
53 | "require-await": "error"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.zip filter=lfs diff=lfs merge=lfs -text
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # npm
2 | node_modules/
3 | .npm/
4 | npm-debug.log
5 |
6 | # mac
7 | .DS_Store
8 |
9 | # Editors
10 | .idea/
11 | .vscode/
12 |
13 | # Xcode
14 | *.xcworkspace
15 |
16 | # Misc
17 | *.gifcask
18 | *.mov
19 | test.js
20 | *.zip
21 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aparajita/sketch-style-master/ec849209d6059e9568163c8b397b5538cd49602d/.npmrc
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright © 2017 Aparajita Fishman
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Style Master
2 | Let’s face it — Sketch’s style organizer leaves a lot to be desired. When you want to apply whole scale changes to your shared style names, you’re faced with endless clicking and manual renaming.
3 |
4 | With Style Master, you can rename all or some of your shared styles at once. With the power of regular expressions, you can even completely redesign the naming hierarchy. The possibilities are limitless.
5 |
6 | ## Install with Sketch Runner
7 |
8 | With Sketch Runner, just go to the `install` command and search for `Style Master`. Runner allows you to manage plugins and do much more to speed up your workflow in Sketch. [Download Runner here](http://www.sketchrunner.com).
9 |
10 |
11 |
12 | ## Manual Installation
13 | 1. Go to the [latest release page](https://github.com/aparajita/sketch-style-master/releases/latest) and download `Styles Master.sketchplugin.zip`.
14 | 1. In Sketch, go to `Plugins > Manage Plugins...`, click on the gear icon, and select `Show Plugins Folder`.
15 | 1. Un-zip the downloaded zip archive and then double-click the `Style Master.sketchplugin` file to install it.
16 |
17 | ## Usage
18 |
19 | Select `Plugins > Style Master > Rename Text Styles` or `Plugins > Style Master > Rename Layer Styles` to bring up the renaming dialog:
20 |
21 | 
22 |
23 | The left side of the dialog controls the renaming process, and the right side displays a live preview of how the styles will be renamed. Initially the preview columns are sized to fit the width of the longest style name (plus some extra space), and the window is sized to fit the width of the preview. You may resize the window, however note that the width of the preview columns will not change.
24 |
25 | **Note:** There are [key equivalents](#key-equivalents) for every item in the interface.
26 |
27 | ### Find field/options
28 | Enter the text you wish to search for in the search field (big surprise!). By default, searching is case sensitive. If you want to do case-insensitive searching, check "Ignore case". As you type in the Find field, the preview dynamically updates to indicate which style names match.
29 |
30 | 
31 |
32 | By checking "Regular expression", you can specify any valid [Javascript regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Writing_a_regular_expression_pattern) in the Find field and use a [special replacement syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter) in the Replace field. This allows you to perform extremely sophisticated manipulation of the style names that would extremely tedious otherwise. For examples of using regular expressions, see the [Cookbook](#cookbook).
33 |
34 | **Note:** If the regular expression is invalid (e.g. unmatched parentheses), it is displayed in red.
35 |
36 | ### Replace field
37 | The text you enter here replaces (or removes, if empty) the portion of the style name matched by the Find field. Note that **all** occurrences of the matching text within a given name will be replaced. So, for example, a simple search for "Sm", replacing with "Lg", would rename "Smile / Center / Sm" to "Agile / Center / Sm". This is a case where using a regular expression is useful to ensure you match only certain portions of a style name.
38 |
39 | **Note:** If the Replace field is empty and the current search matches **entire** style names, those styles will not be included when renaming is performed, and they are not included in the rename count. These non-renameable styles are indicated by `` in the “After” column of the preview.
40 |
41 | ### Preview info/options
42 | Below the Find and Replace fields are items relating to the preview. The number of styles that will be renamed when the `Apply` or `Rename` buttons are clicked is displayed first, followed by options that control the preview display.
43 |
44 | #### Show only matching styles
45 | When this option is off, the preview displays all styles. This is useful when you are trying to figure out what to search for. As you type in the Find field, matching styles are displayed in bold in the "Before" column of the preview:
46 |
47 | 
48 |
49 | As you can see in the image above, "Dark" actually matched 33 styles, but only the first 5 are visible because the remaining 28 styles are much later in the style list. This is where the "Show only matching styles" option is useful. When this option is on, only matching styles are displayed in the preview:
50 |
51 | 
52 |
53 | #### Autoscroll to first matching style
54 | When this option is on (the default), as you type in the Find field, the first matching style is scrolled to the top of the preview. This makes it easy to see if you are matching the right styles.
55 |
56 | However, there may be times when you don't want the preview to scroll as you type in the Find field. For example, you may be trying to match a style name with a long regular expression and you have to scroll to see the style name. In this case, turning the "Autoscroll to first matching style" option off allows you to keep the style name visible as you type.
57 |
58 | ### Preview
59 | When the dialog first opens, the preview on the right side of the dialog displays all of your styles, sorted alphabetically (case is significant).
60 |
61 | As you type in the Find field, matching styles are highlighted in bold in the “Before” column, with the renamed style in the “After” column. As you type in the Replace field, the renamed style names update.
62 |
63 | ### Action buttons
64 | | Button | Action |
65 | | :--- | :--- |
66 | | Cancel | Closes the dialog without making any changes. |
67 | | Apply | Renames the matching styles without closing the dialog. |
68 | | Rename | Renames the matching styles and closes the dialog. |
69 |
70 | ## Key equivalents
71 | | Action | Keys |
72 | |:---|:---|
73 | | Toggle "Ignore case" | `⌘I` |
74 | | Toggle "Regular expression" | `⌘R` |
75 | | Toggle "Show only matching styles" | `⌘M` |
76 | | Toggle "Autoscroll to first matching style" | `⌘S` |
77 | | Perform rename | `⌘⇧A` |
78 | | Perform rename and close the dialog | `Return` or `Enter` |
79 | | Cancel and close the dialog | `Esc` or `⌘.` or `⌘W` |
80 |
81 |
82 | ## Cookbook
83 | Here are some typical tasks you might want to perform with Style Master and how to accomplish them. All of them assume the “Regular expression” option is on.
84 |
85 | **Note:** A great tool for testing out regular expressions (and for explaining what the regular expressions in these examples do) can be found at [regular expressions 101](https://regex101.com/#javascript).
86 |
87 | ---
88 |
89 | ### Changing the “/” style
90 | Did you know that you can use spaces around the “/” characters used to create a hierarchy in text style, layer style, and layer names? It makes the full name much easier to read. It’s easy to do this:
91 |
92 | | Find | Replace |
93 | | :--- | :--- |
94 | | `([^ ])/([^ ])` | `$1 / $2` |
95 |
96 | ---
97 |
98 | ### Rearrange the naming hierarchy
99 | Let’s say you are using a template that orders the text styles in the following hierarchy:
100 |
101 | ```
102 | Size (Body, H1, etc.)
103 | Weight (Regular, Semibold, etc.)
104 | Alignment (Left, Center, Right)
105 | Color
106 | ```
107 |
108 | But you want to reverse the order of Alignment and Weight:
109 |
110 | ```
111 | Size (Body, H1, etc.)
112 | Alignment (Left, Center, Right)
113 | Weight (Regular, Semibold, etc.)
114 | Color
115 | ```
116 |
117 | Here’s how:
118 |
119 | | Find | Replace |
120 | | :--- | :--- |
121 | | `^(.+?)\s*/\s*(.+?)\s*/\s*(.+?)\s*/\s*(.+)` | `$1 / $3 / $2 / $4` |
122 |
123 | ---
124 |
125 | ### Collapsing the naming hierarchy
126 | Let’s say you have the same hierarchy as in the example above, but instead of reordering the Alignment and Weight, you want to combine the Alignment and Color into a single level. So instead of `Body / Regular / Left / Black` you want `Body / Regular / Left - Black`. This would be a soul-crushing task if you had to do it manually with dozens of styles.
127 |
128 | Regular expressions to the rescue!
129 |
130 | | Find | Replace |
131 | | :--- | :--- |
132 | | `^(.+?)\s*/\s*(.+?)\s*/\s*(.+?)\s*/\s*(.+)` | `$1 / $2 / $3 - $4` |
133 |
134 | ---
135 |
136 | ### Expanding the naming hierarchy
137 | This is the opposite of collapsing the naming hierarchy. You have `Body / Regular / Left - Black` but you want `Body / Regular / Left / Black`.
138 |
139 | Here’s how:
140 |
141 | | Find | Replace |
142 | | :--- | :--- |
143 | | `- ([^-]+)$` | `/ $1`
144 |
145 | ## Acknowledgements
146 | Special thanks to other members of the Sketch plugin community for their invaluable foundational work:
147 |
148 | * Mathieu Dutour – [skpm](https://github.com/skpm/skpm)
149 | * Matt Curtis – [MochaJSDelegate](https://github.com/matt-curtis/MochaJSDelegate)
150 | * Roman Nurik – [Plugin nib support](https://github.com/romannurik/Sketch-NibUITemplatePlugin)
151 |
--------------------------------------------------------------------------------
/Style Master.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/RenameStyles.json:
--------------------------------------------------------------------------------
1 | {
2 | "outlets": [
3 | "scrollView",
4 | "window",
5 | "afterLabel",
6 | "beforeLabel",
7 | "versionLabel"
8 | ],
9 | "actions": [
10 | "toggleAutoscroll:",
11 | "handleRename:",
12 | "toggleFindOption:",
13 | "handleCancel:",
14 | "toggleFindOption:",
15 | "toggleShowOnlyMatchingStyles:",
16 | "handleApply:"
17 | ]
18 | }
--------------------------------------------------------------------------------
/Style Master.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/RenameStyles.nib:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aparajita/sketch-style-master/ec849209d6059e9568163c8b397b5538cd49602d/Style Master.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/RenameStyles.nib
--------------------------------------------------------------------------------
/Style Master.sketchplugin/Contents/Resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aparajita/sketch-style-master/ec849209d6059e9568163c8b397b5538cd49602d/Style Master.sketchplugin/Contents/Resources/icon.png
--------------------------------------------------------------------------------
/Style Master.sketchplugin/Contents/Resources/rename-styles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aparajita/sketch-style-master/ec849209d6059e9568163c8b397b5538cd49602d/Style Master.sketchplugin/Contents/Resources/rename-styles.png
--------------------------------------------------------------------------------
/Style Master.sketchplugin/Contents/Resources/rename-styles@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aparajita/sketch-style-master/ec849209d6059e9568163c8b397b5538cd49602d/Style Master.sketchplugin/Contents/Resources/rename-styles@2x.png
--------------------------------------------------------------------------------
/Style Master.sketchplugin/Contents/Sketch/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Style Master",
3 | "version": "1.0.5",
4 | "identifier": "com.aparajita.style-master",
5 | "description": "Shared style renaming.",
6 | "author": "Aparajita Fishman",
7 | "authorEmail": "aparajita@aparajita.com",
8 | "homepage": "https://github.com/aparajita/sketch-style-master",
9 | "compatibleVersion": "66.0",
10 | "bundleVersion": 1,
11 | "icon": "icon.png",
12 | "disableCocoaScriptPreprocessor": true,
13 | "scope": "document",
14 | "appcast": "https://raw.githubusercontent.com/aparajita/sketch-style-master/master/appcast.xml",
15 | "nibProject": "nib",
16 | "nibBundle": "UIBundle",
17 | "commands": [
18 | {
19 | "name": "Rename Text Styles",
20 | "script": "plugin.js",
21 | "handler": "renameTextStyles",
22 | "identifier": "rename-text-styles",
23 | "description": "Rename shared text styles",
24 | "icon": "rename-styles.png"
25 | },
26 | {
27 | "name": "Rename Layer Styles",
28 | "script": "plugin.js",
29 | "handler": "renameLayerStyles",
30 | "identifier": "rename-layer-styles",
31 | "description": "Rename shared layer styles",
32 | "icon": "rename-styles.png"
33 | }
34 | ],
35 | "menu": {
36 | "title": "Style Master",
37 | "items": [
38 | "rename-text-styles",
39 | "rename-layer-styles"
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Style Master.sketchplugin/Contents/Sketch/plugin.js:
--------------------------------------------------------------------------------
1 |
2 | var that = this;
3 | function run (key, context) {
4 | that.context = context;
5 |
6 | var exports =
7 | /******/ (function(modules) { // webpackBootstrap
8 | /******/ // The module cache
9 | /******/ var installedModules = {};
10 | /******/
11 | /******/ // The require function
12 | /******/ function __webpack_require__(moduleId) {
13 | /******/
14 | /******/ // Check if module is in cache
15 | /******/ if(installedModules[moduleId]) {
16 | /******/ return installedModules[moduleId].exports;
17 | /******/ }
18 | /******/ // Create a new module (and put it into the cache)
19 | /******/ var module = installedModules[moduleId] = {
20 | /******/ i: moduleId,
21 | /******/ l: false,
22 | /******/ exports: {}
23 | /******/ };
24 | /******/
25 | /******/ // Execute the module function
26 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
27 | /******/
28 | /******/ // Flag the module as loaded
29 | /******/ module.l = true;
30 | /******/
31 | /******/ // Return the exports of the module
32 | /******/ return module.exports;
33 | /******/ }
34 | /******/
35 | /******/
36 | /******/ // expose the modules object (__webpack_modules__)
37 | /******/ __webpack_require__.m = modules;
38 | /******/
39 | /******/ // expose the module cache
40 | /******/ __webpack_require__.c = installedModules;
41 | /******/
42 | /******/ // define getter function for harmony exports
43 | /******/ __webpack_require__.d = function(exports, name, getter) {
44 | /******/ if(!__webpack_require__.o(exports, name)) {
45 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
46 | /******/ }
47 | /******/ };
48 | /******/
49 | /******/ // define __esModule on exports
50 | /******/ __webpack_require__.r = function(exports) {
51 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
52 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
53 | /******/ }
54 | /******/ Object.defineProperty(exports, '__esModule', { value: true });
55 | /******/ };
56 | /******/
57 | /******/ // create a fake namespace object
58 | /******/ // mode & 1: value is a module id, require it
59 | /******/ // mode & 2: merge all properties of value into the ns
60 | /******/ // mode & 4: return value when already ns object
61 | /******/ // mode & 8|1: behave like require
62 | /******/ __webpack_require__.t = function(value, mode) {
63 | /******/ if(mode & 1) value = __webpack_require__(value);
64 | /******/ if(mode & 8) return value;
65 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
66 | /******/ var ns = Object.create(null);
67 | /******/ __webpack_require__.r(ns);
68 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
69 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
70 | /******/ return ns;
71 | /******/ };
72 | /******/
73 | /******/ // getDefaultExport function for compatibility with non-harmony modules
74 | /******/ __webpack_require__.n = function(module) {
75 | /******/ var getter = module && module.__esModule ?
76 | /******/ function getDefault() { return module['default']; } :
77 | /******/ function getModuleExports() { return module; };
78 | /******/ __webpack_require__.d(getter, 'a', getter);
79 | /******/ return getter;
80 | /******/ };
81 | /******/
82 | /******/ // Object.prototype.hasOwnProperty.call
83 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
84 | /******/
85 | /******/ // __webpack_public_path__
86 | /******/ __webpack_require__.p = "";
87 | /******/
88 | /******/
89 | /******/ // Load entry module and return exports
90 | /******/ return __webpack_require__(__webpack_require__.s = "./src/plugin.js");
91 | /******/ })
92 | /************************************************************************/
93 | /******/ ({
94 |
95 | /***/ "./src/commands/rename-styles.js":
96 | /*!***************************************!*\
97 | !*** ./src/commands/rename-styles.js ***!
98 | \***************************************/
99 | /*! exports provided: renameTextStyles, renameLayerStyles */
100 | /***/ (function(module, __webpack_exports__, __webpack_require__) {
101 |
102 | "use strict";
103 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"renameTextStyles\", function() { return renameTextStyles; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"renameLayerStyles\", function() { return renameLayerStyles; });\n/* harmony import */ var _lib_shared_style_renamer__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../lib/shared-style-renamer */ \"./src/lib/shared-style-renamer.js\");\n\n\n\nfunction renameTextStyles(context) {\n var styles = context.document.documentData().layerTextStyles();\n var renamer = new _lib_shared_style_renamer__WEBPACK_IMPORTED_MODULE_0__[\"SharedStyleRenamer\"](context, styles, 'text');\n renamer.run();\n}\nfunction renameLayerStyles(context) {\n var styles = context.document.documentData().layerStyles();\n var renamer = new _lib_shared_style_renamer__WEBPACK_IMPORTED_MODULE_0__[\"SharedStyleRenamer\"](context, styles, 'layer');\n renamer.run();\n}\n\n//# sourceURL=webpack:///./src/commands/rename-styles.js?");
104 |
105 | /***/ }),
106 |
107 | /***/ "./src/lib/MochaJSDelegate.js":
108 | /*!************************************!*\
109 | !*** ./src/lib/MochaJSDelegate.js ***!
110 | \************************************/
111 | /*! exports provided: default */
112 | /***/ (function(module, __webpack_exports__, __webpack_require__) {
113 |
114 | "use strict";
115 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return MochaJSDelegate; });\n\n\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nvar MochaJSDelegate = /*#__PURE__*/function () {\n function MochaJSDelegate(selectorHandlerDict, superclass) {\n _classCallCheck(this, MochaJSDelegate);\n\n this.uniqueClassName = 'MochaJSDelegate_DynamicClass_' + NSUUID.UUID().UUIDString();\n this.delegateClassDesc = MOClassDescription.allocateDescriptionForClassWithName_superclass_(this.uniqueClassName, superclass || NSObject);\n this.delegateClassDesc.registerClass();\n this.handlers = {};\n\n if (_typeof(selectorHandlerDict) === 'object') {\n var selectors = Object.keys(selectorHandlerDict);\n\n for (var _i = 0, _selectors = selectors; _i < _selectors.length; _i++) {\n var selectorString = _selectors[_i];\n this.setHandlerForSelector(selectorString, selectorHandlerDict[selectorString]);\n }\n }\n }\n\n _createClass(MochaJSDelegate, [{\n key: \"setHandlerForSelector\",\n value: function setHandlerForSelector(selectorString, func) {\n var handlerHasBeenSet = (selectorString in this.handlers);\n this.handlers[selectorString] = func;\n /*\n For some reason, Mocha acts weird about arguments: https://github.com/logancollins/Mocha/issues/28\n We have to basically create a dynamic handler with a likewise dynamic number of predefined arguments.\n */\n\n if (!handlerHasBeenSet) {\n var args = [];\n var regex = /:/g;\n\n while (regex.exec(selectorString)) {\n args.push('arg' + args.length);\n } // JavascriptCore tends to die a horrible death if an uncaught exception occurs in an action method\n\n\n var body = \"{\\n try {\\n return func.apply(this, arguments)\\n }\\n catch(ex) {\\n log(ex)\\n }\\n }\";\n var code = NSString.stringWithFormat('(function (%@) %@)', args.join(', '), body);\n var dynamicFunction = eval(String(code));\n var selector = NSSelectorFromString(selectorString);\n this.delegateClassDesc.addInstanceMethodWithSelector_function_(selector, dynamicFunction);\n }\n }\n }, {\n key: \"removeHandlerForSelector\",\n value: function removeHandlerForSelector(selectorString) {\n delete this.handlers[selectorString];\n }\n }, {\n key: \"getHandlerForSelector\",\n value: function getHandlerForSelector(selectorString) {\n return this.handlers[selectorString];\n }\n }, {\n key: \"getAllHandlers\",\n value: function getAllHandlers() {\n return this.handlers;\n }\n }, {\n key: \"getClass\",\n value: function getClass() {\n return NSClassFromString(this.uniqueClassName);\n }\n }, {\n key: \"getClassInstance\",\n value: function getClassInstance() {\n return this.getClass()[\"new\"]();\n }\n }]);\n\n return MochaJSDelegate;\n}();\n\n\n\n//# sourceURL=webpack:///./src/lib/MochaJSDelegate.js?");
116 |
117 | /***/ }),
118 |
119 | /***/ "./src/lib/shared-style-renamer.js":
120 | /*!*****************************************!*\
121 | !*** ./src/lib/shared-style-renamer.js ***!
122 | \*****************************************/
123 | /*! exports provided: SharedStyleRenamer, renameTextStyles, renameLayerStyles */
124 | /***/ (function(module, __webpack_exports__, __webpack_require__) {
125 |
126 | "use strict";
127 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SharedStyleRenamer\", function() { return SharedStyleRenamer; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"renameTextStyles\", function() { return renameTextStyles; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"renameLayerStyles\", function() { return renameLayerStyles; });\n/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ \"./src/lib/utils.js\");\n/* harmony import */ var _sketch_nibui__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./sketch-nibui */ \"./src/lib/sketch-nibui.js\");\n/* harmony import */ var sketch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! sketch */ \"sketch\");\n/* harmony import */ var sketch__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(sketch__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _nib_RenameStyles_xib__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../nib/RenameStyles.xib */ \"./src/nib/RenameStyles.xib\");\n/* harmony import */ var _nib_RenameStyles_m__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../nib/RenameStyles.m */ \"./src/nib/RenameStyles.m\");\n/* harmony import */ var _manifest_json__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../manifest.json */ \"./src/manifest.json\");\nvar _manifest_json__WEBPACK_IMPORTED_MODULE_5___namespace = /*#__PURE__*/__webpack_require__.t(/*! ../manifest.json */ \"./src/manifest.json\", 1);\n/*\n Handler for 'Rename Text Styles' command.\n*/\n // Code being used\n\nfunction _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === \"undefined\" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === \"number\") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError(\"Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\"); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it[\"return\"] != null) it[\"return\"](); } finally { if (didErr) throw err; } } }; }\n\nfunction _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === \"string\") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === \"Object\" && o.constructor) n = o.constructor.name; if (n === \"Map\" || n === \"Set\") return Array.from(o); if (n === \"Arguments\" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }\n\nfunction _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\n\n\n // webpack build dependencies\n\n\n\n\nvar PREVIEW_COLUMN_COUNT = 2;\nvar PREVIEW_CELL_SPACING = NSMakeSize(16, 2);\nvar PREVIEW_VISIBLE_ROWS = 27;\nvar FIND_FIELD_TAG = 1;\nvar REPLACE_FIELD_TAG = 2;\n\nfunction capitalize(text) {\n return text.charAt(0).toUpperCase() + text.slice(1);\n}\n\nvar SharedStyleRenamer = /*#__PURE__*/function () {\n function SharedStyleRenamer(context, styles, layerType) {\n _classCallCheck(this, SharedStyleRenamer);\n\n this.context = context;\n this.sketch = sketch__WEBPACK_IMPORTED_MODULE_2___default.a;\n this.styles = styles;\n this.styleInfo = [];\n this.renamedStyles = [];\n this.find = '';\n this.replace = '';\n this.cellFontRegular = NSFont.systemFontOfSize(NSFont.systemFontSize());\n this.cellFontBold = NSFont.boldSystemFontOfSize(NSFont.systemFontSize());\n this.layerType = layerType;\n this.dialogTitle = \"Rename \".concat(capitalize(layerType), \" Styles\");\n this.ivars = {\n styles: styles,\n renameCount: 0,\n findPattern: '',\n ignoreCase: false,\n useRegex: false,\n replacePattern: '',\n showMatchingStyles: false,\n autoScroll: true,\n findColor: NSColor.textColor()\n };\n }\n\n _createClass(SharedStyleRenamer, [{\n key: \"makeAlert\",\n value: function makeAlert() {\n var alert = NSAlert[\"new\"]();\n alert.setMessageText(this.dialogTitle);\n var icon = NSImage.alloc().initByReferencingFile(this.context.plugin.urlForResourceNamed('rename-styles@2x.png').path());\n alert.setIcon(icon);\n return alert;\n }\n }, {\n key: \"loadNib\",\n value: function loadNib() {\n this.nib = new _sketch_nibui__WEBPACK_IMPORTED_MODULE_1__[\"NibUI\"](this.context, 'UIBundle', 'RenameStyles', this, this.ivars);\n this.nib.outlets.window.setTitle(this.dialogTitle);\n this.nib.outlets.versionLabel.setStringValue(\"v\".concat(_manifest_json__WEBPACK_IMPORTED_MODULE_5__[\"version\"]));\n }\n }, {\n key: \"windowWillClose\",\n value: function windowWillClose() {\n NSApp.stopModal();\n }\n }, {\n key: \"controlTextDidChange\",\n value: function controlTextDidChange(notification) {\n var tag = notification.object().tag();\n\n if (tag == FIND_FIELD_TAG) {\n this.searchForMatchingStyles();\n } else if (tag == REPLACE_FIELD_TAG) {\n this.updateReplacedNames();\n }\n }\n }, {\n key: \"toggleShowOnlyMatchingStyles\",\n value: function toggleShowOnlyMatchingStyles() {\n if (!this.nib.ivars.showMatchingStyles.boolValue()) {\n this.resetRenamedStyles();\n }\n\n this.searchForMatchingStyles();\n }\n }, {\n key: \"toggleFindOption\",\n value: function toggleFindOption() {\n this.searchForMatchingStyles();\n }\n }, {\n key: \"toggleAutoscroll\",\n value: function toggleAutoscroll() {\n this.scrollToFirstRenamedStyle();\n }\n }, {\n key: \"handleRename\",\n value: function handleRename() {\n this.renameStyles();\n NSApp.stopModal();\n }\n }, {\n key: \"handleApply\",\n value: function handleApply() {\n this.applyRename();\n }\n }, {\n key: \"handleCancel\",\n value: function handleCancel() {\n NSApp.stopModal();\n }\n }, {\n key: \"applyRename\",\n value: function applyRename() {\n this.renameStyles();\n this.initStyleInfo();\n this.nib.ivars.findPattern = '';\n this.nib.ivars.replacePattern = '';\n this.nib.ivars.showMatchingStyles = false;\n this.nib.outlets.window.makeFirstResponder(this.nib.outlets.window.initialFirstResponder());\n this.searchForMatchingStyles();\n }\n }, {\n key: \"scrollToFirstRenamedStyle\",\n value: function scrollToFirstRenamedStyle() {\n if (!this.nib.ivars.autoScroll.boolValue()) {\n return;\n }\n\n var insets = this.nib.outlets.scrollView.contentInsets();\n var point = NSMakePoint(0, 0);\n\n if (this.renamedStyles.length > 0) {\n for (var i = 0; i < this.renamedStyles.length; i++) {\n var info = this.renamedStyles[i];\n\n if (info.newName.length > 0) {\n point = this.matrix.cellFrameAtRow_column(i, 0).origin;\n break;\n }\n }\n } else {\n point = this.matrix.cellFrameAtRow_column(0, 0).origin;\n }\n\n point.y -= insets.top - 1; // Not sure why - 1 is necessary, but it is\n\n this.matrix.scrollPoint(point);\n this.nib.outlets.scrollView.reflectScrolledClipView(this.nib.outlets.scrollView.contentView());\n }\n }, {\n key: \"searchForMatchingStyles\",\n value: function searchForMatchingStyles() {\n // We always want to replace all occurrences of the find string within\n // a style name, so we have to transform a plain search into a RegExp with\n // the 'g' flag, because a plain text replace only replaces the first occurrence.\n var flags = this.nib.ivars.ignoreCase.boolValue() ? 'gi' : 'g';\n var regex = !!this.nib.ivars.useRegex.boolValue(); // When the text field's value is empty, the bound value is returning null,\n // so make sure we have at least an empty string.\n\n var find = String(this.nib.ivars.findPattern || ''); // RegExp constructor can fail, be sure to catch exceptions!\n\n try {\n if (regex) {\n this.find = new RegExp(find, flags);\n } else {\n this.find = new RegExp(Object(_utils__WEBPACK_IMPORTED_MODULE_0__[\"regExpEscape\"])(find), flags);\n }\n\n this.nib.ivars.findColor = NSColor.textColor();\n } catch (ex) {\n this.nib.ivars.findColor = NSColor.redColor();\n find = '';\n this.find = new RegExp('', flags);\n }\n\n this.updateStylesToRename(find.length === 0);\n this.setMatrixData();\n this.scrollToFirstRenamedStyle();\n }\n }, {\n key: \"updateReplacedNames\",\n value: function updateReplacedNames() {\n this.replace = String(this.nib.ivars.replacePattern || '');\n this.updateRenamedStyles();\n this.setMatrixData();\n }\n }, {\n key: \"initStyleInfo\",\n value: function initStyleInfo() {\n var styles = this.styles.objects();\n this.styleInfo = new Array(styles.length);\n\n for (var i = 0; i < styles.length; i++) {\n var style = styles[i];\n this.styleInfo[i] = {\n style: style,\n name: style.name()\n };\n }\n\n this.styleInfo.sort(function (a, b) {\n if (a.name < b.name) {\n return -1;\n }\n\n if (a.name > b.name) {\n return 1;\n }\n\n return 0;\n });\n this.nib.ivars.renameCount = 0;\n this.resetRenamedStyles();\n }\n }, {\n key: \"resetRenamedStyles\",\n value: function resetRenamedStyles() {\n this.renamedStyles = new Array(this.styleInfo.length);\n\n for (var i = 0; i < this.styleInfo.length; i++) {\n var info = this.styleInfo[i];\n this.renamedStyles[i] = {\n style: info.style,\n oldName: info.name,\n newName: ''\n };\n }\n }\n }, {\n key: \"updateStylesToRename\",\n value: function updateStylesToRename(empty) {\n var renamedStyles = [];\n var renameCount = 0;\n\n for (var i = 0; i < this.styleInfo.length; i++) {\n var info = this.styleInfo[i];\n var found = !empty && this.find.test(info.name);\n var newName = void 0;\n\n if (found) {\n newName = info.name.replace(this.find, this.replace);\n\n if (newName.length === 0) {\n newName = '';\n } else {\n renameCount++;\n }\n\n if (this.nib.ivars.showMatchingStyles.boolValue()) {\n renamedStyles.push({\n style: info.style,\n oldName: info.name,\n newName: newName\n });\n } else {\n this.renamedStyles[i].newName = newName;\n }\n } else if (!this.nib.ivars.showMatchingStyles.boolValue()) {\n this.renamedStyles[i].newName = '';\n }\n }\n\n if (this.nib.ivars.showMatchingStyles.boolValue()) {\n this.renamedStyles = renamedStyles;\n }\n\n this.nib.ivars.renameCount = renameCount;\n }\n }, {\n key: \"updateRenamedStyles\",\n value: function updateRenamedStyles() {\n var renameCount = 0;\n\n var _iterator = _createForOfIteratorHelper(this.renamedStyles),\n _step;\n\n try {\n for (_iterator.s(); !(_step = _iterator.n()).done;) {\n var info = _step.value;\n\n if (info.newName) {\n info.newName = info.oldName.replace(this.find, this.replace);\n\n if (info.newName.length === 0) {\n info.newName = '';\n } else {\n renameCount++;\n }\n }\n }\n } catch (err) {\n _iterator.e(err);\n } finally {\n _iterator.f();\n }\n\n this.nib.ivars.renameCount = renameCount;\n }\n }, {\n key: \"renameStyles\",\n value: function renameStyles() {\n var _iterator2 = _createForOfIteratorHelper(this.renamedStyles),\n _step2;\n\n try {\n for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {\n var info = _step2.value;\n\n if (info.newName.length > 0) {\n var copy = info.style.copy();\n copy.setName(info.newName);\n info.style.syncPropertiesFromObject(copy);\n }\n }\n } catch (err) {\n _iterator2.e(err);\n } finally {\n _iterator2.f();\n }\n\n this.context.document.reloadInspector();\n }\n }, {\n key: \"alignLabelWithColumn\",\n value: function alignLabelWithColumn(label, column) {\n var insets = this.nib.outlets.scrollView.contentInsets();\n var scrollViewOrigin = this.nib.outlets.scrollView.frame().origin;\n var cellOrigin = this.matrix.cellFrameAtRow_column(0, column).origin;\n var labelOrigin = label.frame().origin;\n labelOrigin.x = scrollViewOrigin.x + insets.left + cellOrigin.x;\n label.setFrameOrigin(labelOrigin);\n }\n }, {\n key: \"setMatrixData\",\n value: function setMatrixData() {\n var maxWidth = 0;\n this.matrix.renewRows_columns(this.renamedStyles.length, PREVIEW_COLUMN_COUNT);\n this.matrix.sizeToCells();\n var cells = this.matrix.cells();\n\n for (var row = 0; row < this.renamedStyles.length; row++) {\n var info = this.renamedStyles[row]; // After setting the cell's value, get its width so we can calculate\n // the maximum width we'll need for cells.\n\n var index = row * PREVIEW_COLUMN_COUNT;\n var cell = cells[index];\n cell.setFont(info.newName.length === 0 ? this.cellFontRegular : this.cellFontBold);\n cell.setStringValue(info.oldName);\n this.matrix.drawCellAtRow_column(row, 0);\n var size = cell.cellSize();\n maxWidth = Math.max(maxWidth, size.width);\n cell = cells[index + 1];\n cell.setFont(this.cellFontRegular);\n cell.setStringValue(info.newName);\n this.matrix.drawCellAtRow_column(row, 1);\n size = cell.cellSize();\n maxWidth = Math.max(maxWidth, size.width);\n }\n\n return NSMakeSize(maxWidth, cells[0].cellSize().height);\n }\n }, {\n key: \"initMatrix\",\n value: function initMatrix() {\n var BORDER_STYLE = NSBezelBorder;\n var scrollViewSize = this.nib.outlets.scrollView.frame().size;\n var contentSize = NSScrollView.contentSizeForFrameSize_horizontalScrollerClass_verticalScrollerClass_borderType_controlSize_scrollerStyle(scrollViewSize, null, NSScroller, BORDER_STYLE, NSRegularControlSize, NSScrollerStyleOverlay);\n var insets = this.nib.outlets.scrollView.contentInsets();\n contentSize.width -= insets.left + insets.right;\n contentSize.height -= insets.top + insets.bottom; // Start with a default size, we'll fix that later\n\n var cellSize = NSMakeSize(100, 16);\n var cellPrototype = NSCell.alloc().initTextCell('');\n this.matrix = NSMatrix.alloc().initWithFrame_mode_prototype_numberOfRows_numberOfColumns(NSMakeRect(0, 0, cellSize.width * PREVIEW_COLUMN_COUNT, cellSize.height * this.renamedStyles.length), NSListModeMatrix, cellPrototype, this.renamedStyles.length, PREVIEW_COLUMN_COUNT);\n cellSize = this.setMatrixData(); // Add 25% to the cell width to allow for longer names when renaming\n\n cellSize.width *= 1.25; // Make sure the cell width is no less than half of the initial scrollview width\n\n var minWidth = Math.floor(scrollViewSize.width / 2);\n cellSize.width = Math.max(cellSize.width, minWidth);\n this.matrix.setCellSize(CGSizeMake(cellSize.width, cellSize.height));\n this.matrix.setIntercellSpacing(PREVIEW_CELL_SPACING);\n this.matrix.sizeToCells();\n this.nib.outlets.scrollView.setDocumentView(this.matrix);\n this.alignLabelWithColumn(this.nib.outlets.beforeLabel, 0);\n this.alignLabelWithColumn(this.nib.outlets.afterLabel, 1); // Resize the window to fit the matrix\n\n var matrixHeight = cellSize.height * PREVIEW_VISIBLE_ROWS;\n matrixHeight += PREVIEW_CELL_SPACING.height * (PREVIEW_VISIBLE_ROWS - 1);\n var matrixSize = NSMakeSize(this.matrix.frame().size.width, matrixHeight); // Now adjust the containing view width and column labels to fit the matrix\n\n var frameSize = NSScrollView.frameSizeForContentSize_horizontalScrollerClass_verticalScrollerClass_borderType_controlSize_scrollerStyle(matrixSize, null, NSScroller, BORDER_STYLE, NSRegularControlSize, NSScrollerStyleOverlay); // Take content insets into account\n\n frameSize.width += insets.left + insets.right;\n frameSize.height += insets.top + insets.bottom; // Calculate the difference in the old size vs. new size, apply that to the view frame\n\n var sizeDiff = NSMakeSize(frameSize.width - scrollViewSize.width, frameSize.height - scrollViewSize.height);\n var windowFrame = this.nib.outlets.window.frame();\n windowFrame.size.width += sizeDiff.width;\n windowFrame.size.height += sizeDiff.height;\n var minSize = this.nib.outlets.window.minSize();\n windowFrame.size.width = Math.max(windowFrame.size.width, minSize.width);\n windowFrame.size.height = Math.max(windowFrame.size.height, minSize.height);\n this.nib.outlets.window.setFrame_display(windowFrame, true);\n }\n }, {\n key: \"showAlert\",\n value: function showAlert(message) {\n var alert = this.makeAlert();\n alert.setInformativeText(message);\n alert.runModal();\n }\n }, {\n key: \"showFindDialog\",\n value: function showFindDialog() {\n if (this.styles.numberOfSharedStyles() === 0) {\n var alert = this.makeAlert();\n alert.setInformativeText(\"This document has no shared \".concat(this.layerType, \" styles.\"));\n alert.runModal();\n return 0;\n }\n\n this.loadNib();\n this.initStyleInfo();\n this.initMatrix();\n return NSApp.runModalForWindow(this.nib.outlets.window);\n }\n }, {\n key: \"run\",\n value: function run() {\n var response = this.showFindDialog();\n\n if (response !== 0) {\n this.nib.outlets.window.orderOut(null);\n }\n\n return response;\n }\n }]);\n\n return SharedStyleRenamer;\n}();\nfunction renameTextStyles(context) {\n var styles = context.document.documentData().layerTextStyles();\n var renamer = new SharedStyleRenamer(context, styles, 'text');\n renamer.run();\n}\nfunction renameLayerStyles(context) {\n var styles = context.document.documentData().layerStyles();\n var renamer = new SharedStyleRenamer(context, styles, 'layer');\n renamer.run();\n}\n\n//# sourceURL=webpack:///./src/lib/shared-style-renamer.js?");
128 |
129 | /***/ }),
130 |
131 | /***/ "./src/lib/sketch-nibui.js":
132 | /*!*********************************!*\
133 | !*** ./src/lib/sketch-nibui.js ***!
134 | \*********************************/
135 | /*! exports provided: NibUI */
136 | /***/ (function(module, __webpack_exports__, __webpack_require__) {
137 |
138 | "use strict";
139 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"NibUI\", function() { return NibUI; });\n/* harmony import */ var _MochaJSDelegate__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./MochaJSDelegate */ \"./src/lib/MochaJSDelegate.js\");\n/*\n * Copyright 2015 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\")\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\nfunction _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === \"undefined\" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === \"number\") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError(\"Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\"); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it[\"return\"] != null) it[\"return\"](); } finally { if (didErr) throw err; } } }; }\n\nfunction _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === \"string\") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === \"Object\" && o.constructor) n = o.constructor.name; if (n === \"Map\" || n === \"Set\") return Array.from(o); if (n === \"Arguments\" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }\n\nfunction _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\n\nvar NibUI = /*#__PURE__*/function () {\n function NibUI(context, resourceBundleName, nibName, delegate, ivars) {\n _classCallCheck(this, NibUI);\n\n var bundlePath = context.plugin.urlForResourceNamed(resourceBundleName).path();\n this._bundle = NSBundle.bundleWithPath(bundlePath);\n this._nibName = nibName;\n this._delegate = delegate;\n this.outlets = Object.create(null);\n this.ivars = Object.create(null);\n this._delegateProxy = null; // Construct a class that will be the nib's owner\n\n this._createNibOwner(nibName); // Get the list of outlets and actions as defined in the nib\n\n\n var connections = this._loadConnections(nibName);\n\n this._prepareOutletConnections(connections.outlets);\n\n if (delegate) {\n this._connectActionsToDelegate(connections.actions, delegate);\n }\n\n if (ivars) {\n this._addIvars(ivars);\n } // Now that the nib owner class is completely constructed, register it with the ObjC runtime\n\n\n this._registerNibOwner();\n\n if (ivars) {\n this._initIvars(ivars);\n }\n\n this._load();\n } // Create a class name that doesn't exist yet. Note that we can't reuse the same\n // definition lest Sketch will throw an MOJavaScriptException when binding the UI,\n // probably due to JavaScript context / plugin lifecycle incompatibility.\n\n\n _createClass(NibUI, [{\n key: \"_createNibOwner\",\n value: function _createNibOwner(nibName) {\n var className;\n\n do {\n className = nibName + NSUUID.UUID().UUIDString();\n } while (NSClassFromString(className) != null);\n\n this._cls = MOClassDescription.allocateDescriptionForClassWithName_superclass_(className, NSObject); // We need to add the NSObject protocol so it will be KVC compliant\n\n var protocol = MOProtocolDescription.descriptionForProtocolWithName('NSObject');\n\n this._cls.addProtocol(protocol);\n }\n }, {\n key: \"_registerNibOwner\",\n value: function _registerNibOwner() {\n this._cls.registerClass();\n\n this._nibOwner = NSClassFromString(this._cls.name()).alloc().init();\n } // Create setter methods that will be called when connecting each outlet during nib loading.\n // The setter methods register the connected view.\n\n }, {\n key: \"_prepareOutletConnections\",\n value: function _prepareOutletConnections(outlets) {\n var _this = this;\n\n var _loop = function _loop(i) {\n var outletName = outlets[i];\n var selector = \"set\".concat(outletName.charAt(0).toUpperCase()).concat(outletName.substring(1), \":\");\n\n var setterFunc = function setterFunc(view) {\n _this.outlets[outletName] = view;\n };\n\n _this._cls.addInstanceMethodWithSelector_function(NSSelectorFromString(selector), setterFunc);\n };\n\n for (var i = 0; i < outlets.length; i++) {\n _loop(i);\n }\n }\n }, {\n key: \"_connectDelegateMethods\",\n value: function _connectDelegateMethods() {\n if (!this._delegate) {\n return;\n }\n\n var objectsToConnect = [];\n var view = null;\n\n if ('window' in this.outlets) {\n objectsToConnect.push(this.outlets.window);\n view = this.outlets.window.contentView();\n } else if ('view' in this.outlets) {\n view = this.outlets.view;\n }\n\n if (!view) {\n return;\n }\n\n this._checkForTextViewsToConnect(view, objectsToConnect);\n\n if (objectsToConnect.length) {\n var delegateProxy = this._getDelegateProxy();\n\n if (delegateProxy) {\n var _iterator = _createForOfIteratorHelper(objectsToConnect),\n _step;\n\n try {\n for (_iterator.s(); !(_step = _iterator.n()).done;) {\n var object = _step.value;\n object.setDelegate(delegateProxy);\n }\n } catch (err) {\n _iterator.e(err);\n } finally {\n _iterator.f();\n }\n }\n }\n }\n }, {\n key: \"_checkForTextViewsToConnect\",\n value: function _checkForTextViewsToConnect(view, objectsToConnect) {\n var children = view.subviews();\n\n for (var _i = 0, _Array$from = Array.from(children); _i < _Array$from.length; _i++) {\n var childView = _Array$from[_i];\n\n if (childView.isKindOfClass(NSTextField) && childView.isEditable() && childView.tag() > 0) {\n objectsToConnect.push(childView);\n }\n\n this._checkForTextViewsToConnect(childView, objectsToConnect);\n }\n }\n }, {\n key: \"_getDelegateProxy\",\n value: function _getDelegateProxy() {\n if (!this._delegateProxy) {\n var selectors = ['windowWillClose:', 'control:textShouldBeginEditing:', 'controlTextDidBeginEditing:', 'controlTextDidChange:', 'control:textShouldEndEditing:', 'controlTextDidEndEditing:'];\n var delegateConfig = {};\n\n for (var _i2 = 0, _selectors = selectors; _i2 < _selectors.length; _i2++) {\n var selector = _selectors[_i2];\n var methodName = selector.replace(/(:.)/g, function (match, subpattern) {\n return subpattern.charAt(1).toUpperCase();\n }).replace(/:$/, '');\n var method = this._delegate[methodName];\n\n if (method) {\n delegateConfig[selector] = method.bind(this._delegate);\n }\n }\n\n var delegate = new _MochaJSDelegate__WEBPACK_IMPORTED_MODULE_0__[\"default\"](delegateConfig);\n this._delegateProxy = delegate.getClassInstance();\n }\n\n return this._delegateProxy;\n } // Hook up actions with the delegate\n\n }, {\n key: \"_connectActionsToDelegate\",\n value: function _connectActionsToDelegate(actions, delegate) {\n var _this2 = this;\n\n var _iterator2 = _createForOfIteratorHelper(actions),\n _step2;\n\n try {\n var _loop2 = function _loop2() {\n var action = _step2.value;\n var funcName = action.slice(0, -1); // Trim ':' from end of action\n\n var func = delegate[funcName];\n\n if (typeof func === 'function') {\n var forwardingFunc = function forwardingFunc(sender) {\n // javascriptCore tends to die a horrible death if an uncaught exception occurs in an action method\n try {\n func.call(delegate, sender);\n } catch (ex) {\n log(NSString.stringWithFormat('%@: %@\\nStack:\\n%@', ex.name, ex.message, ex.stack));\n }\n };\n\n _this2._cls.addInstanceMethodWithSelector_function(NSSelectorFromString(action), forwardingFunc);\n }\n };\n\n for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {\n _loop2();\n }\n } catch (err) {\n _iterator2.e(err);\n } finally {\n _iterator2.f();\n }\n }\n }, {\n key: \"_addIvars\",\n value: function _addIvars(ivars) {\n var _this3 = this;\n\n var _loop3 = function _loop3() {\n var name = _Object$keys[_i3];\n // Step 1: add an ivar to the nib owner class\n var value = ivars[name];\n\n var typeEncoding = _this3.constructor._typeEncodingForValue(value);\n\n if (!typeEncoding) {\n log(\"Cannot determine the type encoding for the ivar '\".concat(name, \"', value = \").concat(value));\n return \"continue\";\n }\n\n if (_this3._cls.addInstanceVariableWithName_typeEncoding(name, typeEncoding)) {\n // Step 2: add a getter/setter to the ivar proxy object\n Object.defineProperty(_this3.ivars, name, {\n get: function get() {\n return _this3.getIvar(name);\n },\n set: function set(value) {\n _this3.setIvar(name, value);\n }\n });\n } else {\n log('Unable to add ivar: ' + name);\n }\n };\n\n for (var _i3 = 0, _Object$keys = Object.keys(ivars); _i3 < _Object$keys.length; _i3++) {\n var _ret = _loop3();\n\n if (_ret === \"continue\") continue;\n }\n }\n }, {\n key: \"_initIvars\",\n value: function _initIvars(ivars) {\n for (var _i4 = 0, _Object$keys2 = Object.keys(ivars); _i4 < _Object$keys2.length; _i4++) {\n var name = _Object$keys2[_i4];\n this.setIvar(name, ivars[name]);\n }\n }\n }, {\n key: \"_loadConnections\",\n value: function _loadConnections(nibName) {\n var path = \"\".concat(this._bundle.resourcePath(), \"/\").concat(nibName, \".json\");\n var json = NSString.stringWithContentsOfFile_encoding_error(path, NSUTF8StringEncoding, null);\n\n if (json) {\n return JSON.parse(json);\n }\n\n return {\n outlets: [],\n actions: []\n };\n }\n }, {\n key: \"_load\",\n value: function _load() {\n var tloPointer = MOPointer.alloc().initWithValue(null);\n\n if (!this._bundle.loadNibNamed_owner_topLevelObjects(this._nibName, this._nibOwner, tloPointer)) {\n throw new Error(\"Could not load nib '\".concat(this._nibName, \"'\"));\n }\n\n this._connectDelegateMethods();\n }\n }, {\n key: \"getIvar\",\n value: function getIvar(name) {\n return this._nibOwner.valueForKey(name);\n }\n }, {\n key: \"setIvar\",\n value: function setIvar(name, value) {\n this._nibOwner.setValue_forKey(value, name);\n }\n }], [{\n key: \"_typeEncodingForValue\",\n value: function _typeEncodingForValue(value) {\n var valueType = _typeof(value);\n\n switch (valueType) {\n case 'string':\n case 'object':\n return '@';\n\n case 'number':\n return 'd';\n\n case 'boolean':\n return 'i';\n\n default:\n return null;\n }\n }\n }]);\n\n return NibUI;\n}();\n\n//# sourceURL=webpack:///./src/lib/sketch-nibui.js?");
140 |
141 | /***/ }),
142 |
143 | /***/ "./src/lib/utils.js":
144 | /*!**************************!*\
145 | !*** ./src/lib/utils.js ***!
146 | \**************************/
147 | /*! exports provided: regExpEscape */
148 | /***/ (function(module, __webpack_exports__, __webpack_require__) {
149 |
150 | "use strict";
151 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"regExpEscape\", function() { return regExpEscape; });\n/* eslint no-control-regex: 0 */\n\n/**\n Utility functions\n*/\nfunction regExpEscape(s) {\n return String(s).replace(/([-()[\\]{}+?*.$^|,:#
2 |
3 |
4 | -
5 |
6 |
7 | -
8 |
9 |
10 | -
11 |
12 |
13 | -
14 |
15 |
16 | -
17 |
18 |
19 | -
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/docs/all-styles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aparajita/sketch-style-master/ec849209d6059e9568163c8b397b5538cd49602d/docs/all-styles.png
--------------------------------------------------------------------------------
/docs/dialog-start.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aparajita/sketch-style-master/ec849209d6059e9568163c8b397b5538cd49602d/docs/dialog-start.png
--------------------------------------------------------------------------------
/docs/matching-styles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aparajita/sketch-style-master/ec849209d6059e9568163c8b397b5538cd49602d/docs/matching-styles.png
--------------------------------------------------------------------------------
/docs/show-only-matching-styles.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aparajita/sketch-style-master/ec849209d6059e9568163c8b397b5538cd49602d/docs/show-only-matching-styles.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sketch-style-master",
3 | "version": "1.0.5",
4 | "license": "MIT",
5 | "engines": {
6 | "node": ">=6.5",
7 | "sketch": ">=4.0"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/aparajita/sketch-style-master.git"
12 | },
13 | "scripts": {
14 | "test": "npm run build",
15 | "lint": "eslint .",
16 | "clean": "rm -rf ./*.sketchplugin",
17 | "build": "node scripts/build-plugin-js.js && npm run lint && npm run clean && webpack",
18 | "watch": "npm run clean && webpack --watch",
19 | "link": "node scripts/link.js",
20 | "install": "node scripts/install.js",
21 | "dist": "npm run build -- -p",
22 | "enable-hotloading": "node scripts/enable-sketch-plugin-hotloading.js true",
23 | "disable-hotloading": "node scripts/enable-sketch-plugin-hotloading.js false",
24 | "preversion": "npm test && git add -A .",
25 | "postversion": "node scripts/postversion.js",
26 | "push": "scripts/publish.sh"
27 | },
28 | "devDependencies": {
29 | "@babel/core": "^7.11.6",
30 | "@babel/preset-env": "^7.11.5",
31 | "babel-loader": "^8.1.0",
32 | "chalk": "^4.1.0",
33 | "copy-webpack-plugin": "^6.1.0",
34 | "eslint": "^7.9.0",
35 | "file-loader": "^6.1.0",
36 | "fs-extra": "^9.0.1",
37 | "keychain": "^1.3.0",
38 | "plist": "^3.0.1",
39 | "uuid": "^8.3.0",
40 | "webpack": "^4.44.1",
41 | "webpack-cli": "^3.3.12"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aparajita/sketch-style-master/ec849209d6059e9568163c8b397b5538cd49602d/resources/icon.png
--------------------------------------------------------------------------------
/resources/rename-styles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aparajita/sketch-style-master/ec849209d6059e9568163c8b397b5538cd49602d/resources/rename-styles.png
--------------------------------------------------------------------------------
/resources/rename-styles@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aparajita/sketch-style-master/ec849209d6059e9568163c8b397b5538cd49602d/resources/rename-styles@2x.png
--------------------------------------------------------------------------------
/scripts/build-plugin-js.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const chalk = require('chalk')
4 | const fs = require('fs')
5 | const manifest = require('../src/manifest.json')
6 | const path = require('path')
7 |
8 | const sourceTemplate = `'use strict'
9 |
10 | // webpack build dependency
11 | import './manifest.json'
12 |
13 | // Export * from all of the commands
14 | `
15 |
16 | function buildPluginJS() {
17 | const commandDefs = manifest.commands || []
18 | const commands = commandDefs.map(command => command.handler)
19 | const exportedFunctions = []
20 |
21 | // Gather all of the exported command functions from command sources
22 | const commandsDir = path.resolve(__dirname, '../src/commands')
23 | const commandFiles = fs.readdirSync(commandsDir)
24 |
25 | for (const file of commandFiles) {
26 | const source = fs.readFileSync(path.join(commandsDir, file), { encoding: 'utf8' })
27 |
28 | let match
29 | const re = /^export\s+function\s+(\w+)\s*\(\s*context\s*\)/gm
30 | re.lastIndex = 0
31 |
32 | do {
33 | match = re.exec(source)
34 |
35 | if (match) {
36 | exportedFunctions.push({
37 | filename: path.basename(file),
38 | func: match[1]
39 | })
40 | }
41 | }
42 | while (match)
43 | }
44 |
45 | // Make sure all of the commands in the manifest have been exported
46 | let valid = true
47 |
48 | for (let command of commands) {
49 | const finder = value => {
50 | return value.func === command
51 | }
52 |
53 | if (!exportedFunctions.find(finder)) {
54 | console.log(`👎 The manifest command ${chalk.red(command)} has not been exported from any command source file.`)
55 | valid = false
56 | }
57 | }
58 |
59 | if (valid) {
60 | const exportStatments = commandFiles.map(filename => `export * from './commands/${filename}'`)
61 | const source = sourceTemplate + exportStatments.join('\n') + '\n'
62 |
63 | fs.writeFileSync(path.resolve(__dirname, '../src/plugin.js'), source, { encoding: 'utf8' })
64 | }
65 |
66 | return valid
67 | }
68 |
69 | if (require.main === module) {
70 | process.exit(buildPluginJS() ? 0 : 1)
71 | }
72 | else {
73 | module.exports = { buildPluginJS }
74 | }
75 |
--------------------------------------------------------------------------------
/scripts/enable-sketch-plugin-hotloading.js:
--------------------------------------------------------------------------------
1 | const { execSync } = require('child_process')
2 |
3 | const commandTemplate = '/usr/bin/defaults _action_ ~/Library/Preferences/com.bohemiancoding.sketch3.plist AlwaysReloadScript _value_'
4 |
5 | function enableSketchPluginHotloading(enable, alwaysShowStatus) {
6 | let cmd = commandTemplate.replace('_action_', 'read').replace('_value_', '')
7 | const output = execSync(cmd, { encoding: 'utf8' })
8 | const enabled = output.trim() === '1'
9 | const valueChanged = enabled !== enable
10 |
11 | if (valueChanged) {
12 | cmd = commandTemplate.replace('_action_', 'write').replace('_value_', `-bool ${enable ? 'YES' : 'NO'}`)
13 | execSync(cmd)
14 | }
15 |
16 | if (valueChanged || alwaysShowStatus) {
17 | console.log(`🔥 hot plugin reloading ${enable ? 'enabled' : 'disabled'} in Sketch`)
18 | }
19 | }
20 |
21 | if (require.main === module) {
22 | const arg = process.argv[2]
23 | enableSketchPluginHotloading(/(1|y(es)?|true)/i.test(arg), true)
24 | }
25 | else {
26 | module.exports = { enableSketchPluginHotloading }
27 | }
28 |
--------------------------------------------------------------------------------
/scripts/install.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk')
2 | const fs = require('fs-extra')
3 | const { enableSketchPluginHotloading } = require('./enable-sketch-plugin-hotloading')
4 | const utils = require('./utils')
5 |
6 | function installPlugin() {
7 | const pathInfo = utils.getPathInfo()
8 |
9 | try {
10 | // Delete an existing plugin or symlink if it exists
11 | fs.removeSync(pathInfo.targetPath)
12 | }
13 | catch (e) {
14 | // Nothing to delete
15 | }
16 |
17 | fs.copySync(pathInfo.sourcePath, pathInfo.targetPath)
18 | console.log(`👍 ${chalk.green(pathInfo.pluginDir)} copied into the plugins directory`)
19 |
20 | // Turn hot reloading off in Sketch
21 | enableSketchPluginHotloading(false)
22 | }
23 |
24 | if (require.main === module) {
25 | installPlugin()
26 | }
27 | else {
28 | module.exports = { installPlugin }
29 | }
30 |
--------------------------------------------------------------------------------
/scripts/link.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk')
2 | const fs = require('fs-extra')
3 | const { enableSketchPluginHotloading } = require('./enable-sketch-plugin-hotloading')
4 | const utils = require('./utils')
5 |
6 | function linkPlugin() {
7 | const pathInfo = utils.getPathInfo()
8 |
9 | // Delete an existing plugin or symlink if it exists
10 | try {
11 | fs.removeSync(pathInfo.targetPath)
12 | }
13 | catch (e) {
14 | // There was nothing to delete
15 | }
16 |
17 | // Make a new symlink
18 | fs.ensureSymlinkSync(pathInfo.sourcePath, pathInfo.targetPath)
19 | console.log(`👍 ${chalk.green(pathInfo.pluginDir)} symlinked into the plugins directory`)
20 |
21 | // Turn hot reloading on in Sketch
22 | enableSketchPluginHotloading(true)
23 | }
24 |
25 | if (require.main === module) {
26 | linkPlugin()
27 | }
28 | else {
29 | module.exports = { linkPlugin }
30 | }
31 |
--------------------------------------------------------------------------------
/scripts/postversion.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { execSync } = require('child_process')
4 | const fs = require('fs-extra')
5 | const utils = require('./utils')
6 |
7 | const pathInfo = utils.getPathInfo()
8 |
9 | // Remove existing zip
10 | fs.remove(pathInfo.sourcePath + '.zip')
11 | .then(() => {
12 | // Zip the plugin
13 | execSync(`zip -r '${pathInfo.pluginDir}.zip' '${pathInfo.pluginDir}' -x .DS_Store`, { encoding: 'utf8' })
14 | })
15 | .catch(e => {
16 | console.log(e)
17 | })
18 |
--------------------------------------------------------------------------------
/scripts/publish.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | npm version "$1" && git push --follow-tags origin master && npm publish
4 |
--------------------------------------------------------------------------------
/scripts/utils.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | function getPathInfo() {
4 | const sketchPluginDir = `${process.env.HOME}/Library/Application Support/com.bohemiancoding.sketch3/Plugins`
5 | const { name } = require('../src/manifest.json')
6 | const pluginDir = `${name}.sketchplugin`
7 | const sourcePath = path.resolve(__dirname, `../${pluginDir}`)
8 | const targetPath = path.join(sketchPluginDir, pluginDir)
9 |
10 | return {
11 | sketchPluginDir,
12 | pluginDir,
13 | sourcePath,
14 | targetPath
15 | }
16 | }
17 |
18 | module.exports = {
19 | getPathInfo
20 | }
21 |
--------------------------------------------------------------------------------
/src/commands/rename-styles.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import { SharedStyleRenamer } from '../lib/shared-style-renamer'
4 |
5 | export function renameTextStyles(context) {
6 | const styles = context.document.documentData().layerTextStyles()
7 | const renamer = new SharedStyleRenamer(context, styles, 'text')
8 | renamer.run()
9 | }
10 |
11 | export function renameLayerStyles(context) {
12 | const styles = context.document.documentData().layerStyles()
13 | const renamer = new SharedStyleRenamer(context, styles, 'layer')
14 | renamer.run()
15 | }
16 |
--------------------------------------------------------------------------------
/src/lib/MochaJSDelegate.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | export default class MochaJSDelegate {
4 | constructor(selectorHandlerDict, superclass) {
5 | this.uniqueClassName = 'MochaJSDelegate_DynamicClass_' + NSUUID.UUID().UUIDString()
6 | this.delegateClassDesc = MOClassDescription.allocateDescriptionForClassWithName_superclass_(this.uniqueClassName, superclass || NSObject)
7 | this.delegateClassDesc.registerClass()
8 | this.handlers = {}
9 |
10 | if (typeof selectorHandlerDict === 'object') {
11 | const selectors = Object.keys(selectorHandlerDict)
12 |
13 | for (const selectorString of selectors) {
14 | this.setHandlerForSelector(selectorString, selectorHandlerDict[selectorString])
15 | }
16 | }
17 | }
18 |
19 | setHandlerForSelector(selectorString, func) {
20 | const handlerHasBeenSet = (selectorString in this.handlers)
21 | this.handlers[selectorString] = func
22 |
23 | /*
24 | For some reason, Mocha acts weird about arguments: https://github.com/logancollins/Mocha/issues/28
25 | We have to basically create a dynamic handler with a likewise dynamic number of predefined arguments.
26 | */
27 | if (!handlerHasBeenSet) {
28 | const args = []
29 | const regex = /:/g
30 |
31 | while (regex.exec(selectorString)) {
32 | args.push('arg' + args.length)
33 | }
34 |
35 | // JavascriptCore tends to die a horrible death if an uncaught exception occurs in an action method
36 | const body = `{
37 | try {
38 | return func.apply(this, arguments)
39 | }
40 | catch(ex) {
41 | log(ex)
42 | }
43 | }`
44 | const code = NSString.stringWithFormat('(function (%@) %@)', args.join(', '), body)
45 | const dynamicFunction = eval(String(code))
46 | const selector = NSSelectorFromString(selectorString)
47 | this.delegateClassDesc.addInstanceMethodWithSelector_function_(selector, dynamicFunction)
48 | }
49 | }
50 |
51 | removeHandlerForSelector(selectorString) {
52 | delete this.handlers[selectorString]
53 | }
54 |
55 | getHandlerForSelector(selectorString) {
56 | return this.handlers[selectorString]
57 | }
58 |
59 | getAllHandlers() {
60 | return this.handlers
61 | }
62 |
63 | getClass() {
64 | return NSClassFromString(this.uniqueClassName)
65 | }
66 |
67 | getClassInstance() {
68 | return this.getClass().new()
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/lib/shared-style-renamer.js:
--------------------------------------------------------------------------------
1 | /*
2 | Handler for 'Rename Text Styles' command.
3 | */
4 |
5 | 'use strict'
6 |
7 | // Code being used
8 | import { regExpEscape } from './utils'
9 | import { NibUI } from './sketch-nibui'
10 | import sketch from 'sketch'
11 |
12 | // webpack build dependencies
13 | import '../nib/RenameStyles.xib'
14 | import '../nib/RenameStyles.m'
15 |
16 | import { version } from '../manifest.json'
17 |
18 | const PREVIEW_COLUMN_COUNT = 2
19 | const PREVIEW_CELL_SPACING = NSMakeSize(16, 2)
20 | const PREVIEW_VISIBLE_ROWS = 27
21 |
22 | const FIND_FIELD_TAG = 1
23 | const REPLACE_FIELD_TAG = 2
24 |
25 | function capitalize(text) {
26 | return text.charAt(0).toUpperCase() + text.slice(1)
27 | }
28 |
29 | export class SharedStyleRenamer {
30 | constructor(context, styles, layerType) {
31 | this.context = context
32 | this.sketch = sketch
33 | this.styles = styles
34 | this.styleInfo = []
35 | this.renamedStyles = []
36 | this.find = ''
37 | this.replace = ''
38 | this.cellFontRegular = NSFont.systemFontOfSize(NSFont.systemFontSize())
39 | this.cellFontBold = NSFont.boldSystemFontOfSize(NSFont.systemFontSize())
40 | this.layerType = layerType
41 | this.dialogTitle = `Rename ${capitalize(layerType)} Styles`
42 |
43 | this.ivars = {
44 | styles,
45 | renameCount: 0,
46 | findPattern: '',
47 | ignoreCase: false,
48 | useRegex: false,
49 | replacePattern: '',
50 | showMatchingStyles: false,
51 | autoScroll: true,
52 | findColor: NSColor.textColor()
53 | }
54 | }
55 |
56 | makeAlert() {
57 | const alert = NSAlert.new()
58 | alert.setMessageText(this.dialogTitle)
59 |
60 | const icon = NSImage.alloc().initByReferencingFile(this.context.plugin.urlForResourceNamed('rename-styles@2x.png').path())
61 | alert.setIcon(icon)
62 |
63 | return alert
64 | }
65 |
66 | loadNib() {
67 | this.nib = new NibUI(this.context, 'UIBundle', 'RenameStyles', this, this.ivars)
68 | this.nib.outlets.window.setTitle(this.dialogTitle)
69 | this.nib.outlets.versionLabel.setStringValue(`v${version}`)
70 | }
71 |
72 | windowWillClose() {
73 | NSApp.stopModal()
74 | }
75 |
76 | controlTextDidChange(notification) {
77 | const tag = notification.object().tag()
78 |
79 | if (tag == FIND_FIELD_TAG) {
80 | this.searchForMatchingStyles()
81 | }
82 | else if (tag == REPLACE_FIELD_TAG) {
83 | this.updateReplacedNames()
84 | }
85 | }
86 |
87 | toggleShowOnlyMatchingStyles() {
88 | if (!this.nib.ivars.showMatchingStyles.boolValue()) {
89 | this.resetRenamedStyles()
90 | }
91 |
92 | this.searchForMatchingStyles()
93 | }
94 |
95 | toggleFindOption() {
96 | this.searchForMatchingStyles()
97 | }
98 |
99 | toggleAutoscroll() {
100 | this.scrollToFirstRenamedStyle()
101 | }
102 |
103 | handleRename() {
104 | this.renameStyles()
105 | NSApp.stopModal()
106 | }
107 |
108 | handleApply() {
109 | this.applyRename()
110 | }
111 |
112 | handleCancel() {
113 | NSApp.stopModal()
114 | }
115 |
116 | applyRename() {
117 | this.renameStyles()
118 | this.initStyleInfo()
119 | this.nib.ivars.findPattern = ''
120 | this.nib.ivars.replacePattern = ''
121 | this.nib.ivars.showMatchingStyles = false
122 | this.nib.outlets.window.makeFirstResponder(this.nib.outlets.window.initialFirstResponder())
123 | this.searchForMatchingStyles()
124 | }
125 |
126 | scrollToFirstRenamedStyle() {
127 | if (!this.nib.ivars.autoScroll.boolValue()) {
128 | return
129 | }
130 |
131 | const insets = this.nib.outlets.scrollView.contentInsets()
132 | let point = NSMakePoint(0, 0)
133 |
134 | if (this.renamedStyles.length > 0) {
135 | for (let i = 0; i < this.renamedStyles.length; i++) {
136 | const info = this.renamedStyles[i]
137 |
138 | if (info.newName.length > 0) {
139 | point = this.matrix.cellFrameAtRow_column(i, 0).origin
140 | break
141 | }
142 | }
143 | }
144 | else {
145 | point = this.matrix.cellFrameAtRow_column(0, 0).origin
146 | }
147 |
148 | point.y -= insets.top - 1 // Not sure why - 1 is necessary, but it is
149 | this.matrix.scrollPoint(point)
150 | this.nib.outlets.scrollView.reflectScrolledClipView(this.nib.outlets.scrollView.contentView())
151 | }
152 |
153 | searchForMatchingStyles() {
154 | // We always want to replace all occurrences of the find string within
155 | // a style name, so we have to transform a plain search into a RegExp with
156 | // the 'g' flag, because a plain text replace only replaces the first occurrence.
157 | const flags = this.nib.ivars.ignoreCase.boolValue() ? 'gi' : 'g'
158 | const regex = !!this.nib.ivars.useRegex.boolValue()
159 |
160 | // When the text field's value is empty, the bound value is returning null,
161 | // so make sure we have at least an empty string.
162 | let find = String(this.nib.ivars.findPattern || '')
163 |
164 | // RegExp constructor can fail, be sure to catch exceptions!
165 | try {
166 | if (regex) {
167 | this.find = new RegExp(find, flags)
168 | }
169 | else {
170 | this.find = new RegExp(regExpEscape(find), flags)
171 | }
172 |
173 | this.nib.ivars.findColor = NSColor.textColor()
174 | }
175 | catch (ex) {
176 | this.nib.ivars.findColor = NSColor.redColor()
177 | find = ''
178 | this.find = new RegExp('', flags)
179 | }
180 |
181 | this.updateStylesToRename(find.length === 0)
182 | this.setMatrixData()
183 | this.scrollToFirstRenamedStyle()
184 | }
185 |
186 | updateReplacedNames() {
187 | this.replace = String(this.nib.ivars.replacePattern || '')
188 | this.updateRenamedStyles()
189 | this.setMatrixData()
190 | }
191 |
192 | initStyleInfo() {
193 | const styles = this.styles.objects()
194 | this.styleInfo = new Array(styles.length)
195 |
196 | for (let i = 0; i < styles.length; i++) {
197 | const style = styles[i]
198 |
199 | this.styleInfo[i] = {
200 | style,
201 | name: style.name()
202 | }
203 | }
204 |
205 | this.styleInfo.sort((a, b) => {
206 | if (a.name < b.name) {
207 | return -1
208 | }
209 |
210 | if (a.name > b.name) {
211 | return 1
212 | }
213 |
214 | return 0
215 | })
216 |
217 | this.nib.ivars.renameCount = 0
218 | this.resetRenamedStyles()
219 | }
220 |
221 | resetRenamedStyles() {
222 | this.renamedStyles = new Array(this.styleInfo.length)
223 |
224 | for (let i = 0; i < this.styleInfo.length; i++) {
225 | const info = this.styleInfo[i]
226 | this.renamedStyles[i] = {
227 | style: info.style,
228 | oldName: info.name,
229 | newName: ''
230 | }
231 | }
232 | }
233 |
234 | updateStylesToRename(empty) {
235 | const renamedStyles = []
236 | let renameCount = 0
237 |
238 | for (let i = 0; i < this.styleInfo.length; i++) {
239 | const info = this.styleInfo[i]
240 | const found = !empty && this.find.test(info.name)
241 | let newName
242 |
243 | if (found) {
244 | newName = info.name.replace(this.find, this.replace)
245 |
246 | if (newName.length === 0) {
247 | newName = ''
248 | }
249 | else {
250 | renameCount++
251 | }
252 |
253 | if (this.nib.ivars.showMatchingStyles.boolValue()) {
254 | renamedStyles.push({
255 | style: info.style,
256 | oldName: info.name,
257 | newName
258 | })
259 | }
260 | else {
261 | this.renamedStyles[i].newName = newName
262 | }
263 | }
264 | else if (!this.nib.ivars.showMatchingStyles.boolValue()) {
265 | this.renamedStyles[i].newName = ''
266 | }
267 | }
268 |
269 | if (this.nib.ivars.showMatchingStyles.boolValue()) {
270 | this.renamedStyles = renamedStyles
271 | }
272 |
273 | this.nib.ivars.renameCount = renameCount
274 | }
275 |
276 | updateRenamedStyles() {
277 | let renameCount = 0
278 |
279 | for (const info of this.renamedStyles) {
280 | if (info.newName) {
281 | info.newName = info.oldName.replace(this.find, this.replace)
282 |
283 | if (info.newName.length === 0) {
284 | info.newName = ''
285 | }
286 | else {
287 | renameCount++
288 | }
289 | }
290 | }
291 |
292 | this.nib.ivars.renameCount = renameCount
293 | }
294 |
295 | renameStyles() {
296 | for (const info of this.renamedStyles) {
297 | if (info.newName.length > 0) {
298 | const copy = info.style.copy()
299 | copy.setName(info.newName)
300 | info.style.syncPropertiesFromObject(copy)
301 | }
302 | }
303 |
304 | this.context.document.reloadInspector()
305 | }
306 |
307 | alignLabelWithColumn(label, column) {
308 | const insets = this.nib.outlets.scrollView.contentInsets()
309 | const scrollViewOrigin = this.nib.outlets.scrollView.frame().origin
310 | let cellOrigin = this.matrix.cellFrameAtRow_column(0, column).origin
311 | const labelOrigin = label.frame().origin
312 | labelOrigin.x = scrollViewOrigin.x + insets.left + cellOrigin.x
313 | label.setFrameOrigin(labelOrigin)
314 | }
315 |
316 | setMatrixData() {
317 | let maxWidth = 0
318 | this.matrix.renewRows_columns(this.renamedStyles.length, PREVIEW_COLUMN_COUNT)
319 | this.matrix.sizeToCells()
320 | const cells = this.matrix.cells()
321 |
322 | for (let row = 0; row < this.renamedStyles.length; row++) {
323 | const info = this.renamedStyles[row]
324 |
325 | // After setting the cell's value, get its width so we can calculate
326 | // the maximum width we'll need for cells.
327 | const index = row * PREVIEW_COLUMN_COUNT
328 | let cell = cells[index]
329 | cell.setFont(info.newName.length === 0 ? this.cellFontRegular : this.cellFontBold)
330 | cell.setStringValue(info.oldName)
331 | this.matrix.drawCellAtRow_column(row, 0)
332 |
333 | let size = cell.cellSize()
334 | maxWidth = Math.max(maxWidth, size.width)
335 |
336 | cell = cells[index + 1]
337 | cell.setFont(this.cellFontRegular)
338 | cell.setStringValue(info.newName)
339 | this.matrix.drawCellAtRow_column(row, 1)
340 |
341 | size = cell.cellSize()
342 | maxWidth = Math.max(maxWidth, size.width)
343 | }
344 |
345 | return NSMakeSize(maxWidth, cells[0].cellSize().height)
346 | }
347 |
348 | initMatrix() {
349 | const BORDER_STYLE = NSBezelBorder
350 |
351 | const scrollViewSize = this.nib.outlets.scrollView.frame().size
352 | const contentSize = NSScrollView.contentSizeForFrameSize_horizontalScrollerClass_verticalScrollerClass_borderType_controlSize_scrollerStyle(
353 | scrollViewSize,
354 | null,
355 | NSScroller,
356 | BORDER_STYLE,
357 | NSRegularControlSize,
358 | NSScrollerStyleOverlay
359 | )
360 |
361 | const insets = this.nib.outlets.scrollView.contentInsets()
362 | contentSize.width -= insets.left + insets.right
363 | contentSize.height -= insets.top + insets.bottom
364 |
365 | // Start with a default size, we'll fix that later
366 | let cellSize = NSMakeSize(100, 16)
367 | const cellPrototype = NSCell.alloc().initTextCell('')
368 | this.matrix = NSMatrix.alloc().initWithFrame_mode_prototype_numberOfRows_numberOfColumns(
369 | NSMakeRect(0, 0, cellSize.width * PREVIEW_COLUMN_COUNT, cellSize.height * this.renamedStyles.length),
370 | NSListModeMatrix,
371 | cellPrototype,
372 | this.renamedStyles.length,
373 | PREVIEW_COLUMN_COUNT
374 | )
375 |
376 | cellSize = this.setMatrixData()
377 |
378 | // Add 25% to the cell width to allow for longer names when renaming
379 | cellSize.width *= 1.25
380 |
381 | // Make sure the cell width is no less than half of the initial scrollview width
382 | const minWidth = Math.floor(scrollViewSize.width / 2)
383 | cellSize.width = Math.max(cellSize.width, minWidth)
384 |
385 | this.matrix.setCellSize(CGSizeMake(cellSize.width, cellSize.height));
386 | this.matrix.setIntercellSpacing(PREVIEW_CELL_SPACING)
387 | this.matrix.sizeToCells()
388 |
389 | this.nib.outlets.scrollView.setDocumentView(this.matrix)
390 |
391 | this.alignLabelWithColumn(this.nib.outlets.beforeLabel, 0)
392 | this.alignLabelWithColumn(this.nib.outlets.afterLabel, 1)
393 |
394 | // Resize the window to fit the matrix
395 | let matrixHeight = cellSize.height * PREVIEW_VISIBLE_ROWS
396 | matrixHeight += PREVIEW_CELL_SPACING.height * (PREVIEW_VISIBLE_ROWS - 1)
397 | const matrixSize = NSMakeSize(this.matrix.frame().size.width, matrixHeight)
398 |
399 | // Now adjust the containing view width and column labels to fit the matrix
400 | const frameSize = NSScrollView.frameSizeForContentSize_horizontalScrollerClass_verticalScrollerClass_borderType_controlSize_scrollerStyle(
401 | matrixSize,
402 | null,
403 | NSScroller,
404 | BORDER_STYLE,
405 | NSRegularControlSize,
406 | NSScrollerStyleOverlay
407 | )
408 |
409 | // Take content insets into account
410 | frameSize.width += insets.left + insets.right
411 | frameSize.height += insets.top + insets.bottom
412 |
413 | // Calculate the difference in the old size vs. new size, apply that to the view frame
414 | const sizeDiff = NSMakeSize(frameSize.width - scrollViewSize.width, frameSize.height - scrollViewSize.height)
415 | const windowFrame = this.nib.outlets.window.frame()
416 | windowFrame.size.width += sizeDiff.width
417 | windowFrame.size.height += sizeDiff.height
418 |
419 | const minSize = this.nib.outlets.window.minSize()
420 | windowFrame.size.width = Math.max(windowFrame.size.width, minSize.width)
421 | windowFrame.size.height = Math.max(windowFrame.size.height, minSize.height)
422 |
423 | this.nib.outlets.window.setFrame_display(windowFrame, true)
424 | }
425 |
426 | showAlert(message) {
427 | const alert = this.makeAlert()
428 | alert.setInformativeText(message)
429 | alert.runModal()
430 | }
431 |
432 | showFindDialog() {
433 | if (this.styles.numberOfSharedStyles() === 0) {
434 | const alert = this.makeAlert()
435 | alert.setInformativeText(`This document has no shared ${this.layerType} styles.`)
436 | alert.runModal()
437 | return 0
438 | }
439 |
440 | this.loadNib()
441 | this.initStyleInfo()
442 | this.initMatrix()
443 |
444 | return NSApp.runModalForWindow(this.nib.outlets.window)
445 | }
446 |
447 | run() {
448 | const response = this.showFindDialog()
449 |
450 | if (response !== 0) {
451 | this.nib.outlets.window.orderOut(null)
452 | }
453 |
454 | return response
455 | }
456 | }
457 |
458 | export function renameTextStyles(context) {
459 | const styles = context.document.documentData().layerTextStyles()
460 | const renamer = new SharedStyleRenamer(context, styles, 'text')
461 | renamer.run()
462 | }
463 |
464 | export function renameLayerStyles(context) {
465 | const styles = context.document.documentData().layerStyles()
466 | const renamer = new SharedStyleRenamer(context, styles, 'layer')
467 | renamer.run()
468 | }
469 |
--------------------------------------------------------------------------------
/src/lib/sketch-nibui.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License")
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | 'use strict'
18 |
19 | import MochaJSDelegate from './MochaJSDelegate'
20 |
21 |
22 | export class NibUI {
23 | constructor(context, resourceBundleName, nibName, delegate, ivars) {
24 | const bundlePath = context.plugin.urlForResourceNamed(resourceBundleName).path()
25 | this._bundle = NSBundle.bundleWithPath(bundlePath)
26 | this._nibName = nibName
27 | this._delegate = delegate
28 | this.outlets = Object.create(null)
29 | this.ivars = Object.create(null)
30 | this._delegateProxy = null
31 |
32 | // Construct a class that will be the nib's owner
33 | this._createNibOwner(nibName)
34 |
35 | // Get the list of outlets and actions as defined in the nib
36 | const connections = this._loadConnections(nibName)
37 | this._prepareOutletConnections(connections.outlets)
38 |
39 | if (delegate) {
40 | this._connectActionsToDelegate(connections.actions, delegate)
41 | }
42 |
43 | if (ivars) {
44 | this._addIvars(ivars)
45 | }
46 |
47 | // Now that the nib owner class is completely constructed, register it with the ObjC runtime
48 | this._registerNibOwner()
49 |
50 | if (ivars) {
51 | this._initIvars(ivars)
52 | }
53 |
54 | this._load()
55 | }
56 |
57 | // Create a class name that doesn't exist yet. Note that we can't reuse the same
58 | // definition lest Sketch will throw an MOJavaScriptException when binding the UI,
59 | // probably due to JavaScript context / plugin lifecycle incompatibility.
60 | _createNibOwner(nibName) {
61 | let className
62 |
63 | do {
64 | className = nibName + NSUUID.UUID().UUIDString()
65 | }
66 | while (NSClassFromString(className) != null)
67 |
68 | this._cls = MOClassDescription.allocateDescriptionForClassWithName_superclass_(className, NSObject)
69 |
70 | // We need to add the NSObject protocol so it will be KVC compliant
71 | const protocol = MOProtocolDescription.descriptionForProtocolWithName('NSObject')
72 | this._cls.addProtocol(protocol)
73 | }
74 |
75 | _registerNibOwner() {
76 | this._cls.registerClass()
77 | this._nibOwner = NSClassFromString(this._cls.name()).alloc().init()
78 | }
79 |
80 | // Create setter methods that will be called when connecting each outlet during nib loading.
81 | // The setter methods register the connected view.
82 | _prepareOutletConnections(outlets) {
83 | for (let i = 0; i < outlets.length; i++) {
84 | const outletName = outlets[i]
85 | const selector = `set${outletName.charAt(0).toUpperCase()}${outletName.substring(1)}:`
86 | const setterFunc = view => {
87 | this.outlets[outletName] = view
88 | }
89 |
90 | this._cls.addInstanceMethodWithSelector_function(NSSelectorFromString(selector), setterFunc)
91 | }
92 | }
93 |
94 | _connectDelegateMethods() {
95 | if (!this._delegate) {
96 | return
97 | }
98 |
99 | let objectsToConnect = []
100 | let view = null
101 |
102 | if ('window' in this.outlets) {
103 | objectsToConnect.push(this.outlets.window)
104 | view = this.outlets.window.contentView()
105 | }
106 | else if ('view' in this.outlets) {
107 | view = this.outlets.view
108 | }
109 |
110 | if (!view) {
111 | return
112 | }
113 |
114 | this._checkForTextViewsToConnect(view, objectsToConnect)
115 |
116 | if (objectsToConnect.length) {
117 | const delegateProxy = this._getDelegateProxy()
118 |
119 | if (delegateProxy) {
120 | for (const object of objectsToConnect) {
121 | object.setDelegate(delegateProxy)
122 | }
123 | }
124 | }
125 | }
126 |
127 | _checkForTextViewsToConnect(view, objectsToConnect) {
128 | const children = view.subviews()
129 |
130 | for (const childView of Array.from(children)) {
131 | if (childView.isKindOfClass(NSTextField) && childView.isEditable() && childView.tag() > 0) {
132 | objectsToConnect.push(childView)
133 | }
134 |
135 | this._checkForTextViewsToConnect(childView, objectsToConnect)
136 | }
137 | }
138 |
139 | _getDelegateProxy() {
140 | if (!this._delegateProxy) {
141 | const selectors = [
142 | 'windowWillClose:',
143 | 'control:textShouldBeginEditing:',
144 | 'controlTextDidBeginEditing:',
145 | 'controlTextDidChange:',
146 | 'control:textShouldEndEditing:',
147 | 'controlTextDidEndEditing:'
148 | ]
149 | const delegateConfig = {}
150 |
151 | for (const selector of selectors) {
152 | let methodName = selector.replace(/(:.)/g, (match, subpattern) => subpattern.charAt(1).toUpperCase())
153 | .replace(/:$/, '')
154 |
155 | const method = this._delegate[methodName]
156 |
157 | if (method) {
158 | delegateConfig[selector] = method.bind(this._delegate)
159 | }
160 | }
161 |
162 | const delegate = new MochaJSDelegate(delegateConfig)
163 | this._delegateProxy = delegate.getClassInstance()
164 | }
165 |
166 | return this._delegateProxy
167 | }
168 |
169 | // Hook up actions with the delegate
170 | _connectActionsToDelegate(actions, delegate) {
171 | for (const action of actions) {
172 | const funcName = action.slice(0, -1) // Trim ':' from end of action
173 | const func = delegate[funcName]
174 |
175 | if (typeof func === 'function') {
176 | const forwardingFunc = sender => {
177 | // javascriptCore tends to die a horrible death if an uncaught exception occurs in an action method
178 | try {
179 | func.call(delegate, sender)
180 | }
181 | catch (ex) {
182 | log(NSString.stringWithFormat('%@: %@\nStack:\n%@', ex.name, ex.message, ex.stack))
183 | }
184 | }
185 |
186 | this._cls.addInstanceMethodWithSelector_function(NSSelectorFromString(action), forwardingFunc)
187 | }
188 | }
189 | }
190 |
191 | _addIvars(ivars) {
192 | for (const name of Object.keys(ivars)) {
193 | // Step 1: add an ivar to the nib owner class
194 | const value = ivars[name]
195 | const typeEncoding = this.constructor._typeEncodingForValue(value)
196 |
197 | if (!typeEncoding) {
198 | log(`Cannot determine the type encoding for the ivar '${name}', value = ${value}`)
199 | continue
200 | }
201 |
202 | if (this._cls.addInstanceVariableWithName_typeEncoding(name, typeEncoding)) {
203 | // Step 2: add a getter/setter to the ivar proxy object
204 | Object.defineProperty(this.ivars, name, {
205 | get: () => this.getIvar(name),
206 | set: value => { this.setIvar(name, value) }
207 | })
208 | }
209 | else {
210 | log('Unable to add ivar: ' + name)
211 | }
212 | }
213 | }
214 |
215 | _initIvars(ivars) {
216 | for (const name of Object.keys(ivars)) {
217 | this.setIvar(name, ivars[name])
218 | }
219 | }
220 |
221 | static _typeEncodingForValue(value) {
222 | const valueType = typeof value
223 |
224 | switch (valueType) {
225 | case 'string':
226 | case 'object':
227 | return '@'
228 |
229 | case 'number':
230 | return 'd'
231 |
232 | case 'boolean':
233 | return 'i'
234 |
235 | default:
236 | return null
237 | }
238 | }
239 |
240 | _loadConnections(nibName) {
241 | const path = `${this._bundle.resourcePath()}/${nibName}.json`
242 | const json = NSString.stringWithContentsOfFile_encoding_error(path, NSUTF8StringEncoding, null)
243 |
244 | if (json) {
245 | return JSON.parse(json)
246 | }
247 |
248 | return {
249 | outlets: [],
250 | actions: []
251 | }
252 | }
253 |
254 | _load() {
255 | const tloPointer = MOPointer.alloc().initWithValue(null)
256 |
257 | if (!this._bundle.loadNibNamed_owner_topLevelObjects(this._nibName, this._nibOwner, tloPointer)) {
258 | throw new Error(`Could not load nib '${this._nibName}'`)
259 | }
260 |
261 | this._connectDelegateMethods()
262 | }
263 |
264 | getIvar(name) {
265 | return this._nibOwner.valueForKey(name)
266 | }
267 |
268 | setIvar(name, value) {
269 | this._nibOwner.setValue_forKey(value, name)
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | /* eslint no-control-regex: 0 */
2 |
3 | /**
4 | Utility functions
5 | */
6 |
7 | export function regExpEscape(s) {
8 | return String(s).replace(/([-()[\]{}+?*.$^|,:#
2 |
3 | @interface RenameStyles : NSObject
4 |
5 | // View bindings go here
6 | @property IBOutlet NSWindow *window;
7 | @property IBOutlet NSScrollView *scrollView;
8 | @property IBOutlet NSTextField *beforeLabel;
9 | @property IBOutlet NSTextField *afterLabel;
10 | @property IBOutlet NSTextField *versionLabel;
11 | // End of view bindings
12 |
13 | // Control action handlers go here
14 | - (IBAction)handleRename:(id)sender;
15 | - (IBAction)handleApply:(id)sender;
16 | - (IBAction)handleCancel:(id)sender;
17 | - (IBAction)toggleShowOnlyMatchingStyles:(id)sender;
18 | - (IBAction)toggleAutoscroll:(id)sender;
19 | - (IBAction)toggleFindOption:(id)sender;
20 | // End of actions
21 |
22 | @end
23 |
24 | @implementation RenameStylesOwner
25 | @end
26 |
--------------------------------------------------------------------------------
/src/nib/RenameStyles.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
107 |
121 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 | %{value1}@ of %{value2}@ will be renamed
179 | No styles will be renamed
180 |
181 |
182 |
183 |
184 | %{value1}@ of %{value2}@ will be renamed
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
237 |
251 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
--------------------------------------------------------------------------------
/src/nib/SketchNibUI.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXFileReference section */
10 | EEF1280F1B7C591800706072 /* RenameStyles.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RenameStyles.xib; sourceTree = ""; };
11 | EEFEA9D11B7C457100C8FE5A /* RenameStyles.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RenameStyles.m; sourceTree = ""; };
12 | /* End PBXFileReference section */
13 |
14 | /* Begin PBXGroup section */
15 | EE0062781B61FDDA00FE56F8 = {
16 | isa = PBXGroup;
17 | children = (
18 | EEF1280F1B7C591800706072 /* RenameStyles.xib */,
19 | EEFEA9D11B7C457100C8FE5A /* RenameStyles.m */,
20 | );
21 | sourceTree = "";
22 | };
23 | /* End PBXGroup section */
24 |
25 | /* Begin PBXProject section */
26 | EE0062791B61FDDA00FE56F8 /* Project object */ = {
27 | isa = PBXProject;
28 | attributes = {
29 | LastUpgradeCheck = 0630;
30 | };
31 | buildConfigurationList = EE00627C1B61FDDA00FE56F8 /* Build configuration list for PBXProject "SketchNibUI" */;
32 | compatibilityVersion = "Xcode 3.2";
33 | developmentRegion = English;
34 | hasScannedForEncodings = 0;
35 | knownRegions = (
36 | en,
37 | Base,
38 | );
39 | mainGroup = EE0062781B61FDDA00FE56F8;
40 | productRefGroup = EE0062781B61FDDA00FE56F8;
41 | projectDirPath = "";
42 | projectRoot = "";
43 | targets = (
44 | );
45 | };
46 | /* End PBXProject section */
47 |
48 | /* Begin XCBuildConfiguration section */
49 | EE00627D1B61FDDA00FE56F8 /* Debug */ = {
50 | isa = XCBuildConfiguration;
51 | buildSettings = {
52 | };
53 | name = Debug;
54 | };
55 | EE00627E1B61FDDA00FE56F8 /* Release */ = {
56 | isa = XCBuildConfiguration;
57 | buildSettings = {
58 | };
59 | name = Release;
60 | };
61 | /* End XCBuildConfiguration section */
62 |
63 | /* Begin XCConfigurationList section */
64 | EE00627C1B61FDDA00FE56F8 /* Build configuration list for PBXProject "SketchNibUI" */ = {
65 | isa = XCConfigurationList;
66 | buildConfigurations = (
67 | EE00627D1B61FDDA00FE56F8 /* Debug */,
68 | EE00627E1B61FDDA00FE56F8 /* Release */,
69 | );
70 | defaultConfigurationIsVisible = 0;
71 | defaultConfigurationName = Release;
72 | };
73 | /* End XCConfigurationList section */
74 | };
75 | rootObject = EE0062791B61FDDA00FE56F8 /* Project object */;
76 | }
77 |
--------------------------------------------------------------------------------
/src/plugin.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | // webpack build dependency
4 | import './manifest.json'
5 |
6 | // Export * from all of the commands
7 | export * from './commands/rename-styles.js'
8 |
--------------------------------------------------------------------------------
/webpack-lib/config-helpers.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const CopyWebpackPlugin = require('copy-webpack-plugin')
4 | const path = require('path')
5 | const PrependAppendPlugin = require('./plugins/webpack-export-sketch-commands-plugin')
6 | const { v4: uuid } = require('uuid')
7 |
8 | const manifest = require('../src/manifest.json')
9 |
10 | /*
11 | Because of the way webpack works, we have to import dependent files like xibs and their related .m files
12 | in the entry .js file. We want the .js file to be recompiled if the xib or .m file changes. But we don't
13 | want to save the generated nib and connection plist file with a hashed filename. Webpack won't recompile
14 | the .js file if an import doesn't export a changed value, and we don't need the export from xibs or .m files,
15 | so we just return a uuid, which ensures that when a xib or .m file changes, the .js file will be recompiled
16 | as well. I couldn't figure out any other way to force webpack to do this...
17 | */
18 | function forceRecompile() {
19 | return uuid()
20 | }
21 |
22 | function getPluginConfig() {
23 | const plugin = `${manifest.name}.sketchplugin`
24 |
25 | return {
26 | entry: './src/plugin.js',
27 | manifest: './src/manifest.json',
28 | target: `./${plugin}`,
29 | resources: `./${plugin}/Contents/Resources`
30 | }
31 | }
32 |
33 | function makeConfig(pluginConfig, options) {
34 | const { nib } = options
35 | const projectDir = path.dirname(__dirname)
36 |
37 | const config = {
38 | context: projectDir,
39 |
40 | entry: pluginConfig.entry,
41 |
42 | mode: 'development',
43 |
44 | output: {
45 | filename: 'plugin.js',
46 | path: path.resolve(projectDir, pluginConfig.target, 'Contents/Sketch')
47 | },
48 |
49 | target: 'node',
50 |
51 | externals: {
52 | sketch: 'commonjs sketch'
53 | },
54 |
55 | module: {
56 | rules: [
57 | {
58 | test: /\.js$/,
59 | include: /\bsrc\b/,
60 | exclude: /\bnib\b/,
61 | use: [
62 | {
63 | loader: 'babel-loader',
64 | query: {
65 | presets: ['@babel/preset-env']
66 | }
67 | }
68 | ]
69 | }
70 | ]
71 | },
72 |
73 | resolveLoader: {
74 | modules: [
75 | 'node_modules',
76 | path.resolve(__dirname, 'loaders')
77 | ]
78 | },
79 |
80 | plugins: [
81 | new CopyWebpackPlugin({
82 | patterns: [
83 | {
84 | from: path.resolve(projectDir, 'resources'),
85 | to: path.resolve(projectDir, pluginConfig.resources)
86 | },
87 | {
88 | from: path.resolve(projectDir, 'src/manifest.json')
89 | }
90 | ]
91 | }),
92 | new PrependAppendPlugin(manifest)
93 | ]
94 | }
95 |
96 | if (nib) {
97 | configNib(config)
98 | }
99 |
100 | return config
101 | }
102 |
103 | function configNib(config) {
104 |
105 | // The bundle resources path is relative to the skpm webpack config output.path, which is /Sketch
106 | const nibOutputPath = `../Resources/${manifest.nibBundle}/Contents/Resources/`
107 |
108 | const nibConfig = {
109 | module: {
110 | rules: [
111 | {
112 | test: /\.xib$/,
113 | include: /\bnib\b/,
114 | use: [
115 | {
116 | loader: 'file-loader',
117 | options: {
118 | name: '[name].nib',
119 | outputPath: nibOutputPath,
120 | publicPath: forceRecompile
121 | }
122 | },
123 | 'sketch-xib-loader'
124 | ]
125 | },
126 | {
127 | test: /\.m$/,
128 | include: /\bnib\b/,
129 | use: [
130 | {
131 | loader: 'file-loader',
132 | options: {
133 | name: `[name].json`,
134 | outputPath: nibOutputPath,
135 | publicPath: forceRecompile
136 | }
137 | },
138 | 'sketch-xib-connection-loader'
139 | ]
140 | }
141 | ]
142 | }
143 | }
144 |
145 | config.module.rules = config.module.rules.concat(nibConfig.module.rules)
146 | }
147 |
148 | module.exports = {
149 | getPluginConfig,
150 | makeConfig
151 | }
152 |
--------------------------------------------------------------------------------
/webpack-lib/loaders/sketch-xib-connection-loader.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('path')
4 | const plist = require('plist')
5 | const { runIbTool } = require('./sketch-xib-loader-utils')
6 |
7 | function writeConnectionsPlist(loader) {
8 | const parsedPath = path.parse(loader.resourcePath)
9 | const xibPath = path.join(parsedPath.dir, `${parsedPath.name}.xib`)
10 | const args = ['--connections', xibPath]
11 | const result = runIbTool(loader, args)
12 |
13 | if (result.status !== 0) {
14 | return ''
15 | }
16 |
17 | // Extract only the info we need and return JSON
18 | const parsedPlist = plist.parse(result.stdout)
19 |
20 | if (parsedPlist) {
21 | const connections = {
22 | outlets: [],
23 | actions: []
24 | }
25 |
26 | const connectionDict = parsedPlist['com.apple.ibtool.document.connections'] || {}
27 |
28 | for (const key of Object.keys(connectionDict)) {
29 | const connection = connectionDict[key]
30 |
31 | if (connection.type == 'IBCocoaOutletConnection') {
32 | if (!/initialFirstResponder|nextKeyView/.test(connection.label)) {
33 | connections.outlets.push(connection.label)
34 | }
35 | }
36 | else if (connection.type == 'IBCocoaActionConnection') {
37 | connections.actions.push(connection.label)
38 | }
39 | }
40 |
41 | return JSON.stringify(connections, null, 2)
42 | }
43 | else {
44 | loader.emit(new Error('Error parsing connections plist'))
45 | return ''
46 | }
47 | }
48 |
49 | module.exports = function () {
50 | return writeConnectionsPlist(this)
51 | }
52 |
53 | module.exports.raw = true
54 |
--------------------------------------------------------------------------------
/webpack-lib/loaders/sketch-xib-loader-utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('path')
4 | const plist = require('plist')
5 | const { spawnSync } = require('child_process')
6 |
7 | function runIbTool(loader, args) {
8 | const parsedPath = path.parse(loader.resourcePath)
9 | const xibPath = path.join(parsedPath.dir, `${parsedPath.name}.xib`)
10 | const mPath = path.join(parsedPath.dir, `${parsedPath.name}.m`)
11 |
12 | // Tell webpack to cache the compiled file and to track changes to the source files
13 | loader.cacheable && loader.cacheable()
14 | loader.addDependency(xibPath);
15 | loader.addDependency(mPath)
16 |
17 | const result = spawnSync('/usr/bin/ibtool', args, { encoding: 'utf8' })
18 |
19 | if (result.status === 0) {
20 | return result
21 | }
22 | else {
23 | let msg
24 |
25 | if (result.error) {
26 | msg = result.error.message
27 | }
28 | else {
29 | msg = getIbtoolError(result.stdout)
30 | }
31 |
32 | loader.emitError(new Error(`Error compiling ${parsedPath.base}: ${msg}`))
33 | }
34 |
35 | return null
36 | }
37 |
38 | function getIbtoolError(stdout) {
39 | const pl = plist.parse(stdout)
40 | const errors = pl['com.apple.ibtool.errors'] || []
41 | return errors[0].description
42 | }
43 |
44 | module.exports = {
45 | runIbTool
46 | }
47 |
--------------------------------------------------------------------------------
/webpack-lib/loaders/sketch-xib-loader.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('fs')
4 | const os = require('os')
5 | const path = require('path')
6 | const { runIbTool } = require('./sketch-xib-loader-utils')
7 |
8 | function compileXib(loader) {
9 | const parsedPath = path.parse(loader.resourcePath)
10 | const nibPath = path.join(os.tmpdir(), `webpack-${parsedPath.name}.nib`)
11 | const result = runIbTool(loader, ['--compile', nibPath, loader.resourcePath])
12 |
13 | if (result.status === 0) {
14 | try {
15 | return fs.readFileSync(nibPath)
16 | }
17 | catch (e) {
18 | loader.emitError(new Error(`Error reading compiled nib: ${e.message}`))
19 | }
20 | finally {
21 | fs.unlinkSync(nibPath)
22 | }
23 | }
24 |
25 | return ''
26 | }
27 |
28 | module.exports = function () {
29 | return compileXib(this)
30 | }
31 |
32 | module.exports.raw = true
33 |
--------------------------------------------------------------------------------
/webpack-lib/plugins/webpack-export-sketch-commands-plugin.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { ConcatSource } = require('webpack-sources')
4 |
5 | const prefix = `
6 | var that = this;
7 | function run (key, context) {
8 | that.context = context;
9 |
10 | var exports =
11 | `
12 |
13 | const suffix = `
14 | if (key === 'default' && typeof exports === 'function') {
15 | exports(context);
16 | } else {
17 | exports[key](context);
18 | }
19 | }
20 |
21 | that['onRun'] = run.bind(this, 'default');
22 | `
23 |
24 | class ExportSketchCommandsPlugin {
25 | constructor(manifest) {
26 | this.suffix = suffix
27 | const commands = manifest.commands.map(command => command.handler)
28 | commands.forEach(command => {
29 | this.suffix += `that['${command}'] = run.bind(this, '${command}');
30 | `
31 | })
32 | }
33 |
34 | apply(compiler) {
35 | compiler.hooks.compilation.tap('ExportSketchCommandsPlugin', compilation => {
36 | compilation.hooks.optimizeChunkAssets.tapAsync('ExportSketchCommandsPlugin', (chunks, callback) => {
37 | chunks.forEach(chunk => {
38 | chunk.files.forEach(file => {
39 | compilation.assets[file] = new ConcatSource(prefix, compilation.assets[file], this.suffix)
40 | })
41 | })
42 |
43 | callback()
44 | })
45 | })
46 | }
47 | }
48 |
49 | module.exports = ExportSketchCommandsPlugin
50 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const helpers = require('./webpack-lib/config-helpers')
4 |
5 | const pluginConfig = helpers.getPluginConfig()
6 |
7 | // Customize pluginConfig and webpack config options here
8 | const options = {
9 | nib: true
10 | }
11 | // End customize pluginConfig and webpack config
12 |
13 | const config = helpers.makeConfig(pluginConfig, options)
14 |
15 | // Customize webpack config here
16 |
17 | // End customize webpack config
18 |
19 | module.exports = config
20 |
--------------------------------------------------------------------------------