├── .gitignore ├── Contents └── Resources │ └── runner │ ├── index.html │ └── index.js ├── LICENSE ├── README.md ├── TODO.md ├── dist └── blade.sketchplugin │ └── Contents │ ├── Resources │ └── runner │ │ ├── index.html │ │ └── index.js │ └── Sketch │ ├── manifest.json │ └── plugin.js ├── examples ├── testNewBlade.sketch └── testNewBlade │ ├── config.js │ ├── image │ ├── 0001_icon.png │ ├── 0002_icon.png │ └── 0003_Rectangle.png │ ├── index.html │ └── index.js ├── package-lock.json ├── package.json ├── scripts ├── attachHandler.js ├── bundle.js └── copyManifest.js ├── src └── plugin │ ├── .babelrc │ ├── .eslintignore │ ├── .eslintrc │ ├── CLEANUP.md │ ├── common.js │ ├── components │ └── Group.js │ ├── index.js │ ├── parser │ ├── A.js │ ├── App.js │ ├── Button.js │ ├── Case.js │ ├── Group.js │ ├── Ignore.js │ ├── Img.js │ ├── Input.js │ ├── Text.js │ ├── Textarea.js │ ├── Unknown.js │ ├── common.js │ ├── createParserContext.js │ └── index.js │ ├── utils │ ├── core.js │ ├── formatter.js │ └── webview │ │ ├── index.js │ │ ├── panel.js │ │ ├── webview.js │ │ └── window.js │ └── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Node modules 2 | node_modules/ 3 | 4 | # Build folder 5 | Contents/Sketch 6 | 7 | # Ignore knowledge base for now until cleaned up :D 8 | knowledge/ 9 | 10 | # Docs files 11 | docs/.sass-cache 12 | docs/_site 13 | .idea 14 | -------------------------------------------------------------------------------- /Contents/Resources/runner/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Blade Output 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Zhenyu Hou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blade 2 | 3 | [![Version](https://img.shields.io/github/package-json/v/sskyy/blade.svg)](https://github.com/sskyy/blade) 4 | [![Join the chat at https://gitter.im/Sketch-Plugin-Blade/Lobby](https://badges.gitter.im/Sketch-Plugin-Blade/Lobby.svg)](https://gitter.im/Sketch-Plugin-Blade/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | Blade is a sketch plugin designed to generate **prototype** web pages. Add simple annotation to your sketch layers and blade will get the magic done. **Click image below to see [demo video](https://vimeo.com/243630016)**. 7 | 8 | [![Blade New Demo](https://gw.alipayobjects.com/zos/rmsportal/QmWxMLSFKRRCbMrKvfTD.png)](https://vimeo.com/243630016) 9 | 10 | ## Installation 11 | 12 | - Copy blade.sketchplugin in dist folder to sketch plugin folder. 13 | - Create a artboard to group your layers. 14 | - Run `Blade -> Export Current Artboard` on plugin menu. 15 | 16 | You can check the example file and its output in the **example** folder. 17 | 18 | ## Annotation Usage 19 | 20 | Blade use layer name annotation to convert layer to certain html component. 21 | 22 | ### A 23 | 24 | Add link to any kind of layer. 25 | 26 | ``` 27 | [A?url=http://www.github.com/sskyy/blade]xxx 28 | ``` 29 | 30 | ### Case 31 | 32 | Switch between different versions of any layer. Press **Command** key to see selectors. 33 | 34 | ``` 35 | [Case] 36 | [#caseName=red]xxx 37 | [#caseName=blue]xxx 38 | ``` 39 | 40 | ### Group 41 | 42 | Default type of group layer. Can be centered to its container. 43 | 44 | [Group?center=true] 45 | 46 | ### Ignore 47 | 48 | Layer will not show in output. 49 | 50 | ### Img 51 | 52 | Export any kind of layer as a Image. 53 | 54 | ## Roadmap 55 | 56 | ### Components 57 | 58 | - [x] Group( as default) 59 | - [x] A 60 | - [ ] Button 61 | - [x] Case 62 | - [x] Ignore 63 | - [x] Img 64 | - [ ] Input 65 | - [x] Text 66 | - [ ] Textarea 67 | 68 | ### Script support 69 | 70 | Support basic javascript 71 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 2 | - [x]统一打包和开发环境 3 | - [ ] Group/Text/Img/Case/A 做完就可以发一个版本 4 | - [x] 完成所需组件 5 | - [x] 完成导出脚本 6 | - [x] 支持导出图片 7 | - [x] 打包成一个完整的 blade.sketchplugin 文件 8 | - [ ] 做个完整的 demo 9 | - [ ] 完成文档 10 | - [ ] 去 boilerplate context 化 -------------------------------------------------------------------------------- /dist/blade.sketchplugin/Contents/Resources/runner/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Blade Output 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /dist/blade.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Blade", 3 | "identifier": "com.ariesate.blade", 4 | "authorEmail": "skyking_H@hotmail.com", 5 | "disableCocoaScriptPreprocessor": false, 6 | "commands": [ 7 | { 8 | "script": "plugin.js", 9 | "handler": "openRunner", 10 | "name": "Open Runner", 11 | "identifier": "openRunner" 12 | }, 13 | { 14 | "script": "plugin.js", 15 | "handler": "onSelectionChanged", 16 | "name": "Selection Changed", 17 | "identifier": "onSelectionChanged" 18 | }, 19 | { 20 | "script": "plugin.js", 21 | "handler": "exportCurrentLayer", 22 | "name": "Export Current Artboard", 23 | "identifier": "exportCurrentLayer" 24 | }, 25 | { 26 | "script": "plugin.js", 27 | "handler": "sendDataToRunner", 28 | "name": "Send Data To Runner", 29 | "identifier": "sendDataToRunner" 30 | }, 31 | { 32 | "script": "plugin.js", 33 | "handlers": { 34 | "actions": { 35 | "SelectionChanged.finish": "onSelectionChanged" 36 | } 37 | } 38 | } 39 | ], 40 | "menu": { 41 | "isRoot": 1, 42 | "items": [ 43 | { 44 | "title": "Blade", 45 | "items": [ 46 | "exportCurrentLayer", 47 | "openRunner", 48 | "sendDataToRunner" 49 | ] 50 | } 51 | ] 52 | }, 53 | "description": "A sketch plugin to generate prototype web pages.", 54 | "author": "sskyy", 55 | "version": "0.0.4" 56 | } -------------------------------------------------------------------------------- /dist/blade.sketchplugin/Contents/Sketch/plugin.js: -------------------------------------------------------------------------------- 1 | var handlers = 2 | /******/ (function(modules) { // webpackBootstrap 3 | /******/ // The module cache 4 | /******/ var installedModules = {}; 5 | /******/ 6 | /******/ // The require function 7 | /******/ function __webpack_require__(moduleId) { 8 | /******/ 9 | /******/ // Check if module is in cache 10 | /******/ if(installedModules[moduleId]) { 11 | /******/ return installedModules[moduleId].exports; 12 | /******/ } 13 | /******/ // Create a new module (and put it into the cache) 14 | /******/ var module = installedModules[moduleId] = { 15 | /******/ i: moduleId, 16 | /******/ l: false, 17 | /******/ exports: {} 18 | /******/ }; 19 | /******/ 20 | /******/ // Execute the module function 21 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 22 | /******/ 23 | /******/ // Flag the module as loaded 24 | /******/ module.l = true; 25 | /******/ 26 | /******/ // Return the exports of the module 27 | /******/ return module.exports; 28 | /******/ } 29 | /******/ 30 | /******/ 31 | /******/ // expose the modules object (__webpack_modules__) 32 | /******/ __webpack_require__.m = modules; 33 | /******/ 34 | /******/ // expose the module cache 35 | /******/ __webpack_require__.c = installedModules; 36 | /******/ 37 | /******/ // define getter function for harmony exports 38 | /******/ __webpack_require__.d = function(exports, name, getter) { 39 | /******/ if(!__webpack_require__.o(exports, name)) { 40 | /******/ Object.defineProperty(exports, name, { 41 | /******/ configurable: false, 42 | /******/ enumerable: true, 43 | /******/ get: getter 44 | /******/ }); 45 | /******/ } 46 | /******/ }; 47 | /******/ 48 | /******/ // getDefaultExport function for compatibility with non-harmony modules 49 | /******/ __webpack_require__.n = function(module) { 50 | /******/ var getter = module && module.__esModule ? 51 | /******/ function getDefault() { return module['default']; } : 52 | /******/ function getModuleExports() { return module; }; 53 | /******/ __webpack_require__.d(getter, 'a', getter); 54 | /******/ return getter; 55 | /******/ }; 56 | /******/ 57 | /******/ // Object.prototype.hasOwnProperty.call 58 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 59 | /******/ 60 | /******/ // __webpack_public_path__ 61 | /******/ __webpack_require__.p = ""; 62 | /******/ 63 | /******/ // Load entry module and return exports 64 | /******/ return __webpack_require__(__webpack_require__.s = 7); 65 | /******/ }) 66 | /************************************************************************/ 67 | /******/ ([ 68 | /* 0 */ 69 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 70 | 71 | "use strict"; 72 | /* harmony export (immutable) */ __webpack_exports__["f"] = toCSSRGBA; 73 | /* harmony export (immutable) */ __webpack_exports__["b"] = extractEffectStyle; 74 | /* unused harmony export extractBoxStyle */ 75 | /* harmony export (immutable) */ __webpack_exports__["c"] = extractPositionStyle; 76 | /* harmony export (immutable) */ __webpack_exports__["a"] = extractBoxRelatedStyle; 77 | /* unused harmony export extractStyle */ 78 | /* harmony export (immutable) */ __webpack_exports__["e"] = layerToBase64; 79 | /* harmony export (immutable) */ __webpack_exports__["d"] = iteratorToArray; 80 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 81 | 82 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 83 | 84 | function map(arr, handler) { 85 | var result = []; 86 | for (var i = 0; i < arr.count(); i++) { 87 | result.push(handler(arr[i])); 88 | } 89 | return result; 90 | } 91 | 92 | function toCSSRGBA(RGBAStr) { 93 | return 'rgba(' + String(RGBAStr).replace(/[\(\)]/g, '').split(' ').map(function (v) { 94 | var _v$split = v.split(':'), 95 | _v$split2 = _slicedToArray(_v$split, 2), 96 | type = _v$split2[0], 97 | value = _v$split2[1]; 98 | 99 | if (type !== 'a') { 100 | return Math.round(Number(value) * 256); 101 | } 102 | return Number(value); 103 | }).join(',') + ')'; 104 | } 105 | 106 | function makeShadowCSS(shadow, inset) { 107 | return '' + (inset ? 'inset ' : '') + shadow.offsetX() + 'px ' + shadow.offsetY() + 'px ' + shadow.blurRadius() + 'px ' + shadow.spread() + 'px ' + toCSSRGBA(shadow.color()); 108 | } 109 | 110 | function extractEffectStyle(layer) { 111 | var result = {}; 112 | var fills = layer.sketchObject.style().fills(); 113 | var borders = layer.sketchObject.style().borders(); 114 | var shadows = layer.sketchObject.style().shadows(); 115 | var innerShadows = layer.sketchObject.style().innerShadows(); 116 | if (fills.count() > 0) { 117 | Object.assign(result, { 118 | background: map(fills, function (fill) { 119 | return toCSSRGBA(fill.color()); 120 | }).join(',') 121 | }); 122 | } 123 | 124 | if (borders.count() > 0) { 125 | var firstBorder = borders[0]; 126 | Object.assign(result, { 127 | border: firstBorder.thickness() + 'px solid ' + toCSSRGBA(firstBorder.color()) 128 | }); 129 | } 130 | 131 | if (shadows.count() + innerShadows.count() > 0) { 132 | var totalShadows = map(shadows, makeShadowCSS).concat(map(innerShadows, function (s) { 133 | return makeShadowCSS(s, true); 134 | })); 135 | 136 | Object.assign(result, { 137 | boxShadow: totalShadows.join(',') 138 | }); 139 | } 140 | 141 | return result; 142 | } 143 | 144 | function extractBoxStyle(layer) { 145 | return { 146 | width: layer.frame.width, 147 | height: layer.frame.height 148 | }; 149 | } 150 | 151 | function extractPositionStyle(layer) { 152 | return { 153 | position: 'absolute', 154 | left: layer.sketchObject.absoluteRect().rulerX() - layer.container.sketchObject.absoluteRect().rulerX(), 155 | top: layer.sketchObject.absoluteRect().rulerY() - layer.container.sketchObject.absoluteRect().rulerY() 156 | }; 157 | } 158 | function extractBoxRelatedStyle(layer) { 159 | return Object.assign(extractBoxStyle(layer), extractPositionStyle(layer)); 160 | } 161 | 162 | function extractStyle(layer) { 163 | return Object.assign(extractBoxRelatedStyle(layer), extractEffectStyle(layer)); 164 | } 165 | 166 | function layerToBase64(layer) { 167 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 168 | 169 | var fileFolder = NSTemporaryDirectory(); 170 | 171 | var finalOptions = _extends({}, options, { 172 | 'use-id-for-name': true, 173 | scales: '3', 174 | formats: 'png', 175 | output: fileFolder 176 | }); 177 | 178 | var fullPath = fileFolder + '/' + layer.id + '@' + finalOptions.scales + 'x.png'; 179 | 180 | layer.export(finalOptions); 181 | 182 | var url = NSURL.fileURLWithPath(fullPath); 183 | var data = NSData.alloc().initWithContentsOfURL(url); 184 | var base64 = data.base64EncodedStringWithOptions(0); 185 | 186 | NSFileManager.defaultManager().removeItemAtURL_error(url, null); 187 | return 'data:image/png;base64,' + base64; 188 | } 189 | 190 | function iteratorToArray(iter) { 191 | var result = []; 192 | iter.iterate(function (item) { 193 | return result.push(item); 194 | }); 195 | return result; 196 | } 197 | 198 | /***/ }), 199 | /* 1 */ 200 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 201 | 202 | "use strict"; 203 | /* harmony export (immutable) */ __webpack_exports__["b"] = initWithContext; 204 | /* unused harmony export loadFramework */ 205 | /* unused harmony export context */ 206 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return document; }); 207 | /* unused harmony export selection */ 208 | /* unused harmony export sketch */ 209 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "c", function() { return pluginFolderPath; }); 210 | var context = null; 211 | var document = null; 212 | var selection = null; 213 | var sketch = null; 214 | 215 | var pluginFolderPath = null; 216 | var frameworkFolderPath = '/Contents/Resources/frameworks/'; 217 | 218 | function getPluginFolderPath() { 219 | // Get absolute folder path of plugin 220 | var split = context.scriptPath.split('/'); 221 | split.splice(-3, 3); 222 | return split.join('/'); 223 | } 224 | 225 | function initWithContext(ctx) { 226 | // This function needs to be called in the beginning of every entry point! 227 | // Set all env variables according to current context 228 | context = ctx; 229 | document = ctx.document || ctx.actionContext.document || MSDocument.currentDocument(); 230 | selection = document ? document.selectedLayers() : null; 231 | pluginFolderPath = getPluginFolderPath(); 232 | 233 | // Here you could load custom cocoa frameworks if you need to 234 | // loadFramework('FrameworkName', 'ClassName'); 235 | // => would be loaded into ClassName in global namespace! 236 | } 237 | 238 | function loadFramework(frameworkName, frameworkClass) { 239 | // Only load framework if class not already available 240 | if (Mocha && NSClassFromString(frameworkClass) == null) { 241 | var frameworkDir = '' + pluginFolderPath + frameworkFolderPath; 242 | var mocha = Mocha.sharedRuntime(); 243 | return mocha.loadFrameworkWithName_inDirectory(frameworkName, frameworkDir); 244 | } 245 | return false; 246 | } 247 | 248 | 249 | 250 | /***/ }), 251 | /* 2 */ 252 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 253 | 254 | "use strict"; 255 | /* unused harmony export BridgeMessageHandler */ 256 | /* unused harmony export initBridgedWebView */ 257 | /* unused harmony export getFilePath */ 258 | /* harmony export (immutable) */ __webpack_exports__["a"] = createWebView; 259 | /* harmony export (immutable) */ __webpack_exports__["b"] = sendAction; 260 | /* unused harmony export receiveAction */ 261 | /* unused harmony export windowIdentifier */ 262 | /* unused harmony export panelIdentifier */ 263 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__core__ = __webpack_require__(1); 264 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_cocoascript_class__ = __webpack_require__(8); 265 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_cocoascript_class___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_cocoascript_class__); 266 | 267 | 268 | 269 | 270 | // These are just used to identify the window(s) 271 | // Change them to whatever you need e.g. if you need to support multiple 272 | // windows at the same time... 273 | var windowIdentifier = 'sketch-plugin-boilerplate--window'; 274 | var panelIdentifier = 'sketch-plugin-boilerplate--panel'; 275 | 276 | // Since we now create the delegate in js, we need the enviroment 277 | // to stick around for as long as we need a reference to that delegate 278 | coscript.setShouldKeepAround(true); 279 | 280 | // This is a helper delegate, that handles incoming bridge messages 281 | var BridgeMessageHandler = new __WEBPACK_IMPORTED_MODULE_1_cocoascript_class___default.a({ 282 | 'userContentController:didReceiveScriptMessage:': function userContentControllerDidReceiveScriptMessage(controller, message) { 283 | try { 284 | var bridgeMessage = JSON.parse(String(message.body())); 285 | receiveAction(bridgeMessage.name, bridgeMessage.data); 286 | } catch (e) { 287 | log('Could not parse bridge message'); 288 | log(e.message); 289 | } 290 | } 291 | }); 292 | 293 | log('BridgeMessageHandler'); 294 | log(BridgeMessageHandler); 295 | log(BridgeMessageHandler.userContentController_didReceiveScriptMessage); 296 | 297 | function initBridgedWebView(frame) { 298 | var bridgeName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'SketchBridge'; 299 | 300 | var config = WKWebViewConfiguration.alloc().init(); 301 | var messageHandler = BridgeMessageHandler.alloc().init(); 302 | config.userContentController().addScriptMessageHandler_name(messageHandler, bridgeName); 303 | config.preferences().setValue_forKey(true, 'developerExtrasEnabled'); 304 | return WKWebView.alloc().initWithFrame_configuration(frame, config); 305 | } 306 | 307 | function getFilePath(file) { 308 | return __WEBPACK_IMPORTED_MODULE_0__core__["c" /* pluginFolderPath */] + '/Contents/Resources/webview/' + file; 309 | } 310 | 311 | function createWebView(path, frame) { 312 | var webView = initBridgedWebView(frame, 'Sketch'); 313 | var url = path.slice(0, 4) === 'http' ? NSURL.URLWithString(path) : NSURL.fileURLWithPath(getFilePath(path)); 314 | log('File URL'); 315 | log(url); 316 | 317 | webView.setAutoresizingMask(NSViewWidthSizable | NSViewHeightSizable); 318 | webView.loadRequest(NSURLRequest.requestWithURL(url)); 319 | 320 | return webView; 321 | } 322 | 323 | function sendAction(webView, name) { 324 | var payload = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 325 | 326 | if (!webView || !webView.evaluateJavaScript_completionHandler) { 327 | return; 328 | } 329 | // `sketchBridge` is the JS function exposed on window in the webview! 330 | var script = 'sketchBridge(' + JSON.stringify({ name: name, payload: payload }) + ');'; 331 | webView.evaluateJavaScript_completionHandler(script, null); 332 | } 333 | 334 | function receiveAction(name) { 335 | var payload = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 336 | 337 | __WEBPACK_IMPORTED_MODULE_0__core__["a" /* document */].showMessage('I received a message! 😊🎉🎉'); 338 | } 339 | 340 | 341 | 342 | /***/ }), 343 | /* 3 */ 344 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 345 | 346 | "use strict"; 347 | /* harmony export (immutable) */ __webpack_exports__["i"] = parseNameAndQuery; 348 | /* harmony export (immutable) */ __webpack_exports__["m"] = sendCommandToPanel; 349 | /* harmony export (immutable) */ __webpack_exports__["n"] = sendCommandToWindow; 350 | /* harmony export (immutable) */ __webpack_exports__["p"] = showWindow; 351 | /* harmony export (immutable) */ __webpack_exports__["o"] = showPanel; 352 | /* harmony export (immutable) */ __webpack_exports__["f"] = hidePanel; 353 | /* harmony export (immutable) */ __webpack_exports__["k"] = recursiveParse; 354 | /* harmony export (immutable) */ __webpack_exports__["h"] = isWindowOpened; 355 | /* harmony export (immutable) */ __webpack_exports__["b"] = createFolder; 356 | /* harmony export (immutable) */ __webpack_exports__["e"] = getPluginFolderPath; 357 | /* harmony export (immutable) */ __webpack_exports__["d"] = getCurrentFilePath; 358 | /* harmony export (immutable) */ __webpack_exports__["g"] = isFileExist; 359 | /* harmony export (immutable) */ __webpack_exports__["a"] = copyFile; 360 | /* harmony export (immutable) */ __webpack_exports__["q"] = writeToFile; 361 | /* harmony export (immutable) */ __webpack_exports__["l"] = removeFile; 362 | /* harmony export (immutable) */ __webpack_exports__["c"] = exportLayer; 363 | /* harmony export (immutable) */ __webpack_exports__["j"] = parseRawName; 364 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__utils_webview__ = __webpack_require__(4); 365 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 366 | 367 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 368 | 369 | 370 | 371 | 372 | function parseNameAndQuery(inputName) { 373 | var getDefaultValues = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () { 374 | return {}; 375 | }; 376 | 377 | var defaultValues = getDefaultValues(); 378 | var result = { name: '', query: defaultValues, hash: {} }; 379 | var name = String(inputName); 380 | if (name[0] !== '[') return result; 381 | 382 | // name/query/hash 383 | var state = 'none'; 384 | var stack = ''; 385 | 386 | function clearLastStack() { 387 | if (state === 'name') { 388 | result.name = stack; 389 | } else if (state === 'query') { 390 | stack.split('&').forEach(function (subStack) { 391 | var _subStack$split = subStack.split('='), 392 | _subStack$split2 = _slicedToArray(_subStack$split, 2), 393 | queryName = _subStack$split2[0], 394 | queryValue = _subStack$split2[1]; 395 | 396 | var defaultValue = defaultValues[queryName]; 397 | result.query[queryName] = typeof defaultValue === 'boolean' ? Boolean(queryValue) : typeof defaultValue === 'number' ? Number(queryValue) : queryValue; 398 | }); 399 | } else { 400 | // hash 401 | stack.split('&').forEach(function (subStack) { 402 | var _subStack$split3 = subStack.split('='), 403 | _subStack$split4 = _slicedToArray(_subStack$split3, 2), 404 | queryName = _subStack$split4[0], 405 | queryValue = _subStack$split4[1]; 406 | 407 | var defaultValue = defaultValues[queryName]; 408 | result.hash[queryName] = typeof defaultValue === 'boolean' ? Boolean(queryValue) : typeof defaultValue === 'number' ? Number(queryValue) : queryValue; 409 | }); 410 | } 411 | stack = ''; 412 | } 413 | 414 | for (var i = 0; i < name.length; i++) { 415 | var c = name[i]; 416 | if (c === '[' && state === 'none') { 417 | state = 'name'; 418 | } else if (c === '?' && (state === 'name' || state === 'hash')) { 419 | clearLastStack(); 420 | state = 'query'; 421 | } else if (c === '#' && (state === 'name' || state === 'query')) { 422 | clearLastStack(); 423 | state = 'hash'; 424 | } else if (c === ']' && (state === 'name' || state === 'query' || state === 'hash')) { 425 | clearLastStack(); 426 | state = 'end'; 427 | return result; 428 | } else { 429 | stack += c; 430 | } 431 | } 432 | 433 | throw new Error('tag not closed: ' + inputName); 434 | } 435 | 436 | function sendCommandToPanel(path, command, argv) { 437 | __WEBPACK_IMPORTED_MODULE_0__utils_webview__["e" /* sendPanelAction */](path, command, argv); 438 | } 439 | 440 | function sendCommandToWindow(path, command, argv) { 441 | __WEBPACK_IMPORTED_MODULE_0__utils_webview__["f" /* sendWindowAction */](path, command, argv); 442 | } 443 | 444 | function showWindow(path) { 445 | // CAUTION use path as identifier 446 | __WEBPACK_IMPORTED_MODULE_0__utils_webview__["c" /* openWindow */](path, path); 447 | } 448 | 449 | function showPanel(path) { 450 | __WEBPACK_IMPORTED_MODULE_0__utils_webview__["g" /* showPanel */](path, path); 451 | } 452 | 453 | function hidePanel(path) { 454 | __WEBPACK_IMPORTED_MODULE_0__utils_webview__["b" /* hidePanel */](path, path); 455 | } 456 | 457 | function recursiveParse(entry, parsers) { 458 | var context = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 459 | 460 | var _parseNameAndQuery = parseNameAndQuery(entry.name), 461 | name = _parseNameAndQuery.name, 462 | _parseNameAndQuery$qu = _parseNameAndQuery.query, 463 | query = _parseNameAndQuery$qu === undefined ? {} : _parseNameAndQuery$qu, 464 | hash = _parseNameAndQuery.hash; 465 | 466 | var resolvedName = name; 467 | if (resolvedName === '') { 468 | if (entry.isArtboard) { 469 | resolvedName = 'App'; 470 | } else if (entry.isGroup) { 471 | resolvedName = 'Group'; 472 | } else if (entry.isText) { 473 | resolvedName = 'Text'; 474 | } else { 475 | // resolvedName = 'Unknown' 476 | resolvedName = 'Img'; 477 | } 478 | } 479 | 480 | if (parsers[resolvedName] === undefined) { 481 | log('unknown parser ' + resolvedName + ', entry: ' + entry.name + ', parsers: ' + Object.keys(parsers).join(',')); 482 | throw new Error('unknown parser ' + resolvedName); 483 | } 484 | 485 | var _parsers$resolvedName = parsers[resolvedName](entry, context), 486 | _parsers$resolvedName2 = _slicedToArray(_parsers$resolvedName, 2), 487 | result = _parsers$resolvedName2[0], 488 | next = _parsers$resolvedName2[1]; 489 | 490 | if (next && next.length !== 0) { 491 | result.children = next.map(function (child) { 492 | return recursiveParse(child, parsers, context); 493 | }); 494 | } 495 | return Object.assign(result, { 496 | state: Object.assign(result.state || {}, query), 497 | props: Object.assign(result.props || {}, hash) 498 | }); 499 | } 500 | 501 | function isWindowOpened(path) { 502 | return Boolean(__WEBPACK_IMPORTED_MODULE_0__utils_webview__["a" /* findWebView */](path)); 503 | } 504 | 505 | function createFolder(path) { 506 | var manager = NSFileManager.defaultManager(); 507 | manager.createDirectoryAtPath_withIntermediateDirectories_attributes_error(path, true, null, null); 508 | // [file_manager createDirectoryAtPath:[folders objectAtIndex:i] withIntermediateDirectories:true attributes:nil error:nil]; 509 | } 510 | 511 | function getPluginFolderPath(context) { 512 | // Get absolute folder path of plugin 513 | var split = context.scriptPath.split('/'); 514 | split.splice(-3, 3); 515 | return split.join('/'); 516 | } 517 | 518 | function getCurrentFilePath(context) { 519 | return context.document.fileURL().path().replace(/\.sketch$/, ''); 520 | } 521 | 522 | function isFileExist(source) { 523 | var manager = NSFileManager.defaultManager(); 524 | return manager.fileExistsAtPath(source); 525 | } 526 | 527 | function copyFile(source, target) { 528 | var manager = NSFileManager.defaultManager(); 529 | if (!manager.fileExistsAtPath(source)) throw new Error('file not exist ' + source); 530 | // [file_manager copyItemAtPath:org toPath:tar error:nil]; 531 | manager.copyItemAtPath_toPath_error(source, target, null); 532 | } 533 | 534 | function writeToFile(path, content) { 535 | var resultStr = NSString.stringWithFormat('%@', content); 536 | resultStr.writeToFile_atomically(path, true); 537 | } 538 | 539 | function removeFile(path) { 540 | var manager = NSFileManager.defaultManager(); 541 | // [file_manager removeItemAtPath:folder error:nil] 542 | manager.removeItemAtPath_error(path, null); 543 | } 544 | 545 | function exportLayer(layer, targetFolder, name) { 546 | var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; 547 | 548 | var finalOptions = _extends({}, options, { 549 | 'use-id-for-name': true, 550 | scales: '3', 551 | formats: 'png', 552 | output: targetFolder 553 | }); 554 | 555 | var tmpFullPath = targetFolder + '/' + layer.id + '@' + finalOptions.scales + 'x.' + finalOptions.formats; 556 | layer.export(finalOptions); 557 | log('generating image: ' + tmpFullPath); 558 | var manager = NSFileManager.defaultManager(); 559 | var targetPath = targetFolder + '/' + name + '.' + finalOptions.formats; 560 | log('renaming image to ' + targetPath); 561 | manager.moveItemAtURL_toURL_error(NSURL.fileURLWithPath(tmpFullPath), NSURL.fileURLWithPath(targetPath), null); 562 | } 563 | 564 | function parseRawName(inputName) { 565 | var name = String(inputName); 566 | return name.replace(/^\[.+\]/, ''); 567 | } 568 | 569 | /***/ }), 570 | /* 4 */ 571 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 572 | 573 | "use strict"; 574 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__webview__ = __webpack_require__(2); 575 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__window__ = __webpack_require__(10); 576 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__panel__ = __webpack_require__(11); 577 | /* unused harmony reexport windowIdentifier */ 578 | /* unused harmony reexport panelIdentifier */ 579 | /* unused harmony reexport getFilePath */ 580 | /* unused harmony reexport createWebView */ 581 | /* unused harmony reexport sendActionToWebView */ 582 | /* unused harmony reexport receiveAction */ 583 | /* harmony reexport (binding) */ __webpack_require__.d(__webpack_exports__, "c", function() { return __WEBPACK_IMPORTED_MODULE_1__window__["b"]; }); 584 | /* harmony reexport (binding) */ __webpack_require__.d(__webpack_exports__, "f", function() { return __WEBPACK_IMPORTED_MODULE_1__window__["c"]; }); 585 | /* harmony reexport (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return __WEBPACK_IMPORTED_MODULE_1__window__["a"]; }); 586 | /* unused harmony reexport togglePanel */ 587 | /* unused harmony reexport openPanel */ 588 | /* unused harmony reexport closePanel */ 589 | /* harmony reexport (binding) */ __webpack_require__.d(__webpack_exports__, "g", function() { return __WEBPACK_IMPORTED_MODULE_2__panel__["c"]; }); 590 | /* harmony reexport (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return __WEBPACK_IMPORTED_MODULE_2__panel__["a"]; }); 591 | /* unused harmony reexport isPanelOpen */ 592 | /* harmony reexport (binding) */ __webpack_require__.d(__webpack_exports__, "e", function() { return __WEBPACK_IMPORTED_MODULE_2__panel__["b"]; }); 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | /***/ }), 602 | /* 5 */ 603 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 604 | 605 | "use strict"; 606 | /* harmony export (immutable) */ __webpack_exports__["a"] = Text; 607 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__common__ = __webpack_require__(0); 608 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 609 | 610 | 611 | 612 | function Text(layer) { 613 | var node = { 614 | type: 'Text', 615 | state: { 616 | text: String(layer.sketchObject.stringValue()), 617 | style: _extends({ 618 | fontSize: layer.sketchObject.fontSize(), 619 | color: Object(__WEBPACK_IMPORTED_MODULE_0__common__["f" /* toCSSRGBA */])(layer.sketchObject.textColor()) 620 | }, Object(__WEBPACK_IMPORTED_MODULE_0__common__["a" /* extractBoxRelatedStyle */])(layer), { 621 | // TODO align 翻译 622 | align: layer.sketchObject.textAlignment(), 623 | // TODO line spacing 翻译成 line height 624 | // lineHeight: layer.sketchObject.lineSpacing(), 625 | letterSpacing: layer.sketchObject.characterSpacing() || 'inherit', 626 | fontFamily: String(layer.sketchObject.fontPostscriptName()) 627 | }) 628 | } 629 | }; 630 | 631 | return [node, []]; 632 | } 633 | 634 | /***/ }), 635 | /* 6 */ 636 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 637 | 638 | "use strict"; 639 | /* harmony export (immutable) */ __webpack_exports__["a"] = Group; 640 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__common__ = __webpack_require__(3); 641 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__common__ = __webpack_require__(0); 642 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__components_Group__ = __webpack_require__(17); 643 | 644 | 645 | 646 | 647 | var BG_NAME = 'Bg'; 648 | 649 | function Group(group, _ref) { 650 | var createImgRef = _ref.createImgRef; 651 | 652 | var next = []; 653 | var bgLayer = null; 654 | group.iterate(function (layer) { 655 | // TODO 从节点上读数据 656 | if (Object(__WEBPACK_IMPORTED_MODULE_0__common__["i" /* parseNameAndQuery */])(layer.name).name === BG_NAME) { 657 | bgLayer = layer; 658 | } else { 659 | next.push(layer); 660 | } 661 | }); 662 | 663 | var _parseNameAndQuery = Object(__WEBPACK_IMPORTED_MODULE_0__common__["i" /* parseNameAndQuery */])(group.name, __WEBPACK_IMPORTED_MODULE_2__components_Group__["a" /* getDefaultState */]), 664 | query = _parseNameAndQuery.query; 665 | 666 | var node = { 667 | type: 'Group', 668 | state: Object.assign(query, { style: Object(__WEBPACK_IMPORTED_MODULE_1__common__["a" /* extractBoxRelatedStyle */])(group) }) 669 | }; 670 | 671 | if (bgLayer) { 672 | if (bgLayer.isImage || bgLayer.isGroup) { 673 | node.state.style.background = createImgRef(bgLayer); 674 | } else { 675 | Object.assign(node.state.style, Object(__WEBPACK_IMPORTED_MODULE_1__common__["b" /* extractEffectStyle */])(bgLayer)); 676 | } 677 | } 678 | 679 | return [node, next]; 680 | } 681 | 682 | /***/ }), 683 | /* 7 */ 684 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 685 | 686 | "use strict"; 687 | Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); 688 | /* harmony export (immutable) */ __webpack_exports__["openRunner"] = openRunner; 689 | /* harmony export (immutable) */ __webpack_exports__["sendDataToRunner"] = sendDataToRunner; 690 | /* harmony export (immutable) */ __webpack_exports__["exportCurrentLayer"] = exportCurrentLayer; 691 | /* harmony export (immutable) */ __webpack_exports__["onSelectionChanged"] = onSelectionChanged; 692 | /* harmony export (immutable) */ __webpack_exports__["testSendAction"] = testSendAction; 693 | /* harmony export (immutable) */ __webpack_exports__["parseLayer"] = parseLayer; 694 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__utils_core__ = __webpack_require__(1); 695 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__utils_webview__ = __webpack_require__(4); 696 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__common__ = __webpack_require__(3); 697 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__parser__ = __webpack_require__(13); 698 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__parser_createParserContext__ = __webpack_require__(24); 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | var RUNNER_URL = 'http://127.0.0.1:8080/runner.html'; 707 | var parserContext = Object(__WEBPACK_IMPORTED_MODULE_4__parser_createParserContext__["b" /* default */])(); 708 | 709 | function openRunner() { 710 | Object(__WEBPACK_IMPORTED_MODULE_2__common__["p" /* showWindow */])(RUNNER_URL); 711 | } 712 | 713 | function sendDataToRunner(context) { 714 | Object(__WEBPACK_IMPORTED_MODULE_0__utils_core__["b" /* initWithContext */])(context); 715 | if (!context.api) return __WEBPACK_IMPORTED_MODULE_0__utils_core__["a" /* document */].showMessage('error context.api!'); 716 | if (!Object(__WEBPACK_IMPORTED_MODULE_2__common__["h" /* isWindowOpened */])(RUNNER_URL)) { 717 | return __WEBPACK_IMPORTED_MODULE_0__utils_core__["a" /* document */].showMessage('please open runner first!'); 718 | } 719 | 720 | var firstArtboard = void 0; 721 | context.api().selectedDocument.selectedPage.iterate(function (page) { 722 | if (!firstArtboard) firstArtboard = page; 723 | }); 724 | 725 | if (!firstArtboard || !firstArtboard.isArtboard) return __WEBPACK_IMPORTED_MODULE_0__utils_core__["a" /* document */].showMEssage('please select an artboard'); 726 | // const resultStr = NSString.stringWithFormat('%@', JSON.stringify(recursiveParse(firstArtboard, parsers))) 727 | // resultStr.writeToFile_atomically(context.document.fileURL().path().replace(/\.sketch$/, '.json'), true) 728 | var result = Object(__WEBPACK_IMPORTED_MODULE_2__common__["k" /* recursiveParse */])(firstArtboard, __WEBPACK_IMPORTED_MODULE_3__parser__["a" /* default */], parserContext); 729 | Object(__WEBPACK_IMPORTED_MODULE_2__common__["n" /* sendCommandToWindow */])(RUNNER_URL, 'config', result); 730 | return __WEBPACK_IMPORTED_MODULE_0__utils_core__["a" /* document */].showMessage('done!'); 731 | } 732 | 733 | function exportCurrentLayer(context) { 734 | Object(__WEBPACK_IMPORTED_MODULE_0__utils_core__["b" /* initWithContext */])(context); 735 | if (!context.api) return __WEBPACK_IMPORTED_MODULE_0__utils_core__["a" /* document */].showMessage('error context.api!'); 736 | if (!context.document.fileURL()) return __WEBPACK_IMPORTED_MODULE_0__utils_core__["a" /* document */].showMessage('please save your file first!'); 737 | 738 | var sketch = context.api(); 739 | 740 | var firstArtboard = void 0; 741 | sketch.selectedDocument.selectedPage.iterate(function (page) { 742 | if (!firstArtboard) firstArtboard = page; 743 | }); 744 | 745 | if (!firstArtboard || !firstArtboard.isArtboard) return __WEBPACK_IMPORTED_MODULE_0__utils_core__["a" /* document */].showMessage('please select an artboard'); 746 | 747 | var exportFolder = Object(__WEBPACK_IMPORTED_MODULE_2__common__["d" /* getCurrentFilePath */])(context); 748 | if (Object(__WEBPACK_IMPORTED_MODULE_2__common__["g" /* isFileExist */])(exportFolder)) Object(__WEBPACK_IMPORTED_MODULE_2__common__["l" /* removeFile */])(exportFolder); 749 | 750 | Object(__WEBPACK_IMPORTED_MODULE_2__common__["b" /* createFolder */])(exportFolder); 751 | var runnerPath = Object(__WEBPACK_IMPORTED_MODULE_2__common__["e" /* getPluginFolderPath */])(context) + '/Contents/Resources/runner'; 752 | Object(__WEBPACK_IMPORTED_MODULE_2__common__["a" /* copyFile */])(runnerPath + '/index.js', exportFolder + '/index.js'); 753 | Object(__WEBPACK_IMPORTED_MODULE_2__common__["a" /* copyFile */])(runnerPath + '/index.html', exportFolder + '/index.html'); 754 | // TODO add error message 755 | var result = void 0; 756 | try { 757 | result = Object(__WEBPACK_IMPORTED_MODULE_2__common__["k" /* recursiveParse */])(firstArtboard, __WEBPACK_IMPORTED_MODULE_3__parser__["a" /* default */], parserContext); 758 | } catch (e) { 759 | log('parseError: ' + e.message); 760 | return __WEBPACK_IMPORTED_MODULE_0__utils_core__["a" /* document */].showMessage('error: ' + e.message); 761 | } 762 | 763 | Object(__WEBPACK_IMPORTED_MODULE_2__common__["q" /* writeToFile */])(exportFolder + '/config.js', 'sketchBridge({ payload: ' + JSON.stringify(result) + ' })'); 764 | 765 | // handle images 766 | var imageFolder = exportFolder + '/' + __WEBPACK_IMPORTED_MODULE_4__parser_createParserContext__["a" /* IMAGE_FOLDER */]; 767 | Object(__WEBPACK_IMPORTED_MODULE_2__common__["b" /* createFolder */])(imageFolder); 768 | parserContext.getImgRefs(function (_ref) { 769 | var id = _ref.id, 770 | name = _ref.name, 771 | options = _ref.options; 772 | 773 | Object(__WEBPACK_IMPORTED_MODULE_2__common__["c" /* exportLayer */])(sketch.selectedDocument.layerWithID(id), imageFolder, name, options); 774 | }); 775 | 776 | return __WEBPACK_IMPORTED_MODULE_0__utils_core__["a" /* document */].showMessage('done!'); 777 | } 778 | 779 | function onSelectionChanged(context) { 780 | Object(__WEBPACK_IMPORTED_MODULE_0__utils_core__["b" /* initWithContext */])(context); 781 | var currentDocument = context.actionContext.document; 782 | var selectedLayers = currentDocument.selectedLayers(); 783 | 784 | if (!selectedLayers.containsLayers() || selectedLayers.containsMultipleLayers()) { 785 | return Object(__WEBPACK_IMPORTED_MODULE_2__common__["f" /* hidePanel */])(); 786 | } 787 | 788 | var selectedLayer = selectedLayers.firstLayer(); 789 | var directiveName = Object(__WEBPACK_IMPORTED_MODULE_2__common__["i" /* parseNameAndQuery */])(selectedLayer.name()).name; 790 | 791 | if (directiveName == null) { 792 | return Object(__WEBPACK_IMPORTED_MODULE_2__common__["f" /* hidePanel */])(); 793 | } 794 | 795 | var command = context.command; 796 | var lastProps = command.valueForKey_onLayer('props', selectedLayer); 797 | var finalProps = lastProps == null ? {} : lastProps; 798 | 799 | Object(__WEBPACK_IMPORTED_MODULE_2__common__["o" /* showPanel */])(); 800 | Object(__WEBPACK_IMPORTED_MODULE_2__common__["m" /* sendCommandToPanel */])('showProps', finalProps); 801 | } 802 | 803 | // for debug 804 | function testSendAction(context) { 805 | Object(__WEBPACK_IMPORTED_MODULE_0__utils_core__["b" /* initWithContext */])(context); 806 | __WEBPACK_IMPORTED_MODULE_1__utils_webview__["sendAction"]('aaa', { value: true }); 807 | } 808 | 809 | function parseLayer(context) { 810 | Object(__WEBPACK_IMPORTED_MODULE_0__utils_core__["b" /* initWithContext */])(context); 811 | var first = void 0; 812 | context.api().selectedDocument.selectedLayers.iterate(function (layer) { 813 | if (!first) first = layer; 814 | }); 815 | var result = NSString.stringWithFormat('%@', JSON.stringify(__WEBPACK_IMPORTED_MODULE_3__parser__["a" /* default */].Group(first))); 816 | 817 | result.writeToFile_atomically(context.document.fileURL().path().replace(/\.sketch$/, '.json'), true); 818 | __WEBPACK_IMPORTED_MODULE_0__utils_core__["a" /* document */].showMessage('done!'); 819 | } 820 | 821 | /***/ }), 822 | /* 8 */ 823 | /***/ (function(module, exports, __webpack_require__) { 824 | 825 | "use strict"; 826 | 827 | 828 | Object.defineProperty(exports, "__esModule", { 829 | value: true 830 | }); 831 | exports.SuperCall = undefined; 832 | exports.default = ObjCClass; 833 | 834 | var _runtime = __webpack_require__(9); 835 | 836 | exports.SuperCall = _runtime.SuperCall; 837 | 838 | // super when returnType is id and args are void 839 | // id objc_msgSendSuper(struct objc_super *super, SEL op, void) 840 | 841 | const SuperInit = (0, _runtime.SuperCall)(NSStringFromSelector("init"), [], { type: "@" }); 842 | 843 | // Returns a real ObjC class. No need to use new. 844 | function ObjCClass(defn) { 845 | const superclass = defn.superclass || NSObject; 846 | const className = (defn.className || defn.classname || "ObjCClass") + NSUUID.UUID().UUIDString(); 847 | const reserved = new Set(['className', 'classname', 'superclass']); 848 | var cls = MOClassDescription.allocateDescriptionForClassWithName_superclass_(className, superclass); 849 | // Add each handler to the class description 850 | const ivars = []; 851 | for (var key in defn) { 852 | const v = defn[key]; 853 | if (typeof v == 'function' && key !== 'init') { 854 | var selector = NSSelectorFromString(key); 855 | cls.addInstanceMethodWithSelector_function_(selector, v); 856 | } else if (!reserved.has(key)) { 857 | ivars.push(key); 858 | cls.addInstanceVariableWithName_typeEncoding(key, "@"); 859 | } 860 | } 861 | 862 | cls.addInstanceMethodWithSelector_function_(NSSelectorFromString('init'), function () { 863 | const self = SuperInit.call(this); 864 | ivars.map(name => { 865 | Object.defineProperty(self, name, { 866 | get() { 867 | return getIvar(self, name); 868 | }, 869 | set(v) { 870 | (0, _runtime.object_setInstanceVariable)(self, name, v); 871 | } 872 | }); 873 | self[name] = defn[name]; 874 | }); 875 | // If there is a passsed-in init funciton, call it now. 876 | if (typeof defn.init == 'function') defn.init.call(this); 877 | return self; 878 | }); 879 | 880 | return cls.registerClass(); 881 | }; 882 | 883 | function getIvar(obj, name) { 884 | const retPtr = MOPointer.new(); 885 | (0, _runtime.object_getInstanceVariable)(obj, name, retPtr); 886 | return retPtr.value().retain().autorelease(); 887 | } 888 | 889 | /***/ }), 890 | /* 9 */ 891 | /***/ (function(module, exports, __webpack_require__) { 892 | 893 | "use strict"; 894 | 895 | 896 | Object.defineProperty(exports, "__esModule", { 897 | value: true 898 | }); 899 | exports.SuperCall = SuperCall; 900 | exports.CFunc = CFunc; 901 | const objc_super_typeEncoding = '{objc_super="receiver"@"super_class"#}'; 902 | 903 | // You can store this to call your function. this must be bound to the current instance. 904 | function SuperCall(selector, argTypes, returnType) { 905 | const func = CFunc("objc_msgSendSuper", [{ type: '^' + objc_super_typeEncoding }, { type: ":" }, ...argTypes], returnType); 906 | return function (...args) { 907 | const struct = make_objc_super(this, this.superclass()); 908 | const structPtr = MOPointer.alloc().initWithValue_(struct); 909 | return func(structPtr, selector, ...args); 910 | }; 911 | } 912 | 913 | // Recursively create a MOStruct 914 | function makeStruct(def) { 915 | if (typeof def !== 'object' || Object.keys(def).length == 0) { 916 | return def; 917 | } 918 | const name = Object.keys(def)[0]; 919 | const values = def[name]; 920 | 921 | const structure = MOStruct.structureWithName_memberNames_runtime(name, Object.keys(values), Mocha.sharedRuntime()); 922 | 923 | Object.keys(values).map(member => { 924 | structure[member] = makeStruct(values[member]); 925 | }); 926 | 927 | return structure; 928 | } 929 | 930 | function make_objc_super(self, cls) { 931 | return makeStruct({ 932 | objc_super: { 933 | receiver: self, 934 | super_class: cls 935 | } 936 | }); 937 | } 938 | 939 | // Due to particularities of the JS bridge, we can't call into MOBridgeSupport objects directly 940 | // But, we can ask key value coding to do the dirty work for us ;) 941 | function setKeys(o, d) { 942 | const funcDict = NSMutableDictionary.dictionary(); 943 | funcDict.o = o; 944 | Object.keys(d).map(k => funcDict.setValue_forKeyPath(d[k], "o." + k)); 945 | } 946 | 947 | // Use any C function, not just ones with BridgeSupport 948 | function CFunc(name, args, retVal) { 949 | function makeArgument(a) { 950 | if (!a) return null; 951 | const arg = MOBridgeSupportArgument.alloc().init(); 952 | setKeys(arg, { 953 | type64: a.type 954 | }); 955 | return arg; 956 | } 957 | const func = MOBridgeSupportFunction.alloc().init(); 958 | setKeys(func, { 959 | name: name, 960 | arguments: args.map(makeArgument), 961 | returnValue: makeArgument(retVal) 962 | }); 963 | return func; 964 | } 965 | 966 | /* 967 | @encode(char*) = "*" 968 | @encode(id) = "@" 969 | @encode(Class) = "#" 970 | @encode(void*) = "^v" 971 | @encode(CGRect) = "{CGRect={CGPoint=dd}{CGSize=dd}}" 972 | @encode(SEL) = ":" 973 | */ 974 | 975 | function addStructToBridgeSupport(key, structDef) { 976 | // OK, so this is probably the nastiest hack in this file. 977 | // We go modify MOBridgeSupportController behind its back and use kvc to add our own definition 978 | // There isn't another API for this though. So the only other way would be to make a real bridgesupport file. 979 | const symbols = MOBridgeSupportController.sharedController().valueForKey('symbols'); 980 | if (!symbols) throw Error("Something has changed within bridge support so we can't add our definitions"); 981 | // If someone already added this definition, don't re-register it. 982 | if (symbols[key] !== null) return; 983 | const def = MOBridgeSupportStruct.alloc().init(); 984 | setKeys(def, { 985 | name: key, 986 | type: structDef.type 987 | }); 988 | symbols[key] = def; 989 | }; 990 | 991 | // This assumes the ivar is an object type. Return value is pretty useless. 992 | const object_getInstanceVariable = exports.object_getInstanceVariable = CFunc("object_getInstanceVariable", [{ type: "@" }, { type: '*' }, { type: "^@" }], { type: "^{objc_ivar=}" }); 993 | // Again, ivar is of object type 994 | const object_setInstanceVariable = exports.object_setInstanceVariable = CFunc("object_setInstanceVariable", [{ type: "@" }, { type: '*' }, { type: "@" }], { type: "^{objc_ivar=}" }); 995 | 996 | // We need Mocha to understand what an objc_super is so we can use it as a function argument 997 | addStructToBridgeSupport('objc_super', { type: objc_super_typeEncoding }); 998 | 999 | /***/ }), 1000 | /* 10 */ 1001 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1002 | 1003 | "use strict"; 1004 | /* harmony export (immutable) */ __webpack_exports__["b"] = open; 1005 | /* harmony export (immutable) */ __webpack_exports__["a"] = findWebView; 1006 | /* harmony export (immutable) */ __webpack_exports__["c"] = sendAction; 1007 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__webview__ = __webpack_require__(2); 1008 | 1009 | 1010 | function open(identifier) { 1011 | var path = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'index.html'; 1012 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 1013 | 1014 | // Sensible defaults for options 1015 | var _options$width = options.width, 1016 | width = _options$width === undefined ? 800 : _options$width, 1017 | _options$height = options.height, 1018 | height = _options$height === undefined ? 350 : _options$height, 1019 | _options$title = options.title, 1020 | title = _options$title === undefined ? 'Sketch Plugin Boilerplate' : _options$title; 1021 | 1022 | 1023 | var frame = NSMakeRect(0, 0, width, height); 1024 | var masks = NSTitledWindowMask | NSWindowStyleMaskClosable | NSResizableWindowMask; 1025 | var window = NSPanel.alloc().initWithContentRect_styleMask_backing_defer(frame, masks, NSBackingStoreBuffered, false); 1026 | window.setMinSize({ width: 200, height: 200 }); 1027 | 1028 | // We use this dictionary to have a persistant storage of our NSWindow/NSPanel instance 1029 | // Otherwise the instance is stored nowhere and gets release => Window closes 1030 | var threadDictionary = NSThread.mainThread().threadDictionary(); 1031 | threadDictionary[identifier] = window; 1032 | 1033 | var webView = Object(__WEBPACK_IMPORTED_MODULE_0__webview__["a" /* createWebView */])(path, frame); 1034 | 1035 | window.title = title; 1036 | window.center(); 1037 | window.contentView().addSubview(webView); 1038 | 1039 | window.makeKeyAndOrderFront(null); 1040 | } 1041 | 1042 | function findWebView(identifier) { 1043 | var threadDictionary = NSThread.mainThread().threadDictionary(); 1044 | var window = threadDictionary[identifier]; 1045 | return window && window.contentView().subviews()[0]; 1046 | } 1047 | 1048 | function sendAction(identifier, name) { 1049 | var payload = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 1050 | 1051 | return Object(__WEBPACK_IMPORTED_MODULE_0__webview__["b" /* sendAction */])(findWebView(identifier), name, payload); 1052 | } 1053 | 1054 | /***/ }), 1055 | /* 11 */ 1056 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1057 | 1058 | "use strict"; 1059 | /* unused harmony export toggle */ 1060 | /* unused harmony export open */ 1061 | /* harmony export (immutable) */ __webpack_exports__["c"] = show; 1062 | /* unused harmony export close */ 1063 | /* unused harmony export isOpen */ 1064 | /* unused harmony export findWebView */ 1065 | /* harmony export (immutable) */ __webpack_exports__["b"] = sendAction; 1066 | /* harmony export (immutable) */ __webpack_exports__["a"] = hide; 1067 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__core__ = __webpack_require__(1); 1068 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__formatter__ = __webpack_require__(12); 1069 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__webview__ = __webpack_require__(2); 1070 | 1071 | 1072 | 1073 | 1074 | 1075 | function toggle(identifier, path, width) { 1076 | if (isOpen(identifier)) { 1077 | close(identifier); 1078 | } else { 1079 | open(identifier, path, width); 1080 | } 1081 | } 1082 | 1083 | function open(identifier) { 1084 | var path = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'index.html'; 1085 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 1086 | var width = options.width; 1087 | 1088 | var frame = NSMakeRect(0, 0, width || 250, 600); // the height doesn't really matter here 1089 | var contentView = __WEBPACK_IMPORTED_MODULE_0__core__["a" /* document */].documentWindow().contentView(); 1090 | if (!contentView || isOpen()) { 1091 | return false; 1092 | } 1093 | 1094 | var stageView = contentView.subviews().objectAtIndex(0); 1095 | var webView = Object(__WEBPACK_IMPORTED_MODULE_2__webview__["a" /* createWebView */])(path, frame); 1096 | webView.identifier = identifier; 1097 | 1098 | // Inject our webview into the right spot in the subview list 1099 | var views = stageView.subviews(); 1100 | var finalViews = []; 1101 | var pushedWebView = false; 1102 | for (var i = 0; i < views.count(); i++) { 1103 | var view = views.objectAtIndex(i); 1104 | finalViews.push(view); 1105 | // NOTE: change the view identifier here if you want to add 1106 | // your panel anywhere else 1107 | if (!pushedWebView && view.identifier() == 'view_canvas') { 1108 | finalViews.push(webView); 1109 | pushedWebView = true; 1110 | } 1111 | } 1112 | // If it hasn't been pushed yet, push our web view 1113 | // E.g. when inspector is not activated etc. 1114 | if (!pushedWebView) { 1115 | finalViews.push(webView); 1116 | } 1117 | // Finally, update the subviews prop and refresh 1118 | stageView.subviews = finalViews; 1119 | stageView.adjustSubviews(); 1120 | } 1121 | 1122 | function show(identifier) { 1123 | var viewToShow = findWebView(identifier); 1124 | 1125 | if (viewToShow == undefined) { 1126 | return open(identifier); 1127 | } 1128 | 1129 | var contentView = __WEBPACK_IMPORTED_MODULE_0__core__["a" /* document */].documentWindow().contentView(); 1130 | if (!contentView) { 1131 | return false; 1132 | } 1133 | var stageView = contentView.subviews().objectAtIndex(0); 1134 | 1135 | viewToShow.hidden = false; 1136 | stageView.adjustSubviews(); 1137 | } 1138 | 1139 | function close(identifier) { 1140 | var contentView = __WEBPACK_IMPORTED_MODULE_0__core__["a" /* document */].documentWindow().contentView(); 1141 | if (!contentView) { 1142 | return false; 1143 | } 1144 | 1145 | var stageView = contentView.subviews().objectAtIndex(0); 1146 | stageView.subviews = Object(__WEBPACK_IMPORTED_MODULE_1__formatter__["a" /* toArray */])(stageView.subviews()).filter(function (view) { 1147 | return view.identifier() != identifier; 1148 | }); 1149 | stageView.adjustSubviews(); 1150 | } 1151 | 1152 | function isOpen(identifier) { 1153 | return !!findWebView(identifier); 1154 | } 1155 | 1156 | function findWebView(identifier) { 1157 | var contentView = __WEBPACK_IMPORTED_MODULE_0__core__["a" /* document */].documentWindow().contentView(); 1158 | if (!contentView) { 1159 | return false; 1160 | } 1161 | var splitView = contentView.subviews().objectAtIndex(0); 1162 | var views = Object(__WEBPACK_IMPORTED_MODULE_1__formatter__["a" /* toArray */])(splitView.subviews()); 1163 | return views.find(function (view) { 1164 | return view.identifier() == identifier; 1165 | }); 1166 | } 1167 | 1168 | function sendAction(identifier, name) { 1169 | var payload = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 1170 | 1171 | return Object(__WEBPACK_IMPORTED_MODULE_2__webview__["b" /* sendAction */])(findWebView(identifier), name, payload); 1172 | } 1173 | 1174 | function hide(identifier) { 1175 | var contentView = __WEBPACK_IMPORTED_MODULE_0__core__["a" /* document */].documentWindow().contentView(); 1176 | if (!contentView) { 1177 | return false; 1178 | } 1179 | // Search for web view panel 1180 | var stageView = contentView.subviews().objectAtIndex(0); 1181 | var viewToHide = findWebView(identifier); 1182 | 1183 | if (viewToHide == undefined) return; 1184 | 1185 | viewToHide.hidden = true; 1186 | stageView.adjustSubviews(); 1187 | } 1188 | 1189 | /***/ }), 1190 | /* 12 */ 1191 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1192 | 1193 | "use strict"; 1194 | /* harmony export (immutable) */ __webpack_exports__["a"] = toArray; 1195 | function toArray(object) { 1196 | if (Array.isArray(object)) { 1197 | return object; 1198 | } 1199 | var arr = []; 1200 | for (var j = 0; j < object.count(); j++) { 1201 | arr.push(object.objectAtIndex(j)); 1202 | } 1203 | return arr; 1204 | } 1205 | 1206 | /***/ }), 1207 | /* 13 */ 1208 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1209 | 1210 | "use strict"; 1211 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__A__ = __webpack_require__(14); 1212 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__Button__ = __webpack_require__(15); 1213 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__App__ = __webpack_require__(16); 1214 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__Case__ = __webpack_require__(18); 1215 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__Group__ = __webpack_require__(6); 1216 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_5__Unknown__ = __webpack_require__(19); 1217 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_6__Ignore__ = __webpack_require__(20); 1218 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_7__Img__ = __webpack_require__(21); 1219 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_8__Input__ = __webpack_require__(22); 1220 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_9__Text__ = __webpack_require__(5); 1221 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_10__Textarea__ = __webpack_require__(23); 1222 | 1223 | 1224 | 1225 | 1226 | 1227 | 1228 | 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | /* harmony default export */ __webpack_exports__["a"] = ({ 1235 | A: __WEBPACK_IMPORTED_MODULE_0__A__["a" /* default */], 1236 | Button: __WEBPACK_IMPORTED_MODULE_1__Button__["a" /* default */], 1237 | Case: __WEBPACK_IMPORTED_MODULE_3__Case__["a" /* default */], 1238 | Group: __WEBPACK_IMPORTED_MODULE_4__Group__["a" /* default */], 1239 | App: __WEBPACK_IMPORTED_MODULE_2__App__["a" /* default */], 1240 | Unknown: __WEBPACK_IMPORTED_MODULE_5__Unknown__["a" /* default */], 1241 | Ignore: __WEBPACK_IMPORTED_MODULE_6__Ignore__["a" /* default */], 1242 | Img: __WEBPACK_IMPORTED_MODULE_7__Img__["a" /* default */], 1243 | Input: __WEBPACK_IMPORTED_MODULE_8__Input__["a" /* default */], 1244 | Text: __WEBPACK_IMPORTED_MODULE_9__Text__["a" /* default */], 1245 | Textarea: __WEBPACK_IMPORTED_MODULE_10__Textarea__["a" /* default */] 1246 | }); 1247 | 1248 | /***/ }), 1249 | /* 14 */ 1250 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1251 | 1252 | "use strict"; 1253 | /* harmony export (immutable) */ __webpack_exports__["a"] = A; 1254 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__common__ = __webpack_require__(0); 1255 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__Text__ = __webpack_require__(5); 1256 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 1257 | 1258 | 1259 | 1260 | 1261 | function A(layer, context) { 1262 | var node = layer.isText ? _extends({}, Object(__WEBPACK_IMPORTED_MODULE_1__Text__["a" /* default */])(layer, context)[0], { type: 'A' }) : { 1263 | type: 'A', 1264 | state: { 1265 | style: Object(__WEBPACK_IMPORTED_MODULE_0__common__["c" /* extractPositionStyle */])(layer) 1266 | } 1267 | }; 1268 | 1269 | var next = layer.isText ? [] : Object(__WEBPACK_IMPORTED_MODULE_0__common__["d" /* iteratorToArray */])(layer); 1270 | 1271 | return [node, next]; 1272 | } 1273 | 1274 | /***/ }), 1275 | /* 15 */ 1276 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1277 | 1278 | "use strict"; 1279 | /* harmony export (immutable) */ __webpack_exports__["a"] = Button; 1280 | function Button() {} 1281 | 1282 | /***/ }), 1283 | /* 16 */ 1284 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1285 | 1286 | "use strict"; 1287 | /* harmony export (immutable) */ __webpack_exports__["a"] = App; 1288 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__Group__ = __webpack_require__(6); 1289 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 1290 | 1291 | 1292 | 1293 | function App() { 1294 | var _Group = __WEBPACK_IMPORTED_MODULE_0__Group__["a" /* default */].apply(undefined, arguments), 1295 | _Group2 = _slicedToArray(_Group, 2), 1296 | result = _Group2[0], 1297 | next = _Group2[1]; 1298 | 1299 | return [Object.assign(result, { 1300 | type: 'App' 1301 | }), next]; 1302 | } 1303 | 1304 | /***/ }), 1305 | /* 17 */ 1306 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1307 | 1308 | "use strict"; 1309 | /* harmony export (immutable) */ __webpack_exports__["a"] = getDefaultState; 1310 | /* unused harmony export render */ 1311 | // import { createElement } from 'react' 1312 | 1313 | function getDefaultState() { 1314 | return { 1315 | style: {}, 1316 | center: false 1317 | }; 1318 | } 1319 | 1320 | function render(_ref) { 1321 | // const style = { 1322 | // ...state.style 1323 | // } 1324 | // 1325 | // if (state.center) { 1326 | // Object.assign(style, { 1327 | // position: 'relative', 1328 | // marginLeft: 'auto', 1329 | // marginRight: 'auto' 1330 | // }) 1331 | // } 1332 | // return
{children}
1333 | 1334 | var state = _ref.state, 1335 | children = _ref.children; 1336 | } 1337 | 1338 | /***/ }), 1339 | /* 18 */ 1340 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1341 | 1342 | "use strict"; 1343 | /* harmony export (immutable) */ __webpack_exports__["a"] = Case; 1344 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__common__ = __webpack_require__(0); 1345 | 1346 | 1347 | function Case(layer) { 1348 | var node = { 1349 | type: 'Case', 1350 | state: { 1351 | style: Object(__WEBPACK_IMPORTED_MODULE_0__common__["a" /* extractBoxRelatedStyle */])(layer) 1352 | } 1353 | }; 1354 | var next = []; 1355 | layer.iterate(function (sub) { 1356 | next.push(sub); 1357 | }); 1358 | 1359 | return [node, next]; 1360 | } 1361 | 1362 | /***/ }), 1363 | /* 19 */ 1364 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1365 | 1366 | "use strict"; 1367 | /* harmony export (immutable) */ __webpack_exports__["a"] = Unknown; 1368 | function Unknown(layer) { 1369 | return [{ 1370 | type: 'Unknown', 1371 | state: { 1372 | originName: String(layer.name) 1373 | } 1374 | }, []]; 1375 | } 1376 | 1377 | /***/ }), 1378 | /* 20 */ 1379 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1380 | 1381 | "use strict"; 1382 | /* harmony export (immutable) */ __webpack_exports__["a"] = Ignore; 1383 | function Ignore() { 1384 | return [{ 1385 | type: 'Ignore' 1386 | }, []]; 1387 | } 1388 | 1389 | /***/ }), 1390 | /* 21 */ 1391 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1392 | 1393 | "use strict"; 1394 | /* harmony export (immutable) */ __webpack_exports__["a"] = Img; 1395 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__common__ = __webpack_require__(0); 1396 | 1397 | 1398 | function Img(layer, _ref) { 1399 | var createImgRef = _ref.createImgRef; 1400 | 1401 | return [{ 1402 | type: 'Img', 1403 | state: { 1404 | src: createImgRef(layer), 1405 | style: Object(__WEBPACK_IMPORTED_MODULE_0__common__["a" /* extractBoxRelatedStyle */])(layer) 1406 | } 1407 | }, []]; 1408 | } 1409 | 1410 | /***/ }), 1411 | /* 22 */ 1412 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1413 | 1414 | "use strict"; 1415 | /* harmony export (immutable) */ __webpack_exports__["a"] = Input; 1416 | function Input() {} 1417 | 1418 | /***/ }), 1419 | /* 23 */ 1420 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1421 | 1422 | "use strict"; 1423 | /* harmony export (immutable) */ __webpack_exports__["a"] = Textarea; 1424 | function Textarea() {} 1425 | 1426 | /***/ }), 1427 | /* 24 */ 1428 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 1429 | 1430 | "use strict"; 1431 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return IMAGE_FOLDER; }); 1432 | /* harmony export (immutable) */ __webpack_exports__["b"] = createParserContext; 1433 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__common__ = __webpack_require__(0); 1434 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__common__ = __webpack_require__(3); 1435 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 1436 | 1437 | 1438 | 1439 | 1440 | function makeResourcePath(id, name) { 1441 | var idStr = String(id); 1442 | return '' + new Array(4 - idStr.length).fill('0').join('') + idStr + '_' + name; 1443 | } 1444 | 1445 | var IMAGE_FOLDER = 'image'; 1446 | 1447 | function createParserContext() { 1448 | var imageRefs = []; 1449 | var imgSrcId = 0; 1450 | var imgNameId = 0; 1451 | 1452 | function generateName() { 1453 | imgNameId += 1; 1454 | return String(imgNameId); 1455 | } 1456 | 1457 | return { 1458 | createImgRef: function createImgRef(layer) { 1459 | // TODO 生成真实的图片并且做重名检测 1460 | var options = _extends({ 1461 | scales: '2', 1462 | formats: 'png' 1463 | }, Object(__WEBPACK_IMPORTED_MODULE_1__common__["i" /* parseNameAndQuery */])(layer.name).query); 1464 | if (options.formats === 'base64') return Object(__WEBPACK_IMPORTED_MODULE_0__common__["e" /* layerToBase64 */])(layer); 1465 | imgSrcId += 1; 1466 | var name = makeResourcePath(imgSrcId, Object(__WEBPACK_IMPORTED_MODULE_1__common__["j" /* parseRawName */])(layer.name) || generateName()); 1467 | imageRefs.push({ id: layer.id, name: name, options: options }); 1468 | log('get iamge name ' + name); 1469 | log('returing image ref: ' + IMAGE_FOLDER + '/' + name + '.' + options.formats); 1470 | return IMAGE_FOLDER + '/' + name + '.' + options.formats; 1471 | }, 1472 | getImgRefs: function getImgRefs(handler) { 1473 | imageRefs.forEach(handler); 1474 | } 1475 | }; 1476 | } 1477 | 1478 | /***/ }) 1479 | /******/ ]); 1480 | 1481 | var openRunner = handlers.openRunner; 1482 | 1483 | var onSelectionChanged = handlers.onSelectionChanged; 1484 | 1485 | var exportCurrentLayer = handlers.exportCurrentLayer; 1486 | 1487 | var sendDataToRunner = handlers.sendDataToRunner; 1488 | 1489 | var undefined = handlers.undefined; -------------------------------------------------------------------------------- /examples/testNewBlade.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sskyy/blade/216145a80c6dc210e8a822df4069ba95258b8207/examples/testNewBlade.sketch -------------------------------------------------------------------------------- /examples/testNewBlade/config.js: -------------------------------------------------------------------------------- 1 | sketchBridge({ payload: {"type":"App","state":{"style":{"width":378,"height":608,"position":"absolute","left":0,"top":0},"center":false},"children":[{"type":"Text","state":{"text":"Cart","style":{"fontSize":20,"color":"rgba(0,0,0,1)","width":29,"height":23,"position":"absolute","left":59,"top":332,"align":0,"letterSpacing":"inherit","fontFamily":"KinoMT"}},"props":{}},{"type":"Case","state":{"style":{"width":271,"height":270,"position":"absolute","left":48,"top":46}},"children":[{"type":"Group","state":{"style":{"width":270,"height":269,"position":"absolute","left":0,"top":0},"center":false},"children":[{"type":"Img","state":{"src":"image/0001_icon.png","style":{"width":270,"height":269,"position":"absolute","left":0,"top":0}},"props":{}}],"props":{"caseName":"grey"}},{"type":"Group","state":{"style":{"width":271,"height":270,"position":"absolute","left":0,"top":0},"center":false},"children":[{"type":"Img","state":{"src":"image/0002_icon.png","style":{"width":271,"height":270,"position":"absolute","left":0,"top":0}},"props":{}}],"props":{"caseName":"red"}}],"props":{}},{"type":"A","state":{"text":"goto","style":{"fontSize":18,"color":"rgba(0,0,0,1)","width":36,"height":22,"position":"absolute","left":116,"top":332,"align":0,"letterSpacing":"inherit","fontFamily":"Helvetica"},"url":"baidu.com"},"props":{}},{"type":"A","state":{"style":{"position":"absolute","left":74,"top":418},"url":"taobao.com"},"children":[{"type":"Img","state":{"src":"image/0003_Rectangle.png","style":{"width":200,"height":91,"position":"absolute","left":0,"top":0}},"props":{}},{"type":"Text","state":{"text":"Url in Img","style":{"fontSize":18,"color":"rgba(0,0,0,1)","width":78,"height":22,"position":"absolute","left":51,"top":33,"align":0,"letterSpacing":"inherit","fontFamily":"Helvetica"}},"props":{}}],"props":{}}],"props":{}} }) -------------------------------------------------------------------------------- /examples/testNewBlade/image/0001_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sskyy/blade/216145a80c6dc210e8a822df4069ba95258b8207/examples/testNewBlade/image/0001_icon.png -------------------------------------------------------------------------------- /examples/testNewBlade/image/0002_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sskyy/blade/216145a80c6dc210e8a822df4069ba95258b8207/examples/testNewBlade/image/0002_icon.png -------------------------------------------------------------------------------- /examples/testNewBlade/image/0003_Rectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sskyy/blade/216145a80c6dc210e8a822df4069ba95258b8207/examples/testNewBlade/image/0003_Rectangle.png -------------------------------------------------------------------------------- /examples/testNewBlade/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Blade Output 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sketch-plugin-blade", 3 | "version": "0.0.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "copy-dir": { 8 | "version": "0.3.0", 9 | "resolved": "http://registry.npm.taobao.org/copy-dir/download/copy-dir-0.3.0.tgz", 10 | "integrity": "sha1-3rLcL6nJKQ7UfIQVWpmabUX1o1g=", 11 | "dev": true, 12 | "requires": { 13 | "mkdir-p": "0.0.7" 14 | } 15 | }, 16 | "mkdir-p": { 17 | "version": "0.0.7", 18 | "resolved": "http://registry.npm.taobao.org/mkdir-p/download/mkdir-p-0.0.7.tgz", 19 | "integrity": "sha1-JMXb4m2jqZ7xWKHu+aXC3Z3laDw=", 20 | "dev": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sketch-plugin-blade", 3 | "description": "A sketch plugin to generate prototype web pages.", 4 | "author": "sskyy ", 5 | "version": "0.0.4", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/sskyy/blade" 9 | }, 10 | "devDependencies": { 11 | "babel-core": "^6.26.0", 12 | "babel-eslint": "^7.2.3", 13 | "babel-loader": "^7.1.2", 14 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", 15 | "babel-plugin-transform-react-jsx": "^6.24.1", 16 | "babel-preset-es2015": "^6.24.1", 17 | "babel-preset-stage-0": "^6.24.1", 18 | "copy-dir": "^0.3.0", 19 | "eslint": "^4.5.0", 20 | "eslint-config-airbnb": "^15.1.0", 21 | "eslint-plugin-babel": "^4.1.2", 22 | "eslint-plugin-import": "^2.7.0", 23 | "eslint-plugin-jsx-a11y": "^6.0.2", 24 | "eslint-plugin-react": "^7.3.0", 25 | "webpack": "^3.5.5" 26 | }, 27 | "scripts": { 28 | "start": "cd src/plugin && webpack --env.dev --watch && cd ../..", 29 | "build": "cd src/plugin && webpack --env.production && cd ../.. && node ./scripts/attachHandler.js && node ./scripts/copyManifest.js", 30 | "bundle": "npm run build && node ./scripts/bundle.js && node ./scripts/copyManifest.js -p" 31 | }, 32 | "dependencies": { 33 | "babel-plugin-module-alias": "^1.6.0", 34 | "cocoascript-class": "^0.1.2", 35 | "commander": "^2.11.0", 36 | "rimraf": "^2.6.2" 37 | }, 38 | "sketch": { 39 | "name": "Blade", 40 | "identifier": "com.ariesate.blade", 41 | "authorEmail": "skyking_H@hotmail.com", 42 | "disableCocoaScriptPreprocessor": false, 43 | "commands": [ 44 | { 45 | "script": "plugin.js", 46 | "handler": "openRunner", 47 | "name": "Open Runner", 48 | "identifier": "openRunner" 49 | }, 50 | { 51 | "script": "plugin.js", 52 | "handler": "onSelectionChanged", 53 | "name": "Selection Changed", 54 | "identifier": "onSelectionChanged" 55 | }, 56 | { 57 | "script": "plugin.js", 58 | "handler": "exportCurrentLayer", 59 | "name": "Export Current Artboard", 60 | "identifier": "exportCurrentLayer" 61 | }, 62 | { 63 | "script": "plugin.js", 64 | "handler": "sendDataToRunner", 65 | "name": "Send Data To Runner", 66 | "identifier": "sendDataToRunner" 67 | }, 68 | { 69 | "script": "plugin.js", 70 | "handlers": { 71 | "actions": { 72 | "SelectionChanged.finish": "onSelectionChanged" 73 | } 74 | } 75 | } 76 | ], 77 | "menu": { 78 | "isRoot": 1, 79 | "items": [ 80 | { 81 | "title": "Blade", 82 | "items": [ 83 | "exportCurrentLayer", 84 | "openRunner", 85 | "sendDataToRunner" 86 | ] 87 | } 88 | ] 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /scripts/attachHandler.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const commands = require('../package.json').sketch.commands 4 | const file = path.join(__dirname, '../Contents/Sketch/plugin.js') 5 | 6 | commands.forEach(function (command) { 7 | var compiled = fs.readFileSync(file); 8 | compiled += "\n\nvar " + command.handler + " = handlers." + command.handler + ";"; 9 | fs.writeFileSync(file, compiled); 10 | }); 11 | -------------------------------------------------------------------------------- /scripts/bundle.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const copyDir = require('copy-dir') 4 | const rimraf = require('rimraf') 5 | 6 | const dest = path.join(__dirname, '../dist') 7 | if (fs.existsSync(dest)) { 8 | rimraf.sync(dest) 9 | } 10 | fs.mkdir(dest) 11 | const src = path.join(__dirname, '../Contents') 12 | 13 | copyDir.sync(src, `${dest}/blade.sketchplugin/Contents`) -------------------------------------------------------------------------------- /scripts/copyManifest.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | var program = require('commander'); 4 | const pkg = require('../package.json') 5 | 6 | const manifest = pkg.sketch 7 | 8 | program.option('-p, --prod', 'production mode').parse(process.argv) 9 | 10 | const finalManifest = Object.assign({}, manifest, { 11 | name: program.prod ? manifest.name : `${manifest.name}-Dev`, 12 | identifier: program.prod? manifest.identifier : `${manifest.identifier}.dev`, 13 | description: pkg.description, 14 | author: pkg.author.replace(/\s<.+>$/, ''), 15 | authorEmail: pkg.author.replace(/^[\w\s]+<(.+)>$/, '$1'), 16 | version: pkg.version, 17 | menu: program.prod? manifest.menu : { 18 | ...manifest.menu, 19 | items: [{ 20 | ...manifest.menu.items[0], 21 | title: `${manifest.menu.items[0].title}-Dev`, 22 | }] 23 | } 24 | }) 25 | 26 | const dest = program.prod ? 27 | path.join(__dirname, '../dist/blade.sketchplugin/Contents/Sketch/manifest.json'): 28 | path.join(__dirname, '../Contents/Sketch/manifest.json') 29 | 30 | console.log(`writing manifest into ${dest} with mode: ${program.prod}`) 31 | fs.writeFileSync(dest, JSON.stringify(finalManifest , null, 2)) 32 | -------------------------------------------------------------------------------- /src/plugin/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", {"modules": false}] 4 | ], 5 | "plugins": [ 6 | "transform-object-rest-spread", 7 | ["transform-react-jsx", { 8 | "pragma": "createElement" 9 | }], 10 | ["module-alias", [ 11 | { "src": "./utils", "expose": "utils" } 12 | ]] 13 | ] 14 | } -------------------------------------------------------------------------------- /src/plugin/.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | **/dist/** 3 | **/coverage/** 4 | **/lib/** 5 | -------------------------------------------------------------------------------- /src/plugin/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-airbnb" 4 | ], 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "mocha": true, 9 | "jest": true, 10 | "es6": true 11 | }, 12 | "parser": "babel-eslint", 13 | "parserOptions": { 14 | "ecmaVersion": 6, 15 | "ecmaFeatures": { 16 | "jsx": true, 17 | "experimentalObjectRestSpread": true 18 | } 19 | }, 20 | "globals": { 21 | "coscript": false, 22 | "MSDocument": false, 23 | "MSDocumentWindow": false, 24 | "MSPage": false, 25 | "MSSymbolInstance": false, 26 | "MSSymbolMaster": false, 27 | "MSTextLayer": false, 28 | "NSAlert": false, 29 | "NSApp": false, 30 | "NSClassFromString": false, 31 | "NSColor": false, 32 | "NSData": false, 33 | "NSDocument": false, 34 | "NSDocumentController": false, 35 | "NSFileManager": false, 36 | "NSImage": false, 37 | "NSJSONSerialization": false, 38 | "NSMakeRect": false, 39 | "NSMutableData": false, 40 | "NSMutableURLRequest": false, 41 | "NSSaveOperation": false, 42 | "NSString": false, 43 | "NSTextField": false, 44 | "NSTextView": false, 45 | "NSThread": false, 46 | "NSTitledWindowMask": false, 47 | "NSURL": false, 48 | "NSURLRequest": false, 49 | "NSUTF8StringEncoding": false, 50 | "NSUserDefaults": false, 51 | "NSView": false, 52 | "NSViewHeightSizable": false, 53 | "NSViewWidthSizable": false, 54 | "NSWindow": false, 55 | "NSWorkspace": false, 56 | "WKWebView": false, 57 | "WKWebViewConfiguration": false, 58 | "Mocha": false, 59 | "log": false, 60 | "NSBackingStoreBuffered": false, 61 | "NSPanel": false, 62 | "NSResizableWindowMask": false, 63 | "NSWindowStyleMaskClosable": false, 64 | "SPBWebViewMessageHandler": false, 65 | "SPBWebViewMessageUtils": false, 66 | "NSString": false, 67 | "NSTemporaryDirectory": false 68 | }, 69 | "plugins": [ 70 | "react", 71 | "babel" 72 | ], 73 | "rules": { 74 | "func-names": 0, 75 | "arrow-body-style": 0, 76 | "react/sort-comp": 0, 77 | "react/prop-types": 0, 78 | "react/jsx-first-prop-new-line": 0, 79 | "react/jsx-filename-extension": 0, 80 | "react/react-in-jsx-scope": 0, 81 | "import/no-unresolved": 0, 82 | "no-param-reassign": 0, 83 | "no-return-assign": 0, 84 | "max-len": 0, 85 | "consistent-return": 0, 86 | "no-redeclare": 0, 87 | "semi": [ 88 | 2, 89 | "never" 90 | ], 91 | "no-extra-semi": 2, 92 | "import/no-extraneous-dependencies": 0, 93 | "import/extensions": 0, 94 | "jsx-a11y/no-static-element-interactions": 0, 95 | "jsx-a11y/href-no-hash": 0, 96 | "react/no-find-dom-node": 0, 97 | "import/imports-first": 0, 98 | "react/no-string-refs": 0, 99 | "import/prefer-default-export": 0, 100 | "react/forbid-prop-types": 0, 101 | "no-plusplus": 0, 102 | "no-continue": 0, 103 | "no-underscore-dangle": 0, 104 | "no-template-curly-in-string": 0, 105 | "no-nested-ternary": 0, 106 | "no-useless-escape": 0, 107 | "no-unused-vars": [ 108 | "error", 109 | { 110 | "varsIgnorePattern": "(createElement|JSDOM)" 111 | } 112 | ] 113 | } 114 | } -------------------------------------------------------------------------------- /src/plugin/CLEANUP.md: -------------------------------------------------------------------------------- 1 | 2 | - [x] String 处理的地方 3 | - [ ] 错误处理 4 | - [ ] context.api 的问题 5 | - [x] controller/common 整理 6 | - [ ] webpack watch mode 即可搞定? 7 | -------------------------------------------------------------------------------- /src/plugin/common.js: -------------------------------------------------------------------------------- 1 | import * as WebViewUtils from 'utils/webview' 2 | 3 | export function parseNameAndQuery(inputName, getDefaultValues = () => ({ 4 | })) { 5 | const defaultValues = getDefaultValues() 6 | const result = { name: '', query: defaultValues, hash: {} } 7 | const name = String(inputName) 8 | if (name[0] !== '[') return result 9 | 10 | // name/query/hash 11 | let state = 'none' 12 | let stack = '' 13 | 14 | function clearLastStack() { 15 | if (state === 'name') { 16 | result.name = stack 17 | } else if (state === 'query') { 18 | stack.split('&').forEach((subStack) => { 19 | const [queryName, queryValue] = subStack.split('=') 20 | 21 | const defaultValue = defaultValues[queryName] 22 | result.query[queryName] = typeof defaultValue === 'boolean' ? 23 | Boolean(queryValue) : 24 | typeof defaultValue === 'number' ? 25 | Number(queryValue) : 26 | queryValue 27 | }) 28 | } else { 29 | // hash 30 | stack.split('&').forEach((subStack) => { 31 | const [queryName, queryValue] = subStack.split('=') 32 | 33 | const defaultValue = defaultValues[queryName] 34 | result.hash[queryName] = typeof defaultValue === 'boolean' ? 35 | Boolean(queryValue) : 36 | typeof defaultValue === 'number' ? 37 | Number(queryValue) : 38 | queryValue 39 | }) 40 | } 41 | stack = '' 42 | } 43 | 44 | for (let i = 0; i < name.length; i++) { 45 | const c = name[i] 46 | if (c === '[' && state === 'none') { 47 | state = 'name' 48 | } else if (c === '?' && (state === 'name' || state === 'hash')) { 49 | clearLastStack() 50 | state = 'query' 51 | } else if (c === '#' && (state === 'name' || state === 'query')) { 52 | clearLastStack() 53 | state = 'hash' 54 | } else if (c === ']' && (state === 'name' || state === 'query' || state === 'hash')) { 55 | clearLastStack() 56 | state = 'end' 57 | return result 58 | } else { 59 | stack += c 60 | } 61 | } 62 | 63 | throw new Error(`tag not closed: ${inputName}`) 64 | } 65 | 66 | export function sendCommandToPanel(path, command, argv) { 67 | WebViewUtils.sendPanelAction(path, command, argv) 68 | } 69 | 70 | export function sendCommandToWindow(path, command, argv) { 71 | WebViewUtils.sendWindowAction(path, command, argv) 72 | } 73 | 74 | export function showWindow(path) { 75 | // CAUTION use path as identifier 76 | WebViewUtils.openWindow(path, path) 77 | } 78 | 79 | export function showPanel(path) { 80 | WebViewUtils.showPanel(path, path) 81 | } 82 | 83 | export function hidePanel(path) { 84 | WebViewUtils.hidePanel(path, path) 85 | } 86 | 87 | export function recursiveParse(entry, parsers, context = {}) { 88 | const { name, query = {}, hash } = parseNameAndQuery(entry.name) 89 | let resolvedName = name 90 | if (resolvedName === '') { 91 | if (entry.isArtboard) { 92 | resolvedName = 'App' 93 | } else if (entry.isGroup) { 94 | resolvedName = 'Group' 95 | } else if (entry.isText) { 96 | resolvedName = 'Text' 97 | } else { 98 | // resolvedName = 'Unknown' 99 | resolvedName = 'Img' 100 | } 101 | } 102 | 103 | if (parsers[resolvedName] === undefined) { 104 | log(`unknown parser ${resolvedName}, entry: ${entry.name}, parsers: ${Object.keys(parsers).join(',')}`) 105 | throw new Error(`unknown parser ${resolvedName}`) 106 | } 107 | 108 | const [result, next] = parsers[resolvedName](entry, context) 109 | if (next && next.length !== 0) { 110 | result.children = next.map(child => recursiveParse(child, parsers, context)) 111 | } 112 | return Object.assign(result, { 113 | state: Object.assign(result.state || {}, query), 114 | props: Object.assign(result.props || {}, hash), 115 | }) 116 | } 117 | 118 | export function isWindowOpened(path) { 119 | return Boolean(WebViewUtils.findWebView(path)) 120 | } 121 | 122 | export function createFolder(path) { 123 | const manager = NSFileManager.defaultManager() 124 | manager.createDirectoryAtPath_withIntermediateDirectories_attributes_error(path, true, null, null) 125 | // [file_manager createDirectoryAtPath:[folders objectAtIndex:i] withIntermediateDirectories:true attributes:nil error:nil]; 126 | } 127 | 128 | export function getPluginFolderPath (context) { 129 | // Get absolute folder path of plugin 130 | let split = context.scriptPath.split('/'); 131 | split.splice(-3, 3); 132 | return split.join('/'); 133 | } 134 | 135 | export function getCurrentFilePath(context) { 136 | return context.document.fileURL().path().replace(/\.sketch$/, '') 137 | } 138 | 139 | export function isFileExist(source) { 140 | const manager = NSFileManager.defaultManager() 141 | return manager.fileExistsAtPath(source) 142 | } 143 | 144 | export function copyFile(source, target) { 145 | const manager = NSFileManager.defaultManager() 146 | if( !manager.fileExistsAtPath(source)) throw new Error(`file not exist ${source}`) 147 | // [file_manager copyItemAtPath:org toPath:tar error:nil]; 148 | manager.copyItemAtPath_toPath_error(source, target, null) 149 | } 150 | 151 | export function writeToFile(path, content) { 152 | const resultStr = NSString.stringWithFormat('%@', content) 153 | resultStr.writeToFile_atomically(path, true) 154 | } 155 | 156 | export function removeFile(path) { 157 | const manager = NSFileManager.defaultManager() 158 | // [file_manager removeItemAtPath:folder error:nil] 159 | manager.removeItemAtPath_error(path, null) 160 | } 161 | 162 | export function exportLayer(layer, targetFolder, name, options = {}) { 163 | const finalOptions = { 164 | ...options, 165 | 'use-id-for-name': true, 166 | scales: '3', 167 | formats: 'png', 168 | output: targetFolder, 169 | } 170 | 171 | const tmpFullPath = `${targetFolder}/${layer.id}@${finalOptions.scales}x.${finalOptions.formats}` 172 | layer.export(finalOptions) 173 | log(`generating image: ${tmpFullPath}`) 174 | const manager = NSFileManager.defaultManager() 175 | const targetPath = `${targetFolder}/${name}.${finalOptions.formats}` 176 | log(`renaming image to ${targetPath}` ) 177 | manager.moveItemAtURL_toURL_error( 178 | NSURL.fileURLWithPath(tmpFullPath), 179 | NSURL.fileURLWithPath(targetPath), 180 | null 181 | ) 182 | } 183 | 184 | 185 | export function parseRawName(inputName) { 186 | const name = String(inputName) 187 | return name.replace(/^\[.+\]/, '') 188 | } 189 | -------------------------------------------------------------------------------- /src/plugin/components/Group.js: -------------------------------------------------------------------------------- 1 | // import { createElement } from 'react' 2 | 3 | export function getDefaultState() { 4 | return { 5 | style: {}, 6 | center: false 7 | } 8 | } 9 | 10 | export function render({state, children}) { 11 | // const style = { 12 | // ...state.style 13 | // } 14 | // 15 | // if (state.center) { 16 | // Object.assign(style, { 17 | // position: 'relative', 18 | // marginLeft: 'auto', 19 | // marginRight: 'auto' 20 | // }) 21 | // } 22 | // return
{children}
23 | } 24 | -------------------------------------------------------------------------------- /src/plugin/index.js: -------------------------------------------------------------------------------- 1 | import { initWithContext, document } from 'utils/core' 2 | import * as WebViewUtils from 'utils/webview' 3 | import { 4 | parseNameAndQuery, 5 | sendCommandToWindow, 6 | sendCommandToPanel, 7 | showPanel, 8 | hidePanel, 9 | showWindow, 10 | recursiveParse, 11 | isWindowOpened, 12 | createFolder, 13 | getCurrentFilePath, 14 | getPluginFolderPath, 15 | isFileExist, 16 | copyFile, 17 | writeToFile, 18 | removeFile, 19 | exportLayer 20 | } from './common' 21 | import parsers from './parser' 22 | import createParserContext, { IMAGE_FOLDER } from './parser/createParserContext' 23 | 24 | 25 | const RUNNER_URL = 'http://127.0.0.1:8080/runner.html' 26 | const parserContext = createParserContext() 27 | 28 | export function openRunner() { 29 | showWindow(RUNNER_URL) 30 | } 31 | 32 | export function sendDataToRunner(context) { 33 | initWithContext(context) 34 | if (!context.api) return document.showMessage('error context.api!') 35 | if (!isWindowOpened(RUNNER_URL)) { 36 | return document.showMessage('please open runner first!') 37 | } 38 | 39 | let firstArtboard 40 | context.api().selectedDocument.selectedPage.iterate((page) => { 41 | if (!firstArtboard) firstArtboard = page 42 | }) 43 | 44 | if (!firstArtboard || !firstArtboard.isArtboard) return document.showMEssage('please select an artboard') 45 | // const resultStr = NSString.stringWithFormat('%@', JSON.stringify(recursiveParse(firstArtboard, parsers))) 46 | // resultStr.writeToFile_atomically(context.document.fileURL().path().replace(/\.sketch$/, '.json'), true) 47 | const result = recursiveParse(firstArtboard, parsers, parserContext) 48 | sendCommandToWindow(RUNNER_URL, 'config', result) 49 | return document.showMessage('done!') 50 | } 51 | 52 | export function exportCurrentLayer(context) { 53 | initWithContext(context) 54 | if (!context.api) return document.showMessage('error context.api!') 55 | if (!context.document.fileURL()) return document.showMessage('please save your file first!') 56 | 57 | const sketch = context.api() 58 | 59 | let firstArtboard 60 | sketch.selectedDocument.selectedPage.iterate((page) => { 61 | if (!firstArtboard) firstArtboard = page 62 | }) 63 | 64 | if (!firstArtboard || !firstArtboard.isArtboard) return document.showMessage('please select an artboard') 65 | 66 | const exportFolder = getCurrentFilePath(context) 67 | if (isFileExist(exportFolder)) removeFile(exportFolder) 68 | 69 | createFolder(exportFolder) 70 | const runnerPath = `${getPluginFolderPath(context)}/Contents/Resources/runner` 71 | copyFile(`${runnerPath}/index.js`, `${exportFolder}/index.js`) 72 | copyFile(`${runnerPath}/index.html`, `${exportFolder}/index.html`) 73 | // TODO add error message 74 | let result 75 | try { 76 | result = recursiveParse(firstArtboard, parsers, parserContext) 77 | } catch(e) { 78 | log(`parseError: ${e.message}`) 79 | return document.showMessage(`error: ${e.message}`) 80 | } 81 | 82 | writeToFile(`${exportFolder}/config.js`, `sketchBridge({ payload: ${JSON.stringify(result)} })`) 83 | 84 | // handle images 85 | const imageFolder = `${exportFolder}/${IMAGE_FOLDER}` 86 | createFolder(imageFolder) 87 | parserContext.getImgRefs(({id, name, options}) => { 88 | exportLayer(sketch.selectedDocument.layerWithID(id), imageFolder, name, options) 89 | }) 90 | 91 | return document.showMessage('done!') 92 | } 93 | 94 | export function onSelectionChanged(context) { 95 | initWithContext(context) 96 | const currentDocument = context.actionContext.document 97 | const selectedLayers = currentDocument.selectedLayers() 98 | 99 | if (!selectedLayers.containsLayers() || selectedLayers.containsMultipleLayers()) { 100 | return hidePanel() 101 | } 102 | 103 | 104 | const selectedLayer = selectedLayers.firstLayer() 105 | const directiveName = parseNameAndQuery(selectedLayer.name()).name 106 | 107 | if (directiveName == null) { 108 | return hidePanel() 109 | } 110 | 111 | const command = context.command 112 | const lastProps = command.valueForKey_onLayer('props', selectedLayer) 113 | const finalProps = lastProps == null ? {} : lastProps 114 | 115 | showPanel() 116 | sendCommandToPanel('showProps', finalProps) 117 | } 118 | 119 | // for debug 120 | export function testSendAction(context) { 121 | initWithContext(context) 122 | WebViewUtils.sendAction('aaa', { value: true }) 123 | } 124 | 125 | export function parseLayer(context) { 126 | initWithContext(context) 127 | let first 128 | context.api().selectedDocument.selectedLayers.iterate((layer) => { 129 | if (!first) first = layer 130 | }) 131 | const result = NSString.stringWithFormat('%@', JSON.stringify(parsers.Group(first))) 132 | 133 | result.writeToFile_atomically(context.document.fileURL().path().replace(/\.sketch$/, '.json'), true) 134 | document.showMessage('done!') 135 | } 136 | -------------------------------------------------------------------------------- /src/plugin/parser/A.js: -------------------------------------------------------------------------------- 1 | import { extractPositionStyle, iteratorToArray } from './common' 2 | import Text from './Text' 3 | 4 | export default function A(layer, context) { 5 | const node = layer.isText ? 6 | { ...Text(layer, context)[0], type: 'A' }: 7 | { 8 | type: 'A', 9 | state: { 10 | style: extractPositionStyle(layer) 11 | } 12 | } 13 | 14 | const next = layer.isText ? [] : iteratorToArray(layer) 15 | 16 | return [node, next] 17 | } 18 | -------------------------------------------------------------------------------- /src/plugin/parser/App.js: -------------------------------------------------------------------------------- 1 | import Group from './Group' 2 | 3 | export default function App(...argv) { 4 | const [result, next ]= Group(...argv) 5 | return [Object.assign(result, { 6 | type: 'App', 7 | }), next] 8 | } 9 | -------------------------------------------------------------------------------- /src/plugin/parser/Button.js: -------------------------------------------------------------------------------- 1 | export default function Button() {} 2 | -------------------------------------------------------------------------------- /src/plugin/parser/Case.js: -------------------------------------------------------------------------------- 1 | import { extractBoxRelatedStyle } from './common' 2 | 3 | export default function Case(layer) { 4 | const node = { 5 | type: 'Case', 6 | state: { 7 | style: extractBoxRelatedStyle(layer), 8 | }, 9 | } 10 | const next = [] 11 | layer.iterate((sub) => { 12 | next.push(sub) 13 | }) 14 | 15 | return [node, next] 16 | } 17 | -------------------------------------------------------------------------------- /src/plugin/parser/Group.js: -------------------------------------------------------------------------------- 1 | import { parseNameAndQuery } from '../common' 2 | import { extractBoxRelatedStyle, extractEffectStyle } from './common' 3 | import { getDefaultState } from '../components/Group' 4 | 5 | const BG_NAME = 'Bg' 6 | 7 | export default function Group(group, { createImgRef }) { 8 | const next = [] 9 | let bgLayer = null 10 | group.iterate((layer) => { 11 | // TODO 从节点上读数据 12 | if (parseNameAndQuery(layer.name).name === BG_NAME) { 13 | bgLayer = layer 14 | } else { 15 | next.push(layer) 16 | } 17 | }) 18 | 19 | const { query } = parseNameAndQuery(group.name, getDefaultState) 20 | const node = { 21 | type: 'Group', 22 | state: Object.assign(query, { style: extractBoxRelatedStyle(group) }), 23 | } 24 | 25 | if (bgLayer) { 26 | if (bgLayer.isImage || bgLayer.isGroup) { 27 | node.state.style.background = createImgRef(bgLayer) 28 | } else { 29 | Object.assign(node.state.style, extractEffectStyle(bgLayer)) 30 | } 31 | } 32 | 33 | return [node, next] 34 | } 35 | -------------------------------------------------------------------------------- /src/plugin/parser/Ignore.js: -------------------------------------------------------------------------------- 1 | export default function Ignore() { 2 | return [{ 3 | type: 'Ignore', 4 | }, []] 5 | } 6 | -------------------------------------------------------------------------------- /src/plugin/parser/Img.js: -------------------------------------------------------------------------------- 1 | import { extractBoxRelatedStyle } from './common' 2 | 3 | export default function Img(layer, { createImgRef }) { 4 | return [{ 5 | type: 'Img', 6 | state: { 7 | src: createImgRef(layer), 8 | style: extractBoxRelatedStyle(layer), 9 | }, 10 | }, []] 11 | } 12 | -------------------------------------------------------------------------------- /src/plugin/parser/Input.js: -------------------------------------------------------------------------------- 1 | export default function Input(){ 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/plugin/parser/Text.js: -------------------------------------------------------------------------------- 1 | import { toCSSRGBA, extractBoxRelatedStyle } from './common' 2 | 3 | export default function Text(layer) { 4 | const node = { 5 | type: 'Text', 6 | state: { 7 | text: String(layer.sketchObject.stringValue()), 8 | style: { 9 | fontSize: layer.sketchObject.fontSize(), 10 | color: toCSSRGBA(layer.sketchObject.textColor()), 11 | ...extractBoxRelatedStyle(layer), 12 | // TODO align 翻译 13 | align: layer.sketchObject.textAlignment(), 14 | // TODO line spacing 翻译成 line height 15 | // lineHeight: layer.sketchObject.lineSpacing(), 16 | letterSpacing: layer.sketchObject.characterSpacing() || 'inherit', 17 | fontFamily: String(layer.sketchObject.fontPostscriptName()), 18 | }, 19 | }, 20 | } 21 | 22 | return [node, []] 23 | } 24 | -------------------------------------------------------------------------------- /src/plugin/parser/Textarea.js: -------------------------------------------------------------------------------- 1 | export default function Textarea(){ 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/plugin/parser/Unknown.js: -------------------------------------------------------------------------------- 1 | export default function Unknown(layer) { 2 | return [{ 3 | type: 'Unknown', 4 | state: { 5 | originName: String(layer.name), 6 | }, 7 | }, []] 8 | } 9 | -------------------------------------------------------------------------------- /src/plugin/parser/common.js: -------------------------------------------------------------------------------- 1 | function map(arr, handler) { 2 | const result = [] 3 | for (let i = 0; i < arr.count(); i++) { 4 | result.push(handler(arr[i])) 5 | } 6 | return result 7 | } 8 | 9 | export function toCSSRGBA(RGBAStr) { 10 | return `rgba(${String(RGBAStr).replace(/[\(\)]/g, '').split(' ').map((v) => { 11 | const [type, value] = v.split(':') 12 | if (type !== 'a') { 13 | return Math.round(Number(value) * 256) 14 | } 15 | return Number(value) 16 | }) 17 | .join(',')})` 18 | } 19 | 20 | function makeShadowCSS(shadow, inset) { 21 | return `${inset ? 'inset ' : ''}${shadow.offsetX()}px ${shadow.offsetY()}px ${shadow.blurRadius()}px ${shadow.spread()}px ${toCSSRGBA(shadow.color())}` 22 | } 23 | 24 | export function extractEffectStyle(layer) { 25 | const result = {} 26 | const fills = layer.sketchObject.style().fills() 27 | const borders = layer.sketchObject.style().borders() 28 | const shadows = layer.sketchObject.style().shadows() 29 | const innerShadows = layer.sketchObject.style().innerShadows() 30 | if (fills.count() > 0) { 31 | Object.assign(result, { 32 | background: map(fills, (fill) => { 33 | return toCSSRGBA(fill.color()) 34 | }).join(','), 35 | }) 36 | } 37 | 38 | if (borders.count() > 0) { 39 | const firstBorder = borders[0] 40 | Object.assign(result, { 41 | border: `${firstBorder.thickness()}px solid ${toCSSRGBA(firstBorder.color())}`, 42 | }) 43 | } 44 | 45 | if ((shadows.count() + innerShadows.count()) > 0) { 46 | const totalShadows = map(shadows, makeShadowCSS).concat(map(innerShadows, s => makeShadowCSS(s, true))) 47 | 48 | Object.assign(result, { 49 | boxShadow: totalShadows.join(','), 50 | }) 51 | } 52 | 53 | return result 54 | } 55 | 56 | export function extractBoxStyle(layer) { 57 | return { 58 | width: layer.frame.width, 59 | height: layer.frame.height, 60 | } 61 | } 62 | 63 | export function extractPositionStyle(layer) { 64 | return { 65 | position: 'absolute', 66 | left: layer.sketchObject.absoluteRect().rulerX() - layer.container.sketchObject.absoluteRect().rulerX(), 67 | top: layer.sketchObject.absoluteRect().rulerY() - layer.container.sketchObject.absoluteRect().rulerY(), 68 | } 69 | } 70 | export function extractBoxRelatedStyle(layer) { 71 | return Object.assign(extractBoxStyle(layer), extractPositionStyle(layer)) 72 | } 73 | 74 | export function extractStyle(layer) { 75 | return Object.assign( 76 | extractBoxRelatedStyle(layer), 77 | extractEffectStyle(layer), 78 | ) 79 | } 80 | 81 | export function layerToBase64(layer, options = {}) { 82 | const fileFolder = NSTemporaryDirectory() 83 | 84 | const finalOptions = { 85 | ...options, 86 | 'use-id-for-name': true, 87 | scales: '3', 88 | formats: 'png', 89 | output: fileFolder, 90 | } 91 | 92 | const fullPath = `${fileFolder}/${layer.id}@${finalOptions.scales}x.png` 93 | 94 | layer.export(finalOptions) 95 | 96 | const url = NSURL.fileURLWithPath(fullPath) 97 | const data = NSData.alloc().initWithContentsOfURL(url) 98 | const base64 = data.base64EncodedStringWithOptions(0) 99 | 100 | NSFileManager.defaultManager().removeItemAtURL_error(url, null) 101 | return `data:image/png;base64,${base64}` 102 | } 103 | 104 | export function iteratorToArray(iter) { 105 | const result = [] 106 | iter.iterate((item) => result.push(item)) 107 | return result 108 | } 109 | -------------------------------------------------------------------------------- /src/plugin/parser/createParserContext.js: -------------------------------------------------------------------------------- 1 | import { layerToBase64 } from './common' 2 | import { parseNameAndQuery, parseRawName } from '../common' 3 | 4 | function makeResourcePath(id, name) { 5 | const idStr = String(id) 6 | return `${(new Array(4-idStr.length)).fill('0').join('')}${idStr}_${name}` 7 | } 8 | 9 | export const IMAGE_FOLDER = 'image' 10 | 11 | 12 | 13 | export default function createParserContext() { 14 | const imageRefs = [] 15 | let imgSrcId = 0 16 | let imgNameId = 0 17 | 18 | function generateName() { 19 | imgNameId += 1 20 | return String(imgNameId) 21 | } 22 | 23 | return { 24 | createImgRef(layer) { 25 | // TODO 生成真实的图片并且做重名检测 26 | const options = { 27 | scales: '2', 28 | formats: 'png', 29 | ...parseNameAndQuery(layer.name).query 30 | } 31 | if (options.formats === 'base64') return layerToBase64(layer) 32 | imgSrcId += 1 33 | const name = makeResourcePath(imgSrcId, parseRawName(layer.name) || generateName()) 34 | imageRefs.push({ id: layer.id, name, options }) 35 | log(`get iamge name ${name}`) 36 | log(`returing image ref: ${IMAGE_FOLDER}/${name}.${options.formats}`) 37 | return `${IMAGE_FOLDER}/${name}.${options.formats}` 38 | }, 39 | getImgRefs(handler) { 40 | imageRefs.forEach(handler) 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/plugin/parser/index.js: -------------------------------------------------------------------------------- 1 | import A from './A' 2 | import Button from './Button' 3 | import App from './App' 4 | import Case from './Case' 5 | import Group from './Group' 6 | import Unknown from './Unknown' 7 | import Ignore from './Ignore' 8 | import Img from './Img' 9 | import Input from './Input' 10 | import Text from './Text' 11 | import Textarea from './Textarea' 12 | 13 | export default { 14 | A, 15 | Button, 16 | Case, 17 | Group, 18 | App, 19 | Unknown, 20 | Ignore, 21 | Img, 22 | Input, 23 | Text, 24 | Textarea 25 | } 26 | -------------------------------------------------------------------------------- /src/plugin/utils/core.js: -------------------------------------------------------------------------------- 1 | let context = null; 2 | let document = null; 3 | let selection = null; 4 | let sketch = null; 5 | 6 | let pluginFolderPath = null; 7 | let frameworkFolderPath = '/Contents/Resources/frameworks/'; 8 | 9 | function getPluginFolderPath () { 10 | // Get absolute folder path of plugin 11 | let split = context.scriptPath.split('/'); 12 | split.splice(-3, 3); 13 | return split.join('/'); 14 | } 15 | 16 | export function initWithContext (ctx) { 17 | // This function needs to be called in the beginning of every entry point! 18 | // Set all env variables according to current context 19 | context = ctx; 20 | document = ctx.document 21 | || ctx.actionContext.document 22 | || MSDocument.currentDocument(); 23 | selection = document ? document.selectedLayers() : null; 24 | pluginFolderPath = getPluginFolderPath(); 25 | 26 | // Here you could load custom cocoa frameworks if you need to 27 | // loadFramework('FrameworkName', 'ClassName'); 28 | // => would be loaded into ClassName in global namespace! 29 | } 30 | 31 | export function loadFramework (frameworkName, frameworkClass) { 32 | // Only load framework if class not already available 33 | if (Mocha && NSClassFromString(frameworkClass) == null) { 34 | const frameworkDir = `${pluginFolderPath}${frameworkFolderPath}`; 35 | const mocha = Mocha.sharedRuntime(); 36 | return mocha.loadFrameworkWithName_inDirectory(frameworkName, frameworkDir); 37 | } 38 | return false; 39 | } 40 | 41 | export { 42 | context, 43 | document, 44 | selection, 45 | sketch, 46 | pluginFolderPath 47 | }; 48 | -------------------------------------------------------------------------------- /src/plugin/utils/formatter.js: -------------------------------------------------------------------------------- 1 | export function toArray (object) { 2 | if (Array.isArray(object)) { 3 | return object; 4 | } 5 | let arr = []; 6 | for (let j = 0; j < object.count(); j++) { 7 | arr.push(object.objectAtIndex(j)); 8 | } 9 | return arr; 10 | } 11 | -------------------------------------------------------------------------------- /src/plugin/utils/webview/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | windowIdentifier, 3 | panelIdentifier, 4 | getFilePath, 5 | createWebView, 6 | sendAction as sendActionToWebView, 7 | receiveAction 8 | } from './webview'; 9 | 10 | import { 11 | open as openWindow, 12 | sendAction as sendWindowAction, 13 | findWebView 14 | } from './window'; 15 | 16 | import { 17 | toggle as togglePanel, 18 | open as openPanel, 19 | close as closePanel, 20 | isOpen as isPanelOpen, 21 | show as showPanel, 22 | hide as hidePanel, 23 | sendAction as sendPanelAction 24 | } from './panel'; 25 | 26 | export { 27 | windowIdentifier, 28 | panelIdentifier, 29 | getFilePath, 30 | createWebView, 31 | sendActionToWebView, 32 | receiveAction, 33 | 34 | openWindow, 35 | sendWindowAction, 36 | findWebView, 37 | 38 | togglePanel, 39 | openPanel, 40 | closePanel, 41 | showPanel, 42 | hidePanel, 43 | isPanelOpen, 44 | sendPanelAction 45 | }; 46 | -------------------------------------------------------------------------------- /src/plugin/utils/webview/panel.js: -------------------------------------------------------------------------------- 1 | import { document } from 'utils/core'; 2 | import { toArray } from 'utils/formatter'; 3 | import { createWebView, sendAction as sendActionToWebView } from './webview'; 4 | 5 | export function toggle (identifier, path, width) { 6 | if (isOpen(identifier)) { 7 | close(identifier); 8 | } else { 9 | open(identifier, path, width); 10 | } 11 | } 12 | 13 | export function open (identifier, path = 'index.html', options = {}) { 14 | const { width } = options; 15 | const frame = NSMakeRect(0, 0, width || 250, 600); // the height doesn't really matter here 16 | const contentView = document.documentWindow().contentView(); 17 | if (!contentView || isOpen()) { 18 | return false; 19 | } 20 | 21 | const stageView = contentView.subviews().objectAtIndex(0); 22 | let webView = createWebView(path, frame); 23 | webView.identifier = identifier; 24 | 25 | // Inject our webview into the right spot in the subview list 26 | const views = stageView.subviews(); 27 | let finalViews = []; 28 | let pushedWebView = false; 29 | for (let i = 0; i < views.count(); i++) { 30 | const view = views.objectAtIndex(i); 31 | finalViews.push(view); 32 | // NOTE: change the view identifier here if you want to add 33 | // your panel anywhere else 34 | if (!pushedWebView && view.identifier() == 'view_canvas') { 35 | finalViews.push(webView); 36 | pushedWebView = true; 37 | } 38 | } 39 | // If it hasn't been pushed yet, push our web view 40 | // E.g. when inspector is not activated etc. 41 | if (!pushedWebView) { 42 | finalViews.push(webView); 43 | } 44 | // Finally, update the subviews prop and refresh 45 | stageView.subviews = finalViews; 46 | stageView.adjustSubviews(); 47 | } 48 | 49 | 50 | export function show(identifier) { 51 | const viewToShow = findWebView(identifier) 52 | 53 | if (viewToShow == undefined) { 54 | return open(identifier) 55 | } 56 | 57 | const contentView = document.documentWindow().contentView(); 58 | if (!contentView) { 59 | return false; 60 | } 61 | const stageView = contentView.subviews().objectAtIndex(0); 62 | 63 | viewToShow.hidden = false 64 | stageView.adjustSubviews(); 65 | } 66 | 67 | export function close (identifier) { 68 | const contentView = document.documentWindow().contentView(); 69 | if (!contentView) { 70 | return false; 71 | } 72 | 73 | const stageView = contentView.subviews().objectAtIndex(0); 74 | stageView.subviews = toArray(stageView.subviews()).filter(view => { 75 | return view.identifier() != identifier; 76 | }); 77 | stageView.adjustSubviews(); 78 | } 79 | 80 | export function isOpen (identifier) { 81 | return !!findWebView(identifier); 82 | } 83 | 84 | export function findWebView (identifier) { 85 | const contentView = document.documentWindow().contentView(); 86 | if (!contentView) { 87 | return false; 88 | } 89 | const splitView = contentView.subviews().objectAtIndex(0); 90 | const views = toArray(splitView.subviews()); 91 | return views.find(view => view.identifier() == identifier); 92 | } 93 | 94 | export function sendAction (identifier, name, payload = {}) { 95 | return sendActionToWebView(findWebView(identifier), name, payload ); 96 | } 97 | 98 | export function hide(identifier) { 99 | const contentView = document.documentWindow().contentView(); 100 | if (!contentView) { 101 | return false; 102 | } 103 | // Search for web view panel 104 | const stageView = contentView.subviews().objectAtIndex(0); 105 | const viewToHide = findWebView(identifier) 106 | 107 | if (viewToHide == undefined) return 108 | 109 | viewToHide.hidden = true 110 | stageView.adjustSubviews(); 111 | } 112 | -------------------------------------------------------------------------------- /src/plugin/utils/webview/webview.js: -------------------------------------------------------------------------------- 1 | import { pluginFolderPath, document, context } from 'utils/core'; 2 | import ObjCClass from 'cocoascript-class'; 3 | 4 | // These are just used to identify the window(s) 5 | // Change them to whatever you need e.g. if you need to support multiple 6 | // windows at the same time... 7 | let windowIdentifier = 'sketch-plugin-boilerplate--window'; 8 | let panelIdentifier = 'sketch-plugin-boilerplate--panel'; 9 | 10 | // Since we now create the delegate in js, we need the enviroment 11 | // to stick around for as long as we need a reference to that delegate 12 | coscript.setShouldKeepAround(true); 13 | 14 | // This is a helper delegate, that handles incoming bridge messages 15 | export const BridgeMessageHandler = new ObjCClass({ 16 | 'userContentController:didReceiveScriptMessage:': function (controller, message) { 17 | try { 18 | const bridgeMessage = JSON.parse(String(message.body())); 19 | receiveAction(bridgeMessage.name, bridgeMessage.data); 20 | } catch (e) { 21 | log('Could not parse bridge message'); 22 | log(e.message); 23 | } 24 | } 25 | }); 26 | 27 | log('BridgeMessageHandler'); 28 | log(BridgeMessageHandler); 29 | log(BridgeMessageHandler.userContentController_didReceiveScriptMessage); 30 | 31 | export function initBridgedWebView (frame, bridgeName = 'SketchBridge') { 32 | const config = WKWebViewConfiguration.alloc().init(); 33 | const messageHandler = BridgeMessageHandler.alloc().init(); 34 | config.userContentController().addScriptMessageHandler_name(messageHandler, bridgeName); 35 | config.preferences().setValue_forKey(true, 'developerExtrasEnabled') 36 | return WKWebView.alloc().initWithFrame_configuration(frame, config); 37 | } 38 | 39 | export function getFilePath (file) { 40 | return `${pluginFolderPath}/Contents/Resources/webview/${file}`; 41 | } 42 | 43 | export function createWebView (path, frame) { 44 | const webView = initBridgedWebView(frame, 'Sketch'); 45 | const url = path.slice(0, 4) === 'http' ? NSURL.URLWithString(path) : NSURL.fileURLWithPath(getFilePath(path)); 46 | log('File URL'); 47 | log(url); 48 | 49 | webView.setAutoresizingMask(NSViewWidthSizable | NSViewHeightSizable); 50 | webView.loadRequest(NSURLRequest.requestWithURL(url)); 51 | 52 | return webView; 53 | } 54 | 55 | export function sendAction (webView, name, payload = {}) { 56 | if (!webView || !webView.evaluateJavaScript_completionHandler) { 57 | return; 58 | } 59 | // `sketchBridge` is the JS function exposed on window in the webview! 60 | const script = `sketchBridge(${JSON.stringify({name, payload})});`; 61 | webView.evaluateJavaScript_completionHandler(script, null); 62 | } 63 | 64 | export function receiveAction (name, payload = {}) { 65 | document.showMessage('I received a message! 😊🎉🎉'); 66 | } 67 | 68 | export { 69 | windowIdentifier, 70 | panelIdentifier 71 | }; 72 | -------------------------------------------------------------------------------- /src/plugin/utils/webview/window.js: -------------------------------------------------------------------------------- 1 | import { 2 | createWebView, 3 | sendAction as sendActionToWebView 4 | } from './webview'; 5 | 6 | export function open (identifier, path = 'index.html', options = {}) { 7 | // Sensible defaults for options 8 | const { 9 | width = 800, 10 | height = 350, 11 | title = 'Sketch Plugin Boilerplate' 12 | } = options; 13 | 14 | const frame = NSMakeRect(0, 0, width, height); 15 | const masks = NSTitledWindowMask | 16 | NSWindowStyleMaskClosable | 17 | NSResizableWindowMask; 18 | const window = NSPanel.alloc().initWithContentRect_styleMask_backing_defer(frame, masks, NSBackingStoreBuffered, false); 19 | window.setMinSize({width: 200, height: 200}); 20 | 21 | // We use this dictionary to have a persistant storage of our NSWindow/NSPanel instance 22 | // Otherwise the instance is stored nowhere and gets release => Window closes 23 | let threadDictionary = NSThread.mainThread().threadDictionary(); 24 | threadDictionary[identifier] = window; 25 | 26 | const webView = createWebView(path, frame); 27 | 28 | window.title = title; 29 | window.center(); 30 | window.contentView().addSubview(webView); 31 | 32 | window.makeKeyAndOrderFront(null); 33 | } 34 | 35 | export function findWebView (identifier) { 36 | let threadDictionary = NSThread.mainThread().threadDictionary(); 37 | const window = threadDictionary[identifier]; 38 | return window && window.contentView().subviews()[0]; 39 | } 40 | 41 | export function sendAction (identifier, name, payload = {}) { 42 | return sendActionToWebView(findWebView(identifier), name, payload); 43 | } 44 | -------------------------------------------------------------------------------- /src/plugin/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: './index', 5 | output: { 6 | filename: 'plugin.js', 7 | path: path.resolve(__dirname, '../../Contents/Sketch'), 8 | library: 'handlers', 9 | libraryTarget: 'var', 10 | }, 11 | module: { 12 | rules: [ 13 | { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader' }, 14 | ] 15 | } 16 | } 17 | --------------------------------------------------------------------------------