├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── assets
├── demo.gif
├── screenshot.jpg
└── screenshot2.jpg
├── cosmos.config.js
├── demo
└── simple
│ ├── .babelrc
│ ├── index.css
│ ├── index.html
│ ├── index.js
│ ├── package.json
│ └── webpack.config.js
├── images
├── Fitness App.sketch
├── bg.png
├── folder.png
└── ui-video-simple-john-hansen.sketch
├── index.html
├── main.js
├── package.json
├── src
├── __tests__
│ ├── Style.spec.js
│ └── utils.spec.js
├── components
│ ├── Document
│ │ ├── Document.js
│ │ ├── Document.styl
│ │ ├── LayerIndicator.js
│ │ ├── LayerInfo.js
│ │ ├── LayerSelector.js
│ │ ├── LayerSelector
│ │ │ └── __fixtures__
│ │ │ │ └── default.js
│ │ ├── PageSelector.js
│ │ ├── PageSelector
│ │ │ └── __fixtures__
│ │ │ │ └── default.js
│ │ ├── __fixtures__
│ │ │ ├── Fitness App.sketch
│ │ │ ├── document.js
│ │ │ └── ui-video-simple-john-hansen.sketch
│ │ ├── bg.png
│ │ └── folder.png
│ ├── Layer
│ │ ├── Artboard.js
│ │ ├── Bitmap.js
│ │ ├── Group.js
│ │ ├── Layer.js
│ │ ├── Page.js
│ │ ├── ShapeGroup.js
│ │ ├── Symbol.js
│ │ ├── Text.js
│ │ └── __fixtures__
│ │ │ └── layer.js
│ └── Site
│ │ ├── App.js
│ │ ├── App
│ │ └── __fixtures__
│ │ │ └── default.js
│ │ ├── Loading.js
│ │ ├── Loading
│ │ └── __fixtures__
│ │ │ └── default.js
│ │ └── index.css
├── index.js
├── models
│ └── index.js
└── utils
│ ├── JSX.js
│ ├── bplist-parser.js
│ ├── index.js
│ └── pretty-bytes.js
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react",
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | ]
9 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 |
33 | # Optional npm cache directory
34 | .npm
35 |
36 | # Optional REPL history
37 | .node_repl_history
38 | .idea
39 | playground/
40 | .DS_Store
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 asmn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sketch-react
2 |
3 | [https://zjuasmn.github.io/sketch-react](https://zjuasmn.github.io/sketch-react/)
4 |
5 | Project is still in beta, help and advise are welcome.
6 |
7 | The goal of this project is trying to reduce the gap between UI design and front-end development \(for now, just html\). A lot of time of front-end development is spent in matching the given UI design instead of implementing application logic. There are collaboration tools like [Zeplin ](https://www.zeplin.io/) to help marking the design, but programmer still needs to measure and copy css for each element assemble them and copy icons and other assets one by one.
8 |
9 | Here, we take a step further by generate html from .sketch file\(v43+\). So you can copy a group of element as html code and paste it in online or local text editor. Just a copy and paste from design to working code. Enjoy it.
10 |
11 | 
12 |
13 | # Roadmap
14 |
15 | * export to React code
16 | * autoprefix css
17 | * reduce export code size
18 | * svg code optimization
19 | * class style generate from symbol, share style
20 | * optimize position css
21 | * detect row or column pattern of elements
22 | * detect padding
23 | * detect corner element
24 | * minor edit
25 | * visible & lock
26 | * ...
27 |
28 | # Limitation
29 |
30 | Due to the limitation of html, it cannot be 100% math the original design, like background blur, multiple borders, multiple fills, masking, image color adjust and so on.
31 |
32 | **General**
33 |
34 | | Feature | Support | Related CSS Attributes |
35 | | :--- | :--- | :--- |
36 | | Position | Support | top, left |
37 | | Size | Support | width, height |
38 | | rotation, flip | Support | transform |
39 | | Opacity | Support | opacity |
40 | | Blending | Support | mix-blend-mode |
41 | | Lock | No | |
42 | | Hide | Support | display:none |
43 |
44 | **Text**\(rendered as <span/>\)
45 |
46 | | Feature | Support | Related CSS Attributes |
47 | | :--- | :--- | :--- |
48 | | Typeface | Limit\(no fallback\) | font-family |
49 | | Weight | Support | font-weight |
50 | | Color | Limit\(only solid color\) | color |
51 | | Size | Support | font-size |
52 | | Alignment | Support | text-align |
53 | | Width | Limit | \(Auto\)width:auto\(Fixed\)width:?px |
54 | | Spacing-Character | No | letter-spacing |
55 | | Spacing-Line | Limit | line-height |
56 | | Spacing-Paragraph | No | |
57 | | Fills | No | |
58 | | Borders | No | |
59 | | Shadows | No | |
60 | | Blur | No | |
61 |
62 | **Symbol**
63 |
64 | | Feature | Support | Related CSS Attributes |
65 | | :--- | :--- | :--- |
66 | | Size | Limit\(only support same size as symbol master\) | |
67 | | Overrides | no | |
68 |
69 | **Image** \(render as <img/>\)
70 |
71 | | Feature | Support | Related CSS Attributes |
72 | | :--- | :--- | :--- |
73 | | Color Adjust | no | |
74 |
75 | **Group**\(<div/>\)
76 |
77 | | Feature | Support | Related CSS Attributes |
78 | | :--- | :--- | :--- |
79 | | mask | limit\(only when the mask is the most bottom layer and it is simple shape and all sibling layer don't ignore the mask\) | overflow:hidden |
80 |
81 | **Shape**
82 |
83 | Simple Shape\(<div/>\)
84 |
85 | A shape is considered as simple when it is a rectangle or Oval with its points unedited.
86 |
87 | Complex Shape\(<svg/>\)
88 |
89 | complex shape needed to be flatten before render.
90 |
91 | | Feature | Support | Related CSS Attributes |
92 | | :--- | :--- | :--- |
93 | | fill | limit\(only one fill with solid color\) | fill |
94 | | borders | limit\(only one border with solid color\) | stroke,stroke-width |
95 | | shadows & innerShadows | no | |
96 | | blur | no | |
97 | | bool operation | no | |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/assets/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjuasmn/sketch-react/8582552e8538cdf507f6899724d4a9124028d394/assets/demo.gif
--------------------------------------------------------------------------------
/assets/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjuasmn/sketch-react/8582552e8538cdf507f6899724d4a9124028d394/assets/screenshot.jpg
--------------------------------------------------------------------------------
/assets/screenshot2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjuasmn/sketch-react/8582552e8538cdf507f6899724d4a9124028d394/assets/screenshot2.jpg
--------------------------------------------------------------------------------
/cosmos.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | componentPaths: ['src/components'],
3 | publicPath: __dirname + '/public',
4 | webpackConfigPath: './webpack.config.js',
5 | };
--------------------------------------------------------------------------------
/demo/simple/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react",
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | ]
9 | }
--------------------------------------------------------------------------------
/demo/simple/index.css:
--------------------------------------------------------------------------------
1 | html, body, #root {
2 | height: 100%;
3 | margin: 0;
4 | padding: 0;
5 | }
6 | body{
7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
8 | }
9 | .header {
10 | height: 32px;
11 | display: flex;
12 | align-items: center;
13 | padding: 0 16px;
14 | box-shadow: 0 1px 0 0 #b2b2b2;
15 | }
16 |
17 | .header a {
18 | color: #666;
19 | text-decoration: none;
20 | /*cursor: pointer;*/
21 | }
22 |
23 | .header a:hover {
24 | color: #333;
25 | }
26 |
27 | .flex {
28 | flex: 1;
29 | }
30 |
31 | .title {
32 | text-decoration: none;
33 | font-size: 18px;
34 | color: #666;
35 | width: 120px;
36 | }
37 |
38 | .filename {
39 | flex: 1;
40 | text-align: center;
41 | color: #666;
42 | font-size: 16px;
43 |
44 | }
45 |
46 | .title:hover {
47 | color: #333;
48 | }
49 |
50 | .icon {
51 | fill: #999;
52 |
53 | }
54 |
55 | .actions {
56 | width: 120px;
57 | }
58 |
59 | .icon-link {
60 | display: block;
61 | float: right;
62 | height: 16px;
63 | }
64 |
65 | .icon-link:hover .icon {
66 | fill: #333;
67 | }
68 |
69 | .upload-area {
70 | margin: 64px auto 0;
71 | height: 180px;
72 | width: 320px;
73 | padding: 16px;
74 | border: 2px dashed #666;
75 | display: flex;
76 | flex-direction: column;
77 | }
78 | .upload-area .desc{
79 | flex: 1;
80 | text-align: center;
81 | padding-top: 32px;
82 | font-size: 16px;
83 | color: #666;
84 | }
85 | .upload-button {
86 | cursor: pointer;
87 | display: block;
88 | background: #3884ff;
89 | color: white;
90 | text-align: center;
91 | padding: 4px 16px;
92 | border-radius: 3px;
93 | font-size: 14px;
94 | line-height: 24px;
95 | }
96 |
97 | .sample {
98 | margin: 16px auto;
99 | text-align: center;
100 | }
101 | .sample-header{
102 | color: #666;
103 | }
104 | .sample-link{
105 | cursor: pointer;
106 |
107 | font-size: 16px;
108 | line-height: 24px;
109 | margin-top: 8px;
110 | }
111 | .sample-link a{
112 | color: #3884ff;
113 | text-decoration: underline;
114 | }
--------------------------------------------------------------------------------
/demo/simple/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Sketch React
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/demo/simple/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from '../../src/components/Site/App'
4 |
5 | ReactDOM.render(, document.getElementById('root'));
--------------------------------------------------------------------------------
/demo/simple/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simple",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "webpack": "webpack",
8 | "release": "webpack -p",
9 | "watch": "webpack -w",
10 | "dev": "webpack-dev-server"
11 | },
12 | "devDependencies": {
13 | "babel-core": "^6.24.1",
14 | "babel-preset-es2015": "^6.24.1",
15 | "babel-preset-react": "^6.24.1",
16 | "babel-preset-stage-0": "^6.24.1",
17 | "babel-register": "^6.24.1",
18 | "html-webpack-plugin": "^2.28.0",
19 | "stylus": "^0.54.5",
20 | "stylus-loader": "^3.0.1",
21 | "webpack": "^2.4.1",
22 | "webpack-dev-server": "^2.4.5"
23 | },
24 | "dependencies": {
25 | "react": "^15.5.4",
26 | "react-dom": "^15.5.4"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/demo/simple/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const path = require('path');
3 |
4 | module.exports = {
5 | entry: {
6 | index: path.resolve(__dirname, 'index.js'),
7 | },
8 | output: {
9 | path: path.resolve(__dirname, '../..'),
10 | filename: 'main.js',
11 | },
12 | module: {
13 | rules: [
14 | {
15 | test: /\.jsx?$/,
16 | exclude: [
17 | /node_modules/
18 | ],
19 | loaders: ["babel-loader"]
20 | },
21 | {
22 | test: /\.styl$/,
23 | loaders: ['style-loader', 'css-loader', "stylus-loader"]
24 | },
25 | {
26 | test: /\.css$/,
27 | loaders: ['style-loader', 'css-loader']
28 | },
29 | {
30 | test: /\.(png)$/,
31 | loaders: ["file-loader?name=[name].[ext]&outputPath=images/"]
32 | },
33 | ]
34 |
35 | },
36 | resolve: {
37 | extensions: [".js", 'jsx', ".json"],
38 | },
39 | plugins: [new HtmlWebpackPlugin({
40 | template: 'index.html'
41 | })],
42 | };
--------------------------------------------------------------------------------
/images/Fitness App.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjuasmn/sketch-react/8582552e8538cdf507f6899724d4a9124028d394/images/Fitness App.sketch
--------------------------------------------------------------------------------
/images/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjuasmn/sketch-react/8582552e8538cdf507f6899724d4a9124028d394/images/bg.png
--------------------------------------------------------------------------------
/images/folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjuasmn/sketch-react/8582552e8538cdf507f6899724d4a9124028d394/images/folder.png
--------------------------------------------------------------------------------
/images/ui-video-simple-john-hansen.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjuasmn/sketch-react/8582552e8538cdf507f6899724d4a9124028d394/images/ui-video-simple-john-hansen.sketch
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Sketch React
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sketch-view",
3 | "version": "1.0.0",
4 | "main": "src/index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "cosmos": "cosmos"
8 | },
9 | "devDependencies": {
10 | "babel-core": "^6.24.1",
11 | "babel-preset-es2015": "^6.24.1",
12 | "babel-preset-react": "^6.24.1",
13 | "babel-preset-stage-0": "^6.24.1",
14 | "babel-register": "^6.24.1",
15 | "brfs": "^1.4.3",
16 | "chai": "^3.5.0",
17 | "file-loader": "^0.11.1",
18 | "html-webpack-plugin": "^2.28.0",
19 | "jsdom": "^10.1.0",
20 | "mocha": "^3.3.0",
21 | "react-addons-perf": "^15.4.2",
22 | "react-cosmos-webpack": "^2.0.0-beta.11",
23 | "react-dom": "^15.5.4",
24 | "stylus": "^0.54.5",
25 | "stylus-loader": "^3.0.1",
26 | "transform-loader": "^0.2.4",
27 | "webpack": "^2.5.1"
28 | },
29 | "dependencies": {
30 | "babel-polyfill": "^6.23.0",
31 | "big-integer": "^1.6.22",
32 | "classnames": "^2.2.5",
33 | "highlight.js": "^9.11.0",
34 | "is-plain-object": "^2.0.1",
35 | "isomorphic-fetch": "^2.2.1",
36 | "jszip": "^3.1.3",
37 | "lodash": "^4.17.4",
38 | "pretty-bytes": "^4.0.2",
39 | "prop-types": "^15.5.8",
40 | "react": "^15.5.4",
41 | "react-utilities": "^0.5.4",
42 | "react-zeroclipboard": "^3.2.3",
43 | "select": "^1.1.2",
44 | "sketch-constants": "^1.0.2"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/__tests__/Style.spec.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai'
2 | import {Color, Border, Shadow, InnerShadow, Gradient, Fill, Style, TextStyle,ShapePath} from '../models'
3 | import {parse} from '../utils'
4 |
5 |
6 | describe('Color', () => {
7 | it('works', () => {
8 | let json = {
9 | "_class": "color",
10 | "alpha": 0.89,
11 | "blue": 0.592,
12 | "green": 0,
13 | "red": 1
14 | };
15 | let color = parse(json);
16 | expect(color).to.be.an.instanceof(Color);
17 | expect(`${color}`).to.equal('rgba(255,0,151,0.89)');
18 | })
19 | });
20 | describe('border', () => {
21 | it('works', () => {
22 | let json = {
23 | "_class": "border",
24 | "isEnabled": true,
25 | "color": {
26 | "_class": "color",
27 | "alpha": 1,
28 | "blue": 0.592,
29 | "green": 0.592,
30 | "red": 0.592
31 | },
32 | "fillType": 0,
33 | "position": 1,
34 | "thickness": 1
35 | };
36 |
37 | let border = parse(json);
38 | expect(border).to.be.an.instanceof(Border);
39 | expect(`${border}`).to.equal('1px solid rgba(151,151,151,1)');
40 | })
41 | });
42 | describe('shadow and innetShadow', () => {
43 | it('works', () => {
44 | let json = {
45 | "_class": "shadow",
46 | "isEnabled": true,
47 | "blurRadius": 4,
48 | "color": {
49 | "_class": "color",
50 | "alpha": 0.5,
51 | "blue": 0,
52 | "green": 0,
53 | "red": 0
54 | },
55 | "contextSettings": {
56 | "_class": "graphicsContextSettings",
57 | "blendMode": 0,
58 | "opacity": 1
59 | },
60 | "offsetX": 0,
61 | "offsetY": 2,
62 | "spread": 0
63 | };
64 | let shadow = parse(json);
65 | expect(shadow).to.be.an.instanceof(Shadow);
66 | expect(`${shadow}`).to.equal('0px 2px 4px 0px rgba(0,0,0,0.5)');
67 | });
68 | it('works innerShadow', () => {
69 | let json = {
70 | "_class": "innerShadow",
71 | "isEnabled": true,
72 | "blurRadius": 4,
73 | "color": {
74 | "_class": "color",
75 | "alpha": 0.5,
76 | "blue": 0,
77 | "green": 0,
78 | "red": 0
79 | },
80 | "contextSettings": {
81 | "_class": "graphicsContextSettings",
82 | "blendMode": 0,
83 | "opacity": 1
84 | },
85 | "offsetX": 0,
86 | "offsetY": 2,
87 | "spread": 0
88 | };
89 | let innerShadow = parse(json);
90 | expect(innerShadow).to.be.an.instanceof(InnerShadow);
91 | expect(`${innerShadow}`).to.equal('inset 0px 2px 4px 0px rgba(0,0,0,0.5)');
92 | });
93 | it('works for array of shadow', () => {
94 | let json = [
95 | {
96 | "_class": "shadow",
97 | "isEnabled": true,
98 | "blurRadius": 4,
99 | "color": {
100 | "_class": "color",
101 | "alpha": 0.5,
102 | "blue": 0,
103 | "green": 0,
104 | "red": 0
105 | },
106 | "contextSettings": {
107 | "_class": "graphicsContextSettings",
108 | "blendMode": 0,
109 | "opacity": 1
110 | },
111 | "offsetX": 0,
112 | "offsetY": 2,
113 | "spread": 0
114 | },
115 | {
116 | "_class": "innerShadow",
117 | "isEnabled": true,
118 | "blurRadius": 4,
119 | "color": {
120 | "_class": "color",
121 | "alpha": 0.5,
122 | "blue": 0,
123 | "green": 0,
124 | "red": 0
125 | },
126 | "contextSettings": {
127 | "_class": "graphicsContextSettings",
128 | "blendMode": 0,
129 | "opacity": 1
130 | },
131 | "offsetX": 0,
132 | "offsetY": 2,
133 | "spread": 0
134 | },
135 | ];
136 | let shadows = parse(json);
137 | expect(shadows.map(shadow => shadow.toString()).join(',')).to.equal('0px 2px 4px 0px rgba(0,0,0,0.5),inset 0px 2px 4px 0px rgba(0,0,0,0.5)');
138 | })
139 | })
140 | describe(`gradient`, () => {
141 | it('works linear-gradient', () => {
142 | let json = {
143 | "_class": "gradient",
144 | "elipseLength": 0,
145 | "from": "{0.5, 0}",
146 | "gradientType": 0,
147 | "shouldSmoothenOpacity": false,
148 | "stops": [
149 | {
150 | "_class": "gradientStop",
151 | "color": {
152 | "_class": "color",
153 | "alpha": 1,
154 | "blue": 1,
155 | "green": 1,
156 | "red": 1
157 | },
158 | "position": 0
159 | },
160 | {
161 | "_class": "gradientStop",
162 | "color": {
163 | "_class": "color",
164 | "alpha": 1,
165 | "blue": 0,
166 | "green": 0,
167 | "red": 0
168 | },
169 | "position": 1
170 | }
171 | ],
172 | "to": "{0.85485408251402162, 0.64973154574674941}"
173 | };
174 | let gradient = parse(json);
175 | expect(gradient).to.be.an.instanceof(Gradient);
176 | expect(`${gradient}`).to.equal('linear-gradient(0deg, rgba(255,255,255,1) 0%, rgba(0,0,0,1) 100%)');
177 | })
178 | });
179 | describe('fill', () => {
180 | it('works', () => {
181 | let json = {
182 | "_class": "fill",
183 | "isEnabled": true,
184 | "color": {
185 | "_class": "color",
186 | "alpha": 1,
187 | "blue": 0.847,
188 | "green": 0.847,
189 | "red": 0.847
190 | },
191 | "fillType": 0,
192 | };
193 | let fill = parse(json);
194 | expect(fill).to.be.an.instanceof(Fill);
195 | expect(`${fill}`).to.equal('linear-gradient(0deg, rgba(216,216,216,1),rgba(216,216,216,1))');
196 | });
197 | it('works', () => {
198 | let json = {
199 | "_class": "fill",
200 | "isEnabled": true,
201 | "color": {
202 | "_class": "color",
203 | "alpha": 1,
204 | "blue": 0.847,
205 | "green": 0.847,
206 | "red": 0.847
207 | },
208 | "fillType": 1,
209 | "gradient": {
210 | "_class": "gradient",
211 | "elipseLength": 0,
212 | "from": "{0.5, 0}",
213 | "gradientType": 0,
214 | "shouldSmoothenOpacity": false,
215 | "stops": [
216 | {
217 | "_class": "gradientStop",
218 | "color": {
219 | "_class": "color",
220 | "alpha": 1,
221 | "blue": 1,
222 | "green": 1,
223 | "red": 1
224 | },
225 | "position": 0
226 | },
227 | {
228 | "_class": "gradientStop",
229 | "color": {
230 | "_class": "color",
231 | "alpha": 1,
232 | "blue": 0,
233 | "green": 0,
234 | "red": 0
235 | },
236 | "position": 1
237 | }
238 | ],
239 | "to": "{0.85485408251402162, 0.64973154574674941}"
240 | },
241 | };
242 | let fill = parse(json);
243 | expect(fill).to.be.an.instanceof(Fill);
244 | expect(`${fill}`).to.equal('linear-gradient(0deg, rgba(255,255,255,1) 0%, rgba(0,0,0,1) 100%)');
245 | })
246 | });
247 | describe('Style', () => {
248 | it('works', () => {
249 | let json = {
250 | "_class": "style",
251 | "borders": [
252 | {
253 | "_class": "border",
254 | "isEnabled": true,
255 | "color": {
256 | "_class": "color",
257 | "alpha": 1,
258 | "blue": 0.592,
259 | "green": 0.592,
260 | "red": 0.592
261 | },
262 | "fillType": 0,
263 | "position": 1,
264 | "thickness": 1
265 | }
266 | ],
267 | isVisible:true,
268 | "endDecorationType": 0,
269 | "fills": [
270 | {
271 | "_class": "fill",
272 | "isEnabled": true,
273 | "color": {
274 | "_class": "color",
275 | "alpha": 1,
276 | "blue": 0.847,
277 | "green": 0.847,
278 | "red": 0.847
279 | },
280 | "fillType": 1,
281 | "gradient": {
282 | "_class": "gradient",
283 | "elipseLength": 0,
284 | "from": "{0.5, 0}",
285 | "gradientType": 0,
286 | "shouldSmoothenOpacity": false,
287 | "stops": [
288 | {
289 | "_class": "gradientStop",
290 | "color": {
291 | "_class": "color",
292 | "alpha": 1,
293 | "blue": 1,
294 | "green": 1,
295 | "red": 1
296 | },
297 | "position": 0
298 | },
299 | {
300 | "_class": "gradientStop",
301 | "color": {
302 | "_class": "color",
303 | "alpha": 1,
304 | "blue": 0,
305 | "green": 0,
306 | "red": 0
307 | },
308 | "position": 1
309 | }
310 | ],
311 | "to": "{0.85485408251402162, 0.64973154574674941}"
312 | },
313 | "noiseIndex": 0,
314 | "noiseIntensity": 0,
315 | "patternFillType": 0,
316 | "patternTileScale": 1
317 | },
318 | {
319 | "_class": "fill",
320 | "isEnabled": true,
321 | "color": {
322 | "_class": "color",
323 | "alpha": 1,
324 | "blue": 0.847,
325 | "green": 0.847,
326 | "red": 0.847
327 | },
328 | "fillType": 1,
329 | "gradient": {
330 | "_class": "gradient",
331 | "elipseLength": 0,
332 | "from": "{0.5, 0}",
333 | "gradientType": 0,
334 | "shouldSmoothenOpacity": false,
335 | "stops": [
336 | {
337 | "_class": "gradientStop",
338 | "color": {
339 | "_class": "color",
340 | "alpha": 0.5,
341 | "blue": 0.3258815412299212,
342 | "green": 0.3258815412299212,
343 | "red": 0.5080516581632653
344 | },
345 | "position": 0
346 | },
347 | {
348 | "_class": "gradientStop",
349 | "color": {
350 | "_class": "color",
351 | "alpha": 0.5,
352 | "blue": 0,
353 | "green": 0,
354 | "red": 0
355 | },
356 | "position": 1
357 | }
358 | ],
359 | "to": "{0.5, 1}"
360 | },
361 | "noiseIndex": 0,
362 | "noiseIntensity": 0,
363 | "patternFillType": 1,
364 | "patternTileScale": 1
365 | }
366 | ],
367 | "miterLimit": 10,
368 | "startDecorationType": 0,
369 | "innerShadows": [
370 | {
371 | "_class": "innerShadow",
372 | "isEnabled": true,
373 | "blurRadius": 3,
374 | "color": {
375 | "_class": "color",
376 | "alpha": 0.5,
377 | "blue": 0,
378 | "green": 0,
379 | "red": 0
380 | },
381 | "contextSettings": {
382 | "_class": "graphicsContextSettings",
383 | "blendMode": 0,
384 | "opacity": 1
385 | },
386 | "offsetX": 0,
387 | "offsetY": 1,
388 | "spread": 0
389 | }
390 | ],
391 | "shadows": [
392 | {
393 | "_class": "shadow",
394 | "isEnabled": true,
395 | "blurRadius": 4,
396 | "color": {
397 | "_class": "color",
398 | "alpha": 0.5,
399 | "blue": 0,
400 | "green": 0,
401 | "red": 0
402 | },
403 | "contextSettings": {
404 | "_class": "graphicsContextSettings",
405 | "blendMode": 0,
406 | "opacity": 1
407 | },
408 | "offsetX": 0,
409 | "offsetY": 2,
410 | "spread": 0
411 | }
412 | ],
413 | };
414 | let style = parse(json);
415 | expect(style).to.be.an.instanceof(Style);
416 | expect(style.toStyle()).to.eql({
417 | "background": "linear-gradient(0deg, rgba(255,255,255,1) 0%, rgba(0,0,0,1) 100%), linear-gradient(0deg, rgba(130,83,83,0.5) 0%, rgba(0,0,0,0.5) 100%)",
418 | "border": "1px solid rgba(151,151,151,1)",
419 | "boxShadow": "0px 2px 4px 0px rgba(0,0,0,0.5), inset 0px 1px 3px 0px rgba(0,0,0,0.5)",
420 | "boxSizing": "border-box",
421 | "pointerEvent": "auto",
422 | });
423 | })
424 | });
425 | describe('TextStyle', () => {
426 | it('works', () => {
427 | let json = {
428 | "_class": "textStyle",
429 | "encodedAttributes": {
430 | "NSKern": 1,
431 | "MSAttributedStringTextTransformAttribute": 1,
432 | "NSColor": {
433 | "_archive": "YnBsaXN0MDDUAQIDBAUGHyBYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKUHCBEVHFUkbnVsbNQJCgsMDQ4PEFVOU1JHQlxOU0NvbG9yU3BhY2VfEBJOU0N1c3RvbUNvbG9yU3BhY2VWJGNsYXNzTxAsMC42Mzc2NDg4MDk1IDAuMjAxNTQyMjEwNyAwLjIwMTU0MjIxMDcgMC42NQAQAYACgATSEgwTFFROU0lEEAGAA9IWFxgZWiRjbGFzc25hbWVYJGNsYXNzZXNcTlNDb2xvclNwYWNlohobXE5TQ29sb3JTcGFjZVhOU09iamVjdNIWFx0eV05TQ29sb3KiHRtfEA9OU0tleWVkQXJjaGl2ZXLRISJUcm9vdIABAAgAEQAaACMALQAyADcAPQBDAEwAUgBfAHQAewCqAKwArgCwALUAugC8AL4AwwDOANcA5ADnAPQA\/QECAQoBDQEfASIBJwAAAAAAAAIBAAAAAAAAACMAAAAAAAAAAAAAAAAAAAEp"
434 | },
435 | "NSStrikethrough": 0,
436 | "MSAttributedStringFontAttribute": {
437 | "_archive": "YnBsaXN0MDDUAQIDBAUGJidYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkHCA0XGBkaGyJVJG51bGzSCQoLDFYkY2xhc3NfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4AIgALTDg8JEBMWV05TLmtleXNaTlMub2JqZWN0c6IREoADgASiFBWABYAGgAdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNAOAAAAAAAAFdBcmlhbE1U0hwdHh9aJGNsYXNzbmFtZVgkY2xhc3Nlc18QE05TTXV0YWJsZURpY3Rpb25hcnmjHiAhXE5TRGljdGlvbmFyeVhOU09iamVjdNIcHSMkXxAQTlNGb250RGVzY3JpcHRvcqIlIV8QEE5TRm9udERlc2NyaXB0b3JfEA9OU0tleWVkQXJjaGl2ZXLRKClUcm9vdIABAAgAEQAaACMALQAyADcAQQBHAEwAUwBwAHIAdAB7AIMAjgCRAJMAlQCYAJoAnACeALQAygDTANsA4ADrAPQBCgEOARsBJAEpATwBPwFSAWQBZwFsAAAAAAAAAgEAAAAAAAAAKgAAAAAAAAAAAAAAAAAAAW4="
438 | },
439 | "NSUnderline": 1,
440 | "NSParagraphStyle": {
441 | "_archive": "YnBsaXN0MDDUAQIDBAUGb3BYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK8QGAcIGisvNTc6PUBDRklMT1JVWFteYmZna1UkbnVsbNkJCgsMDQ4PEBESExQVFhcVGBlWJGNsYXNzWk5TVGFiU3RvcHNcTlNUZXh0QmxvY2tzXxAPTlNNYXhMaW5lSGVpZ2h0XxASTlNQYXJhZ3JhcGhTcGFjaW5nW05TVGV4dExpc3RzXxAPTlNNaW5MaW5lSGVpZ2h0XE5TSGVhZEluZGVudFtOU0FsaWdubWVudIAXgAKAEiNAAAAAAAAAACNACAAAAAAAAIATI0BCAAAAAAAAEAHSGwkcKlpOUy5vYmplY3RzrR0eHyAhIiMkJSYnKCmAA4AFgAaAB4AIgAmACoALgAyADYAOgA+AEIAR0gksLS5aTlNMb2NhdGlvboAEI0AmAAAAAAAA0jAxMjNaJGNsYXNzbmFtZVgkY2xhc3Nlc1lOU1RleHRUYWKiMjRYTlNPYmplY3TSCSwtGIAE0gksLTmABCNATFMzQAAAANIJLC08gAQjQFVAAAAAAADSCSwtP4AEI0BcVmZgAAAA0gksLUKABCNAYbZmYAAAANIJLC1FgAQjQGVBmaAAAADSCSwtSIAEI0BozMzAAAAA0gksLUuABCNAbFgAAAAAANIJLC1OgAQjQG\/jM0AAAADSCSwtUYAEI0BxtzNAAAAA0gksLVSABCNAc3zMwAAAANIJLC1XgAQjQHVCZmAAAADSMDFZWldOU0FycmF5olk00hsJXCqggBHSGwlfKqFggBSAEdIJY2RlXk5TTWFya2VyRm9ybWF0gBaAFVp7ZGVjaW1hbH0u0jAxaGlaTlNUZXh0TGlzdKJqNFpOU1RleHRMaXN00jAxbG1fEBdOU011dGFibGVQYXJhZ3JhcGhTdHlsZaNsbjRfEBBOU1BhcmFncmFwaFN0eWxlXxAPTlNLZXllZEFyY2hpdmVy0XFyVHJvb3SAAQAIABEAGgAjAC0AMgA3AFIAWABrAHIAfQCKAJwAsQC9AM8A3ADoAOoA7ADuAPcBAAECAQsBDQESAR0BKwEtAS8BMQEzATUBNwE5ATsBPQE\/AUEBQwFFAUcBTAFXAVkBYgFnAXIBewGFAYgBkQGWAZgBnQGfAagBrQGvAbgBvQG\/AcgBzQHPAdgB3QHfAegB7QHvAfgB\/QH\/AggCDQIPAhgCHQIfAigCLQIvAjgCPQI\/AkgCTQJVAlgCXQJeAmACZQJnAmkCawJwAn8CgQKDAo4CkwKeAqECrAKxAssCzwLiAvQC9wL8AAAAAAAAAgEAAAAAAAAAcwAAAAAAAAAAAAAAAAAAAv4="
442 | }
443 | }
444 | };
445 | let textStyle = parse(json);
446 | expect(textStyle).to.be.an.instanceof(TextStyle);
447 |
448 | });
449 | });
450 |
451 | describe('ShapePath', () => {
452 | it('works', () => {
453 | let json =
454 | {
455 | "_class": "shapePath",
456 | "do_objectID": "4E573388-DCD7-42A3-9138-17E043BAAE00",
457 | "exportOptions": {
458 | "_class": "exportOptions",
459 | "exportFormats": [],
460 | "includedLayerIds": [],
461 | "layerOptions": 0,
462 | "shouldTrim": false
463 | },
464 | "frame": {
465 | "_class": "rect",
466 | "constrainProportions": false,
467 | "height": 10,
468 | "width": 10,
469 | "x": 0,
470 | "y": 0
471 | },
472 | "isFlippedHorizontal": false,
473 | "isFlippedVertical": false,
474 | "isLocked": false,
475 | "isVisible": true,
476 | "layerListExpandedType": 0,
477 | "name": "Path",
478 | "nameIsFixed": false,
479 | "resizingType": 0,
480 | "rotation": 0,
481 | "shouldBreakMaskChain": false,
482 | "booleanOperation": -1,
483 | "edited": true,
484 | "path": {
485 | "_class": "path",
486 | "isClosed": true,
487 | "points": [
488 | {
489 | "_class": "curvePoint",
490 | "cornerRadius": 1,
491 | "curveFrom": "{0.80000000000000004, 0.20000000000000001}",
492 | "curveMode": 1,
493 | "curveTo": "{0.80000000000000004, 0.20000000000000001}",
494 | "hasCurveFrom": false,
495 | "hasCurveTo": false,
496 | "point": "{0.80000000000000004, 0.20000000000000001}"
497 | },
498 | {
499 | "_class": "curvePoint",
500 | "cornerRadius": 0,
501 | "curveFrom": "{1, 0.20000000000000001}",
502 | "curveMode": 1,
503 | "curveTo": "{1, 0.20000000000000001}",
504 | "hasCurveFrom": false,
505 | "hasCurveTo": false,
506 | "point": "{1, 0.20000000000000001}"
507 | },
508 | {
509 | "_class": "curvePoint",
510 | "cornerRadius": 0,
511 | "curveFrom": "{1, 1}",
512 | "curveMode": 1,
513 | "curveTo": "{1, 1}",
514 | "hasCurveFrom": false,
515 | "hasCurveTo": false,
516 | "point": "{1, 1}"
517 | },
518 | {
519 | "_class": "curvePoint",
520 | "cornerRadius": 0,
521 | "curveFrom": "{0.20000000000000001, 1}",
522 | "curveMode": 1,
523 | "curveTo": "{0.20000000000000001, 1}",
524 | "hasCurveFrom": false,
525 | "hasCurveTo": false,
526 | "point": "{0.20000000000000001, 1}"
527 | },
528 | {
529 | "_class": "curvePoint",
530 | "cornerRadius": 0,
531 | "curveFrom": "{0.10000000000000005, 0.70000000000000007}",
532 | "curveMode": 2,
533 | "curveTo": "{0.29999999999999999, 0.90000000000000002}",
534 | "hasCurveFrom": true,
535 | "hasCurveTo": true,
536 | "point": "{0.20000000000000001, 0.80000000000000004}"
537 | },
538 | {
539 | "_class": "curvePoint",
540 | "cornerRadius": 0,
541 | "curveFrom": "{0, 0.80000000000000004}",
542 | "curveMode": 1,
543 | "curveTo": "{0, 0.80000000000000004}",
544 | "hasCurveFrom": false,
545 | "hasCurveTo": false,
546 | "point": "{0, 0.80000000000000004}"
547 | },
548 | {
549 | "_class": "curvePoint",
550 | "cornerRadius": 0,
551 | "curveFrom": "{0, 0}",
552 | "curveMode": 1,
553 | "curveTo": "{0, 0}",
554 | "hasCurveFrom": false,
555 | "hasCurveTo": false,
556 | "point": "{0, 0}"
557 | },
558 | {
559 | "_class": "curvePoint",
560 | "cornerRadius": 0,
561 | "curveFrom": "{0.80000000000000004, 0}",
562 | "curveMode": 1,
563 | "curveTo": "{0.80000000000000004, 0}",
564 | "hasCurveFrom": false,
565 | "hasCurveTo": false,
566 | "point": "{0.80000000000000004, 0}"
567 | }
568 | ]
569 | }
570 | };
571 | let shapePath = parse(json);
572 | expect(shapePath).to.be.an.instanceof(ShapePath);
573 | console.log(shapePath.toD());
574 | })
575 | });
--------------------------------------------------------------------------------
/src/__tests__/utils.spec.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai'
2 | import {parseBase64} from '../utils/bplist-parser'
3 | import {getReactCode,cssText2obj,cssText2jsxCode,getFormattedJSXCode} from '../utils/JSX'
4 | import {JSDOM} from 'jsdom'
5 |
6 | describe('bplist-parser', () => {
7 | it('works', () => {
8 | let s = 'YnBsaXN0MDDUAQIDBAUGHyBYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKUHCBEVHFUkbnVsbNQJCgsMDQ4PEFVOU1JHQlxOU0NvbG9yU3BhY2VfEBJOU0N1c3RvbUNvbG9yU3BhY2VWJGNsYXNzTxAsMC42Mzc2NDg4MDk1IDAuMjAxNTQyMjEwNyAwLjIwMTU0MjIxMDcgMC42NQAQAYACgATSEgwTFFROU0lEEAGAA9IWFxgZWiRjbGFzc25hbWVYJGNsYXNzZXNcTlNDb2xvclNwYWNlohobXE5TQ29sb3JTcGFjZVhOU09iamVjdNIWFx0eV05TQ29sb3KiHRtfEA9OU0tleWVkQXJjaGl2ZXLRISJUcm9vdIABAAgAEQAaACMALQAyADcAPQBDAEwAUgBfAHQAewCqAKwArgCwALUAugC8AL4AwwDOANcA5ADnAPQA\/QECAQoBDQEfASIBJwAAAAAAAAIBAAAAAAAAACMAAAAAAAAAAAAAAAAAAAEp';
9 | expect(parseBase64(s)).to.eql({
10 | "alpha": 0.65,
11 | "blue": 0.2015422107,
12 | "green": 0.2015422107,
13 | "red": 0.6376488095,
14 | });
15 | s = "YnBsaXN0MDDUAQIDBAUGJidYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkHCA0XGBkaGyJVJG51bGzSCQoLDFYkY2xhc3NfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4AIgALTDg8JEBMWV05TLmtleXNaTlMub2JqZWN0c6IREoADgASiFBWABYAGgAdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNAOAAAAAAAAFdBcmlhbE1U0hwdHh9aJGNsYXNzbmFtZVgkY2xhc3Nlc18QE05TTXV0YWJsZURpY3Rpb25hcnmjHiAhXE5TRGljdGlvbmFyeVhOU09iamVjdNIcHSMkXxAQTlNGb250RGVzY3JpcHRvcqIlIV8QEE5TRm9udERlc2NyaXB0b3JfEA9OU0tleWVkQXJjaGl2ZXLRKClUcm9vdIABAAgAEQAaACMALQAyADcAQQBHAEwAUwBwAHIAdAB7AIMAjgCRAJMAlQCYAJoAnACeALQAygDTANsA4ADrAPQBCgEOARsBJAEpATwBPwFSAWQBZwFsAAAAAAAAAgEAAAAAAAAAKgAAAAAAAAAAAAAAAAAAAW4=";
16 | expect(parseBase64(s)).to.eql({
17 | "NSFontDescriptorAttributes": {
18 | "NSFontNameAttribute": "ArialMT",
19 | "NSFontSizeAttribute": 24,
20 | }
21 | });
22 | s = "YnBsaXN0MDDUAQIDBAUGb3BYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK8QGAcIGisvNTc6PUBDRklMT1JVWFteYmZna1UkbnVsbNkJCgsMDQ4PEBESExQVFhcVGBlWJGNsYXNzWk5TVGFiU3RvcHNcTlNUZXh0QmxvY2tzXxAPTlNNYXhMaW5lSGVpZ2h0XxASTlNQYXJhZ3JhcGhTcGFjaW5nW05TVGV4dExpc3RzXxAPTlNNaW5MaW5lSGVpZ2h0XE5TSGVhZEluZGVudFtOU0FsaWdubWVudIAXgAKAEiNAAAAAAAAAACNACAAAAAAAAIATI0BCAAAAAAAAEAHSGwkcKlpOUy5vYmplY3RzrR0eHyAhIiMkJSYnKCmAA4AFgAaAB4AIgAmACoALgAyADYAOgA+AEIAR0gksLS5aTlNMb2NhdGlvboAEI0AmAAAAAAAA0jAxMjNaJGNsYXNzbmFtZVgkY2xhc3Nlc1lOU1RleHRUYWKiMjRYTlNPYmplY3TSCSwtGIAE0gksLTmABCNATFMzQAAAANIJLC08gAQjQFVAAAAAAADSCSwtP4AEI0BcVmZgAAAA0gksLUKABCNAYbZmYAAAANIJLC1FgAQjQGVBmaAAAADSCSwtSIAEI0BozMzAAAAA0gksLUuABCNAbFgAAAAAANIJLC1OgAQjQG\/jM0AAAADSCSwtUYAEI0BxtzNAAAAA0gksLVSABCNAc3zMwAAAANIJLC1XgAQjQHVCZmAAAADSMDFZWldOU0FycmF5olk00hsJXCqggBHSGwlfKqFggBSAEdIJY2RlXk5TTWFya2VyRm9ybWF0gBaAFVp7ZGVjaW1hbH0u0jAxaGlaTlNUZXh0TGlzdKJqNFpOU1RleHRMaXN00jAxbG1fEBdOU011dGFibGVQYXJhZ3JhcGhTdHlsZaNsbjRfEBBOU1BhcmFncmFwaFN0eWxlXxAPTlNLZXllZEFyY2hpdmVy0XFyVHJvb3SAAQAIABEAGgAjAC0AMgA3AFIAWABrAHIAfQCKAJwAsQC9AM8A3ADoAOoA7ADuAPcBAAECAQsBDQESAR0BKwEtAS8BMQEzATUBNwE5ATsBPQE\/AUEBQwFFAUcBTAFXAVkBYgFnAXIBewGFAYgBkQGWAZgBnQGfAagBrQGvAbgBvQG\/AcgBzQHPAdgB3QHfAegB7QHvAfgB\/QH\/AggCDQIPAhgCHQIfAigCLQIvAjgCPQI\/AkgCTQJVAlgCXQJeAmACZQJnAmkCawJwAn8CgQKDAo4CkwKeAqECrAKxAssCzwLiAvQC9wL8AAAAAAAAAgEAAAAAAAAAcwAAAAAAAAAAAAAAAAAAAv4=";
23 | expect(parseBase64(s)).to.eql({
24 | "NSAlignment": 1,
25 | "NSHeadIndent": 36,
26 | "NSMaxLineHeight": 2,
27 | "NSMinLineHeight": 2,
28 | "NSParagraphSpacing": 3,
29 | "NSTabStops": [
30 | {
31 | "NSLocation": 11
32 | },
33 | {
34 | "NSLocation": 36
35 | },
36 | {
37 | "NSLocation": 56.650001525878906
38 | },
39 | {
40 | "NSLocation": 85
41 | },
42 | {
43 | "NSLocation": 113.3499984741211
44 | },
45 | {
46 | "NSLocation": 141.6999969482422
47 | },
48 | {
49 | "NSLocation": 170.0500030517578
50 | },
51 | {
52 | "NSLocation": 198.39999389648438
53 | },
54 | {
55 | "NSLocation": 226.75
56 | },
57 | {
58 | "NSLocation": 255.10000610351562
59 | },
60 | {
61 | "NSLocation": 283.45001220703125
62 | },
63 | {
64 | "NSLocation": 311.79998779296875
65 | },
66 | {
67 | "NSLocation": 340.1499938964844
68 | },
69 | ],
70 | "NSTextBlocks": [],
71 | "NSTextLists": [
72 | {
73 | "NSMarkerFormat": "{decimal}."
74 | }
75 | ]
76 | });
77 |
78 | s = "YnBsaXN0MDDUAQIDBAUG5+hYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK8QOQcIERIYKywtLi8wMTI6PkVISU1VVldYWV03YXGBhYiKjZCTlpmcn6KlqKuusbW5ur7Cw8XX2Nvf41UkbnVsbNQJCgsMDQ4PEFhOU1N0cmluZ18QD05TQXR0cmlidXRlSW5mb1xOU0F0dHJpYnV0ZXNWJGNsYXNzgAKANoADgDhZCTEuIAkxMTFh0hMMFBdaTlMub2JqZWN0c6IVFoAEgDOANdMZEwwaIipXTlMua2V5c6cbHB0eHyAhgAWABoAHgAiACYAKgAunIyQlJiYoKYAMgBCAEYAZgBmAGoAxgDJXTlNDb2xvcl8QD05TU3RyaWtldGhyb3VnaF8QH01TQXR0cmlidXRlZFN0cmluZ0ZvbnRBdHRyaWJ1dGVfEChNU0F0dHJpYnV0ZWRTdHJpbmdUZXh0VHJhbnNmb3JtQXR0cmlidXRlW05TVW5kZXJsaW5lXxAQTlNQYXJhZ3JhcGhTdHlsZVZOU0tlcm7UMzQ1DDY3ODlVTlNSR0JcTlNDb2xvclNwYWNlXxASTlNDdXN0b21Db2xvclNwYWNlTxAsMC42Mzc2NDg4MDk1IDAuMjAxNTQyMjEwNyAwLjIwMTU0MjIxMDcgMC42NQAQAYANgA\/SOww8PVROU0lEEAGADtI\/QEFCWiRjbGFzc25hbWVYJGNsYXNzZXNcTlNDb2xvclNwYWNlokNEXE5TQ29sb3JTcGFjZVhOU09iamVjdNI\/QEZHV05TQ29sb3KiRkQQANIMSktMXxAaTlNGb250RGVzY3JpcHRvckF0dHJpYnV0ZXOAGIAS0xkTDE5RVKJPUIATgBSiUlOAFYAWgBdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNAOAAAAAAAAFdBcmlhbE1U0j9AWltfEBNOU011dGFibGVEaWN0aW9uYXJ5o1pcRFxOU0RpY3Rpb25hcnnSP0BeX18QEE5TRm9udERlc2NyaXB0b3KiYERfEBBOU0ZvbnREZXNjcmlwdG9y2QxiY2RlZmdoaWprbG1ub21wN1pOU1RhYlN0b3BzXE5TVGV4dEJsb2Nrc18QD05TTWF4TGluZUhlaWdodF8QEk5TUGFyYWdyYXBoU3BhY2luZ1tOU1RleHRMaXN0c18QD05TTWluTGluZUhlaWdodFxOU0hlYWRJbmRlbnRbTlNBbGlnbm1lbnSAMIAbgCsjQAAAAAAAAAAjQAgAAAAAAACALCNAQgAAAAAAANITDHKArXN0dXZ3eHl6e3x9fn+AHIAegB+AIIAhgCKAI4AkgCWAJoAngCiAKYAq0gyCg4RaTlNMb2NhdGlvboAdI0AmAAAAAAAA0j9AhodZTlNUZXh0VGFiooZE0gyCg3CAHdIMgoOMgB0jQExTM0AAAADSDIKDj4AdI0BVQAAAAAAA0gyCg5KAHSNAXFZmYAAAANIMgoOVgB0jQGG2ZmAAAADSDIKDmIAdI0BlQZmgAAAA0gyCg5uAHSNAaMzMwAAAANIMgoOegB0jQGxYAAAAAADSDIKDoYAdI0Bv4zNAAAAA0gyCg6SAHSNAcbczQAAAANIMgoOngB0jQHN8zMAAAADSDIKDqoAdI0B1QmZgAAAA0j9ArK1XTlNBcnJheaKsRNITDK+AoIAq0hMMsoChs4AtgCrSDLa3uF5OU01hcmtlckZvcm1hdIAvgC5ae2RlY2ltYWx9LtI\/QLu8Wk5TVGV4dExpc3SivURaTlNUZXh0TGlzdNI\/QL\/AXxAXTlNNdXRhYmxlUGFyYWdyYXBoU3R5bGWjv8FEXxAQTlNQYXJhZ3JhcGhTdHlsZSM\/8AAAAAAAANI\/QFzEolxE0xkTDMbOKqcbHB0eHyAhgAWABoAHgAiACYAKgAunIyQlJiYo1YAMgBCAEYAZgBmAGoA0gDIQANI\/QNnaXk5TTXV0YWJsZUFycmF5o9msRNLcDN3eV05TLmRhdGFECAABAYA30j9A4OFdTlNNdXRhYmxlRGF0YaPg4kRWTlNEYXRh0j9A5OVfEBJOU0F0dHJpYnV0ZWRTdHJpbmei5kRfEBJOU0F0dHJpYnV0ZWRTdHJpbmdfEA9OU0tleWVkQXJjaGl2ZXLR6epUcm9vdIABAAgAEQAaACMALQAyADcAcwB5AIIAiwCdAKoAsQCzALUAtwC5AMMAyADTANYA2ADaANwA4wDrAPMA9QD3APkA+wD9AP8BAQEJAQsBDQEPAREBEwEVARcBGQEhATMBVQGAAYwBnwGmAa8BtQHCAdcCBgIIAgoCDAIRAhYCGAIaAh8CKgIzAkACQwJQAlkCXgJmAmkCawJwAo0CjwKRApgCmwKdAp8CogKkAqYCqAK+AtQC3QLlAuoDAAMEAxEDFgMpAywDPwNSA10DagN8A5EDnQOvA7wDyAPKA8wDzgPXA+AD4gPrA\/AD\/gQABAIEBAQGBAgECgQMBA4EEAQSBBQEFgQYBBoEHwQqBCwENQQ6BEQERwRMBE4EUwRVBF4EYwRlBG4EcwR1BH4EgwSFBI4EkwSVBJ4EowSlBK4EswS1BL4EwwTFBM4E0wTVBN4E4wTlBO4E8wT1BP4FAwULBQ4FEwUUBRYFGwUdBR8FIQUmBTUFNwU5BUQFSQVUBVcFYgVnBYEFhQWYBaEFpgWpBbAFuAW6BbwFvgXABcIFxAXGBc4F0AXSBdQF1gXYBdoF3AXeBeAF5QX0BfgF\/QYFBgoGDAYRBh8GIwYqBi8GRAZHBlwGbgZxBnYAAAAAAAACAQAAAAAAAADrAAAAAAAAAAAAAAAAAAAGeA==";
79 | let o = parseBase64(s);
80 | expect(o['NSString']).to.eql("\t1. \t111a");
81 | })
82 | });
83 |
84 | describe('cssText2obj', () => {
85 | it('works', () => {
86 | expect(cssText2obj('font-size : 14px; margin: 1px auto')).to.eql({
87 | fontSize: '14px',
88 | margin: '1px auto',
89 | });
90 | })
91 | });
92 | describe('cssText2jsxCode', () => {
93 | it('works', () => {
94 | expect(cssText2jsxCode('font-size : 14px; margin: 1px auto')).to.eql(`{fontSize: '14px', margin: '1px auto'}`);
95 | })
96 | });
97 | describe('getFormattedJSXCode',()=>{
98 | it('works',()=>{
99 | let dom = new JSDOM(`
`);
100 | let img = dom.window.document.getElementById('img');
101 | expect(getFormattedJSXCode(img)).to.eql(`
\n`);
102 | });
103 | });
104 | describe('getReactCode',()=>{
105 | it('works',()=>{
106 | let dom = new JSDOM(`
`);
107 | let img = dom.window.document.getElementById('img');
108 | expect(getReactCode(img)).to.eql(`import React from 'react'
109 |
110 | export default class MyComp extends React.Component {
111 | render(){
112 | return (
113 |
114 | );
115 | }
116 | }`);
117 | });
118 | });
--------------------------------------------------------------------------------
/src/components/Document/Document.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import JSZip from "jszip";
3 | import {clear, parse} from "../../utils";
4 | import "babel-polyfill";
5 | import Layer from "../Layer/Layer";
6 | import PageSelector from "./PageSelector";
7 | import LayerSelector from "./LayerSelector";
8 | import PropTypes from "prop-types";
9 | import styles from "./Document.styl";
10 | import LayerIndicator from "./LayerIndicator";
11 | import LayerInfo from "./LayerInfo";
12 | import Resolve from 'react-utilities/Resolve'
13 | import {Document as DocumentClass} from '../../models'
14 |
15 | let errorPage =
16 |
Cannot parse this file
17 |
If the file is from old-version sketch, open it with new version(v43 or newer) sketch and save to convert.
18 |
If you find any issue, please {' '}feedback
19 | {' '} to me. Thank you.
;
20 |
21 | export default class Document extends React.PureComponent {
22 | static propTypes = {
23 | blob: PropTypes.oneOfType([
24 | PropTypes.instanceOf(Blob),
25 | PropTypes.instanceOf(Promise),
26 | ])
27 | };
28 |
29 | componentWillMount() {
30 | this.document$ = this.loadBlob(this.props.blob);
31 | }
32 |
33 | componentWillUnmount() {
34 | clear();
35 | }
36 |
37 | async loadBlob(blob) {
38 | let zip = await JSZip.loadAsync(await blob);
39 | let json = JSON.parse(await zip.file('document.json')
40 | .async('string'));
41 | let meta = JSON.parse(await zip.file('meta.json')
42 | .async('string'));
43 | let model = parse(json, zip);
44 | for (let i = 0; i < model.pages.length; ++i) {
45 | let page = model.pages[i] = await model.pages[i].getInstance();
46 |
47 | let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
48 | for (let layer of page.layers) {
49 | minX = Math.min(minX, layer.frame.x);
50 | minY = Math.min(minY, layer.frame.y);
51 | maxX = Math.max(maxX, layer.frame.x + layer.frame.width);
52 | maxY = Math.max(maxY, layer.frame.y + layer.frame.height);
53 | }
54 | minX -= 32;
55 | minY -= 32;
56 | maxX += 32;
57 | maxY += 32;
58 | for (let layer of page.layers) {
59 | layer.frame.x -= minX;
60 | layer.frame.y -= minY;
61 | }
62 | page.frame.x = 0;
63 | page.frame.y = 0;
64 | page.frame.width = maxX - minX;
65 | page.frame.height = maxY - minY;
66 | }
67 | model.meta = meta;
68 | return model;
69 | }
70 |
71 | render() {
72 | let {blob, ...props} = this.props;
73 | return }
76 | rejected={errorPage}
77 | {...props}
78 | >
79 |
80 |
81 | }
82 | }
83 | class DocumentViewer extends React.PureComponent {
84 | static propTypes = {
85 | model: PropTypes.instanceOf(DocumentClass),
86 | };
87 | static childContextTypes = {
88 | onClick: PropTypes.func,
89 | onMouseEnter: PropTypes.func,
90 | onMouseLeave: PropTypes.func,
91 | };
92 |
93 | constructor(props) {
94 | super(props);
95 | this.state = {};
96 | this.layerStack = [];
97 | this.model = this.props.model;
98 | }
99 |
100 | getChildContext() {
101 | return {
102 | onClick: this.selectLayer,
103 | onMouseEnter: this.onLayerEnter,
104 | onMouseLeave: this.onLayerLeave
105 | };
106 | }
107 |
108 | selectPage = (pageID) => {
109 | for (let i = 0; i < this.model.pages.length; ++i) {
110 | if (this.model.pages[i].do_objectID === pageID) {
111 | return this.setState({selectedPage: this.model.pages[i]});
112 | }
113 | }
114 | };
115 |
116 | selectLayer = (layerID) => {
117 | return this.setState({selectedLayer: layerID});
118 | };
119 |
120 | onLayerEnter = (layerID) => {
121 | this.layerStack.push(layerID);
122 | this.setState({hoveredLayer: this.layerStack[this.layerStack.length - 1]});
123 | };
124 |
125 | onLayerLeave = (layerID) => {
126 | if (this.layerStack.length === 0 || layerID !== this.layerStack[this.layerStack.length - 1]) {
127 | console.error('cannot pop layer', this.layerStack, layerID);
128 | }
129 | this.layerStack.pop();
130 | this.setState({hoveredLayer: this.layerStack[this.layerStack.length - 1]});
131 | };
132 |
133 | componentWillMount() {
134 | this.state = {selectedPage: this.props.model.pages[0]};
135 | }
136 |
137 | render() {
138 | let {selectedPage, selectedLayer, hoveredLayer} = this.state;
139 | let {model, ...props} = this.props;
140 |
141 | return (
142 |
143 |
158 | < div className={styles.main}>
159 |
163 |
164 |
165 |
166 |
167 |
168 |
171 |
172 |
173 | );
174 | }
175 | }
--------------------------------------------------------------------------------
/src/components/Document/Document.styl:
--------------------------------------------------------------------------------
1 | :local
2 | .document
3 | display flex
4 | box-sizing border-box
5 | width 100%
6 | min-width 480px
7 | height 100%
8 | min-height 300px
9 | border-top 1px solid #B8B8B8
10 | font-family "Helvetica Neue", Helvetica, Arial, sans-serif
11 | .document-error
12 | composes document
13 | background #f2f2f2
14 | color #777
15 | font-size 16px
16 | line-height 1.6
17 | text-align center
18 | display block
19 | h1
20 | max-width 480px
21 | margin 64px auto 0
22 | text-align center
23 | font-size 18px
24 | font-weight bold
25 | p
26 | max-width 480px
27 | margin 16px auto
28 | a
29 | text-align none
30 | color #3884ff
31 |
32 | .document-loading
33 | composes document
34 | composes: global(spin)
35 | background #f2f2f2
36 | align-items center
37 | justify-content center
38 |
39 | .sidebar-selector
40 | width 240px
41 | height 100%
42 | box-sizing border-box
43 | flex none
44 | border-right 1px solid #B8B8B8
45 | .main
46 | position relative
47 | flex 1
48 | overflow auto
49 | height 100%
50 | background #f2f2f2
51 | .canvas
52 | position relative
53 | min-width 100%
54 | min-height 100%
55 | .sidebar-info
56 | width 240px
57 | box-sizing border-box
58 | flex none
59 | height 100%
60 | border-left 1px solid #B8B8B8
61 |
62 | .treeview-item
63 | display flex
64 | align-items center
65 | cursor default
66 | padding-right 8px
67 | &:global(.even)
68 | background #f0f0f0
69 | &.artboard
70 | background #f9f9f9
71 | box-shadow inset 0 -0.5px 0 0 #B8B8B8, inset 0 0.5px 0 0 #B8B8B8
72 | &:global(.hover), &:hover
73 | box-sizing border-box
74 | box-shadow inset 0 0 0 2px #44c1ff
75 | &:global(.hidden)
76 | color #929292
77 | &:global(.selected)
78 | color white
79 | background #6e9ee1
80 | .icon
81 | fill white
82 | color white
83 | .icon-expand
84 | &::after
85 | border-top-color white
86 | .text
87 | flex 1
88 | margin-left 8px
89 | overflow hidden
90 | text-overflow ellipsis
91 | white-space nowrap
92 | .icon
93 | margin-left 8px
94 | flex none
95 | fill #a6a6a6
96 | color #a6a6a6
97 | width 16px
98 | height 16px
99 | line-height 16px
100 | text-align center
101 | user-select none
102 | &.icon-symbol
103 | fill #8f44b7
104 | &.icon-expand
105 | width 12px
106 | height 16px
107 | margin-left 6px
108 | &.icon-mask
109 | transform rotate(90deg)
110 | width 10px
111 | margin-right -4px
112 | color #4e4e4e
113 | .icon-expand
114 | box-sizing border-box
115 | padding 6px 0 4px
116 | &::after
117 | content ''
118 | display block
119 | width 12px
120 | height 6px
121 | box-sizing border-box
122 | border-width 6px 6px 0
123 | border-color #a6a6a6 rgba(0, 0, 0, 0)
124 | border-style solid
125 | .frame
126 | box-sizing border-box
127 | position absolute
128 | pointer-events none
129 | .frame-hovered
130 | composes frame
131 | border 2px solid #44c0ff
132 | .frame-selected
133 | composes frame
134 | border 1px solid #fa571f
135 | .frame-expand
136 | composes frame
137 | border 1px dotted #fa571f
138 | .frame-note
139 | composes frame
140 | font-size 12px
141 | line-height 16px
142 | color #fa571f
143 | background rgba(248, 248, 248, 0.85)
144 | border-radius 2px
145 | padding 0 4px
146 | transform translate(-50%, -50%)
147 |
148 | .info
149 | background #f2f2f2
150 | height 100%
151 | overflow auto
152 | .empty
153 | font-size 16px
154 | color #999
155 | text-align center
156 | padding 14px
157 | .section-header
158 | color #666
159 | font-weight bold
160 | padding-left 8px
161 | font-size 12px
162 | line-height 27px
163 | background #f9f9f9
164 | box-shadow inset 0 -0.5px 0 0 #B8B8B8, inset 0 0.5px 0 0 #B8B8B8
165 | .previewer
166 | margin 16px
167 | background url(./bg.png)
168 | background-color white
169 | background-size 8px
170 | .code
171 | margin 0
172 | background white
173 | padding 8px;
174 | font-family: monospace;
175 | color: #666;
176 | overflow auto
177 |
178 | .spin
179 | &::before
180 | content ''
181 | display block
182 | width 32px
183 | height 32px
184 | border-radius 50%
185 | border-color #3884ff #3884ff transparent transparent
186 | border-style solid
187 | border-width 4px
188 | animation .5s linear 0s infinite normal none running rotate
189 |
190 | @keyframes rotate {
191 | from {
192 | transform: rotate(0)
193 | }
194 |
195 | to {
196 | transform: rotate(360deg)
197 | }
198 | }
--------------------------------------------------------------------------------
/src/components/Document/LayerIndicator.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import {getObjectById} from "../../utils";
4 | import styles from "./Document.styl";
5 | function getp(a1, a2, b1, b2) { // a1 <= a2, b1 <= b2, select a, hover b
6 | if (b1 <= a1 && a2 <= b2) {
7 | return (a1 + a2) / 2;
8 | } else {
9 | return (b1 + b2) / 2;
10 | }
11 | }
12 | function f(a1, a2, b1, b2) {// a1 <= a2, b1 <= b2
13 | if (a1 > b1) {
14 | return f(b1, b2, a1, a2);
15 | }
16 | // a1 <= b1
17 | if (a2 < b1) {
18 | return [[a2, b1]]
19 | }
20 | if (a2 < b2) {
21 | return [[a1, b1], [a2, b2]];
22 | }
23 | return [[a1, b1], [b2, a2]];
24 | }
25 | function hLine(x1, x2, y, i) {
26 | return
34 |
39 |
41 | {`${Math.round(x2 - x1)}px`}
42 |
43 |
44 | }
45 | function vLine(y1, y2, x, i) {
46 | return
54 |
59 |
61 | {`${Math.round(y2 - y1)}px`}
62 |
63 |
64 | }
65 | function gao(a, b, o) {// select a, hover b, canvas o
66 | let [ax1, ax2, ay1, ay2] = [a.left - o.left, a.left + a.width - o.left, a.top - o.top, a.top + a.height - o.top];
67 | let [bx1, bx2, by1, by2] = [b.left - o.left, b.left + b.width - o.left, b.top - o.top, b.top + b.height - o.top];
68 | let x = getp(ax1, ax2, bx1, bx2);
69 | let y = getp(ay1, ay2, by1, by2);
70 | let hLines = f(ax1, ax2, bx1, bx2).filter(([a, b]) => b - a > 0.5).map(([left, right], i) =>
71 | hLine(left, right, y, i));
72 | let vLines = f(ay1, ay2, by1, by2).filter(([a, b]) => b - a > 0.5).map(([top, bottom], i) =>
73 | vLine(top, bottom, x, i));
74 | return hLines.concat(vLines);
75 | }
76 |
77 | export default class LayerIndicator extends React.PureComponent {
78 | static propTypes = {
79 | selectedLayer: PropTypes.string,
80 | hoveredLayer: PropTypes.string,
81 | };
82 |
83 | render() {
84 | let {selectedLayer, hoveredLayer} = this.props;
85 | if (!selectedLayer && !hoveredLayer) {
86 | return false
87 | }
88 | let canvasDOM = document.getElementById('canvas');
89 | let hoveredDOM = document.getElementById(hoveredLayer);
90 | let selectedlayerDOM = document.getElementById(selectedLayer);
91 |
92 | let r = selectedlayerDOM && selectedlayerDOM.getBoundingClientRect();
93 | let b = hoveredDOM && hoveredDOM.getBoundingClientRect();
94 | let o = canvasDOM.getBoundingClientRect();
95 | let selectedmodel = getObjectById(selectedLayer);
96 | return (
97 |
98 | {b &&
}
104 | {r && b && gao(r, b, o)}
105 | {r &&
111 |
118 |
125 | { (r && !b || selectedLayer === hoveredLayer) &&
126 |
{`${Math.round(selectedmodel.frame.width)}px`}
130 |
131 | }
132 | { (r && !b || selectedLayer === hoveredLayer) &&
133 |
{`${Math.round(selectedmodel.frame.height)}px`}
138 |
139 | }
140 |
}
141 |
142 |
143 | );
144 |
145 | }
146 | }
--------------------------------------------------------------------------------
/src/components/Document/LayerInfo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import styles from "./Document.styl";
4 | import hljs from 'highlight.js'
5 | import select from 'select'
6 |
7 | require('highlight.js/styles/color-brewer.css');
8 | import {getReactCode} from '../../utils'
9 | const NodeTypes = {
10 | Element: 1,
11 | };
12 | function removeIds(o) {
13 | if (o.nodeType === NodeTypes.Element) {
14 | o.removeAttribute('id');
15 | if (o.nodeName !== 'SVG') {
16 | for (let child of o.childNodes) {
17 | removeIds(child);
18 | }
19 | }
20 | }
21 | }
22 |
23 | function getOuterHtmlCode(o, depth = 0, jsx = false) {
24 | let spaces = '';
25 | for (let i = 0; i < depth; ++i) {
26 | spaces += ' ';
27 | }
28 | let tagName = o.tagName.toLowerCase();
29 | switch (tagName) {
30 | case 'img':
31 | case 'path':
32 | case 'span':
33 | return `${spaces}${o.outerHTML}\n`;
34 | default:
35 | return `${spaces}<${tagName} style="${o.style.cssText}">\n${Array.prototype.map.call(o.childNodes, child => getOuterHtmlCode(child, depth + 1)).join('')}${spaces}${tagName}>\n`
36 | }
37 | }
38 | export default class LayerInfo extends React.Component {
39 |
40 | static propTypes = {
41 | layer: PropTypes.string,
42 | };
43 |
44 | componentWillMount() {
45 | this.state = {codeMode: 'react'};
46 | }
47 |
48 | selectCodeMode = (e) => {
49 | this.setState({codeMode: e.target.value});
50 | };
51 |
52 | render() {
53 | let {layer} = this.props;
54 | if (!layer) {
55 | return
58 | } else {
59 | let _o = document.getElementById(layer);
60 | let o = _o.cloneNode();
61 | o.innerHTML = _o.innerHTML;
62 | removeIds(o);
63 | o.style.cssText = o.style.cssText.replace(/top:[^;]*;/, '').replace(/left:[^;]*;/, '');
64 | let ratio = _o.clientWidth > 208 ? 208 / _o.clientWidth : 1;
65 |
66 | return
67 |
81 |
82 |
CSS (outer)
83 |
85 |
86 |
87 |
101 | {this.state.codeMode === 'html' ?
102 |
104 |
:
105 |
107 |
108 | }
109 |
110 |
;
111 | }
112 | }
113 | }
--------------------------------------------------------------------------------
/src/components/Document/LayerSelector.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import classnames from "classnames";
3 | import folderImg from "./folder.png";
4 | import styles from "./Document.styl";
5 | let folder =
;
6 | let symbol = ;
12 | let bitmap = ;
19 | let eye = ;
25 | let lock = ;
29 | export default class LayerSelector extends React.PureComponent {
30 | constructor(props) {
31 | super(props);
32 | }
33 |
34 | toggleExpand = (model, e) => {
35 | if (model.layerListExpandedType === 1) {
36 | model.layerListExpandedType = 0;
37 | } else {
38 | model.layerListExpandedType = 1;
39 | }
40 | e && e.stopPropagation();
41 | e && e.preventDefault();
42 |
43 | this.forceUpdate();
44 | return false;
45 | };
46 |
47 | treeview = (model, depth = 0, hasMask) => {
48 | let {selectedLayer, hoveredLayer, onSelect, onMouseEnter, onMouseLeave} = this.props;
49 |
50 | let canExpanded = model.layers && model._class !== 'shapeGroup' && (
51 | model.layers.length > 0 ||
52 | model._class === 'artboard'
53 | );
54 | let isExpanded = model.layerListExpandedType !== 1;
55 |
56 | let layerMaskFlag = [];
57 | if (canExpanded && isExpanded) {
58 | let underLyingMask = false;
59 | for (let layer of model.layers) {
60 | if (layer.hasClippingMask) {
61 | underLyingMask = true;
62 | layerMaskFlag.push(false);
63 | continue;
64 | }
65 | if (layer.shouldBreakMaskChain) {
66 | underLyingMask = false;
67 | }
68 | layerMaskFlag.push(underLyingMask);
69 | }
70 | layerMaskFlag.reverse();
71 | }
72 | ++this.idx;
73 |
74 | return
75 |
onSelect(model.do_objectID)}
84 | onMouseEnter={() => onMouseEnter(model.do_objectID)}
85 | onMouseLeave={() => onMouseLeave(model.do_objectID)}
86 | >
87 | this.toggleExpand(model, e)}/>
91 | {hasMask &&
↳
}
92 | {model._class === 'group' && folder}
93 | {model._class === 'text' &&
94 |
Aa
}
95 | {model._class === 'shapeGroup' &&
96 |
S
}
97 | {(model._class === 'symbolInstance' || model._class === 'symbolMaster') && symbol}
98 | {model._class === 'bitmap' && bitmap}
99 |
{model.name}
100 | {!model.isVisible && eye}
101 | {model.isVisible && model.isLocked && lock}
102 |
103 | { canExpanded && isExpanded &&
104 |
105 | {model.layers.slice(0).reverse().map((layer, i) =>
106 | this.treeview(layer, depth + 1, layerMaskFlag[i])
107 | )}
108 |
109 | }
110 |
111 | };
112 |
113 | render() {
114 | let {model, selectedLayer, hoveredLayer, onSelect, onMouseEnter, onMouseLeave, style, ...props} = this.props;
115 | this.idx = 0;
116 | return (
117 |
124 |
130 | {model.name}
131 |
132 |
136 | {
137 | model.layers.map((artboard) =>
138 | this.treeview(artboard)
139 | ).reverse()
140 | }
141 |
142 |
);
143 | }
144 | }
--------------------------------------------------------------------------------
/src/components/Document/PageSelector.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 |
4 | export default class PageSelector extends React.PureComponent {
5 | render() {
6 | let {model, selectedPage, onSelect, style, ...props} = this.props;
7 | let {pagesAndArtboards} = model;
8 | return (
9 |
16 |
Pages
21 |
22 |
23 | {
24 | Object.keys(pagesAndArtboards).reverse().map((id, i) =>
25 |
onSelect(id)}>{pagesAndArtboards[id].name}
34 | )
35 | }
36 |
37 |
);
38 | }
39 | }
--------------------------------------------------------------------------------
/src/components/Document/PageSelector/__fixtures__/default.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | selectedPage: "9B2D6A75-D2C0-40F3-88AB-AE4BD625E520",
4 | model: {
5 | "9B2D6A75-D2C0-40F3-88AB-AE4BD625E520": {
6 | "name": "Page 1",
7 | "artboards": {
8 | "A8E5EE18-1107-4A18-BC0C-BE7EAD0135DD": {
9 | "name": "Artboard"
10 | },
11 | "EE2328CD-533A-494C-B438-085002962A3B": {
12 | "name": "Menu"
13 | },
14 | "B8ECDD04-C5B6-496B-B640-7743425317F7": {
15 | "name": "Detail View"
16 | },
17 | "98555297-4575-40EC-AB67-B1C3918F163C": {
18 | "name": "History"
19 | },
20 | "0D7CFF39-0795-427A-A195-749BCFB97FA4": {
21 | "name": "Timeline"
22 | },
23 | "10E1A27E-C298-40A5-8547-A62A031514C6": {
24 | "name": "Friends"
25 | },
26 | "8FC9E048-A8C7-4D6D-9295-ABF623D28EF7": {
27 | "name": "Artboard 2"
28 | },
29 | "DDD74CEB-8B48-487D-92D8-4FD3BDB04411": {
30 | "name": "Profile"
31 | }
32 | }
33 | },
34 | "FE9FC96B-0BC2-4F9D-9103-291E81FFBC89": {
35 | "name": "Test",
36 | "artboards": {
37 | "67721912-B3C8-4E42-ADD7-6B1DAA198C25": {
38 | "name": "Artboard 2"
39 | },
40 | "15B11D08-4924-4C01-B238-D75146184310": {
41 | "name": "xx"
42 | },
43 | "F0A42559-1D43-4F22-8343-4281CB57F7C1": {
44 | "name": "Artboard"
45 | }
46 | }
47 | },
48 | "1C01EF08-06C2-469A-8EB7-0BF12367FF6E": {
49 | "name": "Symbols",
50 | "artboards": {
51 | "32FD6548-E53C-4615-88E6-117FBDACCF42": {
52 | "name": "Background"
53 | },
54 | "156E2FF8-7889-431D-82F1-9021CE955F5E": {
55 | "name": "123"
56 | },
57 | "94D9121A-2939-4CDA-8871-C4934FF48F23": {
58 | "name": "Settings + Settings"
59 | },
60 | "3D529B09-3AA9-41FF-9675-1C6895CC20A9": {
61 | "name": "Artboard"
62 | },
63 | "3C69524A-C8CD-4847-B6C1-62807F339A48": {
64 | "name": "Status Bar"
65 | },
66 | "72C43561-2764-4D7C-8C6A-5A4B3FED4727": {
67 | "name": "Time"
68 | }
69 | }
70 | }
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/src/components/Document/__fixtures__/Fitness App.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjuasmn/sketch-react/8582552e8538cdf507f6899724d4a9124028d394/src/components/Document/__fixtures__/Fitness App.sketch
--------------------------------------------------------------------------------
/src/components/Document/__fixtures__/document.js:
--------------------------------------------------------------------------------
1 | // let fileUrl = require('./ui-video-simple-john-hansen.sketch');
2 | let fileUrl = require('./Fitness App.sketch');
3 |
4 | export default {props: {blob: fetch(fileUrl).then(response => response.blob())}}
--------------------------------------------------------------------------------
/src/components/Document/__fixtures__/ui-video-simple-john-hansen.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjuasmn/sketch-react/8582552e8538cdf507f6899724d4a9124028d394/src/components/Document/__fixtures__/ui-video-simple-john-hansen.sketch
--------------------------------------------------------------------------------
/src/components/Document/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjuasmn/sketch-react/8582552e8538cdf507f6899724d4a9124028d394/src/components/Document/bg.png
--------------------------------------------------------------------------------
/src/components/Document/folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjuasmn/sketch-react/8582552e8538cdf507f6899724d4a9124028d394/src/components/Document/folder.png
--------------------------------------------------------------------------------
/src/components/Layer/Artboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Group from './Group'
3 |
4 | export default class Artboard extends Group {
5 | render() {
6 | let {style, ...props} = this.props;
7 | return ;
14 | }
15 | }
--------------------------------------------------------------------------------
/src/components/Layer/Bitmap.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import classnames from 'classnames'
3 |
4 |
5 | export default class Bitmap extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {};
9 | }
10 |
11 | componentWillMount() {
12 | this.props.model.image.getInstance().then(url => {
13 | this.setState({url});
14 | });
15 | }
16 |
17 | render() {
18 | let {model, style, ...props} = this.props;
19 | let {frame} = model;
20 | return
33 | }
34 | }
--------------------------------------------------------------------------------
/src/components/Layer/Group.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function isMaskGroup(model) {
4 | if (!(model.layers && model.layers.length > 1 && model.layers[0].hasClippingMask)) {
5 | return false;
6 | }
7 | for (let layer of model.layers) {
8 | if (layer.shouldBreakMaskChain) {
9 | return false;
10 | }
11 | }
12 | let mask = model.layers[0];
13 | return mask.isSimple && mask.isSimple();
14 | }
15 |
16 | export default class Group extends React.Component {
17 | render() {
18 | let {model, children, style, ...props} = this.props;
19 | let {frame} = model;
20 |
21 |
22 | let layoutStyles = model.getLayoutStyles();
23 | style = {
24 | position: 'absolute',
25 | height: frame.height,
26 | width: frame.width,
27 | top: frame.y,
28 | left: frame.x,
29 | ...model.style.toStyle(model),
30 | ...style,
31 | ...layoutStyles[model['do_objectID']]
32 | };
33 |
34 | children = React.Children.map(children, child => React.cloneElement(child, {style: layoutStyles[child.props.model['do_objectID']]}))
35 |
36 | if (!isMaskGroup(model)) {
37 | return (
38 |
39 | {children}
40 |
);
41 | }
42 | let mask = model.layers[0];
43 | let maskStyle = mask.style.toStyle(mask);
44 |
45 | return (
46 | {
55 | mask.frame.y || mask.frame.x ?
56 |
61 | {children}
62 |
63 | :
64 | children
65 | }
66 |
67 |
);
68 |
69 | }
70 | }
--------------------------------------------------------------------------------
/src/components/Layer/Layer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Page from "./Page";
3 | import Artboard from "./Artboard";
4 | import ShapeGroup from "./ShapeGroup";
5 | import Group from "./Group";
6 | import SymbolInstance from "./Symbol";
7 | import Text from "./Text";
8 | import Bitmap from "./Bitmap";
9 | import {getSymbolById} from "../../utils";
10 | import PropTypes from "prop-types";
11 |
12 | class PlaceHolder extends React.Component {
13 | render() {
14 | let {model} = this.props;
15 |
16 | let {frame} = model;
17 | if (!frame) {
18 | debugger
19 | }
20 | return {model._class}
;
29 | }
30 | }
31 | function getComp(_class) {
32 | switch (_class) {
33 | case 'mask':
34 | return Mask;
35 | case 'page':
36 | return Page;
37 | case 'artboard':
38 | case 'symbolMaster':
39 | return Artboard;
40 | case `shapeGroup`:
41 | return ShapeGroup;
42 | case `group`:
43 | return Group;
44 | case `text`:
45 | return Text;
46 | case `symbolInstance`:
47 | return SymbolInstance;
48 | case `bitmap`:
49 | return Bitmap;
50 | default:
51 | return PlaceHolder;
52 | }
53 | }
54 | class Mask extends React.Component {
55 | render() {
56 | let {model, children, ...props} = this.props;
57 | let {frame} = model;
58 |
59 | let style = {
60 | position: 'absolute',
61 | overflow: 'hidden',
62 | height: frame.height,
63 | width: frame.width,
64 | top: frame.y,
65 | left: frame.x,
66 | };
67 | return (
68 |
69 | {children}
70 |
);
71 | }
72 | }
73 | export default class Layer extends React.PureComponent {
74 | static contextTypes = {
75 | onClick: PropTypes.func,
76 | onMouseEnter: PropTypes.func,
77 | onMouseLeave: PropTypes.func,
78 | };
79 | onClick = (e) => {
80 | if (this.props.inSymbol) {
81 | return;
82 | }
83 | this.context.onClick(this.props.model.do_objectID);
84 | e.stopPropagation();
85 | };
86 | onMouseEnter = (e) => {
87 | if (this.props.inSymbol) {
88 | return;
89 | }
90 | this.context.onMouseEnter(this.props.model.do_objectID);
91 | // e.stopPropagation();
92 | };
93 | onMouseLeave = (e) => {
94 | if (this.props.inSymbol) {
95 | return;
96 | }
97 | this.context.onMouseLeave(this.props.model.do_objectID);
98 | // e.stopPropagation();
99 | };
100 |
101 | render() {
102 | let {model, inSymbol, ...props} = this.props;
103 | let {layers, _class} = model;
104 | let Comp = getComp(_class);
105 | if (Comp === SymbolInstance) {
106 | layers = getSymbolById(model['symbolID']).layers;
107 | }
108 | return
115 | {layers && layers.map(layer => )}
117 |
118 | }
119 | }
--------------------------------------------------------------------------------
/src/components/Layer/Page.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import classnames from 'classnames'
3 | import Artboard from "./Artboard";
4 |
5 | export default class Page extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 | render() {
11 | let {model, children, ...props} = this.props;
12 |
13 | return (
14 |
17 |
18 | {children}
19 |
20 | {model.layers.map(artboard =>
{artboard.name}
)}
30 |
31 |
);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Layer/ShapeGroup.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {BooleanOperation} from 'sketch-constants'
3 |
4 | export default class ShapeGroup extends React.Component {
5 |
6 | render() {
7 | let {model, children, style, ...props} = this.props;
8 | let {frame} = model;
9 | if (model.isSimple()) {
10 | style = {
11 | ...model.style.toStyle(model),
12 | ...style
13 | };
14 | return {
25 | !('background' in style) && model.style['fills'] && model.style['fills']
26 | .filter(fill => fill['isEnabled'])
27 | .map((fill, i) =>
)
38 | }
39 | } else {
40 | let ds = [];
41 | let d = '';
42 | for (let layer of model.layers) {
43 | if (layer['isVisible']) {
44 | if (layer['booleanOperation'] === BooleanOperation.Union) {
45 | ds.push(d);
46 | d = '';
47 | }
48 | d += layer.toD();
49 | }
50 | }
51 | ds.push(d);
52 | return ;
66 | }
67 |
68 | }
69 | }
--------------------------------------------------------------------------------
/src/components/Layer/Symbol.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 |
4 | export default class Symbol extends React.Component {
5 | render() {
6 | let {model: {frame}, style, ...props} = this.props;
7 | style = {
8 | position: 'absolute',
9 | height: frame.height,
10 | width: frame.width,
11 | top: frame.y,
12 | left: frame.x,
13 | ...style,
14 | };
15 | return ;
16 | }
17 | }
--------------------------------------------------------------------------------
/src/components/Layer/Text.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import classnames from 'classnames'
3 |
4 | export default class Text extends React.Component {
5 | render() {
6 | let {model, style, ...props} = this.props;
7 | let {frame} = model;
8 |
9 | return {model.attributedString.archivedAttributedString.NSString};
20 | }
21 | }
--------------------------------------------------------------------------------
/src/components/Layer/__fixtures__/layer.js:
--------------------------------------------------------------------------------
1 | // let json = require('../../../../playground/9B2D6A75-D2C0-40F3-88AB-AE4BD625E520.json');
2 | // import {parse} from '../../../utils'
3 | //
4 | // export default {props:{model:parse(json)}}
--------------------------------------------------------------------------------
/src/components/Site/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Document from '../Document/Document'
3 | import "./index.css";
4 | import fetch from 'isomorphic-fetch'
5 | import Loading from './Loading'
6 | import qs from 'querystring'
7 |
8 | // https://github.com/github/fetch/issues/89#issuecomment-256610849
9 | function futch(url, opts = {}, onProgress) {
10 | return new Promise((res, rej) => {
11 | let xhr = new XMLHttpRequest();
12 | xhr.responseType = "blob";
13 | xhr.open(opts.method || 'get', url);
14 | for (let k in opts.headers || {})
15 | xhr.setRequestHeader(k, opts.headers[k]);
16 | xhr.onload = e => res(e.target);
17 | xhr.onerror = rej;
18 | // event.loaded / event.total * 100 ; //event.lengthComputable
19 | if (onProgress) {
20 | xhr.upload && xhr.upload.addEventListener('progress', onProgress);
21 | xhr.addEventListener('progress', onProgress);
22 | }
23 | xhr.send(opts.body);
24 | });
25 | }
26 |
27 | class Header extends React.PureComponent {
28 | onClickTitle = () => {
29 | this.fileInput && this.fileInput.click();
30 | };
31 | gao = (input) => {
32 | this.fileInput = input;
33 | };
34 |
35 | render() {
36 | let {file, filename = '', onSelectFile} = this.props;
37 | return
56 | }
57 | }
58 | const simpleFiles = [
59 | 'https://zjuasmn.github.io/sketch-react/images/ui-video-simple-john-hansen.sketch',
60 | 'https://zjuasmn.github.io/sketch-react/images/Fitness%20App.sketch',
61 | ];
62 | function getName(url) {
63 | return decodeURIComponent(url.substring(url.lastIndexOf('/') + 1));
64 | }
65 | export default class App extends React.Component {
66 | constructor(props) {
67 | super(props);
68 | this.state = {};
69 | }
70 |
71 | onSelectFile = (e) => {
72 | if (!e) {
73 | return this.setState({file: null, filename: ''});
74 | }
75 | e.preventDefault();
76 | let file;
77 | if (e.dataTransfer) {
78 | file = e.dataTransfer.files[0];
79 | } else if (e.target.files.length) {
80 | file = e.target.files[0];
81 | }
82 | if (file) {
83 |
84 | this.setState({file, filename: file.name});
85 | }
86 | };
87 | onClickButton = () => {
88 | this.fileInput && this.fileInput.click();
89 | };
90 | gao = (input) => {
91 | this.fileInput = input;
92 | };
93 | selectFileFromURL = (url) => {
94 | this.setState({file: fetch(url).then(r => r.blob()), filename: getName(url)})
95 | };
96 |
97 | componentWillMount() {
98 | console.log('mount', this.state);
99 | let url;
100 | if (this.props.__url) {// for test
101 | url = this.props.__url
102 | } else {
103 | let queryObj = qs.parse(location.search.substr(1));
104 | if ('url' in queryObj) {
105 | url = queryObj['url'];
106 | }
107 | }
108 | if (url) {
109 | this.setState({loading: true, filename: getName(url)});
110 | futch(url, {}, (progress) => {
111 | this.setState({progress})
112 | })
113 | .then(e => {
114 | if (200 <= e.status && e.status < 300) {
115 | this.setState({loading: false, file: e.response});
116 | } else {
117 | throw new Error('fail to fetch file from this url!');
118 |
119 | }
120 | })
121 | .catch(e => {
122 | this.setState({loading: false});
123 | if(e.message) {
124 | alert(e.message);
125 | }else{
126 | alert(`Fail to load from this url. Does it allow cross domain access?`);
127 | }
128 | location.search = '';
129 | });
130 | }
131 | }
132 |
133 | render() {
134 | let {file, filename, loading, progress} = this.state;
135 |
136 | return
137 |
138 | { loading &&
139 | location.search = ''}/>}
146 | {!loading && !file &&
147 |
From Local File
148 |
{
150 | e.preventDefault();
151 | }}
152 | onDragLeave={(e) => {
153 | e.preventDefault();
154 | }}
155 | onDragOver={(e) => {
156 | e.preventDefault();
157 | }}
158 | onDrop={this.onSelectFile}
159 | // onDropCapture={(e) => {
160 | // e.preventDefault();
161 | // console.log(e);
162 | // debugger
163 | // }}
164 | >
165 |
Drop .sketch(v43+) file here
166 |
Or select from computer
168 |
169 |
181 | {/*
*/}
189 |
190 |
Sample Files
191 |
192 | {simpleFiles.map(url =>
193 |
197 | )}
198 |
199 |
200 |
}
201 | {!loading && file && }
202 |
203 | }
204 | }
--------------------------------------------------------------------------------
/src/components/Site/App/__fixtures__/default.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | // __url: 'https://zjuasmn.github.io/sketch-react/images/Fitness%20App.sketch',
4 | }
5 | }
--------------------------------------------------------------------------------
/src/components/Site/Loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import prettyBytes from '../../utils/pretty-bytes'
3 |
4 | export default class Loading extends React.Component {
5 | render() {
6 | let {filename, progress = {total: 0, loaded: 0}, onCancel, style, ...props} = this.props;
7 | let ratio = progress.total ? progress.loaded / progress.total : 0;
8 | return (
9 |
15 |
{filename}
21 |
35 |
{prettyBytes(progress.total)}
46 |
{`${(ratio * 100).toFixed(2)}%`}
56 |
73 | Cancel
74 |
75 |
76 | );
77 | }
78 | }
--------------------------------------------------------------------------------
/src/components/Site/Loading/__fixtures__/default.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | progress: {
4 | total: 15461421,
5 | loaded: 2302000,
6 | },
7 | onCancel: () => alert('cancel click'),
8 | filename: 'sample.sketch',
9 | }
10 | }
--------------------------------------------------------------------------------
/src/components/Site/index.css:
--------------------------------------------------------------------------------
1 | html, body, #root {
2 | height: 100%;
3 | margin: 0;
4 | padding: 0;
5 | }
6 | body{
7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
8 | }
9 | .header {
10 | height: 32px;
11 | display: flex;
12 | align-items: center;
13 | padding: 0 16px;
14 | box-shadow: 0 1px 0 0 #b2b2b2;
15 | }
16 |
17 | .header a {
18 | color: #666;
19 | text-decoration: none;
20 | /*cursor: pointer;*/
21 | }
22 |
23 | .header a:hover {
24 | color: #333;
25 | }
26 |
27 | .flex {
28 | flex: 1;
29 | }
30 |
31 | .title {
32 | text-decoration: none;
33 | font-size: 18px;
34 | color: #666;
35 | width: 120px;
36 | }
37 |
38 | .filename {
39 | flex: 1;
40 | text-align: center;
41 | color: #666;
42 | font-size: 16px;
43 |
44 | }
45 |
46 | .title:hover {
47 | color: #333;
48 | }
49 |
50 | .icon {
51 | fill: #999;
52 |
53 | }
54 |
55 | .actions {
56 | width: 120px;
57 | }
58 |
59 | .icon-link {
60 | display: block;
61 | float: right;
62 | height: 16px;
63 | }
64 |
65 | .icon-link:hover .icon {
66 | fill: #333;
67 | }
68 |
69 | .upload-area {
70 | margin: 64px auto 0;
71 | height: 180px;
72 | width: 320px;
73 | padding: 16px;
74 | border: 2px dashed #666;
75 | display: flex;
76 | flex-direction: column;
77 | }
78 | .upload-area .desc{
79 | flex: 1;
80 | text-align: center;
81 | padding-top: 32px;
82 | font-size: 16px;
83 | color: #666;
84 | }
85 | .upload-button {
86 | cursor: pointer;
87 | display: block;
88 | background: #3884ff;
89 | color: white;
90 | text-align: center;
91 | padding: 4px 16px;
92 | border-radius: 3px;
93 | font-size: 14px;
94 | line-height: 24px;
95 | }
96 |
97 | .sample {
98 | margin: 16px auto;
99 | text-align: center;
100 | }
101 | .sample-header{
102 | color: #666;
103 | }
104 | .sample-link{
105 | cursor: pointer;
106 |
107 | font-size: 16px;
108 | line-height: 24px;
109 | margin-top: 8px;
110 | }
111 | .sample-link a{
112 | color: #3884ff;
113 | text-decoration: underline;
114 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Document from './components/Document/Document'
2 |
3 | export default Document;
--------------------------------------------------------------------------------
/src/models/index.js:
--------------------------------------------------------------------------------
1 | import "babel-polyfill";
2 | import {BlendingMode, CurvePointMode, FillType} from "sketch-constants";
3 | import kebabCase from "lodash/kebabCase";
4 |
5 |
6 | export class Page {
7 | static _class = 'page';
8 | }
9 |
10 | export class Rect {
11 | static _class = 'rect';
12 |
13 | set x(_x) {
14 | this._x = _x;
15 | }
16 |
17 | get x() {
18 | return Math.round(this._x);
19 | }
20 |
21 | set y(_y) {
22 | this._y = _y;
23 | }
24 |
25 | get y() {
26 | return Math.round(this._y);
27 | }
28 |
29 | set width(_width) {
30 | this._width = _width;
31 | }
32 |
33 | get width() {
34 | return Math.round(this._width);
35 | }
36 |
37 | set height(_height) {
38 | this._height = _height;
39 | }
40 |
41 | get height() {
42 | return Math.round(this._height);
43 | }
44 | }
45 | export class ShapeGroup {
46 | static _class = 'shapeGroup';
47 |
48 | isSimple() {
49 | let {layers} = this;
50 | if (layers && layers.length === 1 && !layers[0]['edited']) {
51 | if (layers[0] instanceof Oval && this.frame.width === this.frame.height) {
52 | return true;
53 | }
54 | if (layers[0] instanceof Rectangle) {
55 | return true;
56 | }
57 | }
58 | return false;
59 | }
60 | }
61 | export class Group {
62 | static _class = 'group';
63 |
64 |
65 | wrapperFrame(layers) {
66 | let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
67 | for (let layer of layers) {
68 | minX = Math.min(minX, layer.frame.x);
69 | minY = Math.min(minY, layer.frame.y);
70 | maxX = Math.max(maxX, layer.frame.x + layer.frame.width);
71 | maxY = Math.max(maxY, layer.frame.y + layer.frame.height);
72 | }
73 | minX = Math.max(minX, 0);
74 | maxX = Math.min(maxX, this.frame.width);
75 | minY = Math.max(minY, 0);
76 | maxY = Math.min(maxY, this.frame.height);
77 |
78 | return {x: minX, y: minY, width: maxX - minX, height: maxY - minY};
79 | }
80 |
81 | getLayoutStyles() {
82 | let {layers} = this;
83 |
84 | let visibleLayers = layers.filter(layer => layer.isVisible);
85 | if (!visibleLayers.length) {
86 | return {};
87 | }
88 | let styles = {[this['do_objectID']]: {}};
89 | visibleLayers.forEach(layer => {
90 | styles[layer['do_objectID']] = {};
91 | });
92 | // single layer
93 | if (visibleLayers.length === 1) {
94 | return {
95 | [visibleLayers[0]['do_objectID']]: {
96 | position: 'relative',
97 | left: undefined,
98 | top: undefined,
99 | height: '100%',
100 | width: undefined,
101 | }
102 | };
103 | }
104 | // check bg layer
105 | let {y, x, width, height} = visibleLayers[0].frame;
106 |
107 | let hasBgLayer = false;
108 | if (y === 0 && x === 0 && width === this.frame.width && height === this.frame.height) {
109 | hasBgLayer = true;
110 | }
111 | if (hasBgLayer) {
112 | styles[visibleLayers[0]['do_objectID']] = {
113 | position: 'absolute',
114 | left: 0,
115 | top: 0,
116 | height: '100%',
117 | width: '100%',
118 | };
119 | visibleLayers.splice(0, 1);
120 | }
121 | // for column layout
122 | // if (this['do_objectID'] === "9FFA94D8-633C-44A0-9DDA-F8B2A2EE970C") {
123 | // debugger
124 | // }
125 | let innerFrame = this.wrapperFrame(visibleLayers);
126 | if (innerFrame.x !== 0 || innerFrame.y !== 0 || innerFrame.width !== this.frame.width || innerFrame.height !== this.frame.height) {
127 | Object.assign(styles[this['do_objectID']], {
128 | boxSizing: 'border-box',
129 | padding: `${innerFrame.y}px ${this.frame.width - innerFrame.width - innerFrame.x}px ${this.frame.height - innerFrame.height - innerFrame.y}px ${innerFrame.x}px`,
130 | });
131 | }
132 | // visibleLayers.sort((a, b) => a.frame.y - b.frame.y);
133 |
134 | let isColumn = visibleLayers.every((layer, i) => i === 0 || visibleLayers[i - 1].frame.y + visibleLayers[i - 1].frame.height <= visibleLayers[i].frame.y);
135 | if (isColumn) {
136 | visibleLayers.forEach((layer, i) => {
137 | let style = styles[layer['do_objectID']];
138 | Object.assign(style, {
139 | position: 'relative',
140 | left: undefined,
141 | top: undefined,
142 | });
143 | let leftGap = layer.frame.x - innerFrame.x;
144 | let rightGap = innerFrame.x + innerFrame.width - layer.frame.x - layer.frame.width;
145 | if (leftGap === rightGap) {
146 | if (leftGap !== 0) {
147 | Object.assign(style, {
148 | marginLeft: 'auto',
149 | marginHeight: 'auto',
150 | });
151 | } else {
152 | Object.assign(style, {
153 | width: undefined,
154 | });
155 | }
156 | } else {
157 | if (leftGap) {
158 | Object.assign(style, {
159 | marginLeft: leftGap,
160 | });
161 | }
162 | }
163 | if (i >= 1) {
164 | let topGap = layer.frame.y - visibleLayers[i - 1].frame.y - visibleLayers[i - 1].frame.height;
165 | Object.assign(style, {
166 | marginTop: topGap ? topGap : undefined,
167 | });
168 | }
169 | });
170 | return {styles};
171 | }
172 |
173 | // TODO: row layout
174 | // TODO: grid layout
175 | return {styles};
176 | }
177 | }
178 | export class Artboard extends Group {
179 | static _class = 'artboard';
180 | }
181 | export class SymbolMaster extends Artboard {
182 | static _class = 'symbolMaster';
183 | }
184 | function f2i(f) {
185 | return Math.round(f * 255) || 0;
186 | }
187 |
188 | function f2x(f) {
189 | let s = (Math.round(f * 255) || 0).toString(16);
190 | if (s.length === 1) {
191 | s = '0' + s;
192 | }
193 | return s;
194 | }
195 |
196 | export class Color {
197 | static _class = 'color';
198 |
199 | toString(context) {
200 | let opacity = context && 'opacity' in context ? context['opacity'] : 1;
201 | let alpha = this.alpha === undefined ? 1 : this.alpha;
202 | if (alpha === 1) {
203 | return `#${f2x(this.red)}${f2x(this.green)}${f2x(this.blue)}`
204 | }
205 | return `rgba(${f2i(this.red)},${f2i(this.green)},${f2i(this.blue)},${alpha * opacity})`
206 | }
207 | }
208 |
209 | export class Gradient {
210 | static _class = 'gradient';
211 |
212 | toString(model) {
213 | switch (this.gradientType) {
214 | case 0: // linear-gradient
215 | let {x: x1, y: y1} = s2p(this.from);
216 | let {x: x2, y: y2} = s2p(this.to);
217 | x1 *= model.frame.width;
218 | x2 *= model.frame.width;
219 | y1 *= model.frame.height;
220 | y2 *= model.frame.height;
221 | let angle = 90 + Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
222 | return `linear-gradient(${angle}deg, ${this.stops.join(', ')})`;
223 | default:
224 | return ``;
225 | }
226 | }
227 | }
228 |
229 | export class GradientStop {
230 | static _class = 'gradientStop';
231 |
232 | toString() {
233 | return `${this.color} ${this.position * 100}%`
234 | }
235 | }
236 |
237 | export class Style {
238 | static _class = 'style';
239 |
240 | toStyle(model = {}, isSvg = false) {
241 | let ret = {};
242 | // Lock, ClickThrough
243 | if (model.isLocked) {
244 | Object.assign(ret, {pointerEvent: 'none'});
245 | } else {
246 | if (!model.hasClickThrough) {
247 | Object.assign(ret, {pointerEvent: 'auto'});
248 | }
249 | }
250 | // Visible
251 | if (!model.isVisible) {
252 | Object.assign(ret, {display: 'none'});
253 | }
254 | // Rotation, FlippedHorizontal, FlippedVertical
255 | if (model.rotation || model.isFlippedHorizontal || model.isFlippedVertical) {
256 | let transformString = '';
257 | if (model.rotation) {
258 | transformString += ` rotate(${-model.rotation}deg)`
259 | }
260 | if (model.isFlippedHorizontal || model.isFlippedVertical) {
261 | let a = model.isFlippedHorizontal ? -1 : 1;
262 | let d = model.isFlippedVertical ? -1 : 1;
263 | transformString += ` matrix(${a}, 0, 0, ${d}, 0, 0)`
264 | }
265 | Object.assign(ret, {transform: transformString});
266 | }
267 | if (this['borders']) { // Only accept first border
268 | let borders = this['borders'];
269 | Object.assign(ret, {boxSizing: `border-box`});
270 | let border = borders.filter(border => border['isEnabled']).reverse()[0];
271 | if (border) {
272 | Object.assign(ret, border.toStyle(isSvg));
273 | }
274 | }
275 | let hasContextStyle = false;
276 | if (this['contextSettings']) {
277 | let {opacity, blendMode} = this['contextSettings'];
278 | if (opacity !== 1) {
279 | hasContextStyle = true;
280 | Object.assign(ret, {opacity: this['contextSettings'].opacity});
281 | }
282 | if (blendMode !== BlendingMode.Normal) {
283 | hasContextStyle = true;
284 | Object.assign(ret, {mixBlendMode: getBlendModeString(this['contextSettings'].blendMode)});
285 | }
286 | }
287 | // Complex shape
288 | if (isSvg) {
289 | Object.assign(ret, {fillRule: 'evenodd', fill: 'none'});
290 | if (this['fills']) {
291 | let fills = this['fills'].filter(fill => fill['isEnabled']).reverse();
292 | if (fills.length) {
293 | Object.assign(ret, {fill: fills[0].color.toString()});
294 | }
295 | }
296 | return ret;
297 | }
298 | // Simple shape
299 | if (model instanceof ShapeGroup && model.isSimple()) {
300 | if (model.layers[0] instanceof Oval) {
301 | Object.assign(ret, {borderRadius: '50%'});
302 | }
303 | if (model.layers[0] instanceof Rectangle) {
304 | let path = model.layers[0].path;
305 | Object.assign(ret, {borderRadius: `${[0, 2, 1, 3].map(i => path.points[0]['cornerRadius'] + 'px').join(' ')}`});
306 | }
307 | }
308 | // Shadow, innerShadow
309 | let shadowList = [];
310 | if (this['shadows']) {
311 | shadowList = shadowList.concat(this['shadows'].filter(shadow => shadow['isEnabled']));
312 | }
313 | if (this['innerShadows']) {
314 | shadowList = shadowList.concat(this['innerShadows'].filter(shadow => shadow['isEnabled']));
315 | }
316 | if (shadowList.length) {
317 | Object.assign(ret, {boxShadow: shadowList.map(s => s.toString()).join(', ')});
318 | }
319 | // Fills (simple)
320 | if (model instanceof ShapeGroup && !hasContextStyle && this['fills']) {
321 | let fills = this['fills'].filter(fill => fill['isEnabled']);
322 | if (fills.length && fills.every(fill => fill.blendMode === getBlendModeString(BlendingMode.Normal))) {
323 | Object.assign(ret, {background: fills.map(fill => fill.toString(model)).join(', ')})
324 | }
325 | }
326 | return ret;
327 | }
328 | }
329 | function getBlendModeString(blendMode) {
330 | for (let mode in BlendingMode) {
331 | if (BlendingMode[mode] === blendMode) {
332 | return kebabCase(mode);
333 | }
334 | }
335 | return '';
336 | }
337 | export class GraphicsContextSettings {
338 | static _class = 'graphicsContextSettings';
339 | }
340 | export class Fill {
341 | static _class = 'fill';
342 |
343 | get blendMode() {
344 | let context = this['contextSettings'];
345 | if (!context) {
346 | context = {blendMode: BlendingMode.Normal};
347 | }
348 | return getBlendModeString(context.blendMode);
349 | }
350 |
351 | toString(model) {
352 | switch (this.fillType) {
353 | case FillType.Solid: // flat color
354 | let c = this.color.toString();
355 | return `linear-gradient(0deg, ${c},${c})`;
356 | case FillType.Gradient:// gradient
357 | return this.gradient.toString(model);
358 | default:
359 | return '';
360 | }
361 | }
362 |
363 | toStyle(model) {
364 | switch (this.fillType) {
365 | case FillType.Solid: // flat color
366 | return {background: this.color.toString(), mixBlendMode: this.blendMode};
367 | case FillType.Gradient:// gradient
368 | return {background: this.gradient.toString(model), mixBlendMode: this.blendMode};
369 | default:
370 | return {};
371 | }
372 | }
373 | }
374 |
375 | export class Shadow {
376 | static _class = 'shadow';
377 |
378 | toString() {
379 | return `${this.offsetX}px ${this.offsetY}px ${this.blurRadius}px ${this.spread}px ${this.color}`
380 | }
381 | }
382 | export class InnerShadow extends Shadow {
383 | static _class = 'innerShadow';
384 |
385 | toString() {
386 | return `inset ${super.toString()}`;
387 | }
388 | }
389 | export class Border {
390 | static _class = 'border';
391 |
392 | toString() {
393 | let {color, thickness, fillType} = this;
394 | return `${thickness}px solid ${color}`;
395 | }
396 |
397 | toStyle(isSvg = false) {
398 | if (!isSvg) {
399 | return {border: this.toString()};
400 | } else {
401 | let {color, thickness, fillType} = this;
402 | return {stroke: color, strokeWidth: thickness}
403 | }
404 | }
405 | }
406 |
407 |
408 | export class SymbolInstance {
409 | static _class = 'symbolInstance';
410 | }
411 | export class Document {
412 | static _class = 'document';
413 | }
414 |
415 | export class MSJSONFileReference {
416 | static _class = 'MSJSONFileReference';
417 |
418 | constructor(zip, parse) {
419 | this.zip = zip;
420 | this.parse = parse;
421 | }
422 |
423 | async getInstance() {
424 |
425 | switch (this['_ref_class']) {
426 | case "MSImmutablePage":
427 | let json = await this.zip.file(this['_ref'] + '.json')
428 | .async('string');
429 | return this.parse(JSON.parse(json), this.zip);
430 | case "MSImageData":
431 | let buffer = await this.zip.file(this['_ref'] + '.png')
432 | .async('nodebuffer');
433 | let blob = new Blob([buffer], {type: 'image/png'});
434 | return URL.createObjectURL(blob);
435 | }
436 | }
437 | }
438 | const Alignment = {
439 | Left: 4,
440 | Center: 2,
441 | Right: 1,
442 | Justify: 3,
443 | };
444 | const AlignmentString = {
445 | 4: 'left',
446 | 2: 'center',
447 | 1: 'right',
448 | 3: 'justify',
449 | };
450 | export class TextStyle {
451 | static _class = 'textStyle';
452 |
453 | toStyle(model) {
454 | let style = {};
455 | // window.model = model;
456 | // if(model.attributedString.archivedAttributedString.NSAttributes instanceof Array){
457 | // debugger;
458 | // }
459 |
460 | if (this.encodedAttributes) {
461 | if (typeof this.encodedAttributes.NSParagraphStyle !== 'undefined' && this.encodedAttributes.NSParagraphStyle.NSAlignment !== Alignment.Left) {
462 | Object.assign(style, {textAlign: AlignmentString[this.encodedAttributes.NSParagraphStyle.NSAlignment]});
463 | }
464 | if (typeof this.encodedAttributes.paragraphStyle !== 'undefined' && this.encodedAttributes.paragraphStyle.NSAlignment !== Alignment.Left) {
465 | Object.assign(style, {textAlign: AlignmentString[this.encodedAttributes.paragraphStyle.NSAlignment]});
466 | }
467 |
468 | //Backwards compatible
469 | if (typeof this.encodedAttributes.NSColor === 'undefined' && typeof this.encodedAttributes.MSAttributedStringColorDictionaryAttribute !== 'undefined' ) {
470 | this.encodedAttributes.NSColor = this.encodedAttributes.MSAttributedStringColorDictionaryAttribute;
471 | }
472 | }
473 |
474 | if (model.textBehaviour) {
475 | Object.assign(style, {width: model.frame.width});
476 | } else {
477 | Object.assign(style, {whiteSpace: 'nowrap'});
478 | }
479 | return {
480 | fontSize: this.encodedAttributes.MSAttributedStringFontAttribute.NSFontDescriptorAttributes.NSFontSizeAttribute,
481 | color: this.encodedAttributes.NSColor && Color.prototype.toString.call(this.encodedAttributes.NSColor),
482 | ...style,
483 | };
484 | }
485 |
486 | }
487 | export class Bitmap {
488 | static _class = 'bitmap';
489 | }
490 | function toS(a){
491 | return a.toPrecision(6).replace(/\.?0+$/,'');
492 | }
493 | export class ShapePath {
494 | static _class = 'shapePath';
495 |
496 | getXY(s) {
497 | let {x, y} = s2p(s);
498 | x = this.frame._x + this.frame._width * x;
499 | y = this.frame._y + this.frame._height * y;
500 | return {x, y}
501 | }
502 |
503 | toD() {
504 | let path = this.path;
505 | let {x, y} = this.getXY(path.points[0].point);
506 | let ret = `M${toS(x)},${toS(y)}`;
507 | let n = path['isClosed'] ? path.points.length + 1 : path.points.length;
508 | for (let i = 1; i < n; ++i) {
509 | let now = i;
510 | if (now === path.points.length) {
511 | now = 0;
512 | }
513 | let prev = (i - 1);
514 | let {x: x1, y: y1} = this.getXY(path.points[prev].curveFrom);
515 | let {x: x2, y: y2} = this.getXY(path.points[now].curveTo);
516 | let {x, y} = this.getXY(path.points[now].point);
517 | if (!path.points[now].hasCurveTo && !path.points[now].hasCurveFrom){
518 | ret += `L${toS(x)},${toS(y)}`;
519 | }else {
520 | ret += `C${toS(x1)},${toS(y1)} ${toS(x2)},${toS(y2)} ${toS(x)},${toS(y)}`;
521 | }
522 | }
523 |
524 | if (path['isClosed']) {
525 | ret += 'Z';
526 | }
527 | return ret;
528 | }
529 | }
530 | export class Rectangle extends ShapePath {
531 | static _class = 'rectangle';
532 | }
533 | export class Oval extends ShapePath {
534 | static _class = 'oval';
535 | }
536 | export class Star extends ShapePath {
537 | static _class = 'star';
538 | }
539 | export class Polygon extends ShapePath {
540 | static _class = 'polygon';
541 | }
542 | export class Triangle extends ShapePath {
543 | static _class = 'triangle';
544 | }
545 | export class Path {
546 | static _class = 'path';
547 |
548 | }
549 | function s2p(s) {
550 | let [x, y] = s.substr(1, s.length - 2).split(',').map(Number);
551 | return {x, y}
552 | }
553 | export class CurvePoint {
554 | static _class = 'curvePoint';
555 |
556 | }
557 | export class Text {
558 | static _class = 'text';
559 |
560 | }
561 | export class MSAttributedString {
562 | static _class = "MSAttributedString";
563 | //
564 | // set archivedAttributedString({_archive}) {
565 | // Object.assign(this, parseBase64(_archive));
566 | // }
567 | }
--------------------------------------------------------------------------------
/src/utils/JSX.js:
--------------------------------------------------------------------------------
1 | import camelCase from 'lodash/camelCase'
2 |
3 | export function cssText2obj(cssText) {
4 | let ret = {};
5 | cssText.split(';')
6 | .map(s => s.trim())
7 | .filter(s => s.length > 0)
8 | .forEach(s => {
9 | let [prop, value] = s.split(':').map(s => s.trim());
10 | ret[camelCase(prop)] = value;
11 | });
12 | return ret;
13 | }
14 |
15 | export function cssText2jsxCode(cssText) {
16 | return `{${Object.entries(cssText2obj(cssText)).map(([prop, value]) => `${prop}: '${value}'`).join(', ')}}`;
17 | }
18 |
19 | export function getFormattedJSXCode(o, depth = 0) {
20 | let spaces = '';
21 | for (let i = 0; i < depth; ++i) {
22 | spaces += ' ';
23 | }
24 | let tagName = o.tagName.toLowerCase();
25 | switch (tagName) {
26 | case 'img':
27 | return `${spaces}
\n`;
28 | case 'path':
29 | return `${spaces}\n`;
30 | case 'span':
31 | return `${spaces}${o.innerText}\n`;
32 | default:
33 | if (o.childNodes.length === 0) {
34 | return `${spaces}<${tagName} style={${cssText2jsxCode(o.style.cssText)}}/>\n`;
35 | }
36 | return `${spaces}<${tagName} style={${cssText2jsxCode(o.style.cssText)}}>\n${Array.prototype.map.call(o.childNodes, child => getFormattedJSXCode(child, depth + 1)).join('')}${spaces}${tagName}>\n`
37 | }
38 | }
39 |
40 | export function getReactCode(o) {
41 | return `import React from 'react'
42 |
43 | export default class MyComp extends React.Component {
44 | render(){
45 | return (
46 | ${getFormattedJSXCode(o, 3, true)} );
47 | }
48 | }`
49 | }
--------------------------------------------------------------------------------
/src/utils/bplist-parser.js:
--------------------------------------------------------------------------------
1 | // adapted from http://code.google.com/p/plist/source/browse/trunk/src/com/dd/plist/BinaryPropertyListParser.java
2 |
3 | var bigInt = require("big-integer");
4 | var debug = false;
5 |
6 | exports.maxObjectSize = 100 * 1000 * 1000; // 100Meg
7 | exports.maxObjectCount = 32768;
8 |
9 | // EPOCH = new SimpleDateFormat("yyyy MM dd zzz").parse("2001 01 01 GMT").getTime();
10 | // ...but that's annoying in a static initializer because it can throw exceptions, ick.
11 | // So we just hardcode the correct value.
12 | var EPOCH = 978307200000;
13 |
14 | // UID object definition
15 | function UID(id) {
16 | this.UID = id;
17 | }
18 |
19 | function _parse(o, $objects) {
20 | if (o instanceof UID) {
21 | return _parse($objects[o['UID']], $objects);
22 | } else if (o instanceof Buffer) {
23 | return o.toString();
24 | } else if (typeof o === 'object') {
25 | for (let prop in o) {
26 | o[prop] = _parse(o[prop], $objects);
27 | }
28 | if ('$class' in o) {
29 | if (o['$class']['$classname'] === 'NSMutableArray' || o['$class']['$classname'] === 'NSArray') {
30 | return o['NS.objects'];
31 | }
32 | if (o['$class']['$classname'] === 'NSDictionary' || o['$class']['$classname'] === 'NSMutableDictionary') {
33 | let ret = {};
34 | for (let i = 0; i < o['NS.keys'].length; ++i) {
35 | ret[o['NS.keys'][i]] = o['NS.objects'][i];
36 | }
37 | return ret;
38 | }
39 | if (o['$class']['$classname'] === 'NSColor') {
40 | if (!('NSRGB' in o)) {
41 | return {red: 0, green: 0, blue: 0, alpha: 1};
42 | }
43 | let [red, green, blue, alpha] = o['NSRGB'].substr(0, o['NSRGB'].length - 1).split(' ').map(Number);
44 | return {red, green, blue, alpha};
45 | }
46 | delete o['$class'];
47 | }
48 | return o;
49 | } else if (o instanceof Array) {
50 | return o.map(oo => _parse(oo, $objects))
51 | } else {
52 | return o;
53 | }
54 | }
55 | export function parse(json) {
56 | let {$objects} = json;
57 | return _parse($objects[1], $objects);
58 | }
59 | export function parseBase64(base64Str) {
60 | return parseBuffer(Buffer.from(base64Str, 'base64'));
61 | }
62 | export function parseBuffer(buffer) {
63 | var result = {};
64 |
65 | // check header
66 | var header = buffer.slice(0, 'bplist'.length).toString('utf8');
67 | if (header !== 'bplist') {
68 | throw new Error("Invalid binary plist. Expected 'bplist' at offset 0.");
69 | }
70 |
71 | // Handle trailer, last 32 bytes of the file
72 | var trailer = buffer.slice(buffer.length - 32, buffer.length);
73 | // 6 null bytes (index 0 to 5)
74 | var offsetSize = trailer.readUInt8(6);
75 | if (debug) {
76 | console.log("offsetSize: " + offsetSize);
77 | }
78 | var objectRefSize = trailer.readUInt8(7);
79 | if (debug) {
80 | console.log("objectRefSize: " + objectRefSize);
81 | }
82 | var numObjects = readUInt64BE(trailer, 8);
83 | if (debug) {
84 | console.log("numObjects: " + numObjects);
85 | }
86 | var topObject = readUInt64BE(trailer, 16);
87 | if (debug) {
88 | console.log("topObject: " + topObject);
89 | }
90 | var offsetTableOffset = readUInt64BE(trailer, 24);
91 | if (debug) {
92 | console.log("offsetTableOffset: " + offsetTableOffset);
93 | }
94 |
95 | if (numObjects > exports.maxObjectCount) {
96 | throw new Error("maxObjectCount exceeded");
97 | }
98 |
99 | // Handle offset table
100 | var offsetTable = [];
101 |
102 | for (var i = 0; i < numObjects; i++) {
103 | var offsetBytes = buffer.slice(offsetTableOffset + i * offsetSize, offsetTableOffset + (i + 1) * offsetSize);
104 | offsetTable[i] = readUInt(offsetBytes, 0);
105 | if (debug) {
106 | console.log("Offset for Object #" + i + " is " + offsetTable[i] + " [" + offsetTable[i].toString(16) + "]");
107 | }
108 | }
109 |
110 | // Parses an object inside the currently parsed binary property list.
111 | // For the format specification check
112 | //
113 | // Apple's binary property list parser implementation.
114 | function parseObject(tableOffset) {
115 | var offset = offsetTable[tableOffset];
116 | var type = buffer[offset];
117 | var objType = (type & 0xF0) >> 4; //First 4 bits
118 | var objInfo = (type & 0x0F); //Second 4 bits
119 | switch (objType) {
120 | case 0x0:
121 | return parseSimple();
122 | case 0x1:
123 | return parseInteger();
124 | case 0x8:
125 | return parseUID();
126 | case 0x2:
127 | return parseReal();
128 | case 0x3:
129 | return parseDate();
130 | case 0x4:
131 | return parseData();
132 | case 0x5: // ASCII
133 | return parsePlistString();
134 | case 0x6: // UTF-16
135 | return parsePlistString(true);
136 | case 0xA:
137 | return parseArray();
138 | case 0xD:
139 | return parseDictionary();
140 | default:
141 | throw new Error("Unhandled type 0x" + objType.toString(16));
142 | }
143 |
144 | function parseSimple() {
145 | //Simple
146 | switch (objInfo) {
147 | case 0x0: // null
148 | return null;
149 | case 0x8: // false
150 | return false;
151 | case 0x9: // true
152 | return true;
153 | case 0xF: // filler byte
154 | return null;
155 | default:
156 | throw new Error("Unhandled simple type 0x" + objType.toString(16));
157 | }
158 | }
159 |
160 | function bufferToHexString(buffer) {
161 | var str = '';
162 | var i;
163 | for (i = 0; i < buffer.length; i++) {
164 | if (buffer[i] != 0x00) {
165 | break;
166 | }
167 | }
168 | for (; i < buffer.length; i++) {
169 | var part = '00' + buffer[i].toString(16);
170 | str += part.substr(part.length - 2);
171 | }
172 | return str;
173 | }
174 |
175 | function parseInteger() {
176 | var length = Math.pow(2, objInfo);
177 | if (length > 4) {
178 | var data = buffer.slice(offset + 1, offset + 1 + length);
179 | var str = bufferToHexString(data);
180 | return bigInt(str, 16);
181 | }
182 | if (length < exports.maxObjectSize) {
183 | return readUInt(buffer.slice(offset + 1, offset + 1 + length));
184 | } else {
185 | throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
186 | }
187 | }
188 |
189 | function parseUID() {
190 | var length = objInfo + 1;
191 | if (length < exports.maxObjectSize) {
192 | return new UID(readUInt(buffer.slice(offset + 1, offset + 1 + length)));
193 | } else {
194 | throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
195 | }
196 | }
197 |
198 | function parseReal() {
199 | var length = Math.pow(2, objInfo);
200 | if (length < exports.maxObjectSize) {
201 | var realBuffer = buffer.slice(offset + 1, offset + 1 + length);
202 | if (length === 4) {
203 | return realBuffer.readFloatBE(0);
204 | }
205 | else if (length === 8) {
206 | return realBuffer.readDoubleBE(0);
207 | }
208 | } else {
209 | throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
210 | }
211 | }
212 |
213 | function parseDate() {
214 | if (objInfo != 0x3) {
215 | console.error("Unknown date type :" + objInfo + ". Parsing anyway...");
216 | }
217 | var dateBuffer = buffer.slice(offset + 1, offset + 9);
218 | return new Date(EPOCH + (1000 * dateBuffer.readDoubleBE(0)));
219 | }
220 |
221 | function parseData() {
222 | var dataoffset = 1;
223 | var length = objInfo;
224 | if (objInfo == 0xF) {
225 | var int_type = buffer[offset + 1];
226 | var intType = (int_type & 0xF0) / 0x10;
227 | if (intType != 0x1) {
228 | console.error("0x4: UNEXPECTED LENGTH-INT TYPE! " + intType);
229 | }
230 | var intInfo = int_type & 0x0F;
231 | var intLength = Math.pow(2, intInfo);
232 | dataoffset = 2 + intLength;
233 | if (intLength < 3) {
234 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
235 | } else {
236 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
237 | }
238 | }
239 | if (length < exports.maxObjectSize) {
240 | return buffer.slice(offset + dataoffset, offset + dataoffset + length);
241 | } else {
242 | throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
243 | }
244 | }
245 |
246 | function parsePlistString(isUtf16) {
247 | isUtf16 = isUtf16 || 0;
248 | var enc = "utf8";
249 | var length = objInfo;
250 | var stroffset = 1;
251 | if (objInfo == 0xF) {
252 | var int_type = buffer[offset + 1];
253 | var intType = (int_type & 0xF0) / 0x10;
254 | if (intType != 0x1) {
255 | console.err("UNEXPECTED LENGTH-INT TYPE! " + intType);
256 | }
257 | var intInfo = int_type & 0x0F;
258 | var intLength = Math.pow(2, intInfo);
259 | var stroffset = 2 + intLength;
260 | if (intLength < 3) {
261 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
262 | } else {
263 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
264 | }
265 | }
266 | // length is String length -> to get byte length multiply by 2, as 1 character takes 2 bytes in UTF-16
267 | length *= (isUtf16 + 1);
268 | if (length < exports.maxObjectSize) {
269 | var plistString = new Buffer(buffer.slice(offset + stroffset, offset + stroffset + length));
270 | if (isUtf16) {
271 | plistString = swapBytes(plistString);
272 | enc = "ucs2";
273 | }
274 | return plistString.toString(enc);
275 | } else {
276 | throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
277 | }
278 | }
279 |
280 | function parseArray() {
281 | var length = objInfo;
282 | var arrayoffset = 1;
283 | if (objInfo == 0xF) {
284 | var int_type = buffer[offset + 1];
285 | var intType = (int_type & 0xF0) / 0x10;
286 | if (intType != 0x1) {
287 | console.error("0xa: UNEXPECTED LENGTH-INT TYPE! " + intType);
288 | }
289 | var intInfo = int_type & 0x0F;
290 | var intLength = Math.pow(2, intInfo);
291 | arrayoffset = 2 + intLength;
292 | if (intLength < 3) {
293 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
294 | } else {
295 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
296 | }
297 | }
298 | if (length * objectRefSize > exports.maxObjectSize) {
299 | throw new Error("To little heap space available!");
300 | }
301 | var array = [];
302 | for (var i = 0; i < length; i++) {
303 | var objRef = readUInt(buffer.slice(offset + arrayoffset + i * objectRefSize, offset + arrayoffset + (i + 1) * objectRefSize));
304 | array[i] = parseObject(objRef);
305 | }
306 | return array;
307 | }
308 |
309 | function parseDictionary() {
310 | var length = objInfo;
311 | var dictoffset = 1;
312 | if (objInfo == 0xF) {
313 | var int_type = buffer[offset + 1];
314 | var intType = (int_type & 0xF0) / 0x10;
315 | if (intType != 0x1) {
316 | console.error("0xD: UNEXPECTED LENGTH-INT TYPE! " + intType);
317 | }
318 | var intInfo = int_type & 0x0F;
319 | var intLength = Math.pow(2, intInfo);
320 | dictoffset = 2 + intLength;
321 | if (intLength < 3) {
322 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
323 | } else {
324 | length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
325 | }
326 | }
327 | if (length * 2 * objectRefSize > exports.maxObjectSize) {
328 | throw new Error("To little heap space available!");
329 | }
330 | if (debug) {
331 | console.log("Parsing dictionary #" + tableOffset);
332 | }
333 | var dict = {};
334 | for (var i = 0; i < length; i++) {
335 | var keyRef = readUInt(buffer.slice(offset + dictoffset + i * objectRefSize, offset + dictoffset + (i + 1) * objectRefSize));
336 | var valRef = readUInt(buffer.slice(offset + dictoffset + (length * objectRefSize) + i * objectRefSize, offset + dictoffset + (length * objectRefSize) + (i + 1) * objectRefSize));
337 | var key = parseObject(keyRef);
338 | var val = parseObject(valRef);
339 | if (debug) {
340 | console.log(" DICT #" + tableOffset + ": Mapped " + key + " to " + val);
341 | }
342 | dict[key] = val;
343 | }
344 | return dict;
345 | }
346 | }
347 |
348 | return /*[*/ parse(parseObject(topObject)) /*]*/;
349 | };
350 |
351 | function readUInt(buffer, start) {
352 | start = start || 0;
353 |
354 | var l = 0;
355 | for (var i = start; i < buffer.length; i++) {
356 | l <<= 8;
357 | l |= buffer[i] & 0xFF;
358 | }
359 | return l;
360 | }
361 |
362 | // we're just going to toss the high order bits because javascript doesn't have 64-bit ints
363 | function readUInt64BE(buffer, start) {
364 | var data = buffer.slice(start, start + 8);
365 | return data.readUInt32BE(4, 8);
366 | }
367 |
368 | function swapBytes(buffer) {
369 | var len = buffer.length;
370 | for (var i = 0; i < len; i += 2) {
371 | var a = buffer[i];
372 | buffer[i] = buffer[i + 1];
373 | buffer[i + 1] = a;
374 | }
375 | return buffer;
376 | }
377 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import * as models from '../models/index'
2 | export * from './JSX'
3 | class Dummy {
4 | constructor(json) {
5 | setProps(this, json);
6 | }
7 |
8 | toString() {
9 | return JSON.stringify(this.json);
10 | }
11 | }
12 | let mapping = {};
13 | for (let modelName in models) {
14 | let model = models[modelName];
15 | mapping[model._class] = model;
16 | }
17 |
18 | function isClass(o) {
19 | return o && typeof o === 'object' && '_class' in o && typeof o['_class'] === 'string'
20 | }
21 | function isPlistArchive(o) {
22 | return o && typeof o === 'object' && '_archive' in o;
23 | }
24 | import {parseBase64} from './bplist-parser'
25 | export let objectMapping = {};
26 | export let symbolMapping = {};
27 |
28 | export function clear() {
29 | objectMapping = {};
30 | symbolMapping = {};
31 | }
32 |
33 | export function getObjectById(id) {
34 | return objectMapping[id];
35 | }
36 | export function getSymbolById(id) {
37 | return symbolMapping[id];
38 | }
39 |
40 | export function parse(json, zip) {
41 |
42 | if (isClass(json)) {
43 | let className = json['_class'];
44 | if (className in mapping) {
45 | let ret = new mapping[className](zip, parse);
46 | setProps(ret, json, zip);
47 | if (className === 'symbolMaster') {
48 | symbolMapping[json['symbolID']] = ret;
49 | }
50 | objectMapping[json['do_objectID']] = ret;
51 | return ret;
52 | } else {
53 | return json;
54 | }
55 | } else if (isPlistArchive(json)) {
56 | return parseBase64(json['_archive']);
57 | } else if (typeof json === 'object') {
58 | for (let prop in json) {
59 | json[prop] = parse(json[prop], zip);
60 | }
61 |
62 | return json;
63 | }
64 | else if (json instanceof Array) {
65 | return json.map(o => parse(o, zip));
66 | } else {
67 | return json;
68 | }
69 | }
70 | export function setProps(obj, json, zip) {
71 | for (let prop in json) {
72 | obj[prop] = parse(json[prop], zip);
73 | }
74 | }
75 |
76 |
--------------------------------------------------------------------------------
/src/utils/pretty-bytes.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
3 |
4 | module.exports = num => {
5 | if (!Number.isFinite(num)) {
6 | throw new TypeError(`Expected a finite number, got ${typeof num}: ${num}`);
7 | }
8 |
9 | const neg = num < 0;
10 |
11 | if (neg) {
12 | num = -num;
13 | }
14 |
15 | if (num < 1) {
16 | return (neg ? '-' : '') + num + ' B';
17 | }
18 |
19 | const exponent = Math.min(Math.floor(Math.log(num) / Math.log(1000)), UNITS.length - 1);
20 | const numStr = Number((num / Math.pow(1000, exponent)).toPrecision(3));
21 | const unit = UNITS[exponent];
22 |
23 | return (neg ? '-' : '') + numStr + ' ' + unit;
24 | };
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | module.exports = {
3 | module: {
4 | rules: [
5 | {
6 | test: /\.jsx?$/,
7 | exclude: [
8 | /node_modules/
9 | ],
10 | loaders: ["babel-loader"]
11 | },
12 | {
13 | test: /\.styl$/,
14 | loaders: ['style-loader', 'css-loader', "stylus-loader"]
15 | },
16 | {
17 | test: /\.css$/,
18 | loaders: ['style-loader', 'css-loader']
19 | },
20 | {
21 | test: /\.(sketch|png)$/,
22 | loaders: ["file-loader"]
23 | },
24 | ]
25 |
26 | },
27 | resolve: {
28 | extensions: [".js", 'jsx', ".json"],
29 | },
30 | plugins: [new HtmlWebpackPlugin()],
31 | };
--------------------------------------------------------------------------------