├── .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 | [](https://travis-ci.org/devsigners/sketch-measure-cli)
4 | [](https://badge.fury.io/js/sketch-measure-cli)
5 | [](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 | '',
215 | '',
226 | '',
227 | '',
228 | '',
229 | '',
234 | '',
235 | '',
236 | '
',
237 | '
',
238 | '
',
239 | '
',
240 | '
',
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 | '',
425 | unitCurrent,
426 | unitList.join(''),
427 | '
'
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('');
504 | $.each(this.project.slices, function( index, sliceLayer ){
505 | if(sliceLayer.exportable.length > 0){
506 | var asset = JSON.parse( JSON.stringify( sliceLayer.exportable ) ).pop();
507 | sliceListHTML.push(
508 | '- ',
509 | '
',
510 | '',
511 | '
' + sliceLayer.name + '
',
512 | '' + self.unitSize(sliceLayer.rect.width) + ' × ' + self.unitSize(sliceLayer.rect.height) + '',
513 | '',
514 | ' ');
515 | }
516 | });
517 | 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('');
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 | '',
909 | '',
910 | '',
911 | '',
912 | '',
913 | '
'].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('')
1001 | $.each(layerData.exportable, function(index, exportable) {
1002 | var filePath = path + exportable.path;
1003 | expHTML.push(
1004 | '- ',
1005 | '
' + exportable.path.replace('drawable-', '') + '',
1006 | ' ');
1007 | });
1008 | 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 |
--------------------------------------------------------------------------------