├── .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 | ![](docs/dialog-start.png?raw=true) 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 | ![](docs/show-only-matching-styles.gif?raw=true) 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 | ![](docs/all-styles.png?raw=true) 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 | ![](docs/matching-styles.png?raw=true) 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 | 153 | 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 | --------------------------------------------------------------------------------