├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── README.md ├── assets ├── demo.sketch ├── index.css ├── index.html ├── index.js ├── jQuery.js └── normalize.css ├── bin └── index.js ├── package-lock.json ├── package.json ├── src ├── deps │ └── decodeUtils.js ├── generateImages.js ├── generateMeasurePage.js ├── index.js ├── parseSketchFile.js ├── parseText.js ├── parseText.v50.js ├── transform.js └── utils.js └── test ├── parseSketchFile.spec.js ├── transform.spec.js └── util.spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore deps dir. 2 | src/deps/* 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | extends: standard 2 | env: 3 | node: true 4 | rules: 5 | arrow-parens: 6 | - 2 7 | - "as-needed" 8 | indent: 9 | - "error" 10 | - 2 11 | - SwitchCase: 1 12 | brace-style: 13 | - "off" 14 | - "stroustrup" 15 | - allowSingleLine: true 16 | operator-linebreak: 17 | - 2 18 | - "before" 19 | no-control-regex: off 20 | space-before-function-paren: 21 | - "error" 22 | - "never" 23 | globals: 24 | describe: true 25 | it: true 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .data/ 3 | 4 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "9" 5 | - "8" 6 | before_script: 7 | - npm run lint 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sketch-messure-cli 2 | 3 | [![Build Status](https://travis-ci.org/devsigners/sketch-measure-cli.svg?branch=master)](https://travis-ci.org/devsigners/sketch-measure-cli) 4 | [![npm version](https://badge.fury.io/js/sketch-measure-cli.svg)](https://badge.fury.io/js/sketch-measure-cli) 5 | [![GitHub stars](https://img.shields.io/github/stars/devsigners/sketch-measure-cli.svg)](https://github.com/devsigners/sketch-measure-cli/stargazers) 6 | 7 | [sketch-measure](https://github.com/utom/sketch-measure) is a great plugin for sketch. Sometimes I want to embed it to workflow with cli, and it's really hard. 8 | Neither `sketchtool` nor [`coscript`](https://github.com/marekhrabe/coscript) give the full power to process skech files with sketch-measure. 9 | 10 | And finally I write *sketch-messure-cli* to help to use sketch-measure with cli. **Surely it's not exactly the same as sketch-measure plugin.** 11 | 12 | ## Installation & Usage 13 | 14 | ```bash 15 | npm i -g sketch-measure-cli 16 | ``` 17 | 18 | and then: 19 | 20 | ```bash 21 | sketch-measure convert demo.sketch -d destDir 22 | ``` 23 | 24 | *If you don't set dest dir, the tool will use `working-dir/sketch-file-name` instead.* 25 | 26 | So you can view measure pages in `destDir` 27 | 28 | 29 | ## Attention 30 | 31 | ### Text attributes transform 32 | 33 | Most attributes of text is encoded as base64 string, and is not ease to parse it. Current only the first part of text's style info is used, others are dropped because cannot get the position info. 34 | 35 | ### Symbols 36 | 37 | Symbol artboards are removed because we cannot get preview image of every single symbol. 38 | 39 | 40 | ## Credits 41 | 42 | - [sketch-measure](https://github.com/utom/sketch-measure) to learn how sketch-measure works. 43 | - [react-sketch-viewer](https://github.com/FourwingsY/react-sketch-viewer) to parse text arributes. 44 | -------------------------------------------------------------------------------- /assets/demo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsigners/sketch-measure-cli/07df92e9a7bc8b9601986adae1fe03570dfe6e1d/assets/demo.sketch -------------------------------------------------------------------------------- /assets/index.css: -------------------------------------------------------------------------------- 1 | *{box-sizing:border-box;} 2 | ::-webkit-scrollbar{width:8px;height:8px;background:#202123;border-radius:4px;} 3 | ::-webkit-scrollbar-thumb{border:1px solid rgba(255,255,255,.16);background-color:rgba(0,0,0,.64);border-radius:4px;} 4 | ::-webkit-scrollbar-corner, 5 | ::-webkit-resizer{background:#202123;} 6 | html{width:100vw;height:100vh;overflow:hidden;} 7 | body{-webkit-user-select:none;width:100vw;height:100vh;min-width:1024px;font-family:HelveticaNeue,Helvetica,Arial,sans-serif;font-size:12px;color:#989A9C;background:#191919;letter-spacing:1px;overflow:hidden;} 8 | header{position:relative;height:60px;display:flex;align-items:center;z-index:215;background:#000;} 9 | header .header-center,header .header-left,header .header-right{width:240px;height:60px;background:#0F0F0F;} 10 | header .header-center{display:flex;flex:1;background:#000;} 11 | header .header-center .show-notes,header .header-center .zoom-widget{padding:20px;width:160px;} 12 | header .header-center h1{flex:1;} 13 | header .header-center .show-notes{text-align:right;line-height:20px;} 14 | header .header-center .show-notes .slidebox{margin-left:10px;} 15 | header .header-left{display:flex;} 16 | .zoom-widget{display:flex;} 17 | .zoom-widget button{position:relative;display:block;width:20px;height:20px;border-radius:10px;border:0;background:#3484F5;cursor:pointer;} 18 | .zoom-widget button:after,.zoom-widget button:before{position:absolute;top:9px;left:5px;content:"";display:block;width:10px;height:2px;background:#FFF;} 19 | .zoom-widget button:after{top:5px;left:9px;width:2px;height:10px;} 20 | .zoom-widget button:disabled{background:#222;cursor:default;} 21 | .zoom-widget button:disabled:after,.zoom-widget button:disabled:before{background:#606264;} 22 | .zoom-widget button.zoom-in:after{display:none;} 23 | .zoom-widget .zoom-text{display:block;text-align:center;line-height:20px;flex:1;} 24 | .unit-box{height:60px;} 25 | .unit-box .overlay{content:"";position:absolute;top:60px;left:0;width:100vw;height:calc(100vh - 60px);display:none;background:rgba(0,0,0,.56);z-index:1;} 26 | .unit-box:focus .overlay{display:block;} 27 | .unit-box:focus{height:auto;outline:0;} 28 | .unit-box h3,.unit-box p{position:relative;margin:0;padding:20px 20px 20px 54px;display:block;height:100%;line-height:20px;font-size:12px;font-weight:400;cursor:pointer;} 29 | .unit-box h3,.unit-box:focus p{display:none;} 30 | .unit-box h3:before,.unit-box p:before{position:absolute;top:18px;left:18px;content:"";display:block;width:24px;height:24px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwAgMAAAAqbBEUAAAADFBMVEUAAABhY2VgZGdgYmRubxBCAAAAA3RSTlMA5EpWbVq4AAAALUlEQVQoz2OgOuD8DwTfoBx2EOcvlMP/HwSwcsDEKIeKHETwEuZgRBYiGqkNAFJPNT9Sw3d/AAAAAElFTkSuQmCC) no-repeat;background-size:24px 24px;} 31 | .unit-box ul{position:absolute;margin:1px 0 0 240px;padding:0;list-style:none;background:#0F0F0F;width:240px;height:calc(100vh - 61px);overflow:auto;display:block;transition:all .15s ease;z-index:2;} 32 | .unit-box:focus h3{color:#3A3A3A;display:block;} 33 | .unit-box:focus ul{margin:1px 0 0;} 34 | .unit-box .sub-title,.unit-box label{display:flex;padding:14px 20px;} 35 | .unit-box label{cursor:pointer;} 36 | .unit-box li:hover label{color:#FFF;} 37 | .page-list label span,.unit-box label span{flex:1;} 38 | .page-list input,.unit-box input{position:absolute;visibility:hidden;} 39 | .page-list span:before,.unit-box span:before{float:right;display:block;content:"";width:16px;height:16px;} 40 | .page-list input:checked+span:before,.page-list label:hover span:before,.unit-box input:checked+span:before,.unit-box label:hover span:before{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAADICAMAAAAp4rTzAAAAaVBMVEUAAACYm5yYm500hPaDoMKYucs0hfaYm52Znp6bnZ01hfeYnJ2Zm540hvc8iv81hPWYm52YnJ2ZnJ01h/g2ifmcnJw4h/o1hfY1hfU1hfY1hfU1hfY0hfaZnJ41hfY2h/aYnJw0hfU0hPbpk+wAAAAAInRSTlMAo5LmCAP0nDIVeHFNSg6diGVZNCciGtvSw7arj3xpVkSETvQOmQAAAPlJREFUeNrt1MtuwjAQheFxYhNyIUBCwqXcOu//kHWMl6wqRlWl/1tYs5ojzZEsAH7rUZYbMXZX1VJsfat5SNhqtDI9l/+yyuibps/TOd1qlM+7qB7nZZhPKeMgBlZ58+Go0WkWC6+ux7HU6NyLidz2ankvXoyEq2bbIHYaTa5i6q5RI8Y2ZfkQAAAAAAAAAAAAAAAA4K11UazF2NM5V4itwZmHhL2LKtNzhdYqw3edz9Mt3WqSz2ud29XLUO9SRi0Gqry5LpxLeRZeXU9Tyrh5MZHbrpa39WIkDC7bB7HTuWQQU08XdfLOP/sVAQAAAAAAAAAAAAD4Yz84hg8qlGTZeQAAAABJRU5ErkJggg==);background-size:50px 100px;} 41 | .page-list label:hover span:before,.unit-box label:hover span:before{background-position:0 -50px;} 42 | .page-list input:checked+span:before,.unit-box input:checked+span:before{background-position:0 0;} 43 | .unit-box .sub-title{font-size:12px;background:#000;color:#3A3A3A;} 44 | main{position:relative;height:calc(100vh - 60px);} 45 | .inspector,.navbar{position:absolute;top:0;bottom:0;display:flex;width:240px;background:#232527;z-index:5;} 46 | .inspector{transition:all .15s ease;right:-240px;display:block;} 47 | .inspector.active{right:0;} 48 | .navbar{left:-240px;width:240px;transition:all .15s ease;} 49 | .navbar.on{left:0;} 50 | .navbar nav{width:40px;background:#000204;} 51 | .navbar section{flex:1;} 52 | .tab{margin:0;padding:0;display:flex;width:100%;list-style:none;} 53 | .tab li{display:flex;flex:1;height:60px;cursor:pointer;font-size:12px;justify-content:center;align-items:center;} 54 | .tab li.current{background-color:#232527;} 55 | .tab li:before{content:"";width:40px;height:40px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAEsCAMAAACxJAyMAAAAY1BMVEUAAABfYmTY2NjY2NhgYGPa2tpdYGNfYGHZ2dlgYGRfX2LY2NhfYGPZ2dldX2Pb29vV1dXR0dFdYWNgYGPb29vU1NRdXV1aYWVdXWNYWGJbYGLX19fX19fa2trX19fY2NhgYmTufXliAAAAH3RSTlMA8/LjZq/mrzHHnp3VyIJqRQv7VVQbC0csGjrSX4l65F0P9QAAAvFJREFUeNrt3Nlu4kAURdHrCTMYM8ZAGKr+/yubSkdcRJy3OlI6vdcbj1vIPlEkrgEAAAAAAACAUnjy5XMm8cnrZ0IIIYQQQn5XyDNJyAtCCPnXQn7Nw04IIY6HnxBCzHjYCSGEEEII+ZEhAAAAAAAAAAAAAAAAAAAAAAAAAAD8HOEbllH8BiHjITYid4iNIIQQQgjJE+Kmi8VaE+KO8/lBHTItQ1iIQ96LGOfikLd7R2i0Icd7R2y1IW9VuLtIQ06TeFdLQzbL4F+IKGSYRf9CVCFN6lhupCFt6pgN0pBF6qjeTBkyTx2TkylDbqmjnEp3pE8dxVG6I5eQrKWDWKeO7UE6iOuQrKTLftj6C0sVMi1Tx9VEIT4gsTdViA/I0lQhPiAzU4ZszuHDRRgydPFDrQnxAVE/I7MYs5eETz4gD/vc/3zwAXnYqUKu3tGYKKT3jtZEVt6xMJHaO+Ymsn5klBcT8AFJitoEfECSamoi70X81B1NwAckuZmAD0hS9OZEHc2bqZy6+Fd7MpllSMrF1DR8QIr50XSaVNGsNibUpoq2Hkzoem5uU9Pqu7Z/NwAAAAAAAAAAAAAAAAAAAAAAAAAA/jO3m8n1vcmtQliZWB1jbWKXsVsP+TvkJXu/kCCz26aQ7c6E1uHT2mQOqeP1Qo2gQ13iv8ov3k3Erwv49SNFh67EO9QlR+94ueOU+7qA88taua8kuMnJ8luGF0vLbhZfzCy7VXghGMY6vlDMSRW+qCyzSfxiYi7fEjrJLu7iiNy7uAhOdXJnHp3s5M45jDhbVl0c0VleZRhRWlZFHFFYXmGUZbWNI7aWVxWC/LU1iVH62vI51E2iz6F6Eq9hxNWy6uOI3vJaB6f6W/4QRxwss7Ps7es63dvX7WXD7nbCYXcL2a67uXrX/d6yW24su2EmfGV5SROeNBsTGNr4pB1MY1U9pnBlIvXkMYW1yWz2TVWWVbPfmMywa7ui6NrdYAAAAAAAAAAAAAAAAADw5A96XDaWeHqp+wAAAABJRU5ErkJggg==) no-repeat -50px 0;background-size:100px 150px;overflow:hidden;} 56 | .tab li.current:before{background-position-x:0;} 57 | .tab li.icon-slices:before{background-position-y:-50px;} 58 | .tab li.icon-colors:before{background-position-y:-100px;} 59 | #inspector .code-item{display:none!important;} 60 | #inspector .code-item textarea{padding: 16px;} 61 | #inspector .code-item.select{display: block!important;} 62 | #inspector .tab li{height:40px;} 63 | #inspector .tab li:before{background-size:100px 200px; background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAGQCAMAAADcLN+CAAAC/VBMVEVHcEzZ2dnY2NjZ2dlgYWRgYmRfYWTZ2dnZ2dlfYWNgYWTd3d3Z2dlnZ2dfYWPa2tpgYWPZ2dlgYmRmZmbY2NhfYWXa2tpgY2XY2NhhY2TX19fZ2dna2tpiY2hkZGRgYmTb29vZ2dlgYWTY2NjX19dgYmPZ2dna2trY2NhhZmZgY2fU1NTp6enZ2dlpaWna2tpgYmTY2NhfYWNgYWRfY2TZ2dnX19fY2NjY2NjY2Njf39/V1dXY2NjY2NjY2NjX19fX19fa2tpgYGZfYmNgYmRfYWRfYWRfYWRaWmldXV1fYmRgYWRfYWRfYmNgYmNdY2PX19fX19fX19fY2NjX19fY2NjY2NjX19fY2Nj///9VVVVgYWRgYWRfYWRgYmNfY2NfYWRgYmNgYWNqamrY2NjY2NjY2NjX19fS0tLX19fY2NjX19fY2Nja2trY2NhfYWNgYWNfYWNfYWNwcHBfYmRfX19fYWRgYWTY2NjX19fV1dXY2NjX19fY2NhfYWRgYmNdZmZgYWNhYWFhYWNfYWPX19fX19fX19fY2NjX19fX19dgYWRfYWRfYWNfYWNgYmTY2NjY2NhfYWNfYWRfYmRfYmPY2NjY2NjX19dfYWNfYmTX19fX19fY2NjX19fX19dfYmRfYWNgYmRjY2NfYWPX19fX19fS0tLY2NjZ2dnY2NhgYmRgYWRgYmRfYWPY2NjY2NjX19fX19fY2NjY2NjX19dfYmNgYWRgYWRgYmRfYWNgYWXX19fY2NjY2NhgYmRfYmPa2trY2NjX19fY2NjY2NjX19fY2NjY2NhtbW1gYmRgYWRgYmNfYWPY2NjY2NjY2NhfYmRgYmRfYmRfYWRcZGTX19fX19fY2NjX19dfYmRgYGVfYWRjY2NeYmLX19fY2NjW1tbY2NhiYmJeY2VfYWNVVVXY2NjX19dgYWTY2NhfYmNmZmbX19fY2NhgY2NgYGNdYmJfYWTY2NjX19fY2NjX19fX19dgYmPX19deYmVcXGhgY2PY2NhfYWSjTe8cAAAA/3RSTlMAvv/nwf/o8rHxsR58IHtTUYeHGbm6aGztWycvcCkwjl3e4dYV1zdFxjdFBgyPC5qZp/Wnyxuc5q1YCBJqv/LrjxAbnefmrlgIE2nA8uqPEMK80Ehb4d+wogEGvbntz0fg36QBdnDsiAQJmdxcDjV3ceyJBN0OmDWm9h6rLH6l9h6sLyt+ydf++/Gpydb9+6nT29Pb0LvOpZbOlbaBh274tYBuEvhF/Av1KYP8xvSDPpPv6ehjOV4+ku9jO/exxffFBzthnbRSQ3MH/rRDczHMIeOCMswhZiCLQGYfjBdA3tUl2Scl2QNVaFMUsBQzSlVKOLpPeIx75OTEThZ5KKE32RxrAAALBklEQVR4AezdwVLyUBLF8WOHJgQwCoCAiAoxCIJRwPd/tjFizYdKZzPnWjVW/1a33P0rOQYWRvwxzjnnnHPOOeecc86dSZUIDDWtUgdFLFUaYEi0ShMMLanUBsG5VkrBcCGVLkBwqZUuwdCRSi0QdLVSDwz9X9j64De23jJcSakPht75aUPi1k0dKXUQUldLXQTV523dNCBu3RTxtm6qa+kcDI3YIKURGJqJQUsJKCKp0gdDXasMAj/XeVvvaaXuX3muj8HQ/oXneqo23talSgyGplZJQHE9Oe1GSmdgmN6eNtRSDUG17a3TpFq6Q1ANKd0jpKa9dZ6ZlK4R0lxLUzB0stI9vnsgbr2bl8b4rsfceialBb57lNISDLmWVvjuibj1tXzYGFt/BsFaPxRBt76QDy8LfDHpRLytr/TD6wpfTLt14tav5FN28/CvIpNPExAM9VM+7P2ryPWAtPUX+Wf7eFRxAIZX/a/d/qmsSPOjH9G2fmT2ksmxJRje9Nj8Nddjb6DYSoXZPRj2WiEfg+PhJjMqOhOQ9NJcT5p3b0G0eIl/VixAtXrd/axYgW1zteRVGIrh23HF3QphTJ5HUoqeFwhlepdQKwzriz6vwrAeD/63Cuecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOec6cQehdZMughMRhKaqfzVkHT5kkkWT0CG3ef02dMhkJNIOHHKbqKaBQ8qOUStsSNmRnIcIafdncZw12q1DxyTERtLBfJfkzbR36LgFnRzph+mAHhkE6ujIsTAdXT0WpGPTkG+WG5AVTf3mrfiNt4D21+Cq6Q+DNbjacsIzqFI94Q5UCym1N235cpiAaKWltEj1y+E2wHuCNtjIl8MZ/z1BBQr9cqiBqCWnr4i0QNPT01dEz6kLMdwwF2JIqXeWoc+8swwD8GRiyEDzpoYcPLEYYtAkakjwV7mbWEzxDSiGOzXthvyFBFvJTivswCGVQKGV/Ir8wY0ciAk0pBvLQ2g8xEM8xEN4IjFEoKmroQ6erRi2oNmrYQ+e1khOGrVA00v0pKQHotY2kh+ibQtEvX1df6jve/j/4ZxzzjnnnHPOOeecc84555xzzjnnnHPOOeecc845t77vRwhsPR7UEdjjTGSEsJ7mqgnCupd3SwQ11nc1BPUopRuE9KSlFCGtI3k32iCg9VzfJQVCupLSBUIaammMoM7kXRtBDYwbi2pkDIQpUd0NEVg8e75GYMn8dYowJs/9Wba92KC0aC+jeNa42oBqejfI3/bjAqVVWqvv8uawANP1mXxq3D92ZlJiT2Va04Ndc/zUneunXQqei1gMZ2uQXCZqqBW8J6BtC44nte15f3Jsmi1A0aurab4Cx4uYsg04XtWUF+DYxGKJr8FRJGpJpiC5ElMHJEM1dcGyFdM1SPZqmoJlKZYILDW11EETiWUJlrla3kAjvxCivxES/cKtVVfLHDSZmFogydV0Dpa+mNogGagpBUtHTPEDOLpqSnq/8ZFxuQ7/kbFWgGMtFZYPYCi0Qq3HG4ktumKNxFYfguJCqpyB4VKr1MAxkwr3tGe7bQyOG7Fl4BiqLQfJOhPTIzjWuZqeEP43cB8sT2oZgOel6iti4G+7uyl41pm1dJ4it5bO1IrC/3OF87qecAeu60w+ZC+dTkMOOuCa5vohf03vmjt+x8Gmk8XL9vXhfLGdRY0F2Ipunryl08P5cp/Pm6v/sAcHAgAAAAyD7k99kFUDAAAAAAAAAAAAAAAg7+TXA3Bd2R/A8e/8Zs/OrG3bxksb2062DcpXvKQMith5SW3btm3btm238597FmO+M/x/7j0aHuP/0x9/2mxuNWqC5u7h6eXt44tZfv4BgUHBIaCFhoVHREZFY9Z3MRL7+tsSF48lIVH+quUptZMwKfkplZJaR9Wth+Wd+qpBUIpqaMeoGtII/mjcRJo6SEuXZs1bQLq0xKRg1Qr8WmeozCxCflbZ77SBtqodRrXP8QXIdZO8916XdHeAfCnApMKiaIDiElVSmqp+DgUoU+UYVVGZhFbg9KpqjJZW3QGTOnayo5V3jujSGu3HZ7piVLeKJLTvukscGo7EWEzq0dGOltxT9UIj69PeGNXnn4Yk9ZVY6Yf2ne11TOr/T0Pshaq3GoCWHJiKUQNt32EZJINJlyFY3If2xaRhAclYhqtXaatGYBk5qiFG9avyBRg9ZuDYFu5fj9Pb1XiZgEkDuujFXm/isB/bjJw0uR16sU/BqI+l+VSPaXnTxWvG9Jk5YuvgM2u2h8Rj0hw1Z27YvK7zVUTk/KeLVEDXqAULw1Q9DHJ8mS6LRKpFPBe79XVbvETESyRHmuZjTNY3bdVSpZ5RKvzZksKgZ5cpFaFUkcosw5Tlsyqkqmqmx4qVzlVorM5Zs3ZddZVI7JokTFi/4HfVpcvTby1csMEfjY1FL2/a/EwXpVJetmPC1ApJHLTlzxxftnr/gUZ+zmq2jfFZ8UO1DMzHdXM7qu07dvoXRbMr0g+NsqKN7J4YtXDeMyq1DNc1d1Y39oXRsqeGeMDefasnfA4TZH+BbIE/BjvfXour3un8TOto2KQOBKswOHho45TDMEUdKVc7we/VDXU24aqpzrh3AdKOdjt67DuOdxeRrZ/jazvRvcIBsML29nhcM7dzr5MAIdk9GnRK5lRPpdSuw0QHnu75+xmAhYHzP8BFsd7b0NaJnIVVYhkE50RWon1ZtQrX9M7YjbZZqfPgryw7IEypOWjtuvjjoupaaByv6u6AOLEMBPcZ1Rf+u4fhmmcuonGqS88s6KUsw2Bk5DOX/ruH4aI+zdLQzkrOZVgslmkQL3IF7Q/vxbimf3YI2lVV1A6eVZYBUE+pa2h+Gc+6/hDpWxMgKcY7ZyDMXiQiM8fj2Drd+3Us7rGy3+WHSKFuif2pjKJhcH2pUurpMs7smp9xA8vIFHUEVzWVmJbAezJokIyG/bGJiy9Dc9mzT8YDV7rLEFyVqZ5qB5SqHcPVt3Akpf6zO2GOOpCpPgCu9VQjcN26Kuefs5MGOd/19Y5LAxyAe+JRR76zwHFzoMyogetad9ngf92+Y8PJNpF1fwSygNDt2VllncuzXh6mIoMx4XKeyFZb4nhWyi009smV5V/avI/KOJ9cTGhXotSuwPplzFGt0Dikrq2/HpiRrSZHFWPIlttHRXK6e45ZdOfcrOazzk1wVn19V0TiGudiyocfZStV1DN84tIXDyyYsyBsSucuk+4ppXq1LsagszLt9v27NhGn6JB4dPWdpnITk66qeYeW3QtUaoPSYXv2xhcz1QOM8hnzOSS535LVU+NXvC5DxgJ/OG9jUtTEh2Af+UhtnFtv4Q014kfAb8MhjKrVZDkWN7nJGumLZey4xZgUlLEeS4l6mfOqEMuPk5/FqIo+SVjc48bdGtfHF4vjxBJM6tjfjiW016+tfu0fjSXr9DJMSjp2H43cu2Lbi+aIq8Age6dlaBzcrgIPop2p2xGj+sQloV2+K7bZaI6/PDGpfy872s57KuA6WlaDFIxa3aQmloRx3kNsixKwfF6VjkkbM0KwvDM5Y0Tg0newHO7yM0YN0Vvt49pSuYX3uknt44CHrMGkEXqr/eCJ+mInpT3Uk1PAW+pljPr87vRbQ9zGSPoFwD1dhrY/V7AoLg2TDt+b/2hE0ETV9hIQ+rMaVRhWvrTXj5g1tXJRzri+K/in5LYoZ+jWfMya+8XSoskNF/JPqWRp0ahdZf9rDw5oAAAAEAbZP7U5vgEDAAAAAAAAACDnF2iOmVUrLFoAAAAASUVORK5CYII=); 64 | } 65 | #inspector .tab li.select{background-color:rgba(255,255,255,.05);} 66 | #inspector .tab li.select:before{background-position-x: 0;} 67 | #inspector .tab li.icon-android-panel:before {background-position-y: -50px;} 68 | #inspector .tab li.icon-ios-panel:before {background-position-y: -100px;} 69 | #inspector .tab li.icon-rncss-panel:before {background-position-y: -150px;} 70 | .screen-viewer{position:absolute;width:100vw;height:calc(100vh - 60px);background:#191919 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgAQMAAABJtOi3AAAABlBMVEUAAAAAAAClZ7nPAAAAAnRSTlMAUvP2/eAAAAARSURBVAjXY6ASYD6ATlAHAADy5gMN0fCqFwAAAABJRU5ErkJggg==);background-size:16px 16px;overflow:auto;} 71 | .screen-viewer,.screen-viewer .overlay{position:absolute;width:100vw;height:calc(100vh - 60px);background:#191919 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgAQMAAABJtOi3AAAABlBMVEUAAAAAAAClZ7nPAAAAAnRSTlMAUvP2/eAAAAARSURBVAjXY6ASYD6ATlAHAADy5gMN0fCqFwAAAABJRU5ErkJggg==);background-size:16px 16px;overflow:auto;} 72 | .screen-viewer .overlay{display:none;position:fixed;top:60px;left:0;background:transparent;overflow:hidden;z-index:2;cursor:none;} 73 | .screen-viewer.moving-screen .overlay{display:block;} 74 | .screen-viewer-inner{position:relative;margin:0 auto;} 75 | .screen{margin:0 auto;position:absolute;left:50%;top:50%;background:#FFF;box-shadow:0 2px 10px 0 rgba(0,0,0,.2);transition:all .15s ease;} 76 | .layer{position:absolute;cursor:pointer;} 77 | .hover,.selected{border:1px solid #EE6723;} 78 | .has-slice{border:1px dashed #EE6723;background:rgba(255,85,0,.32);} 79 | .layer b,.layer i{position:absolute;width:5px;height:5px;background:#FFF;border:1px solid #EE6723;border-radius:50%;overflow:hidden;display:none;} 80 | .layer b{width:3px;height:3px;background:#EE6723;} 81 | .selected i{display:block;} 82 | .distance.h div[data-width]:before,.distance.v div[data-height]:before,.selected:after,.selected:before{position:absolute;display:block;left:50%;top:-23px;transform:translateX(-50%);content:attr(data-width);font-size:12px;color:#FFF;height:12px;line-height:12px;padding:4px;background:#EE6723;border-radius:2px;z-index:1;} 83 | .percentage-mode .distance.h div[data-width]:before,.percentage-mode .distance.v div[data-height]:before,.percentage-mode .selected:after,.percentage-mode .selected:before{content:attr(percentage-width);} 84 | .selected.hidden:after,.selected.hidden:before{display:none;} 85 | .distance.v div[data-height]:before,.selected:after{content:attr(data-height);left:auto;right:0;top:50%;transform:translateX(calc(100% + 3px)) translateY(-50%);} 86 | .percentage-mode .distance.v div[data-height]:before,.percentage-mode .selected:after{content:attr(percentage-height);} 87 | .layer .tl{top:-3px;left:-3px;} 88 | .layer .tr{top:-3px;right:-3px;} 89 | .layer .bl{bottom:-3px;left:-3px;} 90 | .layer .br{bottom:-3px;right:-3px;} 91 | .hover{border:1px solid #419bf9;} 92 | .selected{border:1px solid #EE6723;} 93 | .ruler{position:absolute;width:100%;height:100%;border:1px dashed #419bf9;} 94 | .ruler.h{border-left:0;border-right:0;} 95 | .ruler.v{border-top:0;border-bottom:0;} 96 | .distance,.distance div,.distance div:before,.distance:after,.distance:before{position:absolute;} 97 | .distance.v,.distance.v div{width:1px;} 98 | .distance.h,.distance.h div{height:1px;} 99 | .distance.v div{top:0;bottom:0;background:#EE6723;} 100 | .distance.h div{left:0;right:0;background:#EE6723;} 101 | .distance.v:after,.distance.v:before{content:"";top:0;left:-2px;width:5px;height:1px;background:#EE6723;} 102 | .distance.h:after,.distance.h:before{content:"";top:-2px;left:0;width:1px;height:5px;background:#EE6723;} 103 | .distance.v:after{top:auto;bottom:0;} 104 | .distance.h:after{left:auto;right:0;} 105 | .note{position:absolute;margin:-12px 0 0 -12px;width:24px;height:24px;background:#F5A623;border-radius:50%;border:2px solid #FFF;box-shadow:0 0 3px rgba(0,0,0,.24);} 106 | .note:before{content:attr(data-index);font-size:12px;display:block;color:#FFF;text-align:center;width:100%;height:100%;line-height:20px;} 107 | .note:hover{box-shadow:0 0 3px rgba(0,0,0,.64);} 108 | .note div{position:absolute;top:50%;left:30px;border-radius:4px;padding:8px;background:#FFF;box-shadow:0 0 3px rgba(0,0,0,.5);-webkit-user-select:text;color:#222;transform:translateY(-50%);z-index:2;} 109 | .note div:before{content:"";position:absolute;left:-7px;top:50%;width:8px;height:14px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAcCAMAAABf788oAAAANlBMVEUAAAAAAAAAAAAAAAAAAAAAAADR0dEAAAAAAAAAAAAAAAAAAADNzc0AAAAAAADa2trk5OT////5auFFAAAAEXRSTlMAAgYKEhydNCkYDiOfLieWjz4MUj4AAABrSURBVBjTZZFbDoAgEANVFHmIOve/rERjWGj/psnCbjv1gg7nGTpcFmtUdA4Mu7QmMLzGGMHwlvMNhs9yAY3D7qkamcYHr/75ys1IMRePNbbwsRo6oo/qt7rY6Ohxer4GpBFqyFqDFtWqfAD4dQrlWDmShAAAAABJRU5ErkJggg==) no-repeat;background-size:8px 14px;transform:translateY(-50%);} 110 | .slidebox{position:relative;width:40px;height:20px;display:inline-block;} 111 | .slidebox input{visibility:hidden;} 112 | .slidebox label{position:absolute;top:0;left:0;width:40px;height:20px;background:#898989;box-shadow:inset 0 1px 3px 0 rgba(0,0,0,.3);border-radius:10px;cursor:pointer;transition:all .2s ease;} 113 | .slidebox label:after{position:absolute;content:"";top:2px;left:2px;width:16px;height:16px;background-image:linear-gradient(-180deg,#F8F9F6 0,#F5F5F5 100%);box-shadow:0 1px 2px 0 rgba(0,0,0,.3);border-radius:8px;transition:all .15s ease;} 114 | .slidebox input[type=checkbox]:checked+label{background:#3484F5;} 115 | .slidebox input[type=checkbox]:checked+label:after{left:22px;} 116 | .inspector{overflow:auto;} 117 | .inspector h2,.inspector h3{margin:0;padding:0;font-weight:400;} 118 | .inspector h2{font-size:12px;line-height:24px;padding:12px 16px;text-align:center;} 119 | .inspector h3{height:32px;font-size:12px;line-height:32px;padding:0 16px;background-image:linear-gradient(90deg,rgba(0,4,9,.32) 0,rgba(0,2,4,0) 100%);} 120 | .inspector .context{padding:16px;} 121 | .inspector .color,.inspector .item{display:flex;margin:0 0 8px;} 122 | .inspector .item[data-label]:before{content:attr(data-label);display:block;width:70px;font-size:12px;height:24px;line-height:24px;} 123 | .inspector .color:last-child,.inspector .item:last-child{margin:0;} 124 | .inspector .item div,.inspector .item label{flex:1;} 125 | .inspector .item label{padding:0 4px;} 126 | .inspector .item label:first-child{padding:0 4px 0 0;} 127 | .inspector .item label:last-child{padding:0 0 0 4px;} 128 | .inspector .item label:only-child{padding:0;} 129 | .inspector .item label[data-label]:after{content:attr(data-label);font-size:10px;text-align:center;display:block;margin:2px 0 0;} 130 | .inspector input,.inspector textarea{padding:3px 4px;width:100%;background:rgba(255,255,255,.05);border-radius:2px;border:0;line-height:18px;height:24px;font-size:12px;color:#FFF;text-align:center;outline:0;resize:none;} 131 | .inspector textarea{min-width:100%;height:auto;text-align:left;} 132 | .context h4{font-size:12px;font-weight:400;position:0;margin:0 0 16px;color:#FFF;} 133 | .context h4:before{margin:0 10px 0 0;display:inline-block;content:"";width:8px;height:8px;border-radius:1px;background:#3484F5;} 134 | .context .colors{margin:0 0 8px;} 135 | .context .colors:last-child,.context .items-group:last-child{margin:0;} 136 | .context .items-group{margin:0 0 24px;} 137 | .inspector .color{display:block;} 138 | .inspector .color[data-name]:after{content:attr(data-name);font-size:10px;text-align:center;padding:0 0 0 24px;display:block;margin:2px 0 0;} 139 | .inspector .color label,.inspector .color label em,.inspector .color label em i{position:absolute;display:block;width:24px;height:24px;padding:0;} 140 | .inspector .color label em{position:relative;background-color:#fff;background-image:linear-gradient(45deg,#dddadc 25%,transparent 25%,transparent 75%,#dddadc 75%,#dddadc),linear-gradient(45deg,#dddadc 25%,transparent 25%,transparent 75%,#dddadc 75%,#dddadc);background-size:12px 12px;background-position:0 0,6px 6px;border-radius:2px 0 0 2px;overflow:hidden;} 141 | .inspector .color label em i{position:relative;} 142 | .inspector .item .color input{text-align:center;} 143 | .inspector .color input{padding-left:32px;width:100%;text-align:left;} 144 | .gradient .color label:before{position:absolute;top:12px;left:11px;content:"";width:2px;height:32px;background:#FFF;z-index:2;box-shadow:0 0 2px 1px rgba(0,0,0,.2);} 145 | .gradient .color label:after{position:absolute;top:8px;left:8px;content:"";width:8px;height:8px;background:#FFF;z-index:3;border-radius:4px;box-shadow:0 0 2px 1px rgba(0,0,0,.2);} 146 | .gradient .color[data-name] label:before{height:42px;} 147 | .gradient .color:last-child label:before{display:none;} 148 | .navbar section{position:relative;overflow:auto;} 149 | .artboard-list,.asset-list,.color-list{margin:0;padding:0;list-style:none;} 150 | .artboard-list li{padding:16px;display:flex;cursor:pointer;} 151 | .artboard-list li:hover,.asset-list li:hover,.color-list li:hover{background:#191A1E;} 152 | .artboard-list li.active{background:#454748;} 153 | .preview-img{display:block;width:44px;height:44px;text-align:center;background:rgba(0,0,0,.32);} 154 | .preview-img:before{content:' ';display:inline-block;vertical-align:middle;height:100%;} 155 | .preview-img img{max-width:44px;max-height:44px;background:#FFF;vertical-align:middle;} 156 | .pages-select{padding:24px;position:relative;} 157 | .pages-select h3{margin:0;text-align:center;font-size:12px;font-weight:400;color:#FFF;cursor:pointer;} 158 | .page-list em,.pages-select h3 em{font-style:normal;font-size:10px;color:#989A9C;} 159 | .pages-select h3:after{content:"";margin:0 0 0 4px;width:12px;height:6px;display:inline-block;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAMAgMAAAAFXpXmAAAACVBMVEUAAABiZGZgYmThY1mHAAAAAnRSTlMAgJsrThgAAAA1SURBVAjXFckxAQAgDMTAZ6kI1KAHNYjoAipJsmS47Ec303XKnQy3AkIRm5UUUAIhERIhEfoqmRfpcqcFcAAAAABJRU5ErkJggg==) no-repeat;background-size:12px 6px;} 160 | .pages-select .page-list{display:none;position:absolute;left:50%;transform:translateX(-50%);margin:-32px 0 0;padding:8px;border-radius:4px;list-style:none;width:180px;background:#0F0F0F;box-shadow:0 2px 10px 0 rgba(0,0,0,.8);} 161 | .pages-select:focus{outline:0;} 162 | .pages-select:focus .page-list{display:block;} 163 | .pages-select .page-list label{padding:10px;cursor:pointer;} 164 | .pages-select .page-list label,.pages-select .page-list span{display:block;} 165 | .page-list span:before{position:absolute;right:8px;} 166 | .exportable{margin:0;padding:0;list-style:none;background:#191A1E;border:1px solid #2D2F31;border-radius:4px;} 167 | .exportable li{position:relative;padding:0 8px;height:32px;line-height:32px;border-bottom:1px solid #222;white-space:nowrap;text-overflow:ellipsis;} 168 | .exportable li a{color:#FFF;text-decoration:none;outline:0;} 169 | .exportable li:last-child{border:0;} 170 | .exportable img{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;} 171 | .asset-list li,.color-list li{padding:24px;display:flex;cursor:pointer;} 172 | .color-list li{cursor:default;} 173 | .asset-list li picture,.color-list li em{position:relative;display:block;text-align:center;width:32px;height:32px;background-color:#4A4A4A;background-image:linear-gradient(45deg,#000 25%,transparent 25%,transparent 75%,#000 75%,#000),linear-gradient(45deg,#000 25%,transparent 25%,transparent 75%,#000 75%,#000);background-size:8px 8px;background-position:0 0,4px 4px;border-radius:2px;} 174 | .color-list li em{background-color:#fff;background-image:linear-gradient(45deg,#dddadc 25%,transparent 25%,transparent 75%,#dddadc 75%,#dddadc),linear-gradient(45deg,#dddadc 25%,transparent 25%,transparent 75%,#dddadc 75%,#dddadc);border-radius:50%;overflow:hidden;} 175 | .color-list li em i{position:absolute;top:0;left:0;right:0;bottom:0;} 176 | .artboard-list li div,.asset-list li div,.color-list li div{padding:0 0 0 16px;flex:1;} 177 | .artboard-list li div{padding:6px 0 4px 16px;} 178 | .asset-list li picture:before{content:"";display:inline-block;height:100%;vertical-align:middle;} 179 | .asset-list li picture img{vertical-align:middle;max-width:28px;max-height:28px;} 180 | .asset-list li picture:after{content:"";position:absolute;top:0;left:0;right:0;bottom:0;} 181 | .artboard-list h3, .asset-list h3,.color-list h3{margin:0 0 4px;padding:0;color:#FFF;font-size:12px;font-weight:400;overflow:hidden;text-overflow:ellipsis;} 182 | .artboard-list small, .asset-list small,.color-list small{font-size:10px;} 183 | .artboard-list li.active small{color:#FFF;} 184 | .empty{position:absolute;top:30%;left:0;right:0;transform:translateY(-50%);vertical-align:middle;text-align:center;} 185 | .message{position:absolute;top:50%;left:50%;padding:10px 20px;max-width:320px;transform:translateX(-50%) translateY(-50%);text-align:center;border-radius:4px;background:rgba(0,0,0,.96);border: 1px solid rgba(255,255,255,.16);color:#FFF;box-shadow:0 2px 10px rgba(0,0,0,.32);z-index:9;display:none;} 186 | .cursor{position:absolute;display:none;margin:-8px 0 0 -8px;top:50%;left:50%;width:16px;height:17px;background-size:16px 17px;background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAiCAMAAAAJbCvNAAABVlBMVEUAAAAGBgYBAQEBAQEAAAAAAAAAAAAnJycAAAA3NzcDAwMNDQ0AAAAAAAAAAAAhISEbGxsYGBgPDw8CAgIDAwMBAQEAAAAAAAAAAAAAAAAAAAAvLy8UFBQCAgIBAQEAAAAAAAAAAAAAAAAAAAA+Pj5BQUE/Pz8zMzMCAgIDAwMDAwMCAgIBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7OzsAAAAAAAAAAAAAAAD///8AAABhYWEdHR36+vpVVVX9/f329vbz8/Pb29uBgYF0dHRjY2MsLCz39/ft7e3r6+vR0dFxcXFSUlJNTU3X19fKysq4uLh3d3dXV1dJSUlGRkZERETv7+/o6Ojj4+O8vLy1tbW0tLSnp6empqaTk5OKioqGhoZtbW1nZ2dcXFxPT0/Dw8PCwsKwsLCurq6pqamhoaGZmZmNjY16enpeXl5JnG7xAAAAPHRSTlMA/fj9B1EC/jf+/vw8Ew7+/fz85t26mpRoYyX9/OLMoHlKMiL+/f398enY0cC1tLGojouGVh4Z/qpyQy7UG/vLAAACHUlEQVQ4T33QVVdbURBA4bk3QjwQIIa7u9T2QDwQxx2K1+X/v/QhlzRpWN2v51tnZo1IS4ERx4b8J4epaht+5cHpiKzGRfzmxH55R0ftseERZwuIqGGY6xLTLKSNoTlV7W0WY7Z8qjRhflgxADrUOMjcarQJjGgZiqHecBfAuB4Cfb1NwKcHwJ0OdgKc6jWw1S0y5rLXgd18AIqGdtVBGdjpti+r9sTrYqkrBSS1H+BEz4GjnojmvnutQQ79AnxTL8CxFoCkqXnYmakD58AkkPEYADUPQFVDGajOWmsOaxboU4DnEEBN80Dfgoj4og7/pu0QeNQ08NxZ32QPEp41cS6rqvluqT8FX4MJIPcEkHQDn9Qh65rP7le1W/dpKVEAcraArHpTQE71nvZO50TWjAxARU9eAaGwiEtvARJTnQCpNKTPIFEEuNCoiCwYnwGKWYCKG7bGobILcK0+EfHPevcafz4FoboLW9sA9xoQEdmY6Si8gN9BSDZArad+xvf66wVsbTeBMyNiHXrIs9cMOixwp6MW8E9PWmBnG44scOFdlJdi+tMCXQ2QcE/HG8D5Vj8CkO+Axymo7HKoDvmbfd5zA5AoQeYcCqWsLSzNBeZtP2guZ/O3ALEvWlMoveksQ7JH/sk56K6DmjkQTHM8+C+Qof6rg8vLqweNumzHN+7uNhDWeisiMVN1oA3YR10un8+1KSISiI/Z28Dr/QHPa6Em8DfwfAAAAABJRU5ErkJggg==');background-repeat:no-repeat;cursor:none;z-index:1; 187 | } 188 | .cursor.show{display:block;} 189 | .cursor.moving{background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAiCAMAAAAJbCvNAAABMlBMVEUAAAAAAAA2NjYsLCwCAgIAAAAAAAAMDAwBAQECAgIBAQECAgIBAQEBAQEAAAAAAAAAAAA5OTkgICAICAgAAAAcHBwCAgIAAAAAAAAAAAAAAAAAAABDQ0M8PDwxMTE0NDQVFRUCAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPDw8DAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8AAABhYWEcHBz6+vr39/dMTEzs7Oy8vLy4uLhWVlYqKiooKCj09PTw8PC1tbWnp6d2dnZZWVn8/Pzp6enj4+Pd3d3a2trNzc2tra2pqammpqaKioqAgIBzc3NwcHBmZmZdXV1TU1NHR0c+Pj7W1tbR0dHGxsbAwMCampqSkpKPj4+GhoZycnJsbGwkJCTy91wiAAAANnRSTlMAPP7+/ZcN/Pnr487Kv7QlEv7+/fz71jk2GAcE/v7+/fzzpaCUkGZZQTEsIAn826uEg3JtXFGryOxIAAABkElEQVR4AeXS1XLbQBjF8RXIDHbNdiBpOCnDOa4kg2Oww9w2KUPf/xW6s8q2sTXT6X1+t99/NJozK+6lR8ZmRPzDZpxc3sg9y2Sez3aNl+mUsx23TjpzxbJtmnaqMR08ZO8mGuNn4JA8AU75YiXl5ISW5SXwgTwAUGIfcAuM3thpoRnsAJhnC8DcY0gWj3HBmg5qPAdw1nMBfJ9Aai4AQ1aFllzENNcD2jSEsuOkEvyIEB1k44XrEicI6bCigtXoIbzFN5D2PMAbAG4L0im3VVD2AQyOIF3lgeY88MOC9JXBEEkf2rsHwHtLRup749jtikvTQU8Hflkor/hJB00ZdHUwKKwKJRLP70FRh+7ubXDOLRHY4GU4wEE0I7Q1foPyVh4mQeCay1nxx1OeqWC8K6MF4MrCmK/FX/W0PQKCgfr7gLd/ZDvirkiaX3DXRTE381wzHEFp5UvHQPeJmNFImlD8WOJXH35SzFpZGnba7eFPrleK1yMzEQocBtaEWI+R4aC+Va0YRrWm/jm7Uxf/5zeExWvZghlTSgAAAABJRU5ErkJggg==');} -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Spec Export - Sketch Measure 2.4 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /assets/index.js: -------------------------------------------------------------------------------- 1 | var I18N = {}, 2 | lang = navigator.language.toLocaleLowerCase(), 3 | timestamp = new Date().getTime(); 4 | I18N['zh-cn'] = { 5 | "Design resolution": "设计分辨率", 6 | "NOTES": "备注", 7 | "PROPERTIES": "属性", 8 | "FILLS": "填充", 9 | "TYPEFACE": "字体", 10 | "TEXTSTYLE": "名称", 11 | "BORDERS": "边框", 12 | "SHADOWS": "阴影", 13 | "CSS STYLE": "CSS 样式", 14 | "CODE TEMPLATE": "代码模板", 15 | "EXPORTABLE": "导出", 16 | "Gradient": "渐变", 17 | "Color": "颜色", 18 | "Layer Name": "图层名称", 19 | "Weight": "粗细", 20 | "Style name": "样式名称", 21 | "Custom": "自定义", 22 | "Standard": "标准像素", 23 | "iOS Devices": "iOS 设备", 24 | "Points": "标准点", 25 | "Retina": "视网膜", 26 | "Retina HD": "高清视网膜", 27 | "Android Devices": "安卓设备", 28 | "Other Devices": "其他设备", 29 | "Ubuntu Grid": "Ubuntu 网格", 30 | "Web View": "网页", 31 | "Scale": "倍率", 32 | "Unit": "单位", 33 | "Color format": "颜色格式", 34 | "Color hex": "色值", 35 | "ARGB hex": "安卓色值", 36 | "Save": "保存", 37 | "Width": "宽度", 38 | "Height": "高度", 39 | "Top": "上面", 40 | "Right": "右侧", 41 | "Bottom": "下面", 42 | "Left": "左侧", 43 | "Fill / Color": "填充 / 颜色", 44 | "Border": "边框", 45 | "Opacity": "不透明度", 46 | "Radius": "圆角", 47 | "Shadow": "外(内)阴影", 48 | "Style": "样式名称", 49 | "Font size": "字号", 50 | "Line": "行高", 51 | "Typeface": "字体", 52 | "Character": "字间距", 53 | "Paragraph": "段落间距", 54 | "Percentage of artboard": "基于画板百分比单位", 55 | "Mark": "标注", 56 | "All": "全选", 57 | "None": "不全选", 58 | "Select filtered": "选中过滤的", 59 | "Selection of Sketch": "Sketch 选中的画板", 60 | "Current of Sketch": "Sketch 当前的画板", 61 | "Filter": " 过滤", 62 | "Export": "导出", 63 | "Position": "位置", 64 | "Size": "大小", 65 | "Family": "字体", 66 | "Spacing": "空间", 67 | "Content": "内容", 68 | "All artboards": "全部画板", 69 | "No slices added!": "未添加切图", 70 | "No color names added!": "未添加颜色名称", 71 | "Select 1 or 2 layers to make marks!": "请选中 1 至 2 个图层!", 72 | "Select a text layer to make marks!": "请选中 1 个文本图层!", 73 | "Select a layer to make marks!": "请选中 1 个图层!", 74 | "Export spec": "导出规范", 75 | "Export to:": "导出到:", 76 | "Export": "导出", 77 | "Exporting...": "导出中...", 78 | "Export complete!": "导出完成!", 79 | "The slice not in current artboard.": "切图不在当前画板", 80 | "Inside Border": "内边框", 81 | "Outside Border": "外边框", 82 | "Center Border": "中心边框", 83 | "Inner Shadow": "内阴影", 84 | "Outer Shadow": "外阴影", 85 | "No artboards!": "没有画板", 86 | "You need add some artboards.": "您需要添加一些画板", 87 | "No slices added!": "没有添加切图", 88 | "No colors added!": "没有添加颜色", 89 | "Import": "导入", 90 | "Choose a "colors.json"": "选择一个 "colors.json"", 91 | "Choose": "选择", 92 | "Select a layer to add exportable!": "请选中 1 个图层!", 93 | "Import complete!": "导入完成!", 94 | "Processing layer %@ of %@": "图层处理中... %@ \/ %@", 95 | "Advanced mode": "高级模式", 96 | "Export layer influence rect": "导出图层的影响尺寸", 97 | "Set Name...": "设置名称...", 98 | "Import Colors": "导入颜色", 99 | "Export Colors": "导出颜色", 100 | "You can select shape layer to add colors or import colors": "您可以选中矢量图层添加颜色或导入颜色", 101 | "New Version!": "新的版本!", 102 | "Just checked Sketch Measure has a new version (%@)": "刚刚检测到 Sketch Measure 有新版 (%@)", 103 | "Download": "下载", 104 | "Cancel": "取消", 105 | "Donate": "捐赠" 106 | }; 107 | 108 | (function(window) { 109 | 110 | String.prototype.firstUpperCase = function(){ 111 | return this.replace(/\b(\w)(\w*)/g, function($0, $1, $2) { 112 | return $1.toUpperCase() + $2.toLowerCase(); 113 | }); 114 | } 115 | var _ = function(str){ 116 | return (I18N[lang] && I18N[lang][str])? I18N[lang][str]: str; 117 | } 118 | var SMApp = function(project) { 119 | return new SMApp.fn.init(project); 120 | } 121 | SMApp.fn = SMApp.prototype = { 122 | constructor:SMApp, 123 | artboardID: undefined, 124 | selectedID: undefined, 125 | targetID: undefined, 126 | zoomSize: function(size) { 127 | return (size * this.configs.zoom); 128 | }, 129 | percentageSize: function(size, size2){ 130 | return (Math.round( size / size2 * 1000 ) / 10) + "%"; 131 | }, 132 | unitSize: function(length, isText){ 133 | var length = Math.round( length / this.configs.scale * 100 ) / 100, 134 | units = this.configs.unit.split("/"), 135 | unit = units[0]; 136 | if( units.length > 1 && isText){ 137 | unit = units[1]; 138 | } 139 | return length + unit; 140 | }, 141 | scaleSize: function (length){ 142 | return Math.round( length / this.configs.scale * 10 ) / 10; 143 | }, 144 | positive: function(number) { 145 | return number < 0 ? -number :number; 146 | }, 147 | isIntersect: function(selectedRect, targetRect){ 148 | return !( 149 | selectedRect.maxX <= targetRect.x || 150 | selectedRect.x >= targetRect.maxX || 151 | selectedRect.y >= targetRect.maxY || 152 | selectedRect.maxY <= targetRect.y 153 | ); 154 | }, 155 | getID: function(element){ 156 | return '#' + $(element).attr('id'); 157 | }, 158 | getIndex: function(element){ 159 | return $(element).attr('data-index'); 160 | }, 161 | getRect: function( index ){ 162 | var layer = this.current.layers[index]; 163 | return { 164 | x: layer.rect.x, 165 | y: layer.rect.y, 166 | width: layer.rect.width, 167 | height: layer.rect.height, 168 | maxX: layer.rect.x + layer.rect.width, 169 | maxY: layer.rect.y + layer.rect.height 170 | } 171 | }, 172 | getDistance: function(selected, target){ 173 | return { 174 | top: (selected.y - target.y), 175 | right: (target.maxX - selected.maxX), 176 | bottom: (target.maxY - selected.maxY), 177 | left: (selected.x - target.x) 178 | } 179 | }, 180 | message: function(msg){ 181 | var $message = $('#message').text(msg); 182 | $message.hide().fadeIn().delay( 1000 ).fadeOut('fast'); 183 | }, 184 | locationHash: function(options){ 185 | if(options){ 186 | var objHash = {}, 187 | arrHash = []; 188 | $.each(options, function(key, value){ 189 | if( /[a-z]+/.test(key) && !isNaN(value) ){ 190 | objHash[key] = parseInt(value); 191 | arrHash.push(key + value); 192 | } 193 | }); 194 | window.history.replaceState(undefined, undefined, '#' + arrHash.join('-')); 195 | return objHash; 196 | } 197 | else{ 198 | var objHash = {}, 199 | hash = window.location.hash.replace(/[\#\-]([a-z]+)([\d]+)/ig, function(match, key, value){ 200 | objHash[key] = parseInt(value); 201 | }); 202 | return objHash; 203 | } 204 | }, 205 | render: function() { 206 | $('#app').html([ 207 | '
', 208 | '
', 209 | '', 214 | '
', 215 | '
', 216 | '
', 217 | '

', 218 | '
', 219 | '', 220 | '
', 221 | '', 222 | '', 223 | '
', 224 | '
', 225 | '
', 226 | '
XHDPI @2x (dp/sp)
', 227 | '
', 228 | '
', 229 | '', 234 | '
', 235 | '
', 236 | '
', 237 | '', 241 | '
', 242 | '
', 243 | '', 244 | '', 245 | '', 246 | '', 247 | '
', 248 | '
', 249 | '
', 250 | '
', 251 | '', 252 | '
', 253 | '
', 254 | '
' 255 | ].join('')); 256 | this.zoom(); 257 | this.unit(); 258 | this.artboards(); 259 | this.slices(); 260 | this.colors(); 261 | this.screen(); 262 | this.layers(); 263 | this.notes(); 264 | this.events(); 265 | }, 266 | screen: function() { 267 | var imageData = (this.current.imageBase64)? this.current.imageBase64: this.current.imagePath + '?' + timestamp; 268 | 269 | if(!this.maxSize){ 270 | var screenSize = (this.current.width > this.current.height)? this.current.width: this.current.height, 271 | artboardSize = ($('.screen-viewer').outerWidth() > $('.screen-viewer').outerHeight())? $('.screen-viewer').outerWidth(): $('.screen-viewer').outerHeight(); 272 | this.maxSize = (screenSize > artboardSize)? screenSize * 5: artboardSize * 5; 273 | 274 | $('#screen').parent().css({ 275 | width: this.maxSize, 276 | height: this.maxSize 277 | }); 278 | 279 | var scrollMaxX = this.maxSize - $('.screen-viewer').outerWidth(), 280 | scrollMaxY = this.maxSize - $('.screen-viewer').outerHeight(), 281 | scrollLeft = .5 * scrollMaxX, 282 | scrollTop = .5 * scrollMaxY; 283 | 284 | $('.screen-viewer').scrollLeft(scrollLeft); 285 | $('.screen-viewer').scrollTop(scrollTop); 286 | } 287 | 288 | $('#screen').css({ 289 | width: this.zoomSize( this.current.width ), 290 | height: this.zoomSize( this.current.height ), 291 | background: '#FFF url(' + imageData + ') no-repeat', 292 | backgroundSize: this.zoomSize( this.current.width ) + 'px ' + this.zoomSize( this.current.height ) + 'px' 293 | }); 294 | 295 | $('.screen').css({ 296 | marginLeft: - parseInt( this.zoomSize( this.current.width / 2 ) ), 297 | marginTop: - parseInt( this.zoomSize( this.current.height / 2 ) ) 298 | }); 299 | 300 | }, 301 | zoom: function(){ 302 | var zoomText = this.configs.zoom * 100 + '%', 303 | inDisabled = (this.configs.zoom <= .25)? ' disabled="disabled"': '', 304 | outDisabled = (this.configs.zoom >= 4)? ' disabled="disabled"': ''; 305 | $('#zoom').html([ 306 | '', 307 | '', 308 | '' 309 | ].join('')); 310 | }, 311 | unit: function(){ 312 | var self = this, 313 | unitsData = [ 314 | { 315 | units: [ 316 | { 317 | name: _('Standard'), 318 | unit: 'px', 319 | scale: 1 320 | } 321 | ] 322 | }, 323 | { 324 | name: _('iOS Devices'), 325 | units: [ 326 | { 327 | name: _('Points') + ' @1x', 328 | unit: 'pt', 329 | scale: 1 330 | }, 331 | { 332 | name: _('Retina') + ' @2x', 333 | unit: 'pt', 334 | scale: 2 335 | }, 336 | { 337 | name: _('Retina HD') + ' @3x', 338 | unit: 'pt', 339 | scale: 3 340 | } 341 | ] 342 | }, 343 | { 344 | name: _('Android Devices'), 345 | units: [ 346 | { 347 | name: 'LDPI @0.75x', 348 | unit: 'dp/sp', 349 | scale: .75 350 | }, 351 | { 352 | name: 'MDPI @1x', 353 | unit: 'dp/sp', 354 | scale: 1 355 | }, 356 | { 357 | name: 'HDPI @1.5x', 358 | unit: 'dp/sp', 359 | scale: 1.5 360 | }, 361 | { 362 | name: 'XHDPI @2x', 363 | unit: 'dp/sp', 364 | scale: 2 365 | }, 366 | { 367 | name: 'XXHDPI @3x', 368 | unit: 'dp/sp', 369 | scale: 3 370 | }, 371 | { 372 | name: 'XXXHDPI @4x', 373 | unit: 'dp/sp', 374 | scale: 4 375 | } 376 | ] 377 | }, 378 | { 379 | name: _('Web View'), 380 | units: [ 381 | { 382 | name: 'CSS Rem 12px', 383 | unit: 'rem', 384 | scale: 12 385 | }, 386 | { 387 | name: 'CSS Rem 14px', 388 | unit: 'rem', 389 | scale: 14 390 | }, 391 | { 392 | name: 'CSS Rem 16px', 393 | unit: 'rem', 394 | scale: 16 395 | } 396 | ] 397 | } 398 | ], 399 | unitHtml = [], 400 | unitList = [], 401 | unitCurrent = '', 402 | hasCurrent = ''; 403 | $.each(unitsData, function(index, data){ 404 | if(data.name) unitList.push('
  • ' + _(data.name) + '
  • '); 405 | $.each(data.units, function(index, unit){ 406 | var checked = ''; 407 | // if(unit.scale == self.configs.scale){ 408 | if( unit.unit == self.configs.unit && unit.scale == self.configs.scale ){ 409 | checked = ' checked="checked"'; 410 | hasCurrent = _(unit.name); 411 | } 412 | unitList.push('
  • '); 413 | // } 414 | }); 415 | }); 416 | if(!hasCurrent){ 417 | unitCurrent = '
  • '; 418 | hasCurrent = _('Custom') + ' (' + self.configs.scale + ', ' + self.configs.unit + ')'; 419 | } 420 | unitHtml.push( 421 | '
    ', 422 | '

    ' + _('Design resolution') + '

    ', 423 | '

    ' + hasCurrent + '

    ', 424 | '' 428 | ); 429 | $('#unit').html(unitHtml.join('')); 430 | }, 431 | artboards: function(){ 432 | var self = this, 433 | artboardListHTML = [], 434 | pagesSelect = [], 435 | pagesData = {}; 436 | artboardListHTML.push(''); 462 | pagesSelect.push('
    '); 463 | pagesSelect.push('

    ' + _('All artboards') + ' (' + this.project.artboards.length + ')

    '); 464 | pagesSelect.push(''); 470 | pagesSelect.push('
    '); 471 | 472 | $('#artboards') 473 | .html([pagesSelect.join(''), artboardListHTML.join('')].join('')); 474 | return this; 475 | }, 476 | layers: function() { 477 | var self = this, 478 | layersHTML = []; 479 | $.each(this.current.layers, function(index, layer) { 480 | var x = self.zoomSize( layer.rect.x ), 481 | y = self.zoomSize( layer.rect.y ), 482 | width = self.zoomSize( layer.rect.width ), 483 | height = self.zoomSize( layer.rect.height ), 484 | classNames = ['layer']; 485 | 486 | classNames.push('layer-' + layer.objectID); 487 | if(self.selectedIndex == index) classNames.push('selected'); 488 | layersHTML.push([ 489 | '
    ', 490 | '', 491 | '', 492 | '
    ' 493 | ].join('')); 494 | }); 495 | $('#layers').html(layersHTML.join('')); 496 | }, 497 | slices: function(){ 498 | if(!this.project.slices){ 499 | return false; 500 | } 501 | var self = this, 502 | sliceListHTML = []; 503 | sliceListHTML.push(''); 518 | if(this.project.slices.length > 0){ 519 | $('#slices').html(sliceListHTML.join('')); 520 | } 521 | return this; 522 | }, 523 | colors: function(colors){ 524 | if(!this.project.colors){ 525 | return false; 526 | } 527 | var self = this, 528 | colorListHTML = []; 529 | this.project.colorNames = {}; 530 | colorListHTML.push(''); 543 | if(this.project.colors.length > 0){ 544 | $('#colors').html(colorListHTML.join('')); 545 | } 546 | return this; 547 | }, 548 | notes: function(){ 549 | var self = this, 550 | notesHTML = []; 551 | $.each(this.current.notes, function(index, note){ 552 | notesHTML.push('
    ' + note.note + '
    '); 553 | }) 554 | $('#notes').html(notesHTML.join('')); 555 | }, 556 | getEdgeRect: function( event ){ 557 | var offset = $('#screen').offset(); 558 | var x = (event.pageX - offset.left) / this.configs.zoom, 559 | y = (event.pageY - offset.top) / this.configs.zoom, 560 | width = 10, 561 | height = 10, 562 | xScope = ( x >= 0 && x <= this.current.width ), 563 | yScope = ( y >= 0 && y <= this.current.height ); 564 | // left and top 565 | if( x <= 0 && y <= 0 ){ 566 | x = -10; 567 | y = -10; 568 | } 569 | // right and top 570 | else if( x >= this.current.width && y <= 0 ){ 571 | x = this.current.width; 572 | y = -10; 573 | } 574 | // right and bottom 575 | else if( x >= this.current.width && y >= this.current.height ){ 576 | x = this.current.width; 577 | y = this.current.height; 578 | } 579 | // left and bottom 580 | else if( x <= 0 && y >= this.current.height ){ 581 | x = -10; 582 | y = this.current.height; 583 | } 584 | // top 585 | else if( y <= 0 && xScope ){ 586 | x = 0; 587 | y = -10; 588 | width = this.current.width; 589 | } 590 | // right 591 | else if( x >= this.current.width && yScope ){ 592 | x = this.current.width; 593 | y = 0; 594 | height = this.current.height; 595 | } 596 | // bottom 597 | else if( y >= this.current.height && xScope ){ 598 | x = 0; 599 | y = this.current.height; 600 | width = this.current.width; 601 | } 602 | // left 603 | else if( x <= 0 && yScope ){ 604 | x = -10; 605 | y = 0; 606 | height = this.current.height; 607 | } 608 | if( xScope && yScope ){ 609 | x = 0; 610 | y = 0; 611 | width = this.current.width; 612 | height = this.current.height; 613 | } 614 | return { 615 | x: x, 616 | y: y, 617 | width: width, 618 | height: height, 619 | maxX: x + width, 620 | maxY: y + height 621 | } 622 | }, 623 | distance: function(){ 624 | if( ( this.selectedIndex && this.targetIndex && this.selectedIndex != this.targetIndex ) || ( this.selectedIndex && this.tempTargetRect ) ){ 625 | var selectedRect = this.getRect(this.selectedIndex), 626 | targetRect = this.tempTargetRect || this.getRect(this.targetIndex), 627 | topData, rightData, bottomData, leftData, 628 | x = this.zoomSize(selectedRect.x + selectedRect.width / 2), 629 | y = this.zoomSize(selectedRect.y + selectedRect.height / 2); 630 | if(!this.isIntersect(selectedRect, targetRect)){ 631 | if(selectedRect.y > targetRect.maxY){ //top 632 | topData = { 633 | x: x, 634 | y: this.zoomSize(targetRect.maxY), 635 | h: this.zoomSize(selectedRect.y - targetRect.maxY), 636 | distance: this.unitSize(selectedRect.y - targetRect.maxY), 637 | percentageDistance: this.percentageSize((selectedRect.y - targetRect.maxY), this.current.height) 638 | }; 639 | } 640 | if(selectedRect.maxX < targetRect.x){ //right 641 | rightData = { 642 | x: this.zoomSize(selectedRect.maxX), 643 | y: y, 644 | w: this.zoomSize(targetRect.x - selectedRect.maxX), 645 | distance: this.unitSize(targetRect.x - selectedRect.maxX), 646 | percentageDistance: this.percentageSize((targetRect.x - selectedRect.maxX), this.current.width) 647 | } 648 | } 649 | if(selectedRect.maxY < targetRect.y){ //bottom 650 | bottomData = { 651 | x: x, 652 | y: this.zoomSize(selectedRect.maxY), 653 | h: this.zoomSize(targetRect.y - selectedRect.maxY), 654 | distance: this.unitSize(targetRect.y - selectedRect.maxY), 655 | percentageDistance: this.percentageSize((targetRect.y - selectedRect.maxY), this.current.height) 656 | } 657 | } 658 | if(selectedRect.x > targetRect.maxX){ //left 659 | leftData = { 660 | x: this.zoomSize(targetRect.maxX), 661 | y: y, 662 | w: this.zoomSize(selectedRect.x - targetRect.maxX), 663 | distance: this.unitSize(selectedRect.x - targetRect.maxX), 664 | percentageDistance: this.percentageSize((selectedRect.x - targetRect.maxX), this.current.width) 665 | } 666 | } 667 | } 668 | else{ 669 | var distance = this.getDistance(selectedRect, targetRect); 670 | if (distance.top != 0) { //top 671 | topData = { 672 | x: x, 673 | y: (distance.top > 0)? this.zoomSize(targetRect.y): this.zoomSize(selectedRect.y), 674 | h: this.zoomSize(this.positive(distance.top)), 675 | distance: this.unitSize(this.positive(distance.top)), 676 | percentageDistance: this.percentageSize(this.positive(distance.top), this.current.height) 677 | }; 678 | } 679 | if (distance.right != 0) { //right 680 | rightData = { 681 | x: (distance.right > 0)? this.zoomSize(selectedRect.maxX): this.zoomSize(targetRect.maxX), 682 | y: y, 683 | w: this.zoomSize(this.positive(distance.right)), 684 | distance: this.unitSize(this.positive(distance.right)), 685 | percentageDistance: this.percentageSize(this.positive(distance.right), this.current.width) 686 | }; 687 | } 688 | if (distance.bottom != 0) { //bottom 689 | bottomData = { 690 | x: x, 691 | y: (distance.bottom > 0)? this.zoomSize(selectedRect.maxY): this.zoomSize(targetRect.maxY), 692 | h: this.zoomSize(this.positive(distance.bottom)), 693 | distance: this.unitSize(this.positive(distance.bottom)), 694 | percentageDistance: this.percentageSize(this.positive(distance.bottom), this.current.height) 695 | }; 696 | } 697 | if (distance.left != 0) { //left 698 | leftData = { 699 | x: (distance.left > 0)? this.zoomSize(targetRect.x): this.zoomSize(selectedRect.x), 700 | y: y, 701 | w: this.zoomSize(this.positive(distance.left)), 702 | distance: this.unitSize(this.positive(distance.left)), 703 | percentageDistance: this.percentageSize(this.positive(distance.left), this.current.width) 704 | }; 705 | } 706 | } 707 | if (topData) { 708 | $('#td') 709 | .css({ 710 | left: topData.x, 711 | top: topData.y, 712 | height: topData.h, 713 | }) 714 | .show(); 715 | $('#td div') 716 | .attr('data-height', topData.distance) 717 | .attr('percentage-height', topData.percentageDistance); 718 | } 719 | if (rightData) { 720 | $('#rd') 721 | .css({ 722 | left: rightData.x, 723 | top: rightData.y, 724 | width: rightData.w 725 | }) 726 | .show(); 727 | $('#rd div') 728 | .attr('data-width', rightData.distance ) 729 | .attr('percentage-width', rightData.percentageDistance); 730 | } 731 | if (bottomData) { 732 | $('#bd') 733 | .css({ 734 | left: bottomData.x, 735 | top: bottomData.y, 736 | height: bottomData.h, 737 | }) 738 | .show(); 739 | $('#bd div') 740 | .attr('data-height', bottomData.distance ) 741 | .attr('percentage-height', bottomData.percentageDistance); 742 | } 743 | if (leftData) { 744 | $('#ld') 745 | .css({ 746 | left: leftData.x, 747 | top: leftData.y, 748 | width: leftData.w 749 | }) 750 | .show(); 751 | $('#ld div') 752 | .attr('data-width', leftData.distance ) 753 | .attr('percentage-width', leftData.percentageDistance); 754 | } 755 | $('.selected').addClass('hidden'); 756 | } 757 | }, 758 | inspector: function(){ 759 | if(!this.selectedIndex || (!this.current && !this.current.layers && !this.current.layers[this.selectedIndex])) return false; 760 | var self = this, 761 | layerData = this.current.layers[this.selectedIndex], 762 | html = []; 763 | html.push('

    ' + layerData.name + '

    '); 764 | // fix 0 [opacity] 765 | // PROPERTIES 766 | var position = [ 767 | '
    ', 768 | '', 769 | '', 770 | '
    ' 771 | ].join(''), 772 | size = [ 773 | '
    ', 774 | '', 775 | '', 776 | '
    ' 777 | ].join(''), 778 | opacity = (typeof layerData.opacity == 'number')? [ 779 | '
    ', 780 | '', 781 | '', 782 | '
    ' 783 | ].join(''): '', 784 | radius = (layerData.radius)? [ 785 | '
    ', 786 | '', 787 | '', 788 | '
    ' 789 | ].join(''): '', 790 | styleName = (layerData.styleName)? [ 791 | '
    ', 792 | '', 793 | '
    ' 794 | ].join(''): ''; 795 | html.push(this.propertyType('PROPERTIES', [ position, size, opacity, radius, styleName ].join(''))); 796 | // FILLS 797 | if(layerData.fills && layerData.fills.length > 0){ 798 | var fills = [], 799 | fillsData = $.extend(true, [], layerData.fills); 800 | $.each(fillsData.reverse(), function(index, fill){ 801 | fills.push('
    '); 802 | if (fill.fillType == "color") { 803 | fills.push( self.colorItem(fill.color) ); 804 | } 805 | else{ 806 | fills.push('
    '); 807 | $.each(fill.gradient.colorStops, function(index, gradient) { 808 | fills.push(self.colorItem(gradient.color)); 809 | }); 810 | fills.push('
    '); 811 | } 812 | fills.push('
    '); 813 | }); 814 | html.push(this.propertyType('FILLS', fills.join(''))); 815 | } 816 | // TYPEFACE 817 | if(layerData.type == 'text'){ 818 | var fontFamily = [ 819 | '
    ', 820 | '', 821 | '
    ' 822 | ].join(''), 823 | textColor = [ 824 | '
    ', 825 | '
    ', 826 | self.colorItem(layerData.color), 827 | '
    ', 828 | '
    ' 829 | ].join(''), 830 | fontSize = (layerData.fontSize)? [ 831 | '
    ', 832 | '', 833 | '', 834 | '
    ' 835 | ].join(''): '', 836 | spacing = [ 837 | '
    ', 838 | '', 839 | '', 840 | '
    ' 841 | ].join(''), 842 | textStyle = (layerData.textStyle) ? [ 843 | '
    ', 844 | '', 845 | '
    ' 846 | ].join(''): '', 847 | content = [ 848 | '
    ', 849 | '', 850 | '
    ' 851 | ].join(''); 852 | html.push(this.propertyType('TYPEFACE', [ textStyle, fontFamily, textColor, fontSize, spacing, content ].join(''))); 853 | } 854 | // BORDERS 855 | if(layerData.borders && layerData.borders.length > 0){ 856 | var borders = [], 857 | bordersData = $.extend(true, [], layerData.borders); 858 | $.each(bordersData.reverse(), function(index, border) { 859 | borders.push( 860 | '
    ', 861 | '

    ' + _(border.position.firstUpperCase() + ' Border') + '

    ', 862 | '
    ', 863 | '', 864 | '', 865 | '
    '); 866 | borders.push('
    '); 867 | if (border.fillType == "color") { 868 | borders.push(self.colorItem(border.color)); 869 | } 870 | else{ 871 | borders.push('
    '); 872 | $.each(border.gradient.colorStops, function(index, gradient) { 873 | borders.push(self.colorItem(gradient.color)); 874 | }); 875 | borders.push('
    '); 876 | } 877 | borders.push('
    '); 878 | borders.push('
    '); 879 | }); 880 | html.push(this.propertyType('BORDERS', borders.join(''))); 881 | } 882 | // SHADOWS 883 | if(layerData.shadows && layerData.shadows.length > 0){ 884 | var shadows = [], 885 | shadowsData = $.extend(true, [], layerData.shadows); 886 | $.each(shadowsData.reverse(), function(index, shadow) { 887 | shadows.push( 888 | '
    ', 889 | '

    ' + _(shadow.type.firstUpperCase() + ' Shadow') + '

    ', 890 | '
    ', 891 | '', 892 | '', 893 | '
    ', 894 | '
    ', 895 | '', 896 | '', 897 | '
    ', 898 | '
    ', 899 | self.colorItem(shadow.color), 900 | '
    ', 901 | '
    ' 902 | ); 903 | }); 904 | html.push(this.propertyType('SHADOWS', shadows.join(''))); 905 | } 906 | // CODE TEMPLATE 907 | var tab = [ 908 | ''].join('') 914 | 915 | var css = []; 916 | var css = [ 917 | '
    ', 918 | '', 919 | '
    '].join('') 920 | 921 | var rncss = []; 922 | var rncss = [ 923 | '
    ', 924 | '', 925 | '
    '].join('') 926 | 927 | var android = []; 928 | if(layerData.type == "text"){ 929 | android.push( 930 | '
    ', 931 | '', 935 | '
    ' 936 | ); 937 | }else if (layerData.type == "shape"){ 938 | android.push( 939 | '
    ', 940 | '', 944 | '
    ' 945 | ); 946 | } else if (layerData.type = "slice"){ 947 | android.push( 948 | '
    ', 949 | '', 953 | '
    ' 954 | ); 955 | } 956 | 957 | var ios = []; 958 | if(layerData.type == "text"){ 959 | ios.push( 960 | '
    ', 961 | '', 969 | '
    ' 970 | ); 971 | }else if (layerData.type == "shape"){ 972 | ios.push( 973 | '
    ', 974 | '', 980 | '
    ' 981 | ); 982 | } else if (layerData.type = "slice"){ 983 | ios.push( 984 | '
    ', 985 | '', 991 | '
    ' 992 | ); 993 | } 994 | html.push(this.propertyType('CODE TEMPLATE', [ tab, rncss, css, android.join(''), ios.join('') ].join(''), true)); 995 | 996 | // EXPORTABLE 997 | if(layerData.exportable && layerData.exportable.length > 0){ 998 | var expHTML = [], 999 | path = 'assets/' 1000 | expHTML.push('') 1009 | html.push(this.propertyType('EXPORTABLE', expHTML.join(''))); 1010 | } 1011 | self.renderInspector(html); 1012 | }, 1013 | getAndroidWithHeight: function (layerData) { 1014 | return "android:layout_width=\"" + this.unitSize(layerData.rect.width, false) + "\"\r\n" + "android:layout_height=\"" + this.unitSize(layerData.rect.height, false) + "\"\r\n"; 1015 | }, 1016 | getAndroidShapeBackground: function (layerData) { 1017 | var colorCode = ""; 1018 | if (layerData.type != "shape" || typeof(layerData.fills) == 'undefined' || layerData.fills.length == 0) return colorCode; 1019 | var f; 1020 | for (f in layerData.fills) { 1021 | if(layerData.fills[f].fillType == "color"){ 1022 | return "android:background=\"" + layerData.fills[f].color["argb-hex"] + "\"\r\n"; 1023 | } 1024 | } 1025 | return colorCode; 1026 | }, 1027 | getAndroidImageSrc: function (layerData) { 1028 | if (layerData.type != "slice" || typeof(layerData.exportable) == 'undefined' || layerData.exportable == 0) return ""; 1029 | return "android:src=\"\@mipmap/" + layerData.exportable[0].name + "." + layerData.exportable[0].format + "\"\r\n"; 1030 | }, 1031 | getIOSShapeBackground: function (layerData) { 1032 | var colorCode = ""; 1033 | if (layerData.type != "shape" || typeof(layerData.fills) == 'undefined' || layerData.fills.length == 0) return colorCode; 1034 | var f; 1035 | for (f in layerData.fills) { 1036 | if(layerData.fills[f].fillType == "color"){ 1037 | return "view.backgroundColor = [UIColor colorWithRed:" + layerData.fills[f].color.r + "/255.0 green:" + layerData.fills[f].color.g + "/255.0 blue:" + layerData.fills[f].color.b + "/255.0 alpha:" + layerData.fills[f].color.a + "/1.0]\;\r\n"; 1038 | } 1039 | } 1040 | return colorCode; 1041 | }, 1042 | getIOSImageSrc: function (layerData) { 1043 | if (layerData.type != "slice" || typeof(layerData.exportable) == 'undefined' || layerData.exportable == 0) return ""; 1044 | return "imageView.image = [UIImage imageNamed:\@\"" + layerData.exportable[0].name + "." + layerData.exportable[0].format + "\"];\r\n"; 1045 | }, 1046 | renderInspector: function (html) { 1047 | var self = this; 1048 | $('#inspector').addClass('active').html(html.join('')); 1049 | $('#inspector').find('[data-codeType=' + self.configs.codeType +']').addClass('select'); 1050 | $('#code-tab').unbind('click') 1051 | .on('click', 'li', function(){ 1052 | var $this = $(this), id = $this.attr('data-id'); 1053 | self.configs.codeType = $(this).attr('data-codeType') 1054 | $this.parent().find('li.select').removeClass('select') 1055 | $this.addClass('select') 1056 | $("#inspector").find('div.item.select').removeClass('select'); 1057 | $("#inspector").find("#"+id).addClass('select'); 1058 | }); 1059 | $('#code-tab').find('li.select').trigger('click'); 1060 | }, 1061 | propertyType: function(title, content, isCode){ 1062 | var nopadding = isCode? ' style="padding:0"': ''; 1063 | return ['
    ', 1064 | '

    ' + _(title) + '

    ', 1065 | '
    ', 1066 | content, 1067 | '
    ', 1068 | '
    '].join(''); 1069 | }, 1070 | colorItem: function(color){ 1071 | var colorName = (this.project.colorNames)? this.project.colorNames[color['argb-hex']]: ''; 1072 | colorName = (colorName)? ' data-name="' + colorName + '"': ''; 1073 | return [ 1074 | '
    ', 1075 | '', 1076 | '
    '].join(''); 1077 | }, 1078 | changeColorFormat: function(){ 1079 | var self = this; 1080 | $('.color input').each(function(){ 1081 | var $this = $(this), 1082 | colors = JSON.parse( decodeURI( $this.attr('data-color') ) ); 1083 | $this.val(colors[self.configs.colorFormat]); 1084 | }); 1085 | this.colors(); 1086 | }, 1087 | selectedLayer: function(){ 1088 | if( this.selectedIndex == undefined ) return false; 1089 | $('.selected').removeClass('selected'); 1090 | $('#layer-' + this.selectedIndex).addClass('selected'); 1091 | $('#rulers').hide(); 1092 | }, 1093 | removeSelected: function(){ 1094 | if(!this.selectedIndex) return false; 1095 | $('#layer-' + this.selectedIndex).removeClass('selected'); 1096 | $('#rulers').hide(); 1097 | }, 1098 | mouseoverLayer: function(){ 1099 | if( this.targetIndex && this.selectedIndex == this.targetIndex ) return false; 1100 | var $target = $('#layer-' + this.targetIndex); 1101 | $target.addClass('hover'); 1102 | $('#rv').css({ 1103 | left: $target.position().left, 1104 | width: $target.outerWidth() 1105 | }); 1106 | $('#rh').css({ 1107 | top: $target.position().top, 1108 | height: $target.outerHeight() 1109 | }); 1110 | $('#rulers').show(); 1111 | }, 1112 | mouseoutLayer: function(){ 1113 | $('.hover').removeClass('hover'); 1114 | $('#rulers').hide(); 1115 | }, 1116 | hideDistance: function(){ 1117 | $('#td').hide(); 1118 | $('#rd').hide(); 1119 | $('#bd').hide(); 1120 | $('#ld').hide(); 1121 | $('.selected').removeClass('hidden'); 1122 | }, 1123 | zoomRender: function(){ 1124 | var self = this; 1125 | this.targetIndex = undefined; 1126 | $('#rulers').hide(); 1127 | this.hideDistance(); 1128 | this.zoom(); 1129 | this.screen(); 1130 | $('#layers, #notes').html(''); 1131 | setTimeout(function(){ self.layers(); self.notes(); }, 150); 1132 | }, 1133 | events: function() { 1134 | var self = this; 1135 | $('body') 1136 | .on({ 1137 | click: function( event ){ 1138 | if(!$('.screen-viewer').hasClass('moving-screen')){ 1139 | if( $(event.target).hasClass('layer') || $(event.target).hasClass('slice-layer')){ 1140 | var selected = (!$(event.target).hasClass('slice-layer'))? event.target: $('.layer-' + $(event.target).attr('data-objectid')).first(); 1141 | self.selectedIndex = self.getIndex(selected); 1142 | self.hideDistance(); 1143 | self.mouseoutLayer(); 1144 | self.selectedLayer(); 1145 | self.inspector(); 1146 | } 1147 | else{ 1148 | self.removeSelected(); 1149 | self.hideDistance(); 1150 | $('#inspector').removeClass('active'); 1151 | self.selectedIndex = undefined; 1152 | self.tempTargetRect = undefined; 1153 | } 1154 | } 1155 | }, 1156 | mousemove: function( event ){ 1157 | if(!$('.screen-viewer').hasClass('moving-screen')){ 1158 | self.mouseoutLayer(); 1159 | self.hideDistance(); 1160 | if( $(event.target).hasClass('screen-viewer') || $(event.target).hasClass('screen-viewer-inner') ){ 1161 | self.tempTargetRect = self.getEdgeRect( event ); 1162 | self.targetIndex = undefined; 1163 | self.distance(); 1164 | } 1165 | else if($(event.target).hasClass('layer')){ 1166 | self.targetIndex = self.getIndex(event.target); 1167 | self.tempTargetRect = undefined; 1168 | self.mouseoverLayer(); 1169 | self.distance(); 1170 | } 1171 | else{ 1172 | self.tempTargetRect = undefined; 1173 | } 1174 | 1175 | } 1176 | } 1177 | }) 1178 | .on('click', 'header, #inspector, .navbar', function( event ){ 1179 | event.stopPropagation(); 1180 | }) 1181 | .on("dragstart", ".exportable img", function(event){ 1182 | var jQThis = $(this), 1183 | offset = jQThis.offset(); 1184 | jQThis.css({width: "auto", height: "auto"}); 1185 | var left = event.originalEvent.pageX - offset.left - jQThis.width() / 2, 1186 | top = event.originalEvent.pageY - offset.top - jQThis.height() / 2; 1187 | jQThis.css({left: left, top: top}); 1188 | }) 1189 | .on("dragend", ".exportable img", function(event){ 1190 | var jQThis = $(this); 1191 | jQThis.css({left: 0, top: 0, width: "100%", height: "100%"}); 1192 | }); 1193 | // zoom 1194 | $('#zoom') 1195 | .on('click', '.zoom-in:not(disabled)',function( event ){ 1196 | self.configs.zoom -= .25; 1197 | self.zoomRender(); 1198 | event.stopPropagation(); 1199 | }) 1200 | .on('click', '.zoom-out:not(disabled)',function( event ){ 1201 | self.configs.zoom += .25; 1202 | self.zoomRender(); 1203 | event.stopPropagation(); 1204 | }); 1205 | $(window) 1206 | .keydown(function(event){ 1207 | if((event.ctrlKey || event.metaKey) && (event.which == 187 || event.which == 189 || event.which == 48)) { 1208 | if(event.which == 187 && self.configs.zoom < 4){ 1209 | $('.zoom-out').click(); 1210 | } 1211 | else if(event.which == 189 && self.configs.zoom > .25){ 1212 | $('.zoom-in').click(); 1213 | } 1214 | else if(event.which == 48){ 1215 | self.maxSize = undefined; 1216 | self.configs.zoom = 1; 1217 | self.zoomRender(); 1218 | } 1219 | event.preventDefault(); 1220 | return false; 1221 | } 1222 | else if(event.which == 32 ){ 1223 | $('#cursor').show(); 1224 | $('.screen-viewer').addClass('moving-screen'); 1225 | self.mouseoutLayer(); 1226 | self.hideDistance(); 1227 | event.preventDefault(); 1228 | } 1229 | else if(event.which == 18){ 1230 | $('#screen').addClass('percentage-mode'); 1231 | } 1232 | }) 1233 | .keyup(function(event){ 1234 | if(event.which == 32 ){ 1235 | $('#cursor').hide(); 1236 | $('.screen-viewer').removeClass('moving-screen'); 1237 | $('#cursor').removeClass('moving'); 1238 | event.preventDefault(); 1239 | } 1240 | else if(event.which == 18){ 1241 | $('#screen').removeClass('percentage-mode'); 1242 | event.preventDefault(); 1243 | } 1244 | }) 1245 | .mousemove(function(event){ 1246 | $('#cursor') 1247 | .css({ 1248 | left: event.clientX, 1249 | top: event.clientY 1250 | }); 1251 | var $target = $(event.target); 1252 | if( 1253 | $('.screen-viewer').hasClass('moving-screen') && 1254 | $('#cursor').hasClass('moving') 1255 | ){ 1256 | $('.screen-viewer').scrollLeft((self.moveData.x - event.clientX) + self.moveData.scrollLeft); 1257 | $('.screen-viewer').scrollTop((self.moveData.y - event.clientY) + self.moveData.scrollTop); 1258 | event.preventDefault(); 1259 | } 1260 | }) 1261 | .mousedown(function(event){ 1262 | var $target = $(event.target); 1263 | if( 1264 | $('.screen-viewer').hasClass('moving-screen') && 1265 | ( 1266 | $target.hasClass('cursor') || 1267 | $target.hasClass('overlay') 1268 | ) 1269 | ){ 1270 | self.moveData = { 1271 | x: event.clientX, 1272 | y: event.clientY, 1273 | scrollLeft: $('.screen-viewer').scrollLeft(), 1274 | scrollTop: $('.screen-viewer').scrollTop() 1275 | } 1276 | $('#cursor').addClass('moving'); 1277 | event.preventDefault(); 1278 | } 1279 | }) 1280 | .mouseup(function(event){ 1281 | var $target = $(event.target); 1282 | if( 1283 | $('.screen-viewer').hasClass('moving-screen') 1284 | ){ 1285 | $('#cursor').removeClass('moving'); 1286 | event.preventDefault(); 1287 | } 1288 | }) 1289 | 1290 | // unit 1291 | $('#unit') 1292 | .on('change', 'input[name=resolution]', function(){ 1293 | var $checked = $('input[name=resolution]:checked'); 1294 | self.configs.unit = $checked.attr('data-unit'); 1295 | self.configs.scale = Number( $checked.attr('data-scale') ); 1296 | self.targetID = undefined; 1297 | self.layers(); 1298 | self.inspector(); 1299 | $('#unit') 1300 | .blur() 1301 | .find('p') 1302 | .text( 1303 | $checked.attr('data-name') 1304 | ); 1305 | self.slices(); 1306 | }) 1307 | .on('click', 'h3, .overlay', function(){ 1308 | $('#unit').blur(); 1309 | }); 1310 | $('#inspector').on('dblclick', 'input, textarea', function(){ 1311 | $(this).select(); 1312 | }); 1313 | $('#show-notes').change(function(){ 1314 | if( this.checked ){ 1315 | $('#notes').fadeIn('fast'); 1316 | } 1317 | else{ 1318 | $('#notes').fadeOut('fast'); 1319 | } 1320 | }); 1321 | $('#artboards') 1322 | .on('click', '.artboard', function( event ){ 1323 | if(self.artboardIndex == self.getIndex(this)) return; 1324 | self.maxSize = undefined; 1325 | self.artboardIndex = self.getIndex(this); 1326 | self.selectedIndex = undefined; 1327 | self.current = self.project.artboards[self.artboardIndex]; 1328 | self.hideDistance(); 1329 | self.screen(); 1330 | self.layers(); 1331 | self.notes(); 1332 | $('.active').removeClass('active'); 1333 | $(this).addClass('active'); 1334 | self.locationHash({ 1335 | artboard: self.artboardIndex 1336 | }); 1337 | }) 1338 | .on('click', '.pages-select', function( event ){ 1339 | event.stopPropagation(); 1340 | }) 1341 | .on('change', 'input[name=page]', function(event){ 1342 | var pObjectID = $('.page-list input[name=page]:checked').val(); 1343 | $('.pages-select h3').html($(this).parent().find('span').html()); 1344 | $('.artboard-list li').show(); 1345 | if(pObjectID != 'all'){ 1346 | $('.artboard-list li').each(function(){ 1347 | var pageObjectID = $( this ).attr('data-page-id'); 1348 | if(pObjectID != pageObjectID){ 1349 | $( this ).hide(); 1350 | } 1351 | }); 1352 | } 1353 | $('.pages-select').blur(); 1354 | event.stopPropagation(); 1355 | }); 1356 | $('#slices') 1357 | .on('mouseover', 'li', function(){ 1358 | var id = $(this).attr('data-objectid'); 1359 | $('.layer-' + id).addClass('has-slice'); 1360 | }) 1361 | .on('mouseout', 'li', function(){ 1362 | $('.has-slice').removeClass('has-slice'); 1363 | }) 1364 | .on('click', 'li', function(){ 1365 | var id = $(this).attr('data-objectid'); 1366 | if($('.layer-' + id).length > 0){ 1367 | var offsets = $('.layer-' + id).offset(), 1368 | scrolls = { 1369 | left: $('.screen-viewer').scrollLeft(), 1370 | top: $('.screen-viewer').scrollTop() 1371 | }, 1372 | sizes = { 1373 | width: $('.layer-' + id).outerWidth(), 1374 | height: $('.layer-' + id).outerHeight() 1375 | }, 1376 | viewerSizes = { 1377 | width: $('.screen-viewer').outerWidth(), 1378 | height: $('.screen-viewer').outerHeight() 1379 | }; 1380 | $('.screen-viewer').animate({ 1381 | scrollLeft: ( offsets.left + scrolls.left) - ( ( viewerSizes.width - sizes.width ) / 2 ), 1382 | scrollTop: ( offsets.top + scrolls.top - 60) - ( ( viewerSizes.height - sizes.height ) / 2 ) 1383 | }, 150); 1384 | $('.layer-' + id).click(); 1385 | } 1386 | else{ 1387 | self.message(_('The slice not in current artboard.')); 1388 | } 1389 | 1390 | }); 1391 | // color format 1392 | $('#inspector') 1393 | .on('click', '.color label', function(){ 1394 | self.configs.colorFormat = 1395 | ( self.configs.colorFormat == 'color-hex')? 'argb-hex': 1396 | ( self.configs.colorFormat == 'argb-hex')? 'css-rgba': 1397 | ( self.configs.colorFormat == 'css-rgba')? 'ui-color': 1398 | 'color-hex'; 1399 | self.changeColorFormat(); 1400 | }); 1401 | // tab 1402 | $('.tab') 1403 | .on('click', 'li', function(){ 1404 | var $this = $(this), 1405 | id = $this.attr('data-id'); 1406 | 1407 | if($this.hasClass('current')){ 1408 | $('.current').removeClass('current'); 1409 | $('.navbar').removeClass('on'); 1410 | } 1411 | else{ 1412 | $('.current').removeClass('current'); 1413 | $('.navbar section').hide(); 1414 | $this.addClass('current'); 1415 | $('#' + id).show(); 1416 | $('.navbar').addClass('on'); 1417 | } 1418 | 1419 | }); 1420 | $('#notes') 1421 | .on('mousemove', '.note', function(event){ 1422 | event.stopPropagation(); 1423 | self.mouseoutLayer(); 1424 | self.hideDistance(); 1425 | $(this).find('div').show(); 1426 | var width = $(this).find('div').outerWidth(); 1427 | if(width > 160){ 1428 | $(this).find('div').css({ 1429 | width: 160, 1430 | 'white-space': 'normal' 1431 | }) 1432 | } 1433 | }) 1434 | .on('mouseout', '.note', function(){ 1435 | $(this).find('div').hide(); 1436 | }); 1437 | 1438 | } 1439 | } 1440 | var init = SMApp.fn.init = function(project) { 1441 | var path = this.locationHash(); 1442 | this.project = project; 1443 | this.configs = { 1444 | scale: this.project.scale, 1445 | unit: this.project.unit, 1446 | colorFormat: this.project.colorFormat, 1447 | codeType: 'css' 1448 | }; 1449 | this.artboardIndex = (!isNaN(path.artboard))? path.artboard: 0; 1450 | this.current = this.project.artboards[this.artboardIndex]; 1451 | var proportion = $(document).height() / this.current.height; 1452 | if (proportion >= .8) { 1453 | this.configs.zoom = 1; 1454 | } else if (proportion >= .7) { 1455 | this.configs.zoom = .75; 1456 | } else { 1457 | this.configs.zoom = .5; 1458 | } 1459 | this.render(); 1460 | if(!isNaN(path.artboard)){ 1461 | $('.current').removeClass('current'); 1462 | $('.navbar').removeClass('on'); 1463 | } 1464 | if(this.current.imageBase64){ 1465 | $('.tab').remove(); 1466 | $('.navbar').remove(); 1467 | } 1468 | return this; 1469 | }; 1470 | init.prototype = SMApp.fn; 1471 | window.SMApp = SMApp; 1472 | })(window); -------------------------------------------------------------------------------- /assets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress{vertical-align:baseline}template,[hidden]{display:none}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:0;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:bold}button,input{overflow:visible}button,select{text-transform:none}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit} -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { resolve, basename, extname } = require('path') 4 | const program = require('commander') 5 | const pkg = require('../package.json') 6 | const convert = require('../src') 7 | let debug = () => {} 8 | 9 | program 10 | .version(pkg.version) 11 | 12 | program 13 | .command('convert ') 14 | .alias('c') 15 | .description('convert sketch file to static html pages') 16 | .option('-d, --dest ', 'Dest directory which html pages generate to.') 17 | .option('-v, --verbose', 'print details when execute commands.') 18 | .action((sketchFile, options) => { 19 | const src = resolve(sketchFile) 20 | const dest = resolve( 21 | options.dest || basename(sketchFile, extname(sketchFile)) 22 | ) 23 | // load debug module after set process.env.DEBUG 24 | if (options.verbose) { 25 | process.env.DEBUG = 'sketch-measure-cli,sketch-measure-core' 26 | debug = require('debug')('sketch-measure-cli') 27 | } 28 | debug('src: %s', dest) 29 | debug('dest: %s', dest) 30 | convert(src, dest) 31 | .then(() => { 32 | console.log('') 33 | console.log(' Success!') 34 | console.log(` Open file:///${dest.slice(1)}/index.html in browser.`) 35 | console.log(' And you can start a static server for better experience.') 36 | console.log('') 37 | }) 38 | .catch(console.error.bind(console)) 39 | }) 40 | 41 | program.parse(process.argv) 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sketch-measure-cli", 3 | "version": "2.2.1", 4 | "description": "Cli tool for sketch-measure plugin", 5 | "main": "src/index.js", 6 | "bin": { 7 | "sketch-measure": "bin/index.js" 8 | }, 9 | "dependencies": { 10 | "commander": "^3.0.2", 11 | "debug": "^4.1.0", 12 | "mkdirp": "^0.5.1", 13 | "tempfile": "^3.0.0", 14 | "unzipper": "^0.10.5" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^6.5.1", 18 | "eslint-config-standard": "^14.1.0", 19 | "eslint-plugin-import": "^2.18.2", 20 | "eslint-plugin-node": "^10.0.0", 21 | "eslint-plugin-promise": "^4.2.1", 22 | "eslint-plugin-standard": "^4.0.1", 23 | "mocha": "^6.2.2" 24 | }, 25 | "scripts": { 26 | "lint": "eslint src bin test --fix", 27 | "test": "mocha test/*.spec.js" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/devsigners/sketch-measure-cli.git" 32 | }, 33 | "keywords": [ 34 | "sketch-measure", 35 | "sketch" 36 | ], 37 | "author": "creeperyang", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/devsigners/sketch-measure-cli/issues" 41 | }, 42 | "homepage": "https://github.com/devsigners/sketch-measure-cli#readme" 43 | } 44 | -------------------------------------------------------------------------------- /src/deps/decodeUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on https://github.com/FourwingsY/react-sketch-viewer 3 | */ 4 | 5 | // from https://www.npmjs.com/package/bplist-parser 6 | const debug = false; 7 | 8 | const maxObjectSize = 100 * 1000 * 1000; // 100Meg 9 | const maxObjectCount = 32768; 10 | 11 | // EPOCH = new SimpleDateFormat("yyyy MM dd zzz").parse("2001 01 01 GMT").getTime(); 12 | // ...but that's annoying in a static initializer because it can throw exceptions, ick. 13 | // So we just hardcode the correct value. 14 | const EPOCH = 978307200000; 15 | 16 | // UID object definition 17 | var UID = UID = function(id) { 18 | this.UID = id; 19 | } 20 | 21 | var parseBuffer = function (buffer) { 22 | var result = {}; 23 | 24 | // check header 25 | var header = buffer.slice(0, 'bplist'.length).toString('utf8'); 26 | if (header !== 'bplist') { 27 | throw new Error("Invalid binary plist. Expected 'bplist' at offset 0."); 28 | } 29 | 30 | // Handle trailer, last 32 bytes of the file 31 | var trailer = buffer.slice(buffer.length - 32, buffer.length); 32 | // 6 null bytes (index 0 to 5) 33 | var offsetSize = trailer.readUInt8(6); 34 | if (debug) { 35 | console.log("offsetSize: " + offsetSize); 36 | } 37 | var objectRefSize = trailer.readUInt8(7); 38 | if (debug) { 39 | console.log("objectRefSize: " + objectRefSize); 40 | } 41 | var numObjects = readUInt64BE(trailer, 8); 42 | if (debug) { 43 | console.log("numObjects: " + numObjects); 44 | } 45 | var topObject = readUInt64BE(trailer, 16); 46 | if (debug) { 47 | console.log("topObject: " + topObject); 48 | } 49 | var offsetTableOffset = readUInt64BE(trailer, 24); 50 | if (debug) { 51 | console.log("offsetTableOffset: " + offsetTableOffset); 52 | } 53 | 54 | if (numObjects > maxObjectCount) { 55 | throw new Error("maxObjectCount exceeded"); 56 | } 57 | 58 | // Handle offset table 59 | var offsetTable = []; 60 | 61 | for (var i = 0; i < numObjects; i++) { 62 | var offsetBytes = buffer.slice(offsetTableOffset + i * offsetSize, offsetTableOffset + (i + 1) * offsetSize); 63 | offsetTable[i] = readUInt(offsetBytes, 0); 64 | if (debug) { 65 | console.log("Offset for Object #" + i + " is " + offsetTable[i] + " [" + offsetTable[i].toString(16) + "]"); 66 | } 67 | } 68 | 69 | // Parses an object inside the currently parsed binary property list. 70 | // For the format specification check 71 | // 72 | // Apple's binary property list parser implementation. 73 | function parseObject(tableOffset) { 74 | var offset = offsetTable[tableOffset]; 75 | var type = buffer[offset]; 76 | var objType = (type & 0xF0) >> 4; //First 4 bits 77 | var objInfo = (type & 0x0F); //Second 4 bits 78 | switch (objType) { 79 | case 0x0: 80 | return parseSimple(); 81 | case 0x1: 82 | return parseInteger(); 83 | case 0x8: 84 | return parseUID(); 85 | case 0x2: 86 | return parseReal(); 87 | case 0x3: 88 | return parseDate(); 89 | case 0x4: 90 | return parseData(); 91 | case 0x5: // ASCII 92 | return parsePlistString(); 93 | case 0x6: // UTF-16 94 | return parsePlistString(true); 95 | case 0xA: 96 | return parseArray(); 97 | case 0xD: 98 | return parseDictionary(); 99 | default: 100 | throw new Error("Unhandled type 0x" + objType.toString(16)); 101 | } 102 | 103 | function parseSimple() { 104 | //Simple 105 | switch (objInfo) { 106 | case 0x0: // null 107 | return null; 108 | case 0x8: // false 109 | return false; 110 | case 0x9: // true 111 | return true; 112 | case 0xF: // filler byte 113 | return null; 114 | default: 115 | throw new Error("Unhandled simple type 0x" + objType.toString(16)); 116 | } 117 | } 118 | 119 | function bufferToHexString(buffer) { 120 | var str = ''; 121 | var i; 122 | for (i = 0; i < buffer.length; i++) { 123 | if (buffer[i] != 0x00) { 124 | break; 125 | } 126 | } 127 | for (; i < buffer.length; i++) { 128 | var part = '00' + buffer[i].toString(16); 129 | str += part.substr(part.length - 2); 130 | } 131 | return str; 132 | } 133 | 134 | function parseInteger() { 135 | var length = Math.pow(2, objInfo); 136 | if (length > 4) { 137 | var data = buffer.slice(offset + 1, offset + 1 + length); 138 | var str = bufferToHexString(data); 139 | return bigInt(str, 16); 140 | } if (length < maxObjectSize) { 141 | return readUInt(buffer.slice(offset + 1, offset + 1 + length)); 142 | } else { 143 | throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); 144 | } 145 | } 146 | 147 | function parseUID() { 148 | var length = objInfo + 1; 149 | if (length < maxObjectSize) { 150 | return new UID(readUInt(buffer.slice(offset + 1, offset + 1 + length))); 151 | } else { 152 | throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); 153 | } 154 | } 155 | 156 | function parseReal() { 157 | var length = Math.pow(2, objInfo); 158 | if (length < maxObjectSize) { 159 | var realBuffer = buffer.slice(offset + 1, offset + 1 + length); 160 | if (length === 4) { 161 | return realBuffer.readFloatBE(0); 162 | } 163 | else if (length === 8) { 164 | return realBuffer.readDoubleBE(0); 165 | } 166 | } else { 167 | throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); 168 | } 169 | } 170 | 171 | function parseDate() { 172 | if (objInfo != 0x3) { 173 | console.error("Unknown date type :" + objInfo + ". Parsing anyway..."); 174 | } 175 | var dateBuffer = buffer.slice(offset + 1, offset + 9); 176 | return new Date(EPOCH + (1000 * dateBuffer.readDoubleBE(0))); 177 | } 178 | 179 | function parseData() { 180 | var dataoffset = 1; 181 | var length = objInfo; 182 | if (objInfo == 0xF) { 183 | var int_type = buffer[offset + 1]; 184 | var intType = (int_type & 0xF0) / 0x10; 185 | if (intType != 0x1) { 186 | console.error("0x4: UNEXPECTED LENGTH-INT TYPE! " + intType); 187 | } 188 | var intInfo = int_type & 0x0F; 189 | var intLength = Math.pow(2, intInfo); 190 | dataoffset = 2 + intLength; 191 | if (intLength < 3) { 192 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 193 | } else { 194 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 195 | } 196 | } 197 | if (length < maxObjectSize) { 198 | return buffer.slice(offset + dataoffset, offset + dataoffset + length); 199 | } else { 200 | throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); 201 | } 202 | } 203 | 204 | function parsePlistString (isUtf16) { 205 | isUtf16 = isUtf16 || 0; 206 | var enc = "utf8"; 207 | var length = objInfo; 208 | var stroffset = 1; 209 | if (objInfo == 0xF) { 210 | var int_type = buffer[offset + 1]; 211 | var intType = (int_type & 0xF0) / 0x10; 212 | if (intType != 0x1) { 213 | console.err("UNEXPECTED LENGTH-INT TYPE! " + intType); 214 | } 215 | var intInfo = int_type & 0x0F; 216 | var intLength = Math.pow(2, intInfo); 217 | var stroffset = 2 + intLength; 218 | if (intLength < 3) { 219 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 220 | } else { 221 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 222 | } 223 | } 224 | // length is String length -> to get byte length multiply by 2, as 1 character takes 2 bytes in UTF-16 225 | length *= (isUtf16 + 1); 226 | if (length < maxObjectSize) { 227 | var plistString = new Buffer(buffer.slice(offset + stroffset, offset + stroffset + length)); 228 | if (isUtf16) { 229 | plistString = swapBytes(plistString); 230 | enc = "ucs2"; 231 | } 232 | return plistString.toString(enc); 233 | } else { 234 | throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); 235 | } 236 | } 237 | 238 | function parseArray() { 239 | var length = objInfo; 240 | var arrayoffset = 1; 241 | if (objInfo == 0xF) { 242 | var int_type = buffer[offset + 1]; 243 | var intType = (int_type & 0xF0) / 0x10; 244 | if (intType != 0x1) { 245 | console.error("0xa: UNEXPECTED LENGTH-INT TYPE! " + intType); 246 | } 247 | var intInfo = int_type & 0x0F; 248 | var intLength = Math.pow(2, intInfo); 249 | arrayoffset = 2 + intLength; 250 | if (intLength < 3) { 251 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 252 | } else { 253 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 254 | } 255 | } 256 | if (length * objectRefSize > maxObjectSize) { 257 | throw new Error("To little heap space available!"); 258 | } 259 | var array = []; 260 | for (var i = 0; i < length; i++) { 261 | var objRef = readUInt(buffer.slice(offset + arrayoffset + i * objectRefSize, offset + arrayoffset + (i + 1) * objectRefSize)); 262 | array[i] = parseObject(objRef); 263 | } 264 | return array; 265 | } 266 | 267 | function parseDictionary() { 268 | var length = objInfo; 269 | var dictoffset = 1; 270 | if (objInfo == 0xF) { 271 | var int_type = buffer[offset + 1]; 272 | var intType = (int_type & 0xF0) / 0x10; 273 | if (intType != 0x1) { 274 | console.error("0xD: UNEXPECTED LENGTH-INT TYPE! " + intType); 275 | } 276 | var intInfo = int_type & 0x0F; 277 | var intLength = Math.pow(2, intInfo); 278 | dictoffset = 2 + intLength; 279 | if (intLength < 3) { 280 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 281 | } else { 282 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); 283 | } 284 | } 285 | if (length * 2 * objectRefSize > maxObjectSize) { 286 | throw new Error("To little heap space available!"); 287 | } 288 | if (debug) { 289 | console.log("Parsing dictionary #" + tableOffset); 290 | } 291 | var dict = {}; 292 | for (var i = 0; i < length; i++) { 293 | var keyRef = readUInt(buffer.slice(offset + dictoffset + i * objectRefSize, offset + dictoffset + (i + 1) * objectRefSize)); 294 | var valRef = readUInt(buffer.slice(offset + dictoffset + (length * objectRefSize) + i * objectRefSize, offset + dictoffset + (length * objectRefSize) + (i + 1) * objectRefSize)); 295 | var key = parseObject(keyRef); 296 | var val = parseObject(valRef); 297 | if (debug) { 298 | console.log(" DICT #" + tableOffset + ": Mapped " + key + " to " + val); 299 | } 300 | dict[key] = val; 301 | } 302 | return dict; 303 | } 304 | } 305 | 306 | return [ parseObject(topObject) ]; 307 | }; 308 | 309 | function readUInt(buffer, start) { 310 | start = start || 0; 311 | 312 | var l = 0; 313 | for (var i = start; i < buffer.length; i++) { 314 | l <<= 8; 315 | l |= buffer[i] & 0xFF; 316 | } 317 | return l; 318 | } 319 | 320 | // we're just going to toss the high order bits because javascript doesn't have 64-bit ints 321 | function readUInt64BE(buffer, start) { 322 | var data = buffer.slice(start, start + 8); 323 | return data.readUInt32BE(4, 8); 324 | } 325 | 326 | function swapBytes(buffer) { 327 | var len = buffer.length; 328 | for (var i = 0; i < len; i += 2) { 329 | var a = buffer[i]; 330 | buffer[i] = buffer[i+1]; 331 | buffer[i+1] = a; 332 | } 333 | return buffer; 334 | } 335 | 336 | function unarchivePlist(plist) { 337 | const objects = plist["$objects"] 338 | const rootUID = plist["$top"].root.UID 339 | const unarchived = unarchiveObject(objects, objects[rootUID]) 340 | return unarchived 341 | } 342 | 343 | function unarchiveObject(objects, object) { 344 | const type = Object.prototype.toString.call(object) 345 | // apply recursively only when plist is object or array 346 | if (type !== "[object Object]" && type !== "[object Array]") { 347 | return object 348 | } 349 | if (Array.isArray(object)) { 350 | // console.log(object) 351 | const result = [] 352 | for (let element of object) { 353 | const valueUID = element.UID 354 | // console.log(element, valueUID) 355 | if (valueUID) { 356 | result.push(unarchiveObject(objects, objects[valueUID])) 357 | } else { 358 | result.push(unarchiveObject(objects, element)) 359 | } 360 | } 361 | return result 362 | } 363 | 364 | if (typeof object === 'object' && object.UID >= 0) { 365 | return unarchiveObject(objects, objects[object.UID]) 366 | } 367 | 368 | if (typeof object === 'object') { 369 | const result = {} 370 | for (let key in object) { 371 | const value = object[key] 372 | result[key] = unarchiveObject(objects, value) 373 | } 374 | return result 375 | } 376 | 377 | return object 378 | } 379 | 380 | function simplifyPlist(plist) { 381 | const type = Object.prototype.toString.call(plist) 382 | // apply recursively only when plist is object or array 383 | if (type !== "[object Object]" && type !== "[object Array]") { 384 | return plist 385 | } 386 | if (Array.isArray(plist)) { 387 | return plist.map(simplifyPlist) 388 | } 389 | 390 | // for key-value dictionary 391 | const keys = plist["NS.keys"] 392 | const values = plist["NS.objects"] 393 | if (keys && values) { 394 | const result = {} 395 | for (let idx in keys) { 396 | result[keys[idx]] = simplifyPlist(values[idx]) 397 | if (keys[idx] === 'NSAttributes') { 398 | console.log(simplifyPlist(values[idx])) 399 | } 400 | } 401 | return result 402 | } 403 | 404 | // for common objects 405 | if (typeof plist === 'object') { 406 | const result = {} 407 | for (let key in plist) { 408 | result[key] = simplifyPlist(plist[key]) 409 | } 410 | return result 411 | } 412 | return plist 413 | } 414 | 415 | module.exports = { 416 | parseBuffer, 417 | unarchivePlist, 418 | simplifyPlist 419 | } 420 | -------------------------------------------------------------------------------- /src/generateImages.js: -------------------------------------------------------------------------------- 1 | const { promisedExec } = require('./utils') 2 | 3 | // sketchtool path 4 | const sketchtool 5 | = '/Applications/Sketch.app/Contents/Resources/sketchtool/bin/sketchtool' 6 | 7 | module.exports = { 8 | rename, 9 | generateSliceImages, 10 | generatePreviewImages 11 | } 12 | 13 | const RE_IMG = /Exported\s([^\n]+)@2x.png\n?/g 14 | 15 | // We should prevent to duplicate image with save name. 16 | function getFilesFromMsg(msg) { 17 | const files = {} 18 | let match 19 | while ((match = RE_IMG.exec(msg)) != null) { 20 | files[match[1]] = true 21 | } 22 | return Object.keys(files) 23 | } 24 | // sketch removed 'install.sh' From v49 25 | // function install () { 26 | // return promisedExec(`${ROOT}/install.sh`) 27 | // } 28 | 29 | function generatePreviewImages(file, dest, scale) { 30 | return promisedExec(`${sketchtool} -v`).then(() => { 31 | return promisedExec(`${sketchtool} export artboards ${escape(file)} --output=${escape(dest)} --format='png' --use-id-for-name=YES --scales='${scale || '2.0'}'`).then( 32 | msg => { 33 | return getFilesFromMsg(msg) 34 | } 35 | ) 36 | }) 37 | } 38 | 39 | function generateSliceImages(file, dest, scale) { 40 | return promisedExec(`${sketchtool} -v`).then(() => { 41 | return promisedExec(`${sketchtool} export slices ${escape(file)} --output=${escape(dest)} --format='png' --scales='${scale || '2.0'}'`).then( 42 | msg => { 43 | return getFilesFromMsg(msg) 44 | } 45 | ) 46 | }) 47 | } 48 | 49 | function rename(src, dest) { 50 | return promisedExec(`mv ${escape(src)} ${escape(dest)}`) 51 | } 52 | 53 | function escape(url) { 54 | // Wrap with quotes, so space, parenthese and other special characters 55 | // wont interrupt cli. 56 | return `"${url}"` 57 | } 58 | -------------------------------------------------------------------------------- /src/generateMeasurePage.js: -------------------------------------------------------------------------------- 1 | const { createReadStream, createWriteStream, readFileSync } = require('fs') 2 | const { resolve } = require('path') 3 | const { Readable } = require('stream') 4 | const mkdirp = require('mkdirp') 5 | 6 | module.exports = generate 7 | 8 | function copy(src, dest) { 9 | const writer = createWriteStream(dest) 10 | return new Promise((resolve, reject) => { 11 | writer.on('close', resolve) 12 | writer.on('error', reject) 13 | let reader 14 | if (Buffer.isBuffer(src)) { 15 | reader = new Readable() 16 | reader.push(src) 17 | reader.push(null) // End the stream 18 | } else { 19 | reader = createReadStream(src) 20 | } 21 | reader.pipe(writer) 22 | }) 23 | } 24 | 25 | function copyAssets(dest) { 26 | const files = [ 27 | 'index.css', 28 | 'index.js', 29 | 'jQuery.js', 30 | 'normalize.css' 31 | ] 32 | const urls = files.map(v => resolve(__dirname, '../assets', v)) 33 | try { 34 | mkdirp.sync(resolve(dest, 'preview')) 35 | } catch (e) { 36 | return Promise.reject(e) 37 | } 38 | return Promise.all( 39 | urls.map((v, i) => copy(v, resolve(dest, files[i]))) 40 | ) 41 | } 42 | 43 | let INDEX_HTML 44 | function generateIndexHtml(data, dest) { 45 | if (!INDEX_HTML) { 46 | INDEX_HTML = readFileSync(resolve(__dirname, '../assets/index.html'), { 47 | encoding: 'utf8' 48 | }).toString() 49 | } 50 | const html = INDEX_HTML.replace(/__data__/, JSON.stringify(data, null, 2)) 51 | return copy( 52 | Buffer.from(html, 'utf8'), 53 | dest 54 | ) 55 | } 56 | 57 | function generate(data, dest) { 58 | return copyAssets(dest).then(() => { 59 | return generateIndexHtml( 60 | data, 61 | resolve(dest, 'index.html') 62 | ) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const Transformer = require('./transform') 3 | const parseSketchFile = require('./parseSketchFile') 4 | const generatePage = require('./generateMeasurePage') 5 | const { 6 | generatePreviewImages, 7 | generateSliceImages, 8 | rename 9 | } = require('./generateImages') 10 | 11 | module.exports = process 12 | 13 | function process(sketchFile, dest) { 14 | const NAME_MAP = {} 15 | let transformer 16 | return parseSketchFile(sketchFile) 17 | // convert data 18 | .then(data => { 19 | transformer = new Transformer(data.meta, data.pages, { 20 | savePath: dest, 21 | // Don't export symbol artboard. 22 | // Because sketchtool doesn't offer cli to export symbols, we can't 23 | // export single symbol image. 24 | ignoreSymbolPage: true, 25 | // From version 47, sketch support library 26 | foreignSymbols: data.document.foreignSymbols, 27 | layerTextStyles: data.document.layerTextStyles 28 | }) 29 | const processedData = transformer.convert() 30 | processedData.artboards.forEach(artboard => { 31 | NAME_MAP[artboard.objectID] = artboard.slug 32 | }) 33 | return generatePage(processedData, dest) 34 | }) 35 | // process preview images 36 | .then(() => { 37 | return generatePreviewImages(sketchFile, join(dest, 'preview')) 38 | .then(images => { 39 | return Promise.all( 40 | images.map(name => { 41 | const correctName = NAME_MAP[name] 42 | return rename( 43 | join(dest, `preview/${name}@2x.png`), 44 | join(dest, `preview/${correctName}.png`) 45 | ) 46 | }) 47 | ) 48 | }) 49 | }) 50 | // process slice images 51 | .then(() => { 52 | // NOTE: Maybe should use slice layer's scale here. 53 | return generateSliceImages(sketchFile, transformer.assetsPath, transformer.result.scale) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /src/parseSketchFile.js: -------------------------------------------------------------------------------- 1 | const { createReadStream, readFile } = require('fs') 2 | const unzip = require('unzipper') 3 | const tempfile = require('tempfile') 4 | const pReadFile = src => { 5 | return new Promise((resolve, reject) => { 6 | readFile(src, (err, data) => { 7 | err ? reject(err) : resolve(data) 8 | }) 9 | }) 10 | } 11 | 12 | module.exports = parseSketchFile 13 | 14 | function extractZipFile(src, dest, cb) { 15 | const writer = unzip.Extract({ path: dest }) 16 | writer.on('close', cb) 17 | writer.on('error', cb) 18 | createReadStream(src).pipe(writer) 19 | } 20 | 21 | function parseJSONFile(src) { 22 | return pReadFile(src).then(v => JSON.parse(v)) 23 | } 24 | 25 | function parseSketchFile(src) { 26 | const dest = tempfile() 27 | return new Promise((resolve, reject) => { 28 | extractZipFile(src, dest, err => { 29 | if (err) { 30 | return reject(err) 31 | } 32 | const res = { 33 | path: dest 34 | } 35 | resolve( 36 | Promise.all([ 37 | parseJSONFile(`${dest}/meta.json`), 38 | parseJSONFile(`${dest}/document.json`) 39 | ]).then(([meta, document]) => { 40 | res.meta = meta 41 | res.document = document 42 | res.pages = {} 43 | const ids = Object.keys(meta.pagesAndArtboards) 44 | return Promise.all( 45 | ids.map(id => parseJSONFile(`${dest}/pages/${id}.json`)) 46 | ).then(pages => { 47 | res.pages = pages.reduce((acc, val, i) => { 48 | acc[ids[i]] = val 49 | return acc 50 | }, {}) 51 | return res 52 | }) 53 | }) 54 | ) 55 | }) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/parseText.js: -------------------------------------------------------------------------------- 1 | const { 2 | parseBuffer, 3 | unarchivePlist, 4 | simplifyPlist 5 | } = require('./deps/decodeUtils') 6 | 7 | function decodeText(archived) { 8 | const buffer = Buffer.from(archived._archive, 'base64') 9 | const plist = parseBuffer(buffer)[0] 10 | const unarchived = unarchivePlist(plist) 11 | const simplified = simplifyPlist(unarchived) 12 | return simplified 13 | } 14 | 15 | function parseText(layer) { 16 | const decodedTextAttributes = decodeText(layer.attributedString.archivedAttributedString) 17 | const textStyle = new TextStyle(layer) 18 | const text = decodedTextAttributes.NSString 19 | 20 | const res = { 21 | content: text 22 | } 23 | // Text is split to several blocks. 24 | if (decodedTextAttributes.NSAttributeInfo) { 25 | const subTextStyles = decodedTextAttributes.NSAttributeInfo['NS.data'] 26 | const subTexts = [] 27 | for (let i = 0, s = 0, l = subTextStyles.length / 2; i < l; i++) { 28 | const charCount = subTextStyles[i * 2] 29 | const styleIndex = subTextStyles[i * 2 + 1] 30 | subTexts.push({ 31 | content: text.slice(s, s + charCount), 32 | style: textStyle.getTextStyle(styleIndex) 33 | }) 34 | s += charCount 35 | } 36 | // NOTE: In this case, we cannot to calculate every blocks position info, 37 | // and we just keep only first block. 38 | Object.assign(res, subTexts[0].style) 39 | } else { 40 | const style = textStyle.getTextStyle() 41 | Object.assign(res, style) 42 | } 43 | return res 44 | } 45 | 46 | class TextStyle { 47 | constructor(layer) { 48 | this.layer = layer 49 | this.textStyle = layer.style.textStyle 50 | } 51 | 52 | _getStyle(attributes) { 53 | let { 54 | MSAttributedStringFontAttribute, 55 | NSColor, 56 | NSKern, 57 | NSParagraphStyle 58 | } = attributes 59 | // Prevent access error. 60 | if (!NSParagraphStyle) { 61 | NSParagraphStyle = {} 62 | } 63 | 64 | const fontSize = MSAttributedStringFontAttribute.NSFontDescriptorAttributes.NSFontSizeAttribute 65 | const fontFace = MSAttributedStringFontAttribute.NSFontDescriptorAttributes.NSFontNameAttribute 66 | // Default to left 67 | let textAlign = 'left' 68 | switch (NSParagraphStyle.NSAlignment) { 69 | case 1: { 70 | textAlign = 'left' 71 | break 72 | } 73 | case 2: { 74 | textAlign = 'center' 75 | break 76 | } 77 | case 3: { 78 | textAlign = 'right' 79 | break 80 | } 81 | case 4: { 82 | textAlign = 'justify' 83 | break 84 | } 85 | } 86 | const lineHeight = NSParagraphStyle.NSMaxLineHeight 87 | 88 | const style = { 89 | color: this.decodeColor(NSColor), 90 | fontSize, 91 | fontFace, 92 | textAlign, 93 | lineHeight: lineHeight || 1.4 * fontSize, 94 | letterSpacing: NSKern == null ? 0 : NSKern 95 | } 96 | 97 | return style 98 | } 99 | 100 | // get style from textStyle.encodedAttributes 101 | getParagraphStyle() { 102 | const { 103 | MSAttributedStringFontAttribute, 104 | NSColor, 105 | NSKern, 106 | NSParagraphStyle 107 | } = this.textStyle.encodedAttributes 108 | 109 | const fontAttribute = decodeText(MSAttributedStringFontAttribute) 110 | const paragraphStyle = decodeText(NSParagraphStyle) 111 | 112 | return this._getStyle({ 113 | MSAttributedStringFontAttribute: fontAttribute, 114 | NSColor, 115 | NSKern, 116 | NSParagraphStyle: paragraphStyle 117 | }) 118 | } 119 | 120 | getTextStyle(styleIndex = null) { 121 | const decodedTextAttributes = decodeText(this.layer.attributedString.archivedAttributedString) 122 | const { NSAttributes } = decodedTextAttributes 123 | 124 | // has single subText: NSAttributes is an object 125 | if (styleIndex === null) { 126 | return this._getStyle(NSAttributes) 127 | } 128 | 129 | // has many subText: NSAttributes['NS.objects'] is an array of attribute 130 | const textAttribute = NSAttributes['NS.objects'][styleIndex] 131 | return this._getStyle(textAttribute) 132 | } 133 | 134 | decodeColor(NSColor) { 135 | if (!NSColor || (!NSColor.NSComponents && !NSColor.NSRGB)) { 136 | return { 137 | red: 255, 138 | green: 255, 139 | blue: 255, 140 | alpha: 1 141 | } 142 | } 143 | let colors 144 | if (NSColor.NSComponents) { 145 | colors = NSColor.NSComponents.toString('ascii').split(' ') 146 | } else { 147 | // remove \u0000 148 | const re = new RegExp('\u0000', 'g') 149 | colors = NSColor.NSRGB.toString('ascii').replace(re, '').split(' ') 150 | } 151 | const [red, green, blue, alpha] = colors 152 | return { 153 | red: +red, 154 | green: +green, 155 | blue: +blue, 156 | alpha: alpha == null ? 1 : +alpha 157 | } 158 | } 159 | } 160 | 161 | module.exports = parseText 162 | -------------------------------------------------------------------------------- /src/parseText.v50.js: -------------------------------------------------------------------------------- 1 | function parseText(layer) { 2 | const textStyle = new TextStyle(layer) 3 | const text = layer.attributedString.string 4 | 5 | const res = { 6 | content: text 7 | } 8 | 9 | const style = textStyle.getTextStyle() 10 | Object.assign(res, style) 11 | 12 | return res 13 | } 14 | 15 | class TextStyle { 16 | constructor(layer) { 17 | this.layer = layer 18 | this.textStyle = layer.style.textStyle 19 | 20 | this.encodeAttr = layer.style.textStyle.encodedAttributes 21 | } 22 | 23 | _getStyle() { 24 | const fontSize = this.encodeAttr.MSAttributedStringFontAttribute.attributes.size 25 | const fontFace = this.encodeAttr.MSAttributedStringFontAttribute.attributes.name 26 | const paragraphStyle = this.encodeAttr.paragraphStyle || {} 27 | 28 | // Default to left 29 | let textAlign = 'left' 30 | switch (paragraphStyle.alignment) { 31 | case 1: { 32 | textAlign = 'left' 33 | break 34 | } 35 | case 2: { 36 | textAlign = 'center' 37 | break 38 | } 39 | case 3: { 40 | textAlign = 'right' 41 | break 42 | } 43 | case 4: { 44 | textAlign = 'justify' 45 | break 46 | } 47 | } 48 | const lineHeight = paragraphStyle.maximumLineHeight 49 | 50 | const style = { 51 | color: this.encodeAttr.MSAttributedStringColorAttribute, 52 | fontSize, 53 | fontFace, 54 | textAlign, 55 | lineHeight: lineHeight || 1.4 * fontSize, 56 | letterSpacing: this.encodeAttr.kerning == null ? 0 : this.encodeAttr.kerning 57 | } 58 | 59 | return style 60 | } 61 | 62 | getTextStyle() { 63 | return this._getStyle() 64 | } 65 | } 66 | 67 | module.exports = parseText 68 | -------------------------------------------------------------------------------- /src/transform.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const { 3 | toHex, 4 | convertRGBToHex, 5 | toPercentage, 6 | round, 7 | getSlug 8 | } = require('./utils') 9 | const parseText = require('./parseText') 10 | const parseTextV50 = require('./parseText.v50') 11 | 12 | /** 13 | * Layer Types. 14 | * @type {Object} 15 | */ 16 | const TYPE_MAP = { 17 | text: 'text', 18 | slice: 'slice', 19 | symbolInstance: 'symbol', 20 | shape: 'shape' 21 | } 22 | 23 | const FONT_WEIGHT = { 24 | Ultralight: 'lighter', 25 | Thin: '300', 26 | Light: '300', 27 | Medium: 'bold', 28 | Semibold: 'bolder', 29 | Regular: 'normal' 30 | } 31 | 32 | /** 33 | * Transform exportable for slices & symbols (has export size) 34 | * @param {Object} layer layer data 35 | * @param {Object} result result 36 | * @return {Undefined} 37 | */ 38 | function transformExportable(layer, result) { 39 | const type = result.type 40 | if (type === TYPE_MAP.slice || ( 41 | type === TYPE_MAP.symbolInstance 42 | && layer.exportOptions.exportFormats.length 43 | )) { 44 | result.exportable = layer.exportOptions.exportFormats.map(v => { 45 | const prefix = v.prefix || '' 46 | const suffix = v.suffix || '' 47 | return { 48 | name: layer.name, 49 | format: v.fileFormat, 50 | scale: v.scale, 51 | path: prefix + layer.name + suffix + '.' + v.fileFormat 52 | } 53 | }) 54 | } 55 | } 56 | 57 | /** 58 | * Transform frame, get position & size 59 | * @param {Object} layer layer data 60 | * @param {Object} result object to save transformed result 61 | * @param {Object} pos parent layer's position 62 | * @return {Undefined} 63 | */ 64 | function transformFrame(layer, result, { x, y }) { 65 | const frame = layer.frame 66 | result[frame._class] = { 67 | width: round(frame.width, 1), 68 | height: round(frame.height, 1), 69 | x: frame.x + (x || 0), 70 | y: frame.y + (y || 0) 71 | } 72 | } 73 | 74 | /** 75 | * Transform extra info. 76 | * @param {Object} layer layer data 77 | * @param {Object} result object to save transformed result 78 | * @return {Undefined} 79 | */ 80 | function transformExtraInfo(layer, result) { 81 | // Set radius 82 | // if (layer.layers) { 83 | // const first = layer.layers[0] 84 | // if (first && first._class === 'rectangle') { 85 | // result.radius = first.fixedRadius 86 | // } else { 87 | // result.radius = 0 88 | // } 89 | // } 90 | 91 | const radius = layer.fixedRadius 92 | radius && (result.radius = radius) 93 | } 94 | 95 | /** 96 | * Transform main style info: border/shadow/fill/opacity 97 | * @param {Object} layer layer 98 | * @param {Object} result result 99 | * @return {Undefined} 100 | */ 101 | function transformStyle(layer, result) { 102 | const style = layer.style 103 | let opacity 104 | if (style) { 105 | result.borders = transformBorders(style.borders) 106 | result.fills = transformFills(style.fills) 107 | result.shadows = transformShadows(style.shadows).concat( 108 | transformShadows(style.innerShadows) 109 | ) 110 | opacity = style.contextSettings && style.contextSettings.opacity 111 | } 112 | if (opacity == null && result.type !== 'slice') { 113 | opacity = 1 114 | } 115 | result.opacity = opacity 116 | } 117 | 118 | const FILL_TYPES = ['color', 'gradient'] 119 | const BORDER_POSITIONS = ['center', 'inside', 'outside'] 120 | const GRADIENT_TYPES = ['linear', 'radial', 'angular'] 121 | 122 | /** 123 | * Transform layer.style.borders 124 | * @param {Array} borders border style list 125 | * @return {Array} transformed border style 126 | */ 127 | function transformBorders(borders) { 128 | if (!borders || !borders.length) return [] 129 | return borders.filter(v => v.isEnabled) 130 | .map(v => { 131 | const fillType = FILL_TYPES[v.fillType] 132 | const borderData = { 133 | fillType, 134 | position: BORDER_POSITIONS[v.position], 135 | thickness: v.thickness 136 | } 137 | if (fillType === 'color') { 138 | borderData.color = transformColor(v.color) 139 | } else if (fillType === 'gradient') { 140 | borderData.gradient = transformGradient(v.gradient) 141 | } 142 | return borderData 143 | }) 144 | } 145 | 146 | /** 147 | * Transform layer.style.fills 148 | * @param {Array} fills fill style list 149 | * @return {Array} transformed fill style 150 | */ 151 | function transformFills(fills) { 152 | if (!fills || !fills.length) return [] 153 | return fills.filter(v => v.isEnabled) 154 | .map(v => { 155 | const fillType = FILL_TYPES[v.fillType] 156 | const fillData = { 157 | fillType 158 | } 159 | if (fillType === 'color') { 160 | fillData.color = transformColor(v.color) 161 | } else if (fillType === 'gradient') { 162 | fillData.gradient = transformGradient(v.gradient) 163 | } 164 | return fillData 165 | }) 166 | } 167 | 168 | /** 169 | * Transform layer.style.shadows 170 | * @param {Array} shadows shadow style list 171 | * @return {Array} transformed shadow style 172 | */ 173 | function transformShadows(shadows) { 174 | if (!shadows || !shadows.length) return [] 175 | return shadows.filter(v => v.isEnabled) 176 | .map(v => { 177 | return { 178 | type: v._class === 'innerShadow' ? 'inner' : 'outer', 179 | offsetX: v.offsetX, 180 | offsetY: v.offsetY, 181 | blurRadius: v.blurRadius, 182 | spread: v.spread, 183 | color: transformColor(v.color) 184 | } 185 | }) 186 | } 187 | 188 | /** 189 | * Transform color 190 | * @param {Object} color sketch color object 191 | * @return {Object} transformed color object 192 | */ 193 | function transformColor(color) { 194 | if (!color) return null 195 | const r = ~~(color.red * 255) 196 | const g = ~~(color.green * 255) 197 | const b = ~~(color.blue * 255) 198 | const a = color.alpha 199 | return { 200 | r, 201 | g, 202 | b, 203 | a, 204 | 'color-hex': `#${convertRGBToHex(r, g, b)} ${toPercentage(a, 0)}`, 205 | 'argb-hex': `#${toHex(a * 255, 2)}${convertRGBToHex(r, g, b)}`, 206 | 'css-rgba': `rgba(${r},${g},${b},${a})`, 207 | 'ui-color': `(r:${color.red.toFixed(2)} g:${color.green.toFixed(2)} b:${color.blue.toFixed(2)} a:${a.toFixed(2)})` 208 | } 209 | } 210 | 211 | function transformGradient(gradient) { 212 | const stops = gradient.stops.map(stop => { 213 | return { 214 | color: transformColor(stop.color), 215 | position: stop.position 216 | } 217 | }) 218 | const data = { 219 | type: GRADIENT_TYPES[gradient.gradientType], 220 | colorStops: stops, 221 | from: transformPosition(gradient.from), 222 | to: transformPosition(gradient.to) 223 | } 224 | return data 225 | } 226 | 227 | function transformPosition(position) { 228 | const parts = position.slice(1, -1).split(/,\s*/) 229 | return { 230 | x: +parts[0], 231 | y: +parts[1] 232 | } 233 | } 234 | 235 | /** 236 | * transform artboard. 237 | * @param {Object} artboard artboard data 238 | * @param {Object} pageMeta page meta 239 | * @param {Object} extra extra info 240 | * @param {String} appVersion app version 241 | * @return {Object} transformed artboard data. 242 | */ 243 | function transformArtboard(artboard, pageMeta, extra, appVersion) { 244 | pageMeta.width = artboard.frame.width 245 | pageMeta.height = artboard.frame.height 246 | // Set extra.layers, give other transform* functions a way to operate layers. 247 | extra.layers = pageMeta.layers 248 | extra.parentPos = { x: 0, y: 0 } 249 | artboard.layers.forEach(l => { 250 | const layer = transformLayer(l, extra, appVersion) 251 | pageMeta.layers.push(layer) 252 | recursiveAppendLayers(layer, pageMeta.layers) 253 | }) 254 | return pageMeta 255 | function recursiveAppendLayers(layer, store) { 256 | const _appendLayers = layer && layer._appendLayers 257 | if (!_appendLayers || !_appendLayers.length) { 258 | return 259 | } 260 | store.push(_appendLayers[0]) 261 | delete layer._appendLayers 262 | recursiveAppendLayers(_appendLayers[0], store) 263 | store.push(..._appendLayers.slice(1)) 264 | _appendLayers.slice(1).forEach(l => { 265 | recursiveAppendLayers(l, store) 266 | }) 267 | } 268 | } 269 | 270 | /** 271 | * Get layer type. 272 | * @param {Object} layer layer data. 273 | * @return {String} layer type. 274 | */ 275 | function getLayerType(layer, extra) { 276 | if (TYPE_MAP[layer._class]) { 277 | return TYPE_MAP[layer._class] 278 | } else if (layer.exportOptions.exportFormats.length) { 279 | return TYPE_MAP.slice 280 | } 281 | return TYPE_MAP.shape 282 | } 283 | 284 | const REVERSED_KEYS = ['name', 'rotation'] 285 | 286 | function transformLayer(layer, extra, appVersion) { 287 | const result = { 288 | objectID: layer.do_objectID, 289 | type: getLayerType(layer) 290 | } 291 | REVERSED_KEYS.forEach(k => { 292 | result[k] = layer[k] 293 | }) 294 | 295 | transformStyle(layer, result) 296 | transformFrame(layer, result, extra.parentPos || {}) 297 | transformExtraInfo(layer, result) 298 | transformExportable(layer, result) 299 | if (result.type === 'symbol') { 300 | result._appendLayers = handleSymbol(layer, result, Object.assign({}, extra, { 301 | symbolMasterLayer: extra.symbols[layer.symbolID], 302 | processingSymbolID: layer.symbolID 303 | }), appVersion) 304 | } else if (result.type === 'text') { 305 | handleText(layer, result, appVersion, extra.textStyles) 306 | } else if (shouldTransformSubLayers(layer)) { 307 | result._appendLayers = layer.layers.map( 308 | l => transformLayer(l, Object.assign({}, extra, { 309 | parentPos: result.rect 310 | }), appVersion) 311 | ) 312 | } 313 | 314 | appendCss(result) 315 | appendRNCss(result) 316 | 317 | return result 318 | } 319 | 320 | function shouldTransformSubLayers(layer) { 321 | if (!layer || !layer.layers) return false 322 | return layer.layers.length > 1 323 | } 324 | 325 | /** 326 | * If layer's type is symbol, we should special handle it: 327 | * 1. Overwrite objectID. 328 | * 2. Append symbol's content layer. 329 | * @param {Object} layer layer 330 | * @param {Object} result result data 331 | * @param {Object} extra extra info 332 | * @param {String} appVersion app version 333 | * @return {Array} layers should append 334 | */ 335 | function handleSymbol(layer, result, extra, appVersion) { 336 | const symbolMasterLayer = extra.symbolMasterLayer 337 | if (!symbolMasterLayer) { 338 | console.warn(`Miss symbol: ${extra.processingSymbolID}.`) 339 | return 340 | } 341 | const symbolObjectID = symbolMasterLayer.do_objectID 342 | // Overwrite id. 343 | result.objectID = symbolObjectID 344 | 345 | return symbolMasterLayer.layers.map(l => { 346 | const transformedLayer = transformLayer(l, extra, appVersion) 347 | transformedLayer.rect.x += result.rect.x 348 | transformedLayer.rect.y += result.rect.y 349 | return transformedLayer 350 | }) 351 | } 352 | 353 | function handleText(layer, result, appVersion, textStyles) { 354 | if (result.type !== 'text') return 355 | 356 | const textInfo = parseFloat(appVersion) >= 50 ? parseTextV50(layer, result) : parseText(layer, result) 357 | // If fills exists, we should not overwrite color. 358 | if (!layer.style.fills) { 359 | result.color = transformColor(textInfo.color) 360 | } 361 | delete textInfo.color 362 | 363 | if (textStyles.length > 0 && layer.sharedStyleID) { 364 | const _ts = textStyles.find(ts => ts.objectID === layer.sharedStyleID) 365 | _ts && (textInfo.textStyle = _ts.name) 366 | } 367 | 368 | Object.assign(result, textInfo) 369 | } 370 | 371 | function transformCSSColor(color) { 372 | if (color) { 373 | return color.a === 1 ? color['color-hex'].split(' ')[0] : color['css-rgba'] 374 | } 375 | } 376 | 377 | function transformCSSRadius(radius) { 378 | if (radius) { 379 | return `border-radius: ${radius}px;` 380 | } 381 | } 382 | 383 | function transformCSSBorder(border) { 384 | if (border.length) { 385 | const { thickness, color } = border[0] 386 | 387 | return `border: ${thickness}px solid ${transformCSSColor(color)};` 388 | } 389 | } 390 | 391 | function transformCSSBackground(fills) { 392 | if (fills.length) { 393 | const { color } = fills[0] 394 | 395 | return `background: ${transformCSSColor(color)};` 396 | } 397 | } 398 | 399 | function transformCSSShadow(shadows) { 400 | if (shadows.length) { 401 | const { offsetX, offsetY, blurRadius, color } = shadows[0] 402 | 403 | return `box-shadow: ${offsetX}px ${offsetY}px ${blurRadius}px ${transformCSSColor(color)};` 404 | } 405 | } 406 | 407 | function transformCSSOpacity(opacity) { 408 | if (opacity && opacity !== 1) { 409 | return `opacity: ${opacity};` 410 | } 411 | } 412 | 413 | function transformRNBackground(fills) { 414 | if (fills.length) { 415 | const { color } = fills[0] 416 | 417 | return `backgroundColor: '${transformCSSColor(color)}',` 418 | } 419 | } 420 | 421 | function transformRNBorder(border) { 422 | if (border.length) { 423 | const { thickness, color } = border[0] 424 | 425 | return [ 426 | `borderWidth: ${thickness},`, 427 | `borderColor: '${transformCSSColor(color)}',` 428 | ] 429 | } 430 | return [] 431 | } 432 | 433 | function transformRNRadius(radius) { 434 | if (radius) { 435 | return `borderRadius: ${radius},` 436 | } 437 | } 438 | 439 | function transformRNShadow(shadows) { 440 | if (shadows.length) { 441 | const { offsetX, offsetY, blurRadius, color } = shadows[0] 442 | const _shadowColor = color['color-hex'].split(' ')[0] 443 | const _shadowOpacity = color.a 444 | 445 | return [ 446 | `shadowColor: '${_shadowColor}',`, 447 | `shadowOpacity: ${_shadowOpacity},`, 448 | `shadowRadius: ${blurRadius},`, 449 | 'shadowOffset: {', 450 | ` height: ${offsetY},`, 451 | ` width: ${offsetX},`, 452 | '},' 453 | ] 454 | } 455 | return [] 456 | } 457 | 458 | function transformRNOpacity(opacity) { 459 | if (opacity && opacity !== 1) { 460 | return `opacity: ${opacity},` 461 | } 462 | } 463 | 464 | /** 465 | * append css info 466 | * @param {Object} layer layer 467 | * @param {Object} layer result 468 | * @return {Undefined} 469 | */ 470 | function appendCss(result) { 471 | let tmp 472 | const { type } = result 473 | 474 | if (type) { 475 | switch (type) { 476 | case TYPE_MAP.shape: 477 | tmp = [ 478 | `width: ${result.rect.width}px;`, 479 | `height: ${result.rect.height}px;`, 480 | transformCSSBackground(result.fills), 481 | transformCSSBorder(result.borders), 482 | transformCSSRadius(result.radius), 483 | transformCSSShadow(result.shadows), 484 | transformCSSOpacity(result.opacity) 485 | ] 486 | break 487 | case TYPE_MAP.text: 488 | tmp = [ 489 | `font-size: ${result.fontSize};`, 490 | `font-family: ${result.fontFace};`, 491 | `font-weight: ${FONT_WEIGHT[result.fontFace.split('-')[1]]};`, 492 | `color: ${transformCSSColor(result.color)};` 493 | ] 494 | break 495 | default: 496 | tmp = [ 497 | `width: ${result.rect.width};`, 498 | `height: ${result.rect.height};` 499 | ] 500 | break 501 | } 502 | } 503 | 504 | result.css = tmp.filter(t => t) 505 | } 506 | 507 | /** 508 | * append rn css info 509 | * @param {Object} layer layer 510 | * @param {Object} layer result 511 | * @return {Undefined} 512 | */ 513 | function appendRNCss(result) { 514 | let tmp 515 | const { type } = result 516 | 517 | if (type) { 518 | switch (type) { 519 | case TYPE_MAP.shape: 520 | tmp = [ 521 | `width: ${result.rect.width},`, 522 | `height: ${result.rect.height},`, 523 | transformRNBackground(result.fills), 524 | transformRNRadius(result.radius), 525 | transformRNOpacity(result.opacity), 526 | ...transformRNBorder(result.borders), 527 | ...transformRNShadow(result.shadows) 528 | ] 529 | break 530 | case TYPE_MAP.text: 531 | tmp = [ 532 | `fontSize: ${result.fontSize},`, 533 | `fontFamily: '${result.fontFace}',`, 534 | `fontWeight: '${FONT_WEIGHT[result.fontFace.split('-')[1]]}',`, 535 | `color: '${transformCSSColor(result.color)}',` 536 | ] 537 | break 538 | default: 539 | tmp = [ 540 | `width: ${result.rect.width},`, 541 | `height: ${result.rect.height}` 542 | ] 543 | break 544 | } 545 | } 546 | 547 | result.rncss = tmp.filter(t => t) 548 | } 549 | 550 | class Transformer { 551 | constructor(meta, pages, { savePath, ignoreSymbolPage, foreignSymbols, layerTextStyles }) { 552 | this.meta = meta 553 | this.pages = pages 554 | this.savePath = savePath 555 | this.assetsPath = join(savePath, 'assets') 556 | this.ignoreSymbolPage = ignoreSymbolPage 557 | // hardcode some values. 558 | this.result = { 559 | scale: '1', 560 | unit: 'px', 561 | colorFormat: 'color-hex', 562 | artboards: [], 563 | slices: [], 564 | colors: [] 565 | } 566 | this._foreignSymbols = foreignSymbols 567 | this._layerTextStyles = layerTextStyles 568 | } 569 | 570 | getAllSymbols() { 571 | if (this.symbols) { 572 | return this.symbols 573 | } 574 | const symbols = this.symbols = {} 575 | const foreignSymbols = this._foreignSymbols 576 | Object.keys(this.pages).reduce((acc, val) => { 577 | this.pages[val].layers.forEach(v => { 578 | if (this.isSymbol(v)) { 579 | acc[v.symbolID] = v 580 | } 581 | }) 582 | return acc 583 | }, symbols) 584 | if (foreignSymbols) { 585 | foreignSymbols.forEach(v => { 586 | symbols[v.symbolMaster.symbolID] = v.symbolMaster 587 | }) 588 | } 589 | return symbols 590 | } 591 | 592 | getAllTextStyles() { 593 | if (this._layerTextStyles) { 594 | return this._layerTextStyles.objects.map(textStyle => { 595 | return { 596 | objectID: textStyle.do_objectID, 597 | name: textStyle.name 598 | } 599 | }) 600 | } 601 | return {} 602 | } 603 | 604 | convert() { 605 | const pagesAndArtboards = this.meta.pagesAndArtboards 606 | const pages = this.pages 607 | const result = this.result 608 | const symbols = this.getAllSymbols() 609 | const textStyles = this.getAllTextStyles() 610 | Object.keys(pagesAndArtboards).forEach(k => { 611 | const page = pages[k] 612 | const artboards = pagesAndArtboards[k].artboards 613 | const layers = page.layers 614 | var reverseLayerIDs = [] 615 | layers.forEach(layer => { 616 | // Ensure the layer is an artbord, and 617 | // Remove all symbol arbords If ignoreSymbolPage is true. 618 | if (artboards[layer.do_objectID]) { 619 | if (!this.ignoreSymbolPage || !this.isSymbol(layer)) { 620 | reverseLayerIDs.unshift(layer.do_objectID) 621 | } 622 | } 623 | }) 624 | reverseLayerIDs.forEach(id => { 625 | const slug = getSlug(page.name, artboards[id].name) 626 | const pageMeta = { 627 | pageName: page.name, 628 | pageObjectID: k, 629 | name: artboards[id].name, 630 | slug, 631 | objectID: id, 632 | imagePath: `preview/${slug}.png`, 633 | layers: [] 634 | } 635 | let artboard 636 | page.layers.some(l => { 637 | if (l.do_objectID === id) { 638 | artboard = l 639 | return true 640 | } 641 | }) 642 | result.artboards.push(transformArtboard( 643 | artboard, 644 | pageMeta, 645 | { 646 | savePath: this.savePath, 647 | assetsPath: this.assetsPath, 648 | symbols, 649 | textStyles 650 | }, 651 | this.meta.appVersion 652 | )) 653 | }) 654 | }) 655 | return result 656 | } 657 | 658 | isSymbol(layer) { 659 | return layer._class === 'symbolMaster' || layer.symbolID 660 | } 661 | } 662 | 663 | module.exports = Transformer 664 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process') 2 | 3 | function toHex(num, minLength) { 4 | if (typeof num !== 'number' || Number.isNaN(num)) { 5 | throw new Error('First argument should be a number.') 6 | } 7 | let hex = num.toString(16) 8 | if (minLength && hex.length < minLength) { 9 | hex = Array.apply(null, { 10 | length: minLength - hex.length 11 | }).map(v => '0').join('') + hex 12 | } 13 | return hex 14 | } 15 | 16 | exports.toHex = toHex 17 | 18 | function mathRound(num, precision) { 19 | num = +num 20 | if (Number.isNaN(num)) return NaN 21 | precision = +precision 22 | if (Number.isNaN(precision)) precision = 0 23 | 24 | return Number( 25 | '' + Math.round( 26 | num * Math.pow(10, precision) 27 | ) + 'e-' + precision 28 | ) 29 | } 30 | 31 | exports.round = mathRound 32 | 33 | exports.convertRGBToHex = function convertRGBToHex(r, g, b) { 34 | return (toHex(r, 2) + toHex(g, 2) + toHex(b, 2)).toUpperCase() 35 | } 36 | 37 | exports.toPercentage = function toPercentage(num, precision) { 38 | if (typeof num !== 'number' || Number.isNaN(num)) { 39 | throw new Error('First argument should be a number.') 40 | } 41 | if (typeof precision !== 'number') { 42 | precision = 2 43 | } 44 | return (num * 100).toFixed(precision) + '%' 45 | } 46 | 47 | const slugRe = /(\s+|\/+)/g 48 | function getSlug(pageName, artboardName) { 49 | if (!pageName || !artboardName || typeof pageName !== 'string' || typeof artboardName !== 'string') { 50 | throw new Error('Arguments should be non-empty string.') 51 | } 52 | const pn = pageName.replace(slugRe, '-') 53 | const an = artboardName.replace(slugRe, '-') 54 | 55 | return (pn + '-' + an).toLowerCase() 56 | } 57 | 58 | exports.getSlug = getSlug 59 | 60 | exports.promisedExec = function promisedExec(cmd) { 61 | return new Promise((resolve, reject) => { 62 | exec(cmd, (err, stdout) => { 63 | err ? reject(err) : resolve(stdout) 64 | }) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /test/parseSketchFile.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const { resolve } = require('path') 3 | const parseSketchFile = require('../src/parseSketchFile') 4 | 5 | describe('Test parseSketchFile', () => { 6 | it('should parse sketch file correctly', done => { 7 | parseSketchFile( 8 | resolve(__dirname, '../assets/demo.sketch') 9 | ).then(res => { 10 | assert.deepStrictEqual( 11 | Object.keys(res.meta.pagesAndArtboards), 12 | Object.keys(res.pages) 13 | ) 14 | delete res.meta.pagesAndArtboards 15 | assert.deepStrictEqual( 16 | res.meta, 17 | { 18 | commit: '623a23f2c4848acdbb1a38c2689e571eb73eb823', 19 | version: 112, 20 | fonts: [ 21 | 'PingFangSC-Ultralight', 22 | 'PingFangSC-Medium', 23 | 'PingFangSC-Semibold' 24 | ], 25 | compatibilityVersion: 99, 26 | app: 'com.bohemiancoding.sketch3', 27 | autosaved: 0, 28 | variant: 'NONAPPSTORE', 29 | created: 30 | { 31 | commit: '623a23f2c4848acdbb1a38c2689e571eb73eb823', 32 | appVersion: '52.2', 33 | build: 67145, 34 | app: 'com.bohemiancoding.sketch3', 35 | compatibilityVersion: 99, 36 | version: 112, 37 | variant: 'NONAPPSTORE' 38 | }, 39 | saveHistory: ['NONAPPSTORE.67145'], 40 | appVersion: '52.2', 41 | build: 67145 42 | } 43 | ) 44 | assert.ok(res.document) 45 | done() 46 | }).catch(done) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/transform.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const { resolve } = require('path') 3 | const parseSketchFile = require('../src/parseSketchFile') 4 | const Transformer = require('../src/transform') 5 | 6 | describe('Test transform', () => { 7 | it('should transform sketch file data correctly', done => { 8 | parseSketchFile( 9 | resolve(__dirname, '../assets/demo.sketch') 10 | ).then(res => { 11 | const transformer = new Transformer(res.meta, res.pages, { 12 | savePath: 'tmp', 13 | ignoreSymbolPage: true, 14 | foreignSymbols: res.document.foreignSymbols 15 | }) 16 | const result = transformer.convert() 17 | assert.strictEqual( 18 | result.artboards.length, 19 | 1 20 | ) 21 | assert.strictEqual( 22 | result.artboards[0].layers.length, 23 | 13 24 | ) 25 | assert.deepStrictEqual( 26 | result.artboards[0].layers[0], 27 | { 28 | objectID: '2157A837-50BE-445F-9523-443229E95AD0', 29 | type: 'shape', 30 | name: 'Group', 31 | rotation: 0, 32 | borders: [], 33 | fills: [], 34 | shadows: [], 35 | opacity: 1, 36 | rect: { width: 201, height: 80, x: 87, y: 171 }, 37 | css: [ 38 | 'width: 201px;', 39 | 'height: 80px;' 40 | ], 41 | rncss: [ 42 | 'width: 201,', 43 | 'height: 80,' 44 | ] 45 | } 46 | ) 47 | assert.deepStrictEqual( 48 | result.artboards[0].layers[1], 49 | { 50 | objectID: 'AADF9D36-622E-4097-8B33-9780CDF71558', 51 | type: 'shape', 52 | name: 'both', 53 | rotation: 0, 54 | borders: [{ 55 | fillType: 'color', 56 | position: 'center', 57 | thickness: 1, 58 | color: 59 | { 60 | r: 150, 61 | g: 150, 62 | b: 150, 63 | a: 1, 64 | 'argb-hex': '#ff969696', 65 | 'color-hex': '#969696 100%', 66 | 'css-rgba': 'rgba(150,150,150,1)', 67 | 'ui-color': '(r:0.59 g:0.59 b:0.59 a:1.00)' 68 | } 69 | }], 70 | css: [ 71 | 'width: 61px;', 72 | 'height: 80px;', 73 | 'background: #D7D7D7;', 74 | 'border: 1px solid #969696;' 75 | ], 76 | fills: [{ 77 | fillType: 'color', 78 | color: 79 | { 80 | r: 215, 81 | g: 215, 82 | b: 215, 83 | a: 1, 84 | 'argb-hex': '#ffD7D7D7', 85 | 'color-hex': '#D7D7D7 100%', 86 | 'css-rgba': 'rgba(215,215,215,1)', 87 | 'ui-color': '(r:0.85 g:0.85 b:0.85 a:1.00)' 88 | } 89 | }], 90 | shadows: [], 91 | opacity: 1, 92 | rect: { width: 61, height: 80, x: 157, y: 171 }, 93 | rncss: [ 94 | 'width: 61,', 95 | 'height: 80,', 96 | "backgroundColor: '#D7D7D7',", 97 | 'borderWidth: 1,', 98 | "borderColor: '#969696'," 99 | ] 100 | } 101 | ) 102 | assert.deepStrictEqual( 103 | result.artboards[0].layers[2], 104 | { 105 | objectID: 'D7A9F7D6-CFED-4C9A-9EB1-C1131A7F24D0', 106 | type: 'shape', 107 | name: 'backgroundcolor', 108 | rotation: 0, 109 | borders: [], 110 | css: [ 111 | 'width: 61px;', 112 | 'height: 80px;', 113 | 'background: #D7D7D7;' 114 | ], 115 | fills: [{ 116 | fillType: 'color', 117 | color: 118 | { 119 | r: 215, 120 | g: 215, 121 | b: 215, 122 | a: 1, 123 | 'color-hex': '#D7D7D7 100%', 124 | 'argb-hex': '#ffD7D7D7', 125 | 'css-rgba': 'rgba(215,215,215,1)', 126 | 'ui-color': '(r:0.85 g:0.85 b:0.85 a:1.00)' 127 | } 128 | }], 129 | shadows: [], 130 | opacity: 1, 131 | rect: { width: 61, height: 80, x: 227, y: 171 }, 132 | rncss: [ 133 | 'width: 61,', 134 | 'height: 80,', 135 | "backgroundColor: '#D7D7D7'," 136 | ] 137 | } 138 | ) 139 | done() 140 | }).catch(done) 141 | }) 142 | }) 143 | -------------------------------------------------------------------------------- /test/util.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const { 3 | toHex, 4 | convertRGBToHex, 5 | toPercentage, 6 | round, 7 | getSlug, 8 | promisedExec 9 | } = require('../src/utils') 10 | 11 | describe('Test utils', () => { 12 | it('toHex should convert number to hex correctly', () => { 13 | assert.strictEqual(toHex(255), 'ff') 14 | assert.strictEqual(toHex(1, 2), '01') 15 | try { 16 | toHex() 17 | } catch (e) { 18 | assert(e.message, 'First argument should be a number.') 19 | } 20 | }) 21 | it('toPercentage should convert number to percentage correctly', () => { 22 | assert.strictEqual(toPercentage(1.2), '120.00%') 23 | assert.strictEqual(toPercentage(0.078, 1), '7.8%') 24 | try { 25 | toPercentage() 26 | } catch (e) { 27 | assert(e) 28 | } 29 | }) 30 | it('round should enhance Math.round correctly', () => { 31 | assert(Number.isNaN(round())) 32 | assert.strictEqual(round(1.2345, 3), 1.235) 33 | assert.strictEqual(round(1.2), 1) 34 | assert.strictEqual(round(2.5, 0), 3) 35 | }) 36 | it('convertRGBToHex should convert to rgb color correctly', () => { 37 | assert.strictEqual(convertRGBToHex(0, 0, 0), '000000') 38 | assert.strictEqual(convertRGBToHex(255, 10, 9), 'FF0A09') 39 | try { 40 | convertRGBToHex(255, 10) 41 | } catch (e) { 42 | assert(e.message, 'First argument should be a number.') 43 | } 44 | }) 45 | it('getSlug should convert string to slug correctly', () => { 46 | assert.strictEqual(getSlug('a', 'B'), 'a-b') 47 | assert.strictEqual(getSlug('one Two', 'Three four'), 'one-two-three-four') 48 | try { 49 | getSlug(0) 50 | } catch (e) { 51 | assert(e.message, 'Arguments should be non-empty string.') 52 | } 53 | }) 54 | it('promisedExec should exec cmd asynchronously', done => { 55 | promisedExec(`ls ${__dirname}`).then(res => { 56 | assert(res) 57 | done() 58 | }, err => { 59 | done(err) 60 | }) 61 | }) 62 | }) 63 | --------------------------------------------------------------------------------